Managing dependencies
Links express dependencies between Config Units. They indicate that configuration data should be propagated from the upstream (target) unit to the downstream (source) unit -- the Link direction is the opposite of the direction that data flows. Downstream units link to upstream units that they depend upon. Links also sequence apply and destroy actions based on these dependency relationships.
Link types
The UpdateType field on a Link determines what kind of data propagation is performed:
| UpdateType | Purpose |
|---|---|
None |
Dependency sequencing only; no data propagation |
UpgradeUnit |
Merge upstream data and track upstream revision (for variants) |
MergeUnits |
Merge upstream data without revision tracking |
Insert |
Insert upstream data as a string value at a specific path |
TransformPaths |
Read named values from upstream paths and write Go template or CEL expression results to downstream paths |
NeedsProvides |
Match needed values to provided values (default) |
Sequencing apply and destroy actions
All Links, regardless of UpdateType, affect apply and destroy order in bulk actions. The Links create a DAG (directed acyclic graph) within the set of units being operated upon. Apply is performed in reverse topological order (upstream units first) and destroy is performed in topological order (downstream units first).
The None UpdateType is for Links that exist solely for this sequencing purpose. For example, you may want to ensure that units containing Kubernetes CustomResourceDefinitions are applied before units containing corresponding custom resources:
cub link create --space my-space - my-custom-resources my-crds --update-type None --no-auto-update
Here is a more complete example that links the vote, worker, and result components to redis and/or db, and links all of those units to the namespace, appvote-ns. When they are applied together, appvote-ns is applied first, followed by redis and db, and then vote, result, and worker:
cub link create --space appvote-dev - vote redis
cub link create --space appvote-dev - worker redis
cub link create --space appvote-dev - result db
cub link create --space appvote-dev - worker db
for slug in vote worker result redis db; do
cub link create --space appvote-dev - "$slug" appvote-ns
done
cub unit apply --space appvote-dev
Automatic vs manual resolution
For Link types that propagate data (all except None), resolution can be automatic or manual:
- Automatic: Set
AutoUpdateto true on the Link. The downstream unit is updated asynchronously whenever the upstream unit changes.AutoUpdatemust be false for UpdateTypeNone. - Manual: Leave
AutoUpdatefalse (the default). Trigger resolution explicitly with the--resolveflag onunit update.
To resolve all non-auto-update Links on a unit:
cub unit update --space prod --patch --resolve "Link:*" backend
To resolve a specific Link by name:
cub unit update --space prod --patch --resolve "Link:prod/my-link-slug" backend
To preview what a resolve will do without committing the change, add --dry-run:
cub unit update --space prod --patch --resolve "Link:*" --dry-run backend
When resolving Link:*, Links with UpdateType None are automatically skipped.
UpgradeUnit links
UpgradeUnit Links are used with variants (clones). When you clone a unit, ConfigHub automatically creates an UpgradeUnit Link from the clone (downstream) to the original (upstream). A unit can have at most one outgoing UpgradeUnit Link.
The Link records which upstream revision was last merged and includes a WhereMutation filter so that upgrade operations only overwrite changes that originated from the upstream, preserving local customizations.
Upgrade can be triggered in two ways:
# Using the --upgrade flag (uses the link's WhereMutation filter)
cub unit update --space prod --patch --upgrade backend-clone
# Using --resolve on the UpgradeUnit link
cub unit update --space prod --patch --resolve "Link:*" backend-clone
Both approaches produce the same result. For bulk upgrades across spaces:
cub unit update --space "*" --patch --upgrade --where "UpstreamUnit.Slug = 'backend' AND Space.Labels.Environment = 'prod'"
A unit's UpstreamUnitID is read-only. To establish, change, or remove the upstream relationship after a unit has been created, create, modify, or delete the unit's UpgradeUnit Link instead. See creating and managing variants for more details.
MergeUnits links
MergeUnits Links merge all upstream configuration data into the downstream unit, with downstream changes treated as overrides. This is similar to UpgradeUnit but without updating the unit's UpstreamUnitID and UpstreamRevisionNum. More flexible: a unit can have any number of incoming and outgoing MergeUnits Links.
Merging from live state
When UseLiveState is true, the merge pulls from the upstream unit's LiveState (the actual deployed state) rather than its Data (the desired state). The main use case is to receive data from a renderer bridge, such as ConfigMapRenderer (see application configuration), ArgoCDRenderer (see rendered manifests), or FluxRenderer. For example:
cub link create --space my-space - my-configmap my-config --use-live-state --auto-update --update-type MergeUnits
Because the rendered data is merged with the downstream unit's data, overrides can be applied to the merged result. Note that overrides are not recommended for ConfigMaps with ConfigMapFormat File, since the configuration data is represented as a single string.
Splitting and combining units
With WhereResource, MergeUnits can be used to split one unit into multiple units. For example, separating CustomResourceDefinitions from other resources:
cub link create --space my-space - my-crds source-unit --update-type MergeUnits --where-resource "ResourceType LIKE '%CustomResourceDefinition'"
cub link create --space my-space - my-resources source-unit --update-type MergeUnits --where-resource "ResourceType !~~ '%CustomResourceDefinition'"
It can also combine resources from multiple upstream units into one downstream unit using multiple Links.
WhereMutation filter
For both MergeUnits and UpgradeUnit Links, WhereMutation controls which downstream mutations can be overwritten during merge. This is how local customizations are preserved through upgrades.
Insert links
Insert Links take the entire configuration data from the upstream unit and insert it as a string value at a specific path in the downstream unit. This is useful for embedding structured data from one format into a field of a resource in a different format.
For example, you might store a JSON IAM policy in an AppConfig/JSON unit and insert it into the spec.policy field of a Kubernetes custom resource:
# Create the Kubernetes unit with a placeholder
cub unit create --space my-space ecr-repository ecr-repository.yaml
# Create the JSON policy unit
cub unit create --space my-space ecr-policy ecr-policy.json --toolchain AppConfig/JSON
# Create an Insert link with a Binding identifying the target path
cat <<'EOF' | cub link create --space my-space - ecr-repository ecr-policy --update-type Insert --from-stdin
Bindings:
- NeededResource:
ResourceType: ecr.services.k8s.aws/v1alpha1/Repository
ResourceName: customer-hosted-ns/customer-hosted-app
NeededPath: spec.policy
AutoUpdate: false
EOF
# Resolve the link to perform the insertion
cub unit update --space my-space --patch --resolve "Link:*" ecr-repository
Insert Links require exactly one Binding that specifies NeededResource.ResourceName, NeededResource.ResourceType, and NeededPath to identify the insertion point in the downstream unit. The ProvidedResource and ProvidedPath fields must not be specified in the Binding. Insert Links support both AutoUpdate true (for automatic propagation when the upstream unit changes) and false (for manual resolution). WhereResource can filter the upstream data before insertion.
TransformPaths links
TransformPaths Links read one or more named values from specified paths in the upstream unit and write the result of a Go template or CEL expression to specified paths in the downstream unit. Use them when the downstream value isn't a straight copy — when you want to derive it from one or more upstream values, combine it with Space or Unit metadata, or apply a small transformation.
Unlike NeedsProvides, the upstream and downstream paths are independent — there's no symmetric Binding. Unlike Insert, the value written can be a function of multiple upstream values rather than the entire upstream document.
For example, suppose you maintain the application version in a separate unit and want each environment's Deployment image to be tagged with <version>-<env>, where the environment comes from the downstream Unit's Space slug:
# Upstream unit holds the version in spec.version
cub unit create --space my-space app-version app-version.yaml
# Downstream Deployment unit, in a per-environment Space (e.g. "app-prod")
cub unit create --space app-prod app app-deployment.yaml
# TransformPaths link: read .spec.version from upstream and write the
# expression result to the container image in the downstream Deployment
cat <<'EOF' | cub link create --space app-prod - app my-space/app-version --update-type TransformPaths --auto-update --from-stdin
UpstreamPaths:
- Name: version
Path: spec.version
Resource:
ResourceName: /app-version
ResourceType: example.com/v1/AppVersion
DownstreamPaths:
- Path: spec.template.spec.containers.?name:container-name=web.image
Resource:
ResourceName: default/app
ResourceType: apps/v1/Deployment
Expression: '"nginx:" + params.version + "-" + functionContext.SpaceSlug'
Evaluator: cel
Parameters: [version]
DataType: string
EOF
For Go templates, the same expression reads:
nginx:{{.Params.version}}-{{.SpaceSlug}}
Expression scope
The expression has two top-level scopes:
- The downstream Unit's FunctionContext — for Go templates as top-level fields (
{{.UnitSlug}},{{.SpaceSlug}},{{.UnitLabels.Environment}}, …), for CEL under thefunctionContextvariable (functionContext.UnitSlug, …). Same field names the function handler uses. - The named upstream values — for Go templates under
.Params.<name>, for CEL underparams.<name>. Names come from bothUpstreamPathsandUpstreamGetters(see below).
Parameters and identifier rules
Each PathExpression lists the upstream values it uses in Parameters. Every entry must match a Name from UpstreamPaths or UpstreamGetters on the same Link. The Name itself must be a legal Go and CEL identifier (starts with a letter or underscore, then letters/digits/underscores) so it can appear unquoted in expressions.
DownstreamPaths data types
DataType selects the type written by set-attributes. Supported values are string, int, and bool. The expression always renders to a string; ConfigHub then coerces the result (strconv.Atoi for int, strconv.ParseBool for bool). A coercion failure aborts the resolve.
UpstreamGetters — derive values from a ConfigHub function
When the upstream value is not directly addressable by a single path — it has to be computed from the upstream resources — declare an UpstreamGetters entry. Each entry runs a non-mutating ConfigHub function whose OutputType is AttributeValueList; the Value of the first returned AttributeValue is bound to Name and joins UpstreamPaths in the expression scope.
FunctionInvocation.WhereResource is an additional per-invocation resource filter that is AND-combined with Link.WhereResource. Use it when different getters need to look at different upstream resources within the same multi-resource Unit.
UpstreamGetters:
- Name: apiReplicas
FunctionInvocation:
FunctionName: get-cel
WhereResource: "ConfigHub.ResourceName = 'default/api'"
Arguments:
- Value: '[{"ResourceName": r.metadata.?namespace.orValue("") + "/" + r.metadata.name, "ResourceType": r.apiVersion + "/" + r.kind, "Path": "spec.replicas", "Value": r.spec.replicas}]'
All getters (and UpstreamPaths, via get-paths) run in a single function invocation on the upstream Unit. Worker functions are not supported.
DownstreamSetters — invoke mutating functions on the downstream Unit
DownstreamPaths only writes per-path values. When you need to invoke a mutating function — for example to update many container images with one call to set-image-reference, or to render a partial resource with set-cel — declare a DownstreamSetters entry. Each setter is a FunctionInvocation that runs after argument expansion. FunctionInvocation.WhereResource is AND-combined with Link.WhereResource and lets each setter target a different subset of downstream resources.
DownstreamSetters:
- Parameters: [version]
FunctionInvocation:
FunctionName: set-string-path
Arguments:
- Value: apps/v1/Deployment
- Value: spec.template.spec.containers.0.image
- Value: "nginx:{{.Params.version}}-{{.SpaceSlug}}"
Evaluator: template
Argument Value is template-expanded client-side when the argument's Evaluator is set (template or cel) and the parameter's DataType is string. The scope is the same as for DownstreamPaths expressions, narrowed to the Parameters list on the setter. Non-string parameters and arguments without Evaluator are passed through unchanged. All setters run in a single function invocation on the downstream Unit. Worker functions are not supported.
Evaluation semantics
A TransformPaths resolve runs in two phases:
- Upstream phase — one invocation collects values for
UpstreamPaths(viaget-paths) andUpstreamGetterson the upstream Unit. - Downstream phase —
DownstreamSettersrun in one invocation, thenDownstreamPathsare written in oneset-attributesinvocation. Both summaries are merged.
If any UpstreamPath or UpstreamGetter returns no value (for example, the path doesn't exist after the WhereResource filter, or the getter returns an empty list), the resolve is aborted: nothing is written and the explicit (--resolve) caller gets an error. The auto-update path logs the failure and continues with subsequent Links.
When to choose TransformPaths
- Use NeedsProvides when the upstream attribute is part of the registered needs/provides catalog and you want auto-discovery to match it.
- Use Insert when you want to drop the entire upstream document into one field as a string.
- Use TransformPaths when the path is concrete on both sides and the value needs computation, or when you want to fold in Space/Unit metadata via expressions.
NeedsProvides links
NeedsProvides is the default Link type. It matches needed values in the downstream unit with provided values from the upstream unit, and propagates the provided values to the needed locations. See needs/provides for the conceptual background.
Placeholders (see also managing variants) indicate values that need to be supplied. They can be replaced by various means, but most commonly by creating Links to dependencies or by mutating triggers.
To create a NeedsProvides Link from a backend unit to a ns unit:
cub link create --space prod - backend ns
In this case, the name from a Namespace resource in the ns unit could be inserted into the namespace field of resources in the backend unit where the values are set to confighubplaceholder. ConfigHub discovers that Units containing Namespace resources provide namespace names and that resources like Deployment and Service need them. The identification and extraction of needed and provided values is performed by functions (get-needed and get-provided), and ConfigHub matches them and uses the set-attributes function to update the needed values.
Variants aren't required in order to use NeedsProvides Links, but in Kubernetes the main reason to use placeholder values is to leave them unbound until the configuration is linked to a providing unit.
NeededPaths and ProvidedPaths
NeededPaths and ProvidedPaths are stored on each Unit and updated automatically when the unit's data changes. These stored paths enable efficient matching when Links are resolved.
Bindings
After a NeedsProvides Link is resolved (either automatically or manually), Bindings are stored on the Link recording which values were propagated. For example, a namespace binding:
{
"AttributeName": "resource-name",
"AutoUpdate": true,
"DataType": "string",
"NeededPath": "metadata.namespace",
"NeededResource": {
"ResourceCategory": "Resource",
"ResourceName": "confighubplaceholder/mydep",
"ResourceType": "apps/v1/Deployment"
},
"OriginalValue": "confighubplaceholder",
"ProvidedPath": "metadata.name",
"ProvidedResource": {
"ResourceCategory": "Resource",
"ResourceName": "/test-ns",
"ResourceType": "v1/Namespace"
}
}
Once established, Bindings enable updated values from the upstream unit to continue propagating to the corresponding downstream unit on subsequent resolutions.
Manual bindings
Bindings can also be created manually, with AutoUpdate false in the Binding. This is useful for propagating values to or from non-standard locations or for resource types for which the built-in needs/provides identification hasn't been added yet:
cat <<'EOF' | cub link create --space my-space - subnet route-table --update-type NeedsProvides --auto-update --from-stdin
Bindings:
- AttributeName: resource-name
DataType: string
ProvidedResource:
ResourceType: example.services.k8s.aws/v1alpha1/RouteTable
ResourceName: /my-route-table
ProvidedPath: metadata.name
NeededResource:
ResourceType: example.services.k8s.aws/v1alpha1/Subnet
ResourceName: /my-subnet
NeededPath: spec.routeTableRefs.0.from.name
AutoUpdate: false
EOF
Additionally, you can define Attributes in a Space to register additional paths for automated needs/provides binding with custom resource types.
Using live state
Setting UseLiveState to true on a MergeUnits, Insert, Upsert, or TransformPaths Link causes data propagation to use the upstream unit's LiveState (the actual deployed state) rather than its Data (the desired state). This is useful for propagating information that is only known after deployment, such as dynamically assigned addresses or rendered configuration.