diff --git a/cmd/up.go b/cmd/up.go index d825d11d5..2983c21b7 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -74,6 +74,19 @@ func NewUpCmd(f *flags.GlobalFlags) *cobra.Command { upCmd := &cobra.Command{ Use: "up [flags] [workspace-path|workspace-name]", 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 { @@ -159,6 +172,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..5d7f0dd82 100644 --- a/pkg/devcontainer/config/metadata.go +++ b/pkg/devcontainer/config/metadata.go @@ -12,3 +12,13 @@ type ImageMetadata struct { DevContainerActions `json:",inline"` NonComposeBase `json:",inline"` } + +// AddConfigToImageMetadata add a configuration to the given image metadata. +// This will be used to generate the final image metadata. +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 0dc56bf03..afa7c2ebc 100644 --- a/pkg/devcontainer/config/parse.go +++ b/pkg/devcontainer/config/parse.go @@ -67,6 +67,49 @@ func SaveDevContainerJSON(config *DevContainerConfig) error { return nil } +// ParseDevContainerJSONFile parse the given a devcontainer.json file. +func ParseDevContainerJSONFile(jsonFilePath string) (*DevContainerConfig, error) { + var err error + path, err := filepath.Abs(jsonFilePath) + if err != nil { + return nil, errors.Wrap(err, "make path absolute") + } + + bytes, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + devContainer := &DevContainerConfig{} + err = json.Unmarshal(jsonc.ToJSON(bytes), devContainer) + if err != nil { + return nil, err + } + devContainer.Origin = path + return replaceLegacy(devContainer) +} + +// ParseDevContainerUserJSON check if a file named devcontainer.user.json exists in the same directory as +// the devcontainer.json file and parse it if it does. +func ParseDevContainerUserJSON(config *DevContainerConfig) (*DevContainerConfig, error) { + filename := filepath.Base(config.Origin) + filename = strings.TrimSuffix(filename, filepath.Ext(filename)) + + devContainerUserUserFilename := fmt.Sprintf("%s.user.json", filename) + devContainerUserUserFilePath := filepath.Join(filepath.Dir(config.Origin), devContainerUserUserFilename) + + _, err := os.Stat(devContainerUserUserFilePath) + if err == nil { + userConfig, err := ParseDevContainerJSONFile(devContainerUserUserFilePath) + if err != nil { + return nil, err + } + return userConfig, nil + } + return nil, nil +} + +// ParseDevContainerJSON check if a file named devcontainer.json exists in the given directory and parse it if it does func ParseDevContainerJSON(folder, relativePath string) (*DevContainerConfig, error) { path := "" if relativePath != "" { @@ -91,26 +134,7 @@ func ParseDevContainerJSON(folder, relativePath string) (*DevContainerConfig, er } } } - - var err error - path, err = filepath.Abs(path) - if err != nil { - return nil, errors.Wrap(err, "make path absolute") - } - - bytes, err := os.ReadFile(path) - if err != nil { - return nil, err - } - - devContainer := &DevContainerConfig{} - err = json.Unmarshal(jsonc.ToJSON(bytes), devContainer) - if err != nil { - return nil, err - } - - 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"`