Skip to content
Install the CLI

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.

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).

configMap = scope.mkResource {
apiVersion = "v1";
kind = "ConfigMap";
name = "echo-config";
data = {
"config.yaml" = builtins.toJSON {
listenPort = 8080;
};
};
};

scope.mkResource and scope.mkClusterResource arguments

Section titled “scope.mkResource and scope.mkClusterResource arguments”
ArgumentTypeDefaultDescription
apiVersionstringRequiredKubernetes API group and version (for example, "v1" or "networking.k8s.io/v1").
kindstringRequiredKubernetes resource kind (for example, "ConfigMap" or "Service").
namestringRequiredResource metadata.name. Must be a valid lowercase RFC 1123 name (or RBAC colon-separated name for Roles and ClusterRoles).
specattrset{ }Resource .spec body. Omitted from the rendered YAML when empty ({ }).
dataattrset{ }Resource .data body (for ConfigMap). Omitted when empty.
stringDataattrset{ }Resource .stringData body. Omitted when empty.
labelsattrset{ }Additional labels merged onto the cluster and instance labels (clusterLabels // instanceLabels // labels).
annotationsattrset{ }Additional metadata.annotations merged with Kix system annotations.
outputsattrset{ }Custom fields merged onto the returned resource’s .out surface. Setting a key to null suppresses an auto-derived .out field.
requireslist of resources[ ]Explicit deployment dependencies that are not already referenced via .out string interpolation inside the manifest.
extraDepslist of resources[ ]Escape-hatch dependencies used when a dependency does not appear in the manifest and requires is not applicable.
extraattrset{ }Top-level manifest fields for kinds that place configuration outside spec (such as rules, roleRef, subjects, webhooks, or provisioner).
ownership.cedelist 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).

extra merges top-level fields onto the manifest, subject to strict shadowing checks:

  • extra cannot set apiVersion or kind.
  • extra.metadata cannot set name or namespace (labels and annotations inside extra.metadata merge additively with the top-level labels and annotations arguments).
  • extra cannot set spec, data, or stringData when 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 in metadata.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. Set factBehavior.endpoint = "conduct" when a resource acts like a Service or 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 on scope.mkCRD or scope.mkCRDRef and automatically stamped onto custom resources built with scope.mkCR or crd.out.mkCR as metadata.annotations."kix.run/readiness" (path must 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 = ""; }

Kix provides five workload builders on scope:

BuilderapiVersion and kindSupports availability.max
scope.mkDeploymentapps/v1 DeploymentYes ("none", "pod", "node")
scope.mkStatefulSetapps/v1 StatefulSetYes ("none", "pod", "node")
scope.mkDaemonSetapps/v1 DaemonSetNo (runs one pod per matching node)
scope.mkJobbatch/v1 JobYes ("none", "pod", "node")
scope.mkCronJobbatch/v1 CronJobYes ("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.

deployment = scope.mkDeployment {
name = "api";
availability.max = "node";
spec = {
replicas = kix.availability.pick availability {
none = 1;
pod = 2;
node = 3;
zone = 3;
};
selector.matchLabels = scope.selectorLabels;
template.spec = {
serviceAccount = self.serviceAccount;
containers = [
{
name = "api";
image = kix.image.ref config.image;
ports = [ (kix.port "http" 8080) ];
resources = config.resources;
}
];
};
};
};

When you call a workload builder, Kix performs the following steps before sealing the resource:

  1. Selector propagation: On Deployment, StatefulSet, and DaemonSet, spec.selector.matchLabels is required and is automatically merged into spec.template.metadata.labels when spec.template.metadata.labels is omitted. On Job, either spec.selector.matchLabels or spec.template.metadata.labels is required (spec.template.metadata.labels defaults to spec.selector.matchLabels when omitted); on CronJob, spec.jobTemplate.spec.template.metadata.labels is required. Both mkJob and mkCronJob throw an error if neither selector nor pod template labels are provided.
  2. serviceAccount shorthand: Setting spec.template.spec.serviceAccount = self.serviceAccount (passing the ServiceAccount resource directly) sets serviceAccountName = self.serviceAccount.out.name (creating a deployment dependency) and clears the shorthand key.
  3. StatefulSet headlessService shorthand: On scope.mkStatefulSet, passing spec.headlessService = self.headlessSvc checks that self.headlessSvc is a Service resource in the same namespace and sets spec.serviceName = self.headlessSvc.name (without a string-context dependency, avoiding a cycle with the headless Service’s selector). Cluster validation then verifies that spec.serviceName matches a headless Service (clusterIP = "None") in the same namespace.
  4. Availability spread and PDB derivation: For Deployment and StatefulSet workloads with replicas > 1 on a cluster with non-empty cluster.topology.failureDomainKeys (cluster.capabilities.multiNode is not false) and without custom spec.template.spec.topologySpreadConstraints, Kix injects topologySpreadConstraints across the cluster’s failure-domain keys (DoNotSchedule for the zone key at "zone" and for the host key at "node", and ScheduleAnyway otherwise) and generates a companion PodDisruptionBudget (maxUnavailable = 1) unless an existing PodDisruptionBudget in the instance already selects the workload’s pods. See Availability levels and derived pod spread.
  5. 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. If cluster.networkPolicy.enable = true, Kix also synthesizes a CiliumNetworkPolicy from the workload’s resource references and meta.network.intents.

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):

ValidatorBuildersWhat it enforces
checkSelectorSubsetmkDeployment, mkStatefulSet, mkDaemonSetWhen 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.
checkNamespacedRefsAll fiveEvery 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; }).
checkNamedPortsmkDeployment, mkStatefulSet, mkDaemonSetAny 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.
checkContainerResourcesAll fiveWhen 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.
checkDuplicateNamesAll fiveContainer names (across containers and initContainers), volume names, named ports within a container, and environment variable names within a container must not contain duplicates.
checkFieldRefsAll fiveDownward 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).
checkPrivilegedAll fivePods using hostNetwork = true, hostPID = true, a hostPath volume, or securityContext.privileged = true are checked against cluster.capabilities.privilegedContainers (rejected when false, warned when null).
checkDaemonSetmkDaemonSetChecks cluster.capabilities.daemonSets (rejected when false, warned when null).
checkSpreadFeasiblemkDeployment, mkStatefulSet, mkDaemonSetMulti-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).

To create a Service targeting a Kix workload, pair kix.service.fromWorkload with scope.mkResource:

service = scope.mkResource (
kix.service.fromWorkload {
name = "api";
ports = [
{
name = "http";
port = 80;
targetPort = "http";
}
];
} self.deployment
);

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; }.

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:

rwService = scope.mkServiceRef {
name = "${self.pgCluster.out.name}-rw";
selector = {
"cnpg.io/cluster" = self.pgCluster.out.name;
"cnpg.io/instanceRole" = "primary";
};
ports = [
{
name = "postgres";
port = 5432;
}
];
requires = [ self.pgCluster ];
};
scope.mkServiceRef argumentTypeDefaultDescription
namestringRequiredRuntime Service name in scope.namespaceName.
selectorattrset of stringsRequiredNon-empty pod label selector of the operator-managed pods backing this Service. Used to synthesize network policies.
portslist of { name?; port; protocol?; }RequiredNon-empty list of ports exposed by the runtime Service.
labelsattrset{ }Additional metadata labels on the runtime Service (out.labels).
requireslist 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).

Operators that expose admission webhooks can build the webhook Service alongside any ValidatingWebhookConfiguration and MutatingWebhookConfiguration resources in one call with scope.mkWebhookService:

webhook = scope.mkWebhookService {
name = "cert-manager-webhook";
selector = self.webhookDeployment.out.selector;
port = 443;
targetPort = "https";
validating = [
{
name = "webhook.cert-manager.io";
path = "/validate";
rules = [
{
apiGroups = [ "cert-manager.io" ];
apiVersions = [ "v1" ];
operations = [ "CREATE" "UPDATE" ];
resources = [ "*/*" ];
}
];
}
];
};

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.


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 functionTypeDescription
scope.instanceNamestringName of the current package instance (instances.<name>).
scope.namespaceNamestringTarget Kubernetes namespace name string.
scope.namespaceresourceAuto-generated v1/Namespace resource for scope.namespaceName.
scope.namespacesattrset of resourcesMap of all v1/Namespace resources in the cluster (keyed by namespace name).
scope.forNamespacestring -> scopeReturns a child scope targeting the given namespace name. Resources built with the child scope automatically depend on its v1/Namespace resource.
scope.forServicedep -> scopeReturns 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.requireMetricsApistring -> nullVerifies 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 -> stringAttaches the ordering and endpoint-fact string context of the passengers resource list to the given string value.
scope.augmentAllEdges[ resources ] -> attrset -> attrsetApplies scope.augmentEdge passengers to every string attribute in outAttrs (leaving non-string attributes unchanged).
scope.forwardEdgesdep -> [ resources ] -> attrsetRe-exports dep.out with passengers’ string context attached to every string value, and merges in any ServiceRef markers from dep.raw.
# Deploying a RoleBinding into kube-system while the main package runs in its own namespace
kubeSystemScope = scope.forNamespace "kube-system";
leaderElectionBinding = kubeSystemScope.mkResource {
apiVersion = "rbac.authorization.k8s.io/v1";
kind = "RoleBinding";
name = "${scope.instanceName}-leader-election";
...
};

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:

HelperKeys includedWhen to use
scope.selectorLabelsapp.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.selectorAlias for scope.selectorLabelsSame as scope.selectorLabels.
scope.labels.commonscope.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.clusterScopedapp.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").


Every resource returned by a scope builder is a resource object with the following top-level attributes and .out fields:

AttributeDescription
outAttribute set of string-context-carrying output fields for downstream consumers (detailed below).
manifestStamped manifest attribute set (rawManifest) with the resource’s identity and forwarded fact string context attached to every string value.
rawManifestManifest 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.
outPathPath 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.

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 fieldTypeDescription
All resourcesout.namestringmetadata.name with store-path context attached.
All resourcesout.fqdnstring"${name}.${namespace}.svc.${clusterDomain}" for namespaced resources (or name for cluster-scoped resources), with connect context attached.
All resourcesout.identityHashstring32-character store hash of the resource’s JSON identity file (outPath), with store-path context attached.
All resourcesout.selectorattrset of stringsDerived from spec.selector.matchLabels when present, otherwise metadata.labels, with the resource’s store-path context attached to every label value.
v1/Serviceout.portslist of attrsetsRaw spec.ports list ([ { name?; port; targetPort?; protocol?; } ]) when spec.ports is present.
v1/Serviceout.portstringFirst port number in spec.ports as a string (with connect context attached), added automatically when built via kix.service.fromWorkload or scope.mkServiceRef.
v1/Serviceout.url{ scheme ? "http", port ? null } -> stringBuilds "${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.volumevariousSecret consumption helpers. See Secrets, CRDs, and RBAC.
CustomResourceDefinition (mkCRD / mkCRDRef)out.cr, out.mkCRfunctionsCustom 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:

spec.template.metadata.annotations = {
"checksum/config" = self.configMap.out.identityHash;
};

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:

outputs = {
fqdn = null; # Suppresses out.fqdn
grpcEndpoint = "${name}.${scope.namespaceName}.svc.cluster.local:9090";
};

When package B depends on package A (build = { packageA }: ...), packageA resolves to the consumer view built from A’s root entry:

  1. Kix starts with root.resource.out.
  2. If root is an attribute set with root.out, Kix merges root.resource.out // root.out (root.out values keep whatever string context they already carry; Kix does not automatically attach root.resource’s context to plain strings in root.out).
  3. Every key in the merged out set 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 either packageA.fqdn or packageA.out.fqdn.
  4. Any CRD resources in A’s crds = { ... }; part folder are attached at the top level of the view as packageA.crds.<Kind> (not under packageA.out, unless explicitly added to root.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:

root = {
resource = self.deployment;
out = {
# Carries self.service's store and endpoint context through interpolation
internalUrl = "http://${self.service.out.fqdn}:8080";
# Workload spec transformer (for example, injecting annotations)
withReloadOn = configMaps: workloadSpec:
lib.recursiveUpdate workloadSpec {
metadata.annotations."reloader.stakater.com/auto" = "true";
};
# Scoped custom resource builder
mkServiceMonitor = consumerScope: args:
self.crds.ServiceMonitor.out.mkCR consumerScope args;
};
};

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:

  1. 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 in resource.out.
  2. When resource B interpolates resourceA.out.name, resourceA.out.fqdn, or resourceA.out.selector anywhere inside B’s manifest, resourceA’s store-path context propagates into B’s serialized manifest JSON.
  3. scope.mkResource calls builtins.getContext on B’s serialized manifest JSON, extracts the 32-character store hash of every referenced resource (plus "requires:<hash>" for entries in requires and "extra:<hash>" for entries in extraDeps), and writes the comma-separated list to metadata.annotations."kix.run/depends-on".
MechanismHow it is writtenWhen 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.

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:

  1. Point the Service at self.deployment.rawSelector (which contains the exact same label key-value pairs, but without Nix string context).
  2. Because rawSelector omits the dependency from Service to Deployment, the dependency now flows Service -> Deployment (via self.service.out.fqdn in the pod spec).
deployment = scope.mkDeployment {
name = "cluster-node";
spec = {
selector.matchLabels = scope.selectorLabels;
template.spec.containers = [
{
name = "node";
env = kix.mkEnvVars {
PEER_DISCOVERY_HOST = self.headlessService.out.fqdn;
};
...
}
];
};
};
headlessService = scope.mkResource {
apiVersion = "v1";
kind = "Service";
name = "cluster-node-headless";
spec = {
clusterIP = "None";
selector = self.deployment.rawSelector; # Breaks the Nix evaluation cycle
ports = [ { name = "peer"; port = 7946; } ];
};
};

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:

  1. Any attribute whose value is null is removed from its parent attribute set.
  2. Any null element in a list is removed.
  3. 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 to null (and is removed from its parent).
  4. Literal empty rule: An attribute set or list that was already empty ({ } or [ ]) in the input is preserved as { } or [ ] in the rendered YAML.

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:

volumes = [
{
name = "scratch";
# Preserved as `emptyDir: {}` even when both sizeLimit and medium are null
emptyDir = kix.keep {
sizeLimit = config.scratchSizeLimit; # null by default
medium = config.scratchMedium; # null by default
};
}
];
InputResult 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.