diff --git a/pkg/devcontainer/config.go b/pkg/devcontainer/config.go index d7358c5f9..73c181e33 100644 --- a/pkg/devcontainer/config.go +++ b/pkg/devcontainer/config.go @@ -192,8 +192,8 @@ func (r *runner) substitute( if parsedConfig.WorkspaceFolder != "" { substitutionContext.ContainerWorkspaceFolder = parsedConfig.WorkspaceFolder } - if parsedConfig.WorkspaceMount != "" { - substitutionContext.WorkspaceMount = parsedConfig.WorkspaceMount + if parsedConfig.WorkspaceMount != nil { + substitutionContext.WorkspaceMount = *parsedConfig.WorkspaceMount } if options.WorkspaceMountConsistency != "" { diff --git a/pkg/devcontainer/config/config.go b/pkg/devcontainer/config/config.go index 6c1f6ae5d..0f83c60fa 100644 --- a/pkg/devcontainer/config/config.go +++ b/pkg/devcontainer/config/config.go @@ -219,7 +219,8 @@ type NonComposeBase struct { RunArgs []string `json:"runArgs,omitempty"` // The --mount parameter for docker run. The default is to mount the project folder at /workspaces/$project. - WorkspaceMount string `json:"workspaceMount,omitempty"` + // Per the devcontainer spec, empty string suppresses the default workspace mount. + WorkspaceMount *string `json:"workspaceMount,omitempty"` } type DockerfileContainer struct { diff --git a/pkg/devcontainer/config/extends.go b/pkg/devcontainer/config/extends.go index dee5196c8..b68ae114b 100644 --- a/pkg/devcontainer/config/extends.go +++ b/pkg/devcontainer/config/extends.go @@ -185,7 +185,7 @@ func mergeContainerScalars(result, child *DevContainerConfig) { if child.WorkspaceFolder != "" { result.WorkspaceFolder = child.WorkspaceFolder } - if child.WorkspaceMount != "" { + if child.WorkspaceMount != nil { result.WorkspaceMount = child.WorkspaceMount } if child.ShutdownAction != "" { diff --git a/pkg/devcontainer/config/result.go b/pkg/devcontainer/config/result.go index 652b184b4..d5630d325 100644 --- a/pkg/devcontainer/config/result.go +++ b/pkg/devcontainer/config/result.go @@ -26,8 +26,11 @@ type DevContainerConfigWithPath struct { } func GetMounts(result *Result) []*Mount { - workspaceMount := ParseMount(result.SubstitutionContext.WorkspaceMount) - mounts := []*Mount{&workspaceMount} + var mounts []*Mount + if result.SubstitutionContext.WorkspaceMount != "" { + workspaceMount := ParseMount(result.SubstitutionContext.WorkspaceMount) + mounts = append(mounts, &workspaceMount) + } for _, m := range result.MergedConfig.Mounts { if m.Type == "bind" { mounts = append(mounts, m) diff --git a/pkg/devcontainer/config_test.go b/pkg/devcontainer/config_test.go index 9cace35e7..ed28b7b37 100644 --- a/pkg/devcontainer/config_test.go +++ b/pkg/devcontainer/config_test.go @@ -267,7 +267,7 @@ func (s *SubstituteTestSuite) TestSubstitute_WorkspaceMountConsistencyReplacesEx rawConfig := &config.DevContainerConfig{ ImageContainer: config.ImageContainer{Image: "alpine:latest"}, NonComposeBase: config.NonComposeBase{ - WorkspaceMount: "type=bind,source=/src,target=/ws,consistency='consistent'", + WorkspaceMount: ptr("type=bind,source=/src,target=/ws,consistency='consistent'"), }, } options := provider2.CLIOptions{ diff --git a/pkg/devcontainer/run.go b/pkg/devcontainer/run.go index 25568744b..251e69fbf 100644 --- a/pkg/devcontainer/run.go +++ b/pkg/devcontainer/run.go @@ -350,9 +350,18 @@ func getWorkspace( workspaceFolder, workspaceID string, conf *config.DevContainerConfig, ) (string, string) { - if conf.WorkspaceMount != "" { - mount := config.ParseMount(conf.WorkspaceMount) - ws := conf.WorkspaceMount + if conf.WorkspaceMount != nil { + // Explicit empty string means suppress the workspace mount entirely. + if *conf.WorkspaceMount == "" { + containerMountFolder := conf.WorkspaceFolder + if containerMountFolder == "" { + containerMountFolder = "/workspaces/" + workspaceID + } + return "", containerMountFolder + } + + mount := config.ParseMount(*conf.WorkspaceMount) + ws := *conf.WorkspaceMount if needsDefaultConsistency() && !mountHasConsistency(ws) { ws += ",consistency='consistent'" } diff --git a/pkg/devcontainer/run_test.go b/pkg/devcontainer/run_test.go index 4f16cc7fc..444673a28 100644 --- a/pkg/devcontainer/run_test.go +++ b/pkg/devcontainer/run_test.go @@ -131,11 +131,13 @@ func searchString(s, substr string) bool { return false } +func strPtr(s string) *string { return &s } + func TestGetWorkspace_CustomWorkspaceMount(t *testing.T) { customMount := "type=bind,source=/host/src,target=/custom-ws" conf := &config.DevContainerConfig{ NonComposeBase: config.NonComposeBase{ - WorkspaceMount: customMount, + WorkspaceMount: strPtr(customMount), }, } @@ -185,17 +187,30 @@ func TestGetWorkspace_DefaultMountWithWorkspaceFolder(t *testing.T) { func TestGetWorkspace_EmptyWorkspaceMount(t *testing.T) { conf := &config.DevContainerConfig{ NonComposeBase: config.NonComposeBase{ - WorkspaceMount: "", + WorkspaceMount: strPtr(""), }, } mount, folder := getWorkspace("/home/user/project", "ws-id", conf) + if mount != "" { + t.Fatalf("expected empty mount string (suppressed), got %q", mount) + } if folder != "/workspaces/ws-id" { t.Fatalf("expected default folder, got %q", folder) } +} + +func TestGetWorkspace_NilWorkspaceMount(t *testing.T) { + conf := &config.DevContainerConfig{} + + mount, folder := getWorkspace("/home/user/project", "ws-id", conf) + if !contains(mount, "type=bind") { - t.Fatalf("expected default bind mount, got %q", mount) + t.Fatalf("expected default bind mount for nil WorkspaceMount, got %q", mount) + } + if folder != "/workspaces/ws-id" { + t.Fatalf("expected default folder, got %q", folder) } } @@ -203,7 +218,7 @@ func TestGetWorkspace_UserConsistencyPreserved(t *testing.T) { customMount := "type=bind,source=/src,target=/ws,consistency=delegated" conf := &config.DevContainerConfig{ NonComposeBase: config.NonComposeBase{ - WorkspaceMount: customMount, + WorkspaceMount: strPtr(customMount), }, } diff --git a/pkg/devcontainer/single.go b/pkg/devcontainer/single.go index 884104803..a4adf329d 100644 --- a/pkg/devcontainer/single.go +++ b/pkg/devcontainer/single.go @@ -428,8 +428,12 @@ func (r *runner) getDockerlessRunOptions( substitutionContext *config.SubstitutionContext, buildInfo *config.BuildInfo, ) (*driver.RunOptions, error) { - // parse workspace mount - workspaceMountParsed := config.ParseMount(substitutionContext.WorkspaceMount) + // parse workspace mount — nil when suppressed via workspaceMount: "" + var workspaceMountPtr *config.Mount + if substitutionContext.WorkspaceMount != "" { + parsed := config.ParseMount(substitutionContext.WorkspaceMount) + workspaceMountPtr = &parsed + } // add metadata as label here marshalled, err := metadata.MarshalImageMetadata(buildInfo.ImageMetadata.Raw) @@ -496,7 +500,7 @@ func (r *runner) getDockerlessRunOptions( }, Privileged: mergedConfig.Privileged, Init: mergedConfig.Init, - WorkspaceMount: &workspaceMountParsed, + WorkspaceMount: workspaceMountPtr, Mounts: mounts, Userns: substitutionContext.Userns, UidMap: substitutionContext.UidMap, @@ -509,8 +513,12 @@ func (r *runner) getRunOptions( substitutionContext *config.SubstitutionContext, buildInfo *config.BuildInfo, ) (*driver.RunOptions, error) { - // parse workspace mount - workspaceMountParsed := config.ParseMount(substitutionContext.WorkspaceMount) + // parse workspace mount — nil when suppressed via workspaceMount: "" + var workspaceMountPtr *config.Mount + if substitutionContext.WorkspaceMount != "" { + parsed := config.ParseMount(substitutionContext.WorkspaceMount) + workspaceMountPtr = &parsed + } // add metadata as label here marshalled, err := metadata.MarshalImageMetadata(buildInfo.ImageMetadata.Raw) @@ -553,7 +561,7 @@ func (r *runner) getRunOptions( Labels: labels, Privileged: mergedConfig.Privileged, Init: mergedConfig.Init, - WorkspaceMount: &workspaceMountParsed, + WorkspaceMount: workspaceMountPtr, SecurityOpt: mergedConfig.SecurityOpt, Mounts: mergedConfig.Mounts, Userns: substitutionContext.Userns, diff --git a/pkg/driver/kubernetes/run.go b/pkg/driver/kubernetes/run.go index 3171d4b4c..6575a708e 100644 --- a/pkg/driver/kubernetes/run.go +++ b/pkg/driver/kubernetes/run.go @@ -104,6 +104,11 @@ func (k *KubernetesDriver) runContainer( ) (err error) { // get workspace mount mount := options.WorkspaceMount + if mount == nil { + return fmt.Errorf( + "workspace mount is suppressed; cannot run in Kubernetes without a workspace mount", + ) + } if mount.Target == "" { return fmt.Errorf("workspace mount target is empty") }