Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- If you don't want `vals` expansion when pushing images (e.g., in air-gapped environments), set `docker.useVals = false`.
- The `kubenix` package is now built with `writeShellApplication`; `vals` and `kubectl` are runtime dependencies and are no longer exposed as standalone binaries in the package output (e.g. `${kubenix}/bin/kubectl` no longer exists).
- The `customTypes.*.module` option type changed from `unspecified` to `deferredModule` (default is now a freeform attrs module). Configs that passed non-module values may need to adapt (`c4b461b`).
- Modules evaluated through `kubenix.evalModules.<system>` no longer receive a `pkgs` carrying the kubenix overlay, so `pkgs.kubenix.evalModules` is not available inside a module body. Use the `kubenix` module argument instead, which has always exposed `evalModules` alongside `lib` and `modules`. Applying `kubenix.overlays.default` to your own nixpkgs is unaffected.

### Added

- `lib/eval-modules.nix`, the module evaluator as a plain function of `pkgs`, so consumers without flake machinery (nixpkgs, for one) can evaluate modules with `import <kubenix>/lib/eval-modules.nix { inherit pkgs; }`.
- Image registry, name, and tag can now use `vals` syntax for dynamic/secret expansion at runtime.
- Customizable registry protocol (`docker.registry.protocol` and `docker.images.*.registry.protocol`).
- Support for Kubernetes 1.28 through 1.36.
Expand All @@ -54,6 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- `overlays.default` now evaluates modules against the package set it extends. It previously returned the flake's own `evalModules.<system>`, which is pinned to kubenix's nixpkgs, so overlays and `config` applied by the consumer were discarded; it also only resolved for systems kubenix itself declares.
- ConfigMap `data` and `binaryData` keys with a leading underscore are no longer stripped (#44).
- Custom resources are no longer assumed to have a `spec` field.
- `resultYAML` now includes hash labels by building from `kubernetes.generated.items`.
Expand Down
14 changes: 14 additions & 0 deletions flake/checks.nix
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@
in
{
checks = {
# Pins the interface that `evalModules` and the `kubenix` special argument expose to
# module authors, so refactors of the evaluator have something to be measured against.
eval-modules = import ../tests/eval-modules.nix {
inherit pkgs;
# Imported directly rather than taken from perSystem, so this check covers the entry
# point a non-flake consumer uses. Every other check reaches it through the flake.
evalModules = import ../lib/eval-modules.nix { inherit pkgs; };
};

overlay = import ../tests/overlay.nix {
inherit pkgs;
overlay = self.overlays.default;
};

# Check that all packages build
# TODO: impure examples (helm/image/pod) aren't packages yet; once their default.nix
# is pure they join `self'.packages` and build here automatically (matches packages.nix).
Expand Down
44 changes: 8 additions & 36 deletions flake/eval-modules.nix
Original file line number Diff line number Diff line change
@@ -1,44 +1,16 @@
{ inputs, self, config, ... }:
{ inputs, config, ... }:
let
# evalModules with same interface as lib.evalModules and kubenix as special argument
mkEvalModules = system:
let
pkgs = inputs.nixpkgs.legacyPackages.${system};
in
attrs @ { module ? null, modules ? [ module ], ... }:
let
lib' = pkgs.lib.extend (lib: _self: import ../lib/upstreamables.nix { inherit lib pkgs; });
attrs' = builtins.removeAttrs attrs [ "module" ];
in
lib'.evalModules (pkgs.lib.recursiveUpdate
{
modules = modules ++ [{
config._module.args = {
inherit pkgs;
name = "default";
};
}];
specialArgs = {
pkgs = import inputs.nixpkgs {
inherit (pkgs.stdenv.hostPlatform) system;
overlays = [ self.overlays.default ];
config.allowUnsupportedSystem = true;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why the reason for this was but it seems like we lost it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a regression -- I was experimenting with agentic workflows and created a pull request before self-reviewing. Sorry for the delay; I'll re-request review when it's ready.

};

kubenix = {
lib = import ../lib { inherit pkgs; inherit (pkgs) lib; };
evalModules = self.evalModules.${pkgs.stdenv.hostPlatform.system};
modules = self.nixosModules.kubenix;
};
};
}
attrs');
# Kept in lib/ so consumers without flake machinery, such as nixpkgs, can import the evaluator
# directly. The flake only binds it to a package set per system.
mkEvalModules = pkgs: import ../lib/eval-modules.nix { inherit pkgs; };
in
{
flake.evalModules = inputs.nixpkgs.lib.genAttrs config.systems mkEvalModules;
flake.evalModules = inputs.nixpkgs.lib.genAttrs config.systems (
system: mkEvalModules inputs.nixpkgs.legacyPackages.${system}
);

# Share this system's evalModules with the other perSystem modules.
perSystem = { system, ... }: {
_module.args.evalModules = mkEvalModules system;
_module.args.evalModules = mkEvalModules inputs.nixpkgs.legacyPackages.${system};
};
}
8 changes: 5 additions & 3 deletions flake/overlays.nix
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{ self, ... }:
{ ... }:
{
flake.overlays.default = _final: prev: {
kubenix.evalModules = self.evalModules.${prev.stdenv.hostPlatform.system};
# Bound to the package set being extended, so modules evaluated through it see the consumer's
# nixpkgs. Going via self.evalModules.<system> instead would pin them to kubenix's own.
flake.overlays.default = final: _prev: {
kubenix.evalModules = import ../lib/eval-modules.nix { pkgs = final; };
};
}
29 changes: 29 additions & 0 deletions lib/eval-modules.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# evalModules with same interface as lib.evalModules and kubenix as special argument
{ pkgs }:
let
evalModules = attrs @ { module ? null, modules ? [ module ], ... }:
let
lib' = pkgs.lib.extend (lib: _self: import ./upstreamables.nix { inherit lib pkgs; });
attrs' = builtins.removeAttrs attrs [ "module" ];
in
lib'.evalModules (pkgs.lib.recursiveUpdate
{
modules = modules ++ [{
config._module.args = {
inherit pkgs;
name = "default";
};
}];
specialArgs = {
inherit pkgs;

kubenix = {
lib = import ./. { inherit pkgs; inherit (pkgs) lib; };
inherit evalModules;
modules = import ../modules;
};
};
}
attrs');
in
evalModules
66 changes: 66 additions & 0 deletions tests/eval-modules.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{ pkgs
, evalModules
, ...
}:
let
inherit (pkgs) lib;

pod = evalModules {
module = { kubenix, ... }: {
imports = [ kubenix.modules.k8s ];
kubernetes.resources.pods.example.spec.containers.example.image = "nginx";
};
};

# Reaches every member of the `kubenix` special argument from inside a module: `modules` in
# `imports` position, `lib` in a value position, and `evalModules` for a nested evaluation.
probe = evalModules {
module = { kubenix, lib, ... }: {
imports = [ kubenix.modules.k8s ];

options.probe = lib.mkOption { type = lib.types.attrs; };

config.probe = {
libNamespaces = builtins.attrNames kubenix.lib;
nested = (kubenix.evalModules {
module = { kubenix, ... }: {
imports = [ kubenix.modules.k8s ];
kubernetes.resources.configMaps.nested.data.key = "value";
};
}).config.kubernetes.api.resources.configMaps.nested.metadata.name;
};
};
};

assertions = [
{
message = "evalModules renders a Pod with apiVersion, kind and name";
assertion =
let o = pod.config.kubernetes.api.resources.pods.example;
in o.apiVersion == "v1" && o.kind == "Pod" && o.metadata.name == "example";
}
{
message = "kubenix.modules is reachable from a module's imports";
assertion = pod.config.kubernetes.api.resources.pods ? example;
}
{
message = "kubenix.lib exposes the k8s, docker and helm namespaces";
assertion = probe.config.probe.libNamespaces == [ "docker" "helm" "k8s" ];
}
{
message = "kubenix.evalModules supports nested evaluation";
assertion = probe.config.probe.nested == "nested";
}
];

failures = map (a: a.message) (builtins.filter (a: !a.assertion) assertions);
in
pkgs.runCommand "kubenix-eval-modules-interface"
{
passthru = { inherit pod probe; };
}
(
if failures == [ ]
then "echo success > $out"
else "echo ${lib.escapeShellArg (builtins.concatStringsSep "\n" failures)} >&2; exit 1"
)
35 changes: 35 additions & 0 deletions tests/overlay.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{ pkgs
, overlay
, ...
}:
let
inherit (pkgs) lib;

marker = "sees-extended-package-set";

overlaid = pkgs.extend (
lib.composeExtensions overlay (_final: _prev: { kubenixOverlayMarker = marker; })
);

assertions = [
{
message = "the overlay provides pkgs.kubenix.evalModules";
assertion = (overlaid ? kubenix) && builtins.isFunction overlaid.kubenix.evalModules;
}
{
message = "the overlay's evalModules evaluates modules against the extended package set";
assertion = (overlaid.kubenix.evalModules {
module = { pkgs, lib, ... }: {
options.marker = lib.mkOption { default = pkgs.kubenixOverlayMarker or null; };
};
}).config.marker == marker;
}
];

failures = map (a: a.message) (builtins.filter (a: !a.assertion) assertions);
in
pkgs.runCommand "kubenix-overlay" { } (
if failures == [ ]
then "echo success > $out"
else "echo ${lib.escapeShellArg (builtins.concatStringsSep "\n" failures)} >&2; exit 1"
)