diff --git a/cmd/agent/workspace/delete.go b/cmd/agent/workspace/delete.go index 5fa815e38..ba70378aa 100644 --- a/cmd/agent/workspace/delete.go +++ b/cmd/agent/workspace/delete.go @@ -82,11 +82,15 @@ func removeContainer(ctx context.Context, workspaceInfo *provider2.AgentWorkspac return err } - err = runner.Delete(ctx) - if err != nil { - return err + if workspaceInfo.Workspace.Source.Container != "" { + log.Infof("Skipping container deletion, since it was not created by DevPod") + } else { + err = runner.Delete(ctx) + if err != nil { + return err + } + log.Debugf("Successfully removed DevPod container from server") } - log.Debugf("Successfully removed DevPod container from server") return nil } diff --git a/cmd/agent/workspace/up.go b/cmd/agent/workspace/up.go index 1a37742e3..dcd8e3964 100644 --- a/cmd/agent/workspace/up.go +++ b/cmd/agent/workspace/up.go @@ -201,9 +201,12 @@ func prepareWorkspace(ctx context.Context, workspaceInfo *provider2.AgentWorkspa } else if workspaceInfo.Workspace.Source.Image != "" { log.Debugf("Prepare Image") return PrepareImage(workspaceInfo.ContentFolder, workspaceInfo.Workspace.Source.Image) + } else if workspaceInfo.Workspace.Source.Container != "" { + log.Debugf("Workspace is a container, nothing to do") + return nil } - return fmt.Errorf("either workspace repository, image or local-folder is required") + return fmt.Errorf("either workspace repository, image, container or local-folder is required") } func InitContentFolder(workspaceInfo *provider2.AgentWorkspaceInfo, log log.Logger) (bool, error) { diff --git a/pkg/devcontainer/config/container_details.go b/pkg/devcontainer/config/container_details.go index 052096662..927b0a52c 100644 --- a/pkg/devcontainer/config/container_details.go +++ b/pkg/devcontainer/config/container_details.go @@ -23,6 +23,9 @@ type ContainerDetails struct { type ContainerDetailsConfig struct { Labels map[string]string `json:"Labels,omitempty"` + // WorkingDir specifies default working directory inside the container + WorkingDir string `json:"WorkingDir,omitempty"` + // LegacyUser shouldn't get used anymore and is only there for backwards compatibility, please // use the label config.UserLabel instead LegacyUser string `json:"User,omitempty"` diff --git a/pkg/devcontainer/run.go b/pkg/devcontainer/run.go index 31c8c088a..44248f0fc 100644 --- a/pkg/devcontainer/run.go +++ b/pkg/devcontainer/run.go @@ -116,7 +116,7 @@ func (r *runner) Up(ctx context.Context, options UpOptions) (*config.Result, err // check if its a compose devcontainer.json var result *config.Result - if isDockerFileConfig(substitutedConfig.Config) || substitutedConfig.Config.Image != "" { + if isDockerFileConfig(substitutedConfig.Config) || substitutedConfig.Config.Image != "" || r.WorkspaceConfig.Workspace.Source.Container != "" { result, err = r.runSingleContainer( ctx, substitutedConfig, @@ -178,6 +178,15 @@ func (r *runner) prepare( // will be gracefully handled by the auto-detection mechanism if err != nil && !os.IsNotExist(err) { return nil, nil, errors.Wrap(err, "parsing devcontainer.json") + } else if r.WorkspaceConfig.Workspace.Source.Container != "" { + rawParsedConfig = &config.DevContainerConfig{ + DevContainerConfigBase: config.DevContainerConfigBase{ + // Default workspace directory for containers + // Upon inspecting the container, this would be updated to the correct folder, if needed + WorkspaceFolder: "/", + }, + Origin: "", + } } else if rawParsedConfig == nil { r.Log.Infof("Couldn't find a devcontainer.json") r.Log.Infof("Try detecting project programming language...") diff --git a/pkg/devcontainer/single.go b/pkg/devcontainer/single.go index ed3154993..33be73399 100644 --- a/pkg/devcontainer/single.go +++ b/pkg/devcontainer/single.go @@ -21,11 +21,16 @@ func (r *runner) runSingleContainer(ctx context.Context, parsedConfig *config.Su return nil, fmt.Errorf("find dev container: %w", err) } + workspaceIsARunningContainer := r.WorkspaceConfig.Workspace.Source.Container != "" + // does the container already exist? var ( mergedConfig *config.MergedDevContainerConfig ) - if !options.Recreate && containerDetails != nil { + // if options.Recreate is true, and workspace is a running container, we should not rebuild + if options.Recreate && workspaceIsARunningContainer { + return nil, fmt.Errorf("cannot recreate a running container not created by DevPod") + } else if !options.Recreate && containerDetails != nil { // start container if not running if strings.ToLower(containerDetails.State.Status) != "running" { err = r.Driver.StartDevContainer(ctx, r.ID) @@ -34,6 +39,10 @@ func (r *runner) runSingleContainer(ctx context.Context, parsedConfig *config.Su } } + if workspaceIsARunningContainer && containerDetails.Config.WorkingDir != "" { + substitutionContext.ContainerWorkspaceFolder = containerDetails.Config.WorkingDir + } + imageMetadataConfig, err := metadata.GetImageMetadataFromContainer(containerDetails, substitutionContext, r.Log) if err != nil { return nil, err diff --git a/pkg/driver/docker/docker.go b/pkg/driver/docker/docker.go index 52dd91d2b..0108437d9 100644 --- a/pkg/driver/docker/docker.go +++ b/pkg/driver/docker/docker.go @@ -40,11 +40,21 @@ func NewDockerDriver(workspaceInfo *provider2.AgentWorkspaceInfo, log log.Logger dockerCommand = workspaceInfo.Agent.Docker.Path } + dockerEnvVars := workspaceInfo.Agent.Docker.Env + if workspaceInfo.Workspace.Source.Container != "" { + // Initialize the map if it's nil + if dockerEnvVars == nil { + dockerEnvVars = make(map[string]string) + } + + dockerEnvVars["DEVPOD_RUNNING_CONTAINER_ID"] = workspaceInfo.Workspace.Source.Container + } + log.Debugf("Using docker command '%s'", dockerCommand) return &dockerDriver{ Docker: &docker.DockerHelper{ DockerCommand: dockerCommand, - Environment: makeEnvironment(workspaceInfo.Agent.Docker.Env, log), + Environment: makeEnvironment(dockerEnvVars, log), }, Log: log, } @@ -154,8 +164,17 @@ func (d *dockerDriver) FindDevContainer(ctx context.Context, workspaceId string) containerDetails, err := d.Docker.FindDevContainer(ctx, []string{config.DockerIDLabel + "=" + workspaceId}) if err != nil { return nil, err - } else if containerDetails == nil { - return nil, nil + } + + if containerDetails == nil && d.Docker.Environment != nil { + runningContainerID := config.ListToObject(d.Docker.Environment)["DEVPOD_RUNNING_CONTAINER_ID"] + if runningContainerID != "" { + containerDetails, err = d.Docker.FindContainerByID(ctx, []string{runningContainerID}) + } + } + + if err != nil || containerDetails == nil { + return nil, err } if containerDetails.Config.LegacyUser != "" { diff --git a/pkg/provider/workspace.go b/pkg/provider/workspace.go index e3004d113..0f0c48a37 100644 --- a/pkg/provider/workspace.go +++ b/pkg/provider/workspace.go @@ -10,9 +10,10 @@ import ( ) var ( - WorkspaceSourceGit = "git:" - WorkspaceSourceLocal = "local:" - WorkspaceSourceImage = "image:" + WorkspaceSourceGit = "git:" + WorkspaceSourceLocal = "local:" + WorkspaceSourceImage = "image:" + WorkspaceSourceContainer = "container:" ) type Workspace struct { @@ -105,6 +106,9 @@ type WorkspaceSource struct { // Image is the docker image to use Image string `json:"image,omitempty"` + + // Container is the container to use + Container string `json:"container,omitempty"` } type ContainerWorkspaceInfo struct { @@ -200,6 +204,8 @@ func (w WorkspaceSource) String() string { return WorkspaceSourceLocal + w.LocalFolder } else if w.Image != "" { return WorkspaceSourceImage + w.Image + } else if w.Container != "" { + return WorkspaceSourceContainer + w.Container } return "" @@ -222,6 +228,10 @@ func ParseWorkspaceSource(source string) *WorkspaceSource { return &WorkspaceSource{ Image: strings.TrimPrefix(source, WorkspaceSourceImage), } + } else if strings.HasPrefix(source, WorkspaceSourceContainer) { + return &WorkspaceSource{ + Container: strings.TrimPrefix(source, WorkspaceSourceContainer), + } } return nil diff --git a/pkg/workspace/workspace.go b/pkg/workspace/workspace.go index 5f426b9c0..532ad6a78 100644 --- a/pkg/workspace/workspace.go +++ b/pkg/workspace/workspace.go @@ -206,6 +206,14 @@ func ResolveWorkspace( } } + // configure dev container source + if workspace.Source.Container != "" { + err = provider2.SaveWorkspaceConfig(workspace) + if err != nil { + return nil, errors.Wrap(err, "save workspace") + } + } + // create workspace client var workspaceClient client.BaseWorkspaceClient if provider.IsProxyProvider() { @@ -436,6 +444,14 @@ func resolve( return workspace, nil } + // is container? + if strings.HasPrefix(name, provider2.WorkspaceSourceContainer) { + workspace.Source = provider2.WorkspaceSource{ + Container: strings.Split(name, ":")[1], + } + return workspace, nil + } + // is git? gitRepository, gitPRReference, gitBranch, gitCommit := git.NormalizeRepository(name) if strings.HasSuffix(name, ".git") || git.PingRepository(gitRepository) { @@ -458,7 +474,7 @@ func resolve( return workspace, nil } - return nil, fmt.Errorf("%s is neither a local folder, git repository or docker image", name) + return nil, fmt.Errorf("%s is neither a local folder, git repository, docker image or container", name) } var contentRegEx = regexp.MustCompile(`content="([^"]+)"`)