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
1 change: 1 addition & 0 deletions cmd/workspace/up/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ func (cmd *UpCmd) buildWorkspaceOptions(workspace *provider2.Workspace) provider
baseOptions.ID = workspace.ID
baseOptions.DevContainerPath = workspace.DevContainerPath
baseOptions.DevContainerImage = workspace.DevContainerImage
baseOptions.DevContainerSource = workspace.DevContainerSource
baseOptions.IDE = workspace.IDE.Name
baseOptions.IDEOptions = nil
baseOptions.Source = workspace.Source.String()
Expand Down
1 change: 1 addition & 0 deletions cmd/workspace/up/up_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ func (cmd *UpCmd) resolveParams(
ReconfigureProvider: cmd.Reconfigure,
DevContainerImage: cmd.DevContainerImage,
DevContainerPath: cmd.DevContainerPath,
DevContainerSource: cmd.DevContainerSource,
SSHConfigPath: cmd.SSHConfigPath,
SSHConfigIncludePath: devsyConfig.ContextOption(
config.ContextOptionSSHConfigIncludePath,
Expand Down
11 changes: 8 additions & 3 deletions pkg/devcontainer/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ const (
// each supported source in order and falling back to an auto-detected default
// when none applies.
func (r *runner) getRawConfig(options provider.CLIOptions) (*config.DevContainerConfig, error) {
if options.DevContainerSource != "" {
return r.rawConfigFromSource(options)
source := options.DevContainerSource
if source == "" {
source = r.workspaceConfig.Workspace.DevContainerSource
}
if source != "" {
return r.rawConfigFromSource(source, options)
}
if conf := r.rawConfigFromWorkspace(); conf != nil {
return conf, nil
Expand Down Expand Up @@ -169,9 +173,10 @@ func workspaceMountFolderWarning(conf *config.DevContainerConfig) string {
}

func (r *runner) rawConfigFromSource(
source string,
options provider.CLIOptions,
) (*config.DevContainerConfig, error) {
spec, err := ParseSourceSpec(options.DevContainerSource)
spec, err := ParseSourceSpec(source)
if err != nil {
return nil, err
}
Expand Down
35 changes: 35 additions & 0 deletions pkg/devcontainer/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,41 @@ func TestGetRawConfig_SourceImageBypassesDiscovery(t *testing.T) {
}
}

func TestGetRawConfig_PersistedSourceImageBypassesDiscovery(t *testing.T) {
folder := t.TempDir()
seedAmbiguousProfiles(t, folder)
r := newRunnerAt(folder)
// Simulate a restart: the CLI option is absent, but the override was
// persisted on the workspace during the first up.
const image = "python"
r.workspaceConfig.Workspace.DevContainerSource = "image:" + image

conf, err := r.getRawConfig(provider2.CLIOptions{})
if err != nil {
t.Fatalf("getRawConfig: %v", err)
}
if conf.Image != image {
t.Errorf("Image = %q, want %q", conf.Image, image)
}
if len(conf.Features) != 0 {
t.Errorf("Features = %v, want none (project features must be ignored)", conf.Features)
}
}

func TestGetRawConfig_CLISourceOverridesPersisted(t *testing.T) {
folder := t.TempDir()
r := newRunnerAt(folder)
r.workspaceConfig.Workspace.DevContainerSource = "image:persisted"

conf, err := r.getRawConfig(provider2.CLIOptions{DevContainerSource: "image:cli"})
if err != nil {
t.Fatalf("getRawConfig: %v", err)
}
if conf.Image != "cli" {
t.Errorf("Image = %q, want %q (CLI option must win over persisted)", conf.Image, "cli")
}
}

func TestGetRawConfig_SourceNoneWithImageBypassesDiscovery(t *testing.T) {
folder := t.TempDir()
seedAmbiguousProfiles(t, folder)
Expand Down
5 changes: 5 additions & 0 deletions pkg/provider/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ type Workspace struct {
// DevContainerPath is the relative path where the devcontainer.json is located.
DevContainerPath string `json:"devContainerPath,omitempty"`

// DevContainerSource is the devcontainer source override (e.g. "image:<ref>"
// or "none") that ignores the project's devcontainer.json. It is persisted so
// restarts reuse the same override instead of falling back to discovery.
DevContainerSource string `json:"devContainerSource,omitempty"`

// DevContainerConfig holds the config for the devcontainer.json.
DevContainerConfig *devcontainerconfig.DevContainerConfig `json:"devContainerConfig,omitempty"`

Expand Down
6 changes: 6 additions & 0 deletions pkg/workspace/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type ResolveParams struct {
ReconfigureProvider bool
DevContainerImage string
DevContainerPath string
DevContainerSource string
SSHConfigPath string
SSHConfigIncludePath string
Source *providerpkg.WorkspaceSource
Expand Down Expand Up @@ -131,6 +132,11 @@ func applyDevContainerOverrides(workspace *providerpkg.Workspace, params Resolve
workspace.DevContainerPath = params.DevContainerPath
changed = true
}
if params.DevContainerSource != "" &&
workspace.DevContainerSource != params.DevContainerSource {
workspace.DevContainerSource = params.DevContainerSource
changed = true
}

if !changed && workspace.Source.Container == "" {
return nil
Expand Down
16 changes: 16 additions & 0 deletions pkg/workspace/workspace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,19 @@ func TestApplyDevContainerOverrides_SetsImageAndPath(t *testing.T) {
assert.Equal(t, testDevContainerImage, loaded.DevContainerImage)
assert.Equal(t, testDevContainerPath, loaded.DevContainerPath)
}

func TestApplyDevContainerOverrides_PersistsSource(t *testing.T) {
setupTestPathManager(t)

ws := &providerpkg.Workspace{ID: "ws-source", Context: testDefaultContext}
const source = "image:ghcr.io/example/image:latest"
err := applyDevContainerOverrides(ws, ResolveParams{DevContainerSource: source})
require.NoError(t, err)

assert.Equal(t, source, ws.DevContainerSource)

// The source override must survive a restart, so it has to be persisted.
loaded, err := providerpkg.LoadWorkspaceConfig(testDefaultContext, ws.ID)
require.NoError(t, err)
assert.Equal(t, source, loaded.DevContainerSource)
}
Loading