Reference / Package authoring
Resources and out
Inside a package’s build block, you use scope functions (scope.mkDeployment, scope.mkResource, and others) to create Kubernetes objects. In addition to validating your YAML fields and stamping standard labels, every scope builder returns an object with an .out attribute (such as .out.name or .out.fqdn). Whenever one resource reads another resource’s .out field, Kix automatically records that dependency so everything deploys in the right order.
mkResource
Section titled “mkResource”scope.mkResource constructs a namespaced Kubernetes resource (setting metadata.namespace = scope.namespaceName), while scope.mkClusterResource constructs a cluster-scoped resource (where metadata.namespace is omitted). scope.mkResource always sets a namespace regardless of kind; only scope.helm.importChart automatically dispatches between cluster-scoped and namespaced builders based on its built-in cluster-scoped kind list (APIService, CertificateSigningRequest, ClusterRole, ClusterRoleBinding, CSIDriver, CSINode, CustomResourceDefinition, IngressClass, MutatingWebhookConfiguration, Namespace, Node, PersistentVolume, PriorityClass, RuntimeClass, StorageClass, ValidatingAdmissionPolicy, ValidatingAdmissionPolicyBinding, ValidatingWebhookConfiguration, and VolumeAttachment).
1configMap = scope.mkResource {2 apiVersion = "v1";3 kind = "ConfigMap";4 name = "echo-config";5 data = {6 "config.yaml" = builtins.toJSON {7 listenPort = 8080;8 };9 };10};scope.mkResource and scope.mkClusterResource arguments
Section titled “scope.mkResource and scope.mkClusterResource arguments”| Argument | Type | Default | Description |
|---|---|---|---|
apiVersion | string | Required | Kubernetes API group and version (for example, "v1" or "networking.k8s.io/v1"). |
kind | string | Required | Kubernetes resource kind (for example, "ConfigMap" or "Service"). |
name | string | Required | Resource metadata.name. Must be a valid lowercase RFC 1123 name (or RBAC colon-separated name for Roles and ClusterRoles). |
spec | attrset | { } | Resource .spec body. Omitted from the rendered YAML when empty ({ }). |
data | attrset | { } | Resource .data body (for ConfigMap). Omitted when empty. |
stringData | attrset | { } | Resource .stringData body. Omitted when empty. |
labels | attrset | { } | Additional labels merged onto the cluster and instance labels (clusterLabels // instanceLabels // labels). |
annotations | attrset | { } | Additional metadata.annotations merged with Kix system annotations. |
outputs | attrset | { } | Custom fields merged onto the returned resource’s .out surface. Setting a key to null suppresses an auto-derived .out field. |
requires | list of resources | [ ] | Explicit deployment dependencies that are not already referenced via .out string interpolation inside the manifest. |
extraDeps | list of resources | [ ] | Escape-hatch dependencies used when a dependency does not appear in the manifest and requires is not applicable. |
extra | attrset | { } | Top-level manifest fields for kinds that place configuration outside spec (such as rules, roleRef, subjects, webhooks, or provisioner). |
ownership.cede | list of strings | [ ] | Dot-separated field paths (with optional [*], [<index>], or [<key>="<value>"] list selectors) ceded to in-cluster controllers for Server-Side Apply (kix.run/cede-fields). |
factBehavior | { endpoint?: "conduct" | "terminate" } | { } | Overrides how the network-policy engine treats endpoints on custom resources ("conduct" forwards traffic to backing pods; "terminate" stops at the resource). |
Rules on extra
Section titled “Rules on extra”extra merges top-level fields onto the manifest, subject to strict shadowing checks:
extracannot setapiVersionorkind.extra.metadatacannot setnameornamespace(labelsandannotationsinsideextra.metadatamerge additively with the top-levellabelsandannotationsarguments).extracannot setspec,data, orstringDatawhen the corresponding top-level argument is also non-empty.
ownership.cede, factBehavior, and custom resource readiness
Section titled “ownership.cede, factBehavior, and custom resource readiness”ownership.cede: Lists manifest field paths (such as[ "spec.replicas" ],[ "webhooks[*].clientConfig.caBundle" ], or[ ".webhooks[name=\"x\"].rules" ]) that an in-cluster controller mutates at runtime. Paths use dot-separated field names with[*],[<index>], or[<key>="<value>"]bracket selectors for list elements (.*is treated as a literal field named*, not a wildcard). Kix records these paths inmetadata.annotations."kix.run/cede-fields"so drift detection and Server-Side Apply do not fight the controller. See Server-Side Apply, field ownership, and drift.factBehavior: Controls how Kix’s network-policy engine propagates endpoint facts across custom resources. SetfactBehavior.endpoint = "conduct"when a resource acts like aServiceor proxy in front of backing pods, or"terminate"when the resource is the final destination.readiness(on CRDs and CRs): Custom resource readiness predicates are declared once onscope.mkCRDorscope.mkCRDRefand automatically stamped onto custom resources built withscope.mkCRorcrd.out.mkCRasmetadata.annotations."kix.run/readiness"(pathmust be a JSON pointer string starting with/):- Condition check:
{ condition = { type = "Ready"; status = "True"; }; } - Field equality:
{ path = "/status/phase"; equals = "Running"; } - Field non-equality:
{ path = "/status/currentRevision"; notEquals = ""; }
- Condition check:
Workloads
Section titled “Workloads”Kix provides five workload builders on scope:
| Builder | apiVersion and kind | Supports availability.max |
|---|---|---|
scope.mkDeployment | apps/v1 Deployment | Yes ("none", "pod", "node") |
scope.mkStatefulSet | apps/v1 StatefulSet | Yes ("none", "pod", "node") |
scope.mkDaemonSet | apps/v1 DaemonSet | No (runs one pod per matching node) |
scope.mkJob | batch/v1 Job | Yes ("none", "pod", "node") |
scope.mkCronJob | batch/v1 CronJob | Yes ("none", "pod", "node") |
Use scope.mkOptional cond resource (or if cond then ... else null) when a workload or resource is enabled conditionally.
A Job built with scope.mkJob may not set spec.ttlSecondsAfterFinished; the build refuses it. Kubernetes would delete the finished Job, every comparison with the live cluster would then see it as missing, and Kix would run it again. A finished Job stays until a later build no longer contains it, and Kix deletes it then. Give a Job whose content may change either a name that changes with it (a hash of its inputs) or the kix.run/rerun: on-change annotation, because a Job’s pod template is immutable. A CronJob’s jobTemplate may still set ttlSecondsAfterFinished, because Kubernetes creates those Jobs.
1deployment = scope.mkDeployment {2 name = "api";3 availability.max = "node";4 spec = {5 replicas = kix.availability.pick availability {6 none = 1;7 pod = 2;8 node = 3;9 zone = 3;10 };11 selector.matchLabels = scope.selectorLabels;12 template.spec = {13 serviceAccount = self.serviceAccount;14 containers = [15 {16 name = "api";17 image = kix.image.ref config.image;18 ports = [ (kix.port "http" 8080) ];19 resources = config.resources;20 }21 ];22 };23 };24};Automatic workload processing
Section titled “Automatic workload processing”When you call a workload builder, Kix performs the following steps before sealing the resource:
- Selector propagation: On
Deployment,StatefulSet, andDaemonSet,spec.selector.matchLabelsis required and is automatically merged intospec.template.metadata.labelswhenspec.template.metadata.labelsis omitted. OnJob, eitherspec.selector.matchLabelsorspec.template.metadata.labelsis required (spec.template.metadata.labelsdefaults tospec.selector.matchLabelswhen omitted); onCronJob,spec.jobTemplate.spec.template.metadata.labelsis required. BothmkJobandmkCronJobthrow an error if neither selector nor pod template labels are provided. serviceAccountshorthand: Settingspec.template.spec.serviceAccount = self.serviceAccount(passing theServiceAccountresource directly) setsserviceAccountName = self.serviceAccount.out.name(creating a deployment dependency) and clears the shorthand key.- StatefulSet
headlessServiceshorthand: Onscope.mkStatefulSet, passingspec.headlessService = self.headlessSvcchecks thatself.headlessSvcis aServiceresource in the same namespace and setsspec.serviceName = self.headlessSvc.name(without a string-context dependency, avoiding a cycle with the headless Service’s selector). Cluster validation then verifies thatspec.serviceNamematches a headlessService(clusterIP = "None") in the same namespace. - Availability spread and PDB derivation: For
DeploymentandStatefulSetworkloads withreplicas > 1on a cluster with non-emptycluster.topology.failureDomainKeys(cluster.capabilities.multiNodeis notfalse) and without customspec.template.spec.topologySpreadConstraints, Kix injectstopologySpreadConstraintsacross the cluster’s failure-domain keys (DoNotSchedulefor the zone key at"zone"and for the host key at"node", andScheduleAnywayotherwise) and generates a companionPodDisruptionBudget(maxUnavailable = 1) unless an existingPodDisruptionBudgetin the instance already selects the workload’s pods. See Availability levels and derived pod spread. - Network policy and DNS wiring: When the cluster has a
"dns"role provider, workloads that use default cluster DNS (ClusterFirst) automatically depend on the DNS service and receive egress permission to UDP/TCP port 53. Ifcluster.networkPolicy.enable = true, Kix also synthesizes aCiliumNetworkPolicyfrom the workload’s resource references andmeta.network.intents.
Build-time pod validators
Section titled “Build-time pod validators”scope.mkDeployment, scope.mkStatefulSet, and scope.mkDaemonSet run all nine static checks below at Nix evaluation time, while scope.mkJob and scope.mkCronJob run the five pod-template checks (checkPrivileged, checkNamespacedRefs, checkContainerResources, checkDuplicateNames, and checkFieldRefs):
| Validator | Builders | What it enforces |
|---|---|---|
checkSelectorSubset | mkDeployment, mkStatefulSet, mkDaemonSet | When spec.template.metadata.labels is explicitly set, every key-value pair in spec.selector.matchLabels must be present with the same value in spec.template.metadata.labels. |
checkNamespacedRefs | All five | Every ConfigMap, Secret, PersistentVolumeClaim, and ServiceAccount mounted or referenced by the pod must carry string context (from .out or kix.untrackedRef) and belong to the same metadata.namespace as the workload (unless reflected via mkSecretRef { reflect = true; }). |
checkNamedPorts | mkDeployment, mkStatefulSet, mkDaemonSet | Any string port name referenced by a container’s livenessProbe, readinessProbe, or startupProbe (httpGet.port, tcpSocket.port, or grpc.port) must be declared in that container’s ports list. |
checkContainerResources | All five | When a container sets both requests and limits for cpu or memory, both quantities must fit in a 64-bit integer and limits must be >= requests. |
checkDuplicateNames | All five | Container names (across containers and initContainers), volume names, named ports within a container, and environment variable names within a container must not contain duplicates. |
checkFieldRefs | All five | Downward API env[].valueFrom.fieldRef.fieldPath values must be in the supported Kubernetes pod field path set (metadata.name, metadata.namespace, metadata.uid, metadata.labels, metadata.annotations, metadata.labels['<key>'], metadata.annotations['<key>'], spec.nodeName, spec.serviceAccountName, status.hostIP, status.hostIPs, status.podIP, status.podIPs). |
checkPrivileged | All five | Pods using hostNetwork = true, hostPID = true, a hostPath volume, or securityContext.privileged = true are checked against cluster.capabilities.privilegedContainers (rejected when false, warned when null). |
checkDaemonSet | mkDaemonSet | Checks cluster.capabilities.daemonSets (rejected when false, warned when null). |
checkSpreadFeasible | mkDeployment, mkStatefulSet, mkDaemonSet | Multi-replica workloads (spec.replicas > 1) with required pod anti-affinity or a DoNotSchedule topology spread constraint are checked against cluster.capabilities.multiNode (rejected when false, warned when null). |
Services and service refs
Section titled “Services and service refs”Building Services
Section titled “Building Services”To create a Service targeting a Kix workload, pair kix.service.fromWorkload with scope.mkResource:
1service = scope.mkResource (2 kix.service.fromWorkload {3 name = "api";4 ports = [5 {6 name = "http";7 port = 80;8 targetPort = "http";9 }10 ];11 } self.deployment12);When a Service has spec.type = "LoadBalancer", Kix verifies that the cluster includes a package providing the "loadBalancer" role, or that spec.loadBalancerClass is wrapped with kix.providedLoadBalancerClass { class = "..."; by = providerDep; }.
scope.mkServiceRef
Section titled “scope.mkServiceRef”When an operator creates a Service at runtime (for example, CloudNativePG creating <cluster>-rw after reconciling a Cluster custom resource), the Service manifest does not exist in Nix. Use scope.mkServiceRef to create a typed handle for that runtime Service:
1rwService = scope.mkServiceRef {2 name = "${self.pgCluster.out.name}-rw";3 selector = {4 "cnpg.io/cluster" = self.pgCluster.out.name;5 "cnpg.io/instanceRole" = "primary";6 };7 ports = [8 {9 name = "postgres";10 port = 5432;11 }12 ];13 requires = [ self.pgCluster ];14};scope.mkServiceRef argument | Type | Default | Description |
|---|---|---|---|
name | string | Required | Runtime Service name in scope.namespaceName. |
selector | attrset of strings | Required | Non-empty pod label selector of the operator-managed pods backing this Service. Used to synthesize network policies. |
ports | list of { name?; port; protocol?; } | Required | Non-empty list of ports exposed by the runtime Service. |
labels | attrset | { } | Additional metadata labels on the runtime Service (out.labels). |
requires | list of resources | [ ] | Resources (typically the operator custom resource) that must be applied before the runtime Service will exist. |
scope.mkServiceRef emits a synthetic PackageInstance import marker (service-ref-<instance>-<name>), registers endpoint facts for network-policy derivation, and returns an object with the same .out fields as a standard Service (name, fqdn, port, ports, selector, and labels).
scope.mkWebhookService
Section titled “scope.mkWebhookService”Operators that expose admission webhooks can build the webhook Service alongside any ValidatingWebhookConfiguration and MutatingWebhookConfiguration resources in one call with scope.mkWebhookService:
1webhook = scope.mkWebhookService {2 name = "cert-manager-webhook";3 selector = self.webhookDeployment.out.selector;4 port = 443;5 targetPort = "https";6 validating = [7 {8 name = "webhook.cert-manager.io";9 path = "/validate";10 rules = [11 {12 apiGroups = [ "cert-manager.io" ];13 apiVersions = [ "v1" ];14 operations = [ "CREATE" "UPDATE" ];15 resources = [ "*/*" ];16 }17 ];18 }19 ];20};It returns { service; validatingConfiguration?; mutatingConfiguration?; } (validatingConfiguration and mutatingConfiguration are omitted when their input lists are empty) and automatically wires clientConfig.service (name, namespace, path, and port) on each webhook entry to point to the generated Service.
forNamespace and labels
Section titled “forNamespace and labels”Scope attributes and namespace helpers
Section titled “Scope attributes and namespace helpers”Every package receives a scope configured for its target instances.<name>.namespace. The table below lists the context attributes and namespace helpers on scope:
| Attribute or function | Type | Description |
|---|---|---|
scope.instanceName | string | Name of the current package instance (instances.<name>). |
scope.namespaceName | string | Target Kubernetes namespace name string. |
scope.namespace | resource | Auto-generated v1/Namespace resource for scope.namespaceName. |
scope.namespaces | attrset of resources | Map of all v1/Namespace resources in the cluster (keyed by namespace name). |
scope.forNamespace | string -> scope | Returns a child scope targeting the given namespace name. Resources built with the child scope automatically depend on its v1/Namespace resource. |
scope.forService | dep -> scope | Returns a child scope targeting the namespace of an upstream dependency dep (reads dep._meta.namespaceName, throwing an error if missing). Used when creating resources like RoleBinding or Certificate that must live in a provider’s namespace. |
scope.requireMetricsApi | string -> null | Verifies that the cluster serves metrics.k8s.io for the named subject string (returning null when provided, emitting a warning when "unknown", or throwing when "absent"). HorizontalPodAutoscaler resources run this check automatically in scope.mkResource. |
scope.augmentEdge | [ resources ] -> string -> string | Attaches the ordering and endpoint-fact string context of the passengers resource list to the given string value. |
scope.augmentAllEdges | [ resources ] -> attrset -> attrset | Applies scope.augmentEdge passengers to every string attribute in outAttrs (leaving non-string attributes unchanged). |
scope.forwardEdges | dep -> [ resources ] -> attrset | Re-exports dep.out with passengers’ string context attached to every string value, and merges in any ServiceRef markers from dep.raw. |
1# Deploying a RoleBinding into kube-system while the main package runs in its own namespace2kubeSystemScope = scope.forNamespace "kube-system";3
4leaderElectionBinding = kubeSystemScope.mkResource {5 apiVersion = "rbac.authorization.k8s.io/v1";6 kind = "RoleBinding";7 name = "${scope.instanceName}-leader-election";8 ...9};Label helpers
Section titled “Label helpers”Every resource built with scope.mkResource or scope.mkClusterResource receives clusterLabels // instanceLabels // labels on metadata.labels (where instanceLabels is { "app.kubernetes.io/instance" = instanceName; "app.kubernetes.io/managed-by" = "kix"; } under labelStyle = "kix", or { release = instanceName; "app.kubernetes.io/managed-by" = "kix"; } under labelStyle = "helm"). Workload builders also merge the workload’s selector labels (and "app.kubernetes.io/name" = instanceName on Deployment, StatefulSet, and DaemonSet) into labels.
scope also exposes pre-populated label sets for use in workload selectors and custom resource templates:
| Helper | Keys included | When to use |
|---|---|---|
scope.selectorLabels | app.kubernetes.io/name, app.kubernetes.io/instance (or app, release when scope.labelStyle == "helm", or instances.<ns>.<name>.selectorLabels when overridden) | Default workload spec.selector.matchLabels. |
scope.labels.selector | Alias for scope.selectorLabels | Same as scope.selectorLabels. |
scope.labels.common | scope.selectorLabels // instanceLabels (app.kubernetes.io/name, app.kubernetes.io/instance, and app.kubernetes.io/managed-by = "kix", or app, release, and app.kubernetes.io/managed-by = "kix" when scope.labelStyle == "helm") | Standard combined selector and instance labels for sub-resources or operator templates. |
scope.labels.clusterScoped | app.kubernetes.io/name = scope.instanceName and app.kubernetes.io/managed-by = "kix" | Minimal instance identification labels without selector overrides. |
To switch an instance’s scope.selectorLabels and instanceLabels to Helm’s legacy label keys (app and release), set labelStyle = "helm" on the instance in the cluster definition (instances.<ns>.<name>.labelStyle = "helm").
Common resource fields
Section titled “Common resource fields”Every resource returned by a scope builder is a resource object with the following top-level attributes and .out fields:
Top-level resource attributes
Section titled “Top-level resource attributes”| Attribute | Description |
|---|---|
out | Attribute set of string-context-carrying output fields for downstream consumers (detailed below). |
manifest | Stamped manifest attribute set (rawManifest) with the resource’s identity and forwarded fact string context attached to every string value. |
rawManifest | Manifest attribute set with kix.keep markers resolved and Kix annotations (kix.run/depends-on, kix.run/expects, kix.run/holds-data) stamped, before the resource’s own string context is attached. |
outPath | Path to the resource’s JSON identity file (k8s-<kind>-<ns>-<name>) in Kix’s local store, produced by builtins.toFile. |
rawSelector | (Workloads only) Plain attribute set of spec.selector.matchLabels without string context. See String-context dependency behavior. |
Auto-derived .out fields by resource kind
Section titled “Auto-derived .out fields by resource kind”All string values inside resource.out carry the store-path context of resource, so referencing any resource.out.* value inside another resource’s manifest automatically records a deployment dependency (kix.run/depends-on).
| Resource kind | .out field | Type | Description |
|---|---|---|---|
| All resources | out.name | string | metadata.name with store-path context attached. |
| All resources | out.fqdn | string | "${name}.${namespace}.svc.${clusterDomain}" for namespaced resources (or name for cluster-scoped resources), with connect context attached. |
| All resources | out.identityHash | string | 32-character store hash of the resource’s JSON identity file (outPath), with store-path context attached. |
| All resources | out.selector | attrset of strings | Derived from spec.selector.matchLabels when present, otherwise metadata.labels, with the resource’s store-path context attached to every label value. |
v1/Service | out.ports | list of attrsets | Raw spec.ports list ([ { name?; port; targetPort?; protocol?; } ]) when spec.ports is present. |
v1/Service | out.port | string | First port number in spec.ports as a string (with connect context attached), added automatically when built via kix.service.fromWorkload or scope.mkServiceRef. |
v1/Service | out.url | { scheme ? "http", port ? null } -> string | Builds "${scheme}://${name}.${namespace}.svc.${clusterDomain}:${toString effectivePort}" (with connect context attached), using port when passed or the first port’s .port in spec.ports when port == null. |
v1/Secret (mkSecret / mkSecretRef) | out.keys, out.type, out.keyRef, out.mkEnv, out.envFrom, out.volume | various | Secret consumption helpers. See Secrets, CRDs, and RBAC. |
CustomResourceDefinition (mkCRD / mkCRDRef) | out.cr, out.mkCR | functions | Custom resource constructors bound to the CRD’s apiVersion, kind, and readiness. See Secrets, CRDs, and RBAC. |
Triggering rollouts on ConfigMap changes with out.identityHash
Section titled “Triggering rollouts on ConfigMap changes with out.identityHash”Kubernetes does not automatically restart pods when a mounted ConfigMap changes. Interpolating self.configMap.out.identityHash into spec.template.metadata.annotations both triggers a rolling update whenever the ConfigMap data changes and creates the deployment dependency from the workload to the ConfigMap:
1spec.template.metadata.annotations = {2 "checksum/config" = self.configMap.out.identityHash;3};Customizing .out with outputs
Section titled “Customizing .out with outputs”Pass outputs = { ... }; to any scope.mk* builder to add domain-specific fields to .out. Any string in outputs automatically receives the resource’s store-path context. To suppress an auto-derived field (such as fqdn on a headless Service that should not be addressed directly), set that key to null in outputs:
1outputs = {2 fqdn = null; # Suppresses out.fqdn3 grpcEndpoint = "${name}.${scope.namespaceName}.svc.cluster.local:9090";4};Package-level out surfaces
Section titled “Package-level out surfaces”When package B depends on package A (build = { packageA }: ...), packageA resolves to the consumer view built from A’s root entry:
- Kix starts with
root.resource.out. - If
rootis an attribute set withroot.out, Kix mergesroot.resource.out // root.out(root.outvalues keep whatever string context they already carry; Kix does not automatically attachroot.resource’s context to plain strings inroot.out). - Every key in the merged
outset is promoted to the top level of the dependency object (alongside any top-level part folders and the system keys_type,_packageInstance,root,out,raw, and_meta), so consumers can write eitherpackageA.fqdnorpackageA.out.fqdn. - Any CRD resources in
A’scrds = { ... };part folder are attached at the top level of the view aspackageA.crds.<Kind>(not underpackageA.out, unless explicitly added toroot.out).
Exporting functions and builders on root.out
Section titled “Exporting functions and builders on root.out”root.out can export strings (using .out interpolation or scope.augmentEdge when they should carry dependency context), attribute sets, workload transformers, or resource builders:
1root = {2 resource = self.deployment;3 out = {4 # Carries self.service's store and endpoint context through interpolation5 internalUrl = "http://${self.service.out.fqdn}:8080";6
7 # Workload spec transformer (for example, injecting annotations)8 withReloadOn = configMaps: workloadSpec:9 lib.recursiveUpdate workloadSpec {10 metadata.annotations."reloader.stakater.com/auto" = "true";11 };12
13 # Scoped custom resource builder14 mkServiceMonitor = consumerScope: args:15 self.crds.ServiceMonitor.out.mkCR consumerScope args;16 };17};String-context dependency behavior
Section titled “String-context dependency behavior”How .out references become kix.run/depends-on annotations
Section titled “How .out references become kix.run/depends-on annotations”In Nix, strings derived from a built resource carry hidden metadata called string context listing the store paths needed to produce that string. Kix uses this mechanism to construct the deployment dependency graph:
- Each
scope.mk*resource writes its sealed JSON identity file to Kix’s local store (outPath) and attaches that store path’s context to every string inresource.out. - When resource
BinterpolatesresourceA.out.name,resourceA.out.fqdn, orresourceA.out.selectoranywhere insideB’s manifest,resourceA’s store-path context propagates intoB’s serialized manifest JSON. scope.mkResourcecallsbuiltins.getContextonB’s serialized manifest JSON, extracts the 32-character store hash of every referenced resource (plus"requires:<hash>"for entries inrequiresand"extra:<hash>"for entries inextraDeps), and writes the comma-separated list tometadata.annotations."kix.run/depends-on".
The three ways to record a dependency
Section titled “The three ways to record a dependency”| Mechanism | How it is written | When to use it |
|---|---|---|
Implicit (.out reference) | Use other.out.name, other.out.fqdn, other.out.selector, other.out.keyRef, etc. inside spec, data, labels, or annotations. | Default for all resources where the consumer’s manifest naturally mentions the provider. |
Explicit (requires) | requires = [ self.migrationJob ]; | When resource B must wait for resource A at deploy time, even though B’s YAML manifest does not contain any string from A. |
Escape hatch (extraDeps) | extraDeps = [ ... ]; | Rare cases where a dependency must be recorded outside normal manifest references and requires. |
If you list a resource in requires that is already referenced via .out in the manifest, Kix emits a redundant-requires warning so you can remove the duplicate entry.
Breaking selector cycles with rawSelector
Section titled “Breaking selector cycles with rawSelector”Normally, a Service sets spec.selector = self.deployment.out.selector;, which makes the Service depend on the Deployment (Deployment deploys first, then Service).
Occasionally, a workload’s pod spec needs to interpolate the Service’s DNS name (self.service.out.fqdn) in an environment variable or startup flag. If the Service also uses self.deployment.out.selector, Nix evaluation enters an infinite recursion (deployment -> service -> deployment).
To break the cycle:
- Point the
Serviceatself.deployment.rawSelector(which contains the exact same label key-value pairs, but without Nix string context). - Because
rawSelectoromits the dependency fromServicetoDeployment, the dependency now flowsService->Deployment(viaself.service.out.fqdnin the pod spec).
1deployment = scope.mkDeployment {2 name = "cluster-node";3 spec = {4 selector.matchLabels = scope.selectorLabels;5 template.spec.containers = [6 {7 name = "node";8 env = kix.mkEnvVars {9 PEER_DISCOVERY_HOST = self.headlessService.out.fqdn;10 };11 ...12 }13 ];14 };15};16
17headlessService = scope.mkResource {18 apiVersion = "v1";19 kind = "Service";20 name = "cluster-node-headless";21 spec = {22 clusterIP = "None";23 selector = self.deployment.rawSelector; # Breaks the Nix evaluation cycle24 ports = [ { name = "peer"; port = 7946; } ];25 };26};For StatefulSet headless Services, prefer the built-in spec.headlessService = self.headlessService; shorthand on scope.mkStatefulSet, which wires serviceName without creating a cycle so the Service can keep self.statefulSet.out.selector.
Before serializing a resource manifest to YAML, Kix normalizes the attribute set by stripping null fields bottom-up:
- Any attribute whose value is
nullis removed from its parent attribute set. - Any
nullelement in a list is removed. - Collapse rule: If an attribute set or list had at least one element and all of its elements were removed because they evaluated to
null, the now-empty container itself collapses tonull(and is removed from its parent). - Literal empty rule: An attribute set or list that was already empty (
{ }or[ ]) in the input is preserved as{ }or[ ]in the rendered YAML.
Why kix.keep exists
Section titled “Why kix.keep exists”In Kubernetes YAML, an empty object {} often carries semantic meaning (for example, emptyDir = {}; on a volume, or securityContext = {};). If you construct an attribute set with optional subfields that might all default to null, the collapse rule will remove the entire attribute set when all subfields are null.
Wrapping the attribute set in kix.keep marks it so Kix preserves { } even when all of its child attributes evaluate to null:
1volumes = [2 {3 name = "scratch";4 # Preserved as `emptyDir: {}` even when both sizeLimit and medium are null5 emptyDir = kix.keep {6 sizeLimit = config.scratchSizeLimit; # null by default7 medium = config.scratchMedium; # null by default8 };9 }10];kix.keep behavior
Section titled “kix.keep behavior”| Input | Result after manifest normalization |
|---|---|
{ a = null; b = null; } (without kix.keep) | Collapses to null and is omitted from the parent object. |
kix.keep { a = null; b = null; } | Preserved as { }. |
kix.keep { a = 1; b = null; } | Normalized to { a = 1; }. |
{ } (literal empty attrset) | Preserved as { } automatically (kix.keep is not needed for literal { }). |
[ ] (literal empty list) | Preserved as [ ] automatically. |
kix.keep attaches an internal marker attribute (_kixKeep = true;) to an attribute set (or immediately normalizes a list to [ ] when all of its elements normalize to null). Because _kixKeep is an attribute, it survives both lib.recursiveUpdate and attribute set merge (//), and is resolved when rawManifest is constructed so _kixKeep never appears in rawManifest or the rendered YAML.