support snow.flake

This commit is contained in:
do butterflies cry? 2026-02-20 02:08:04 +10:00
parent e4ab4f4b5a
commit 45c53f025a
10 changed files with 352 additions and 41 deletions

View file

@ -20,12 +20,10 @@ mix.newMixture args (mixture: {
includes.public = [
./nexus
];
submods.public = [
./snow
];
nixosModules = rec {
default = cerulean;
cerulean = ./nixos;
};
version = "0.2.3";
overlays = [
@ -33,4 +31,9 @@ mix.newMixture args (mixture: {
# hence we can rely on a nixpkg binary cache.
inputs.deploy-rs.overlays.default
];
nixosModules = rec {
default = cerulean;
cerulean = ./nixos;
};
})

View file

@ -28,7 +28,11 @@
# options declarations
(import ./nixpkgs.nix (args // {contextName = "hosts";}))
]
++ lib.optional (_cerulean.homeManager != null) [./home-manager.nix];
++ (
if _cerulean.homeManager != null
then [./home-manager.nix]
else []
);
environment.systemPackages = with _cerulean.inputs; [
deploy-rs.packages.${system}.default

View file

@ -25,6 +25,10 @@
pathExists
;
in {
imports = [
_cerulean.homeManager.nixosModules.default
];
home-manager = {
users =
config.users.users

View file

@ -75,19 +75,19 @@ in {
# NOTE: _module.args is a special option that allows us to
# NOTE: set extend specialArgs from inside the modules.
# WARNING: pkgs is a reserved specialArg
_module.args = removeAttrs repos ["pkgs"];
_module.args = removeAttrs repos ["pkgs" "default"];
nixpkgs =
if contextName == "hosts"
then {
flake.source = lib.mkOverride 200 base; # DEBUG: temp while getting base to work
overlays = lib.mkOverride 200 (defaultPkgs.overlays or {});
config = lib.mkOverride 200 (defaultPkgs.config or {});
flake.source = lib.mkForce base; # DEBUG: temp while getting base to work
overlays = lib.mkForce (defaultPkgs.overlays or {});
config = lib.mkForce (defaultPkgs.config or {});
}
else if contextName == "homes"
then {
config = lib.mkOverride 200 (defaultPkgs.config or {});
overlays = lib.mkOverride 200 (defaultPkgs.overlays or []);
config = lib.mkForce (defaultPkgs.config or {});
overlays = lib.mkForce (defaultPkgs.overlays or []);
}
else {};
};

View file

@ -11,8 +11,174 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
{...}: {
imports = [
./nodes
];
}
{
this,
self,
inputs,
systems,
nt,
mix,
...
} @ args: let
inherit
(builtins)
all
attrNames
elem
mapAttrs
warn
;
inherit (inputs.nixpkgs) lib;
inherit (nt) findImport;
in
mix.newMixture args (mixture: let
inherit (mixture) mapNodes;
in {
includes.private = [
./lib/nodes.nix
];
inherit findImport;
# snow.flake
flake = flakeInputs: root: let
module = lib.evalModules {
class = "snowflake";
# TODO: abort if inputs contains reserved names
specialArgs =
flakeInputs
// {
inherit root;
inherit systems;
inherit (this) snow; # please don't be infinite recursion...
inputs = flakeInputs;
};
modules = [
./module.nix
];
};
nodes = module.config.nodes;
in rec {
nixosConfigurations = mapNodes nodes (
{
base,
lib,
name,
node,
groupModules,
...
}: let
homeManager =
if node.homeManager != null
then node.homeManager
else if nodes.homeManager != null
then nodes.homeManager
else
warn ''
[snowflake] Neither `nodes.homeManager` nor `nodes.nodes.${name}.homeManager` were specified!
[snowflake] home-manager will NOT be used! User configuration will be ignored!
''
null;
userArgs = nodes.args // node.args;
ceruleanArgs = {
inherit systems root base;
inherit (node) system;
inherit (this) snow;
_cerulean = {
inherit inputs userArgs ceruleanArgs homeManager;
specialArgs = userArgs // ceruleanArgs;
};
};
specialArgs = assert (userArgs
|> attrNames
|> all (argName:
! ceruleanArgs ? argName
|| abort ''
`specialArgs` are like super important to Cerulean my love... </3
But `args.${argName}` is a reserved argument name :(
''));
ceruleanArgs._cerulean.specialArgs;
in
lib.nixosSystem {
inherit (node) system;
inherit specialArgs;
modules =
[
self.nixosModules.default
(findImport (root + "/hosts/${name}"))
# inputs.microvm.nixosModules.microvm
]
++ (groupModules root)
++ node.modules
++ nodes.modules;
}
);
deploy.nodes = mapNodes nodes ({
name,
node,
...
}: let
inherit
(node.deploy)
ssh
user
sudo
interactiveSudo
remoteBuild
rollback
autoRollback
magicRollback
activationTimeout
confirmTimeout
;
nixosFor = system: inputs.deploy-rs.lib.${system}.activate.nixos;
in {
hostname = ssh.host;
profilesOrder = ["default"]; # profiles priority
profiles.default = {
path = nixosFor node.system nixosConfigurations.${name};
user = user;
sudo = sudo;
interactiveSudo = interactiveSudo;
fastConnection = false;
autoRollback = autoRollback -> rollback;
magicRollback = magicRollback -> rollback;
activationTimeout = activationTimeout;
confirmTimeout = confirmTimeout;
remoteBuild = remoteBuild;
sshUser = ssh.user;
sshOpts =
ssh.opts
++ (
if elem "-p" ssh.opts
then []
else ["-p" (toString ssh.port)]
)
++ (
if elem "-A" ssh.opts
then []
else ["-A"]
);
};
});
checks =
inputs.deploy-rs.lib
|> mapAttrs (system: deployLib:
deployLib.deployChecks deploy);
};
})

View file

@ -0,0 +1,96 @@
# Copyright 2025-2026 _cry64 (Emile Clark-Boman)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
{nt, ...}: let
inherit
(builtins)
concatLists
elem
filter
isAttrs
mapAttrs
pathExists
typeOf
;
rootGroupName = "all";
parseGroupsDecl = groups: let
validGroup = g:
isAttrs g
|| throw ''
Cerulean Nexus groups must be provided as attribute sets, got "${typeOf g}" instead!
Ensure all the group definitions are attribute sets under your call to `cerulean.mkNexus`.
NOTE: Groups can be accessed via `self.groups.PATH.TO.YOUR.GROUP`
'';
delegate = parent: gName: g: let
result =
(g
// {
_name = gName;
_parent = parent;
})
|> mapAttrs (name: value:
if elem name ["_name" "_parent"]
# ignore metadata fields
then value
else assert validGroup value; (delegate result name value));
in
result;
in
assert validGroup groups;
delegate null rootGroupName groups;
getGroupModules = root: groups:
# ensure root group is always added
groups
# add all inherited groups via _parent
|> map (let
delegate = g:
if g._parent == null
then [g]
else [g] ++ delegate (g._parent);
in
delegate)
# flatten recursion result
|> concatLists
# find import location
|> map (group: nt.findImport (root + "/groups/${group._name}"))
# filter by uniqueness
|> nt.prim.unique
# ignore missing groups
|> filter pathExists;
in {
mapNodes = nodes: f:
nodes.nodes
|> mapAttrs (name: node: let
# use per-node base or default to nodes' base
base =
if node.base != null
then node.base
else if nodes.base != null
then nodes.base
else
abort ''
Cerulean cannot construct nodes node "${name}" without a base package source.
Ensure `nodes.nodes.*.base` or `nodes.base` is a flake reference to the github:NixOS/nixpkgs repository.
'';
in
f rec {
inherit name node base;
inherit (base) lib;
groups = node.groups (parseGroupsDecl nodes.groups);
groupModules = root: getGroupModules root groups;
});
}

23
cerulean/snow/module.nix Normal file
View file

@ -0,0 +1,23 @@
# Copyright 2025-2026 _cry64 (Emile Clark-Boman)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
{
root,
snow,
...
}: {
imports = [
./nodes
(snow.findImport (root + "/snow"))
];
}

View file

@ -11,7 +11,11 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
{lib, ...}: {
{
lib,
specialArgs,
...
}: {
options.nodes = let
inherit
(lib)
@ -24,26 +28,35 @@
Cerulean node declarations.
'';
type = types.submoduleWith {
imports = [./shared.nix];
inherit specialArgs;
options = {
groups = mkOption {
type = types.attrs;
default = {};
example = lib.literalExpression "{ servers = { staging = {}; production = {}; }; }";
description = ''
Hierarchical groups that nodes can be a member of.
'';
};
modules = [
{
imports = [./shared.nix];
nodes = mkOption {
type = types.attrsOf (types.submoduleWith (import ./submodule.nix));
# example = { ... }; # TODO
description = ''
Node (host systems) declarations.
'';
};
};
options = {
groups = mkOption {
type = types.attrs;
default = {};
example = lib.literalExpression "{ servers = { staging = {}; production = {}; }; }";
description = ''
Hierarchical groups that nodes can be a member of.
'';
};
nodes = mkOption {
type = types.attrsOf (types.submoduleWith {
inherit specialArgs;
modules = [(import ./submodule.nix)];
});
# example = { ... }; # TODO
description = ''
Node (host systems) declarations.
'';
};
};
}
];
};
};
}

View file

@ -12,8 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
{
inputs,
lib,
systems,
...
}: {
imports = [./shared.nix];
@ -34,8 +34,8 @@
'';
};
system = types.nullOr mkOption {
type = types.enum inputs.systems;
system = mkOption {
type = types.nullOr (types.enum systems);
default = null;
example = "x86_64-linux";
description = ''
@ -44,8 +44,9 @@
};
groups = mkOption {
type = types.functionTo types.list;
default = [];
# TODO: write a custom group type that validates better than types.attrs lol
type = types.functionTo (types.listOf types.attrs);
default = groups: [];
example = lib.literalExpression "( groups: [ groups.servers groups.secure-boot ] )";
description = ''
A function from the `groups` hierarchy to a list of groups this node inherits from.

View file

@ -43,5 +43,6 @@
{
inherit inputs self nt;
inherit (nt) mix;
systems = import inputs.systems;
};
}