Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions cmd/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -144,6 +150,17 @@ func (cmd *UpCmd) validate() error {
}
cmd.ExtraDevContainerPath = absPath
}
if cmd.WorkspaceMountConsistency != "" {
switch cmd.WorkspaceMountConsistency {
case MountConsistencyConsistent, MountConsistencyCached, MountConsistencyDelegated:
default:
return fmt.Errorf(
"invalid --workspace-mount-consistency value %q: must be one of %s, %s, %s",
cmd.WorkspaceMountConsistency,
MountConsistencyConsistent, MountConsistencyCached, MountConsistencyDelegated,
)
}
}
return nil
}

Expand Down Expand Up @@ -298,6 +315,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) {
Expand Down
56 changes: 56 additions & 0 deletions cmd/up_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: MountConsistencyConsistent},
{name: "cached", value: MountConsistencyCached},
{name: "delegated", value: MountConsistencyDelegated},
}
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: 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},
}
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)
}
})
}
}
7 changes: 7 additions & 0 deletions pkg/devcontainer/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,13 @@ func (r *runner) substitute(
substitutionContext.WorkspaceMount = parsedConfig.WorkspaceMount
}

if options.WorkspaceMountConsistency != "" {
substitutionContext.WorkspaceMount = mountSetConsistency(
substitutionContext.WorkspaceMount,
options.WorkspaceMountConsistency,
)
}

if options.DevContainerImage != "" {
parsedConfig.Build = nil
parsedConfig.Dockerfile = ""
Expand Down
48 changes: 48 additions & 0 deletions pkg/devcontainer/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,4 +247,52 @@ func (s *SubstituteTestSuite) TestSubstitute_AdditionalFeaturesEmpty() {
s.Nil(result.Config.Features)
}

func (s *SubstituteTestSuite) TestSubstitute_WorkspaceMountConsistencyApplied() {
const testVal = "delegated"
rawConfig := &config.DevContainerConfig{
ImageContainer: config.ImageContainer{Image: "alpine:latest"},
}
options := provider2.CLIOptions{
WorkspaceMountConsistency: testVal,
}

_, ctx, err := s.runner.substitute(options, rawConfig)

s.NoError(err)
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{
WorkspaceMount: "type=bind,source=/src,target=/ws,consistency='consistent'",
},
}
options := provider2.CLIOptions{
WorkspaceMountConsistency: testVal,
}

_, ctx, err := s.runner.substitute(options, rawConfig)

s.NoError(err)
s.Contains(ctx.WorkspaceMount, "consistency='"+testVal+"'")
s.NotContains(ctx.WorkspaceMount, "consistency='consistent'")
}

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 }
16 changes: 16 additions & 0 deletions pkg/devcontainer/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
44 changes: 44 additions & 0 deletions pkg/devcontainer/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,47 @@ func TestMountHasConsistency(t *testing.T) {
}
}
}

func TestMountSetConsistency(t *testing.T) {
const testConsistency = "delegated"
wantSuffix := "consistency='" + testConsistency + "'"
tests := []struct {
name string
mount string
value string
want string
}{
{
name: "appends when no consistency present",
mount: "type=bind,source=/s,target=/t",
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: testConsistency,
want: "type=bind,source=/s,target=/t," + wantSuffix,
},
{
name: "replaces existing unquoted value",
mount: "type=bind,source=/s,target=/t,consistency=cached",
value: testConsistency,
want: "type=bind,source=/s,target=/t," + wantSuffix,
},
}
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,
)
}
})
}
}
1 change: 1 addition & 0 deletions pkg/provider/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
Loading