Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5fd2b88
feat(agent): implement AgentDelivery interface for container binary d…
skevetter May 12, 2026
c39204c
ci: add delivery label to PR integration test matrix
skevetter May 12, 2026
7bdd166
refactor(e2e): remove redundant delivery E2E tests
skevetter May 12, 2026
eaed848
fix(delivery): clean up agent delivery volumes on workspace deletion
skevetter May 12, 2026
5f311af
fix(delivery): use defer for volume cleanup to cover nil-container path
skevetter May 12, 2026
e5258fc
fix(test): extract repeated string literals as constants to satisfy g…
skevetter May 12, 2026
ad219fd
feat(delivery): make helper image configurable with direct-copy fallback
skevetter May 12, 2026
c78cd5b
fix(delivery): buffer binary before populate attempts, add fallback test
skevetter May 12, 2026
9133d8d
fix(test): use defaultDockerCmd constant instead of string literal in…
skevetter May 12, 2026
8c3c30e
feat(agent): implement AgentDelivery interface for container binary d…
skevetter May 12, 2026
e0ad863
feat(delivery): add KubernetesDelivery strategy for K8s agent binary …
skevetter May 12, 2026
3d4a495
fix(delivery): move KubernetesDriver case before IsRemoteDocker check
skevetter May 12, 2026
238a38f
fix(delivery): use gzip compression and atomic write for K8s binary i…
skevetter May 12, 2026
8dac681
fix(delivery): stream raw binary instead of gzip for K8s delivery
skevetter May 12, 2026
ab8a127
fix(delivery): route K8s driver to LegacyShellDelivery for reliable i…
skevetter May 12, 2026
d2d5364
fix(delivery): update e2e test to use BinarySource instead of removed…
skevetter May 13, 2026
8c47ee9
fix(delivery): add delivery label to CI test matrix so e2e tests run
skevetter May 13, 2026
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
6 changes: 6 additions & 0 deletions .github/workflows/pr-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,12 @@ jobs:
install-kind: false
requires-secret: false

- label: delivery
runner: ubuntu-latest
free-disk-space: false
install-kind: false
requires-secret: false

- label: exec
runner: ubuntu-latest
free-disk-space: false
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 @@ -10,6 +10,7 @@ import (
// Register tests.
_ "github.com/devsy-org/devsy/e2e/tests/build"
_ "github.com/devsy-org/devsy/e2e/tests/context"
_ "github.com/devsy-org/devsy/e2e/tests/delivery"
_ "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
120 changes: 120 additions & 0 deletions e2e/tests/delivery/delivery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package delivery

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

"github.com/devsy-org/devsy/e2e/framework"
"github.com/devsy-org/devsy/pkg/agent/delivery"
"github.com/devsy-org/devsy/pkg/devcontainer/config"
"github.com/devsy-org/devsy/pkg/driver"
"github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega"
)

var _ = ginkgo.Describe("agent delivery", ginkgo.Label("delivery"), func() {
ginkgo.Context("LocalDockerDelivery", func() {
ginkgo.It("should create volume, populate binary, and mount into RunOptions",
ginkgo.SpecTimeout(framework.TimeoutShort()),
func(ctx context.Context) {
d := &delivery.LocalDockerDelivery{DockerCommand: "docker"}
workspaceID := "e2e-delivery-local"

runOpts := &driver.RunOptions{
Mounts: []*config.Mount{},
Env: map[string]string{},
}

binaryPath := findTestBinary()

err := d.DeliverPreStart(ctx, delivery.PreStartOptions{
WorkspaceID: workspaceID,
RunOptions: runOpts,
BinarySource: binarySourceFromPath(binaryPath),
Arch: "amd64",
})
framework.ExpectNoError(err)
ginkgo.DeferCleanup(func() {
_ = d.Cleanup(context.Background(), workspaceID)
})

gomega.Expect(runOpts.Mounts).To(gomega.HaveLen(1))
expectedVolume := "devsy-agent-" + workspaceID
gomega.Expect(runOpts.Mounts[0].Source).To(gomega.Equal(expectedVolume))
gomega.Expect(runOpts.Mounts[0].Type).To(gomega.Equal("volume"))

// Verify binary in volume via docker run
out, err := exec.CommandContext(ctx, "docker", "run", "--rm",
"-v", "devsy-agent-"+workspaceID+":/opt/devsy",
"busybox:latest", "test", "-x", "/opt/devsy/devsy",
).CombinedOutput()
framework.ExpectNoError(
err, "binary should be executable in volume: %s", string(out),
)
})
})

ginkgo.Context("RemoteDockerDelivery", func() {
ginkgo.It("should copy binary into running container via docker cp",
ginkgo.SpecTimeout(framework.TimeoutShort()),
func(ctx context.Context) {
containerName := "e2e-delivery-remote"

out, err := exec.CommandContext(ctx, "docker", "run", "-d",
"--name", containerName,
"busybox:latest", "sleep", "120",
).CombinedOutput()
framework.ExpectNoError(err, "failed to start test container: %s", string(out))
ginkgo.DeferCleanup(func() {
_ = exec.Command("docker", "rm", "-f", containerName).Run()
})

d := &delivery.RemoteDockerDelivery{
DockerCommand: "docker",
ContainerID: containerName,
}

binaryPath := findTestBinary()

err = d.DeliverPostStart(ctx, delivery.PostStartOptions{
WorkspaceID: "e2e-workspace",
BinarySource: binarySourceFromPath(binaryPath),
Arch: "amd64",
})
framework.ExpectNoError(err)

// Verify binary exists and is executable
out, err = exec.CommandContext(ctx, "docker", "exec", containerName,
"test", "-x", "/usr/local/bin/devsy",
).CombinedOutput()
framework.ExpectNoError(err, "binary should be executable: %s", string(out))
})
})
})

func binarySourceFromPath(path string) delivery.BinarySourceFunc {
return func(_ context.Context, _ string) (io.ReadCloser, error) {
// #nosec G304 -- test helper with controlled paths
return os.Open(path)
}
}

func findTestBinary() string {
candidates := []string{"/bin/sh", "/bin/busybox"}
for _, c := range candidates {
if _, err := os.Stat(c); err == nil {
return c
}
}

binDir, _ := os.Getwd()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Improve error handling in binary discovery logic.

Two issues:

  1. Line 113: os.Getwd() error is silently ignored. If the working directory cannot be determined, binDir will be an empty string, and filepath.Join will produce a path relative to the current directory that likely doesn't exist.

  2. Line 119: The final fallback returns /bin/sh even though we already verified it doesn't exist in the loop at lines 106-111. This will cause the test to fail with a confusing error when os.Open() is later called on a non-existent path.

Consider either panicking with a clear message if no suitable binary is found, or checking the error from os.Getwd() and handling it appropriately.

🛡️ Proposed fix
 func findTestBinary() string {
 	candidates := []string{"/bin/sh", "/bin/busybox"}
 	for _, c := range candidates {
 		if _, err := os.Stat(c); err == nil {
 			return c
 		}
 	}
 
-	binDir, _ := os.Getwd()
+	binDir, err := os.Getwd()
+	if err != nil {
+		panic("failed to get working directory: " + err.Error())
+	}
 	devsy := filepath.Join(binDir, "bin", "devsy-linux-amd64")
 	if _, err := os.Stat(devsy); err == nil {
 		return devsy
 	}
 
-	return "/bin/sh"
+	panic("no suitable test binary found: checked /bin/sh, /bin/busybox, and bin/devsy-linux-amd64")
 }

Also applies to: 119-119

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/tests/delivery/delivery.go` at line 113, The code silently ignores
os.Getwd() and unconditionally returns "/bin/sh" even when it doesn't exist; fix
by checking and handling the error returned by os.Getwd() (the binDir variable)
and aborting/returning an error if Getwd fails, then change the binary discovery
logic (the loop building paths with filepath.Join and the final fallback) to
return a clear error (or panic) when no candidate exists instead of returning
"/bin/sh"; include the list of attempted paths in the error message so callers
(and the later os.Open call) see exactly which files were tried.

devsy := filepath.Join(binDir, "bin", "devsy-linux-amd64")
if _, err := os.Stat(devsy); err == nil {
return devsy
}

return "/bin/sh"
}
9 changes: 9 additions & 0 deletions pkg/agent/delivery/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type FactoryOptions struct {
WorkspaceID string
DockerCommand string
DockerEnv []string
HelperImage string
IsRemoteDocker bool
ContainerID string
ExecFunc inject.ExecFunc
Expand All @@ -32,6 +33,13 @@ func NewAgentDelivery(opts FactoryOptions) AgentDelivery {
DownloadURL: "",
}

case driverType == provider.KubernetesDriver:
log.Debugf("using legacy shell delivery for kubernetes driver")
return &LegacyShellDelivery{
ExecFunc: opts.ExecFunc,
DownloadURL: "",
}

case opts.IsRemoteDocker:
log.Debugf("using remote docker delivery (docker cp)")
return &RemoteDockerDelivery{
Expand All @@ -46,6 +54,7 @@ func NewAgentDelivery(opts FactoryOptions) AgentDelivery {
return &LocalDockerDelivery{
DockerCommand: opts.DockerCommand,
Environment: opts.DockerEnv,
HelperImage: opts.HelperImage,
}
}
log.Debugf("using remote docker delivery for non-local docker daemon")
Expand Down
10 changes: 8 additions & 2 deletions pkg/agent/delivery/factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func TestNewAgentDelivery_LocalDocker(t *testing.T) {
Driver: provider.DockerDriver,
},
},
DockerCommand: "docker",
DockerCommand: defaultDockerCmd,
}

d := NewAgentDelivery(opts)
Expand Down Expand Up @@ -73,17 +73,23 @@ func TestNewAgentDelivery_CustomDriver(t *testing.T) {
assert.Equal(t, PhasePostStart, d.Phase())
}

func TestNewAgentDelivery_KubernetesDriver_FallsToLegacy(t *testing.T) {
func TestNewAgentDelivery_KubernetesDriver(t *testing.T) {
execFn := func(ctx context.Context, cmd string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error {
return nil
}

opts := FactoryOptions{
WorkspaceConfig: &provider.AgentWorkspaceInfo{
Agent: provider.ProviderAgentConfig{
Driver: provider.KubernetesDriver,
},
},
ExecFunc: execFn,
}

d := NewAgentDelivery(opts)
assert.IsType(t, &LegacyShellDelivery{}, d)
assert.Equal(t, PhasePostStart, d.Phase())
}

func TestIsDockerLocal(t *testing.T) {
Expand Down
57 changes: 57 additions & 0 deletions pkg/agent/delivery/kubernetes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package delivery

import (
"context"
"fmt"

"github.com/devsy-org/devsy/pkg/agent"
"github.com/devsy-org/devsy/pkg/inject"
"github.com/devsy-org/devsy/pkg/log"
)

var _ AgentDelivery = (*KubernetesDelivery)(nil)

type KubernetesDelivery struct {
ExecFunc inject.ExecFunc
}

func (d *KubernetesDelivery) Phase() DeliveryPhase {
return PhasePostStart
}

func (d *KubernetesDelivery) DeliverPreStart(_ context.Context, _ PreStartOptions) error {
return fmt.Errorf("KubernetesDelivery does not support pre-start delivery")
}

func (d *KubernetesDelivery) DeliverPostStart(ctx context.Context, opts PostStartOptions) error {
if opts.BinarySource == nil {
return fmt.Errorf("binary source is required for kubernetes delivery")
}
if d.ExecFunc == nil {
return fmt.Errorf("exec function is required for kubernetes delivery")
}

binary, err := opts.BinarySource(ctx, opts.Arch)
if err != nil {
return fmt.Errorf("acquire binary: %w", err)
}
defer func() { _ = binary.Close() }()

destPath := agent.ContainerDevsyHelperLocation
script := fmt.Sprintf(
`set -e; t=$(mktemp %s.XXXXXX); cat > "$t" && chmod 755 "$t" && mv "$t" %s || { rm -f "$t"; exit 1; }`,
destPath,
destPath,
)

if err := d.ExecFunc(ctx, script, binary, nil, nil); err != nil {
return fmt.Errorf("write binary to container: %w", err)
}

log.Debugf("delivered agent binary to kubernetes container via exec")
return nil
}

func (d *KubernetesDelivery) Cleanup(_ context.Context, _ string) error {
return nil
}
113 changes: 113 additions & 0 deletions pkg/agent/delivery/kubernetes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package delivery

import (
"bytes"
"context"
"fmt"
"io"
"strings"
"testing"

"github.com/devsy-org/devsy/pkg/agent"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestKubernetesDelivery_Phase(t *testing.T) {
d := &KubernetesDelivery{}
assert.Equal(t, PhasePostStart, d.Phase())
}

func TestKubernetesDelivery_DeliverPreStart_ReturnsError(t *testing.T) {
d := &KubernetesDelivery{}
err := d.DeliverPreStart(context.Background(), PreStartOptions{})
require.Error(t, err)
assert.Contains(t, err.Error(), "does not support pre-start")
}

func TestKubernetesDelivery_DeliverPostStart_RequiresBinarySource(t *testing.T) {
d := &KubernetesDelivery{
ExecFunc: func(_ context.Context, _ string, _ io.Reader, _ io.Writer, _ io.Writer) error {
return nil
},
}
err := d.DeliverPostStart(context.Background(), PostStartOptions{})
require.Error(t, err)
assert.Contains(t, err.Error(), "binary source is required")
}

func TestKubernetesDelivery_DeliverPostStart_RequiresExecFunc(t *testing.T) {
d := &KubernetesDelivery{}
err := d.DeliverPostStart(context.Background(), PostStartOptions{
BinarySource: fakeBinarySource,
})
require.Error(t, err)
assert.Contains(t, err.Error(), "exec function is required")
}

func TestKubernetesDelivery_DeliverPostStart_WritesBinary(t *testing.T) {
binaryData := "test-binary-content"
var capturedCmd string
var capturedStdin bytes.Buffer

execFn := func(_ context.Context, cmd string, stdin io.Reader, _ io.Writer, _ io.Writer) error {
capturedCmd = cmd
if stdin != nil {
_, _ = io.Copy(&capturedStdin, stdin)
}
return nil
}

d := &KubernetesDelivery{ExecFunc: execFn}
err := d.DeliverPostStart(context.Background(), PostStartOptions{
BinarySource: func(_ context.Context, _ string) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader(binaryData)), nil
},
Arch: "amd64",
})

require.NoError(t, err)

destPath := agent.ContainerDevsyHelperLocation
assert.Contains(t, capturedCmd, "cat >")
assert.Contains(t, capturedCmd, destPath)
assert.Contains(t, capturedCmd, "chmod 755")

assert.Equal(t, binaryData, capturedStdin.String())
}

func TestKubernetesDelivery_DeliverPostStart_BinarySourceError(t *testing.T) {
execFn := func(_ context.Context, _ string, _ io.Reader, _ io.Writer, _ io.Writer) error {
return nil
}

d := &KubernetesDelivery{ExecFunc: execFn}
err := d.DeliverPostStart(context.Background(), PostStartOptions{
BinarySource: func(_ context.Context, _ string) (io.ReadCloser, error) {
return nil, fmt.Errorf("download failed")
},
})

require.Error(t, err)
assert.Contains(t, err.Error(), "acquire binary")
}

func TestKubernetesDelivery_DeliverPostStart_ExecError(t *testing.T) {
execFn := func(_ context.Context, _ string, _ io.Reader, _ io.Writer, _ io.Writer) error {
return fmt.Errorf("exec failed")
}

d := &KubernetesDelivery{ExecFunc: execFn}
err := d.DeliverPostStart(context.Background(), PostStartOptions{
BinarySource: fakeBinarySource,
})

require.Error(t, err)
assert.Contains(t, err.Error(), "write binary to container")
}

func TestKubernetesDelivery_Cleanup_IsNoOp(t *testing.T) {
d := &KubernetesDelivery{}
err := d.Cleanup(context.Background(), "workspace-123")
assert.NoError(t, err)
}
Loading
Loading