Reference / Package authoring
Secrets, CRDs, and RBAC
Mistakes in secret key names, custom resource schemas, or RBAC permission rules are notoriously easy to miss until a pod fails to start in the cluster. Instead of passing those objects to scope.mkResource, you build them with dedicated scope functions (scope.mkSecret, scope.mkCRD, scope.rbac.*) so Kix can verify key names, schema fields, and role bindings during compilation.
Secrets and secret refs
Section titled “Secrets and secret refs”scope.mkResource refuses v1/Secret manifests directly. Instead, create secrets with scope.mkSecret (when Kix renders the Secret resource) or scope.mkSecretRef (when an operator or external controller populates the Secret in the cluster). All secrets defined in a package’s build return set belong in the secrets = { ... }; folder.
1secrets.dbCredentials = scope.mkSecret {2 name = "myapp-db-credentials";3 source = ./secrets/db-credentials.sops.yaml;4 hash = "sha256:4f8b42c22dd3729b519ba6f68d2da7cc5b2d606d05daed5ad5128cc03e6c6358";5 keys = [ "username" "password" "host" ];6 stringData = {7 host = self.postgres.out.fqdn;8 };9 plainKeys = [ "host" ];10};scope.mkSecret arguments
Section titled “scope.mkSecret arguments”| Argument | Type | Default | Description |
|---|---|---|---|
name | string | Required | metadata.name of the Secret. |
keys | list of strings | Auto-derived from stringData, extra.data, and extra.stringData | Declared secret key names. Required when source is set without inline keys or when type = "kubernetes.io/service-account-token". |
type | string | "Opaque" | Kubernetes Secret type (for example, "Opaque", "kubernetes.io/tls", or "kubernetes.io/service-account-token"). |
stringData | attrset of strings | { } | Inline plaintext key-value entries. Marks each key’s provenance as "inline" in kix.run/secret-sources (unless listed in plainKeys), which is flagged by security.secretSource unless meta.classification = "configuration" or "inline" is accepted in cluster.secrets.accept. |
source | path or string or null | null | Path to a SOPS-encrypted file decrypted at deploy time by kix deploy. Marks keys without inline values as "sops". |
hash | "sha256:<64-hex>" or null | null | Expected SHA-256 content hash of the encrypted source file. Required whenever source is a path. |
plainKeys | list of strings | [ ] | Key names whose values are already set in stringData, extra.data, or extra.stringData and hold non-sensitive configuration (such as a username or hostname). Keys listed in plainKeys are recorded with provenance "plain" in kix.run/secret-sources; listing a key without a value throws an evaluation error. |
meta.classification | "configuration" or null | null | Marks an inline Secret as non-credential configuration (used when an upstream application requires a Secret rather than a ConfigMap for its config file). |
meta.rotationPolicy | "manual" | "automated" | "ephemeral" | "immutable" or null | null | Documents how the secret is rotated (kix.run/rotation-policy). |
labels | attrset | { } | Extra metadata.labels. |
annotations | attrset | { } | Extra metadata.annotations. |
requires | list of resources | [ ] | Explicit deployment dependencies. |
extra | attrset | { } | Extra top-level fields (immutable = true, data, stringData, etc.). Keys in extra.data and extra.stringData are counted as valued inline keys alongside stringData. |
Every key in keys must have either a value (in stringData, extra.data, or extra.stringData) or a SOPS source. A key with neither throws an evaluation error, except when type == "kubernetes.io/service-account-token" (where Kubernetes populates the token and Kix records "controller" in kix.run/secret-sources). scope.mkSecret never sets kix.run/cede-fields; for Secrets created and populated at runtime by an operator or external controller, use scope.mkSecretRef.
scope.mkSecretRef
Section titled “scope.mkSecretRef”Use scope.mkSecretRef when an operator or external process creates the Secret object itself at runtime (for example, cert-manager creating a TLS Secret or CloudNativePG creating an application credentials Secret):
1secrets.appUser = scope.mkSecretRef {2 name = "${self.pgCluster.out.name}-app";3 keys = [ "username" "password" "uri" ];4 requires = [ self.pgCluster ];5};scope.mkSecretRef argument | Type | Default | Description |
|---|---|---|---|
name | string | Required | Runtime Secret name in scope.namespaceName. |
keys | list of strings | Required | Keys expected to exist on the runtime Secret. May be [ ] when workloads mount or import the whole Secret (envFrom or volume) without reading individual keys by name. |
type | string | "Opaque" | Expected Secret type, recorded on the marker as kix.run/secret-type. |
requires | list of resources | [ ] | Resources (such as the Certificate or database Cluster CR) that must be applied before the runtime Secret is checked. |
reflect | bool | false | Marks the Secret reference as reflected into scope.namespaceName. |
scope.mkSecretRef emits a synthetic PackageInstance import marker (secret-ref-<instance>-<name>) carrying kix.run/mode: "import", kix.run/secret-type, and (when keys is non-empty) kix.run/import-secret-keys so kix deploy verifies that both the Secret and all declared keys exist at deploy time.
Secret .out consumption helpers
Section titled “Secret .out consumption helpers”Both scope.mkSecret and scope.mkSecretRef return the same .out helpers. Every helper validates requested key names against the Secret’s declared keys at Nix evaluation time and attaches the Secret’s store-path context so the consuming workload automatically depends on the Secret:
.out field or function | Signature | Returned value |
|---|---|---|
out.name | string | Secret metadata.name (with store context and namespace metadata). |
out.keys | list of strings | Declared key names on the Secret. |
out.type | string | Declared Secret type. |
out.keyRef | key -> attrset | { valueFrom.secretKeyRef = { name = out.name; key = key; }; }. Throws an evaluation error if key is not in out.keys. Ready to pass to kix.mkEnvVars. |
out.mkEnv | { ENV_VAR = "key"; ... } -> list | Maps environment variable names to Secret keys, validates every key against out.keys, and returns a sorted list of { name; valueFrom.secretKeyRef; } entries. |
out.envFrom | attrset | { secretRef.name = out.name; } for container envFrom lists. |
out.volume | volName -> attrset | { name = volName; secret.secretName = out.name; } for pod volumes lists (or use kix.mount.secret). |
1# Consuming a Secret via keyRef, mkEnv, and envFrom2env = kix.mkEnv {3 DB_HOST = self.postgres.out.fqdn;4 DB_PASS = self.secrets.dbCredentials.out.keyRef "password";5} (self.secrets.dbCredentials.out.mkEnv {6 DB_USER = "username";7});8
9envFrom = [ self.secrets.dbCredentials.out.envFrom ];To validate keys on a Secret passed across packages as a build dependency, use kix.checkSecretKeys.
CRDs and CRD refs
Section titled “CRDs and CRD refs”Like Secrets, CustomResourceDefinition resources cannot be created with scope.mkResource. Use scope.mkCRD when your package renders the CRD manifest, or scope.mkCRDRef when the CRD is installed by an external component or operator at runtime. Place CRDs in the crds = { ... }; folder of build so Kix exposes them on the dependency view at dep.crds.<Kind> (or on root.out.crds.<Kind> when using Helm mkParts or kix.mkImport { crds = [ ... ]; }, which is accessed at dep.out.crds.<Kind>).
scope.mkCRD arguments
Section titled “scope.mkCRD arguments”1crds.ServiceMonitor = scope.mkCRD {2 spec = (builtins.fromJSON (builtins.readFile ./crds/servicemonitors.json)).spec;3 readiness = null;4 holdsData = false;5};| Argument | Type | Default | Description |
|---|---|---|---|
spec | attrset | Required | Full CustomResourceDefinition .spec (group, names.kind, names.plural, scope, and versions with at least one storage = true and served = true version). metadata.name is derived automatically as "${spec.names.plural}.${spec.group}". |
labels | attrset | { } | Extra metadata.labels. |
annotations | attrset | { } | Extra metadata.annotations. |
ownership.cede | list of strings | [ ] | Field paths ceded to controllers (for example, [ "spec.conversion.webhook.clientConfig.caBundle" ]). |
readiness | attrset or null | null | Default readiness check stamped on custom resources of this CRD ({ condition = { type; status; }; }, { path; equals; }, or { path; notEquals; }). It can also list failed, the states that mean a custom resource will not become ready until its spec changes: each entry is a condition ({ condition = { type; status; reason ? }; }) or a status field ({ path; equals; }), and may set message (the status fields to quote), hint (what to do next), and rejected = true (read a path entry as a rejected spec). kix deploy fails the resource as soon as the controller reports one. See Declare readiness for custom resources. |
factBehavior | { endpoint = "conduct" | "terminate"; podSelector?; podNamespace?; selfScrapes?; } or null | null | Default network-policy endpoint fact behavior for custom resources of this CRD. When non-null, endpoint is required, and "terminate" also requires podSelector (an attrset of labels or a function cr: attrs). |
usesAtLeast | (cr: usage) or null | null | Function from a custom resource to the least it always uses, keyed like a ResourceQuota’s spec.hard (pods, requests.cpu, requests.storage, and so on). Kix adds it to the namespace’s ResourceQuota check. Return { } when objects of this kind run no pods. See Declaring quota usage. |
holdsData | bool or null | null | Set to true when custom resources of this CRD manage stateful data (such as database clusters or object-store buckets), or false when they do not. Propagates kix.run/holds-data onto custom resources for deletion-protection checks. When null, no annotation is written (and stateful packages that create custom resources of this CRD fail build validation until holdsData is declared). |
scope.mkCRDRef arguments
Section titled “scope.mkCRDRef arguments”Use scope.mkCRDRef when a package creates custom resources for a CRD that is installed outside the package’s own manifests (or dynamically by an operator):
1crds.Certificate = scope.mkCRDRef {2 group = "cert-manager.io";3 kind = "Certificate";4 plural = "certificates";5 version = "v1";6 crdScope = "Namespaced";7 installedBy = deps.certManager.root;8 readiness = {9 condition = {10 type = "Ready";11 status = "True";12 };13 };14};| Argument | Type | Default | Description |
|---|---|---|---|
group | string | Required | API group (for example, "cert-manager.io"). |
kind | string | Required | PascalCase kind name (for example, "Certificate"). |
plural | string | Required | Lowercase plural name (for example, "certificates"). |
version | string | Required | Default API version used when building custom resources of this CRD (throws an evaluation error if omitted or null). |
servedVersions | list of strings | [ version ] | All served API versions accepted by the CRD. |
crdScope | "Namespaced" | "Cluster" | "Namespaced" | Whether custom resources of this CRD are namespaced or cluster-scoped. |
installedBy | resource, list of resources, or null | null | Operator workload or resource (or list of resources) that installs this CRD at runtime. Attaches its store context to the CRD ref marker so custom resources built with scope.mkCR wait for the installer. Every custom resource in a build must depend on a CRD, a CRD ref marker, or an APIService (enforced by validation check 21). |
readiness | attrset or null | null | Default readiness predicate stamped on custom resources of this CRD, in the same form as for scope.mkCRD, including failed entries with an optional hint. |
factBehavior | { endpoint = "conduct" | "terminate"; podSelector?; podNamespace?; selfScrapes?; } or null | null | Default network-policy endpoint behavior for custom resources of this CRD (same requirements as on scope.mkCRD). |
usesAtLeast | (cr: usage) or null | null | The least a custom resource always uses, as for scope.mkCRD. |
holdsData | bool or null | null | Whether custom resources of this CRD hold stateful data (true, false, or null when undeclared). |
Creating custom resources (scope.mkCR, crd.out.mkCR, and crd.out.cr)
Section titled “Creating custom resources (scope.mkCR, crd.out.mkCR, and crd.out.cr)”Once a CRD is defined via scope.mkCRD or scope.mkCRDRef (or received from a dependency at dep.crds.<Kind> or dep.out.crds.<Kind>), construct custom resources using any of these three equivalent forms (crd.mkCR and crd.cr are also available directly on the handle):
scope.mkCR crd args: Builds the custom resource inscope, automatically settingapiVersion("${crd.group}/${crd.version}"orargs.version),kind, cluster vs. namespaced scoping,readiness,factBehavior,holdsData,usesAtLeast, and the deployment dependency oncrd.crd.out.mkCR consumerScope args(orcrd.mkCR consumerScope args): Identical toconsumerScope.mkCR crd args. Convenient when calling a CRD exported by a dependency (prometheus.out.crds.ServiceMonitor.mkCR scope { name = "echo"; spec = { ... }; }oroperator.crds.Widget.out.mkCR scope { name = "w"; spec = { ... }; }).scope.mkResource (crd.out.cr args)(orcrd.cr args): Returns the attribute set forscope.mkResource(orscope.mkClusterResourcewhencrdScope == "Cluster").
Declaring quota usage (usesAtLeast and kix.usage)
Section titled “Declaring quota usage (usesAtLeast and kix.usage)”When a namespace has a ResourceQuota, Kix adds up the least its objects always use and refuses the build when that total is over a limit. Built-in workloads (Deployments, StatefulSets, Jobs, Pods) are counted from their pod templates. A custom resource is counted only when its kind declares usesAtLeast: a function from the custom resource to its usage, keyed like a ResourceQuota’s spec.hard. Set it where the kind is declared: on scope.mkCRD, on scope.mkCRDRef, or in the crds entry of kix.mkImport.
Declare usesAtLeast on every kind the operator always turns into pods or claims, with kix.usage:
| Helper | Signature | Description |
|---|---|---|
kix.usage.ofPod | containers or podSpec -> usage | One pod’s usage from its regular containers (pods = 1, plus requests.* and limits.*). A container’s missing request counts as its limit, as in Kubernetes. |
kix.usage.times | count -> usage -> usage | Multiplies every value by count. |
kix.usage.add | [ usage ] -> usage | Adds a list of usages. Values may be integers or quantity strings ("20Gi", "500m"). |
1usesAtLeast =2 cr:3 kix.usage.times (cr.spec.instances or 1) (4 kix.usage.ofPod [ { resources = cr.spec.resources or { }; } ]5 );Count only what the operator always runs and whose size the custom resource sets. A figure that is too high fails a valid build, so when unsure, leave a key out.
For a kind whose objects run no pods (a monitoring rule, a certificate, a policy), declare usesAtLeast = _: { }. Kix then knows the kind adds nothing and leaves it out of the refusal message. A declaration that returns { } for any other reason, such as a value it reads being unset, also means “uses nothing”, so return the real amount or leave the kind undeclared.
When a namespace is over a quota, the error lists what Kix counted and what it could not count, so you can see that the real usage may be higher:
1namespace "app" exceeds ResourceQuota "app-quota" for pods (uses at least 2, limit is 1)2 Kubernetes will reject pods over this limit at deploy time.3
4 Counted (2 pods):5 Deployment/w: 2 pods6
7 Not counted (actual usage may be higher):8 DaemonSet/w: 1 pod per matching node (Kix does not know how many nodes the cluster has)9 35 PrometheusRule, 1 Widget: pod usage is not declared by their packages (most custom resources create no pods, but Kix cannot verify this)10
11 To fix this, raise the pods limit in "app-quota" or run fewer pods in namespace "app".A custom kind under “Not counted” means its package declares no usesAtLeast. Add one to the kind’s declaration to have Kix count it, or _: { } to drop it from the list.
CRD spec construction helpers (scope.crd.*)
Section titled “CRD spec construction helpers (scope.crd.*)”When authoring a CRD schema in Nix instead of loading a vendored JSON file, use the composable transformers in scope.crd, which build an attribute set you can pass to scope.mkCRD:
| Helper | Signature | Description |
|---|---|---|
scope.crd.fromKind | kind -> extra -> crdAttrs | Creates a base Namespaced CRD definition for PascalCase kind (plural = "${lib.toLower kind}s", singular = lib.toLower kind, listKind = "${kind}List", one served storage version "v1", subresources.status = { }, and a base openAPIV3Schema with spec and status), deep-merged with extra (such as { spec.group = "example.com"; }). |
scope.crd.clusterScoped | crdAttrs -> crdAttrs | Sets spec.scope = "Cluster". |
scope.crd.withVersion | version -> crdAttrs -> crdAttrs | Sets name = version on every version entry in spec.versions. |
scope.crd.withShortNames | [ strings ] -> crdAttrs -> crdAttrs | Sets spec.names.shortNames. |
scope.crd.withCategories | [ strings ] -> crdAttrs -> crdAttrs | Sets spec.names.categories. |
scope.crd.withPrinterColumns | [ columns ] -> crdAttrs -> crdAttrs | Sets additionalPrinterColumns on all versions in spec.versions. |
scope.crd.withSchema | specSchema -> crdAttrs -> crdAttrs | Replaces schema.openAPIV3Schema.properties.spec on all versions in spec.versions. |
scope.crd.withLabels | labels -> crdAttrs -> crdAttrs | Replaces top-level labels on the CRD definition. |
scope.crd.withAnnotations | annotations -> crdAttrs -> crdAttrs | Merges annotations onto the CRD definition’s top-level annotations. |
RBAC helpers
Section titled “RBAC helpers”Creating a ServiceAccount
Section titled “Creating a ServiceAccount”Create a ServiceAccount with scope.mkResource and pass it to a workload’s spec.template.spec.serviceAccount shorthand:
1serviceAccount = scope.mkResource {2 apiVersion = "v1";3 kind = "ServiceAccount";4 name = "my-operator";5};6
7deployment = scope.mkDeployment {8 name = "my-operator";9 spec = {10 selector.matchLabels = scope.selectorLabels;11 template.spec = {12 serviceAccount = self.serviceAccount; # Sets serviceAccountName and deployment dependency13 containers = [ ... ];14 };15 };16};Policy rule DSL (scope.rbac.*)
Section titled “Policy rule DSL (scope.rbac.*)”Inside a package, use scope.rbac (also exposed on the top-level cluster library as kix.rbac, though not on the package kix argument) to construct RBAC policy rules by API group and intent instead of writing raw apiGroups, resources, and verbs arrays by hand:
1rules =2 let3 inherit (scope.rbac) core group restrictTo grant watch reconcile readWrite emit;4 certManager = group "cert-manager.io" [ "certificates" "issuers" ];5 in6 grant [7 # Watch and update status on Certificates and Issuers in cert-manager.io8 (reconcile [ certManager.certificates certManager.issuers ])9
10 # Read Secrets and ConfigMaps in the core ("") API group11 (watch [ core.secrets core.configmaps ])12
13 # Update a specific ConfigMap by resourceName14 (readWrite (restrictTo [ "my-operator-leader" ] core.configmaps))15
16 # Emit Kubernetes Events17 (emit core.events)18 ];Target constructors and modifiers
Section titled “Target constructors and modifiers”| Function or value | Signature | Description |
|---|---|---|
scope.rbac.group | apiGroup -> [ resources ] -> attrset | Returns an attribute set mapping each resource name in resources to a target ref ({ apiGroup; resource; resourceNames = null; }). Also defines __functor, so the returned set can be called as a function grp "otherresource" for ad-hoc resource names in apiGroup. |
scope.rbac.core | attrset (callable as string -> ref) | Pre-built group for the core "" API group with attributes configmaps, endpoints, events, namespaces, nodes, persistentvolumeclaims, persistentvolumes, pods, secrets, serviceaccounts, and services (plus __functor for other core resources, such as core "replicationcontrollers"). |
scope.rbac.sub | suffix -> ref -> ref | Returns a target ref for the subresource "${ref.resource}/${suffix}" (where suffix is a single string such as "status" or "finalizers", with resourceNames = null). |
scope.rbac.restrictTo | [ resourceNames ] -> ref -> ref | Restricts a single target ref to the listed resourceNames. |
Intent constructors and rule normalizer
Section titled “Intent constructors and rule normalizer”Each intent function accepts either a single target ref or a list of target refs and returns a list of policy rule attribute sets. Pass a list of intent results to scope.rbac.grant to flatten and merge rules that share the same apiGroups, verbs, and resourceNames:
| Function | Signature / Verbs granted | Subresources added |
|---|---|---|
scope.rbac.watch | [ "get" "list" "watch" ] | None |
scope.rbac.reconcile | [ "get" "list" "watch" ] on ref, [ "patch" "update" ] on ref, and [ "patch" "update" ] on sub "status" ref | status |
scope.rbac.finalize | [ "update" ] on sub "finalizers" ref only | finalizers (only) |
scope.rbac.watchAndFinalize | [ "get" "list" "watch" ] on ref, plus [ "update" ] on sub "finalizers" ref | finalizers |
scope.rbac.own | [ "get" "list" "watch" "create" "delete" ] | None |
scope.rbac.readWrite | [ "get" "list" "watch" "create" "update" "delete" ] | None |
scope.rbac.full | [ "get" "list" "watch" "create" "update" "delete" "patch" ] | None |
scope.rbac.emit | [ "create" "patch" ] | None (designed for core.events) |
scope.rbac.allow | [ verbs ] -> refOrRefs -> [ rules ] | Custom verb list |
scope.rbac.grant | [ intentLists ] -> [ rules ] | Flattens a list of intent rule lists and merges rules with identical (apiGroups, verbs, resourceNames). |
Role and RoleBinding set builders (mkRoleSet and mkClusterRoleSet)
Section titled “Role and RoleBinding set builders (mkRoleSet and mkClusterRoleSet)”scope.rbac.mkRoleSet and scope.rbac.mkClusterRoleSet generate paired Role + RoleBinding (or ClusterRole + ClusterRoleBinding) parts in a single call:
1rbac =2 let3 inherit (scope.rbac) core group grant reconcile watch;4 widgets = group "example.com" [ "widgets" ];5 in6 scope.rbac.mkClusterRoleSet scope {7 subjects = [8 {9 kind = "ServiceAccount";10 name = self.serviceAccount.out.name;11 namespace = scope.namespaceName;12 }13 ];14 roles = {15 "my-operator-controller" = grant [16 (reconcile widgets.widgets)17 (watch [ core.pods core.services ])18 ];19 };20 };| Builder | Signature | Returned parts attrset |
|---|---|---|
scope.rbac.mkRoleSet | scope -> { subjects, roles, labels ? { } } | For each <roleName> in roles, returns <roleName> (Role) and "${roleName}-binding" (RoleBinding). |
scope.rbac.mkClusterRoleSet | scope -> { subjects, roles, labels ? { } } | For each <roleName> in roles, returns <roleName> (ClusterRole) and "${roleName}-binding" (ClusterRoleBinding). |
Each entry in roles can be a list of policy rules (roles.<roleName> = [ ... ]) or { rules = [ ... ]; bindingName ? roleName; }. The subjects list is passed through unchanged to the binding’s subjects field, so each entry must be a Kubernetes subject attribute set ({ kind = "ServiceAccount"; name = self.serviceAccount.out.name; namespace = scope.namespaceName; }). Referencing self.serviceAccount.out.name attaches its Nix string context so the binding automatically depends on the ServiceAccount.