Skip to content
Draft
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
2 changes: 1 addition & 1 deletion cmd/internal/agentworkspace/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ func (w *workspaceInitializer) ensureDockerInstalled(ctx context.Context) (strin
dockerCmd := w.getDockerCommand()

if command.Exists(dockerCmd) {
log.Debug("docker command exists, skipping installation")
log.Debug("docker CLI found")
return "", nil
}

Expand Down
1 change: 1 addition & 0 deletions e2e/e2e_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
_ "github.com/devsy-org/devsy/e2e/tests/configread"
_ "github.com/devsy-org/devsy/e2e/tests/context"
_ "github.com/devsy-org/devsy/e2e/tests/delivery"
_ "github.com/devsy-org/devsy/e2e/tests/dockercontext"
_ "github.com/devsy-org/devsy/e2e/tests/dockerinstall"
_ "github.com/devsy-org/devsy/e2e/tests/down"
_ "github.com/devsy-org/devsy/e2e/tests/exec"
Expand Down
144 changes: 144 additions & 0 deletions e2e/tests/dockercontext/dockercontext.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package dockercontext

import (
"context"
"os"
"os/exec"
"path/filepath"

"github.com/devsy-org/devsy/e2e/framework"
docker "github.com/devsy-org/devsy/pkg/docker"
"github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega"
)

var _ = ginkgo.Describe(
"docker context test suite",
ginkgo.Label("docker-context"),
func() {
var (
initialDir string
dockerHelper *docker.DockerHelper
f *framework.Framework
)

ginkgo.BeforeEach(func(ctx context.Context) {
var err error
initialDir, err = os.Getwd()
framework.ExpectNoError(err)

dockerHelper = &docker.DockerHelper{DockerCommand: "docker"}
if pingErr := dockerHelper.Ping(ctx); pingErr != nil {
ginkgo.Skip("docker daemon is unreachable: " + pingErr.Error())
}

f, err = framework.SetupDockerProvider(filepath.Join(initialDir, "bin"), "docker")
framework.ExpectNoError(err)
})

ginkgo.It(
"persisted non-default Docker context survives credential injection",
ginkgo.SpecTimeout(framework.TimeoutShort()),
func(ctx context.Context) {
const testContextName = "devsy-test"

_ = exec.CommandContext(ctx, "docker", "context", "rm", testContextName).Run()

err := exec.CommandContext(
ctx,
"docker",
"context",
"create",
testContextName,
"--docker",
"host=unix:///var/run/docker.sock",
).Run()
framework.ExpectNoError(err)
ginkgo.DeferCleanup(func(cleanupCtx context.Context) {
_ = exec.CommandContext(cleanupCtx, "docker", "context", "use", "default").Run()
_ = exec.CommandContext(cleanupCtx, "docker", "context", "rm", testContextName).
Run()
})

err = exec.CommandContext(ctx, "docker", "context", "use", testContextName).Run()
framework.ExpectNoError(err)

origContext, hasContext := os.LookupEnv("DOCKER_CONTEXT")
origHost, hasHost := os.LookupEnv("DOCKER_HOST")
_ = os.Unsetenv("DOCKER_CONTEXT")
_ = os.Unsetenv("DOCKER_HOST")
ginkgo.DeferCleanup(func() {
if hasContext {
_ = os.Setenv("DOCKER_CONTEXT", origContext)
}
if hasHost {
_ = os.Setenv("DOCKER_HOST", origHost)
}
})

tempDir, err := framework.CopyToTempDir("tests/up/testdata/docker")
framework.ExpectNoError(err)
ginkgo.DeferCleanup(framework.CleanupTempDir, initialDir, tempDir)
ginkgo.DeferCleanup(f.DevsyWorkspaceDelete, tempDir)

stdout, stderr, err := f.DevsyUpStreams(ctx, tempDir)
framework.ExpectNoError(err)

combined := stdout + "\n" + stderr
gomega.Expect(combined).To(gomega.ContainSubstring("context=" + testContextName))
},
)

ginkgo.It(
"explicit DOCKER_CONTEXT overrides the persisted default",
ginkgo.SpecTimeout(framework.TimeoutShort()),
func(ctx context.Context) {
const explicitContextName = "devsy-test-explicit"

_ = exec.CommandContext(ctx, "docker", "context", "rm", explicitContextName).Run()

err := exec.CommandContext(
ctx,
"docker",
"context",
"create",
explicitContextName,
"--docker",
"host=unix:///var/run/docker.sock",
).Run()
framework.ExpectNoError(err)
ginkgo.DeferCleanup(func(cleanupCtx context.Context) {
_ = exec.CommandContext(cleanupCtx, "docker", "context", "rm", explicitContextName).
Run()
})

err = exec.CommandContext(ctx, "docker", "context", "use", "default").Run()
framework.ExpectNoError(err)

origContext, hasContext := os.LookupEnv("DOCKER_CONTEXT")
_ = os.Setenv("DOCKER_CONTEXT", explicitContextName)
ginkgo.DeferCleanup(func() {
if hasContext {
_ = os.Setenv("DOCKER_CONTEXT", origContext)
} else {
_ = os.Unsetenv("DOCKER_CONTEXT")
}
})

tempDir, err := framework.CopyToTempDir("tests/up/testdata/docker")
framework.ExpectNoError(err)
ginkgo.DeferCleanup(framework.CleanupTempDir, initialDir, tempDir)
ginkgo.DeferCleanup(f.DevsyWorkspaceDelete, tempDir)

stdout, stderr, err := f.DevsyUpStreams(ctx, tempDir)
framework.ExpectNoError(err)

combined := stdout + "\n" + stderr
gomega.Expect(combined).
To(gomega.ContainSubstring("context=" + explicitContextName))
gomega.Expect(combined).
To(gomega.ContainSubstring("docker_context=" + explicitContextName))
},
)
},
)
108 changes: 108 additions & 0 deletions pkg/docker/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,50 @@ func runCmdCombined(ctx context.Context, cmd *exec.Cmd) error {
return nil
}

const envUnset = "<unset>"

// RuntimeDiagnostics resolves information about the effective docker runtime configuration.
func (r *DockerHelper) RuntimeDiagnostics(ctx context.Context) map[string]string {
cctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

diagnostics := map[string]string{
"command": r.DockerCommand,
"docker_config": resolveEnvValue(r.Environment, "DOCKER_CONFIG"),
"docker_host": resolveEnvValue(r.Environment, "DOCKER_HOST"),
"docker_context": resolveEnvValue(r.Environment, "DOCKER_CONTEXT"),
}

if r.IsPodman() {
runtimeDiagnosticsPodman(r, diagnostics)
return diagnostics
}

r.runtimeDiagnosticsDocker(cctx, diagnostics)
return diagnostics
}

func resolveEnvValue(env []string, key string) string {
if val, ok := envValue(env, key); ok {
return val
}
if val := os.Getenv(key); val != "" {
return val
}
return envUnset
}

func runtimeDiagnosticsPodman(r *DockerHelper, diag map[string]string) {
diag["context"] = "<not applicable>"
if host := resolveEnvValue(r.Environment, "CONTAINER_HOST"); host != envUnset {
diag["endpoint"] = host
} else if host := resolveEnvValue(r.Environment, "DOCKER_HOST"); host != envUnset {
diag["endpoint"] = host
} else {
diag["endpoint"] = "<not applicable>"
}
}

// Ping reports whether the runtime daemon is reachable, returning its own
// message (e.g. "Cannot connect to Podman") on failure. It runs a bare `info`
// and judges reachability by exit status: `--format` field names differ
Expand Down Expand Up @@ -762,6 +806,70 @@ func (r *DockerHelper) buildCmd(ctx context.Context, args ...string) *exec.Cmd {
return cmd
}

func (r *DockerHelper) resolveDockerContext(ctx context.Context, envContext string) string {
if envContext != envUnset {
return envContext
}
out, err := r.buildCmd(ctx, "context", "show").Output()
if err == nil {
if cur := strings.TrimSpace(string(out)); cur != "" {
return cur
}
}
return "default"
}

func (r *DockerHelper) getEndpointFromContext(ctx context.Context, contextName string) string {
out, err := r.buildCmd(
ctx,
"context",
"inspect",
contextName,
"--format",
"{{.Endpoints.docker.Host}}",
).Output()
if err == nil {
if endpoint := strings.TrimSpace(string(out)); endpoint != "" && endpoint != "<no value>" {
return endpoint
}
}
return ""
}

func (r *DockerHelper) endpointForContext(ctx context.Context, contextName string) string {
if ep := r.getEndpointFromContext(ctx, contextName); ep != "" {
return ep
}
if contextName == "default" {
return "unix:///var/run/docker.sock"
}
return "<unknown>"
}

func (r *DockerHelper) resolveDockerEndpoint(
ctx context.Context,
activeContext, envContext, dockerHost string,
) string {
if envContext != envUnset {
return r.endpointForContext(ctx, envContext)
}
if dockerHost != envUnset {
return dockerHost
}
return r.endpointForContext(ctx, activeContext)
}

func (r *DockerHelper) runtimeDiagnosticsDocker(ctx context.Context, diag map[string]string) {
activeContext := r.resolveDockerContext(ctx, diag["docker_context"])
diag["context"] = activeContext
diag["endpoint"] = r.resolveDockerEndpoint(
ctx,
activeContext,
diag["docker_context"],
diag["docker_host"],
)
}

// PrepareForGroupCancellation sets the Cancel function of the given exec.Cmd
// to kill the entire process group, allowing for cleanup of child processes.
// This is necessary because exec.Cmd does not automatically kill child processes
Expand Down
Loading
Loading