From 825609b7e9b52576814f3b5fc10632d2f2b36852 Mon Sep 17 00:00:00 2001 From: lukasmetzner Date: Wed, 13 May 2026 15:25:29 +0200 Subject: [PATCH 1/5] fix: recover stale luks device mapper --- internal/driver/node_test.go | 42 +++++++------- internal/volumes/cryptsetup.go | 88 +++++++++++++++++++++++------ internal/volumes/resize.go | 4 +- test/integration/cryptsetup_test.go | 1 + 4 files changed, 94 insertions(+), 41 deletions(-) diff --git a/internal/driver/node_test.go b/internal/driver/node_test.go index ded37375..a05a22b4 100644 --- a/internal/driver/node_test.go +++ b/internal/driver/node_test.go @@ -23,14 +23,14 @@ type nodeServiceTestEnv struct { volumeResizeService *mock.VolumeResizeService } -func newNodeServerTestEnv() nodeServiceTestEnv { +func newNodeServerTestEnv(t *testing.T) nodeServiceTestEnv { var ( volumeMountService = &mock.VolumeMountService{} volumeResizeService = &mock.VolumeResizeService{} volumeStatsService = &mock.VolumeStatsService{} ) return nodeServiceTestEnv{ - ctx: context.Background(), + ctx: t.Context(), service: NewNodeService( slog.New(slog.DiscardHandler), "1", @@ -46,9 +46,9 @@ func newNodeServerTestEnv() nodeServiceTestEnv { } func TestNodeServiceNodePublishVolume(t *testing.T) { - env := newNodeServerTestEnv() + env := newNodeServerTestEnv(t) - env.volumeMountService.PublishFunc = func(ctx context.Context, targetPath string, devicePath string, opts volumes.MountOpts) error { + env.volumeMountService.PublishFunc = func(_ context.Context, targetPath string, devicePath string, opts volumes.MountOpts) error { if targetPath != "target" { t.Errorf("unexpected target path passed to volume service: %s", targetPath) } @@ -82,10 +82,10 @@ func TestNodeServiceNodePublishVolume(t *testing.T) { } func TestNodeServiceNodePublishBlockVolume(t *testing.T) { - env := newNodeServerTestEnv() + env := newNodeServerTestEnv(t) env.volumeMountService.PublishFunc = func( - ctx context.Context, targetPath, devicePath string, opts volumes.MountOpts, + _ context.Context, targetPath, devicePath string, opts volumes.MountOpts, ) error { if targetPath != "target" { t.Errorf("unexpected target path: %s", targetPath) @@ -115,9 +115,9 @@ func TestNodeServiceNodePublishBlockVolume(t *testing.T) { } func TestNodeServiceNodePublishPublishError(t *testing.T) { - env := newNodeServerTestEnv() + env := newNodeServerTestEnv(t) - env.volumeMountService.PublishFunc = func(ctx context.Context, targetPath string, devicePath string, opts volumes.MountOpts) error { + env.volumeMountService.PublishFunc = func(_ context.Context, targetPath string, devicePath string, opts volumes.MountOpts) error { return io.EOF } @@ -145,7 +145,7 @@ func TestNodeServiceNodePublishPublishError(t *testing.T) { } func TestNodeServiceNodePublishVolumeInputErrors(t *testing.T) { - env := newNodeServerTestEnv() + env := newNodeServerTestEnv(t) testCases := []struct { Name string @@ -214,9 +214,9 @@ func TestNodeServiceNodePublishVolumeInputErrors(t *testing.T) { } func TestNodeServiceNodeUnpublishVolume(t *testing.T) { - env := newNodeServerTestEnv() + env := newNodeServerTestEnv(t) - env.volumeMountService.UnpublishFunc = func(ctx context.Context, targetPath string) error { + env.volumeMountService.UnpublishFunc = func(_ context.Context, targetPath string) error { if targetPath != "target" { t.Errorf("unexpected target path passed to volume service: %s", targetPath) } @@ -233,9 +233,9 @@ func TestNodeServiceNodeUnpublishVolume(t *testing.T) { } func TestNodeServiceNodeUnpublishUnpublishError(t *testing.T) { - env := newNodeServerTestEnv() + env := newNodeServerTestEnv(t) - env.volumeMountService.UnpublishFunc = func(ctx context.Context, targetPath string) error { + env.volumeMountService.UnpublishFunc = func(_ context.Context, targetPath string) error { return io.EOF } @@ -249,7 +249,7 @@ func TestNodeServiceNodeUnpublishUnpublishError(t *testing.T) { } func TestNodeServiceNodeUnpublishVolumeInputErrors(t *testing.T) { - env := newNodeServerTestEnv() + env := newNodeServerTestEnv(t) testCases := []struct { Name string @@ -283,7 +283,7 @@ func TestNodeServiceNodeUnpublishVolumeInputErrors(t *testing.T) { } func TestNodeServiceNodeGetCapabilities(t *testing.T) { - env := newNodeServerTestEnv() + env := newNodeServerTestEnv(t) req := &proto.NodeGetCapabilitiesRequest{} resp, err := env.service.NodeGetCapabilities(env.ctx, req) @@ -312,7 +312,7 @@ func TestNodeServiceNodeGetCapabilities(t *testing.T) { } func TestNodeServiceNodeGetInfo(t *testing.T) { - env := newNodeServerTestEnv() + env := newNodeServerTestEnv(t) req := &proto.NodeGetInfoRequest{} resp, err := env.service.NodeGetInfo(env.ctx, req) @@ -328,7 +328,7 @@ func TestNodeServiceNodeGetInfo(t *testing.T) { } func TestNodeServiceNodeExpandVolume(t *testing.T) { - env := newNodeServerTestEnv() + env := newNodeServerTestEnv(t) env.volumeMountService.PathExistsFunc = func(path string) (bool, error) { if path != "volumePath" { @@ -336,7 +336,7 @@ func TestNodeServiceNodeExpandVolume(t *testing.T) { } return true, nil } - env.volumeResizeService.ResizeFunc = func(ctx context.Context, volumePath string) error { + env.volumeResizeService.ResizeFunc = func(_ context.Context, volumePath string) error { if volumePath != "volumePath" { t.Errorf("unexpected volume path passed to volume service: %s", volumePath) } @@ -353,7 +353,7 @@ func TestNodeServiceNodeExpandVolume(t *testing.T) { } func TestNodeServiceNodeExpandBlockVolume(t *testing.T) { - env := newNodeServerTestEnv() + env := newNodeServerTestEnv(t) env.volumeMountService.PathExistsFunc = func(path string) (bool, error) { if path != "volumePath" { @@ -361,7 +361,7 @@ func TestNodeServiceNodeExpandBlockVolume(t *testing.T) { } return true, nil } - env.volumeResizeService.ResizeFunc = func(ctx context.Context, volumePath string) error { + env.volumeResizeService.ResizeFunc = func(_ context.Context, volumePath string) error { t.Errorf("This function should never be called.") return nil } @@ -382,7 +382,7 @@ func TestNodeServiceNodeExpandBlockVolume(t *testing.T) { } func TestNodeServiceNodeExpandVolumeInputErrors(t *testing.T) { - env := newNodeServerTestEnv() + env := newNodeServerTestEnv(t) testCases := []struct { Name string diff --git a/internal/volumes/cryptsetup.go b/internal/volumes/cryptsetup.go index 0ed29842..89f817e7 100644 --- a/internal/volumes/cryptsetup.go +++ b/internal/volumes/cryptsetup.go @@ -11,6 +11,19 @@ import ( const cryptsetupExecuable = "cryptsetup" +type Status struct { + Device string + Type string + Active bool +} + +func (s Status) IsZombie() bool { + if !s.Active { + return false + } + return s.Device == "" || s.Type == "" || s.Device == "(null)" || s.Type == "n/a" +} + type CryptSetup struct { logger *slog.Logger } @@ -19,15 +32,32 @@ func NewCryptSetup(logger *slog.Logger) *CryptSetup { return &CryptSetup{logger: logger} } -func (cs *CryptSetup) IsActive(ctx context.Context, luksDeviceName string) (bool, error) { - output, code, err := command(ctx, cryptsetupExecuable, "status", luksDeviceName) +func (cs *CryptSetup) Status(ctx context.Context, device string) (Status, error) { + status := Status{Active: false} + output, code, err := cryptsetup(ctx, "status", device) if err != nil { if code == 4 { - return false, nil + return status, nil } - return false, fmt.Errorf("unable to check LUKS device %s activity: %s", luksDeviceName, output) + return status, fmt.Errorf("unable to check LUKS device %s activity: %w", device, err) } - return true, nil + + status.Active = true + for line := range strings.SplitSeq(output, "\n") { + key, value, ok := strings.Cut(strings.TrimSpace(line), ":") + if !ok { + continue + } + value = strings.TrimSpace(value) + switch key { + case "type": + status.Type = value + case "device": + status.Device = value + } + } + + return status, nil } func (cs *CryptSetup) Format(ctx context.Context, devicePath string, passphrase string) error { @@ -35,7 +65,7 @@ func (cs *CryptSetup) Format(ctx context.Context, devicePath string, passphrase "formatting LUKS device", "devicePath", devicePath, ) - output, _, err := commandWithStdin(ctx, passphrase, cryptsetupExecuable, "luksFormat", "--type", "luks1", devicePath) + output, _, err := cryptsetupWithStdin(ctx, passphrase, "luksFormat", "--type", "luks1", devicePath) if err != nil { return fmt.Errorf("unable to format device %s with LUKS: %s", devicePath, output) } @@ -43,19 +73,41 @@ func (cs *CryptSetup) Format(ctx context.Context, devicePath string, passphrase } func (cs *CryptSetup) Open(ctx context.Context, devicePath string, luksDeviceName string, passphrase string) error { - active, err := cs.IsActive(ctx, luksDeviceName) + status, err := cs.Status(ctx, luksDeviceName) if err != nil { return err } - if active { - return nil + if status.Active { + if !status.IsZombie() { + cs.logger.Debug( + "luks device already active", + "devicePath", devicePath, + "luksDeviceName", luksDeviceName, + ) + return nil + } + + // If the volume is not correctly unpublished, a prior attach might leave a dm-crypt + // target whose backing block device disappeared. Reusing this mapper would mount + // on a dead device so we have to close it first. + cs.logger.Warn( + "detected zombie luks device; cleaning up", + "devicePath", devicePath, + "luksDeviceName", luksDeviceName, + "type", status.Type, + "device", status.Device, + ) + if err := cs.Close(ctx, luksDeviceName); err != nil { + return fmt.Errorf("failed to close zombie LUKS device %s: %w", luksDeviceName, err) + } } + cs.logger.Info( "opening LUKS device", "devicePath", devicePath, "luksDeviceName", luksDeviceName, ) - output, _, err := commandWithStdin(ctx, passphrase, cryptsetupExecuable, "luksOpen", "--allow-discards", devicePath, luksDeviceName) + output, _, err := cryptsetupWithStdin(ctx, passphrase, "luksOpen", "--allow-discards", devicePath, luksDeviceName) if err != nil { return fmt.Errorf("unable to open LUKS device %s: %s", devicePath, output) } @@ -63,18 +115,18 @@ func (cs *CryptSetup) Open(ctx context.Context, devicePath string, luksDeviceNam } func (cs *CryptSetup) Close(ctx context.Context, luksDeviceName string) error { - active, err := cs.IsActive(ctx, luksDeviceName) + status, err := cs.Status(ctx, luksDeviceName) if err != nil { return err } - if !active { + if !status.Active { return nil } cs.logger.Info( "closing LUKS device", "luksDeviceName", luksDeviceName, ) - output, _, err := command(ctx, cryptsetupExecuable, "luksClose", luksDeviceName) + output, _, err := cryptsetup(ctx, "luksClose", luksDeviceName) if err != nil { return fmt.Errorf("unable to close LUKS device %s: %s", luksDeviceName, output) } @@ -86,7 +138,7 @@ func (cs *CryptSetup) Resize(ctx context.Context, luksDeviceName string) error { "resizing LUKS device", "luksDeviceName", luksDeviceName, ) - output, _, err := command(ctx, cryptsetupExecuable, "resize", luksDeviceName) + output, _, err := cryptsetup(ctx, "resize", luksDeviceName) if err != nil { return fmt.Errorf("unable to resize LUKS device %s: %s", luksDeviceName, output) } @@ -102,12 +154,12 @@ func GenerateLUKSDevicePath(luksDeviceName string) string { return "/dev/mapper/" + luksDeviceName } -func command(ctx context.Context, name string, args ...string) (string, int, error) { - return commandWithStdin(ctx, "", name, args...) +func cryptsetup(ctx context.Context, args ...string) (string, int, error) { + return cryptsetupWithStdin(ctx, "", args...) } -func commandWithStdin(ctx context.Context, stdin string, name string, args ...string) (string, int, error) { - cmd := exec.CommandContext(ctx, name, args...) +func cryptsetupWithStdin(ctx context.Context, stdin string, args ...string) (string, int, error) { + cmd := exec.CommandContext(ctx, cryptsetupExecuable, args...) if stdin != "" { cmd.Stdin = strings.NewReader(stdin) } diff --git a/internal/volumes/resize.go b/internal/volumes/resize.go index 8647d0ce..90aa44f4 100644 --- a/internal/volumes/resize.go +++ b/internal/volumes/resize.go @@ -45,11 +45,11 @@ func (l *LinuxResizeService) Resize(ctx context.Context, volumePath string) erro ) luksDeviceName := GenerateLUKSDeviceName(devicePath) - active, err := l.cryptSetup.IsActive(ctx, luksDeviceName) + status, err := l.cryptSetup.Status(ctx, luksDeviceName) if err != nil { return err } - if active { + if status.Active { luksDevicePath := GenerateLUKSDevicePath(luksDeviceName) if err := l.cryptSetup.Resize(ctx, luksDeviceName); err != nil { return err diff --git a/test/integration/cryptsetup_test.go b/test/integration/cryptsetup_test.go index 1e65cd84..0b2d7856 100644 --- a/test/integration/cryptsetup_test.go +++ b/test/integration/cryptsetup_test.go @@ -15,6 +15,7 @@ func TestCryptSetup(t *testing.T) { return } + ctx := t.Context() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) cryptSetup := volumes.NewCryptSetup(logger) name := "fake" From ca5c2886b4b13f82500ccfa46b213633432ebca9 Mon Sep 17 00:00:00 2001 From: lukasmetzner Date: Wed, 13 May 2026 15:32:51 +0200 Subject: [PATCH 2/5] refactor: rename luks device status struct --- internal/volumes/cryptsetup.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/volumes/cryptsetup.go b/internal/volumes/cryptsetup.go index 89f817e7..f1e7bed7 100644 --- a/internal/volumes/cryptsetup.go +++ b/internal/volumes/cryptsetup.go @@ -11,13 +11,13 @@ import ( const cryptsetupExecuable = "cryptsetup" -type Status struct { +type LUKSDeviceStatus struct { Device string Type string Active bool } -func (s Status) IsZombie() bool { +func (s LUKSDeviceStatus) IsZombie() bool { if !s.Active { return false } @@ -32,8 +32,8 @@ func NewCryptSetup(logger *slog.Logger) *CryptSetup { return &CryptSetup{logger: logger} } -func (cs *CryptSetup) Status(ctx context.Context, device string) (Status, error) { - status := Status{Active: false} +func (cs *CryptSetup) Status(ctx context.Context, device string) (LUKSDeviceStatus, error) { + status := LUKSDeviceStatus{Active: false} output, code, err := cryptsetup(ctx, "status", device) if err != nil { if code == 4 { From 27425bfa431bd884e12256428863ad07e1cad983 Mon Sep 17 00:00:00 2001 From: lukasmetzner Date: Fri, 29 May 2026 15:14:03 +0200 Subject: [PATCH 3/5] refactor: cleanup --- test/integration/cryptsetup_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/test/integration/cryptsetup_test.go b/test/integration/cryptsetup_test.go index 0b2d7856..7d9ab591 100644 --- a/test/integration/cryptsetup_test.go +++ b/test/integration/cryptsetup_test.go @@ -24,7 +24,6 @@ func TestCryptSetup(t *testing.T) { t.Fatal(err) } passphrase := "passphrase" - ctx := t.Context() if err := cryptSetup.Format(ctx, device, passphrase); err != nil { t.Fatal(err) From 09cd36fc5b853df1a6241d8dc9fd351a8fc18b2d Mon Sep 17 00:00:00 2001 From: lukasmetzner Date: Fri, 29 May 2026 15:45:26 +0200 Subject: [PATCH 4/5] refactor: parse luks status --- internal/volumes/cryptsetup.go | 20 ++++-- internal/volumes/cryptsetup_test.go | 108 ++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 internal/volumes/cryptsetup_test.go diff --git a/internal/volumes/cryptsetup.go b/internal/volumes/cryptsetup.go index f1e7bed7..2f59961d 100644 --- a/internal/volumes/cryptsetup.go +++ b/internal/volumes/cryptsetup.go @@ -9,7 +9,7 @@ import ( "strings" ) -const cryptsetupExecuable = "cryptsetup" +const cryptsetupExecutable = "cryptsetup" type LUKSDeviceStatus struct { Device string @@ -33,16 +33,22 @@ func NewCryptSetup(logger *slog.Logger) *CryptSetup { } func (cs *CryptSetup) Status(ctx context.Context, device string) (LUKSDeviceStatus, error) { - status := LUKSDeviceStatus{Active: false} output, code, err := cryptsetup(ctx, "status", device) if err != nil { if code == 4 { - return status, nil + return LUKSDeviceStatus{Active: false}, nil } - return status, fmt.Errorf("unable to check LUKS device %s activity: %w", device, err) + return LUKSDeviceStatus{Active: false}, fmt.Errorf("unable to check LUKS device %s status: %w", device, err) } - status.Active = true + return parseLUKSStatus(output), nil +} + +// parseLUKSStatus extracts the relevant fields from the output of +// `cryptsetup status`. It assumes the device is active; a non-active device is +// reported through the exit code, not the output. +func parseLUKSStatus(output string) LUKSDeviceStatus { + status := LUKSDeviceStatus{Active: true} for line := range strings.SplitSeq(output, "\n") { key, value, ok := strings.Cut(strings.TrimSpace(line), ":") if !ok { @@ -57,7 +63,7 @@ func (cs *CryptSetup) Status(ctx context.Context, device string) (LUKSDeviceStat } } - return status, nil + return status } func (cs *CryptSetup) Format(ctx context.Context, devicePath string, passphrase string) error { @@ -159,7 +165,7 @@ func cryptsetup(ctx context.Context, args ...string) (string, int, error) { } func cryptsetupWithStdin(ctx context.Context, stdin string, args ...string) (string, int, error) { - cmd := exec.CommandContext(ctx, cryptsetupExecuable, args...) + cmd := exec.CommandContext(ctx, cryptsetupExecutable, args...) if stdin != "" { cmd.Stdin = strings.NewReader(stdin) } diff --git a/internal/volumes/cryptsetup_test.go b/internal/volumes/cryptsetup_test.go new file mode 100644 index 00000000..59c2a435 --- /dev/null +++ b/internal/volumes/cryptsetup_test.go @@ -0,0 +1,108 @@ +package volumes + +import "testing" + +func TestLUKSDeviceStatusIsZombie(t *testing.T) { + testCases := []struct { + Name string + Status LUKSDeviceStatus + Want bool + }{ + { + Name: "inactive is never a zombie", + Status: LUKSDeviceStatus{Active: false}, + Want: false, + }, + { + Name: "inactive with missing fields is never a zombie", + Status: LUKSDeviceStatus{Active: false, Type: "", Device: ""}, + Want: false, + }, + { + Name: "healthy active device", + Status: LUKSDeviceStatus{Active: true, Type: "LUKS1", Device: "/dev/sdb"}, + Want: false, + }, + { + Name: "active with empty device", + Status: LUKSDeviceStatus{Active: true, Type: "LUKS1", Device: ""}, + Want: true, + }, + { + Name: "active with empty type", + Status: LUKSDeviceStatus{Active: true, Type: "", Device: "/dev/sdb"}, + Want: true, + }, + { + Name: "active with (null) device", + Status: LUKSDeviceStatus{Active: true, Type: "LUKS1", Device: "(null)"}, + Want: true, + }, + { + Name: "active with n/a type", + Status: LUKSDeviceStatus{Active: true, Type: "n/a", Device: "/dev/sdb"}, + Want: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + if got := testCase.Status.IsZombie(); got != testCase.Want { + t.Errorf("IsZombie() = %v, want %v", got, testCase.Want) + } + }) + } +} + +func TestParseLUKSStatus(t *testing.T) { + testCases := []struct { + Name string + Output string + Want LUKSDeviceStatus + }{ + { + Name: "healthy device", + Output: `/dev/mapper/pv-1 is active and is in use. + type: LUKS1 + cipher: aes-xts-plain64 + keysize: 512 bits + device: /dev/sdb + sector size: 512 + offset: 4096 sectors + size: 2093056 sectors + mode: read/write`, + Want: LUKSDeviceStatus{Active: true, Type: "LUKS1", Device: "/dev/sdb"}, + }, + { + Name: "stale device with (null) backing", + Output: `/dev/mapper/pv-1 is active. + type: n/a + cipher: aes-xts-plain64 + keysize: 512 bits + device: (null) + mode: read/write`, + Want: LUKSDeviceStatus{Active: true, Type: "n/a", Device: "(null)"}, + }, + { + Name: "device line missing entirely", + Output: `/dev/mapper/pv-1 is active. + type: LUKS1 + cipher: aes-xts-plain64`, + Want: LUKSDeviceStatus{Active: true, Type: "LUKS1", Device: ""}, + }, + { + Name: "empty output", + Output: "", + Want: LUKSDeviceStatus{Active: true}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + got := parseLUKSStatus(testCase.Output) + if got != testCase.Want { + t.Errorf("parseLUKSStatus() = %+v, want %+v", got, testCase.Want) + } + }) + } +} From 09312bea8c8f1e1d2003014047df3201df70349c Mon Sep 17 00:00:00 2001 From: lukasmetzner Date: Mon, 1 Jun 2026 09:54:05 +0200 Subject: [PATCH 5/5] style: create luks device status --- internal/volumes/cryptsetup.go | 71 ++++++++++++++--------------- internal/volumes/cryptsetup_test.go | 2 +- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/internal/volumes/cryptsetup.go b/internal/volumes/cryptsetup.go index 2f59961d..18a41c02 100644 --- a/internal/volumes/cryptsetup.go +++ b/internal/volumes/cryptsetup.go @@ -11,19 +11,6 @@ import ( const cryptsetupExecutable = "cryptsetup" -type LUKSDeviceStatus struct { - Device string - Type string - Active bool -} - -func (s LUKSDeviceStatus) IsZombie() bool { - if !s.Active { - return false - } - return s.Device == "" || s.Type == "" || s.Device == "(null)" || s.Type == "n/a" -} - type CryptSetup struct { logger *slog.Logger } @@ -41,29 +28,7 @@ func (cs *CryptSetup) Status(ctx context.Context, device string) (LUKSDeviceStat return LUKSDeviceStatus{Active: false}, fmt.Errorf("unable to check LUKS device %s status: %w", device, err) } - return parseLUKSStatus(output), nil -} - -// parseLUKSStatus extracts the relevant fields from the output of -// `cryptsetup status`. It assumes the device is active; a non-active device is -// reported through the exit code, not the output. -func parseLUKSStatus(output string) LUKSDeviceStatus { - status := LUKSDeviceStatus{Active: true} - for line := range strings.SplitSeq(output, "\n") { - key, value, ok := strings.Cut(strings.TrimSpace(line), ":") - if !ok { - continue - } - value = strings.TrimSpace(value) - switch key { - case "type": - status.Type = value - case "device": - status.Device = value - } - } - - return status + return NewLUKSDeviceStatus(output), nil } func (cs *CryptSetup) Format(ctx context.Context, devicePath string, passphrase string) error { @@ -180,3 +145,37 @@ func cryptsetupWithStdin(ctx context.Context, stdin string, args ...string) (str } return output, 0, nil } + +type LUKSDeviceStatus struct { + Device string + Type string + Active bool +} + +func NewLUKSDeviceStatus(output string) LUKSDeviceStatus { + // Assumes the device is active; a non-active device is + // reported through the exit code, not the output. + status := LUKSDeviceStatus{Active: true} + for line := range strings.SplitSeq(output, "\n") { + key, value, ok := strings.Cut(strings.TrimSpace(line), ":") + if !ok { + continue + } + value = strings.TrimSpace(value) + switch key { + case "type": + status.Type = value + case "device": + status.Device = value + } + } + + return status +} + +func (s LUKSDeviceStatus) IsZombie() bool { + if !s.Active { + return false + } + return s.Device == "" || s.Type == "" || s.Device == "(null)" || s.Type == "n/a" +} diff --git a/internal/volumes/cryptsetup_test.go b/internal/volumes/cryptsetup_test.go index 59c2a435..3709c97a 100644 --- a/internal/volumes/cryptsetup_test.go +++ b/internal/volumes/cryptsetup_test.go @@ -99,7 +99,7 @@ func TestParseLUKSStatus(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.Name, func(t *testing.T) { - got := parseLUKSStatus(testCase.Output) + got := NewLUKSDeviceStatus(testCase.Output) if got != testCase.Want { t.Errorf("parseLUKSStatus() = %+v, want %+v", got, testCase.Want) }