From a5a51c2675ee01ca758b93297c4c8ed37f4c1b00 Mon Sep 17 00:00:00 2001 From: Alain Kaeslin Date: Thu, 22 Feb 2024 11:25:33 +0100 Subject: [PATCH] Introduce UseCloudscaleDefaults to allow setting default cloudscale dns servers. --- subnets.go | 70 +++++++++++- subnets_test.go | 113 +++++++++++++++++++ test/integration/subnets_integration_test.go | 78 ++++++++++++- 3 files changed, 252 insertions(+), 9 deletions(-) diff --git a/subnets.go b/subnets.go index 7b39fcf..1319fe1 100644 --- a/subnets.go +++ b/subnets.go @@ -2,12 +2,16 @@ package cloudscale import ( "context" + "encoding/json" "fmt" "net/http" + "reflect" ) const subnetBasePath = "v1/subnets" +var UseCloudscaleDefaults = []string{"CLOUDSCALE_DEFAULTS"} + type Subnet struct { TaggedResource // Just use omitempty everywhere. This makes it easy to use restful. Errors @@ -28,10 +32,10 @@ type SubnetStub struct { type SubnetCreateRequest struct { TaggedResourceRequest - CIDR string `json:"cidr,omitempty"` - Network string `json:"network,omitempty"` - GatewayAddress string `json:"gateway_address,omitempty"` - DNSServers []string `json:"dns_servers,omitempty"` + CIDR string `json:"cidr,omitempty"` + Network string `json:"network,omitempty"` + GatewayAddress string `json:"gateway_address,omitempty"` + DNSServers *[]string `json:"dns_servers,omitempty"` } type SubnetUpdateRequest struct { @@ -40,6 +44,64 @@ type SubnetUpdateRequest struct { DNSServers *[]string `json:"dns_servers"` } +func (request SubnetUpdateRequest) MarshalJSON() ([]byte, error) { + type Alias SubnetUpdateRequest // Create an alias to avoid recursion + + if request.DNSServers == nil { + return json.Marshal(&struct { + Alias + DNSServers []string `json:"dns_servers,omitempty"` + }{ + Alias: (Alias)(request), + }) + } + + if reflect.DeepEqual(*request.DNSServers, UseCloudscaleDefaults) { + return json.Marshal(&struct { + Alias + DNSServers []string `json:"dns_servers"` // important: no omitempty + }{ + Alias: (Alias)(request), + DNSServers: nil, + }) + } + + return json.Marshal(&struct { + Alias + }{ + Alias: (Alias)(request), + }) +} + +func (request SubnetCreateRequest) MarshalJSON() ([]byte, error) { + type Alias SubnetCreateRequest // Create an alias to avoid recursion + + if request.DNSServers == nil { + return json.Marshal(&struct { + Alias + DNSServers []string `json:"dns_servers,omitempty"` + }{ + Alias: (Alias)(request), + }) + } + + if reflect.DeepEqual(*request.DNSServers, UseCloudscaleDefaults) { + return json.Marshal(&struct { + Alias + DNSServers []string `json:"dns_servers"` // important: no omitempty + }{ + Alias: (Alias)(request), + DNSServers: nil, + }) + } + + return json.Marshal(&struct { + Alias + }{ + Alias: (Alias)(request), + }) +} + type SubnetService interface { Create(ctx context.Context, createRequest *SubnetCreateRequest) (*Subnet, error) Get(ctx context.Context, subnetID string) (*Subnet, error) diff --git a/subnets_test.go b/subnets_test.go index 85d9e9c..f31e783 100644 --- a/subnets_test.go +++ b/subnets_test.go @@ -1,6 +1,7 @@ package cloudscale import ( + "encoding/json" "fmt" "net/http" "reflect" @@ -47,3 +48,115 @@ func TestSubnets_List(t *testing.T) { } } + +func TestMarshalingOfDNSServersInSubnetUpdateRequest(t *testing.T) { + testCases := []struct { + name string + request SubnetUpdateRequest + expected string + }{ + { + name: "one dns server", + request: SubnetUpdateRequest{ + DNSServers: &[]string{"8.8.8.8"}, + }, + expected: "{\"dns_servers\":[\"8.8.8.8\"]}", + }, + { + name: "two dns servers", + request: SubnetUpdateRequest{ + DNSServers: &[]string{"8.8.8.8", "8.8.4.4"}, + }, + expected: "{\"dns_servers\":[\"8.8.8.8\",\"8.8.4.4\"]}", + }, + { + name: "no dns servers", + request: SubnetUpdateRequest{ + DNSServers: &[]string{}, + }, + expected: "{\"dns_servers\":[]}", + }, + { + name: "defaults", + request: SubnetUpdateRequest{ + DNSServers: &UseCloudscaleDefaults, + }, + expected: "{\"dns_servers\":null}", + }, + { + name: "gateway", + request: SubnetUpdateRequest{ + GatewayAddress: "192.168.1.1", + }, + expected: "{\"gateway_address\":\"192.168.1.1\"}", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + b, err := json.Marshal(tc.request) + if err != nil { + t.Errorf("Error marshaling JSON: %v", err) + } + if actualOutput := string(b); actualOutput != tc.expected { + t.Errorf("Unexpected JSON output:\nExpected: %s\nActual: %s", tc.expected, actualOutput) + } + }) + } +} + +func TestMarshalingOfDNSServersInSubnetSubnetCreateRequest(t *testing.T) { + testCases := []struct { + name string + request SubnetCreateRequest + expected string + }{ + { + name: "one dns server", + request: SubnetCreateRequest{ + DNSServers: &[]string{"8.8.8.8"}, + }, + expected: "{\"dns_servers\":[\"8.8.8.8\"]}", + }, + { + name: "two dns servers", + request: SubnetCreateRequest{ + DNSServers: &[]string{"8.8.8.8", "8.8.4.4"}, + }, + expected: "{\"dns_servers\":[\"8.8.8.8\",\"8.8.4.4\"]}", + }, + { + name: "no dns servers", + request: SubnetCreateRequest{ + DNSServers: &[]string{}, + }, + expected: "{\"dns_servers\":[]}", + }, + { + name: "defaults", + request: SubnetCreateRequest{ + DNSServers: &UseCloudscaleDefaults, + }, + expected: "{\"dns_servers\":null}", + }, + { + name: "gateway", + request: SubnetCreateRequest{ + GatewayAddress: "192.168.1.1", + }, + expected: "{\"gateway_address\":\"192.168.1.1\"}", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + b, err := json.Marshal(tc.request) + if err != nil { + t.Errorf("Error marshaling JSON: %v", err) + } + if actualOutput := string(b); actualOutput != tc.expected { + t.Errorf("Unexpected JSON output:\nExpected: %s\nActual: %s", tc.expected, actualOutput) + } + }) + } +} diff --git a/test/integration/subnets_integration_test.go b/test/integration/subnets_integration_test.go index 9783eae..cee72a4 100644 --- a/test/integration/subnets_integration_test.go +++ b/test/integration/subnets_integration_test.go @@ -10,6 +10,8 @@ import ( "testing" ) +const numberOfDefaultEntries = 2 + func TestIntegrationSubnet_GetAndList(t *testing.T) { integrationTest(t) @@ -75,7 +77,7 @@ func TestIntegrationSubnet_CRUD(t *testing.T) { createSubnetRequest := &cloudscale.SubnetCreateRequest{ CIDR: "192.168.192.0/22", GatewayAddress: "192.168.192.2", - DNSServers: []string{"77.109.128.2", "213.144.129.20"}, + DNSServers: &[]string{"77.109.128.2", "213.144.129.20"}, Network: network.UUID, } expected, err := client.Subnets.Create(context.TODO(), createSubnetRequest) @@ -134,6 +136,11 @@ func TestIntegrationSubnet_Update(t *testing.T) { t.Fatalf("Subnets.Create returned error %s\n", err) } + // assert initial DNSServers, no option was passed, hence defaults should be used + if actualDNSServers := subnet.DNSServers; !(len(actualDNSServers) == 2) { + t.Errorf("Subnet DNSServers length\ngot=%#v\nwant=%#v", len(actualDNSServers), 2) + } + // update gateway expectedGateway := "10.255.255.254" updateRequest := &cloudscale.SubnetUpdateRequest{ @@ -193,9 +200,9 @@ func TestIntegrationSubnet_Update(t *testing.T) { t.Errorf("Subnet DNSServers\ngot=%#v\nwant=%#v", actualDNSServers, []string{}) } - // update to Default DNSServer by submitting nil + // update to Default DNSServer updateRequest = &cloudscale.SubnetUpdateRequest{ - DNSServers: nil, + DNSServers: &cloudscale.UseCloudscaleDefaults, } err = client.Subnets.Update(context.Background(), subnet.UUID, updateRequest) @@ -208,8 +215,69 @@ func TestIntegrationSubnet_Update(t *testing.T) { t.Fatalf("Subnets.Get returned error %s\n", err) } - if actualDNSServers := updatedSubnet.DNSServers; !reflect.DeepEqual(actualDNSServers, []string{}) { - t.Errorf("Subnet DNSServers\ngot=%#v\nwant=%#v", actualDNSServers, []string{}) + // assert default servers + if actualNumberOfEntries := len(updatedSubnet.DNSServers); !(actualNumberOfEntries == numberOfDefaultEntries) { + t.Errorf("Subnet DNSServers length\ngot=%#v\nwant=%#v", actualNumberOfEntries, numberOfDefaultEntries) + } + + err = client.Subnets.Delete(context.Background(), subnet.UUID) + if err != nil { + t.Fatalf("Networks.Delete returned error %s\n", err) + } + + err = client.Networks.Delete(context.Background(), network.UUID) + if err != nil { + t.Fatalf("Networks.Delete returned error %s\n", err) + } +} + +func TestIntegrationSubnet_InitialEmptyDNSServers(t *testing.T) { + integrationTest(t) + + autoCreateSubnet := false + createNetworkRequest := &cloudscale.NetworkCreateRequest{ + Name: testRunPrefix, + AutoCreateIPV4Subnet: &autoCreateSubnet, + } + network, err := client.Networks.Create(context.TODO(), createNetworkRequest) + if err != nil { + t.Fatalf("Networks.Create returned error %s\n", err) + } + + createSubnetRequest := &cloudscale.SubnetCreateRequest{ + Network: network.UUID, + CIDR: "10.0.0.0/8", + DNSServers: &[]string{}, + } + + subnet, err := client.Subnets.Create(context.TODO(), createSubnetRequest) + if err != nil { + t.Fatalf("Subnets.Create returned error %s\n", err) + } + + // assert initial DNSServers + if actualDNSServers := subnet.DNSServers; !reflect.DeepEqual(actualDNSServers, []string{}) { + t.Errorf("Subnet DNSServers\ngot=%#v\nwant=%#v", subnet.DNSServers, []string{}) + } + + // update DNSServers + updateRequest := &cloudscale.SubnetUpdateRequest{ + DNSServers: &cloudscale.UseCloudscaleDefaults, + } + + err = client.Subnets.Update(context.Background(), subnet.UUID, updateRequest) + if err != nil { + t.Fatalf("Subnets.Update returned error %s\n", err) + } + + updatedSubnet, err := client.Subnets.Get(context.Background(), subnet.UUID) + if err != nil { + t.Fatalf("Subnets.Get returned error %s\n", err) + } + + // assert default servers + if actualNumberOfEntries := len(updatedSubnet.DNSServers); !(actualNumberOfEntries == numberOfDefaultEntries) { + t.Errorf("Subnet DNSServers length\ngot=%#v\nwant=%#v", actualNumberOfEntries, numberOfDefaultEntries) } err = client.Subnets.Delete(context.Background(), subnet.UUID)