From 8e46e8ffb09de7f11a8367a0ae89e2e3e4f3ba07 Mon Sep 17 00:00:00 2001 From: ameade <847570+ameade@users.noreply.github.com> Date: Fri, 20 Jan 2023 16:07:40 -0500 Subject: [PATCH] Luks backchannel Track and report potentially in use LUKS passphrases for volumes and snapshots Adds additional Trident REST API PUT .../volume/{volume}/luksPassphraseNames for use by the Trident node plugin to inform Trident of a volumes list of potential passphrases. Adds new field "config.luksPassphraseNames", a list of strings, to volume and snapshot API responses. --- core/orchestrator_core.go | 53 +++ core/orchestrator_core_test.go | 196 +++++++++++ core/types.go | 1 + frontend/csi/controller_api/rest.go | 19 + frontend/csi/controller_api/rest_test.go | 42 +++ frontend/csi/controller_api/types.go | 1 + frontend/csi/node_server.go | 56 ++- frontend/csi/utils.go | 88 +++++ frontend/csi/utils_test.go | 325 ++++++++++++++++++ frontend/rest/controller_handlers.go | 141 ++++++-- frontend/rest/controller_handlers_test.go | 176 ++++++++++ frontend/rest/controller_routes.go | 7 + go.mod | 1 + go.sum | 1 + mocks/mock_core/mock_core.go | 14 + .../mock_controller_api.go | 14 + storage/snapshot.go | 22 +- storage/volume.go | 7 + utils/devices.go | 58 +--- utils/devices_linux.go | 7 +- utils/devices_linux_test.go | 45 ++- utils/devices_test.go | 2 +- utils/iscsi.go | 2 +- 23 files changed, 1156 insertions(+), 122 deletions(-) create mode 100644 frontend/rest/controller_handlers_test.go diff --git a/core/orchestrator_core.go b/core/orchestrator_core.go index d25a40804..05d461d9e 100644 --- a/core/orchestrator_core.go +++ b/core/orchestrator_core.go @@ -1918,6 +1918,34 @@ func (o *TridentOrchestrator) addVolumeFinish( return externalVol, nil } +// UpdateVolumeLUKSPassphraseNames updates the LUKS passphrase names stored on a volume in the cache and persistent store +func (o *TridentOrchestrator) UpdateVolume(ctx context.Context, volume string, passphraseNames *[]string) error { + if o.bootstrapError != nil { + return o.bootstrapError + } + + o.mutex.Lock() + defer o.mutex.Unlock() + defer o.updateMetrics() + vol, err := o.storeClient.GetVolume(ctx, volume) + if err != nil { + return err + } + + // We want to make sure the persistence layer is updated before we update the core copy + // So we have to deep copy the volume from the cache to construct the volume to pass into the store client + newVolume := storage.NewVolume(vol.Config.ConstructClone(), vol.BackendUUID, vol.Pool, vol.Orphaned, vol.State) + if passphraseNames != nil { + newVolume.Config.LUKSPassphraseNames = *passphraseNames + } + err = o.storeClient.UpdateVolume(ctx, newVolume) + if err != nil { + return err + } + o.volumes[volume] = newVolume + return nil +} + func (o *TridentOrchestrator) CloneVolume( ctx context.Context, volumeConfig *storage.VolumeConfig, ) (externalVol *storage.VolumeExternal, err error) { @@ -2030,11 +2058,27 @@ func (o *TridentOrchestrator) cloneVolumeInitial( // Clear these values as they were copied from the source volume Config cloneConfig.SubordinateVolumes = make(map[string]interface{}) cloneConfig.ShareSourceVolume = "" + cloneConfig.LUKSPassphraseNames = sourceVolume.Config.LUKSPassphraseNames // Override this value only if SplitOnClone has been defined in clone volume's config if volumeConfig.SplitOnClone != "" { cloneConfig.SplitOnClone = volumeConfig.SplitOnClone } + // If it's from snapshot, we need the LUKS passphrases value from the snapshot + isLUKS, err := strconv.ParseBool(cloneConfig.LUKSEncryption) + // If the LUKSEncryption is not a bool (or is empty string) assume we are not making a LUKS volume + if err != nil { + isLUKS = false + } + if cloneConfig.CloneSourceSnapshot != "" && isLUKS { + snapshotID := storage.MakeSnapshotID(cloneConfig.CloneSourceVolume, cloneConfig.CloneSourceSnapshot) + sourceSnapshot, found := o.snapshots[snapshotID] + if !found { + return nil, utils.NotFoundError("Could not find source snapshot for volume") + } + cloneConfig.LUKSPassphraseNames = sourceSnapshot.Config.LUKSPassphraseNames + } + // With the introduction of Virtual Pools we will try our best to place the cloned volume in the same // Virtual Pool. For cases where attributes are not defined in the PVC (source/clone) but instead in the // backend storage pool, e.g. splitOnClone, we would like the cloned PV to have the same attribute value @@ -3771,6 +3815,7 @@ func (o *TridentOrchestrator) CreateSnapshot( // Complete the snapshot config snapshotConfig.InternalName = snapshotConfig.Name snapshotConfig.VolumeInternalName = volume.Config.InternalName + snapshotConfig.LUKSPassphraseNames = volume.Config.LUKSPassphraseNames // Ensure a snapshot is even possible before creating the transaction if err := backend.CanSnapshot(ctx, snapshotConfig, volume.Config); err != nil { @@ -3803,6 +3848,14 @@ func (o *TridentOrchestrator) CreateSnapshot( snapshotConfig.Name, snapshotConfig.VolumeName, backend.Name(), err) } + // Ensure we store all potential LUKS passphrases, there may have been a passphrase rotation during the snapshot process + volume = o.volumes[snapshotConfig.VolumeName] + for _, v := range volume.Config.LUKSPassphraseNames { + if !utils.SliceContainsString(snapshotConfig.LUKSPassphraseNames, v) { + snapshotConfig.LUKSPassphraseNames = append(snapshotConfig.LUKSPassphraseNames, v) + } + } + // Save references to new snapshot if err = o.storeClient.AddSnapshot(ctx, snapshot); err != nil { return nil, err diff --git a/core/orchestrator_core_test.go b/core/orchestrator_core_test.go index 3cb995347..bd5f9816c 100644 --- a/core/orchestrator_core_test.go +++ b/core/orchestrator_core_test.go @@ -822,6 +822,202 @@ func TestAddStorageClassVolumes(t *testing.T) { cleanup(t, orchestrator) } +func TestUpdateVolume_LUKSPassphraseNames(t *testing.T) { + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Positive case: luksPassphraseNames field updated + orchestrator := getOrchestrator(t, false) + vol := &storage.Volume{ + Config: &storage.VolumeConfig{Name: "test-vol", LUKSPassphraseNames: []string{}}, + BackendUUID: "12345", + } + orchestrator.volumes[vol.Config.Name] = vol + err := orchestrator.storeClient.AddVolume(context.TODO(), vol) + assert.NoError(t, err) + assert.Empty(t, orchestrator.volumes[vol.Config.Name].Config.LUKSPassphraseNames) + + err = orchestrator.UpdateVolume(context.TODO(), "test-vol", &[]string{"A"}) + desiredPassphraseNames := []string{"A"} + assert.NoError(t, err) + assert.Equal(t, desiredPassphraseNames, orchestrator.volumes[vol.Config.Name].Config.LUKSPassphraseNames) + + storedVol, err := orchestrator.storeClient.GetVolume(context.TODO(), "test-vol") + assert.NoError(t, err) + assert.Equal(t, desiredPassphraseNames, storedVol.Config.LUKSPassphraseNames) + + err = orchestrator.storeClient.DeleteVolume(context.TODO(), vol) + assert.NoError(t, err) + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Positive case: luksPassphraseNames field nil + orchestrator = getOrchestrator(t, false) + vol = &storage.Volume{ + Config: &storage.VolumeConfig{Name: "test-vol", LUKSPassphraseNames: []string{}}, + BackendUUID: "12345", + } + orchestrator.volumes[vol.Config.Name] = vol + err = orchestrator.storeClient.AddVolume(context.TODO(), vol) + assert.NoError(t, err) + assert.Empty(t, orchestrator.volumes[vol.Config.Name].Config.LUKSPassphraseNames) + + err = orchestrator.UpdateVolume(context.TODO(), "test-vol", nil) + desiredPassphraseNames = []string{} + assert.NoError(t, err) + assert.Equal(t, desiredPassphraseNames, orchestrator.volumes[vol.Config.Name].Config.LUKSPassphraseNames) + + storedVol, err = orchestrator.storeClient.GetVolume(context.TODO(), "test-vol") + assert.NoError(t, err) + assert.Equal(t, desiredPassphraseNames, storedVol.Config.LUKSPassphraseNames) + + err = orchestrator.storeClient.DeleteVolume(context.TODO(), vol) + assert.NoError(t, err) + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: failed to update persistence, volume not found + orchestrator = getOrchestrator(t, false) + vol = &storage.Volume{ + Config: &storage.VolumeConfig{Name: "test-vol", LUKSPassphraseNames: []string{}}, + BackendUUID: "12345", + } + orchestrator.volumes[vol.Config.Name] = vol + + err = orchestrator.UpdateVolume(context.TODO(), "test-vol", &[]string{"A"}) + desiredPassphraseNames = []string{} + assert.Error(t, err) + assert.Equal(t, desiredPassphraseNames, orchestrator.volumes[vol.Config.Name].Config.LUKSPassphraseNames) + + _, err = orchestrator.storeClient.GetVolume(context.TODO(), "test-vol") + // Not found + assert.Error(t, err) + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: bootstrap error + orchestrator = getOrchestrator(t, false) + bootstrapError := fmt.Errorf("my bootstrap error") + orchestrator.bootstrapError = bootstrapError + + err = orchestrator.UpdateVolume(context.TODO(), "test-vol", &[]string{"A"}) + assert.Error(t, err) + assert.ErrorIs(t, err, bootstrapError) +} + +func TestCloneVolume_SnapshotDataSource_LUKS(t *testing.T) { + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Positive case: luksPassphraseNames field updated + // // Setup + mockPools := tu.GetFakePools() + orchestrator := getOrchestrator(t, false) + + // Make a backend + poolNames := []string{tu.SlowSnapshots} + pools := make(map[string]*fake.StoragePool, len(poolNames)) + for _, poolName := range poolNames { + pools[poolName] = mockPools[poolName] + } + volumes := make([]fake.Volume, 0) + cfg, err := fakedriver.NewFakeStorageDriverConfigJSON("slow-block", "block", pools, volumes) + assert.NoError(t, err) + _, err = orchestrator.AddBackend(ctx(), cfg, "") + assert.NoError(t, err) + defer orchestrator.DeleteBackend(ctx(), "slow-block") + + // Make a StorageClass + storageClass := &storageclass.Config{Name: "specific"} + _, err = orchestrator.AddStorageClass(ctx(), storageClass) + defer orchestrator.DeleteStorageClass(ctx(), storageClass.Name) + assert.NoError(t, err) + + // Create the original volume + volConfig := tu.GenerateVolumeConfig("block", 1, "specific", config.Block) + volConfig.LUKSEncryption = "true" + volConfig.LUKSPassphraseNames = []string{"A", "B"} + _, err = orchestrator.AddVolume(ctx(), volConfig) + assert.NoError(t, err) + defer orchestrator.DeleteVolume(ctx(), volConfig.Name) + + // Create a snapshot + snapshotConfig := generateSnapshotConfig("test-snapshot", volConfig.Name, volConfig.InternalName) + snapshot, err := orchestrator.CreateSnapshot(ctx(), snapshotConfig) + assert.NoError(t, err) + assert.Equal(t, snapshot.Config.LUKSPassphraseNames, []string{"A", "B"}) + defer orchestrator.DeleteSnapshot(ctx(), volConfig.Name, snapshotConfig.Name) + + // "rotate" the luksPassphraseNames of the volume + err = orchestrator.UpdateVolume(ctx(), volConfig.Name, &[]string{"A"}) + assert.NoError(t, err) + vol, err := orchestrator.GetVolume(ctx(), volConfig.Name) + assert.NoError(t, err) + volConfig = vol.Config + assert.Equal(t, vol.Config.LUKSPassphraseNames, []string{"A"}) + + // Now clone the snapshot and ensure everything looks fine + cloneName := volConfig.Name + "_clone" + cloneConfig := &storage.VolumeConfig{ + Name: cloneName, + StorageClass: volConfig.StorageClass, + CloneSourceVolume: volConfig.Name, + CloneSourceSnapshot: snapshotConfig.Name, + VolumeMode: volConfig.VolumeMode, + } + cloneResult, err := orchestrator.CloneVolume(ctx(), cloneConfig) + assert.NoError(t, err) + assert.Equal(t, []string{"A", "B"}, cloneResult.Config.LUKSPassphraseNames) + defer orchestrator.DeleteVolume(ctx(), cloneResult.Config.Name) +} + +func TestCloneVolume_VolumeDataSource_LUKS(t *testing.T) { + // // Setup + mockPools := tu.GetFakePools() + orchestrator := getOrchestrator(t, false) + + // Create backend + poolNames := []string{tu.SlowSnapshots} + pools := make(map[string]*fake.StoragePool, len(poolNames)) + for _, poolName := range poolNames { + pools[poolName] = mockPools[poolName] + } + volumes := make([]fake.Volume, 0) + cfg, err := fakedriver.NewFakeStorageDriverConfigJSON("slow-block", "block", pools, volumes) + assert.NoError(t, err) + _, err = orchestrator.AddBackend(ctx(), cfg, "") + assert.NoError(t, err) + defer orchestrator.DeleteBackend(ctx(), "slow-block") + + // Create a StorageClass + storageClass := &storageclass.Config{Name: "specific"} + _, err = orchestrator.AddStorageClass(ctx(), storageClass) + defer orchestrator.DeleteStorageClass(ctx(), storageClass.Name) + assert.NoError(t, err) + + // Create the Volume + volConfig := tu.GenerateVolumeConfig("block", 1, "specific", config.Block) + volConfig.LUKSEncryption = "true" + volConfig.LUKSPassphraseNames = []string{"A", "B"} + // Create the source volume + _, err = orchestrator.AddVolume(ctx(), volConfig) + assert.NoError(t, err) + defer orchestrator.DeleteVolume(ctx(), volConfig.Name) + + // Create a snapshot + snapshotConfig := generateSnapshotConfig("test-snapshot", volConfig.Name, volConfig.InternalName) + snapshot, err := orchestrator.CreateSnapshot(ctx(), snapshotConfig) + assert.NoError(t, err) + assert.Equal(t, []string{"A", "B"}, snapshot.Config.LUKSPassphraseNames) + defer orchestrator.DeleteSnapshot(ctx(), volConfig.Name, snapshotConfig.Name) + + // Now clone the volume and ensure everything looks fine + cloneName := volConfig.Name + "_clone" + cloneConfig := &storage.VolumeConfig{ + Name: cloneName, + StorageClass: volConfig.StorageClass, + CloneSourceVolume: volConfig.Name, + VolumeMode: volConfig.VolumeMode, + } + cloneResult, err := orchestrator.CloneVolume(ctx(), cloneConfig) + assert.NoError(t, err) + assert.Equal(t, []string{"A", "B"}, cloneResult.Config.LUKSPassphraseNames) + defer orchestrator.DeleteVolume(ctx(), cloneResult.Config.Name) +} + // This test is modeled after TestAddStorageClassVolumes, but we don't need all the // tests around storage class deletion, etc. func TestCloneVolumes(t *testing.T) { diff --git a/core/types.go b/core/types.go index 90a661812..4449bdc12 100644 --- a/core/types.go +++ b/core/types.go @@ -37,6 +37,7 @@ type Orchestrator interface { RemoveBackendConfigRef(ctx context.Context, backendUUID, configRef string) (err error) AddVolume(ctx context.Context, volumeConfig *storage.VolumeConfig) (*storage.VolumeExternal, error) + UpdateVolume(ctx context.Context, volume string, passphraseNames *[]string) error AttachVolume(ctx context.Context, volumeName, mountpoint string, publishInfo *utils.VolumePublishInfo) error CloneVolume(ctx context.Context, volumeConfig *storage.VolumeConfig) (*storage.VolumeExternal, error) DetachVolume(ctx context.Context, volumeName, mountpoint string) error diff --git a/frontend/csi/controller_api/rest.go b/frontend/csi/controller_api/rest.go index 946cb5e4d..d79088478 100644 --- a/frontend/csi/controller_api/rest.go +++ b/frontend/csi/controller_api/rest.go @@ -230,3 +230,22 @@ func (c *ControllerRestClient) UpdateVolumePublication( } return nil } + +func (c *ControllerRestClient) UpdateVolumeLUKSPassphraseNames( + ctx context.Context, volumeName string, passphraseNames []string, +) error { + operations := passphraseNames + body, err := json.Marshal(operations) + if err != nil { + return fmt.Errorf("could not marshal JSON; %v", err) + } + url := config.VolumeURL + "/" + volumeName + "/luksPassphraseNames" + resp, _, err := c.InvokeAPI(ctx, body, "PUT", url, false, false) + if err != nil { + return fmt.Errorf("could not log into the Trident CSI Controller: %v", err) + } + if resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusOK { + return fmt.Errorf("could not update volume LUKS passphrase names") + } + return nil +} diff --git a/frontend/csi/controller_api/rest_test.go b/frontend/csi/controller_api/rest_test.go index 69fc573b6..9851c28c9 100644 --- a/frontend/csi/controller_api/rest_test.go +++ b/frontend/csi/controller_api/rest_test.go @@ -6,6 +6,7 @@ import ( "context" "encoding/json" "fmt" + "io" "io/ioutil" "net/http" "net/http/httptest" @@ -415,3 +416,44 @@ func TestCreateTLSRestClient(t *testing.T) { e = os.Remove("data.txt") assert.NoError(t, e) } + +func TestUpdateVolumeLUKSPassphraseNames(t *testing.T) { + // Positive + controllerRestClient := ControllerRestClient{} + ctx = context.Background() + mockUpdateVolumeLUKSPassphraseNames := func(w http.ResponseWriter, r *http.Request) { + passphraseNames := new([]string) + body, err := io.ReadAll(io.LimitReader(r.Body, config.MaxRESTRequestSize)) + assert.NoError(t, err) + + err = json.Unmarshal(body, passphraseNames) + assert.NoError(t, err, "Got: ", body) + + createResponse(w, "", http.StatusOK) + } + + server := getHttpServer(config.VolumeURL+"/"+"test-vol/luksPassphraseNames", mockUpdateVolumeLUKSPassphraseNames) + controllerRestClient.url = server.URL + err := controllerRestClient.UpdateVolumeLUKSPassphraseNames(ctx, "test-vol", []string{"A"}) + assert.NoError(t, err) + server.Close() + + // Negative: Volume not found + controllerRestClient = ControllerRestClient{} + ctx = context.Background() + mockUpdateVolumeLUKSPassphraseNames = func(w http.ResponseWriter, r *http.Request) { + createResponse(w, "", http.StatusNotFound) + } + + server = getHttpServer(config.VolumeURL+"/"+"test-vol/luksPassphraseNames", mockUpdateVolumeLUKSPassphraseNames) + controllerRestClient.url = server.URL + err = controllerRestClient.UpdateVolumeLUKSPassphraseNames(ctx, "test-vol", []string{"A"}) + assert.Error(t, err) + server.Close() + + // Negative: Cannot connect to trident api + controllerRestClient = ControllerRestClient{} + ctx = context.Background() + err = controllerRestClient.UpdateVolumeLUKSPassphraseNames(ctx, "test-vol", []string{"A"}) + assert.Error(t, err) +} diff --git a/frontend/csi/controller_api/types.go b/frontend/csi/controller_api/types.go index 7cd132df4..e1adcb4b3 100644 --- a/frontend/csi/controller_api/types.go +++ b/frontend/csi/controller_api/types.go @@ -21,4 +21,5 @@ type TridentController interface { DeleteNode(ctx context.Context, name string) error GetChap(ctx context.Context, volume, node string) (*utils.IscsiChapInfo, error) UpdateVolumePublication(ctx context.Context, publication *utils.VolumePublicationExternal) error + UpdateVolumeLUKSPassphraseNames(ctx context.Context, volume string, passphraseNames []string) error } diff --git a/frontend/csi/node_server.go b/frontend/csi/node_server.go index 7c8907b3f..32fac2e88 100644 --- a/frontend/csi/node_server.go +++ b/frontend/csi/node_server.go @@ -1013,11 +1013,14 @@ func (p *Plugin) nodeStageISCSIVolume( SharedTarget: sharedTarget, } publishInfo.UseCHAP = useCHAP - isLUKS, ok := req.PublishContext["LUKSEncryption"] - if !ok { - isLUKS = "" + var isLUKS bool + if req.PublishContext["LUKSEncryption"] != "" { + isLUKS, err = strconv.ParseBool(req.PublishContext["LUKSEncryption"]) + if err != nil { + return nil, fmt.Errorf("could not parse LUKSEncryption into a bool, got %v", publishInfo.LUKSEncryption) + } } - publishInfo.LUKSEncryption = isLUKS + publishInfo.LUKSEncryption = strconv.FormatBool(isLUKS) err = unstashIscsiTargetPortals(publishInfo, req.PublishContext) if nil != err { @@ -1064,6 +1067,17 @@ func (p *Plugin) nodeStageISCSIVolume( if err != nil { return nil, err } + if isLUKS { + luksDevice, err := utils.NewLUKSDeviceFromMappingPath(ctx, publishInfo.DevicePath, req.VolumeContext["internalName"]) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + // Ensure we update the passphrase incase it has never been set before + err = ensureLUKSVolumePassphrase(ctx, p.restClient, luksDevice, volumeId, req.GetSecrets(), true) + if err != nil { + return nil, status.Error(codes.Internal, "could not set LUKS volume passphrase") + } + } volTrackingInfo := &utils.VolumeTrackingInfo{ VolumePublishInfo: *publishInfo, @@ -1247,31 +1261,6 @@ func (p *Plugin) nodeUnstageISCSIVolumeRetry( return &csi.NodeUnstageVolumeResponse{}, nil } -// publishLUKSVolume ensures the LUKS device has the most recent passphrase, if specified -func (p *Plugin) publishLUKSVolume(ctx context.Context, volumeId string, secrets map[string]string, luksMappingPath string) error { - luksPassphraseName, luksPassphrase, previousLUKSPassphraseName, previousLUKSPassphrase := utils.GetLUKSPassphrasesFromSecretMap(secrets) - if luksPassphrase == "" || previousLUKSPassphrase == "" { - Logc(ctx).Infof("Single LUKS passphrase provided to NodePublishVolume, skipping passphrase rotation") - return nil - } - if luksPassphraseName == "" || previousLUKSPassphraseName == "" { - Logc(ctx).Infof("LUKS passphrase names cannot be empty, skipping passphrase rotation") - return nil - } - - devicePath, err := utils.GetUnderlyingDevicePathForLUKSDevice(ctx, luksMappingPath) - if err != nil { - Logc(ctx).WithError(err).Error("Could not determine underlying device for LUKS mapping, skipping passphrase rotation.") - return nil - } - luksDevice := utils.NewLUKSDevice(devicePath, volumeId) - _, err = utils.EnsureCurrentLUKSDevicePassphrase(ctx, luksDevice, volumeId, secrets) - if err != nil { - Logc(ctx).WithError(err).Error("Failed to rotate LUKS passphrase.") - } - return nil -} - func (p *Plugin) nodePublishISCSIVolume( ctx context.Context, req *csi.NodePublishVolumeRequest, ) (*csi.NodePublishVolumeResponse, error) { @@ -1297,9 +1286,14 @@ func (p *Plugin) nodePublishISCSIVolume( return nil, fmt.Errorf("could not parse LUKSEncryption into a bool, got %v", publishInfo.LUKSEncryption) } if isLUKS { - err = p.publishLUKSVolume(ctx, req.GetVolumeId(), req.GetSecrets(), publishInfo.DevicePath) + // Rotate the LUKS passphrase if needed, on failure, log and continue to publish + luksDevice, err := utils.NewLUKSDeviceFromMappingPath(ctx, publishInfo.DevicePath, req.VolumeContext["internalName"]) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + err = ensureLUKSVolumePassphrase(ctx, p.restClient, luksDevice, req.GetVolumeId(), req.GetSecrets(), false) if err != nil { - return nil, fmt.Errorf("could not publish LUKS volume; %v", err) + Logc(ctx).WithError(err).Error("Failed to ensure current LUKS passphrase.") } } } diff --git a/frontend/csi/utils.go b/frontend/csi/utils.go index 80de101e3..d5dcd96fc 100644 --- a/frontend/csi/utils.go +++ b/frontend/csi/utils.go @@ -15,6 +15,7 @@ import ( "google.golang.org/grpc" "github.com/netapp/trident/config" + controllerAPI "github.com/netapp/trident/frontend/csi/controller_api" . "github.com/netapp/trident/logger" "github.com/netapp/trident/utils" ) @@ -233,3 +234,90 @@ func performProtocolSpecificReconciliation(ctx context.Context, trackingInfo *ut return false, nil } + +// ensureLUKSVolumePassphrase ensures the LUKS device has the most recent passphrase and notifies the Trident controller +// of any possibly in use passphrases. If forceUpdate is true, the Trident controller will be notified of the current +// passphrase name, regardless of a rotation. +func ensureLUKSVolumePassphrase(ctx context.Context, restClient controllerAPI.TridentController, luksDevice utils.LUKSDeviceInterface, volumeId string, secrets map[string]string, forceUpdate bool) error { + luksPassphraseName, luksPassphrase, previousLUKSPassphraseName, previousLUKSPassphrase := utils.GetLUKSPassphrasesFromSecretMap(secrets) + if luksPassphrase == "" { + return fmt.Errorf("LUKS passphrase cannot be empty") + } + if luksPassphraseName == "" { + return fmt.Errorf("LUKS passphrase name cannot be empty") + } + + // Check if passphrase is already up-to-date + current, err := luksDevice.CheckPassphrase(ctx, luksPassphrase) + if err != nil { + return fmt.Errorf("could not verify passphrase %s; %v", luksPassphraseName, err) + } + if current { + Logc(ctx).WithFields(log.Fields{ + "volume": volumeId, + }).Debugf("Current LUKS passphrase name '%s'.", luksPassphraseName) + if forceUpdate { + luksPassphraseNames := []string{luksPassphraseName} + err = restClient.UpdateVolumeLUKSPassphraseNames(ctx, volumeId, luksPassphraseNames) + if err != nil { + return fmt.Errorf("could not update current passphrase name for LUKS volume; %v", err) + } + } + return nil + } + + // Check if previous passphrase is set, otherwise we can't rotate + var previous bool + if previousLUKSPassphrase != "" { + if previousLUKSPassphraseName == "" { + return fmt.Errorf("previous LUKS passphrase name cannot be empty if previous LUKS passphrase is also specified") + } + previous, err = luksDevice.CheckPassphrase(ctx, previousLUKSPassphrase) + if err != nil { + return fmt.Errorf("could not verify passphrase %s; %v", luksPassphraseName, err) + } + } + if !previous { + return fmt.Errorf("no working passphrase provided") + } + Logc(ctx).WithFields(log.Fields{ + "volume": volumeId, + }).Debugf("Current LUKS passphrase name '%s'.", previousLUKSPassphraseName) + + // Send up current and previous passphrase names, if rotation fails + luksPassphraseNames := []string{luksPassphraseName, previousLUKSPassphraseName} + err = restClient.UpdateVolumeLUKSPassphraseNames(ctx, volumeId, luksPassphraseNames) + if err != nil { + return fmt.Errorf("could not update passphrase names for LUKS volume, skipping passphrase rotation; %v", err) + } + + // Rotate + Logc(ctx).WithFields(log.Fields{ + "volume": volumeId, + "current-luks-passphrase-name": previousLUKSPassphraseName, + "new-luks-passphrase-name": luksPassphraseName, + }).Info("Rotating LUKS passphrase.") + err = luksDevice.RotatePassphrase(ctx, volumeId, previousLUKSPassphrase, luksPassphrase) + if err != nil { + Logc(ctx).WithFields(log.Fields{ + "volume": volumeId, + "current-luks-passphrase-name": previousLUKSPassphraseName, + "new-luks-passphrase-name": luksPassphraseName, + }).WithError(err).Errorf("Failed to rotate LUKS passphrase.") + return fmt.Errorf("failed to rotate LUKS passphrase") + } + Logc(ctx).Infof("Rotated LUKS passphrase") + + isCurrent, err := luksDevice.CheckPassphrase(ctx, luksPassphrase) + if err != nil { + return fmt.Errorf("could not check current passphrase for LUKS volume; %v", err) + } else if isCurrent { + // Send only current passphrase up + luksPassphraseNames = []string{luksPassphraseName} + err = restClient.UpdateVolumeLUKSPassphraseNames(ctx, volumeId, luksPassphraseNames) + if err != nil { + return fmt.Errorf("could not update passphrase names for LUKS volume after rotation; %v", err) + } + } + return nil +} diff --git a/frontend/csi/utils_test.go b/frontend/csi/utils_test.go index 58c7530b1..78578b8ca 100644 --- a/frontend/csi/utils_test.go +++ b/frontend/csi/utils_test.go @@ -3,13 +3,16 @@ package csi import ( "context" "errors" + "fmt" "testing" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/netapp/trident/config" + mockControllerAPI "github.com/netapp/trident/mocks/mock_frontend/mock_csi/mock_controller_api" "github.com/netapp/trident/mocks/mock_utils" + "github.com/netapp/trident/mocks/mock_utils/mock_luks" "github.com/netapp/trident/utils" ) @@ -140,3 +143,325 @@ func TestPerformProtocolSpecificReconciliation_BOF(t *testing.T) { assert.False(t, res) assert.Error(t, err) } + +func TestEnsureLUKSVolumePassphrase(t *testing.T) { + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Positive case: Passphrase already latest + mockCtrl := gomock.NewController(t) + mockClient := mockControllerAPI.NewMockTridentController(mockCtrl) + mockLUKSDevice := mock_luks.NewMockLUKSDeviceInterface(mockCtrl) + secrets := map[string]string{ + "luks-passphrase-name": "A", + "luks-passphrase": "passphraseA", + } + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseA").Return(true, nil) + err := ensureLUKSVolumePassphrase(context.TODO(), mockClient, mockLUKSDevice, "test-vol", secrets, false) + assert.NoError(t, err) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Positive case: Passphrase already latest, force update + mockCtrl = gomock.NewController(t) + mockClient = mockControllerAPI.NewMockTridentController(mockCtrl) + mockLUKSDevice = mock_luks.NewMockLUKSDeviceInterface(mockCtrl) + secrets = map[string]string{ + "luks-passphrase-name": "A", + "luks-passphrase": "passphraseA", + } + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseA").Return(true, nil) + mockClient.EXPECT().UpdateVolumeLUKSPassphraseNames(gomock.Any(), "test-vol", []string{"A"}).Return(nil) + err = ensureLUKSVolumePassphrase(context.TODO(), mockClient, mockLUKSDevice, "test-vol", secrets, true) + assert.NoError(t, err) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Positive case: Passphrase not correct, but previous passphrase is correct, rotation needed + mockCtrl = gomock.NewController(t) + mockClient = mockControllerAPI.NewMockTridentController(mockCtrl) + mockLUKSDevice = mock_luks.NewMockLUKSDeviceInterface(mockCtrl) + secrets = map[string]string{ + "luks-passphrase-name": "B", + "luks-passphrase": "passphraseB", + "previous-luks-passphrase-name": "A", + "previous-luks-passphrase": "passphraseA", + } + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseB").Return(false, nil) + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseA").Return(true, nil) + mockClient.EXPECT().UpdateVolumeLUKSPassphraseNames(gomock.Any(), "test-vol", []string{"B", "A"}).Return(nil) + mockLUKSDevice.EXPECT().RotatePassphrase(gomock.Any(), "test-vol", "passphraseA", "passphraseB").Return(nil) + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseB").Return(true, nil) + mockClient.EXPECT().UpdateVolumeLUKSPassphraseNames(gomock.Any(), "test-vol", []string{"B"}).Return(nil) + err = ensureLUKSVolumePassphrase(context.TODO(), mockClient, mockLUKSDevice, "test-vol", secrets, false) + assert.NoError(t, err) + mockCtrl.Finish() +} + +func TestEnsureLUKSVolumePassphrase_Error(t *testing.T) { + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: Checking passphrase is correct errors + mockCtrl := gomock.NewController(t) + mockClient := mockControllerAPI.NewMockTridentController(mockCtrl) + mockLUKSDevice := mock_luks.NewMockLUKSDeviceInterface(mockCtrl) + secrets := map[string]string{ + "luks-passphrase-name": "B", + "luks-passphrase": "passphraseB", + "previous-luks-passphrase-name": "A", + "previous-luks-passphrase": "passphraseA", + } + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseB").Return(false, fmt.Errorf("test error")) + err := ensureLUKSVolumePassphrase(context.TODO(), mockClient, mockLUKSDevice, "test-vol", secrets, false) + assert.Error(t, err) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: Checking previous passphrase is correct errors + mockCtrl = gomock.NewController(t) + mockClient = mockControllerAPI.NewMockTridentController(mockCtrl) + mockLUKSDevice = mock_luks.NewMockLUKSDeviceInterface(mockCtrl) + secrets = map[string]string{ + "luks-passphrase-name": "B", + "luks-passphrase": "passphraseB", + "previous-luks-passphrase-name": "A", + "previous-luks-passphrase": "passphraseA", + } + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseB").Return(false, nil) + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseA").Return(false, fmt.Errorf("test error")) + err = ensureLUKSVolumePassphrase(context.TODO(), mockClient, mockLUKSDevice, "test-vol", secrets, false) + assert.Error(t, err) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: Sending pre-rotation passphrases to trident controller fails + mockCtrl = gomock.NewController(t) + mockClient = mockControllerAPI.NewMockTridentController(mockCtrl) + mockLUKSDevice = mock_luks.NewMockLUKSDeviceInterface(mockCtrl) + secrets = map[string]string{ + "luks-passphrase-name": "B", + "luks-passphrase": "passphraseB", + "previous-luks-passphrase-name": "A", + "previous-luks-passphrase": "passphraseA", + } + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseB").Return(false, nil) + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseA").Return(true, nil) + mockClient.EXPECT().UpdateVolumeLUKSPassphraseNames(gomock.Any(), "test-vol", []string{"B", "A"}).Return(fmt.Errorf("test error")) + err = ensureLUKSVolumePassphrase(context.TODO(), mockClient, mockLUKSDevice, "test-vol", secrets, false) + assert.Error(t, err) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: Passphrase rotation fails + mockCtrl = gomock.NewController(t) + mockClient = mockControllerAPI.NewMockTridentController(mockCtrl) + mockLUKSDevice = mock_luks.NewMockLUKSDeviceInterface(mockCtrl) + secrets = map[string]string{ + "luks-passphrase-name": "B", + "luks-passphrase": "passphraseB", + "previous-luks-passphrase-name": "A", + "previous-luks-passphrase": "passphraseA", + } + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseB").Return(false, nil) + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseA").Return(true, nil) + mockClient.EXPECT().UpdateVolumeLUKSPassphraseNames(gomock.Any(), "test-vol", []string{"B", "A"}).Return(nil) + mockLUKSDevice.EXPECT().RotatePassphrase(gomock.Any(), "test-vol", "passphraseA", "passphraseB").Return(fmt.Errorf("test error")) + err = ensureLUKSVolumePassphrase(context.TODO(), mockClient, mockLUKSDevice, "test-vol", secrets, false) + assert.Error(t, err) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: Verifying passphrase rotation fails + mockCtrl = gomock.NewController(t) + mockClient = mockControllerAPI.NewMockTridentController(mockCtrl) + mockLUKSDevice = mock_luks.NewMockLUKSDeviceInterface(mockCtrl) + secrets = map[string]string{ + "luks-passphrase-name": "B", + "luks-passphrase": "passphraseB", + "previous-luks-passphrase-name": "A", + "previous-luks-passphrase": "passphraseA", + } + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseB").Return(false, nil) + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseA").Return(true, nil) + mockClient.EXPECT().UpdateVolumeLUKSPassphraseNames(gomock.Any(), "test-vol", []string{"B", "A"}).Return(nil) + mockLUKSDevice.EXPECT().RotatePassphrase(gomock.Any(), "test-vol", "passphraseA", "passphraseB").Return(nil) + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseB").Return(true, fmt.Errorf("test error")) + err = ensureLUKSVolumePassphrase(context.TODO(), mockClient, mockLUKSDevice, "test-vol", secrets, false) + assert.Error(t, err) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: Sending post-rotation passphrases to trident controller fails + mockCtrl = gomock.NewController(t) + mockClient = mockControllerAPI.NewMockTridentController(mockCtrl) + mockLUKSDevice = mock_luks.NewMockLUKSDeviceInterface(mockCtrl) + secrets = map[string]string{ + "luks-passphrase-name": "B", + "luks-passphrase": "passphraseB", + "previous-luks-passphrase-name": "A", + "previous-luks-passphrase": "passphraseA", + } + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseB").Return(false, nil) + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseA").Return(true, nil) + mockClient.EXPECT().UpdateVolumeLUKSPassphraseNames(gomock.Any(), "test-vol", []string{"B", "A"}).Return(nil) + mockLUKSDevice.EXPECT().RotatePassphrase(gomock.Any(), "test-vol", "passphraseA", "passphraseB").Return(nil) + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseB").Return(true, nil) + mockClient.EXPECT().UpdateVolumeLUKSPassphraseNames(gomock.Any(), "test-vol", []string{"B"}).Return(fmt.Errorf("test error")) + err = ensureLUKSVolumePassphrase(context.TODO(), mockClient, mockLUKSDevice, "test-vol", secrets, false) + assert.Error(t, err) + mockCtrl.Finish() +} + +func TestEnsureLUKSVolumePassphrase_InvalidSecret(t *testing.T) { + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: No passphrases + mockCtrl := gomock.NewController(t) + mockClient := mockControllerAPI.NewMockTridentController(mockCtrl) + mockLUKSDevice := mock_luks.NewMockLUKSDeviceInterface(mockCtrl) + secrets := map[string]string{} + err := ensureLUKSVolumePassphrase(context.TODO(), mockClient, mockLUKSDevice, "test-vol", secrets, false) + assert.Error(t, err) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: passphrase but no passphrase name + mockCtrl = gomock.NewController(t) + mockClient = mockControllerAPI.NewMockTridentController(mockCtrl) + mockLUKSDevice = mock_luks.NewMockLUKSDeviceInterface(mockCtrl) + secrets = map[string]string{ + "luks-passphrase": "passphraseA", + } + err = ensureLUKSVolumePassphrase(context.TODO(), mockClient, mockLUKSDevice, "test-vol", secrets, false) + assert.Error(t, err) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: passphrase but empty passphrase name + mockCtrl = gomock.NewController(t) + mockClient = mockControllerAPI.NewMockTridentController(mockCtrl) + mockLUKSDevice = mock_luks.NewMockLUKSDeviceInterface(mockCtrl) + secrets = map[string]string{ + "luks-passphrase-name": "A", + "luks-passphrase": "", + } + err = ensureLUKSVolumePassphrase(context.TODO(), mockClient, mockLUKSDevice, "test-vol", secrets, false) + assert.Error(t, err) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: passphrase name but no passphrase name + mockCtrl = gomock.NewController(t) + mockClient = mockControllerAPI.NewMockTridentController(mockCtrl) + mockLUKSDevice = mock_luks.NewMockLUKSDeviceInterface(mockCtrl) + secrets = map[string]string{ + "luks-passphrase-name": "A", + } + err = ensureLUKSVolumePassphrase(context.TODO(), mockClient, mockLUKSDevice, "test-vol", secrets, false) + assert.Error(t, err) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: passphrase name but empty passphrase name + mockCtrl = gomock.NewController(t) + mockClient = mockControllerAPI.NewMockTridentController(mockCtrl) + mockLUKSDevice = mock_luks.NewMockLUKSDeviceInterface(mockCtrl) + secrets = map[string]string{ + "luks-passphrase-name": "A", + "luks-passphrase": "", + } + err = ensureLUKSVolumePassphrase(context.TODO(), mockClient, mockLUKSDevice, "test-vol", secrets, false) + assert.Error(t, err) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: passphrase valid, previous luks passphrase valid but no name + mockCtrl = gomock.NewController(t) + mockClient = mockControllerAPI.NewMockTridentController(mockCtrl) + mockLUKSDevice = mock_luks.NewMockLUKSDeviceInterface(mockCtrl) + secrets = map[string]string{ + "luks-passphrase-name": "A", + "luks-passphrase": "passphraseA", + "previous-luks-passphrase": "passphraseB", + } + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseA").Return(true, nil) + err = ensureLUKSVolumePassphrase(context.TODO(), mockClient, mockLUKSDevice, "test-vol", secrets, false) + assert.NoError(t, err) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: passphrase valid, previous luks passphrase valid but empty name + mockCtrl = gomock.NewController(t) + mockClient = mockControllerAPI.NewMockTridentController(mockCtrl) + mockLUKSDevice = mock_luks.NewMockLUKSDeviceInterface(mockCtrl) + secrets = map[string]string{ + "luks-passphrase-name": "A", + "luks-passphrase": "passphraseA", + "previous-luks-passphrase-name": "", + "previous-luks-passphrase": "passphraseB", + } + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseA").Return(true, nil) + err = ensureLUKSVolumePassphrase(context.TODO(), mockClient, mockLUKSDevice, "test-vol", secrets, false) + assert.NoError(t, err) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: passphrase valid, previous luks passphrase name valid but no passphrase + mockCtrl = gomock.NewController(t) + mockClient = mockControllerAPI.NewMockTridentController(mockCtrl) + mockLUKSDevice = mock_luks.NewMockLUKSDeviceInterface(mockCtrl) + secrets = map[string]string{ + "luks-passphrase-name": "A", + "luks-passphrase": "passphraseA", + "previous-luks-passphrase-name": "B", + } + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseA").Return(true, nil) + err = ensureLUKSVolumePassphrase(context.TODO(), mockClient, mockLUKSDevice, "test-vol", secrets, false) + assert.NoError(t, err) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: passphrase valid, previous luks passphrase name valid but empty passphrase + mockCtrl = gomock.NewController(t) + mockClient = mockControllerAPI.NewMockTridentController(mockCtrl) + mockLUKSDevice = mock_luks.NewMockLUKSDeviceInterface(mockCtrl) + secrets = map[string]string{ + "luks-passphrase-name": "A", + "luks-passphrase": "passphraseA", + "previous-luks-passphrase-name": "B", + "previous-luks-passphrase": "", + } + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseA").Return(true, nil) + err = ensureLUKSVolumePassphrase(context.TODO(), mockClient, mockLUKSDevice, "test-vol", secrets, false) + assert.NoError(t, err) + mockCtrl.Finish() +} + +func TestEnsureLUKSVolumePassphrase_NoCorrectPassphraseProvided(t *testing.T) { + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: Passphrase not correct and no previous specified + mockCtrl := gomock.NewController(t) + mockClient := mockControllerAPI.NewMockTridentController(mockCtrl) + mockLUKSDevice := mock_luks.NewMockLUKSDeviceInterface(mockCtrl) + secrets := map[string]string{ + "luks-passphrase-name": "A", + "luks-passphrase": "passphraseA", + } + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseA").Return(false, nil) + err := ensureLUKSVolumePassphrase(context.TODO(), mockClient, mockLUKSDevice, "test-vol", secrets, false) + assert.Error(t, err) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: Passphrase not correct and incorrect previous + mockCtrl = gomock.NewController(t) + mockClient = mockControllerAPI.NewMockTridentController(mockCtrl) + mockLUKSDevice = mock_luks.NewMockLUKSDeviceInterface(mockCtrl) + secrets = map[string]string{ + "luks-passphrase-name": "B", + "luks-passphrase": "passphraseB", + "previous-luks-passphrase-name": "A", + "previous-luks-passphrase": "passphraseA", + } + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseB").Return(false, nil) + mockLUKSDevice.EXPECT().CheckPassphrase(gomock.Any(), "passphraseA").Return(false, nil) + err = ensureLUKSVolumePassphrase(context.TODO(), mockClient, mockLUKSDevice, "test-vol", secrets, false) + assert.Error(t, err) + mockCtrl.Finish() +} diff --git a/frontend/rest/controller_handlers.go b/frontend/rest/controller_handlers.go index 42a2afd3e..ab4a62a36 100644 --- a/frontend/rest/controller_handlers.go +++ b/frontend/rest/controller_handlers.go @@ -164,7 +164,7 @@ func UpdateGeneric( w http.ResponseWriter, r *http.Request, response httpResponse, - updater func(map[string]string, []byte) int, + updater func(http.ResponseWriter, *http.Request, httpResponse, map[string]string, []byte) int, ) { var err error var httpStatusCode int @@ -192,7 +192,7 @@ func UpdateGeneric( httpStatusCode = httpStatusCodeForGetUpdateList(err) return } - httpStatusCode = updater(vars, body) + httpStatusCode = updater(w, r, response, vars, body) } type DeleteResponse struct { @@ -310,13 +310,18 @@ func (r *UpdateBackendResponse) logFailure(ctx context.Context) { func UpdateBackend(w http.ResponseWriter, r *http.Request) { response := &UpdateBackendResponse{} UpdateGeneric(w, r, response, - func(vars map[string]string, body []byte) int { + func(w http.ResponseWriter, r *http.Request, response httpResponse, vars map[string]string, body []byte) int { + updateResponse, ok := response.(*UpdateBackendResponse) + if !ok { + response.setError(fmt.Errorf("response object must be of type UpdateBackendResponse")) + return http.StatusInternalServerError + } backend, err := orchestrator.UpdateBackend(r.Context(), vars["backend"], string(body), "") if err != nil { - response.Error = err.Error() + updateResponse.Error = err.Error() } if backend != nil { - response.BackendID = backend.Name + updateResponse.BackendID = backend.Name } return httpStatusCodeForGetUpdateList(err) }, @@ -326,19 +331,24 @@ func UpdateBackend(w http.ResponseWriter, r *http.Request) { func UpdateBackendState(w http.ResponseWriter, r *http.Request) { response := &UpdateBackendResponse{} UpdateGeneric(w, r, response, - func(vars map[string]string, body []byte) int { + func(w http.ResponseWriter, r *http.Request, response httpResponse, vars map[string]string, body []byte) int { + updateResponse, ok := response.(*UpdateBackendResponse) + if !ok { + response.setError(fmt.Errorf("response object must be of type UpdateBackendResponse")) + return http.StatusInternalServerError + } request := new(storage.UpdateBackendStateRequest) err := json.Unmarshal(body, request) if err != nil { - response.setError(fmt.Errorf("invalid JSON: %s", err.Error())) + updateResponse.setError(fmt.Errorf("invalid JSON: %s", err.Error())) return httpStatusCodeForGetUpdateList(err) } backend, err := orchestrator.UpdateBackendState(r.Context(), vars["backend"], request.State) if err != nil { - response.Error = err.Error() + updateResponse.Error = err.Error() } if backend != nil { - response.BackendID = backend.Name + updateResponse.BackendID = backend.Name } return httpStatusCodeForGetUpdateList(err) }, @@ -541,6 +551,72 @@ func DeleteVolume(w http.ResponseWriter, r *http.Request) { }) } +type UpdateVolumeResponse struct { + Volume *storage.VolumeExternal `json:"volume"` + Error string `json:"error,omitempty"` +} + +func (r *UpdateVolumeResponse) setError(err error) { + r.Error = err.Error() +} + +func (r *UpdateVolumeResponse) isError() bool { + return r.Error != "" +} + +func (r *UpdateVolumeResponse) logSuccess(ctx context.Context) { + Logc(ctx).WithFields(log.Fields{ + "handler": "UpdateVolume", + }).Info("Updated a volume.") +} + +func (r *UpdateVolumeResponse) logFailure(ctx context.Context) { + Logc(ctx).WithFields(log.Fields{ + "handler": "UpdateVolume", + }).Error(r.Error) +} + +func volumeLUKSPassphraseNamesUpdater(_ http.ResponseWriter, r *http.Request, response httpResponse, vars map[string]string, body []byte) int { + updateResponse, ok := response.(*UpdateVolumeResponse) + if !ok { + response.setError(fmt.Errorf("response object must be of type UpdateVolumeResponse")) + return http.StatusInternalServerError + } + passphraseNames := new([]string) + volume, err := orchestrator.GetVolume(r.Context(), vars["volume"]) + if err != nil { + updateResponse.Error = err.Error() + if utils.IsNotFoundError(err) { + return http.StatusNotFound + } else { + return http.StatusInternalServerError + } + } else { + updateResponse.Volume = volume + } + err = json.Unmarshal(body, passphraseNames) + if err != nil { + updateResponse.setError(fmt.Errorf("invalid JSON: %s", err.Error())) + return http.StatusBadRequest + } + + err = orchestrator.UpdateVolume(r.Context(), vars["volume"], passphraseNames) + if err != nil { + response.setError(fmt.Errorf("failed to update LUKS passphrase names for volume %s: %s", vars["volume"], err.Error())) + if utils.IsNotFoundError(err) { + return http.StatusNotFound + } + return http.StatusInternalServerError + } + + return http.StatusOK +} + +func UpdateVolumeLUKSPassphraseNames(w http.ResponseWriter, r *http.Request) { + response := &UpdateVolumeResponse{} + UpdateGeneric(w, r, response, volumeLUKSPassphraseNamesUpdater) +} + type ImportVolumeResponse struct { Volume *storage.VolumeExternal `json:"volume"` Error string `json:"error,omitempty"` @@ -640,35 +716,40 @@ func (i *UpgradeVolumeResponse) logFailure(ctx context.Context) { func UpgradeVolume(w http.ResponseWriter, r *http.Request) { response := &UpgradeVolumeResponse{} UpdateGeneric(w, r, response, - func(_ map[string]string, body []byte) int { + func(w http.ResponseWriter, r *http.Request, response httpResponse, _ map[string]string, body []byte) int { + updateResponse, ok := response.(*UpgradeVolumeResponse) + if !ok { + response.setError(fmt.Errorf("response object must be of type UpgradeVolumeResponse")) + return http.StatusInternalServerError + } upgradeVolumeRequest := new(storage.UpgradeVolumeRequest) err := json.Unmarshal(body, upgradeVolumeRequest) if err != nil { - response.setError(fmt.Errorf("invalid JSON: %s", err.Error())) + updateResponse.setError(fmt.Errorf("invalid JSON: %s", err.Error())) return httpStatusCodeForGetUpdateList(err) } if err = upgradeVolumeRequest.Validate(); err != nil { - response.setError(err) + updateResponse.setError(err) return httpStatusCodeForAdd(err) } k8sHelperFrontend, err := orchestrator.GetFrontend(r.Context(), controllerhelpers.KubernetesHelper) if err != nil { - response.setError(err) + updateResponse.setError(err) return httpStatusCodeForAdd(err) } k8sHelper, ok := k8sHelperFrontend.(k8shelper.K8SControllerHelperPlugin) if !ok { err = fmt.Errorf("unable to obtain K8S helper frontend") - response.setError(err) + updateResponse.setError(err) return httpStatusCodeForAdd(err) } volume, err := k8sHelper.UpgradeVolume(r.Context(), upgradeVolumeRequest) if err != nil { - response.Error = err.Error() + updateResponse.Error = err.Error() } if volume != nil { - response.Volume = volume + updateResponse.Volume = volume } return httpStatusCodeForGetUpdateList(err) }, @@ -824,7 +905,12 @@ func AddNode(w http.ResponseWriter, r *http.Request) { const auditMsg = "AddNode endpoint called." UpdateGeneric(w, r, response, - func(_ map[string]string, body []byte) int { + func(w http.ResponseWriter, r *http.Request, response httpResponse, _ map[string]string, body []byte) int { + updateResponse, ok := response.(*AddNodeResponse) + if !ok { + response.setError(fmt.Errorf("response object must be of type AddNodeResponse")) + return http.StatusInternalServerError + } node := new(utils.Node) err := json.Unmarshal(body, node) if err != nil { @@ -839,24 +925,24 @@ func AddNode(w http.ResponseWriter, r *http.Request) { } if err != nil { err = fmt.Errorf("could not get CSI helper frontend") - response.setError(err) + updateResponse.setError(err) return httpStatusCodeForAdd(err) } helper, ok := csiFrontend.(controllerhelpers.ControllerHelper) if !ok { err = fmt.Errorf("could not get CSI hybrid frontend") - response.setError(err) + updateResponse.setError(err) return httpStatusCodeForAdd(err) } topologyLabels, err := helper.GetNodeTopologyLabels(r.Context(), node.Name) if err != nil { - response.setError(err) + updateResponse.setError(err) return httpStatusCodeForAdd(err) } node.TopologyLabels = topologyLabels node.TopologyLabels = topologyLabels - response.setTopologyLabels(topologyLabels) + updateResponse.setTopologyLabels(topologyLabels) Logc(r.Context()).WithField("node", node.Name).Info("Determined topology labels for node: ", topologyLabels) @@ -867,7 +953,7 @@ func AddNode(w http.ResponseWriter, r *http.Request) { if err != nil { response.setError(err) } - response.Name = node.Name + updateResponse.Name = node.Name return httpStatusCodeForAdd(err) }, ) @@ -1072,17 +1158,22 @@ func (r *UpdateVolumePublicationResponse) logFailure(ctx context.Context) { func UpdateVolumePublication(w http.ResponseWriter, r *http.Request) { response := &UpdateVolumePublicationResponse{} UpdateGeneric(w, r, response, - func(vars map[string]string, body []byte) int { + func(w http.ResponseWriter, r *http.Request, response httpResponse, _ map[string]string, body []byte) int { + updateResponse, ok := response.(*UpdateVolumePublicationResponse) + if !ok { + response.setError(fmt.Errorf("response object must be of type UpdateVolumePublicationResponse")) + return http.StatusInternalServerError + } request := new(utils.VolumePublicationExternal) err := json.Unmarshal(body, request) if err != nil { - response.setError(fmt.Errorf("invalid JSON: %s", err.Error())) + updateResponse.setError(fmt.Errorf("invalid JSON: %s", err.Error())) return httpStatusCodeForGetUpdateList(err) } err = orchestrator.UpdateVolumePublication(r.Context(), request.VolumeName, request.NodeName, request.NotSafeToAttach) if err != nil { - response.Error = err.Error() + updateResponse.Error = err.Error() } return httpStatusCodeForGetUpdateList(err) }, diff --git a/frontend/rest/controller_handlers_test.go b/frontend/rest/controller_handlers_test.go new file mode 100644 index 000000000..ade2e04a8 --- /dev/null +++ b/frontend/rest/controller_handlers_test.go @@ -0,0 +1,176 @@ +package rest + +import ( + "fmt" + "io" + "net/http" + "strings" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + http_test "github.com/stretchr/testify/http" + + mockcore "github.com/netapp/trident/mocks/mock_core" + "github.com/netapp/trident/storage" + "github.com/netapp/trident/utils" +) + +func generateHTTPRequest(method, body string) *http.Request { + return &http.Request{ + Method: method, + Body: io.NopCloser(strings.NewReader(body)), + } +} + +func TestVolumeLUKSPassphraseNamesUpdater(t *testing.T) { + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Positive case: Volume found, replace "/config/luksPassphraseNames" with empty list + volume := &storage.VolumeExternal{Config: &storage.VolumeConfig{Name: "test"}} + writer := &http_test.TestResponseWriter{} + response := &UpdateVolumeResponse{} + body := `[]` + request := generateHTTPRequest(http.MethodPut, body) + + mockCtrl := gomock.NewController(t) + mockOrchestrator := mockcore.NewMockOrchestrator(mockCtrl) + orchestrator = mockOrchestrator + mockOrchestrator.EXPECT().GetVolume(request.Context(), volume.Config.Name).Return(volume, nil) + mockOrchestrator.EXPECT().UpdateVolume(request.Context(), volume.Config.Name, &[]string{}).Return(nil) + + rc := volumeLUKSPassphraseNamesUpdater(writer, request, response, map[string]string{"volume": volume.Config.Name}, []byte(body)) + + assert.Equal(t, http.StatusOK, rc) + assert.Equal(t, volume, response.Volume) + assert.Equal(t, "", response.Error) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Positive case: Volume found, replace "/config/luksPassphraseNames" with non-empty list + volume = &storage.VolumeExternal{Config: &storage.VolumeConfig{Name: "test"}} + writer = &http_test.TestResponseWriter{} + response = &UpdateVolumeResponse{} + body = `["super-secret-passphrase-1"]` + request = generateHTTPRequest(http.MethodPut, body) + + mockCtrl = gomock.NewController(t) + mockOrchestrator = mockcore.NewMockOrchestrator(mockCtrl) + orchestrator = mockOrchestrator + mockOrchestrator.EXPECT().GetVolume(request.Context(), volume.Config.Name).Return(volume, nil) + mockOrchestrator.EXPECT().UpdateVolume(request.Context(), volume.Config.Name, &[]string{"super-secret-passphrase-1"}).Return(nil) + + rc = volumeLUKSPassphraseNamesUpdater(writer, request, response, map[string]string{"volume": volume.Config.Name}, []byte(body)) + + assert.Equal(t, http.StatusOK, rc) + assert.Equal(t, volume, response.Volume) + assert.Equal(t, "", response.Error) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: Invalid response object provided + volume = &storage.VolumeExternal{Config: &storage.VolumeConfig{Name: "test"}} + writer = &http_test.TestResponseWriter{} + invalidResponse := &UpgradeVolumeResponse{} // Wrong type! + body = `[]` + request = generateHTTPRequest(http.MethodPut, body) + + mockOrchestrator.EXPECT().GetVolume(request.Context(), volume.Config.Name).Return(volume, nil) + + rc = volumeLUKSPassphraseNamesUpdater(writer, request, invalidResponse, map[string]string{"volume": volume.Config.Name}, []byte(body)) + + assert.Equal(t, http.StatusInternalServerError, rc) + assert.Equal(t, volume, response.Volume) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: Patch modify on invalid(integer) /config/luksPassphraseNames value + volume = &storage.VolumeExternal{Config: &storage.VolumeConfig{Name: "test"}} + writer = &http_test.TestResponseWriter{} + response = &UpdateVolumeResponse{} + body = `[1]` + request = generateHTTPRequest(http.MethodPut, body) + + mockOrchestrator.EXPECT().GetVolume(request.Context(), volume.Config.Name).Return(volume, nil) + + rc = volumeLUKSPassphraseNamesUpdater(writer, request, response, map[string]string{"volume": volume.Config.Name}, []byte(body)) + + assert.Equal(t, http.StatusBadRequest, rc) + assert.Equal(t, volume, response.Volume) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: Fail to get volume, not found error + volume = &storage.VolumeExternal{Config: &storage.VolumeConfig{Name: "test"}} + writer = &http_test.TestResponseWriter{} + response = &UpdateVolumeResponse{} + body = `["super-secret-passphrase-1"]` + request = generateHTTPRequest(http.MethodPut, body) + + mockCtrl = gomock.NewController(t) + mockOrchestrator = mockcore.NewMockOrchestrator(mockCtrl) + orchestrator = mockOrchestrator + mockOrchestrator.EXPECT().GetVolume(request.Context(), volume.Config.Name).Return(volume, utils.NotFoundError("test error")) + + rc = volumeLUKSPassphraseNamesUpdater(writer, request, response, map[string]string{"volume": volume.Config.Name}, []byte(body)) + + assert.Equal(t, http.StatusNotFound, rc) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: Fail to get volume, random error + volume = &storage.VolumeExternal{Config: &storage.VolumeConfig{Name: "test"}} + writer = &http_test.TestResponseWriter{} + response = &UpdateVolumeResponse{} + body = `["super-secret-passphrase-1"]` + request = generateHTTPRequest(http.MethodPut, body) + + mockCtrl = gomock.NewController(t) + mockOrchestrator = mockcore.NewMockOrchestrator(mockCtrl) + orchestrator = mockOrchestrator + mockOrchestrator.EXPECT().GetVolume(request.Context(), volume.Config.Name).Return(volume, fmt.Errorf("test error")) + + rc = volumeLUKSPassphraseNamesUpdater(writer, request, response, map[string]string{"volume": volume.Config.Name}, []byte(body)) + + assert.Equal(t, http.StatusInternalServerError, rc) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: Fail to update LUKS passphrase names + volume = &storage.VolumeExternal{Config: &storage.VolumeConfig{Name: "test"}} + writer = &http_test.TestResponseWriter{} + response = &UpdateVolumeResponse{} + body = `["super-secret-passphrase-1"]` + request = generateHTTPRequest(http.MethodPut, body) + + mockCtrl = gomock.NewController(t) + mockOrchestrator = mockcore.NewMockOrchestrator(mockCtrl) + orchestrator = mockOrchestrator + mockOrchestrator.EXPECT().GetVolume(request.Context(), volume.Config.Name).Return(volume, nil) + mockOrchestrator.EXPECT().UpdateVolume(request.Context(), volume.Config.Name, &[]string{"super-secret-passphrase-1"}).Return(fmt.Errorf("test error")) + + rc = volumeLUKSPassphraseNamesUpdater(writer, request, response, map[string]string{"volume": volume.Config.Name}, []byte(body)) + + assert.Equal(t, http.StatusInternalServerError, rc) + assert.Equal(t, volume, response.Volume) + mockCtrl.Finish() + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: Fail to update LUKS passphrase names, not found error + volume = &storage.VolumeExternal{Config: &storage.VolumeConfig{Name: "test"}} + writer = &http_test.TestResponseWriter{} + response = &UpdateVolumeResponse{} + body = `["super-secret-passphrase-1"]` + request = generateHTTPRequest(http.MethodPut, body) + + mockCtrl = gomock.NewController(t) + mockOrchestrator = mockcore.NewMockOrchestrator(mockCtrl) + orchestrator = mockOrchestrator + mockOrchestrator.EXPECT().GetVolume(request.Context(), volume.Config.Name).Return(volume, nil) + mockOrchestrator.EXPECT().UpdateVolume(request.Context(), volume.Config.Name, &[]string{"super-secret-passphrase-1"}).Return(utils.NotFoundError("test error")) + + rc = volumeLUKSPassphraseNamesUpdater(writer, request, response, map[string]string{"volume": volume.Config.Name}, []byte(body)) + + assert.Equal(t, http.StatusNotFound, rc) + assert.Equal(t, volume, response.Volume) + mockCtrl.Finish() +} diff --git a/frontend/rest/controller_routes.go b/frontend/rest/controller_routes.go index f3c241394..14277d2fe 100644 --- a/frontend/rest/controller_routes.go +++ b/frontend/rest/controller_routes.go @@ -112,6 +112,13 @@ var controllerRoutes = Routes{ nil, DeleteVolume, }, + Route{ + "UpdateVolume", + "PUT", + config.VolumeURL + "/{volume}/luksPassphraseNames", + nil, + UpdateVolumeLUKSPassphraseNames, + }, Route{ "ImportVolume", "POST", diff --git a/go.mod b/go.mod index 87990000b..bc934efc7 100755 --- a/go.mod +++ b/go.mod @@ -110,6 +110,7 @@ require ( github.com/prometheus/procfs v0.8.0 // indirect github.com/rivo/uniseg v0.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.0 // indirect github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect go.mongodb.org/mongo-driver v1.10.0 // indirect go.opentelemetry.io/otel v1.11.1 // indirect diff --git a/go.sum b/go.sum index 3f7e028fd..075771c7c 100755 --- a/go.sum +++ b/go.sum @@ -485,6 +485,7 @@ github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/mocks/mock_core/mock_core.go b/mocks/mock_core/mock_core.go index 640fc6703..4cfa13d94 100644 --- a/mocks/mock_core/mock_core.go +++ b/mocks/mock_core/mock_core.go @@ -986,6 +986,20 @@ func (mr *MockOrchestratorMockRecorder) UpdateBackendState(arg0, arg1, arg2 inte return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateBackendState", reflect.TypeOf((*MockOrchestrator)(nil).UpdateBackendState), arg0, arg1, arg2) } +// UpdateVolume mocks base method. +func (m *MockOrchestrator) UpdateVolume(arg0 context.Context, arg1 string, arg2 *[]string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateVolume", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateVolume indicates an expected call of UpdateVolume. +func (mr *MockOrchestratorMockRecorder) UpdateVolume(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateVolume", reflect.TypeOf((*MockOrchestrator)(nil).UpdateVolume), arg0, arg1, arg2) +} + // UpdateVolumePublication mocks base method. func (m *MockOrchestrator) UpdateVolumePublication(arg0 context.Context, arg1, arg2 string, arg3 *bool) error { m.ctrl.T.Helper() diff --git a/mocks/mock_frontend/mock_csi/mock_controller_api/mock_controller_api.go b/mocks/mock_frontend/mock_csi/mock_controller_api/mock_controller_api.go index 87a7d7cfd..d441d0ca2 100644 --- a/mocks/mock_frontend/mock_csi/mock_controller_api/mock_controller_api.go +++ b/mocks/mock_frontend/mock_csi/mock_controller_api/mock_controller_api.go @@ -112,6 +112,20 @@ func (mr *MockTridentControllerMockRecorder) InvokeAPI(arg0, arg1, arg2, arg3, a return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InvokeAPI", reflect.TypeOf((*MockTridentController)(nil).InvokeAPI), arg0, arg1, arg2, arg3, arg4, arg5) } +// UpdateVolumeLUKSPassphraseNames mocks base method. +func (m *MockTridentController) UpdateVolumeLUKSPassphraseNames(arg0 context.Context, arg1 string, arg2 []string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateVolumeLUKSPassphraseNames", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateVolumeLUKSPassphraseNames indicates an expected call of UpdateVolumeLUKSPassphraseNames. +func (mr *MockTridentControllerMockRecorder) UpdateVolumeLUKSPassphraseNames(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateVolumeLUKSPassphraseNames", reflect.TypeOf((*MockTridentController)(nil).UpdateVolumeLUKSPassphraseNames), arg0, arg1, arg2) +} + // UpdateVolumePublication mocks base method. func (m *MockTridentController) UpdateVolumePublication(arg0 context.Context, arg1 *utils.VolumePublicationExternal) error { m.ctrl.T.Helper() diff --git a/storage/snapshot.go b/storage/snapshot.go index 46c3a74df..f23fddfb6 100644 --- a/storage/snapshot.go +++ b/storage/snapshot.go @@ -15,11 +15,12 @@ const ( var snapshotIDRegex = regexp.MustCompile(`^(?P[^\s/]+)/(?P[^\s/]+)$`) type SnapshotConfig struct { - Version string `json:"version,omitempty"` - Name string `json:"name,omitempty"` - InternalName string `json:"internalName,omitempty"` - VolumeName string `json:"volumeName,omitempty"` - VolumeInternalName string `json:"volumeInternalName,omitempty"` + Version string `json:"version,omitempty"` + Name string `json:"name,omitempty"` + InternalName string `json:"internalName,omitempty"` + VolumeName string `json:"volumeName,omitempty"` + VolumeInternalName string `json:"volumeInternalName,omitempty"` + LUKSPassphraseNames []string `json:"luksPassphraseNames,omitempty"` } func (c *SnapshotConfig) ID() string { @@ -104,11 +105,12 @@ func (s *Snapshot) ConstructPersistent() *SnapshotPersistent { func (s *Snapshot) ConstructClone() *Snapshot { return &Snapshot{ Config: &SnapshotConfig{ - Version: s.Config.Version, - Name: s.Config.Name, - InternalName: s.Config.InternalName, - VolumeName: s.Config.VolumeName, - VolumeInternalName: s.Config.VolumeInternalName, + Version: s.Config.Version, + Name: s.Config.Name, + InternalName: s.Config.InternalName, + VolumeName: s.Config.VolumeName, + VolumeInternalName: s.Config.VolumeInternalName, + LUKSPassphraseNames: s.Config.LUKSPassphraseNames, }, Created: s.Created, SizeBytes: s.SizeBytes, diff --git a/storage/volume.go b/storage/volume.go index 1dfaf8988..c5721a769 100644 --- a/storage/volume.go +++ b/storage/volume.go @@ -54,6 +54,7 @@ type VolumeConfig struct { RequisiteTopologies []map[string]string `json:"requisiteTopologies,omitempty"` PreferredTopologies []map[string]string `json:"preferredTopologies,omitempty"` AllowedTopologies []map[string]string `json:"allowedTopologies,omitempty"` + LUKSPassphraseNames []string `json:"luksPassphraseNames,omitempty"` MirrorHandle string `json:"mirrorHandle,omitempty"` // IsMirrorDestination is whether the volume is currently the destination in a mirror relationship IsMirrorDestination bool `json:"mirrorDestination,omitempty"` @@ -238,6 +239,12 @@ func (r *UpgradeVolumeRequest) Validate() error { return nil } +type PatchRequestStringSlice struct { + Op string `json:"op"` + Path string `json:"path"` + Value []string `json:"value"` +} + type ByVolumeExternalName []*VolumeExternal func (a ByVolumeExternalName) Len() int { return len(a) } diff --git a/utils/devices.go b/utils/devices.go index fcfa0ae00..acfbbe6a7 100644 --- a/utils/devices.go +++ b/utils/devices.go @@ -1162,9 +1162,17 @@ func waitForMultipathDeviceForLUN(ctx context.Context, lunID int, iSCSINodeName return err } -func NewLUKSDevice(rawDevicePath, volumeId string) *LUKSDevice { +func NewLUKSDevice(rawDevicePath, volumeId string) (*LUKSDevice, error) { luksDeviceName := luksDevicePrefix + volumeId - return &LUKSDevice{rawDevicePath, luksDeviceName} + return &LUKSDevice{rawDevicePath, luksDeviceName}, nil +} + +func NewLUKSDeviceFromMappingPath(ctx context.Context, mappingPath, volumeId string) (*LUKSDevice, error) { + rawDevicePath, err := GetUnderlyingDevicePathForLUKSDevice(ctx, mappingPath) + if err != nil { + return nil, fmt.Errorf("could not determine underlying device for LUKS mapping; %v", err) + } + return NewLUKSDevice(rawDevicePath, volumeId) } // MappedDevicePath returns the location of the LUKS device when opened. @@ -1283,52 +1291,6 @@ func EnsureLUKSDeviceMappedOnHost(ctx context.Context, luksDevice LUKSDeviceInte return luksFormatted, nil } -// EnsureCurrentLUKSDevicePassphrase ensures the specified device is using the current LUKS passphrase -// returns whether a passphrase rotation occurred -func EnsureCurrentLUKSDevicePassphrase(ctx context.Context, luksDevice LUKSDeviceInterface, name string, secrets map[string]string) (bool, error) { - luksPassphraseName, luksPassphrase, previousLUKSPassphraseName, previousLUKSPassphrase := GetLUKSPassphrasesFromSecretMap(secrets) // Check current passphrase - current, err := luksDevice.CheckPassphrase(ctx, luksPassphrase) - if err != nil { - return false, fmt.Errorf("could not validate passphrase %s; %v", luksPassphraseName, err) - } - if current { - Logc(ctx).WithFields(log.Fields{ - "volume": name, - }).Debugf("Current LUKS passphrase name '%s'.", luksPassphraseName) - return false, nil - } - - // Check previous passphrase - previous, err := luksDevice.CheckPassphrase(ctx, previousLUKSPassphrase) - if err != nil { - return false, fmt.Errorf("could not validate passphrase %s; %v", luksPassphraseName, err) - } - if !previous { - return false, fmt.Errorf("no working passphrase provided") - } else { - Logc(ctx).WithFields(log.Fields{ - "volume": name, - }).Debugf("Current LUKS passphrase name '%s'.", previousLUKSPassphraseName) - } - - // Rotate if needed - Logc(ctx).WithFields(log.Fields{ - "volume": name, - "current-luks-passphrase-name": previousLUKSPassphraseName, - "new-luks-passphrase-name": luksPassphraseName, - }).Info("Rotating LUKS passphrase.") - err = luksDevice.RotatePassphrase(ctx, name, previousLUKSPassphrase, luksPassphrase) - if err != nil { - Logc(ctx).WithFields(log.Fields{ - "volume": name, - "current-luks-passphrase-name": previousLUKSPassphraseName, - "new-luks-passphrase-name": luksPassphraseName, - }).WithError(err).Errorf("Failed to rotate LUKS passphrase.") - return false, fmt.Errorf("failed to rotate LUKS passphrase") - } - return true, nil -} - func ResizeLUKSDevice(ctx context.Context, luksDevicePath, luksPassphrase string) error { luksDeviceName := filepath.Base(luksDevicePath) luksDevice := &LUKSDevice{"", luksDeviceName} diff --git a/utils/devices_linux.go b/utils/devices_linux.go index 8db20b33c..6a96715ff 100644 --- a/utils/devices_linux.go +++ b/utils/devices_linux.go @@ -306,7 +306,7 @@ func (d *LUKSDevice) RotatePassphrase(ctx context.Context, volumeId, previousLUK Logc(ctx).WithFields(log.Fields{ "volume": volumeId, "device": d.RawDevicePath(), - "MappedDevicePath": d.MappedDevicePath(), + "mappedDevicePath": d.MappedDevicePath(), }).Info("Rotating LUKS passphrase for encrypted volume.") // make sure the new passphrase is valid @@ -338,7 +338,7 @@ func (d *LUKSDevice) RotatePassphrase(ctx context.Context, volumeId, previousLUK Logc(ctx).WithFields(log.Fields{ "volume": volumeId, "device": d.RawDevicePath(), - "MappedDevicePath": d.MappedDevicePath(), + "mappedDevicePath": d.MappedDevicePath(), }).Info("Rotated LUKS passphrase for encrypted volume.") return nil } @@ -395,11 +395,12 @@ func GetUnderlyingDevicePathForLUKSDevice(ctx context.Context, luksDevicePath st func (d *LUKSDevice) CheckPassphrase(ctx context.Context, luksPassphrase string) (bool, error) { device := d.RawDevicePath() luksDeviceName := d.MappedDeviceName() - _, err := execCommandWithTimeoutAndInput(ctx, "cryptsetup", luksCommandTimeout, true, luksPassphrase, "open", device, luksDeviceName, "--type", "luks2", "--test-passphrase") + output, err := execCommandWithTimeoutAndInput(ctx, "cryptsetup", luksCommandTimeout, true, luksPassphrase, "open", device, luksDeviceName, "--type", "luks2", "--test-passphrase") if err != nil { if exiterr, ok := err.(*exec.ExitError); ok && exiterr.ExitCode() == luksCryptsetupBadPassphraseReturnCode { return false, nil } + Logc(ctx).WithError(err).Errorf("Cryptsetup command failed, output: %s", output) return false, err } return true, nil diff --git a/utils/devices_linux_test.go b/utils/devices_linux_test.go index b1ed7b199..4dbc6302c 100644 --- a/utils/devices_linux_test.go +++ b/utils/devices_linux_test.go @@ -606,7 +606,8 @@ func TestCheckPassphrase(t *testing.T) { }() // //////////////////////////////////////////////////////////////////////////////////////////////////////////// // Positive case: Correct passphrase - luksDevice := NewLUKSDevice("", "test-pvc") + luksDevice, err := NewLUKSDevice("", "test-pvc") + assert.NoError(t, err) correct, err := luksDevice.CheckPassphrase(context.Background(), "passphrase") assert.True(t, correct) assert.NoError(t, err) @@ -614,7 +615,8 @@ func TestCheckPassphrase(t *testing.T) { // //////////////////////////////////////////////////////////////////////////////////////////////////////////// // Positive case: Not correct passphrase execReturnCode = 2 - luksDevice = NewLUKSDevice("", "test-pvc") + luksDevice, err = NewLUKSDevice("", "test-pvc") + assert.NoError(t, err) correct, err = luksDevice.CheckPassphrase(context.Background(), "passphrase") assert.False(t, correct) assert.NoError(t, err) @@ -622,12 +624,49 @@ func TestCheckPassphrase(t *testing.T) { // //////////////////////////////////////////////////////////////////////////////////////////////////////////// // Negative case: error execReturnCode = 4 - luksDevice = NewLUKSDevice("", "test-pvc") + luksDevice, err = NewLUKSDevice("", "test-pvc") + assert.NoError(t, err) correct, err = luksDevice.CheckPassphrase(context.Background(), "passphrase") assert.False(t, correct) assert.Error(t, err) } +// //////////////////////////////////////////////////////////////////////////////////////////////////////////// +func TestNewLUKSDeviceFromMappingPath(t *testing.T) { + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Positive case + execCmd = fakeExecCommand + execReturnCode = 0 + // Reset exec command after tests + defer func() { + execCmd = exec.CommandContext + }() + execReturnValue = `/dev/mapper/luks-pvc-test is active and is in use. + type: LUKS2 + cipher: aes-xts-plain64 + keysize: 512 bits + key location: keyring + device: /dev/sdb + sector size: 512 + offset: 32768 sectors + size: 2064384 sectors + mode: read/write` + luksDevice, err := NewLUKSDeviceFromMappingPath(context.TODO(), "/dev/mapper/luks-pvc-test", "pvc-test") + assert.NoError(t, err) + + assert.Equal(t, luksDevice.RawDevicePath(), "/dev/sdb") + assert.Equal(t, luksDevice.MappedDevicePath(), "/dev/mapper/luks-pvc-test") + assert.Equal(t, luksDevice.MappedDeviceName(), "luks-pvc-test") + + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Negative case: error attempting to get device path + execReturnCode = 2 + // Reset exec command after tests + luksDevice, err = NewLUKSDeviceFromMappingPath(context.TODO(), "/dev/mapper/luks-pvc-test", "pvc-test") + assert.Error(t, err) + assert.Nil(t, luksDevice) +} + // //////////////////////////////////////////////////////////////////////////////////////////////////////////// func TestResize_Positive(t *testing.T) { execCmd = fakeExecCommand diff --git a/utils/devices_test.go b/utils/devices_test.go index 8ca565aee..875ba2809 100644 --- a/utils/devices_test.go +++ b/utils/devices_test.go @@ -14,7 +14,7 @@ import ( // //////////////////////////////////////////////////////////////////////////////////////////////////////////// func TestNewLUKSDevice(t *testing.T) { - luksDevice := NewLUKSDevice("/dev/sdb", "pvc-test") + luksDevice, _ := NewLUKSDevice("/dev/sdb", "pvc-test") assert.Equal(t, luksDevice.RawDevicePath(), "/dev/sdb") assert.Equal(t, luksDevice.MappedDevicePath(), "/dev/mapper/luks-pvc-test") diff --git a/utils/iscsi.go b/utils/iscsi.go index 7615510f9..92733a1d5 100644 --- a/utils/iscsi.go +++ b/utils/iscsi.go @@ -216,7 +216,7 @@ func AttachISCSIVolume(ctx context.Context, name, mountpoint string, publishInfo } if isLUKSDevice { - luksDevice := NewLUKSDevice(devicePath, name) + luksDevice, _ := NewLUKSDevice(devicePath, name) luksFormatted, err = EnsureLUKSDeviceMappedOnHost(ctx, luksDevice, name, secrets) if err != nil { return err