Skip to content

Commit 33bda71

Browse files
committed
Add support of devcontainer.user.json file
1 parent 31dd410 commit 33bda71

File tree

22 files changed

+1480
-31
lines changed

22 files changed

+1480
-31
lines changed

cmd/up.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,19 @@ func NewUpCmd(f *flags.GlobalFlags) *cobra.Command {
7474
upCmd := &cobra.Command{
7575
Use: "up [flags] [workspace-path|workspace-name]",
7676
Short: "Starts a new workspace",
77+
PreRunE: func(_ *cobra.Command, args []string) error {
78+
absExtraDevContainerPaths := []string{}
79+
for _, extraPath := range cmd.ExtraDevContainerPaths {
80+
absExtraPath, err := filepath.Abs(extraPath)
81+
if err != nil {
82+
return err
83+
}
84+
85+
absExtraDevContainerPaths = append(absExtraDevContainerPaths, absExtraPath)
86+
}
87+
cmd.ExtraDevContainerPaths = absExtraDevContainerPaths
88+
return nil
89+
},
7790
RunE: func(_ *cobra.Command, args []string) error {
7891
devPodConfig, err := config.LoadConfig(cmd.Context, cmd.Provider)
7992
if err != nil {
@@ -159,6 +172,7 @@ func NewUpCmd(f *flags.GlobalFlags) *cobra.Command {
159172
upCmd.Flags().StringVar(&cmd.DevContainerImage, "devcontainer-image", "", "The container image to use, this will override the devcontainer.json value in the project")
160173
upCmd.Flags().StringVar(&cmd.DevContainerPath, "devcontainer-path", "", "The path to the devcontainer.json relative to the project")
161174
upCmd.Flags().StringVar(&cmd.DevContainerSource, "devcontainer-source", "", "External devcontainer.json source")
175+
upCmd.Flags().StringArrayVar(&cmd.ExtraDevContainerPaths, "extra-devcontainer-path", []string{}, "The path to additional devcontainer.json files to override original devcontainer.json")
162176
upCmd.Flags().StringVar(&cmd.EnvironmentTemplate, "environment-template", "", "Environment template to use")
163177
_ = upCmd.Flags().MarkHidden("environment-template")
164178
upCmd.Flags().StringArrayVar(&cmd.ProviderOptions, "provider-option", []string{}, "Provider option in the form KEY=VALUE")

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+
```

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ require (
6262

6363
require (
6464
cloud.google.com/go/compute/metadata v0.5.0 // indirect
65+
dario.cat/mergo v1.0.1 // indirect
6566
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
6667
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
6768
github.com/Azure/go-autorest v14.2.0+incompatible // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIA
2626
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
2727
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
2828
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
29+
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
30+
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
2931
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
3032
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
3133
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=

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: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,11 @@ type ImageMetadata struct {
1212
DevContainerActions `json:",inline"`
1313
NonComposeBase `json:",inline"`
1414
}
15+
16+
func AddConfigToImageMetadata(config *DevContainerConfig, imageMetadataConfig *ImageMetadataConfig) {
17+
userMetadata := &ImageMetadata{}
18+
userMetadata.DevContainerConfigBase = config.DevContainerConfigBase
19+
userMetadata.DevContainerActions = config.DevContainerActions
20+
userMetadata.NonComposeBase = config.NonComposeBase
21+
imageMetadataConfig.Config = append(imageMetadataConfig.Config, userMetadata)
22+
}

pkg/devcontainer/config/parse.go

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

70+
func ParseDevContainerJSONFile(jsonFilePath string) (*DevContainerConfig, error) {
71+
var err error
72+
path, err := filepath.Abs(jsonFilePath)
73+
if err != nil {
74+
return nil, errors.Wrap(err, "make path absolute")
75+
}
76+
77+
bytes, err := os.ReadFile(path)
78+
if err != nil {
79+
return nil, err
80+
}
81+
82+
devContainer := &DevContainerConfig{}
83+
err = json.Unmarshal(jsonc.ToJSON(bytes), devContainer)
84+
if err != nil {
85+
return nil, err
86+
}
87+
devContainer.Origin = path
88+
return replaceLegacy(devContainer)
89+
}
90+
91+
92+
func ParseDevContainerUserJSON(config *DevContainerConfig) (*DevContainerConfig, error) {
93+
filename := filepath.Base(config.Origin)
94+
filename = strings.TrimSuffix(filename, filepath.Ext(filename))
95+
96+
devContainerUserUserFilename := fmt.Sprintf("%s.user.json", filename)
97+
devContainerUserUserFilePath := filepath.Join(filepath.Dir(config.Origin), devContainerUserUserFilename)
98+
99+
_, err = os.Stat(devContainerUserUserFilePath)
100+
if err == nil {
101+
userConfig, err := ParseDevContainerJSONFile(devContainerUserUserFilePath)
102+
if err != nil {
103+
return nil, err
104+
}
105+
return userConfig, nil
106+
}
107+
return nil, nil
108+
}
109+
70110
func ParseDevContainerJSON(folder, relativePath string) (*DevContainerConfig, error) {
71111
path := ""
72112
if relativePath != "" {
@@ -91,26 +131,7 @@ func ParseDevContainerJSON(folder, relativePath string) (*DevContainerConfig, er
91131
}
92132
}
93133
}
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)
134+
return ParseDevContainerJSONFile(path)
114135
}
115136

116137
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
@@ -206,6 +206,7 @@ type CLIOptions struct {
206206
GitCloneStrategy git.CloneStrategy `json:"gitCloneStrategy,omitempty"`
207207
FallbackImage string `json:"fallbackImage,omitempty"`
208208
GitSSHSigningKey string `json:"gitSshSigningKey,omitempty"`
209+
ExtraDevContainerPaths []string `json:"extraDevContainerPaths,omitempty"`
209210

210211
// build options
211212
Repository string `json:"repository,omitempty"`
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
version = 1
2+
3+
test_patterns = [
4+
"*_test.go"
5+
]
6+
7+
[[analyzers]]
8+
name = "go"
9+
enabled = true
10+
11+
[analyzers.meta]
12+
import_path = "dario.cat/mergo"

0 commit comments

Comments
 (0)