diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c25644..0efa665 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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.` 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 /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. @@ -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.`, 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`. diff --git a/flake/checks.nix b/flake/checks.nix index 8d3bf0b..843dc1b 100644 --- a/flake/checks.nix +++ b/flake/checks.nix @@ -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). diff --git a/flake/eval-modules.nix b/flake/eval-modules.nix index a5fd791..fe48f12 100644 --- a/flake/eval-modules.nix +++ b/flake/eval-modules.nix @@ -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; - }; - - 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}; }; } diff --git a/flake/overlays.nix b/flake/overlays.nix index 738527d..9b8a6a9 100644 --- a/flake/overlays.nix +++ b/flake/overlays.nix @@ -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. instead would pin them to kubenix's own. + flake.overlays.default = final: _prev: { + kubenix.evalModules = import ../lib/eval-modules.nix { pkgs = final; }; }; } diff --git a/lib/eval-modules.nix b/lib/eval-modules.nix new file mode 100644 index 0000000..b096f4e --- /dev/null +++ b/lib/eval-modules.nix @@ -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 diff --git a/tests/eval-modules.nix b/tests/eval-modules.nix new file mode 100644 index 0000000..5952ede --- /dev/null +++ b/tests/eval-modules.nix @@ -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" + ) diff --git a/tests/overlay.nix b/tests/overlay.nix new file mode 100644 index 0000000..1b9f31d --- /dev/null +++ b/tests/overlay.nix @@ -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" +)