# Creating and managing variants

A common task is to manage [variants](../background/concepts/variant.md) of the same configuration, for example, development, staging, and production variants.

ConfigHub offers several merge-based mechanisms for keeping related units in sync. Pick the one that matches your goal:

| Goal | Mechanism |
| --- | --- |
| Inherit config properties | Clone/upgrade, UpgradeUnit Links |
| Repeatedly render or generate config data | MergeUnits Links |
| Combine or split config units, with updates | MergeUnits Links |
| Repeatedly upload config data | `--merge-external-source` |
| Merge a ChangeSet into other variants or upstream | `--merge-source` |

## Creating variants

In ConfigHub you can copy (known as _clone_ in the UI) config [units](../background/entities/unit.md) in a way that tracks their relationship in order to help you keep them in sync. The new copied unit is said to be _downstream_ from the original _upstream_ unit.

Different deployment environments would have different [spaces](../background/entities/space.md) corresponding to them, so typically one would clone a unit to one [or more](../background/concepts/bulk-operations.md) other spaces.

You can copy one unit to another space with the [CLI](./cli-usage.md) like this:

```
cub unit create --space prod --upstream-space dev --upstream-unit backend
```

You could copy multiple units to multiple spaces also. To copy all units from dev into multiple prod spaces (e.g., corresponding to multiple clusters or regions), you could do something like:

```
cub unit create --space dev --where-space "Labels.Environment = 'prod'"
```

When a unit is cloned, ConfigHub records the upstream unit and the upstream revision the clone was created from on the new downstream unit (`UpstreamUnitID` and `UpstreamRevisionNum`), and automatically creates an [UpgradeUnit Link](../background/entities/link.md) from the clone (downstream) to the original (upstream). The Link tracks which upstream revision was last merged and carries a `WhereMutation` filter that constrains which downstream changes can be overwritten by future upgrades. The default `WhereMutation` only allows mutations that originated from the clone or a prior upgrade or merge — anything else (like a hand edit, function invocation, or trigger output) is treated as a local override and preserved.

## Establishing or changing the upstream relationship after creation

A unit's `UpstreamUnitID` is read-only and cannot be set or modified directly with `cub unit update`. To establish, change, or remove the upstream relationship after a unit has been created, manipulate the unit's UpgradeUnit Link.

To establish a relationship between two units that already exist, create an UpgradeUnit Link with `--make-current`. `--make-current` pins the Link's `UpstreamLastMergedRevisionNum` and `DownstreamLastMergedRevisionNum` to the current head revisions of the two units, so the next upgrade only merges changes made _after_ this point — it does not retroactively merge the upstream's full history into the downstream:

```
cub link create --space prod --update-type UpgradeUnit --make-current upgrade-mydown mydown myupstream
```

To change which unit is upstream, delete the existing UpgradeUnit Link and create a new one. Deleting an UpgradeUnit Link clears the downstream unit's `UpstreamUnitID` and `UpstreamRevisionNum`. Without `--make-current`, creating a new UpgradeUnit Link performs an initial merge using the new upstream's full history.

Note: a unit can have at most one outgoing UpgradeUnit Link.

## Reversing the upstream relationship

You may want to invert which unit is upstream. A common case: you started with a running app, then decided to copy it to a base unit so you can derive other variants from the base. The original (now an arbitrary variant) should become downstream of the base.

Reverse an UpgradeUnit Link with `cub link update --reverse`:

```
cub link update --space prod --patch --reverse upgrade-mydown
```

When the downstream and upstream units are in the same space, the Link is reversed in place. For cross-space Links, ConfigHub creates new reversed Link copies in the other space and deletes the originals (because a Link must live in the same space as its `FromUnit`).

Reversing also clears the Link's `WhereMutation`, since the heuristic that scoped it to changes "from the original upstream" no longer applies.

For bulk reversal — for example, after copying every unit in a dev space to a new base space and wanting the dev units to become downstream of the base — use `cub link create --reverse` against the original UpgradeUnit Links, then delete the originals:

```
# Copy every unit from app-dev to app-base; this auto-creates UpgradeUnit
# links in app-base pointing back to app-dev.
cub unit create --space app-dev --where "Slug LIKE '%'" --dest-space app-base

# Reverse: create reversed UpgradeUnit links in app-dev pointing to app-base.
cub link create --space app-base --where "UpdateType = 'UpgradeUnit'" --reverse

# Delete the now-redundant original links in app-base.
cub link delete --space app-base --where "UpdateType = 'UpgradeUnit'"
```

## Keeping variants in sync

The upstream and downstream units can all be modified independently, but if you want to propagate a change from the upstream unit to the downstream units, ConfigHub uses the UpgradeUnit Link's tracked revision to know which upstream changes still need to be merged.

To upgrade and copy more recent changes from the upstream unit, you can execute a command like this:

```
cub unit update --space "*" --patch --upgrade --where "UpstreamUnit.Slug = 'backend' AND Space.Labels.Environment = 'prod'"
```

Alternatively, since the relationship is represented as a Link, you can also upgrade via the resolve mechanism:

```
cub unit update --space prod --patch --resolve "Link:*" backend-clone
```

Both approaches produce the same result. The `--upgrade` flag uses the UpgradeUnit Link's `WhereMutation` filter if one is set. The `--resolve` approach works with any Link of type `UpgradeUnit` or `MergeUnits`.

Upgrading the units modifies the configuration data but does not automatically apply the changes. To preview the changes first, add `--dry-run`. To inspect the resulting diff, add `-o mutations`:

```
cub unit update --space prod --patch --upgrade --dry-run -o mutations backend-clone
```

Merging upstream changes preserves changes made independently in the downstream unit — they are treated as overrides. ConfigHub supports this by tracking the source of each granular change via [Mutation](../background/entities/mutation.md) metadata stored on the unit (`MutationSources`), described in [How merging works](#how-merging-works) below.

## Ad hoc merges across variants and from downstream to upstream

Upgrade propagates changes _from_ the upstream unit _to_ selected downstream units. For other directions — between sibling variants, from a downstream back to its upstream, or from a previous range of revisions of the same unit (for rebase-like flows) — use `--merge-source` with `--merge-base` and `--merge-end`. The base/end pair specifies a revision range on the source unit to merge into the target unit:

```
cub unit update \
    --patch \
    --space prod-west \
    --merge-source prod-east/backend \
    --merge-base Before:ChangeSet:prod-east/prodfix42 \
    --merge-end ChangeSet:prod-east/prodfix42 \
    backend
```

To apply a range of changes from a unit to itself (for rebase-like flows after a restore), use `--merge-source Self`:

```
cub unit update \
    --patch \
    --space acme-dev \
    --filter acme-home/acme-app \
    --merge-source Self \
    --merge-base Before:ChangeSet:acme-home/release-v452 \
    --merge-end ChangeSet:acme-home/release-v452 \
    --change-desc "Reapply release 452"
```

See [merging and rebasing](./change-apply.md#merging-and-rebasing) for additional examples.

## Merging external sources into ConfigHub

When the base for a unit lives outside ConfigHub — for example, a YAML file in a Git repository, a chart from a registry, or a manifest produced by an external pipeline — use `--merge-external-source` with `cub unit create` and `cub unit update` to bring in changes from that source while preserving anything ConfigHub-side functions, links, or hand edits added on top:

```
# Create the unit, recording deployment.yaml as the external source.
cub unit create --space prod mydep deployment.yaml --merge-external-source deployment.yaml

# Apply ConfigHub-side defaults and customizations.
cub function do --space prod --unit mydep set-pod-container-security-context-defaults

# Later, after the upstream file changes, merge the new version in.
cub unit update --space prod mydep deployment-v2.yaml --merge-external-source deployment.yaml
```

The first invocation records `deployment.yaml` as the external source so subsequent updates with the same `--merge-external-source` are treated as continued merges from the same logical source rather than full replacements. Mutations made on the ConfigHub side (the security-context function above, link resolutions, etc.) are preserved through the merge.

By default the merge uses the latest revision whose source is `MergeExternal` as its base — i.e., the last externally-merged version of the file. Pass `--merge-base` to override that selection when you need to rebase the merge against a different revision of the unit (for example, to re-apply changes from an older external snapshot, or to reset the cursor after rewriting external history). It accepts the same revision syntax as `--restore` and `--merge-base` for `--merge-source`:

```
cub unit update --space prod mydep deployment-v2.yaml \
    --merge-external-source deployment.yaml \
    --merge-base Tag:initial-external
```

`--merge-end` is not used with `--merge-external-source`; the new external data passed on the command line plays that role.

## How merging works

`--upgrade`, `--resolve` against an UpgradeUnit/MergeUnits Link, `--merge-source`, and `--merge-external-source` all share the same merge engine. Internally, it is a **four-way merge**:

1. Diff the source between two source revisions — for upgrade and MergeUnits, that range is the Link's `UpstreamLastMergedRevisionNum` to the upstream unit's current `HeadRevisionNum`; for `--merge-source` it's `--merge-base` to `--merge-end`; for `--merge-external-source` it's the previously-recorded external file (or the revision selected by `--merge-base`) to the new external file.
2. Diff the downstream unit between the previously-merged downstream revision and its current head, so that the downstream's existing local differences from the source baseline are recognized.
3. Subtract any changes in (1) that conflict with the downstream's local differences from (2) — those are the local "overrides" that get preserved by default.
4. Apply the remaining changes as a patch to the downstream unit's head.

`UpstreamRevisionNum` on the unit and `UpstreamLastMergedRevisionNum` on the Link are updated when the merge completes. `UpstreamLastMergedRevisionNum` can be edited if you need to advance or rewind the merge cursor without doing a real merge.

### MutationSources: which mutation last touched each path

Every time a unit's data changes, ConfigHub records which mutation last set each configuration path on the unit's `MutationSources`. That includes hand edits, function invocations (whether ad hoc, triggered, or part of a clone or upgrade), needs/provides resolutions, and merge results. `MutationSources` is what lets the merge engine tell "the upstream changed this field" apart from "the downstream changed this field locally."

A few consequences worth knowing:

- **Restore preserves provenance.** `cub unit update --restore` rewinds both the unit's `Data` and its `MutationSources` to the chosen revision, so subsequent merges treat the restored state as the truth about who-touched-what.
- **You can "reset" `MutationSources` by removing all data and adding it back.** Updating a unit to empty config and then back to the desired config replaces every entry in `MutationSources` with the new mutation. This is the supported way to drop accumulated provenance when you want a future upgrade to overwrite paths it would otherwise consider local overrides.
- **Closing a difference re-opens the path.** If you change a field on the downstream so it again matches the upstream, the field is no longer a difference between them, so a subsequent upstream change to that field will propagate (assuming `WhereMutation` allows it). There is no "sticky" override list, except through `WhereMutation`, described below — overrides are inferred from the diff at merge time.
- **Setting an override to the same value the upstream already has is not an override.** It looks identical to the merged-in upstream value, so the merge engine has nothing to subtract.

### `WhereMutation`: scoping which downstream paths can be overwritten

The merge engine narrows step (3) using the Link's `WhereMutation` (or the request's `--where-mutation`). It is a [filter](../background/concepts/filters.md) over the downstream unit's mutation history that selects which downstream paths are eligible to be overwritten by the source-side changes.

The auto-created UpgradeUnit Link sets a default `WhereMutation` that allows only mutations whose source was a clone, upgrade, or merge from this same upstream. The practical effect: a downstream change made by `cub function do`, a hand edit, a needs/provides binding, or a trigger is a local override and is preserved through `--upgrade`. Mutations from the original clone or prior upgrades are upgrade-eligible and will be overwritten by newer upstream changes.

`WhereMutation` is empty by default in two cases:

- The Link was created with `cub link create` (rather than auto-created during a clone).
- The Link was reversed via `cub link update --reverse` or `cub link create --reverse`.

An empty `WhereMutation` means the whole downstream unit is eligible to be overwritten — every conflict is resolved in favor of the source. To get the upgrade-style "preserve local overrides" behavior on these Links, you have to populate `WhereMutation` yourself. To go the other direction and clear it on a Link that has one set:

```
cub link update --space prod --patch --where-mutation="" upgrade-backend
```

`WhereMutation` can also be used proactively to "protect" parts of a unit from upgrade — for example, a Link whose `WhereMutation` excludes mutations under `spec.replicas` will never overwrite the downstream's replica count.

### `WhereResource`: scoping which upstream resources participate

`WhereResource` on a Link (or `--where-resource` on the operation) filters the source side: only resources matching the expression contribute to the merge. It is useful for splitting a single upstream unit across multiple downstream units (e.g., separating CustomResourceDefinitions from the rest), or for narrowing an upgrade to a subset of resources.

### Strategic merge patch and merge keys

ConfigHub treats configuration as data, not as text, so it merges resource-by-resource and field-by-field — independent edits to different fields of the same resource don't conflict. For Kubernetes resources, ConfigHub uses its own implementation of [strategic merge patch](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/), which is aware of Kubernetes [merge keys](https://kubernetes.io/docs/reference/using-api/server-side-apply/#merge-strategy) (the `x-kubernetes-patch-merge-key` markers on associative lists like `containers`, `volumes`, `env`, and `ports`).

This has a few important implications:

- **Multi-line string fields are merged as text, except for embedded JSON.** Free-form text (like a shell script in a ConfigMap value) is merged line-by-line rather than character-by-character. ConfigHub auto-detects JSON-valued strings and merges them as structured data instead. For configuration files that you want managed as structured data — properties, env, INI, TOML, YAML, JSON — store them in their own units rather than embedding them as strings in a ConfigMap; see [application configuration](./app-config.md) for the recommended pattern.
- **Renames look like delete + add.** Changing the name of a resource, or the merge key of a list element (e.g., a container's `name`), looks to the merge engine like a deletion of the old element followed by addition of a new one. ConfigHub attempts to detect renames via a similarity heuristic — if the bodies match closely, it carries the renamed element forward in place and preserves any independent downstream overrides on it. If they differ enough to fall outside the heuristic, the downstream sees a delete + add and any downstream overrides on the deleted element are lost.
- **Reordering may not do what you expect.** ConfigHub tries to preserve the order of merge-keyed list elements, but reordering elements both upstream and downstream can produce surprising merge results. If you care about ordering, change it on one side at a time.

If you're unsure how a merge will resolve, dry-run it and inspect the diff:

```
cub unit update --space prod --patch --upgrade --dry-run -o mutations backend-clone
```

`-o mutations` shows the per-path mutations that the merge would apply, which is usually clearer than diffing the resulting YAML by hand.

## Base units

You may choose to maintain _base_ units which you copy to create all of the variants you plan to deploy. Base units are similar to abstract classes in that they aren't intended to be deployed. For example, they need not have attached deployment [targets](../background/entities/target.md). You can use labels to skip these in bulk applies, or just filter out units without targets (where `TargetID IS NOT NULL`).

This approach enables a clearer separation of common attributes and custom attributes.

## Sample units

A _sample_ unit is similar to a base unit in that it isn't intended to be deployed, but it is meant as a starting point for other units rather than as a mechanism for driving changes to a specific set of variants derived from it.

## Placeholders for undetermined values

Base units, and sometimes other new config units, contain default values for most fields, but some may require specific values that cannot yet be provided. This is indicated by _placeholders_.

A [placeholder](../background/concepts/placeholders.md) is a special string or number in the config data which indicates that you _must_ replace it with a real value before the configuration is valid. ConfigHub uses the string "confighubplaceholder" to indicate where a string field needs to be supplied with a value and 9 9s (999999999) for integer fields.

The `vet-placeholders` function can be installed as a trigger to prevent applying configuration units with unreplaced placeholders, and `get-placeholders` can return a list of field paths containing placeholders.

[Triggers](../background/entities/trigger.md) are good for setting general properties of an environment: scale/cost (e.g., replicated vs not, backups vs not), privacy (e.g., public vs not, data with real PII vs not), security (e.g. must be encrypted vs not, can't run as root), and the like.

[Links](../background/entities/link.md) better address more specific resource properties like hostnames, IP addresses, etc.

See [managing dependencies](./dependencies.md) for guidance regarding how to replace placeholders with variant-specific values.
