Mutation Sources
Every Config Unit carries a record, called MutationSources, of which change last set each individual value in its configuration data. Where a Revision is a point-in-time snapshot of a unit's data and a Mutation records the source of one granular change, MutationSources is the accumulated result: a per-path index of who-and-what most recently touched each field. It serves two purposes — an audit trail ("git blame" for your configuration) and the information ConfigHub's merge engine uses to decide how to combine changes from different sources.
MutationSources is read-only in the sense that it is maintained automatically as the unit changes; you don't edit it the way you edit configuration data. You can, however, adjust the per-path protection flags it stores (see Protection, below).
What MutationSources records
MutationSources is organized to mirror the configuration data:
- One entry per resource. A unit's data is a collection of resources (for example, Kubernetes objects). MutationSources has one entry per resource, identified by type and name, plus a stable identifier that follows the resource across name changes and across variants. Both current and prior names are retained as aliases, so a resource can still be matched after it is renamed.
- A map of paths to changes. Within each resource entry is a map from configuration path (for example,
spec.replicas, or a specific container's image) to information about the change that last set that path: the kind of change (added, updated, or deleted), a reference to the Mutation (and therefore the Revision, function, link, trigger, or other source) that made it, and an eligibility flag described below.
Internally these structures are referred to as a ResourceMutationList (the list of per-resource entries) and, within each, a PathMutationMap (the per-path index). MutationSources is larger than the configuration it describes, so it is not a field of the Unit and no form of cub unit get returns it. Read it with:
cub unit mutation-sources --space my-space my-unit
That prints the structure as JSON, so -o jq= can address a path within it. The list itself is the payload, so the first resource's per-path index is .[0].PathMutationMap:
cub unit mutation-sources --space my-space my-unit -o jq='.[0].PathMutationMap'
For a past Revision, cub revision get --space my-space -o mutations my-unit 3 shows what that revision recorded.
Because ConfigHub treats configuration as data rather than text, paths address individual fields and individual elements of merge-keyed lists (such as a container matched by name), not line numbers. This is what lets ConfigHub attribute and merge changes field-by-field.
Viewing mutation sources
That raw JSON is exact but unwieldy. For a readable view, cub unit get <unit> -o mutations renders a unit's MutationSources as a per-resource diff, grouped by the Protected flag so you can see at a glance which fields are protected and which a merge may overwrite:
- Locally overridden (preserved during merges) — paths whose Protected is
true. - Eligible for upstream merges — paths whose Protected is
false.
cub unit get --space my-space my-unit -o mutations
Add --verbose to also show each field's value, which change last set it (function, merge source, link, or trigger), and a summary table of the contributing Mutations.
Where -o mutations groups by resource and path, cub unit blame turns the same record into one line per field — the value, what set it, whose change it was, and how long ago:
cub unit blame --space my-space my-unit
Blame follows a value to where it was actually set. A field a variant took from its base reads as the base's change, and a field the base took from a rendered chart reads as that chart — so "did the chart set this, or did we?" is one command rather than three. --no-upstream reports only this unit's own record. A field marked * is a protected local override. --path asks about one field, --verbose shows each change description and the full upstream chain, and -o json emits one object per field.
The same -o mutations flag previews a pending change before it is written — for example, the per-path effect of an upgrade — when added to cub unit update ... --dry-run; see advanced merging techniques.
How merging uses MutationSources
When ConfigHub merges changes between related units — propagating an upstream change into a cloned variant via Upgrade, merging a range of changes from another unit, or resolving a link — it needs to know which target paths it may overwrite. A path the target has marked protected is a local override the merge leaves alone; every other path is the merge's to update.
MutationSources is what makes this distinction possible. Because each path records which mutation last set it, the merge engine can recognize that, say, the downstream unit's replica count was set by a local function invocation rather than inherited from upstream, and therefore should not be overwritten by an upgrade.
It is also what makes replay possible. A merge walks its range and, where a source revision records function invocations, re-runs them against the target instead of copying the paths they touched upstream; MutationSources is what says which paths belonged to which invocation, so a revision that mixes a replayable function with something else can be split and handled per mutation. What the merge did with each step is recorded back onto the resulting mutation as ReplayOutcome.
There are two complementary mechanisms that use this information to protect local overrides:
- The stored Protected flag on each path, which is what decides by default.
- A filter over mutation history (
WhereMutation), set on a Link. It is empty by default; set to select everything that did not originate from a clone or a prior upgrade/merge, it protects a hand edit, a function invocation, a needs/provides binding, and trigger output whether or not those paths carry the flag. The two are unioned, so a filter protects further paths and never re-opens one the flag claimed.
For the full description of the merge algorithm, the Upgrade and merge commands, and how overrides are detected and preserved, see the Creating and managing variants guide.
Protection
Each path in MutationSources carries a boolean Protected that records whether the path is a local override a merge must not overwrite:
true— the value is a local override, and a merge must not overwrite it.false— a future merge may overwrite the value. This is the default: a change protects nothing unless it says so.
Protection is something you ask for. A hand edit, a function invocation, a trigger, or a needs/provides binding leaves each path it writes as protected as it found it — so an ordinary downstream change can still be updated by a later merge from upstream, and a path that was already protected stays protected. Content that arrives from a clone, an upgrade, a merge, or an external source is recorded unprotected, because accepting a change is not authoring it.
These flags matter most when a merge's override-preservation subtraction step is turned off (see advanced merging techniques): in that mode, the stored Protected flags are the sole mechanism deciding which paths a merge may overwrite.
You can adjust the flags directly to change which fields a future merge is allowed to overwrite — for example, to protect a field that would otherwise be eligible, or to re-open one you previously protected:
# Protect the downstream replica count from being overwritten by merges.
cub unit set-protection my-unit \
--protect "apps/v1/Deployment:my-namespace/my-app:spec.replicas"
Re-open a path with --unprotect instead. Either way, a new Revision is produced only if a flag actually changes.
A change that is meant to hold a field against upstream can claim what it writes as it writes it, with --protect on cub unit update, cub function set, and cub run:
cub unit update --space my-space my-unit ./data.yaml --protect \
--change-desc "Pin replicas for this environment"
Every path that change writes becomes a local override, the same state cub unit set-protection --protect produces. Leave it off for anything applying a value chosen somewhere else — replaying a change, copying an already-reviewed value, a script propagating a release: those record no ownership, so the next merge can still update the path. Either way, a change never removes protection; that is what --unprotect is for.
Provenance and audit
Independently of merging, MutationSources answers "what last changed this field, and why?" Each path points at the Mutation responsible, which in turn carries the source — an update, a function invocation (ad hoc, triggered, or part of a clone/upgrade), a link resolution, an import, and so on. Restoring a unit to a prior Revision restores its MutationSources too, so the provenance always reflects the state you restored.