From 3939e3b54b69520ebcde2a8e60cffd9ff27800bc Mon Sep 17 00:00:00 2001 From: Michael Weibel Date: Tue, 21 Jul 2026 17:56:48 +0200 Subject: [PATCH 1/6] router: add initial API client --- cloudscale.go | 8 + generic_service.go | 27 ++ router.go | 116 +++++++ router_test.go | 327 +++++++++++++++++++ test/integration/cloudscale_test.go | 23 ++ test/integration/routers_integration_test.go | 122 +++++++ 6 files changed, 623 insertions(+) create mode 100644 router.go create mode 100644 router_test.go create mode 100644 test/integration/routers_integration_test.go diff --git a/cloudscale.go b/cloudscale.go index 2afb66b..f9599bc 100644 --- a/cloudscale.go +++ b/cloudscale.go @@ -40,6 +40,7 @@ type Client struct { VolumeSnapshots VolumeSnapshotService Networks NetworkService Subnets SubnetService + Routers RouterService FloatingIPs FloatingIPsService ServerGroups ServerGroupService ObjectsUsers ObjectsUsersService @@ -86,6 +87,13 @@ func NewClient(httpClient *http.Client) *Client { client: c, path: subnetBasePath, } + c.Routers = RouterServiceOperations{ + GenericServiceOperations: GenericServiceOperations[Router, RouterCreateRequest, RouterUpdateRequest]{ + client: c, + path: routerBasePath, + }, + client: c, + } c.FloatingIPs = GenericServiceOperations[FloatingIP, FloatingIPCreateRequest, FloatingIPUpdateRequest]{ client: c, path: floatingIPsBasePath, diff --git a/generic_service.go b/generic_service.go index f00c15c..876474d 100644 --- a/generic_service.go +++ b/generic_service.go @@ -2,6 +2,7 @@ package cloudscale import ( "context" + "errors" "fmt" "net/http" "time" @@ -31,6 +32,7 @@ type GenericDeleteService[TResource any] interface { type GenericWaitForService[TResource any] interface { WaitFor(ctx context.Context, resourceID string, condition func(resource *TResource) (bool, error), opts ...backoff.RetryOption) (*TResource, error) + WaitForDeleted(ctx context.Context, resourceID string, opts ...backoff.RetryOption) error } type GenericServiceOperations[TResource any, TCreateRequest any, TUpdateRequest any] struct { @@ -166,3 +168,28 @@ func (g GenericServiceOperations[TResource, TCreateRequest, TUpdateRequest]) Wai return nil, fmt.Errorf("condition not met yet") // Continue retrying }, options...) } + +func (g GenericServiceOperations[TResource, TCreateRequest, TUpdateRequest]) WaitForDeleted( + ctx context.Context, + resourceID string, + opts ...backoff.RetryOption, +) error { + // Prepend the default backoff option. + // If a user passes their own WithBackOff option, it will override this default. + options := append([]backoff.RetryOption{ + backoff.WithBackOff(backoff.NewConstantBackOff(2 * time.Second)), + backoff.WithMaxElapsedTime(5 * time.Minute), + }, opts...) + + _, err := backoff.Retry(ctx, func() (struct{}, error) { + _, err := g.Get(ctx, resourceID) + if err == nil { + return struct{}{}, fmt.Errorf("resource %q not deleted yet", resourceID) + } + if cerr, ok := errors.AsType[*ErrorResponse](err); ok && cerr.StatusCode == http.StatusNotFound { + return struct{}{}, nil + } + return struct{}{}, err + }, options...) + return err +} diff --git a/router.go b/router.go new file mode 100644 index 0000000..f15185d --- /dev/null +++ b/router.go @@ -0,0 +1,116 @@ +package cloudscale + +import ( + "context" + "fmt" + "net/http" + "net/netip" + "time" +) + +const routerBasePath = "v1/routers" + +type Router struct { + ZonalResource + TaggedResource + HREF string `json:"href"` + UUID string `json:"uuid"` + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` + Status string `json:"status"` + InternetGateway bool `json:"internet_gateway"` + InternetGatewayAddresses []IPAddress `json:"internet_gateway_addresses,omitempty"` + InternalInterfaces []InternalInterface `json:"internal_interfaces,omitempty"` +} + +type IPAddress struct { + Address netip.Addr `json:"address"` + Subnet SubnetStub `json:"subnet"` + Version int `json:"version"` + ReversePTR *string `json:"reverse_ptr"` +} +type InternalInterface struct { + UUID string `json:"uuid"` + Network NetworkStub `json:"network"` + Addresses []IPAddress `json:"addresses"` + Type string `json:"type"` + MACAddress string `json:"mac_address"` +} + +type RouterCreateRequest struct { + ZonalResourceRequest + TaggedResourceRequest + Name string `json:"name"` + InternetGateway bool `json:"internet_gateway"` +} + +// RouterUpdateRequest is not implemented yet because the API is not implemented yet +type RouterUpdateRequest struct{} + +type RouterService interface { + GenericCreateService[Router, RouterCreateRequest] + GenericGetService[Router] + GenericListService[Router] + // GenericUpdateService[Router, RouterUpdateRequest] + GenericDeleteService[Router] + GenericWaitForService[Router] + // CreateInterface creates a new interface attached to this router + CreateInterface(ctx context.Context, routerUUID string, createReq CreateInterfaceRequest) (*InternalInterface, error) + // DeleteInterface removes an interface attached to this router + DeleteInterface(ctx context.Context, routerUUID, interfaceUUID string) error +} + +type CreateInterfaceRequest struct { + Network string `json:"network"` + Addresses []CreateAddressRequest `json:"addresses"` +} +type CreateAddressRequest struct { + Subnet string `json:"subnet"` + Address netip.Addr `json:"address"` +} + +type RouterServiceOperations struct { + GenericServiceOperations[Router, RouterCreateRequest, RouterUpdateRequest] + client *Client +} + +func (r RouterServiceOperations) CreateInterface(ctx context.Context, routerUUID string, createReq CreateInterfaceRequest) (*InternalInterface, error) { + path := fmt.Sprintf("%s/%s/create_interface", routerBasePath, routerUUID) + ctx = WithOperationPath(ctx, routerBasePath+"/:id/create_interface") + req, err := r.client.NewRequest(ctx, http.MethodPost, path, createReq) + if err != nil { + return nil, err + } + res := &InternalInterface{} + if err := r.client.Do(ctx, req, res); err != nil { + return nil, err + } + return res, nil +} + +func (r RouterServiceOperations) DeleteInterface(ctx context.Context, routerUUID, interfaceUUID string) error { + path := fmt.Sprintf("%s/%s/delete_interface", routerBasePath, routerUUID) + ctx = WithOperationPath(ctx, routerBasePath+"/:id/delete_interface") + + type DeleteInterfaceRequest struct { + Interface string `json:"interface"` + } + deleteReq := DeleteInterfaceRequest{Interface: interfaceUUID} + + req, err := r.client.NewRequest(ctx, http.MethodPost, path, deleteReq) + if err != nil { + return err + } + return r.client.Do(ctx, req, nil) +} + +const ( + RouterActive = "active" +) + +var RouterIsActive = func(router *Router) (bool, error) { + if router.Status == RouterActive { + return true, nil + } + return false, fmt.Errorf("waiting for status: %s, current status: %s", RouterActive, router.Status) +} diff --git a/router_test.go b/router_test.go new file mode 100644 index 0000000..49a638f --- /dev/null +++ b/router_test.go @@ -0,0 +1,327 @@ +package cloudscale + +import ( + "encoding/json" + "fmt" + "net/http" + "net/netip" + "reflect" + "testing" + "time" +) + +func TestRouters_Create(t *testing.T) { + setup() + defer teardown() + + routerRequest := &RouterCreateRequest{ + Name: "gw", + InternetGateway: true, + } + + mux.HandleFunc("/v1/routers", func(w http.ResponseWriter, r *http.Request) { + testHTTPMethod(t, r, http.MethodPost) + + expected := map[string]any{ + "name": "gw", + "internet_gateway": true, + } + + var v map[string]any + if err := json.NewDecoder(r.Body).Decode(&v); err != nil { + t.Fatalf("decode json: %v", err) + } + + if !reflect.DeepEqual(v, expected) { + t.Errorf("Request body\n got=%#v\nwant=%#v", v, expected) + } + + _, _ = fmt.Fprint(w, `{"uuid": "42cec963-fcd2-482f-bdb6-24461b2d47b1"}`) + }) + + router, err := client.Routers.Create(ctx, routerRequest) + if err != nil { + t.Errorf("Routers.Create returned error: %v", err) + } + + if id := router.UUID; id != "42cec963-fcd2-482f-bdb6-24461b2d47b1" { + t.Errorf("expected id '42cec963-fcd2-482f-bdb6-24461b2d47b1', received '%s'", id) + } +} + +func TestRouters_Get(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v1/routers/cfde831a-4e87-4a75-960f-89b0148aa2cc", func(w http.ResponseWriter, r *http.Request) { + testHTTPMethod(t, r, http.MethodGet) + _, _ = fmt.Fprint(w, `{ + "href": "https://api.cloudscale.ch/v1/routers/cfde831a-4e87-4a75-960f-89b0148aa2cc", + "uuid": "cfde831a-4e87-4a75-960f-89b0148aa2cc", + "name": "gw", + "zone": {"slug": "lpg1"}, + "created_at": "2019-05-27T16:45:32.241824Z", + "status": "up", + "internet_gateway": true, + "internet_gateway_addresses": [ + { + "address": "203.0.113.1", + "subnet": { + "href": "https://api.cloudscale.ch/v1/subnets/8a04e678-4f1c-4d5f-9e40-8f0eaf1d0e0d", + "cidr": "203.0.113.0/24", + "uuid": "8a04e678-4f1c-4d5f-9e40-8f0eaf1d0e0d" + }, + "version": 4, + "reverse_ptr": "203-0-113-1.cust.example.com" + }, + { + "address": "2001:db8::1", + "subnet": { + "href": "https://api.cloudscale.ch/v1/subnets/9204e678-4f1c-4d5f-9e40-8f0eaf1d0eaa", + "cidr": "2001:db8::/32", + "uuid": "9204e678-4f1c-4d5f-9e40-8f0eaf1d0eaa" + }, + "version": 6, + "reverse_ptr": "203-0-113-1.cust.example.com" + } + ], + "internal_interfaces": [ + { + "uuid": "1e0c6f9c-9f0d-4d1b-9f0d-8f0eaf1d0e0d", + "network": { + "href": "https://api.cloudscale.ch/v1/networks/7f0eaf1d-0e0d-4d5f-9e40-8f0eaf1d0e0d", + "name": "my-network", + "uuid": "7f0eaf1d-0e0d-4d5f-9e40-8f0eaf1d0e0d" + }, + "addresses": [ + { + "address": "10.0.0.1", + "subnet": { + "href": "https://api.cloudscale.ch/v1/subnets/3d6ca1f4-5aea-41f5-b724-0f3054b60e85", + "cidr": "10.0.0.0/24", + "uuid": "3d6ca1f4-5aea-41f5-b724-0f3054b60e85" + }, + "version": 4, + "reverse_ptr": null + } + ], + "type": "private", + "mac_address": "00:00:5e:00:53:ab" + } + ] + }`) + }) + + router, err := client.Routers.Get(ctx, "cfde831a-4e87-4a75-960f-89b0148aa2cc") + if err != nil { + t.Errorf("Routers.Get returned error: %v", err) + } + + expected := &Router{ + ZonalResource: ZonalResource{ + Zone: ZoneStub{Slug: "lpg1"}, + }, + HREF: "https://api.cloudscale.ch/v1/routers/cfde831a-4e87-4a75-960f-89b0148aa2cc", + UUID: "cfde831a-4e87-4a75-960f-89b0148aa2cc", + Name: "gw", + CreatedAt: time.Date(2019, time.Month(5), 27, 16, 45, 32, 241824000, time.UTC), + Status: "up", + InternetGateway: true, + InternetGatewayAddresses: []IPAddress{ + { + Address: netip.MustParseAddr("203.0.113.1"), + Subnet: SubnetStub{ + HREF: "https://api.cloudscale.ch/v1/subnets/8a04e678-4f1c-4d5f-9e40-8f0eaf1d0e0d", + CIDR: "203.0.113.0/24", + UUID: "8a04e678-4f1c-4d5f-9e40-8f0eaf1d0e0d", + }, + Version: 4, + ReversePTR: new("203-0-113-1.cust.example.com"), + }, + { + Address: netip.MustParseAddr("2001:db8::1"), + Subnet: SubnetStub{ + HREF: "https://api.cloudscale.ch/v1/subnets/9204e678-4f1c-4d5f-9e40-8f0eaf1d0eaa", + CIDR: "2001:db8::/32", + UUID: "9204e678-4f1c-4d5f-9e40-8f0eaf1d0eaa", + }, + Version: 6, + ReversePTR: new("203-0-113-1.cust.example.com"), + }, + }, + InternalInterfaces: []InternalInterface{ + { + UUID: "1e0c6f9c-9f0d-4d1b-9f0d-8f0eaf1d0e0d", + Network: NetworkStub{ + HREF: "https://api.cloudscale.ch/v1/networks/7f0eaf1d-0e0d-4d5f-9e40-8f0eaf1d0e0d", + Name: "my-network", + UUID: "7f0eaf1d-0e0d-4d5f-9e40-8f0eaf1d0e0d", + }, + Addresses: []IPAddress{ + { + Address: netip.MustParseAddr("10.0.0.1"), + Subnet: SubnetStub{ + HREF: "https://api.cloudscale.ch/v1/subnets/3d6ca1f4-5aea-41f5-b724-0f3054b60e85", + CIDR: "10.0.0.0/24", + UUID: "3d6ca1f4-5aea-41f5-b724-0f3054b60e85", + }, + Version: 4, + ReversePTR: nil, + }, + }, + Type: "private", + MACAddress: "00:00:5e:00:53:ab", + }, + }, + } + + if !reflect.DeepEqual(router, expected) { + t.Errorf("Routers.Get\n got=%#v\nwant=%#v", router, expected) + } +} + +func TestRouters_List(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v1/routers", func(w http.ResponseWriter, r *http.Request) { + testHTTPMethod(t, r, http.MethodGet) + _, _ = fmt.Fprint(w, `[{"uuid": "47cec963-fcd2-482f-bdb6-24461b2d47b1"}]`) + }) + + routers, err := client.Routers.List(ctx) + if err != nil { + t.Errorf("Routers.List returned error: %v", err) + } + + expected := []Router{{UUID: "47cec963-fcd2-482f-bdb6-24461b2d47b1"}} + if !reflect.DeepEqual(routers, expected) { + t.Errorf("Routers.List\n got=%#v\nwant=%#v", routers, expected) + } +} + +func TestRouters_CreateInterface(t *testing.T) { + setup() + defer teardown() + + createReq := CreateInterfaceRequest{ + Network: "7f0eaf1d-0e0d-4d5f-9e40-8f0eaf1d0e0d", + Addresses: []CreateAddressRequest{ + { + Subnet: "3d6ca1f4-5aea-41f5-b724-0f3054b60e85", + Address: netip.MustParseAddr("10.0.0.1"), + }, + }, + } + + mux.HandleFunc("/v1/routers/cfde831a-4e87-4a75-960f-89b0148aa2cc/create_interface", func(w http.ResponseWriter, r *http.Request) { + testHTTPMethod(t, r, http.MethodPost) + + expected := map[string]any{ + "network": "7f0eaf1d-0e0d-4d5f-9e40-8f0eaf1d0e0d", + "addresses": []any{ + map[string]any{ + "subnet": "3d6ca1f4-5aea-41f5-b724-0f3054b60e85", + "address": "10.0.0.1", + }, + }, + } + + var v map[string]any + if err := json.NewDecoder(r.Body).Decode(&v); err != nil { + t.Fatalf("decode json: %v", err) + } + + if !reflect.DeepEqual(v, expected) { + t.Errorf("Request body\n got=%#v\nwant=%#v", v, expected) + } + + _, _ = fmt.Fprint(w, `{ + "uuid": "1e0c6f9c-9f0d-4d1b-9f0d-8f0eaf1d0e0d", + "network": { + "href": "https://api.cloudscale.ch/v1/networks/7f0eaf1d-0e0d-4d5f-9e40-8f0eaf1d0e0d", + "name": "my-network", + "uuid": "7f0eaf1d-0e0d-4d5f-9e40-8f0eaf1d0e0d" + }, + "addresses": [ + { + "address": "10.0.0.1", + "subnet": { + "href": "https://api.cloudscale.ch/v1/subnets/3d6ca1f4-5aea-41f5-b724-0f3054b60e85", + "cidr": "10.0.0.0/24", + "uuid": "3d6ca1f4-5aea-41f5-b724-0f3054b60e85" + }, + "version": 4, + "reverse_ptr": null + } + ], + "type": "vip", + "mac_address": "aa:bb:cc:dd:ee:ff" + }`) + }) + + iface, err := client.Routers.CreateInterface(ctx, "cfde831a-4e87-4a75-960f-89b0148aa2cc", createReq) + if err != nil { + t.Errorf("Routers.CreateInterface returned error: %v", err) + } + + expected := &InternalInterface{ + UUID: "1e0c6f9c-9f0d-4d1b-9f0d-8f0eaf1d0e0d", + Network: NetworkStub{ + HREF: "https://api.cloudscale.ch/v1/networks/7f0eaf1d-0e0d-4d5f-9e40-8f0eaf1d0e0d", + Name: "my-network", + UUID: "7f0eaf1d-0e0d-4d5f-9e40-8f0eaf1d0e0d", + }, + Addresses: []IPAddress{ + { + Address: netip.MustParseAddr("10.0.0.1"), + Subnet: SubnetStub{ + HREF: "https://api.cloudscale.ch/v1/subnets/3d6ca1f4-5aea-41f5-b724-0f3054b60e85", + CIDR: "10.0.0.0/24", + UUID: "3d6ca1f4-5aea-41f5-b724-0f3054b60e85", + }, + Version: 4, + ReversePTR: nil, + }, + }, + Type: "vip", + MACAddress: "aa:bb:cc:dd:ee:ff", + } + + if !reflect.DeepEqual(iface, expected) { + t.Errorf("Routers.CreateInterface\n got=%#v\nwant=%#v", iface, expected) + } +} + +func TestRouters_DeleteInterface(t *testing.T) { + setup() + defer teardown() + + called := false + mux.HandleFunc("/v1/routers/cfde831a-4e87-4a75-960f-89b0148aa2cc/delete_interface", func(w http.ResponseWriter, r *http.Request) { + testHTTPMethod(t, r, http.MethodPost) + + expected := map[string]any{ + "interface": "1e0c6f9c-9f0d-4d1b-9f0d-8f0eaf1d0e0d", + } + + var v map[string]any + if err := json.NewDecoder(r.Body).Decode(&v); err != nil { + t.Fatalf("decode json: %v", err) + } + + if !reflect.DeepEqual(v, expected) { + t.Errorf("Request body\n got=%#v\nwant=%#v", v, expected) + } + called = true + }) + + err := client.Routers.DeleteInterface(ctx, "cfde831a-4e87-4a75-960f-89b0148aa2cc", "1e0c6f9c-9f0d-4d1b-9f0d-8f0eaf1d0e0d") + if err != nil { + t.Errorf("Routers.DeleteInterface returned error: %v", err) + } + + if !called { + t.Error("expected delete_interface endpoint to be called") + } +} diff --git a/test/integration/cloudscale_test.go b/test/integration/cloudscale_test.go index 8d3ea96..7044715 100644 --- a/test/integration/cloudscale_test.go +++ b/test/integration/cloudscale_test.go @@ -49,6 +49,7 @@ func TestMain(m *testing.M) { foundResource = foundResource || DeleteRemainingServerGroups() foundResource = foundResource || DeleteRemainingVolumeSnapshots() foundResource = foundResource || DeleteRemainingVolumes() + foundResource = foundResource || DeleteRemainingRouters() foundResource = foundResource || DeleteRemainingSubnets() foundResource = foundResource || DeleteRemainingNetworks() foundResource = foundResource || DeleteRemainingObjectsUsers() @@ -237,6 +238,28 @@ func DeleteRemainingCustomImages() bool { return foundResource } +func DeleteRemainingRouters() bool { + foundResource := false + + routers, err := client.Routers.List(context.Background()) + if err != nil { + log.Fatalf("Routers.List returned error %s\n", err) + } + + for _, router := range routers { + if strings.HasPrefix(router.Name, testRunPrefix) { + foundResource = true + log.Printf("Found not deleted router: %s (%s)\n", router.Name, router.UUID) + err = client.Routers.Delete(context.Background(), router.UUID) + if err != nil { + log.Fatalf("Routers.Delete returned error %s\n", err) + } + } + } + + return foundResource +} + func DeleteRemainingLoadBalancers() bool { foundResource := false diff --git a/test/integration/routers_integration_test.go b/test/integration/routers_integration_test.go new file mode 100644 index 0000000..2e4e65e --- /dev/null +++ b/test/integration/routers_integration_test.go @@ -0,0 +1,122 @@ +//go:build integration + +package integration + +import ( + "net/netip" + "testing" + "time" + + "github.com/cloudscale-ch/cloudscale-go-sdk/v9" +) + +func TestIntegrationRouter_CRUD(t *testing.T) { + t.Parallel() + + createRouterRequest := &cloudscale.RouterCreateRequest{ + Name: testRunPrefix, + InternetGateway: true, + ZonalResourceRequest: cloudscale.ZonalResourceRequest{Zone: testZone}, + } + + expected, err := client.Routers.Create(t.Context(), createRouterRequest) + if err != nil { + t.Fatalf("Routers.Create returned error %s\n", err) + } + + router, err := client.Routers.Get(t.Context(), expected.UUID) + if err != nil { + t.Fatalf("Routers.Get returned error %s\n", err) + } + + if uuid := router.UUID; uuid != expected.UUID { + t.Errorf("Router.UUID got=%s\nwant=%s", uuid, expected.UUID) + } + + if h := time.Since(router.CreatedAt).Hours(); !(-1 < h && h < 1) { + t.Errorf("router.CreatedAt outside of expected range. got=%v", router.CreatedAt) + } + + if !router.InternetGateway { + t.Errorf("router.InternetGateway got=%v\nwant=%v", router.InternetGateway, true) + } + + if _, err := client.Routers.WaitFor(t.Context(), router.UUID, cloudscale.RouterIsActive); err != nil { + t.Errorf("router not in active state: %v", err) + } + + routers, err := client.Routers.List(t.Context()) + if err != nil { + t.Fatalf("Routers.List returned error %s\n", err) + } + + if numRouters := len(routers); numRouters != 1 { + t.Errorf("Routers.List got=%d\nwant>=%d\n", numRouters, 1) + } + + // Set up a network with a subnet so we can attach an interface to the router. + createNetworkRequest := &cloudscale.NetworkCreateRequest{ + Name: testRunPrefix, + AutoCreateIPV4Subnet: new(false), + } + network, err := client.Networks.Create(t.Context(), createNetworkRequest) + if err != nil { + t.Fatalf("Networks.Create returned error %s\n", err) + } + + createSubnetRequest := &cloudscale.SubnetCreateRequest{ + Network: network.UUID, + CIDR: "192.168.99.0/24", + } + subnet, err := client.Subnets.Create(t.Context(), createSubnetRequest) + if err != nil { + t.Fatalf("Subnets.Create returned error %s\n", err) + } + + createInterfaceRequest := cloudscale.CreateInterfaceRequest{ + Network: network.UUID, + Addresses: []cloudscale.CreateAddressRequest{ + { + Subnet: subnet.UUID, + Address: netip.MustParseAddr("192.168.99.10"), + }, + }, + } + iface, err := client.Routers.CreateInterface(t.Context(), router.UUID, createInterfaceRequest) + if err != nil { + t.Fatalf("Routers.CreateInterface returned error %s\n", err) + } + + if iface.UUID == "" { + t.Error("Routers.CreateInterface returned interface without UUID") + } + if networkUUID := iface.Network.UUID; networkUUID != network.UUID { + t.Errorf("interface.Network.UUID got=%s\nwant=%s", networkUUID, network.UUID) + } + if numAddresses := len(iface.Addresses); numAddresses != 1 { + t.Fatalf("interface Addresses got=%d\nwant=%d", numAddresses, 1) + } + if subnetUUID := iface.Addresses[0].Subnet.UUID; subnetUUID != subnet.UUID { + t.Errorf("interface.Addresses[0].Subnet.UUID got=%s\nwant=%s", subnetUUID, subnet.UUID) + } + if addr := iface.Addresses[0].Address; addr != netip.MustParseAddr("192.168.99.10") { + t.Errorf("interface.Addresses[0].Address got=%s\nwant=%s", addr, "192.168.99.10") + } + + // Clean up: only remove router and, once it's deleted, the network. Sub-resources like interfaces attached to the router + // and subnets within the network are removed automatically. + + err = client.Routers.Delete(t.Context(), router.UUID) + if err != nil { + t.Fatalf("Routers.Delete returned error %s\n", err) + } + + if err := client.Routers.WaitForDeleted(t.Context(), router.UUID); err != nil { + t.Errorf("router %q has not been deleted: %v", router.UUID, err) + } + + err = client.Networks.Delete(t.Context(), network.UUID) + if err != nil { + t.Fatalf("Networks.Delete returned error %s\n", err) + } +} From 288d4f2e6e257b3cbbfe7f20de8980fe953ce51a Mon Sep 17 00:00:00 2001 From: Michael Weibel Date: Fri, 24 Jul 2026 14:40:28 +0200 Subject: [PATCH 2/6] router: use string instead of netip, replace WaitForDeleted with waitForDeleted helper --- generic_service.go | 27 ------------- router.go | 7 ++-- router_test.go | 11 +++-- .../integration/{helper.go => helper_test.go} | 24 +++++++++++ test/integration/routers_integration_test.go | 40 ++++++++++++------- 5 files changed, 58 insertions(+), 51 deletions(-) rename test/integration/{helper.go => helper_test.go} (64%) diff --git a/generic_service.go b/generic_service.go index 876474d..f00c15c 100644 --- a/generic_service.go +++ b/generic_service.go @@ -2,7 +2,6 @@ package cloudscale import ( "context" - "errors" "fmt" "net/http" "time" @@ -32,7 +31,6 @@ type GenericDeleteService[TResource any] interface { type GenericWaitForService[TResource any] interface { WaitFor(ctx context.Context, resourceID string, condition func(resource *TResource) (bool, error), opts ...backoff.RetryOption) (*TResource, error) - WaitForDeleted(ctx context.Context, resourceID string, opts ...backoff.RetryOption) error } type GenericServiceOperations[TResource any, TCreateRequest any, TUpdateRequest any] struct { @@ -168,28 +166,3 @@ func (g GenericServiceOperations[TResource, TCreateRequest, TUpdateRequest]) Wai return nil, fmt.Errorf("condition not met yet") // Continue retrying }, options...) } - -func (g GenericServiceOperations[TResource, TCreateRequest, TUpdateRequest]) WaitForDeleted( - ctx context.Context, - resourceID string, - opts ...backoff.RetryOption, -) error { - // Prepend the default backoff option. - // If a user passes their own WithBackOff option, it will override this default. - options := append([]backoff.RetryOption{ - backoff.WithBackOff(backoff.NewConstantBackOff(2 * time.Second)), - backoff.WithMaxElapsedTime(5 * time.Minute), - }, opts...) - - _, err := backoff.Retry(ctx, func() (struct{}, error) { - _, err := g.Get(ctx, resourceID) - if err == nil { - return struct{}{}, fmt.Errorf("resource %q not deleted yet", resourceID) - } - if cerr, ok := errors.AsType[*ErrorResponse](err); ok && cerr.StatusCode == http.StatusNotFound { - return struct{}{}, nil - } - return struct{}{}, err - }, options...) - return err -} diff --git a/router.go b/router.go index f15185d..3d44adb 100644 --- a/router.go +++ b/router.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/http" - "net/netip" "time" ) @@ -24,7 +23,7 @@ type Router struct { } type IPAddress struct { - Address netip.Addr `json:"address"` + Address string `json:"address"` Subnet SubnetStub `json:"subnet"` Version int `json:"version"` ReversePTR *string `json:"reverse_ptr"` @@ -65,8 +64,8 @@ type CreateInterfaceRequest struct { Addresses []CreateAddressRequest `json:"addresses"` } type CreateAddressRequest struct { - Subnet string `json:"subnet"` - Address netip.Addr `json:"address"` + Subnet string `json:"subnet"` + Address string `json:"address"` } type RouterServiceOperations struct { diff --git a/router_test.go b/router_test.go index 49a638f..680ebf6 100644 --- a/router_test.go +++ b/router_test.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "net/http" - "net/netip" "reflect" "testing" "time" @@ -129,7 +128,7 @@ func TestRouters_Get(t *testing.T) { InternetGateway: true, InternetGatewayAddresses: []IPAddress{ { - Address: netip.MustParseAddr("203.0.113.1"), + Address: "203.0.113.1", Subnet: SubnetStub{ HREF: "https://api.cloudscale.ch/v1/subnets/8a04e678-4f1c-4d5f-9e40-8f0eaf1d0e0d", CIDR: "203.0.113.0/24", @@ -139,7 +138,7 @@ func TestRouters_Get(t *testing.T) { ReversePTR: new("203-0-113-1.cust.example.com"), }, { - Address: netip.MustParseAddr("2001:db8::1"), + Address: "2001:db8::1", Subnet: SubnetStub{ HREF: "https://api.cloudscale.ch/v1/subnets/9204e678-4f1c-4d5f-9e40-8f0eaf1d0eaa", CIDR: "2001:db8::/32", @@ -159,7 +158,7 @@ func TestRouters_Get(t *testing.T) { }, Addresses: []IPAddress{ { - Address: netip.MustParseAddr("10.0.0.1"), + Address: "10.0.0.1", Subnet: SubnetStub{ HREF: "https://api.cloudscale.ch/v1/subnets/3d6ca1f4-5aea-41f5-b724-0f3054b60e85", CIDR: "10.0.0.0/24", @@ -209,7 +208,7 @@ func TestRouters_CreateInterface(t *testing.T) { Addresses: []CreateAddressRequest{ { Subnet: "3d6ca1f4-5aea-41f5-b724-0f3054b60e85", - Address: netip.MustParseAddr("10.0.0.1"), + Address: "10.0.0.1", }, }, } @@ -274,7 +273,7 @@ func TestRouters_CreateInterface(t *testing.T) { }, Addresses: []IPAddress{ { - Address: netip.MustParseAddr("10.0.0.1"), + Address: "10.0.0.1", Subnet: SubnetStub{ HREF: "https://api.cloudscale.ch/v1/subnets/3d6ca1f4-5aea-41f5-b724-0f3054b60e85", CIDR: "10.0.0.0/24", diff --git a/test/integration/helper.go b/test/integration/helper_test.go similarity index 64% rename from test/integration/helper.go rename to test/integration/helper_test.go index f268b96..8004f57 100644 --- a/test/integration/helper.go +++ b/test/integration/helper_test.go @@ -4,9 +4,13 @@ package integration import ( "context" + "errors" "math/rand" "reflect" "testing" + "time" + + "github.com/cenkalti/backoff/v5" "github.com/cloudscale-ch/cloudscale-go-sdk/v9" ) @@ -46,3 +50,23 @@ func assertEqual(t *testing.T, expected any, actual any) { t.Errorf("Assertion failed:\nexpected: %#v\n actual: %#v", expected, actual) } } + +// waitForDeleted calls existsFunc in a backoff loop until exists is false. +func waitForDeleted(ctx context.Context, existsFunc func() (exists bool, err error)) error { + options := []backoff.RetryOption{ + backoff.WithBackOff(backoff.NewConstantBackOff(2 * time.Second)), + backoff.WithMaxElapsedTime(5 * time.Minute), + } + + _, err := backoff.Retry(ctx, func() (struct{}, error) { + exists, err := existsFunc() + if !exists { + return struct{}{}, nil + } + if err == nil { + return struct{}{}, errors.New("resource not deleted yet") + } + return struct{}{}, err + }, options...) + return err +} diff --git a/test/integration/routers_integration_test.go b/test/integration/routers_integration_test.go index 2e4e65e..ab57e3b 100644 --- a/test/integration/routers_integration_test.go +++ b/test/integration/routers_integration_test.go @@ -3,7 +3,8 @@ package integration import ( - "net/netip" + "errors" + "net/http" "testing" "time" @@ -21,12 +22,12 @@ func TestIntegrationRouter_CRUD(t *testing.T) { expected, err := client.Routers.Create(t.Context(), createRouterRequest) if err != nil { - t.Fatalf("Routers.Create returned error %s\n", err) + t.Fatalf("Routers.Create returned error %s", err) } router, err := client.Routers.Get(t.Context(), expected.UUID) if err != nil { - t.Fatalf("Routers.Get returned error %s\n", err) + t.Fatalf("Routers.Get returned error %s", err) } if uuid := router.UUID; uuid != expected.UUID { @@ -61,7 +62,7 @@ func TestIntegrationRouter_CRUD(t *testing.T) { } network, err := client.Networks.Create(t.Context(), createNetworkRequest) if err != nil { - t.Fatalf("Networks.Create returned error %s\n", err) + t.Fatalf("Networks.Create returned error %s", err) } createSubnetRequest := &cloudscale.SubnetCreateRequest{ @@ -70,7 +71,7 @@ func TestIntegrationRouter_CRUD(t *testing.T) { } subnet, err := client.Subnets.Create(t.Context(), createSubnetRequest) if err != nil { - t.Fatalf("Subnets.Create returned error %s\n", err) + t.Fatalf("Subnets.Create returned error %s", err) } createInterfaceRequest := cloudscale.CreateInterfaceRequest{ @@ -78,13 +79,13 @@ func TestIntegrationRouter_CRUD(t *testing.T) { Addresses: []cloudscale.CreateAddressRequest{ { Subnet: subnet.UUID, - Address: netip.MustParseAddr("192.168.99.10"), + Address: "192.168.99.10", }, }, } iface, err := client.Routers.CreateInterface(t.Context(), router.UUID, createInterfaceRequest) if err != nil { - t.Fatalf("Routers.CreateInterface returned error %s\n", err) + t.Fatalf("Routers.CreateInterface returned error %s", err) } if iface.UUID == "" { @@ -99,7 +100,7 @@ func TestIntegrationRouter_CRUD(t *testing.T) { if subnetUUID := iface.Addresses[0].Subnet.UUID; subnetUUID != subnet.UUID { t.Errorf("interface.Addresses[0].Subnet.UUID got=%s\nwant=%s", subnetUUID, subnet.UUID) } - if addr := iface.Addresses[0].Address; addr != netip.MustParseAddr("192.168.99.10") { + if addr := iface.Addresses[0].Address; addr != "192.168.99.10" { t.Errorf("interface.Addresses[0].Address got=%s\nwant=%s", addr, "192.168.99.10") } @@ -108,15 +109,26 @@ func TestIntegrationRouter_CRUD(t *testing.T) { err = client.Routers.Delete(t.Context(), router.UUID) if err != nil { - t.Fatalf("Routers.Delete returned error %s\n", err) - } - - if err := client.Routers.WaitForDeleted(t.Context(), router.UUID); err != nil { - t.Errorf("router %q has not been deleted: %v", router.UUID, err) + t.Fatalf("Routers.Delete returned error %s", err) + } + + err = waitForDeleted(t.Context(), func() (exists bool, err error) { + r, err := client.Routers.Get(t.Context(), router.UUID) + if err != nil { + if cerr, ok := errors.AsType[*cloudscale.ErrorResponse](err); ok && cerr.StatusCode == http.StatusNotFound { + return false, nil + } + return true, err + } + t.Logf("router %q still exists with status %q", r.UUID, r.Status) + return true, nil + }) + if err != nil { + t.Errorf("waiting for router delete failed: %v", err) } err = client.Networks.Delete(t.Context(), network.UUID) if err != nil { - t.Fatalf("Networks.Delete returned error %s\n", err) + t.Fatalf("Networks.Delete returned error %s", err) } } From ce3805616bebef7a9a99701d3aecac708950ca33 Mon Sep 17 00:00:00 2001 From: Michael Weibel Date: Mon, 3 Aug 2026 13:07:19 +0200 Subject: [PATCH 3/6] router: adjust to new APIs --- router.go | 39 +++++++++----------- router_test.go | 24 +++--------- test/integration/routers_integration_test.go | 6 ++- 3 files changed, 27 insertions(+), 42 deletions(-) diff --git a/router.go b/router.go index 3d44adb..9adbfdf 100644 --- a/router.go +++ b/router.go @@ -12,14 +12,14 @@ const routerBasePath = "v1/routers" type Router struct { ZonalResource TaggedResource - HREF string `json:"href"` - UUID string `json:"uuid"` - Name string `json:"name"` - CreatedAt time.Time `json:"created_at"` - Status string `json:"status"` - InternetGateway bool `json:"internet_gateway"` - InternetGatewayAddresses []IPAddress `json:"internet_gateway_addresses,omitempty"` - InternalInterfaces []InternalInterface `json:"internal_interfaces,omitempty"` + HREF string `json:"href"` + UUID string `json:"uuid"` + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` + Status string `json:"status"` + InternetGateway bool `json:"internet_gateway"` + InternetGatewayAddresses []IPAddress `json:"internet_gateway_addresses,omitempty"` + Interfaces []RouterInterface `json:"interfaces,omitempty"` } type IPAddress struct { @@ -28,7 +28,7 @@ type IPAddress struct { Version int `json:"version"` ReversePTR *string `json:"reverse_ptr"` } -type InternalInterface struct { +type RouterInterface struct { UUID string `json:"uuid"` Network NetworkStub `json:"network"` Addresses []IPAddress `json:"addresses"` @@ -54,7 +54,7 @@ type RouterService interface { GenericDeleteService[Router] GenericWaitForService[Router] // CreateInterface creates a new interface attached to this router - CreateInterface(ctx context.Context, routerUUID string, createReq CreateInterfaceRequest) (*InternalInterface, error) + CreateInterface(ctx context.Context, routerUUID string, createReq CreateInterfaceRequest) (*RouterInterface, error) // DeleteInterface removes an interface attached to this router DeleteInterface(ctx context.Context, routerUUID, interfaceUUID string) error } @@ -73,14 +73,14 @@ type RouterServiceOperations struct { client *Client } -func (r RouterServiceOperations) CreateInterface(ctx context.Context, routerUUID string, createReq CreateInterfaceRequest) (*InternalInterface, error) { - path := fmt.Sprintf("%s/%s/create_interface", routerBasePath, routerUUID) - ctx = WithOperationPath(ctx, routerBasePath+"/:id/create_interface") +func (r RouterServiceOperations) CreateInterface(ctx context.Context, routerUUID string, createReq CreateInterfaceRequest) (*RouterInterface, error) { + path := fmt.Sprintf("%s/%s/interfaces", routerBasePath, routerUUID) + ctx = WithOperationPath(ctx, routerBasePath+"/:id/interfaces") req, err := r.client.NewRequest(ctx, http.MethodPost, path, createReq) if err != nil { return nil, err } - res := &InternalInterface{} + res := &RouterInterface{} if err := r.client.Do(ctx, req, res); err != nil { return nil, err } @@ -88,15 +88,10 @@ func (r RouterServiceOperations) CreateInterface(ctx context.Context, routerUUID } func (r RouterServiceOperations) DeleteInterface(ctx context.Context, routerUUID, interfaceUUID string) error { - path := fmt.Sprintf("%s/%s/delete_interface", routerBasePath, routerUUID) - ctx = WithOperationPath(ctx, routerBasePath+"/:id/delete_interface") + path := fmt.Sprintf("%s/%s/interfaces/%s", routerBasePath, routerUUID, interfaceUUID) + ctx = WithOperationPath(ctx, routerBasePath+"/:id/interfaces/:interface_id") - type DeleteInterfaceRequest struct { - Interface string `json:"interface"` - } - deleteReq := DeleteInterfaceRequest{Interface: interfaceUUID} - - req, err := r.client.NewRequest(ctx, http.MethodPost, path, deleteReq) + req, err := r.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return err } diff --git a/router_test.go b/router_test.go index 680ebf6..bd92eb3 100644 --- a/router_test.go +++ b/router_test.go @@ -84,7 +84,7 @@ func TestRouters_Get(t *testing.T) { "reverse_ptr": "203-0-113-1.cust.example.com" } ], - "internal_interfaces": [ + "interfaces": [ { "uuid": "1e0c6f9c-9f0d-4d1b-9f0d-8f0eaf1d0e0d", "network": { @@ -148,7 +148,7 @@ func TestRouters_Get(t *testing.T) { ReversePTR: new("203-0-113-1.cust.example.com"), }, }, - InternalInterfaces: []InternalInterface{ + Interfaces: []RouterInterface{ { UUID: "1e0c6f9c-9f0d-4d1b-9f0d-8f0eaf1d0e0d", Network: NetworkStub{ @@ -213,7 +213,7 @@ func TestRouters_CreateInterface(t *testing.T) { }, } - mux.HandleFunc("/v1/routers/cfde831a-4e87-4a75-960f-89b0148aa2cc/create_interface", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/v1/routers/cfde831a-4e87-4a75-960f-89b0148aa2cc/interfaces", func(w http.ResponseWriter, r *http.Request) { testHTTPMethod(t, r, http.MethodPost) expected := map[string]any{ @@ -264,7 +264,7 @@ func TestRouters_CreateInterface(t *testing.T) { t.Errorf("Routers.CreateInterface returned error: %v", err) } - expected := &InternalInterface{ + expected := &RouterInterface{ UUID: "1e0c6f9c-9f0d-4d1b-9f0d-8f0eaf1d0e0d", Network: NetworkStub{ HREF: "https://api.cloudscale.ch/v1/networks/7f0eaf1d-0e0d-4d5f-9e40-8f0eaf1d0e0d", @@ -297,21 +297,9 @@ func TestRouters_DeleteInterface(t *testing.T) { defer teardown() called := false - mux.HandleFunc("/v1/routers/cfde831a-4e87-4a75-960f-89b0148aa2cc/delete_interface", func(w http.ResponseWriter, r *http.Request) { - testHTTPMethod(t, r, http.MethodPost) - - expected := map[string]any{ - "interface": "1e0c6f9c-9f0d-4d1b-9f0d-8f0eaf1d0e0d", - } - - var v map[string]any - if err := json.NewDecoder(r.Body).Decode(&v); err != nil { - t.Fatalf("decode json: %v", err) - } + mux.HandleFunc("/v1/routers/cfde831a-4e87-4a75-960f-89b0148aa2cc/interfaces/1e0c6f9c-9f0d-4d1b-9f0d-8f0eaf1d0e0d", func(w http.ResponseWriter, r *http.Request) { + testHTTPMethod(t, r, http.MethodDelete) - if !reflect.DeepEqual(v, expected) { - t.Errorf("Request body\n got=%#v\nwant=%#v", v, expected) - } called = true }) diff --git a/test/integration/routers_integration_test.go b/test/integration/routers_integration_test.go index ab57e3b..fcff052 100644 --- a/test/integration/routers_integration_test.go +++ b/test/integration/routers_integration_test.go @@ -104,8 +104,10 @@ func TestIntegrationRouter_CRUD(t *testing.T) { t.Errorf("interface.Addresses[0].Address got=%s\nwant=%s", addr, "192.168.99.10") } - // Clean up: only remove router and, once it's deleted, the network. Sub-resources like interfaces attached to the router - // and subnets within the network are removed automatically. + // Clean up: first remove interface to test this path, then remove router and, once it's deleted, the network. + if err := client.Routers.DeleteInterface(t.Context(), router.UUID, iface.UUID); err != nil { + t.Errorf("Routers.DeleteInterface returned error: %s", err) + } err = client.Routers.Delete(t.Context(), router.UUID) if err != nil { From 4dca6184e81eb989ec82b65de6d96b543a3c1206 Mon Sep 17 00:00:00 2001 From: Michael Weibel Date: Tue, 4 Aug 2026 16:27:16 +0200 Subject: [PATCH 4/6] fix: delete interface before deleting router --- test/integration/cloudscale_test.go | 8 +++++++ test/integration/routers_integration_test.go | 23 +++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/test/integration/cloudscale_test.go b/test/integration/cloudscale_test.go index 7044715..fce58a8 100644 --- a/test/integration/cloudscale_test.go +++ b/test/integration/cloudscale_test.go @@ -250,6 +250,14 @@ func DeleteRemainingRouters() bool { if strings.HasPrefix(router.Name, testRunPrefix) { foundResource = true log.Printf("Found not deleted router: %s (%s)\n", router.Name, router.UUID) + // Interfaces must be removed before the router can be deleted. + for _, iface := range router.Interfaces { + log.Printf("Found not deleted router interface: %s on router %s\n", iface.UUID, router.UUID) + err = client.Routers.DeleteInterface(context.Background(), router.UUID, iface.UUID) + if err != nil { + log.Fatalf("Routers.DeleteInterface returned error %s\n", err) + } + } err = client.Routers.Delete(context.Background(), router.UUID) if err != nil { log.Fatalf("Routers.Delete returned error %s\n", err) diff --git a/test/integration/routers_integration_test.go b/test/integration/routers_integration_test.go index fcff052..93006e8 100644 --- a/test/integration/routers_integration_test.go +++ b/test/integration/routers_integration_test.go @@ -104,11 +104,31 @@ func TestIntegrationRouter_CRUD(t *testing.T) { t.Errorf("interface.Addresses[0].Address got=%s\nwant=%s", addr, "192.168.99.10") } - // Clean up: first remove interface to test this path, then remove router and, once it's deleted, the network. + // Clean up: delete interface if err := client.Routers.DeleteInterface(t.Context(), router.UUID, iface.UUID); err != nil { t.Errorf("Routers.DeleteInterface returned error: %s", err) } + // Verify the interface is actually gone before deleting the router: the router's + // Interfaces list must no longer contain it. + err = waitForDeleted(t.Context(), func() (exists bool, err error) { + r, err := client.Routers.Get(t.Context(), router.UUID) + if err != nil { + return true, err + } + for _, i := range r.Interfaces { + if i.UUID == iface.UUID { + t.Logf("interface %q still attached to router %q", iface.UUID, router.UUID) + return true, nil + } + } + return false, nil + }) + if err != nil { + t.Errorf("waiting for interface delete failed: %v", err) + } + + // Clean up: delete router err = client.Routers.Delete(t.Context(), router.UUID) if err != nil { t.Fatalf("Routers.Delete returned error %s", err) @@ -129,6 +149,7 @@ func TestIntegrationRouter_CRUD(t *testing.T) { t.Errorf("waiting for router delete failed: %v", err) } + // Clean up: delete network err = client.Networks.Delete(t.Context(), network.UUID) if err != nil { t.Fatalf("Networks.Delete returned error %s", err) From 0021bdd5ea745b54024878783e7597e5580f82ca Mon Sep 17 00:00:00 2001 From: Michael Weibel Date: Fri, 7 Aug 2026 09:39:33 +0200 Subject: [PATCH 5/6] fix naming & length assertion --- test/integration/routers_integration_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/routers_integration_test.go b/test/integration/routers_integration_test.go index 93006e8..8ed0bbb 100644 --- a/test/integration/routers_integration_test.go +++ b/test/integration/routers_integration_test.go @@ -11,7 +11,7 @@ import ( "github.com/cloudscale-ch/cloudscale-go-sdk/v9" ) -func TestIntegrationRouter_CRUD(t *testing.T) { +func TestIntegrationRouter_CR_D(t *testing.T) { t.Parallel() createRouterRequest := &cloudscale.RouterCreateRequest{ @@ -51,7 +51,7 @@ func TestIntegrationRouter_CRUD(t *testing.T) { t.Fatalf("Routers.List returned error %s\n", err) } - if numRouters := len(routers); numRouters != 1 { + if numRouters := len(routers); numRouters < 1 { t.Errorf("Routers.List got=%d\nwant>=%d\n", numRouters, 1) } From 4b5f03eefa6658e9d279bdaeb640b1188cd40a0c Mon Sep 17 00:00:00 2001 From: Michael Weibel Date: Mon, 10 Aug 2026 11:38:38 +0200 Subject: [PATCH 6/6] refactor: rename interface -> serverInterface --- servers.go | 54 +++++++++---------- ..._balancer_pool_members_integration_test.go | 2 +- test/integration/networks_integration_test.go | 16 +++--- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/servers.go b/servers.go index ee747af..d3b7047 100644 --- a/servers.go +++ b/servers.go @@ -24,7 +24,7 @@ type Server struct { Flavor FlavorStub `json:"flavor"` Image ImageServerStub `json:"image"` Volumes []VolumeStub `json:"volumes"` - Interfaces []Interface `json:"interfaces"` + Interfaces []ServerInterface `json:"interfaces"` SSHFingerprints []string `json:"ssh_fingerprints"` SSHHostKeys []string `json:"ssh_host_keys"` AntiAfinityWith []ServerStub `json:"anti_affinity_with"` @@ -65,13 +65,13 @@ type VolumeStub struct { SizeGB int `json:"size_gb"` } -type Interface struct { - Type string `json:"type,omitempty"` - Network NetworkStub `json:"network,omitempty"` - Addresses []Address `json:"addresses,omitempty"` +type ServerInterface struct { + Type string `json:"type,omitempty"` + Network NetworkStub `json:"network,omitempty"` + Addresses []ServerAddress `json:"addresses,omitempty"` } -type Address struct { +type ServerAddress struct { Version int `json:"version"` Address string `json:"address"` PrefixLength int `json:"prefix_length"` @@ -83,30 +83,30 @@ type Address struct { type ServerRequest struct { ZonalResourceRequest TaggedResourceRequest - Name string `json:"name"` - Flavor string `json:"flavor"` - Image string `json:"image"` - Zone string `json:"zone,omitempty"` - VolumeSizeGB int `json:"volume_size_gb,omitempty"` - Volumes *[]ServerVolumeRequest `json:"volumes,omitempty"` - Interfaces *[]InterfaceRequest `json:"interfaces,omitempty"` - BulkVolumeSizeGB int `json:"bulk_volume_size_gb,omitempty"` - SSHKeys []string `json:"ssh_keys"` - Password string `json:"password,omitempty"` - UsePublicNetwork *bool `json:"use_public_network,omitempty"` - UsePrivateNetwork *bool `json:"use_private_network,omitempty"` - UseIPV6 *bool `json:"use_ipv6,omitempty"` - AntiAffinityWith string `json:"anti_affinity_with,omitempty"` - ServerGroups []string `json:"server_groups,omitempty"` - UserData string `json:"user_data,omitempty"` + Name string `json:"name"` + Flavor string `json:"flavor"` + Image string `json:"image"` + Zone string `json:"zone,omitempty"` + VolumeSizeGB int `json:"volume_size_gb,omitempty"` + Volumes *[]ServerVolumeRequest `json:"volumes,omitempty"` + Interfaces *[]ServerInterfaceRequest `json:"interfaces,omitempty"` + BulkVolumeSizeGB int `json:"bulk_volume_size_gb,omitempty"` + SSHKeys []string `json:"ssh_keys"` + Password string `json:"password,omitempty"` + UsePublicNetwork *bool `json:"use_public_network,omitempty"` + UsePrivateNetwork *bool `json:"use_private_network,omitempty"` + UseIPV6 *bool `json:"use_ipv6,omitempty"` + AntiAffinityWith string `json:"anti_affinity_with,omitempty"` + ServerGroups []string `json:"server_groups,omitempty"` + UserData string `json:"user_data,omitempty"` } type ServerUpdateRequest struct { TaggedResourceRequest - Name string `json:"name,omitempty"` - Status string `json:"status,omitempty"` - Flavor string `json:"flavor,omitempty"` - Interfaces *[]InterfaceRequest `json:"interfaces,omitempty"` + Name string `json:"name,omitempty"` + Status string `json:"status,omitempty"` + Flavor string `json:"flavor,omitempty"` + Interfaces *[]ServerInterfaceRequest `json:"interfaces,omitempty"` } type ServerVolumeRequest struct { @@ -114,7 +114,7 @@ type ServerVolumeRequest struct { Type string `json:"type,omitempty"` } -type InterfaceRequest struct { +type ServerInterfaceRequest struct { Network string `json:"network,omitempty"` Addresses *[]AddressRequest `json:"addresses,omitempty"` } diff --git a/test/integration/load_balancer_pool_members_integration_test.go b/test/integration/load_balancer_pool_members_integration_test.go index 40b8a1d..b13c67e 100644 --- a/test/integration/load_balancer_pool_members_integration_test.go +++ b/test/integration/load_balancer_pool_members_integration_test.go @@ -214,7 +214,7 @@ func TestIntegrationLoadBalancerPoolMember_MonitorStatus(t *testing.T) { // Step 2: Create a server on the private network serverRequest := getDefaultServerRequest() - serverRequest.Interfaces = &[]cloudscale.InterfaceRequest{{Network: network.UUID}} + serverRequest.Interfaces = &[]cloudscale.ServerInterfaceRequest{{Network: network.UUID}} serverRequest.SSHKeys = []string{} serverRequest.Password = randomNotVerySecurePassword(10) diff --git a/test/integration/networks_integration_test.go b/test/integration/networks_integration_test.go index 6891919..6b73281 100644 --- a/test/integration/networks_integration_test.go +++ b/test/integration/networks_integration_test.go @@ -112,16 +112,16 @@ func TestIntegrationNetwork_CreateAttached(t *testing.T) { cases := []struct { name string - in *[]cloudscale.InterfaceRequest + in *[]cloudscale.ServerInterfaceRequest expectedNumNetworks int expectedIP string }{ - {"Attach by network UUID", &[]cloudscale.InterfaceRequest{ + {"Attach by network UUID", &[]cloudscale.ServerInterfaceRequest{ { Network: network.UUID, }, }, 1, `192\.168\.42\.[0-9]*`}, - {"Attach by subnet UUID", &[]cloudscale.InterfaceRequest{ + {"Attach by subnet UUID", &[]cloudscale.ServerInterfaceRequest{ { Addresses: &[]cloudscale.AddressRequest{ { @@ -130,7 +130,7 @@ func TestIntegrationNetwork_CreateAttached(t *testing.T) { }, }, }, 1, `192\.168\.42\.[0-9]*`}, - {"Attach by subnet UUID with predefined IP", &[]cloudscale.InterfaceRequest{ + {"Attach by subnet UUID with predefined IP", &[]cloudscale.ServerInterfaceRequest{ { Addresses: &[]cloudscale.AddressRequest{ { @@ -140,7 +140,7 @@ func TestIntegrationNetwork_CreateAttached(t *testing.T) { }, }, }, 1, `192\.168\.42\.242`}, - {"Attach by network UUID without IP (Layer 2)", &[]cloudscale.InterfaceRequest{ + {"Attach by network UUID without IP (Layer 2)", &[]cloudscale.ServerInterfaceRequest{ { Network: "public", }, @@ -233,7 +233,7 @@ func TestIntegrationNetwork_Reattach(t *testing.T) { t.Fatalf("Subnets.Create returned error %s\n", err) } - interfaces := []cloudscale.InterfaceRequest{ + interfaces := []cloudscale.ServerInterfaceRequest{ {Network: "public"}, } createServerRequest := &cloudscale.ServerRequest{ @@ -268,7 +268,7 @@ func TestIntegrationNetwork_Reattach(t *testing.T) { Subnet: subnet.UUID, Address: "192.168.77.77", }} - interfaces = append(interfaces, cloudscale.InterfaceRequest{ + interfaces = append(interfaces, cloudscale.ServerInterfaceRequest{ Addresses: &addresses, }) updateRequest := cloudscale.ServerUpdateRequest{ @@ -323,7 +323,7 @@ func TestIntegrationNetwork_Reorder(t *testing.T) { t.Fatalf("Subnets.Create returned error %s\n", err) } - interfaces := []cloudscale.InterfaceRequest{ + interfaces := []cloudscale.ServerInterfaceRequest{ {Network: "public"}, {Network: network.UUID}, }