Skip to content

Commit

Permalink
fixup! Add support of devcontainer.user.json file
Browse files Browse the repository at this point in the history
  • Loading branch information
aacebedo committed Oct 25, 2024
1 parent d3a9bec commit a51dba6
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 53 deletions.
14 changes: 14 additions & 0 deletions cmd/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,19 @@ func NewUpCmd(f *flags.GlobalFlags) *cobra.Command {
upCmd := &cobra.Command{
Use: "up",
Short: "Starts a new workspace",
PreRunE: func(_ *cobra.Command, args []string) error {
absExtraDevContainerPaths := []string{}
for _, extraPath := range cmd.ExtraDevContainerPaths {
absExtraPath, err := filepath.Abs(extraPath)
if err != nil {
return err
}

absExtraDevContainerPaths = append(absExtraDevContainerPaths, absExtraPath)
}
cmd.ExtraDevContainerPaths = absExtraDevContainerPaths
return nil
},
RunE: func(_ *cobra.Command, args []string) error {
devPodConfig, err := config.LoadConfig(cmd.Context, cmd.Provider)
if err != nil {
Expand Down Expand Up @@ -158,6 +171,7 @@ func NewUpCmd(f *flags.GlobalFlags) *cobra.Command {
upCmd.Flags().StringVar(&cmd.DevContainerImage, "devcontainer-image", "", "The container image to use, this will override the devcontainer.json value in the project")
upCmd.Flags().StringVar(&cmd.DevContainerPath, "devcontainer-path", "", "The path to the devcontainer.json relative to the project")
upCmd.Flags().StringVar(&cmd.DevContainerSource, "devcontainer-source", "", "External devcontainer.json source")
upCmd.Flags().StringArrayVar(&cmd.ExtraDevContainerPaths, "extra-devcontainer-path", []string{}, "The path to additional devcontainer.json files to override original devcontainer.json")
upCmd.Flags().StringVar(&cmd.EnvironmentTemplate, "environment-template", "", "Environment template to use")
_ = upCmd.Flags().MarkHidden("environment-template")
upCmd.Flags().StringArrayVar(&cmd.ProviderOptions, "provider-option", []string{}, "Provider option in the form KEY=VALUE")
Expand Down
27 changes: 16 additions & 11 deletions docs/pages/developing-in-workspaces/create-a-workspace.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,20 @@ You can create a workspace either from the DevPod CLI or through the DevPod desk
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.

:::info
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.
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.
:::

:::info
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.
This can be useful when customization of a versioned devcontainer is needed.
:::

### Via DevPod Desktop Application

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.
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.

:::info Add Provider
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'
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
:::

You can also configure one of the additional settings:
Expand All @@ -34,19 +39,19 @@ Under the hood, the Desktop Application will call the CLI command `devpod up REP
:::

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

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.

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/...`
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/`
:::

### Via DevPod CLI

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:
```
# Add a provider if you haven't already
# Add a provider if you havent already
devpod provider add docker
```

Expand Down Expand Up @@ -99,15 +104,15 @@ devpod up ghcr.io/my-org/my-repo:latest
DevPod will create the following `.devcontainer.json`:
```
{
"image": "ghcr.io/my-org/my-repo:latest"
image”: “ghcr.io/my-org/my-repo:latest
}
```

#### Existing local container

If you have a local container running, you can create a workspace from it by running:
```
devpod up my-workspace --source container:$CONTAINER_ID
devpod up my-workspace --source container:$CONTAINER_ID
```

This only works with the `docker` provider.
Expand All @@ -124,7 +129,7 @@ When recreating a workspace, changes only to the project path or mounted volumes

### Via DevPod Desktop Application

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.
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.

### Via DevPod CLI

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

### Via DevPod Desktop Application

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

### Via DevPod CLI

Run the following command to reset an existing workspace:
```
devpod up my-workspace --reset
```
```
30 changes: 30 additions & 0 deletions pkg/devcontainer/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,21 @@ func (r *runner) runDockerCompose(
return nil, errors.Wrap(err, "get image metadata from container")
}

userConfig, err := config.ParseDevContainerUserJSON(parsedConfig.Config)
if err != nil {
return nil, err
} else if userConfig != nil {
config.AddConfigToImageMetadata(userConfig, imageMetadataConfig)
}

for _, v := range options.ExtraDevContainerPaths {
extraConfig, err := config.ParseDevContainerJSONFile(v)
if err != nil {
return nil, err
}
config.AddConfigToImageMetadata(extraConfig, imageMetadataConfig)
}

mergedConfig, err := config.MergeConfiguration(parsedConfig.Config, imageMetadataConfig.Config)
if err != nil {
return nil, errors.Wrap(err, "merge config")
Expand Down Expand Up @@ -332,6 +347,21 @@ func (r *runner) startContainer(
return nil, errors.Wrap(err, "inspect image")
}

userConfig, err := config.ParseDevContainerUserJSON(parsedConfig.Config)
if err != nil {
return nil, err
} else if userConfig != nil {
config.AddConfigToImageMetadata(userConfig, imageMetadata)
}

for _, v := range options.ExtraDevContainerPaths {
extraConfig, err := config.ParseDevContainerJSONFile(v)
if err != nil {
return nil, err
}
config.AddConfigToImageMetadata(extraConfig, imageMetadata)
}

mergedConfig, err := config.MergeConfiguration(parsedConfig.Config, imageMetadata.Config)
if err != nil {
return nil, errors.Wrap(err, "merge configuration")
Expand Down
8 changes: 8 additions & 0 deletions pkg/devcontainer/config/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ type ImageMetadata struct {
DevContainerActions `json:",inline"`
NonComposeBase `json:",inline"`
}

func AddConfigToImageMetadata(config *DevContainerConfig, imageMetadataConfig *ImageMetadataConfig) {
userMetadata := &ImageMetadata{}
userMetadata.DevContainerConfigBase = config.DevContainerConfigBase
userMetadata.DevContainerActions = config.DevContainerActions
userMetadata.NonComposeBase = config.NonComposeBase
imageMetadataConfig.Config = append(imageMetadataConfig.Config, userMetadata)
}
80 changes: 38 additions & 42 deletions pkg/devcontainer/config/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"strings"
"unicode/utf8"

"dario.cat/mergo"
doublestar "github.com/bmatcuk/doublestar/v4"
"github.com/pkg/errors"
"github.com/tidwall/jsonc"
Expand Down Expand Up @@ -68,33 +67,9 @@ func SaveDevContainerJSON(config *DevContainerConfig) error {
return nil
}

func ParseDevContainerJSON(folder, relativePath string) (*DevContainerConfig, error) {
path := ""
if relativePath != "" {
path = path2.Join(filepath.ToSlash(folder), relativePath)
_, err := os.Stat(path)
if err != nil {
return nil, fmt.Errorf("devcontainer path %s doesn't exist: %w", path, err)
}
} else {
path = filepath.Join(folder, ".devcontainer", "devcontainer.json")
_, err := os.Stat(path)
if err != nil {
path = filepath.Join(folder, ".devcontainer.json")
_, err = os.Stat(path)
if err != nil {
matches, err := doublestar.FilepathGlob(filepath.ToSlash(filepath.Clean(folder)) + "/.devcontainer/**/devcontainer.json")
if err != nil {
return nil, err
} else if len(matches) == 0 {
return nil, nil
}
}
}
}

func ParseDevContainerJSONFile(jsonFilePath string) (*DevContainerConfig, error) {
var err error
path, err = filepath.Abs(path)
path, err := filepath.Abs(jsonFilePath)
if err != nil {
return nil, errors.Wrap(err, "make path absolute")
}
Expand All @@ -109,33 +84,54 @@ func ParseDevContainerJSON(folder, relativePath string) (*DevContainerConfig, er
if err != nil {
return nil, err
}
devContainer.Origin = path
return replaceLegacy(devContainer)
}


filename := filepath.Base(path)
func ParseDevContainerUserJSON(config *DevContainerConfig) (*DevContainerConfig, error) {
filename := filepath.Base(config.Origin)
filename = strings.TrimSuffix(filename, filepath.Ext(filename))

userFilename := fmt.Sprintf("%s.user.json", filename)
userfilePath, err := filepath.Abs(filepath.Join(filepath.Dir(path), userFilename))
if err != nil {
return nil, errors.Wrap(err, "make path absolute")
}
_, err = os.Stat(userfilePath)
devContainerUserUserFilename := fmt.Sprintf("%s.user.json", filename)
devContainerUserUserFilePath := filepath.Join(filepath.Dir(config.Origin), devContainerUserUserFilename)

_, err = os.Stat(devContainerUserUserFilePath)
if err == nil {
bytes, err = os.ReadFile(userfilePath)
userConfig, err := ParseDevContainerJSONFile(devContainerUserUserFilePath)
if err != nil {
return nil, err
}
return userConfig, nil
}
return nil, nil
}

devContainerUser := &DevContainerConfig{}
err = json.Unmarshal(jsonc.ToJSON(bytes), devContainerUser)
func ParseDevContainerJSON(folder, relativePath string) (*DevContainerConfig, error) {
path := ""
if relativePath != "" {
path = path2.Join(filepath.ToSlash(folder), relativePath)
_, err := os.Stat(path)
if err != nil {
return nil, err
return nil, fmt.Errorf("devcontainer path %s doesn't exist: %w", path, err)
}
if err := mergo.Merge(devContainer, devContainerUser, mergo.WithAppendSlice, mergo.WithOverride); err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("unable to update devcontainer.json with %s", userFilename))
} else {
path = filepath.Join(folder, ".devcontainer", "devcontainer.json")
_, err := os.Stat(path)
if err != nil {
path = filepath.Join(folder, ".devcontainer.json")
_, err = os.Stat(path)
if err != nil {
matches, err := doublestar.FilepathGlob(filepath.ToSlash(filepath.Clean(folder)) + "/.devcontainer/**/devcontainer.json")
if err != nil {
return nil, err
} else if len(matches) == 0 {
return nil, nil
}
}
}
}
devContainer.Origin = path
return replaceLegacy(devContainer)
return ParseDevContainerJSONFile(path)
}

func replaceLegacy(config *DevContainerConfig) (*DevContainerConfig, error) {
Expand Down
30 changes: 30 additions & 0 deletions pkg/devcontainer/single.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ func (r *runner) runSingleContainer(
return nil, err
}

userConfig, err := config.ParseDevContainerUserJSON(parsedConfig.Config)
if err != nil {
return nil, err
} else if userConfig != nil {
config.AddConfigToImageMetadata(userConfig, imageMetadataConfig)
}

for _, v := range options.ExtraDevContainerPaths {
extraConfig, err := config.ParseDevContainerJSONFile(v)
if err != nil {
return nil, err
}
config.AddConfigToImageMetadata(extraConfig, imageMetadataConfig)
}

mergedConfig, err = config.MergeConfiguration(parsedConfig.Config, imageMetadataConfig.Config)
if err != nil {
return nil, errors.Wrap(err, "merge config")
Expand Down Expand Up @@ -102,6 +117,21 @@ func (r *runner) runSingleContainer(
}
}

userConfig, err := config.ParseDevContainerUserJSON(parsedConfig.Config)
if err != nil {
return nil, err
} else if userConfig != nil {
config.AddConfigToImageMetadata(userConfig, buildInfo.ImageMetadata)
}

for _, v := range options.ExtraDevContainerPaths {
extraConfig, err := config.ParseDevContainerJSONFile(v)
if err != nil {
return nil, err
}
config.AddConfigToImageMetadata(extraConfig, buildInfo.ImageMetadata)
}

// merge configuration
mergedConfig, err = config.MergeConfiguration(parsedConfig.Config, buildInfo.ImageMetadata.Config)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions pkg/provider/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ type CLIOptions struct {
GitCloneStrategy git.CloneStrategy `json:"gitCloneStrategy,omitempty"`
FallbackImage string `json:"fallbackImage,omitempty"`
GitSSHSigningKey string `json:"gitSshSigningKey,omitempty"`
ExtraDevContainerPaths []string `json:"extraDevContainerPaths,omitempty"`

// build options
Repository string `json:"repository,omitempty"`
Expand Down

0 comments on commit a51dba6

Please sign in to comment.