Skip to content

Backing Units

ConfigHub's own entities — Triggers, Filters, Views, Invocations, Attributes, Links, ChangeWorkflows, Targets, Spaces, and Components — are configuration too. A Trigger states a function, its arguments, and which Units it applies to; a Space states which Triggers it runs and where it releases. A backing Unit is a Unit that holds one such entity's configuration as a YAML document, so the entity gets what a Unit already has: a Revision history recording who changed what, diffs, restore and revert, ChangeSets, and validation by Triggers, whose Validation Errors stop an invalid change from reaching the entity.

A backing Unit is optional. An entity without one is created and changed with its cub commands as before. Giving it one is a matter of passing --with-backing-units when creating or updating it, or of applying a document for it with cub apply. See managing entities with backing Units for the commands.

The document

A backing Unit's toolchain is ConfigHub/YAML, and its data is a single document describing one entity, in the entity's own field names, with an EntityType saying what kind of entity it is:

EntityType: Trigger
Slug: ha-replicas
Event: Mutation
ToolchainType: Kubernetes/YAML
FunctionName: vet-celexpr
Arguments:
- ParameterName: validation-expr
  Value: r.kind != "Deployment" || r.spec.replicas > 1
UnitFilter: prod-units
Disabled: false
Warn: false

The document holds the fields an update of the entity can set, and nothing else:

  • Read-only fields are left out. IDs, timestamps, hashes, Version, and everything else ConfigHub computes are not configuration. Neither is HiddenReason, which says how an entity is presented.
  • References are names, not IDs. A field that refers to another entity is named for the entity it refers to, without the ID suffix: UnitFilter, not UnitFilterID, and AllowedChangeWorkflows, not AllowedChangeWorkflowIDs. Its value is the referent's slug when it is in the same Space, and <space>/<slug> when it is in another. A Space or a Component is named by its slug alone, since it is in no Space.
  • The Space is left out. A Space-resident entity's document does not say which Space the entity is in. Together with unqualified names for entities in the same Space, this means the same document describes the "same" Trigger in every Space it is applied to: UnitFilter: prod-units means the Filter of that name in whichever Space the Trigger is in. IDs would point every copy at the original's Filter.
  • Fields are stated even at their zero values, such as Disabled: false, so the document says what the whole configuration is, and keys are sorted.

cub <entity> edit shows an entity as the same document, whether or not the entity has a backing Unit. A document is checked against the entity type's schema when it is written to the entity: a field the type does not have, a name that does not resolve, or a change to a field that cannot change after creation, such as an Invocation's ToolchainType, is refused, naming every problem at once.

One entity per Unit, in the entity's Space

A backing Unit backs exactly one entity, and holds exactly one document. A ConfigHub/YAML Unit with two documents is refused. This is the same reasoning as one Kubernetes resource per Unit, applied to ConfigHub's entities: each entity gets its own Revisions, its own diff, its own Validation Errors, and its own restore, so a change to one Trigger is not entangled with changes to the others.

The entity names its backing Unit in its read-only BackingUnitID. cub <entity> get shows it as a Backing Unit row, and cub unit get on the Unit shows the entity it backs as a Backs row. A backing Unit that ConfigHub creates is named id<entity UUID>, which can't collide with another Unit's slug and never needs renaming, with the display name <EntityType> <entity slug>, which is kept current when the entity is renamed. It is labeled confighub.com/EntityType with the entity's type.

A backing Unit is in the same Space as the entity it backs, so the Space's permissions govern both, and listing a Space's Units finds the configuration of everything in it. Spaces and Components are in no Space, so their backing Units go in a Space you choose, such as a platform team's home Space.

Backing Units are hidden, with the reason BackingUnit in their HiddenReason, so that cub unit list and the UI show the Space's application configuration rather than a Unit for every Trigger and Filter. A list or bulk operation includes them when given --include-hidden=BackingUnit, and a --where that names Units by Slug or ID, or names HiddenReason, selects them without it. Getting a Unit by name is unaffected. Triggers select hidden Units like any others, which is what lets them validate backing Units.

Keeping the entity and its Unit in step

A backing Unit and its entity change in both directions.

From the entity to the Unit. Every write to the entity — from cub <entity> update, the UI, or anything else — renders the entity into its backing Unit, as a Revision whose source is Refresh, attributed to the user who made the write. When the rendering is what the Unit already holds, nothing is written. A Refresh also follows a change that changes the document without changing the entity, such as renaming a Filter that a Trigger names: the Trigger's document names the Filter by its new slug. The first write of an entity created from a document is usually followed by a Refresh too, which fills in the fields the document left out with the values the entity has, such as Disabled: false.

From the Unit to the entity. A change made to the Unit — by editing its data, running a function on it, restoring a Revision, or applying a document — is not written to the entity until you say so, with --from-backing-units on the entity's create or update command, or through cub apply or cub <entity> edit, which do that for you. The write takes what the Unit changed since the Revision the entity last took: the difference between that Revision's document and the Unit's head, applied to the entity as a merge patch. A field the Unit's change removed is cleared on the entity, and a field the change did not touch is left as the entity has it.

The Revision the entity holds is the Unit's LastReleasedRevisionNum, the same field a Release advances for a Unit with a Target. Taking a change from the Unit advances it to the Unit's head, and so does a Refresh. So a backing Unit whose HeadRevisionNum is ahead of its LastReleasedRevisionNum has a change its entity has not taken yet. A backing Unit can't have a Target: what it holds is delivered to its entity by ConfigHub itself, and its ProviderType is ConfigHub.

If the entity is written directly while its Unit has a change it has not taken, the Refresh writes the entity's current state as the Unit's new head. The untaken change is still in the Unit's Revisions, and can be rebased onto the new head like any other change.

Validating entity configuration

Writing an entity from its backing Unit is refused while the Unit has Validation Errors, in the same way a Release is refused for a Unit with them. The Unit keeps the change, to be fixed and taken later, and the entity keeps its configuration. Because a Unit's Triggers run after each change to its data, a change has to wait for them: the awaiting/triggers Validation Error refuses the entity write until they have finished, and cub waits for them.

A Trigger whose ToolchainType is ConfigHub/YAML runs on backing Units. This gives you the same kind of policy over ConfigHub's configuration that you have over application configuration: that every document is valid for its type (vet-schemas), that every where expression in it parses (vet-where-expressions), that no field that can't change was changed (vet-immutable), or any rule you write in CEL (vet-celexpr and vet-cel), such as that no Trigger in a production Space is disabled. See validating backing Unit documents.

Applying documents

cub apply creates or updates entities from documents, the way kubectl apply creates or updates Kubernetes objects from manifests. Each document is written to the entity's backing Unit, which is created if the entity has none, the Unit's Triggers run, and the entity is then created or patched from the Unit. Documents that name each other are written in order: a Filter before the Trigger that names it, and Triggers before the Targets and Spaces that select them.

kubectl apply remembers what it applied last in the last-applied-configuration annotation, so that it can tell a field you removed from a field someone else set. A backing Unit plays that part. Its Revisions and MutationSources record what each source of documents last sent, so applying a document changes what the document changed since it was last applied and nothing else:

  • A field the document changed is set.
  • A field the document stopped stating is removed.
  • A field the document did not change is left as it is, even if it was changed in ConfigHub since: a label someone added with cub <entity> update, an edit with cub <entity> edit, or a value a Refresh filled in. To put back a value changed in ConfigHub, change the document, or restore the backing Unit.

Each backing Unit belongs to the source that applied it, named by --source (cub-apply by default) and recorded in its UploadSource label, much as an ApplySet records the objects it applied. A document for an entity another source applied is refused unless you pass --adopt. An entity that has no backing Unit is adopted by the first document applied to it: it gets a backing Unit made from its current configuration, and the document's fields are then applied.

Removing an entity. A document left out of an apply removes nothing by default. With --prune, the documents given are taken to be all of the source's, and the backing Unit of each entity the source applied before whose document is missing is emptied. An empty backing Unit says its entity should not exist, the way an emptied Kubernetes Unit says its object should not. A Refresh leaves an empty Unit alone, and taking a change from it is refused. Pruning then deletes the entity and keeps the empty Unit, with its history. If the document comes back, the Unit is revived and a new entity is created from it. cub <entity> delete --from-backing-units prunes any entity whose backing Unit is empty, however it was emptied.

Deleting and moving

  • Deleting an entity deletes its backing Unit too, subject to the Unit's delete gates.
  • Deleting a backing Unit is refused while it backs an entity. With --detach, the entity's BackingUnitID is cleared and the entity carries on without a backing Unit.
  • Moving an entity to another Space with cub <entity> move moves its backing Unit with it, in the same operation. A reference to an entity left behind in the original Space gains that Space's name in the document, which is recorded as a Revision. Moving a backing Unit on its own with cub unit move is refused; move the entity instead. The backing Unit of a Space or a Component, which can be in any Space, may be moved freely.

Permissions

Writing an entity from its backing Unit is authorized against the entity, as any write to it is: creating it needs permission to create it in its Space, and patching it needs Edit permission on it. Being able to change the Unit does not by itself let you change the entity. Creating a backing Unit needs permission to create Units in the Space it goes in, which for a Space or a Component is the Space you name for it.

Which entities have backing Units

Triggers, Filters, Views, Invocations, Attributes, Links, ChangeWorkflows, Targets, Spaces, and Components can have backing Units. We plan to add Units, whose document will be their metadata, and Organizations.

Cloning an entity into another Space does not yet clone its backing Unit. We plan to make a clone's backing Unit downstream of the original's, as a cloned Unit is downstream of its upstream, so that a change to a platform team's Triggers in one environment can be promoted to the others with an upgrade, as application configuration is.