Skip to content

[Request]: Surface lastExitCode on ContainerSnapshot #1501

@chrisgeo

Description

@chrisgeo

Suggested triage: type=Feature, label=enhancement. (External contributor — cannot self-apply.)

Feature or enhancement request details

Summary

Add an optional lastExitCode: Int32? field to ContainerSnapshot, populated from the existing ExitMonitor callback when a container transitions to .stopped.

A draft PR with the implementation is open at #1503.

Motivation

External orchestrators that drive the API server (the canonical use case is a Compose-spec orchestrator implementing depends_on: condition: service_completed_successfully) need to distinguish between clean exit (exit code 0) and failed exit (non-zero exit code) of a one-shot container.

Today ContainerSnapshot exposes RuntimeStatus.stopped, but the underlying exit code is not surfaced. The orchestrator can observe "the container stopped" but not "the container stopped successfully". This forces consumers to treat .stopped as a success, silently misinterpreting non-zero exits.

The same data already exists in the daemon's path: ContainersService.handleContainerExit(id:code:context:) receives code: ExitStatus? from the existing ExitMonitor callback wiring. The proposed change just stamps that value onto the snapshot.

Proposed change

// Sources/ContainerResource/Container/ContainerSnapshot.swift
public var lastExitCode: Int32?

public init(
    configuration: ContainerConfiguration,
    status: RuntimeStatus,
    networks: [Attachment],
    startedDate: Date? = nil,
    lastExitCode: Int32? = nil
) { ... }
// Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift
// In handleContainerExit, terminal-state block:
state.snapshot.status = .stopped
state.snapshot.lastExitCode = code?.exitCode   // NEW
state.snapshot.networks = []

Wire compatibility

ContainerSnapshot is marshaled as Codable JSON over XPC. Adding an optional field is forward-compatible:

  • Older clients reading from a newer server: ignore the new key.
  • Newer clients reading from an older server: decode lastExitCode as nil (the documented "never exited or exit not captured" case).

Scope

Intentionally minimal — in-memory only. The exit code lives in the in-memory ContainerState snapshot for the duration of API server uptime. A daemon restart resets all snapshots to .stopped without exit codes (existing behavior). Bundle persistence for survival across daemon restarts (e.g. an exit_status.json) is a deliberate out-of-scope follow-up.

Use case context

This is a straightforward upstream-ing of a small change we have been carrying in a downstream fork to power Compose-style orchestration on top of apple/container (container-compose). We would much prefer to consume the field directly from apple/container's ContainerSnapshot rather than maintain a fork. We have existing internal coverage of this change against a real apple/container daemon; happy to share notes.

Code of Conduct

I agree to follow this project's Code of Conduct.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions