Skip to content

Commit 99d5208

Browse files
committed
Add support of devcontainer.user.json file
1 parent a2bf7f7 commit 99d5208

File tree

7 files changed

+145
-31
lines changed

7 files changed

+145
-31
lines changed

cmd/up.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,19 @@ func NewUpCmd(f *flags.GlobalFlags) *cobra.Command {
8181
upCmd := &cobra.Command{
8282
Use: "up [flags] [workspace-path|workspace-name]",
8383
Short: "Starts a new workspace",
84+
PreRunE: 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
95+
return nil
96+
},
8497
RunE: func(cobraCmd *cobra.Command, args []string) error {
8598
devPodConfig, err := config.LoadConfig(cmd.Context, cmd.Provider)
8699
if err != nil {
@@ -108,6 +121,7 @@ func NewUpCmd(f *flags.GlobalFlags) *cobra.Command {
108121
upCmd.Flags().StringArrayVar(&cmd.IDEOptions, "ide-option", []string{}, "IDE option in the form KEY=VALUE")
109122
upCmd.Flags().StringVar(&cmd.DevContainerImage, "devcontainer-image", "", "The container image to use, this will override the devcontainer.json value in the project")
110123
upCmd.Flags().StringVar(&cmd.DevContainerPath, "devcontainer-path", "", "The path to the devcontainer.json relative to the project")
124+
upCmd.Flags().StringArrayVar(&cmd.ExtraDevContainerPaths, "extra-devcontainer-path", []string{}, "The path to additional devcontainer.json files to override original devcontainer.json")
111125
upCmd.Flags().StringVar(&cmd.EnvironmentTemplate, "environment-template", "", "Environment template to use")
112126
_ = upCmd.Flags().MarkHidden("environment-template")
113127
upCmd.Flags().StringVar(&cmd.EnvironmentTemplateVersion, "environment-template-version", "", "Specific version of DevPodEnvironmentTemplate to use. Empty for latest.")

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
@@ -197,6 +197,21 @@ func (r *runner) runDockerCompose(
197197
return nil, errors.Wrap(err, "get image metadata from container")
198198
}
199199

200+
userConfig, err := config.ParseDevContainerUserJSON(parsedConfig.Config)
201+
if err != nil {
202+
return nil, err
203+
} else if userConfig != nil {
204+
config.AddConfigToImageMetadata(userConfig, imageMetadataConfig)
205+
}
206+
207+
for _, v := range options.ExtraDevContainerPaths {
208+
extraConfig, err := config.ParseDevContainerJSONFile(v)
209+
if err != nil {
210+
return nil, err
211+
}
212+
config.AddConfigToImageMetadata(extraConfig, imageMetadataConfig)
213+
}
214+
200215
mergedConfig, err := config.MergeConfiguration(parsedConfig.Config, imageMetadataConfig.Config)
201216
if err != nil {
202217
return nil, errors.Wrap(err, "merge config")
@@ -332,6 +347,21 @@ func (r *runner) startContainer(
332347
return nil, errors.Wrap(err, "inspect image")
333348
}
334349

350+
userConfig, err := config.ParseDevContainerUserJSON(parsedConfig.Config)
351+
if err != nil {
352+
return nil, err
353+
} else if userConfig != nil {
354+
config.AddConfigToImageMetadata(userConfig, imageMetadata)
355+
}
356+
357+
for _, v := range options.ExtraDevContainerPaths {
358+
extraConfig, err := config.ParseDevContainerJSONFile(v)
359+
if err != nil {
360+
return nil, err
361+
}
362+
config.AddConfigToImageMetadata(extraConfig, imageMetadata)
363+
}
364+
335365
mergedConfig, err := config.MergeConfiguration(parsedConfig.Config, imageMetadata.Config)
336366
if err != nil {
337367
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
@@ -61,6 +61,21 @@ func (r *runner) runSingleContainer(
6161
return nil, err
6262
}
6363

64+
userConfig, err := config.ParseDevContainerUserJSON(parsedConfig.Config)
65+
if err != nil {
66+
return nil, err
67+
} else if userConfig != nil {
68+
config.AddConfigToImageMetadata(userConfig, imageMetadataConfig)
69+
}
70+
71+
for _, v := range options.ExtraDevContainerPaths {
72+
extraConfig, err := config.ParseDevContainerJSONFile(v)
73+
if err != nil {
74+
return nil, err
75+
}
76+
config.AddConfigToImageMetadata(extraConfig, imageMetadataConfig)
77+
}
78+
6479
mergedConfig, err = config.MergeConfiguration(parsedConfig.Config, imageMetadataConfig.Config)
6580
if err != nil {
6681
return nil, errors.Wrap(err, "merge config")
@@ -102,6 +117,21 @@ func (r *runner) runSingleContainer(
102117
}
103118
}
104119

120+
userConfig, err := config.ParseDevContainerUserJSON(parsedConfig.Config)
121+
if err != nil {
122+
return nil, err
123+
} else if userConfig != nil {
124+
config.AddConfigToImageMetadata(userConfig, buildInfo.ImageMetadata)
125+
}
126+
127+
for _, v := range options.ExtraDevContainerPaths {
128+
extraConfig, err := config.ParseDevContainerJSONFile(v)
129+
if err != nil {
130+
return nil, err
131+
}
132+
config.AddConfigToImageMetadata(extraConfig, buildInfo.ImageMetadata)
133+
}
134+
105135
// merge configuration
106136
mergedConfig, err = config.MergeConfiguration(parsedConfig.Config, buildInfo.ImageMetadata.Config)
107137
if err != nil {

pkg/provider/workspace.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ type CLIOptions struct {
220220
FallbackImage string `json:"fallbackImage,omitempty"`
221221
GitSSHSigningKey string `json:"gitSshSigningKey,omitempty"`
222222
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)
223+
ExtraDevContainerPaths []string `json:"extraDevContainerPaths,omitempty"`
223224

224225
// build options
225226
Repository string `json:"repository,omitempty"`

0 commit comments

Comments
 (0)