From 03f0a0aea24a76a1d7530d421377f2eea07377b8 Mon Sep 17 00:00:00 2001 From: Chris Hodapp Date: Fri, 16 Oct 2026 00:37:01 -0700 Subject: [PATCH 1/2] feat(disko): add the caisson-disko and disko integrations A disko configuration is a disk layout, with the NixOS configurations installed onto it declared inside it. Two integrations take it: - caisson-disko, the native one, owns the `caisson-disko` class. The layout is evaluated with caisson's own `evalModules` over `disko.devices`, typed with disko's type library, and the class adds `assertions`, which refuse a layout. Its outputs are the layout product: the device tree as data and its hash, the NixOS settings the devices produce, disko's metadata, and the scripts and packages for a package set. The NixOS configurations inside get the layout's `fileSystems`, `boot` and `swapDevices` through the registered NixOS module `caisson-disko-layout`, and nothing of disko. - disko, the upstream-compatible one, owns the `disko` class. A file written for disko is its module as it is. The NixOS configurations inside get the registered NixOS module `disko-layout`, which imports disko's NixOS module and defines `disko.devices` from the layout at default priority, so disko's command, `disko-install`, the image builder and the install test work on them. Both publish the layout as `diskoConfigurations.`, the device tree with disko's internal attributes left out, which disko's command reads. Both pass up the NixOS configuration declared as `default` under the name of the layout, through `lib.caisson.integrations.renameDefault`. caisson-core is locked to nix-caisson/caisson-core#50, which keeps the eval-weight scenario under its ceiling with the two integrations added. Co-Authored-By: Claude Opus 5.5 --- README.md | 5 +- configs/structural/impl/default.nix | 2 + docs/development/repository-structure.md | 5 +- docs/development/testing-architecture.md | 2 +- docs/development/testing.md | 7 +- flake.lock | 6 +- lib-overlays/caisson-disko/default.nix | 368 ++++++++++++++++++ lib-overlays/disko/default.nix | 204 ++++++++++ lib-overlays/integrations/default.nix | 23 ++ modules/caisson-disko/core | 1 + modules/caisson-disko/default/default.nix | 38 ++ modules/disko/core | 1 + modules/disko/default/default.nix | 37 ++ modules/generic/core/default.nix | 2 + .../nixos/caisson-disko-layout/default.nix | 60 +++ modules/nixos/disko-layout/default.nix | 39 ++ tests/argument-errors/check.sh | 8 +- tests/dependencies/flake.lock | 27 +- tests/dependencies/flake.nix | 2 + tests/pinned-world/default.nix | 253 +++++++++++- tests/unit/lib-overlays.nix | 39 ++ 21 files changed, 1104 insertions(+), 25 deletions(-) create mode 100644 lib-overlays/caisson-disko/default.nix create mode 100644 lib-overlays/disko/default.nix create mode 120000 modules/caisson-disko/core create mode 100644 modules/caisson-disko/default/default.nix create mode 120000 modules/disko/core create mode 100644 modules/disko/default/default.nix create mode 100644 modules/nixos/caisson-disko-layout/default.nix create mode 100644 modules/nixos/disko-layout/default.nix diff --git a/README.md b/README.md index d15b7756..fccec4db 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,10 @@ source by. | `home-manager` | `home-manager` | Home Manager configurations | | `home-manager-minimal` | `home-manager` | the `homeManager` class with Home Manager's necessary modules alone, the configuration importing what it uses from `modulesPath`; a second integration over the class `home-manager` owns | | `terranix` | `terranix` | Terranix and Terraform configurations | -| `colmena` | `colmena` | colmena configurations (class `caisson-colmena`), projected onto the hive colmena's binary reads; not compatible with colmena's hive modules | +| `caisson-colmena` | `colmena` | colmena configurations whose machines are NixOS configurations declared inside them, published as the hive colmena's command reads; the colmena integration we recommend for new configurations | +| `colmena` | `colmena` | a hive written for colmena, taken as it is, with each node a NixOS configuration; for porting an existing hive and the modules written for it | +| `caisson-disko` | `disko` | disk layouts, with the NixOS configurations installed onto each declared inside it; a machine gets the layout's file systems and no disko scripts; the disko integration we recommend for new configurations | +| `disko` | `disko` | disk layouts written as disko reads them, whose machines import disko's NixOS module, so disko's command, `disko-install` and its image builder work on them | | `system-manager` | `system-manager` | system-manager configurations on foreign distros | | `structural` | none | a configuration that only exports and holds configurations beneath it: the top of a repository whose point is what it exports, such as caisson itself, or a layer at any depth | diff --git a/configs/structural/impl/default.nix b/configs/structural/impl/default.nix index 00ad2b9d..61fa480a 100644 --- a/configs/structural/impl/default.nix +++ b/configs/structural/impl/default.nix @@ -26,6 +26,8 @@ home-manager-minimal caisson-colmena colmena + caisson-disko + disko terranix system-manager ; diff --git a/docs/development/repository-structure.md b/docs/development/repository-structure.md index 749dbdea..631f4c74 100644 --- a/docs/development/repository-structure.md +++ b/docs/development/repository-structure.md @@ -26,7 +26,8 @@ without nixpkgs can depend on it directly. The layer users reach for: the integrations (`structural`, `flake-parts`, `nixpkgs`, `nixos`, `nixos-minimal`, `home-manager`, -`home-manager-minimal`, `colmena`, `terranix`, `system-manager`), each +`home-manager-minimal`, `caisson-colmena`, `colmena`, `caisson-disko`, +`disko`, `terranix`, `system-manager`), each a library overlay contributing its `lib.caisson` namespace, registering its module class where it has a class (`nixos-minimal` and `home-manager-minimal` @@ -49,7 +50,7 @@ inject the same names beside `self`. ## The pinned world The upstream world caisson is tested against (nixpkgs, home-manager, -colmena, terranix, system-manager, and the test tooling) is pinned in +colmena, disko, terranix, system-manager, and the test tooling) is pinned in `tests/dependencies/flake.lock`, a lock only the checks and formatter partitions read, so caisson's `flake.nix` carries no churning pin. The pinned-world suite, `tests/pinned-world`, composes caisson with those diff --git a/docs/development/testing-architecture.md b/docs/development/testing-architecture.md index 9fd644d2..0b2faffb 100644 --- a/docs/development/testing-architecture.md +++ b/docs/development/testing-architecture.md @@ -311,6 +311,6 @@ The checks partition merges outputs from all test and example flakes into the | `literate-flake-default` | `examples/literate-flake/` | Example default package builds | | `literate-flake-greeting` | `examples/literate-flake/` | Example greeting package builds | | `debug-disabled` | `checks.nix` (inline) | `self.debug` is not exposed in production | -| `pinned-world` | `checks.nix` + `tests/pinned-world/` | caisson composed with the pinned upstream world (nixpkgs, home-manager, colmena, terranix, system-manager) evaluates end to end; the `drift` workflow runs the same check against advanced pins | +| `pinned-world` | `checks.nix` + `tests/pinned-world/` | caisson composed with the pinned upstream world (nixpkgs, home-manager, colmena, disko, terranix, system-manager) evaluates end to end; the `drift` workflow runs the same check against advanced pins | | `argument-errors` | `checks.nix` + `tests/argument-errors/` | The argument error of every entry point comes from Nix itself, raised at the call site with no frame of caisson | | `eval-weight` | `checks.nix` + `tests/eval-weight/` | Framework evaluation cost held to committed ceilings ([guide](https://nix-caisson.github.io/docs/eval-weight.html)) | diff --git a/docs/development/testing.md b/docs/development/testing.md index 088050d1..f1be2f88 100644 --- a/docs/development/testing.md +++ b/docs/development/testing.md @@ -23,9 +23,10 @@ plumbing works. ## Pinned-World Suite - **Scope:** caisson composed with concrete pinned versions of the upstream - world (nixpkgs, home-manager, colmena, terranix, system-manager) and - exercised end to end: a home configuration, a NixOS system with - homes declared inside it, a colmena hive, a terranix and a system-manager + world (nixpkgs, home-manager, colmena, disko, terranix, system-manager) + and exercised end to end: a home configuration, a NixOS system with + homes declared inside it, a colmena hive, disk layouts with the + machines installed onto them, a terranix and a system-manager configuration, and the composition guarantees of the real exported overlays. Every upstream expectation caisson relies on is a probe here. - **Location:** `tests/pinned-world/default.nix`; the pins in diff --git a/flake.lock b/flake.lock index 96d5171c..f69bc317 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "caisson-core": { "locked": { - "lastModified": 1792046586, - "narHash": "sha256-3B8nnMAKVnZzm0oAmBCS24OAcSOqFOgA/m7sWnEZgJY=", + "lastModified": 1792136033, + "narHash": "sha256-vdOZ5tKU+s5fWLNDxJG1YzcfTV1oDqrrRMLkO52CC3w=", "owner": "nix-caisson", "repo": "caisson-core", - "rev": "8b44b618b3cc9cc4bb18c24c182fedd3795d6eee", + "rev": "02f9189a4f2ce2652fbdefa550ec03fabd101fbf", "type": "github" }, "original": { diff --git a/lib-overlays/caisson-disko/default.nix b/lib-overlays/caisson-disko/default.nix new file mode 100644 index 00000000..3e5b5b93 --- /dev/null +++ b/lib-overlays/caisson-disko/default.nix @@ -0,0 +1,368 @@ +# SPDX-License-Identifier: MIT +# +# The caisson-disko integration, the native disko integration: it owns +# the `caisson-disko` class. A disko configuration is a disk layout: a +# module of that class with `disko.devices`, disko's grammar for the +# disks, typed with disko's type library. It declares the NixOS +# configurations installed onto the layout the way any configuration +# declares NixOS configurations, under `caisson.nixos.configurations`, +# and it may declare none: a data array, a removable disk. +# +# The evaluation is caisson's own `evalModules` over that option, the +# framework modules, the selection and the configuration's module. It +# runs on the composed library, with the type library built from the +# disko source the composition declares. Its outputs are the layout +# product: the device tree as data, the NixOS settings the devices +# produce (`fileSystems`, `boot`, `swapDevices`), and the scripts that +# format, mount and destroy the disks, each a function of a package +# set. The class also declares `assertions`, which a module of the +# class uses to refuse a layout; every output fails with the messages +# of the assertions that do not hold. +# +# The default module of the class adds caisson's `caisson-disko-layout` +# NixOS module to the NixOS configurations declared inside the layout. +# That module gives a NixOS configuration the layout's `fileSystems`, +# `boot` and `swapDevices`, and nothing of disko itself: a NixOS +# configuration beneath a caisson-disko configuration carries no disko +# module, so the scripts that format and destroy the disks are not +# reachable from it. They are outputs of the layout alone. +# +# A disko configuration is published as `diskoConfigurations.`, +# in the form disko's command reads: `disko.devices`, the evaluated +# device tree with disko's internal attributes left out. +{ + mkLibOverlay, + ... +}: +{ + + imports = [ + # The library of nixpkgs, which this overlay calls through the + # composed library, imported by key so it is composed wherever + # this is. + ((mkLibOverlay ../nixpkgs-lib) // { key = "nixpkgs-lib"; }) + # What an integration is written from, imported by key so it is + # composed wherever this integration is. + ((mkLibOverlay ../integrations) // { key = "integrations"; }) + ]; + + overlay = + final: prev: + let + + resolveEcosystemSrc = final.caisson.integrations.resolveEcosystemSrc { + name = "disko"; + context = "caisson.caisson-disko"; + }; + + # A disko source: a disko flake or a disko source tree, either of + # which holds the type library at `lib/default.nix` and the NixOS + # module at `module.nix`. + assertDiskoEcosystemSrc = + ecosystemSrc: + let + path = if builtins.isAttrs ecosystemSrc then ecosystemSrc.outPath or null else ecosystemSrc; + in + if path != null && builtins.pathExists "${path}/lib/default.nix" then + ecosystemSrc + else + throw "lib.caisson.caisson-disko requires `ecosystemSrc` to be a disko flake or source tree (holding lib/default.nix)."; + + # The disko source of a configuration: `explicit` when given, else + # what the composition declares or pins under the name `disko`, as + # recorded on `manifest`. + ecosystemSrcOf = + { explicit, manifest }: + assertDiskoEcosystemSrc (resolveEcosystemSrc { + inherit explicit manifest; + }); + + # disko's type library, from the disko source, over `lib`. The + # types are built from `rootMountPoint`, the directory the mount + # script mounts the layout under. + diskoLibOf = + { + lib, + src, + rootMountPoint, + }: + import "${src}/lib" { inherit lib rootMountPoint; }; + + # The option a disko configuration is evaluated against: disko's + # devices grammar, typed by disko's type library. + devicesOptions = + diskoLib: + { lib, ... }: + { + options.disko.devices = lib.mkOption { + type = diskoLib.toplevel; + default = { }; + description = "The disk layout, in disko's devices grammar."; + }; + }; + + # The assertions of the class: a module of the class adds one to + # refuse a layout, with a message saying why. + assertionsOptions = + { lib, ... }: + { + options.assertions = lib.mkOption { + type = lib.types.listOf ( + lib.types.submodule { + options = { + assertion = lib.mkOption { + type = lib.types.bool; + description = "Whether the layout is accepted."; + }; + message = lib.mkOption { + type = lib.types.str; + description = "Why the layout is refused, when `assertion` is false."; + }; + }; + } + ); + default = [ ]; + description = '' + Conditions the layout must meet. Every output of the disko + configuration fails with the message of each assertion that + is false. + ''; + }; + }; + + # The device tree as data: disko's internal attributes, whose + # names begin with `_`, and functions left out. + stripInternal = + value: + if builtins.isAttrs value then + final.concatMapAttrs ( + name: inner: + if final.hasPrefix "_" name || builtins.isFunction inner then + { } + else + { ${name} = stripInternal inner; } + ) value + else if builtins.isList value then + builtins.map stripInternal (builtins.filter (inner: !builtins.isFunction inner) value) + else + value; + + # The layout product of an evaluated disko configuration, from + # its `disko.devices`, the assertions it must meet, the mount + # point its types were built from and whether its scripts are + # checked with shellcheck. `context` names the integration in the + # message of a refused layout. + layoutOf = + { + context, + devices, + assertions, + rootMountPoint, + checked, + }: + let + failed = builtins.map (assertion: assertion.message) ( + builtins.filter (assertion: !assertion.assertion) assertions + ); + accepted = + if failed == [ ] then + devices + else + throw '' + ${context}: the layout is refused: + ${builtins.concatStringsSep "\n" (builtins.map (message: "- ${message}") failed)} + ''; + serializableLayout = stripInternal accepted; + in + { + inherit serializableLayout rootMountPoint; + # The identity of the layout: the hash of the device tree as + # data, so two evaluations that lower to the same disks have + # the same hash. + layoutHash = builtins.hashString "sha256" (builtins.toJSON serializableLayout); + # The NixOS settings the devices produce, `fileSystems`, + # `boot` and `swapDevices`, each a definition for a NixOS + # option, which may be built with `mkMerge` or `mkIf`. + nixosConfig = accepted._config; + # disko's record of which device depends on which. + meta = accepted._meta; + # The packages the scripts run, for a package set. + packages = pkgs: accepted._packages pkgs; + # The scripts disko makes from the layout, for a package set: + # `format`, `mount`, `unmount`, `destroy`, `formatMount`, + # `destroyFormatMount`, and each of them without its + # dependencies (`formatNoDeps` and so on). + scripts = pkgs: accepted._scripts { inherit pkgs checked; }; + }; + + # The evaluator's call, on the lib of the view being evaluated: + # the devices option and the assertions, the framework modules of + # the class, the selection and the configuration's module, with + # the composition's pinned sources as the `inputs` special + # argument. `ecosystemArgs`, which only the twin's pattern admits, + # is merged over the call last. + evaluate = + args: + { lib, manifest }: + let + selection = lib.caisson.integrations; + src = ecosystemSrcOf { + explicit = args.ecosystemSrc or null; + inherit manifest; + }; + rootMountPoint = if (args.rootMountPoint or null) != null then args.rootMountPoint else "/mnt"; + diskoLib = diskoLibOf { inherit lib src rootMountPoint; }; + registry = lib.caisson.modules.caisson-disko or { }; + frameworkModules = selection.frameworkModules "caisson-disko" registry; + moduleImports = selection.moduleImportsOf "caisson-disko" { inherit lib manifest; } args; + + # The configuration's module: the module passed, else the + # configuration registered under the configuration's name + # (`configs/caisson-disko/`), else none. + name = manifest.name or null; + registered = lib.caisson.configs.caisson-disko or { }; + configModule = + if (args.configModule or null) != null then + args.configModule + else if name != null then + registered.${name} or null + else + null; + + evaluated = lib.evalModules ( + { + class = "caisson-disko"; + specialArgs = { + inherit lib; + inputs = manifest.sources; + } + // (if (args.pkgSets or null) != null then { inherit (args) pkgSets; } else { }) + // (if (args.specialArgs or null) != null then args.specialArgs else { }); + modules = [ + (devicesOptions diskoLib) + assertionsOptions + ] + ++ frameworkModules + ++ moduleImports registry + ++ (if configModule == null then [ ] else [ configModule ]); + } + // (if (args.ecosystemArgs or null) != null then args.ecosystemArgs else { }) + ); + in + { + value = evaluated; + outputs = layoutOf { + context = "lib.caisson.caisson-disko"; + inherit (evaluated.config) assertions; + inherit rootMountPoint; + devices = evaluated.config.disko.devices; + checked = args.checked or false; + }; + }; + + # A disko configuration is published as + # `diskoConfigurations.`, which disko's command reads with + # its own evaluator: `disko.devices`, the device tree as data. + exportsTo = { + attrset = "diskoConfigurations"; + value = manifest: { disko.devices = manifest.outputs.serializableLayout; }; + }; + + configuration = + args: + final.caisson.integrations.mkModuleConfiguration { + type = "caisson-disko"; + defaultPkgs = args.defaultPkgs or null; + inherit exportsTo; + evaluate = evaluate args; + }; + + integration = final.caisson.integrations.mkIntegration { + name = "caisson-disko"; + class = "caisson-disko"; + # What these return is a configuration, a function of + # `{ name, parent }`: the parent that declares it under + # `caisson.caisson-disko.configurations.` finalizes it. + mkConfiguration = + { + # The disko configuration's module: `disko.devices`, and + # the NixOS configurations installed onto the layout under + # `caisson.nixos.configurations`. When absent, the + # configuration registered under the configuration's name + # (`lib.caisson.configs.caisson-disko.`), if any. + # Further modules of the class are selected with + # `moduleImports`, from the registry. + configModule ? null, + # The package sets, handed to the modules as the `pkgSets` + # special argument. + pkgSets ? null, + # The package set the configurations beneath this one run + # on: a function that receives the package sets available + # to each, by package config name, and returns the set to + # run on. + defaultPkgs ? null, + # The disko flake or source tree; resolved from the + # composition's declarations when absent. + ecosystemSrc ? null, + # The selection over the caisson-disko class of the + # registry. It replaces the default of the class, which is + # every entry named `default` followed by what the + # configurations above added. + moduleImports ? null, + # A selection added to that selection, whichever it is. + extraModuleImports ? null, + # Extra module arguments, merged over those the framework + # supplies. + specialArgs ? null, + # The directory the mount script mounts the layout under; + # "/mnt" when absent. + rootMountPoint ? null, + # Whether the scripts are checked with shellcheck when they + # are built; false when absent. + checked ? null, + }@args: + configuration args; + # The same arguments and `ecosystemArgs`, the evaluator's + # arguments merged over the composed call last. + mkConfigurationWithEcosystemArgs = + { + configModule ? null, + pkgSets ? null, + defaultPkgs ? null, + ecosystemSrc ? null, + moduleImports ? null, + extraModuleImports ? null, + specialArgs ? null, + rootMountPoint ? null, + checked ? null, + ecosystemArgs ? null, + }@args: + configuration args; + # What the upstream-compatible disko integration builds on, so + # both integrations read disko the same way and make the same + # layout product. + extra = { + # The disko source of a configuration: `explicit` when given, + # else what the composition declares or pins under the name + # `disko`, as recorded on `manifest`; refused when it is no + # disko source. + inherit ecosystemSrcOf; + # disko's type library from `{ lib, src, rootMountPoint }`. + inherit diskoLibOf; + # The module declaring `disko.devices`, for a type library. + inherit devicesOptions; + # The layout product of an evaluated layout. + inherit layoutOf; + # How a disko configuration is published: as + # `diskoConfigurations.`. + inherit exportsTo; + }; + }; + in + { + caisson = (prev.caisson or { }) // { + caisson-disko = integration.namespace; + }; + }; + +} diff --git a/lib-overlays/disko/default.nix b/lib-overlays/disko/default.nix new file mode 100644 index 00000000..14cac2e8 --- /dev/null +++ b/lib-overlays/disko/default.nix @@ -0,0 +1,204 @@ +# SPDX-License-Identifier: MIT +# +# The disko integration, the upstream-compatible one: it owns the +# `disko` class. A disko configuration is a disk layout written as +# disko reads it, a module that sets `disko.devices`, and a file +# written for disko is the configuration's module as it is. The +# configuration also declares the NixOS configurations installed onto +# the layout, under `caisson.nixos.configurations`. The native +# integration, caisson-disko, keeps disko out of those NixOS +# configurations instead. +# +# The layout is evaluated as disko evaluates a layout on its own: +# `evalModules` over `disko.devices`, typed with disko's type library, +# with the framework modules, the selection and the configuration's +# module beside it. The outputs are the layout product the native +# integration makes, and the disko source the layout was evaluated +# with. +# +# The default module of the class adds caisson's `disko-layout` NixOS +# module to the NixOS configurations declared inside the layout. That +# module imports disko's own NixOS module and defines `disko.devices` +# from the layout, so a NixOS configuration beneath a disko +# configuration is what disko's tools expect: disko's command finds +# `config.disko.devices` and the scripts under `system.build`, +# `disko-install` overrides the disks, and disko's image builder and +# install test extend the configuration as they do for any NixOS +# configuration that imports disko's module. +# +# A disko configuration is published as `diskoConfigurations.`, +# as the native integration publishes it. +{ + mkLibOverlay, + ... +}: +{ + + imports = [ + # The library of nixpkgs, which this overlay calls through the + # composed library, imported by key so it is composed wherever + # this is. + ((mkLibOverlay ../nixpkgs-lib) // { key = "nixpkgs-lib"; }) + # What an integration is written from, imported by key so it is + # composed wherever this integration is. + ((mkLibOverlay ../integrations) // { key = "integrations"; }) + # The native disko integration, whose type library, devices option, + # layout product and publishing this one builds on. + ((mkLibOverlay ../caisson-disko) // { key = "caisson-disko"; }) + ]; + + overlay = + final: prev: + let + native = final.caisson.caisson-disko; + + # The evaluator's call, on the lib of the view being evaluated: + # the devices option, the framework modules of the class, the + # selection and the configuration's module, with the + # composition's pinned sources as the `inputs` special argument. + # `ecosystemArgs`, which only the twin's pattern admits, is merged + # over the call last. + evaluate = + args: + { lib, manifest }: + let + selection = lib.caisson.integrations; + src = native.ecosystemSrcOf { + explicit = args.ecosystemSrc or null; + inherit manifest; + }; + rootMountPoint = if (args.rootMountPoint or null) != null then args.rootMountPoint else "/mnt"; + diskoLib = native.diskoLibOf { inherit lib src rootMountPoint; }; + registry = lib.caisson.modules.disko or { }; + frameworkModules = selection.frameworkModules "disko" registry; + moduleImports = selection.moduleImportsOf "disko" { inherit lib manifest; } args; + + # The configuration's module: the module passed, else the + # configuration registered under the configuration's name + # (`configs/disko/`), else none. + name = manifest.name or null; + registered = lib.caisson.configs.disko or { }; + configModule = + if (args.configModule or null) != null then + args.configModule + else if name != null then + registered.${name} or null + else + null; + + evaluated = lib.evalModules ( + { + class = "disko"; + specialArgs = { + inherit lib; + inputs = manifest.sources; + } + // (if (args.pkgSets or null) != null then { inherit (args) pkgSets; } else { }) + // (if (args.specialArgs or null) != null then args.specialArgs else { }); + modules = [ + (native.devicesOptions diskoLib) + ] + ++ frameworkModules + ++ moduleImports registry + ++ (if configModule == null then [ ] else [ configModule ]); + } + // (if (args.ecosystemArgs or null) != null then args.ecosystemArgs else { }) + ); + in + { + value = evaluated; + outputs = + native.layoutOf { + context = "lib.caisson.disko"; + assertions = [ ]; + inherit rootMountPoint; + devices = evaluated.config.disko.devices; + checked = args.checked or false; + } + // { + # The disko source the layout was evaluated with, whose + # NixOS module the NixOS configurations beneath import. + ecosystemSrc = src; + }; + }; + + configuration = + args: + final.caisson.integrations.mkModuleConfiguration { + type = "disko"; + defaultPkgs = args.defaultPkgs or null; + inherit (native) exportsTo; + evaluate = evaluate args; + }; + + integration = final.caisson.integrations.mkIntegration { + name = "disko"; + class = "disko"; + # What these return is a configuration, a function of + # `{ name, parent }`: the parent that declares it under + # `caisson.disko.configurations.` finalizes it. + mkConfiguration = + { + # The disko configuration's module: `disko.devices`, as a + # file written for disko sets it, and the NixOS + # configurations installed onto the layout under + # `caisson.nixos.configurations`. When absent, the + # configuration registered under the configuration's name + # (`lib.caisson.configs.disko.`), if any. Further + # modules of the class are selected with `moduleImports`, + # from the registry. + configModule ? null, + # The package sets, handed to the modules as the `pkgSets` + # special argument. + pkgSets ? null, + # The package set the configurations beneath this one run + # on: a function that receives the package sets available + # to each, by package config name, and returns the set to + # run on. + defaultPkgs ? null, + # The disko flake or source tree; resolved from the + # composition's declarations when absent. + ecosystemSrc ? null, + # The selection over the disko class of the registry. It + # replaces the default of the class, which is every entry + # named `default` followed by what the configurations above + # added. + moduleImports ? null, + # A selection added to that selection, whichever it is. + extraModuleImports ? null, + # Extra module arguments, merged over those the framework + # supplies. + specialArgs ? null, + # The directory the mount script mounts the layout under; + # "/mnt" when absent. + rootMountPoint ? null, + # Whether the scripts are checked with shellcheck when they + # are built; false when absent. + checked ? null, + }@args: + configuration args; + # The same arguments and `ecosystemArgs`, the evaluator's + # arguments merged over the composed call last. + mkConfigurationWithEcosystemArgs = + { + configModule ? null, + pkgSets ? null, + defaultPkgs ? null, + ecosystemSrc ? null, + moduleImports ? null, + extraModuleImports ? null, + specialArgs ? null, + rootMountPoint ? null, + checked ? null, + ecosystemArgs ? null, + }@args: + configuration args; + }; + in + { + caisson = (prev.caisson or { }) // { + disko = integration.namespace; + }; + }; + +} diff --git a/lib-overlays/integrations/default.nix b/lib-overlays/integrations/default.nix index 77843291..ea953386 100644 --- a/lib-overlays/integrations/default.nix +++ b/lib-overlays/integrations/default.nix @@ -592,6 +592,28 @@ } // (if named ? description then { inherit (named) description; } else { }); + # An `exported` selection that passes up the configuration + # declared under the name `default` under `name` instead, and + # every other configuration under the name it is declared under. + # A disko configuration selects the NixOS configurations installed + # onto it this way, with its own name, so the machine declared as + # `default` inside the layout `nas-1` is published as + # `nixosConfigurations.nas-1`. A configuration declared under + # `name` beside `default` is refused, since both would be passed + # up under that name. + renameDefault = + name: configurations: + if !(configurations ? default) then + configurations + else if configurations ? ${name} then + throw '' + The configurations `default` and `${name}` are declared side by side, + and the configuration `default` is passed up under the name `${name}`. + Rename one of them, or set `exported` to pass them up another way. + '' + else + builtins.removeAttrs configurations [ "default" ] // { ${name} = configurations.default; }; + # A published name, from the segments `caisson-core.elide` keeps # of a path: the segments in path order, separated by `/`. Every # name caisson publishes is written here. @@ -786,6 +808,7 @@ integrationsIn mkEvaluation publish + renameDefault topValue resolveEcosystemSrc mkIntegration diff --git a/modules/caisson-disko/core b/modules/caisson-disko/core new file mode 120000 index 00000000..3715354a --- /dev/null +++ b/modules/caisson-disko/core @@ -0,0 +1 @@ +../generic/core \ No newline at end of file diff --git a/modules/caisson-disko/default/default.nix b/modules/caisson-disko/default/default.nix new file mode 100644 index 00000000..60edaffe --- /dev/null +++ b/modules/caisson-disko/default/default.nix @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: MIT +# +# The default module of the caisson-disko class. +# +# The NixOS configurations declared inside a disko configuration are +# installed onto its layout. This module adds caisson's NixOS module +# `caisson-disko-layout`, which gives a NixOS configuration the +# `fileSystems`, `boot` and `swapDevices` of the layout, to the modules +# that a NixOS configuration declared inside this disko configuration +# imports by default. A NixOS configuration that passes +# `moduleImports` selects its modules itself, and then it lists +# `caisson-disko-layout` itself, as `caisson/caisson-disko-layout` in a +# flake that lists caisson in `projects`. +# +# The NixOS configuration declared as `default` is passed up under the +# name of the disko configuration, so the machine installed onto the +# layout `nas-1` is published as `nixosConfigurations.nas-1`, beside +# `diskoConfigurations.nas-1`. A disko configuration that sets +# `caisson.nixos.exported` passes its NixOS configurations up its own +# way. +# +# `closure-lib` is the library of caisson, where `caisson-disko-layout` +# is registered under that name. +{ closure-lib, ... }: +{ lib, ... }: +{ + # The options under `caisson` are declared by the core module of the + # class. + imports = [ closure-lib.caisson.modules.caisson-disko.core ]; + + caisson.forChildren.extraDefaultModuleImports.nixos = _lib: [ + closure-lib.caisson.modules.nixos.caisson-disko-layout + ]; + + caisson.nixos.exported = lib.mkDefault ( + lib.caisson.integrations.renameDefault lib.caisson-core.evalManifest.name + ); +} diff --git a/modules/disko/core b/modules/disko/core new file mode 120000 index 00000000..3715354a --- /dev/null +++ b/modules/disko/core @@ -0,0 +1 @@ +../generic/core \ No newline at end of file diff --git a/modules/disko/default/default.nix b/modules/disko/default/default.nix new file mode 100644 index 00000000..a1e6846d --- /dev/null +++ b/modules/disko/default/default.nix @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: MIT +# +# The default module of the disko class. +# +# The NixOS configurations declared inside a disko configuration are +# installed onto its layout. This module adds caisson's NixOS module +# `disko-layout`, which imports disko's NixOS module and defines +# `disko.devices` from the layout, to the modules that a NixOS +# configuration declared inside this disko configuration imports by +# default. A NixOS configuration that passes `moduleImports` selects +# its modules itself, and then it lists `disko-layout` itself, as +# `caisson/disko-layout` in a flake that lists caisson in `projects`. +# +# The NixOS configuration declared as `default` is passed up under the +# name of the disko configuration, so the machine installed onto the +# layout `nas-1` is published as `nixosConfigurations.nas-1`, beside +# `diskoConfigurations.nas-1`. A disko configuration that sets +# `caisson.nixos.exported` passes its NixOS configurations up its own +# way. +# +# `closure-lib` is the library of caisson, where `disko-layout` is +# registered under that name. +{ closure-lib, ... }: +{ lib, ... }: +{ + # The options under `caisson` are declared by the core module of the + # class. + imports = [ closure-lib.caisson.modules.disko.core ]; + + caisson.forChildren.extraDefaultModuleImports.nixos = _lib: [ + closure-lib.caisson.modules.nixos.disko-layout + ]; + + caisson.nixos.exported = lib.mkDefault ( + lib.caisson.integrations.renameDefault lib.caisson-core.evalManifest.name + ); +} diff --git a/modules/generic/core/default.nix b/modules/generic/core/default.nix index afb5b627..76be8d6e 100644 --- a/modules/generic/core/default.nix +++ b/modules/generic/core/default.nix @@ -29,7 +29,9 @@ ./caisson (closure-lib.caisson.integrations.configurationsModuleFor [ "caisson-colmena" + "caisson-disko" "colmena" + "disko" "flake-parts" "home-manager" "nixos" diff --git a/modules/nixos/caisson-disko-layout/default.nix b/modules/nixos/caisson-disko-layout/default.nix new file mode 100644 index 00000000..38b4bd38 --- /dev/null +++ b/modules/nixos/caisson-disko-layout/default.nix @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: MIT +# +# A NixOS module for a NixOS configuration installed onto a disk +# layout: a NixOS configuration declared inside a caisson-disko +# configuration. It gives the NixOS configuration the settings the +# layout's devices produce, `fileSystems`, `boot` and `swapDevices`, +# read from the layout through the manifest, at default priority. +# +# The default module of the caisson-disko class adds this module to +# every NixOS configuration declared inside a caisson-disko +# configuration. A NixOS configuration that passes `moduleImports` +# selects its modules itself, and then it lists this module itself. +# +# The NixOS configuration gets nothing of disko itself: no disko module +# and no `disko` option. The scripts that format and destroy the disks +# are outputs of the layout and are not reachable from the NixOS +# configuration. A module of the NixOS configuration that needs more of +# its layout reads it through +# `lib.caisson-core.evalManifest.nearest.caisson-disko`. +# +# The settings are copied under a fixed set of keys, since the module +# system needs the keys a module defines before it evaluates `config`. +# Those are the keys disko produces today. A layout that produces +# settings under any other key is refused rather than left out. +{ ... }: +{ lib, ... }: +let + layout = + lib.caisson-core.evalManifest.nearest.caisson-disko or (throw '' + caisson's `caisson-disko-layout` NixOS module gives a NixOS configuration + the settings of the disk layout it is installed onto, and this NixOS + configuration is not declared inside a caisson-disko configuration. + ''); + + nixosConfig = layout.outputs.nixosConfig; + + copiedKeys = [ + "boot" + "fileSystems" + "swapDevices" + ]; + + copied = + key: default: + let + others = builtins.attrNames (builtins.removeAttrs nixosConfig copiedKeys); + in + lib.throwIfNot (others == [ ]) '' + The disk layout `${layout.name or "default"}` produces NixOS settings under + ${builtins.concatStringsSep ", " others}, which caisson's `caisson-disko-layout` + NixOS module does not copy into the NixOS configuration. + '' (nixosConfig.${key} or default); +in +{ + _file = "caisson:caisson-disko-layout"; + + fileSystems = copied "fileSystems" { }; + boot = copied "boot" { }; + swapDevices = copied "swapDevices" [ ]; +} diff --git a/modules/nixos/disko-layout/default.nix b/modules/nixos/disko-layout/default.nix new file mode 100644 index 00000000..581f7730 --- /dev/null +++ b/modules/nixos/disko-layout/default.nix @@ -0,0 +1,39 @@ +# SPDX-License-Identifier: MIT +# +# A NixOS module for a NixOS configuration installed onto a disk layout +# of the upstream-compatible disko integration: a NixOS configuration +# declared inside a disko configuration. It imports disko's own NixOS +# module, from the disko source the layout was evaluated with, and +# defines `disko.devices` from the layout: the device tree as data, +# read from the layout through the manifest, at default priority. +# +# The NixOS configuration then holds what disko's tools read. +# disko's command finds `config.disko.devices` and builds the scripts +# under `system.build`; `disko-install` replaces the disks with +# `extendModules` and `mkVMOverride`, which wins over the default +# priority of this definition. The options disko's module declares +# beside `disko.devices`, such as `disko.imageBuilder` and +# `disko.tests`, are options of the NixOS configuration, set in its +# own modules. +# +# The default module of the disko class adds this module to every +# NixOS configuration declared inside a disko configuration. A NixOS +# configuration that passes `moduleImports` selects its modules itself, +# and then it lists this module itself. +{ ... }: +{ lib, ... }: +let + layout = + lib.caisson-core.evalManifest.nearest.disko or (throw '' + caisson's `disko-layout` NixOS module gives a NixOS configuration the disk + layout it is installed onto, and this NixOS configuration is not declared + inside a disko configuration. + ''); +in +{ + _file = "caisson:disko-layout"; + + imports = [ "${layout.outputs.ecosystemSrc}/module.nix" ]; + + disko.devices = layout.outputs.serializableLayout; +} diff --git a/tests/argument-errors/check.sh b/tests/argument-errors/check.sh index 83896992..027482c1 100755 --- a/tests/argument-errors/check.sh +++ b/tests/argument-errors/check.sh @@ -21,8 +21,8 @@ shift flags=("$@") # Every entry point takes `configModule`. The structural, caisson-colmena, -# flake-parts, nixos, nixos-minimal, home-manager and -# home-manager-minimal entry points require nothing: each finds the +# caisson-disko, disko, flake-parts, nixos, nixos-minimal, home-manager +# and home-manager-minimal entry points require nothing: each finds the # configuration registered under the name of the configuration. The others require # `configModule`, and take `pkgSets`, which the nixos and home-manager # entry points do not: a NixOS configuration and a home take their @@ -35,6 +35,8 @@ namespaces=( flake-parts structural caisson-colmena + caisson-disko + disko terranix system-manager ) @@ -93,7 +95,7 @@ given_for() { } requires_nothing() { case "$1" in - structural | caisson-colmena | flake-parts | nixos | nixos-minimal | home-manager | home-manager-minimal) return 0 ;; + structural | caisson-colmena | caisson-disko | disko | flake-parts | nixos | nixos-minimal | home-manager | home-manager-minimal) return 0 ;; *) return 1 ;; esac } diff --git a/tests/dependencies/flake.lock b/tests/dependencies/flake.lock index cbf5c792..293e02b3 100644 --- a/tests/dependencies/flake.lock +++ b/tests/dependencies/flake.lock @@ -2,11 +2,11 @@ "nodes": { "caisson-core": { "locked": { - "lastModified": 1792046586, - "narHash": "sha256-3B8nnMAKVnZzm0oAmBCS24OAcSOqFOgA/m7sWnEZgJY=", + "lastModified": 1792136033, + "narHash": "sha256-vdOZ5tKU+s5fWLNDxJG1YzcfTV1oDqrrRMLkO52CC3w=", "owner": "nix-caisson", "repo": "caisson-core", - "rev": "8b44b618b3cc9cc4bb18c24c182fedd3795d6eee", + "rev": "02f9189a4f2ce2652fbdefa550ec03fabd101fbf", "type": "github" }, "original": { @@ -39,6 +39,26 @@ "type": "github" } }, + "disko": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1789770686, + "narHash": "sha256-uZkBR7yHdIKUFB5SZdfgh1qkGfI3XmYmI/lTiquxbck=", + "owner": "nix-community", + "repo": "disko", + "rev": "725ea35e410ad83be4931d1bff7e090eacaf3563", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "disko", + "type": "github" + } + }, "flake-compat": { "flake": false, "locked": { @@ -337,6 +357,7 @@ "inputs": { "caisson-core": "caisson-core", "colmena": "colmena", + "disko": "disko", "flake-parts": "flake-parts", "home-manager": "home-manager", "nix-unit": "nix-unit", diff --git a/tests/dependencies/flake.nix b/tests/dependencies/flake.nix index 79ca8411..1e402170 100644 --- a/tests/dependencies/flake.nix +++ b/tests/dependencies/flake.nix @@ -19,5 +19,7 @@ inputs.terranix.inputs.nixpkgs.follows = "nixpkgs"; inputs.system-manager.url = "github:numtide/system-manager"; inputs.system-manager.inputs.nixpkgs.follows = "nixpkgs"; + inputs.disko.url = "github:nix-community/disko"; + inputs.disko.inputs.nixpkgs.follows = "nixpkgs"; outputs = { ... }: { }; } diff --git a/tests/pinned-world/default.nix b/tests/pinned-world/default.nix index a7ac3282..bd5e13c5 100644 --- a/tests/pinned-world/default.nix +++ b/tests/pinned-world/default.nix @@ -2,8 +2,8 @@ # # The pinned-world suite: caisson, the tree this file lives in, # composed with concrete pinned versions of the upstream world -# (nixpkgs, home-manager, colmena, terranix, system-manager, read -# from tests/dependencies) and exercised end to end. Every upstream +# (nixpkgs, home-manager, colmena, disko, terranix, system-manager, +# read from tests/dependencies) and exercised end to end. Every upstream # expectation caisson relies on is held by a probe here, so a pin # that moves and breaks an expectation fails at evaluation, in this # suite, rather than in a consumer. @@ -63,6 +63,8 @@ let registered."caisson/home-manager" registered."caisson/caisson-colmena" registered."caisson/colmena" + registered."caisson/caisson-disko" + registered."caisson/disko" registered."caisson/terranix" registered."caisson/system-manager" registered."caisson/structural" @@ -77,10 +79,12 @@ let # `framework` overlay publishes from `caisson-core`. expectedCaissonNames = [ "caisson-colmena" + "caisson-disko" "callConsumerFlake" "callFlake" "colmena" "configs" + "disko" "ecosystemSrc" "elide" "eval-weight" @@ -176,9 +180,10 @@ let system.stateVersion = "25.05"; }; - # The colmena probes compose with declared ecosystems: a node's - # `ecosystemSrc` is colmena's, and nixpkgs resolves from the - # declaration, as it does in a consumer flake. + # The colmena and disko probes compose with declared ecosystems: a + # node resolves the colmena source, a layout resolves the disko + # source, and nixpkgs resolves from the declaration, as it does in a + # consumer flake. hiveLib = core.mkLib ( declaresPkgs // { @@ -187,11 +192,80 @@ let caisson = inputs.caisson; }; defaultEcosystemSrc = { - inherit (inputs) nixpkgs colmena flake-parts; + inherit (inputs) + nixpkgs + colmena + flake-parts + disko + ; }; } ); + # Disk layouts as disko writes them, from disko's examples: a disk + # with an ESP and LUKS around LVM, and one with mdadm, ZFS, LUKS and + # LVM over three disks. + luksLvmLayout = import "${inputs.disko}/example/luks-lvm.nix"; + complexLayout = import "${inputs.disko}/example/complex.nix"; + + # What a machine installed onto a layout needs beside the layout, + # which gives it its file systems. + diskoMachineBase = { + boot.loader.grub.enable = false; + system.stateVersion = "25.05"; + }; + + # The machine NixOS builds from disko's own NixOS module, the layout + # and `diskoMachineBase`, with no caisson involved. + upstreamDiskoMachine = + layout: + import "${inputs.nixpkgs}/nixos/lib/eval-config.nix" { + system = "x86_64-linux"; + modules = [ + "${inputs.disko}/module.nix" + layout + diskoMachineBase + ]; + }; + + # A top holding a disko configuration of `integration` named + # `nas-1`, over `layout`. With `machine`, a NixOS module, the layout + # declares a NixOS configuration named `default` from it, installed + # onto the layout. + layoutTop = + { + integration, + layout, + machine ? null, + }: + hiveLib.caisson.structural.mkTopConfiguration { + moduleImports = _modules: [ ]; + configModule = + { lib, ... }: + { + caisson.${integration}.configurations.nas-1 = lib.caisson.${integration}.mkConfiguration { + configModule = + { lib, ... }: + { + imports = [ layout ]; + } + // ( + if machine == null then + { } + else + { + caisson.nixos.configurations.default = lib.caisson.nixos.mkConfiguration { + configModule = machine; + }; + } + ); + }; + }; + }; + + # The manifest of the disko configuration `nas-1` of a `layoutTop`. + layoutManifest = integration: top: top.caisson.manifest.children.${integration}.nas-1; + # A top holding a colmena configuration named `default`, with a NixOS # configuration declared inside it for each machine given, by name. # `machineArgs` are further arguments of each machine's constructor. @@ -387,6 +461,8 @@ let "caisson/home-manager" "caisson/caisson-colmena" "caisson/colmena" + "caisson/caisson-disko" + "caisson/disko" "caisson/terranix" "caisson/system-manager" "caisson/structural" @@ -480,6 +556,8 @@ let "home-manager" "caisson-colmena" "colmena" + "caisson-disko" + "disko" "terranix" "system-manager" ]; @@ -792,14 +870,17 @@ let # An integration declares `top` when a configuration of it can be # a top, and only then has `mkTopConfiguration`. A colmena - # configuration is always declared beneath another configuration, - # so the caisson-colmena integration declares no `top`. + # configuration and a disko configuration are always declared + # beneath another configuration, so their integrations declare no + # `top`. onlyAnIntegrationWithATopHasMkTopConfiguration = hiveLib.caisson.nixos ? mkTopConfiguration && hiveLib.caisson.home-manager ? mkTopConfiguration && hiveLib.caisson.structural ? mkTopConfiguration && hiveLib.caisson.flake-parts ? mkTopConfiguration - && !(hiveLib.caisson.caisson-colmena ? mkTopConfiguration); + && !(hiveLib.caisson.caisson-colmena ? mkTopConfiguration) + && !(hiveLib.caisson.caisson-disko ? mkTopConfiguration) + && !(hiveLib.caisson.disko ? mkTopConfiguration); # A colmena configuration declared under the name `default` beneath # a top fills the top's `colmenaHive`. Its nodes are the NixOS @@ -1105,6 +1186,160 @@ let }).colmenaHive ).success; + # A machine installed onto a caisson-disko layout gets the file + # systems, boot settings and swap the layout produces, and nothing + # of disko: it has no `disko` option and no disko scripts. It is + # the system NixOS builds from disko's own module and the same + # layout. The machine declared as `default` inside the layout + # `nas-1` is published as `nixosConfigurations.nas-1`, beside + # `diskoConfigurations.nas-1`. + caissonDiskoMachinesGetTheLayoutsSettings = + let + top = layoutTop { + integration = "caisson-disko"; + layout = luksLvmLayout; + machine = diskoMachineBase; + }; + machine = top.nixosConfigurations.nas-1; + upstream = upstreamDiskoMachine luksLvmLayout; + in + builtins.attrNames top.nixosConfigurations == [ "nas-1" ] + && builtins.attrNames top.diskoConfigurations == [ "nas-1" ] + && !(machine.options ? disko) + && !(machine.config.system.build ? formatScript) + && machine.config.fileSystems."/".device == upstream.config.fileSystems."/".device + && machine.config.system.build.toplevel.drvPath == upstream.config.system.build.toplevel.drvPath; + + # A caisson-disko layout is published in the form disko's command + # reads. disko's own evaluator, given the published layout, makes + # the format script it makes from the layout as written, and the + # layout's own `scripts` output is that script too. + caissonDiskoPublishesWhatDiskosCommandReads = + let + top = layoutTop { + integration = "caisson-disko"; + layout = complexLayout; + }; + upstream = import inputs.disko { inherit (pkgs) lib; }; + formatOf = layout: (upstream._cliFormat layout pkgs).drvPath; + outputs = (layoutManifest "caisson-disko" top).outputs; + in + formatOf top.diskoConfigurations.nas-1 == formatOf complexLayout + && (outputs.scripts pkgs).format.drvPath == formatOf complexLayout + && outputs.nixosConfig ? fileSystems + && builtins.isString outputs.layoutHash; + + # A layout with no machine installed onto it is published as a + # layout and nothing else. + caissonDiskoLayoutsNeedNoMachine = + let + top = layoutTop { + integration = "caisson-disko"; + layout = luksLvmLayout; + }; + in + builtins.attrNames top.diskoConfigurations == [ "nas-1" ] && !(top ? nixosConfigurations); + + # A module of the caisson-disko class refuses a layout with an + # assertion, and then every output of the layout fails. + caissonDiskoAssertionsRefuseALayout = + let + top = layoutTop { + integration = "caisson-disko"; + layout = { + imports = [ luksLvmLayout ]; + assertions = [ + { + assertion = false; + message = "the probe refuses every layout"; + } + ]; + }; + machine = diskoMachineBase; + }; + fails = expr: !(builtins.tryEval (builtins.deepSeq expr true)).success; + in + fails top.diskoConfigurations.nas-1 && fails top.nixosConfigurations.nas-1.config.fileSystems; + + # A machine installed onto a layout of the upstream-compatible disko + # integration carries disko's own NixOS module with the layout, so + # it is what disko's tools read: the scripts under `system.build` + # and `config.disko.devices`. It is the system NixOS builds from + # disko's module and the same layout, with the same format script. + # The layout is defined at default priority, so `disko-install`, + # which replaces the disks with `mkVMOverride`, still replaces them. + diskoMachinesGetDiskosModule = + let + top = layoutTop { + integration = "disko"; + layout = luksLvmLayout; + machine = diskoMachineBase; + }; + machine = top.nixosConfigurations.nas-1; + upstream = upstreamDiskoMachine luksLvmLayout; + installed = machine.extendModules { + modules = [ + ( + { lib, ... }: + { + disko.devices.disk.main = lib.mkVMOverride { + device = "/dev/vdz"; + type = "disk"; + }; + } + ) + ]; + }; + in + builtins.attrNames top.nixosConfigurations == [ "nas-1" ] + && builtins.attrNames top.diskoConfigurations == [ "nas-1" ] + && machine.config.disko.devices.disk ? main + && machine.config.system.build.toplevel.drvPath == upstream.config.system.build.toplevel.drvPath + && + machine.config.system.build.formatScript.drvPath + == upstream.config.system.build.formatScript.drvPath + && installed.config.disko.devices.disk.main.device == "/dev/vdz"; + + # A layout declared inside a colmena configuration passes up the + # machine installed onto it under the name of the layout, so the + # machine is the node `nas-1` of the hive, and the top publishes it + # as `nixosConfigurations.nas-1` beside `diskoConfigurations.nas-1`. + diskoLayoutsInsideColmenaAreNodes = + let + top = hiveLib.caisson.structural.mkTopConfiguration { + moduleImports = _modules: [ ]; + configModule = + { lib, ... }: + { + caisson.caisson-colmena.configurations.default = lib.caisson.caisson-colmena.mkConfiguration { + configModule = + { lib, ... }: + { + caisson.caisson-disko.configurations.nas-1 = lib.caisson.caisson-disko.mkConfiguration { + configModule = + { lib, ... }: + { + imports = [ luksLvmLayout ]; + caisson.nixos.configurations.default = lib.caisson.nixos.mkConfiguration { + configModule = diskoMachineBase // { + networking.hostName = "nas-1"; + }; + }; + }; + }; + }; + }; + }; + }; + hive = top.colmenaHive; + in + builtins.attrNames hive.nodes == [ "nas-1" ] + && hive.deploymentConfig.nas-1.targetHost == "nas-1" + && builtins.attrNames top.nixosConfigurations == [ "nas-1" ] + && builtins.attrNames top.diskoConfigurations == [ "nas-1" ] + && + hive.toplevel.nas-1.drvPath == top.nixosConfigurations.nas-1.config.system.build.toplevel.drvPath; + terranixConfigurationEvaluatesEndToEnd = let terraform = composed.lib.caisson.terranix.mkConfiguration { diff --git a/tests/unit/lib-overlays.nix b/tests/unit/lib-overlays.nix index 541edbab..8c4eb263 100644 --- a/tests/unit/lib-overlays.nix +++ b/tests/unit/lib-overlays.nix @@ -118,6 +118,8 @@ let "flake-parts" "structural" "caisson-colmena" + "caisson-disko" + "disko" "terranix" "system-manager" ]; @@ -2882,6 +2884,41 @@ in expected = [ "renamed" ]; }; + # `renameDefault` passes up the configuration declared as + # `default` under the name it is given, and every other + # configuration under its own name. A configuration declared under + # that name beside `default` is refused. + "test: renameDefault passes up default under the name given" = { + expr = + let + inherit (myLib.caisson.integrations) renameDefault; + refused = + configurations: + !(builtins.tryEval (builtins.deepSeq (renameDefault "nas-1" configurations) true)).success; + in + { + renamed = renameDefault "nas-1" { + default = "installed"; + installer = "beside"; + }; + withoutDefault = renameDefault "nas-1" { installer = "beside"; }; + clash = refused { + default = "installed"; + nas-1 = "also"; + }; + }; + expected = { + renamed = { + nas-1 = "installed"; + installer = "beside"; + }; + withoutDefault = { + installer = "beside"; + }; + clash = true; + }; + }; + # `publish` names the entries of an output attribute set from # their paths. Entries with the same path cannot be told apart, # and `publish` refuses them. The message names the output @@ -5255,6 +5292,8 @@ in byName = builtins.elem name [ "structural" "caisson-colmena" + "caisson-disko" + "disko" "flake-parts" "nixos" "nixos-minimal" From 2c7af1701854d703fb32531c709cf3c5fb5b08b1 Mon Sep 17 00:00:00 2001 From: Chris Hodapp Date: Fri, 16 Oct 2026 01:08:45 -0700 Subject: [PATCH 2/2] maint: lock caisson-core to main nix-caisson/caisson-core#50 is merged; the locks move from its branch commit to its merge on main. Co-Authored-By: Claude Opus 5.5 --- flake.lock | 6 +++--- tests/dependencies/flake.lock | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index f69bc317..fe8c4f6f 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "caisson-core": { "locked": { - "lastModified": 1792136033, - "narHash": "sha256-vdOZ5tKU+s5fWLNDxJG1YzcfTV1oDqrrRMLkO52CC3w=", + "lastModified": 1792137836, + "narHash": "sha256-qKXYUYEdAhlXOHyKf4TL08r3U+sO5cVw4Vzfnbky4qk=", "owner": "nix-caisson", "repo": "caisson-core", - "rev": "02f9189a4f2ce2652fbdefa550ec03fabd101fbf", + "rev": "dd14868354f79771b05ad2c187e4af13dc072a60", "type": "github" }, "original": { diff --git a/tests/dependencies/flake.lock b/tests/dependencies/flake.lock index 293e02b3..b4734d08 100644 --- a/tests/dependencies/flake.lock +++ b/tests/dependencies/flake.lock @@ -2,11 +2,11 @@ "nodes": { "caisson-core": { "locked": { - "lastModified": 1792136033, - "narHash": "sha256-vdOZ5tKU+s5fWLNDxJG1YzcfTV1oDqrrRMLkO52CC3w=", + "lastModified": 1792137836, + "narHash": "sha256-qKXYUYEdAhlXOHyKf4TL08r3U+sO5cVw4Vzfnbky4qk=", "owner": "nix-caisson", "repo": "caisson-core", - "rev": "02f9189a4f2ce2652fbdefa550ec03fabd101fbf", + "rev": "dd14868354f79771b05ad2c187e4af13dc072a60", "type": "github" }, "original": {