Options

deployment.enable

Whether to enable rendering objects into named deployment units.

Type: boolean

Default:

false

Example:

true

Declared by:

deployment.declaredUnits

Every unit this configuration declares. ekn reads it only to warn: pruning an object whose unit is not in this list is correct, and is also one line of configuration away from deleting ArgoCD and the CNI.

Type: list of string (read only)

Default:

[ ]

Declared by:

deployment.deployBranch

Git branch that rendered manifests are committed to, and that GitOps tooling (ArgoCD/Flux) should sync from. One branch per easykubenix instance – an “environment” is just whichever instance you evaluate, same as one NixOS system is one environment.

Type: string

Example:

"production"

Declared by:

deployment.engine.pause

Workloads ekn kubeapply --pause-engine scales to zero for the duration of a direct apply, and restores afterwards.

A direct apply and a running GitOps engine are two writers of the same objects, and whichever ran last wins – so a full deploy against a live engine is undone at the next sync, with nothing reporting it.

Name the engine’s reconciler, and nothing else. The test is which workloads hold cluster-wide write permission, not which ones belong to the engine. On a stock ArgoCD install (measured on nixlab2 from the ClusterRoles bound to each ServiceAccount) that is the application controller alone: the repo server holds no write bindings at all, so scaling it buys nothing and costs the controller its target state on resume.

Two things this does not stop, both worth knowing rather than working around. argocd-server keeps cluster-wide delete and patch, so a person clicking Sync in the UI during an apply still writes – scaling it too would take away the UI people want for watching. And an ApplicationSet controller regenerates Applications; if you run one, it belongs in this list.

Each workload carries ekn.dev/paused-replicas while it is down, so an ekn that dies mid-apply leaves the count to restore on the cluster rather than in a file. A later run finds it and can finish.

These objects are excluded from the apply set while paused, and protected from its prune. They are usually in kubernetes.generated themselves, declaring replicas: 1, so applying them would wake the engine in the middle of the apply that paused it. The cost is that this mode can never update the engine’s own reconciler; ekn says so rather than passing over it.

Type: list of (submodule)

Default:

[ ]

Example:

[
  {
    namespace = "argocd";
    name = "argo-cd-argocd-application-controller";
    kind = "StatefulSet";
  }
]

Declared by:

deployment.engine.pause.*.kind

What to scale. Only these two have a replica count ekn can restore.

Type: one of “Deployment”, “StatefulSet”

Default:

"Deployment"

Declared by:

deployment.engine.pause.*.name

Workload name.

Type: string

Declared by:

deployment.engine.pause.*.namespace

Namespace the workload lives in.

Type: string

Declared by:

deployment.fieldManager

The server-side-apply field manager this instance writes as, for every apply that does not name another one.

Set this rather than each unit’s fieldManager. A whole-instance ekn kubeapply is one group with no unit, so before this option it could not name a manager at all and wrote as the built-in default. A deployment that set deployment.units.<name>.fieldManager on every unit therefore still had two managers on its objects: the units’ one from a targeted apply, and the built-in one from a whole-instance apply.

That split is not cosmetic. Server-side apply removes a field only when its owning manager stops declaring it, so a field the built-in manager wrote and the successor does not declare stays on the object for ever – the successor’s apply cannot drop what it does not own, and force does not help, because two different fields are not a conflict. A probe that changes handler type is then refused by the API server forever: may not specify more than 1 handler type.

ekn reclaim moves existing ownership onto this manager. This option is what stops the split happening again.

deployment.units.<name>.fieldManager defaults to this and still overrides it, for a bootstrap unit whose successor differs from the instance’s.

Type: string

Default:

"ekn"

Example:

"argocd-controller"

Declared by:

deployment.handAppliedUnits

The units a whole-instance ekn kubeapply --prune must not touch: every unit whose objects do not reach kubernetes.generated.

ekn selects ekn.dev/environment=E,ekn.dev/deployment-unit notin (these). Each of these units renders a whole nested instance that only ekn kubeapply --target <name> applies, so its objects carry the environment label without ever being in a whole-instance apply’s desired set. Without the exclusion that prune deletes them, and for a bootstrap unit that is ArgoCD and the CNI.

Computed by inverting the routing-only test, not by listing the nested ones. A unit is left in scope only when it is demonstrably class = "kubernetes" with no modules of its own; anything else, including a class nobody has written yet, is excluded. A wrong exclusion leaves an object running, a wrong inclusion deletes it, and the two mistakes are not worth the same.

modules is a list of deferred modules, so it cannot be serialised for ekn to test itself. This option is the answer, evaluated here.

Type: list of string (read only)

Default:

[ ]

Declared by:

deployment.path

Subdirectory within the branch where rendered manifests are stored.

Type: string

Default:

"./"

Example:

"./clusters/my-cluster"

Declared by:

deployment.sourceBranch

Git branch the exact source tree (working copy, including uncommitted edits) is snapshotted to at deploy time, paired 1:1 with deployBranch via two-parent commits – every deploy commit also points at the source commit that produced it. Null disables this snapshot/dual-commit behavior, falling back to a plain single-branch commit.

Type: null or string

Default:

null

Example:

"production-source"

Declared by:

deployment.tofuUnits

Every deployment.units.<name> whose class is “tf”, with the OpenTofu configuration it rendered and the tofu binary that can apply it.

The parallel to kubernetes.deploymentUnits, and deliberately not an entry in it. That attrset is a published schema – ekn validates it with pydantic, and every entry must carry objects – so widening it to hold entries that have none would cost each consumer a branch and buy nothing.

Under deployment rather than tofu, because tofu.* inside a “tf” instance is that instance’s own configuration. These are a Kubernetes instance’s view of the units below it.

configFile and tofu are store paths, and neither is built at evaluation time – ekn tofu realises both before it runs anything. dependencies is the transitive closure, deepest first, and ekn tofu takes that order literally: each entry is its own tofu invocation, run before this unit’s.

Every dependency of a “tf” unit is itself a “tf” unit. An assertion refuses one that crosses classes, because dependencies means “apply this one too, as part of applying me” and the two classes are not applied by the same thing.

Type: anything (read only)

Declared by:

deployment.unitFieldManagers

Each unit’s fieldManager, so a whole-instance apply can write each object as the manager its own unit declares.

A whole-instance apply is one group and wrote everything as the instance’s default manager, even though every routed object carries the unit that names one. That is right for an apply that runs again – it keeps conflict detection – and wrong for a full deploy standing in for a paused engine, which has a successor and does not run again.

ekn reads this only while the engine is paused. Two writers sharing one manager name are one manager to the API server, so each apply’s field set replaces the other’s; using the engine’s name while the engine runs is worse than the orphaned fields it fixes.

Type: attribute set of string (read only)

Default:

{ }

Declared by:

deployment.units

Named GitOps sync targets – pure path-routing within the single deployBranch/sourceBranch pair for this instance. The target is the single source of truth for where its manifests land, and how many controllers exist (Argo, Flux, both, neither) is up to whatever object references the target, not the target itself.

A target’s objects come from either of two places, and it can use both at once:

  • This instance’s own objects, routed by name with ekn.deploymentUnit – rather than embedding a reference to the Application/Kustomization that happens to sync them.

  • modules, a whole separate easykubenix configuration evaluated just for this target. Its objects render into the target’s path and stay out of this instance’s kubernetes.generated entirely, which is what makes it usable for bootstrapping a GitOps engine.

Type: attribute set of (submodule)

Default:

{ }

Example:

{
  apps.path = "clusters/home/apps";
  bootstrap = {
    path = "bootstrap";
    modules = [ ./bootstrap/argocd.nix ];
  };
}

Declared by:

deployment.units.<name>.annotations

Annotations stamped onto every object in this target, with the same semantics as labels above.

The per-object form is the useful one here: ArgoCD’s argocd.argoproj.io/tracking-id – its default ownership mechanism since 3.0 – encodes each object’s own group/kind/namespace/name, so a constant cannot express it. See ekn.lib.argocdTrackingId.

Type: attribute set of (string or function that evaluates to a(n) (null or string))

Default:

{ }

Example:

{
  "argocd.argoproj.io/tracking-id" = ekn.lib.argocdTrackingId {
    app = "root";
    namespace = "default";
  };
}

Declared by:

deployment.units.<name>.class

What kind of configuration modules is, which decides the base modules the nested instance is evaluated with and what this unit produces.

kubernetes is a whole easykubenix configuration rendering Kubernetes objects, which is every unit that existed before this option.

tf is an OpenTofu configuration rendering config.tf.json – see tofu.nix and design/opentofu.md. It declares no Kubernetes option at all, so its objects reach neither kubernetes.generated nor kubernetes.deploymentUnits, and nothing routed with ekn.deploymentUnit may name it.

The module system enforces this rather than trusting it. The nested evaluation is given this class, and a module carrying the other one fails at the import naming both.

Type: one of “kubernetes”, “tf”

Default:

"kubernetes"

Declared by:

deployment.units.<name>.dependencies

Other deployment units this one needs on the cluster. ekn kubeapply --target ‹name› contributes each of their objects to its apply plan, transitively.

Apply-time only. ekn commit still writes each unit to its own path and nothing else: a dependency is about what has to be on the cluster, not about where a manifest lives.

The use it exists for is a bootstrap unit that needs credentials it cannot render. Put the Secrets in their own unit, depend on it, and re-seeding them is ekn kubeapply --target <secrets> on its own – rather than a full bootstrap apply, which is the thing you do only after breaking a cluster badly enough that ArgoCD or the CNI is gone.

Not an ordering constraint. Objects from every contributing unit go into one apply, and ekn.resourcePriority orders that apply by kind as always.

Each unit keeps its own prune scope, because ekn.dev/deployment-unit is rendered per unit. Naming a dependency does not widen what this unit’s --prune deletes.

A cycle is an error, and so is a name no deployment.units entry declares.

Type: list of string

Default:

[ ]

Example:

[ "bootstrap-secrets" ]

Declared by:

deployment.units.<name>.fieldManager

Server-side-apply field manager ekn kubeapply --target ‹name› writes as. Apply-time only – it never appears in the rendered manifests, because it is a property of who applied an object rather than of the object.

Set it to the name of the controller that takes the objects over afterwards, which for a bootstrap target is the point of the whole exercise. Two managers declaring the same field is an SSA conflict, forcing Force=true on the successor’s side, and worse: SSA only drops a field when its owning manager stops declaring it, so every field ekn set and the successor does not stays owned by ekn forever, because a bootstrap apply never runs again. Applying as the successor makes the handover complete instead – fields the bootstrap set and steady state does not are simply removed.

The cost is that a second apply of this target silently overwrites the successor’s fields instead of reporting a conflict, since they are now the same manager. That is the trade, and it is acceptable only because a bootstrap target runs once by definition.

Type: string

Default:

config.deployment.fieldManager

Example:

"argocd-controller"

Declared by:

deployment.units.<name>.labels

Labels stamped onto every object in this target – both the ones routed here with ekn.deploymentUnit and the ones modules renders. Baked into the rendered manifests, so the committed YAML and what ekn kubeapply --target ‹name› applies agree.

One entry is here by default: ekn.dev/deployment-unit, holding this unit’s name. It is what makes kubectl get all -A -l ekn.dev/deployment-unit=‹name› answer, and it is what scopes pruning. mkForce a different string if the name does not suit. Do not remove it and do not make it a function: an assertion rejects both, because a unit that cannot be selected by this label cannot be pruned, and its objects look unowned to the next whole-instance prune.

A value may be a function of the object rather than a string, for metadata that has to encode the object’s own identity – ekn.lib.argocdTrackingId is one. Such a function may return null to leave that one object alone, which is not a nicety: ArgoCD deliberately puts no tracking annotation on a CRD, so stamping one anyway would make every CRD permanently OutOfSync.

These win over labels the object already carries. That is deliberate and it matters: Helm charts routinely set app.kubernetes.io/instance to their release name, which is exactly the key a GitOps engine may be using to decide ownership.

An object routed here also appears in kubernetes.generated, and carries the same stamp there. Both copies must agree: a whole-instance apply and a --target apply write as the same field manager, so a stamp on only one of them flips with whichever apply ran last. See stampRouted in kubernetes.nix.

Type: attribute set of (string or function that evaluates to a(n) (null or string))

Default:

{ }

Example:

{ "app.kubernetes.io/instance" = "root"; }

Declared by:

deployment.units.<name>.modules

Modules making up a whole separate easykubenix configuration, rendered into this target’s path and nowhere else. Its objects never reach this instance’s kubernetes.generated, so a plain ekn kubeapply (or ekn validate) does not see them – only ekn kubeapply --target ‹name› does.

That separation is the point: this is for the objects a GitOps engine cannot sync because they are what makes it able to sync anything – the engine itself, its credentials, its root Application. They get applied once, by hand, and then have a different lifecycle from everything else.

The nested instance is a complete easykubenix configuration, not a cut-down one, so it can render a Helm chart like any other. It receives its parent’s evaluated config as the parent module argument, and its ekn.environment defaults to this instance’s.

Definitions concatenate, so several modules can each add to one target’s list.

Type: list of module

Default:

[ ]

Example:

[ ./bootstrap/argocd.nix ]

Declared by:

deployment.units.<name>.path

Subdirectory within deployBranch/sourceBranch where this target’s manifests are stored.

Type: string

Default:

"./"

Declared by:

ekn.assertCached

Substituters to ask, before applying anything, whether they can serve every store path this apply names. A path on none of them refuses the apply.

Defaults to ekn.cacheTo, the destinations this same run has just pushed to. So the check asks whether the push arrived, and no second list exists to drift from the first. cacheTo = null leaves this empty and turns the check off, which is right for an instance with no CSI-backed store: there is nothing to assert and it should not pay for the check.

The list has to be the caches the nodes carry, not the ones the deploying machine happens to have configured – those are usually more, and a path only the deployer can fetch passes a check and then fails to mount. cacheTo is already that list, curated by whoever knows where the paths are consumed, so this follows it rather than asking for the same knowledge twice. Set this option directly where the two differ.

What it is for. A CSI-mounted store path can only be substituted – a node cannot build it, because the volume names an output path. So a path on no substituter is a mount that fails on a node, minutes later and well away from the apply that caused it. Asserting first turns that into a refusal with the path named.

ekn never consults the local store, only these substituters, so a path this machine happens to have built does not pass.

ekn walks the whole closure rather than the named paths, which is the shape that actually bites – a present top path whose closure member is absent.

A path is missing when every substituter said so, and the check is broken when one could not answer at all. ekn refuses either way and says which, because a cache that is down and a path that was never pushed need different fixes.

Any store URI works – https, ssh-ng, s3, file – because the query goes through nanopynix rather than a scheme-specific fetcher. See issue #37.

The default answers “did the push arrive”, and that is a weaker question than “can a cold node get this”. A destination that is itself a workload in the cluster – pynixd on nixlab2 – serves a path exactly as long as that workload runs. The check unions its substituters, so one of them holding the path is enough to pass, and a path held only by pynixd passes while pynixd is up.

Drop that destination from this list to ask the harder question. A list of only the caches that do not depend on the cluster reports a pynixd-only path as missing, which is the outage this guard exists for rather than a false positive.

This is the guard, not the fix. It does not push anything; see ekn.cacheTo.

Type: list of string

Default:

[ ]

Example:

[
  "https://nixkube.cachix.org"
]

Declared by:

ekn.cacheAcceptNewHostKeys

Accept the host key of an ssh:///ssh-ng:// destination in ekn.cacheTo the first time ekn meets it, instead of failing the push. ekn does this by adding -o StrictHostKeyChecking=accept-new to NIX_SSHOPTS, which is what Nix passes to OpenSSH.

A key that changed is still refused. accept-new trusts a host the first time only, so this gives up first-contact trust and keeps the protection that matters for a host you already deploy to.

The default is true because the alternative is not a safer deploy. ekn deploy cannot prompt, so an unknown key stops the deploy with an error that names the connection and not the key, and the operator answers it by running ssh -o StrictHostKeyChecking=accept-new nix@host by hand – the same decision, made under time pressure and with less information. Issue #18.

Set this to false where the host keys are distributed ahead of time, for example by NixOS programs.ssh.knownHosts. A push to an unknown host then fails, and ekn names the host key as the cause.

Your own NIX_SSHOPTS wins: ekn adds nothing when that variable already sets StrictHostKeyChecking.

Type: boolean

Default:

true

Declared by:

ekn.cacheTimeoutSec

Seconds ekn deploy gives the push to ekn.cacheTo before it gives up. null waits forever.

There is a default because forever is what it used to do. ssh has no connect timeout of its own here, so a host that drops packets rather than refusing them leaves the push in SYN-SENT until the kernel exhausts its SYN retries, with no output the whole time – which reads as a slow validate or a hung process rather than as a cache nobody can reach. Issue #20 has the measurement.

--cache-allow-failure does not help with that on its own: it decides what to do once the push returns, and the point here is that it does not return. The two work together – this bounds the wait, and that flag decides whether the deploy continues afterwards.

This bounds the whole push, not the connect, because that is the part a store URI gives us any control over. So it has to be larger than a real push of the manifest’s store paths over the slowest link you deploy across, and the default is generous for that reason rather than tuned.

Type: null or (positive integer, meaning >0)

Default:

600

Example:

120

Declared by:

ekn.cacheTo

Destination Nix store URI (e.g. “ssh-ng://user@host:2222”), or a list of them, that ekn deploy and ekn kubeapply push to before they act. null disables the cache push entirely.

This is the only knob. What gets pushed is not a choice: it is every store path the rendered manifest names, found in the manifest’s text. That is the same set ekn.assertCached asks about, so the push and the assertion cannot disagree about what an apply names.

The text, and not a closure, because Nix string context is what a module can take away. nixkube’s discardStringContext strips it from every resource annotated nixkube/discard. Measured on nixkube’s own instance:

store paths named as text in manifest.json 4 paths in its closure 1 (itself)

The four are the node and pynixd environments – exactly the paths a node needs to boot.

A stripped path is also not realised on the deploying machine, and ekn refuses the push rather than moving a smaller set. Set nixkube.discardStringContext = false to keep the context that realises them.

The destination substitutes what it can reach itself, so a path both ends can fetch from a public cache never crosses this link.

A list is allowed because one destination cannot always be enough. A store path is only useful where whatever mounts it can reach it, and a single destination cannot serve a path whose consumer is that destination. On nixlab2 cacheTo is pynixd, and pynixd’s own environment is mounted over CSI on a node – so pushing that leg here can never bootstrap it, and it needs a substituter that does not depend on pynixd.

Every destination is pushed to, in order. A failure in any of them fails the push, subject to --cache-allow-failure: a path that reached only some of the stores that need it is the case this option exists to avoid.

A plain string stays valid and means what it always did. This option does not decide which destinations an instance needs – that is a property of where its paths are consumed.

Type: null or string or list of string

Default:

null

Example:

"ssh-ng://nix@cache.example.com:2222"

Declared by:

ekn.clusterUid

The cluster this configuration is allowed to reach: the uid of the kube-system Namespace. ekn kubeapply, ekn reclaim, ekn clusterdiff and ekn secrets refuse to act against any other.

The ambient kubeconfig is not a neutral default. It is usually a valid, reachable, credentialed cluster that happens to be the wrong one, and there is no error to notice: the apply succeeds until something unrelated collides. This option is what removes the default rather than guarding it.

Kubernetes has no cluster ID resource. kube-system is created once at bootstrap and never re-created, its uid is server-generated and never reused, and reading it needs no permission an apply does not already have.

Left null, every one of those commands refuses and prints the line to paste here. Passing --i-dont-know-which-cluster-this-is runs anyway; it is deliberately hard to type and reads badly in a reviewed file, which is the point.

A rebuilt cluster is a different cluster. It gets a new uid and this value has to change with it. That is the correct answer, not an inconvenience: nothing else would have told you.

Type: null or string matching the pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$

Default:

null

Example:

"3b2a1f0e-9d8c-4b7a-8e6f-5d4c3b2a1f0e"

Declared by:

ekn.environment

Value of the ekn.dev/environment label stamped on every object ekn applies, and half of the selector it lists objects back by when pruning.

The other half is ekn.dev/deployment-unit, which every deployment unit renders onto its own objects. Together they give two prune scopes:

  • ekn kubeapply --prune deletes objects carrying this environment and no unit label, which the current apply did not produce.

  • ekn kubeapply --target <name> --prune deletes objects carrying this environment and that unit’s label, which the current apply did not produce.

The environment label is stamped by ekn at apply time rather than rendered. That is what confines pruning to objects ekn itself applied: an object a GitOps engine synced from the committed YAML never carries it, so ekn and that engine cannot end up deleting each other’s work.

Required, with no default, and that is deliberate.

A default is a value every project shares until somebody thinks to change it. Two easykubenix projects that both took it and both deploy to one cluster do not conflict quietly: each apply prunes the other’s objects, because from where it stands they are objects carrying its own label that its own apply did not produce. The scope is per kind, not the whole cluster, but a kind both projects use is the normal case.

Nothing can detect that from inside one project, so the name has to be a decision somebody made. Only a deploy needs it: rendering a manifest never reads this, so a configuration that only builds manifestJSONFile does not have to answer.

Changing the value of a project that already deployed is not free. Live objects carry the label they were applied with, and --prune finds objects by that label alone. Pick a different word and the next prune no longer recognises anything the previous one would have: those objects are not deleted, they are orphaned, and nothing reports it.

Type: string

Example:

"acme-production"

Declared by:

ekn.preApplyCommand

Program ekn kubeapply runs before it touches the cluster, and before the ekn.cacheTo push. A non-zero exit aborts the apply.

It runs before the push on purpose. The case it exists for is a path the push cannot place: a store the cluster reads but this machine cannot write through ekn.cacheTo, or a cache that has to hold a path before the workload serving ekn.cacheTo restarts. A hook that ran afterwards could not repair what the push depends on.

ekn passes the JSON manifest of the objects this apply will send as the first argument, and in EKN_MANIFEST. It is the objects themselves, so a --target slice is the slice and a seeded Secret carries its resolved value. EKN_ENVIRONMENT holds ekn.environment, and EKN_TARGET the --target names joined by commas, or the empty string for a whole-instance apply.

A non-zero exit always aborts, and there is no flag to soften that. The hook decides what it tolerates; ekn tells it what the deploy tolerates and lets the script logic deal with it:

EKN_CACHE_PUSH 1 unless --no-cache-push EKN_CACHE_ALLOW_FAILURE 1 with --cache-allow-failure

Both are 1 or 0, so [ "$EKN_CACHE_ALLOW_FAILURE" = 1 ] needs no case handling. A hook that wants to be advisory reads them and exits 0 itself.

Every store path the apply names is in that manifest as text, so a hook that has to seed a cache can read them out of it rather than being told them:

grep -o ‘/nix/store/[a-z0-9]{32}-[^"]*’ “$1” | sort -u

types.path, and Nix string context is kept, so ekn builds the program before it runs it. ekn then executes that path directly.

Wrap a package in lib.getExe. pkgs.writeScriptBin and its relatives build a directory with the script under bin/, and ekn executes the path it is given.

This is the pre half of issue #5. kluctl.preDeployScript is the same hook in the namespace it predates.

Type: null or absolute path

Default:

null

Example:

lib.getExe (
  pkgs.writeScriptBin "seed-the-cache" ''
    #!${pkgs.runtimeShell}
    set -euo pipefail
    grep -o '/nix/store/[a-z0-9]\{32\}-[^"]*' "$1" | sort -u \
      | ${pkgs.cachix}/bin/cachix push nixkube
  ''
)

Declared by:

ekn.resourcePriority

Apply order by object kind, lowest number first. Objects sharing a number apply together as one barrier, which is fully applied (and, for CustomResourceDefinitions, waited on to become Established) before the next barrier starts.

Defaults to Helm’s InstallOrder, numbered in steps of ten so kinds can be slotted between two neighbours without renumbering the rest.

A kind absent from this set applies at 1000, which is every custom resource. That gives four bands to number against:

  • 10-380 Helm’s order as-is

  • 381-999 late, but still before custom resources

  • 1000 custom resources and anything else unlisted

  • 1001+ after custom resources

APIService and the two admission webhook configurations sit in that last band, which is the one place this deviates from Helm. Helm sorts unknown kinds after its whole list, leaving all three ahead of every custom resource they intercept; during a bootstrap their backing workload was applied seconds earlier and is not serving yet, so each intercepted request costs a full timeout or fails discovery outright.

The four in-apiserver admission kinds deviate the other way: ValidatingAdmissionPolicy, MutatingAdmissionPolicy and their bindings sit right after CustomResourceDefinition. They need no backend, so they apply before every object they admit, and a pod of the same apply is mutated and validated like any later one.

Definitions merge per key, so setting one kind keeps the defaults for every other. Replacing the set wholesale takes lib.mkForce.

Type: attribute set of signed integer

Default:

{
  APIService = 1010;
  ClusterRole = 160;
  ClusterRoleBinding = 180;
  ClusterRoleBindingList = 190;
  ClusterRoleList = 170;
  ConfigMap = 110;
  CronJob = 330;
  CustomResourceDefinition = 150;
  DaemonSet = 250;
  Deployment = 290;
  HorizontalPodAutoscaler = 300;
  Ingress = 350;
  IngressClass = 340;
  Job = 320;
  LimitRange = 50;
  MutatingAdmissionPolicy = 152;
  MutatingAdmissionPolicyBinding = 154;
  MutatingWebhookConfiguration = 1020;
  Namespace = 20;
  NetworkPolicy = 30;
  PersistentVolume = 130;
  PersistentVolumeClaim = 140;
  Pod = 260;
  PodDisruptionBudget = 70;
  PodSecurityPolicy = 60;
  PriorityClass = 10;
  ReplicaSet = 280;
  ReplicationController = 270;
  ResourceQuota = 40;
  Role = 200;
  RoleBinding = 220;
  RoleBindingList = 230;
  RoleList = 210;
  Secret = 90;
  SecretList = 100;
  Service = 240;
  ServiceAccount = 80;
  StatefulSet = 310;
  StorageClass = 120;
  ValidatingAdmissionPolicy = 152;
  ValidatingAdmissionPolicyBinding = 154;
  ValidatingWebhookConfiguration = 1030;
}

Declared by:

helm.package

The kubernetes-helm package to use.

Type: package

Default:

pkgs.kubernetes-helm

Declared by:

helm.releases

Attribute set of helm releases.

A thin wrapper over ekn.lib.importHelm, which is the primitive and takes the same arguments. Use the function directly when building objects from a plain function rather than from a module – it returns a config fragment you place with lib.mkMerge, with no option to declare and read back.

Type: attribute set of (submodule)

Default:

{ }

Declared by:

helm.releases.<name>.apiVersions

API versions to tell Helm the target cluster has (--api-versions), for a chart that gates templates on .Capabilities.APIVersions.Has.

helm template normally fills that list from the live cluster. Rendering here runs against no cluster, so the list is empty and every such check answers “absent” – however many CRDs the real cluster has.

The quiet failure is the one to watch for. A chart that calls fail stops the render with a readable message (cilium does this). A chart that wraps the template in if .Capabilities.APIVersions.Has instead emits nothing: the render succeeds, the apply succeeds, every Application reports Healthy, and the object is simply absent. Measured on a real cluster: ArgoCD gates all seven of its ServiceMonitor templates this way and offers no override value, so the component deploying everything else was the one thing nothing scraped. Six further charts that same repository pins carry the same gate – argo-workflows, oauth2-proxy, reloader, openebs, aws-ebs-csi – so it is a class rather than one chart’s quirk, and the right place to set this is wherever every chart is rendered rather than on each one that bites.

Nothing downstream can catch it. A render missing a gated object and a render that correctly had none are byte-identical – there is no artefact to compare, no object to be absent from a diff, nothing for validation to reject. That is why it has to be answered here, where the question is asked. The chart is not wrong: its if exists so it does not emit a ServiceMonitor into a cluster that would reject one. It answered accurately about the cluster helm template was pointed at, which was no cluster.

Helm matches these strings literally, and charts disagree on which spelling they test – ArgoCD asks for monitoring.coreos.com/v1, external-secrets for monitoring.coreos.com/v1/ServiceMonitor. One form leaves the other chart rendering nothing, so pass both.

Type: list of string

Default:

[ ]

Example:

[
  "monitoring.coreos.com/v1"
  "monitoring.coreos.com/v1/ServiceMonitor"
]

Declared by:

helm.releases.<name>.chart

Helm chart to use

Type: package or absolute path

Declared by:

helm.releases.<name>.crdSplit

Route CustomResourceDefinitions to kubernetes.crds instead of kubernetes.resources. See ekn.lib.importYaml for the measured cost of turning it off, and for the pipeline stages a split CRD skips.

Type: boolean

Default:

true

Declared by:

helm.releases.<name>.includeCRDs

Whether to include CRDs.

Warning: Always including CRDs here is dangerous and can break CRs in your cluster as CRDs may be updated unintentionally. An interactive helm install NEVER updates CRDs, only installs them when they are not existing. See https://github.com/helm/community/blob/aa8e13054d91ee69857b13149a9652be09133a61/hips/hip-0011.md

Only set this to true if you know what you are doing and are manually checking the included CRDs for breaking changes whenever updating the Helm chart.

Type: boolean

Default:

false

Declared by:

helm.releases.<name>.kubeVersion

Kubernetes version to build chart for

Type: string

Default:

"1.37.1"

Declared by:

helm.releases.<name>.name

Helm release name

Type: string

Default:

"‹name›"

Declared by:

helm.releases.<name>.namespace

Namespace to install the helm chart to.

This is .Release.Namespace and --namespace, and it also creates the Namespace object. It does NOT put a namespace on an object whose template omits one – see ekn.lib.importHelm.

Type: null or string

Default:

null

Declared by:

helm.releases.<name>.noHooks

Wether to include Helm hooks.

Without this all hooks run immediately on apply since we are bypassing the Helm CLI. However, some charts only have minor validation hooks (e.g., upgrade version skew validation) and are safe to ignore.

Type: boolean

Default:

false

Declared by:

helm.releases.<name>.transformers

Functions from this release’s whole object list to a new one, applied in order. Passed straight through to ekn.lib.importHelm and on to ekn.lib.importYaml, where the semantics are documented – they run before the CRD split and before the objects are grouped by namespace, they see CRDs, and a marker is safe in one.

This replaced a per-object overrides option. map f expresses that, so the two hooks bought nothing but an ordering rule to remember.

Type: list of function that evaluates to a(n) list of (attribute set)

Default:

[ ]

Example:

[ (objects: map (object: object // { metadata = object.metadata // { namespace = "app"; }; }) objects) ]

Declared by:

helm.releases.<name>.values

Values to pass to chart

Type: Kubernetes-shaped JSON value (plain JSON, plus explicit mkNamedList/mkNumberedList/mkReplaceList/mkReplaceWhere override-by-name/index/prefix/predicate support)

Default:

{ }

Declared by:

importyaml

Kubernetes manifests imported from YAML.

A thin wrapper over ekn.lib.importYaml, which is the primitive and takes the same arguments. Use the function directly when building objects from a plain function rather than from a module – it returns a config fragment you place with lib.mkMerge, with no option to declare and read back.

Type: attribute set of (submodule)

Default:

{ }

Declared by:

importyaml.<name>.crdSplit

Route CustomResourceDefinitions to kubernetes.crds instead of kubernetes.resources. See ekn.lib.importYaml for the measured cost of turning it off, and for the pipeline stages a split CRD skips.

Type: boolean

Default:

true

Declared by:

importyaml.<name>.src

Should be either a derivation or URL for builtins.fetchTree

Type: package or string

Declared by:

importyaml.<name>.transformers

Functions from this source’s whole object list to a new one, applied in order. Passed straight through to ekn.lib.importYaml, where the semantics are documented – they run before the CRD split and before the objects are grouped by namespace, they see CRDs, and a marker is safe in one.

This replaced a per-object overrides option. map f expresses that, so the two hooks bought nothing but an ordering rule to remember.

Type: list of function that evaluates to a(n) list of (attribute set)

Default:

[ ]

Example:

[ (objects: map (object: object // { metadata = object.metadata // { namespace = "app"; }; }) objects) ]

Declared by:

kluctl.package

The kluctl package to use.

Type: package

Default:

pkgs.kluctl

Declared by:

kluctl.deployment

Anything to be rendered into deployment.yaml

Type: Kubernetes-shaped JSON value (plain JSON, plus explicit mkNamedList/mkNumberedList/mkReplaceList/mkReplaceWhere override-by-name/index/prefix/predicate support)

Default:

{ }

Declared by:

kluctl.excludeDeploymentUnits

Names of deployment.units whose objects kluctl should not deploy. Use this once a target has its own deployment path (e.g. a one-time bootstrap apply plus the GitOps controller syncing itself from there) to avoid kluctl and that controller fighting over the same objects. Objects routed to a target not listed here still deploy via kluctl as normal, so a GitOps migration can move one target at a time instead of all-or-nothing.

Type: list of string

Default:

[ ]

Example:

[
  "bootstrap"
]

Declared by:

kluctl.files

Attribute set where name is filename and value is string to be put into the file

Type: attribute set of string

Default:

{ }

Declared by:

kluctl.postDeployScript

Bash script that runs just after deploying

Type: strings concatenated with “\n”

Default:

""

Declared by:

kluctl.preDeployScript

Bash script that runs just before deploying, useful to push manifests to a binary cache. JSON manifest file is passed as first argument

Type: strings concatenated with “\n”

Default:

""

Declared by:

kluctl.project

Anything to be rendered into .kluctl.yaml

Type: Kubernetes-shaped JSON value (plain JSON, plus explicit mkNamedList/mkNumberedList/mkReplaceList/mkReplaceWhere override-by-name/index/prefix/predicate support)

Default:

{
  targets = [
    {
      name = "local";
    }
  ];
}

Declared by:

kubernetes.package

The kubernetes package to use.

Type: package

Default:

pkgs.kubernetes

Declared by:

kubernetes.apiMappingFile

A JSON file to extend apiMappings. Generated by calling kubectl api-resources --output=json > mappings.json

Type: absolute path

Default:

/home/runner/work/easykubenix/easykubenix/easykubenix/apiResources/v1.33.json

Declared by:

kubernetes.apiMappings

Map of kind to apiVersion. Merged with mappings from apiMappingFile.

Type: attribute set of string

Default:

{ }

Example:

{
  Cluster = "cluster.x-k8s.io/v1beta1";
  HCloudMachineTemplate = "infrastructure.cluster.x-k8s.io/v1beta1";
  HCloudRemediationTemplate = "infrastructure.cluster.x-k8s.io/v1beta1";
  HelmChartProxy = "addons.cluster.x-k8s.io/v1alpha1";
  HelmReleaseProxy = "addons.cluster.x-k8s.io/v1alpha1";
  HetznerCluster = "infrastructure.cluster.x-k8s.io/v1beta1";
  KubeadmConfigTemplate = "bootstrap.cluster.x-k8s.io/v1beta1";
  KubeadmControlPlane = "controlplane.cluster.x-k8s.io/v1beta1";
  MachineDeployment = "cluster.x-k8s.io/v1beta1";
  MachineHealthCheck = "cluster.x-k8s.io/v1beta1";
}

Declared by:

kubernetes.clusterInfo.ipFamilies

IP families the cluster’s service CIDR serves – what a Service’s spec.ipFamilies and spec.ipFamilyPolicy must fit for admission (the API server’s --service-cluster-ip-range). One entry is a single-stack cluster, both is DualStack. A Service the stack cannot serve is denied outright at admission; declaring the real stack here moves that denial to evaluation time with the Service named, and picks validation.serviceSubnet to match. The default is the single-stack IPv4 cluster that 10.96.0.0/16 serves.

Type: non-empty (list of (one of “IPv4”, “IPv6”))

Default:

[
  "IPv4"
]

Declared by:

kubernetes.clusterInfo.serviceCidr.ipv4

The cluster’s IPv4 service CIDR. The default is the range the single-stack IPv4 default of ipFamilies describes. Set it to null only together with dropping IPv4 from ipFamilies.

Type: null or string matching the pattern ([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}

Default:

"10.96.0.0/16"

Declared by:

kubernetes.clusterInfo.serviceCidr.ipv6

The cluster’s IPv6 service CIDR; null – the default – on an IPv4-only cluster. Set it together with adding IPv6 to ipFamilies.

Type: null or string

Default:

null

Declared by:

kubernetes.coerceLabelsAndAnnotations

Kubernetes labels/annotations are strictly map[string]string – every value must be a string, anywhere a labels or annotations attrset appears (not just top-level metadata: pod templates, job templates, PVC templates etc. each carry their own).

When enabled (the default), Nix bool values are converted via lib.boolToString and int values via builtins.toString automatically. Values are always verified to be strings regardless of this setting – disabling it only turns bools/ints from “auto-fixed” into “hard error at eval time”, it never disables the check itself.

Type: boolean

Default:

true

Declared by:

kubernetes.crds

Pre-rendered, already-complete Kubernetes objects (typically CustomResourceDefinitions from Nix-rendered Helm charts) that bypass kubernetes.objects’ per-object submodule and its expensive ekn.lib.kubeValueType value-checking entirely. Still passes through ekn.deploymentUnit GitOps routing and the final YAML render, just skips generators/transformers/filters and the auto-defaulting kubernetes.objects provides.

Type: list of (attribute set)

Default:

[ ]

Declared by:

kubernetes.deploymentUnits

Objects grouped by the deployment.units.<name> they route to (via ekn.deploymentUnit), each group paired with that target’s resolved {path}. Objects with no ekn.deploymentUnit set are omitted. The branch(es) these all land on come from the instance-wide deployment.deployBranch/deployment.sourceBranch, not from the target. This is derived before the ekn field is stripped from rendered Kubernetes objects.

Each entry also carries dependencies: every unit that one needs on the cluster, transitively, itself excluded. See deployment.units.<name>.dependencies.

Type: anything (read only)

Declared by:

kubernetes.filters

List of functions that filter objects

Type: list of function that evaluates to a(n) boolean

Default:

[ ]

Declared by:

kubernetes.generated

The final, generated Kubernetes list objects

Type: anything (read only)

Declared by:

kubernetes.generatedByPath

The final, generated Kubernetes objects by attrPath

Type: anything (read only)

Declared by:

kubernetes.generatedExportable

generated, minus every object holding an ekn.envSeed reference.

The two lists differ by who applies them, not by what they contain:

  • generated is for ekn, which resolves a seed reference at apply time by substituting the environment variable it names.

  • generatedExportable is for anything that hands objects to a different applier – a GitOps target that ArgoCD then syncs, kluctl, or a manifest a person feeds to kubectl apply.

A seeded object must not reach the second kind. Nothing else can resolve the reference, so it would apply $ekn:env:VARNAME as the literal value and overwrite a live credential with it.

ekn validate deliberately stays on generated: a reference is a plain string, so the manifest is schema-valid and the schema check has nothing to object to.

Type: anything (read only)

Declared by:

kubernetes.generatedWithEkn

Like generated, but with each object’s ekn routing metadata still attached (generated strips it). Consumed by anything that needs to see EKN metadata alongside the rendered object, e.g. deploymentUnits.

Type: anything (read only)

Declared by:

kubernetes.generators

List of functions that generate object attrsets

Type: list of function that evaluates to a(n) (attribute set)

Default:

[ ]

Example:

''
  kubernetes.generators = [
    (
      object:
      lib.optionalAttrs
        (
          (lib.elem (object.kind or "") [
            "Deployment"
            "StatefulSet"
            "DaemonSet"
          ])
          && object.metadata.annotations.genvpa or "true" == "true"
          && !lib.hasAttrByPath [
            object.metadata.namespace
            "VerticalPodAutoscaler"
            object.metadata.name
          ] config.kubernetes.objects
        )
        {
          apiVersion = "autoscaling.k8s.io/v1";
          kind = "VerticalPodAutoscaler";
          metadata = { inherit (object.metadata) name namespace; };
          spec = {
            targetRef = {
              inherit (object) apiVersion kind;
              inherit (object.metadata) name;
            };
            updatePolicy.updateMode = "InPlaceOrRecreate";
          };
        }
    )
  ];
''

Declared by:

kubernetes.novalidateKeys

{kind, namespace, name} triples for every object with ekn.novalidate = true – deliberately just the identifying keys, not the full objects (which generated/manifestJSONFile already provide), so ekn validate can force_json this cheaply without re-forcing the entire generated set a second time. Used by Validate.run() to skip applying objects that can never be meaningfully verified in its throwaway etcd+apiserver harness (e.g. an aggregated APIService with no real backing Service running).

Type: anything (read only)

Declared by:

kubernetes.objectChecks

Functions from a rendered object to { errors ? [ ]; warnings ? [ ]; }, lists of messages. An error throws and a warning prints when an output forces the object.

Use this instead of assertions for a check of one object. An assertion walks every object for any output, so ekn kubeapply --target would pay for the whole instance. A check here costs only the objects an output holds.

Not a transformers entry: any transformer turns on the deep marker pass over every object, and transformers skip kubernetes.crds.

Type: list of function that evaluates to a(n) (attribute set)

Default:

[ ]

Example:

[
  (object: {
    errors = lib.optional (object.kind == "Pod")
      "${object.metadata.name}: use a Deployment, not a bare Pod";
  })
]

Declared by:

kubernetes.objects

Kubernetes objects, grouped by namespace, then kind. apiVersion is automatically injected (if apiMappings for the object exists) kind is automatically injected metadata.name is automatically injected metadata.namespace is automatically injected if namespace isn’t “none”

Type: attribute set of open submodule of attribute set of open submodule of attribute set of open submodule of (Kubernetes-shaped JSON value (plain JSON, plus explicit mkNamedList/mkNumberedList/mkReplaceList/mkReplaceWhere override-by-name/index/prefix/predicate support)) with mkIfExists support with mkIfExists support with mkIfExists support

Default:

{ }

Example:

{
  kubernetes = {
    objects = {
      easykubenix = {
        ConfigMap = {
          myconfig = {
            data = {
              key = "value";
            };
          };
        };
      };
      none = {
        Namespace = {
          easykubenix = { };
        };
      };
    };
  };
}

Declared by:

kubernetes.objects.<name>.<name>.<name>.apiVersion

The object’s apiVersion. Evaluation fails when it is not set and the kind has no kubernetes.apiMappings entry.

Type: string

Default: the kubernetes.apiMappings entry of the object’s kind

kubernetes.objects.<name>.<name>.<name>.ekn

EKN-only metadata for this Kubernetes object.

Type: submodule

Default:

{ }

kubernetes.objects.<name>.<name>.<name>.ekn.after

Objects that must exist before ekn kubeapply --converge creates this one. Rendered as the kappy.dev/after annotation; the converging apply holds the object, without sending it, until each exists, for up to --wait-seconds.

For what the API server accepts and a controller then abandons: a CloudNativePG Cluster created before its managed role’s password Secret records the role as unreconcilable and never retries it. A dependency may be something no render contains, such as a Secret a controller writes.

Creation only: an object that already exists is applied whatever its dependencies are doing. The barrier apply and the validation harness ignore it.

Type: list of (submodule)

Default:

[ ]

kubernetes.objects.<name>.<name>.<name>.ekn.after.*.apiVersion

The apiVersion of the object to wait for.

Type: string

kubernetes.objects.<name>.<name>.<name>.ekn.after.*.kind

The kind of the object to wait for.

Type: string

kubernetes.objects.<name>.<name>.<name>.ekn.after.*.name

The name of the object to wait for.

Type: string

kubernetes.objects.<name>.<name>.<name>.ekn.after.*.namespace

Null for a cluster-scoped object.

Type: null or string

Default:

null

kubernetes.objects.<name>.<name>.<name>.ekn.deploymentUnit

Name of the deployment.units.<name> this object routes to. Deliberately single-valued, not a list: an object synced by two GitOps targets at once means two controllers independently reconciling (and potentially pruning) the same resource.

The field itself is EKN-only and is stripped before Kubernetes manifests are rendered. Its value is not: the object’s copy in kubernetes.deploymentUnits carries it in the ekn.dev/deployment-unit label, so the unit an object belongs to survives to the cluster whatever applies it. The copy in kubernetes.generated is unstamped, as it is for every other per-unit label.

Type: null or string

Default:

null

kubernetes.objects.<name>.<name>.<name>.ekn.novalidate

Skip this object in ekn validate’s ephemeral etcd+kube-apiserver harness. For objects that can never be meaningfully verified there regardless of manifest correctness – e.g. an aggregated APIService, whose backing Service/Pod never actually runs in a throwaway, single-shot apiserver with no controllers or kubelets, so the apiserver 500s/503s on it (or on discovery calls that touch it) no matter what. Applied for real via ekn kubeapply/GitOps as normal; this only affects the validation harness.

Type: boolean

Default:

false

kubernetes.objects.<name>.<name>.<name>.ekn.probe

How ekn kubeapply sees that this object is in effect: a server dry-run and the answer it must give. Exactly one of matches and denied. Rendered as the kappy.dev/probe annotation.

The apply holds every object of a later ekn.resourcePriority until the probe passes, for up to --ready-seconds. A probe that does not pass fails this object and all it holds.

For an admission policy, which the API server loads asynchronously and which has no status that says so. Set it on the last object of a set, the binding: a policy acts only once its binding exists, and a probe on the policy holds its own binding back. The validation harness ignores it.

Type: null or (submodule)

Default:

null

kubernetes.objects.<name>.<name>.<name>.ekn.probe.denied

In effect when the dry-run is refused with this substring in the message. For a validating policy or a webhook. Use a string only this object says, such as its name: a Pod Security refusal also refuses.

Type: null or string

Default:

null

kubernetes.objects.<name>.<name>.<name>.ekn.probe.matches

In effect when the dry-run is admitted and the stored object contains this partial object. Lists compare item by item and must have the same length. For a mutating policy.

Type: null or (attribute set of anything)

Default:

null

kubernetes.objects.<name>.<name>.<name>.ekn.probe.object

The manifest to server-dry-run. It must pass validation and Pod Security in its namespace, its namespace must sort before this object, and its name must not exist.

Type: attribute set of anything

Default:

{ }

kubernetes.objects.<name>.<name>.<name>.kind

The object’s kind.

Type: string

Default: the attribute name above the object

kubernetes.objects.<name>.<name>.<name>.metadata

The object’s metadata. Other fields than these pass through as written. namespace is set from the attribute name above the kind, except under none.

Type: open submodule of (Kubernetes-shaped JSON value (plain JSON, plus explicit mkNamedList/mkNumberedList/mkReplaceList/mkReplaceWhere override-by-name/index/prefix/predicate support))

Default:

{ }

kubernetes.objects.<name>.<name>.<name>.metadata.annotations

The object’s annotations.

Type: (attribute set of ((string or signed integer convertible to it) or boolean convertible to it)) or value <null> (singular enum) convertible to it

Default:

{ }

kubernetes.objects.<name>.<name>.<name>.metadata.labels

The object’s labels.

Type: (attribute set of ((string or signed integer convertible to it) or boolean convertible to it)) or value <null> (singular enum) convertible to it

Default:

{ }

kubernetes.objects.<name>.<name>.<name>.metadata.name

The object’s name.

Type: string

Default: the object’s attribute name

kubernetes.rawFiles

Escape hatch from the module system for objects that must reach the cluster/git byte-identical to how they exist on disk – the normal kubernetes.resources -> generated pipeline round-trips every object through Nix’s own attrset representation, which builtins.toJSON always serializes with alphabetized keys, and which gains new sibling fields (ekn, metadata merged in by a module, etc.) that weren’t present when the object was first authored. Both of those permanently invalidate a SOPS whole-document MAC computed before either happened – fine for ekn kubeapply’s own decrypt (ekn.sops.maybe_decrypt passes --ignore-mac, since each field’s own AES-GCM tag is still verified independently), but fatal for a MAC-verifying consumer that offers no such bypass (e.g. ArgoCD’s ksops-enabled repo-server). A raw file sidesteps the round-trip entirely: it must already be the complete manifest from before it was SOPS-encrypted, so nothing about its structure changes between encryption and decryption.

Type: list of (submodule)

Default:

[ ]

Declared by:

kubernetes.rawFiles.*.deploymentUnit

Same routing as ekn.deploymentUnit on a normal object – which deployment.units entry this file belongs to. Null omits it from every deploymentUnits entry (still kubeapply-able via the full kubernetes.rawFiles list, just never committed to a GitOps branch).

A raw file gets none of its unit’s labels/annotations. Nix never parses it – that is the whole point of a raw file – so there is nothing here to stamp. Write any label you want into the file.

ekn.dev/deployment-unit is the exception, and ekn adds it at apply time (see _raw_manifest_in_unit in ekn/src/ekn/eval.py). It is the prune scope, and a routed object without it is deleted by the next whole-instance --prune: that selector excludes the hand-applied units by label value, and notin matches an object with no such label at all. ekn commit still writes the file byte-identical.

Type: null or string

Default:

null

Declared by:

kubernetes.rawFiles.*.path

A complete, already-rendered Kubernetes manifest file (JSON or YAML, apiVersion/kind/metadata all present). Tracked as a Nix input (so it’s pinned/reproducible like everything else), but never parsed into resources/generated – ekn kubeapply/ekn commit read it directly instead.

For the same reason it carries no ekn.dev/manifest-hash unless the file holds one, so ekn kubeapply applies it on every run.

Type: absolute path

Declared by:

kubernetes.resources

Alias of kubernetes.objects.

Type: attribute set of open submodule of attribute set of open submodule of attribute set of open submodule of (Kubernetes-shaped JSON value (plain JSON, plus explicit mkNamedList/mkNumberedList/mkReplaceList/mkReplaceWhere override-by-name/index/prefix/predicate support)) with mkIfExists support with mkIfExists support with mkIfExists support

Declared by:

kubernetes.resources.<name>.<name>.<name>.apiVersion

The object’s apiVersion. Evaluation fails when it is not set and the kind has no kubernetes.apiMappings entry.

Type: string

Default: the kubernetes.apiMappings entry of the object’s kind

kubernetes.resources.<name>.<name>.<name>.ekn

EKN-only metadata for this Kubernetes object.

Type: submodule

Default:

{ }

kubernetes.resources.<name>.<name>.<name>.ekn.after

Objects that must exist before ekn kubeapply --converge creates this one. Rendered as the kappy.dev/after annotation; the converging apply holds the object, without sending it, until each exists, for up to --wait-seconds.

For what the API server accepts and a controller then abandons: a CloudNativePG Cluster created before its managed role’s password Secret records the role as unreconcilable and never retries it. A dependency may be something no render contains, such as a Secret a controller writes.

Creation only: an object that already exists is applied whatever its dependencies are doing. The barrier apply and the validation harness ignore it.

Type: list of (submodule)

Default:

[ ]

kubernetes.resources.<name>.<name>.<name>.ekn.after.*.apiVersion

The apiVersion of the object to wait for.

Type: string

kubernetes.resources.<name>.<name>.<name>.ekn.after.*.kind

The kind of the object to wait for.

Type: string

kubernetes.resources.<name>.<name>.<name>.ekn.after.*.name

The name of the object to wait for.

Type: string

kubernetes.resources.<name>.<name>.<name>.ekn.after.*.namespace

Null for a cluster-scoped object.

Type: null or string

Default:

null

kubernetes.resources.<name>.<name>.<name>.ekn.deploymentUnit

Name of the deployment.units.<name> this object routes to. Deliberately single-valued, not a list: an object synced by two GitOps targets at once means two controllers independently reconciling (and potentially pruning) the same resource.

The field itself is EKN-only and is stripped before Kubernetes manifests are rendered. Its value is not: the object’s copy in kubernetes.deploymentUnits carries it in the ekn.dev/deployment-unit label, so the unit an object belongs to survives to the cluster whatever applies it. The copy in kubernetes.generated is unstamped, as it is for every other per-unit label.

Type: null or string

Default:

null

kubernetes.resources.<name>.<name>.<name>.ekn.novalidate

Skip this object in ekn validate’s ephemeral etcd+kube-apiserver harness. For objects that can never be meaningfully verified there regardless of manifest correctness – e.g. an aggregated APIService, whose backing Service/Pod never actually runs in a throwaway, single-shot apiserver with no controllers or kubelets, so the apiserver 500s/503s on it (or on discovery calls that touch it) no matter what. Applied for real via ekn kubeapply/GitOps as normal; this only affects the validation harness.

Type: boolean

Default:

false

kubernetes.resources.<name>.<name>.<name>.ekn.probe

How ekn kubeapply sees that this object is in effect: a server dry-run and the answer it must give. Exactly one of matches and denied. Rendered as the kappy.dev/probe annotation.

The apply holds every object of a later ekn.resourcePriority until the probe passes, for up to --ready-seconds. A probe that does not pass fails this object and all it holds.

For an admission policy, which the API server loads asynchronously and which has no status that says so. Set it on the last object of a set, the binding: a policy acts only once its binding exists, and a probe on the policy holds its own binding back. The validation harness ignores it.

Type: null or (submodule)

Default:

null

kubernetes.resources.<name>.<name>.<name>.ekn.probe.denied

In effect when the dry-run is refused with this substring in the message. For a validating policy or a webhook. Use a string only this object says, such as its name: a Pod Security refusal also refuses.

Type: null or string

Default:

null

kubernetes.resources.<name>.<name>.<name>.ekn.probe.matches

In effect when the dry-run is admitted and the stored object contains this partial object. Lists compare item by item and must have the same length. For a mutating policy.

Type: null or (attribute set of anything)

Default:

null

kubernetes.resources.<name>.<name>.<name>.ekn.probe.object

The manifest to server-dry-run. It must pass validation and Pod Security in its namespace, its namespace must sort before this object, and its name must not exist.

Type: attribute set of anything

Default:

{ }

kubernetes.resources.<name>.<name>.<name>.kind

The object’s kind.

Type: string

Default: the attribute name above the object

kubernetes.resources.<name>.<name>.<name>.metadata

The object’s metadata. Other fields than these pass through as written. namespace is set from the attribute name above the kind, except under none.

Type: open submodule of (Kubernetes-shaped JSON value (plain JSON, plus explicit mkNamedList/mkNumberedList/mkReplaceList/mkReplaceWhere override-by-name/index/prefix/predicate support))

Default:

{ }

kubernetes.resources.<name>.<name>.<name>.metadata.annotations

The object’s annotations.

Type: (attribute set of ((string or signed integer convertible to it) or boolean convertible to it)) or value <null> (singular enum) convertible to it

Default:

{ }

kubernetes.resources.<name>.<name>.<name>.metadata.labels

The object’s labels.

Type: (attribute set of ((string or signed integer convertible to it) or boolean convertible to it)) or value <null> (singular enum) convertible to it

Default:

{ }

kubernetes.resources.<name>.<name>.<name>.metadata.name

The object’s name.

Type: string

Default: the object’s attribute name

kubernetes.sopsAgeIdentities

SOPS age decrypt identities some GitOps engine needs mounted as a Secret before it can sync anything referencing them (e.g. ArgoCD’s ksops-enabled repo-server, see argocd.nix’s ksops.enable). Purely additive metadata – any module wanting this generic bootstrap behavior appends an entry here instead of hand-rolling its own bootstrap script. ekn kubeapply ensures each declared identity exists (generating a fresh age keypair the first time one is missing) before applying, and – when sopsConfigFile is set – registers the public key as a recipient there and re-runs sops updatekeys on sopsFiles, aborting the whole apply rather than proceeding half-configured if either step fails. This is the one place ekn actually generates key material – everywhere else, ekn only ever decrypts what SOPS already produced (see ekn.sops.maybe_decrypt).

Type: list of (submodule)

Default:

[ ]

Declared by:

kubernetes.sopsAgeIdentities.*.key

Key within the Secret’s data the age identity is stored under.

Type: non-empty string

Default:

"key.txt"

Declared by:

kubernetes.sopsAgeIdentities.*.namespace

Namespace the identity Secret lives in.

Type: non-empty string

Declared by:

kubernetes.sopsAgeIdentities.*.secretName

Name of the Secret holding the age identity.

Type: non-empty string

Declared by:

kubernetes.sopsAgeIdentities.*.sopsConfigFile

Path to the .sops.yaml this identity’s age public key should be registered in as a recipient (idempotent – skipped if already present in every matching creation rule). Null (the default) leaves .sops.yaml untouched; the public key is only logged for manual handling.

Type: null or absolute path

Default:

null

Declared by:

kubernetes.sopsAgeIdentities.*.sopsFiles

Already-SOPS-encrypted files to run sops updatekeys on after the recipient is added to sopsConfigFile, so their data key gets rewrapped for the new identity too. Only consulted when sopsConfigFile is set.

Type: list of absolute path

Default:

[ ]

Declared by:

kubernetes.templates

Typed resource template functions, usually created with the Adios-backed template helper.

Type: attribute set of function that evaluates to a(n) (Kubernetes-shaped JSON value (plain JSON, plus explicit mkNamedList/mkNumberedList/mkReplaceList/mkReplaceWhere override-by-name/index/prefix/predicate support))

Default:

{ }

Declared by:

kubernetes.transformers

List of functions that transform object attrsets

Type: list of function that evaluates to a(n) (attribute set)

Default:

[ ]

Example:

''
  kubernetes.transformers = [
    (
      object:
      # Apply annotations to all LoadBalancers
      if object.kind == "Service" && object.spec.type or null == "LoadBalancer" then
        lib.recursiveUpdate object {
          # IPv4 is scarce, share!
          metadata.annotations."metallb.io/allow-shared-ip" = "true";
          # Lowest TTL cloudflare allows
          metadata.annotations."external-dns.alpha.kubernetes.io/ttl" = "60";
        }
      # Make all services require dualstack
      else if object.kind == "Service" then
        lib.recursiveUpdate object {
          spec.ipFamilyPolicy = "RequireDualStack";
        }
      # Set lowest cloudflare TTL for ingress and gapi routes
      else if
        lib.elem object.kind [
          "Ingress"
          "HTTPRoute"
        ]
      then
        lib.recursiveUpdate object {
          metadata.annotations."external-dns.alpha.kubernetes.io/ttl" = "60";
        }
      else
        object
    )
  ];
''

Declared by:

validation.debug

Whether to enable validation debugging.

Type: boolean

Default:

false

Example:

true

Declared by:

validation.etcdPackage

The etcd package to use.

Type: package

Default:

pkgs.etcd

Declared by:

validation.kubeadmConfig

The kubeadm ClusterConfiguration that makes the validation harness’s certificates. Every default is mkDefault, so a definition here replaces one field and keeps the rest.

Type: Kubernetes-shaped JSON value (plain JSON, plus explicit mkNamedList/mkNumberedList/mkReplaceList/mkReplaceWhere override-by-name/index/prefix/predicate support)

Default:

null

Declared by:

validation.openapiSpec

A directory of Kubernetes OpenAPI v3 group-version documents, in the layout of the Kubernetes source tree’s api/openapi-spec/v3. validation.schemaCheck and ekn schemacheck --offline read the built-in kinds’ schemas from it.

Type: string

Default:

"${config.kubernetes.package.src}/api/openapi-spec/v3"

Declared by:

validation.podSubnet

The pod CIDR the validation harness gives kubeadm.

Type: string

Default:

"10.97.0.0/16"

Declared by:

validation.schemaCheck

A build that checks every rendered object against its JSON schema: built-in kinds from validation.openapiSpec, custom resources from the CRDs in the same render, with the CRDs’ CEL rules. Rendered ValidatingAdmissionPolicies and their bindings are evaluated against the objects they match. No network and no API server, so it runs in the build sandbox. It fails on any violation, and lists the objects whose kind it has no schema for.

Type: package (read only)

Declared by:

validation.serviceSubnet

The service CIDRs of the validation harness, comma-separated. The API server’s --service-cluster-ip-range and the kubeadm certificates both use it.

Type: string

Default: the CIDRs of kubernetes.clusterInfo.serviceCidr, IPv4 first

Declared by: