Skip to content

Commit b0e382c

Browse files
committed
Add support of devcontainer.user.json file
1 parent 99f5279 commit b0e382c

File tree

7 files changed

+139
-23
lines changed

7 files changed

+139
-23
lines changed

cmd/up.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@ func NewUpCmd(f *flags.GlobalFlags) *cobra.Command {
8282
Use: "up [flags] [workspace-path|workspace-name]",
8383
Short: "Starts a new workspace",
8484
RunE: func(cobraCmd *cobra.Command, args []string) error {
85+
absExtraDevContainerPaths := []string{}
86+
for _, extraPath := range cmd.ExtraDevContainerPaths {
87+
absExtraPath, err := filepath.Abs(extraPath)
88+
if err != nil {
89+
return err
90+
}
91+
92+
absExtraDevContainerPaths = append(absExtraDevContainerPaths, absExtraPath)
93+
}
94+
cmd.ExtraDevContainerPaths = absExtraDevContainerPaths
8595
devPodConfig, err := config.LoadConfig(cmd.Context, cmd.Provider)
8696
if err != nil {
8797
return err
@@ -98,6 +108,11 @@ func NewUpCmd(f *flags.GlobalFlags) *cobra.Command {
98108
if err != nil {
99109
return fmt.Errorf("prepare workspace client: %w", err)
100110
}
111+
112+
if len(cmd.ExtraDevContainerPaths)!=0 && client.Provider() != "docker" {
113+
return fmt.Errorf("Extra devcontainer file is only supported with local provider")
114+
}
115+
101116
telemetry.CollectorCLI.SetClient(client)
102117

103118
return cmd.Run(ctx, devPodConfig, client, args, logger)
@@ -113,6 +128,7 @@ func NewUpCmd(f *flags.GlobalFlags) *cobra.Command {
113128
upCmd.Flags().StringArrayVar(&cmd.IDEOptions, "ide-option", []string{}, "IDE option in the form KEY=VALUE")
114129
upCmd.Flags().StringVar(&cmd.DevContainerImage, "devcontainer-image", "", "The container image to use, this will override the devcontainer.json value in the project")
115130
upCmd.Flags().StringVar(&cmd.DevContainerPath, "devcontainer-path", "", "The path to the devcontainer.json relative to the project")
131+
upCmd.Flags().StringArrayVar(&cmd.ExtraDevContainerPaths, "extra-devcontainer-path", []string{}, "The path to additional devcontainer.json files to override original devcontainer.json")
116132
upCmd.Flags().StringArrayVar(&cmd.ProviderOptions, "provider-option", []string{}, "Provider option in the form KEY=VALUE")
117133
upCmd.Flags().BoolVar(&cmd.Reconfigure, "reconfigure", false, "Reconfigure the options for this workspace. Only supported in DevPod Pro right now.")
118134
upCmd.Flags().BoolVar(&cmd.Recreate, "recreate", false, "If true will remove any existing containers and recreate them")

docs/pages/developing-in-workspaces/create-a-workspace.mdx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ Upon successful creation, DevPod will make the development container available t
1313
A workspace is defined through a `devcontainer.json`. If DevPod can't find one, it will automatically try to guess the programming language of your project and provide a fitting template.
1414
:::
1515

16+
:::info
17+
It is possible to override a 'devcontainer.json' with specific user settings such as mounts by creating a file named 'devcontainer.user.json' in the same directory as the 'devcontainer.json' of the workspace.
18+
This can be useful when customization of a versioned devcontainer is needed.
19+
:::
20+
1621
### Via DevPod Desktop Application
1722

1823
Navigate to the 'Workspaces' view and click on the 'Create' button in the title. Enter the git repository you want to work on or select a local folder.
@@ -34,7 +39,7 @@ Under the hood, the Desktop Application will call the CLI command `devpod up REP
3439
:::
3540

3641
:::info Note
37-
You can set the location of your devpod home by passing the `--devpod-home={home_path}` flag,
42+
You can set the location of your devpod home by passing the `--devpod-home={home_path}` flag,
3843
or by setting the env var `DEVPOD_HOME` to your desired home directory.
3944

4045
This can be useful if you are having trouble with a workspace trying to mount to a windows location when it should be mounting to a path inside the WSL VM.
@@ -107,7 +112,7 @@ DevPod will create the following `.devcontainer.json`:
107112

108113
If you have a local container running, you can create a workspace from it by running:
109114
```
110-
devpod up my-workspace --source container:$CONTAINER_ID
115+
devpod up my-workspace --source container:$CONTAINER_ID
111116
```
112117

113118
This only works with the `docker` provider.
@@ -148,4 +153,4 @@ Navigate to the 'Workspaces' view and press on the 'More Options' button on the
148153
Run the following command to reset an existing workspace:
149154
```
150155
devpod up my-workspace --reset
151-
```
156+
```

pkg/devcontainer/compose.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,21 @@ func (r *runner) runDockerCompose(
200200
return nil, errors.Wrap(err, "get image metadata from container")
201201
}
202202

203+
userConfig, err := config.ParseDevContainerUserJSON(parsedConfig.Config)
204+
if err != nil {
205+
return nil, err
206+
} else if userConfig != nil {
207+
config.AddConfigToImageMetadata(userConfig, imageMetadataConfig)
208+
}
209+
210+
for _, v := range options.ExtraDevContainerPaths {
211+
extraConfig, err := config.ParseDevContainerJSONFile(v)
212+
if err != nil {
213+
return nil, err
214+
}
215+
config.AddConfigToImageMetadata(extraConfig, imageMetadataConfig)
216+
}
217+
203218
mergedConfig, err := config.MergeConfiguration(parsedConfig.Config, imageMetadataConfig.Config)
204219
if err != nil {
205220
return nil, errors.Wrap(err, "merge config")
@@ -351,6 +366,21 @@ func (r *runner) startContainer(
351366
return nil, errors.Wrap(err, "inspect image")
352367
}
353368

369+
userConfig, err := config.ParseDevContainerUserJSON(parsedConfig.Config)
370+
if err != nil {
371+
return nil, err
372+
} else if userConfig != nil {
373+
config.AddConfigToImageMetadata(userConfig, imageMetadata)
374+
}
375+
376+
for _, v := range options.ExtraDevContainerPaths {
377+
extraConfig, err := config.ParseDevContainerJSONFile(v)
378+
if err != nil {
379+
return nil, err
380+
}
381+
config.AddConfigToImageMetadata(extraConfig, imageMetadata)
382+
}
383+
354384
mergedConfig, err := config.MergeConfiguration(parsedConfig.Config, imageMetadata.Config)
355385
if err != nil {
356386
return nil, errors.Wrap(err, "merge configuration")

pkg/devcontainer/config/metadata.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,13 @@ type ImageMetadata struct {
1212
DevContainerActions `json:",inline"`
1313
NonComposeBase `json:",inline"`
1414
}
15+
16+
// AddConfigToImageMetadata add a configuration to the given image metadata.
17+
// This will be used to generate the final image metadata.
18+
func AddConfigToImageMetadata(config *DevContainerConfig, imageMetadataConfig *ImageMetadataConfig) {
19+
userMetadata := &ImageMetadata{}
20+
userMetadata.DevContainerConfigBase = config.DevContainerConfigBase
21+
userMetadata.DevContainerActions = config.DevContainerActions
22+
userMetadata.NonComposeBase = config.NonComposeBase
23+
imageMetadataConfig.Config = append(imageMetadataConfig.Config, userMetadata)
24+
}

pkg/devcontainer/config/parse.go

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,49 @@ func SaveDevContainerJSON(config *DevContainerConfig) error {
6767
return nil
6868
}
6969

70+
// ParseDevContainerJSONFile parse the given a devcontainer.json file.
71+
func ParseDevContainerJSONFile(jsonFilePath string) (*DevContainerConfig, error) {
72+
var err error
73+
path, err := filepath.Abs(jsonFilePath)
74+
if err != nil {
75+
return nil, errors.Wrap(err, "make path absolute")
76+
}
77+
78+
bytes, err := os.ReadFile(path)
79+
if err != nil {
80+
return nil, err
81+
}
82+
83+
devContainer := &DevContainerConfig{}
84+
err = json.Unmarshal(jsonc.ToJSON(bytes), devContainer)
85+
if err != nil {
86+
return nil, err
87+
}
88+
devContainer.Origin = path
89+
return replaceLegacy(devContainer)
90+
}
91+
92+
// ParseDevContainerUserJSON check if a file named devcontainer.user.json exists in the same directory as
93+
// the devcontainer.json file and parse it if it does.
94+
func ParseDevContainerUserJSON(config *DevContainerConfig) (*DevContainerConfig, error) {
95+
filename := filepath.Base(config.Origin)
96+
filename = strings.TrimSuffix(filename, filepath.Ext(filename))
97+
98+
devContainerUserUserFilename := fmt.Sprintf("%s.user.json", filename)
99+
devContainerUserUserFilePath := filepath.Join(filepath.Dir(config.Origin), devContainerUserUserFilename)
100+
101+
_, err := os.Stat(devContainerUserUserFilePath)
102+
if err == nil {
103+
userConfig, err := ParseDevContainerJSONFile(devContainerUserUserFilePath)
104+
if err != nil {
105+
return nil, err
106+
}
107+
return userConfig, nil
108+
}
109+
return nil, nil
110+
}
111+
112+
// ParseDevContainerJSON check if a file named devcontainer.json exists in the given directory and parse it if it does
70113
func ParseDevContainerJSON(folder, relativePath string) (*DevContainerConfig, error) {
71114
path := ""
72115
if relativePath != "" {
@@ -91,26 +134,7 @@ func ParseDevContainerJSON(folder, relativePath string) (*DevContainerConfig, er
91134
}
92135
}
93136
}
94-
95-
var err error
96-
path, err = filepath.Abs(path)
97-
if err != nil {
98-
return nil, errors.Wrap(err, "make path absolute")
99-
}
100-
101-
bytes, err := os.ReadFile(path)
102-
if err != nil {
103-
return nil, err
104-
}
105-
106-
devContainer := &DevContainerConfig{}
107-
err = json.Unmarshal(jsonc.ToJSON(bytes), devContainer)
108-
if err != nil {
109-
return nil, err
110-
}
111-
112-
devContainer.Origin = path
113-
return replaceLegacy(devContainer)
137+
return ParseDevContainerJSONFile(path)
114138
}
115139

116140
func replaceLegacy(config *DevContainerConfig) (*DevContainerConfig, error) {

pkg/devcontainer/single.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,21 @@ func (r *runner) runSingleContainer(
7373
return nil, err
7474
}
7575

76+
userConfig, err := config.ParseDevContainerUserJSON(parsedConfig.Config)
77+
if err != nil {
78+
return nil, err
79+
} else if userConfig != nil {
80+
config.AddConfigToImageMetadata(userConfig, imageMetadataConfig)
81+
}
82+
83+
for _, v := range options.ExtraDevContainerPaths {
84+
extraConfig, err := config.ParseDevContainerJSONFile(v)
85+
if err != nil {
86+
return nil, err
87+
}
88+
config.AddConfigToImageMetadata(extraConfig, imageMetadataConfig)
89+
}
90+
7691
mergedConfig, err = config.MergeConfiguration(parsedConfig.Config, imageMetadataConfig.Config)
7792
if err != nil {
7893
return nil, errors.Wrap(err, "merge config")
@@ -122,6 +137,21 @@ func (r *runner) runSingleContainer(
122137
}
123138
}
124139

140+
userConfig, err := config.ParseDevContainerUserJSON(parsedConfig.Config)
141+
if err != nil {
142+
return nil, err
143+
} else if userConfig != nil {
144+
config.AddConfigToImageMetadata(userConfig, buildInfo.ImageMetadata)
145+
}
146+
147+
for _, v := range options.ExtraDevContainerPaths {
148+
extraConfig, err := config.ParseDevContainerJSONFile(v)
149+
if err != nil {
150+
return nil, err
151+
}
152+
config.AddConfigToImageMetadata(extraConfig, buildInfo.ImageMetadata)
153+
}
154+
125155
// merge configuration
126156
mergedConfig, err = config.MergeConfiguration(parsedConfig.Config, buildInfo.ImageMetadata.Config)
127157
if err != nil {

pkg/provider/workspace.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ type CLIOptions struct {
222222
GitSSHSigningKey string `json:"gitSshSigningKey,omitempty"`
223223
SSHAuthSockID string `json:"sshAuthSockID,omitempty"` // ID to use when looking for SSH_AUTH_SOCK, defaults to a new random ID if not set (only used for browser IDEs)
224224
StrictHostKeyChecking bool `json:"strictHostKeyChecking,omitempty"`
225+
ExtraDevContainerPaths []string `json:"extraDevContainerPaths,omitempty"`
225226

226227
// build options
227228
Repository string `json:"repository,omitempty"`

0 commit comments

Comments
 (0)