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’spathand stay out of this instance’skubernetes.generatedentirely, 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 --prunedeletes objects carrying this environment and no unit label, which the current apply did not produce.ekn kubeapply --target <name> --prunedeletes 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-380Helm’s order as-is381-999late, but still before custom resources1000custom resources and anything else unlisted1001+ 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:
generatedis forekn, which resolves a seed reference at apply time by substituting the environment variable it names.generatedExportableis for anything that hands objects to a different applier – a GitOps target that ArgoCD then syncs,kluctl, or a manifest a person feeds tokubectl 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: