Skip to content
14 changes: 7 additions & 7 deletions .github/workflows/pr-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,28 @@ on:
jobs:
lint:
name: Lint
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
steps:
- name: Setup up Go 1.x
- name: Setup up Go 1.23
uses: actions/setup-go@v5
with:
go-version: "^1.15"
go-version: "1.23"
- name: Check out code
uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.58.2
version: v1.63.4
args: --timeout=5m

build:
name: Test & Build
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
steps:
- name: Setup up Go 1.x
- name: Setup up Go 1.23
uses: actions/setup-go@v5
with:
go-version: "^1.15"
go-version: "1.23"

- name: Check out code
uses: actions/checkout@v4
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ env:
jobs:
push:
name: Push images
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04

steps:
- name: Check out code
Expand Down Expand Up @@ -67,7 +67,7 @@ jobs:

release:
name: Release
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04

# Run only if previous job has succeeded
needs: [push]
Expand Down
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module github.com/shapeblue/cloudstack-csi-driver

go 1.21
go 1.23

toolchain go1.23.5

require (
github.com/apache/cloudstack-go/v2 v2.16.1
Expand All @@ -12,6 +14,7 @@ require (
golang.org/x/sys v0.20.0
golang.org/x/text v0.16.0
google.golang.org/grpc v1.65.0
google.golang.org/protobuf v1.34.1
gopkg.in/gcfg.v1 v1.2.3
k8s.io/api v0.29.7
k8s.io/apimachinery v0.29.7
Expand Down Expand Up @@ -67,7 +70,6 @@ require (
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
Expand Down
9 changes: 7 additions & 2 deletions pkg/cloud/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
)

// Interface is the CloudStack client interface.

//nolint:interfacebloat
type Interface interface {
GetNodeInfo(ctx context.Context, vmName string) (*VM, error)
GetVMByID(ctx context.Context, vmID string) (*VM, error)
Expand All @@ -24,10 +26,12 @@ type Interface interface {
DetachVolume(ctx context.Context, volumeID string) error
ExpandVolume(ctx context.Context, volumeID string, newSizeInGB int64) error

CreateVolumeFromSnapshot(ctx context.Context, zoneID, name, domainID, projectID, snapshotID string, sizeInGB int64) (*Volume, error)
CreateVolumeFromSnapshot(ctx context.Context, zoneID, name, projectID, snapshotID string, sizeInGB int64) (*Volume, error)
GetSnapshotByID(ctx context.Context, snapshotID string) (*Snapshot, error)
CreateSnapshot(ctx context.Context, volumeID string) (*Snapshot, error)
GetSnapshotByName(ctx context.Context, name string) (*Snapshot, error)
CreateSnapshot(ctx context.Context, volumeID, name string) (*Snapshot, error)
DeleteSnapshot(ctx context.Context, snapshotID string) error
ListSnapshots(ctx context.Context, volumeID, snapshotID string) ([]*Snapshot, error)
}

// Volume represents a CloudStack volume.
Expand Down Expand Up @@ -70,6 +74,7 @@ type VM struct {
var (
ErrNotFound = errors.New("not found")
ErrTooManyResults = errors.New("too many results")
ErrAlreadyExists = errors.New("already exists")
)

// client is the implementation of Interface.
Expand Down
2 changes: 1 addition & 1 deletion pkg/cloud/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package cloud
import (
"fmt"

"gopkg.in/gcfg.v1"
gcfg "gopkg.in/gcfg.v1"
)

// Config holds CloudStack connection configuration.
Expand Down
152 changes: 128 additions & 24 deletions pkg/cloud/fake/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package fake

import (
"context"
"errors"

"github.com/hashicorp/go-uuid"

Expand All @@ -12,13 +13,13 @@ import (
)

const zoneID = "a1887604-237c-4212-a9cd-94620b7880fa"
const snapshotID = "9d076136-657b-4c84-b279-455da3ea484c"

type fakeConnector struct {
node *cloud.VM
snapshot *cloud.Snapshot
volumesByID map[string]cloud.Volume
volumesByName map[string]cloud.Volume
node *cloud.VM
volumesByID map[string]cloud.Volume
volumesByName map[string]cloud.Volume
snapshotsByID map[string]*cloud.Snapshot
snapshotsByName map[string][]*cloud.Snapshot
}

// New returns a new fake implementation of the
Expand All @@ -38,20 +39,15 @@ func New() cloud.Interface {
ZoneID: zoneID,
}

snapshot := &cloud.Snapshot{
ID: "9d076136-657b-4c84-b279-455da3ea484c",
Name: "pvc-vol-snap-1",
DomainID: "51f0fcb5-db16-4637-94f5-30131010214f",
ZoneID: zoneID,
VolumeID: "4f1f610d-6f17-4ff9-9228-e4062af93e54",
CreatedAt: "2025-07-07 16:13:06",
}
snapshotsByID := make(map[string]*cloud.Snapshot)
snapshotsByName := make(map[string][]*cloud.Snapshot)

return &fakeConnector{
node: node,
snapshot: snapshot,
volumesByID: map[string]cloud.Volume{volume.ID: volume},
volumesByName: map[string]cloud.Volume{volume.Name: volume},
node: node,
volumesByID: map[string]cloud.Volume{volume.ID: volume},
volumesByName: map[string]cloud.Volume{volume.Name: volume},
snapshotsByID: snapshotsByID,
snapshotsByName: snapshotsByName,
}
}

Expand All @@ -72,6 +68,9 @@ func (f *fakeConnector) ListZonesID(_ context.Context) ([]string, error) {
}

func (f *fakeConnector) GetVolumeByID(_ context.Context, volumeID string) (*cloud.Volume, error) {
if volumeID == "" {
return nil, errors.New("invalid volume ID: empty string")
}
vol, ok := f.volumesByID[volumeID]
if ok {
return &vol, nil
Expand All @@ -81,6 +80,9 @@ func (f *fakeConnector) GetVolumeByID(_ context.Context, volumeID string) (*clou
}

func (f *fakeConnector) GetVolumeByName(_ context.Context, name string) (*cloud.Volume, error) {
if name == "" {
return nil, errors.New("invalid volume name: empty string")
}
vol, ok := f.volumesByName[name]
if ok {
return &vol, nil
Expand Down Expand Up @@ -137,18 +139,120 @@ func (f *fakeConnector) ExpandVolume(_ context.Context, volumeID string, newSize
return cloud.ErrNotFound
}

func (f *fakeConnector) CreateVolumeFromSnapshot(ctx context.Context, zoneID, name, domainID, projectID, snapshotID string, sizeInGB int64) (*cloud.Volume, error) {
return nil, nil
func (f *fakeConnector) CreateVolumeFromSnapshot(_ context.Context, zoneID, name, _, _ string, sizeInGB int64) (*cloud.Volume, error) {
vol := &cloud.Volume{
ID: "fake-vol-from-snap-" + name,
Name: name,
Size: util.GigaBytesToBytes(sizeInGB),
DiskOfferingID: "fake-disk-offering",
ZoneID: zoneID,
}
f.volumesByID[vol.ID] = *vol
f.volumesByName[vol.Name] = *vol

return vol, nil
}

func (f *fakeConnector) GetSnapshotByID(ctx context.Context, snapshotID string) (*cloud.Snapshot, error) {
return f.snapshot, nil
func (f *fakeConnector) CreateSnapshot(_ context.Context, volumeID, name string) (*cloud.Snapshot, error) {
if name == "" {
return nil, errors.New("invalid snapshot name: empty string")
}
for _, snap := range f.snapshotsByName[name] {
if snap.VolumeID == volumeID {
// Allow multiple snapshots with the same name for the same volume
continue
}

// Name conflict: same name, different volume
return nil, cloud.ErrAlreadyExists
}
id, _ := uuid.GenerateUUID()
newSnap := &cloud.Snapshot{
ID: id,
Name: name,
DomainID: "fake-domain",
ZoneID: zoneID,
VolumeID: volumeID,
CreatedAt: "2025-07-07T16:13:06-0700",
}
f.snapshotsByID[newSnap.ID] = newSnap
f.snapshotsByName[name] = append(f.snapshotsByName[name], newSnap)

return newSnap, nil
}

func (f *fakeConnector) CreateSnapshot(ctx context.Context, volumeID string) (*cloud.Snapshot, error) {
return f.snapshot, nil
func (f *fakeConnector) GetSnapshotByID(_ context.Context, snapshotID string) (*cloud.Snapshot, error) {
snap, ok := f.snapshotsByID[snapshotID]
if ok {
return snap, nil
}

return nil, cloud.ErrNotFound
}

func (f *fakeConnector) DeleteSnapshot(ctx context.Context, snapshotID string) error {
func (f *fakeConnector) GetSnapshotByName(_ context.Context, name string) (*cloud.Snapshot, error) {
if name == "" {
return nil, errors.New("invalid snapshot name: empty string")
}
snaps, ok := f.snapshotsByName[name]
if ok && len(snaps) > 0 {
return snaps[0], nil // Return the first for compatibility
}

return nil, cloud.ErrNotFound
}

// ListSnapshots returns all matching snapshots; pagination must be handled by the controller.
func (f *fakeConnector) ListSnapshots(_ context.Context, volumeID, snapshotID string) ([]*cloud.Snapshot, error) {
if snapshotID != "" {
result := make([]*cloud.Snapshot, 0, 1)
if snap, ok := f.snapshotsByID[snapshotID]; ok {
result = append(result, snap)
}

return result, nil
}
if volumeID != "" {
count := 0
for _, snap := range f.snapshotsByID {
if snap.VolumeID == volumeID {
count++
}
}
result := make([]*cloud.Snapshot, 0, count)
for _, snap := range f.snapshotsByID {
if snap.VolumeID == volumeID {
result = append(result, snap)
}
}

return result, nil
}
result := make([]*cloud.Snapshot, 0, len(f.snapshotsByID))
for _, snap := range f.snapshotsByID {
result = append(result, snap)
}

return result, nil
}

func (f *fakeConnector) DeleteSnapshot(_ context.Context, snapshotID string) error {
snap, ok := f.snapshotsByID[snapshotID]
if !ok {
return cloud.ErrNotFound
}

delete(f.snapshotsByID, snapshotID)

name := snap.Name
snaps := f.snapshotsByName[name]
for i, s := range snaps {
if s.ID == snapshotID {
f.snapshotsByName[name] = append(snaps[:i], snaps[i+1:]...)

break
}
}

return nil
}
Loading
Loading