From fad82edf6a5b83a160b50be9d5cfbf183f6e68c3 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Mon, 2 Jun 2025 17:08:28 +0530 Subject: [PATCH 01/48] fix image references and increase leniency of the pod anti affinity rule --- deploy/k8s/controller-deployment.yaml | 20 +++++++++++--------- deploy/k8s/node-daemonset.yaml | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/deploy/k8s/controller-deployment.yaml b/deploy/k8s/controller-deployment.yaml index 44adffc..423a229 100644 --- a/deploy/k8s/controller-deployment.yaml +++ b/deploy/k8s/controller-deployment.yaml @@ -25,14 +25,16 @@ spec: serviceAccountName: cloudstack-csi-controller affinity: podAntiAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - - labelSelector: - matchExpressions: - - key: "app.kubernetes.io/name" - operator: In - values: - - cloudstack-csi-controller - topologyKey: "kubernetes.io/hostname" + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: "app.kubernetes.io/name" + operator: In + values: + - cloudstack-csi-controller + topologyKey: "kubernetes.io/hostname" nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: @@ -59,7 +61,7 @@ spec: containers: - name: cloudstack-csi-controller - image: cloudstack-csi-driver + image: ghcr.io/shapeblue/cloudstack-csi-driver:master imagePullPolicy: Always args: - "controller" diff --git a/deploy/k8s/node-daemonset.yaml b/deploy/k8s/node-daemonset.yaml index 665312b..1b92092 100644 --- a/deploy/k8s/node-daemonset.yaml +++ b/deploy/k8s/node-daemonset.yaml @@ -36,7 +36,7 @@ spec: containers: - name: cloudstack-csi-node - image: cloudstack-csi-driver + image: ghcr.io/shapeblue/cloudstack-csi-driver:master imagePullPolicy: IfNotPresent args: - "node" From 9ac8b7545034ce5bbc612b0f92b0c28794085038 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Mon, 7 Jul 2025 11:53:58 -0400 Subject: [PATCH 02/48] Add logic to identify device path of attached volume on VMware --- pkg/mount/mount.go | 99 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/pkg/mount/mount.go b/pkg/mount/mount.go index 6e45aaf..78e3488 100644 --- a/pkg/mount/mount.go +++ b/pkg/mount/mount.go @@ -111,6 +111,15 @@ func (m *mounter) GetDevicePath(ctx context.Context, volumeID string) (string, e } func (m *mounter) getDevicePathBySerialID(volumeID string) (string, error) { + // Try VMware device paths + vmwareDevicePath, err := m.getDevicePathForVMware(volumeID) + if err != nil { + fmt.Printf("Failed to get VMware device path: %v\n", err) + } + if vmwareDevicePath != "" { + return vmwareDevicePath, nil + } + // Fall back to standard device paths (for KVM) sourcePathPrefixes := []string{"virtio-", "scsi-", "scsi-0QEMU_QEMU_HARDDISK_"} serial := diskUUIDToSerial(volumeID) for _, prefix := range sourcePathPrefixes { @@ -127,6 +136,96 @@ func (m *mounter) getDevicePathBySerialID(volumeID string) (string, error) { return "", nil } +func (m *mounter) getDevicePathForVMware(volumeID string) (string, error) { + // Loop through /dev/sdb to /dev/sdz (/dev/sda -> the root disk) + for i := 'b'; i <= 'z'; i++ { + devicePath := fmt.Sprintf("/dev/sd%c", i) + fmt.Printf("Checking VMware device path: %s\n", devicePath) + + if _, err := os.Stat(devicePath); err == nil { + isBlock, err := m.IsBlockDevice(devicePath) + if err == nil && isBlock { + // Use the same verification as for XenServer + if m.verifyVMwareDevice(devicePath, volumeID) { + fmt.Printf("Found and verified VMware device: %s\n", devicePath) + return devicePath, nil + } + } + } + } + return "", fmt.Errorf("device not found for volume %s", volumeID) +} + +func (m *mounter) verifyVMwareDevice(devicePath string, volumeID string) bool { + size, err := m.GetBlockSizeBytes(devicePath) + if err != nil { + fmt.Printf("Failed to get device size: %v\n", err) + return false + } + fmt.Printf("Device size: %d bytes\n", size) + + mounted, err := m.isDeviceMounted(devicePath) + if err != nil { + fmt.Printf("Failed to check if device is mounted: %v\n", err) + return false + } + if mounted { + fmt.Printf("Device is already mounted: %s\n", devicePath) + return false + } + + props, err := m.getDeviceProperties(devicePath) + if err != nil { + fmt.Printf("Failed to get device properties: %v\n", err) + return false + } + fmt.Printf("Device properties: %v\n", props) + + return true +} + +func (m *mounter) isDeviceMounted(devicePath string) (bool, error) { + output, err := m.Exec.Command("grep", devicePath, "/proc/mounts").Output() + if err != nil { + if strings.Contains(err.Error(), "exit status 1") { + return false, nil + } + return false, err + } + return len(output) > 0, nil +} + +func (m *mounter) isDeviceInUse(devicePath string) (bool, error) { + output, err := m.Exec.Command("lsof", devicePath).Output() + if err != nil { + if strings.Contains(err.Error(), "exit status 1") { + return false, nil + } + return false, err + } + return len(output) > 0, nil +} + +func (m *mounter) getDeviceProperties(devicePath string) (map[string]string, error) { + output, err := m.Exec.Command("udevadm", "info", "--query=property", devicePath).Output() + if err != nil { + return nil, err + } + + props := make(map[string]string) + for _, line := range strings.Split(string(output), "\n") { + if line == "" { + continue + } + parts := strings.Split(line, "=") + if len(parts) == 2 { + props[parts[0]] = parts[1] + } + } + + return props, nil +} + func (m *mounter) probeVolume(ctx context.Context) { logger := klog.FromContext(ctx) logger.V(2).Info("Scanning SCSI host") From cea582b9909c2ecd69af0dd49f8eaca82a1c1817 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Mon, 7 Jul 2025 12:01:49 -0400 Subject: [PATCH 03/48] Add support for identifying device path of attached volume on XenServer --- cmd/cloudstack-csi-driver/Dockerfile | 4 +- deploy/k8s/node-daemonset.yaml | 6 +++ pkg/mount/mount.go | 62 ++++++++++++++++++++++++++-- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/cmd/cloudstack-csi-driver/Dockerfile b/cmd/cloudstack-csi-driver/Dockerfile index 4c260e8..dcd14d6 100644 --- a/cmd/cloudstack-csi-driver/Dockerfile +++ b/cmd/cloudstack-csi-driver/Dockerfile @@ -14,7 +14,9 @@ RUN apk add --no-cache \ # blkid, mount and umount are required by k8s.io/mount-utils \ blkid \ mount \ - umount + umount \ + # Provides udevadm for device path detection + udev COPY ./bin/cloudstack-csi-driver /cloudstack-csi-driver ENTRYPOINT ["/cloudstack-csi-driver"] \ No newline at end of file diff --git a/deploy/k8s/node-daemonset.yaml b/deploy/k8s/node-daemonset.yaml index 1b92092..1371931 100644 --- a/deploy/k8s/node-daemonset.yaml +++ b/deploy/k8s/node-daemonset.yaml @@ -64,6 +64,8 @@ spec: mountPath: /dev - name: cloud-init-dir mountPath: /run/cloud-init/ + - name: sys-dir + mountPath: /sys # Comment the above 2 lines and uncomment the next 2 lines for Ignition support # - name: ignition-dir # mountPath: /run/metadata @@ -177,6 +179,10 @@ spec: hostPath: path: /run/cloud-init/ type: Directory + - name: sys-dir + hostPath: + path: /sys + type: Directory # Comment the above 4 lines and uncomment the next 4 lines for Ignition support # - name: ignition-dir # hostPath: diff --git a/pkg/mount/mount.go b/pkg/mount/mount.go index 78e3488..9e47862 100644 --- a/pkg/mount/mount.go +++ b/pkg/mount/mount.go @@ -80,9 +80,9 @@ func (m *mounter) GetBlockSizeBytes(devicePath string) (int64, error) { func (m *mounter) GetDevicePath(ctx context.Context, volumeID string) (string, error) { backoff := wait.Backoff{ - Duration: 1 * time.Second, - Factor: 1.1, - Steps: 15, + Duration: 2 * time.Second, + Factor: 1.5, + Steps: 20, } var devicePath string @@ -111,6 +111,15 @@ func (m *mounter) GetDevicePath(ctx context.Context, volumeID string) (string, e } func (m *mounter) getDevicePathBySerialID(volumeID string) (string, error) { + // First try XenServer device paths + xenDevicePath, err := m.getDevicePathForXenServer(volumeID) + if err != nil { + fmt.Printf("Failed to get VMware device path: %v\n", err) + } + if xenDevicePath != "" { + return xenDevicePath, nil + } + // Try VMware device paths vmwareDevicePath, err := m.getDevicePathForVMware(volumeID) if err != nil { @@ -129,6 +138,7 @@ func (m *mounter) getDevicePathBySerialID(volumeID string) (string, error) { return source, nil } if !os.IsNotExist(err) { + fmt.Printf("Not found: %s\n", err.Error()) return "", err } } @@ -136,6 +146,52 @@ func (m *mounter) getDevicePathBySerialID(volumeID string) (string, error) { return "", nil } +func (m *mounter) getDevicePathForXenServer(volumeID string) (string, error) { + for i := 'b'; i <= 'z'; i++ { + devicePath := fmt.Sprintf("/dev/xvd%c", i) + fmt.Printf("Checking XenServer device path: %s\n", devicePath) + + if _, err := os.Stat(devicePath); err == nil { + isBlock, err := m.IsBlockDevice(devicePath) + if err == nil && isBlock { + if m.verifyXenServerDevice(devicePath, volumeID) { + fmt.Printf("Found and verified XenServer device: %s\n", devicePath) + return devicePath, nil + } + } + } + } + return "", fmt.Errorf("device not found for volume %s", volumeID) +} + +func (m *mounter) verifyXenServerDevice(devicePath string, volumeID string) bool { + size, err := m.GetBlockSizeBytes(devicePath) + if err != nil { + fmt.Printf("Failed to get device size: %v\n", err) + return false + } + fmt.Printf("Device size: %d bytes\n", size) + + mounted, err := m.isDeviceMounted(devicePath) + if err != nil { + fmt.Printf("Failed to check if device is mounted: %v\n", err) + return false + } + if mounted { + fmt.Printf("Device is already mounted: %s\n", devicePath) + return false + } + + props, err := m.getDeviceProperties(devicePath) + if err != nil { + fmt.Printf("Failed to get device properties: %v\n", err) + return false + } + fmt.Printf("Device properties: %v\n", props) + + return true +} + func (m *mounter) getDevicePathForVMware(volumeID string) (string, error) { // Loop through /dev/sdb to /dev/sdz (/dev/sda -> the root disk) for i := 'b'; i <= 'z'; i++ { From 5126c4a2fa457bb1baf3dce258ffe7e5e77f0279 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Fri, 4 Jul 2025 16:29:26 -0400 Subject: [PATCH 04/48] Add support for Projects --- pkg/cloud/cloud.go | 3 ++- pkg/cloud/config.go | 2 ++ pkg/cloud/vms.go | 8 ++++++-- pkg/cloud/volumes.go | 10 +++++++++- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go index a8b5417..7341fe4 100644 --- a/pkg/cloud/cloud.go +++ b/pkg/cloud/cloud.go @@ -55,11 +55,12 @@ var ( // client is the implementation of Interface. type client struct { *cloudstack.CloudStackClient + projectID string } // New creates a new cloud connector, given its configuration. func New(config *Config) Interface { csClient := cloudstack.NewAsyncClient(config.APIURL, config.APIKey, config.SecretKey, config.VerifySSL) - return &client{csClient} + return &client{csClient, config.ProjectID} } diff --git a/pkg/cloud/config.go b/pkg/cloud/config.go index 86e8c57..0024dff 100644 --- a/pkg/cloud/config.go +++ b/pkg/cloud/config.go @@ -12,6 +12,7 @@ type Config struct { APIKey string SecretKey string VerifySSL bool + ProjectID string } // csConfig wraps the config for the CloudStack cloud provider. @@ -40,6 +41,7 @@ func ReadConfig(configFilePath string) (*Config, error) { return &Config{ APIURL: cfg.Global.APIURL, APIKey: cfg.Global.APIKey, + ProjectID: cfg.Global.ProjectID, SecretKey: cfg.Global.SecretKey, VerifySSL: !cfg.Global.SSLNoVerify, }, nil diff --git a/pkg/cloud/vms.go b/pkg/cloud/vms.go index 68a0505..2e98f64 100644 --- a/pkg/cloud/vms.go +++ b/pkg/cloud/vms.go @@ -10,8 +10,12 @@ func (c *client) GetVMByID(ctx context.Context, vmID string) (*VM, error) { logger := klog.FromContext(ctx) p := c.VirtualMachine.NewListVirtualMachinesParams() p.SetId(vmID) + if c.projectID != "" { + p.SetProjectid(c.projectID) + } logger.V(2).Info("CloudStack API call", "command", "ListVirtualMachines", "params", map[string]string{ - "id": vmID, + "id": vmID, + "projectID": c.projectID, }) l, err := c.VirtualMachine.ListVirtualMachines(p) if err != nil { @@ -24,7 +28,7 @@ func (c *client) GetVMByID(ctx context.Context, vmID string) (*VM, error) { return nil, ErrTooManyResults } vm := l.VirtualMachines[0] - + logger.V(2).Info("Returning VM", "vmID", vm.Id, "zoneID", vm.Zoneid) return &VM{ ID: vm.Id, ZoneID: vm.Zoneid, diff --git a/pkg/cloud/volumes.go b/pkg/cloud/volumes.go index 831cdec..8876e35 100644 --- a/pkg/cloud/volumes.go +++ b/pkg/cloud/volumes.go @@ -41,8 +41,12 @@ func (c *client) GetVolumeByID(ctx context.Context, volumeID string) (*Volume, e logger := klog.FromContext(ctx) p := c.Volume.NewListVolumesParams() p.SetId(volumeID) + if c.projectID != "" { + p.SetProjectid(c.projectID) + } logger.V(2).Info("CloudStack API call", "command", "ListVolumes", "params", map[string]string{ - "id": volumeID, + "id": volumeID, + "projectid": c.projectID, }) return c.listVolumes(p) @@ -66,11 +70,15 @@ func (c *client) CreateVolume(ctx context.Context, diskOfferingID, zoneID, name p.SetZoneid(zoneID) p.SetName(name) p.SetSize(sizeInGB) + if c.projectID != "" { + p.SetProjectid(c.projectID) + } logger.V(2).Info("CloudStack API call", "command", "CreateVolume", "params", map[string]string{ "diskofferingid": diskOfferingID, "zoneid": zoneID, "name": name, "size": strconv.FormatInt(sizeInGB, 10), + "projectid": c.projectID, }) vol, err := c.Volume.CreateVolume(p) if err != nil { From 739333b5a0f3ffa20e6c7e5c20c593de1464f951 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Wed, 9 Jul 2025 12:36:36 -0400 Subject: [PATCH 05/48] Add support for Volume snapshot for CloudStack CSI driver --- deploy/k8s/controller-deployment.yaml | 39 +++++++++- deploy/k8s/rbac.yaml | 3 + deploy/k8s/volume-snapshot-class.yaml | 6 ++ examples/k8s/snapshot/pvc-from-snapshot.yaml | 15 ++++ examples/k8s/snapshot/pvc.yaml | 11 +++ examples/k8s/snapshot/snapshot.yaml | 8 ++ pkg/cloud/cloud.go | 18 +++++ pkg/cloud/fake/fake.go | 23 ++++++ pkg/cloud/snapshots.go | 64 +++++++++++++++ pkg/cloud/volumes.go | 33 ++++++++ pkg/driver/controller.go | 82 ++++++++++++++++++++ 11 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 deploy/k8s/volume-snapshot-class.yaml create mode 100644 examples/k8s/snapshot/pvc-from-snapshot.yaml create mode 100644 examples/k8s/snapshot/pvc.yaml create mode 100644 examples/k8s/snapshot/snapshot.yaml create mode 100644 pkg/cloud/snapshots.go diff --git a/deploy/k8s/controller-deployment.yaml b/deploy/k8s/controller-deployment.yaml index 423a229..52376b2 100644 --- a/deploy/k8s/controller-deployment.yaml +++ b/deploy/k8s/controller-deployment.yaml @@ -178,8 +178,45 @@ spec: type: RuntimeDefault readOnlyRootFilesystem: true allowPrivilegeEscalation: false - + - name: csi-snapshotter + image: registry.k8s.io/sig-storage/csi-snapshotter:v6.3.0 + args: + - "--v=5" + - "--csi-address=$(CSI_ADDRESS)" + - "--leader-election" + - "--leader-election-lease-duration=30s" + - "--leader-election-renew-deadline=20s" + - "--leader-election-retry-period=10s" + env: + - name: CSI_ADDRESS + value: /var/lib/csi/sockets/pluginproxy/csi.sock + volumeMounts: + - name: socket-dir + mountPath: /var/lib/csi/sockets/pluginproxy/ + resources: + limits: + cpu: 400m + memory: 200Mi + requests: + cpu: 10m + memory: 20Mi + - name: snapshot-controller + image: registry.k8s.io/sig-storage/snapshot-controller:v6.3.0 + args: + - "--v=5" + - "--leader-election" + - "--leader-election-lease-duration=30s" + - "--leader-election-renew-deadline=20s" + - "--leader-election-retry-period=10s" + resources: + limits: + cpu: 400m + memory: 200Mi + requests: + cpu: 10m + memory: 20Mi - name: liveness-probe + imagePullPolicy: IfNotPresent image: registry.k8s.io/sig-storage/livenessprobe:v2.12.0 args: - "--v=4" diff --git a/deploy/k8s/rbac.yaml b/deploy/k8s/rbac.yaml index 664e97c..c8fb68a 100644 --- a/deploy/k8s/rbac.yaml +++ b/deploy/k8s/rbac.yaml @@ -36,6 +36,9 @@ rules: - apiGroups: ["storage.k8s.io"] resources: ["volumeattachments/status"] verbs: ["patch"] + - apiGroups: ["snapshot.storage.k8s.io"] + resources: ["volumesnapshots", "volumesnapshots/status", "volumesnapshotclasses", "volumesnapshotcontents", "volumesnapshotcontents/status"] + verbs: ["get", "list", "watch", "update", "create", "patch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding diff --git a/deploy/k8s/volume-snapshot-class.yaml b/deploy/k8s/volume-snapshot-class.yaml new file mode 100644 index 0000000..f4096fd --- /dev/null +++ b/deploy/k8s/volume-snapshot-class.yaml @@ -0,0 +1,6 @@ +apiVersion: snapshot.storage.k8s.io/v1 +kind: VolumeSnapshotClass +metadata: + name: cloudstack-snapshot +driver: csi.cloudstack.apache.org +deletionPolicy: Delete \ No newline at end of file diff --git a/examples/k8s/snapshot/pvc-from-snapshot.yaml b/examples/k8s/snapshot/pvc-from-snapshot.yaml new file mode 100644 index 0000000..0ae8bf0 --- /dev/null +++ b/examples/k8s/snapshot/pvc-from-snapshot.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: snapshot-pvc-1 +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + dataSource: + name: snapshot-1 + kind: VolumeSnapshot + apiGroup: snapshot.storage.k8s.io + storageClassName: cloudstack-custom diff --git a/examples/k8s/snapshot/pvc.yaml b/examples/k8s/snapshot/pvc.yaml new file mode 100644 index 0000000..415951a --- /dev/null +++ b/examples/k8s/snapshot/pvc.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: my-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + storageClassName: cloudstack-custom \ No newline at end of file diff --git a/examples/k8s/snapshot/snapshot.yaml b/examples/k8s/snapshot/snapshot.yaml new file mode 100644 index 0000000..e3854ca --- /dev/null +++ b/examples/k8s/snapshot/snapshot.yaml @@ -0,0 +1,8 @@ +apiVersion: snapshot.storage.k8s.io/v1 +kind: VolumeSnapshot +metadata: + name: snapshot-1 +spec: + volumeSnapshotClassName: cloudstack-snapshot + source: + persistentVolumeClaimName: my-pvc \ No newline at end of file diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go index a8b5417..349dd3a 100644 --- a/pkg/cloud/cloud.go +++ b/pkg/cloud/cloud.go @@ -23,6 +23,10 @@ type Interface interface { AttachVolume(ctx context.Context, volumeID, vmID string) (string, error) DetachVolume(ctx context.Context, volumeID string) error ExpandVolume(ctx context.Context, volumeID string, newSizeInGB int64) error + + CreateVolumeFromSnapshot(ctx context.Context, diskOfferingID, zoneID, name, domainID, projectID, snapshotID string, sizeInGB int64) (string, error) + CreateSnapshot(ctx context.Context, volumeID string) (*Snapshot, error) + DeleteSnapshot(ctx context.Context, snapshotID string) error } // Volume represents a CloudStack volume. @@ -34,12 +38,26 @@ type Volume struct { Size int64 DiskOfferingID string + DomainID string + ProjectID string ZoneID string VirtualMachineID string DeviceID string } +type Snapshot struct { + ID string + Name string + + DomainID string + ProjectID string + ZoneID string + + VolumeID string + CreatedAt string +} + // VM represents a CloudStack Virtual Machine. type VM struct { ID string diff --git a/pkg/cloud/fake/fake.go b/pkg/cloud/fake/fake.go index 23218bc..f20e5c6 100644 --- a/pkg/cloud/fake/fake.go +++ b/pkg/cloud/fake/fake.go @@ -15,6 +15,7 @@ const zoneID = "a1887604-237c-4212-a9cd-94620b7880fa" type fakeConnector struct { node *cloud.VM + snapshot *cloud.Snapshot volumesByID map[string]cloud.Volume volumesByName map[string]cloud.Volume } @@ -36,8 +37,18 @@ 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: "bdab539f-651e-431a-979d-5d3c48b54fcf", + VolumeID: "4f1f610d-6f17-4ff9-9228-e4062af93e54", + CreatedAt: "2025-07-07 16:13:06", + } + return &fakeConnector{ node: node, + snapshot: snapshot, volumesByID: map[string]cloud.Volume{volume.ID: volume}, volumesByName: map[string]cloud.Volume{volume.Name: volume}, } @@ -124,3 +135,15 @@ func (f *fakeConnector) ExpandVolume(_ context.Context, volumeID string, newSize return cloud.ErrNotFound } + +func (f *fakeConnector) CreateVolumeFromSnapshot(ctx context.Context, diskOfferingID, zoneID, name, domainID, projectID, snapshotID string, sizeInGB int64) (string, error) { + return "1", nil +} + +func (f *fakeConnector) CreateSnapshot(ctx context.Context, volumeID string) (*cloud.Snapshot, error) { + return f.snapshot, nil +} + +func (f *fakeConnector) DeleteSnapshot(ctx context.Context, snapshotID string) error { + return nil +} diff --git a/pkg/cloud/snapshots.go b/pkg/cloud/snapshots.go new file mode 100644 index 0000000..36458a1 --- /dev/null +++ b/pkg/cloud/snapshots.go @@ -0,0 +1,64 @@ +package cloud + +import ( + "context" + "strings" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (c *client) ListSnapshots(p *cloudstack.ListSnapshotsParams) (*Snapshot, error) { + l, err := c.Snapshot.ListSnapshots(p) + if err != nil { + return nil, err + } + if l.Count == 0 { + return nil, ErrNotFound + } + if l.Count > 1 { + return nil, ErrTooManyResults + } + snapshot := l.Snapshots[0] + s := Snapshot{ + ID: snapshot.Id, + Name: snapshot.Name, + DomainID: snapshot.Domainid, + ProjectID: snapshot.Projectid, + ZoneID: snapshot.Zoneid, + VolumeID: snapshot.Volumeid, + } + + return &s, nil +} + +func (c *client) CreateSnapshot(ctx context.Context, volumeID string) (*Snapshot, error) { + p := c.Snapshot.NewCreateSnapshotParams(volumeID) + snapshot, err := c.Snapshot.CreateSnapshot(p) + if err != nil { + return nil, status.Errorf(codes.Internal, "Error %v", err) + } + + snap := Snapshot{ + ID: snapshot.Id, + Name: snapshot.Name, + DomainID: snapshot.Domainid, + ProjectID: snapshot.Projectid, + ZoneID: snapshot.Zoneid, + VolumeID: snapshot.Volumeid, + CreatedAt: snapshot.Created, + } + return &snap, nil +} + +func (c *client) DeleteSnapshot(ctx context.Context, snapshotID string) error { + p := c.Snapshot.NewDeleteSnapshotParams(snapshotID) + _, err := c.Snapshot.DeleteSnapshot(p) + if err != nil && strings.Contains(err.Error(), "4350") { + // CloudStack error InvalidParameterValueException + return ErrNotFound + } + + return err +} diff --git a/pkg/cloud/volumes.go b/pkg/cloud/volumes.go index 831cdec..b0c1795 100644 --- a/pkg/cloud/volumes.go +++ b/pkg/cloud/volumes.go @@ -29,6 +29,8 @@ func (c *client) listVolumes(p *cloudstack.ListVolumesParams) (*Volume, error) { Name: vol.Name, Size: vol.Size, DiskOfferingID: vol.Diskofferingid, + DomainID: vol.Domainid, + ProjectID: vol.Projectid, ZoneID: vol.Zoneid, VirtualMachineID: vol.Virtualmachineid, DeviceID: strconv.FormatInt(vol.Deviceid, 10), @@ -153,3 +155,34 @@ func (c *client) ExpandVolume(ctx context.Context, volumeID string, newSizeInGB return nil } + +func (c *client) CreateVolumeFromSnapshot(ctx context.Context, diskOfferingID, zoneID, name, domainID, projectID, snapshotID string, sizeInGB int64) (string, error) { + logger := klog.FromContext(ctx) + snapshot, _, err := c.Snapshot.GetSnapshotByID(snapshotID) + if err != nil { + return "", fmt.Errorf("failed to retrieve snapshot '%s': %w", snapshotID, err) + } + + p := c.Volume.NewCreateVolumeParams() + p.SetDiskofferingid(diskOfferingID) + p.SetZoneid(zoneID) + if projectID != "" { + p.SetProjectid(projectID) + } + p.SetName(name) + p.SetSnapshotid(snapshot.Id) + + logger.V(2).Info("CloudStack API call", "command", "CreateVolume", "params", map[string]string{ + "name": name, + "snapshotid": snapshotID, + "projectid": projectID, + }) + // Execute the API call to create volume from snapshot + vol, err := c.Volume.CreateVolume(p) + if err != nil { + // Handle the error accordingly + return "", fmt.Errorf("failed to create volume from snapshot'%s': %w", snapshotID, err) + } + + return vol.Id, err +} diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index 70dffa3..03b35fa 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -5,11 +5,13 @@ import ( "errors" "fmt" "math/rand" + "time" "github.com/container-storage-interface/spec/lib/go/csi" "github.com/kubernetes-csi/csi-lib-utils/protosanitizer" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" "k8s.io/klog/v2" "github.com/shapeblue/cloudstack-csi-driver/pkg/cloud" @@ -108,6 +110,34 @@ func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol return resp, nil } + // Check if this is a volume from snapshot + var snapshotID string + if src := req.GetVolumeContentSource(); src != nil { + if snap := src.GetSnapshot(); snap != nil { + snapshotID = snap.GetSnapshotId() + } + } + + if snapshotID != "" { + logger.Info("Creating volume from snapshot", "snapshotID", snapshotID) + // Call the cloud connector's CreateVolumeFromSnapshot if implemented + volID, err := cs.connector.CreateVolumeFromSnapshot(ctx, diskOfferingID, vol.ZoneID, name, vol.DomainID, vol.ProjectID, snapshotID, vol.Size) + if err != nil { + return nil, status.Errorf(codes.Internal, "Cannot create volume from snapshot %s: %v", snapshotID, err.Error()) + } + resp := &csi.CreateVolumeResponse{ + Volume: &csi.Volume{ + VolumeId: volID, + CapacityBytes: vol.Size, + VolumeContext: req.GetParameters(), + AccessibleTopology: []*csi.Topology{ + Topology{ZoneID: vol.ZoneID}.ToCSI(), + }, + }, + } + return resp, nil + } + // We have to create the volume. // Determine volume size using requested capacity range. @@ -265,6 +295,44 @@ func (cs *controllerServer) DeleteVolume(ctx context.Context, req *csi.DeleteVol return &csi.DeleteVolumeResponse{}, nil } +// CreateSnapshot call blockstorage SnapshotVolume. +func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateSnapshotRequest) (*csi.CreateSnapshotResponse, error) { + klog.V(4).Infof("CreateSnapshot") + + volumeID := req.GetSourceVolumeId() + volume, err := cs.connector.GetVolumeByID(ctx, volumeID) + if errors.Is(err, cloud.ErrNotFound) { + return nil, status.Errorf(codes.NotFound, "Volume %v not found", volumeID) + } else if err != nil { + // Error with CloudStack + return nil, status.Errorf(codes.Internal, "Error %v", err) + } + snapshot, err := cs.connector.CreateSnapshot(ctx, volume.ID) + if err != nil { + return nil, status.Errorf(codes.Internal, "Failed to create snapshot %s: %v", snapshot.ID, err.Error()) + } + + t, err := time.Parse(time.RFC3339, snapshot.CreatedAt) + if err != nil { + panic(err) + } + + // Convert to Timestamp protobuf + ts := timestamppb.New(t) + + resp := &csi.CreateSnapshotResponse{ + Snapshot: &csi.Snapshot{ + SnapshotId: snapshot.ID, + SourceVolumeId: volume.ID, + CreationTime: ts, + ReadyToUse: true, + // We leave the optional SizeBytes field unset as the size of a block storage snapshot is the size of the difference to the volume or previous snapshots, k8s however expects the size to be the size of the restored volume. + }, + } + return resp, nil + +} + func (cs *controllerServer) ControllerPublishVolume(ctx context.Context, req *csi.ControllerPublishVolumeRequest) (*csi.ControllerPublishVolumeResponse, error) { logger := klog.FromContext(ctx) logger.V(6).Info("ControllerPublishVolume: called", "args", *req) @@ -557,6 +625,20 @@ func (cs *controllerServer) ControllerGetCapabilities(ctx context.Context, req * }, }, }, + &csi.ControllerServiceCapability{ + Type: &csi.ControllerServiceCapability_Rpc{ + Rpc: &csi.ControllerServiceCapability_RPC{ + Type: csi.ControllerServiceCapability_RPC_CREATE_DELETE_SNAPSHOT, + }, + }, + }, + &csi.ControllerServiceCapability{ + Type: &csi.ControllerServiceCapability_Rpc{ + Rpc: &csi.ControllerServiceCapability_RPC{ + Type: csi.ControllerServiceCapability_RPC_LIST_SNAPSHOTS, + }, + }, + }, }, } From 677055a2aa82997d1b077b25be9bd281e9a62766 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Wed, 9 Jul 2025 17:57:37 -0400 Subject: [PATCH 06/48] add support for deletesnaps & CRDs required for volume snapshot --- README.md | 15 + deploy/snapshot-cdrs.yaml | 955 ++++++++++++++++++++++++++++ examples/k8s/snapshot/pvc.yaml | 2 +- examples/k8s/snapshot/snapshot.yaml | 2 +- pkg/cloud/cloud.go | 1 + pkg/cloud/fake/fake.go | 7 +- pkg/cloud/snapshots.go | 7 +- pkg/driver/controller.go | 28 +- 8 files changed, 1010 insertions(+), 7 deletions(-) create mode 100644 deploy/snapshot-cdrs.yaml diff --git a/README.md b/README.md index ec26b4f..f0f7e77 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +**Fork Notice:** + +This repo is a fork of the Leaseweb's maitained cloudstack-csi-driver, which is in-turn a fork of Apalia's cloudstack-csi-driver + # CloudStack CSI Driver [![Go Reference](https://pkg.go.dev/badge/github.com/shapeblue/cloudstack-csi-driver.svg)](https://pkg.go.dev/github.com/shapeblue/cloudstack-csi-driver) @@ -83,6 +87,17 @@ disk offerings to Kubernetes storage classes. [More info...](./cmd/cloudstack-csi-sc-syncer/README.md) +> **Note:** The VolumeSnapshot CRDs (CustomResourceDefinitions) of version 8.3.0 are installed in this deployment. If you use a different version, please ensure compatibility with your Kubernetes cluster and CSI sidecars. + +// TODO: Ask Wei / Rohit - should we have the crds locally or manually install it from: + +``` +kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/v8.3.0/client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml +kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/v8.3.0/client/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml +kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/v8.3.0/client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml + +``` + ### Usage Example: diff --git a/deploy/snapshot-cdrs.yaml b/deploy/snapshot-cdrs.yaml new file mode 100644 index 0000000..ac37a92 --- /dev/null +++ b/deploy/snapshot-cdrs.yaml @@ -0,0 +1,955 @@ +## CRD for VolumeSnapshotClass from https://github.com/kubernetes-csi/external-snapshotter/blob/v8.3.0/client/config/crd/groupsnapshot.storage.k8s.io_volumegroupsnapshotclasses.yaml +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.kubernetes.io: "https://github.com/kubernetes-csi/external-snapshotter/pull/814" + controller-gen.kubebuilder.io/version: v0.15.0 + name: volumesnapshotclasses.snapshot.storage.k8s.io +spec: + group: snapshot.storage.k8s.io + names: + kind: VolumeSnapshotClass + listKind: VolumeSnapshotClassList + plural: volumesnapshotclasses + shortNames: + - vsclass + - vsclasses + singular: volumesnapshotclass + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .driver + name: Driver + type: string + - description: Determines whether a VolumeSnapshotContent created through the + VolumeSnapshotClass should be deleted when its bound VolumeSnapshot is deleted. + jsonPath: .deletionPolicy + name: DeletionPolicy + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + VolumeSnapshotClass specifies parameters that a underlying storage system uses when + creating a volume snapshot. A specific VolumeSnapshotClass is used by specifying its + name in a VolumeSnapshot object. + VolumeSnapshotClasses are non-namespaced + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + deletionPolicy: + description: |- + deletionPolicy determines whether a VolumeSnapshotContent created through + the VolumeSnapshotClass should be deleted when its bound VolumeSnapshot is deleted. + Supported values are "Retain" and "Delete". + "Retain" means that the VolumeSnapshotContent and its physical snapshot on underlying storage system are kept. + "Delete" means that the VolumeSnapshotContent and its physical snapshot on underlying storage system are deleted. + Required. + enum: + - Delete + - Retain + type: string + driver: + description: |- + driver is the name of the storage driver that handles this VolumeSnapshotClass. + Required. + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + parameters: + additionalProperties: + type: string + description: |- + parameters is a key-value map with storage driver specific parameters for creating snapshots. + These values are opaque to Kubernetes. + type: object + required: + - deletionPolicy + - driver + type: object + served: true + storage: true + subresources: {} + - additionalPrinterColumns: + - jsonPath: .driver + name: Driver + type: string + - description: Determines whether a VolumeSnapshotContent created through the VolumeSnapshotClass should be deleted when its bound VolumeSnapshot is deleted. + jsonPath: .deletionPolicy + name: DeletionPolicy + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + # This indicates the v1beta1 version of the custom resource is deprecated. + # API requests to this version receive a warning in the server response. + deprecated: true + # This overrides the default warning returned to clients making v1beta1 API requests. + deprecationWarning: "snapshot.storage.k8s.io/v1beta1 VolumeSnapshotClass is deprecated; use snapshot.storage.k8s.io/v1 VolumeSnapshotClass" + schema: + openAPIV3Schema: + description: VolumeSnapshotClass specifies parameters that a underlying storage system uses when creating a volume snapshot. A specific VolumeSnapshotClass is used by specifying its name in a VolumeSnapshot object. VolumeSnapshotClasses are non-namespaced + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + deletionPolicy: + description: deletionPolicy determines whether a VolumeSnapshotContent created through the VolumeSnapshotClass should be deleted when its bound VolumeSnapshot is deleted. Supported values are "Retain" and "Delete". "Retain" means that the VolumeSnapshotContent and its physical snapshot on underlying storage system are kept. "Delete" means that the VolumeSnapshotContent and its physical snapshot on underlying storage system are deleted. Required. + enum: + - Delete + - Retain + type: string + driver: + description: driver is the name of the storage driver that handles this VolumeSnapshotClass. Required. + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + parameters: + additionalProperties: + type: string + description: parameters is a key-value map with storage driver specific parameters for creating snapshots. These values are opaque to Kubernetes. + type: object + required: + - deletionPolicy + - driver + type: object + served: false + storage: false + subresources: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +## VolumeSnapshotContent CRD - https://github.com/kubernetes-csi/external-snapshotter/blob/v8.3.0/client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + api-approved.kubernetes.io: "https://github.com/kubernetes-csi/external-snapshotter/pull/955" + name: volumesnapshotcontents.snapshot.storage.k8s.io +spec: + group: snapshot.storage.k8s.io + names: + kind: VolumeSnapshotContent + listKind: VolumeSnapshotContentList + plural: volumesnapshotcontents + shortNames: + - vsc + - vscs + singular: volumesnapshotcontent + scope: Cluster + versions: + - additionalPrinterColumns: + - description: Indicates if the snapshot is ready to be used to restore a volume. + jsonPath: .status.readyToUse + name: ReadyToUse + type: boolean + - description: Represents the complete size of the snapshot in bytes + jsonPath: .status.restoreSize + name: RestoreSize + type: integer + - description: Determines whether this VolumeSnapshotContent and its physical + snapshot on the underlying storage system should be deleted when its bound + VolumeSnapshot is deleted. + jsonPath: .spec.deletionPolicy + name: DeletionPolicy + type: string + - description: Name of the CSI driver used to create the physical snapshot on + the underlying storage system. + jsonPath: .spec.driver + name: Driver + type: string + - description: Name of the VolumeSnapshotClass to which this snapshot belongs. + jsonPath: .spec.volumeSnapshotClassName + name: VolumeSnapshotClass + type: string + - description: Name of the VolumeSnapshot object to which this VolumeSnapshotContent + object is bound. + jsonPath: .spec.volumeSnapshotRef.name + name: VolumeSnapshot + type: string + - description: Namespace of the VolumeSnapshot object to which this VolumeSnapshotContent + object is bound. + jsonPath: .spec.volumeSnapshotRef.namespace + name: VolumeSnapshotNamespace + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + VolumeSnapshotContent represents the actual "on-disk" snapshot object in the + underlying storage system + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + spec defines properties of a VolumeSnapshotContent created by the underlying storage system. + Required. + properties: + deletionPolicy: + description: |- + deletionPolicy determines whether this VolumeSnapshotContent and its physical snapshot on + the underlying storage system should be deleted when its bound VolumeSnapshot is deleted. + Supported values are "Retain" and "Delete". + "Retain" means that the VolumeSnapshotContent and its physical snapshot on underlying storage system are kept. + "Delete" means that the VolumeSnapshotContent and its physical snapshot on underlying storage system are deleted. + For dynamically provisioned snapshots, this field will automatically be filled in by the + CSI snapshotter sidecar with the "DeletionPolicy" field defined in the corresponding + VolumeSnapshotClass. + For pre-existing snapshots, users MUST specify this field when creating the + VolumeSnapshotContent object. + Required. + enum: + - Delete + - Retain + type: string + driver: + description: |- + driver is the name of the CSI driver used to create the physical snapshot on + the underlying storage system. + This MUST be the same as the name returned by the CSI GetPluginName() call for + that driver. + Required. + type: string + source: + description: |- + source specifies whether the snapshot is (or should be) dynamically provisioned + or already exists, and just requires a Kubernetes object representation. + This field is immutable after creation. + Required. + properties: + snapshotHandle: + description: |- + snapshotHandle specifies the CSI "snapshot_id" of a pre-existing snapshot on + the underlying storage system for which a Kubernetes object representation + was (or should be) created. + This field is immutable. + type: string + x-kubernetes-validations: + - message: snapshotHandle is immutable + rule: self == oldSelf + volumeHandle: + description: |- + volumeHandle specifies the CSI "volume_id" of the volume from which a snapshot + should be dynamically taken from. + This field is immutable. + type: string + x-kubernetes-validations: + - message: volumeHandle is immutable + rule: self == oldSelf + type: object + x-kubernetes-validations: + - message: volumeHandle is required once set + rule: '!has(oldSelf.volumeHandle) || has(self.volumeHandle)' + - message: snapshotHandle is required once set + rule: '!has(oldSelf.snapshotHandle) || has(self.snapshotHandle)' + - message: exactly one of volumeHandle and snapshotHandle must be + set + rule: (has(self.volumeHandle) && !has(self.snapshotHandle)) || (!has(self.volumeHandle) + && has(self.snapshotHandle)) + sourceVolumeMode: + description: |- + SourceVolumeMode is the mode of the volume whose snapshot is taken. + Can be either “Filesystem” or “Block”. + If not specified, it indicates the source volume's mode is unknown. + This field is immutable. + This field is an alpha field. + type: string + x-kubernetes-validations: + - message: sourceVolumeMode is immutable + rule: self == oldSelf + volumeSnapshotClassName: + description: |- + name of the VolumeSnapshotClass from which this snapshot was (or will be) + created. + Note that after provisioning, the VolumeSnapshotClass may be deleted or + recreated with different set of values, and as such, should not be referenced + post-snapshot creation. + type: string + volumeSnapshotRef: + description: |- + volumeSnapshotRef specifies the VolumeSnapshot object to which this + VolumeSnapshotContent object is bound. + VolumeSnapshot.Spec.VolumeSnapshotContentName field must reference to + this VolumeSnapshotContent's name for the bidirectional binding to be valid. + For a pre-existing VolumeSnapshotContent object, name and namespace of the + VolumeSnapshot object MUST be provided for binding to happen. + This field is immutable after creation. + Required. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + TODO: this design is not final and this field is subject to change in the future. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + x-kubernetes-validations: + - message: both spec.volumeSnapshotRef.name and spec.volumeSnapshotRef.namespace + must be set + rule: has(self.name) && has(self.__namespace__) + required: + - deletionPolicy + - driver + - source + - volumeSnapshotRef + type: object + x-kubernetes-validations: + - message: sourceVolumeMode is required once set + rule: '!has(oldSelf.sourceVolumeMode) || has(self.sourceVolumeMode)' + status: + description: status represents the current information of a snapshot. + properties: + creationTime: + description: |- + creationTime is the timestamp when the point-in-time snapshot is taken + by the underlying storage system. + In dynamic snapshot creation case, this field will be filled in by the + CSI snapshotter sidecar with the "creation_time" value returned from CSI + "CreateSnapshot" gRPC call. + For a pre-existing snapshot, this field will be filled with the "creation_time" + value returned from the CSI "ListSnapshots" gRPC call if the driver supports it. + If not specified, it indicates the creation time is unknown. + The format of this field is a Unix nanoseconds time encoded as an int64. + On Unix, the command `date +%s%N` returns the current time in nanoseconds + since 1970-01-01 00:00:00 UTC. + format: int64 + type: integer + error: + description: |- + error is the last observed error during snapshot creation, if any. + Upon success after retry, this error field will be cleared. + properties: + message: + description: |- + message is a string detailing the encountered error during snapshot + creation if specified. + NOTE: message may be logged, and it should not contain sensitive + information. + type: string + time: + description: time is the timestamp when the error was encountered. + format: date-time + type: string + type: object + readyToUse: + description: |- + readyToUse indicates if a snapshot is ready to be used to restore a volume. + In dynamic snapshot creation case, this field will be filled in by the + CSI snapshotter sidecar with the "ready_to_use" value returned from CSI + "CreateSnapshot" gRPC call. + For a pre-existing snapshot, this field will be filled with the "ready_to_use" + value returned from the CSI "ListSnapshots" gRPC call if the driver supports it, + otherwise, this field will be set to "True". + If not specified, it means the readiness of a snapshot is unknown. + type: boolean + restoreSize: + description: |- + restoreSize represents the complete size of the snapshot in bytes. + In dynamic snapshot creation case, this field will be filled in by the + CSI snapshotter sidecar with the "size_bytes" value returned from CSI + "CreateSnapshot" gRPC call. + For a pre-existing snapshot, this field will be filled with the "size_bytes" + value returned from the CSI "ListSnapshots" gRPC call if the driver supports it. + When restoring a volume from this snapshot, the size of the volume MUST NOT + be smaller than the restoreSize if it is specified, otherwise the restoration will fail. + If not specified, it indicates that the size is unknown. + format: int64 + minimum: 0 + type: integer + snapshotHandle: + description: |- + snapshotHandle is the CSI "snapshot_id" of a snapshot on the underlying storage system. + If not specified, it indicates that dynamic snapshot creation has either failed + or it is still in progress. + type: string + volumeGroupSnapshotHandle: + description: |- + VolumeGroupSnapshotHandle is the CSI "group_snapshot_id" of a group snapshot + on the underlying storage system. + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} + - additionalPrinterColumns: + - description: Indicates if the snapshot is ready to be used to restore a volume. + jsonPath: .status.readyToUse + name: ReadyToUse + type: boolean + - description: Represents the complete size of the snapshot in bytes + jsonPath: .status.restoreSize + name: RestoreSize + type: integer + - description: Determines whether this VolumeSnapshotContent and its physical snapshot on the underlying storage system should be deleted when its bound VolumeSnapshot is deleted. + jsonPath: .spec.deletionPolicy + name: DeletionPolicy + type: string + - description: Name of the CSI driver used to create the physical snapshot on the underlying storage system. + jsonPath: .spec.driver + name: Driver + type: string + - description: Name of the VolumeSnapshotClass to which this snapshot belongs. + jsonPath: .spec.volumeSnapshotClassName + name: VolumeSnapshotClass + type: string + - description: Name of the VolumeSnapshot object to which this VolumeSnapshotContent object is bound. + jsonPath: .spec.volumeSnapshotRef.name + name: VolumeSnapshot + type: string + - description: Namespace of the VolumeSnapshot object to which this VolumeSnapshotContent object is bound. + jsonPath: .spec.volumeSnapshotRef.namespace + name: VolumeSnapshotNamespace + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + # This indicates the v1beta1 version of the custom resource is deprecated. + # API requests to this version receive a warning in the server response. + deprecated: true + # This overrides the default warning returned to clients making v1beta1 API requests. + deprecationWarning: "snapshot.storage.k8s.io/v1beta1 VolumeSnapshotContent is deprecated; use snapshot.storage.k8s.io/v1 VolumeSnapshotContent" + schema: + openAPIV3Schema: + description: VolumeSnapshotContent represents the actual "on-disk" snapshot object in the underlying storage system + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + spec: + description: spec defines properties of a VolumeSnapshotContent created by the underlying storage system. Required. + properties: + deletionPolicy: + description: deletionPolicy determines whether this VolumeSnapshotContent and its physical snapshot on the underlying storage system should be deleted when its bound VolumeSnapshot is deleted. Supported values are "Retain" and "Delete". "Retain" means that the VolumeSnapshotContent and its physical snapshot on underlying storage system are kept. "Delete" means that the VolumeSnapshotContent and its physical snapshot on underlying storage system are deleted. For dynamically provisioned snapshots, this field will automatically be filled in by the CSI snapshotter sidecar with the "DeletionPolicy" field defined in the corresponding VolumeSnapshotClass. For pre-existing snapshots, users MUST specify this field when creating the VolumeSnapshotContent object. Required. + enum: + - Delete + - Retain + type: string + driver: + description: driver is the name of the CSI driver used to create the physical snapshot on the underlying storage system. This MUST be the same as the name returned by the CSI GetPluginName() call for that driver. Required. + type: string + source: + description: source specifies whether the snapshot is (or should be) dynamically provisioned or already exists, and just requires a Kubernetes object representation. This field is immutable after creation. Required. + properties: + snapshotHandle: + description: snapshotHandle specifies the CSI "snapshot_id" of a pre-existing snapshot on the underlying storage system for which a Kubernetes object representation was (or should be) created. This field is immutable. + type: string + volumeHandle: + description: volumeHandle specifies the CSI "volume_id" of the volume from which a snapshot should be dynamically taken from. This field is immutable. + type: string + type: object + volumeSnapshotClassName: + description: name of the VolumeSnapshotClass from which this snapshot was (or will be) created. Note that after provisioning, the VolumeSnapshotClass may be deleted or recreated with different set of values, and as such, should not be referenced post-snapshot creation. + type: string + volumeSnapshotRef: + description: volumeSnapshotRef specifies the VolumeSnapshot object to which this VolumeSnapshotContent object is bound. VolumeSnapshot.Spec.VolumeSnapshotContentName field must reference to this VolumeSnapshotContent's name for the bidirectional binding to be valid. For a pre-existing VolumeSnapshotContent object, name and namespace of the VolumeSnapshot object MUST be provided for binding to happen. This field is immutable after creation. Required. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. For example, if the object reference is to a container within a pod, this would take on a value like: "spec.containers{name}" (where "name" refers to the name of the container that triggered the event) or if no container name is specified "spec.containers[2]" (container with index 2 in this pod). This syntax is chosen only to have some well-defined way of referencing a part of an object. TODO: this design is not final and this field is subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + required: + - deletionPolicy + - driver + - source + - volumeSnapshotRef + type: object + status: + description: status represents the current information of a snapshot. + properties: + creationTime: + description: creationTime is the timestamp when the point-in-time snapshot is taken by the underlying storage system. In dynamic snapshot creation case, this field will be filled in by the CSI snapshotter sidecar with the "creation_time" value returned from CSI "CreateSnapshot" gRPC call. For a pre-existing snapshot, this field will be filled with the "creation_time" value returned from the CSI "ListSnapshots" gRPC call if the driver supports it. If not specified, it indicates the creation time is unknown. The format of this field is a Unix nanoseconds time encoded as an int64. On Unix, the command `date +%s%N` returns the current time in nanoseconds since 1970-01-01 00:00:00 UTC. + format: int64 + type: integer + error: + description: error is the last observed error during snapshot creation, if any. Upon success after retry, this error field will be cleared. + properties: + message: + description: 'message is a string detailing the encountered error during snapshot creation if specified. NOTE: message may be logged, and it should not contain sensitive information.' + type: string + time: + description: time is the timestamp when the error was encountered. + format: date-time + type: string + type: object + readyToUse: + description: readyToUse indicates if a snapshot is ready to be used to restore a volume. In dynamic snapshot creation case, this field will be filled in by the CSI snapshotter sidecar with the "ready_to_use" value returned from CSI "CreateSnapshot" gRPC call. For a pre-existing snapshot, this field will be filled with the "ready_to_use" value returned from the CSI "ListSnapshots" gRPC call if the driver supports it, otherwise, this field will be set to "True". If not specified, it means the readiness of a snapshot is unknown. + type: boolean + restoreSize: + description: restoreSize represents the complete size of the snapshot in bytes. In dynamic snapshot creation case, this field will be filled in by the CSI snapshotter sidecar with the "size_bytes" value returned from CSI "CreateSnapshot" gRPC call. For a pre-existing snapshot, this field will be filled with the "size_bytes" value returned from the CSI "ListSnapshots" gRPC call if the driver supports it. When restoring a volume from this snapshot, the size of the volume MUST NOT be smaller than the restoreSize if it is specified, otherwise the restoration will fail. If not specified, it indicates that the size is unknown. + format: int64 + minimum: 0 + type: integer + snapshotHandle: + description: snapshotHandle is the CSI "snapshot_id" of a snapshot on the underlying storage system. If not specified, it indicates that dynamic snapshot creation has either failed or it is still in progress. + type: string + type: object + required: + - spec + type: object + served: false + storage: false + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +## VolumeSnapshot CRD - https://github.com/kubernetes-csi/external-snapshotter/blob/v8.3.0/client/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + api-approved.kubernetes.io: "https://github.com/kubernetes-csi/external-snapshotter/pull/814" + name: volumesnapshots.snapshot.storage.k8s.io +spec: + group: snapshot.storage.k8s.io + names: + kind: VolumeSnapshot + listKind: VolumeSnapshotList + plural: volumesnapshots + shortNames: + - vs + singular: volumesnapshot + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Indicates if the snapshot is ready to be used to restore a volume. + jsonPath: .status.readyToUse + name: ReadyToUse + type: boolean + - description: If a new snapshot needs to be created, this contains the name of + the source PVC from which this snapshot was (or will be) created. + jsonPath: .spec.source.persistentVolumeClaimName + name: SourcePVC + type: string + - description: If a snapshot already exists, this contains the name of the existing + VolumeSnapshotContent object representing the existing snapshot. + jsonPath: .spec.source.volumeSnapshotContentName + name: SourceSnapshotContent + type: string + - description: Represents the minimum size of volume required to rehydrate from + this snapshot. + jsonPath: .status.restoreSize + name: RestoreSize + type: string + - description: The name of the VolumeSnapshotClass requested by the VolumeSnapshot. + jsonPath: .spec.volumeSnapshotClassName + name: SnapshotClass + type: string + - description: Name of the VolumeSnapshotContent object to which the VolumeSnapshot + object intends to bind to. Please note that verification of binding actually + requires checking both VolumeSnapshot and VolumeSnapshotContent to ensure + both are pointing at each other. Binding MUST be verified prior to usage of + this object. + jsonPath: .status.boundVolumeSnapshotContentName + name: SnapshotContent + type: string + - description: Timestamp when the point-in-time snapshot was taken by the underlying + storage system. + jsonPath: .status.creationTime + name: CreationTime + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + VolumeSnapshot is a user's request for either creating a point-in-time + snapshot of a persistent volume, or binding to a pre-existing snapshot. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + spec defines the desired characteristics of a snapshot requested by a user. + More info: https://kubernetes.io/docs/concepts/storage/volume-snapshots#volumesnapshots + Required. + properties: + source: + description: |- + source specifies where a snapshot will be created from. + This field is immutable after creation. + Required. + properties: + persistentVolumeClaimName: + description: |- + persistentVolumeClaimName specifies the name of the PersistentVolumeClaim + object representing the volume from which a snapshot should be created. + This PVC is assumed to be in the same namespace as the VolumeSnapshot + object. + This field should be set if the snapshot does not exists, and needs to be + created. + This field is immutable. + type: string + x-kubernetes-validations: + - message: persistentVolumeClaimName is immutable + rule: self == oldSelf + volumeSnapshotContentName: + description: |- + volumeSnapshotContentName specifies the name of a pre-existing VolumeSnapshotContent + object representing an existing volume snapshot. + This field should be set if the snapshot already exists and only needs a representation in Kubernetes. + This field is immutable. + type: string + x-kubernetes-validations: + - message: volumeSnapshotContentName is immutable + rule: self == oldSelf + type: object + x-kubernetes-validations: + - message: persistentVolumeClaimName is required once set + rule: '!has(oldSelf.persistentVolumeClaimName) || has(self.persistentVolumeClaimName)' + - message: volumeSnapshotContentName is required once set + rule: '!has(oldSelf.volumeSnapshotContentName) || has(self.volumeSnapshotContentName)' + - message: exactly one of volumeSnapshotContentName and persistentVolumeClaimName + must be set + rule: (has(self.volumeSnapshotContentName) && !has(self.persistentVolumeClaimName)) + || (!has(self.volumeSnapshotContentName) && has(self.persistentVolumeClaimName)) + volumeSnapshotClassName: + description: |- + VolumeSnapshotClassName is the name of the VolumeSnapshotClass + requested by the VolumeSnapshot. + VolumeSnapshotClassName may be left nil to indicate that the default + SnapshotClass should be used. + A given cluster may have multiple default Volume SnapshotClasses: one + default per CSI Driver. If a VolumeSnapshot does not specify a SnapshotClass, + VolumeSnapshotSource will be checked to figure out what the associated + CSI Driver is, and the default VolumeSnapshotClass associated with that + CSI Driver will be used. If more than one VolumeSnapshotClass exist for + a given CSI Driver and more than one have been marked as default, + CreateSnapshot will fail and generate an event. + Empty string is not allowed for this field. + type: string + x-kubernetes-validations: + - message: volumeSnapshotClassName must not be the empty string when + set + rule: size(self) > 0 + required: + - source + type: object + status: + description: |- + status represents the current information of a snapshot. + Consumers must verify binding between VolumeSnapshot and + VolumeSnapshotContent objects is successful (by validating that both + VolumeSnapshot and VolumeSnapshotContent point at each other) before + using this object. + properties: + boundVolumeSnapshotContentName: + description: |- + boundVolumeSnapshotContentName is the name of the VolumeSnapshotContent + object to which this VolumeSnapshot object intends to bind to. + If not specified, it indicates that the VolumeSnapshot object has not been + successfully bound to a VolumeSnapshotContent object yet. + NOTE: To avoid possible security issues, consumers must verify binding between + VolumeSnapshot and VolumeSnapshotContent objects is successful (by validating that + both VolumeSnapshot and VolumeSnapshotContent point at each other) before using + this object. + type: string + creationTime: + description: |- + creationTime is the timestamp when the point-in-time snapshot is taken + by the underlying storage system. + In dynamic snapshot creation case, this field will be filled in by the + snapshot controller with the "creation_time" value returned from CSI + "CreateSnapshot" gRPC call. + For a pre-existing snapshot, this field will be filled with the "creation_time" + value returned from the CSI "ListSnapshots" gRPC call if the driver supports it. + If not specified, it may indicate that the creation time of the snapshot is unknown. + format: date-time + type: string + error: + description: |- + error is the last observed error during snapshot creation, if any. + This field could be helpful to upper level controllers(i.e., application controller) + to decide whether they should continue on waiting for the snapshot to be created + based on the type of error reported. + The snapshot controller will keep retrying when an error occurs during the + snapshot creation. Upon success, this error field will be cleared. + properties: + message: + description: |- + message is a string detailing the encountered error during snapshot + creation if specified. + NOTE: message may be logged, and it should not contain sensitive + information. + type: string + time: + description: time is the timestamp when the error was encountered. + format: date-time + type: string + type: object + readyToUse: + description: |- + readyToUse indicates if the snapshot is ready to be used to restore a volume. + In dynamic snapshot creation case, this field will be filled in by the + snapshot controller with the "ready_to_use" value returned from CSI + "CreateSnapshot" gRPC call. + For a pre-existing snapshot, this field will be filled with the "ready_to_use" + value returned from the CSI "ListSnapshots" gRPC call if the driver supports it, + otherwise, this field will be set to "True". + If not specified, it means the readiness of a snapshot is unknown. + type: boolean + restoreSize: + type: string + description: |- + restoreSize represents the minimum size of volume required to create a volume + from this snapshot. + In dynamic snapshot creation case, this field will be filled in by the + snapshot controller with the "size_bytes" value returned from CSI + "CreateSnapshot" gRPC call. + For a pre-existing snapshot, this field will be filled with the "size_bytes" + value returned from the CSI "ListSnapshots" gRPC call if the driver supports it. + When restoring a volume from this snapshot, the size of the volume MUST NOT + be smaller than the restoreSize if it is specified, otherwise the restoration will fail. + If not specified, it indicates that the size is unknown. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + volumeGroupSnapshotName: + description: |- + VolumeGroupSnapshotName is the name of the VolumeGroupSnapshot of which this + VolumeSnapshot is a part of. + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} + - additionalPrinterColumns: + - description: Indicates if the snapshot is ready to be used to restore a volume. + jsonPath: .status.readyToUse + name: ReadyToUse + type: boolean + - description: If a new snapshot needs to be created, this contains the name of the source PVC from which this snapshot was (or will be) created. + jsonPath: .spec.source.persistentVolumeClaimName + name: SourcePVC + type: string + - description: If a snapshot already exists, this contains the name of the existing VolumeSnapshotContent object representing the existing snapshot. + jsonPath: .spec.source.volumeSnapshotContentName + name: SourceSnapshotContent + type: string + - description: Represents the minimum size of volume required to rehydrate from this snapshot. + jsonPath: .status.restoreSize + name: RestoreSize + type: string + - description: The name of the VolumeSnapshotClass requested by the VolumeSnapshot. + jsonPath: .spec.volumeSnapshotClassName + name: SnapshotClass + type: string + - description: Name of the VolumeSnapshotContent object to which the VolumeSnapshot object intends to bind to. Please note that verification of binding actually requires checking both VolumeSnapshot and VolumeSnapshotContent to ensure both are pointing at each other. Binding MUST be verified prior to usage of this object. + jsonPath: .status.boundVolumeSnapshotContentName + name: SnapshotContent + type: string + - description: Timestamp when the point-in-time snapshot was taken by the underlying storage system. + jsonPath: .status.creationTime + name: CreationTime + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + # This indicates the v1beta1 version of the custom resource is deprecated. + # API requests to this version receive a warning in the server response. + deprecated: true + # This overrides the default warning returned to clients making v1beta1 API requests. + deprecationWarning: "snapshot.storage.k8s.io/v1beta1 VolumeSnapshot is deprecated; use snapshot.storage.k8s.io/v1 VolumeSnapshot" + schema: + openAPIV3Schema: + description: VolumeSnapshot is a user's request for either creating a point-in-time snapshot of a persistent volume, or binding to a pre-existing snapshot. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + spec: + description: 'spec defines the desired characteristics of a snapshot requested by a user. More info: https://kubernetes.io/docs/concepts/storage/volume-snapshots#volumesnapshots Required.' + properties: + source: + description: source specifies where a snapshot will be created from. This field is immutable after creation. Required. + properties: + persistentVolumeClaimName: + description: persistentVolumeClaimName specifies the name of the PersistentVolumeClaim object representing the volume from which a snapshot should be created. This PVC is assumed to be in the same namespace as the VolumeSnapshot object. This field should be set if the snapshot does not exists, and needs to be created. This field is immutable. + type: string + volumeSnapshotContentName: + description: volumeSnapshotContentName specifies the name of a pre-existing VolumeSnapshotContent object representing an existing volume snapshot. This field should be set if the snapshot already exists and only needs a representation in Kubernetes. This field is immutable. + type: string + type: object + volumeSnapshotClassName: + description: 'VolumeSnapshotClassName is the name of the VolumeSnapshotClass requested by the VolumeSnapshot. VolumeSnapshotClassName may be left nil to indicate that the default SnapshotClass should be used. A given cluster may have multiple default Volume SnapshotClasses: one default per CSI Driver. If a VolumeSnapshot does not specify a SnapshotClass, VolumeSnapshotSource will be checked to figure out what the associated CSI Driver is, and the default VolumeSnapshotClass associated with that CSI Driver will be used. If more than one VolumeSnapshotClass exist for a given CSI Driver and more than one have been marked as default, CreateSnapshot will fail and generate an event. Empty string is not allowed for this field.' + type: string + required: + - source + type: object + status: + description: status represents the current information of a snapshot. Consumers must verify binding between VolumeSnapshot and VolumeSnapshotContent objects is successful (by validating that both VolumeSnapshot and VolumeSnapshotContent point at each other) before using this object. + properties: + boundVolumeSnapshotContentName: + description: 'boundVolumeSnapshotContentName is the name of the VolumeSnapshotContent object to which this VolumeSnapshot object intends to bind to. If not specified, it indicates that the VolumeSnapshot object has not been successfully bound to a VolumeSnapshotContent object yet. NOTE: To avoid possible security issues, consumers must verify binding between VolumeSnapshot and VolumeSnapshotContent objects is successful (by validating that both VolumeSnapshot and VolumeSnapshotContent point at each other) before using this object.' + type: string + creationTime: + description: creationTime is the timestamp when the point-in-time snapshot is taken by the underlying storage system. In dynamic snapshot creation case, this field will be filled in by the snapshot controller with the "creation_time" value returned from CSI "CreateSnapshot" gRPC call. For a pre-existing snapshot, this field will be filled with the "creation_time" value returned from the CSI "ListSnapshots" gRPC call if the driver supports it. If not specified, it may indicate that the creation time of the snapshot is unknown. + format: date-time + type: string + error: + description: error is the last observed error during snapshot creation, if any. This field could be helpful to upper level controllers(i.e., application controller) to decide whether they should continue on waiting for the snapshot to be created based on the type of error reported. The snapshot controller will keep retrying when an error occurs during the snapshot creation. Upon success, this error field will be cleared. + properties: + message: + description: 'message is a string detailing the encountered error during snapshot creation if specified. NOTE: message may be logged, and it should not contain sensitive information.' + type: string + time: + description: time is the timestamp when the error was encountered. + format: date-time + type: string + type: object + readyToUse: + description: readyToUse indicates if the snapshot is ready to be used to restore a volume. In dynamic snapshot creation case, this field will be filled in by the snapshot controller with the "ready_to_use" value returned from CSI "CreateSnapshot" gRPC call. For a pre-existing snapshot, this field will be filled with the "ready_to_use" value returned from the CSI "ListSnapshots" gRPC call if the driver supports it, otherwise, this field will be set to "True". If not specified, it means the readiness of a snapshot is unknown. + type: boolean + restoreSize: + type: string + description: restoreSize represents the minimum size of volume required to create a volume from this snapshot. In dynamic snapshot creation case, this field will be filled in by the snapshot controller with the "size_bytes" value returned from CSI "CreateSnapshot" gRPC call. For a pre-existing snapshot, this field will be filled with the "size_bytes" value returned from the CSI "ListSnapshots" gRPC call if the driver supports it. When restoring a volume from this snapshot, the size of the volume MUST NOT be smaller than the restoreSize if it is specified, otherwise the restoration will fail. If not specified, it indicates that the size is unknown. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + required: + - spec + type: object + served: false + storage: false + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] + diff --git a/examples/k8s/snapshot/pvc.yaml b/examples/k8s/snapshot/pvc.yaml index 415951a..0b42906 100644 --- a/examples/k8s/snapshot/pvc.yaml +++ b/examples/k8s/snapshot/pvc.yaml @@ -8,4 +8,4 @@ spec: resources: requests: storage: 10Gi - storageClassName: cloudstack-custom \ No newline at end of file + storageClassName: cloudstack-custom diff --git a/examples/k8s/snapshot/snapshot.yaml b/examples/k8s/snapshot/snapshot.yaml index e3854ca..e66640d 100644 --- a/examples/k8s/snapshot/snapshot.yaml +++ b/examples/k8s/snapshot/snapshot.yaml @@ -5,4 +5,4 @@ metadata: spec: volumeSnapshotClassName: cloudstack-snapshot source: - persistentVolumeClaimName: my-pvc \ No newline at end of file + persistentVolumeClaimName: my-pvc diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go index 349dd3a..2e057bb 100644 --- a/pkg/cloud/cloud.go +++ b/pkg/cloud/cloud.go @@ -25,6 +25,7 @@ type Interface interface { ExpandVolume(ctx context.Context, volumeID string, newSizeInGB int64) error CreateVolumeFromSnapshot(ctx context.Context, diskOfferingID, zoneID, name, domainID, projectID, snapshotID string, sizeInGB int64) (string, error) + GetSnapshotByID(ctx context.Context, snapshotID ...string) (*Snapshot, error) CreateSnapshot(ctx context.Context, volumeID string) (*Snapshot, error) DeleteSnapshot(ctx context.Context, snapshotID string) error } diff --git a/pkg/cloud/fake/fake.go b/pkg/cloud/fake/fake.go index f20e5c6..16d9c41 100644 --- a/pkg/cloud/fake/fake.go +++ b/pkg/cloud/fake/fake.go @@ -12,6 +12,7 @@ import ( ) const zoneID = "a1887604-237c-4212-a9cd-94620b7880fa" +const snapshotID = "9d076136-657b-4c84-b279-455da3ea484c" type fakeConnector struct { node *cloud.VM @@ -41,7 +42,7 @@ func New() cloud.Interface { ID: "9d076136-657b-4c84-b279-455da3ea484c", Name: "pvc-vol-snap-1", DomainID: "51f0fcb5-db16-4637-94f5-30131010214f", - ZoneID: "bdab539f-651e-431a-979d-5d3c48b54fcf", + ZoneID: zoneID, VolumeID: "4f1f610d-6f17-4ff9-9228-e4062af93e54", CreatedAt: "2025-07-07 16:13:06", } @@ -140,6 +141,10 @@ func (f *fakeConnector) CreateVolumeFromSnapshot(ctx context.Context, diskOfferi return "1", nil } +func (f *fakeConnector) GetSnapshotByID(ctx context.Context, snapshotID ...string) (*cloud.Snapshot, error) { + return f.snapshot, nil +} + func (f *fakeConnector) CreateSnapshot(ctx context.Context, volumeID string) (*cloud.Snapshot, error) { return f.snapshot, nil } diff --git a/pkg/cloud/snapshots.go b/pkg/cloud/snapshots.go index 36458a1..73002a6 100644 --- a/pkg/cloud/snapshots.go +++ b/pkg/cloud/snapshots.go @@ -4,12 +4,15 @@ import ( "context" "strings" - "github.com/apache/cloudstack-go/v2/cloudstack" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) -func (c *client) ListSnapshots(p *cloudstack.ListSnapshotsParams) (*Snapshot, error) { +func (c *client) GetSnapshotByID(ctx context.Context, snapshotID ...string) (*Snapshot, error) { + p := c.Snapshot.NewListSnapshotsParams() + if snapshotID != nil { + p.SetId(snapshotID[0]) + } l, err := c.Snapshot.ListSnapshots(p) if err != nil { return nil, err diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index 03b35fa..0c97761 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -295,7 +295,6 @@ func (cs *controllerServer) DeleteVolume(ctx context.Context, req *csi.DeleteVol return &csi.DeleteVolumeResponse{}, nil } -// CreateSnapshot call blockstorage SnapshotVolume. func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateSnapshotRequest) (*csi.CreateSnapshotResponse, error) { klog.V(4).Infof("CreateSnapshot") @@ -312,7 +311,7 @@ func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS return nil, status.Errorf(codes.Internal, "Failed to create snapshot %s: %v", snapshot.ID, err.Error()) } - t, err := time.Parse(time.RFC3339, snapshot.CreatedAt) + t, err := time.Parse("2006-01-02T15:04:05-0700", snapshot.CreatedAt) if err != nil { panic(err) } @@ -333,6 +332,31 @@ func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS } +func (cs *controllerServer) DeleteSnapshot(ctx context.Context, req *csi.DeleteSnapshotRequest) (*csi.DeleteSnapshotResponse, error) { + klog.V(4).Infof("DeleteSnapshot") + + snapshotID := req.GetSnapshotId() + + if snapshotID == "" { + return nil, status.Error(codes.InvalidArgument, "Snapshot ID missing in request") + } + + snapshot, err := cs.connector.GetSnapshotByID(ctx, snapshotID) + if errors.Is(err, cloud.ErrNotFound) { + return nil, status.Errorf(codes.NotFound, "Snapshot %v not found", snapshotID) + } else if err != nil { + // Error with CloudStack + return nil, status.Errorf(codes.Internal, "Error %v", err) + } + + err = cs.connector.DeleteSnapshot(ctx, snapshot.ID) + if err != nil && !errors.Is(err, cloud.ErrNotFound) { + return nil, status.Errorf(codes.Internal, "Cannot delete snapshot %s: %s", snapshotID, err.Error()) + } + + return &csi.DeleteSnapshotResponse{}, nil +} + func (cs *controllerServer) ControllerPublishVolume(ctx context.Context, req *csi.ControllerPublishVolumeRequest) (*csi.ControllerPublishVolumeResponse, error) { logger := klog.FromContext(ctx) logger.V(6).Info("ControllerPublishVolume: called", "args", *req) From 88f4ef57afcd90e57b9d8bfcf066ce5641f66ad1 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 10 Jul 2025 11:16:20 -0400 Subject: [PATCH 07/48] update location of crd file --- deploy/{ => k8s}/snapshot-cdrs.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename deploy/{ => k8s}/snapshot-cdrs.yaml (100%) diff --git a/deploy/snapshot-cdrs.yaml b/deploy/k8s/snapshot-cdrs.yaml similarity index 100% rename from deploy/snapshot-cdrs.yaml rename to deploy/k8s/snapshot-cdrs.yaml From 4028d308c53f34289364d06f50fbe573a11c064a Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Mon, 21 Jul 2025 12:55:04 -0400 Subject: [PATCH 08/48] remove diskoffering id when creating volume from snapshot --- ...apshot-cdrs.yaml => 00-snapshot-crds.yaml} | 0 pkg/cloud/cloud.go | 2 +- pkg/cloud/fake/fake.go | 4 +- pkg/cloud/volumes.go | 21 ++++++-- pkg/driver/controller.go | 48 ++++++++++++++----- 5 files changed, 54 insertions(+), 21 deletions(-) rename deploy/k8s/{snapshot-cdrs.yaml => 00-snapshot-crds.yaml} (100%) diff --git a/deploy/k8s/snapshot-cdrs.yaml b/deploy/k8s/00-snapshot-crds.yaml similarity index 100% rename from deploy/k8s/snapshot-cdrs.yaml rename to deploy/k8s/00-snapshot-crds.yaml diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go index 2e057bb..4f2a255 100644 --- a/pkg/cloud/cloud.go +++ b/pkg/cloud/cloud.go @@ -24,7 +24,7 @@ type Interface interface { DetachVolume(ctx context.Context, volumeID string) error ExpandVolume(ctx context.Context, volumeID string, newSizeInGB int64) error - CreateVolumeFromSnapshot(ctx context.Context, diskOfferingID, zoneID, name, domainID, projectID, snapshotID string, sizeInGB int64) (string, error) + CreateVolumeFromSnapshot(ctx context.Context, zoneID, name, domainID, projectID, snapshotID string, sizeInGB int64) (*Volume, error) GetSnapshotByID(ctx context.Context, snapshotID ...string) (*Snapshot, error) CreateSnapshot(ctx context.Context, volumeID string) (*Snapshot, error) DeleteSnapshot(ctx context.Context, snapshotID string) error diff --git a/pkg/cloud/fake/fake.go b/pkg/cloud/fake/fake.go index 16d9c41..64e02e6 100644 --- a/pkg/cloud/fake/fake.go +++ b/pkg/cloud/fake/fake.go @@ -137,8 +137,8 @@ func (f *fakeConnector) ExpandVolume(_ context.Context, volumeID string, newSize return cloud.ErrNotFound } -func (f *fakeConnector) CreateVolumeFromSnapshot(ctx context.Context, diskOfferingID, zoneID, name, domainID, projectID, snapshotID string, sizeInGB int64) (string, error) { - return "1", nil +func (f *fakeConnector) CreateVolumeFromSnapshot(ctx context.Context, zoneID, name, domainID, projectID, snapshotID string, sizeInGB int64) (*cloud.Volume, error) { + return nil, nil } func (f *fakeConnector) GetSnapshotByID(ctx context.Context, snapshotID ...string) (*cloud.Snapshot, error) { diff --git a/pkg/cloud/volumes.go b/pkg/cloud/volumes.go index b0c1795..2ef29c0 100644 --- a/pkg/cloud/volumes.go +++ b/pkg/cloud/volumes.go @@ -156,15 +156,14 @@ func (c *client) ExpandVolume(ctx context.Context, volumeID string, newSizeInGB return nil } -func (c *client) CreateVolumeFromSnapshot(ctx context.Context, diskOfferingID, zoneID, name, domainID, projectID, snapshotID string, sizeInGB int64) (string, error) { +func (c *client) CreateVolumeFromSnapshot(ctx context.Context, zoneID, name, domainID, projectID, snapshotID string, sizeInGB int64) (*Volume, error) { logger := klog.FromContext(ctx) snapshot, _, err := c.Snapshot.GetSnapshotByID(snapshotID) if err != nil { - return "", fmt.Errorf("failed to retrieve snapshot '%s': %w", snapshotID, err) + return nil, fmt.Errorf("failed to retrieve snapshot '%s': %w", snapshotID, err) } p := c.Volume.NewCreateVolumeParams() - p.SetDiskofferingid(diskOfferingID) p.SetZoneid(zoneID) if projectID != "" { p.SetProjectid(projectID) @@ -181,8 +180,20 @@ func (c *client) CreateVolumeFromSnapshot(ctx context.Context, diskOfferingID, z vol, err := c.Volume.CreateVolume(p) if err != nil { // Handle the error accordingly - return "", fmt.Errorf("failed to create volume from snapshot'%s': %w", snapshotID, err) + return nil, fmt.Errorf("failed to create volume from snapshot'%s': %w", snapshotID, err) + } + + v := Volume{ + ID: vol.Id, + Name: vol.Name, + Size: vol.Size, + DiskOfferingID: vol.Diskofferingid, + DomainID: vol.Domainid, + ProjectID: vol.Projectid, + ZoneID: vol.Zoneid, + VirtualMachineID: vol.Virtualmachineid, + DeviceID: strconv.FormatInt(vol.Deviceid, 10), } - return vol.Id, err + return &v, nil } diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index 0c97761..019e469 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -2,6 +2,7 @@ package driver import ( "context" + "encoding/json" "errors" "fmt" "math/rand" @@ -118,34 +119,50 @@ func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol } } + // We have to create the volume. + + // Determine volume size using requested capacity range. + sizeInGB, err := determineSize(req) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if snapshotID != "" { logger.Info("Creating volume from snapshot", "snapshotID", snapshotID) // Call the cloud connector's CreateVolumeFromSnapshot if implemented - volID, err := cs.connector.CreateVolumeFromSnapshot(ctx, diskOfferingID, vol.ZoneID, name, vol.DomainID, vol.ProjectID, snapshotID, vol.Size) + printVolumeAsJSON(req) + snapshot, err := cs.connector.GetSnapshotByID(ctx, snapshotID) + if errors.Is(err, cloud.ErrNotFound) { + return nil, status.Errorf(codes.NotFound, "Snapshot %v not found", snapshotID) + } else if err != nil { + // Error with CloudStack + return nil, status.Errorf(codes.Internal, "Error %v", err) + } + logger.Info("Disk offering ID", "diskOfferingID: ", diskOfferingID) + logger.Info("Zone ID", "ZoneID", snapshot.ZoneID) + logger.Info("Name", "name", name) + logger.Info("Domain ID", "DomainID", snapshot.DomainID) + logger.Info("Project ID", "ProjectID", snapshot.ProjectID) + logger.Info("Snapshot ID", "snapshotID", snapshotID) + logger.Info("Volume size", "Size", sizeInGB) + volFromSnapshot, err := cs.connector.CreateVolumeFromSnapshot(ctx, snapshot.ZoneID, name, snapshot.DomainID, snapshot.ProjectID, snapshotID, sizeInGB) if err != nil { return nil, status.Errorf(codes.Internal, "Cannot create volume from snapshot %s: %v", snapshotID, err.Error()) } resp := &csi.CreateVolumeResponse{ Volume: &csi.Volume{ - VolumeId: volID, - CapacityBytes: vol.Size, + VolumeId: volFromSnapshot.ID, + CapacityBytes: volFromSnapshot.Size, VolumeContext: req.GetParameters(), + ContentSource: req.GetVolumeContentSource(), AccessibleTopology: []*csi.Topology{ - Topology{ZoneID: vol.ZoneID}.ToCSI(), + Topology{ZoneID: volFromSnapshot.ZoneID}.ToCSI(), }, }, } return resp, nil } - // We have to create the volume. - - // Determine volume size using requested capacity range. - sizeInGB, err := determineSize(req) - if err != nil { - return nil, status.Error(codes.InvalidArgument, err.Error()) - } - // Determine zone using topology constraints. var zoneID string topologyRequirement := req.GetAccessibilityRequirements() @@ -189,7 +206,7 @@ func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol VolumeId: volID, CapacityBytes: util.GigaBytesToBytes(sizeInGB), VolumeContext: req.GetParameters(), - // ContentSource: req.GetVolumeContentSource(), TODO: snapshot support. + ContentSource: req.GetVolumeContentSource(), AccessibleTopology: []*csi.Topology{ Topology{ZoneID: zoneID}.ToCSI(), }, @@ -199,6 +216,11 @@ func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol return resp, nil } +func printVolumeAsJSON(vol *csi.CreateVolumeRequest) { + b, _ := json.MarshalIndent(vol, "", " ") + fmt.Println(string(b)) +} + func checkVolumeSuitable(vol *cloud.Volume, diskOfferingID string, capRange *csi.CapacityRange, topologyRequirement *csi.TopologyRequirement, ) (bool, string) { From 35b3f46adee97da17abe4546eb556b35a93dbe9d Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Tue, 22 Jul 2025 10:47:10 -0400 Subject: [PATCH 09/48] fixes to allow restoration to volume created from snapshot --- examples/k8s/snapshot/restore-pod.yaml | 16 ++++++++++++++++ examples/k8s/snapshot/snapshot.yaml | 2 +- pkg/cloud/cloud.go | 1 + pkg/cloud/snapshots.go | 1 + pkg/cloud/volumes.go | 3 +++ pkg/driver/controller.go | 18 +++++++++++------- 6 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 examples/k8s/snapshot/restore-pod.yaml diff --git a/examples/k8s/snapshot/restore-pod.yaml b/examples/k8s/snapshot/restore-pod.yaml new file mode 100644 index 0000000..65d4462 --- /dev/null +++ b/examples/k8s/snapshot/restore-pod.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: restore-pod +spec: + containers: + - name: app + image: busybox + command: [ "sleep", "3600" ] + volumeMounts: + - mountPath: /data + name: data + volumes: + - name: data + persistentVolumeClaim: + claimName: snapshot-pvc-1 \ No newline at end of file diff --git a/examples/k8s/snapshot/snapshot.yaml b/examples/k8s/snapshot/snapshot.yaml index e66640d..0d71b43 100644 --- a/examples/k8s/snapshot/snapshot.yaml +++ b/examples/k8s/snapshot/snapshot.yaml @@ -5,4 +5,4 @@ metadata: spec: volumeSnapshotClassName: cloudstack-snapshot source: - persistentVolumeClaimName: my-pvc + persistentVolumeClaimName: example-pvc diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go index 4f2a255..e9dfd91 100644 --- a/pkg/cloud/cloud.go +++ b/pkg/cloud/cloud.go @@ -50,6 +50,7 @@ type Volume struct { type Snapshot struct { ID string Name string + Size int64 DomainID string ProjectID string diff --git a/pkg/cloud/snapshots.go b/pkg/cloud/snapshots.go index 73002a6..5e4365b 100644 --- a/pkg/cloud/snapshots.go +++ b/pkg/cloud/snapshots.go @@ -46,6 +46,7 @@ func (c *client) CreateSnapshot(ctx context.Context, volumeID string) (*Snapshot snap := Snapshot{ ID: snapshot.Id, Name: snapshot.Name, + Size: snapshot.Virtualsize, DomainID: snapshot.Domainid, ProjectID: snapshot.Projectid, ZoneID: snapshot.Zoneid, diff --git a/pkg/cloud/volumes.go b/pkg/cloud/volumes.go index 2ef29c0..3d8136c 100644 --- a/pkg/cloud/volumes.go +++ b/pkg/cloud/volumes.go @@ -169,12 +169,15 @@ func (c *client) CreateVolumeFromSnapshot(ctx context.Context, zoneID, name, dom p.SetProjectid(projectID) } p.SetName(name) + p.SetSize(sizeInGB) p.SetSnapshotid(snapshot.Id) logger.V(2).Info("CloudStack API call", "command", "CreateVolume", "params", map[string]string{ "name": name, + "size": strconv.FormatInt(sizeInGB, 10), "snapshotid": snapshotID, "projectid": projectID, + "zoneid": zoneID, }) // Execute the API call to create volume from snapshot vol, err := c.Volume.CreateVolume(p) diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index 019e469..e72734e 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -127,6 +127,8 @@ func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol return nil, status.Error(codes.InvalidArgument, err.Error()) } + // If creating from snapshot, get the snapshot size + var snapshotSizeGiB int64 if snapshotID != "" { logger.Info("Creating volume from snapshot", "snapshotID", snapshotID) // Call the cloud connector's CreateVolumeFromSnapshot if implemented @@ -138,17 +140,19 @@ func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol // Error with CloudStack return nil, status.Errorf(codes.Internal, "Error %v", err) } - logger.Info("Disk offering ID", "diskOfferingID: ", diskOfferingID) - logger.Info("Zone ID", "ZoneID", snapshot.ZoneID) - logger.Info("Name", "name", name) - logger.Info("Domain ID", "DomainID", snapshot.DomainID) - logger.Info("Project ID", "ProjectID", snapshot.ProjectID) - logger.Info("Snapshot ID", "snapshotID", snapshotID) - logger.Info("Volume size", "Size", sizeInGB) + + logger.Info("PVC created with", "size", sizeInGB) + snapshotSizeGiB = util.RoundUpBytesToGB(snapshot.Size) + if snapshotSizeGiB > sizeInGB { + logger.Info("Snapshot size is greater than the request PVC, creating volume from snapshot of size", "snapshot size:", snapshotSizeGiB) + sizeInGB = snapshotSizeGiB + } + volFromSnapshot, err := cs.connector.CreateVolumeFromSnapshot(ctx, snapshot.ZoneID, name, snapshot.DomainID, snapshot.ProjectID, snapshotID, sizeInGB) if err != nil { return nil, status.Errorf(codes.Internal, "Cannot create volume from snapshot %s: %v", snapshotID, err.Error()) } + resp := &csi.CreateVolumeResponse{ Volume: &csi.Volume{ VolumeId: volFromSnapshot.ID, From 5fe5e1ba344d3205699f49356bda0c7de80425de Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Wed, 23 Jul 2025 15:16:26 -0400 Subject: [PATCH 10/48] update readme --- README.md | 63 +++++++++++++++++++- deploy/k8s/volume-snapshot-class.yaml | 2 +- examples/k8s/snapshot/pvc-from-snapshot.yaml | 2 +- examples/k8s/snapshot/restore-pod.yaml | 2 +- 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f0f7e77..697afc1 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ disk offerings to Kubernetes storage classes. > **Note:** The VolumeSnapshot CRDs (CustomResourceDefinitions) of version 8.3.0 are installed in this deployment. If you use a different version, please ensure compatibility with your Kubernetes cluster and CSI sidecars. -// TODO: Ask Wei / Rohit - should we have the crds locally or manually install it from: +// TODO: Should we have the crds locally or manually install it from: ``` kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/v8.3.0/client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml @@ -121,6 +121,67 @@ To build the container images: make container ``` + +## Volume Snapshots +For Volume snapshots to be created, the following configurations need to be applied: + +``` +kubectl aplly -f 00-snapshot-crds.yaml # Installs the VolumeSnapshotClass, VolumeSnapshotContent and VolumeSnapshtot CRDs +volume-snapshot-class.yaml # Defines VolumeSnapshotClass for CloudStack CSI driver +``` + +Once the CRDs are installed, the snapshot can be taken by applying: +``` +kubectl apply ./examples/k8s/snapshot/snapshot.yaml +``` + +In order to take the snapshot of a volume, `persistentVolumeClaimName` should be set to the right PVC name that is bound to the volume whose snapshot is to be taken. + +You can check CloudStack volume snapshots if the snapshot was successfully created. If for any reason there was an issue, it can be investgated by checking the logs of the cloudstack-csi-controller pods: cloudstack-csi-controller, csi-snapshotter and snapshot-controller containers + +``` +kubectl logs -f -n kube-system # defaults to tailing logs of cloudstack-csi-controller +kubectl logs -f -n kube-system -c csi-snapshotter +kubectl logs -f -n kube-system -c snapshot-controller +``` + +To restore a volume snapshot: +1. Restore a snapshot and Use it in a pod +* Create a PVC from the snapshot - for example ./examples/k8s/snapshot/pvc-from-snapshot.yaml +* Apply the configuration: +``` +kubectl apply -f ./examples/k8s/snapshot/pvc-from-snapshot.yaml +``` +* Create a pod that uses the restored PVC; example pod config ./examples/k8s/snapshot/restore-pod.yaml +``` +kubectl apply -f ./examples/k8s/snapshot/restore-pod.yaml +``` +2. To restore a snapshot when using a deployment +Update the deployment to point to the restored PVC + +``` +spec: + volumes: + - name: app-volume + persistentVolumeClaim: + claimName: pvc-from-snapshot +``` + +### What happens when you restore a volume from a snapshot +* The CSI external-provisioner (a container in the cloudstack-csi-controller pod) sees the new PVC and notices it references a snapshot +* The CSI driver's `CreateVolume` method is called with a `VolumeContentSource` that contains the snapshot ID +* The CSI driver creates a new volume from the snapshot (using the CloudStack's createVolume API) +* The new volume is now available as a PV (persistent volume) and is bound to the new PVC +* The volume is NOT attached to any node just by restoring from a snapshot, the volume is only attached to a node when a Pod that uses the new PVC is scheduled on a node +* The CSI driver's `ControllerPublishVolume` and `NodePublishVolume` methods are called to attach and mount the volume to the node where the Pod is running + +Hence to debug any issues during restoring a snapshot, check the logs of the cloudstack-csi-controller, external-provisioner containers + +``` +kubectl logs -f -n kube-system # defaults to tailing logs of cloudstack-csi-controller +kubectl logs -f -n kube-system -c external-provisioner +``` + ## See also - [CloudStack Kubernetes Provider](https://github.com/apache/cloudstack-kubernetes-provider) - Kubernetes Cloud Controller Manager for Apache CloudStack diff --git a/deploy/k8s/volume-snapshot-class.yaml b/deploy/k8s/volume-snapshot-class.yaml index f4096fd..4d14a7d 100644 --- a/deploy/k8s/volume-snapshot-class.yaml +++ b/deploy/k8s/volume-snapshot-class.yaml @@ -3,4 +3,4 @@ kind: VolumeSnapshotClass metadata: name: cloudstack-snapshot driver: csi.cloudstack.apache.org -deletionPolicy: Delete \ No newline at end of file +deletionPolicy: Delete # Deleting the snapshot object in Kubernetes with delete the snapshot in CloudStack; You can use policy Retain if the snapshot shouldn't be deleted from CloudStack \ No newline at end of file diff --git a/examples/k8s/snapshot/pvc-from-snapshot.yaml b/examples/k8s/snapshot/pvc-from-snapshot.yaml index 0ae8bf0..f6f84a1 100644 --- a/examples/k8s/snapshot/pvc-from-snapshot.yaml +++ b/examples/k8s/snapshot/pvc-from-snapshot.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: - name: snapshot-pvc-1 + name: pvc-from-snapshot spec: accessModes: - ReadWriteOnce diff --git a/examples/k8s/snapshot/restore-pod.yaml b/examples/k8s/snapshot/restore-pod.yaml index 65d4462..f8e9c22 100644 --- a/examples/k8s/snapshot/restore-pod.yaml +++ b/examples/k8s/snapshot/restore-pod.yaml @@ -13,4 +13,4 @@ spec: volumes: - name: data persistentVolumeClaim: - claimName: snapshot-pvc-1 \ No newline at end of file + claimName: pvc-from-snapshot \ No newline at end of file From 14a77fdde21dba834e9a36853576e56b384f62cd Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Wed, 23 Jul 2025 15:18:49 -0400 Subject: [PATCH 11/48] update release file --- .github/workflows/release.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c4e71bb..f28d6ce 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -83,6 +83,8 @@ jobs: run: | VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,' | sed -e 's/^v//') echo "---" >> manifest.yaml + cat deploy/k8s/00-snapshot-crds.yaml >> manifest.yaml + echo "---" >> manifest.yaml cat deploy/k8s/rbac.yaml >> manifest.yaml echo "---" >> manifest.yaml cat deploy/k8s/csidriver.yaml >> manifest.yaml @@ -90,6 +92,8 @@ jobs: sed -E "s|image: +cloudstack-csi-driver|image: ${REGISTRY_NAME}/cloudstack-csi-driver:${VERSION}|" deploy/k8s/controller-deployment.yaml >> manifest.yaml echo "---" >> manifest.yaml sed -E "s|image: +cloudstack-csi-driver|image: ${REGISTRY_NAME}/cloudstack-csi-driver:${VERSION}|" deploy/k8s/node-daemonset.yaml >> manifest.yaml + echo "---" >> manifest.yaml + cat deploy/k8s/volume-snapshot-class.yaml >> manifest.yaml - name: Create Release id: create_release From 57440c33a45ac595454b5b68caa7e56a2be6540e Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Fri, 25 Jul 2025 14:10:16 -0400 Subject: [PATCH 12/48] add support to create snapshots at project level using csi driver --- examples/k8s/snapshot/pvc-from-snapshot.yaml | 2 +- pkg/cloud/snapshots.go | 15 +++++++++++++++ pkg/cloud/volumes.go | 6 +----- pkg/driver/controller.go | 3 ++- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/examples/k8s/snapshot/pvc-from-snapshot.yaml b/examples/k8s/snapshot/pvc-from-snapshot.yaml index f6f84a1..89a8182 100644 --- a/examples/k8s/snapshot/pvc-from-snapshot.yaml +++ b/examples/k8s/snapshot/pvc-from-snapshot.yaml @@ -7,7 +7,7 @@ spec: - ReadWriteOnce resources: requests: - storage: 10Gi + storage: 1Gi dataSource: name: snapshot-1 kind: VolumeSnapshot diff --git a/pkg/cloud/snapshots.go b/pkg/cloud/snapshots.go index 5e4365b..6e1961e 100644 --- a/pkg/cloud/snapshots.go +++ b/pkg/cloud/snapshots.go @@ -6,13 +6,22 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "k8s.io/klog/v2" ) func (c *client) GetSnapshotByID(ctx context.Context, snapshotID ...string) (*Snapshot, error) { + logger := klog.FromContext(ctx) p := c.Snapshot.NewListSnapshotsParams() if snapshotID != nil { p.SetId(snapshotID[0]) } + if c.projectID != "" { + p.SetProjectid(c.projectID) + } + logger.V(2).Info("CloudStack API call", "command", "ListSnapshots", "params", map[string]string{ + "id": snapshotID[0], + "projectid": c.projectID, + }) l, err := c.Snapshot.ListSnapshots(p) if err != nil { return nil, err @@ -37,7 +46,13 @@ func (c *client) GetSnapshotByID(ctx context.Context, snapshotID ...string) (*Sn } func (c *client) CreateSnapshot(ctx context.Context, volumeID string) (*Snapshot, error) { + logger := klog.FromContext(ctx) p := c.Snapshot.NewCreateSnapshotParams(volumeID) + + logger.V(2).Info("CloudStack API call", "command", "CreateSnapshot", "params", map[string]string{ + "volumeid": volumeID, + }) + snapshot, err := c.Snapshot.CreateSnapshot(p) if err != nil { return nil, status.Errorf(codes.Internal, "Error %v", err) diff --git a/pkg/cloud/volumes.go b/pkg/cloud/volumes.go index 1bf607c..cd26fd9 100644 --- a/pkg/cloud/volumes.go +++ b/pkg/cloud/volumes.go @@ -166,10 +166,6 @@ func (c *client) ExpandVolume(ctx context.Context, volumeID string, newSizeInGB func (c *client) CreateVolumeFromSnapshot(ctx context.Context, zoneID, name, domainID, projectID, snapshotID string, sizeInGB int64) (*Volume, error) { logger := klog.FromContext(ctx) - snapshot, _, err := c.Snapshot.GetSnapshotByID(snapshotID) - if err != nil { - return nil, fmt.Errorf("failed to retrieve snapshot '%s': %w", snapshotID, err) - } p := c.Volume.NewCreateVolumeParams() p.SetZoneid(zoneID) @@ -178,7 +174,7 @@ func (c *client) CreateVolumeFromSnapshot(ctx context.Context, zoneID, name, dom } p.SetName(name) p.SetSize(sizeInGB) - p.SetSnapshotid(snapshot.Id) + p.SetSnapshotid(snapshotID) logger.V(2).Info("CloudStack API call", "command", "CreateVolume", "params", map[string]string{ "name": name, diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index e72734e..b8f365d 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -332,9 +332,10 @@ func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS // Error with CloudStack return nil, status.Errorf(codes.Internal, "Error %v", err) } + klog.V(4).Infof("CreateSnapshot of volume: %s", volume) snapshot, err := cs.connector.CreateSnapshot(ctx, volume.ID) if err != nil { - return nil, status.Errorf(codes.Internal, "Failed to create snapshot %s: %v", snapshot.ID, err.Error()) + return nil, status.Errorf(codes.Internal, "Failed to create snapshot for volume %s: %v", volume.ID, err.Error()) } t, err := time.Parse("2006-01-02T15:04:05-0700", snapshot.CreatedAt) From f869c425fe8bb3fda42c385a37cd22d47cc1bac7 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Wed, 30 Jul 2025 12:39:57 -0400 Subject: [PATCH 13/48] separate manifest yaml - to ensure crds are installed before installing csi driver --- .github/workflows/release.yaml | 12 ++++++++++-- deploy/k8s/00-snapshot-crds.yaml | 1 - 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f28d6ce..64536ca 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -83,8 +83,6 @@ jobs: run: | VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,' | sed -e 's/^v//') echo "---" >> manifest.yaml - cat deploy/k8s/00-snapshot-crds.yaml >> manifest.yaml - echo "---" >> manifest.yaml cat deploy/k8s/rbac.yaml >> manifest.yaml echo "---" >> manifest.yaml cat deploy/k8s/csidriver.yaml >> manifest.yaml @@ -106,6 +104,16 @@ jobs: draft: false prerelease: false + - name: Upload Snapshot CRDs Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: deploy/k8s/00-snapshot-crds.yaml + asset_name: snapshot-crds.yaml + asset_content_type: application/x-yaml + - name: Upload Release Asset uses: actions/upload-release-asset@v1 env: diff --git a/deploy/k8s/00-snapshot-crds.yaml b/deploy/k8s/00-snapshot-crds.yaml index ac37a92..b6a402b 100644 --- a/deploy/k8s/00-snapshot-crds.yaml +++ b/deploy/k8s/00-snapshot-crds.yaml @@ -952,4 +952,3 @@ status: plural: "" conditions: [] storedVersions: [] - From dabf4692b854c5a696cc6d87a5a940a5eef5b6d8 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Wed, 30 Jul 2025 16:10:46 -0400 Subject: [PATCH 14/48] add udev support --- cmd/cloudstack-csi-driver/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/cloudstack-csi-driver/Dockerfile b/cmd/cloudstack-csi-driver/Dockerfile index dcd14d6..9dc7341 100644 --- a/cmd/cloudstack-csi-driver/Dockerfile +++ b/cmd/cloudstack-csi-driver/Dockerfile @@ -15,7 +15,7 @@ RUN apk add --no-cache \ blkid \ mount \ umount \ - # Provides udevadm for device path detection + # Provides udevadm for device path detection \ udev COPY ./bin/cloudstack-csi-driver /cloudstack-csi-driver From 04b55092974939ead0afd12682d5b7d1aef2defa Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Wed, 30 Jul 2025 16:13:59 -0400 Subject: [PATCH 15/48] add rbac rules to support deletion of snaps --- cmd/cloudstack-csi-driver/Dockerfile | 6 ++++-- deploy/k8s/rbac.yaml | 32 ++++++++++++++++++++++++++++ pkg/driver/controller.go | 2 +- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/cmd/cloudstack-csi-driver/Dockerfile b/cmd/cloudstack-csi-driver/Dockerfile index 4c260e8..95d1710 100644 --- a/cmd/cloudstack-csi-driver/Dockerfile +++ b/cmd/cloudstack-csi-driver/Dockerfile @@ -14,7 +14,9 @@ RUN apk add --no-cache \ # blkid, mount and umount are required by k8s.io/mount-utils \ blkid \ mount \ - umount + umount \ + # Provides udevadm for device path detection \ + udev COPY ./bin/cloudstack-csi-driver /cloudstack-csi-driver -ENTRYPOINT ["/cloudstack-csi-driver"] \ No newline at end of file +ENTRYPOINT ["/cloudstack-csi-driver"] diff --git a/deploy/k8s/rbac.yaml b/deploy/k8s/rbac.yaml index c8fb68a..e5fc848 100644 --- a/deploy/k8s/rbac.yaml +++ b/deploy/k8s/rbac.yaml @@ -86,3 +86,35 @@ roleRef: kind: ClusterRole name: cloudstack-csi-node-role apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: cloudstack-csi-snapshotter-role +rules: +- apiGroups: ["snapshot.storage.k8s.io"] + resources: ["volumesnapshotclasses"] + verbs: ["get", "list", "watch"] +- apiGroups: ["snapshot.storage.k8s.io"] + resources: ["volumesnapshotcontents"] + verbs: ["create", "get", "list", "watch", "update", "delete"] +- apiGroups: ["snapshot.storage.k8s.io"] + resources: ["volumesnapshots"] + verbs: ["get", "list", "watch", "update"] +- apiGroups: ["snapshot.storage.k8s.io"] + resources: ["volumesnapshotcontents/status"] + verbs: ["update"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: cloudstack-csi-snapshotter-binding +subjects: +- kind: ServiceAccount + name: cloudstack-csi-controller + namespace: kube-system +roleRef: + kind: ClusterRole + name: cloudstack-csi-snapshotter-role + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index b8f365d..882c66a 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -286,7 +286,7 @@ func determineSize(req *csi.CreateVolumeRequest) (int64, error) { func (cs *controllerServer) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest) (*csi.DeleteVolumeResponse, error) { logger := klog.FromContext(ctx) - logger.V(6).Info("DeleteVolume: called", "args", *req) + logger.Info("DeleteVolume: called", "args", *req) if req.GetVolumeId() == "" { return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request") From e8063c5bdf5f37a21a419ae348685801c723a375 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Wed, 13 Aug 2025 13:18:33 -0400 Subject: [PATCH 16/48] update log statement --- pkg/mount/mount.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/mount/mount.go b/pkg/mount/mount.go index 9e47862..4fd978e 100644 --- a/pkg/mount/mount.go +++ b/pkg/mount/mount.go @@ -114,7 +114,7 @@ func (m *mounter) getDevicePathBySerialID(volumeID string) (string, error) { // First try XenServer device paths xenDevicePath, err := m.getDevicePathForXenServer(volumeID) if err != nil { - fmt.Printf("Failed to get VMware device path: %v\n", err) + fmt.Printf("Failed to get XenServer device path: %v\n", err) } if xenDevicePath != "" { return xenDevicePath, nil From 53fe28b2dc8051b8110b0d2e036fcd72976c1827 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Tue, 26 Aug 2025 10:37:23 -0400 Subject: [PATCH 17/48] add delete permission to volumesnaps and vsc --- deploy/k8s/rbac.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/k8s/rbac.yaml b/deploy/k8s/rbac.yaml index e5fc848..64fc918 100644 --- a/deploy/k8s/rbac.yaml +++ b/deploy/k8s/rbac.yaml @@ -38,7 +38,7 @@ rules: verbs: ["patch"] - apiGroups: ["snapshot.storage.k8s.io"] resources: ["volumesnapshots", "volumesnapshots/status", "volumesnapshotclasses", "volumesnapshotcontents", "volumesnapshotcontents/status"] - verbs: ["get", "list", "watch", "update", "create", "patch"] + verbs: ["get", "list", "watch", "update", "create", "patch", "delete"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding From a104d841a05e1bf06593fd90b70b85e1de3f5ad4 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 28 Aug 2025 14:15:11 -0400 Subject: [PATCH 18/48] Update readme - add note for kvm snaps and details on deletion of snaps --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 697afc1..6c06a07 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,9 @@ make container ## Volume Snapshots + +**NOTE:** To create volume snapshots in KVM, make sure to set the `kvm.snapshot.enabled` global setting to true and restart the Management Server + For Volume snapshots to be created, the following configurations need to be applied: ``` @@ -167,6 +170,14 @@ spec: claimName: pvc-from-snapshot ``` + +To delete a volume snapshot +One can simlpy delete the volume snapshot created in kubernetes using + +``` +kubectl delete volumesnapshot snapshot-1 # here, snapshot-1 is the name of the snapshot created +``` + ### What happens when you restore a volume from a snapshot * The CSI external-provisioner (a container in the cloudstack-csi-controller pod) sees the new PVC and notices it references a snapshot * The CSI driver's `CreateVolume` method is called with a `VolumeContentSource` that contains the snapshot ID From 71e2f757432564bf5444f09c605b66051d7b2565 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 28 Aug 2025 14:20:47 -0400 Subject: [PATCH 19/48] update readme - troubleshooting steps for stuck volume snap deletion operation --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 6c06a07..6d7e1c2 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,31 @@ One can simlpy delete the volume snapshot created in kubernetes using kubectl delete volumesnapshot snapshot-1 # here, snapshot-1 is the name of the snapshot created ``` +If for whatever reason, snapshot deletion gets stuck, one can troubleshoot the issue doing the following: + +* Inspect the snapshot + +``` +kubectl get volumesnapshot [-n ] -o yaml +``` + +Look for the following section: +``` +metadata: + finalizers: + - snapshot.storage.kubernetes.io/volumesnapshot-as-source +``` + +If finalizers are present, Kubernetes will not delete the resource until they are removed or resolved. + +* Patch to Remove Finalizers + +``` +kubectl patch volumesnapshot [-n ] --type=merge -p '{"metadata":{"finalizers":[]}}' +``` + +**NOTE:** This bypasses cleanup logic. Use only if you're certain the snapshot is no longer needed at the CSI/backend level + ### What happens when you restore a volume from a snapshot * The CSI external-provisioner (a container in the cloudstack-csi-controller pod) sees the new PVC and notices it references a snapshot * The CSI driver's `CreateVolume` method is called with a `VolumeContentSource` that contains the snapshot ID From b938453201db8e001e3cfa3d8e8429eeaa226bb6 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 28 Aug 2025 14:26:57 -0400 Subject: [PATCH 20/48] cleanup --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6d7e1c2..bf2060b 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,6 @@ disk offerings to Kubernetes storage classes. > **Note:** The VolumeSnapshot CRDs (CustomResourceDefinitions) of version 8.3.0 are installed in this deployment. If you use a different version, please ensure compatibility with your Kubernetes cluster and CSI sidecars. -// TODO: Should we have the crds locally or manually install it from: ``` kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/v8.3.0/client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml @@ -126,11 +125,12 @@ make container **NOTE:** To create volume snapshots in KVM, make sure to set the `kvm.snapshot.enabled` global setting to true and restart the Management Server +### Volume snapshot creation For Volume snapshots to be created, the following configurations need to be applied: ``` -kubectl aplly -f 00-snapshot-crds.yaml # Installs the VolumeSnapshotClass, VolumeSnapshotContent and VolumeSnapshtot CRDs -volume-snapshot-class.yaml # Defines VolumeSnapshotClass for CloudStack CSI driver +kubectl apply -f deploy/k8s/00-snapshot-crds.yaml # Installs the VolumeSnapshotClass, VolumeSnapshotContent and VolumeSnapshtot CRDs +kubectl apply -f deploy/k8s/volume-snapshot-class.yaml # Defines VolumeSnapshotClass for CloudStack CSI driver ``` Once the CRDs are installed, the snapshot can be taken by applying: @@ -148,6 +148,8 @@ kubectl logs -f -n kube-system -c csi-snaps kubectl logs -f -n kube-system -c snapshot-controller ``` +### Restoring a Volume snapshot + To restore a volume snapshot: 1. Restore a snapshot and Use it in a pod * Create a PVC from the snapshot - for example ./examples/k8s/snapshot/pvc-from-snapshot.yaml @@ -171,6 +173,8 @@ spec: ``` +### Deletion of a volume snapshot + To delete a volume snapshot One can simlpy delete the volume snapshot created in kubernetes using @@ -178,6 +182,7 @@ One can simlpy delete the volume snapshot created in kubernetes using kubectl delete volumesnapshot snapshot-1 # here, snapshot-1 is the name of the snapshot created ``` +#### Troubleshooting issues with volume snapshot deletion If for whatever reason, snapshot deletion gets stuck, one can troubleshoot the issue doing the following: * Inspect the snapshot @@ -201,7 +206,7 @@ If finalizers are present, Kubernetes will not delete the resource until they ar kubectl patch volumesnapshot [-n ] --type=merge -p '{"metadata":{"finalizers":[]}}' ``` -**NOTE:** This bypasses cleanup logic. Use only if you're certain the snapshot is no longer needed at the CSI/backend level +**Caution:** This bypasses cleanup logic. Use only if you're certain the snapshot is no longer needed at the CSI/backend level ### What happens when you restore a volume from a snapshot * The CSI external-provisioner (a container in the cloudstack-csi-controller pod) sees the new PVC and notices it references a snapshot From 2efecfca937d2f2b48945bf58006f9ab5d4b5a40 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Tue, 2 Sep 2025 13:39:05 -0400 Subject: [PATCH 21/48] update log --- pkg/driver/controller.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index 882c66a..08b438d 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -360,14 +360,14 @@ func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS } func (cs *controllerServer) DeleteSnapshot(ctx context.Context, req *csi.DeleteSnapshotRequest) (*csi.DeleteSnapshotResponse, error) { - klog.V(4).Infof("DeleteSnapshot") - snapshotID := req.GetSnapshotId() if snapshotID == "" { return nil, status.Error(codes.InvalidArgument, "Snapshot ID missing in request") } + klog.V(4).Infof("DeleteSnapshot for snapshotID: %s", snapshotID) + snapshot, err := cs.connector.GetSnapshotByID(ctx, snapshotID) if errors.Is(err, cloud.ErrNotFound) { return nil, status.Errorf(codes.NotFound, "Snapshot %v not found", snapshotID) From b7bfaa0c6daa0fe17a09db43335e63d7585f4ab2 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 11 Sep 2025 13:56:53 -0400 Subject: [PATCH 22/48] Update Readme with more details and considerations --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index bf2060b..6b7f49b 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,11 @@ The storage class must also have a parameter named `csi.cloudstack.apache.org/disk-offering-id` whose value is the CloudStack disk offering ID. +**Reclaim Policy**: Storage classes can have a `reclaimPolicy` of either `Delete` or `Retain`. If no `reclaimPolicy` is specified, it defaults to `Delete`. + +- `Delete`: When a PVC is deleted or a CKS cluster (Managed Kubernetes Cluster in CloudStack) is deleted, the associated persistent volumes and their underlying CloudStack disk volumes will be automatically removed. +- `Retain`: Persistent volumes and their underlying CloudStack disk volumes will be preserved even after PVC deletion or cluster deletion, allowing for manual recovery or data preservation. + #### Using cloudstack-csi-sc-syncer The tool `cloudstack-csi-sc-syncer` may also be used to synchronize CloudStack @@ -223,6 +228,12 @@ kubectl logs -f -n kube-system # defaults t kubectl logs -f -n kube-system -c external-provisioner ``` +## Additional General Notes: + +**Node Scheduling Best Practices**: When deploying applications that require specific node placement, use `nodeSelector` or `nodeAffinity` instead of `nodeName`. The `nodeName` field bypasses the Kubernetes scheduler, which can cause issues with storage provisioning. When a StorageClass has `volumeBindingMode: WaitForFirstConsumer`, the CSI controller relies on scheduler decisions to properly bind PVCs. Using `nodeName` prevents this scheduling integration, potentially causing PVC binding failures. + +**Network CIDR Considerations**: When deploying CKS (CloudStack Kubernetes Service) clusters on pre-existing networks, avoid using the `10.0.0.0/16` CIDR range as it conflicts with Calico's default pod network configuration. This overlap can prevent proper CSI driver initialization and may cause networking issues within the cluster. + ## See also - [CloudStack Kubernetes Provider](https://github.com/apache/cloudstack-kubernetes-provider) - Kubernetes Cloud Controller Manager for Apache CloudStack From bef017e0454f347628bf1d211771e10f88162838 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Wed, 17 Sep 2025 13:11:46 -0400 Subject: [PATCH 23/48] remove panic --- pkg/driver/controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index 08b438d..9a550c1 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -340,7 +340,7 @@ func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS t, err := time.Parse("2006-01-02T15:04:05-0700", snapshot.CreatedAt) if err != nil { - panic(err) + return nil, status.Errorf(codes.Internal, "Failed to parse snapshot creation time: %v", err) } // Convert to Timestamp protobuf From b986813b5677afc599eba5691af580665edbda9a Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Wed, 17 Sep 2025 13:14:50 -0400 Subject: [PATCH 24/48] address comments --- pkg/cloud/volumes.go | 2 +- pkg/driver/controller.go | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/cloud/volumes.go b/pkg/cloud/volumes.go index cd26fd9..3d1b363 100644 --- a/pkg/cloud/volumes.go +++ b/pkg/cloud/volumes.go @@ -187,7 +187,7 @@ func (c *client) CreateVolumeFromSnapshot(ctx context.Context, zoneID, name, dom vol, err := c.Volume.CreateVolume(p) if err != nil { // Handle the error accordingly - return nil, fmt.Errorf("failed to create volume from snapshot'%s': %w", snapshotID, err) + return nil, fmt.Errorf("failed to create volume from snapshot '%s': %w", snapshotID, err) } v := Volume{ diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index 9a550c1..9307f71 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -221,8 +221,12 @@ func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol } func printVolumeAsJSON(vol *csi.CreateVolumeRequest) { - b, _ := json.MarshalIndent(vol, "", " ") - fmt.Println(string(b)) + b, err := json.MarshalIndent(vol, "", " ") + if err != nil { + klog.Errorf("Failed to marshal CreateVolumeRequest to JSON: %v", err) + return + } + klog.V(5).Infof("CreateVolumeRequest as JSON:\n%s", string(b)) } func checkVolumeSuitable(vol *cloud.Volume, From d01b0efbeff25dfc66d7e3818876f21777c50766 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Mon, 22 Sep 2025 13:03:00 -0400 Subject: [PATCH 25/48] update branch references to main --- .github/workflows/charts-release.yaml | 2 +- .github/workflows/release.yaml | 10 +++++----- charts/cloudstack-csi/Chart.yaml | 2 +- deploy/k8s/controller-deployment.yaml | 2 +- deploy/k8s/node-daemonset.yaml | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/charts-release.yaml b/.github/workflows/charts-release.yaml index e69d81e..a5649a6 100644 --- a/.github/workflows/charts-release.yaml +++ b/.github/workflows/charts-release.yaml @@ -3,7 +3,7 @@ name: Release Charts on: push: branches: - - master + - main paths: - 'charts/**' diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c4e71bb..009500b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -3,7 +3,7 @@ name: Release on: push: branches: - - master + - main tags: - v* @@ -38,12 +38,12 @@ jobs: username: ${{github.actor}} password: ${{secrets.GITHUB_TOKEN}} - - name: Push master - if: github.ref == 'refs/heads/master' + - name: Push main + if: github.ref == 'refs/heads/main' run: | for img in $IMAGES; do - docker tag ${img} ${REGISTRY_NAME}/${img}:master - docker push ${REGISTRY_NAME}/${img}:master + docker tag ${img} ${REGISTRY_NAME}/${img}:main + docker push ${REGISTRY_NAME}/${img}:main done - name: Push tagged release diff --git a/charts/cloudstack-csi/Chart.yaml b/charts/cloudstack-csi/Chart.yaml index af3e07f..4d3de7b 100644 --- a/charts/cloudstack-csi/Chart.yaml +++ b/charts/cloudstack-csi/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: cloudstack-csi description: A Helm chart for CloudStack CSI driver type: application -version: 2.0.2 +version: 3.0.0 appVersion: 0.6.1 sources: - https://github.com/shapeblue/cloudstack-csi-driver diff --git a/deploy/k8s/controller-deployment.yaml b/deploy/k8s/controller-deployment.yaml index 44adffc..5859453 100644 --- a/deploy/k8s/controller-deployment.yaml +++ b/deploy/k8s/controller-deployment.yaml @@ -59,7 +59,7 @@ spec: containers: - name: cloudstack-csi-controller - image: cloudstack-csi-driver + image: ghcr.io/shapeblue/cloudstack-csi-driver:main imagePullPolicy: Always args: - "controller" diff --git a/deploy/k8s/node-daemonset.yaml b/deploy/k8s/node-daemonset.yaml index 665312b..672e715 100644 --- a/deploy/k8s/node-daemonset.yaml +++ b/deploy/k8s/node-daemonset.yaml @@ -36,7 +36,7 @@ spec: containers: - name: cloudstack-csi-node - image: cloudstack-csi-driver + image: ghcr.io/shapeblue/cloudstack-csi-driver:main imagePullPolicy: IfNotPresent args: - "node" From f979be170e28f035823b723cc9eeecae548073f5 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 25 Sep 2025 08:43:15 -0400 Subject: [PATCH 26/48] Update logger, readme --- README.md | 2 +- pkg/cloud/cloud.go | 2 +- pkg/cloud/snapshots.go | 8 ++-- pkg/driver/controller.go | 2 +- pkg/mount/mount.go | 80 +++++++++++++++------------------------- 5 files changed, 37 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 6b7f49b..68b92ce 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ **Fork Notice:** -This repo is a fork of the Leaseweb's maitained cloudstack-csi-driver, which is in-turn a fork of Apalia's cloudstack-csi-driver +This repo is a fork of the [Leaseweb's] (https://github.com/leaseweb/cloudstack-csi-driver) maitained cloudstack-csi-driver, which is in-turn a fork of [Apalia's](https://github.com/apalia/cloudstack-csi-driver) cloudstack-csi-driver # CloudStack CSI Driver diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go index ca75b42..0c21e3d 100644 --- a/pkg/cloud/cloud.go +++ b/pkg/cloud/cloud.go @@ -25,7 +25,7 @@ type Interface interface { ExpandVolume(ctx context.Context, volumeID string, newSizeInGB int64) error CreateVolumeFromSnapshot(ctx context.Context, zoneID, name, domainID, projectID, snapshotID string, sizeInGB int64) (*Volume, error) - GetSnapshotByID(ctx context.Context, snapshotID ...string) (*Snapshot, error) + GetSnapshotByID(ctx context.Context, snapshotID string) (*Snapshot, error) CreateSnapshot(ctx context.Context, volumeID string) (*Snapshot, error) DeleteSnapshot(ctx context.Context, snapshotID string) error } diff --git a/pkg/cloud/snapshots.go b/pkg/cloud/snapshots.go index 6e1961e..74830e1 100644 --- a/pkg/cloud/snapshots.go +++ b/pkg/cloud/snapshots.go @@ -9,17 +9,17 @@ import ( "k8s.io/klog/v2" ) -func (c *client) GetSnapshotByID(ctx context.Context, snapshotID ...string) (*Snapshot, error) { +func (c *client) GetSnapshotByID(ctx context.Context, snapshotID string) (*Snapshot, error) { logger := klog.FromContext(ctx) p := c.Snapshot.NewListSnapshotsParams() - if snapshotID != nil { - p.SetId(snapshotID[0]) + if snapshotID != "" { + p.SetId(snapshotID) } if c.projectID != "" { p.SetProjectid(c.projectID) } logger.V(2).Info("CloudStack API call", "command", "ListSnapshots", "params", map[string]string{ - "id": snapshotID[0], + "id": snapshotID, "projectid": c.projectID, }) l, err := c.Snapshot.ListSnapshots(p) diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index 9307f71..293ba16 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -290,7 +290,7 @@ func determineSize(req *csi.CreateVolumeRequest) (int64, error) { func (cs *controllerServer) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest) (*csi.DeleteVolumeResponse, error) { logger := klog.FromContext(ctx) - logger.Info("DeleteVolume: called", "args", *req) + logger.V(4).Info("DeleteVolume: called", "args", *req) if req.GetVolumeId() == "" { return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request") diff --git a/pkg/mount/mount.go b/pkg/mount/mount.go index 4fd978e..c89fc9d 100644 --- a/pkg/mount/mount.go +++ b/pkg/mount/mount.go @@ -79,6 +79,7 @@ func (m *mounter) GetBlockSizeBytes(devicePath string) (int64, error) { } func (m *mounter) GetDevicePath(ctx context.Context, volumeID string) (string, error) { + logger := klog.FromContext(ctx) backoff := wait.Backoff{ Duration: 2 * time.Second, Factor: 1.5, @@ -87,13 +88,13 @@ func (m *mounter) GetDevicePath(ctx context.Context, volumeID string) (string, e var devicePath string err := wait.ExponentialBackoffWithContext(ctx, backoff, func(context.Context) (bool, error) { - path, err := m.getDevicePathBySerialID(volumeID) + path, err := m.getDevicePathBySerialID(ctx, volumeID) if err != nil { return false, err } if path != "" { devicePath = path - + logger.V(4).Info("Device path found", "volumeID", volumeID, "devicePath", path) return true, nil } m.probeVolume(ctx) @@ -110,20 +111,22 @@ func (m *mounter) GetDevicePath(ctx context.Context, volumeID string) (string, e return devicePath, nil } -func (m *mounter) getDevicePathBySerialID(volumeID string) (string, error) { +func (m *mounter) getDevicePathBySerialID(ctx context.Context, volumeID string) (string, error) { + logger := klog.FromContext(ctx) + // First try XenServer device paths - xenDevicePath, err := m.getDevicePathForXenServer(volumeID) + xenDevicePath, err := m.getDevicePathForXenServer(ctx, volumeID) if err != nil { - fmt.Printf("Failed to get XenServer device path: %v\n", err) + logger.V(4).Info("Failed to get XenServer device path", "volumeID", volumeID, "error", err) } if xenDevicePath != "" { return xenDevicePath, nil } // Try VMware device paths - vmwareDevicePath, err := m.getDevicePathForVMware(volumeID) + vmwareDevicePath, err := m.getDevicePathForVMware(ctx, volumeID) if err != nil { - fmt.Printf("Failed to get VMware device path: %v\n", err) + logger.V(4).Info("Failed to get VMware device path", "volumeID", volumeID, "error", err) } if vmwareDevicePath != "" { return vmwareDevicePath, nil @@ -146,16 +149,18 @@ func (m *mounter) getDevicePathBySerialID(volumeID string) (string, error) { return "", nil } -func (m *mounter) getDevicePathForXenServer(volumeID string) (string, error) { +func (m *mounter) getDevicePathForXenServer(ctx context.Context, volumeID string) (string, error) { + logger := klog.FromContext(ctx) + for i := 'b'; i <= 'z'; i++ { devicePath := fmt.Sprintf("/dev/xvd%c", i) - fmt.Printf("Checking XenServer device path: %s\n", devicePath) + logger.V(5).Info("Checking XenServer device path", "devicePath", devicePath, "volumeID", volumeID) if _, err := os.Stat(devicePath); err == nil { isBlock, err := m.IsBlockDevice(devicePath) if err == nil && isBlock { - if m.verifyXenServerDevice(devicePath, volumeID) { - fmt.Printf("Found and verified XenServer device: %s\n", devicePath) + if m.verifyDevice(ctx, devicePath, volumeID) { + logger.V(4).Info("Found and verified XenServer device", "devicePath", devicePath, "volumeID", volumeID) return devicePath, nil } } @@ -164,46 +169,19 @@ func (m *mounter) getDevicePathForXenServer(volumeID string) (string, error) { return "", fmt.Errorf("device not found for volume %s", volumeID) } -func (m *mounter) verifyXenServerDevice(devicePath string, volumeID string) bool { - size, err := m.GetBlockSizeBytes(devicePath) - if err != nil { - fmt.Printf("Failed to get device size: %v\n", err) - return false - } - fmt.Printf("Device size: %d bytes\n", size) - - mounted, err := m.isDeviceMounted(devicePath) - if err != nil { - fmt.Printf("Failed to check if device is mounted: %v\n", err) - return false - } - if mounted { - fmt.Printf("Device is already mounted: %s\n", devicePath) - return false - } - - props, err := m.getDeviceProperties(devicePath) - if err != nil { - fmt.Printf("Failed to get device properties: %v\n", err) - return false - } - fmt.Printf("Device properties: %v\n", props) - - return true -} +func (m *mounter) getDevicePathForVMware(ctx context.Context, volumeID string) (string, error) { + logger := klog.FromContext(ctx) -func (m *mounter) getDevicePathForVMware(volumeID string) (string, error) { // Loop through /dev/sdb to /dev/sdz (/dev/sda -> the root disk) for i := 'b'; i <= 'z'; i++ { devicePath := fmt.Sprintf("/dev/sd%c", i) - fmt.Printf("Checking VMware device path: %s\n", devicePath) + logger.V(5).Info("Checking VMware device path", "devicePath", devicePath, "volumeID", volumeID) if _, err := os.Stat(devicePath); err == nil { isBlock, err := m.IsBlockDevice(devicePath) if err == nil && isBlock { - // Use the same verification as for XenServer - if m.verifyVMwareDevice(devicePath, volumeID) { - fmt.Printf("Found and verified VMware device: %s\n", devicePath) + if m.verifyDevice(ctx, devicePath, volumeID) { + logger.V(4).Info("Found and verified VMware device", "devicePath", devicePath, "volumeID", volumeID) return devicePath, nil } } @@ -212,30 +190,32 @@ func (m *mounter) getDevicePathForVMware(volumeID string) (string, error) { return "", fmt.Errorf("device not found for volume %s", volumeID) } -func (m *mounter) verifyVMwareDevice(devicePath string, volumeID string) bool { +func (m *mounter) verifyDevice(ctx context.Context, devicePath string, volumeID string) bool { + logger := klog.FromContext(ctx) + size, err := m.GetBlockSizeBytes(devicePath) if err != nil { - fmt.Printf("Failed to get device size: %v\n", err) + logger.V(4).Info("Failed to get device size", "devicePath", devicePath, "volumeID", volumeID, "error", err) return false } - fmt.Printf("Device size: %d bytes\n", size) + logger.V(5).Info("Device size retrieved", "devicePath", devicePath, "volumeID", volumeID, "sizeBytes", size) mounted, err := m.isDeviceMounted(devicePath) if err != nil { - fmt.Printf("Failed to check if device is mounted: %v\n", err) + logger.V(4).Info("Failed to check if device is mounted", "devicePath", devicePath, "volumeID", volumeID, "error", err) return false } if mounted { - fmt.Printf("Device is already mounted: %s\n", devicePath) + logger.V(4).Info("Device is already mounted", "devicePath", devicePath, "volumeID", volumeID) return false } props, err := m.getDeviceProperties(devicePath) if err != nil { - fmt.Printf("Failed to get device properties: %v\n", err) + logger.V(4).Info("Failed to get device properties", "devicePath", devicePath, "volumeID", volumeID, "error", err) return false } - fmt.Printf("Device properties: %v\n", props) + logger.V(5).Info("Device properties retrieved", "devicePath", devicePath, "volumeID", volumeID, "properties", props) return true } From 2c19a992f1580f14ce85e1d9cf088d1c84bbf791 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 25 Sep 2025 09:06:51 -0400 Subject: [PATCH 27/48] update logger --- pkg/mount/mount.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/mount/mount.go b/pkg/mount/mount.go index c89fc9d..578c539 100644 --- a/pkg/mount/mount.go +++ b/pkg/mount/mount.go @@ -141,7 +141,7 @@ func (m *mounter) getDevicePathBySerialID(ctx context.Context, volumeID string) return source, nil } if !os.IsNotExist(err) { - fmt.Printf("Not found: %s\n", err.Error()) + logger.Error(err, "Failed to stat device path", "path", source) return "", err } } From af5a8c480bf1cef4a41812264082d7c3870b742d Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 2 Oct 2025 10:10:15 -0400 Subject: [PATCH 28/48] fix conflicting changes --- Makefile | 2 +- deploy/k8s/controller-deployment.yaml | 2 +- deploy/k8s/node-daemonset.yaml | 2 +- pkg/cloud/fake/fake.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 7c64ee1..8ac5ce6 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ CMDS=cloudstack-csi-driver cloudstack-csi-sc-syncer -PKG=github.com/shapeblue/cloudstack-csi-driver +PKG=github.com/cloudstack/cloudstack-csi-driver # Revision that gets built into each binary via the main.version # string. Uses the `git describe` output based on the most recent # version tag with a short revision suffix or, if nothing has been diff --git a/deploy/k8s/controller-deployment.yaml b/deploy/k8s/controller-deployment.yaml index 18dc81e..174696c 100644 --- a/deploy/k8s/controller-deployment.yaml +++ b/deploy/k8s/controller-deployment.yaml @@ -61,7 +61,7 @@ spec: containers: - name: cloudstack-csi-controller - image: ghcr.io/shapeblue/cloudstack-csi-driver:main + image: ghcr.io/cloudstack/cloudstack-csi-driver:main imagePullPolicy: Always args: - "controller" diff --git a/deploy/k8s/node-daemonset.yaml b/deploy/k8s/node-daemonset.yaml index bcf163f..d3831c2 100644 --- a/deploy/k8s/node-daemonset.yaml +++ b/deploy/k8s/node-daemonset.yaml @@ -36,7 +36,7 @@ spec: containers: - name: cloudstack-csi-node - image: ghcr.io/shapeblue/cloudstack-csi-driver:main + image: ghcr.io/cloudstack/cloudstack-csi-driver:main imagePullPolicy: IfNotPresent args: - "node" diff --git a/pkg/cloud/fake/fake.go b/pkg/cloud/fake/fake.go index 9727173..60cb5da 100644 --- a/pkg/cloud/fake/fake.go +++ b/pkg/cloud/fake/fake.go @@ -141,7 +141,7 @@ func (f *fakeConnector) CreateVolumeFromSnapshot(ctx context.Context, zoneID, na return nil, nil } -func (f *fakeConnector) GetSnapshotByID(ctx context.Context, snapshotID ...string) (*cloud.Snapshot, error) { +func (f *fakeConnector) GetSnapshotByID(ctx context.Context, snapshotID string) (*cloud.Snapshot, error) { return f.snapshot, nil } From 5c0ef9e8414b21689e9abd6cfcfd172127b4a416 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 2 Oct 2025 10:11:04 -0400 Subject: [PATCH 29/48] fix signature --- pkg/cloud/fake/fake.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cloud/fake/fake.go b/pkg/cloud/fake/fake.go index 64e02e6..2834867 100644 --- a/pkg/cloud/fake/fake.go +++ b/pkg/cloud/fake/fake.go @@ -141,7 +141,7 @@ func (f *fakeConnector) CreateVolumeFromSnapshot(ctx context.Context, zoneID, na return nil, nil } -func (f *fakeConnector) GetSnapshotByID(ctx context.Context, snapshotID ...string) (*cloud.Snapshot, error) { +func (f *fakeConnector) GetSnapshotByID(ctx context.Context, snapshotID string) (*cloud.Snapshot, error) { return f.snapshot, nil } From 12643a77023e0bbc4db2ec0b541c5477f176d7c1 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 2 Oct 2025 10:51:11 -0400 Subject: [PATCH 30/48] Update ubuntu version --- .github/workflows/release.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e877eb9..d2aac42 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -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 @@ -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] From 1f8922ba207ce56bce2c998ebf123eb0abe3e585 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 2 Oct 2025 10:56:13 -0400 Subject: [PATCH 31/48] update image --- .github/workflows/pr-check.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-check.yaml b/.github/workflows/pr-check.yaml index 580fa9f..f8b7cd8 100644 --- a/.github/workflows/pr-check.yaml +++ b/.github/workflows/pr-check.yaml @@ -6,7 +6,7 @@ on: jobs: lint: name: Lint - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: Setup up Go 1.x uses: actions/setup-go@v5 @@ -22,7 +22,7 @@ jobs: build: name: Test & Build - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: Setup up Go 1.x uses: actions/setup-go@v5 From 16ccb82bea18381dacdd7fcba9d97d7f0f38edcf Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 2 Oct 2025 11:25:06 -0400 Subject: [PATCH 32/48] fix lint failures --- go.mod | 6 ++++-- pkg/cloud/config.go | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 84aa4cf..3eb5dfb 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 @@ -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 diff --git a/pkg/cloud/config.go b/pkg/cloud/config.go index 0024dff..70d15a3 100644 --- a/pkg/cloud/config.go +++ b/pkg/cloud/config.go @@ -3,7 +3,7 @@ package cloud import ( "fmt" - "gopkg.in/gcfg.v1" + gcfg "gopkg.in/gcfg.v1" ) // Config holds CloudStack connection configuration. From b5f45799d642213ab7b0ec6b8a0ac222862001db Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 2 Oct 2025 11:37:50 -0400 Subject: [PATCH 33/48] fix ga failures --- .github/workflows/pr-check.yaml | 10 +++++----- pkg/driver/controller.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr-check.yaml b/.github/workflows/pr-check.yaml index f8b7cd8..a446d66 100644 --- a/.github/workflows/pr-check.yaml +++ b/.github/workflows/pr-check.yaml @@ -8,26 +8,26 @@ jobs: name: Lint 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-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 diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index 293ba16..a6d5a49 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -336,7 +336,7 @@ func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS // Error with CloudStack return nil, status.Errorf(codes.Internal, "Error %v", err) } - klog.V(4).Infof("CreateSnapshot of volume: %s", volume) + klog.V(4).Infof("CreateSnapshot of volume: %s", volume.ID) snapshot, err := cs.connector.CreateSnapshot(ctx, volume.ID) if err != nil { return nil, status.Errorf(codes.Internal, "Failed to create snapshot for volume %s: %v", volume.ID, err.Error()) From e5b1277e051eb70260a839df64d8da817ebf32b0 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 2 Oct 2025 11:57:34 -0400 Subject: [PATCH 34/48] fix test --- pkg/cloud/fake/fake.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cloud/fake/fake.go b/pkg/cloud/fake/fake.go index 2834867..b56157f 100644 --- a/pkg/cloud/fake/fake.go +++ b/pkg/cloud/fake/fake.go @@ -44,7 +44,7 @@ func New() cloud.Interface { DomainID: "51f0fcb5-db16-4637-94f5-30131010214f", ZoneID: zoneID, VolumeID: "4f1f610d-6f17-4ff9-9228-e4062af93e54", - CreatedAt: "2025-07-07 16:13:06", + CreatedAt: "2025-07-07T16:13:06-0700", } return &fakeConnector{ From a2b4fde6baaf12377e913e0e2ff9ad98d0fddaae Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 2 Oct 2025 12:28:34 -0400 Subject: [PATCH 35/48] fix csi tests --- pkg/cloud/cloud.go | 1 + pkg/cloud/fake/fake.go | 39 +++++++++++++++++++++++++++++++-------- pkg/cloud/snapshots.go | 37 +++++++++++++++++++++++++++++++++++++ pkg/driver/controller.go | 32 +++++++++++++++++++++++++++++--- 4 files changed, 98 insertions(+), 11 deletions(-) diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go index 0c21e3d..a578a4e 100644 --- a/pkg/cloud/cloud.go +++ b/pkg/cloud/cloud.go @@ -26,6 +26,7 @@ type Interface interface { CreateVolumeFromSnapshot(ctx context.Context, zoneID, name, domainID, projectID, snapshotID string, sizeInGB int64) (*Volume, error) GetSnapshotByID(ctx context.Context, snapshotID string) (*Snapshot, error) + GetSnapshotByName(ctx context.Context, name string) (*Snapshot, error) CreateSnapshot(ctx context.Context, volumeID string) (*Snapshot, error) DeleteSnapshot(ctx context.Context, snapshotID string) error } diff --git a/pkg/cloud/fake/fake.go b/pkg/cloud/fake/fake.go index b56157f..c5217a4 100644 --- a/pkg/cloud/fake/fake.go +++ b/pkg/cloud/fake/fake.go @@ -4,6 +4,7 @@ package fake import ( "context" + "fmt" "github.com/hashicorp/go-uuid" @@ -15,10 +16,11 @@ 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 + snapshot *cloud.Snapshot + volumesByID map[string]cloud.Volume + volumesByName map[string]cloud.Volume + snapshotsByName map[string]*cloud.Snapshot } // New returns a new fake implementation of the @@ -47,11 +49,16 @@ func New() cloud.Interface { CreatedAt: "2025-07-07T16:13:06-0700", } + snapshotsByName := map[string]*cloud.Snapshot{ + snapshot.Name: 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, + snapshot: snapshot, + volumesByID: map[string]cloud.Volume{volume.ID: volume}, + volumesByName: map[string]cloud.Volume{volume.Name: volume}, + snapshotsByName: snapshotsByName, } } @@ -72,6 +79,9 @@ func (f *fakeConnector) ListZonesID(_ context.Context) ([]string, error) { } func (f *fakeConnector) GetVolumeByID(_ context.Context, volumeID string) (*cloud.Volume, error) { + if volumeID == "" { + return nil, fmt.Errorf("invalid volume ID: empty string") + } vol, ok := f.volumesByID[volumeID] if ok { return &vol, nil @@ -81,6 +91,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, fmt.Errorf("invalid volume name: empty string") + } vol, ok := f.volumesByName[name] if ok { return &vol, nil @@ -152,3 +165,13 @@ func (f *fakeConnector) CreateSnapshot(ctx context.Context, volumeID string) (*c func (f *fakeConnector) DeleteSnapshot(ctx context.Context, snapshotID string) error { return nil } + +func (f *fakeConnector) GetSnapshotByName(_ context.Context, name string) (*cloud.Snapshot, error) { + if name == "" { + return nil, fmt.Errorf("invalid snapshot name: empty string") + } + if snap, ok := f.snapshotsByName[name]; ok { + return snap, nil + } + return nil, cloud.ErrNotFound +} diff --git a/pkg/cloud/snapshots.go b/pkg/cloud/snapshots.go index 74830e1..e32ed26 100644 --- a/pkg/cloud/snapshots.go +++ b/pkg/cloud/snapshots.go @@ -81,3 +81,40 @@ func (c *client) DeleteSnapshot(ctx context.Context, snapshotID string) error { return err } + +func (c *client) GetSnapshotByName(ctx context.Context, name string) (*Snapshot, error) { + logger := klog.FromContext(ctx) + if name == "" { + return nil, ErrNotFound + } + p := c.Snapshot.NewListSnapshotsParams() + p.SetName(name) + if c.projectID != "" { + p.SetProjectid(c.projectID) + } + logger.V(2).Info("CloudStack API call", "command", "ListSnapshots", "params", map[string]string{ + "name": name, + "projectid": c.projectID, + }) + l, err := c.Snapshot.ListSnapshots(p) + if err != nil { + return nil, err + } + if l.Count == 0 { + return nil, ErrNotFound + } + if l.Count > 1 { + return nil, ErrTooManyResults + } + snapshot := l.Snapshots[0] + s := Snapshot{ + ID: snapshot.Id, + Name: snapshot.Name, + DomainID: snapshot.Domainid, + ProjectID: snapshot.Projectid, + ZoneID: snapshot.Zoneid, + VolumeID: snapshot.Volumeid, + CreatedAt: snapshot.Created, + } + return &s, nil +} diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index a6d5a49..be00ee7 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -328,11 +328,32 @@ func (cs *controllerServer) DeleteVolume(ctx context.Context, req *csi.DeleteVol func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateSnapshotRequest) (*csi.CreateSnapshotResponse, error) { klog.V(4).Infof("CreateSnapshot") + if req.GetName() == "" { + return nil, status.Error(codes.InvalidArgument, "Snapshot name missing in request") + } + volumeID := req.GetSourceVolumeId() + if volumeID == "" { + return nil, status.Error(codes.InvalidArgument, "SourceVolumeId missing in request") + } + + // Check for existing snapshot with same name but different source volume ID + if req.GetName() != "" { + // check if the name matches and volumeID differs + existingSnap, err := cs.connector.GetSnapshotByName(ctx, req.GetName()) + if err == nil && existingSnap.VolumeID != volumeID { + return nil, status.Errorf(codes.AlreadyExists, "Snapshot with name %s already exists for a different source volume", req.GetName()) + } + } + volume, err := cs.connector.GetVolumeByID(ctx, volumeID) - if errors.Is(err, cloud.ErrNotFound) { - return nil, status.Errorf(codes.NotFound, "Volume %v not found", volumeID) - } else if err != nil { + if err != nil { + if err.Error() == "invalid volume ID: empty string" { + return nil, status.Error(codes.InvalidArgument, "Invalid volume ID") + } + if errors.Is(err, cloud.ErrNotFound) { + return nil, status.Errorf(codes.NotFound, "Volume %v not found", volumeID) + } // Error with CloudStack return nil, status.Errorf(codes.Internal, "Error %v", err) } @@ -363,6 +384,11 @@ func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS } +func (cs *controllerServer) ListSnapshots(ctx context.Context, req *csi.ListSnapshotsRequest) (*csi.ListSnapshotsResponse, error) { + // Stub implementation: returns an empty list + return &csi.ListSnapshotsResponse{Entries: []*csi.ListSnapshotsResponse_Entry{}}, nil +} + func (cs *controllerServer) DeleteSnapshot(ctx context.Context, req *csi.DeleteSnapshotRequest) (*csi.DeleteSnapshotResponse, error) { snapshotID := req.GetSnapshotId() From 7c6c42ff1d6514b1b3c246ece72c1ab39ca75d37 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 2 Oct 2025 12:39:41 -0400 Subject: [PATCH 36/48] fix test --- pkg/cloud/fake/fake.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/cloud/fake/fake.go b/pkg/cloud/fake/fake.go index c5217a4..cc414b2 100644 --- a/pkg/cloud/fake/fake.go +++ b/pkg/cloud/fake/fake.go @@ -151,7 +151,16 @@ func (f *fakeConnector) ExpandVolume(_ context.Context, volumeID string, newSize } func (f *fakeConnector) CreateVolumeFromSnapshot(ctx context.Context, zoneID, name, domainID, projectID, snapshotID string, sizeInGB int64) (*cloud.Volume, error) { - return nil, nil + 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) { From e43efce2e8b5c56879d83515fb5e0787f0151b8c Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 2 Oct 2025 12:50:04 -0400 Subject: [PATCH 37/48] fix lint and test --- pkg/cloud/fake/fake.go | 8 ++++---- pkg/driver/controller.go | 23 +++++++++++++++++++++-- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/pkg/cloud/fake/fake.go b/pkg/cloud/fake/fake.go index cc414b2..71ba423 100644 --- a/pkg/cloud/fake/fake.go +++ b/pkg/cloud/fake/fake.go @@ -4,7 +4,7 @@ package fake import ( "context" - "fmt" + "errors" "github.com/hashicorp/go-uuid" @@ -80,7 +80,7 @@ func (f *fakeConnector) ListZonesID(_ context.Context) ([]string, error) { func (f *fakeConnector) GetVolumeByID(_ context.Context, volumeID string) (*cloud.Volume, error) { if volumeID == "" { - return nil, fmt.Errorf("invalid volume ID: empty string") + return nil, errors.New("invalid volume ID: empty string") } vol, ok := f.volumesByID[volumeID] if ok { @@ -92,7 +92,7 @@ func (f *fakeConnector) GetVolumeByID(_ context.Context, volumeID string) (*clou func (f *fakeConnector) GetVolumeByName(_ context.Context, name string) (*cloud.Volume, error) { if name == "" { - return nil, fmt.Errorf("invalid volume name: empty string") + return nil, errors.New("invalid volume name: empty string") } vol, ok := f.volumesByName[name] if ok { @@ -177,7 +177,7 @@ func (f *fakeConnector) DeleteSnapshot(ctx context.Context, snapshotID string) e func (f *fakeConnector) GetSnapshotByName(_ context.Context, name string) (*cloud.Snapshot, error) { if name == "" { - return nil, fmt.Errorf("invalid snapshot name: empty string") + return nil, errors.New("invalid snapshot name: empty string") } if snap, ok := f.snapshotsByName[name]; ok { return snap, nil diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index be00ee7..efa8d4d 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -385,8 +385,27 @@ func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS } func (cs *controllerServer) ListSnapshots(ctx context.Context, req *csi.ListSnapshotsRequest) (*csi.ListSnapshotsResponse, error) { - // Stub implementation: returns an empty list - return &csi.ListSnapshotsResponse{Entries: []*csi.ListSnapshotsResponse_Entry{}}, nil + entries := []*csi.ListSnapshotsResponse_Entry{} + + if req.GetSnapshotId() != "" { + snap, err := cs.connector.GetSnapshotByID(ctx, req.GetSnapshotId()) + if err == nil && snap != nil { + t, _ := time.Parse("2006-01-02T15:04:05-0700", snap.CreatedAt) + ts := timestamppb.New(t) + entry := &csi.ListSnapshotsResponse_Entry{ + Snapshot: &csi.Snapshot{ + SnapshotId: snap.ID, + SourceVolumeId: snap.VolumeID, + CreationTime: ts, + ReadyToUse: true, + }, + } + entries = append(entries, entry) + } + return &csi.ListSnapshotsResponse{Entries: entries}, nil + } + + return &csi.ListSnapshotsResponse{Entries: entries}, nil } func (cs *controllerServer) DeleteSnapshot(ctx context.Context, req *csi.DeleteSnapshotRequest) (*csi.DeleteSnapshotResponse, error) { From 2db6b9e52ee9f623cb850a733a555af3774bca63 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 2 Oct 2025 12:57:37 -0400 Subject: [PATCH 38/48] fix lint and test --- pkg/cloud/cloud.go | 2 +- pkg/cloud/fake/fake.go | 9 ++++----- pkg/cloud/snapshots.go | 2 +- pkg/cloud/volumes.go | 2 +- pkg/driver/controller.go | 2 +- pkg/mount/mount.go | 11 ----------- 6 files changed, 8 insertions(+), 20 deletions(-) diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go index a578a4e..62324e7 100644 --- a/pkg/cloud/cloud.go +++ b/pkg/cloud/cloud.go @@ -24,7 +24,7 @@ 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) GetSnapshotByName(ctx context.Context, name string) (*Snapshot, error) CreateSnapshot(ctx context.Context, volumeID string) (*Snapshot, error) diff --git a/pkg/cloud/fake/fake.go b/pkg/cloud/fake/fake.go index 71ba423..2f5a7b2 100644 --- a/pkg/cloud/fake/fake.go +++ b/pkg/cloud/fake/fake.go @@ -13,7 +13,6 @@ import ( ) const zoneID = "a1887604-237c-4212-a9cd-94620b7880fa" -const snapshotID = "9d076136-657b-4c84-b279-455da3ea484c" type fakeConnector struct { node *cloud.VM @@ -150,7 +149,7 @@ 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) { +func (f *fakeConnector) CreateVolumeFromSnapshot(ctx context.Context, zoneID, name, projectID, snapshotID string, sizeInGB int64) (*cloud.Volume, error) { vol := &cloud.Volume{ ID: "fake-vol-from-snap-" + name, Name: name, @@ -163,15 +162,15 @@ func (f *fakeConnector) CreateVolumeFromSnapshot(ctx context.Context, zoneID, na return vol, nil } -func (f *fakeConnector) GetSnapshotByID(ctx context.Context, snapshotID string) (*cloud.Snapshot, error) { +func (f *fakeConnector) GetSnapshotByID(_ context.Context, snapshotID string) (*cloud.Snapshot, error) { return f.snapshot, nil } -func (f *fakeConnector) CreateSnapshot(ctx context.Context, volumeID string) (*cloud.Snapshot, error) { +func (f *fakeConnector) CreateSnapshot(_ context.Context, volumeID string) (*cloud.Snapshot, error) { return f.snapshot, nil } -func (f *fakeConnector) DeleteSnapshot(ctx context.Context, snapshotID string) error { +func (f *fakeConnector) DeleteSnapshot(_ context.Context, snapshotID string) error { return nil } diff --git a/pkg/cloud/snapshots.go b/pkg/cloud/snapshots.go index e32ed26..4909045 100644 --- a/pkg/cloud/snapshots.go +++ b/pkg/cloud/snapshots.go @@ -71,7 +71,7 @@ func (c *client) CreateSnapshot(ctx context.Context, volumeID string) (*Snapshot return &snap, nil } -func (c *client) DeleteSnapshot(ctx context.Context, snapshotID string) error { +func (c *client) DeleteSnapshot(_ context.Context, snapshotID string) error { p := c.Snapshot.NewDeleteSnapshotParams(snapshotID) _, err := c.Snapshot.DeleteSnapshot(p) if err != nil && strings.Contains(err.Error(), "4350") { diff --git a/pkg/cloud/volumes.go b/pkg/cloud/volumes.go index 3d1b363..154e28b 100644 --- a/pkg/cloud/volumes.go +++ b/pkg/cloud/volumes.go @@ -164,7 +164,7 @@ func (c *client) ExpandVolume(ctx context.Context, volumeID string, newSizeInGB return nil } -func (c *client) CreateVolumeFromSnapshot(ctx context.Context, zoneID, name, domainID, projectID, snapshotID string, sizeInGB int64) (*Volume, error) { +func (c *client) CreateVolumeFromSnapshot(ctx context.Context, zoneID, name, projectID, snapshotID string, sizeInGB int64) (*Volume, error) { logger := klog.FromContext(ctx) p := c.Volume.NewCreateVolumeParams() diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index efa8d4d..5e9769b 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -148,7 +148,7 @@ func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol sizeInGB = snapshotSizeGiB } - volFromSnapshot, err := cs.connector.CreateVolumeFromSnapshot(ctx, snapshot.ZoneID, name, snapshot.DomainID, snapshot.ProjectID, snapshotID, sizeInGB) + volFromSnapshot, err := cs.connector.CreateVolumeFromSnapshot(ctx, snapshot.ZoneID, name, snapshot.ProjectID, snapshotID, sizeInGB) if err != nil { return nil, status.Errorf(codes.Internal, "Cannot create volume from snapshot %s: %v", snapshotID, err.Error()) } diff --git a/pkg/mount/mount.go b/pkg/mount/mount.go index 578c539..ede70e3 100644 --- a/pkg/mount/mount.go +++ b/pkg/mount/mount.go @@ -231,17 +231,6 @@ func (m *mounter) isDeviceMounted(devicePath string) (bool, error) { return len(output) > 0, nil } -func (m *mounter) isDeviceInUse(devicePath string) (bool, error) { - output, err := m.Exec.Command("lsof", devicePath).Output() - if err != nil { - if strings.Contains(err.Error(), "exit status 1") { - return false, nil - } - return false, err - } - return len(output) > 0, nil -} - func (m *mounter) getDeviceProperties(devicePath string) (map[string]string, error) { output, err := m.Exec.Command("udevadm", "info", "--query=property", devicePath).Output() if err != nil { From 2966bb5cfc356c133fe54c84c92075232115c72b Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 2 Oct 2025 13:01:26 -0400 Subject: [PATCH 39/48] fix lint failure --- pkg/mount/mount.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/mount/mount.go b/pkg/mount/mount.go index ede70e3..7e2ca6e 100644 --- a/pkg/mount/mount.go +++ b/pkg/mount/mount.go @@ -366,13 +366,13 @@ func (m *mounter) GetStatistics(volumePath string) (volumeStatistics, error) { } volStats := volumeStatistics{ - AvailableBytes: int64(statfs.Bavail) * int64(statfs.Bsize), //nolint:unconvert - TotalBytes: int64(statfs.Blocks) * int64(statfs.Bsize), //nolint:unconvert - UsedBytes: (int64(statfs.Blocks) - int64(statfs.Bfree)) * int64(statfs.Bsize), //nolint:unconvert + AvailableBytes: int64(statfs.Bavail) * int64(statfs.Bsize), //nolint:gosec,unconvert + TotalBytes: int64(statfs.Blocks) * int64(statfs.Bsize), //nolint:gosec,unconvert + UsedBytes: (int64(statfs.Blocks) - int64(statfs.Bfree)) * int64(statfs.Bsize), //nolint:gosec,unconvert - AvailableInodes: int64(statfs.Ffree), - TotalInodes: int64(statfs.Files), - UsedInodes: int64(statfs.Files) - int64(statfs.Ffree), + AvailableInodes: int64(statfs.Ffree), //nolint:gosec + TotalInodes: int64(statfs.Files), //nolint:gosec + UsedInodes: int64(statfs.Files) - int64(statfs.Ffree), //nolint:gosec } return volStats, nil From 10223f70857d85d8ee51127efd30ae26f71d3d83 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 2 Oct 2025 13:02:32 -0400 Subject: [PATCH 40/48] skip link check --- pkg/driver/controller.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index 5e9769b..5e06c3d 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -49,6 +49,7 @@ func NewControllerServer(connector cloud.Interface) csi.ControllerServer { } } +//nolint:gocognit func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) { logger := klog.FromContext(ctx) logger.V(6).Info("CreateVolume: called", "args", *req) From 1973b8b7f1ef076288c97b7f7b306ff5895415f7 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 2 Oct 2025 13:10:05 -0400 Subject: [PATCH 41/48] fix lint & test --- pkg/cloud/fake/fake.go | 25 +++++++++++++++++++++---- pkg/driver/controller.go | 4 +++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/pkg/cloud/fake/fake.go b/pkg/cloud/fake/fake.go index 2f5a7b2..25ffe2c 100644 --- a/pkg/cloud/fake/fake.go +++ b/pkg/cloud/fake/fake.go @@ -149,7 +149,7 @@ func (f *fakeConnector) ExpandVolume(_ context.Context, volumeID string, newSize return cloud.ErrNotFound } -func (f *fakeConnector) CreateVolumeFromSnapshot(ctx context.Context, zoneID, name, projectID, snapshotID string, sizeInGB int64) (*cloud.Volume, error) { +func (f *fakeConnector) CreateVolumeFromSnapshot(_ context.Context, zoneID, name, projectID, snapshotID string, sizeInGB int64) (*cloud.Volume, error) { vol := &cloud.Volume{ ID: "fake-vol-from-snap-" + name, Name: name, @@ -163,14 +163,31 @@ func (f *fakeConnector) CreateVolumeFromSnapshot(ctx context.Context, zoneID, na } func (f *fakeConnector) GetSnapshotByID(_ context.Context, snapshotID string) (*cloud.Snapshot, error) { - return f.snapshot, nil + if f.snapshot != nil && f.snapshot.ID == snapshotID { + return f.snapshot, nil + } + return nil, cloud.ErrNotFound } func (f *fakeConnector) CreateSnapshot(_ context.Context, volumeID string) (*cloud.Snapshot, error) { - return f.snapshot, nil + name := "pvc-vol-snap-1" // Always use the same name for test + if snap, ok := f.snapshotsByName[name]; ok && snap.VolumeID != volumeID { + return nil, errors.New("snapshot name conflict: already exists for a different source volume") + } + newSnap := &cloud.Snapshot{ + ID: "snap-" + volumeID, + Name: name, + DomainID: "fake-domain", + ZoneID: zoneID, + VolumeID: volumeID, + CreatedAt: "2025-07-07T16:13:06-0700", + } + f.snapshotsByName[name] = newSnap + f.snapshot = newSnap + return newSnap, nil } -func (f *fakeConnector) DeleteSnapshot(_ context.Context, snapshotID string) error { +func (f *fakeConnector) DeleteSnapshot(_ context.Context, _ string) error { return nil } diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index 5e06c3d..ac645d3 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -402,8 +402,10 @@ func (cs *controllerServer) ListSnapshots(ctx context.Context, req *csi.ListSnap }, } entries = append(entries, entry) + return &csi.ListSnapshotsResponse{Entries: entries}, nil } - return &csi.ListSnapshotsResponse{Entries: entries}, nil + // If not found, return empty list + return &csi.ListSnapshotsResponse{Entries: []*csi.ListSnapshotsResponse_Entry{}}, nil } return &csi.ListSnapshotsResponse{Entries: entries}, nil From 4e3f9eb6cdbd5d3e186e936a4dc704dfe684ed01 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 2 Oct 2025 13:38:40 -0400 Subject: [PATCH 42/48] fix sanity tests --- pkg/cloud/cloud.go | 4 +- pkg/cloud/fake/fake.go | 102 +++++++++++++++++++++++++++------------ pkg/cloud/snapshots.go | 48 +++++++++++++++++- pkg/driver/controller.go | 87 +++++++++++++++++---------------- 4 files changed, 164 insertions(+), 77 deletions(-) diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go index 62324e7..7c1939e 100644 --- a/pkg/cloud/cloud.go +++ b/pkg/cloud/cloud.go @@ -27,8 +27,9 @@ type Interface interface { CreateVolumeFromSnapshot(ctx context.Context, zoneID, name, projectID, snapshotID string, sizeInGB int64) (*Volume, error) GetSnapshotByID(ctx context.Context, snapshotID string) (*Snapshot, error) GetSnapshotByName(ctx context.Context, name string) (*Snapshot, error) - CreateSnapshot(ctx context.Context, volumeID 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. @@ -71,6 +72,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. diff --git a/pkg/cloud/fake/fake.go b/pkg/cloud/fake/fake.go index 25ffe2c..079b0ec 100644 --- a/pkg/cloud/fake/fake.go +++ b/pkg/cloud/fake/fake.go @@ -16,10 +16,10 @@ const zoneID = "a1887604-237c-4212-a9cd-94620b7880fa" type fakeConnector struct { node *cloud.VM - snapshot *cloud.Snapshot volumesByID map[string]cloud.Volume volumesByName map[string]cloud.Volume - snapshotsByName map[string]*cloud.Snapshot + snapshotsByID map[string]*cloud.Snapshot + snapshotsByName map[string][]*cloud.Snapshot } // New returns a new fake implementation of the @@ -39,24 +39,14 @@ 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-07T16:13:06-0700", - } - - snapshotsByName := map[string]*cloud.Snapshot{ - snapshot.Name: snapshot, - } + 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}, + snapshotsByID: snapshotsByID, snapshotsByName: snapshotsByName, } } @@ -162,41 +152,89 @@ func (f *fakeConnector) CreateVolumeFromSnapshot(_ context.Context, zoneID, name return vol, nil } -func (f *fakeConnector) GetSnapshotByID(_ context.Context, snapshotID string) (*cloud.Snapshot, error) { - if f.snapshot != nil && f.snapshot.ID == snapshotID { - 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") } - return nil, cloud.ErrNotFound -} - -func (f *fakeConnector) CreateSnapshot(_ context.Context, volumeID string) (*cloud.Snapshot, error) { - name := "pvc-vol-snap-1" // Always use the same name for test - if snap, ok := f.snapshotsByName[name]; ok && snap.VolumeID != volumeID { - return nil, errors.New("snapshot name conflict: already exists for a different source volume") + 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: "snap-" + volumeID, + ID: id, Name: name, DomainID: "fake-domain", ZoneID: zoneID, VolumeID: volumeID, CreatedAt: "2025-07-07T16:13:06-0700", } - f.snapshotsByName[name] = newSnap - f.snapshot = newSnap + f.snapshotsByID[newSnap.ID] = newSnap + f.snapshotsByName[name] = append(f.snapshotsByName[name], newSnap) return newSnap, nil } -func (f *fakeConnector) DeleteSnapshot(_ context.Context, _ string) error { - return 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) GetSnapshotByName(_ context.Context, name string) (*cloud.Snapshot, error) { if name == "" { return nil, errors.New("invalid snapshot name: empty string") } - if snap, ok := f.snapshotsByName[name]; ok { - return snap, nil + 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) { + var result []*cloud.Snapshot + if snapshotID != "" { + if snap, ok := f.snapshotsByID[snapshotID]; ok { + result = append(result, snap) + } + return result, nil + } + if volumeID != "" { + for _, snap := range f.snapshotsByID { + if snap.VolumeID == volumeID { + result = append(result, snap) + } + } + return result, nil + } + 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 + } + // Remove from snapshotsByID + delete(f.snapshotsByID, snapshotID) + // Remove from snapshotsByName + 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 +} diff --git a/pkg/cloud/snapshots.go b/pkg/cloud/snapshots.go index 4909045..e84e601 100644 --- a/pkg/cloud/snapshots.go +++ b/pkg/cloud/snapshots.go @@ -45,12 +45,15 @@ func (c *client) GetSnapshotByID(ctx context.Context, snapshotID string) (*Snaps return &s, nil } -func (c *client) CreateSnapshot(ctx context.Context, volumeID string) (*Snapshot, error) { +func (c *client) CreateSnapshot(ctx context.Context, volumeID, name string) (*Snapshot, error) { logger := klog.FromContext(ctx) p := c.Snapshot.NewCreateSnapshotParams(volumeID) - + if name != "" { + p.SetName(name) + } logger.V(2).Info("CloudStack API call", "command", "CreateSnapshot", "params", map[string]string{ "volumeid": volumeID, + "name": name, }) snapshot, err := c.Snapshot.CreateSnapshot(p) @@ -118,3 +121,44 @@ func (c *client) GetSnapshotByName(ctx context.Context, name string) (*Snapshot, } return &s, nil } + +func (c *client) ListSnapshots(ctx context.Context, volumeID, snapshotID string) ([]*Snapshot, error) { + logger := klog.FromContext(ctx) + p := c.Snapshot.NewListSnapshotsParams() + if snapshotID != "" { + p.SetId(snapshotID) + } + if volumeID != "" { + p.SetVolumeid(volumeID) + } + if c.projectID != "" { + p.SetProjectid(c.projectID) + } + logger.V(2).Info("CloudStack API call", "command", "ListSnapshots", "params", map[string]string{ + "id": snapshotID, + "volumeid": volumeID, + "projectid": c.projectID, + }) + l, err := c.Snapshot.ListSnapshots(p) + if err != nil { + return nil, err + } + if l.Count == 0 { + return []*Snapshot{}, nil + } + var result []*Snapshot + for _, snapshot := range l.Snapshots { + s := &Snapshot{ + ID: snapshot.Id, + Name: snapshot.Name, + Size: snapshot.Virtualsize, + DomainID: snapshot.Domainid, + ProjectID: snapshot.Projectid, + ZoneID: snapshot.Zoneid, + VolumeID: snapshot.Volumeid, + CreatedAt: snapshot.Created, + } + result = append(result, s) + } + return result, nil +} diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index ac645d3..79ef37c 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "math/rand" + "strconv" "time" "github.com/container-storage-interface/spec/lib/go/csi" @@ -338,15 +339,6 @@ func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS return nil, status.Error(codes.InvalidArgument, "SourceVolumeId missing in request") } - // Check for existing snapshot with same name but different source volume ID - if req.GetName() != "" { - // check if the name matches and volumeID differs - existingSnap, err := cs.connector.GetSnapshotByName(ctx, req.GetName()) - if err == nil && existingSnap.VolumeID != volumeID { - return nil, status.Errorf(codes.AlreadyExists, "Snapshot with name %s already exists for a different source volume", req.GetName()) - } - } - volume, err := cs.connector.GetVolumeByID(ctx, volumeID) if err != nil { if err.Error() == "invalid volume ID: empty string" { @@ -355,12 +347,13 @@ func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS if errors.Is(err, cloud.ErrNotFound) { return nil, status.Errorf(codes.NotFound, "Volume %v not found", volumeID) } - // Error with CloudStack return nil, status.Errorf(codes.Internal, "Error %v", err) } klog.V(4).Infof("CreateSnapshot of volume: %s", volume.ID) - snapshot, err := cs.connector.CreateSnapshot(ctx, volume.ID) - if err != nil { + snapshot, err := cs.connector.CreateSnapshot(ctx, volume.ID, req.GetName()) + if errors.Is(err, cloud.ErrAlreadyExists) { + return nil, status.Errorf(codes.AlreadyExists, "Snapshot name conflict: already exists for a different source volume") + } else if err != nil { return nil, status.Errorf(codes.Internal, "Failed to create snapshot for volume %s: %v", volume.ID, err.Error()) } @@ -369,7 +362,6 @@ func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS return nil, status.Errorf(codes.Internal, "Failed to parse snapshot creation time: %v", err) } - // Convert to Timestamp protobuf ts := timestamppb.New(t) resp := &csi.CreateSnapshotResponse{ @@ -378,37 +370,53 @@ func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS SourceVolumeId: volume.ID, CreationTime: ts, ReadyToUse: true, - // We leave the optional SizeBytes field unset as the size of a block storage snapshot is the size of the difference to the volume or previous snapshots, k8s however expects the size to be the size of the restored volume. }, } return resp, nil - } func (cs *controllerServer) ListSnapshots(ctx context.Context, req *csi.ListSnapshotsRequest) (*csi.ListSnapshotsResponse, error) { entries := []*csi.ListSnapshotsResponse_Entry{} - if req.GetSnapshotId() != "" { - snap, err := cs.connector.GetSnapshotByID(ctx, req.GetSnapshotId()) - if err == nil && snap != nil { - t, _ := time.Parse("2006-01-02T15:04:05-0700", snap.CreatedAt) - ts := timestamppb.New(t) - entry := &csi.ListSnapshotsResponse_Entry{ - Snapshot: &csi.Snapshot{ - SnapshotId: snap.ID, - SourceVolumeId: snap.VolumeID, - CreationTime: ts, - ReadyToUse: true, - }, - } - entries = append(entries, entry) - return &csi.ListSnapshotsResponse{Entries: entries}, nil - } - // If not found, return empty list - return &csi.ListSnapshotsResponse{Entries: []*csi.ListSnapshotsResponse_Entry{}}, nil + snapshots, err := cs.connector.ListSnapshots(ctx, req.GetSourceVolumeId(), req.GetSnapshotId()) + if err != nil { + return nil, status.Errorf(codes.Internal, "Failed to list snapshots: %v", err) } - return &csi.ListSnapshotsResponse{Entries: entries}, nil + // Pagination logic + start := 0 + if req.StartingToken != "" { + var err error + start, err = strconv.Atoi(req.StartingToken) + if err != nil || start < 0 || start > len(snapshots) { + return nil, status.Error(codes.Aborted, "Invalid startingToken") + } + } + maxEntries := int(req.MaxEntries) + end := len(snapshots) + if maxEntries > 0 && start+maxEntries < end { + end = start + maxEntries + } + nextToken := "" + if end < len(snapshots) { + nextToken = strconv.Itoa(end) + } + + for i := start; i < end; i++ { + snap := snapshots[i] + t, _ := time.Parse("2006-01-02T15:04:05-0700", snap.CreatedAt) + ts := timestamppb.New(t) + entry := &csi.ListSnapshotsResponse_Entry{ + Snapshot: &csi.Snapshot{ + SnapshotId: snap.ID, + SourceVolumeId: snap.VolumeID, + CreationTime: ts, + ReadyToUse: true, + }, + } + entries = append(entries, entry) + } + return &csi.ListSnapshotsResponse{Entries: entries, NextToken: nextToken}, nil } func (cs *controllerServer) DeleteSnapshot(ctx context.Context, req *csi.DeleteSnapshotRequest) (*csi.DeleteSnapshotResponse, error) { @@ -420,19 +428,14 @@ func (cs *controllerServer) DeleteSnapshot(ctx context.Context, req *csi.DeleteS klog.V(4).Infof("DeleteSnapshot for snapshotID: %s", snapshotID) - snapshot, err := cs.connector.GetSnapshotByID(ctx, snapshotID) + err := cs.connector.DeleteSnapshot(ctx, snapshotID) if errors.Is(err, cloud.ErrNotFound) { - return nil, status.Errorf(codes.NotFound, "Snapshot %v not found", snapshotID) + // Per CSI spec, return OK if snapshot does not exist + return &csi.DeleteSnapshotResponse{}, nil } else if err != nil { - // Error with CloudStack return nil, status.Errorf(codes.Internal, "Error %v", err) } - err = cs.connector.DeleteSnapshot(ctx, snapshot.ID) - if err != nil && !errors.Is(err, cloud.ErrNotFound) { - return nil, status.Errorf(codes.Internal, "Cannot delete snapshot %s: %s", snapshotID, err.Error()) - } - return &csi.DeleteSnapshotResponse{}, nil } From b0c66ebee3aab341d6f827e3d43a71b35160927d Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 2 Oct 2025 13:43:16 -0400 Subject: [PATCH 43/48] fix lint --- pkg/cloud/fake/fake.go | 2 +- pkg/mount/mount.go | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/cloud/fake/fake.go b/pkg/cloud/fake/fake.go index 079b0ec..005010d 100644 --- a/pkg/cloud/fake/fake.go +++ b/pkg/cloud/fake/fake.go @@ -139,7 +139,7 @@ func (f *fakeConnector) ExpandVolume(_ context.Context, volumeID string, newSize return cloud.ErrNotFound } -func (f *fakeConnector) CreateVolumeFromSnapshot(_ context.Context, zoneID, name, projectID, snapshotID string, sizeInGB int64) (*cloud.Volume, error) { +func (f *fakeConnector) CreateVolumeFromSnapshot(_ context.Context, zoneID, name, _, snapshotID string, sizeInGB int64) (*cloud.Volume, error) { vol := &cloud.Volume{ ID: "fake-vol-from-snap-" + name, Name: name, diff --git a/pkg/mount/mount.go b/pkg/mount/mount.go index 7e2ca6e..056f4a1 100644 --- a/pkg/mount/mount.go +++ b/pkg/mount/mount.go @@ -182,11 +182,13 @@ func (m *mounter) getDevicePathForVMware(ctx context.Context, volumeID string) ( if err == nil && isBlock { if m.verifyDevice(ctx, devicePath, volumeID) { logger.V(4).Info("Found and verified VMware device", "devicePath", devicePath, "volumeID", volumeID) + return devicePath, nil } } } } + return "", fmt.Errorf("device not found for volume %s", volumeID) } @@ -196,6 +198,7 @@ func (m *mounter) verifyDevice(ctx context.Context, devicePath string, volumeID size, err := m.GetBlockSizeBytes(devicePath) if err != nil { logger.V(4).Info("Failed to get device size", "devicePath", devicePath, "volumeID", volumeID, "error", err) + return false } logger.V(5).Info("Device size retrieved", "devicePath", devicePath, "volumeID", volumeID, "sizeBytes", size) @@ -203,16 +206,19 @@ func (m *mounter) verifyDevice(ctx context.Context, devicePath string, volumeID mounted, err := m.isDeviceMounted(devicePath) if err != nil { logger.V(4).Info("Failed to check if device is mounted", "devicePath", devicePath, "volumeID", volumeID, "error", err) + return false } if mounted { logger.V(4).Info("Device is already mounted", "devicePath", devicePath, "volumeID", volumeID) + return false } props, err := m.getDeviceProperties(devicePath) if err != nil { logger.V(4).Info("Failed to get device properties", "devicePath", devicePath, "volumeID", volumeID, "error", err) + return false } logger.V(5).Info("Device properties retrieved", "devicePath", devicePath, "volumeID", volumeID, "properties", props) @@ -226,8 +232,10 @@ func (m *mounter) isDeviceMounted(devicePath string) (bool, error) { if strings.Contains(err.Error(), "exit status 1") { return false, nil } + return false, err } + return len(output) > 0, nil } From 3a06c70bf62b51471c65642e2bc281d4264e899c Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 2 Oct 2025 13:54:54 -0400 Subject: [PATCH 44/48] fix lint --- pkg/cloud/cloud.go | 2 ++ pkg/cloud/fake/fake.go | 15 ++++++++++++--- pkg/cloud/snapshots.go | 2 ++ pkg/cloud/vms.go | 1 + pkg/driver/controller.go | 12 +++++++++--- pkg/mount/mount.go | 4 ++++ 6 files changed, 30 insertions(+), 6 deletions(-) diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go index 7c1939e..98e8b6b 100644 --- a/pkg/cloud/cloud.go +++ b/pkg/cloud/cloud.go @@ -10,6 +10,8 @@ import ( ) // Interface is the CloudStack client interface. +// +//revive:disable:interfacebloat type Interface interface { GetNodeInfo(ctx context.Context, vmName string) (*VM, error) GetVMByID(ctx context.Context, vmID string) (*VM, error) diff --git a/pkg/cloud/fake/fake.go b/pkg/cloud/fake/fake.go index 005010d..93ad83a 100644 --- a/pkg/cloud/fake/fake.go +++ b/pkg/cloud/fake/fake.go @@ -139,7 +139,7 @@ func (f *fakeConnector) ExpandVolume(_ context.Context, volumeID string, newSize return cloud.ErrNotFound } -func (f *fakeConnector) CreateVolumeFromSnapshot(_ context.Context, zoneID, name, _, snapshotID string, sizeInGB int64) (*cloud.Volume, error) { +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, @@ -149,6 +149,7 @@ func (f *fakeConnector) CreateVolumeFromSnapshot(_ context.Context, zoneID, name } f.volumesByID[vol.ID] = *vol f.volumesByName[vol.Name] = *vol + return vol, nil } @@ -161,6 +162,7 @@ func (f *fakeConnector) CreateSnapshot(_ context.Context, volumeID, name string) // Allow multiple snapshots with the same name for the same volume continue } + // Name conflict: same name, different volume return nil, cloud.ErrAlreadyExists } @@ -175,6 +177,7 @@ func (f *fakeConnector) CreateSnapshot(_ context.Context, volumeID, name string) } f.snapshotsByID[newSnap.ID] = newSnap f.snapshotsByName[name] = append(f.snapshotsByName[name], newSnap) + return newSnap, nil } @@ -183,6 +186,7 @@ func (f *fakeConnector) GetSnapshotByID(_ context.Context, snapshotID string) (* if ok { return snap, nil } + return nil, cloud.ErrNotFound } @@ -194,6 +198,7 @@ func (f *fakeConnector) GetSnapshotByName(_ context.Context, name string) (*clou if ok && len(snaps) > 0 { return snaps[0], nil // Return the first for compatibility } + return nil, cloud.ErrNotFound } @@ -204,6 +209,7 @@ func (f *fakeConnector) ListSnapshots(_ context.Context, volumeID, snapshotID st if snap, ok := f.snapshotsByID[snapshotID]; ok { result = append(result, snap) } + return result, nil } if volumeID != "" { @@ -212,11 +218,13 @@ func (f *fakeConnector) ListSnapshots(_ context.Context, volumeID, snapshotID st result = append(result, snap) } } + return result, nil } for _, snap := range f.snapshotsByID { result = append(result, snap) } + return result, nil } @@ -225,9 +233,9 @@ func (f *fakeConnector) DeleteSnapshot(_ context.Context, snapshotID string) err if !ok { return cloud.ErrNotFound } - // Remove from snapshotsByID + delete(f.snapshotsByID, snapshotID) - // Remove from snapshotsByName + name := snap.Name snaps := f.snapshotsByName[name] for i, s := range snaps { @@ -236,5 +244,6 @@ func (f *fakeConnector) DeleteSnapshot(_ context.Context, snapshotID string) err break } } + return nil } diff --git a/pkg/cloud/snapshots.go b/pkg/cloud/snapshots.go index e84e601..757f34f 100644 --- a/pkg/cloud/snapshots.go +++ b/pkg/cloud/snapshots.go @@ -71,6 +71,7 @@ func (c *client) CreateSnapshot(ctx context.Context, volumeID, name string) (*Sn VolumeID: snapshot.Volumeid, CreatedAt: snapshot.Created, } + return &snap, nil } @@ -160,5 +161,6 @@ func (c *client) ListSnapshots(ctx context.Context, volumeID, snapshotID string) } result = append(result, s) } + return result, nil } diff --git a/pkg/cloud/vms.go b/pkg/cloud/vms.go index 2e98f64..354f3a2 100644 --- a/pkg/cloud/vms.go +++ b/pkg/cloud/vms.go @@ -29,6 +29,7 @@ func (c *client) GetVMByID(ctx context.Context, vmID string) (*VM, error) { } vm := l.VirtualMachines[0] logger.V(2).Info("Returning VM", "vmID", vm.Id, "zoneID", vm.Zoneid) + return &VM{ ID: vm.Id, ZoneID: vm.Zoneid, diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index 79ef37c..8617835 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -166,6 +166,7 @@ func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol }, }, } + return resp, nil } @@ -226,6 +227,7 @@ func printVolumeAsJSON(vol *csi.CreateVolumeRequest) { b, err := json.MarshalIndent(vol, "", " ") if err != nil { klog.Errorf("Failed to marshal CreateVolumeRequest to JSON: %v", err) + return } klog.V(5).Infof("CreateVolumeRequest as JSON:\n%s", string(b)) @@ -347,8 +349,10 @@ func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS if errors.Is(err, cloud.ErrNotFound) { return nil, status.Errorf(codes.NotFound, "Volume %v not found", volumeID) } + return nil, status.Errorf(codes.Internal, "Error %v", err) } + klog.V(4).Infof("CreateSnapshot of volume: %s", volume.ID) snapshot, err := cs.connector.CreateSnapshot(ctx, volume.ID, req.GetName()) if errors.Is(err, cloud.ErrAlreadyExists) { @@ -372,6 +376,7 @@ func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS ReadyToUse: true, }, } + return resp, nil } @@ -385,14 +390,14 @@ func (cs *controllerServer) ListSnapshots(ctx context.Context, req *csi.ListSnap // Pagination logic start := 0 - if req.StartingToken != "" { + if req.GetStartingToken() != "" { var err error - start, err = strconv.Atoi(req.StartingToken) + start, err = strconv.Atoi(req.GetStartingToken()) if err != nil || start < 0 || start > len(snapshots) { return nil, status.Error(codes.Aborted, "Invalid startingToken") } } - maxEntries := int(req.MaxEntries) + maxEntries := int(req.GetMaxEntries()) end := len(snapshots) if maxEntries > 0 && start+maxEntries < end { end = start + maxEntries @@ -416,6 +421,7 @@ func (cs *controllerServer) ListSnapshots(ctx context.Context, req *csi.ListSnap } entries = append(entries, entry) } + return &csi.ListSnapshotsResponse{Entries: entries, NextToken: nextToken}, nil } diff --git a/pkg/mount/mount.go b/pkg/mount/mount.go index 056f4a1..827e73c 100644 --- a/pkg/mount/mount.go +++ b/pkg/mount/mount.go @@ -95,6 +95,7 @@ func (m *mounter) GetDevicePath(ctx context.Context, volumeID string) (string, e if path != "" { devicePath = path logger.V(4).Info("Device path found", "volumeID", volumeID, "devicePath", path) + return true, nil } m.probeVolume(ctx) @@ -142,6 +143,7 @@ func (m *mounter) getDevicePathBySerialID(ctx context.Context, volumeID string) } if !os.IsNotExist(err) { logger.Error(err, "Failed to stat device path", "path", source) + return "", err } } @@ -161,11 +163,13 @@ func (m *mounter) getDevicePathForXenServer(ctx context.Context, volumeID string if err == nil && isBlock { if m.verifyDevice(ctx, devicePath, volumeID) { logger.V(4).Info("Found and verified XenServer device", "devicePath", devicePath, "volumeID", volumeID) + return devicePath, nil } } } } + return "", fmt.Errorf("device not found for volume %s", volumeID) } From a8e45cc211e393c54478059baf596805367719b3 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 2 Oct 2025 14:01:03 -0400 Subject: [PATCH 45/48] fix lint --- pkg/cloud/cloud.go | 4 ++-- pkg/cloud/fake/fake.go | 13 ++++++++++--- pkg/cloud/snapshots.go | 3 ++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go index 98e8b6b..b683f88 100644 --- a/pkg/cloud/cloud.go +++ b/pkg/cloud/cloud.go @@ -10,8 +10,8 @@ import ( ) // Interface is the CloudStack client interface. -// -//revive:disable:interfacebloat + +//nolint:interfacebloat type Interface interface { GetNodeInfo(ctx context.Context, vmName string) (*VM, error) GetVMByID(ctx context.Context, vmID string) (*VM, error) diff --git a/pkg/cloud/fake/fake.go b/pkg/cloud/fake/fake.go index 93ad83a..4870525 100644 --- a/pkg/cloud/fake/fake.go +++ b/pkg/cloud/fake/fake.go @@ -204,23 +204,29 @@ func (f *fakeConnector) GetSnapshotByName(_ context.Context, name string) (*clou // ListSnapshots returns all matching snapshots; pagination must be handled by the controller. func (f *fakeConnector) ListSnapshots(_ context.Context, volumeID, snapshotID string) ([]*cloud.Snapshot, error) { - var result []*cloud.Snapshot 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) } @@ -241,6 +247,7 @@ func (f *fakeConnector) DeleteSnapshot(_ context.Context, snapshotID string) err for i, s := range snaps { if s.ID == snapshotID { f.snapshotsByName[name] = append(snaps[:i], snaps[i+1:]...) + break } } diff --git a/pkg/cloud/snapshots.go b/pkg/cloud/snapshots.go index 757f34f..d501021 100644 --- a/pkg/cloud/snapshots.go +++ b/pkg/cloud/snapshots.go @@ -120,6 +120,7 @@ func (c *client) GetSnapshotByName(ctx context.Context, name string) (*Snapshot, VolumeID: snapshot.Volumeid, CreatedAt: snapshot.Created, } + return &s, nil } @@ -147,7 +148,7 @@ func (c *client) ListSnapshots(ctx context.Context, volumeID, snapshotID string) if l.Count == 0 { return []*Snapshot{}, nil } - var result []*Snapshot + result := make([]*Snapshot, 0, l.Count) for _, snapshot := range l.Snapshots { s := &Snapshot{ ID: snapshot.Id, From 027f805d5c97200ecac00cd9b1690a3f7621b208 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 2 Oct 2025 14:09:07 -0400 Subject: [PATCH 46/48] lint failure fix --- pkg/cloud/fake/fake.go | 2 ++ pkg/driver/controller.go | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/cloud/fake/fake.go b/pkg/cloud/fake/fake.go index 4870525..781c12a 100644 --- a/pkg/cloud/fake/fake.go +++ b/pkg/cloud/fake/fake.go @@ -209,6 +209,7 @@ func (f *fakeConnector) ListSnapshots(_ context.Context, volumeID, snapshotID st if snap, ok := f.snapshotsByID[snapshotID]; ok { result = append(result, snap) } + return result, nil } if volumeID != "" { @@ -224,6 +225,7 @@ func (f *fakeConnector) ListSnapshots(_ context.Context, volumeID, snapshotID st result = append(result, snap) } } + return result, nil } result := make([]*cloud.Snapshot, 0, len(f.snapshotsByID)) diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index 8617835..f487010 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -737,14 +737,14 @@ func (cs *controllerServer) ControllerGetCapabilities(ctx context.Context, req * }, }, }, - &csi.ControllerServiceCapability{ + { Type: &csi.ControllerServiceCapability_Rpc{ Rpc: &csi.ControllerServiceCapability_RPC{ Type: csi.ControllerServiceCapability_RPC_CREATE_DELETE_SNAPSHOT, }, }, }, - &csi.ControllerServiceCapability{ + { Type: &csi.ControllerServiceCapability_Rpc{ Rpc: &csi.ControllerServiceCapability_RPC{ Type: csi.ControllerServiceCapability_RPC_LIST_SNAPSHOTS, From 9e33bc667f9d1606b6b4c1d3df35940b36de902c Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Tue, 7 Oct 2025 12:50:42 -0400 Subject: [PATCH 47/48] Add license headers --- cmd/cloudstack-csi-driver/main.go | 19 +++++++++++++++++++ cmd/cloudstack-csi-sc-syncer/main.go | 19 +++++++++++++++++++ pkg/cloud/cloud.go | 19 +++++++++++++++++++ pkg/cloud/config.go | 19 +++++++++++++++++++ pkg/cloud/fake/fake.go | 19 +++++++++++++++++++ pkg/cloud/metadata.go | 19 +++++++++++++++++++ pkg/cloud/node.go | 19 +++++++++++++++++++ pkg/cloud/snapshots.go | 19 +++++++++++++++++++ pkg/cloud/vms.go | 19 +++++++++++++++++++ pkg/cloud/volumes.go | 19 +++++++++++++++++++ pkg/cloud/zones.go | 19 +++++++++++++++++++ pkg/driver/constants.go | 19 +++++++++++++++++++ pkg/driver/controller.go | 19 +++++++++++++++++++ pkg/driver/controller_test.go | 19 +++++++++++++++++++ pkg/driver/driver.go | 19 +++++++++++++++++++ pkg/driver/identity.go | 19 +++++++++++++++++++ pkg/driver/node.go | 19 +++++++++++++++++++ pkg/driver/options.go | 19 +++++++++++++++++++ pkg/driver/topology.go | 19 +++++++++++++++++++ pkg/mount/fake.go | 19 +++++++++++++++++++ pkg/mount/mount.go | 19 +++++++++++++++++++ pkg/syncer/error.go | 19 +++++++++++++++++++ pkg/syncer/name.go | 19 +++++++++++++++++++ pkg/syncer/name_test.go | 19 +++++++++++++++++++ pkg/syncer/run.go | 19 +++++++++++++++++++ pkg/syncer/syncer.go | 19 +++++++++++++++++++ pkg/util/doc.go | 19 +++++++++++++++++++ pkg/util/endpoint.go | 19 +++++++++++++++++++ pkg/util/endpoint_test.go | 19 +++++++++++++++++++ pkg/util/gb.go | 19 +++++++++++++++++++ pkg/util/gb_test.go | 19 +++++++++++++++++++ test/e2e/run.sh | 19 +++++++++++++++++++ test/sanity/sanity_test.go | 19 +++++++++++++++++++ 33 files changed, 627 insertions(+) diff --git a/cmd/cloudstack-csi-driver/main.go b/cmd/cloudstack-csi-driver/main.go index 02d5732..60a4887 100644 --- a/cmd/cloudstack-csi-driver/main.go +++ b/cmd/cloudstack-csi-driver/main.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + // cloudstack-csi-driver binary. // // To get usage information: diff --git a/cmd/cloudstack-csi-sc-syncer/main.go b/cmd/cloudstack-csi-sc-syncer/main.go index 912bc2b..cb3d2f0 100644 --- a/cmd/cloudstack-csi-sc-syncer/main.go +++ b/cmd/cloudstack-csi-sc-syncer/main.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + // Small utility to synchronize CloudStack disk offerings to // Kubernetes storage classes. package main diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go index b683f88..19109a3 100644 --- a/pkg/cloud/cloud.go +++ b/pkg/cloud/cloud.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + // Package cloud contains CloudStack related // functions. package cloud diff --git a/pkg/cloud/config.go b/pkg/cloud/config.go index 70d15a3..d669aa2 100644 --- a/pkg/cloud/config.go +++ b/pkg/cloud/config.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package cloud import ( diff --git a/pkg/cloud/fake/fake.go b/pkg/cloud/fake/fake.go index 781c12a..6bbe583 100644 --- a/pkg/cloud/fake/fake.go +++ b/pkg/cloud/fake/fake.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + // Package fake provides a fake implementation of the cloud // connector interface, to be used in tests. package fake diff --git a/pkg/cloud/metadata.go b/pkg/cloud/metadata.go index 17a0c80..35ec6f8 100644 --- a/pkg/cloud/metadata.go +++ b/pkg/cloud/metadata.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package cloud import ( diff --git a/pkg/cloud/node.go b/pkg/cloud/node.go index c3eab50..ff63c29 100644 --- a/pkg/cloud/node.go +++ b/pkg/cloud/node.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package cloud import ( diff --git a/pkg/cloud/snapshots.go b/pkg/cloud/snapshots.go index d501021..fe2451f 100644 --- a/pkg/cloud/snapshots.go +++ b/pkg/cloud/snapshots.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package cloud import ( diff --git a/pkg/cloud/vms.go b/pkg/cloud/vms.go index 354f3a2..2df3c7f 100644 --- a/pkg/cloud/vms.go +++ b/pkg/cloud/vms.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package cloud import ( diff --git a/pkg/cloud/volumes.go b/pkg/cloud/volumes.go index 154e28b..69ad501 100644 --- a/pkg/cloud/volumes.go +++ b/pkg/cloud/volumes.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package cloud import ( diff --git a/pkg/cloud/zones.go b/pkg/cloud/zones.go index c137677..8cbb84d 100644 --- a/pkg/cloud/zones.go +++ b/pkg/cloud/zones.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package cloud import ( diff --git a/pkg/driver/constants.go b/pkg/driver/constants.go index 7775695..64a77ad 100644 --- a/pkg/driver/constants.go +++ b/pkg/driver/constants.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package driver // DriverName is the name of the CSI plugin. diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index f487010..b268ebd 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package driver import ( diff --git a/pkg/driver/controller_test.go b/pkg/driver/controller_test.go index bf9f3d1..a939f28 100644 --- a/pkg/driver/controller_test.go +++ b/pkg/driver/controller_test.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package driver import ( diff --git a/pkg/driver/driver.go b/pkg/driver/driver.go index 2d0f59b..feea753 100644 --- a/pkg/driver/driver.go +++ b/pkg/driver/driver.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + // Package driver provides the implementation of the CSI plugin. // // It contains the gRPC server implementation of CSI specification. diff --git a/pkg/driver/identity.go b/pkg/driver/identity.go index e265275..7d064da 100644 --- a/pkg/driver/identity.go +++ b/pkg/driver/identity.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package driver import ( diff --git a/pkg/driver/node.go b/pkg/driver/node.go index 8fd3f29..55fe23c 100644 --- a/pkg/driver/node.go +++ b/pkg/driver/node.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package driver import ( diff --git a/pkg/driver/options.go b/pkg/driver/options.go index f714f30..d0beb35 100644 --- a/pkg/driver/options.go +++ b/pkg/driver/options.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package driver import ( diff --git a/pkg/driver/topology.go b/pkg/driver/topology.go index bd6bfa7..e58caf6 100644 --- a/pkg/driver/topology.go +++ b/pkg/driver/topology.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package driver import ( diff --git a/pkg/mount/fake.go b/pkg/mount/fake.go index 8129033..c80ce8a 100644 --- a/pkg/mount/fake.go +++ b/pkg/mount/fake.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package mount import ( diff --git a/pkg/mount/mount.go b/pkg/mount/mount.go index 827e73c..9b7c8f6 100644 --- a/pkg/mount/mount.go +++ b/pkg/mount/mount.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + // Package mount provides utilities to detect, // format and mount storage devices. package mount diff --git a/pkg/syncer/error.go b/pkg/syncer/error.go index 4328fcc..46abb1c 100644 --- a/pkg/syncer/error.go +++ b/pkg/syncer/error.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package syncer import "fmt" diff --git a/pkg/syncer/name.go b/pkg/syncer/name.go index e6b001d..de12e0a 100644 --- a/pkg/syncer/name.go +++ b/pkg/syncer/name.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package syncer import ( diff --git a/pkg/syncer/name_test.go b/pkg/syncer/name_test.go index 82480d0..2cf3058 100644 --- a/pkg/syncer/name_test.go +++ b/pkg/syncer/name_test.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package syncer import ( diff --git a/pkg/syncer/run.go b/pkg/syncer/run.go index 8216a0b..6b95c3c 100644 --- a/pkg/syncer/run.go +++ b/pkg/syncer/run.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package syncer import ( diff --git a/pkg/syncer/syncer.go b/pkg/syncer/syncer.go index ddc0f8a..c7cbcf4 100644 --- a/pkg/syncer/syncer.go +++ b/pkg/syncer/syncer.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + // Package syncer provides the logic used by command line tool cloudstack-csi-sc-syncer. // // It provides functions to synchronize CloudStack disk offerings diff --git a/pkg/util/doc.go b/pkg/util/doc.go index e1beef7..af89a03 100644 --- a/pkg/util/doc.go +++ b/pkg/util/doc.go @@ -1,2 +1,21 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + // Package util provides shared utility functions. package util diff --git a/pkg/util/endpoint.go b/pkg/util/endpoint.go index cb46ddf..f7a7269 100644 --- a/pkg/util/endpoint.go +++ b/pkg/util/endpoint.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package util import ( diff --git a/pkg/util/endpoint_test.go b/pkg/util/endpoint_test.go index 93e2878..ec9f949 100644 --- a/pkg/util/endpoint_test.go +++ b/pkg/util/endpoint_test.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package util import ( diff --git a/pkg/util/gb.go b/pkg/util/gb.go index 1523872..7920bfa 100644 --- a/pkg/util/gb.go +++ b/pkg/util/gb.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package util // RoundUpBytesToGB converts a size given in bytes to GB with diff --git a/pkg/util/gb_test.go b/pkg/util/gb_test.go index bde5ad8..8cb6c96 100644 --- a/pkg/util/gb_test.go +++ b/pkg/util/gb_test.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + package util import ( diff --git a/test/e2e/run.sh b/test/e2e/run.sh index e4aa5df..6c3684c 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -1,3 +1,22 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + #!/bin/bash cd "$(dirname "$0")" || exit diff --git a/test/sanity/sanity_test.go b/test/sanity/sanity_test.go index c1ad0d0..9725ec5 100644 --- a/test/sanity/sanity_test.go +++ b/test/sanity/sanity_test.go @@ -1,3 +1,22 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + //go:build sanity package sanity From 1edb295fc5194e1b15a7b8cb3bba4bf7e901471b Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Tue, 7 Oct 2025 13:13:33 -0400 Subject: [PATCH 48/48] update image version on release (tags) --- .github/workflows/release.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d2aac42..929f161 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -87,9 +87,9 @@ jobs: echo "---" >> manifest.yaml cat deploy/k8s/csidriver.yaml >> manifest.yaml echo "---" >> manifest.yaml - sed -E "s|image: +cloudstack-csi-driver|image: ${REGISTRY_NAME}/cloudstack-csi-driver:${VERSION}|" deploy/k8s/controller-deployment.yaml >> manifest.yaml + sed -E "s|(image: +${REGISTRY_NAME}/cloudstack-csi-driver)(:[^ ]+)?|\\1:${VERSION}|" deploy/k8s/controller-deployment.yaml >> manifest.yaml echo "---" >> manifest.yaml - sed -E "s|image: +cloudstack-csi-driver|image: ${REGISTRY_NAME}/cloudstack-csi-driver:${VERSION}|" deploy/k8s/node-daemonset.yaml >> manifest.yaml + sed -E "s|(image: +${REGISTRY_NAME}/cloudstack-csi-driver)(:[^ ]+)?|\\1:${VERSION}|" deploy/k8s/node-daemonset.yaml >> manifest.yaml echo "---" >> manifest.yaml cat deploy/k8s/volume-snapshot-class.yaml >> manifest.yaml