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.