Skip to content

feat(api): surface lastExitCode on ContainerSnapshot#1503

Draft
chrisgeo wants to merge 1 commit intoapple:mainfrom
full-chaos:feat/chaos-1320-last-exit-code
Draft

feat(api): surface lastExitCode on ContainerSnapshot#1503
chrisgeo wants to merge 1 commit intoapple:mainfrom
full-chaos:feat/chaos-1320-last-exit-code

Conversation

@chrisgeo
Copy link
Copy Markdown

@chrisgeo chrisgeo commented May 2, 2026

Companion issue: #1501

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Motivation and Context

ContainerSnapshot today exposes RuntimeStatus.stopped but not the underlying exit code. External orchestrators (e.g. a Compose-spec orchestrator implementing depends_on: condition: service_completed_successfully) need to distinguish clean exit (exit code 0) from failed exit (non-zero) of a one-shot container. Without this, they have to treat .stopped as success and silently misinterpret non-zero exits.

The data already exists internally: ContainersService.handleContainerExit(id:code:context:) receives a non-nil ExitStatus? from the existing ExitMonitor callback wiring whenever a container actually exits via the runtime. This PR just stamps that value onto the snapshot.

See #1501 for the full motivation and design notes.

What this PR changes

  • Sources/ContainerResource/Container/ContainerSnapshot.swift: new optional lastExitCode: Int32? field with a defaulted init parameter so all existing construction sites compile unchanged.
  • Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift: one assignment in the terminal-state block of handleContainerExit, taking exitCode from the existing code: ExitStatus? parameter.

Total: 2 files, +8/-1.

Wire compatibility

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

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

Scope (deliberately minimal)

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

Testing

  • Tested locally (full swift build clean on macOS 26 / Apple silicon, including downstream targets that consume ContainerSnapshot).
  • Added/updated tests — no existing ContainerSnapshotTests in Tests/ContainerResourceTests/. Happy to add a Codable round-trip test if maintainers prefer; opened as draft to confirm scope first.
  • Added/updated docs — public API doc comments added on the new field; no separate docs page to update.

Status

Draft. Filed alongside #1501. Marked draft until a maintainer confirms the proposed surface is acceptable; will mark ready and add tests on request.

Adds an optional lastExitCode: Int32? field to ContainerSnapshot,
populated in ContainersService.handleContainerExit when the container
transitions to .stopped. The exit code is taken from the existing
ExitMonitor callback's ExitStatus value.

Motivation
----------

External orchestrators that drive the API server (Docker-Compose-style
service ordering is the canonical use case) need to distinguish between
'container exited cleanly' and 'container exited but the host gave up
on it'. The compose-spec depends_on: condition: service_completed_successfully
gate is exactly this distinction. Today, ContainerSnapshot exposes
.stopped without an exit code, so consumers can only fall back to a
stopped-on-time check and silently misinterpret a non-zero exit as
success.

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 (which the doc comment already documents as 'never exited
    or exit not captured').

Scope
-----

In-memory only. The exit code lives in the in-memory ContainerState
snapshot for the duration of apiserver uptime. A daemon restart resets
all snapshots to .stopped without exit codes (existing behavior). A
follow-up patch can add bundle persistence (exit_status.json) if the
daemon-restart case becomes operationally relevant.

Files
-----

- Sources/ContainerResource/Container/ContainerSnapshot.swift: new
  field + init parameter (default nil for backward compat at all
  existing construction sites; SandboxService.swift's snapshot
  constructor is unchanged).
- Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift:
  one assignment in handleContainerExit's terminal state block, taking
  exitCode from the existing 'code: ExitStatus?' parameter.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants