diff --git a/pkg/v2/boot_manager.models.go b/pkg/v2/boot_manager.models.go index 77ce76d..94fb177 100644 --- a/pkg/v2/boot_manager.models.go +++ b/pkg/v2/boot_manager.models.go @@ -145,6 +145,26 @@ type InstallNewOSPayload struct { Password string `json:"password,omitempty"` PartitionsConfig PartitionsConfig `json:"partitions_config,omitempty"` UserData *string `json:"cloud_init_user_data"` + + // Public IPv4 fields + IPv4Address string `json:"ipv4_address,omitempty"` + IPv4Netmask string `json:"ipv4_netmask,omitempty"` + IPv4Gateway string `json:"ipv4_gateway,omitempty"` + + // Local/Private IPv4 fields — nullable; omitempty omits nil from JSON payload + LocalIPv4Address *string `json:"local_ipv4_address,omitempty"` + LocalIPv4Netmask *string `json:"local_ipv4_netmask,omitempty"` + LocalIPv4Gateway *string `json:"local_ipv4_gateway,omitempty"` + + // Public IPv6 fields + IPv6Address string `json:"ipv6_address,omitempty"` + IPv6Netmask string `json:"ipv6_netmask,omitempty"` + IPv6Gateway string `json:"ipv6_gateway,omitempty"` + + // Local/Private IPv6 fields — nullable; omitempty omits nil from JSON payload + LocalIPv6Address *string `json:"local_ipv6_address,omitempty"` + LocalIPv6Netmask *string `json:"local_ipv6_netmask,omitempty"` + LocalIPv6Gateway *string `json:"local_ipv6_gateway,omitempty"` } func (p *InstallNewOSPayload) CopyWithoutSensitiveData() *InstallNewOSPayload { @@ -155,9 +175,26 @@ func (p *InstallNewOSPayload) CopyWithoutSensitiveData() *InstallNewOSPayload { UserHostname: p.UserHostname, PartitionsConfig: p.PartitionsConfig, UserData: p.UserData, + IPv4Address: p.IPv4Address, + IPv4Netmask: p.IPv4Netmask, + IPv4Gateway: p.IPv4Gateway, + LocalIPv4Address: p.LocalIPv4Address, + LocalIPv4Netmask: p.LocalIPv4Netmask, + LocalIPv4Gateway: p.LocalIPv4Gateway, + IPv6Address: p.IPv6Address, + IPv6Netmask: p.IPv6Netmask, + IPv6Gateway: p.IPv6Gateway, + LocalIPv6Address: p.LocalIPv6Address, + LocalIPv6Netmask: p.LocalIPv6Netmask, + LocalIPv6Gateway: p.LocalIPv6Gateway, } } +// StringPtr returns a pointer to the given string value. +func StringPtr(s string) *string { + return &s +} + const ( NoOSValue = "noos" ) diff --git a/pkg/v2/boot_manager.models_test.go b/pkg/v2/boot_manager.models_test.go index c6b9a5e..d26d2a5 100644 --- a/pkg/v2/boot_manager.models_test.go +++ b/pkg/v2/boot_manager.models_test.go @@ -1,6 +1,7 @@ package v2 import ( + "encoding/json" "testing" "github.com/stretchr/testify/require" @@ -142,3 +143,238 @@ func TestOperatingSystem_IsPrivateNetworkAvailable(t *testing.T) { }) } } + +func TestInstallNewOSPayload_JSONWithLocalIPv4Fields(t *testing.T) { + t.Run("LocalIPv4FieldsSet", func(t *testing.T) { + payload := &InstallNewOSPayload{ + OSVersion: "20.04", + OSTemplate: "ubuntu", + OSArch: "x86_64", + UserHostname: "test-host", + LocalIPv4Address: StringPtr("192.168.1.10"), + LocalIPv4Netmask: StringPtr("255.255.255.0"), + LocalIPv4Gateway: StringPtr("192.168.1.1"), + } + + data, err := json.Marshal(payload) + require.NoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(data, &result) + require.NoError(t, err) + + require.Equal(t, "192.168.1.10", result["local_ipv4_address"]) + require.Equal(t, "255.255.255.0", result["local_ipv4_netmask"]) + require.Equal(t, "192.168.1.1", result["local_ipv4_gateway"]) + }) + + t.Run("LocalIPv4FieldsNil_SendsNull", func(t *testing.T) { + payload := &InstallNewOSPayload{ + OSVersion: "20.04", + OSTemplate: "ubuntu", + OSArch: "x86_64", + UserHostname: "test-host", + LocalIPv4Address: nil, + LocalIPv4Netmask: nil, + LocalIPv4Gateway: nil, + } + + data, err := json.Marshal(payload) + require.NoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(data, &result) + require.NoError(t, err) + + require.Nil(t, result["local_ipv4_address"]) + require.Nil(t, result["local_ipv4_netmask"]) + require.Nil(t, result["local_ipv4_gateway"]) + }) + + t.Run("PublicIPv4FieldsOmittedWhenEmpty", func(t *testing.T) { + payload := &InstallNewOSPayload{ + OSVersion: "20.04", + OSTemplate: "ubuntu", + OSArch: "x86_64", + UserHostname: "test-host", + } + + data, err := json.Marshal(payload) + require.NoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(data, &result) + require.NoError(t, err) + + _, hasIPv4Address := result["ipv4_address"] + _, hasIPv4Netmask := result["ipv4_netmask"] + _, hasIPv4Gateway := result["ipv4_gateway"] + require.False(t, hasIPv4Address, "ipv4_address should be omitted when empty") + require.False(t, hasIPv4Netmask, "ipv4_netmask should be omitted when empty") + require.False(t, hasIPv4Gateway, "ipv4_gateway should be omitted when empty") + }) + + t.Run("PublicIPv4FieldsSet", func(t *testing.T) { + payload := &InstallNewOSPayload{ + OSVersion: "20.04", + OSTemplate: "ubuntu", + OSArch: "x86_64", + UserHostname: "test-host", + IPv4Address: "1.2.3.4", + IPv4Netmask: "255.255.255.248", + IPv4Gateway: "1.2.3.1", + } + + data, err := json.Marshal(payload) + require.NoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(data, &result) + require.NoError(t, err) + + require.Equal(t, "1.2.3.4", result["ipv4_address"]) + require.Equal(t, "255.255.255.248", result["ipv4_netmask"]) + require.Equal(t, "1.2.3.1", result["ipv4_gateway"]) + }) +} + +func TestInstallNewOSPayload_JSONWithIPv6Fields(t *testing.T) { + t.Run("LocalIPv6FieldsSet", func(t *testing.T) { + payload := &InstallNewOSPayload{ + OSVersion: "20.04", + OSTemplate: "ubuntu", + OSArch: "x86_64", + UserHostname: "test-host", + LocalIPv6Address: StringPtr("fd00::10"), + LocalIPv6Netmask: StringPtr("ffff:ffff:ffff::"), + LocalIPv6Gateway: StringPtr("fd00::1"), + } + + data, err := json.Marshal(payload) + require.NoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(data, &result) + require.NoError(t, err) + + require.Equal(t, "fd00::10", result["local_ipv6_address"]) + require.Equal(t, "ffff:ffff:ffff::", result["local_ipv6_netmask"]) + require.Equal(t, "fd00::1", result["local_ipv6_gateway"]) + }) + + t.Run("LocalIPv6FieldsNil_SendsNull", func(t *testing.T) { + payload := &InstallNewOSPayload{ + OSVersion: "20.04", + OSTemplate: "ubuntu", + OSArch: "x86_64", + UserHostname: "test-host", + LocalIPv6Address: nil, + LocalIPv6Netmask: nil, + LocalIPv6Gateway: nil, + } + + data, err := json.Marshal(payload) + require.NoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(data, &result) + require.NoError(t, err) + + require.Nil(t, result["local_ipv6_address"]) + require.Nil(t, result["local_ipv6_netmask"]) + require.Nil(t, result["local_ipv6_gateway"]) + }) + + t.Run("PublicIPv6FieldsSet", func(t *testing.T) { + payload := &InstallNewOSPayload{ + OSVersion: "20.04", + OSTemplate: "ubuntu", + OSArch: "x86_64", + UserHostname: "test-host", + IPv6Address: "2001:db8::10", + IPv6Netmask: "ffff:ffff:ffff::", + IPv6Gateway: "2001:db8::1", + } + + data, err := json.Marshal(payload) + require.NoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(data, &result) + require.NoError(t, err) + + require.Equal(t, "2001:db8::10", result["ipv6_address"]) + require.Equal(t, "ffff:ffff:ffff::", result["ipv6_netmask"]) + require.Equal(t, "2001:db8::1", result["ipv6_gateway"]) + }) +} + +func TestInstallNewOSPayload_CopyWithoutSensitiveData(t *testing.T) { + userData := "#cloud-config" + payload := &InstallNewOSPayload{ + OSVersion: "20.04", + OSTemplate: "ubuntu", + OSArch: "x86_64", + UserSSHKey: "ssh-rsa AAAAB3...", + UserHostname: "test-host", + Password: "secret-password", + PartitionsConfig: PartitionsConfig{}, + UserData: &userData, + IPv4Address: "1.2.3.4", + IPv4Netmask: "255.255.255.248", + IPv4Gateway: "1.2.3.1", + LocalIPv4Address: StringPtr("192.168.1.10"), + LocalIPv4Netmask: StringPtr("255.255.255.0"), + LocalIPv4Gateway: StringPtr("192.168.1.1"), + IPv6Address: "2001:db8::10", + IPv6Netmask: "ffff:ffff:ffff::", + IPv6Gateway: "2001:db8::1", + LocalIPv6Address: StringPtr("fd00::10"), + LocalIPv6Netmask: StringPtr("ffff:ffff:ffff::"), + LocalIPv6Gateway: StringPtr("fd00::1"), + } + + copied := payload.CopyWithoutSensitiveData() + + // Non-sensitive fields should be copied + require.Equal(t, "20.04", copied.OSVersion) + require.Equal(t, "ubuntu", copied.OSTemplate) + require.Equal(t, "x86_64", copied.OSArch) + require.Equal(t, "test-host", copied.UserHostname) + require.Equal(t, payload.PartitionsConfig, copied.PartitionsConfig) + require.Equal(t, &userData, copied.UserData) + + // IPv4 fields should be copied + require.Equal(t, "1.2.3.4", copied.IPv4Address) + require.Equal(t, "255.255.255.248", copied.IPv4Netmask) + require.Equal(t, "1.2.3.1", copied.IPv4Gateway) + require.Equal(t, "192.168.1.10", *copied.LocalIPv4Address) + require.Equal(t, "255.255.255.0", *copied.LocalIPv4Netmask) + require.Equal(t, "192.168.1.1", *copied.LocalIPv4Gateway) + + // IPv6 fields should be copied + require.Equal(t, "2001:db8::10", copied.IPv6Address) + require.Equal(t, "ffff:ffff:ffff::", copied.IPv6Netmask) + require.Equal(t, "2001:db8::1", copied.IPv6Gateway) + require.Equal(t, "fd00::10", *copied.LocalIPv6Address) + require.Equal(t, "ffff:ffff:ffff::", *copied.LocalIPv6Netmask) + require.Equal(t, "fd00::1", *copied.LocalIPv6Gateway) + + // Sensitive fields should NOT be copied + require.Empty(t, copied.UserSSHKey) + require.Empty(t, copied.Password) +} + +func TestStringPtr(t *testing.T) { + t.Run("ReturnsPointerToString", func(t *testing.T) { + ptr := StringPtr("hello") + require.NotNil(t, ptr) + require.Equal(t, "hello", *ptr) + }) + + t.Run("EmptyString", func(t *testing.T) { + ptr := StringPtr("") + require.NotNil(t, ptr) + require.Equal(t, "", *ptr) + }) +} diff --git a/pkg/v2/boot_manager_test.go b/pkg/v2/boot_manager_test.go index d27b4ed..c51b887 100644 --- a/pkg/v2/boot_manager_test.go +++ b/pkg/v2/boot_manager_test.go @@ -10,6 +10,10 @@ import ( "github.com/stretchr/testify/require" ) +const emptyResultBody = `{ + "result": {} + }` + func TestServiceClient_OperatingSystems(t *testing.T) { t.Run("Success", func(t *testing.T) { // Prepare @@ -303,9 +307,7 @@ func TestServiceClient_PartitionsValidate(t *testing.T) { func TestServiceClient_ReinstallOS(t *testing.T) { t.Run("Success", func(t *testing.T) { // Prepare - body := `{ - "result": {} - }` + body := emptyResultBody fakeResp := httptest.NewFakeResponse(200, body) //nolint:bodyclose fakeTransport := httptest.NewFakeTransport(fakeResp, nil) client := newFakeClient("http://fake", fakeTransport) @@ -328,6 +330,58 @@ func TestServiceClient_ReinstallOS(t *testing.T) { require.Equal(t, 200, respRes.StatusCode) }) + t.Run("SuccessWithLocalIPv4Fields", func(t *testing.T) { + // Prepare + body := emptyResultBody + fakeResp := httptest.NewFakeResponse(200, body) //nolint:bodyclose + fakeTransport := httptest.NewFakeTransport(fakeResp, nil) + client := newFakeClient("http://fake", fakeTransport) + + payload := &InstallNewOSPayload{ + OSVersion: "20.04", + OSTemplate: "ubuntu", + OSArch: "x86_64", + UserHostname: "test-host", + LocalIPv4Address: StringPtr("192.168.1.10"), + LocalIPv4Netmask: StringPtr("255.255.255.0"), + LocalIPv4Gateway: StringPtr("192.168.1.1"), + } + + // Execute + respRes, err := client.InstallNewOS(context.Background(), payload, "resourceid") + + // Analyse + require.NoError(t, err) + require.NotNil(t, respRes) + require.Equal(t, 200, respRes.StatusCode) + }) + + t.Run("SuccessWithNilLocalIPv4Fields", func(t *testing.T) { + // Prepare — nil local IPv4 fields send null to clear the private subnet + body := emptyResultBody + fakeResp := httptest.NewFakeResponse(200, body) //nolint:bodyclose + fakeTransport := httptest.NewFakeTransport(fakeResp, nil) + client := newFakeClient("http://fake", fakeTransport) + + payload := &InstallNewOSPayload{ + OSVersion: "20.04", + OSTemplate: "ubuntu", + OSArch: "x86_64", + UserHostname: "test-host", + LocalIPv4Address: nil, + LocalIPv4Netmask: nil, + LocalIPv4Gateway: nil, + } + + // Execute + respRes, err := client.InstallNewOS(context.Background(), payload, "resourceid") + + // Analyse + require.NoError(t, err) + require.NotNil(t, respRes) + require.Equal(t, 200, respRes.StatusCode) + }) + t.Run("HTTPError", func(t *testing.T) { // Prepare body := httpErrorBody