From c42dbd30494751dd1774c439cbd3690382a820f8 Mon Sep 17 00:00:00 2001 From: Julian Bigler Date: Mon, 22 Dec 2025 14:00:46 +0100 Subject: [PATCH 01/12] add crud for volume snapshots --- cloudscale.go | 5 + .../volume_snapshot_integration_test.go | 108 +++++++ volume_snapshots.go | 35 ++ volume_snapshots_test.go | 302 ++++++++++++++++++ 4 files changed, 450 insertions(+) create mode 100644 test/integration/volume_snapshot_integration_test.go create mode 100644 volume_snapshots.go create mode 100644 volume_snapshots_test.go diff --git a/cloudscale.go b/cloudscale.go index 823421a..d169311 100644 --- a/cloudscale.go +++ b/cloudscale.go @@ -37,6 +37,7 @@ type Client struct { Regions RegionService Servers ServerService Volumes VolumeService + VolumeSnapshots VolumeSnapshotService Networks NetworkService Subnets SubnetService FloatingIPs FloatingIPsService @@ -92,6 +93,10 @@ func NewClient(httpClient *http.Client) *Client { client: c, path: volumeBasePath, } + c.VolumeSnapshots = GenericServiceOperations[VolumeSnapshot, VolumeSnapshotRequest, VolumeSnapshotUpdateRequest]{ + client: c, + path: volumeSnapshotsBasePath, + } c.ServerGroups = GenericServiceOperations[ServerGroup, ServerGroupRequest, ServerGroupRequest]{ client: c, path: serverGroupsBasePath, diff --git a/test/integration/volume_snapshot_integration_test.go b/test/integration/volume_snapshot_integration_test.go new file mode 100644 index 0000000..0be9e12 --- /dev/null +++ b/test/integration/volume_snapshot_integration_test.go @@ -0,0 +1,108 @@ +//go:build integration + +package integration + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/cloudscale-ch/cloudscale-go-sdk/v6" +) + +func TestVolumeSnapshotIntegration_CRUD(t *testing.T) { + integrationTest(t) + + ctx := context.Background() + + // A source volume is needed to create a snapshot. + volumeCreateRequest := &cloudscale.VolumeRequest{ + Name: "test-volume-for-snapshot", + SizeGB: 50, + Type: "ssd", + } + volume, err := client.Volumes.Create(ctx, volumeCreateRequest) + if err != nil { + t.Fatalf("Volume.Create: %v", err) + } + + snapshotCreateRequest := &cloudscale.VolumeSnapshotRequest{ + Name: "test-snapshot", + SourceVolume: volume.UUID, + } + snapshot, err := client.VolumeSnapshots.Create(ctx, snapshotCreateRequest) + if err != nil { + t.Fatalf("VolumeSnapshots.Create: %v", err) + } + + retrieved, err := client.VolumeSnapshots.Get(ctx, snapshot.UUID) + if err != nil { + t.Fatalf("VolumeSnapshots.Get: %v", err) + } + if retrieved.UUID != snapshot.UUID { + t.Errorf("Expected UUID %s, got %s", snapshot.UUID, retrieved.UUID) + } + if retrieved.Name != "test-snapshot" { + t.Errorf("Expected retrieved snapshot name 'test-snapshot', got '%s'", retrieved.Name) + } + + snapshotUpdateRequest := &cloudscale.VolumeSnapshotUpdateRequest{ + Name: "updated-snapshot", + } + err = client.VolumeSnapshots.Update(ctx, snapshot.UUID, snapshotUpdateRequest) + if err != nil { + t.Fatalf("VolumeSnapshots.Update: %v", err) + } + + // Get snapshot again to verify the update + updatedSnapshot, err := client.VolumeSnapshots.Get(ctx, snapshot.UUID) + if err != nil { + t.Fatalf("VolumeSnapshots.Get after update: %v", err) + } + if updatedSnapshot.Name != "updated-snapshot" { + t.Errorf("Expected updated snapshot name 'updated-snapshot', got '%s'", updatedSnapshot.Name) + } + + snapshots, err := client.VolumeSnapshots.List(ctx) + if err != nil { + t.Fatalf("VolumeSnapshots.List: %v", err) + } + if len(snapshots) == 0 { + t.Error("Expected at least one snapshot") + } + + if err := client.VolumeSnapshots.Delete(ctx, snapshot.UUID); err != nil { + t.Fatalf("Warning: failed to delete snapshot %s: %v", snapshot.UUID, err) + } + + // Wait for snapshot to be fully deleted before deleting volume + err = waitForSnapshotDeletion(ctx, snapshot.UUID, 10) + if err != nil { + t.Fatalf("Snapshot deletion timeout: %v", err) + } + + if err := client.Volumes.Delete(ctx, volume.UUID); err != nil { + t.Fatalf("Warning: failed to delete volume %s: %v", volume.UUID, err) + } +} + +// waitForSnapshotDeletion polls the API until the snapshot no longer exists +func waitForSnapshotDeletion(ctx context.Context, snapshotUUID string, maxWaitSeconds int) error { + for i := 0; i < maxWaitSeconds; i++ { + _, err := client.VolumeSnapshots.Get(ctx, snapshotUUID) + if err != nil { + // If we get a 404 error, the snapshot is deleted + if apiErr, ok := err.(*cloudscale.ErrorResponse); ok { + if apiErr.StatusCode == 404 { + return nil + } + } + // Some other error occurred + return err + } + // Snapshot still exists, wait 1 second and try again + time.Sleep(1 * time.Second) + } + return fmt.Errorf("snapshot %s still exists after %d seconds", snapshotUUID, maxWaitSeconds) +} diff --git a/volume_snapshots.go b/volume_snapshots.go new file mode 100644 index 0000000..8457354 --- /dev/null +++ b/volume_snapshots.go @@ -0,0 +1,35 @@ +package cloudscale + +import "context" + +type VolumeSnapshot struct { + HREF string `json:"href"` + UUID string `json:"uuid"` + Name string `json:"name"` + SizeGB int `json:"size_gb"` + CreatedAt string `json:"created_at"` + Volume VolumeStub `json:"volume"` + Zone Zone `json:"zone"` + Tags *TagMap `json:"tags"` +} + +type VolumeSnapshotRequest struct { + Name string `json:"name"` + SourceVolume string `json:"source_volume"` + Tags *TagMap `json:"tags,omitempty"` +} + +type VolumeSnapshotUpdateRequest struct { + Name string `json:"name,omitempty"` + Tags *TagMap `json:"tags,omitempty"` +} + +const volumeSnapshotsBasePath = "v1/volume-snapshots" + +type VolumeSnapshotService interface { + Create(ctx context.Context, createRequest *VolumeSnapshotRequest) (*VolumeSnapshot, error) + Get(ctx context.Context, snapshotID string) (*VolumeSnapshot, error) + Update(ctx context.Context, snapshotID string, updateRequest *VolumeSnapshotUpdateRequest) error + Delete(ctx context.Context, snapshotID string) error + List(ctx context.Context, opts ...ListRequestModifier) ([]VolumeSnapshot, error) +} diff --git a/volume_snapshots_test.go b/volume_snapshots_test.go new file mode 100644 index 0000000..972b449 --- /dev/null +++ b/volume_snapshots_test.go @@ -0,0 +1,302 @@ +package cloudscale + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestVolumeSnapshots_Create(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v1/volume-snapshots", func(w http.ResponseWriter, r *http.Request) { + testHTTPMethod(t, r, http.MethodPost) + + // Verify the request body has correct field name + var req VolumeSnapshotRequest + json.NewDecoder(r.Body).Decode(&req) + if req.SourceVolume != "volume-uuid" { + t.Errorf("Expected SourceVolume to be 'volume-uuid', got '%s'", req.SourceVolume) + } + + fmt.Fprint(w, `{"uuid": "snapshot-uuid", "name": "test-snapshot"}`) + }) + + volumeCreateRequest := &VolumeSnapshotRequest{ + Name: "test-snapshot", + SourceVolume: "volume-uuid", + } + + snapshot, err := client.VolumeSnapshots.Create(context.Background(), volumeCreateRequest) + + if err != nil { + t.Errorf("VolumeSnapshots.Create returned error: %v", err) + } + + expected := &VolumeSnapshot{UUID: "snapshot-uuid", Name: "test-snapshot"} + if !reflect.DeepEqual(snapshot, expected) { + t.Errorf("VolumeSnapshots.Create\n got=%#v\nwant=%#v", snapshot, expected) + } +} + +func TestVolumeSnapshots_Get(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v1/volume-snapshots/snapshot-uuid", func(w http.ResponseWriter, r *http.Request) { + testHTTPMethod(t, r, http.MethodGet) + fmt.Fprint(w, `{ + "uuid": "snapshot-uuid", + "name": "test-snapshot", + "size_gb": 50, + "created_at": "2024-01-15T10:30:00Z", + "volume": { + "uuid": "volume-uuid" + }, + "zone": { + "slug": "lpg1" + }, + "tags": {} + }`) + }) + + snapshot, err := client.VolumeSnapshots.Get(context.Background(), "snapshot-uuid") + + if err != nil { + t.Errorf("VolumeSnapshots.Get returned error: %v", err) + } + + expected := &VolumeSnapshot{ + UUID: "snapshot-uuid", + Name: "test-snapshot", + SizeGB: 50, + CreatedAt: "2024-01-15T10:30:00Z", + Volume: VolumeStub{ + UUID: "volume-uuid", + }, + Zone: Zone{ + Slug: "lpg1", + }, + Tags: &TagMap{}, + } + + if !reflect.DeepEqual(snapshot, expected) { + t.Errorf("VolumeSnapshots.Get\n got=%#v\nwant=%#v", snapshot, expected) + } +} + +func TestVolumeSnapshots_Update(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v1/volume-snapshots/snapshot-uuid", func(w http.ResponseWriter, r *http.Request) { + testHTTPMethod(t, r, http.MethodPatch) + + var req VolumeSnapshotUpdateRequest + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + + if req.Name != "updated-snapshot" { + t.Errorf("Expected Name to be 'updated-snapshot', got '%s'", req.Name) + } + + w.WriteHeader(http.StatusNoContent) + }) + + updateRequest := &VolumeSnapshotUpdateRequest{ + Name: "updated-snapshot", + } + + err := client.VolumeSnapshots.Update(context.Background(), "snapshot-uuid", updateRequest) + + if err != nil { + t.Errorf("VolumeSnapshots.Update returned error: %v", err) + } +} + +func TestVolumeSnapshots_Update_WithTags(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v1/volume-snapshots/snapshot-uuid", func(w http.ResponseWriter, r *http.Request) { + testHTTPMethod(t, r, http.MethodPatch) + + var req VolumeSnapshotUpdateRequest + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + + if req.Tags == nil { + t.Error("Expected Tags to be set") + } + + if (*req.Tags)["environment"] != "production" { + t.Errorf("Expected tag 'environment' to be 'production', got '%s'", (*req.Tags)["environment"]) + } + + w.WriteHeader(http.StatusNoContent) + }) + + tags := TagMap{"environment": "production"} + updateRequest := &VolumeSnapshotUpdateRequest{ + Tags: &tags, + } + + err := client.VolumeSnapshots.Update(context.Background(), "snapshot-uuid", updateRequest) + + if err != nil { + t.Errorf("VolumeSnapshots.Update returned error: %v", err) + } +} + +func TestVolumeSnapshots_Delete(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v1/volume-snapshots/snapshot-uuid", func(w http.ResponseWriter, r *http.Request) { + testHTTPMethod(t, r, http.MethodDelete) + w.WriteHeader(http.StatusNoContent) + }) + + err := client.VolumeSnapshots.Delete(context.Background(), "snapshot-uuid") + + if err != nil { + t.Errorf("VolumeSnapshots.Delete returned error: %v", err) + } +} + +func TestVolumeSnapshots_List(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v1/volume-snapshots", func(w http.ResponseWriter, r *http.Request) { + testHTTPMethod(t, r, http.MethodGet) + fmt.Fprint(w, `[ + { + "uuid": "snapshot-uuid-1", + "name": "snapshot-1", + "size_gb": 50, + "created_at": "2024-01-15T10:30:00Z", + "volume": { + "uuid": "volume-uuid-1" + }, + "zone": { + "slug": "lpg1" + }, + "tags": {} + }, + { + "uuid": "snapshot-uuid-2", + "name": "snapshot-2", + "size_gb": 100, + "created_at": "2024-01-16T11:00:00Z", + "volume": { + "uuid": "volume-uuid-2" + }, + "zone": { + "slug": "rma1" + }, + "tags": {"environment": "test"} + } + ]`) + }) + + snapshots, err := client.VolumeSnapshots.List(context.Background()) + + if err != nil { + t.Errorf("VolumeSnapshots.List returned error: %v", err) + } + + if len(snapshots) != 2 { + t.Errorf("Expected 2 snapshots, got %d", len(snapshots)) + } + + expected := []VolumeSnapshot{ + { + UUID: "snapshot-uuid-1", + Name: "snapshot-1", + SizeGB: 50, + CreatedAt: "2024-01-15T10:30:00Z", + Volume: VolumeStub{ + UUID: "volume-uuid-1", + }, + Zone: Zone{ + Slug: "lpg1", + }, + Tags: &TagMap{}, + }, + { + UUID: "snapshot-uuid-2", + Name: "snapshot-2", + SizeGB: 100, + CreatedAt: "2024-01-16T11:00:00Z", + Volume: VolumeStub{ + UUID: "volume-uuid-2", + }, + Zone: Zone{ + Slug: "rma1", + }, + Tags: &TagMap{"environment": "test"}, + }, + } + + if !reflect.DeepEqual(snapshots, expected) { + t.Errorf("VolumeSnapshots.List\n got=%#v\nwant=%#v", snapshots, expected) + } +} +func TestVolumeSnapshots_List_WithFilters(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v1/volume-snapshots", func(w http.ResponseWriter, r *http.Request) { + testHTTPMethod(t, r, http.MethodGet) + + // Check if filter query params are present + query := r.URL.Query() + if query.Get("tag:environment") != "production" { + t.Errorf("Expected tag:environment filter to be 'production', got '%s'", query.Get("tag:environment")) + } + + fmt.Fprint(w, `[ + { + "uuid": "snapshot-uuid-1", + "name": "snapshot-1", + "size_gb": 50, + "created_at": "2024-01-15T10:30:00Z", + "volume": { + "uuid": "volume-uuid-1", + "name": "volume-1" + }, + "zone": { + "slug": "lpg1" + }, + "tags": {"environment": "production"} + } + ]`) + }) + + tagFilter := TagMap{"environment": "production"} + snapshots, err := client.VolumeSnapshots.List( + context.Background(), + WithTagFilter(tagFilter), + ) + + if err != nil { + t.Errorf("VolumeSnapshots.List returned error: %v", err) + } + + if len(snapshots) != 1 { + t.Errorf("Expected 1 snapshot, got %d", len(snapshots)) + } + + if snapshots[0].UUID != "snapshot-uuid-1" { + t.Errorf("Expected snapshot UUID 'snapshot-uuid-1', got '%s'", snapshots[0].UUID) + } +} From 680e79b47645dee2a9e626fd2e89876254b5d315 Mon Sep 17 00:00:00 2001 From: Julian Bigler Date: Mon, 22 Dec 2025 14:36:42 +0100 Subject: [PATCH 02/12] improve integration test name to match other tests --- ...integration_test.go => volume_snapshots_integration_test.go} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename test/integration/{volume_snapshot_integration_test.go => volume_snapshots_integration_test.go} (98%) diff --git a/test/integration/volume_snapshot_integration_test.go b/test/integration/volume_snapshots_integration_test.go similarity index 98% rename from test/integration/volume_snapshot_integration_test.go rename to test/integration/volume_snapshots_integration_test.go index 0be9e12..02e5764 100644 --- a/test/integration/volume_snapshot_integration_test.go +++ b/test/integration/volume_snapshots_integration_test.go @@ -11,7 +11,7 @@ import ( "github.com/cloudscale-ch/cloudscale-go-sdk/v6" ) -func TestVolumeSnapshotIntegration_CRUD(t *testing.T) { +func TestIntegrationVolumeSnapshot_CRUD(t *testing.T) { integrationTest(t) ctx := context.Background() From dda9c21b8e11edd5bebef5021d7a0fc4f6c1cf1d Mon Sep 17 00:00:00 2001 From: Julian Bigler Date: Mon, 29 Dec 2025 16:01:17 +0100 Subject: [PATCH 03/12] use embedded structs for zone and tags --- volume_snapshots.go | 18 ++++++++++-------- volume_snapshots_test.go | 35 +++++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/volume_snapshots.go b/volume_snapshots.go index 8457354..b9ec58a 100644 --- a/volume_snapshots.go +++ b/volume_snapshots.go @@ -1,27 +1,29 @@ package cloudscale -import "context" +import ( + "context" +) type VolumeSnapshot struct { + ZonalResource + TaggedResource HREF string `json:"href"` UUID string `json:"uuid"` Name string `json:"name"` SizeGB int `json:"size_gb"` CreatedAt string `json:"created_at"` Volume VolumeStub `json:"volume"` - Zone Zone `json:"zone"` - Tags *TagMap `json:"tags"` } type VolumeSnapshotRequest struct { - Name string `json:"name"` - SourceVolume string `json:"source_volume"` - Tags *TagMap `json:"tags,omitempty"` + TaggedResourceRequest + Name string `json:"name"` + SourceVolume string `json:"source_volume"` } type VolumeSnapshotUpdateRequest struct { - Name string `json:"name,omitempty"` - Tags *TagMap `json:"tags,omitempty"` + TaggedResourceRequest + Name string `json:"name,omitempty"` } const volumeSnapshotsBasePath = "v1/volume-snapshots" diff --git a/volume_snapshots_test.go b/volume_snapshots_test.go index 972b449..87be4bf 100644 --- a/volume_snapshots_test.go +++ b/volume_snapshots_test.go @@ -78,10 +78,14 @@ func TestVolumeSnapshots_Get(t *testing.T) { Volume: VolumeStub{ UUID: "volume-uuid", }, - Zone: Zone{ - Slug: "lpg1", + ZonalResource: ZonalResource{ + Zone{ + Slug: "lpg1", + }, + }, + TaggedResource: TaggedResource{ + Tags: TagMap{}, }, - Tags: &TagMap{}, } if !reflect.DeepEqual(snapshot, expected) { @@ -145,8 +149,11 @@ func TestVolumeSnapshots_Update_WithTags(t *testing.T) { }) tags := TagMap{"environment": "production"} + updateRequest := &VolumeSnapshotUpdateRequest{ - Tags: &tags, + TaggedResourceRequest: TaggedResourceRequest{ + Tags: &tags, + }, } err := client.VolumeSnapshots.Update(context.Background(), "snapshot-uuid", updateRequest) @@ -227,10 +234,14 @@ func TestVolumeSnapshots_List(t *testing.T) { Volume: VolumeStub{ UUID: "volume-uuid-1", }, - Zone: Zone{ - Slug: "lpg1", + ZonalResource: ZonalResource{ + Zone{ + Slug: "lpg1", + }, + }, + TaggedResource: TaggedResource{ + Tags: TagMap{}, }, - Tags: &TagMap{}, }, { UUID: "snapshot-uuid-2", @@ -240,10 +251,14 @@ func TestVolumeSnapshots_List(t *testing.T) { Volume: VolumeStub{ UUID: "volume-uuid-2", }, - Zone: Zone{ - Slug: "rma1", + ZonalResource: ZonalResource{ + Zone{ + Slug: "rma1", + }, + }, + TaggedResource: TaggedResource{ + Tags: TagMap{"environment": "test"}, }, - Tags: &TagMap{"environment": "test"}, }, } From 97ec39e18a49e6859f4f9b76a9c1ed789e7331d6 Mon Sep 17 00:00:00 2001 From: Julian Bigler Date: Mon, 29 Dec 2025 16:02:07 +0100 Subject: [PATCH 04/12] include volume snapshots in tags integration test --- test/integration/tags_integration_test.go | 88 +++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/test/integration/tags_integration_test.go b/test/integration/tags_integration_test.go index 1233e8f..e283d70 100644 --- a/test/integration/tags_integration_test.go +++ b/test/integration/tags_integration_test.go @@ -174,6 +174,94 @@ func TestIntegrationTags_Volume(t *testing.T) { } } +func TestIntegrationTags_Snapshot(t *testing.T) { + integrationTest(t) + + createVolumeRequest := cloudscale.VolumeRequest{ + Name: testRunPrefix, + SizeGB: 3, + } + + volume, err := client.Volumes.Create(context.Background(), &createVolumeRequest) + if err != nil { + t.Fatalf("Volumes.Create returned error %s\n", err) + } + + snapshotCreateRequest := &cloudscale.VolumeSnapshotRequest{ + Name: "test-snapshot", + SourceVolume: volume.UUID, + } + initialTags := getInitialTags() + snapshotCreateRequest.Tags = &initialTags + + snapshot, err := client.VolumeSnapshots.Create(context.Background(), snapshotCreateRequest) + if err != nil { + t.Fatalf("VolumeSnapshots.Create: %v", err) + } + + getResult, err := client.VolumeSnapshots.Get(context.Background(), snapshot.UUID) + if err != nil { + t.Errorf("VolumeSnapshots.Get returned error %s\n", err) + } + if !reflect.DeepEqual(getResult.Tags, initialTags) { + t.Errorf("Tagging failed, could not tag, is at %s\n", getResult.Tags) + } + + updateRequest := cloudscale.VolumeSnapshotUpdateRequest{} + newTags := getNewTags() + updateRequest.Tags = &newTags + + err = client.VolumeSnapshots.Update(context.Background(), snapshot.UUID, &updateRequest) + if err != nil { + t.Errorf("VolumeSnapshots.Update returned error: %v", err) + } + getResult2, err := client.VolumeSnapshots.Get(context.Background(), snapshot.UUID) + if err != nil { + t.Errorf("VolumeSnapshots.Get returned error %s\n", err) + } + if !reflect.DeepEqual(getResult2.Tags, newTags) { + t.Errorf("Tagging failed, could not tag, is at %s\n", getResult.Tags) + } + + // test querying with tags + initialTagsKeyOnly := getInitialTagsKeyOnly() + for _, tags := range []cloudscale.TagMap{initialTags, initialTagsKeyOnly} { + res, err := client.VolumeSnapshots.List(context.Background(), cloudscale.WithTagFilter(tags)) + if err != nil { + t.Errorf("VolumeSnapshots.List returned error %s\n", err) + } + if len(res) > 0 { + t.Errorf("Expected no result when filter with %#v, got: %#v", tags, res) + } + } + + newTagsKeyOnly := getNewTagsKeyOnly() + for _, tags := range []cloudscale.TagMap{newTags, newTagsKeyOnly} { + res, err := client.VolumeSnapshots.List(context.Background(), cloudscale.WithTagFilter(tags)) + if err != nil { + t.Errorf("VolumeSnapshots.List returned error %s\n", err) + } + if len(res) != 1 { + t.Errorf("Expected exactly one result when filter with %#v, got: %#v", tags, len(res)) + } + } + + err = client.VolumeSnapshots.Delete(context.Background(), snapshot.UUID) + if err != nil { + t.Fatalf("VolumeSnapshots.Delete returned error %s\n", err) + } + + // Wait for snapshot to be fully deleted before deleting volume + err = waitForSnapshotDeletion(context.Background(), snapshot.UUID, 10) + if err != nil { + t.Fatalf("Snapshot deletion timeout: %v", err) + } + + if err := client.Volumes.Delete(context.Background(), volume.UUID); err != nil { + t.Fatalf("Warning: failed to delete volume %s: %v", volume.UUID, err) + } +} + func TestIntegrationTags_FloatingIP(t *testing.T) { integrationTest(t) From 0f13af93c3e9a2ea2fe7245f1d6a6bcaa398e41a Mon Sep 17 00:00:00 2001 From: Julian Bigler Date: Mon, 29 Dec 2025 17:38:30 +0100 Subject: [PATCH 05/12] add omitempty to future-proof the sdk for api changes --- volume_snapshots.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/volume_snapshots.go b/volume_snapshots.go index b9ec58a..9dddd60 100644 --- a/volume_snapshots.go +++ b/volume_snapshots.go @@ -7,18 +7,18 @@ import ( type VolumeSnapshot struct { ZonalResource TaggedResource - HREF string `json:"href"` - UUID string `json:"uuid"` - Name string `json:"name"` - SizeGB int `json:"size_gb"` - CreatedAt string `json:"created_at"` - Volume VolumeStub `json:"volume"` + HREF string `json:"href,omitempty"` + UUID string `json:"uuid,omitempty"` + Name string `json:"name,omitempty"` + SizeGB int `json:"size_gb,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + Volume VolumeStub `json:"volume,omitempty"` } type VolumeSnapshotRequest struct { TaggedResourceRequest - Name string `json:"name"` - SourceVolume string `json:"source_volume"` + Name string `json:"name,omitempty"` + SourceVolume string `json:"source_volume,omitempty"` } type VolumeSnapshotUpdateRequest struct { From a172488a6868055ee991c07ccd71e7267ae59194 Mon Sep 17 00:00:00 2001 From: Julian Bigler Date: Mon, 29 Dec 2025 17:47:21 +0100 Subject: [PATCH 06/12] move update operation to separate integration test --- .../volume_snapshots_integration_test.go | 56 ++++++++++++++++--- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/test/integration/volume_snapshots_integration_test.go b/test/integration/volume_snapshots_integration_test.go index 02e5764..6af82eb 100644 --- a/test/integration/volume_snapshots_integration_test.go +++ b/test/integration/volume_snapshots_integration_test.go @@ -47,6 +47,54 @@ func TestIntegrationVolumeSnapshot_CRUD(t *testing.T) { t.Errorf("Expected retrieved snapshot name 'test-snapshot', got '%s'", retrieved.Name) } + snapshots, err := client.VolumeSnapshots.List(ctx) + if err != nil { + t.Fatalf("VolumeSnapshots.List: %v", err) + } + if len(snapshots) == 0 { + t.Error("Expected at least one snapshot") + } + + if err := client.VolumeSnapshots.Delete(ctx, snapshot.UUID); err != nil { + t.Fatalf("Warning: failed to delete snapshot %s: %v", snapshot.UUID, err) + } + + // Wait for snapshot to be fully deleted before deleting volume + err = waitForSnapshotDeletion(ctx, snapshot.UUID, 10) + if err != nil { + t.Fatalf("Snapshot deletion timeout: %v", err) + } + + if err := client.Volumes.Delete(ctx, volume.UUID); err != nil { + t.Fatalf("Warning: failed to delete volume %s: %v", volume.UUID, err) + } +} + +func TestIntegrationVolumeSnapshot_Update(t *testing.T) { + integrationTest(t) + + ctx := context.Background() + + // A source volume is needed to create a snapshot. + volumeCreateRequest := &cloudscale.VolumeRequest{ + Name: "test-volume-for-snapshot", + SizeGB: 50, + Type: "ssd", + } + volume, err := client.Volumes.Create(ctx, volumeCreateRequest) + if err != nil { + t.Fatalf("Volume.Create: %v", err) + } + + snapshotCreateRequest := &cloudscale.VolumeSnapshotRequest{ + Name: "test-snapshot", + SourceVolume: volume.UUID, + } + snapshot, err := client.VolumeSnapshots.Create(ctx, snapshotCreateRequest) + if err != nil { + t.Fatalf("VolumeSnapshots.Create: %v", err) + } + snapshotUpdateRequest := &cloudscale.VolumeSnapshotUpdateRequest{ Name: "updated-snapshot", } @@ -64,14 +112,6 @@ func TestIntegrationVolumeSnapshot_CRUD(t *testing.T) { t.Errorf("Expected updated snapshot name 'updated-snapshot', got '%s'", updatedSnapshot.Name) } - snapshots, err := client.VolumeSnapshots.List(ctx) - if err != nil { - t.Fatalf("VolumeSnapshots.List: %v", err) - } - if len(snapshots) == 0 { - t.Error("Expected at least one snapshot") - } - if err := client.VolumeSnapshots.Delete(ctx, snapshot.UUID); err != nil { t.Fatalf("Warning: failed to delete snapshot %s: %v", snapshot.UUID, err) } From 96c1eaa624c94ebb85810a1427b8627866f86c13 Mon Sep 17 00:00:00 2001 From: Julian Bigler Date: Mon, 29 Dec 2025 17:49:01 +0100 Subject: [PATCH 07/12] use testZone for resource creation in integration tests --- test/integration/volume_snapshots_integration_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/integration/volume_snapshots_integration_test.go b/test/integration/volume_snapshots_integration_test.go index 6af82eb..5d9aa8a 100644 --- a/test/integration/volume_snapshots_integration_test.go +++ b/test/integration/volume_snapshots_integration_test.go @@ -21,6 +21,9 @@ func TestIntegrationVolumeSnapshot_CRUD(t *testing.T) { Name: "test-volume-for-snapshot", SizeGB: 50, Type: "ssd", + ZonalResourceRequest: cloudscale.ZonalResourceRequest{ + Zone: testZone, + }, } volume, err := client.Volumes.Create(ctx, volumeCreateRequest) if err != nil { @@ -80,6 +83,9 @@ func TestIntegrationVolumeSnapshot_Update(t *testing.T) { Name: "test-volume-for-snapshot", SizeGB: 50, Type: "ssd", + ZonalResourceRequest: cloudscale.ZonalResourceRequest{ + Zone: testZone, + }, } volume, err := client.Volumes.Create(ctx, volumeCreateRequest) if err != nil { From 31eefaa4d650d4b08695f8be34aebb4b1409d725 Mon Sep 17 00:00:00 2001 From: Julian Bigler Date: Mon, 29 Dec 2025 18:03:14 +0100 Subject: [PATCH 08/12] add status and check for volume status deleting in waitForSnapshotDeletion helper --- .../volume_snapshots_integration_test.go | 19 +++++++++++++++---- volume_snapshots.go | 1 + 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/test/integration/volume_snapshots_integration_test.go b/test/integration/volume_snapshots_integration_test.go index 5d9aa8a..93eba60 100644 --- a/test/integration/volume_snapshots_integration_test.go +++ b/test/integration/volume_snapshots_integration_test.go @@ -136,18 +136,29 @@ func TestIntegrationVolumeSnapshot_Update(t *testing.T) { // waitForSnapshotDeletion polls the API until the snapshot no longer exists func waitForSnapshotDeletion(ctx context.Context, snapshotUUID string, maxWaitSeconds int) error { for i := 0; i < maxWaitSeconds; i++ { - _, err := client.VolumeSnapshots.Get(ctx, snapshotUUID) + snapshot, err := client.VolumeSnapshots.Get(ctx, snapshotUUID) if err != nil { - // If we get a 404 error, the snapshot is deleted + if apiErr, ok := err.(*cloudscale.ErrorResponse); ok { if apiErr.StatusCode == 404 { + // if we get a 404 error, snapshot is gone, deletion completed return nil } } - // Some other error occurred + // some other error occurred return err } - // Snapshot still exists, wait 1 second and try again + + // if snapshot still exists, it must be in state deleting + if snapshot.Status != "deleting" { + return fmt.Errorf( + "snapshot %s exists but is in unexpected state %q while waiting for deletion", + snapshotUUID, + snapshot.Status, + ) + } + + // snapshot still exists, wait 1 second and try again time.Sleep(1 * time.Second) } return fmt.Errorf("snapshot %s still exists after %d seconds", snapshotUUID, maxWaitSeconds) diff --git a/volume_snapshots.go b/volume_snapshots.go index 9dddd60..154d927 100644 --- a/volume_snapshots.go +++ b/volume_snapshots.go @@ -13,6 +13,7 @@ type VolumeSnapshot struct { SizeGB int `json:"size_gb,omitempty"` CreatedAt string `json:"created_at,omitempty"` Volume VolumeStub `json:"volume,omitempty"` + Status string `json:"status,omitempty"` } type VolumeSnapshotRequest struct { From 4b198cd3544ad4d335492904fbfc35e9c142a81b Mon Sep 17 00:00:00 2001 From: Julian Bigler Date: Mon, 29 Dec 2025 18:08:45 +0100 Subject: [PATCH 09/12] remove unit tests, they only test genric_service.go behaviour --- volume_snapshots_test.go | 317 --------------------------------------- 1 file changed, 317 deletions(-) delete mode 100644 volume_snapshots_test.go diff --git a/volume_snapshots_test.go b/volume_snapshots_test.go deleted file mode 100644 index 87be4bf..0000000 --- a/volume_snapshots_test.go +++ /dev/null @@ -1,317 +0,0 @@ -package cloudscale - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "reflect" - "testing" -) - -func TestVolumeSnapshots_Create(t *testing.T) { - setup() - defer teardown() - - mux.HandleFunc("/v1/volume-snapshots", func(w http.ResponseWriter, r *http.Request) { - testHTTPMethod(t, r, http.MethodPost) - - // Verify the request body has correct field name - var req VolumeSnapshotRequest - json.NewDecoder(r.Body).Decode(&req) - if req.SourceVolume != "volume-uuid" { - t.Errorf("Expected SourceVolume to be 'volume-uuid', got '%s'", req.SourceVolume) - } - - fmt.Fprint(w, `{"uuid": "snapshot-uuid", "name": "test-snapshot"}`) - }) - - volumeCreateRequest := &VolumeSnapshotRequest{ - Name: "test-snapshot", - SourceVolume: "volume-uuid", - } - - snapshot, err := client.VolumeSnapshots.Create(context.Background(), volumeCreateRequest) - - if err != nil { - t.Errorf("VolumeSnapshots.Create returned error: %v", err) - } - - expected := &VolumeSnapshot{UUID: "snapshot-uuid", Name: "test-snapshot"} - if !reflect.DeepEqual(snapshot, expected) { - t.Errorf("VolumeSnapshots.Create\n got=%#v\nwant=%#v", snapshot, expected) - } -} - -func TestVolumeSnapshots_Get(t *testing.T) { - setup() - defer teardown() - - mux.HandleFunc("/v1/volume-snapshots/snapshot-uuid", func(w http.ResponseWriter, r *http.Request) { - testHTTPMethod(t, r, http.MethodGet) - fmt.Fprint(w, `{ - "uuid": "snapshot-uuid", - "name": "test-snapshot", - "size_gb": 50, - "created_at": "2024-01-15T10:30:00Z", - "volume": { - "uuid": "volume-uuid" - }, - "zone": { - "slug": "lpg1" - }, - "tags": {} - }`) - }) - - snapshot, err := client.VolumeSnapshots.Get(context.Background(), "snapshot-uuid") - - if err != nil { - t.Errorf("VolumeSnapshots.Get returned error: %v", err) - } - - expected := &VolumeSnapshot{ - UUID: "snapshot-uuid", - Name: "test-snapshot", - SizeGB: 50, - CreatedAt: "2024-01-15T10:30:00Z", - Volume: VolumeStub{ - UUID: "volume-uuid", - }, - ZonalResource: ZonalResource{ - Zone{ - Slug: "lpg1", - }, - }, - TaggedResource: TaggedResource{ - Tags: TagMap{}, - }, - } - - if !reflect.DeepEqual(snapshot, expected) { - t.Errorf("VolumeSnapshots.Get\n got=%#v\nwant=%#v", snapshot, expected) - } -} - -func TestVolumeSnapshots_Update(t *testing.T) { - setup() - defer teardown() - - mux.HandleFunc("/v1/volume-snapshots/snapshot-uuid", func(w http.ResponseWriter, r *http.Request) { - testHTTPMethod(t, r, http.MethodPatch) - - var req VolumeSnapshotUpdateRequest - err := json.NewDecoder(r.Body).Decode(&req) - if err != nil { - t.Errorf("Failed to decode request body: %v", err) - } - - if req.Name != "updated-snapshot" { - t.Errorf("Expected Name to be 'updated-snapshot', got '%s'", req.Name) - } - - w.WriteHeader(http.StatusNoContent) - }) - - updateRequest := &VolumeSnapshotUpdateRequest{ - Name: "updated-snapshot", - } - - err := client.VolumeSnapshots.Update(context.Background(), "snapshot-uuid", updateRequest) - - if err != nil { - t.Errorf("VolumeSnapshots.Update returned error: %v", err) - } -} - -func TestVolumeSnapshots_Update_WithTags(t *testing.T) { - setup() - defer teardown() - - mux.HandleFunc("/v1/volume-snapshots/snapshot-uuid", func(w http.ResponseWriter, r *http.Request) { - testHTTPMethod(t, r, http.MethodPatch) - - var req VolumeSnapshotUpdateRequest - err := json.NewDecoder(r.Body).Decode(&req) - if err != nil { - t.Errorf("Failed to decode request body: %v", err) - } - - if req.Tags == nil { - t.Error("Expected Tags to be set") - } - - if (*req.Tags)["environment"] != "production" { - t.Errorf("Expected tag 'environment' to be 'production', got '%s'", (*req.Tags)["environment"]) - } - - w.WriteHeader(http.StatusNoContent) - }) - - tags := TagMap{"environment": "production"} - - updateRequest := &VolumeSnapshotUpdateRequest{ - TaggedResourceRequest: TaggedResourceRequest{ - Tags: &tags, - }, - } - - err := client.VolumeSnapshots.Update(context.Background(), "snapshot-uuid", updateRequest) - - if err != nil { - t.Errorf("VolumeSnapshots.Update returned error: %v", err) - } -} - -func TestVolumeSnapshots_Delete(t *testing.T) { - setup() - defer teardown() - - mux.HandleFunc("/v1/volume-snapshots/snapshot-uuid", func(w http.ResponseWriter, r *http.Request) { - testHTTPMethod(t, r, http.MethodDelete) - w.WriteHeader(http.StatusNoContent) - }) - - err := client.VolumeSnapshots.Delete(context.Background(), "snapshot-uuid") - - if err != nil { - t.Errorf("VolumeSnapshots.Delete returned error: %v", err) - } -} - -func TestVolumeSnapshots_List(t *testing.T) { - setup() - defer teardown() - - mux.HandleFunc("/v1/volume-snapshots", func(w http.ResponseWriter, r *http.Request) { - testHTTPMethod(t, r, http.MethodGet) - fmt.Fprint(w, `[ - { - "uuid": "snapshot-uuid-1", - "name": "snapshot-1", - "size_gb": 50, - "created_at": "2024-01-15T10:30:00Z", - "volume": { - "uuid": "volume-uuid-1" - }, - "zone": { - "slug": "lpg1" - }, - "tags": {} - }, - { - "uuid": "snapshot-uuid-2", - "name": "snapshot-2", - "size_gb": 100, - "created_at": "2024-01-16T11:00:00Z", - "volume": { - "uuid": "volume-uuid-2" - }, - "zone": { - "slug": "rma1" - }, - "tags": {"environment": "test"} - } - ]`) - }) - - snapshots, err := client.VolumeSnapshots.List(context.Background()) - - if err != nil { - t.Errorf("VolumeSnapshots.List returned error: %v", err) - } - - if len(snapshots) != 2 { - t.Errorf("Expected 2 snapshots, got %d", len(snapshots)) - } - - expected := []VolumeSnapshot{ - { - UUID: "snapshot-uuid-1", - Name: "snapshot-1", - SizeGB: 50, - CreatedAt: "2024-01-15T10:30:00Z", - Volume: VolumeStub{ - UUID: "volume-uuid-1", - }, - ZonalResource: ZonalResource{ - Zone{ - Slug: "lpg1", - }, - }, - TaggedResource: TaggedResource{ - Tags: TagMap{}, - }, - }, - { - UUID: "snapshot-uuid-2", - Name: "snapshot-2", - SizeGB: 100, - CreatedAt: "2024-01-16T11:00:00Z", - Volume: VolumeStub{ - UUID: "volume-uuid-2", - }, - ZonalResource: ZonalResource{ - Zone{ - Slug: "rma1", - }, - }, - TaggedResource: TaggedResource{ - Tags: TagMap{"environment": "test"}, - }, - }, - } - - if !reflect.DeepEqual(snapshots, expected) { - t.Errorf("VolumeSnapshots.List\n got=%#v\nwant=%#v", snapshots, expected) - } -} -func TestVolumeSnapshots_List_WithFilters(t *testing.T) { - setup() - defer teardown() - - mux.HandleFunc("/v1/volume-snapshots", func(w http.ResponseWriter, r *http.Request) { - testHTTPMethod(t, r, http.MethodGet) - - // Check if filter query params are present - query := r.URL.Query() - if query.Get("tag:environment") != "production" { - t.Errorf("Expected tag:environment filter to be 'production', got '%s'", query.Get("tag:environment")) - } - - fmt.Fprint(w, `[ - { - "uuid": "snapshot-uuid-1", - "name": "snapshot-1", - "size_gb": 50, - "created_at": "2024-01-15T10:30:00Z", - "volume": { - "uuid": "volume-uuid-1", - "name": "volume-1" - }, - "zone": { - "slug": "lpg1" - }, - "tags": {"environment": "production"} - } - ]`) - }) - - tagFilter := TagMap{"environment": "production"} - snapshots, err := client.VolumeSnapshots.List( - context.Background(), - WithTagFilter(tagFilter), - ) - - if err != nil { - t.Errorf("VolumeSnapshots.List returned error: %v", err) - } - - if len(snapshots) != 1 { - t.Errorf("Expected 1 snapshot, got %d", len(snapshots)) - } - - if snapshots[0].UUID != "snapshot-uuid-1" { - t.Errorf("Expected snapshot UUID 'snapshot-uuid-1', got '%s'", snapshots[0].UUID) - } -} From c2b65ca63db47e53bd213a1d9a5dc9079bb23150 Mon Sep 17 00:00:00 2001 From: Julian Bigler Date: Tue, 30 Dec 2025 09:23:59 +0100 Subject: [PATCH 10/12] use generic interface definitions --- volume_snapshots.go | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/volume_snapshots.go b/volume_snapshots.go index 154d927..4e46b4c 100644 --- a/volume_snapshots.go +++ b/volume_snapshots.go @@ -1,8 +1,6 @@ package cloudscale -import ( - "context" -) +const volumeSnapshotsBasePath = "v1/volume-snapshots" type VolumeSnapshot struct { ZonalResource @@ -27,12 +25,11 @@ type VolumeSnapshotUpdateRequest struct { Name string `json:"name,omitempty"` } -const volumeSnapshotsBasePath = "v1/volume-snapshots" - type VolumeSnapshotService interface { - Create(ctx context.Context, createRequest *VolumeSnapshotRequest) (*VolumeSnapshot, error) - Get(ctx context.Context, snapshotID string) (*VolumeSnapshot, error) - Update(ctx context.Context, snapshotID string, updateRequest *VolumeSnapshotUpdateRequest) error - Delete(ctx context.Context, snapshotID string) error - List(ctx context.Context, opts ...ListRequestModifier) ([]VolumeSnapshot, error) + GenericCreateService[VolumeSnapshot, VolumeSnapshotRequest] + GenericGetService[VolumeSnapshot] + GenericListService[VolumeSnapshot] + GenericUpdateService[VolumeSnapshot, VolumeSnapshotUpdateRequest] + GenericDeleteService[VolumeSnapshot] + GenericWaitForService[VolumeSnapshot] } From d8bd470b4b919b464062451a35d7f3f7898f3e58 Mon Sep 17 00:00:00 2001 From: Julian Bigler Date: Wed, 7 Jan 2026 14:46:17 +0100 Subject: [PATCH 11/12] make naming consistent: rename create request to VolumeSnapshotCreateRequest to match VolumeSnapshotUpdateRequest --- cloudscale.go | 2 +- test/integration/tags_integration_test.go | 2 +- test/integration/volume_snapshots_integration_test.go | 4 ++-- volume_snapshots.go | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cloudscale.go b/cloudscale.go index d169311..2deb7b8 100644 --- a/cloudscale.go +++ b/cloudscale.go @@ -93,7 +93,7 @@ func NewClient(httpClient *http.Client) *Client { client: c, path: volumeBasePath, } - c.VolumeSnapshots = GenericServiceOperations[VolumeSnapshot, VolumeSnapshotRequest, VolumeSnapshotUpdateRequest]{ + c.VolumeSnapshots = GenericServiceOperations[VolumeSnapshot, VolumeSnapshotCreateRequest, VolumeSnapshotUpdateRequest]{ client: c, path: volumeSnapshotsBasePath, } diff --git a/test/integration/tags_integration_test.go b/test/integration/tags_integration_test.go index e283d70..f76670f 100644 --- a/test/integration/tags_integration_test.go +++ b/test/integration/tags_integration_test.go @@ -187,7 +187,7 @@ func TestIntegrationTags_Snapshot(t *testing.T) { t.Fatalf("Volumes.Create returned error %s\n", err) } - snapshotCreateRequest := &cloudscale.VolumeSnapshotRequest{ + snapshotCreateRequest := &cloudscale.VolumeSnapshotCreateRequest{ Name: "test-snapshot", SourceVolume: volume.UUID, } diff --git a/test/integration/volume_snapshots_integration_test.go b/test/integration/volume_snapshots_integration_test.go index 93eba60..a425c29 100644 --- a/test/integration/volume_snapshots_integration_test.go +++ b/test/integration/volume_snapshots_integration_test.go @@ -30,7 +30,7 @@ func TestIntegrationVolumeSnapshot_CRUD(t *testing.T) { t.Fatalf("Volume.Create: %v", err) } - snapshotCreateRequest := &cloudscale.VolumeSnapshotRequest{ + snapshotCreateRequest := &cloudscale.VolumeSnapshotCreateRequest{ Name: "test-snapshot", SourceVolume: volume.UUID, } @@ -92,7 +92,7 @@ func TestIntegrationVolumeSnapshot_Update(t *testing.T) { t.Fatalf("Volume.Create: %v", err) } - snapshotCreateRequest := &cloudscale.VolumeSnapshotRequest{ + snapshotCreateRequest := &cloudscale.VolumeSnapshotCreateRequest{ Name: "test-snapshot", SourceVolume: volume.UUID, } diff --git a/volume_snapshots.go b/volume_snapshots.go index 4e46b4c..6bcc3d1 100644 --- a/volume_snapshots.go +++ b/volume_snapshots.go @@ -14,7 +14,7 @@ type VolumeSnapshot struct { Status string `json:"status,omitempty"` } -type VolumeSnapshotRequest struct { +type VolumeSnapshotCreateRequest struct { TaggedResourceRequest Name string `json:"name,omitempty"` SourceVolume string `json:"source_volume,omitempty"` @@ -26,7 +26,7 @@ type VolumeSnapshotUpdateRequest struct { } type VolumeSnapshotService interface { - GenericCreateService[VolumeSnapshot, VolumeSnapshotRequest] + GenericCreateService[VolumeSnapshot, VolumeSnapshotCreateRequest] GenericGetService[VolumeSnapshot] GenericListService[VolumeSnapshot] GenericUpdateService[VolumeSnapshot, VolumeSnapshotUpdateRequest] From 99760f9f8e7ca11f4b5b315ced4f27df903146d0 Mon Sep 17 00:00:00 2001 From: Julian Bigler Date: Wed, 7 Jan 2026 15:39:45 +0100 Subject: [PATCH 12/12] improve naming of integration test snapshot resources by using testRunPrefix --- test/integration/tags_integration_test.go | 2 +- .../volume_snapshots_integration_test.go | 21 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/test/integration/tags_integration_test.go b/test/integration/tags_integration_test.go index f76670f..0269bd3 100644 --- a/test/integration/tags_integration_test.go +++ b/test/integration/tags_integration_test.go @@ -188,7 +188,7 @@ func TestIntegrationTags_Snapshot(t *testing.T) { } snapshotCreateRequest := &cloudscale.VolumeSnapshotCreateRequest{ - Name: "test-snapshot", + Name: testRunPrefix, SourceVolume: volume.UUID, } initialTags := getInitialTags() diff --git a/test/integration/volume_snapshots_integration_test.go b/test/integration/volume_snapshots_integration_test.go index a425c29..94e6d57 100644 --- a/test/integration/volume_snapshots_integration_test.go +++ b/test/integration/volume_snapshots_integration_test.go @@ -18,7 +18,7 @@ func TestIntegrationVolumeSnapshot_CRUD(t *testing.T) { // A source volume is needed to create a snapshot. volumeCreateRequest := &cloudscale.VolumeRequest{ - Name: "test-volume-for-snapshot", + Name: testRunPrefix, SizeGB: 50, Type: "ssd", ZonalResourceRequest: cloudscale.ZonalResourceRequest{ @@ -30,8 +30,9 @@ func TestIntegrationVolumeSnapshot_CRUD(t *testing.T) { t.Fatalf("Volume.Create: %v", err) } + volumeName := fmt.Sprintf("%s-snapshot", testRunPrefix) snapshotCreateRequest := &cloudscale.VolumeSnapshotCreateRequest{ - Name: "test-snapshot", + Name: volumeName, SourceVolume: volume.UUID, } snapshot, err := client.VolumeSnapshots.Create(ctx, snapshotCreateRequest) @@ -46,8 +47,8 @@ func TestIntegrationVolumeSnapshot_CRUD(t *testing.T) { if retrieved.UUID != snapshot.UUID { t.Errorf("Expected UUID %s, got %s", snapshot.UUID, retrieved.UUID) } - if retrieved.Name != "test-snapshot" { - t.Errorf("Expected retrieved snapshot name 'test-snapshot', got '%s'", retrieved.Name) + if retrieved.Name != volumeName { + t.Errorf("Expected snapshot name '%s', got '%s'", volumeName, retrieved.Name) } snapshots, err := client.VolumeSnapshots.List(ctx) @@ -80,7 +81,7 @@ func TestIntegrationVolumeSnapshot_Update(t *testing.T) { // A source volume is needed to create a snapshot. volumeCreateRequest := &cloudscale.VolumeRequest{ - Name: "test-volume-for-snapshot", + Name: testRunPrefix, SizeGB: 50, Type: "ssd", ZonalResourceRequest: cloudscale.ZonalResourceRequest{ @@ -93,7 +94,7 @@ func TestIntegrationVolumeSnapshot_Update(t *testing.T) { } snapshotCreateRequest := &cloudscale.VolumeSnapshotCreateRequest{ - Name: "test-snapshot", + Name: testRunPrefix, SourceVolume: volume.UUID, } snapshot, err := client.VolumeSnapshots.Create(ctx, snapshotCreateRequest) @@ -101,8 +102,10 @@ func TestIntegrationVolumeSnapshot_Update(t *testing.T) { t.Fatalf("VolumeSnapshots.Create: %v", err) } + updatedName := fmt.Sprintf("%s-updated", testRunPrefix) + snapshotUpdateRequest := &cloudscale.VolumeSnapshotUpdateRequest{ - Name: "updated-snapshot", + Name: updatedName, } err = client.VolumeSnapshots.Update(ctx, snapshot.UUID, snapshotUpdateRequest) if err != nil { @@ -114,8 +117,8 @@ func TestIntegrationVolumeSnapshot_Update(t *testing.T) { if err != nil { t.Fatalf("VolumeSnapshots.Get after update: %v", err) } - if updatedSnapshot.Name != "updated-snapshot" { - t.Errorf("Expected updated snapshot name 'updated-snapshot', got '%s'", updatedSnapshot.Name) + if updatedSnapshot.Name != updatedName { + t.Errorf("Expected updated snapshot name '%s', got '%s'", updatedName, updatedSnapshot.Name) } if err := client.VolumeSnapshots.Delete(ctx, snapshot.UUID); err != nil {