333 lines
12 KiB
Nix
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;
|
|
};
|
|
};
|
|
}
|