jaco apply

Apply a jaco.yaml + compose pair to the cluster. The leader validates both files, replicates the new Deployment revision through raft, and the scheduler converges replicas to match.

Synopsis

jaco apply <jaco.yaml> [--compose <path>] [--dry-run]
           [--server <host:port> --token <op>]
           [--ca-cert <path>] [--socket <path>]

Flags

flagdefaultmeaning
--server <addr>leader gRPC; omit to use the local socket
--token <op>JACO_TOKENoperator bearer token (with --server)
--ca-cert <path>/var/lib/jaco/node/ca.crtcluster CA PEM
--socket <path>/var/run/jaco/jaco.socklocal jacod unix socket
--compose <path>auto-detectpath to the compose file
--dry-runfalseprint the diff and exit without applying

When --compose is unset, the CLI looks for compose.yml then compose.yaml in the same directory as <jaco.yaml>. If neither exists, apply fails with no compose file found next to <jaco.yaml>; pass --compose explicitly.

Auth

Operator token (TCP) or unix-socket trust (local).

Behavior

  1. The CLI reads both files from disk.
    1. If jaco.yaml declares a top-level environment: <path>, the CLI loads the named env file (path resolved relative to the jaco.yaml's directory) and substitutes every ${VAR} in the compose document — $VAR, ${VAR}, ${VAR:-default}, ${VAR:?msg}, $$ escape. The interpolation source is the env file only; the operator's process environment is NOT consulted. A missing env file fails the apply with load environment file <path>: …; a required ${VAR:?msg} reference whose variable is absent fails with interpolate ${VAR} at line N col M: required variable ….
    2. The CLI folds every service-level env_file: into the service's environment: map (compose-spec precedence: the explicit environment: value wins for matching keys; see Supported compose fields → env_file resolution).
    3. The fully-resolved compose bytes plus the original jaco.yaml bytes go to the daemon via Deploy.Apply. The daemon never reads the operator's filesystem.
  2. The daemon validates the jaco.yaml against the closed schema (deployment, services, routes; see manifests/jaco-yaml.md) and the compose file against the supported-field allowlist (see manifests/compose.md). Unknown fields, unknown services, unknown hosts, unknown networks, a cross-deployment collision, attempts to publish a reserved port (80/443), and two services in the same deployment publishing the same host port (port_conflict) reject the apply with a typed error and no state changes.
  3. Cross-check: every services[*].name in the jaco.yaml must match a key in the compose file. A route targeting a service that sets network_mode: none or network_mode: service:<name> is rejected — those services have no reachable listener of their own, so the route would publish a dead upstream.
  4. Privileged admission (issue #119). A compose service that sets privileged: true or a non-empty security_opt: list requires: the calling operator token has allows_privileged=true (see jaco token issue --allow-privileged), AND the service carries labels: { "jaco.io/allow-privileged": "true" }. A missing token flag rejects with PermissionDenied naming the first offending service; a missing label rejects in step 2 with validation_failed. Admitted privileged workloads write one privileged_workload_admitted audit event per gated service.
  5. The leader writes a new Deployment{applied_revision: N+1} through raft. The scheduler reconciles ReplicaDesired and the runtime converges containers.
  6. The RPC returns Applied revision: <N+1> once the leader has committed the new revision. Container start + health is observed asynchronously; jaco status -w shows replicas moving through pending → pulling → running.

With --dry-run the apply returns the Diff (adds, updates, removes) without committing. Privileged admission still runs under dry-run so the diff reflects what the live apply would decide. The diff itself currently surfaces as No changes on a no-op apply; richer per-entity diffs are tracked separately.

Exit codes

  • 0 — apply succeeded, or --dry-run returned a diff.
  • 1validation_failed, unknown_service, unknown_host, unknown_network, reserved_port, port_conflict, cannot place N replicas on M pinned hosts, PermissionDenied (privileged admission), quorum_lost, no_leader, or any auth / transport error.

See Status and errors for the closed code set.

Examples

End-to-end apply with the manifest pair side-by-side:

export JACO_TOKEN=<operator_token>
export LEADER=node-1:7000
jaco apply --server $LEADER ./hello/jaco.yaml
# Applied revision: 1

Dry-run on the local daemon, using an explicit compose path:

sudo jaco apply --compose ./hello/services.yml --dry-run ./hello/jaco.yaml
# No changes

Re-applying with a bumped image rolls one replica at a time:

# edit ./hello/docker-compose.yml: image: nginx:1.28
jaco apply --server $LEADER ./hello/jaco.yaml
jaco status --server $LEADER hello -w        # observe the rollout

If the apply rejects, the cluster state is unchanged. Re-edit and try again — there is no partial-apply state to clean up.

See also