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
92 changes: 92 additions & 0 deletions .github/workflows/publish-agent-helper.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: Publish Agent Helper Image

on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: optional version override (defaults to the latest git tag)
required: false
type: string
pull_request:
paths:
- images/agent-helper/**
- hack/agent_helper_image/**
- .github/workflows/publish-agent-helper.yml

env:
IMAGE: ghcr.io/${{ github.repository_owner }}/agent-helper

jobs:
publish:
name: Build and publish agent-helper image
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0

- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod

- uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1
with:
version: 0.16.0

- name: cross-compile helper binaries
run: go run ./hack/agent_helper_image

- name: resolve version
id: version
run: |
set -euo pipefail
if [ -n "${{ inputs.tag }}" ]; then
version="${{ inputs.tag }}"
elif [ "${{ github.event_name }}" = "release" ]; then
version="${{ github.ref_name }}"
else
version="$(git describe --tags --abbrev=0)"
fi
echo "value=$version" >> "$GITHUB_OUTPUT"

- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0

# Publish only on release / manual dispatch; pull_request runs build-only.
- name: generate GitHub App token
id: app-token
if: ${{ github.event_name != 'pull_request' }}
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: ${{ secrets.DEVSY_GITHUB_APP_ID }}
private-key: ${{ secrets.DEVSY_GITHUB_APP_PRIVATE_KEY }}

- name: log in to ghcr
if: ${{ github.event_name != 'pull_request' }}
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with:
registry: ghcr.io
username: ${{ steps.app-token.outputs.app-slug }}
password: ${{ steps.app-token.outputs.token }}

- name: docker metadata
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ env.IMAGE }}
tags: |
type=raw,value=${{ steps.version.outputs.value }}
type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }}

- name: build and push
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: images/agent-helper
file: images/agent-helper/Dockerfile
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
11 changes: 4 additions & 7 deletions cmd/internal/agentworkspace/clean.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"github.com/devsy-org/devsy/cmd/flags"
pkgconfig "github.com/devsy-org/devsy/pkg/config"
cliflags "github.com/devsy-org/devsy/pkg/flags"
"github.com/devsy-org/devsy/pkg/flags/names"
"github.com/devsy-org/devsy/pkg/log"
Expand All @@ -17,7 +18,6 @@ const (
cleanVolumePrefix = "devsy-agent-"
cleanVolumeMountPath = "/opt/devsy"
cleanBinaryName = "devsy"
cleanHelperImage = "busybox:latest"
cleanDefaultDockerCmd = "docker"
)

Expand Down Expand Up @@ -55,8 +55,8 @@ This forces a fresh binary injection on the next workspace start.`,
cliflags.String(
&cmd.HelperImage,
names.HelperImage,
cleanHelperImage,
"Helper image for volume operations",
"",
"Helper image for volume operations (default "+pkgconfig.DefaultHelperImage+", or $"+pkgconfig.EnvHelperImage+")",
),
)
return cleanCmd
Expand Down Expand Up @@ -125,8 +125,5 @@ func (cmd *CleanCmd) dockerCommand() string {
}

func (cmd *CleanCmd) helperImage() string {
if cmd.HelperImage != "" {
return cmd.HelperImage
}
return cleanHelperImage
return pkgconfig.HelperImage(cmd.HelperImage)
}
59 changes: 59 additions & 0 deletions hack/agent_helper_image/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Cross-compiles the agent-helper volume helper for each published arch into
// images/agent-helper/dist, producing the per-arch binaries the FROM scratch
// image copies in via TARGETARCH.
package main

import (
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
)

type target struct {
zig string // Zig -target triple
out string // output name, matching the Dockerfile's TARGETARCH
}

var targets = []target{
{zig: "x86_64-linux-musl", out: "helper-amd64"},
{zig: "aarch64-linux-musl", out: "helper-arm64"},
}

func main() {
dir := flag.String("dir", "images/agent-helper", "agent-helper image directory")
flag.Parse()

if err := run(*dir); err != nil {
fmt.Fprintln(os.Stderr, "agent_helper_image:", err)
os.Exit(1)
}
}

func run(dir string) error {
distDir := filepath.Join(dir, "dist")
if err := os.MkdirAll(distDir, 0o755); err != nil {
return err
}

src := filepath.Join(dir, "helper.zig")
for _, t := range targets {
out := filepath.Join(distDir, t.out)
cmd := exec.Command("zig", "build-exe", src,
"-target", t.zig, "-O", "ReleaseSmall", "-femit-bin="+out,
"--cache-dir", filepath.Join(distDir, ".zig-cache"))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("build %s: %w", t.out, err)
}

info, err := os.Stat(out)
if err != nil {
return err
}
fmt.Printf("built %s (%d bytes)\n", out, info.Size())
}
return nil
}
2 changes: 2 additions & 0 deletions images/agent-helper/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist/
.zig-cache/
4 changes: 4 additions & 0 deletions images/agent-helper/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FROM scratch
ARG TARGETARCH
COPY dist/helper-${TARGETARCH} /helper
ENTRYPOINT ["/helper"]
10 changes: 10 additions & 0 deletions images/agent-helper/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# agent-helper image

## Build locally

Requires [Zig](https://ziglang.org) 0.16.0.

```sh
go run ./hack/agent_helper_image # cross-compiles dist/helper-{amd64,arm64}
docker buildx build --platform linux/arm64 --load -t agent-helper:dev images/agent-helper
```
52 changes: 52 additions & 0 deletions images/agent-helper/helper.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//! Minimal volume-helper for agent delivery.
//!
//! helper populate <path> read stdin, write it to <path>, chmod 0755
//! helper clean <path> remove <path> (idempotent; missing file is ok)
const std = @import("std");
const linux = std.os.linux;

fn die(msg: []const u8) noreturn {
std.debug.print("helper: {s}\n", .{msg});
linux.exit(1);
}

fn check(rc: usize, what: []const u8) usize {
if (@as(isize, @bitCast(rc)) < 0) die(what);
return rc;
}

fn cmdPopulate(path: [:0]const u8) void {
const fd: i32 = @intCast(check(
linux.open(path.ptr, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, 0o755),
"open",
));
var buf: [64 * 1024]u8 = undefined;
while (true) {
const n = check(linux.read(0, &buf, buf.len), "read");
if (n == 0) break;
var off: usize = 0;
while (off < n) off += check(linux.write(fd, buf[off..].ptr, n - off), "write");
}
_ = check(linux.fchmod(fd, 0o755), "fchmod");
_ = linux.close(fd);
}

fn cmdClean(path: [:0]const u8) void {
const err = @as(isize, @bitCast(linux.unlink(path.ptr)));
// ENOENT (-2) is fine: nothing to remove.
if (err < 0 and err != -2) die("unlink");
}

pub fn main(init: std.process.Init.Minimal) !void {
var it = std.process.Args.Iterator.init(init.args);
_ = it.next(); // argv[0]
const sub = it.next() orelse die("usage: helper <populate|clean> <path>");
const path = it.next() orelse die("usage: helper <populate|clean> <path>");
if (std.mem.eql(u8, sub, "populate")) {
cmdPopulate(path);
} else if (std.mem.eql(u8, sub, "clean")) {
cmdClean(path);
} else {
die("unknown subcommand");
}
}
14 changes: 5 additions & 9 deletions pkg/agent/delivery/local_docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@ import (
var _ AgentDelivery = (*LocalDockerDelivery)(nil)

const (
defaultDockerCmd = "docker"
podmanCmd = "podman"
volumePrefix = "devsy-agent-"
volumeMountPath = "/opt/devsy"
defaultHelperImage = "busybox:latest"
defaultDockerCmd = "docker"
podmanCmd = "podman"
volumePrefix = "devsy-agent-"
volumeMountPath = "/opt/devsy"

// cmdRun / flagRM build throwaway helper-container invocations.
cmdRun = "run"
Expand Down Expand Up @@ -141,10 +140,7 @@ func (d *LocalDockerDelivery) createVolume(
}

func (d *LocalDockerDelivery) helperImageName() string {
if d.HelperImage != "" {
return d.HelperImage
}
return defaultHelperImage
return pkgconfig.HelperImage(d.HelperImage)
}

func (d *LocalDockerDelivery) expectedVersion() string {
Expand Down
3 changes: 3 additions & 0 deletions pkg/config/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ const (
// EnvAgentURL overrides the agent download URL.
EnvAgentURL = "DEVSY_AGENT_URL"

// EnvHelperImage overrides the helper image for agent volume operations.
EnvHelperImage = "DEVSY_HELPER_IMAGE"

// EnvAgentPreferDownload forces agent binary download even if a local copy exists.
EnvAgentPreferDownload = "DEVSY_AGENT_PREFER_DOWNLOAD"

Expand Down
17 changes: 17 additions & 0 deletions pkg/config/helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package config

import "os"

const DefaultHelperImage = "busybox:latest"

// HelperImage resolves the helper image: explicit value, then
// DEVSY_HELPER_IMAGE, then DefaultHelperImage.
func HelperImage(explicit string) string {
if explicit != "" {
return explicit
}
if env := os.Getenv(EnvHelperImage); env != "" {
return env
}
return DefaultHelperImage
}
26 changes: 26 additions & 0 deletions pkg/config/helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package config

import "testing"

func TestHelperImage(t *testing.T) {
t.Run("explicit wins over env and default", func(t *testing.T) {
t.Setenv(EnvHelperImage, "env-image")
if got := HelperImage("explicit-image"); got != "explicit-image" {
t.Fatalf("got %q, want explicit-image", got)
}
})

t.Run("env used when no explicit value", func(t *testing.T) {
t.Setenv(EnvHelperImage, "env-image")
if got := HelperImage(""); got != "env-image" {
t.Fatalf("got %q, want env-image", got)
}
})

t.Run("default when nothing set", func(t *testing.T) {
t.Setenv(EnvHelperImage, "")
if got := HelperImage(""); got != DefaultHelperImage {
t.Fatalf("got %q, want %q", got, DefaultHelperImage)
}
})
}
4 changes: 2 additions & 2 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,8 @@ type ProviderDockerDriverConfig struct {
// Environment variables to set when running docker commands
Env map[string]string `json:"env,omitempty"`

// HelperImage is used by LocalDockerDelivery for volume population.
// When empty, defaults to busybox:latest. A direct-copy fallback is used if the helper container approach fails.
// HelperImage overrides the helper image for volume operations. Empty falls
// back to DEVSY_HELPER_IMAGE, then config.DefaultHelperImage.
HelperImage string `json:"helperImage,omitempty"`

// Runtime identifies the container runtime explicitly (docker, podman, nerdctl).
Expand Down
Loading