Code Map

CCM is a declarative configuration management engine written in Go. A manifest or a command names resources and the desired state for each; the engine drives every resource to that state and reports what changed. This code map is a deep-dive into how the Go code is built, for contributors and for anyone who wants to understand the machine behind the manifests.

Snapshot

Generated 2026-07-10 against commit d1b8674 on branch main. The working tree was clean at capture time. Commits after this one may make parts of this map stale.

The mental model

CCM has one core and many faces. The core is a small loop: a Manager holds the run’s facts, data, and session; a Resource decides whether the system already matches the desired state; a Provider makes the platform-specific change when it does not. Every entry point drives that same loop. The CLI applies one resource. The apply engine walks a manifest of many. The background agent applies manifests on a timer. A piped wire API applies a resource sent as JSON. The logic underneath is identical, so behavior does not drift between how a resource is invoked.

Providers are selected at run time. A resource type declares a capability interface, and each provider registers a factory that reports whether it can manage the resource on this host. The registry picks the best match from the gathered facts, so the same manifest runs apt on Debian and dnf on RHEL with no change to the resource.

Entry pointsCLI · ensureManifest · applyAgentWire APIManagerfacts · data · sessionFacts · DataHiera · TemplatesResource lifecycleResourcedecides desired vs actualProvideracts on the platformSystempackages · files · servicesregistryselects byfacts
One core loop, many faces. Every entry point drives the same manager, resource, and provider machinery.

Explore

  • Architecture

    Package layering and the three seams that keep the engine composable: the model contracts, the provider registry, and the manager.

  • The Resource-Provider Model

    How a concrete resource type is built, and how one resource is driven to its desired state: decide, act, and verify.

  • The Apply Engine

    How a manifest of many resources is parsed, resolved against data, and executed in order, with require as a gate and events as the shared state.

  • The Agent

    The background loop that applies manifests on a timer, watches remote sources, refreshes facts and data, and remediates failing health checks.

  • Data, Facts, and Templates

    How facts are gathered, how Hiera layers a hierarchy into resolved data, and how templates render that data into resource fields.

  • Registration and Discovery

    How stable resources announce themselves into a JetStream store keyed by subject, and how other nodes look them up or watch them live.

  • Observability

    How each apply becomes a recorded event, a Prometheus counter, and a session summary, and how health checks feed remediation.

  • Reference and Map

    The CLI command surface, a source map of every package, the key types and where they are explained, and a glossary. {class=“children children-type-tree children-sort-”}

Next

Start with the Architecture page for the package layering and the contracts every subsystem programs against.

Subsections of Code Map

Architecture

CCM is built around a strict dependency inversion. The model package defines contracts and almost no behavior. Concrete resource types and providers depend on those contracts and on a global registry, never on the orchestrator. The manager package is the orchestrator that satisfies the central contract and wires in facts, data, sessions, NATS, and logging. That inversion is why a provider can self-register without the manager knowing it exists, and why the same resource logic runs from the CLI, a manifest, or the agent.

Where it lives

model: the contracts and shared structs. internal/registry: the global provider directory. manager: the concrete orchestrator, type CCM. Key files: model/resource.go, model/provider.go, model/manager.go, model/transaction.go, internal/registry/registry.go, manager/manager.go, manager/opts.go.

The layers

Read the stack from the outside in. Entry points build a manager and hand it work. The apply engine turns a manifest into resources. Each resource embeds a shared base that owns the apply flow. The base calls into the concrete type, which decides what to change and calls a provider. Providers do the platform work. Every layer below the manager programs against model interfaces only.

Entry points · cmd/ · agent/Apply engine · resources/applyResource types · resources/file · package · service · exec · archive · scaffoldShared base · resources/baseProviders · posix · apt · dnf · systemd · httpmodel contracts · every layer depends on theseinternal/registryprovider directory
Every layer below the manager depends downward only, and only on the model contracts.

The contracts

The model package is deliberately thin. It holds interfaces and shared value structs, and defers behavior to the packages that implement them.

Resource
The runtime behavior of a managed thing: Type, Name, ResourceId, Provider, Properties, plus Apply, Healthcheck, and Info. Implemented by the per-type Type structs under resources/*/type.go. Defined at model/resource.go:26.
ResourceProperties
The declarative desired state: CommonProperties(), Validate(), ResolveTemplates(), ResolveDeferredTemplates(), ToYamlManifest(). Every per-type properties struct embeds CommonResourceProperties. Defined at model/resource.go:43.
Provider
Intentionally tiny: only Name() string (model/provider.go:8). Each resource package widens it into a capability interface, so the registry can stay type-agnostic while types get a rich contract.
ProviderFactory
The registration unit: TypeName(), Name(), IsManageable(facts, props), and New(Logger, CommandRunner). Defined at model/provider.go:12.
Manager
The large seam the whole engine programs against: facts, data, logging, execution, sessions and events, registration, templating, and NATS. Defined at model/manager.go:25.

The registry as a plugin bus

internal/registry is a global directory keyed by resource type then provider name. Each provider package exposes a Register() that calls registry.MustRegister(&factory{}) from an init(). The manager triggers all of these through a single blank import, _ "github.com/choria-io/ccm/resources" at manager/manager.go:27, so it never names a concrete provider. Duplicate provider names within a type are rejected with ErrDuplicateProvider.

Resolution happens when a resource applies. FindSuitableProvider (internal/registry/registry.go:168) probes every factory for the type with IsManageable(facts, props), keeps the ones that report they can manage the resource, sorts them ascending by the returned priority integer, and takes the first.

Load-bearing decision

Lower priority value wins. selectProviders sorts ascending and FindSuitableProvider takes provs[0] (internal/registry/registry.go:113). A test comment calls this the “highest priority” even though the numeric value is the lowest, so read the number, not the word.

The manager

The CCM struct (manager/manager.go:36) is the concrete model.Manager. It is built with NewManager(log, userLogger, opts...) and functional options from manager/opts.go (WithNatsContext, WithSessionDirectory, WithRegistrationDestination, WithNoop, WithEnvironmentData, and others). It defaults to an in-memory session store and a no-op registration publisher, so a bare manager is safe to run offline.

Facts
Facts() caches gathered facts; SystemFacts() always re-gathers with a 2s default deadline; SetFacts and MergeFacts override or overlay the cache.
Data
SetData deep-merges resolved data with an external overlay that always wins; Data() returns a copy.
Sessions
StartSession, RecordEvent, SessionSummary, plus ShouldRefresh and IsResourceFailed, which read the last recorded event for a resource to drive subscribe and require.
Templating
TemplateEnvironment(ctx) assembles the render environment and injects the registration lookup and KV-get closures.
Noop
NoopMode() and SetNoopMode gate every mutating branch in the resource types.

Cross-manager safety

Some platform tooling is not safe to run in parallel. CCM guards it with process-wide mutexes, PackageGlobalLock and ServiceGlobalLock (model/global_locks.go:14), that serialize all package and service operations even across concurrent managers and manifests. The stated reason is that concurrent dpkg/apt or systemd operations would corrupt their databases.

Load-bearing decision

TransactionEvent carries the outcome flags (Changed, Refreshed, Failed, Skipped, Noop) that are mirrored in metrics, the session summary, the CLI output, and CommonResourceState. A comment at model/transaction.go:45 warns that a change to this struct must be propagated to all four. Treat it as a shared schema, not a local struct.

Two type-dispatch switches are hand-maintained and carry a matching TODO: NewResourcePropertiesFromYaml (model/resource.go:158) and ResourceInfo (manager/manager.go:397) both map type names to constructors by hand, and the intent is to make the registry carry that mapping so new types register once. The SelectProvider boilerplate is likewise duplicated across every resources/*/type.go with a // TODO: move to base.

Next

Continue to The Resource-Provider Model to see how a concrete type and its providers are built on these contracts.

The Resource-Provider Model

Every resource type in CCM is built from three parts. A Type decides whether the system already matches the desired state. A Provider makes the platform-specific change. A shared base.Base owns the flow that connects them: gates, requirements, health checks, and turning the outcome into an event. The split is deliberate. The type stays portable and unit-testable against a mock provider, and the flow is written once for all seven resource types.

Where it lives

resources/base: the shared apply flow. resources/file, resources/package, resources/service and their posix, apt, dnf, systemd provider subpackages: the concrete types and their platform work. model/resource_*.go: the properties and state structs. Key files: resources/base/base.go, resources/file/type.go, resources/file/posix/posix.go.

The inversion

base.Base owns the apply flow but does not know any resource type. It calls back into the concrete type through a small interface, base.EmbeddedResource (resources/base/base.go:27): ApplyResource, SelectProvider, NewTransactionEvent, and Type. Each type embeds *base.Base and, in its New constructor, sets Base.Resource = t so the base can call back into it. That back-pointer is what lets one copy of the control-gating, require-checking, and event-mapping code serve every type.

Type
Embeds *base.Base; holds the typed properties, the manager, and the resolved provider. Its ApplyResource reads status, compares against the desired state, calls a provider verb, and re-verifies.
Provider interface
Widens model.Provider per type. FileProvider adds CreateDirectory, Store, SetAttributes, Remove, Status (resources/file/file.go:18). PackageProvider and ServiceProvider add their own verbs.
Concrete provider
The only place OS mutation lives. The posix file provider is the sole caller of os.MkdirAll, os.Rename, os.Chown, and os.RemoveAll (resources/file/posix/posix.go).
Load-bearing decision

Real work must live in the provider, never in the type. The Type.ApplyResource only decides and verifies; all platform mutation sits behind the capability interface. This keeps a type portable across providers and keeps its decision logic OS-agnostic and testable with a mocked provider. A type that stubbed work inline would break both properties.

Applying one resource

The flow below is base.applyOrHealthCheck (resources/base/base.go:74). The type-specific decisions happen inside the type’s ApplyResource.

  1. Select the provider The type resolves through registry.FindSuitableProvider, which filters providers by IsManageable(facts, props) and picks the lowest priority number. The result is cached.
  2. Gate on control checkControl evaluates control.if and control.unless expressions. If they say do not manage, the event is marked Skipped and returned.
  3. Resolve deferred templates File overrides ResolveDeferredTemplates so content and source render only after the control gate, letting unless protect against template errors on a resource that will be skipped.
  4. Check requirements For each require entry, Manager.IsResourceFailed reads the last recorded event. Any failed or unmet requirement marks this event Skipped with UnmetRequirements and returns.
  5. Read status and decide provider.Status reads the actual state, and isDesiredState compares it against the properties, returning a human-readable mismatch reason.
  6. Act, or describe A switch calls the provider verb. Every mutating branch is guarded by if !noop. In noop mode the verb is skipped and a noopMessage describes what would have happened.
  7. Verify convergence After a real change, the type re-reads status and re-runs isDesiredState. If the system did not converge it returns ErrDesiredStateFailed with the reason.
  8. Finalize and record FinalizeState writes the flags onto the state, base maps them onto the event, and the caller logs and records it.
Select provider by factsControl + require gatesprovider.Status → actualmatchesdesired?Stable · no changeAct via provider verbnoop: describe, do not actRe-read + verifyelse ErrDesiredStateFailedFinalizeState → eventyesno
Decide, act, verify. The provider does the work; the type only decides and confirms.

Ensure states

Ensure semantics are per type, and deliberately loose where the platform is unreliable.

TypeStatesNotes
filepresent, absent, directoryCompares ensure, content checksum, owner, group, and mode.
packagepresent, absent, latest, <version>present means “anything but absent”; latest is treated as “not absent” because the platform can lie about latest; an exact version uses VersionCmp.
servicerunning, stoppedIndependent enable axis handled in a second switch; defaults to running.

Two invariants worth internalizing

Load-bearing decision

In noop mode, Changed stays true. The provider verbs are skipped, but the flow still records that a change would have happened: package and service force changed = true when noop && refreshState, and set a NoopMessage. A noop event therefore reports both Noop=true and Changed=true. The convergence re-check is skipped under noop, since nothing actually changed.

Convergence is verified, not assumed. After a non-noop change the type re-reads status and re-checks the desired state, failing with the specific mismatch reason if the system did not converge. This is why isDesiredState returns a reason string, not just a bool.

Subscribe and refresh

Only the service type uses refresh. Inside its ApplyResource it calls ShouldRefresh(properties.Subscribe), which asks the manager whether the last recorded event for each subscribed resource had Changed == true. A refresh is suppressed unless the service is ensured running, and skipped when the service is currently stopped, since starting it already covers the change. When it fires, the provider restarts the service and the event is marked Refreshed.

Next

Continue to The Apply Engine to see how a manifest of many resources is parsed, ordered, and executed.

The Apply Engine

The apply engine turns a manifest into a sequence of applied resources. It resolves the manifest from a file, an HTTP tarball, or a NATS object store; layers Hiera data and overrides over it; builds each resource; and runs them in declaration order, recording an event after each so later resources can see what earlier ones did.

Where it lives

resources/apply: the manifest parser and executor. resources/applyresource and resources/applyresource/ccmmanifest: the apply meta-resource that lets one manifest apply another. Key files: resources/apply/apply.go, resources/apply/jet.go, resources/apply/validation.go, resources/applyresource/ccmmanifest/ccmmanifest.go.

Resolving a manifest

ResolveManifestReader (resources/apply/apply.go:395) is the core resolver. It runs before any resource executes, and its ordering is precise: data is resolved and templates are expanded before the schema is checked, so a runtime template does not fail a structural validation.

  1. Resolve the source ResolveManifestUrl dispatches on scheme: obj:// to the object store, http(s) to a tarball fetch, empty scheme to a local file. Archive paths untar, find manifest.yaml, and set the working directory.
  2. Parse the manifest Unmarshal the top-level data, hierarchy, and overrides, plus the ccm block with pre_message, post_message, fail_on_error, resources, and resources_jet_file.
  3. Resolve Hiera hiera.ResolveYaml consumes hierarchy.order, merge, and overrides, returning the resolved data and validation rules. Overriding data is deep-merged on top, then the rules are enforced.
  4. Publish data mgr.SetData stores the resolved data and the template environment is built from it, so resource fields can reference Data.
  5. Produce the resource list Either the inline ccm.resources, or, if resources_jet_file is set, the rendered output of a Jet template. Multi-name blocks are flattened in place, preserving order.
  6. Parse and template each resource NewValidatedResourcePropertiesFromYaml builds typed properties per type, resolves templates, and validates.
  7. Validate against the schema Last, the resolved payload is checked against schemas/manifest.json, substituting placeholders for still-deferred template fields. NO_SCHEMA_VALIDATION=1 bypasses this.
Sourceobj · http · fileParseHiera+ overridesResource listValidateExecute · sessionBuild resourceApply / checkRecord event+ publishnext resource · declaration orderfail_on_error: stop
Resolve once, then loop the resources in declaration order, recording each event before the next runs.

Execution and ordering

Execute (resources/apply/apply.go:603) opens a session, then iterates the resources. For each it builds the concrete resource through the ResourceFactory, calls Apply or Healthcheck, logs the result, records the event, and publishes any register_when_stable entries. When fail_on_error is set, a failed resource stops the run after the current entry.

Load-bearing decision

There is no dependency graph and no topological sort. Resources run in manifest declaration order. require is a fail-gate, not a scheduler: a resource whose required references failed or were themselves skipped is skipped, not reordered. Authors must place producers before consumers. This is the single most important invariant of the engine.

Cross-resource behavior is stateful through the session. Because Execute records each event immediately, IsResourceFailed and ShouldRefresh inspect the last event for a reference, so a later resource sees an earlier one’s change. This is again why declaration order matters.

Generating resources with Jet

When resources_jet_file is set instead of inline resources, jetParseManifestResources (resources/apply/jet.go:19) renders a Jet template with the delimiters [[ and ]], chosen so they do not collide with the {{ }} templating used for scalar fields. The resolved Hiera Data is in scope, so a template can loop over Data.packages to emit a resource per item. The rendered YAML is then parsed exactly like inline resources. Jet generates the resource list; ordinary templating fills in individual fields.

Nested applies

The apply resource type lets one manifest apply another. Its sole provider, ccmmanifest.Provider (resources/applyresource/ccmmanifest/ccmmanifest.go:31), snapshots the manager’s noop, data, and working directory, runs the child inside the parent’s session so events are shared, and restores the manager afterward.

Load-bearing decision

A nested apply can only strengthen execution, never relax it. Noop is turned on only if the parent is not already noop and the child requests it; health-check-only is the OR of parent and child. A child cannot escape the parent’s noop or health-check state. A parent can also forbid nested applies entirely with WithDenyApplyResources unless the child sets AllowApply.

Recursion is bounded by DefaultMaxRecursionDepth = 10. Note that the depth guard is not fully threaded through the apply-resource boundary today: Type.ApplyResource calls ApplyManifest with a hardcoded depth of 0, so the guard is effectively aspirational for deeply nested manifests. Two TODOs in the executor also flag that a resource-factory error currently aborts the whole run rather than recording a failed event, and that resource dispatch should move into the registry.

Next

Continue to The Agent to see how the engine is driven continuously on a timer.

The Agent

The agent runs CCM continuously. It resolves facts and external data, fetches manifests from one or more sources, applies them on an interval, and runs health checks that can trigger a remediating apply between intervals. It is a single scheduler driving many per-source workers.

Where it lives

agent: the loop and its workers. Key files: agent/agent.go (the Agent type and the control loop), agent/worker.go (the per-manifest worker), agent/config.go, agent/http.go and agent/object.go (remote source watchers), agent/nc.go (a caching NATS connection).

One scheduler, many workers

The Agent (agent/agent.go:30) creates one worker per manifest. Each worker only watches its own source and requests an apply; it never schedules one. A single applyTicker in Run drives every scheduled apply, which serializes fact refreshes and prevents two manifests from applying at once. Workers signal the loop through a buffered-size-one applyTrigger channel, and the send is coalesced so a burst of source changes collapses into one priority apply.

DefaultInterval
5 minutes. The apply cadence, floored at MinInterval of 30 seconds.
MinFactUpdateInterval
2 minutes. Facts are not re-gathered more often than this, independent of the apply interval.
applyTrigger
Buffered channel of size one. A worker whose source changed pushes a priority apply that runs even inside the interval window.
Sources
Dispatched by scheme in worker.cacheManifest: obj:// to a JetStream object store watcher, http(s) to a conditional-GET fetcher, empty scheme to a local file.

The loop

Interval tick · 5mfloor 30sRefresh facts + dataApply each manifestWorkers watch sourcesfile · http · obj://Health tickRun checksno fact refreshcritical?remediateno → ok
Scheduled applies on the left; health checks and remediation on the right feed the same apply step.

Facts, data, and resilience

Facts and data resolution sit behind a mutex and a retry policy. getFacts skips entirely when the cached facts are younger than the 2-minute floor. Otherwise it retries under jittered backoff, and after a configured number of failures it falls back to the last good facts rather than blocking the loop. getData resolves external data through Hiera on each cycle and falls back the same way. This is why a transient NATS or HTTP outage does not stall applies: the agent keeps running on the last known-good inputs.

Health checks are deliberately independent of applies. runHealthChecks runs each worker in health-check-only mode and does not refresh facts or data. A worker reporting a critical result increments a remediation counter and queues a priority apply, but the queued applies fire only after every check completes, so applies and checks never interleave.

Metrics and shutdown

When a monitor port is configured, the agent registers Prometheus collectors and serves /metrics. It exposes apply and health-check durations, remediation counts, manifest fetch counts and errors, and facts and data resolve timing. Shutdown is graceful: Run returns on context cancellation after waiting for the workers, and Stop closes the manager. Workers use a cancel-with-cause context, so a manifest deleted from an object store propagates a readable reason.

Load-bearing decision

All fact and data mutation happens under the agent mutex, and every scheduled or triggered apply acquires it. The single ticker plus the size-one trigger channel is what keeps concurrent manifests from applying over each other. Removing the shared lock or the single scheduler would reintroduce apply races.

Two items are reserved rather than active. AgentHealthCheckTime is registered but never observed, so the health-check duration series stays empty. A TODO in agent.go notes the intent to watch the KV for external data and only re-fetch on change, rather than re-resolving every cycle.

Next

Continue to Data, Facts, and Templates to see how the inputs the agent refreshes are produced.

Data, Facts, and Templates

Three cooperating packages feed every run. facts gathers what is true about the host. hiera layers a hierarchy of data blocks into one resolved map, driven by those facts. templates renders that data into resource fields. The manager wires them together and hands the result to the apply engine.

Where it lives

facts: system fact collectors backed by gopsutil, plus file-based facts. hiera: the hierarchy resolver and its comment-driven validation. templates: the render environment and the expression engines. Key files: facts/facts.go, hiera/resolver.go, hiera/validate.go, templates/templates.go, templates/expr.go, templates/jet.go.

Facts

facts.Gather (facts/facts.go:17) builds a map from the built-in families, host, network, partition, cpu, and memory, each backed by gopsutil and each skippable with a config flag. It then merges file-based facts on top: for the system config directory and then the user directory, it reads facts.json, facts.yaml, and a sorted facts.d/ directory, deep-merging each in order so later sources win.

Load-bearing decision

File facts refuse symlinks and require absolute config directories. Symlinked fact files and directories are rejected outright. This is a security invariant: fact data drives what gets installed and where, so it must not be redirectable through a planted symlink.

Runtime caching lives in the manager, not the facts package. Facts() gathers once and memoizes; SystemFacts() always re-gathers with a 2-second default deadline; SetFacts and MergeFacts let callers override or overlay, which is how the agent reuses its last good facts when a gather fails.

Hiera

Factsgopsutil + facts.dHierarchy + overrideslevels templated by factsMergefirst · deepResolved data+ validateTemplate envlookup · kvGet · registrationsResource fields{{ }} · ${ } · jet
Facts drive the hierarchy; the merged data and facts feed the template environment that renders resource fields.

Resolve (hiera/resolver.go:98) starts from the data: block as the base, then walks hierarchy.order. Each level is a template like os:{{ lookup('facts.host.info.platformFamily') }}. A level counts as matched only when its embedded expression produces a non-empty value, so a level whose fact is missing is skipped. The matched string is looked up in overrides: and merged into the base.

merge: first
The default. The first matching level wins; its top-level keys replace the base, and resolution returns immediately.
merge: deep
Every matching level accumulates. Maps merge recursively and slices concatenate, in hierarchy order.
Sources
ResolveUrl dispatches by scheme: a local YAML or JSON file, an http(s) URL with basic auth, or a NATS JetStream KV document at kv://Bucket/Key.
Validation
YAML comments @require and @validate <expr> become rules. Resolution returns the rules so a multi-source caller validates once after merging.
Load-bearing decision

Hiera resolution runs in a restricted sandbox. During resolution only facts and lookup() are available, with missing keys returning empty rather than erroring. File, KV, and registration functions are excluded. The full function set only exists later, at render time, in the manager-built environment. This keeps hierarchy selection from reaching into the network.

Templates

{{ expr }} and ${ expr } are aliases. Both route through the same expr-lang evaluator, so ${ Data.package_name } and {{ Data.package_name }} are identical. A separate jet(...) renderer handles Jet templates with [[ ]] delimiters, used mainly to generate resource lists.

Field access
Inside an expression, use Go field names: Data.app_name, Facts.os, Environ.X.
lookup(key, default)
Takes a lowercased, dotted path rooted at the environment: lookup('data.nested.key'), lookup('facts.env'). It marshals the environment to JSON and queries it with gjson.
Type preservation
When the whole string is a single expression, its native typed value is returned, so a templated port stays an integer. Mixed strings are stringified and concatenated.
Deferred fields
Struct fields tagged template:"deferred", such as file content, render in a second pass after the control gate, so an unless can protect a resource from a template error.
Load-bearing decision

kvGet(bucket, key) and the kv://Bucket/Key Hiera source are different mechanisms and are easy to conflate. kvGet is an in-template lookup of a single KV value at render time. kv:// is a whole-document Hiera data source resolved during data resolution.

One syntax is reserved but not implemented. The Resolve doc comment and a test reference Puppet-style env:%{env} placeholders, but only {{ }} and ${ } are recognized. A %{...} entry is treated as a literal key with no matched expression, so it is skipped. Real hierarchies use env:{{ lookup('facts.env') }}.

Next

Continue to Registration and Discovery to see how resolved resources announce themselves for others to find.

Registration and Discovery

Registration is service discovery without a discovery daemon. When a managed resource reaches a stable state, it publishes an entry into a NATS JetStream stream. Other nodes read those entries, usually from a template, to build configuration such as a load-balancer upstream or a Prometheus target list. Publishing is a side effect of apply; lookup is a template function or a CLI query.

Where it lives

registration: the transport, subject grammar, stream, lookup, and watch. model/registration.go and model/registration_ttl.go: the entry and TTL data model. Key files: registration/subject.go, registration/stream.go, registration/nats.go, registration/lookup.go, registration/watch.go.

The publish gate

A resource carries a register_when_stable list. After the resource applies, publishRegistration (resources/apply/apply.go:736) decides whether to announce it. The gate is conservative: it publishes only when the resource has entries, the manager is not in noop mode, the event did not fail, and every health check returned OK. A resource with no health checks passes that last test.

Load-bearing decision

A missing registration publisher is tolerated, not fatal. The manager defaults to a no-op publisher, so a register_when_stable manifest runs safely on a node with no registration backend. This is what lets the same manifest apply on nodes with and without NATS.

A KV store built from a stream

Node A · applyresource stable +health checks passJetStreamREGISTRATION1 msg / subjectTTL · rollup · markersNode B · lookup()renders configWatcherregister / removepublishchoria.ccm.registration.v1.<cluster>.<protocol>.<service>.<address>.<instance>
The stream behaves like a KV store keyed on the entry tuple; readers take the last message per subject.

Each entry maps to a single subject. The address contributes its IP with dots turned into underscores so it is one token, and an InstanceId FNV hash of the full tuple forms the final token, so every unique cluster, service, protocol, address, and port maps to one stable subject. The stream captures the whole namespace with MaxMsgsPerSubject: 1 and AllowRollup, and reliable publishes carry a Nats-Rollup: sub header.

Load-bearing decision

One message per subject plus rollup plus a hashed per-instance subject makes the stream behave like a KV store keyed on the entry tuple. Re-registering overwrites in place, and a read is simply “last message per subject.” Full KV semantics only hold with the JetStream destination; the plain nats destination is best-effort and omits the TTL and rollup headers.

Expiry, lookup, and watch

Expiry is two-tiered. A per-entry Nats-TTL header sits under the stream-wide MaxAge, and on removal the server writes a subject delete marker so watchers see the removal rather than a silent disappearance. The stream denies hard deletes but allows purge, so JetStreamRemove can retire a single entry.

Lookup
JetStreamLookup builds a wildcard filter subject, fetches the last message per matching subject, drops server marker messages, and returns entries sorted by address then port.
Watch
JetStreamWatch streams WatchEvents: normal messages become Register events, marker messages become Remove events with the entry reconstructed from the subject.
From a template
The manager exposes lookup as registrations(cluster, protocol, service, ip), so a rendered config file discovers its peers directly.
Prometheus
RegistrationEntries.PrometheusFileSD() converts entries into file_sd JSON, grouped by cluster, service, and protocol.
Load-bearing decision

Both lookup and watch special-case the Nats-Marker-Reason header, per ADR-43. Lookup drops markers; watch turns them into removals. A reader that ignored markers would surface expired or purged entries as live services.

The Priority and Annotations fields travel on every entry but, within this subsystem, are only consumed by the Prometheus conversion. Priority is validated and sorted around but not otherwise acted on here, reserved for downstream consumers such as SRV-style weighting.

Next

Continue to Observability to see how each apply is recorded, measured, and health-checked.

Observability

Every applied resource produces one event. That event is recorded to a session store, translated into Prometheus counters, and aggregated into a summary at the end of the run. Health checks run alongside and feed the agent’s remediation. Together these are how a CCM run is observed after the fact and monitored while it runs.

Where it lives

internal/session: the session stores. internal/metrics: the Prometheus collectors. internal/healthcheck: the goss and nagios runners. model/transaction.go and model/healthcheck.go: the event and health-check types. Key files: internal/session/directory.go, internal/session/util.go, internal/metrics/metrics.go.

The event flow

Apply resourceTransactionEventKSUID id · flagsSession storememory (default)directory: JSON / eventMetricschoria_ccm_*Summaryon StopSessionrecord
One event per resource, recorded once, then fanned out to counters and the run summary.

Sessions and events

StartSession records a start event, each applied resource records a TransactionEvent, and StopSession builds a SessionSummary from all events. The memory store keeps events in a slice and is the default. The directory store writes one JSON file per event, named by the event’s KSUID.

Load-bearing decision

Event IDs are KSUIDs, and that choice does double duty. KSUIDs are timestamp-prefixed and lexicographically sortable, so sorting the .event filenames reconstructs chronological order without reading timestamps. Parsing the ID also whitelists it to base62 with no path separators, which is the directory store’s defense against path traversal, since the ID becomes a filename.

The directory store is append-only and crash-friendly. Each record is an independent file write with no shared index to corrupt, and replay is a directory scan that reads each event’s protocol field to pick the concrete type before unmarshaling.

Metrics

Collectors live under the choria namespace and ccm subsystem. updateMetrics (internal/session/util.go:12) runs from both stores’ RecordEvent. It increments a total and exactly one outcome counter, chosen by a priority order: noop, then changed, skipped, refreshed, failed, error, and stable.

Load-bearing decision

Noop is checked first in that priority order. A noop run has Changed=true on its event, but it counts as noop, not changed, so the counters stay mutually consistent: each event increments the total plus exactly one state counter.

Durations are recorded with timers around the work: manifest apply, resource apply, fact gather, and per-check health-check time. RegisterMetrics registers the collectors and ListenAndServe serves them at /metrics when a port is set.

Health checks

Health checks run for both apply and health-check-only modes. Each check dispatches by format.

nagios
Runs a command through the manager's runner and maps its exit code to a status: 0 is OK, 1 Warning, 2 Critical, anything else Unknown. Retries up to tries, sleeping between attempts, stopping on OK.
goss
Renders the goss rules through the template engine, writes a temp spec, validates it, and reports Critical when any check failed, else OK. It never emits Warning or Unknown.
Status
HealthCheckStatus values equal the nagios exit codes, so the plugin format maps directly and goss reuses the same enum.

A non-OK result or an execution error marks the event failed. In the agent, a critical result increments a remediation counter and queues a priority apply, so a failing check drives a corrective run. Several Agent* metrics are declared here but recorded at their call sites in the agent, and AgentHealthCheckTime is currently registered without a call site.

Next

Continue to the Reference and Map for the CLI surface, the source map, and a glossary.

Reference and Map

This page is the index to the codebase: the commands a user can run, the packages that back them, and the types that recur across the subsystem pages.

Where it lives

cmd: the entire CLI, one file per command, built on choria-io/fisk. main is in cmd/ccm.go. Shared plumbing, including manager construction and the .env reader, is in cmd/util.go.

Command surface

Every ccm command builds a manager and drives a subsystem covered elsewhere in this map. The global flags --debug and --info set log verbosity.

CommandPurposeDrives
ccm ensure <type>Manage one resource imperatively: archive, exec, file, package, scaffold, serviceResource-Provider Model
ccm ensure api pipedApply a resource sent as JSON or YAML on stdinResource-Provider Model
ccm apply <manifest>Apply a manifest from a file, obj://, or https:// tarballApply Engine
ccm agent --config <file>Run the continuous manifest daemonThe Agent
ccm facts [query]Show system facts, with an optional gjson queryData, Facts, and Templates
ccm hiera parse <input>Resolve a Hiera input against factsData, Facts, and Templates
ccm registration create / query / watch / rm / initPublish, read, watch, remove entries, or provision the streamRegistration and Discovery
ccm session new / reportCreate a session store or summarize oneObservability
ccm status <type> <name>Read the current state of a resourceResource-Provider Model

Every ensure subcommand and the piped API funnel through one factory, resources.NewResourceFromProperties, so the CLI, manifests, and the wire API run identical resource logic. The status command’s valid types are generated at run time from the registry, so it tracks whatever providers are registered.

Source map

PackageContents
cmd/The fisk CLI surface, main, one file per command, shared helpers in util.go.
agent/The continuous runner: config, worker loop, and NATS, HTTP, and object-store sources.
facts/System fact collectors backed by gopsutil, plus file-based facts.
hiera/The hierarchical data resolver and comment-driven validation.
templates/The render environment and the expr, Jet, and Go template engines.
manager/The concrete model.Manager (type CCM), its options, and logger adapters.
model/Core interfaces and shared structs; per-resource property types; modelmocks/.
registration/Service registration over JetStream: stream, subjects, publish, lookup, watch.
resources/Resource implementations, the shared base, the apply engine, and provider subpackages.
internal/registry/The global provider directory and FindSuitableProvider.
internal/session/The directory and memory session stores.
internal/metrics/Prometheus collectors and the /metrics server.
internal/healthcheck/The goss and nagios health-check runners.
internal/ (other)cmdrunner, backoff, fs (embedded schemas), and util helpers.

Key types

TypeRoleExplained in
model.Manager (CCM)The orchestrator every layer programs againstArchitecture
model.Provider / ProviderFactoryThe provider seam and its registration unitArchitecture
model.Resource / ResourcePropertiesA managed thing and its declarative desired stateResource-Provider Model
base.Base / EmbeddedResourceThe shared apply flow and its callback interfaceResource-Provider Model
model.Apply / TransactionEventA parsed manifest and the per-resource result recordApply Engine
hiera.Resolve / templates.EnvData resolution and the render environmentData, Facts, and Templates
model.RegistrationEntryA published service-discovery entryRegistration and Discovery
model.SessionStore / SessionSummaryEvent storage and run aggregationObservability
agent.AgentThe continuous loop and its workersThe Agent

Glossary

Resource
A system component to manage: a package, file, service, exec, archive, or scaffold. Declares a desired state.
Provider
The platform-specific implementation for a resource type, such as apt, dnf, systemd, or posix. Selected at run time by facts.
Ensure
The desired state of a resource, such as present, absent, running, or a package version.
Manifest
A YAML document of data, a hierarchy, and a list of resources, applied as a unit.
Facts
Gathered truths about the host, used to select providers and drive the data hierarchy.
Hiera
The hierarchical data resolver that merges layered data blocks, chosen by facts, into one resolved map.
Session
The record of a run: a sequence of events, stored in memory or on disk, that drives require and subscribe.
Noop
Dry-run mode. No change is made, but the event still reports what would have changed.
Require
A fail-gate: a resource is skipped if a named prior resource failed. Not a scheduler.
Subscribe / refresh
A resource, in practice a service, restarts when a named prior resource changed.
register_when_stable
A resource property that publishes a discovery entry once the resource is stable and healthy.
Back to the start

Return to the Code Map overview for the mental model and the full page list.