Skip to content

Commit be5d094

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

File tree

7 files changed

+147
-31
lines changed

7 files changed

+147
-31
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: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,20 @@ You can create a workspace either from the DevPod CLI or through the DevPod desk
1010
Upon successful creation, DevPod will make the development container available through the ssh host `WORKSPACE_NAME.devpod`. Alternatively, DevPod can automatically open the workspace in a locally installed IDE, such as VS Code or Intellij.
1111

1212
:::info
13-
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.
13+
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.
14+
:::
15+
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.
1419
:::
1520

1621
### Via DevPod Desktop Application
1722

18-
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.
23+
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.
1924

2025
:::info Add Provider
21-
If you haven't configured a provider yet, DevPod will automatically open the provider modal for you. You can later add providers in the same way by navigating to 'Providers' > 'Add'
26+
If you havent configured a provider yet, DevPod will automatically open the provider modal for you. You can later add providers in the same way by navigating to Providers > Add
2227
:::
2328

2429
You can also configure one of the additional settings:
@@ -34,19 +39,19 @@ 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.
4146

42-
For example: setting `devpod-home=/mnt/c/Users/MyUser/` will result in a workspace path of something like `/mnt/c/Users/MyUser/.devpod/contexts/default/workspaces/...`
47+
For example: setting `devpod-home=/mnt/c/Users/MyUser/` will result in a workspace path of something like `/mnt/c/Users/MyUser/.devpod/contexts/default/workspaces/`
4348
:::
4449

4550
### Via DevPod CLI
4651

4752
Make sure to [install the DevPod CLI locally](../getting-started/install.mdx#optional-install-devpod-cli) and select a provider you would like to host the workspace on (such as local docker) via:
4853
```
49-
# Add a provider if you haven't already
54+
# Add a provider if you havent already
5055
devpod provider add docker
5156
```
5257

@@ -99,15 +104,15 @@ devpod up ghcr.io/my-org/my-repo:latest
99104
DevPod will create the following `.devcontainer.json`:
100105
```
101106
{
102-
"image": "ghcr.io/my-org/my-repo:latest"
107+
image”: “ghcr.io/my-org/my-repo:latest
103108
}
104109
```
105110

106111
#### Existing local container
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.
@@ -124,7 +129,7 @@ When recreating a workspace, changes only to the project path or mounted volumes
124129

125130
### Via DevPod Desktop Application
126131

127-
Navigate to the 'Workspaces' view and press on the 'More Options' button on the workspace you want to recreate. Then press 'Rebuild' and confirm to rebuild the workspace.
132+
Navigate to the Workspaces view and press on the More Options button on the workspace you want to recreate. Then press Rebuild and confirm to rebuild the workspace.
128133

129134
### Via DevPod CLI
130135

@@ -141,11 +146,11 @@ Some scenarios require pulling in the latest changes from a git repository or re
141146

142147
### Via DevPod Desktop Application
143148

144-
Navigate to the 'Workspaces' view and press on the 'More Options' button on the workspace you want to reset. Then press 'Reset' and confirm.
149+
Navigate to the Workspaces view and press on the More Options button on the workspace you want to reset. Then press Reset and confirm.
145150

146151
### Via DevPod CLI
147152

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)