Reference / Package authoring
Package anatomy
A Kix package is a reusable blueprint for an application or cluster service. It declares the settings operators can customize (options), the other services it needs (build arguments), and the Kubernetes objects it creates (build). Reach for this page when you are writing a new package from scratch or adding typed configuration options to an existing one.
1{2 scope,3 lib,4 kix,5 availability,6 ...7}:8
9{10 meta = {11 version = "1.10.0";12 description = "HTTP echo server for testing";13 availability.max = "node";14 };15
16 options = {17 image = kix.options.image {18 repository = "hashicorp/http-echo";19 tag = "1.0.0";20 digest = "sha256:fcb74f691c8b0bd1ebff992c0a3ce2bf29f61ccb2f3d85fe72c4bd8b34e05061";21 };22 replicas = lib.mkOption {23 type = lib.types.ints.positive;24 default = kix.availability.pick availability {25 none = 1;26 pod = 2;27 node = 3;28 zone = 3;29 };30 description = "Number of echo server pods.";31 };32 resources = kix.options.resources {33 requests = {34 cpu = "50m";35 memory = "64Mi";36 };37 limits.memory = "64Mi";38 };39 ingress = kix.options.ingress;40 };41
42 build = [43 (44 { self, config }:45 {46 deployment = scope.mkDeployment {47 name = "echo";48 spec = {49 replicas = config.replicas;50 selector.matchLabels = scope.selectorLabels;51 template.spec.containers = [52 {53 name = "echo";54 image = kix.image.ref config.image;55 ports = [ (kix.port "http" 5678) ];56 resources = config.resources;57 }58 ];59 };60 };61
62 service = scope.mkResource (63 kix.service.fromWorkload { name = "echo"; } self.deployment64 );65
66 root = self.service;67 }68 )69
70 (kix.expose { service = "service"; })71 ];72}Root function and return shape
Section titled “Root function and return shape”A package file is either a Nix function taking system arguments or a plain attribute set containing a build attribute.
When written as a function, Kix calls it with four Phase 1 arguments before evaluating options:
| Outer argument | Type | Description |
|---|---|---|
scope | attrset | Instance-scoped builders (mkResource, mkDeployment, mkSecret, mkCRD, forNamespace, selectorLabels, and related helpers). |
lib | attrset | Standard nixpkgs.lib. |
kix | attrset | Framework helpers (kix.options, kix.availability, kix.image, kix.expose, kix.monitor, kix.mount, kix.contracts, and related helpers). |
availability | string | Effective availability level for the instance ("none", "pod", "node", or "zone"), clamped to meta.availability.max. Map it to replica counts or settings with kix.availability.pick availability { none = ...; pod = ...; node = ...; zone = ...; } and kix.availability.atLeast "<min>" availability. |
Include ... in the outer function header ({ scope, lib, kix, ... }:) so the package accepts future framework arguments. Do not declare cross-package dependencies in the outer function header; declare them in build entries instead.
Top-level package attributes
Section titled “Top-level package attributes”The package returns an attribute set with the following top-level keys:
| Attribute | Type | Required | Description |
|---|---|---|---|
meta | attrset | No | Package metadata, platform role declarations, availability ceiling, compatibility bounds, and network intents. |
options | attrset | No | NixOS module option declarations (lib.mkOption or kix.options.*) exposed to instances.<name>.config. |
config | attrset or module function | No | Package-level configuration module merged with options. Values inside config that the author wraps in lib.mkDefault are lowered from priority 1000 to 1250 (between option defaults at 1500 and instance or injected lib.mkDefault values at 1000). |
deps | attrset | No | Per-dependency configuration injection (config), warnings, and assertions keyed by build argument name. |
build | function or list of functions | Yes | One or more functions returning named resource parts and a root entry. |
warnings | { config, self, deps }: [ string ] | No | Evaluation-time warnings collected into cluster.warnings. |
assertions | { config, self, deps }: [ { assertion; message; } ] | No | Evaluation-time assertions checked before building manifests. |
The root entry and part reachability
Section titled “The root entry and part reachability”Across all build entries, the merged parts attribute set must define root. root designates the primary entry point of the package and determines what downstream packages receive when they depend on this instance.
root accepts either a direct resource or an extended attribute set:
1# Direct resource2root = self.service;3
4# Extended root with extra dependencies, post-deploy hooks, and custom outputs5root = {6 resource = self.service;7 requires = [ self.migrationJob ];8 postHooks = [ self.smokeTest ];9 out = {10 endpoint = "http://${self.service.out.fqdn}:8080";11 };12};root field | Type | Default | Description |
|---|---|---|---|
resource | resource | Required | Primary resource of the package. Must also be assigned to a named attribute in build (for example, service = ...; root = self.service;). |
requires | list of resources | [ ] | Additional resources that must be ready before the package instance is considered ready. |
postHooks | list of resources | [ ] | Post-deployment resources (such as kix.healthCheck jobs) that run after root.resource and requires are ready. |
out | attrset | { } | Extra fields merged onto root.resource.out and promoted to the top level of the dependency object passed to consumers. |
Every resource defined in build must be reachable from root.resource, root.requires, or root.postHooks (either because root depends on it through .out string context or requires, or because it depends on a reachable resource such as an RBAC RoleBinding referencing a reachable ServiceAccount). Resources that are disconnected from root emit an orphaned-resource warning during evaluation.
root.requires and a resource’s own requires order different things. A resource’s requires becomes a kix.run/depends-on annotation on that resource, so deploy applies it after the listed resources are ready. root.requires extends the dependencies of the package’s PackageInstance, so consumers of the package wait for the listed resources, but nothing inside the package does. An operator that watches kinds its own package defines therefore puts the CRDs in the controller’s requires (requires = builtins.attrValues self.crds; on the mkDeployment call). With the CRDs only in root.requires, a first deploy can start the controller before the API server serves its kinds, and the controller crashes and restarts until it does. The scorecard rule architecture.controllerRequiresCrds warns about this shape.
meta declares static package properties that Kix inspects during dependency binding, cluster validation, and scorecard evaluation.
1meta = {2 version = "2.51.0";3 description = "Prometheus monitoring system and time series database";4 roles = [ "metricsApi" ];5 defaultAliases = [ "monitoring" ];6 availability.max = "node";7 podSecurity = "restricted";8 lifecycle = "stateful";9 owner = "platform-observability";10 annotationDomains = [ "monitoring.coreos.com" ];11 compatibleWith.grafana = {12 minVersion = "10.0.0";13 reason = "Requires Grafana 10+ data source provisioning format";14 };15 k8s = {16 minVersion = "1.28";17 maxVersion = "1.32";18 };19};Summary of meta fields
Section titled “Summary of meta fields”| Field | Type | Default | Description |
|---|---|---|---|
version | string | None | Upstream software version ([vV]?<digits>(.<digits>)*[-<suffix>][+<build>]). Checked by meta.compatibleWith and recorded on the PackageInstance. |
description | string | None | Non-empty one-line summary of the package. Checked by the governance.hasDescription scorecard rule (info severity). |
roles | list of strings | [ ] | Platform capabilities provided by this package. |
defaultAliases | list of strings | [ ] | Default dependency-injection names registered when instances.<name>.aliases is empty. |
availability.max | "none" | "pod" | "node" | "zone" | None | Highest availability level the package supports. |
compatibleWith | attrset of { minVersion?; maxVersion?; reason?; } | { } | Version bounds for upstream dependencies keyed by dependency argument name. |
k8s | { minVersion?; maxVersion?; reason?; } | None | Supported Kubernetes minor or patch version range, checked against k8sVersion. |
conflictsWith | list of strings | [ ] | Packages that cannot coexist with this package ("<name>" for same-namespace, "cluster:<name>" for cluster-wide). |
podSecurity | "privileged" | "baseline" | "restricted" | None | Pod Security Standards enforcement level applied to the instance namespace. |
unsafe | list of strings | [ ] | Acknowledged security or architectural exceptions ("raw-pvc", "host-network", "privileged", "run-as-root", "cluster-admin"). |
annotationDomains | list of strings | [ ] | Annotation domain prefixes (such as "cert-manager.io") owned and consumed by this package. |
network | attrset | { } | Ambient network intents (intents.<part>.{egress,ingress}) and externalIngressEntities. |
lifecycle | "stateful" | None | Marks the PackageInstance with kix.run/lifecycle: stateful. |
scope | "cluster" | "namespace" | unset | "cluster": consumers in every namespace find an instance by name, and auto-instantiation places the package once per cluster (if a second namespace needs it from availablePackages, evaluation fails; declare one instance instead). "namespace": an instance is never used from another namespace, not even with ref. Unset: an instance in another namespace is used only when the consumer names it with deps.<arg> = ref.<namespace>.<instance>. Packages that claim a role or declare a cluster: conflict still serve the whole cluster, as do instances that Kix creates from availablePackages or enables via optional = true to satisfy a dependency. A kix.mkImport stand-in defaults to "cluster". |
owner | string | None | Team or owner identifier used by governance.hasOwner and byOwner policy rules. |
deprecated | { message; replacement?; } | None | Emits an evaluation warning when the package is instantiated. |
meta.version
Section titled “meta.version”Version strings must match <digits>(.<digits>)* with an optional leading v or V, an optional -<suffix>, and an optional +<build> (for example, "1.28.2", "v2.4.0-rc1", "1.31.2-gke.1209000", or "1.31.2+rke2r1").
When comparing versions in compatibleWith and k8s:
- Numeric segments are compared as integers from left to right.
- A
-<suffix>whose first dot-separated identifier starts withalpha,beta,rc,pre,dev, orpreviewis treated as a pre-release and sorts before the corresponding release (1.0.0-rc1 < 1.0.0). - All other
-<suffix>values (such as-r0,-debian-12, or-gke.1209000) and+<build>suffixes are treated as vendor build metadata and compare equal to the base version (1.0.0-debian-12 == 1.0.0).
meta.roles and meta.defaultAliases
Section titled “meta.roles and meta.defaultAliases”meta.roles registers the instance as a provider of one or more cluster roles from a closed vocabulary:
| Role | Required out fields | Effect |
|---|---|---|
"cni" | None | Satisfies the cluster CNI capability check. |
"dns" | fqdn | Provides cluster DNS resolution; workloads automatically receive a dependency and network egress rule to the DNS service. |
"loadBalancer" | None | Satisfies Service resources of type = "LoadBalancer". |
"metricsApi" | None | Satisfies consumers that call scope.requireMetricsApi (such as HorizontalPodAutoscaler). |
"network-policy-enforcer" | netpol | Enables cluster-wide NetworkPolicy enforcement when cluster.networkPolicy.enable = true. |
"registryMirror" | None | Registers a cluster container registry mirror. |
"snapshotController" | crds | Provides the VolumeSnapshot CRD family for backup and restore workflows. |
"storageClasses" | storageClassName | Provides a dynamic StorageClass for PersistentVolumeClaim resources. |
Each role in meta.roles also registers that role name as a dependency alias so other packages can request { dns ? null }: or { loadBalancer ? null }:. At most one active instance in a cluster may provide a given role unless disambiguated.
meta.defaultAliases provides additional dependency-injection names when the instance does not set instances.<name>.aliases. If an instance sets an aliases list, even an empty one, defaultAliases are replaced while meta.roles remain active.
meta.availability
Section titled “meta.availability”meta.availability.max declares the highest availability topology the package architecture supports ("none", "pod", "node", or "zone"). When a cluster requests a higher level (for example, "zone"), Kix clamps availability inside the package to meta.availability.max so replica counts and topologySpreadConstraints never exceed what the software can safely run.
Individual workloads can further clamp their own ceiling by passing availability.max to scope.mkDeployment, scope.mkStatefulSet, scope.mkJob, or scope.mkCronJob.
meta.compatibleWith and meta.k8s
Section titled “meta.compatibleWith and meta.k8s”meta.compatibleWith.<dep> checks the meta.version of the bound <dep> provider at evaluation time:
1meta.compatibleWith.postgres = {2 minVersion = "15";3 maxVersion = "17";4 reason = "Uses PostgreSQL 15+ logical replication syntax";5};Bounds are inclusive (minVersion <= actual <= maxVersion) and compare only up to the segment precision of the bound. For example, maxVersion = "1.29" accepts "1.29.4", while maxVersion = "1.29.0" rejects "1.29.4". meta.k8s uses the same comparison rules against k8sVersion.
meta.conflictsWith
Section titled “meta.conflictsWith”Entries in meta.conflictsWith prevent incompatible packages from being deployed together:
"<pkg>"forbids another instance of<pkg>in the same namespace."cluster:<pkg>"forbids<pkg>anywhere in the same cluster.- Listing the package’s own name in
"cluster:<self>"enforces a cluster-wide singleton.
An instance counts when it answers <pkg> by its name or its aliases, or when its package lists <pkg> in meta.defaultAliases. The last case holds even when the instance sets its own aliases, so two instances of a singleton package fail whatever aliases they set.
meta.podSecurity and meta.unsafe
Section titled “meta.podSecurity and meta.unsafe”Setting meta.podSecurity to "restricted", "baseline", or "privileged" applies the corresponding pod-security.kubernetes.io/enforce label to the generated Namespace resource. When multiple instances share a namespace, the least restrictive level wins (privileged > baseline > restricted).
meta.unsafe acknowledges intentional security or isolation trade-offs so the scorecard records them as reviewed grants (architecture.unsafeGrants) rather than unexpected violations:
meta.unsafe token | What it permits |
|---|---|
"raw-pvc" | Constructing a raw PersistentVolumeClaim via scope.mkResource instead of a managed storage operator. |
"host-network" | Setting hostNetwork = true, hostPID = true, or hostIPC = true on a pod spec. |
"privileged" | Setting securityContext.privileged = true on a container. |
"run-as-root" | Running containers with runAsNonRoot = false or runAsUser = 0. |
"cluster-admin" | Binding a ServiceAccount to the built-in cluster-admin ClusterRole or granting wildcard * verbs and resources. |
meta.network
Section titled “meta.network”When cluster.networkPolicy.enable = true, Kix derives NetworkPolicy resources from the dependency graph. Workloads that need ambient network access not represented by a Kix resource dependency (such as scraping all pods or calling external webhooks) declare that access in meta.network:
1meta.network = {2 intents.deployment = {3 egress = [4 {5 to = "apiserver";6 }7 {8 to = "world";9 ports = [ { port = 443; protocol = "TCP"; } ];10 }11 ];12 ingress = [13 {14 from = "all-pods";15 ports = [ { port = 9090; protocol = "TCP"; } ];16 }17 ];18 };19 externalIngressEntities = [ "world" ];20};intents.<partName>.egress: list of rules wheretois"world","all-pods","apiserver","dns", or{ entities = [ ... ]; }, with optionalports = [ { port; protocol ? "TCP"; } ].intents.<partName>.ingress: list of rules wherefromis"world","all-pods", or{ entities = [ ... ]; }, with optionalports.externalIngressEntities: list of Cilium entity names (such as["world"]or["remote-node"]) used when this package acts as a cluster ingress controller.
options
Section titled “options”options defines the configuration schema for instances.<name>.config using standard NixOS module options (lib.mkOption) or kix.options.* helpers.
1options = {2 logLevel = lib.mkOption {3 type = lib.types.enum [ "debug" "info" "warn" "error" ];4 default = "info";5 description = "Minimum log severity emitted by the server.";6 };7
8 extraEnv = lib.mkOption {9 type = lib.types.attrsOf lib.types.str;10 default = { };11 description = "Additional environment variables passed to the main container.";12 };13};Evaluation order and module priority
Section titled “Evaluation order and module priority”When Kix evaluates an instance, it merges configuration layers using the NixOS module system in the following priority order (lowest number wins):
- Option defaults (
options.<name>.default): priority1500. - Package
configdefaults (lib.mkDefaultvalues inside the top-levelconfigblock in the package file): lowered from priority1000to priority1250so package-level computed defaults override option defaults (1500) while yielding to dependency injections and instance config (including instancelib.mkDefaultat1000). Unwrapped definitions in the packageconfigblock keep normal module priority (100). - Dependency config injections (
deps.<arg>.configfrom downstream consumers) and instanceconfig(instances.<name>.config): priority100(orlib.mkDefaultat1000,lib.mkForceat50).
Both the package config block and instance config modules can take { config, lib, clusterConfig, ... }: as module arguments to compute values from sibling options or cluster settings.
The option default contract
Section titled “The option default contract”Every package in a flake checked by kix.mkFlake or kix.checkPackageOptions must obey three rules on options defaults so users can override individual keys without unexpectedly wiping sibling defaults:
- No untyped options with non-empty attrset defaults. An option without a
type(or typed aslib.types.anything/lib.types.raw) cannot have a non-empty attribute set as itsdefault, because setting one subkey ininstances.<name>.configreplaces the entire attribute set. Use a submodule (lib.types.submodule) instead. - No
attrsOforlazyAttrsOfwith non-empty defaults.lib.types.attrsOfreplaces the default map wholesale when a user sets a single key unless merged carefully, and prevents per-key schema documentation. Define known keys as explicit options or put user extensions inextra*options withdefault = { };. - Submodule defaults must match field defaults. If a
lib.types.submoduleoption hasdefault = { foo = "bar"; };, thefoooption inside that submodule must also declaredefault = "bar";so specifying another field in the submodule does not dropfoo.
Reusable option sets
Section titled “Reusable option sets”kix.options.* provides pre-typed option schemas that pair directly with composition helpers like kix.expose, kix.monitor, and kix.healthCheck.
kix.options.* helpers
Section titled “kix.options.* helpers”| Helper | Arguments / shape | Generated options and types |
|---|---|---|
kix.options.image | { repository ? null, tag ? null, digest ? null, pullPolicy ? "IfNotPresent", tagText ? null, description ? ... } | Submodule option with repository (str), tag (str), digest (nullOr str, defaults to digest while config.tag == tag, otherwise null), pullPolicy (nullOr str, default pullPolicy), plus pin record-keeping options sourceRev, promotedBy, resolvedAt, and note (all nullOr str, default null). Consumed by kix.image.ref. |
kix.options.resources | { requests ? { }, limits ? { }, description ? ... } | Submodule option with requests and limits (each supporting cpu, memory, and ephemeral-storage defaults plus freeform hugepages-<size> or extended <domain>/<name> quantities validated by kix.types.quantity; setting a field to null clears its default) and claims (nullOr (listOf (submodule { name; request ? null; }))). Cleaned via apply so it passes directly to a container’s resources field. |
kix.options.env | option value (not a function) | lib.mkOption of type attrsOf (nullOr envValue) with default = { }, where each value is a string, a { valueFrom = ...; } source, or null (to clear a package default). Consumed by kix.mergeEnv. |
kix.options.ingress | option attrset (not a function) | Attribute set of options { host; annotations; } where host is nullOr str (default null) and annotations is attrsOf str (default { }). Assign as options.ingress = kix.options.ingress;; read by kix.expose from config.ingress. |
kix.options.monitor | option attrset (not a function) | Attribute set of options { enabled; interval; } where enabled is bool (default true) and interval is nullOr str (default null). Assign as options.metrics = kix.options.monitor;; read by kix.monitor from config.metrics. |
kix.options.alerts | option attrset (not a function) | Attribute set { enabled; } where enabled is bool (default true), for packages that render alert rules without a kix.monitor scrape. Assign as options.metrics = kix.options.alerts;; read by kix.alerts from config.metrics.enabled. |
kix.options.healthCheck | option attrset (not a function) | Attribute set { enabled; retryFor; } where enabled is bool (default false) and retryFor is int (default 90). Assign as options.healthCheck = kix.options.healthCheck;; read by kix.healthCheck from config.healthCheck. |
kix.options.oidc | { scopes, autoLogin ? null } | Attribute set of options { scopes; autoLogin?; } where scopes is listOf str that must include "openid" (default scopes), and autoLogin is bool (default false, included only when autoLogin != null). Setting the removed enabled or realm options fails evaluation in kix.contracts.oidc.client with migration steps. |
kix.options.mail | option attrset (not a function) | Attribute set { from; } where from is nullOr str holding one @ (default null): the sender address that replaces the smtp server’s out.from. Assign as options.mail = kix.options.mail; and pass config.mail.from to kix.contracts.smtp.settings. |
Resource quantities (kix.types.quantity and kix.parseQuantity)
Section titled “Resource quantities (kix.types.quantity and kix.parseQuantity)”kix.types.quantity is a NixOS option type that accepts standard Kubernetes resource quantity strings (such as "100m", "0.5", "256Mi", "2Gi", or "1e3") and rejects malformed suffixes or numbers exceeding 64-bit signed integer limits or 18 significant digits.
To compare or compute resource quantities in Nix code, use kix.parseQuantity:
1# Returns millicores (integer) when the first argument is "cpu"2cpuMillis = kix.parseQuantity "cpu" config.resources.requests.cpu; # "250m" -> 250, "2" -> 20003
4# Returns bytes (integer) for memory or storage5memBytes = kix.parseQuantity "memory" config.resources.requests.memory; # "256Mi" -> 268435456Escape hatches and guard helpers
Section titled “Escape hatches and guard helpers”Packages often expose an escape hatch option (extraSpec, extraEnv, extraArgs, or settings) so cluster operators can set fields not covered by first-class options. Use Kix’s guard helpers to merge operator overrides while protecting invariants owned by the package:
| Helper | Signature | Behavior |
|---|---|---|
kix.mergeGuarded | { who, locked, what ? "extraSpec" } base overrides | Recursively merges overrides onto base, but throws an evaluation error naming who and what if overrides touches any attribute path (a list of path segments, such as [ [ "replicas" ] [ "selector" ] ] or [ [ "template" "spec" "serviceAccountName" ] ]) listed in locked, or replaces a parent of a locked path with a non-attrset value. |
kix.mergeEnv | { who, owned ? [ ], defaults ? { } } userEnv | Validates that userEnv does not set any environment variable claimed by owned (where each entry is { vars ? { }; names ? [ ]; from; fix; }, even as null), and returns the merged attribute set defaults // userEnv // <all owned.vars> ready to pass to kix.mkEnvVars. |
kix.guardArgs | { who, option ? "extraArgs", owned } userArgs | Validates CLI flags and returns userArgs unchanged. userArgs can be a list of flag strings (matching --flag, --flag=value, or a one-letter -f with an attached value) or an attrset keyed by flag name, like a VictoriaMetrics CR’s extraArgs (matching each key with or without leading dashes). owned maps each refused flag to its reason and fix; the error reads <who>: <option> sets "<flag>", which this package does not accept: <reason>. |
1# Guarding CLI flags and environment variables owned by typed options2args = [3 "--storage.tsdb.retention.time=${config.retention}"4 "--web.listen-address=:9090"5] ++ kix.guardArgs {6 who = "prometheus";7 owned = {8 "--storage.tsdb.retention.time" = "set retention instead.";9 "--web.listen-address" = "the Service and the probes use port 9090; remove it.";10 };11} config.extraArgs;12
13env = kix.mkEnvVars (14 kix.mergeEnv {15 who = "myapp";16 defaults = { LOG_FORMAT = "json"; };17 owned = [18 {19 vars = {20 PORT = "8080";21 DATABASE_HOST = database.host;22 };23 from = "the package and its `database` dependency";24 fix = "Wire the database with `deps.database` instead.";25 }26 ];27 } config.extraEnv28);build and deps
Section titled “build and deps”build function arguments
Section titled “build function arguments”Each function in build is called via pattern matching on its attribute set parameter list. Kix recognizes the following built-in framework arguments, and treats any other argument name as a cross-package dependency request:
| Built-in argument | Description |
|---|---|
self | Lazy fixed point of the merged parts attribute set returned across all build entries. Use self.<partName> to reference sibling resources in the same package. |
config | Evaluated package options (options merged with package config, dependency injections, and instances.<name>.config). |
clusterConfig | Evaluated cluster module configuration (such as clusterConfig.cluster.domain, clusterConfig.cluster.name, clusterConfig.k8sVersion, clusterConfig.cluster.availability, and clusterConfig.networkPolicy.enable). |
scope | Instance-scoped resource builders (identical to outer scope). |
lib | Standard nixpkgs.lib. |
kix | Framework helpers. |
deps | Attribute set of all bound dependencies declared across build entries and deps. |
availability | Effective availability level string for the instance ("none", "pod", "node", or "zone"). |
Declaring dependencies
Section titled “Declaring dependencies”Any argument to a build function outside the built-in list above requests another package instance by instance name, aliases, meta.defaultAliases, or meta.roles:
- Required dependency (
{ postgres, ... }:without?): Kix requires a matching instance (or auto-instantiates the package when it is listed in the cluster’savailablePackages). - Optional dependency (
{ prometheus ? null, ... }:with? null): Bound to the provider’s output object if present in the cluster, ornullif absent.
When a dependency is bound, the argument receives the provider’s consumer view:
dep.out: Full output attribute set (dep.root.resource.out // dep.root.out).- Top-level convenience fields: every key in
dep.outis promoted directly ontodep(for example,postgres.fqdnandpostgres.port), alongside any top-level part folders from the provider’sbuild(such asdep.crdsanddep.secrets) and the view’s system keys (_type,_packageInstance,root,out,raw, and_meta). Aroot.outkey or part folder that collides with a system key (or a part folder other thancrdsandsecretsthat collides with aroot.outkey) throws an evaluation error. dep.crds: Attribute set of CRD resources from the provider’scrdspart folder (unless overridden byroot.out.crds).
Enriching dependencies with deps.<arg>
Section titled “Enriching dependencies with deps.<arg>”The top-level deps attribute set lets a consumer package inject configuration into an upstream provider or attach dependency-specific assertions and warnings:
1deps.postgres = {2 config = consumerConfig: {3 databases.${scope.instanceName} = {4 owner = consumerConfig.dbUser;5 };6 };7 assertions = pg: [8 {9 assertion = pg.out.tlsEnabled or false;10 message = "myapp requires postgres to have TLS enabled.";11 }12 ];13 warnings = pg:14 lib.optional (pg.out.maxConnections or 100 < 200)15 "myapp recommends postgres maxConnections >= 200.";16};deps.<arg> field | Type | Description |
|---|---|---|
config | attrset or consumerConfig: attrset | Configuration module injected into the upstream provider instance. Throws an error if the provider instance’s config in the cluster definition locked the target option with kix.mkLocked. |
assertions | dep: [ { assertion; message; } ] | Assertions evaluated only when <arg> is non-null. |
warnings | dep: [ string ] | Warnings evaluated only when <arg> is non-null. |
Built-in dependency contracts (kix.contracts.*)
Section titled “Built-in dependency contracts (kix.contracts.*)”kix.contracts.* provides shared contract validators (postgresDatabase, redis, smtp, oidc, oidcProvider, gateway, and otlpEndpoint) that verify the out surface passed between providers (including kix.mkImport stand-ins) and consumers:
- Provider validation (
check): A provider passes itsroot.outthroughkix.contracts.<name>.check who { ... }, which verifies every required and optional contract field and returnsout. - Consumer validation (
assertionsor helper functions): A consumer checks a bound dependency indeps.<alias>.assertionswithkix.contracts.<name>.assertions who(or reads it through a contract helper such askix.contracts.smtp.settings,kix.contracts.smtp.server,kix.contracts.oidc.client, orkix.contracts.gateway.require), which checks all required contract fields and validates the shape of any present optional fields. - Credentials on contracts (
out.credentials): Every contract that carries a login places the secret or secret instance atout.credentials(read viadep.out.credentials.out.keyRef "<key>") rather than taking a second secret dependency.
1deps.database.assertions = kix.contracts.postgresDatabase.assertions "vaultwarden";Build entries
Section titled “Build entries”build can be a single function or a list of functions (or file paths to functions). When build is a list:
- Kix inspects the function arguments of every entry and takes the union of all dependency requests. If any entry declares a dependency without a default (
{ redis }:), the dependency is required for the package even if another entry declares{ redis ? null }:. - Each entry is called with its requested arguments and returns an attribute set of named parts.
- Kix merges the returned attribute sets across all entries with
mergeCollide. Any duplicate part name acrossbuildentries throws an evaluation error.
Parts, folders, and conditional null entries
Section titled “Parts, folders, and conditional null entries”Each attribute in a build return set is one of the following:
- A resource created by
scope.mkResource,scope.mkDeployment,scope.mkSecret,scope.mkCRD, or a provider builder. null, which Kix filters out automatically. Useif config.metrics.enabled then scope.mkResource { ... } else nullorscope.mkOptional config.metrics.enabled (scope.mkResource { ... })for conditional resources.- A nested folder (up to 3 levels deep) grouping related resources, such as
rbac = { sa = ...; role = ...; binding = ...; };. Top-level folders are also exposed on the instance’s consumer view. - Known folders (
secretsandcrds):- Both
secretsandcrdsmust be attribute sets of resources rather than a single resource directly (assigning a resource directly tosecretsorcrdsthrows an evaluation error), and any entry inside them that is not a Kix resource (_type = "k8s-resource") emits an evaluation warning. - Place
scope.mkSecret/scope.mkSecretRefresources undersecrets = { ... };andscope.mkCRD/scope.mkCRDRefresources undercrds = { ... };. CRDs placed incrdsare exposed to downstream consumers atdep.crds.<Kind>.
- Both
Composable build-entry helpers
Section titled “Composable build-entry helpers”Because build accepts a list of functions, cross-cutting concerns are packaged as standalone build entries:
1build = [2 (3 { self, config }:4 {5 deployment = ...;6 service = ...;7 root = self.service;8 }9 )10 (kix.expose { service = "service"; })11 (kix.monitor { service = "service"; port = "metrics"; })12 (kix.healthCheck {13 runtime = kix.healthCheck.runtimes.shell;14 script = ''15 curl -sf "http://$SERVICE_FQDN:8080/healthz"16 '';17 env = { self, ... }: { SERVICE_FQDN = self.service.out.fqdn; };18 })19];See Composition helpers for the full reference on kix.expose, kix.monitor, kix.alerts, and kix.healthCheck.
Evaluation-only store (no shell builders)
Section titled “Evaluation-only store (no shell builders)”Kix evaluates packages in its local store without running shell build scripts. Package and cluster definitions can evaluate expressions, read files from the repository (builtins.readFile, builtins.fromJSON), write files with builtins.toFile or builtins.path, and render Helm charts through scope.helm.fetchHelmChart and scope.helm.importChart.
Do not call pkgs.runCommand, pkgs.stdenv.mkDerivation, or pkgs.writeText inside a package or cluster definition; evaluation fails under the default kix builder.
Package diagnostics
Section titled “Package diagnostics”Top-level warnings and assertions
Section titled “Top-level warnings and assertions”A package can define top-level warnings and assertions functions. Both receive { config, self, deps } after options and build parts are wired. Because deps only contains bound dependencies, an absent optional dependency is omitted from deps (test for it with !(deps ? <name>) or (deps.<name> or null) == null):
1warnings =2 { config, deps, ... }:3 lib.optional (config.ingress.host != null && !(deps ? gateway) && !(deps ? ingressController))4 "echo: ingress.host is set, but neither gateway nor ingressController is present in the cluster.";5
6assertions =7 { config, ... }:8 [9 {10 assertion = !(config.tls.enable && config.tls.secretName == null);11 message = "echo: config.tls.secretName must be set when config.tls.enable is true.";12 }13 ];Failed assertions abort evaluation immediately with all failed messages. warnings are collected into cluster.warnings and reported by kix check and kix build.
Structural build-time warnings
Section titled “Structural build-time warnings”During package evaluation, Kix also emits automatic structural warnings for common wiring mistakes:
- Orphaned resources: A part defined in
buildis not reachable fromroot.resource,root.requires, orroot.postHooks. - Redundant
requires: A resource lists a dependency inrequires = [ ... ]that is already tracked implicitly through.outstring context in its manifest. - Duplicate
requires: The same resource appears more than once in arequireslist. - Unordered
postHooks: A resource inroot.postHooksdoes not depend onroot.resource(either directly or transitively).
kix.checkPackageOptions and kix.packageOptionViolations
Section titled “kix.checkPackageOptions and kix.packageOptionViolations”kix.checkPackageOptions validates that a package’s options block obeys the option default contract across every availability level up to meta.availability.max:
1kix.checkPackageOptions {2 package = import ./pkgs/myapp;3 who = "myapp";4}kix.checkPackageOptions returns null when valid or throws an evaluation error listing all violations. To inspect the violations as data without throwing, call kix.packageOptionViolations { package = ...; who = "..."; }, which returns a list of violation attribute sets (empty when valid). kix.mkFlake runs kix.checkPackageOptions automatically across all exported packages.
kix.checkSecretKeys
Section titled “kix.checkSecretKeys”When a package accepts an external Secret dependency ({ credentialsSecret ? null }:), wrap the dependency with kix.checkSecretKeys before reading its keys. kix.checkSecretKeys verifies that dep.out.keys declares all required keys and at least one complete key group in anyOf, then returns dep (or throws an evaluation error if any required keys are missing):
1build =2 {3 self,4 config,5 credentialsSecret ? null,6 ...7 }:8 let9 checkedSecret = kix.checkSecretKeys {10 who = "myapp";11 arg = "credentialsSecret";12 dep = credentialsSecret;13 keys = [ "username" ];14 anyOf = [15 [ "password" ]16 [ "api-token" ]17 ];18 };19 in20 {21 ...22 };| Field | Type | Default | Description |
|---|---|---|---|
who | string | Required | Consumer package name used in error messages. |
arg | string | Required | Dependency argument name. |
dep | resource or null | Required | Bound Secret dependency. Returned unchanged when dep is null or when all key requirements are satisfied (or if dep.out.keys is not set). |
keys | list of strings | [ ] | Keys that must all be present in dep.out.keys. |
anyOf | list of lists of strings or null | null | When non-null, every key in at least one inner list ([ [ "a" "b" ] [ "c" ] ]) must be present in dep.out.keys. |