Skip to content
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
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")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Uppercase logging string

The changed docker CLI found message violates the repository convention that logging strings remain lowercase, making this diagnostic inconsistent with the prescribed log format.

Suggested change
log.Debug("docker CLI found")
log.Debug("docker cli found")

Context Used: AGENTS.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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
160 changes: 160 additions & 0 deletions e2e/tests/dockercontext/dockercontext.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package dockercontext

import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"

"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
activeEndpoint 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())
}

diag := dockerHelper.RuntimeDiagnostics(ctx)
activeEndpoint = diag["endpoint"]
if activeEndpoint == "" || activeEndpoint == "<unknown>" {
activeEndpoint = "unix:///var/run/docker.sock"
}

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) {
testContextName := fmt.Sprintf("devsy-test-%d", time.Now().UnixNano())

//nolint:gosec // activeEndpoint is resolved from local docker info/diagnostics
err := exec.CommandContext(
ctx,
"docker",
"context",
"create",
testContextName,
"--docker",
"host="+activeEndpoint,
).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)
}
})

origShow, showErr := exec.CommandContext(ctx, "docker", "context", "show").Output()
framework.ExpectNoError(showErr)
initialPersistedContext := strings.TrimSpace(string(origShow))
if initialPersistedContext == "" {
initialPersistedContext = "default"
}
ginkgo.DeferCleanup(func(cleanupCtx context.Context) {
_ = os.Unsetenv("DOCKER_CONTEXT")
//nolint:gosec // initialPersistedContext was captured from docker context show
_ = exec.CommandContext(cleanupCtx, "docker", "context", "use", initialPersistedContext).
Run()
//nolint:gosec // testContextName is unique to this test
_ = exec.CommandContext(cleanupCtx, "docker", "context", "rm", testContextName).
Run()
})
//nolint:gosec // testContextName is unique to this test
err = exec.CommandContext(ctx, "docker", "context", "use", testContextName).Run()
framework.ExpectNoError(err)

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) {
explicitContextName := fmt.Sprintf("devsy-test-explicit-%d", time.Now().UnixNano())

//nolint:gosec // activeEndpoint is resolved from local docker info/diagnostics
err := exec.CommandContext(
ctx,
"docker",
"context",
"create",
explicitContextName,
"--docker",
"host="+activeEndpoint,
).Run()
framework.ExpectNoError(err)
ginkgo.DeferCleanup(func(cleanupCtx context.Context) {
//nolint:gosec // explicitContextName is unique to this test
_ = exec.CommandContext(cleanupCtx, "docker", "context", "rm", explicitContextName).
Run()
})

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))
},
)
},
)
111 changes: 111 additions & 0 deletions pkg/docker/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,53 @@ 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 {
if val != "" {
return val
}
return envUnset
}
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 +809,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