Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions cmd/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ func NewUpCmd(f *flags.GlobalFlags) *cobra.Command {
Use: "up [flags] [workspace-path|workspace-name]",
Short: "Starts a new workspace",
RunE: func(cobraCmd *cobra.Command, args []string) error {
absExtraDevContainerPaths := []string{}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a nitpick but could you move this to RunE please? We initialize all other options there as well and I'd like to keep it consistent

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed it

for _, extraPath := range cmd.ExtraDevContainerPaths {
absExtraPath, err := filepath.Abs(extraPath)
if err != nil {
return err
}

absExtraDevContainerPaths = append(absExtraDevContainerPaths, absExtraPath)
}
cmd.ExtraDevContainerPaths = absExtraDevContainerPaths
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These paths will only work for local providers like docker. Could you add a warning/error when users are trying to create a non-local workspace please?

You can check this here via client.AgentLocal()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed it

devPodConfig, err := config.LoadConfig(cmd.Context, cmd.Provider)
if err != nil {
return err
Expand All @@ -98,6 +108,11 @@ func NewUpCmd(f *flags.GlobalFlags) *cobra.Command {
if err != nil {
return fmt.Errorf("prepare workspace client: %w", err)
}

if len(cmd.ExtraDevContainerPaths)!=0 && client.Provider() != "docker" {
return fmt.Errorf("Extra devcontainer file is only supported with local provider")
}

telemetry.CollectorCLI.SetClient(client)

return cmd.Run(ctx, devPodConfig, client, args, logger)
Expand All @@ -113,6 +128,7 @@ func NewUpCmd(f *flags.GlobalFlags) *cobra.Command {
upCmd.Flags().StringArrayVar(&cmd.IDEOptions, "ide-option", []string{}, "IDE option in the form KEY=VALUE")
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().StringArrayVar(&cmd.ExtraDevContainerPaths, "extra-devcontainer-path", []string{}, "The path to additional devcontainer.json files to override original devcontainer.json")
upCmd.Flags().StringArrayVar(&cmd.ProviderOptions, "provider-option", []string{}, "Provider option in the form KEY=VALUE")
upCmd.Flags().BoolVar(&cmd.Reconfigure, "reconfigure", false, "Reconfigure the options for this workspace. Only supported in DevPod Pro right now.")
upCmd.Flags().BoolVar(&cmd.Recreate, "recreate", false, "If true will remove any existing containers and recreate them")
Expand Down
11 changes: 8 additions & 3 deletions docs/pages/developing-in-workspaces/create-a-workspace.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ Upon successful creation, DevPod will make the development container available t
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.
Expand All @@ -34,7 +39,7 @@ 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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -148,4 +153,4 @@ Navigate to the 'Workspaces' view and press on the 'More Options' button on the
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 @@ -200,6 +200,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 @@ -351,6 +366,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
10 changes: 10 additions & 0 deletions pkg/devcontainer/config/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
64 changes: 44 additions & 20 deletions pkg/devcontainer/config/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand All @@ -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) {
Expand Down
30 changes: 30 additions & 0 deletions pkg/devcontainer/single.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,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 @@ -122,6 +137,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 @@ -222,6 +222,7 @@ type CLIOptions struct {
GitSSHSigningKey string `json:"gitSshSigningKey,omitempty"`
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)
StrictHostKeyChecking bool `json:"strictHostKeyChecking,omitempty"`
ExtraDevContainerPaths []string `json:"extraDevContainerPaths,omitempty"`

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