From d5c91b188a6b69466684234ed212a36c9ebcb18b Mon Sep 17 00:00:00 2001 From: Samuel K Date: Mon, 4 May 2026 01:34:31 -0500 Subject: [PATCH 1/3] feat(cmd): add --workspace-mount-consistency flag to up command Allow users to specify the bind mount consistency mode (consistent, cached, delegated) for the workspace mount via CLI flag. The value is appended to the workspace mount string when no consistency is already configured. --- cmd/up.go | 13 ++++++++ cmd/up_test.go | 56 +++++++++++++++++++++++++++++++++ pkg/devcontainer/config.go | 5 +++ pkg/devcontainer/config_test.go | 46 +++++++++++++++++++++++++++ pkg/provider/workspace.go | 1 + 5 files changed, 121 insertions(+) diff --git a/cmd/up.go b/cmd/up.go index d87f3050c..a63ec7abd 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -144,6 +144,16 @@ func (cmd *UpCmd) validate() error { } cmd.ExtraDevContainerPath = absPath } + if cmd.WorkspaceMountConsistency != "" { + switch cmd.WorkspaceMountConsistency { + case "consistent", "cached", "delegated": + default: + return fmt.Errorf( + "invalid --workspace-mount-consistency value %q: must be one of consistent, cached, delegated", + cmd.WorkspaceMountConsistency, + ) + } + } return nil } @@ -298,6 +308,9 @@ func (cmd *UpCmd) registerWorkspaceFlags(upCmd *cobra.Command) { StringArrayVar(&cmd.CacheFrom, "cache-from", []string{}, "Cache sources for the build (e.g., myregistry.io/cache:latest or type=registry,ref=...). "+ "Takes priority over devcontainer.json build.cacheFrom") + upCmd.Flags(). + StringVar(&cmd.WorkspaceMountConsistency, "workspace-mount-consistency", "", + "Consistency mode for the workspace bind mount (consistent, cached, delegated)") } func (cmd *UpCmd) registerTestingFlags(upCmd *cobra.Command) { diff --git a/cmd/up_test.go b/cmd/up_test.go index 602642f47..620b0b9b6 100644 --- a/cmd/up_test.go +++ b/cmd/up_test.go @@ -56,3 +56,59 @@ func TestUpCmd_FlagParsesValue(t *testing.T) { flag := upCmd.Flags().Lookup("default-user-env-probe") assert.Equal(t, probeNone, flag.Value.String()) } + +func TestUpCmd_WorkspaceMountConsistencyFlag(t *testing.T) { + upCmd := NewUpCmd(&flags.GlobalFlags{}) + flag := upCmd.Flags().Lookup("workspace-mount-consistency") + require.NotNil(t, flag) + assert.Equal(t, "", flag.DefValue) +} + +func TestUpCmd_WorkspaceMountConsistencyFlagParsesValue(t *testing.T) { + tests := []struct { + name string + value string + }{ + {name: "consistent", value: "consistent"}, + {name: "cached", value: "cached"}, + {name: "delegated", value: "delegated"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + upCmd := NewUpCmd(&flags.GlobalFlags{}) + err := upCmd.ParseFlags([]string{"--workspace-mount-consistency", tt.value}) + require.NoError(t, err) + + flag := upCmd.Flags().Lookup("workspace-mount-consistency") + assert.Equal(t, tt.value, flag.Value.String()) + }) + } +} + +func TestUpCmd_ValidateWorkspaceMountConsistency(t *testing.T) { + tests := []struct { + name string + value string + wantErr bool + }{ + {name: "empty is valid", value: "", wantErr: false}, + {name: "consistent", value: "consistent", wantErr: false}, + {name: "cached", value: "cached", wantErr: false}, + {name: "delegated", value: "delegated", wantErr: false}, + {name: "invalid value", value: "bogus", wantErr: true}, + {name: "partial match", value: "cache", wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := &UpCmd{GlobalFlags: &flags.GlobalFlags{}} + cmd.WorkspaceMountConsistency = tt.value + err := cmd.validate() + if tt.wantErr { + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid --workspace-mount-consistency") + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/devcontainer/config.go b/pkg/devcontainer/config.go index ada9ccba5..9846a4e4f 100644 --- a/pkg/devcontainer/config.go +++ b/pkg/devcontainer/config.go @@ -189,6 +189,11 @@ func (r *runner) substitute( substitutionContext.WorkspaceMount = parsedConfig.WorkspaceMount } + if options.WorkspaceMountConsistency != "" && + !mountHasConsistency(substitutionContext.WorkspaceMount) { + substitutionContext.WorkspaceMount += ",consistency=" + options.WorkspaceMountConsistency + } + if options.DevContainerImage != "" { parsedConfig.Build = nil parsedConfig.Dockerfile = "" diff --git a/pkg/devcontainer/config_test.go b/pkg/devcontainer/config_test.go index db7c604a1..977ec229e 100644 --- a/pkg/devcontainer/config_test.go +++ b/pkg/devcontainer/config_test.go @@ -247,4 +247,50 @@ func (s *SubstituteTestSuite) TestSubstitute_AdditionalFeaturesEmpty() { s.Nil(result.Config.Features) } +func (s *SubstituteTestSuite) TestSubstitute_WorkspaceMountConsistencyApplied() { + rawConfig := &config.DevContainerConfig{ + ImageContainer: config.ImageContainer{Image: "alpine:latest"}, + } + options := provider2.CLIOptions{ + WorkspaceMountConsistency: "delegated", + } + + _, ctx, err := s.runner.substitute(options, rawConfig) + + s.NoError(err) + s.Contains(ctx.WorkspaceMount, "consistency=delegated") +} + +func (s *SubstituteTestSuite) TestSubstitute_WorkspaceMountConsistencyNotOverrideExisting() { + rawConfig := &config.DevContainerConfig{ + ImageContainer: config.ImageContainer{Image: "alpine:latest"}, + NonComposeBase: config.NonComposeBase{ + WorkspaceMount: "type=bind,source=/src,target=/ws,consistency=cached", + }, + } + options := provider2.CLIOptions{ + WorkspaceMountConsistency: "delegated", + } + + _, ctx, err := s.runner.substitute(options, rawConfig) + + s.NoError(err) + s.Contains(ctx.WorkspaceMount, "consistency=cached") + s.NotContains(ctx.WorkspaceMount, "consistency=delegated") +} + +func (s *SubstituteTestSuite) TestSubstitute_WorkspaceMountConsistencyEmpty() { + rawConfig := &config.DevContainerConfig{ + ImageContainer: config.ImageContainer{Image: "alpine:latest"}, + } + options := provider2.CLIOptions{ + WorkspaceMountConsistency: "", + } + + _, ctx, err := s.runner.substitute(options, rawConfig) + + s.NoError(err) + s.NotContains(ctx.WorkspaceMount, "consistency=delegated") +} + func ptr(s string) *string { return &s } diff --git a/pkg/provider/workspace.go b/pkg/provider/workspace.go index cebab0f49..7345c662a 100644 --- a/pkg/provider/workspace.go +++ b/pkg/provider/workspace.go @@ -238,6 +238,7 @@ type CLIOptions struct { GidMap []string `json:"gidMap,omitempty"` IDLabels []string `json:"idLabels,omitempty"` GPUAvailability string `json:"gpuAvailability,omitempty"` + WorkspaceMountConsistency string `json:"workspaceMountConsistency,omitempty"` // dotfiles options DotfilesRepo string `json:"dotfilesRepo,omitempty"` From fc40362b4f3bf0128a7ec050dc8d887c2bdbf3fe Mon Sep 17 00:00:00 2001 From: Samuel K Date: Mon, 4 May 2026 01:44:57 -0500 Subject: [PATCH 2/3] fix: replace existing mount consistency when flag is explicitly set On macOS, getWorkspace() injects consistency='consistent' as the default before substitute() runs. The previous logic only appended when no consistency value existed, causing the user's explicit --workspace-mount-consistency flag to be silently ignored. Now mountSetConsistency() replaces any existing consistency value when the flag is set, and uses single-quoted values to match existing style. --- pkg/devcontainer/config.go | 8 ++++--- pkg/devcontainer/config_test.go | 10 ++++---- pkg/devcontainer/run.go | 16 +++++++++++++ pkg/devcontainer/run_test.go | 42 +++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 8 deletions(-) diff --git a/pkg/devcontainer/config.go b/pkg/devcontainer/config.go index 9846a4e4f..6a0800931 100644 --- a/pkg/devcontainer/config.go +++ b/pkg/devcontainer/config.go @@ -189,9 +189,11 @@ func (r *runner) substitute( substitutionContext.WorkspaceMount = parsedConfig.WorkspaceMount } - if options.WorkspaceMountConsistency != "" && - !mountHasConsistency(substitutionContext.WorkspaceMount) { - substitutionContext.WorkspaceMount += ",consistency=" + options.WorkspaceMountConsistency + if options.WorkspaceMountConsistency != "" { + substitutionContext.WorkspaceMount = mountSetConsistency( + substitutionContext.WorkspaceMount, + options.WorkspaceMountConsistency, + ) } if options.DevContainerImage != "" { diff --git a/pkg/devcontainer/config_test.go b/pkg/devcontainer/config_test.go index 977ec229e..7a163bcd0 100644 --- a/pkg/devcontainer/config_test.go +++ b/pkg/devcontainer/config_test.go @@ -258,14 +258,14 @@ func (s *SubstituteTestSuite) TestSubstitute_WorkspaceMountConsistencyApplied() _, ctx, err := s.runner.substitute(options, rawConfig) s.NoError(err) - s.Contains(ctx.WorkspaceMount, "consistency=delegated") + s.Contains(ctx.WorkspaceMount, "consistency='delegated'") } -func (s *SubstituteTestSuite) TestSubstitute_WorkspaceMountConsistencyNotOverrideExisting() { +func (s *SubstituteTestSuite) TestSubstitute_WorkspaceMountConsistencyReplacesExisting() { rawConfig := &config.DevContainerConfig{ ImageContainer: config.ImageContainer{Image: "alpine:latest"}, NonComposeBase: config.NonComposeBase{ - WorkspaceMount: "type=bind,source=/src,target=/ws,consistency=cached", + WorkspaceMount: "type=bind,source=/src,target=/ws,consistency='consistent'", }, } options := provider2.CLIOptions{ @@ -275,8 +275,8 @@ func (s *SubstituteTestSuite) TestSubstitute_WorkspaceMountConsistencyNotOverrid _, ctx, err := s.runner.substitute(options, rawConfig) s.NoError(err) - s.Contains(ctx.WorkspaceMount, "consistency=cached") - s.NotContains(ctx.WorkspaceMount, "consistency=delegated") + s.Contains(ctx.WorkspaceMount, "consistency='delegated'") + s.NotContains(ctx.WorkspaceMount, "consistency='consistent'") } func (s *SubstituteTestSuite) TestSubstitute_WorkspaceMountConsistencyEmpty() { diff --git a/pkg/devcontainer/run.go b/pkg/devcontainer/run.go index b305f3c76..51e89bf91 100644 --- a/pkg/devcontainer/run.go +++ b/pkg/devcontainer/run.go @@ -322,6 +322,22 @@ func mountHasConsistency(mount string) bool { return false } +func mountSetConsistency(mount, value string) string { + quoted := "consistency='" + value + "'" + var parts []string + for part := range strings.SplitSeq(mount, ",") { + if strings.HasPrefix(part, "consistency=") { + parts = append(parts, quoted) + } else { + parts = append(parts, part) + } + } + if !mountHasConsistency(mount) { + parts = append(parts, quoted) + } + return strings.Join(parts, ",") +} + func needsDefaultConsistency() bool { return runtime.GOOS != "linux" } diff --git a/pkg/devcontainer/run_test.go b/pkg/devcontainer/run_test.go index 13126ea64..b355f859f 100644 --- a/pkg/devcontainer/run_test.go +++ b/pkg/devcontainer/run_test.go @@ -235,3 +235,45 @@ func TestMountHasConsistency(t *testing.T) { } } } + +func TestMountSetConsistency(t *testing.T) { + tests := []struct { + name string + mount string + value string + want string + }{ + { + name: "appends when no consistency present", + mount: "type=bind,source=/s,target=/t", + value: "delegated", + want: "type=bind,source=/s,target=/t,consistency='delegated'", + }, + { + name: "replaces existing single-quoted default", + mount: "type=bind,source=/s,target=/t,consistency='consistent'", + value: "delegated", + want: "type=bind,source=/s,target=/t,consistency='delegated'", + }, + { + name: "replaces existing unquoted value", + mount: "type=bind,source=/s,target=/t,consistency=cached", + value: "delegated", + want: "type=bind,source=/s,target=/t,consistency='delegated'", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := mountSetConsistency(tt.mount, tt.value) + if got != tt.want { + t.Errorf( + "mountSetConsistency(%q, %q) =\n %q\nwant:\n %q", + tt.mount, + tt.value, + got, + tt.want, + ) + } + }) + } +} From cd064cee46ea2a775cabccf62112dd30bf23cd99 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Mon, 4 May 2026 02:00:06 -0500 Subject: [PATCH 3/3] fix: extract mount consistency string literals to constants (goconst) --- cmd/up.go | 11 +++++++++-- cmd/up_test.go | 12 ++++++------ pkg/devcontainer/config_test.go | 10 ++++++---- pkg/devcontainer/run_test.go | 14 ++++++++------ 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/cmd/up.go b/cmd/up.go index a63ec7abd..d4ad540d7 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -33,6 +33,12 @@ import ( "github.com/spf13/cobra" ) +const ( + MountConsistencyConsistent = "consistent" + MountConsistencyCached = "cached" + MountConsistencyDelegated = "delegated" +) + // UpCmd holds the up cmd flags. type UpCmd struct { provider2.CLIOptions @@ -146,11 +152,12 @@ func (cmd *UpCmd) validate() error { } if cmd.WorkspaceMountConsistency != "" { switch cmd.WorkspaceMountConsistency { - case "consistent", "cached", "delegated": + case MountConsistencyConsistent, MountConsistencyCached, MountConsistencyDelegated: default: return fmt.Errorf( - "invalid --workspace-mount-consistency value %q: must be one of consistent, cached, delegated", + "invalid --workspace-mount-consistency value %q: must be one of %s, %s, %s", cmd.WorkspaceMountConsistency, + MountConsistencyConsistent, MountConsistencyCached, MountConsistencyDelegated, ) } } diff --git a/cmd/up_test.go b/cmd/up_test.go index 620b0b9b6..fe38cbe9c 100644 --- a/cmd/up_test.go +++ b/cmd/up_test.go @@ -69,9 +69,9 @@ func TestUpCmd_WorkspaceMountConsistencyFlagParsesValue(t *testing.T) { name string value string }{ - {name: "consistent", value: "consistent"}, - {name: "cached", value: "cached"}, - {name: "delegated", value: "delegated"}, + {name: "consistent", value: MountConsistencyConsistent}, + {name: "cached", value: MountConsistencyCached}, + {name: "delegated", value: MountConsistencyDelegated}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -92,9 +92,9 @@ func TestUpCmd_ValidateWorkspaceMountConsistency(t *testing.T) { wantErr bool }{ {name: "empty is valid", value: "", wantErr: false}, - {name: "consistent", value: "consistent", wantErr: false}, - {name: "cached", value: "cached", wantErr: false}, - {name: "delegated", value: "delegated", wantErr: false}, + {name: "consistent", value: MountConsistencyConsistent, wantErr: false}, + {name: "cached", value: MountConsistencyCached, wantErr: false}, + {name: "delegated", value: MountConsistencyDelegated, wantErr: false}, {name: "invalid value", value: "bogus", wantErr: true}, {name: "partial match", value: "cache", wantErr: true}, } diff --git a/pkg/devcontainer/config_test.go b/pkg/devcontainer/config_test.go index 7a163bcd0..9cace35e7 100644 --- a/pkg/devcontainer/config_test.go +++ b/pkg/devcontainer/config_test.go @@ -248,20 +248,22 @@ func (s *SubstituteTestSuite) TestSubstitute_AdditionalFeaturesEmpty() { } func (s *SubstituteTestSuite) TestSubstitute_WorkspaceMountConsistencyApplied() { + const testVal = "delegated" rawConfig := &config.DevContainerConfig{ ImageContainer: config.ImageContainer{Image: "alpine:latest"}, } options := provider2.CLIOptions{ - WorkspaceMountConsistency: "delegated", + WorkspaceMountConsistency: testVal, } _, ctx, err := s.runner.substitute(options, rawConfig) s.NoError(err) - s.Contains(ctx.WorkspaceMount, "consistency='delegated'") + s.Contains(ctx.WorkspaceMount, "consistency='"+testVal+"'") } func (s *SubstituteTestSuite) TestSubstitute_WorkspaceMountConsistencyReplacesExisting() { + const testVal = "delegated" rawConfig := &config.DevContainerConfig{ ImageContainer: config.ImageContainer{Image: "alpine:latest"}, NonComposeBase: config.NonComposeBase{ @@ -269,13 +271,13 @@ func (s *SubstituteTestSuite) TestSubstitute_WorkspaceMountConsistencyReplacesEx }, } options := provider2.CLIOptions{ - WorkspaceMountConsistency: "delegated", + WorkspaceMountConsistency: testVal, } _, ctx, err := s.runner.substitute(options, rawConfig) s.NoError(err) - s.Contains(ctx.WorkspaceMount, "consistency='delegated'") + s.Contains(ctx.WorkspaceMount, "consistency='"+testVal+"'") s.NotContains(ctx.WorkspaceMount, "consistency='consistent'") } diff --git a/pkg/devcontainer/run_test.go b/pkg/devcontainer/run_test.go index b355f859f..4f16cc7fc 100644 --- a/pkg/devcontainer/run_test.go +++ b/pkg/devcontainer/run_test.go @@ -237,6 +237,8 @@ func TestMountHasConsistency(t *testing.T) { } func TestMountSetConsistency(t *testing.T) { + const testConsistency = "delegated" + wantSuffix := "consistency='" + testConsistency + "'" tests := []struct { name string mount string @@ -246,20 +248,20 @@ func TestMountSetConsistency(t *testing.T) { { name: "appends when no consistency present", mount: "type=bind,source=/s,target=/t", - value: "delegated", - want: "type=bind,source=/s,target=/t,consistency='delegated'", + value: testConsistency, + want: "type=bind,source=/s,target=/t," + wantSuffix, }, { name: "replaces existing single-quoted default", mount: "type=bind,source=/s,target=/t,consistency='consistent'", - value: "delegated", - want: "type=bind,source=/s,target=/t,consistency='delegated'", + value: testConsistency, + want: "type=bind,source=/s,target=/t," + wantSuffix, }, { name: "replaces existing unquoted value", mount: "type=bind,source=/s,target=/t,consistency=cached", - value: "delegated", - want: "type=bind,source=/s,target=/t,consistency='delegated'", + value: testConsistency, + want: "type=bind,source=/s,target=/t," + wantSuffix, }, } for _, tt := range tests {