Compare commits

...
Sign in to create a new pull request.

22 commits

Author SHA1 Message Date
4726d81d90
Merge branch 'v0.2.5-alpha' 2026-03-08 17:20:45 +10:00
6c7f335fbd
features for v0.2.6-alpha 2026-03-08 17:14:06 +10:00
902f9d7508
provide systems and snow as flake inputs 2026-03-08 17:13:37 +10:00
02ded5d4f0
TEMP fix for cerubld not having permissions 2026-03-08 02:21:51 +10:00
630389a598
migrate to cerubld user 2026-03-07 23:48:38 +10:00
6b579dff1e
set hostname by default 2026-03-07 23:46:51 +10:00
b486ee8cb7
propagate node config and hostname to nixos modules 2026-03-07 23:46:02 +10:00
aec16966ae
add missing license 2026-03-07 20:21:26 +10:00
39ec2e62d0
fix nixpkgs disabled when home-manager.useGlobalPkgs 2026-03-07 20:10:36 +10:00
1c68485dcf
cerulean now manages trivial home-manager options 2026-03-07 19:39:57 +10:00
34a8c23537
provide per-user args (ie username) 2026-03-07 19:29:26 +10:00
169bf2bf48
ACTUALLY use users.users.<name>.manageHome 2026-03-07 18:52:06 +10:00
23449396f7
clean TODO 2026-03-07 18:41:43 +10:00
c49fdc9769
rename home-manager.nix -> home.nix
add options.users.users.<name>.manageHome
2026-03-07 18:41:43 +10:00
f985e7ee70
rename channels.default -> channels.base 2026-03-07 18:41:43 +10:00
ba7763801f
use better path notation 2026-03-07 16:48:35 +10:00
ef5bc33856
enable hm 2026-03-07 13:05:54 +10:00
d9432d87a4
v0.2.4-alpha 2026-03-07 11:24:20 +10:00
77ddfcde7d
Merge branch 'bleeding' 2026-03-07 11:24:20 +10:00
930eafa818
ignore target 2026-03-07 11:09:00 +10:00
087f679e67 add modifiable homeManager 2026-02-18 23:56:05 +10:00
d5211287bd TEMP: use base 2026-02-18 20:13:43 +10:00
12 changed files with 296 additions and 121 deletions

24
TODO.md
View file

@ -1,12 +1,14 @@
## Next
- [ ] use the Nix module system instead of projectOnto for `cerulean.mkNexus`
- [ ] formalize how the snow flake system compiles outputs, this would remove the need for `mapNodes`
- [ ] groups should allow you to set node configuration defaults
- [ ] add `options.experimental` for snowflake
- [ ] add `legacyImports` support
- [ ] support hs system per dir, ie hosts/<name>/overlays or hosts/<name>/nixpkgs.nix
## Queued
- [X] base should automatically be set as the default (dont do anything with the default)
- [X] try to remove common foot guns, ie abort if the user provides the home-manager or microvm nixosModules
since cerulean ALREADY provides these
- [ ] per node home configuration is a lil jank rn
- [ ] deploy port should default to the first port given to `services.openssh`
@ -23,29 +25,19 @@
- [ ] go through all flake inputs (recursively) and ENSURE we remove all duplicates by using follows!!
- [X] rename nixos-modules/ to nixos/
- [X] ensure all machines are in groups.all by default
- [X] fix nixpkgs.nix not working (default not respected)
- [X] remove dependence on nixpkgs
- [ ] allow multiple privesc methods, the standard is pam_ssh_agent_auth
## Low Priority
- [X] rename extraModules to modules?
- [X] rename specialArgs to args?
- [ ] make an extension to the nix module system (different to mix)
that allows transformations (ie a stop post config, ie outputs, which
it then returns instead of config)
- [ ] support `legacyImports` (?)
- [ ] patch microvm so that acpi=off https://github.com/microvm-nix/microvm.nix/commit/b59a26962bb324cc0a134756a323f3e164409b72
cause otherwise 2GB causes a failure
- [ ] rewrite the ceru cli in rust
- [ ] make `ceru` do local and remote deployments
- [ ] write the cerulean cli
- [ ] support `legacyImports`
```nix
# REF: foxora

View file

@ -21,7 +21,7 @@ mix.newMixture args (mixture: {
./snow
];
version = "0.2.3";
version = "0.2.5-alpha";
# WARNING: legacy
mkFlake = mixture.snow.flake;

34
cerulean/home/default.nix Normal file
View file

@ -0,0 +1,34 @@
# 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.
{
username,
lib,
...
}: {
# NOTE: you can access the system configuration via the `osConfig` arg
# WARNING: required for home-manager to work
programs.home-manager.enable = true; # user must apply lib.mkForce
# Nicely reload systemd units when changing configs
systemd.user.startServices = lib.mkDefault "sd-switch";
home = {
username = lib.mkDefault username;
homeDirectory = lib.mkDefault "/home/${username}";
sessionVariables = {
NIX_SHELL_PRESERVE_PROMPT = lib.mkDefault 1;
};
};
}

View file

@ -13,29 +13,34 @@
# limitations under the License.
{
root,
pkgs,
system,
hostname,
node,
pkgs,
lib,
_cerulean,
...
} @ args: {
imports = with _cerulean.inputs;
imports =
[
_cerulean.inputs.sops-nix.nixosModules.sops
# _cerulean.inputs.microvm.nixosModules.microvm
# add support for `options.legacyImports`
# ./legacy-imports.nix
# user configuration
(import (root + "/nixpkgs.nix"))
# options declarations
# nixos options declarations
(import ./nixpkgs.nix (args // {contextName = "hosts";}))
sops-nix.nixosModules.sops
# microvm.nixosModules.microvm
# user's nixpkg configuration
(import /${root}/nixpkgs.nix)
]
++ (
if _cerulean.homeManager != null
then [./home-manager.nix]
else []
);
# homemanager options declarations
++ (lib.optional (_cerulean.homeManager != null) ./home.nix)
# remote deployment configuration
++ (lib.optional (node.deploy.ssh.host != null) ./remote-deploy);
networking.hostName = lib.mkDefault hostname;
environment.systemPackages =
(with pkgs; [

View file

@ -1,49 +0,0 @@
# 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,
config,
lib,
_cerulean,
...
} @ args: let
inherit
(builtins)
attrNames
filter
pathExists
;
in {
imports = [
_cerulean.homeManager.nixosModules.default
];
home-manager = {
users =
config.users.users
|> attrNames
|> filter (x: pathExists (root + "/homes/${x}"))
|> (x:
lib.genAttrs x (y:
import (root + "/homes/${y}")));
extraSpecialArgs = _cerulean.specialArgs;
sharedModules = [
# user configuration
(import (root + "/nixpkgs.nix"))
# options declarations
(import ./nixpkgs.nix (args // {contextName = "homes";}))
];
};
}

82
cerulean/nixos/home.nix Normal file
View file

@ -0,0 +1,82 @@
# 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.
{
_cerulean,
config,
root,
lib,
...
} @ args: let
inherit
(builtins)
pathExists
;
inherit
(lib)
filterAttrs
mapAttrs
;
in {
imports = [
_cerulean.homeManager.nixosModules.default
];
options = {
users.users = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule {
options.manageHome = lib.mkOption {
type = lib.types.bool;
default = true;
example = false;
description = ''
Whether Cerulean should automatically enable home-manager for this user,
and manage their home configuration declaratively.
Enabled by default, but can be disabled if necessary.
'';
};
});
};
};
config = {
home-manager = {
useUserPackages = lib.mkDefault false;
useGlobalPkgs = lib.mkDefault true;
overwriteBackup = lib.mkDefault false;
backupFileExtension = lib.mkDefault "bak";
users =
config.users.users
|> filterAttrs (name: value: value.manageHome && pathExists /${root}/homes/${name})
|> mapAttrs (name: _: {...}: {
imports = [/${root}/homes/${name}];
# per-user arguments
_module.args.username = name;
});
extraSpecialArgs = _cerulean.specialArgs;
sharedModules = [
../home
(import /${root}/nixpkgs.nix)
# options declarations
(import ./nixpkgs.nix (args // {contextName = "homes";}))
];
};
};
}

View file

@ -31,7 +31,7 @@ in {
default = {};
description = "Declare package repositories";
example = {
"pkgs" = {
"npkgs" = {
source = "inputs.nixpkgs";
system = "x86-64-linux";
config = {
@ -53,7 +53,7 @@ in {
config = let
repos =
cfg
|> (xs: removeAttrs xs ["default"])
|> (xs: removeAttrs xs ["base"])
|> mapAttrs (
name: args:
lib.mkForce (
@ -65,30 +65,31 @@ in {
)
);
# XXX: TODO: would it work to use `base` instead of having default?
defaultPkgs =
cfg.default or (throw ''
Your `nixpkgs.nix` file does not declare a default package source.
Ensure you set `nixpkgs.channels.*.default = ...;`
'');
basePkgs = cfg.base or {};
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" "default"];
_module.args = removeAttrs repos ["pkgs" "base"];
nixpkgs =
nixpkgs = let
nixpkgsConfig = {
config = lib.mkForce (basePkgs.config or {});
overlays = lib.mkForce (basePkgs.overlays or []);
};
nixpkgsHostsConfig =
nixpkgsConfig
// {
flake.source = lib.mkForce base;
};
nixpkgsHomesConfig = lib.mkIf (!config.home-manager.useGlobalPkgs) nixpkgsConfig;
in
if contextName == "hosts"
then {
flake.source = lib.mkForce base; # DEBUG: temp while getting base to work
overlays = lib.mkForce (defaultPkgs.overlays or {});
config = lib.mkForce (defaultPkgs.config or {});
}
then nixpkgsHostsConfig
else if contextName == "homes"
then {
config = lib.mkForce (defaultPkgs.config or {});
overlays = lib.mkForce (defaultPkgs.overlays or []);
}
then nixpkgsHomesConfig
else {};
};
}

View file

@ -0,0 +1,82 @@
{
config,
node,
lib,
pkgs,
hostname,
...
}: let
user = node.deploy.ssh.user;
cfg = config.users.users.${user};
DEFAULT_USER = "cerubld";
isStandardDeployUser = user == DEFAULT_USER;
in {
assertions = [
{
assertion = builtins.length node.deploy.ssh.publicKeys != 0;
message = ''
The Cerulean deployment user `${user}` for node `${hostname}` must have at least
one publicKey authorized for ssh deployment! Try setting `nodes.nodes.<name>.deploy.ssh.publicKeys = [ ... ]` <3
'';
}
# {
# assertion = cfg.isSystemUser && !cfg.isNormalUser;
# message = ''
# The Cerulean deployment user `${user}` for node `${hostname}` has been configured incorrectly.
# Ensure `users.users.${user}.isSystemUser == true` and `users.users.${user}.isNormalUser == false`.
# '';
# }
];
warnings = lib.optional (node.deploy.warnNonstandardDeployUser && !isStandardDeployUser) ''
The Cerulean deplyment user `${user}` for node `${hostname}` has been overriden.
It is recommended to leave this user as `${DEFAULT_USER}` unless you TRULY understand what you are doing!
This message can be disabled by setting `<node>.deploy.warnNonstandardBuildUser = false`.
'';
# prefer sudo-rs over sudo
security.sudo-rs = {
enable = true;
wheelNeedsPassword = true;
# allow the build user to run nix commands
extraRules = [
{
users = [user];
runAs = "${node.deploy.user}:ALL";
commands = [
# "${pkgs.nix}/bin/nix"
"ALL" # XXX: WARNING: FIX: TODO: DO NOT FUCKING USE `ALL`
];
}
];
};
# XXX: WARNING: FIX: TODO: use `trusted-public-keys` instead
nix.settings.trusted-users = [user];
# ensure deployment user has SSH permissions
services.openssh.settings.AllowUsers = [user];
users = lib.mkIf isStandardDeployUser {
groups.${user} = {};
users.${user} = {
enable = true;
description = "Cerulean's user for building and remote deployment.";
isSystemUser = true;
group = user;
createHome = true;
home = "/var/lib/cerulean/cerubld";
useDefaultShell = false;
shell = pkgs.bash;
openssh.authorizedKeys.keys = node.deploy.ssh.publicKeys;
};
};
}

View file

@ -48,16 +48,22 @@ in
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;
};
(flakeInputs
// {
inherit systems root;
inherit (this) snow;
inputs = flakeInputs;
})
|> (x: builtins.removeAttrs x ["self" "nodes"]);
modules = [
./module.nix
({config, ...}: {
_module.args = {
self = config;
nodes = config.nodes.nodes;
};
})
];
};
@ -86,9 +92,10 @@ in
userArgs = nodes.args // node.args;
ceruleanArgs = {
inherit systems root base;
inherit systems root base nodes node;
inherit (node) system;
inherit (this) snow;
hostname = name;
_cerulean = {
inherit inputs userArgs ceruleanArgs homeManager;
@ -111,7 +118,7 @@ in
modules =
[
self.nixosModules.default
(findImport (root + "/hosts/${name}"))
(findImport /${root}/hosts/${name})
]
++ (groupModules root)
++ node.modules
@ -128,7 +135,6 @@ in
(node.deploy)
ssh
user
sudoCmd
interactiveSudo
remoteBuild
rollback
@ -140,14 +146,17 @@ in
nixosFor = system: inputs.deploy-rs.lib.${system}.activate.nixos;
in {
hostname = ssh.host;
hostname =
if ssh.host != null
then ssh.host
else "";
profilesOrder = ["default"]; # profiles priority
profiles.default = {
path = nixosFor node.system nixosConfigurations.${name};
user = user;
sudo = sudoCmd;
sudo = "sudo -u";
interactiveSudo = interactiveSudo;
fastConnection = false;

View file

@ -65,7 +65,7 @@
# flatten recursion result
|> concatLists
# find import location
|> map (group: nt.findImport (root + "/groups/${group._name}"))
|> map (group: nt.findImport /${root}/groups/${group._name})
# filter by uniqueness
|> nt.prim.unique
# ignore missing groups

View file

@ -18,6 +18,6 @@
}: {
imports = [
./nodes
(snow.findImport (root + "/snow"))
(snow.findImport /${root}/snow)
];
}

View file

@ -59,23 +59,32 @@
default = "root";
example = "admin";
description = ''
The user that the system derivation will be deployed to. The command specified in
The user that the system derivation will be built with. The command specified in
`<node>.deploy.sudoCmd` will be used if `<node>.deploy.user` is not the
same as `<node>.deploy.ssh.user` the same as above).
'';
};
sudoCmd = mkOption {
type = types.str;
default = "sudo -u";
example = "doas -u";
warnNonstandardDeployUser = mkOption {
type = types.bool;
default = true;
example = false;
description = ''
Which sudo command to use. Must accept at least two arguments:
1. the user name to execute commands as
2. the rest is the command to execute
Disables the warning that shows when `deploy.ssh.user` is set to a non-standard value.
'';
};
# sudoCmd = mkOption {
# type = types.str;
# default = "sudo -u";
# example = "doas -u";
# description = ''
# Which sudo command to use. Must accept at least two arguments:
# 1. the user name to execute commands as
# 2. the rest is the command to execute
# '';
# };
interactiveSudo = mkOption {
type = types.bool;
default = false;
@ -145,8 +154,8 @@
ssh = {
host = mkOption {
type = types.str;
default = "";
type = types.nullOr types.str;
default = null;
example = "dobutterfliescry.net";
description = ''
The host to connect to over ssh during deployment
@ -171,6 +180,16 @@
'';
};
publicKeys = mkOption {
type = types.listOf types.str;
default = [];
example = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIeyZuUUmyUYrYaEJwEMvcXqZFYm1NaZab8klOyK6Imr me@puter"];
description = ''
SSH public keys that will be authorized to the deployment user.
This key is intended solely for deployment, allowing for fine-grained permission control.
'';
};
opts = mkOption {
type = types.listOf types.str;
default = [];