diff --git a/cmd/up.go b/cmd/up.go index b3c8760ce..fc553f2e1 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -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 { @@ -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") diff --git a/docs/pages/developing-in-workspaces/create-a-workspace.mdx b/docs/pages/developing-in-workspaces/create-a-workspace.mdx index 001852150..6e7d3c1b4 100644 --- a/docs/pages/developing-in-workspaces/create-a-workspace.mdx +++ b/docs/pages/developing-in-workspaces/create-a-workspace.mdx @@ -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 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’ ::: 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 ::: :::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 haven’t already devpod provider add docker ``` @@ -99,7 +104,7 @@ 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” } ``` @@ -107,7 +112,7 @@ DevPod will create the following `.devcontainer.json`: 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. @@ -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 @@ -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 -``` \ No newline at end of file +``` diff --git a/pkg/devcontainer/compose.go b/pkg/devcontainer/compose.go index 02499ee6e..16a6c7dea 100644 --- a/pkg/devcontainer/compose.go +++ b/pkg/devcontainer/compose.go @@ -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") @@ -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") diff --git a/pkg/devcontainer/config/metadata.go b/pkg/devcontainer/config/metadata.go index a0278b981..9c4275eba 100644 --- a/pkg/devcontainer/config/metadata.go +++ b/pkg/devcontainer/config/metadata.go @@ -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) +} diff --git a/pkg/devcontainer/config/parse.go b/pkg/devcontainer/config/parse.go index 68b1a79f5..7c13438a0 100644 --- a/pkg/devcontainer/config/parse.go +++ b/pkg/devcontainer/config/parse.go @@ -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" @@ -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") } @@ -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) { diff --git a/pkg/devcontainer/single.go b/pkg/devcontainer/single.go index 8f9710187..77f63a8d9 100644 --- a/pkg/devcontainer/single.go +++ b/pkg/devcontainer/single.go @@ -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") @@ -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 { diff --git a/pkg/provider/workspace.go b/pkg/provider/workspace.go index 0477489b3..d41179a6a 100644 --- a/pkg/provider/workspace.go +++ b/pkg/provider/workspace.go @@ -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"`