ncm/flake.nix
2026-07-09 09:24:39 -06:00

333 lines
12 KiB
Nix

{
description = "sfos (self-host os)";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
systems.url = "github:nix-systems/default";
colmena = {
url = "github:zhaofengli/colmena";
inputs.nixpkgs.follows = "nixpkgs";
};
devenv.url = "github:cachix/devenv";
nixos-generators = {
url = "github:nix-community/nixos-generators";
inputs.nixpkgs.follows = "nixpkgs";
};
nix-editor = {
url = "github:snowfallorg/nix-editor";
inputs.nixpkgs.follows = "nixpkgs";
};
ess = {
url = "github:acaloiaro/ess/v2.18.0";
inputs.nixpkgs.follows = "nixpkgs";
};
agenix = {
url = "github:ryantm/agenix";
inputs.nixpkgs.follows = "nixpkgs";
};
agenix-rekey = {
url = "github:oddlama/agenix-rekey";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = inputs @ {
agenix,
agenix-rekey,
colmena,
devenv,
nixpkgs,
nix-editor,
nixos-generators,
self,
systems,
...
}: let
lib = nixpkgs.lib;
pkgs = nixpkgs.legacyPackages."x86_64-linux";
forEachSystem = nixpkgs.lib.genAttrs (import systems);
# Automatically discover hosts from operate/hosts directory
hostsDir = ./operate/hosts;
# Filter to only include directories (exclude files like rekey.nix)
hostNames = builtins.attrNames (
lib.filterAttrs (name: type: type == "directory") (builtins.readDir hostsDir)
);
# Helper to create nixosConfiguration from a host name
# Note: nixosConfigurations are only used by agenix-rekey for discovering hosts/secrets.
# Colmena uses the separate `hosts` paths below, so this override doesn't affect colmena.
mkHost = hostName:
nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
pkgs = import nixpkgs {
system = "x86_64-linux";
config.allowUnfree = true;
overlays = [
inputs.agenix.overlays.default
inputs.agenix-rekey.overlays.default
# Override nomad_1_11 to use version 1.11.3
(final: prev: {
nomad_1_11 = prev.nomad_1_11.overrideAttrs (oldAttrs: rec {
version = "1.11.3";
src = prev.fetchFromGitHub {
owner = "hashicorp";
repo = "nomad";
rev = "v${version}";
hash = "sha256-J+w53HlMlrXX5yKjDYhf3rSGt1pmOyNcPlOqyUrkLWE=";
};
vendorHash = "sha256-67etQUjcPXz4VVpNXLVusQlEybxEqKfYQcNTNL4X8bA=";
});
})
];
};
specialArgs = {
inherit self inputs;
inherit (inputs) agenix;
agenix-rekey = inputs.agenix-rekey;
};
modules = [
# Host configs import agenix and agenix-rekey modules themselves
(hostsDir + "/${hostName}")
# Define a dummy 'deployment' option for colmena-specific configs.
# This allows the host configs (which include deployment settings for colmena)
# to be imported into nixosSystem without errors. The deployment settings are
# simply ignored in nixosSystem context and only used by colmena.
{
options.deployment = lib.mkOption {
type = lib.types.attrs;
default = {};
description = "Colmena deployment settings (ignored in nixosSystem)";
};
}
];
};
# Generate host configurations dynamically
hosts = lib.genAttrs hostNames (name: hostsDir + "/${name}");
in {
inherit lib;
# Create nixosConfigurations for agenix-rekey
nixosConfigurations = lib.genAttrs hostNames mkHost;
colmenaHive = colmena.lib.makeHive self.outputs.colmena;
colmena =
{
meta = {
nixpkgs = import nixpkgs {
system = "x86_64-linux";
config.allowUnfree = true;
overlays = [
inputs.agenix.overlays.default
inputs.agenix-rekey.overlays.default
# Override nomad_1_11 to use version 1.11.3
(final: prev: {
nomad_1_11 = prev.nomad_1_11.overrideAttrs (oldAttrs: rec {
version = "1.11.3";
src = prev.fetchFromGitHub {
owner = "hashicorp";
repo = "nomad";
rev = "v${version}";
hash = "sha256-J+w53HlMlrXX5yKjDYhf3rSGt1pmOyNcPlOqyUrkLWE=";
};
vendorHash = "sha256-67etQUjcPXz4VVpNXLVusQlEybxEqKfYQcNTNL4X8bA=";
});
})
];
};
specialArgs = {
inherit self;
inherit inputs;
inherit (inputs) agenix;
agenix-rekey = inputs.agenix-rekey;
};
};
}
// hosts;
devShells = forEachSystem (system: let
config = self.devShells.${system}.default.config;
in {
default = devenv.lib.mkShell {
inherit inputs pkgs;
modules = [
{
languages = {
nix.enable = true;
};
packages = with pkgs; [
hcloud
inputs.nix-editor.packages.${system}.default
inputs.colmena.packages.${system}.colmena
inputs.ess.packages.${system}.default
inputs.nixos-generators.packages.${system}.default
inputs.agenix-rekey.packages.${system}.default
jq
linode-cli
opentofu
pre-commit
ssh-to-age
];
enterShell =
#bash
''
run-show-help
'';
pre-commit.hooks.env-sample-sync = {
enable = true;
always_run = true;
pass_filenames = false;
name = "env-sample-sync";
description = "Sync secrets to env.sample";
entry = "${inputs.ess.packages.${system}.default}/bin/ess";
};
scripts = {
run-show-help = {
description = "Show this help text";
exec = ''
echo
echo Helper scripts available:
echo
${pkgs.gnused}/bin/sed -e 's| |XX|g' \
-e 's|=| |' <<EOF | \
${pkgs.util-linuxMinimal}/bin/column -t | \
${pkgs.gnused}/bin/sed -e 's|XX| |g'
${pkgs.lib.generators.toKeyValue {} (pkgs.lib.mapAttrs (name: value: value.description) config.scripts)}
EOF
'';
};
ncm-prepare = {
description = "Prepare cluster nodes for colmena: SSH keys, agenix-rekey secrets, and host configs. Skips existing nodes by default; pass --overwrite [node1,node2] to re-initialize specific nodes, or --overwrite alone for all.";
exec = ''
#!/run/current-system/sw/bin/bash
set -euo pipefail
: "''${TF_VAR_admin_ssh_public_key:?TF_VAR_admin_ssh_public_key must be set}"
overwrite_all=false
overwrite_nodes=""
expect_nodes=false
for arg in "$@"; do
if [ "$expect_nodes" = "true" ]; then
case "$arg" in
--*) overwrite_all=true ;;
*) overwrite_nodes="$arg" ;;
esac
expect_nodes=false
fi
[ "$arg" = "--overwrite" ] && expect_nodes=true
done
[ "$expect_nodes" = "true" ] && overwrite_all=true
should_overwrite() {
local hostName="$1"
[ "$overwrite_all" = "true" ] && return 0
[ -n "$overwrite_nodes" ] || return 1
case ",$overwrite_nodes," in
*",$hostName,"*) return 0 ;;
*) return 1 ;;
esac
}
hosts_dir="./operate/hosts"
mkdir -p "$hosts_dir"
echo "Fetching node details from OpenTofu..."
node_details=$(tofu -chdir="./deploy" output -json | jq '.cluster_node_details.value')
failed_hosts=""
prepared_hosts=""
skipped_hosts=""
prepared_count=0
prepare_host() {
local hostName="$1" host="$2"
local tmp_dir
tmp_dir=$(mktemp -d)
trap "rm -rf '$tmp_dir'" RETURN
echo " Scanning SSH host key from $host..."
local ssh_public_key
ssh_public_key=$(ssh-keyscan -T 15 "$host" 2>/dev/null | grep ssh-ed25519 | head -1 | cut -d " " -f 2,3)
[ -n "$ssh_public_key" ] || { echo " No ed25519 key returned from $host"; return 1; }
echo " Converting SSH key to age format..."
local age_pubkey
age_pubkey=$(echo "$ssh_public_key" | ssh-to-age 2>/dev/null)
[ -n "$age_pubkey" ] || { echo " ssh-to-age conversion failed"; return 1; }
echo " Building config..."
cp -rf operate/templates/. "$tmp_dir/"
nix-editor "$tmp_dir/default.nix" users.users.sfos.openssh.authorizedKeys.keys \
-i -v "[\"$TF_VAR_admin_ssh_public_key\"]"
nix-editor "$tmp_dir/default.nix" users.users.root.openssh.authorizedKeys.keys \
-i -v "[\"$TF_VAR_admin_ssh_public_key\"]"
nix-editor "$tmp_dir/default.nix" networking.hostName -i -v "\"$hostName\""
nix-editor "$tmp_dir/default.nix" deployment.targetHost -i -v "\"$host\""
nix-editor "$tmp_dir/default.nix" age.rekey.hostPubkey -i -v "\"$age_pubkey\""
nix-editor "$tmp_dir/default.nix" age.rekey.storageMode -i -v "\"local\""
nix-editor "$tmp_dir/default.nix" age.rekey.localStorageDir \
-i -v "../../../secrets/rekeyed/$hostName"
local host_dir="$hosts_dir/$hostName"
rm -rf "$host_dir"
cp -r "$tmp_dir/." "$host_dir"
echo " Configured (age: $age_pubkey)"
}
while IFS='|' read -r hostName host _private_ip _generation; do
echo ""
echo "Preparing $hostName ($host)..."
host_dir="$hosts_dir/$hostName"
if [ -d "$host_dir" ] && ! should_overwrite "$hostName"; then
echo " Skipping (already configured; use --overwrite $hostName to force)"
skipped_hosts="$skipped_hosts $hostName"
continue
fi
rc=0
prepare_host "$hostName" "$host" || rc=$?
if [ "$rc" -eq 0 ]; then
prepared_hosts="$prepared_hosts $hostName"
prepared_count=$((prepared_count + 1))
else
failed_hosts="$failed_hosts $hostName"
fi
done < <(echo "$node_details" | jq -r 'to_entries[] | "\(.key)|\(.value.ipv4)|\(.value.private_ipv4)|\(.value.generation)"')
echo ""
[ -n "$skipped_hosts" ] && echo " Already configured (skipped):$skipped_hosts"
if [ -n "$failed_hosts" ]; then
echo " Failed to prepare:$failed_hosts"
echo " Check SSH connectivity and retry."
fi
if [ "$prepared_count" -gt 0 ]; then
echo "Rekeying secrets for all configured hosts..."
nix run .#agenix-rekey.x86_64-linux.rekey
git add ./operate/hosts ./secrets/rekeyed
echo ""
echo " Prepared $prepared_count host(s):$prepared_hosts"
echo " Next: colmena apply --impure"
fi
'';
};
};
}
];
};
});
agenix-rekey = inputs.agenix-rekey.configure {
userFlake = self;
nixosConfigurations = self.nixosConfigurations;
};
};
}