Skip to content

Commit

Permalink
feat: support setting up a running container as devcontainer
Browse files Browse the repository at this point in the history
  • Loading branch information
amitds1997 committed Dec 11, 2023
1 parent 1a3341f commit b5e98cc
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 14 deletions.
12 changes: 8 additions & 4 deletions cmd/agent/workspace/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
5 changes: 4 additions & 1 deletion cmd/agent/workspace/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions pkg/devcontainer/config/container_details.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
11 changes: 10 additions & 1 deletion pkg/devcontainer/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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...")
Expand Down
11 changes: 10 additions & 1 deletion pkg/devcontainer/single.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
25 changes: 22 additions & 3 deletions pkg/driver/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down Expand Up @@ -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 != "" {
Expand Down
16 changes: 13 additions & 3 deletions pkg/provider/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import (
)

var (
WorkspaceSourceGit = "git:"
WorkspaceSourceLocal = "local:"
WorkspaceSourceImage = "image:"
WorkspaceSourceGit = "git:"
WorkspaceSourceLocal = "local:"
WorkspaceSourceImage = "image:"
WorkspaceSourceContainer = "container:"
)

type Workspace struct {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 ""
Expand All @@ -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
Expand Down
18 changes: 17 additions & 1 deletion pkg/workspace/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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) {
Expand All @@ -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="([^"]+)"`)
Expand Down

0 comments on commit b5e98cc

Please sign in to comment.