diff --git a/docs/encrypted-drives.md b/docs/encrypted-drives.md index d31b342a..4dd75dd3 100644 --- a/docs/encrypted-drives.md +++ b/docs/encrypted-drives.md @@ -1,4 +1,86 @@ -## πŸ”’ Encrypted Drives using LUKS +## πŸ“œ Table of Contents + +1. [πŸ”’ Encrypted Block Storage](#encrypted-block-storage) + - [Example StorageClass](#example-storageclass-with-blockstorage) + - [Example PVC](#example-pvc-for-blockstorage) +2. [πŸ”’ Encrypted Drives using LUKS](#encrypted-drives-using-luks) + - [Example StorageClass with LUKS](#example-storageclass-with-luks) + - [Example PVC with LUKS](#example-pvc-with-luks) + +**NOTE**: LUKS encryption allows users to bring their own keys and manage them, while BlockStorage encryption is managed by Linode and it's automatically handled on the backend. + +### Encrypted Block Storage + +**Notes**: + +1. **Setting Up Encryption**: In the provided StorageClasses, encryption is activated by specifying `linodebs.csi.linode.com/encrypted: "true"` in the `parameters` field. This signals the CSI driver to provision volumes with encryption enabled, provided encryption is supported in the specified region. +2. **Retention and Expansion Options**: + - The `linode-block-storage-encrypted` StorageClass uses the default `Delete` reclaim policy, meaning that volumes created with this StorageClass will be deleted when the associated PVC is deleted. + - In contrast, the `linode-block-storage-retain-encrypted` StorageClass uses the `Retain` policy. This allows the volume to persist even after the PVC is deleted, ensuring data is preserved until manually removed. + - Both StorageClasses support volume expansion through the `allowVolumeExpansion: true` setting, allowing users to resize volumes as needed without data loss. + +3. **Default StorageClass Annotation**: By marking both StorageClasses with `storageclass.kubernetes.io/is-default-class: "true"`, they’re eligible to act as default classes. However, Kubernetes will only treat one StorageClass as the actual default. Consider applying this annotation only to the preferred default StorageClass. +4. **Region Compatibility**: Ensure that encryption is supported in the Linode region where the volumes will be created. If encryption is not available in a specific region, the CSI driver will return an error. + - To check if the region has encryption capability visit https://techdocs.akamai.com/linode-api/reference/get-regions + - For your specific region, check the `capabilities` and see if `Block Storage Encryption` is listed in it. +5. **Usage in PersistentVolumeClaims (PVCs)**: Use the `storageClassName` field in a PVC to reference the desired StorageClass (`linode-block-storage-encrypted` or `linode-block-storage-retain-encrypted`). Each PVC will inherit the encryption settings defined in the referenced StorageClass. + +#### Example StorageClass with BlockStorage + +```yaml +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: linode-block-storage-encrypted + namespace: kube-system +parameters: + linodebs.csi.linode.com/encrypted: "true" +allowVolumeExpansion: true +provisioner: linodebs.csi.linode.com +--- +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: linode-block-storage-retain-encrypted + namespace: kube-system +parameters: + linodebs.csi.linode.com/encrypted: "true" +allowVolumeExpansion: true +provisioner: linodebs.csi.linode.com +reclaimPolicy: Retain +``` + +#### Example PVC for BlockStorage + +```yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: csi-example-pvc-encrypted +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + storageClassName: linode-block-storage-encrypted +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: csi-example-pvc-encrypted-retain +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + storageClassName: linode-block-storage-retain-encrypted +``` + +--- + +### Encrypted Drives using LUKS **Notes:** @@ -12,7 +94,7 @@ The CSI driver is careful to otherwise keep the secret on an ephemeral tmpfs mount and otherwise refuses to continue. -#### πŸ”‘ Example StorageClass +#### Example StorageClass with LUKS > [!TIP] > To use an encryption key per PVC you can make a new StorageClass/Secret @@ -45,7 +127,7 @@ stringData: luksKey: "SECRETGOESHERE" ``` -#### πŸ“ Example PVC +#### Example PVC with LUKS ```yaml apiVersion: v1 @@ -72,3 +154,4 @@ spec: storage: 10Gi storageClassName: linode-block-storage-retain-luks ``` +--- \ No newline at end of file diff --git a/helm-chart/csi-driver/values.yaml b/helm-chart/csi-driver/values.yaml index a552df2a..7554b59b 100644 --- a/helm-chart/csi-driver/values.yaml +++ b/helm-chart/csi-driver/values.yaml @@ -10,7 +10,6 @@ enableMetrics: false # default metrics address port metricsPort: 8081 - # (OPTIONAL) Label prefix for the Linode Block Storage volumes created by this driver. volumeLabelPrefix: "" diff --git a/internal/driver/controllerserver.go b/internal/driver/controllerserver.go index 70e7a083..8460d3cf 100644 --- a/internal/driver/controllerserver.go +++ b/internal/driver/controllerserver.go @@ -75,7 +75,7 @@ func (cs *ControllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol // Prepare the volume parameters such as name and SizeGB from the request. // This step may involve calculations or adjustments based on the request's content. - volName, sizeGB, size, err := cs.prepareVolumeParams(ctx, req) + params, err := cs.prepareVolumeParams(ctx, req) if err != nil { metrics.RecordMetrics(metrics.ControllerCreateVolumeTotal, metrics.ControllerCreateVolumeDuration, metrics.Failed, functionStartTime) return &csi.CreateVolumeResponse{}, err @@ -93,7 +93,7 @@ func (cs *ControllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol } // Create the volume - vol, err := cs.createAndWaitForVolume(ctx, volName, sizeGB, req.GetParameters()[VolumeTags], sourceVolInfo, accessibilityRequirements) + vol, err := cs.createAndWaitForVolume(ctx, params.VolumeName, req.GetParameters(), params.EncryptionStatus, params.TargetSizeGB, sourceVolInfo, params.Region) if err != nil { metrics.RecordMetrics(metrics.ControllerCreateVolumeTotal, metrics.ControllerCreateVolumeDuration, metrics.Failed, functionStartTime) return &csi.CreateVolumeResponse{}, err @@ -103,7 +103,7 @@ func (cs *ControllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol volContext := cs.createVolumeContext(ctx, req, vol) // Prepare and return response - resp := cs.prepareCreateVolumeResponse(ctx, vol, size, volContext, sourceVolInfo, contentSource) + resp := cs.prepareCreateVolumeResponse(ctx, vol, params.Size, volContext, sourceVolInfo, contentSource) // Record function completion metrics.RecordMetrics(metrics.ControllerCreateVolumeTotal, metrics.ControllerCreateVolumeDuration, metrics.Completed, functionStartTime) diff --git a/internal/driver/controllerserver_helper.go b/internal/driver/controllerserver_helper.go index 48eecbfe..251b6877 100644 --- a/internal/driver/controllerserver_helper.go +++ b/internal/driver/controllerserver_helper.go @@ -80,8 +80,21 @@ const ( // devicePathKey is the key used in the publish context map when a volume is // published/attached to an instance. devicePathKey = "devicePath" + + // volumeEncryption is the key used in the context map for encryption + VolumeEncryption = Name + "/encrypted" ) +// Struct to return volume parameters when prepareVolumeParams is called + +type VolumeParams struct { + VolumeName string + TargetSizeGB int + Size int64 + EncryptionStatus string + Region string +} + // canAttach indicates whether or not another volume can be attached to the // Linode with the given ID. // @@ -186,9 +199,9 @@ func (cs *ControllerServer) getContentSourceVolume(ctx context.Context, contentS // attemptCreateLinodeVolume creates a Linode volume while ensuring idempotency. // It checks for existing volumes with the same label and either returns the existing // volume or creates a new one, optionally cloning from a source volume. -func (cs *ControllerServer) attemptCreateLinodeVolume(ctx context.Context, label string, sizeGB int, tags string, sourceVolume *linodevolumes.LinodeVolumeKey, accessibilityRequirements *csi.TopologyRequirement) (*linodego.Volume, error) { +func (cs *ControllerServer) attemptCreateLinodeVolume(ctx context.Context, label, tags, volumeEncryption string, sizeGB int, sourceVolume *linodevolumes.LinodeVolumeKey, region string) (*linodego.Volume, error) { log := logger.GetLogger(ctx) - log.V(4).Info("Attempting to create Linode volume", "label", label, "sizeGB", sizeGB, "tags", tags) + log.V(4).Info("Attempting to create Linode volume", "label", label, "sizeGB", sizeGB, "tags", tags, "encryptionStatus", volumeEncryption, "region", region) // List existing volumes with the specified label jsonFilter, err := json.Marshal(map[string]string{"label": label}) @@ -216,7 +229,7 @@ func (cs *ControllerServer) attemptCreateLinodeVolume(ctx context.Context, label return cs.cloneLinodeVolume(ctx, label, sourceVolume.VolumeID) } - return cs.createLinodeVolume(ctx, label, sizeGB, tags, accessibilityRequirements) + return cs.createLinodeVolume(ctx, label, tags, volumeEncryption, sizeGB, region) } // Helper function to extract region from topology @@ -237,24 +250,16 @@ func getRegionFromTopology(requirements *csi.TopologyRequirement) string { // createLinodeVolume creates a new Linode volume with the specified label, size, and tags. // It returns the created volume or an error if the creation fails. -func (cs *ControllerServer) createLinodeVolume(ctx context.Context, label string, sizeGB int, tags string, accessibilityRequirements *csi.TopologyRequirement) (*linodego.Volume, error) { +func (cs *ControllerServer) createLinodeVolume(ctx context.Context, label, tags, encryptionStatus string, sizeGB int, region string) (*linodego.Volume, error) { log := logger.GetLogger(ctx) - log.V(4).Info("Creating Linode volume", "label", label, "sizeGB", sizeGB, "tags", tags) - - // Get the region from req.AccessibilityRequirements if it exists. Fall back to the controller's metadata region if not specified. - region := cs.metadata.Region - if accessibilityRequirements != nil { - if topologyRegion := getRegionFromTopology(accessibilityRequirements); topologyRegion != "" { - log.V(4).Info("Using region from topology", "region", topologyRegion) - region = topologyRegion - } - } + log.V(4).Info("Creating Linode volume", "label", label, "sizeGB", sizeGB, "tags", tags, "encryptionStatus", encryptionStatus, "region", region) // Prepare the volume creation request with region, label, and size. volumeReq := linodego.VolumeCreateOptions{ - Region: region, - Label: label, - Size: sizeGB, + Region: region, + Label: label, + Size: sizeGB, + Encryption: encryptionStatus, } // If tags are provided, split them into a slice for the request. @@ -272,6 +277,30 @@ func (cs *ControllerServer) createLinodeVolume(ctx context.Context, label string return result, nil } +// isEncryptionSupported is a helper function that checks if the specified region supports volume encryption. +// It returns true or false based on the support for encryption in that region. +func (cs *ControllerServer) isEncryptionSupported(ctx context.Context, region string) (bool, error) { + log := logger.GetLogger(ctx) + log.V(4).Info("Checking if encryption is supported for region", "region", region) + + // Get the specifications of specified region from Linode API + regionDetails, err := cs.client.GetRegion(ctx, region) + if err != nil { + return false, errInternal("failed to fetch region %s: %v", region, err) + } + + // Check if encryption is supported in the specified region + for _, capability := range regionDetails.Capabilities { + if capability == "Block Storage Encryption" { + return true, nil + } + } + + // If the region was found but does not support encryption, return false + log.V(4).Info("Encryption not supported in the specified region", "region", region) + return false, nil +} + // cloneLinodeVolume clones a Linode volume using the specified source ID and label. // It returns the cloned volume or an error if the cloning fails. func (cs *ControllerServer) cloneLinodeVolume(ctx context.Context, label string, sourceID int) (*linodego.Volume, error) { @@ -403,25 +432,60 @@ func (cs *ControllerServer) validateCreateVolumeRequest(ctx context.Context, req // prepareVolumeParams prepares the volume parameters for creation. // It extracts the capacity range from the request, calculates the size, // and generates a normalized volume name. Returns the volume name and size in GB. -func (cs *ControllerServer) prepareVolumeParams(ctx context.Context, req *csi.CreateVolumeRequest) (volumeName string, targetSizeGB int, size int64, err error) { +func (cs *ControllerServer) prepareVolumeParams(ctx context.Context, req *csi.CreateVolumeRequest) (*VolumeParams, error) { log := logger.GetLogger(ctx) log.V(4).Info("Entering prepareVolumeParams()", "req", req) defer log.V(4).Info("Exiting prepareVolumeParams()") - + // By default, encryption is disabled + encryptionStatus := "disabled" // Retrieve the capacity range from the request to determine the size limits for the volume. capRange := req.GetCapacityRange() // Get the requested size in bytes, handling any potential errors. - size, err = getRequestCapacitySize(capRange) + size, err := getRequestCapacitySize(capRange) if err != nil { - return "", 0, 0, err + return nil, err + } + + // Get the region from req.AccessibilityRequirements if it exists. Fall back to the controller's metadata region if not specified. + accessibilityRequirements := req.GetAccessibilityRequirements() + region := cs.metadata.Region + if accessibilityRequirements != nil { + if topologyRegion := getRegionFromTopology(accessibilityRequirements); topologyRegion != "" { + log.V(4).Info("Using region from topology", "region", topologyRegion) + region = topologyRegion + } } preKey := linodevolumes.CreateLinodeVolumeKey(0, req.GetName()) - volumeName = preKey.GetNormalizedLabelWithPrefix(cs.driver.volumeLabelPrefix) - targetSizeGB = bytesToGB(size) + volumeName := preKey.GetNormalizedLabelWithPrefix(cs.driver.volumeLabelPrefix) + targetSizeGB := bytesToGB(size) + + // Check if encryption should be enabled + if req.GetParameters()[VolumeEncryption] == True { + supported, err := cs.isEncryptionSupported(ctx, region) + if err != nil { + return nil, err + } + if !supported { + return nil, errInternal("Volume encryption is not supported in the %s region", region) + } + encryptionStatus = "enabled" + } - log.V(4).Info("Volume parameters prepared", "volumeName", volumeName, "targetSizeGB", targetSizeGB) - return volumeName, targetSizeGB, size, nil + log.V(4).Info("Volume parameters prepared", "parameters", &VolumeParams{ + VolumeName: volumeName, + TargetSizeGB: targetSizeGB, + Size: size, + EncryptionStatus: encryptionStatus, + Region: region, + }) + return &VolumeParams{ + VolumeName: volumeName, + TargetSizeGB: targetSizeGB, + Size: size, + EncryptionStatus: encryptionStatus, + Region: region, + }, nil } // createVolumeContext creates a context map for the volume based on the request parameters. @@ -448,12 +512,12 @@ func (cs *ControllerServer) createVolumeContext(ctx context.Context, req *csi.Cr // createAndWaitForVolume attempts to create a new volume and waits for it to become active. // It logs the process and handles any errors that occur during creation or waiting. -func (cs *ControllerServer) createAndWaitForVolume(ctx context.Context, name string, sizeGB int, tags string, sourceInfo *linodevolumes.LinodeVolumeKey, accessibilityRequirements *csi.TopologyRequirement) (*linodego.Volume, error) { +func (cs *ControllerServer) createAndWaitForVolume(ctx context.Context, name string, parameters map[string]string, encryptionStatus string, sizeGB int, sourceInfo *linodevolumes.LinodeVolumeKey, region string) (*linodego.Volume, error) { log := logger.GetLogger(ctx) - log.V(4).Info("Entering createAndWaitForVolume()", "name", name, "sizeGB", sizeGB, "tags", tags) + log.V(4).Info("Entering createAndWaitForVolume()", "name", name, "sizeGB", sizeGB, "tags", parameters[VolumeTags], "encryptionStatus", encryptionStatus, "region", region) defer log.V(4).Info("Exiting createAndWaitForVolume()") - vol, err := cs.attemptCreateLinodeVolume(ctx, name, sizeGB, tags, sourceInfo, accessibilityRequirements) + vol, err := cs.attemptCreateLinodeVolume(ctx, name, parameters[VolumeTags], encryptionStatus, sizeGB, sourceInfo, region) if err != nil { return nil, err } diff --git a/internal/driver/controllerserver_helper_test.go b/internal/driver/controllerserver_helper_test.go index 4d2a0eb9..39184a18 100644 --- a/internal/driver/controllerserver_helper_test.go +++ b/internal/driver/controllerserver_helper_test.go @@ -217,6 +217,53 @@ func TestCreateVolumeContext(t *testing.T) { } } +func TestCreateVolumeContext_Encryption(t *testing.T) { + vol := &linodego.Volume{ + Region: "us-east", + } + tests := []struct { + name string + req *csi.CreateVolumeRequest + expectedResult map[string]string + }{ + { + name: "Encrypted volume with encryption enabled", + req: &csi.CreateVolumeRequest{ + Name: "encrypted-volume", + Parameters: map[string]string{ + VolumeEncryption: True, + }, + }, + expectedResult: map[string]string{ + VolumeTopologyRegion: "us-east", + }, + }, + { + name: "Unencrypted volume", + req: &csi.CreateVolumeRequest{ + Name: "unencrypted-volume", + Parameters: map[string]string{}, + }, + expectedResult: map[string]string{ + VolumeTopologyRegion: "us-east", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cs := &ControllerServer{} + ctx := context.Background() + result := cs.createVolumeContext(ctx, tt.req, vol) + + // Use reflect.DeepEqual to compare maps; log specific missing keys if the test fails + if !reflect.DeepEqual(result, tt.expectedResult) { + t.Errorf("createVolumeContext() = %v, want %v", result, tt.expectedResult) + } + }) + } +} + func TestCreateAndWaitForVolume(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -226,21 +273,11 @@ func TestCreateAndWaitForVolume(t *testing.T) { client: mockClient, } - topology := &csi.TopologyRequirement{ - Preferred: []*csi.Topology{ - { - Segments: map[string]string{ - VolumeTopologyRegion: "us-east", - }, - }, - }, - } - testCases := []struct { name string volumeName string sizeGB int - tags string + parameters map[string]string sourceInfo *linodevolumes.LinodeVolumeKey setupMocks func() expectedVolume *linodego.Volume @@ -250,7 +287,9 @@ func TestCreateAndWaitForVolume(t *testing.T) { name: "Successful volume creation", volumeName: "test-volume", sizeGB: 20, - tags: "tag1,tag2", + parameters: map[string]string{ + VolumeTags: "tag1,tag2", + }, sourceInfo: nil, setupMocks: func() { mockClient.EXPECT().ListVolumes(gomock.Any(), gomock.Any()).Return(nil, nil) @@ -264,7 +303,9 @@ func TestCreateAndWaitForVolume(t *testing.T) { name: "Volume creation fails", volumeName: "test-volume", sizeGB: 20, - tags: "tag1,tag2", + parameters: map[string]string{ + VolumeTags: "tag1,tag2", + }, sourceInfo: nil, setupMocks: func() { mockClient.EXPECT().ListVolumes(gomock.Any(), gomock.Any()).Return(nil, nil) @@ -277,7 +318,9 @@ func TestCreateAndWaitForVolume(t *testing.T) { name: "Volume exists with different size", volumeName: "existing-volume", sizeGB: 30, - tags: "tag1,tag2", + parameters: map[string]string{ + VolumeTags: "tag1,tag2", + }, sourceInfo: nil, setupMocks: func() { mockClient.EXPECT().ListVolumes(gomock.Any(), gomock.Any()).Return([]linodego.Volume{ @@ -291,7 +334,9 @@ func TestCreateAndWaitForVolume(t *testing.T) { name: "Volume creation from source", volumeName: "cloned-volume", sizeGB: 40, - tags: "tag1,tag2", + parameters: map[string]string{ + VolumeTags: "tag1,tag2", + }, sourceInfo: &linodevolumes.LinodeVolumeKey{VolumeID: 789}, setupMocks: func() { mockClient.EXPECT().ListVolumes(gomock.Any(), gomock.Any()).Return(nil, nil) @@ -305,7 +350,9 @@ func TestCreateAndWaitForVolume(t *testing.T) { name: "Volume creation timeout", volumeName: "timeout-volume", sizeGB: 50, - tags: "tag1,tag2", + parameters: map[string]string{ + VolumeTags: "tag1,tag2", + }, sourceInfo: nil, setupMocks: func() { mockClient.EXPECT().ListVolumes(gomock.Any(), gomock.Any()).Return(nil, nil) @@ -320,8 +367,8 @@ func TestCreateAndWaitForVolume(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { tc.setupMocks() - - volume, err := cs.createAndWaitForVolume(context.Background(), tc.volumeName, tc.sizeGB, tc.tags, tc.sourceInfo, topology) + encryptionStatus := "disabled" + volume, err := cs.createAndWaitForVolume(context.Background(), tc.volumeName, tc.parameters, encryptionStatus, tc.sizeGB, tc.sourceInfo, "us-east") if err != nil && !reflect.DeepEqual(tc.expectedError, err) { if tc.expectedError != nil { @@ -420,26 +467,134 @@ func TestPrepareVolumeParams(t *testing.T) { } ctx := context.Background() - volumeName, sizeGB, size, err := cs.prepareVolumeParams(ctx, tt.req) + params, err := cs.prepareVolumeParams(ctx, tt.req) - if err != nil && !reflect.DeepEqual(tt.expectedError, err) { - if tt.expectedError != nil { - t.Errorf("expected error %v, got %v", tt.expectedError, err) - } else { - t.Errorf("expected no error but got %v", err) - } + // First, verify that the error matches the expectation + if (err != nil && tt.expectedError == nil) || (err == nil && tt.expectedError != nil) { + t.Errorf("expected error %v, got %v", tt.expectedError, err) + } else if err != nil && tt.expectedError != nil && err.Error() != tt.expectedError.Error() { + t.Errorf("expected error message %v, got %v", tt.expectedError.Error(), err.Error()) } - if !reflect.DeepEqual(volumeName, tt.expectedName) { - t.Errorf("Expected volume name: %s, but got: %s", tt.expectedName, volumeName) + // Only check params fields if params is not nil + if params != nil { + if params.VolumeName != tt.expectedName { + t.Errorf("Expected volume name: %s, but got: %s", tt.expectedName, params.VolumeName) + } + + if params.TargetSizeGB != tt.expectedSizeGB { + t.Errorf("Expected size in GB: %d, but got: %d", tt.expectedSizeGB, params.TargetSizeGB) + } + + if params.Size != tt.expectedSize { + t.Errorf("Expected size in bytes: %d, but got: %d", tt.expectedSize, params.Size) + } + } else if err == nil { + // If params is nil and no error was expected, the test should fail + t.Errorf("expected non-nil params, got nil") } + }) + } +} - if !reflect.DeepEqual(sizeGB, tt.expectedSizeGB) { - t.Errorf("Expected size in GB: %d, but got: %d", tt.expectedSizeGB, sizeGB) +func TestPrepareVolumeParams_Encryption(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockClient := mocks.NewMockLinodeClient(ctrl) + cs := &ControllerServer{ + client: mockClient, + driver: &LinodeDriver{ + volumeLabelPrefix: "csi-linode-pv-", + }, + metadata: Metadata{Region: "us-east"}, + } + ctx := context.Background() + + tests := []struct { + name string + req *csi.CreateVolumeRequest + setupMocks func() + expectedEncrypt string + expectedError error + }{ + { + name: "Encryption supported and enabled", + req: &csi.CreateVolumeRequest{ + Name: "encrypted-volume", + Parameters: map[string]string{ + VolumeEncryption: True, + }, + CapacityRange: &csi.CapacityRange{ + RequiredBytes: 10 << 30, // 10 GiB + }, + }, + setupMocks: func() { + mockClient.EXPECT().GetRegion(gomock.Any(), "us-east").Return(&linodego.Region{ + Capabilities: []string{"Block Storage Encryption"}, + }, nil) + }, + expectedEncrypt: "enabled", + expectedError: nil, + }, + { + name: "Encryption not supported in region", + req: &csi.CreateVolumeRequest{ + Name: "unencrypted-volume", + Parameters: map[string]string{ + VolumeEncryption: True, + }, + CapacityRange: &csi.CapacityRange{ + RequiredBytes: 10 << 30, // 10 GiB + }, + }, + setupMocks: func() { + mockClient.EXPECT().GetRegion(gomock.Any(), "us-east").Return(&linodego.Region{ + Capabilities: []string{}, + }, nil) + }, + expectedEncrypt: "disabled", + expectedError: errInternal("Volume encryption is not supported in the us-east region"), + }, + { + name: "Encryption disabled in parameters", + req: &csi.CreateVolumeRequest{ + Name: "unencrypted-volume", + Parameters: map[string]string{ + VolumeEncryption: "false", + }, + CapacityRange: &csi.CapacityRange{ + RequiredBytes: 10 << 30, // 10 GiB + }, + }, + setupMocks: func() {}, + expectedEncrypt: "disabled", + expectedError: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setupMocks() + + // Call prepareVolumeParams and capture the result and error + params, err := cs.prepareVolumeParams(ctx, tt.req) + + // Verify that the error matches the expected error + if (err != nil && tt.expectedError == nil) || (err == nil && tt.expectedError != nil) { + t.Errorf("expected error %v, got %v", tt.expectedError, err) + } else if err != nil && tt.expectedError != nil && err.Error() != tt.expectedError.Error() { + t.Errorf("expected error message %v, got %v", tt.expectedError.Error(), err.Error()) } - if !reflect.DeepEqual(size, tt.expectedSize) { - t.Errorf("Expected size in bytes: %d, but got: %d", tt.expectedSize, size) + // Only proceed to check params fields if params is non-nil and no error was expected + if params != nil && err == nil { + if params.EncryptionStatus != tt.expectedEncrypt { + t.Errorf("Expected encryption status %v, got %v", tt.expectedEncrypt, params.EncryptionStatus) + } + } else if params == nil && err == nil { + // Fail the test if params is nil but no error was expected + t.Errorf("expected non-nil params, got nil") } }) } diff --git a/internal/driver/controllerserver_test.go b/internal/driver/controllerserver_test.go index 100654d9..b3741739 100644 --- a/internal/driver/controllerserver_test.go +++ b/internal/driver/controllerserver_test.go @@ -212,11 +212,11 @@ func TestControllerPublishVolume(t *testing.T) { }, expectLinodeClientCalls: func(m *mocks.MockLinodeClient) { m.EXPECT().GetInstance(gomock.Any(), gomock.Any()).Return(&linodego.Instance{ID: 1003, Specs: &linodego.InstanceSpec{Memory: 16 << 10}}, nil) + m.EXPECT().GetVolume(gomock.Any(), gomock.Any()).Return(&linodego.Volume{ID: 1001, LinodeID: createLinodeID(1003), Size: 10, Status: linodego.VolumeActive}, nil).AnyTimes() m.EXPECT().WaitForVolumeLinodeID(gomock.Any(), 630706045, gomock.Any(), gomock.Any()).Return(&linodego.Volume{ID: 1001, LinodeID: createLinodeID(1003), Size: 10, Status: linodego.VolumeActive}, nil) m.EXPECT().AttachVolume(gomock.Any(), 630706045, gomock.Any()).Return(&linodego.Volume{ID: 1001, LinodeID: createLinodeID(1003), Size: 10, Status: linodego.VolumeActive}, nil) m.EXPECT().ListInstanceVolumes(gomock.Any(), 1003, gomock.Any()).Return([]linodego.Volume{{ID: 1001, LinodeID: createLinodeID(1003), Size: 10, Status: linodego.VolumeActive}}, nil) m.EXPECT().ListInstanceDisks(gomock.Any(), 1003, gomock.Any()).Return([]linodego.InstanceDisk{}, nil) - m.EXPECT().GetVolume(gomock.Any(), gomock.Any()).Return(&linodego.Volume{ID: 1001, LinodeID: createLinodeID(1003), Size: 10, Status: linodego.VolumeActive}, nil) }, expectedError: nil, }, @@ -553,6 +553,7 @@ func TestListVolumes(t *testing.T) { } if linodeVolume == nil { t.Fatalf("no matching linode volume for %#v", volume) + return } if want, got := int64(linodeVolume.Size<<30), volume.GetCapacityBytes(); want != got { @@ -623,6 +624,11 @@ func (c *fakeLinodeClient) ListInstanceDisks(_ context.Context, _ int, _ *linode return c.disks, nil } +//nolint:nilnil // TODO: re-work tests +func (flc *fakeLinodeClient) GetRegion(context.Context, string) (*linodego.Region, error) { + return nil, nil +} + //nolint:nilnil // TODO: re-work tests func (flc *fakeLinodeClient) GetInstance(context.Context, int) (*linodego.Instance, error) { return nil, nil diff --git a/internal/driver/driver_test.go b/internal/driver/driver_test.go index 000b4173..b32cbaf9 100644 --- a/internal/driver/driver_test.go +++ b/internal/driver/driver_test.go @@ -52,6 +52,7 @@ func TestDriverSuite(t *testing.T) { Memory: 4 << 30, // 4GiB } linodeDriver := GetLinodeDriver(context.Background()) + // variables that are picked up from the environment enableMetrics := "true" metricsPort := "10251" if err := linodeDriver.SetupLinodeDriver(context.Background(), fakeCloudProvider, mounter, deviceUtils, md, driver, vendorVersion, bsPrefix, encrypt, enableMetrics, metricsPort); err != nil { diff --git a/internal/driver/examples/kubernetes/csi-linode-blockstorage-encrypted.yaml b/internal/driver/examples/kubernetes/csi-linode-blockstorage-encrypted.yaml new file mode 100644 index 00000000..c042381c --- /dev/null +++ b/internal/driver/examples/kubernetes/csi-linode-blockstorage-encrypted.yaml @@ -0,0 +1,44 @@ +apiVersion: v1 +kind: Pod +metadata: + name: csi-example-encryption-pod +spec: + containers: + - name: csi-example-encryption-pod + image: ubuntu + command: + - sleep + - "1000000" + volumeMounts: + - mountPath: /data + name: csi-volume + tolerations: + - key: "node-role.kubernetes.io/control-plane" + operator: "Exists" + effect: "NoSchedule" + volumes: + - name: csi-volume + persistentVolumeClaim: + claimName: pvc-encrypted-example +--- +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: linode-block-storage-encrypted + namespace: kube-system +parameters: + linodebs.csi.linode.com/encrypted: "true" +allowVolumeExpansion: true +provisioner: linodebs.csi.linode.com +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: pvc-encrypted-example +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + storageClassName: linode-block-storage-encrypted diff --git a/mocks/mock_cryptsetupclient.go b/mocks/mock_cryptsetupclient.go index e16b5910..8804fef7 100644 --- a/mocks/mock_cryptsetupclient.go +++ b/mocks/mock_cryptsetupclient.go @@ -21,6 +21,7 @@ import ( type MockDevice struct { ctrl *gomock.Controller recorder *MockDeviceMockRecorder + isgomock struct{} } // MockDeviceMockRecorder is the mock recorder for MockDevice. @@ -186,6 +187,7 @@ func (mr *MockDeviceMockRecorder) VolumeKeyGet(keyslot, passphrase any) *gomock. type MockCryptSetupClient struct { ctrl *gomock.Controller recorder *MockCryptSetupClientMockRecorder + isgomock struct{} } // MockCryptSetupClientMockRecorder is the mock recorder for MockCryptSetupClient. diff --git a/mocks/mock_device.go b/mocks/mock_device.go index 8336065d..4a481858 100644 --- a/mocks/mock_device.go +++ b/mocks/mock_device.go @@ -19,6 +19,7 @@ import ( type MockDeviceUtils struct { ctrl *gomock.Controller recorder *MockDeviceUtilsMockRecorder + isgomock struct{} } // MockDeviceUtilsMockRecorder is the mock recorder for MockDeviceUtils. diff --git a/mocks/mock_filesystem.go b/mocks/mock_filesystem.go index bd02b93e..76b869fa 100644 --- a/mocks/mock_filesystem.go +++ b/mocks/mock_filesystem.go @@ -22,6 +22,7 @@ import ( type MockFileInterface struct { ctrl *gomock.Controller recorder *MockFileInterfaceMockRecorder + isgomock struct{} } // MockFileInterfaceMockRecorder is the mock recorder for MockFileInterface. @@ -89,6 +90,7 @@ func (mr *MockFileInterfaceMockRecorder) Write(arg0 any) *gomock.Call { type MockFileSystem struct { ctrl *gomock.Controller recorder *MockFileSystemMockRecorder + isgomock struct{} } // MockFileSystemMockRecorder is the mock recorder for MockFileSystem. diff --git a/mocks/mock_linodeclient.go b/mocks/mock_linodeclient.go index af305dae..d1e916ba 100644 --- a/mocks/mock_linodeclient.go +++ b/mocks/mock_linodeclient.go @@ -21,6 +21,7 @@ import ( type MockLinodeClient struct { ctrl *gomock.Controller recorder *MockLinodeClientMockRecorder + isgomock struct{} } // MockLinodeClientMockRecorder is the mock recorder for MockLinodeClient. @@ -128,6 +129,21 @@ func (mr *MockLinodeClientMockRecorder) GetInstance(arg0, arg1 any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInstance", reflect.TypeOf((*MockLinodeClient)(nil).GetInstance), arg0, arg1) } +// GetRegion mocks base method. +func (m *MockLinodeClient) GetRegion(ctx context.Context, regionID string) (*linodego.Region, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRegion", ctx, regionID) + ret0, _ := ret[0].(*linodego.Region) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRegion indicates an expected call of GetRegion. +func (mr *MockLinodeClientMockRecorder) GetRegion(ctx, regionID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRegion", reflect.TypeOf((*MockLinodeClient)(nil).GetRegion), ctx, regionID) +} + // GetVolume mocks base method. func (m *MockLinodeClient) GetVolume(arg0 context.Context, arg1 int) (*linodego.Volume, error) { m.ctrl.T.Helper() diff --git a/mocks/mock_metadata.go b/mocks/mock_metadata.go index 6f56daeb..d1b5842a 100644 --- a/mocks/mock_metadata.go +++ b/mocks/mock_metadata.go @@ -21,6 +21,7 @@ import ( type MockMetadataClient struct { ctrl *gomock.Controller recorder *MockMetadataClientMockRecorder + isgomock struct{} } // MockMetadataClientMockRecorder is the mock recorder for MockMetadataClient. diff --git a/mocks/mock_safe-mounter.go b/mocks/mock_safe-mounter.go index 7252d1b4..2197ecf1 100644 --- a/mocks/mock_safe-mounter.go +++ b/mocks/mock_safe-mounter.go @@ -23,6 +23,7 @@ import ( type MockMounter struct { ctrl *gomock.Controller recorder *MockMounterMockRecorder + isgomock struct{} } // MockMounterMockRecorder is the mock recorder for MockMounter. @@ -190,6 +191,7 @@ func (mr *MockMounterMockRecorder) Unmount(target any) *gomock.Call { type MockExecutor struct { ctrl *gomock.Controller recorder *MockExecutorMockRecorder + isgomock struct{} } // MockExecutorMockRecorder is the mock recorder for MockExecutor. @@ -266,6 +268,7 @@ func (mr *MockExecutorMockRecorder) LookPath(file any) *gomock.Call { type MockCommand struct { ctrl *gomock.Controller recorder *MockCommandMockRecorder + isgomock struct{} } // MockCommandMockRecorder is the mock recorder for MockCommand. diff --git a/pkg/linode-client/linode_client.go b/pkg/linode-client/linode_client.go index 15f9a008..762855a3 100644 --- a/pkg/linode-client/linode_client.go +++ b/pkg/linode-client/linode_client.go @@ -15,6 +15,7 @@ type LinodeClient interface { ListInstanceVolumes(ctx context.Context, instanceID int, options *linodego.ListOptions) ([]linodego.Volume, error) ListInstanceDisks(ctx context.Context, instanceID int, options *linodego.ListOptions) ([]linodego.InstanceDisk, error) + GetRegion(ctx context.Context, regionID string) (*linodego.Region, error) GetInstance(context.Context, int) (*linodego.Instance, error) GetVolume(context.Context, int) (*linodego.Volume, error) diff --git a/pkg/mount-manager/safe_mounter_test.go b/pkg/mount-manager/safe_mounter_test.go index 8e96cbd3..73281ea2 100644 --- a/pkg/mount-manager/safe_mounter_test.go +++ b/pkg/mount-manager/safe_mounter_test.go @@ -22,14 +22,15 @@ func TestNewSafeMounter(t *testing.T) { safeMounter := NewSafeMounter() if safeMounter == nil { - t.Fatalf("Expected non-nil SafeFormatAndMount, got nil") + t.Fatal("Expected non-nil SafeFormatAndMount, got nil") + return } if safeMounter.Interface == nil { - t.Error("Expected non-nil Interface, got nil") + t.Fatal("Expected non-nil Interface, got nil") } if safeMounter.Exec == nil { - t.Error("Expected non-nil Exec, got nil") + t.Fatal("Expected non-nil Exec, got nil") } }