Reference / Package authoring
Helm and imports
Not everything in your cluster starts as a native Kix package: you might want to deploy a complex third-party operator published as a Helm chart, or connect your workloads to a managed database or cloud ingress controller that already exists outside Kix. This page covers the two bridges for bringing external software into Kix’s dependency graph: Helm chart wrappers (kix.helmChart and scope.helm.importChart) and external service markers (kix.mkImport).
Helm helpers
Section titled “Helm helpers”Helm chart wrappers are a fallback bridge for third-party software that does not have a native Kix package yet. Compared to native Kix packages, Helm charts use untyped values maps (unless wrapped with custom options), rely on YAML heuristics rather than .out references for dependency and network-policy discovery, ignore cluster.availability scaling by default, and often require overrides to satisfy scorecard rules. See Trade-offs of wrapping a Helm chart for when to use each tier.
Kix provides two entry points for Helm charts:
kix.helmChart: A turn-key package constructor for quick chart adoption that exposes avaluesoption (options.values) and wraps the rendered resources automatically.scope.helm.importChart: The full chart-adoption builder used inside custom packages that expose typedoptions, translate them into Helmvalues, and patch or extend the rendered resources.
Quick adoption with kix.helmChart
Section titled “Quick adoption with kix.helmChart”1{ kix, ... }:2kix.helmChart {3 repo = "https://stakater.github.io/stakater-charts";4 name = "reloader";5 version = "1.0.115";6 hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";7 metaExtra = {8 description = "Kubernetes controller that watches ConfigMaps and Secrets and triggers rollouts";9 };10}kix.helmChart argument | Type | Default | Description |
|---|---|---|---|
repo | string | Required | Helm repository URL (https://... or oci://...). |
name | string | Required | Upstream chart name. |
version | string | Required | Chart version string (also used as default meta.version). |
hash | string | Required | Fixed-output Nix SRI hash ("sha256-...") of the fetched chart tarball. |
releaseName | string | name | Helm release name passed to helm template. |
helmVersion | "3" or "4" | "4" | Major version of the Helm in the Kix runtime used to render the chart ("4" is 4.3.1, "3" is 3.19.1; see Helm versions). |
root | { kind, name } or null | null | Explicit root resource selector; when null, Kix selects the primary Service or workload automatically (chart.autoRoot). |
includeCrds | bool | true | Whether to pass --include-crds to helm template. |
strip | { helmAnnotations ? true; helmLabels ? true; defaultStrategy ? true; } | { } | Controls normalization of Helm metadata annotations (helmAnnotations), app.kubernetes.io/managed-by and helm.sh/chart labels (helmLabels), and default RollingUpdate strategy blocks (defaultStrategy). |
patchResources | list -> list | lib.id | Function transforming the list of parsed chart resource attribute sets before Kix reconstructs the DAG and wraps them. |
metaExtra | attrset | { } | Additional fields merged into the package’s meta block. |
Full adoption with scope.helm.fetchHelmChart and scope.helm.importChart
Section titled “Full adoption with scope.helm.fetchHelmChart and scope.helm.importChart”In a typed package, fetch the chart tarball with scope.helm.fetchHelmChart and render it into Kix resources with scope.helm.importChart:
1{2 scope,3 lib,4 kix,5 ...6}:7
8let9 chart = scope.helm.fetchHelmChart {10 repo = "https://charts.jetstack.io";11 name = "cert-manager";12 version = "1.14.4";13 hash = "sha256-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=";14 };15in16{17 meta = {18 version = "1.14.4";19 description = "X.509 certificate controller for Kubernetes";20 };21
22 options = {23 replicas = lib.mkOption {24 type = lib.types.ints.positive;25 default = 1;26 description = "Controller replica count.";27 };28 values = lib.mkOption {29 type = lib.types.attrsOf lib.types.anything;30 default = { };31 description = "Extra Helm values merged over typed options.";32 };33 };34
35 build =36 { self, config }:37 let38 rendered = scope.helm.importChart {39 inherit chart scope;40 releaseName = "cert-manager";41 values = lib.recursiveUpdate {42 replicaCount = config.replicas;43 installCRDs = true;44 } config.values;45 patchResources = builtins.filter (r: r.kind != "Pod");46 };47 in48 rendered.mkParts {49 root = rendered.get "Deployment" "cert-manager";50 };51}scope.helm.importChart arguments
Section titled “scope.helm.importChart arguments”| Argument | Type | Default | Description |
|---|---|---|---|
chart | derivation | Required | Chart tarball derivation returned by scope.helm.fetchHelmChart { repo, name, version, hash }. |
scope | attrset | Required | Package scope (automatically configured with labelStyle = "helm" for wrapped resources). |
values | attrset | { } | Nix attribute set serialized to values.yaml and passed to helm template --values. |
releaseName | string | "release" | Release name passed to helm template. |
namespace | string | scope.namespaceName | Namespace passed to helm template --namespace. |
helmVersion | "3" or "4" | "4" | Major version of the Helm in the Kix runtime used to render the chart ("4" is 4.3.1, "3" is 3.19.1; see Helm versions). |
includeCrds | bool | true | Passes --include-crds and wraps rendered CRDs with scope.mkCRD. |
includeHooks | bool | true | When false, passes --no-hooks to helm template. Chart test hooks (helm.sh/hook containing "test") are always dropped when wrapping resources. |
extraFlags | list of strings | [ ] | Extra CLI arguments passed to helm template, one argument per list element (cannot contain --values, --namespace, --kube-version, or --include-crds). |
strip | { helmAnnotations ? true; helmLabels ? true; defaultStrategy ? true; } | { } | Normalization booleans controlling whether Kix strips meta.helm.sh/*, helm.sh/*, and checksum/* annotations (helmAnnotations), app.kubernetes.io/managed-by and helm.sh/chart labels (helmLabels), and default RollingUpdate strategy blocks (defaultStrategy). To filter or modify rendered resources, use patchResources. |
preserveAnnotationPrefixes | list of strings | [ ] | Keeps matching annotation prefixes (such as "argocd.argoproj.io/") when strip.helmAnnotations is enabled. |
patchResources | list -> list | lib.id | Transforms the list of parsed chart resource attribute sets (after List expansion and test-hook filtering) before DAG reconstruction and scope.mk* wrapping. |
extraEdges | list of { from = { kind; name; }; to = { kind; name; }; } | [ ] | Additional intra-chart dependency edges (from depends on to) merged with Kix’s automatic DAG reconstruction (for example, [ { from = { kind = "Deployment"; name = "my-app"; }; to = { kind = "Job"; name = "my-migration"; }; } ]). |
networkDeps | list of strings | [ ] | Additional context-carrying strings (such as nested dep.out.byKind outputs) attached to chart workloads for network-policy edge discovery. |
deps | list of dependency views | [ ] | Upstream Kix dependency views whose top-level context-carrying out.* strings are attached to chart workloads for network-policy edge discovery. |
Automatic intra-chart DAG reconstruction
Section titled “Automatic intra-chart DAG reconstruction”Raw Helm YAML files have no deployment ordering between objects. When scope.helm.importChart wraps a chart, Kix inspects every rendered manifest and reconstructs the intra-chart dependency graph automatically:
- Workloads depend on the
ServiceAccountnamed inspec.template.spec.serviceAccountName, anyConfigMap,Secret, orPersistentVolumeClaimmounted involumesor referenced inenv/envFrom, and anyCRDwhose custom resources are present. RoleBindingandClusterRoleBindingresources depend on their targetRole/ClusterRoleandServiceAccountsubjects.Serviceresources depend on the workload whose pod template labels matchspec.selector.Ingress,HTTPRoute,ServiceMonitor, and webhook configurations depend on their targetService.
Use extraEdges when a chart has a runtime dependency that is not visible in standard Kubernetes reference fields (for example, an application Deployment that expects a chart Job to initialize an external schema).
Collection methods on importChart results
Section titled “Collection methods on importChart results”scope.helm.importChart returns a collection attribute set with the following selectors and helpers:
| Attribute or method | Signature | Description |
|---|---|---|
all | list of resources | All wrapped Kix resources in the chart. |
attrs | attrset of resources | Map of <partKey> = resource (for example, "deployment-cert-manager"). |
crds | attrset of CRD handles | Map of <Kind> = { cr; mkCR; } CRD builder handles for all CRDs rendered by the chart. |
get | kind -> name -> resource | Looks up a single resource by kind and metadata.name. Throws an evaluation error listing available resources if not found. |
find | kind -> name -> resource or null | Same as get, but returns null if the resource is not present. |
byKind | kind -> list of resources | Returns the list of wrapped resources of the given kind. |
workloads | list of resources | All Deployment, StatefulSet, DaemonSet, Job, and CronJob resources in the chart. |
excluding | [ resources ] -> list | Returns all minus the listed resources. |
depsOf | resource -> list | Returns the reconstructed intra-chart dependencies of resource. |
edges | list of { from; to; type; } | List of reconstructed intra-chart dependency edge records, where from and to are wrapped Kix resources and type names the rule that inferred the edge. |
autoRoot | resource | Automatically selects the chart’s sole Service, or (when there are no Services) its sole Deployment, StatefulSet, or DaemonSet. Throws an evaluation error if there are multiple Services, no Services and multiple workloads, or neither Services nor workloads. |
replaceResource | kind -> name -> newResource -> collection | Replaces a wrapped chart resource with a custom Kix resource and re-evaluates all downstream chart resources so they depend on newResource. |
mkParts | { root, extraParts ? { }, extraRequires ? [ ], out ? { } } -> parts | Produces the build return set (attrs // extraParts // { root = { resource = root; requires = ...; out = ...; }; }), wiring all other chart resources into root.requires and populating root.out.byKind.<Kind>.<name> (plus root.out.crds when the chart renders CRDs). root is required. When root is a workload (Deployment, StatefulSet, DaemonSet, Job, or CronJob) selected by exactly one Service in the chart, that Service’s name, fqdn, selector, ports, and url are also promoted onto root.out. |
Extracting CRDs only with scope.helm.importChartCrds
Section titled “Extracting CRDs only with scope.helm.importChartCrds”When you write a native Kix package for an operator but want to load its CRD definitions from the upstream Helm chart tarball, use scope.helm.importChartCrds:
1crdList = scope.helm.importChartCrds {2 inherit chart scope;3};scope.helm.importChartCrds accepts { chart, scope, releaseName ? "release", values ? { }, helmVersion ? "4" } and returns a list of wrapped CustomResourceDefinition resources. Pass helmVersion = "3"; to render the chart’s CRDs with Helm 3.
Helm versions
Section titled “Helm versions”kix.helmChart, scope.helm.importChart, and scope.helm.importChartCrds render charts with Helm 4 (4.3.1) by default. If an upstream chart relies on template behavior that changed in Helm 4, set helmVersion = "3"; to render that chart with Helm 3 (3.19.1). Upstream Helm 3 receives security fixes until 10 February 2027. Passing any other major version fails evaluation and lists the shipped versions.
The exact Helm release for each major version is pinned in kix/lib/helm-versions.json and shipped with Kix. A Kix minor release can change a Helm version in the Kix runtime (for example, a Helm security or patch update). A patch release can only add one, so a Kix patch release still ships every Helm version that the Kix library of an earlier patch in the same minor names. Because helm template output feeds into resource identity hashes (kix.run/identity-hash), the Kix library and Kix must name the same Helm version. When you upgrade Kix to a new minor release, align flake.lock with kix lib update. Kix stops before it evaluates a project whose Kix library has another minor version (see When Kix and the Kix library do not match).
How project builders fetch and render charts
Section titled “How project builders fetch and render charts”How fetchHelmChart and importChart run depends on the project’s builder in kix.toml:
builder = "kix"(default): Kix runs thehelmprograms in the Kix runtime (3.19.1or4.3.1) to pull each chart tarball, verify its SRIhash, runhelm template, and store the rendered manifests in Kix’s local store (~/.cache/kix/store).builder = "nix": The machine’s Nix fetches the chart and runs the same Helm release (kix/lib/helm-versions.json) with the samehelm templateflags during evaluation (allow-import-from-derivationmust betrue, the Nix default). A chart renders the same way under either builder.
mkImport
Section titled “mkImport”Clusters frequently rely on services or platform components that live outside the Kix cluster configuration, such as a cloud-managed PostgreSQL instance (RDS or Cloud SQL), an external OIDC provider, or a CNI / storage driver pre-installed by a managed Kubernetes control plane (EKS, GKE, or k3s).
kix.mkImport creates an import definition that can be bound in instances.<namespace>.<name>.package (or inside a cluster flavor). Imports participate in dependency resolution, role binding, and contract checking without emitting workload manifests. At build time, Kix emits a kix.run/v1alpha1 PackageInstance marker with metadata.annotations."kix.run/mode" = "import" (and spec.mode = "import") so kix deploy can verify any expected in-cluster resources before deploying consumers.
1# Importing an external RDS PostgreSQL database in cluster configuration2{ ref, packages, kix, ... }:3{4 instances.app.rds-login = {5 package = packages.secret;6 config.source = ./secrets/rds-login.sops.yaml;7 config.keys = [ "username" "password" ];8 };9 instances.app.database.package = kix.mkImport {10 scope = "namespace";11 out = {12 host = "app.abc123.eu-west-1.rds.amazonaws.com";13 port = "5432";14 database = "app";15 tls = true;16 credentials = ref.app.rds-login;17 };18 };19}kix.mkImport arguments
Section titled “kix.mkImport arguments”| Argument | Type | Default | Description |
|---|---|---|---|
from | package or null | null | Optional Kix package evaluated against a synthetic import scope to derive its default out surface and root resource kind and apiVersion hints. Does not inherit meta.roles or crds from from.meta (unless from’s root.out exposes out.crds). |
out | attrset | { } | Output fields exposed to downstream consumers (host, fqdn, port, credentials, storageClassName, etc.). May contain ref.<namespace>.<instance> at any depth of plain attribute sets (see Referencing instances in out and namespace scope). When both from and out are set, out overrides matching keys on the package-derived out. |
kind | string or null | Derived from from’s root resource, or null | Optional Kubernetes kind of an existing in-cluster resource ("Service", "Deployment", "StorageClass", etc.). When present, Kix stamps kix.run/import-kind (and kix.run/import-name when out.name is set) on the PackageInstance import marker so kix deploy checks that the resource exists in the cluster. |
apiVersion | string or null | Derived from from’s root resource, or null | API version stamped as kix.run/import-apiversion on the import marker when kind is present. Not inferred from kind when from is null. |
roles | list of strings | [ ] | Platform roles satisfied by this import ("cni", "dns", "loadBalancer", "metricsApi", "storageClasses", etc.). Not inherited from from.meta.roles. |
crds | list of { group, kind, plural, version, servedVersions?, crdScope?, factBehavior?, readiness?, usesAtLeast?, holdsData? } | [ ] | Pre-installed CRDs provided by this import (version is required on each entry). Wrapped as CRD reference handles, emitted as CRD ref markers while the import is active, and exposed to consumers at dep.out.crds.<Kind>. |
scope | "cluster" | "namespace" | "cluster" | Which namespaces resolve a dependency to this import by name. The default offers it to every namespace, unlike a managed package with meta.scope unset; "namespace" keeps it to its own namespace, even against ref. When several imports answer one name, name the one you mean with deps.<arg> = ref.<namespace>.<instance>. |
Referencing instances in out and namespace scope
Section titled “Referencing instances in out and namespace scope”In a cluster definition, out may contain ref.<namespace>.<instance> at any depth of plain attribute sets. Write { ref, ... }: in the cluster module header so ref is in scope. Kix replaces each ref with that instance (so expressions such as dep.out.credentials.out.keyRef "password" work in the consumer without consumer changes) and adds the referenced instance to the import marker’s dependencies, so kix deploy applies the secret before the import.
A ref in out does not activate an optional instance. A ref to an inactive instance, or to the import itself, fails the build.
A pod reads Secrets only from its own namespace, and Kix refuses a Secret from another namespace at build time. Because of this, an import that carries a login in out.credentials (such as an external RDS or Cloud SQL database matching kix.contracts.postgresDatabase, an authenticated smtp server, or an external oidc provider) sits in the consumer’s namespace beside its secret, with scope = "namespace". An import without a login can keep the default scope = "cluster" and serve every namespace. When several namespaces connect to the same external service with their own secrets, a Nix let binding can share the common settings across the per-namespace imports.
Three ways to use kix.mkImport
Section titled “Three ways to use kix.mkImport”- Direct
outimport (out = { ... }): Provide the exact.outcontract expected by consumers (such ashost,port,database, andcredentialsfor an external database, orstorageClassName = "gp3"withroles = [ "storageClasses" ]). - Package-derived import (
from = pkgs.coredns; roles = [ "dns" ];): Evaluates the referenced Kix package in an import scope to extract its default.outsurface and rootkind/apiVersionwhile discarding its deployment manifests. Passrolesorcrdsexplicitly when the import should also claim platform roles or expose CRD ref markers. - Hybrid import (
from = pkgs.coredns; roles = [ "dns" ]; out = { ... };): Starts fromfrom’s default.outsurface and overrides specific output fields inout.
Role shadowing and optional = true
Section titled “Role shadowing and optional = true”When a cluster flavor defines a pre-installed capability using kix.mkImport:
- An import instance with
optional = true(such asplatform-dnson cloud flavors) activates only when at least one active instance in the cluster depends on it through a required build argument (by name, alias, or role) or an explicitdeps.<arg> = ref.<namespace>.<instance>. Providing a core platform role does not by itself activate anoptional = trueimport; imports that must always be active (such asplatform-storage) setoptional = false. - If the cluster operator explicitly instantiates a managed Kix package that provides the same role (for example, deploying a managed
ciliumorcorednsinstance), the managed instance automatically shadows and deactivates the flavor’s"shadowable"import instance so the two never conflict.