Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support setting up existing container as devcontainer #839

Merged
Merged
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
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 @@ -202,9 +202,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
38 changes: 38 additions & 0 deletions e2e/tests/up/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"path/filepath"
"strings"

"github.com/docker/docker/api/types"
"github.com/loft-sh/devpod/e2e/framework"
"github.com/loft-sh/devpod/pkg/devcontainer/config"
docker "github.com/loft-sh/devpod/pkg/docker"
Expand Down Expand Up @@ -51,6 +52,43 @@ var _ = DevPodDescribe("devpod up test suite", func() {
err = f.DevPodUp(ctx, tempDir)
framework.ExpectNoError(err)
}, ginkgo.SpecTimeout(framework.GetTimeout()))
ginkgo.It("should start a new workspace with existing running container", func(ctx context.Context) {
tempDir, err := framework.CopyToTempDir("tests/up/testdata/no-devcontainer")
framework.ExpectNoError(err)
ginkgo.DeferCleanup(framework.CleanupTempDir, initialDir, tempDir)

f := framework.NewDefaultFramework(initialDir + "/bin")

_ = f.DevPodProviderDelete(ctx, "docker")
err = f.DevPodProviderAdd(ctx, "docker")
framework.ExpectNoError(err)
err = f.DevPodProviderUse(ctx, "docker")
framework.ExpectNoError(err)

err = dockerHelper.Run(ctx, []string{"run", "-d", "--label", "devpod-e2e-test-container=true", "-w", "/workspaces/e2e", "mcr.microsoft.com/vscode/devcontainers/base:alpine", "sleep", "infinity"}, nil, nil, nil)
framework.ExpectNoError(err)

ids, err := dockerHelper.FindContainer(ctx, []string{
"devpod-e2e-test-container=true",
})
framework.ExpectNoError(err)
gomega.Expect(ids).To(gomega.HaveLen(1), "1 container is created")
ginkgo.DeferCleanup(dockerHelper.Remove, ids[0])
ginkgo.DeferCleanup(dockerHelper.Stop, ids[0])

var containerDetails []types.ContainerJSON
err = dockerHelper.Inspect(ctx, ids, "container", &containerDetails)
framework.ExpectNoError(err)

containerDetail := containerDetails[0]
gomega.Expect(containerDetail.Config.WorkingDir).To(gomega.Equal("/workspaces/e2e"))

ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), tempDir)

// Wait for devpod workspace to come online (deadline: 30s)
err = f.DevPodUp(ctx, tempDir, "--source", fmt.Sprintf("container:%s", containerDetail.ID))
framework.ExpectNoError(err)
}, ginkgo.SpecTimeout(framework.GetTimeout()))
ginkgo.It("should start a new workspace and substitute devcontainer.json variables", func(ctx context.Context) {
tempDir, err := framework.CopyToTempDir("tests/up/testdata/docker-variables")
framework.ExpectNoError(err)
Expand Down
6 changes: 6 additions & 0 deletions pkg/devcontainer/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type MergedDevContainerConfig struct {
ImageContainer `json:",inline"`
ComposeContainer `json:",inline"`
DockerfileContainer `json:",inline"`
RunningContainer `json:",inline"`

// Origin is the origin from where this config was loaded
Origin string `json:"-"`
Expand All @@ -29,6 +30,7 @@ type DevContainerConfig struct {
ImageContainer `json:",inline"`
ComposeContainer `json:",inline"`
DockerfileContainer `json:",inline"`
RunningContainer `json:",inline"`

// Origin is the origin from where this config was loaded
Origin string `json:"-"`
Expand Down Expand Up @@ -217,6 +219,10 @@ type DockerfileContainer struct {
Build *ConfigBuildOptions `json:"build,omitempty"`
}

type RunningContainer struct {
ContainerID string `json:"containerID,omitempty"`
}

func (d DockerfileContainer) GetDockerfile() string {
if d.Dockerfile != "" {
return d.Dockerfile
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
14 changes: 13 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 != "" || substitutedConfig.Config.ContainerID != "" {
result, err = r.runSingleContainer(
ctx,
substitutedConfig,
Expand Down Expand Up @@ -165,6 +165,18 @@ func (r *runner) prepare(
} else {
rawParsedConfig.Origin = path.Join(filepath.ToSlash(r.LocalWorkspaceFolder), ".devcontainer.devpod.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 found set
WorkspaceFolder: "/",
},
RunningContainer: config.RunningContainer{
ContainerID: r.WorkspaceConfig.Workspace.Source.Container,
},
Origin: "",
}
} else {
var err error

Expand Down
10 changes: 9 additions & 1 deletion pkg/devcontainer/single.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ func (r *runner) runSingleContainer(ctx context.Context, parsedConfig *config.Su
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 && parsedConfig.Config.ContainerID != "" {
return nil, fmt.Errorf("cannot recreate 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 +37,11 @@ func (r *runner) runSingleContainer(ctx context.Context, parsedConfig *config.Su
}
}

// if we are working with a non-managed container, and it has set workingDir, set it as the workspaceFolder
if parsedConfig.Config.ContainerID != "" && containerDetails.Config.WorkingDir != "" {
substitutionContext.ContainerWorkspaceFolder = containerDetails.Config.WorkingDir
}

imageMetadataConfig, err := metadata.GetImageMetadataFromContainer(containerDetails, substitutionContext, r.Log)
if err != nil {
return nil, err
Expand Down
4 changes: 4 additions & 0 deletions pkg/docker/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import (

type DockerHelper struct {
DockerCommand string
// for a running container, we cannot pass down the container ID to the driver without introducing
// changes in the driver interface (which we do not want to do). So, to get around this, we pass
// it down to the driver during docker helper initialization.
ContainerID string
// allow command to have a custom environment
Environment []string
}
Expand Down
9 changes: 8 additions & 1 deletion pkg/driver/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func NewDockerDriver(workspaceInfo *provider2.AgentWorkspaceInfo, log log.Logger
Docker: &docker.DockerHelper{
DockerCommand: dockerCommand,
Environment: makeEnvironment(workspaceInfo.Agent.Docker.Env, log),
ContainerID: workspaceInfo.Workspace.Source.Container,
},
Log: log,
}
Expand Down Expand Up @@ -151,7 +152,13 @@ func (d *dockerDriver) ComposeHelper() (*compose.ComposeHelper, error) {
}

func (d *dockerDriver) FindDevContainer(ctx context.Context, workspaceId string) (*config.ContainerDetails, error) {
containerDetails, err := d.Docker.FindDevContainer(ctx, []string{config.DockerIDLabel + "=" + workspaceId})
var containerDetails *config.ContainerDetails
var err error
if d.Docker.ContainerID != "" {
containerDetails, err = d.Docker.FindContainerByID(ctx, []string{d.Docker.ContainerID})
} else {
containerDetails, err = d.Docker.FindDevContainer(ctx, []string{config.DockerIDLabel + "=" + workspaceId})
}
if err != nil {
return nil, err
} else if containerDetails == nil {
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 @@ -108,6 +109,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 @@ -203,6 +207,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 @@ -225,6 +231,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
8 changes: 8 additions & 0 deletions pkg/workspace/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,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
Loading