Skip to main content
Version: V3

Configuration foundation

Configuration foundation​

The v3 configuration foundation separates declared configuration from runtime secrets. infrahub_sync.configuration.ConfigurationPackage is a strict versioned envelope. Its checksum covers only declared JSON content, including credential references and behavior-affecting package metadata. Generated code, local paths, registry identity, and resolved secret values never enter the checksum.

Durable or public callers parse untrusted declarations with parse_configuration_package(). This boundary converts Pydantic failures into locations and reason codes without including rejected values. Calling Pydantic's low-level ConfigurationPackage.model_validate() directly is reserved for already-trusted data.

Credential-bearing adapter settings use an exact reference node:

token:
$credential: netbox-token

The package declares what that name means without carrying its value:

credentials:
netbox-token:
provider: env
identifier: NETBOX_TOKEN

A $credential node is only accepted where a credential is actually resolved: the credential setting paths of the adapter filling that role, and those of the declared store. The same node anywhere else — a non-credential setting, a schema-mapping static value — is refused, because nothing would resolve it and the adapter would receive the node itself.

Exactly one function turns a declared setting path into a pointer: _settings_pointer in credentials.py. The declared-content walk builds the same pointer incrementally through the same component escaping. This is a contract, not a tidiness preference: the allowed-location set and the walk must produce byte-identical strings, or a reference is accepted by the per-path check and then refused by the walk as not credential-bearing. Any further component that renders these pointers — registry persistence, an API response, a CLI diff — calls that function rather than formatting its own.

Registry persistence and identity​

The product store owns an append-only registry. A configuration has a server-generated config_id and immutable versions numbered by an integer registry_version starting at 1 and scoped to that config_id. A version row also carries the package_checksum and the declared content that produced it. Nothing updates or deletes a version.

registry_version is deliberately not called config_version. That name already belongs to the saved-plan value in infrahub_sync/plan/config_version.py: an opaque printable-ASCII string, compared for equality and never parsed, which product_runs.configuration_reference already stores. Runs and plans bind to (config_id, registry_version, package_checksum); the saved-plan manifest keeps its own config_version field. Two names, two concepts, one table — keep them apart.

Registering a checksum that already exists under a configuration returns the existing version and writes no row. A new checksum takes the next integer, allocated with the store's existing idiom: read MAX + 1, insert, catch the uniqueness violation, retry within a bounded budget, raise a typed error on exhaustion. Uniqueness is enforced by indexes on (config_id, registry_version) and (config_id, package_checksum), so identical content under two different configurations is accepted — configurations are independent, not deduplicated against each other. Because a losing writer recomputes MAX + 1 on a fresh connection and an exhausted one never commits, the sequence cannot develop gaps.

Credential validation happens at the ProductProjection boundary, not in the store classes, matching where redaction already lives. A caller reaching for SQLiteRunStore or PostgreSQLRunStore directly bypasses it; neither is part of the package's public surface.

Schema migration and dialect​

Migration is additive: the store introspects the existing column set and emits ALTER TABLE ADD COLUMN only for what is missing. There is no schema version table, no rebuild, and no backfill. Construction is idempotent, so a process restart re-runs it safely.

The store selects its introspection and constraint statements from an explicit dialect argument that each provider passes. Never infer the dialect from the placeholder string or by running a statement to see whether it fails: the test suite's PostgreSQL profile is a SQLite file behind a %s-to-? adapter, so a placeholder check misidentifies it, and provoking an error on a connection mid-DDL aborts the transaction and discards uncommitted CREATE TABLE statements.

The run-binding columns are inert on purpose​

product_runs carries nullable config_id, registry_version, and package_checksum columns that no code path reads or writes, and ProductRun has no matching fields. They are not dead code and not an oversight: the columns exist so the write-time constraint below can exist before any writer does. The later change that binds registered runs owns the model fields and the behavior.

A run row is valid in exactly three states: all three binding columns NULL with a non-empty configuration_reference (pre-registry and unregistered rows), or all three non-NULL (a registered row). Partial binding is refused at write time — SQLite uses a pair of BEFORE INSERT and BEFORE UPDATE triggers, PostgreSQL a CHECK constraint. Refusing the partial state removes a whole family of later "which field wins" ambiguities. Both triggers must be present for SQLite enforcement to hold, so the existence check requires both before it skips recreation.

Declaring adapter configuration capabilities​

Every adapter accepted by the registry needs an AdapterConfigurationCapabilities record. The declaration is connection-free: it names the adapter's allowed roles, closed set of allowed settings, credential-bearing setting paths, qualified destination write operations, schema-validation availability, and an optional bounded configuration validator. Credential paths must be inside the allowed setting set. It must never contain clients, connections, credentials, or orchestration objects.

Bundled declarations live in infrahub_sync.configuration.capabilities. Package validation refuses adapters without a bundled declaration because Sync cannot prove which of their settings are credentials. A future adapter-extension and registration surface can contribute the same record through plugin metadata; it must preserve contract version 1 and the exact credential-reference rules.

The version-1 envelope refuses adapters_path and per-role adapter overrides. Those fields select machine-local or arbitrary adapter code whose credential-bearing settings cannot be proven by a bundled capability declaration. Because they are always absent from an accepted package, they are excluded from the declared content the checksum covers. Adapter names match the registered declaration exactly: a case variant is refused rather than folded, because the name is hashed as declared and every consumer resolves it verbatim. Redis accepts only its declared store settings; its URLs, usernames, and passwords follow the same credential-reference rule as adapter settings. Registered version-1 packages also refuse adapter settings outside each bundled declaration, including custom HTTP headers and request parameters on Prometheus, GenericRESTAPI, and its PeeringManager subclass. Declared URLs, base URLs, and endpoint paths cannot contain user information, query parameters, or fragments. Declared url and base_url must be absolute http or https URLs; api_endpoint and endpoint must be relative paths carrying neither scheme nor authority. GenericRESTAPI and PeeringManager packages cannot select alternate URL or credential environment variables; their schema-mapping request endpoints must be relative paths without authority, user information, queries, or fragments. Use the declared credential-reference settings instead. These restrictions apply to the registered package contract without changing the legacy SyncConfig runtime surface.

The bundled examples/ remain legacy filesystem SyncConfig inputs, not package declarations ready for registration. Before registration, replace credential-bearing values and placeholders with declared credential references. Convert non-secret settings and placeholders, such as ACI url and verify, to valid declared literal values or another mechanism supported by a future contract. Explicitly remove fields that legacy SyncConfig ignored, or deliberately model their behavior in a later contract version; do not silently carry them forward. For example, legacy parsing drops the top-level description in examples/aci_to_infrahub/config.yml; version 1 does not add an inert field to retain it.

The declaration is intentionally narrower than the complete adapter conformance profile. It provides the durable seam configuration validation needs without claiming that every adapter behavior has been qualified.