Compare commits

..

2 commits

Author SHA1 Message Date
2e536d7040
pin sops-nix (im oopid) 2026-03-05 19:26:02 +10:00
e3a2a17e6d cli init 2026-02-27 13:03:33 +10:00
22 changed files with 234 additions and 297 deletions

1
.gitignore vendored
View file

@ -1,2 +1 @@
/hidden /hidden
target/

24
TODO.md
View file

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

View file

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

View file

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

View file

@ -0,0 +1,49 @@
# 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";}))
];
};
}

View file

@ -1,82 +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.
{
_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 = {}; default = {};
description = "Declare package repositories"; description = "Declare package repositories";
example = { example = {
"npkgs" = { "pkgs" = {
source = "inputs.nixpkgs"; source = "inputs.nixpkgs";
system = "x86-64-linux"; system = "x86-64-linux";
config = { config = {
@ -53,7 +53,7 @@ in {
config = let config = let
repos = repos =
cfg cfg
|> (xs: removeAttrs xs ["base"]) |> (xs: removeAttrs xs ["default"])
|> mapAttrs ( |> mapAttrs (
name: args: name: args:
lib.mkForce ( lib.mkForce (
@ -65,31 +65,30 @@ in {
) )
); );
basePkgs = cfg.base or {}; # 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 = ...;`
'');
in { in {
# NOTE: _module.args is a special option that allows us to # NOTE: _module.args is a special option that allows us to
# NOTE: set extend specialArgs from inside the modules. # NOTE: set extend specialArgs from inside the modules.
# WARNING: pkgs is a reserved specialArg # WARNING: pkgs is a reserved specialArg
_module.args = removeAttrs repos ["pkgs" "base"]; _module.args = removeAttrs repos ["pkgs" "default"];
nixpkgs = let nixpkgs =
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" if contextName == "hosts"
then nixpkgsHostsConfig 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 {});
}
else if contextName == "homes" else if contextName == "homes"
then nixpkgsHomesConfig then {
config = lib.mkForce (defaultPkgs.config or {});
overlays = lib.mkForce (defaultPkgs.overlays or []);
}
else {}; else {};
}; };
} }

View file

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

View file

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

View file

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

View file

@ -59,32 +59,23 @@
default = "root"; default = "root";
example = "admin"; example = "admin";
description = '' description = ''
The user that the system derivation will be built with. The command specified in The user that the system derivation will be deployed to. The command specified in
`<node>.deploy.sudoCmd` will be used if `<node>.deploy.user` is not the `<node>.deploy.sudoCmd` will be used if `<node>.deploy.user` is not the
same as `<node>.deploy.ssh.user` the same as above). same as `<node>.deploy.ssh.user` the same as above).
''; '';
}; };
warnNonstandardDeployUser = mkOption { sudoCmd = mkOption {
type = types.bool; type = types.str;
default = true; default = "sudo -u";
example = false; example = "doas -u";
description = '' description = ''
Disables the warning that shows when `deploy.ssh.user` is set to a non-standard value. 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
''; '';
}; };
# 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 { interactiveSudo = mkOption {
type = types.bool; type = types.bool;
default = false; default = false;
@ -154,8 +145,8 @@
ssh = { ssh = {
host = mkOption { host = mkOption {
type = types.nullOr types.str; type = types.str;
default = null; default = "";
example = "dobutterfliescry.net"; example = "dobutterfliescry.net";
description = '' description = ''
The host to connect to over ssh during deployment The host to connect to over ssh during deployment
@ -180,16 +171,6 @@
''; '';
}; };
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 { opts = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
default = []; default = [];

1
cli/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

7
cli/Cargo.lock generated Normal file
View file

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "snow"
version = "0.1.0"

8
cli/Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "snow"
description = "NixOS Derivative"
version = "0.1.0"
authors = ["_cry64 <them@dobutterfliescry.net>"]
edition = "2024"
[dependencies]

22
cli/LICENSE Normal file
View file

@ -0,0 +1,22 @@
MIT License
Copyright (c) 2026 _cry64 (Emile Clark-Boman)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

8
cli/src/main.rs Normal file
View file

@ -0,0 +1,8 @@
use std::env;
mod rocli;
fn main() {
let args: Vec<String> = env::args().collect();
println!("Hello, world!");
}

9
cli/src/rocli/command.rs Normal file
View file

@ -0,0 +1,9 @@
pub struct Command {
pub name: String,
pub version: Option<String>,
pub action: Action,
pub subcommands: Vec<Commands>,
pub flags: Vec<Flag>,
}

4
cli/src/rocli/mod.rs Normal file
View file

@ -0,0 +1,4 @@
mod command;
mod parse;
pub use parse::normalize_args;

33
cli/src/rocli/parse.rs Normal file
View file

@ -0,0 +1,33 @@
/// Parse the GNU convention CLI argument syntax.
///
/// # Examples
/// ```rs
/// assert!(parse(vec!["--flag=value"]) == vec!["--flag", "value"]);
/// assert!(parse(vec!["--flag value"]) == vec!["--flag", "value"]);
/// assert!(parse(vec!["-abe"]) == vec!["-a", "-b", "-e"]);
/// assert!(parse(vec!["-abef=32"]) == vec!["-a", "-b", "-e", "-f", "32"]);
/// ```
///
/// # Credit
/// Based on [github:ksk001100/seahorse `src/utils.rs`](https://github.com/ksk001100/seahorse/blob/master/src/utils.rs)
pub fn normalize_args(args: Vec<String>) -> Vec<String> {
args.iter().fold(Vec::<String>::new(), |mut acc, el| {
if !el.starts_with('-') {
acc.push(el.to_owned());
return acc;
}
let mut split = el.splitn(2, '=').map(|s| s.to_owned()).collect();
if el.starts_with("--") {
acc.append(&mut split);
} else {
let flags = split[0].chars().skip(1).map(|c| format!("-{c}"));
acc.append(&mut flags.collect());
if let Some(value) = split.get(1) {
acc.push(value.to_owned());
}
}
acc
})
}

21
flake.lock generated
View file

@ -185,9 +185,30 @@
"microvm": "microvm", "microvm": "microvm",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"nt": "nt", "nt": "nt",
"sops-nix": "sops-nix",
"systems": "systems_3" "systems": "systems_3"
} }
}, },
"sops-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1772495394,
"narHash": "sha256-hmIvE/slLKEFKNEJz27IZ8BKlAaZDcjIHmkZ7GCEjfw=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "1d9b98a29a45abe9c4d3174bd36de9f28755e3ff",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "sops-nix",
"type": "github"
}
},
"spectrum": { "spectrum": {
"flake": false, "flake": false,
"locked": { "locked": {