Skip to content
This repository was archived by the owner on Apr 15, 2026. It is now read-only.
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
261 changes: 67 additions & 194 deletions internal/container/apple.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package container

import (
"context"
"encoding/json"
"fmt"
"strings"
"time"

"github.com/jmgilman/headjack/internal/exec"
Expand All @@ -16,154 +14,103 @@ type AppleConfig struct {
// 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,
}
}

func (r *appleRuntime) Run(ctx context.Context, cfg *RunConfig) (*Container, error) {
args := buildRunArgs(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"`
}

result, err := r.exec.Run(ctx, &exec.RunOptions{
Name: r.binaryName,
Args: args,
})
if err != nil {
stderr := string(result.Stderr)
if isAlreadyExistsError(stderr) {
return nil, ErrAlreadyExists
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
}
return nil, cliError("run container", result, err)
// If parsing fails, createdAt remains zero value
}

// Container ID is returned on stdout
containerID := strings.TrimSpace(string(result.Stdout))

return &Container{
ID: containerID,
Name: cfg.Name,
Image: cfg.Image,
Status: StatusRunning,
CreatedAt: time.Now(),
}, nil
}

func (r *appleRuntime) Exec(ctx context.Context, id string, cfg ExecConfig) error {
// Verify container exists and is running
container, err := r.Get(ctx, id)
if err != nil {
return err
}
if container.Status != StatusRunning {
return ErrNotRunning
}

args := buildExecArgs(id, cfg)

if cfg.Interactive {
return r.execInteractive(ctx, args)
}

result, err := r.exec.Run(ctx, &exec.RunOptions{
Name: r.binaryName,
Args: args,
})
if err != nil {
return cliError("exec in container", result, err)
}

return nil
}

func (r *appleRuntime) Stop(ctx context.Context, id string) error {
// Verify container exists
c, err := r.Get(ctx, id)
if err != nil {
return err
}

// No-op if already stopped
if c.Status == StatusStopped {
return nil
ID: c.Configuration.ID,
Name: c.Configuration.ID,
Image: c.Configuration.Image.Reference,
Status: parseContainerStatus(c.Status),
CreatedAt: createdAt,
}

result, err := r.exec.Run(ctx, &exec.RunOptions{
Name: r.binaryName,
Args: []string{"stop", id},
})
if err != nil {
return cliError("stop container", result, err)
}

return nil
}

func (r *appleRuntime) Start(ctx context.Context, id string) error {
// Verify container exists
c, err := r.Get(ctx, id)
if err != nil {
return err
}

// No-op if already running
if c.Status == StatusRunning {
return nil
}

result, err := r.exec.Run(ctx, &exec.RunOptions{
Name: r.binaryName,
Args: []string{"start", id},
})
if err != nil {
return cliError("start container", result, err)
}

return nil
// 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 (r *appleRuntime) Remove(ctx context.Context, id string) error {
result, err := r.exec.Run(ctx, &exec.RunOptions{
Name: r.binaryName,
Args: []string{"rm", id},
})
if err != nil {
stderr := string(result.Stderr)
if isNotFoundError(stderr) {
return ErrNotFound
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 cliError("remove container", result, err)
}

return nil
}

func (r *appleRuntime) Get(ctx context.Context, id string) (*Container, error) {
result, err := r.exec.Run(ctx, &exec.RunOptions{
Name: r.binaryName,
Args: []string{"inspect", id},
})
if err != nil {
stderr := string(result.Stderr)
if isNotFoundError(stderr) {
return nil, ErrNotFound
}
return nil, cliError("inspect container", result, err)
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(result.Stdout, &infos); err != nil {
if err := json.Unmarshal(data, &infos); err != nil {
return nil, fmt.Errorf("parse container info: %w", err)
}

Expand All @@ -174,29 +121,10 @@ func (r *appleRuntime) Get(ctx context.Context, id string) (*Container, error) {
return infos[0].toContainer(), nil
}

func (r *appleRuntime) List(ctx context.Context, filter ListFilter) ([]Container, error) {
args := []string{"list", "--format", "json"}

if filter.Name != "" {
args = append(args, "--filter", "name="+filter.Name)
}

result, err := r.exec.Run(ctx, &exec.RunOptions{
Name: r.binaryName,
Args: args,
})
if err != nil {
return nil, cliError("list containers", result, err)
}

// Handle empty list
stdout := strings.TrimSpace(string(result.Stdout))
if stdout == "" || stdout == "[]" {
return []Container{}, nil
}

// parseList parses the JSON output of `container list`.
func (p *appleParser) parseList(data []byte) ([]Container, error) {
var items []appleListItem
if err := json.Unmarshal(result.Stdout, &items); err != nil {
if err := json.Unmarshal(data, &items); err != nil {
return nil, fmt.Errorf("parse container list: %w", err)
}

Expand All @@ -207,58 +135,3 @@ func (r *appleRuntime) List(ctx context.Context, filter ListFilter) ([]Container

return containers, nil
}

func (r *appleRuntime) Build(ctx context.Context, cfg *BuildConfig) error {
args := buildBuildArgs(cfg)

result, err := r.exec.Run(ctx, &exec.RunOptions{
Name: r.binaryName,
Args: args,
})
if err != nil {
return fmt.Errorf("%w: %s", ErrBuildFailed, strings.TrimSpace(string(result.Stderr)))
}

return nil
}

// appleInspect represents the JSON output of `container inspect`.
type appleInspect struct {
Status string `json:"status"`
Configuration struct {
ID string `json:"id"`
Image struct {
Reference string `json:"reference"`
} `json:"image"`
} `json:"configuration"`
}

func (c *appleInspect) toContainer() *Container {
return &Container{
ID: c.Configuration.ID,
Name: c.Configuration.ID,
Image: c.Configuration.Image.Reference,
Status: parseContainerStatus(c.Status),
}
}

// appleListItem represents a single item in `container list` JSON output.
// Note: Apple container list has same format as inspect.
type appleListItem struct {
Status string `json:"status"`
Configuration struct {
ID string `json:"id"`
Image struct {
Reference string `json:"reference"`
} `json:"image"`
} `json:"configuration"`
}

func (c *appleListItem) toContainer() Container {
return Container{
ID: c.Configuration.ID,
Name: c.Configuration.ID,
Image: c.Configuration.Image.Reference,
Status: parseContainerStatus(c.Status),
}
}
Loading