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
84 changes: 78 additions & 6 deletions FORK_CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,84 @@ Additional upstream PRs of interest (not exhaustive):

Notes and suggested next steps:

- Convert each bullet above into a CHANGELOG section with short user-facing notes and example usage (e.g., how to use the new build target, how to set dns_search).
- Update CLI --help and README to document new/changed flags and behaviors (dnsSearch/dns_search, build.target, named-volume behavior, entrypoint handling).
- Where possible, link to the full upstream PR discussions for context (links provided above for the main PRs found).
- Upstream apple/container v0.10.0 already includes many of the core engine changes referenced above (notably: ClientContainer rework [#1139], runtime flag for create/run [#1109], --init and --init-image support [#1244, #937], container export/commit [#1172], support for multiple network plugins [#1151], build --pull [#844], named-volume auto-create warning [#1108], memory validation [#1208], and related CLI/output changes such as a --format option for system status [#1237]).

- Items present in this fork but NOT included in apple/container v0.10.0 (should be tracked or upstreamed):
- Remove RuntimeStatus type (commit: c509a2f)
- Fix incorrect waiting when container is already running (commit: 8a4e5bb)
- Remove unnecessary 30s timeout when container already started (commit: eeddb26)
- dnsSearch / dns_search support for service name resolution (commit: d509f8a)
- Multi-stage build target support (build.target) (commit: 02ca646)
- Debug output showing the exact container CLI command being executed (commit: 4968a86)
- Ensure --entrypoint is passed before image name in run (commit: 84201f9)
- Named-volume full-destination-path preservation and regression test (commits: b1badf8, 8edb8a9)
- Fork-specific CI/release workflow additions (commits: 3f20dbf, 98b7fc4, 1d284fb)

- Recommended actions:
1. Update this FORK_CHANGES.md and add a short CHANGELOG.md that clearly separates what was upstreamed in apple/container@0.10.0 and what remains unique to this fork.
2. Update README and CLI --help strings for fork-only features (dns_search, build.target, entrypoint behavior, named-volume handling) and add migration notes where appropriate.
3. For each fork-only item, decide whether to upstream as a PR against apple/container or keep it as a fork patch; open PRs for items that are broadly useful (dns_search, build.target, entrypoint fix, named-volume behavior).

TODOs:
- Create a detailed CHANGELOG.md entry describing user-facing changes and migration notes.
- Update README and CLI --help strings to reflect fork capabilities.
- Create a detailed CHANGELOG.md entry describing user-facing changes and migration notes, split into "Upstream in container@0.10.0" and "Fork-only changes".
- Update README and CLI --help strings to reflect fork capabilities and any CLI differences.
- Audit tests that depend on fork-only behavior and mark or adapt them for upstream compatibility.

(Generated by repo inspection and upstream PR search.)
(Generated by repository inspection against apple/container v0.10.0.)

---

Proposed features to target for the next Apple Containers release

Based on the active development in the apple/container main branch (post-0.9.0), several high-impact features are landing that the Container-Compose fork is uniquely positioned to capitalize on. To stay ahead of the next release, focus development and testing on the following areas.

### 1. Robust Service Lifecycle (Restart Policies)

The Change: PR #1258 adds a native `--restart` policy to the `container run` command.

- Compose Feature to Add: Implement the `restart: always`, `restart: on-failure`, and `restart: unless-stopped` keys in docker-compose.yaml so the fork maps those keys to the new engine `--restart` flag.
- Testing Priority: Test "zombie" container cleanup. Since the engine is adding native restart support, ensure that `container-compose down` correctly stops and removes containers that the engine might be trying to restart automatically.

### 2. High-Performance Host-Container File Transfer

The Change: PR #1190 introduces a native `container cp` command.

- Compose Feature to Add: Use this to implement a "Sync" or "Hot Reload" feature that programmatically moves files into a running service container as an alternative to bind mounts for improved performance.
- Testing Priority: Verify large file transfers and directory structures. This is a significant improvement over the current "mount-only" storage strategy in 0.9.0.

### 3. Native "Init" Process Management

The Change: PR #1244 adds an `--init` flag to `run/create`.

- Compose Feature to Add: Add an `init: true` boolean to the service definition that maps to the engine `--init` flag when starting containers.
- Testing Priority: Test applications that spawn many child processes (Node.js, Python with workers). Using the native `--init` flag will prevent orphan processes from remaining in the micro-VM after the service stops.

### 4. Advanced Networking & Multi-Plugin Support

The Change: PR #1151 and #1227 enable multiple network plugins and loading configurations from files.

- Compose Feature to Add: Support complex `networks:` definitions in Compose to allow combinations of bridge, host-only, and routed networks for services within the same stack.
- Testing Priority: IPv6 connectivity. PR #1174 adds IPv6 gateway support — validate IPv6 addressing, routing, and DNS resolution across custom networks.

### 5. "Snapshot-based" Deployments

The Change: PR #1172 adds `container commit` (exporting a container to an image).

- Compose Feature to Add: Implement a `container-compose checkpoint <service>` command that commits a running container to a local image for future `up` commands or for fast rollbacks.
- Testing Priority: Validate database checkpoints and restore flows; ensure image metadata and layers are handled consistently across commits.

### Suggested Testing Matrix for the Fork

| Feature | Target PR | Test Case |
| --- | --- | --- |
| **Persistence** | #1108 / #1190 | Verify that named volumes aren't "lost" and `cp` works across them. |
| **Security** | #1152 / #1166 | Ensure Compose-generated containers respect the new SELinux-off-by-default boot. |
| **Reliability** | #1208 | Launch a Compose stack with `mem_limit: 128mb` and verify the CLI surfaces validation errors correctly. |

### Strategic Recommendation

The most valuable addition would be **Auto-Start support**. With Apple adding `LaunchAgent` support (#1176) and a `--system-start` flag (#1201), the fork could introduce a `container-compose install-service` command that generates macOS LaunchAgents to auto-start stacks on boot.

---

Would you like help drafting the Swift logic to map `restart: always` and related Compose keys to the engine `--restart` flag? (Can produce a focused patch for Sources/Container-Compose/Commands/ComposeUp.swift.)
8 changes: 7 additions & 1 deletion Sources/Container-Compose/Codable Structs/Service.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ public struct Service: Codable, Hashable {
/// Platform architecture for the service
public let platform: String?

/// Native init flag to request an init process (maps to container --init)
public let `init`: Bool?

/// Service-specific config usage (primarily for Swarm)
public let configs: [ServiceConfig]?

Expand All @@ -107,7 +110,7 @@ public struct Service: Codable, Hashable {
// Defines custom coding keys to map YAML keys to Swift properties
enum CodingKeys: String, CodingKey {
case image, build, deploy, restart, healthcheck, volumes, environment, env_file, ports, command, depends_on, user,
container_name, networks, hostname, entrypoint, privileged, read_only, working_dir, configs, secrets, stdin_open, tty, platform, dns_search
container_name, networks, hostname, entrypoint, privileged, read_only, working_dir, configs, secrets, stdin_open, tty, platform, `init`, dns_search
}

/// Public memberwise initializer for testing
Expand Down Expand Up @@ -223,6 +226,9 @@ public struct Service: Codable, Hashable {
stdin_open = try container.decodeIfPresent(Bool.self, forKey: .stdin_open)
tty = try container.decodeIfPresent(Bool.self, forKey: .tty)
platform = try container.decodeIfPresent(String.self, forKey: .platform)
// Decode optional init flag (YAML key: init)
`init` = try container.decodeIfPresent(Bool.self, forKey: .`init`)

dns_search = try container.decodeIfPresent(String.self, forKey: .dns_search)
}

Expand Down
42 changes: 42 additions & 0 deletions Sources/Container-Compose/Commands/CheckpointCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import ArgumentParser
import Foundation
import ContainerizationExtras
import ContainerAPIClient

public struct CheckpointCommand: AsyncParsableCommand {
public init() {}

public static let configuration = CommandConfiguration(commandName: "checkpoint", abstract: "Commit/export a running service container to an image")

@Argument(help: "Service name to checkpoint")
var service: String

@Option(name: .long, help: "Image tag to use for the checkpointed image")
var tag: String?

public mutating func run() async throws {
let project = deriveProjectName(cwd: FileManager.default.currentDirectoryPath)
let containerName = "\(project)-\(service)"
let imageTag: String
if let t = tag {
imageTag = t
} else {
let ts = Int(Date().timeIntervalSince1970)
imageTag = "\(project)-\(service):checkpoint-\(ts)"
}

let args = Self.makeCommitArgs(containerName: containerName, imageName: imageTag)

print("Executing: container \(args.joined(separator: " "))")
_ = try await streamCommand("container", args: args, onStdout: { print($0) }, onStderr: { print($0) })
print("Checkpointed \(containerName) -> \(imageTag)")
}

// Builds the CLI args to pass to `container` for committing/exporting a container to an image.
public static func makeCommitArgs(containerName: String, imageName: String) -> [String] {
// Upstream container supports `commit` or `export` depending on version; prefer 'commit' if available.
// Construct: ["commit", "<container>", "--output", "<image>"] or ["export", "<container>", "--tag", "<image>"]
// For broad compatibility, use `commit` followed by container and image tag as arguments.
return ["commit", containerName, imageName]
}
}
55 changes: 55 additions & 0 deletions Sources/Container-Compose/Commands/ComposeUp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import ArgumentParser
import ContainerCommands
//import ContainerClient
import ContainerAPIClient
import ContainerCommands
import ContainerizationExtras
import Foundation
@preconcurrency import Rainbow
Expand Down Expand Up @@ -744,6 +745,60 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable {
}

// MARK: CommandLine Functions

/// Helper for building the `container run` argument list for a service. Used by tests.
public static func makeRunArgs(service: Service, serviceName: String, dockerCompose: DockerCompose, projectName: String, detach: Bool, cwd: String, environmentVariables: [String: String]) throws -> [String] {
var runArgs: [String] = []

// Add detach flag if specified
if detach {
runArgs.append("-d")
}

// Determine container name
let containerName: String
if let explicit = service.container_name {
containerName = explicit
} else {
containerName = "\(projectName)-\(serviceName)"
}
runArgs.append("--name")
runArgs.append(containerName)

// Map restart policy if present
if let restart = service.restart {
runArgs.append("--restart")
runArgs.append(restart)
}

// Map init flag if present (support both explicit Bool and optional presence)
// Note: Service may not include an `init` field; this helper will check for a computed property on Service via KeyedDecoding.
if let mirrorInit = Mirror(reflecting: service).children.first(where: { $0.label == "init" }), let value = mirrorInit.value as? Bool, value {
runArgs.append("--init")
}

// Ensure entrypoint flag is placed before the image name when provided
let imageToRun = service.image ?? "\(serviceName):latest"
if let entrypointParts = service.entrypoint, let entrypointCmd = entrypointParts.first {
runArgs.append("--entrypoint")
runArgs.append(entrypointCmd)
// image follows flags
runArgs.append(imageToRun)
// append any remaining entrypoint args or command after image
if entrypointParts.count > 1 {
runArgs.append(contentsOf: entrypointParts.dropFirst())
} else if let commandParts = service.command {
runArgs.append(contentsOf: commandParts)
}
} else {
runArgs.append(imageToRun)
if let commandParts = service.command {
runArgs.append(contentsOf: commandParts)
}
}

return runArgs
}
extension ComposeUp {

/// Runs a command, streams stdout and stderr via closures, and completes when the process exits.
Expand Down
27 changes: 27 additions & 0 deletions Tests/Container-Compose-StaticTests/CheckpointCommandTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import XCTest
@testable import ContainerComposeCore

final class CheckpointCommandTests: XCTestCase {
func testMakeCommitArgsIncludesContainerAndImage() throws {
let container = "proj-web"
let image = "proj-web:checkpoint-12345"

// TDD: helper to be implemented that builds container commit/export args
let args = CheckpointCommand.makeCommitArgs(containerName: container, imageName: image)

// Accept either 'commit' or 'export' as the upstream CLI made use of one of these verbs
XCTAssertTrue(args.contains("commit") || args.contains("export"), "Expected commit or export verb in args: \(args)")
XCTAssertTrue(args.contains(container), "Expected container name present in args: \(args)")
XCTAssertTrue(args.contains(image), "Expected image name present in args: \(args)")
}

func testMakeCommitArgsAcceptsCustomTag() throws {
let container = "proj-api"
let customImage = "myregistry.local/proj-api:ckpt-1"

let args = CheckpointCommand.makeCommitArgs(containerName: container, imageName: customImage)

XCTAssertTrue(args.contains(container))
XCTAssertTrue(args.contains(customImage))
}
}
56 changes: 56 additions & 0 deletions Tests/Container-Compose-StaticTests/ComposeUpMappingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import XCTest
@testable import ContainerComposeCore
import Yams

final class ComposeUpMappingTests: XCTestCase {
func testRestartPolicyMapping() throws {
let yaml = """
services:
web:
image: nginx:latest
restart: always
"""
let dockerCompose = try YAMLDecoder().decode(DockerCompose.self, from: yaml)
guard let service = dockerCompose.services["web"] ?? nil else { return XCTFail("Service 'web' missing") }

// Expected: a helper that builds run args from a service. Tests written first (TDD).
let args = try ComposeUp.makeRunArgs(service: service, serviceName: "web", dockerCompose: dockerCompose, projectName: "proj", detach: false, cwd: "/tmp", environmentVariables: [:])

XCTAssertTrue(args.contains("--restart"), "Expected --restart flag present in args: \(args)")
XCTAssertTrue(args.contains("always"), "Expected restart value 'always' present in args: \(args)")
}

func testInitFlagMapping() throws {
let yaml = """
services:
app:
image: busybox:latest
init: true
"""
let dockerCompose = try YAMLDecoder().decode(DockerCompose.self, from: yaml)
guard let service = dockerCompose.services["app"] ?? nil else { return XCTFail("Service 'app' missing") }

let args = try ComposeUp.makeRunArgs(service: service, serviceName: "app", dockerCompose: dockerCompose, projectName: "proj", detach: false, cwd: "/tmp", environmentVariables: [:])

XCTAssertTrue(args.contains("--init"), "Expected --init flag present in args: \(args)")
}

func testEntrypointPlacedBeforeImage() throws {
let yaml = """
services:
api:
image: nginx:latest
entrypoint: ["/bin/sh", "-c"]
"""
let dockerCompose = try YAMLDecoder().decode(DockerCompose.self, from: yaml)
guard let service = dockerCompose.services["api"] ?? nil else { return XCTFail("Service 'api' missing") }

let args = try ComposeUp.makeRunArgs(service: service, serviceName: "api", dockerCompose: dockerCompose, projectName: "proj", detach: false, cwd: "/tmp", environmentVariables: [:])

guard let entryIdx = args.firstIndex(of: "--entrypoint"), let imageIdx = args.firstIndex(of: "nginx:latest") else {
return XCTFail("Expected both --entrypoint and image in args: \(args)")
}

XCTAssertTrue(entryIdx < imageIdx, "Expected --entrypoint to appear before image, but args: \(args)")
}
}
10 changes: 10 additions & 0 deletions docs/FORK_README_UPDATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Fork README additions (draft)

Planned changes to leverage apple/container v0.10.0 features:

- Map Compose `restart:` keys to engine `--restart` flag.
- Map `init: true` to engine `--init` flag and support `--init-image` selection.
- Ensure `--entrypoint` is passed in the correct position relative to the image name.
- Add a new `checkpoint` subcommand that uses `container commit`/export.

Tests were added (ComposeUpMappingTests) to drive the implementation of the first set of changes.
28 changes: 28 additions & 0 deletions docs/checkpoint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# checkpoint: export/commit service

This document describes the new `container-compose checkpoint <service>` command planned for the fork.

Goal

- Provide a simple way to commit/export a running service container into a local image for fast rollbacks, CI snapshots, or distributing a running container's state.

Behavior

- The command maps a service name to the running container name using the project naming convention (e.g., `<project>-<service>`).
- It invokes the underlying `container` engine command to export/commit the container to an image (upstream apple/container provides export/commit support in v0.10.0, PR #1172).
- The resulting image tag will default to `<project>-<service>:checkpoint-<timestamp>` unless explicitly provided via `--tag`.

Examples

- Export the running `web` service to an image with an auto-generated tag:

container-compose checkpoint web

- Export with a specific tag:

container-compose checkpoint web --tag myregistry.local/myproj/web:checkpoint-1

Notes

- This feature depends on apple/container@0.10.0 or later (PR #1172) which exposes commit/export functionality.
- Implementation details will be TDD-driven; tests in Tests/Container-Compose-StaticTests/CheckpointCommandTests.swift assert the command constructs the expected `container` CLI invocation.
Loading