From c985cca676567223d01f3ea4a139ead3dbecd276 Mon Sep 17 00:00:00 2001 From: Joshua Gilman Date: Sun, 4 Jan 2026 14:19:09 -0800 Subject: [PATCH 1/4] refactor(container): remove Apple Containerization support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Current behavior: Headjack supported three container runtimes: Docker, Podman, and Apple Containerization Framework. Apple Containerization required macOS 26+ and used the `container` CLI binary. New behavior: Headjack now supports only Docker and Podman runtimes. All Apple Containerization code, tests, and documentation references have been removed. ADR-002 is preserved with "Superseded" status for historical context. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- README.md | 2 +- .../adr-002-apple-containerization.md | 6 +- docs/docs/decisions/adr-003-go-language.md | 2 +- docs/docs/decisions/adr-005-no-gpg-support.md | 6 +- docs/docs/decisions/index.md | 6 +- docs/docs/explanation/image-customization.md | 2 +- docs/docs/how-to/build-custom-image.md | 4 +- docs/docs/how-to/install.md | 4 +- docs/docs/reference/cli/config.md | 2 +- docs/docs/reference/configuration.md | 4 +- docs/docs/reference/images/labels.md | 31 +- docs/docs/tutorials/custom-image.md | 8 +- docs/docs/tutorials/getting-started.md | 2 +- integration/integration_test.go | 17 +- internal/cmd/root.go | 16 +- internal/cmd/run.go | 11 +- internal/config/config.go | 7 +- internal/container/apple.go | 137 ---- internal/container/apple_test.go | 620 ------------------ internal/instance/manager.go | 5 - internal/instance/manager_test.go | 41 +- 21 files changed, 43 insertions(+), 890 deletions(-) delete mode 100644 internal/container/apple.go delete mode 100644 internal/container/apple_test.go diff --git a/README.md b/README.md index 4bc8c94..0e3ea57 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Full documentation is available at [headjack.gilman.io](https://headjack.gilman. ## Requirements - macOS or Linux -- [Docker](https://www.docker.com/) (default), [Podman](https://podman.io/), or Apple Containerization (macOS 26+) +- [Docker](https://www.docker.com/) (default) or [Podman](https://podman.io/) - Git ## License diff --git a/docs/docs/decisions/adr-002-apple-containerization.md b/docs/docs/decisions/adr-002-apple-containerization.md index 8cabc4f..adcb3b9 100644 --- a/docs/docs/decisions/adr-002-apple-containerization.md +++ b/docs/docs/decisions/adr-002-apple-containerization.md @@ -8,7 +8,11 @@ description: Decision to use Apple Containerization Framework for agent isolatio ## Status -Accepted +Superseded + +:::info Supersession Note +Apple Containerization support was removed from Headjack. The project now supports only Docker and Podman runtimes for cross-platform compatibility. This ADR is preserved for historical context. +::: ## Context diff --git a/docs/docs/decisions/adr-003-go-language.md b/docs/docs/decisions/adr-003-go-language.md index fced5a2..ae66e55 100644 --- a/docs/docs/decisions/adr-003-go-language.md +++ b/docs/docs/decisions/adr-003-go-language.md @@ -69,7 +69,7 @@ Use **Go** as the implementation language for Headjack. - **Verbosity**: More boilerplate than scripting languages - **Error handling**: Explicit error checking adds code volume -- **No Apple Containerization SDK**: Must shell out regardless (not a Go-specific limitation) +- **No container CLI SDK**: Must shell out to Docker/Podman CLI (not a Go-specific limitation) ### Neutral diff --git a/docs/docs/decisions/adr-005-no-gpg-support.md b/docs/docs/decisions/adr-005-no-gpg-support.md index 787e7ed..280bea9 100644 --- a/docs/docs/decisions/adr-005-no-gpg-support.md +++ b/docs/docs/decisions/adr-005-no-gpg-support.md @@ -12,15 +12,15 @@ Accepted ## Context -Developers commonly use GPG to sign git commits. When running agents in isolated Apple Containerization instances, the host's GPG keys and agent are not directly accessible. +Developers commonly use GPG to sign git commits. When running agents in isolated container instances, the host's GPG keys and agent are not directly accessible. We investigated two approaches to enable GPG signing from within containers: ### Option 1: GPG Agent Forwarding via TCP Bridge -GPG agent forwarding works by proxying the host's `gpg-agent` socket into the container. However, Unix sockets don't cross VM boundaries (each Apple Container is a separate VM). +GPG agent forwarding works by proxying the host's `gpg-agent` socket into the container. This requires mounting the socket into the container. -A workaround exists using `socat` to bridge Unix socket → TCP on the host, then TCP → Unix socket in the container. This was validated empirically and works, including with hardware tokens (Yubikey). +A TCP bridge approach using `socat` to bridge Unix socket → TCP on the host, then TCP → Unix socket in the container was validated empirically and works, including with hardware tokens (Yubikey). **Complexity:** - Requires `socat` installed on host diff --git a/docs/docs/decisions/index.md b/docs/docs/decisions/index.md index 7c53749..957d28d 100644 --- a/docs/docs/decisions/index.md +++ b/docs/docs/decisions/index.md @@ -17,7 +17,7 @@ An Architecture Decision Record (ADR) is a document that captures an important a | ADR | Title | Status | |-----|-------|--------| | [ADR-001](./adr-001-macos-only) | Initial macOS-Only Platform Support | Superseded | -| [ADR-002](./adr-002-apple-containerization) | Apple Containerization Framework | Accepted | +| [ADR-002](./adr-002-apple-containerization) | Apple Containerization Framework | Superseded | | [ADR-003](./adr-003-go-language) | Go as Implementation Language | Accepted | | [ADR-004](./adr-004-cli-agents) | CLI-Based Agents over API-Based | Accepted | | [ADR-005](./adr-005-no-gpg-support) | Defer GPG Commit Signing Support | Accepted | @@ -27,7 +27,7 @@ An Architecture Decision Record (ADR) is a document that captures an important a These decisions reflect several key themes in Headjack's design: -- **Simplicity over generality**: initial single-platform scope (macOS), OCI images only, CLI agents only -- **Leverage existing ecosystems**: Apple Containerization, Go CLI patterns, standard OCI tooling +- **Simplicity over generality**: OCI images only, CLI agents only +- **Leverage existing ecosystems**: Docker/Podman, Go CLI patterns, standard OCI tooling - **Defer complexity**: GPG support deferred, Nix support left to users - **Optimize for the common case**: Subscription-based agents, opinionated base images diff --git a/docs/docs/explanation/image-customization.md b/docs/docs/explanation/image-customization.md index 8ca488d..5e756f6 100644 --- a/docs/docs/explanation/image-customization.md +++ b/docs/docs/explanation/image-customization.md @@ -6,7 +6,7 @@ description: OCI images approach vs alternatives like Nix # Image Customization -Headjack runs agents in containers, and those containers need the right tools installed. How do you customize the environment when your project needs specific languages, frameworks, or system packages? Headjack answers this with standard OCI images, delegating all customization to Docker, Podman, or Apple Container tooling you already know. +Headjack runs agents in containers, and those containers need the right tools installed. How do you customize the environment when your project needs specific languages, frameworks, or system packages? Headjack answers this with standard OCI images, delegating all customization to Docker or Podman tooling you already know. ## The Customization Problem diff --git a/docs/docs/how-to/build-custom-image.md b/docs/docs/how-to/build-custom-image.md index 80efee1..f87220e 100644 --- a/docs/docs/how-to/build-custom-image.md +++ b/docs/docs/how-to/build-custom-image.md @@ -27,7 +27,7 @@ hjk run feat/auth --base ghcr.io/gilmanlab/headjack:dind ### Prerequisites -- Docker, Podman, or Apple Container installed +- Docker or Podman installed - Familiarity with Dockerfile syntax ### Create a Dockerfile @@ -85,7 +85,7 @@ podman build -t my-custom-headjack:latest -f Dockerfile.headjack . ### Build for multiple architectures -For teams with both Intel and Apple Silicon Macs (using Docker buildx): +For teams with both Intel and ARM Macs (using Docker buildx): ```bash docker buildx build \ diff --git a/docs/docs/how-to/install.md b/docs/docs/how-to/install.md index 517b948..4463a80 100644 --- a/docs/docs/how-to/install.md +++ b/docs/docs/how-to/install.md @@ -12,9 +12,9 @@ This guide is being written. Check back soon! ## Prerequisites -- macOS or Linux (Apple Containerization requires macOS 26+) +- macOS or Linux - Git -- Container runtime (Docker, Podman, or Apple Container) +- Container runtime (Docker or Podman) ## Installation Steps diff --git a/docs/docs/reference/cli/config.md b/docs/docs/reference/cli/config.md index 3afe7d3..e2502fc 100644 --- a/docs/docs/reference/cli/config.md +++ b/docs/docs/reference/cli/config.md @@ -62,7 +62,7 @@ Common configuration keys: | `storage.worktrees` | string | Directory for git worktrees | | `storage.catalog` | string | Path to the instance catalog file | | `storage.logs` | string | Directory for session logs | -| `runtime.name` | string | Container runtime (`podman`, `apple`, `docker`) | +| `runtime.name` | string | Container runtime (`podman`, `docker`) | ## Configuration File diff --git a/docs/docs/reference/configuration.md b/docs/docs/reference/configuration.md index b485f77..4e315a8 100644 --- a/docs/docs/reference/configuration.md +++ b/docs/docs/reference/configuration.md @@ -64,7 +64,7 @@ Container runtime configuration. | Key | Type | Default | Description | |-----|------|---------|-------------| -| `runtime.name` | string | `docker` | Container runtime to use. Valid values: `podman`, `apple`, `docker`. | +| `runtime.name` | string | `docker` | Container runtime to use. Valid values: `podman`, `docker`. | | `runtime.flags` | map[string]any | `{}` | Additional flags to pass to the container runtime. | ## Example Configuration @@ -145,7 +145,7 @@ Headjack validates configuration values when loading and setting them: - `default.agent` must be one of: `claude`, `gemini`, `codex` (or empty) - `default.base_image` is required and cannot be empty -- `runtime.name` must be one of: `podman`, `apple`, `docker` +- `runtime.name` must be one of: `podman`, `docker` - All storage paths are required Invalid values will result in an error message describing the validation failure. diff --git a/docs/docs/reference/images/labels.md b/docs/docs/reference/images/labels.md index a1344a1..da19392 100644 --- a/docs/docs/reference/images/labels.md +++ b/docs/docs/reference/images/labels.md @@ -132,32 +132,6 @@ LABEL io.headjack.docker.flags="privileged=true" | `systemd` | `privileged=true cgroupns=host volume=/sys/fs/cgroup:/sys/fs/cgroup:rw` | | `dind` | Inherited from `systemd` | ---- - -### io.headjack.apple.flags - -Reserved for Apple Containerization Framework-specific flags. - -| Property | Value | -|----------|-------| -| Key | `io.headjack.apple.flags` | -| Value type | String (space-separated key=value pairs) | -| Default | None | - -#### Description - -This label is reserved for specifying flags specific to the Apple Containerization Framework. It follows the same format as `io.headjack.podman.flags`. - -#### Example - -```dockerfile -LABEL io.headjack.apple.flags="rosetta=true" -``` - -#### Usage in Official Images - -Not currently used in official images. - ## Building Custom Images When building custom images that extend the official Headjack images, labels are not automatically inherited. You must explicitly set any labels you need. @@ -203,7 +177,7 @@ LABEL io.headjack.init="/usr/local/bin/init.sh" ## Label Inspection -You can inspect image labels using Docker, Podman, or Apple Container: +You can inspect image labels using Docker or Podman: ```bash # Using Docker @@ -211,9 +185,6 @@ docker inspect ghcr.io/gilmanlab/headjack:systemd --format='{{json .Config.Label # Using Podman podman inspect ghcr.io/gilmanlab/headjack:systemd --format='{{json .Config.Labels}}' | jq - -# Using Apple Container -container inspect ghcr.io/gilmanlab/headjack:systemd --format='{{json .Config.Labels}}' | jq ``` Example output: diff --git a/docs/docs/tutorials/custom-image.md b/docs/docs/tutorials/custom-image.md index 0a53e33..0c855e8 100644 --- a/docs/docs/tutorials/custom-image.md +++ b/docs/docs/tutorials/custom-image.md @@ -15,7 +15,7 @@ This tutorial takes approximately 30-40 minutes to complete. Before starting, ensure you have: - Completed the [Getting Started](./getting-started) tutorial -- Docker, Podman, or Apple Container installed on your machine +- Docker or Podman installed on your machine - Basic familiarity with Dockerfile syntax - A project with specific runtime requirements (Python version, Node.js packages, system tools, etc.) @@ -183,7 +183,7 @@ podman build -t my-app-headjack:latest -f Dockerfile.headjack . ``` :::note -Build with the same container runtime that Headjack uses. Check your configuration with `hjk config` and look for `runtime.name`. Images built with one runtime (Docker, Podman, or Apple Container) are not automatically available to others unless pushed to a registry. +Build with the same container runtime that Headjack uses. Check your configuration with `hjk config` and look for `runtime.name`. Images built with one runtime (Docker or Podman) are not automatically available to others unless pushed to a registry. ::: The build takes several minutes as it compiles Python and Node.js. You will see output for each step: @@ -282,7 +282,7 @@ hjk config default.base_image ghcr.io/your-org/my-app-headjack:latest ## Step 12: Build for Multiple Architectures -If your team uses both Intel and Apple Silicon Macs, build a multi-architecture image: +If your team uses both Intel and ARM Macs, build a multi-architecture image: ```bash docker buildx build \ @@ -292,7 +292,7 @@ docker buildx build \ -f Dockerfile.headjack . ``` -This creates an image that works on both architectures. Docker, Podman, and Apple Container automatically pull the correct variant. +This creates an image that works on both architectures. Docker and Podman automatically pull the correct variant. ## Complete Dockerfile diff --git a/docs/docs/tutorials/getting-started.md b/docs/docs/tutorials/getting-started.md index e21fba6..97b9aa7 100644 --- a/docs/docs/tutorials/getting-started.md +++ b/docs/docs/tutorials/getting-started.md @@ -34,7 +34,7 @@ import useBaseUrl from '@docusaurus/useBaseUrl'; Before starting, ensure you have: -- **macOS or Linux with a container runtime installed** - Headjack supports Docker (default), Podman, or Apple Container +- **macOS or Linux with a container runtime installed** - Headjack supports Docker (default) or Podman - **Git installed** - Verify with `git --version` - **A Claude Pro/Max subscription OR an Anthropic API key** - For Claude Code authentication - **A git repository to work in** - Any project repository will work diff --git a/integration/integration_test.go b/integration/integration_test.go index 0f96a43..5b4b9a5 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -79,17 +79,7 @@ func TestScripts(t *testing.T) { // detectRuntime auto-detects the available container runtime. func detectRuntime() string { - if runtime.GOOS == "darwin" { - // Prefer Apple Containerization on macOS - if _, err := exec.LookPath("container"); err == nil { - return "apple" - } - // Fall back to Docker - if _, err := exec.LookPath("docker"); err == nil { - return "docker" - } - } - // Linux: prefer Docker for CI consistency + // Prefer Docker for cross-platform consistency if _, err := exec.LookPath("docker"); err == nil { return "docker" } @@ -173,8 +163,6 @@ func evalCondition(cond string, runtimeName string) (bool, error) { return runtimeName == "podman", nil case "docker": return runtimeName == "docker", nil - case "apple": - return runtimeName == "apple", nil case "linux": return runtime.GOOS == "linux", nil case "darwin": @@ -198,9 +186,6 @@ func cmdCleanupContainers(ts *testscript.TestScript, neg bool, args []string) { var cmd *exec.Cmd switch runtimeName { - case "apple": - // Apple container CLI cleanup - cmd = exec.Command("sh", "-c", `container list --format json 2>/dev/null | jq -r '.[].configuration.id // empty' | grep '^hjk-' | while read id; do container stop "$id" 2>/dev/null; container rm "$id" 2>/dev/null; done`) case "docker": cmd = exec.Command("sh", "-c", `docker ps -a --format '{{.Names}}' 2>/dev/null | grep '^hjk-' | xargs -r docker rm -f 2>/dev/null`) default: // podman diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 79ad313..a0832c6 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -27,9 +27,6 @@ import ( // baseDeps lists the external binaries that must always be available. var baseDeps = []string{"git"} -// runtimeNameApple is the runtime name for Apple's container framework. -const runtimeNameApple = "apple" - // runtimeNameDocker is the runtime name for Docker. const runtimeNameDocker = "docker" @@ -129,13 +126,8 @@ func checkDependencies() error { // getRuntimeBinary returns the binary name for the configured runtime. func getRuntimeBinary() string { - if appConfig != nil { - switch appConfig.Runtime.Name { - case runtimeNameApple: - return "container" - case runtimeNameDocker: - return runtimeBinaryDocker - } + if appConfig != nil && appConfig.Runtime.Name != "" { + return appConfig.Runtime.Name // Runtime name matches binary name (docker, podman) } // Default to docker return runtimeBinaryDocker @@ -173,8 +165,6 @@ func initManager() error { runtimeName = appConfig.Runtime.Name } switch runtimeName { - case runtimeNameApple: - runtime = container.NewAppleRuntime(executor, container.AppleConfig{}) case runtimeNameDocker: runtime = container.NewDockerRuntime(executor, container.DockerConfig{}) default: @@ -212,8 +202,6 @@ func initManager() error { // runtimeNameToType converts a runtime name string to RuntimeType. func runtimeNameToType(name string) instance.RuntimeType { switch name { - case runtimeNameApple: - return instance.RuntimeApple case runtimeNameDocker: return instance.RuntimeDocker default: diff --git a/internal/cmd/run.go b/internal/cmd/run.go index edb7c0c..b1527bc 100644 --- a/internal/cmd/run.go +++ b/internal/cmd/run.go @@ -258,8 +258,6 @@ func formatInstanceNotRunningHint(cmd *cobra.Command, err *instance.NotRunningEr func runtimeLogsCommand(runtimeName, containerID string) string { switch runtimeName { - case runtimeNameApple: - return "container logs " + containerID case runtimeNameDocker: return "docker logs " + containerID default: @@ -326,18 +324,11 @@ func buildCreateConfig(cmd *cobra.Command, repoPath, branch, image string, image return cfg } - // Check runtime compatibility (devcontainer only works with Docker/Podman) + // Create devcontainer runtime wrapping the underlying runtime runtimeName := runtimeNameDocker if appCfg := ConfigFromContext(cmd.Context()); appCfg != nil && appCfg.Runtime.Name != "" { runtimeName = appCfg.Runtime.Name } - - if runtimeName == runtimeNameApple { - fmt.Println("Warning: devcontainer.json detected but Apple runtime does not support devcontainers, using vanilla mode") - return cfg - } - - // Create devcontainer runtime wrapping the underlying runtime dcRuntime := createDevcontainerRuntime(cmd, runtimeName) if dcRuntime == nil { // Fall back to vanilla mode if we can't create the devcontainer runtime diff --git a/internal/config/config.go b/internal/config/config.go index fef7da8..9789bc8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -43,7 +43,6 @@ var validAgents = map[string]bool{ // validRuntimes contains the allowed runtime names (unexported). var validRuntimes = map[string]bool{ "podman": true, - "apple": true, "docker": true, } @@ -81,7 +80,7 @@ type StorageConfig struct { // RuntimeConfig holds container runtime configuration. type RuntimeConfig struct { - Name string `mapstructure:"name" validate:"omitempty,oneof=podman apple docker"` + Name string `mapstructure:"name" validate:"omitempty,oneof=podman docker"` Flags map[string]any `mapstructure:"flags"` } @@ -220,7 +219,7 @@ func (l *Loader) Set(key, value string) error { // Validate runtime name if setting runtime.name if key == "runtime.name" && value != "" { if !validRuntimes[value] { - return fmt.Errorf("%w: %s (valid: podman, apple, docker)", ErrInvalidRuntime, value) + return fmt.Errorf("%w: %s (valid: podman, docker)", ErrInvalidRuntime, value) } } @@ -322,5 +321,5 @@ func IsValidRuntime(name string) bool { // ValidRuntimeNames returns the list of valid runtime names. func ValidRuntimeNames() []string { - return []string{"podman", "apple", "docker"} + return []string{"podman", "docker"} } diff --git a/internal/container/apple.go b/internal/container/apple.go deleted file mode 100644 index 6789c9f..0000000 --- a/internal/container/apple.go +++ /dev/null @@ -1,137 +0,0 @@ -package container - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/jmgilman/headjack/internal/exec" -) - -// AppleConfig holds Apple Containerization-specific runtime configuration. -type AppleConfig struct { - // Currently empty - all flags go through RunConfig.Flags after merging - // at the manager level. Kept for future runtime-specific settings. -} - -// appleRuntime implements Runtime using Apple Containerization CLI. -// All common functionality is provided by the embedded baseRuntime. -type appleRuntime struct { - baseRuntime - config AppleConfig -} - -// appleParser implements containerParser for Apple Containerization JSON output. -type appleParser struct{} - -// NewAppleRuntime creates a Runtime using Apple Containerization CLI. -func NewAppleRuntime(e exec.Executor, cfg AppleConfig) Runtime { - parser := &appleParser{} - return &appleRuntime{ - baseRuntime: baseRuntime{ - exec: e, - binaryName: "container", - execCommand: []string{"container", "exec"}, - listArgs: []string{"list"}, - parser: parser, - }, - config: cfg, - } -} - -// appleInspect represents the JSON output of `container inspect`. -type appleInspect struct { - Status string `json:"status"` - Created string `json:"created"` // ISO 8601 format if available - Configuration struct { - ID string `json:"id"` - Image struct { - Reference string `json:"reference"` - } `json:"image"` - } `json:"configuration"` -} - -func (c *appleInspect) toContainer() *Container { - // Parse created timestamp if available - // Apple Containerization uses ISO 8601 format - var createdAt time.Time - if c.Created != "" { - // Try RFC3339Nano first (most precise), then RFC3339 - if parsed, err := time.Parse(time.RFC3339Nano, c.Created); err == nil { - createdAt = parsed - } else if parsed, err := time.Parse(time.RFC3339, c.Created); err == nil { - createdAt = parsed - } - // If parsing fails, createdAt remains zero value - } - - return &Container{ - ID: c.Configuration.ID, - Name: c.Configuration.ID, - Image: c.Configuration.Image.Reference, - Status: parseContainerStatus(c.Status), - CreatedAt: createdAt, - } -} - -// appleListItem represents a single item in `container list` JSON output. -// Note: Apple container list has similar format to inspect. -type appleListItem struct { - Status string `json:"status"` - Created string `json:"created"` // ISO 8601 format if available - Configuration struct { - ID string `json:"id"` - Image struct { - Reference string `json:"reference"` - } `json:"image"` - } `json:"configuration"` -} - -func (c *appleListItem) toContainer() Container { - // Parse created timestamp if available - var createdAt time.Time - if c.Created != "" { - if parsed, err := time.Parse(time.RFC3339Nano, c.Created); err == nil { - createdAt = parsed - } else if parsed, err := time.Parse(time.RFC3339, c.Created); err == nil { - createdAt = parsed - } - } - - return Container{ - ID: c.Configuration.ID, - Name: c.Configuration.ID, - Image: c.Configuration.Image.Reference, - Status: parseContainerStatus(c.Status), - CreatedAt: createdAt, - } -} - -// parseInspect parses the JSON output of `container inspect`. -func (p *appleParser) parseInspect(data []byte) (*Container, error) { - var infos []appleInspect - if err := json.Unmarshal(data, &infos); err != nil { - return nil, fmt.Errorf("parse container info: %w", err) - } - - if len(infos) == 0 { - return nil, ErrNotFound - } - - return infos[0].toContainer(), nil -} - -// parseList parses the JSON output of `container list`. -func (p *appleParser) parseList(data []byte) ([]Container, error) { - var items []appleListItem - if err := json.Unmarshal(data, &items); err != nil { - return nil, fmt.Errorf("parse container list: %w", err) - } - - containers := make([]Container, len(items)) - for i, item := range items { - containers[i] = item.toContainer() - } - - return containers, nil -} diff --git a/internal/container/apple_test.go b/internal/container/apple_test.go deleted file mode 100644 index a756a2b..0000000 --- a/internal/container/apple_test.go +++ /dev/null @@ -1,620 +0,0 @@ -package container - -import ( - "context" - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/jmgilman/headjack/internal/exec" - "github.com/jmgilman/headjack/internal/exec/mocks" -) - -func TestNewAppleRuntime(t *testing.T) { - mockExec := &mocks.ExecutorMock{} - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - - require.NotNil(t, runtime) -} - -func TestAppleRuntime_Run(t *testing.T) { - ctx := context.Background() - - t.Run("creates container successfully with default init command", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(_ context.Context, opts *exec.RunOptions) (*exec.Result, error) { - assert.Equal(t, "container", opts.Name) - assert.Contains(t, opts.Args, "run") - assert.Contains(t, opts.Args, "--detach") - assert.Contains(t, opts.Args, "--name") - assert.Contains(t, opts.Args, "test-container") - assert.Contains(t, opts.Args, "ubuntu:24.04") - // Default init command should be "sleep infinity" - assert.Contains(t, opts.Args, "sleep") - assert.Contains(t, opts.Args, "infinity") - - return &exec.Result{ - Stdout: []byte("abc123def456\n"), - ExitCode: 0, - }, nil - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - container, err := runtime.Run(ctx, &RunConfig{ - Name: "test-container", - Image: "ubuntu:24.04", - }) - - require.NoError(t, err) - assert.Equal(t, "abc123def456", container.ID) - assert.Equal(t, "test-container", container.Name) - assert.Equal(t, "ubuntu:24.04", container.Image) - assert.Equal(t, StatusRunning, container.Status) - }) - - t.Run("uses custom init command when specified", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(_ context.Context, opts *exec.RunOptions) (*exec.Result, error) { - // Custom init command should be at the end - assert.Contains(t, opts.Args, "/lib/systemd/systemd") - - return &exec.Result{ - Stdout: []byte("abc123\n"), - ExitCode: 0, - }, nil - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - _, err := runtime.Run(ctx, &RunConfig{ - Name: "test", - Image: "ubuntu", - Init: "/lib/systemd/systemd", - }) - - require.NoError(t, err) - }) - - t.Run("includes image-specific flags from RunConfig", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(_ context.Context, opts *exec.RunOptions) (*exec.Result, error) { - assert.Contains(t, opts.Args, "--custom-flag") - - return &exec.Result{ - Stdout: []byte("abc123\n"), - ExitCode: 0, - }, nil - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - _, err := runtime.Run(ctx, &RunConfig{ - Name: "test", - Image: "ubuntu", - Flags: []string{"--custom-flag"}, - }) - - require.NoError(t, err) - }) - - t.Run("includes privileged flag when configured", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(_ context.Context, opts *exec.RunOptions) (*exec.Result, error) { - assert.Contains(t, opts.Args, "--privileged") - - return &exec.Result{ - Stdout: []byte("abc123\n"), - ExitCode: 0, - }, nil - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - _, err := runtime.Run(ctx, &RunConfig{ - Name: "test", - Image: "ubuntu", - Flags: []string{"--privileged"}, - }) - - require.NoError(t, err) - }) - - t.Run("includes custom flags from RunConfig", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(_ context.Context, opts *exec.RunOptions) (*exec.Result, error) { - assert.Contains(t, opts.Args, "--memory=2g") - assert.Contains(t, opts.Args, "--cpus=2") - - return &exec.Result{ - Stdout: []byte("abc123\n"), - ExitCode: 0, - }, nil - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - _, err := runtime.Run(ctx, &RunConfig{ - Name: "test", - Image: "ubuntu", - Flags: []string{"--memory=2g", "--cpus=2"}, - }) - - require.NoError(t, err) - }) - - t.Run("includes volume mounts", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(ctx context.Context, opts *exec.RunOptions) (*exec.Result, error) { - assert.Contains(t, opts.Args, "-v") - assert.Contains(t, opts.Args, "/host/path:/container/path") - - return &exec.Result{ - Stdout: []byte("abc123\n"), - ExitCode: 0, - }, nil - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - _, err := runtime.Run(ctx, &RunConfig{ - Name: "test", - Image: "ubuntu", - Mounts: []Mount{ - {Source: "/host/path", Target: "/container/path"}, - }, - }) - - require.NoError(t, err) - }) - - t.Run("includes read-only mount flag", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(ctx context.Context, opts *exec.RunOptions) (*exec.Result, error) { - assert.Contains(t, opts.Args, "/host:/container:ro") - - return &exec.Result{ - Stdout: []byte("abc123\n"), - ExitCode: 0, - }, nil - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - _, err := runtime.Run(ctx, &RunConfig{ - Name: "test", - Image: "ubuntu", - Mounts: []Mount{ - {Source: "/host", Target: "/container", ReadOnly: true}, - }, - }) - - require.NoError(t, err) - }) - - t.Run("includes environment variables", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(ctx context.Context, opts *exec.RunOptions) (*exec.Result, error) { - assert.Contains(t, opts.Args, "-e") - assert.Contains(t, opts.Args, "FOO=bar") - - return &exec.Result{ - Stdout: []byte("abc123\n"), - ExitCode: 0, - }, nil - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - _, err := runtime.Run(ctx, &RunConfig{ - Name: "test", - Image: "ubuntu", - Env: []string{"FOO=bar"}, - }) - - require.NoError(t, err) - }) - - t.Run("returns ErrAlreadyExists when container exists", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(ctx context.Context, opts *exec.RunOptions) (*exec.Result, error) { - return &exec.Result{ - Stderr: []byte("container already exists"), - ExitCode: 1, - }, errors.New("exit code 1") - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - _, err := runtime.Run(ctx, &RunConfig{ - Name: "existing", - Image: "ubuntu", - }) - - assert.ErrorIs(t, err, ErrAlreadyExists) - }) -} - -func TestAppleRuntime_Exec(t *testing.T) { - ctx := context.Background() - - t.Run("executes command in running container", func(t *testing.T) { - callCount := 0 - mockExec := &mocks.ExecutorMock{ - RunFunc: func(ctx context.Context, opts *exec.RunOptions) (*exec.Result, error) { - callCount++ - if callCount == 1 { - // Get call - Apple Container format - return &exec.Result{ - Stdout: []byte(`[{"status":"running","configuration":{"id":"abc123","image":{"reference":"ubuntu"}}}]`), - }, nil - } - // Exec call - assert.Equal(t, "container", opts.Name) - assert.Contains(t, opts.Args, "exec") - assert.Contains(t, opts.Args, "abc123") - assert.Contains(t, opts.Args, "bash") - - return &exec.Result{ExitCode: 0}, nil - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - err := runtime.Exec(ctx, "abc123", &ExecConfig{ - Command: []string{"bash"}, - }) - - require.NoError(t, err) - }) - - t.Run("includes workdir when specified", func(t *testing.T) { - callCount := 0 - mockExec := &mocks.ExecutorMock{ - RunFunc: func(ctx context.Context, opts *exec.RunOptions) (*exec.Result, error) { - callCount++ - if callCount == 1 { - // Get call - Apple Container format - return &exec.Result{ - Stdout: []byte(`[{"status":"running","configuration":{"id":"abc123","image":{"reference":"ubuntu"}}}]`), - }, nil - } - assert.Contains(t, opts.Args, "-w") - assert.Contains(t, opts.Args, "/app") - - return &exec.Result{ExitCode: 0}, nil - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - err := runtime.Exec(ctx, "abc123", &ExecConfig{ - Command: []string{"ls"}, - Workdir: "/app", - }) - - require.NoError(t, err) - }) - - t.Run("returns ErrNotFound when container missing", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(ctx context.Context, opts *exec.RunOptions) (*exec.Result, error) { - return &exec.Result{ - Stderr: []byte("container not found"), - ExitCode: 1, - }, errors.New("exit code 1") - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - err := runtime.Exec(ctx, "missing", &ExecConfig{ - Command: []string{"bash"}, - }) - - assert.ErrorIs(t, err, ErrNotFound) - }) - - t.Run("returns ErrNotRunning when container stopped", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(ctx context.Context, opts *exec.RunOptions) (*exec.Result, error) { - // Get call - Apple Container format with stopped status - return &exec.Result{ - Stdout: []byte(`[{"status":"stopped","configuration":{"id":"abc123","image":{"reference":"ubuntu"}}}]`), - }, nil - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - err := runtime.Exec(ctx, "abc123", &ExecConfig{ - Command: []string{"bash"}, - }) - - assert.ErrorIs(t, err, ErrNotRunning) - }) -} - -func TestAppleRuntime_Stop(t *testing.T) { - ctx := context.Background() - - t.Run("stops running container", func(t *testing.T) { - callCount := 0 - mockExec := &mocks.ExecutorMock{ - RunFunc: func(ctx context.Context, opts *exec.RunOptions) (*exec.Result, error) { - callCount++ - if callCount == 1 { - // Get call - Apple Container format - return &exec.Result{ - Stdout: []byte(`[{"status":"running","configuration":{"id":"abc123","image":{"reference":"ubuntu"}}}]`), - }, nil - } - // Stop call - assert.Equal(t, "container", opts.Name) - assert.Equal(t, []string{"stop", "abc123"}, opts.Args) - - return &exec.Result{ExitCode: 0}, nil - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - err := runtime.Stop(ctx, "abc123") - - require.NoError(t, err) - assert.Equal(t, 2, callCount) - }) - - t.Run("no-op for already stopped container", func(t *testing.T) { - callCount := 0 - mockExec := &mocks.ExecutorMock{ - RunFunc: func(ctx context.Context, opts *exec.RunOptions) (*exec.Result, error) { - callCount++ - // Get call - Apple Container format with stopped status - return &exec.Result{ - Stdout: []byte(`[{"status":"stopped","configuration":{"id":"abc123","image":{"reference":"ubuntu"}}}]`), - }, nil - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - err := runtime.Stop(ctx, "abc123") - - require.NoError(t, err) - assert.Equal(t, 1, callCount) // Only Get call, no Stop call - }) - - t.Run("returns ErrNotFound when container missing", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(ctx context.Context, opts *exec.RunOptions) (*exec.Result, error) { - return &exec.Result{ - Stderr: []byte("container not found"), - ExitCode: 1, - }, errors.New("exit code 1") - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - err := runtime.Stop(ctx, "missing") - - assert.ErrorIs(t, err, ErrNotFound) - }) -} - -func TestAppleRuntime_Remove(t *testing.T) { - ctx := context.Background() - - t.Run("removes container", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(ctx context.Context, opts *exec.RunOptions) (*exec.Result, error) { - assert.Equal(t, "container", opts.Name) - assert.Equal(t, []string{"rm", "abc123"}, opts.Args) - - return &exec.Result{ExitCode: 0}, nil - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - err := runtime.Remove(ctx, "abc123") - - require.NoError(t, err) - }) - - t.Run("returns ErrNotFound when container missing", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(ctx context.Context, opts *exec.RunOptions) (*exec.Result, error) { - return &exec.Result{ - Stderr: []byte("no such container"), - ExitCode: 1, - }, errors.New("exit code 1") - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - err := runtime.Remove(ctx, "missing") - - assert.ErrorIs(t, err, ErrNotFound) - }) -} - -func TestAppleRuntime_Get(t *testing.T) { - ctx := context.Background() - - t.Run("returns container info", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(ctx context.Context, opts *exec.RunOptions) (*exec.Result, error) { - assert.Equal(t, "container", opts.Name) - assert.Contains(t, opts.Args, "inspect") - assert.Contains(t, opts.Args, "abc123") - - // Apple Container format - return &exec.Result{ - Stdout: []byte(`[{"status":"running","configuration":{"id":"abc123def456","image":{"reference":"ubuntu:24.04"}}}]`), - }, nil - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - container, err := runtime.Get(ctx, "abc123") - - require.NoError(t, err) - assert.Equal(t, "abc123def456", container.ID) - assert.Equal(t, "abc123def456", container.Name) // Name is set to ID in Apple Container format - assert.Equal(t, "ubuntu:24.04", container.Image) - assert.Equal(t, StatusRunning, container.Status) - }) - - t.Run("parses stopped state", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(ctx context.Context, opts *exec.RunOptions) (*exec.Result, error) { - // Apple Container format with exited status - return &exec.Result{ - Stdout: []byte(`[{"status":"exited","configuration":{"id":"abc123","image":{"reference":"ubuntu"}}}]`), - }, nil - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - container, err := runtime.Get(ctx, "abc123") - - require.NoError(t, err) - assert.Equal(t, StatusStopped, container.Status) - }) - - t.Run("returns ErrNotFound when container missing", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(ctx context.Context, opts *exec.RunOptions) (*exec.Result, error) { - return &exec.Result{ - Stderr: []byte("not found"), - ExitCode: 1, - }, errors.New("exit code 1") - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - _, err := runtime.Get(ctx, "missing") - - assert.ErrorIs(t, err, ErrNotFound) - }) -} - -func TestAppleRuntime_List(t *testing.T) { - ctx := context.Background() - - t.Run("returns empty list", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(ctx context.Context, opts *exec.RunOptions) (*exec.Result, error) { - return &exec.Result{ - Stdout: []byte("[]"), - }, nil - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - containers, err := runtime.List(ctx, ListFilter{}) - - require.NoError(t, err) - assert.Empty(t, containers) - }) - - t.Run("returns container list", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(ctx context.Context, opts *exec.RunOptions) (*exec.Result, error) { - // Apple Container format - return &exec.Result{ - Stdout: []byte(`[{"status":"running","configuration":{"id":"abc","image":{"reference":"ubuntu"}}},{"status":"stopped","configuration":{"id":"def","image":{"reference":"alpine"}}}]`), - }, nil - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - containers, err := runtime.List(ctx, ListFilter{}) - - require.NoError(t, err) - require.Len(t, containers, 2) - assert.Equal(t, "abc", containers[0].ID) - assert.Equal(t, "abc", containers[0].Name) // Name is set to ID in Apple Container format - assert.Equal(t, StatusRunning, containers[0].Status) - assert.Equal(t, "def", containers[1].ID) - assert.Equal(t, StatusStopped, containers[1].Status) - }) - - t.Run("includes name filter", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(ctx context.Context, opts *exec.RunOptions) (*exec.Result, error) { - assert.Contains(t, opts.Args, "--filter") - assert.Contains(t, opts.Args, "name=my-prefix") - - return &exec.Result{Stdout: []byte("[]")}, nil - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - _, err := runtime.List(ctx, ListFilter{Name: "my-prefix"}) - - require.NoError(t, err) - }) -} - -func TestAppleRuntime_Build(t *testing.T) { - ctx := context.Background() - - t.Run("builds image", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(ctx context.Context, opts *exec.RunOptions) (*exec.Result, error) { - assert.Equal(t, "container", opts.Name) - assert.Contains(t, opts.Args, "build") - assert.Contains(t, opts.Args, "-t") - assert.Contains(t, opts.Args, "myimage:latest") - assert.Contains(t, opts.Args, "/build/context") - - return &exec.Result{ExitCode: 0}, nil - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - err := runtime.Build(ctx, &BuildConfig{ - Context: "/build/context", - Tag: "myimage:latest", - }) - - require.NoError(t, err) - }) - - t.Run("includes dockerfile path", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(ctx context.Context, opts *exec.RunOptions) (*exec.Result, error) { - assert.Contains(t, opts.Args, "-f") - assert.Contains(t, opts.Args, "custom.Dockerfile") - - return &exec.Result{ExitCode: 0}, nil - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - err := runtime.Build(ctx, &BuildConfig{ - Context: "/build/context", - Dockerfile: "custom.Dockerfile", - Tag: "myimage:latest", - }) - - require.NoError(t, err) - }) - - t.Run("returns ErrBuildFailed on failure", func(t *testing.T) { - mockExec := &mocks.ExecutorMock{ - RunFunc: func(ctx context.Context, opts *exec.RunOptions) (*exec.Result, error) { - return &exec.Result{ - Stderr: []byte("build error: missing base image"), - ExitCode: 1, - }, errors.New("exit code 1") - }, - } - - runtime := NewAppleRuntime(mockExec, AppleConfig{}) - err := runtime.Build(ctx, &BuildConfig{ - Context: "/build/context", - Tag: "myimage:latest", - }) - - require.ErrorIs(t, err, ErrBuildFailed) - assert.Contains(t, err.Error(), "missing base image") - }) -} diff --git a/internal/instance/manager.go b/internal/instance/manager.go index f76f492..476692f 100644 --- a/internal/instance/manager.go +++ b/internal/instance/manager.go @@ -72,7 +72,6 @@ type RuntimeType string // Runtime type constants. const ( RuntimePodman RuntimeType = "podman" - RuntimeApple RuntimeType = "apple" RuntimeDocker RuntimeType = "docker" ) @@ -150,7 +149,6 @@ type imageRuntimeConfig struct { const ( labelInit = "io.headjack.init" labelPodmanFlags = "io.headjack.podman.flags" - labelAppleFlags = "io.headjack.apple.flags" labelDockerFlags = "io.headjack.docker.flags" ) @@ -158,7 +156,6 @@ const ( // Returns default values if the registry client is nil or metadata cannot be fetched. // Runtime-specific flags are extracted based on the configured runtime type: // - Podman: io.headjack.podman.flags -// - Apple: io.headjack.apple.flags // - Docker: io.headjack.docker.flags func (m *Manager) getImageRuntimeConfig(ctx context.Context, image string) imageRuntimeConfig { cfg := imageRuntimeConfig{ @@ -186,8 +183,6 @@ func (m *Manager) getImageRuntimeConfig(ctx context.Context, image string) image switch m.runtimeType { case RuntimePodman: flagsLabel = labelPodmanFlags - case RuntimeApple: - flagsLabel = labelAppleFlags case RuntimeDocker: flagsLabel = labelDockerFlags } diff --git a/internal/instance/manager_test.go b/internal/instance/manager_test.go index d8d825c..264b5a4 100644 --- a/internal/instance/manager_test.go +++ b/internal/instance/manager_test.go @@ -43,9 +43,9 @@ func TestNewManager(t *testing.T) { }) t.Run("respects explicit RuntimeType", func(t *testing.T) { - mgr := NewManager(nil, nil, nil, nil, nil, ManagerConfig{RuntimeType: RuntimeApple}) + mgr := NewManager(nil, nil, nil, nil, nil, ManagerConfig{RuntimeType: RuntimePodman}) - assert.Equal(t, RuntimeApple, mgr.runtimeType) + assert.Equal(t, RuntimePodman, mgr.runtimeType) }) } @@ -1297,7 +1297,7 @@ func TestGetImageRuntimeConfig(t *testing.T) { assert.Equal(t, true, cfg.Flags["privileged"]) }) - t.Run("ignores podman flags when using apple runtime", func(t *testing.T) { + t.Run("ignores podman flags when using docker runtime", func(t *testing.T) { reg := ®istrymocks.ClientMock{ GetMetadataFunc: func(ctx context.Context, ref string) (*registry.ImageMetadata, error) { return ®istry.ImageMetadata{ @@ -1310,45 +1310,22 @@ func TestGetImageRuntimeConfig(t *testing.T) { } mgr := NewManager(nil, nil, nil, nil, reg, ManagerConfig{ - RuntimeType: RuntimeApple, - }) - - cfg := mgr.getImageRuntimeConfig(ctx, "myimage:systemd") - - assert.Equal(t, "/lib/systemd/systemd", cfg.Init) - assert.Nil(t, cfg.Flags, "podman flags should be ignored for apple runtime") - }) - - t.Run("extracts apple flags when using apple runtime", func(t *testing.T) { - reg := ®istrymocks.ClientMock{ - GetMetadataFunc: func(ctx context.Context, ref string) (*registry.ImageMetadata, error) { - return ®istry.ImageMetadata{ - Labels: map[string]string{ - "io.headjack.init": "/lib/systemd/systemd", - "io.headjack.apple.flags": "network=bridge memory=2g", - }, - }, nil - }, - } - - mgr := NewManager(nil, nil, nil, nil, reg, ManagerConfig{ - RuntimeType: RuntimeApple, + RuntimeType: RuntimeDocker, }) cfg := mgr.getImageRuntimeConfig(ctx, "myimage:systemd") assert.Equal(t, "/lib/systemd/systemd", cfg.Init) - assert.Equal(t, "bridge", cfg.Flags["network"]) - assert.Equal(t, "2g", cfg.Flags["memory"]) + assert.Nil(t, cfg.Flags, "podman flags should be ignored for docker runtime") }) - t.Run("ignores apple flags when using podman runtime", func(t *testing.T) { + t.Run("ignores docker flags when using podman runtime", func(t *testing.T) { reg := ®istrymocks.ClientMock{ GetMetadataFunc: func(ctx context.Context, ref string) (*registry.ImageMetadata, error) { return ®istry.ImageMetadata{ Labels: map[string]string{ - "io.headjack.init": "/lib/systemd/systemd", - "io.headjack.apple.flags": "network=bridge memory=2g", + "io.headjack.init": "/lib/systemd/systemd", + "io.headjack.docker.flags": "privileged=true memory=4g", }, }, nil }, @@ -1361,7 +1338,7 @@ func TestGetImageRuntimeConfig(t *testing.T) { cfg := mgr.getImageRuntimeConfig(ctx, "myimage:systemd") assert.Equal(t, "/lib/systemd/systemd", cfg.Init) - assert.Nil(t, cfg.Flags, "apple flags should be ignored for podman runtime") + assert.Nil(t, cfg.Flags, "docker flags should be ignored for podman runtime") }) t.Run("extracts docker flags when using docker runtime", func(t *testing.T) { From 1ffacd8c617730f086a263b94faf53b102dd1dad Mon Sep 17 00:00:00 2001 From: Joshua Gilman Date: Sun, 4 Jan 2026 14:32:41 -0800 Subject: [PATCH 2/4] fix(container): remove remaining Apple Containerization references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Current behavior: Several files still contained Apple Containerization references after the initial removal commit. New behavior: - justfile: Remove apple auto-detection and integration-test-apple target - ADR-002: Update addendum to reflect runtime evolution and removal - Design docs: Remove Apple from runtime support tables and diagrams - Code comments: Update to show Docker/Podman only - Dockerfile: Update iptables workaround comment 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- docs/designs/devcontainer-integration.md | 12 ++++-------- .../docs/decisions/adr-002-apple-containerization.md | 9 ++++----- images/dind/Dockerfile | 4 ++-- internal/cmd/run.go | 1 - internal/container/common.go | 2 +- internal/container/container.go | 2 +- internal/instance/manager.go | 2 +- justfile | 12 ++---------- 8 files changed, 15 insertions(+), 29 deletions(-) diff --git a/docs/designs/devcontainer-integration.md b/docs/designs/devcontainer-integration.md index d6a3e3d..13895e0 100644 --- a/docs/designs/devcontainer-integration.md +++ b/docs/designs/devcontainer-integration.md @@ -10,7 +10,7 @@ This document describes the design for integrating devcontainer support into Hea Headjack uses a layered architecture for container management: -1. **Runtime Interface** (`internal/container/container.go`): Abstracts container operations (Run, Exec, Stop, etc.) across Docker, Podman, and Apple Containerization +1. **Runtime Interface** (`internal/container/container.go`): Abstracts container operations (Run, Exec, Stop, etc.) across Docker and Podman 2. **Instance Manager** (`internal/instance/manager.go`): Orchestrates instance lifecycle, creating worktrees, containers, and managing sessions 3. **Image Labels**: Runtime configuration is extracted from OCI image labels (`io.headjack.init`, `io.headjack.podman.flags`, etc.) 4. **Configuration Merging**: Flags from config files take precedence over image labels @@ -83,9 +83,6 @@ The devcontainer CLI supports Docker and Podman via the `--docker-path` flag: |---------|-----------|-------| | Docker | ✅ | Native support | | Podman | ✅ | Via `--docker-path podman` | -| Apple Containerization | ❌ | Not Docker-compatible | - -Attempting to use devcontainer mode with Apple Containerization will result in an error. ### RunConfig Extension @@ -103,7 +100,7 @@ type RunConfig struct { } ``` -- Vanilla runtimes (Docker, Podman, Apple) ignore `WorkspaceFolder` +- Vanilla runtimes (Docker, Podman) ignore `WorkspaceFolder` - `DevcontainerRuntime` uses `WorkspaceFolder` and ignores `Image` ### DevcontainerRuntime Implementation @@ -237,11 +234,10 @@ This matches user expectations from VS Code, GitHub Codespaces, and DevPod. │ CLI Layer (cmd/run.go) │ │ │ │ │ ├── --image flag passed? │ -│ │ └── Yes: Use vanilla runtime (Docker/Podman/Apple) │ +│ │ └── Yes: Use vanilla runtime (Docker/Podman) │ │ │ │ │ └── devcontainer.json exists? │ -│ ├── Yes + Docker/Podman: Use DevcontainerRuntime │ -│ ├── Yes + Apple: Error (not supported) │ +│ ├── Yes: Use DevcontainerRuntime │ │ └── No: Use vanilla runtime with default image │ │ │ │ Instance Manager receives containerRuntime interface │ diff --git a/docs/docs/decisions/adr-002-apple-containerization.md b/docs/docs/decisions/adr-002-apple-containerization.md index adcb3b9..e04e535 100644 --- a/docs/docs/decisions/adr-002-apple-containerization.md +++ b/docs/docs/decisions/adr-002-apple-containerization.md @@ -86,14 +86,15 @@ Use **Apple Containerization Framework** as the isolation technology for Headjac - By adopting early, we participate in the framework's growth through usage and bug reports - The iptables-legacy workaround for Docker-in-Docker is stable but adds base image complexity -## Addendum: Multi-Runtime Support +## Addendum: Runtime Evolution -While Apple Containerization Framework remains the recommended runtime for its superior isolation properties, Headjack now supports multiple container runtimes to accommodate different user preferences and environments: +This ADR originally established Apple Containerization Framework as the isolation technology. After further development, Headjack evolved to support multiple runtimes and eventually removed Apple Containerization support in favor of Docker and Podman for cross-platform compatibility. + +Current supported runtimes: | Runtime | Configuration | Binary | Notes | |---------|--------------|--------|-------| | Docker | `runtime.name: docker` | `docker` | Default runtime. Cross-platform, widely available. | -| Apple | `runtime.name: apple` | `container` | Recommended for macOS 26+. VM-level isolation. | | Podman | `runtime.name: podman` | `podman` | Daemonless alternative. | Users can configure their preferred runtime via: @@ -101,5 +102,3 @@ Users can configure their preferred runtime via: ```bash hjk config runtime.name docker ``` - -This flexibility allows teams to use familiar tooling while still benefiting from Headjack's instance and session management. diff --git a/images/dind/Dockerfile b/images/dind/Dockerfile index 11adf60..5ff01d0 100644 --- a/images/dind/Dockerfile +++ b/images/dind/Dockerfile @@ -13,8 +13,8 @@ ARG DOCKER_GPG_FINGERPRINT=9DC858229FC7DD38854AE2D88D81803C0EBFCD88 ARG USERNAME=developer # ============================================================================= -# iptables-legacy Workaround (ADR-002) -# Required for Docker-in-Docker in Apple Containerization Framework +# iptables-legacy Workaround +# Required for Docker-in-Docker in certain container environments # ============================================================================= # hadolint ignore=DL3008 diff --git a/internal/cmd/run.go b/internal/cmd/run.go index b1527bc..b6ef93b 100644 --- a/internal/cmd/run.go +++ b/internal/cmd/run.go @@ -307,7 +307,6 @@ func getOrCreateInstance(cmd *cobra.Command, mgr *instance.Manager, repoPath, br // Devcontainer mode is used when: // - No --base flag was explicitly passed (imageExplicit is false) // - A devcontainer.json exists in the repo -// - The runtime is Docker or Podman (not Apple) func buildCreateConfig(cmd *cobra.Command, repoPath, branch, image string, imageExplicit bool) instance.CreateConfig { cfg := instance.CreateConfig{ Branch: branch, diff --git a/internal/container/common.go b/internal/container/common.go index 36dd41c..b4f10d9 100644 --- a/internal/container/common.go +++ b/internal/container/common.go @@ -25,7 +25,7 @@ type containerParser interface { } // baseRuntime provides shared functionality for container runtimes. -// Concrete implementations (Podman, Apple) configure this with runtime-specific settings +// Concrete implementations (Docker, Podman) configure this with runtime-specific settings // and provide a containerParser for JSON parsing. type baseRuntime struct { exec exec.Executor diff --git a/internal/container/container.go b/internal/container/container.go index 82804f4..483dd40 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -130,6 +130,6 @@ type Runtime interface { // ExecCommand returns the command prefix for executing commands in a container. // This is used by the multiplexer to build commands that run inside containers. - // For example, Apple returns ["container", "exec"] and Podman returns ["podman", "exec"]. + // For example, Docker returns ["docker", "exec"] and Podman returns ["podman", "exec"]. ExecCommand() []string } diff --git a/internal/instance/manager.go b/internal/instance/manager.go index 476692f..4837b6c 100644 --- a/internal/instance/manager.go +++ b/internal/instance/manager.go @@ -79,7 +79,7 @@ const ( type ManagerConfig struct { WorktreesDir string // Directory for storing worktrees (e.g., ~/.local/share/headjack/git) LogsDir string // Directory for storing logs (e.g., ~/.local/share/headjack/logs) - RuntimeType RuntimeType // Container runtime type (docker, podman, or apple) + RuntimeType RuntimeType // Container runtime type (docker or podman) ConfigFlags flags.Flags // Flags from config file (take precedence over image labels) Executor exec.Executor // Command executor (for devcontainer runtime creation) } diff --git a/justfile b/justfile index a04a762..c659df5 100644 --- a/justfile +++ b/justfile @@ -65,9 +65,7 @@ integration-test runtime="auto": # Determine runtime if [ "{{runtime}}" = "auto" ]; then - if command -v container &>/dev/null && [ "$(uname)" = "Darwin" ]; then - RUNTIME=apple - elif command -v docker &>/dev/null; then + if command -v docker &>/dev/null; then RUNTIME=docker elif command -v podman &>/dev/null; then RUNTIME=podman @@ -92,10 +90,6 @@ integration-test-docker: integration-test-podman: just integration-test podman -# Run integration tests with Apple Containerization (macOS only) -integration-test-apple: - just integration-test apple - # Run a specific integration test script by name integration-test-one script runtime="auto": #!/usr/bin/env bash @@ -104,9 +98,7 @@ integration-test-one script runtime="auto": go build -o hjk . if [ "{{runtime}}" = "auto" ]; then - if command -v container &>/dev/null && [ "$(uname)" = "Darwin" ]; then - RUNTIME=apple - elif command -v docker &>/dev/null; then + if command -v docker &>/dev/null; then RUNTIME=docker elif command -v podman &>/dev/null; then RUNTIME=podman From 1b631e227639f04d09df0af5aa25d611a81162bc Mon Sep 17 00:00:00 2001 From: Joshua Gilman Date: Sun, 4 Jan 2026 14:38:55 -0800 Subject: [PATCH 3/4] fix(container): update listArgs comment to remove Apple reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/container/common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/container/common.go b/internal/container/common.go index b4f10d9..9ee7f0f 100644 --- a/internal/container/common.go +++ b/internal/container/common.go @@ -31,7 +31,7 @@ type baseRuntime struct { exec exec.Executor binaryName string execCommand []string - listArgs []string // e.g., ["ps", "-a"] for Podman, ["list"] for Apple + listArgs []string // e.g., ["ps", "-a"] for Docker/Podman parser containerParser // Runtime-specific JSON parser } From a4b52a4a42637d2acf2064417038c41efc9d685c Mon Sep 17 00:00:00 2001 From: Joshua Gilman Date: Sun, 4 Jan 2026 14:41:37 -0800 Subject: [PATCH 4/4] chore: remove Apple reference from gocontainer --- .goreleaser.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index b90fbab..7bf216f 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -76,5 +76,5 @@ homebrew_casks: ids: - darwin caveats: | - Headjack requires a container runtime (Podman, Docker, or Apple Container). + Headjack requires a container runtime (Podman, Docker). Get started: hjk auth claude && hjk run feat/my-feature --agent claude "Your task"