Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c42dbd3
add crud for volume snapshots
disperate Dec 22, 2025
680e79b
improve integration test name to match other tests
disperate Dec 22, 2025
dda9c21
use embedded structs for zone and tags
disperate Dec 29, 2025
97ec39e
include volume snapshots in tags integration test
disperate Dec 29, 2025
0f13af9
add omitempty to future-proof the sdk for api changes
disperate Dec 29, 2025
a172488
move update operation to separate integration test
disperate Dec 29, 2025
96c1eaa
use testZone for resource creation in integration tests
disperate Dec 29, 2025
31eefaa
add status and check for volume status deleting in waitForSnapshotDel…
disperate Dec 29, 2025
4b198cd
remove unit tests, they only test genric_service.go behaviour
disperate Dec 29, 2025
c2b65ca
use generic interface definitions
disperate Dec 30, 2025
d8bd470
make naming consistent: rename create request to VolumeSnapshotCreate…
disperate Jan 7, 2026
cfe09fd
add VolumeCreateRequest with support for VolumeSnapshotUUID
disperate Jan 7, 2026
f3fd0b0
add integration test for creating volume from snapshot
disperate Jan 7, 2026
68060c3
add integration test tagging a volume created from a snapshot
disperate Jan 7, 2026
99760f9
improve naming of integration test snapshot resources by using testRu…
disperate Jan 7, 2026
d14a0cb
improve cleanup of volume snapshots in integration tests
disperate Jan 13, 2026
c971fe3
fix source_volume parsing
disperate Feb 13, 2026
2102b07
Merge branch 'julian/add-snapshot-crud-operations' into julian/add-vo…
disperate Feb 13, 2026
edaf597
Merge pull request #74 from cloudscale-ch/julian/add-volume-creation-…
disperate Feb 13, 2026
ae95a6f
rename Volume to SourceVolume to closer match API
disperate Feb 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion cloudscale.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type Client struct {
Regions RegionService
Servers ServerService
Volumes VolumeService
VolumeSnapshots VolumeSnapshotService
Networks NetworkService
Subnets SubnetService
FloatingIPs FloatingIPsService
Expand Down Expand Up @@ -88,10 +89,14 @@ func NewClient(httpClient *http.Client) *Client {
client: c,
path: floatingIPsBasePath,
}
c.Volumes = GenericServiceOperations[Volume, VolumeRequest, VolumeRequest]{
c.Volumes = GenericServiceOperations[Volume, VolumeCreateRequest, VolumeUpdateRequest]{
client: c,
path: volumeBasePath,
}
c.VolumeSnapshots = GenericServiceOperations[VolumeSnapshot, VolumeSnapshotCreateRequest, VolumeSnapshotUpdateRequest]{
client: c,
path: volumeSnapshotsBasePath,
}
c.ServerGroups = GenericServiceOperations[ServerGroup, ServerGroupRequest, ServerGroupRequest]{
client: c,
path: serverGroupsBasePath,
Expand Down
23 changes: 23 additions & 0 deletions test/integration/cloudscale_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func TestMain(m *testing.M) {
foundResource := false
foundResource = foundResource || DeleteRemainingServer()
foundResource = foundResource || DeleteRemainingServerGroups()
foundResource = foundResource || DeleteRemainingVolumeSnapshots()
foundResource = foundResource || DeleteRemainingVolumes()
foundResource = foundResource || DeleteRemainingSubnets()
foundResource = foundResource || DeleteRemainingNetworks()
Expand Down Expand Up @@ -106,6 +107,28 @@ func DeleteRemainingServerGroups() bool {
return foundResource
}

func DeleteRemainingVolumeSnapshots() bool {
foundResource := false

snapshots, err := client.VolumeSnapshots.List(context.Background())
if err != nil {
log.Fatalf("VolumeSnapshots.List returned error %s\n", err)
}

for _, snapshot := range snapshots {
if strings.HasPrefix(snapshot.Name, testRunPrefix) {
foundResource = true
log.Printf("Found not deleted snapshot: %s (%s)\n", snapshot.Name, snapshot.UUID)
err = client.VolumeSnapshots.Delete(context.Background(), snapshot.UUID)
if err != nil {
log.Fatalf("VolumeSnapshots.Delete returned error %s\n", err)
}
}
}

return foundResource
}

func DeleteRemainingVolumes() bool {
foundResource := false

Expand Down
195 changes: 193 additions & 2 deletions test/integration/tags_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func TestIntegrationTags_Server(t *testing.T) {
func TestIntegrationTags_Volume(t *testing.T) {
integrationTest(t)

createRequest := cloudscale.VolumeRequest{
createRequest := cloudscale.VolumeCreateRequest{
Name: testRunPrefix,
SizeGB: 3,
}
Expand All @@ -129,7 +129,7 @@ func TestIntegrationTags_Volume(t *testing.T) {
t.Errorf("Tagging failed, could not tag, is at %s\n", getResult.Tags)
}

updateRequest := cloudscale.VolumeRequest{}
updateRequest := cloudscale.VolumeUpdateRequest{}
newTags := getNewTags()
updateRequest.Tags = &newTags

Expand Down Expand Up @@ -174,6 +174,197 @@ func TestIntegrationTags_Volume(t *testing.T) {
}
}

func TestIntegrationTags_VolumeFromSnapshot(t *testing.T) {
integrationTest(t)
ctx := context.Background()

// first create a volume and a snapshot
createVolumeRequest := &cloudscale.VolumeCreateRequest{
Name: testRunPrefix,
SizeGB: 3,
}
sourceVolume, err := client.Volumes.Create(ctx, createVolumeRequest)
if err != nil {
t.Fatalf("Volumes.Create returned error %s\n", err)
}
snapshotCreateRequest := &cloudscale.VolumeSnapshotCreateRequest{
Name: testRunPrefix,
SourceVolume: sourceVolume.UUID,
}
snapshot, err := client.VolumeSnapshots.Create(ctx, snapshotCreateRequest)
if err != nil {
t.Fatalf("VolumeSnapshots.Create: %v", err)
}

createVolumeFromSnapshotRequest := &cloudscale.VolumeCreateRequest{
Name: fmt.Sprintf("%s-from-snapshot", testRunPrefix),
VolumeSnapshotUUID: snapshot.UUID,
}

initialTags := getInitialTags()
createVolumeFromSnapshotRequest.Tags = &initialTags

volume, err := client.Volumes.Create(ctx, createVolumeFromSnapshotRequest)
if err != nil {
t.Fatalf("Volumes.Create returned error %s\n", err)
}

getResult, err := client.Volumes.Get(ctx, volume.UUID)
if err != nil {
t.Errorf("Volumes.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.VolumeUpdateRequest{}
newTags := getNewTags()
updateRequest.Tags = &newTags

err = client.Volumes.Update(ctx, volume.UUID, &updateRequest)
if err != nil {
t.Errorf("Volumes.Update returned error: %v", err)
}
getResult2, err := client.Volumes.Get(ctx, volume.UUID)
if err != nil {
t.Errorf("Volumes.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.Volumes.List(ctx, cloudscale.WithTagFilter(tags))
if err != nil {
t.Errorf("Volumes.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.Volumes.List(ctx, cloudscale.WithTagFilter(tags))
if err != nil {
t.Errorf("Volumes.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))
}
}

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
// As a volume has been created, deletion can take a few seconds longer
err = waitForSnapshotDeletion(ctx, snapshot.UUID, 30)
if err != nil {
t.Fatalf("Snapshot deletion timeout: %v", err)
}

if err := client.Volumes.Delete(ctx, sourceVolume.UUID); err != nil {
t.Fatalf("Volumes.Delete returned error %s: %v", sourceVolume.UUID, err)
}

err = client.Volumes.Delete(ctx, volume.UUID)
if err != nil {
t.Fatalf("Volumes.Delete returned error %s\n", err)
}
}

func TestIntegrationTags_Snapshot(t *testing.T) {
integrationTest(t)

createVolumeRequest := cloudscale.VolumeCreateRequest{
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.VolumeSnapshotCreateRequest{
Name: testRunPrefix,
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)

Expand Down
Loading