Add container-compose plugin#3
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2931d8d78c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let imageName = service.effectiveImageName(projectName: project.name) | ||
| try await ensureImageAvailable(serviceName: serviceName, service: service, imageName: imageName, policy: pullPolicy) | ||
|
|
||
| // Create and start new container | ||
| try await createAndStartNewContainer( |
There was a problem hiding this comment.
Skip creation when reusing an existing container
handleExistingContainer() can decide to reuse the runtime container for both --no-recreate and the "config unchanged" path, but createAndStartContainer() always falls through into createAndStartNewContainer() anyway. In practice, a second container compose up (or any run with --no-recreate) still tries to create the same container ID again, so reuse never works and the command will fail instead of leaving the existing service running.
Useful? React with 👍 / 👎.
| // Add default network if no networks specified | ||
| if networks.isEmpty { | ||
| networks["default"] = Network(name: "default", driver: "bridge", external: false) |
There was a problem hiding this comment.
Preserve the runtime default network when Compose omits networks
Synthesizing a project-level default network here turns every compose file without an explicit networks: section into a custom-network project. up() then hits ensureComposeNetworks()/mapServiceNetworkIds(), both of which reject non-default networking on macOS versions earlier than 26, so even a minimal file like services: {web: {image: alpine}} now fails on supported macOS 15 instead of using ClientNetwork.defaultNetworkName.
Useful? React with 👍 / 👎.
| // Wait for process completion | ||
| let result = try await process.wait() |
There was a problem hiding this comment.
Apply health-check timeouts to probe processes
HealthCheck.timeout is parsed into the model, but the runner ignores it and waits indefinitely for process.wait(). Any probe that hangs instead of exiting—e.g. a curl to a blackholed address—will block depends_on.condition: service_healthy, compose up --wait, and compose health forever rather than failing the check after the configured timeout.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ed178323f1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| process: ProcessConfiguration( | ||
| executable: service.command?.first ?? "/bin/sh", | ||
| arguments: Array((service.command ?? ["/bin/sh", "-c"]).dropFirst()), | ||
| environment: service.environment.map { "\($0.key)=\($0.value)" }, | ||
| workingDirectory: service.workingDir ?? "/" |
There was a problem hiding this comment.
Hash reused containers from the real image process config
handleExistingContainer() derives expectedHash from service.command/workingDir only, while container creation later resolves image ENTRYPOINT, CMD, and image WORKDIR before writing the com.apple.container.compose.config-hash label. For any service that relies on image defaults (for example image: postgres with no command:), the stored hash and the next-run hash can never match, so the "config unchanged" path recreates the container on every compose up even when nothing changed.
Useful? React with 👍 / 👎.
| if let configuredImage = service.image, | ||
| (try? await ClientImage.get(reference: configuredImage)) != nil | ||
| { | ||
| log.info("Using existing image '\(imageName)' for service '\(serviceName)'") | ||
| buildCache[cacheKey] = imageName |
There was a problem hiding this comment.
Still build services that declare both
build: and image:
This short-circuits as soon as the tagged image already exists locally, so a service like the new build + image test case will skip its Dockerfile build and run whatever is already tagged under image: instead. In the common case where that tag is a pulled base image (for example node:18-alpine), compose up starts the wrong filesystem and command instead of the application image built from the compose source.
Useful? React with 👍 / 👎.
| return Volume( | ||
| name: name, | ||
| driver: volume.driver ?? "local", | ||
| external: external |
There was a problem hiding this comment.
Preserve top-level volume
name: when resolving mounts
ComposeVolume.name is parsed but discarded here, so later ensureVolume()/down --volumes operate on the logical key (data) instead of the actual resource name (prod-data, shared-data, etc.). Any compose file that aliases a named or external volume will either fail to find the existing volume or create/delete the wrong one.
Useful? React with 👍 / 👎.
| if let cpus = service.cpus { | ||
| config.resources.cpus = Int(cpus) ?? 4 |
There was a problem hiding this comment.
Don't silently turn non-integer CPU limits into 4 cores
Compose CPU limits are strings and commonly use fractional values like "0.5"; here every non-integer falls through to the default of 4. That means a service asking for half a core, or even a typo in cpus, gets the platform default allocation instead of an error or an intentional conversion, which can materially over-provision workloads.
Useful? React with 👍 / 👎.
Problem
This branch started from a practical goal: run a real Docker Compose development stack on top of Apple's
containertooling without asking the application repository to fork its compose files or invent container-specific workflow glue.The immediate workload that drove the implementation was
resq-fullstack, which relies on normal Compose features such as multi-service orchestration,depends_on, health checks,env_file, profile selection, bind mounts, named volumes,extra_hosts, and image build-or-reuse behavior.containerdid not have a Compose-compatible plugin for that workflow, so bringing up a realistic stack required a new plugin layer that can translate Compose concepts into the existing container runtime model.Scope
This PR adds a first-party
container composeplugin that implements the Compose-style orchestration layer inPlugins/container-compose.It is stacked on top of #1 because Compose
extra_hostsneeds the corehostsplumbing introduced there.What this changes
Plugins/container-composepackage, plugin config, CLI entrypoints, and debug executableextra_hosts, health checks, dependency ordering, project naming/path resolution, volume copy-up behavior, and image reuse in the plugin layerhost-gatewaysentinel in the plugin before populating coreconfig.hostsentrypoint: ''behavior6 GiBwhen Compose does not specify a memory limit, which keeps theresq-fullstackfrontend alive on this runtimeValidation
swift test --package-path Plugins/container-composeswift test --package-path Plugins/container-compose --filter ExtraHostsTests/testResolvedComposeHostAddressMapsHostGatewaySentinel