# ------------------------------------------------------------ # Taskfile – Docker‑based Nix development & Colmena deployment # ------------------------------------------------------------ version: "3" vars: # Base directory of the repository – pass it when you invoke `task`, # e.g. `task -v ROOT_DIR=$(pwd) …`. You can also export it in your shell. ROOT_DIR: "{{ .ROOT_DIR }}" # Convenience paths (expanded by `task` before being passed to Docker) FLAKE_DIR: "{{ .ROOT_DIR }}" AGE_KEY_PATH: "{{ .ROOT_DIR }}/age.key" SECRETS_YAML_PATH: "{{ .ROOT_DIR }}/hosts/blade/secrets.yaml" # ----------------------------------------------------------------- # Docker helpers (the Nix container always gets the repo mounted at # /workspace). `DOCKER_NIX_CMD` is used for *any* command that runs # inside the container. # ----------------------------------------------------------------- DOCKER_COMPOSE_CMD: "docker compose -f {{ .ROOT_DIR }}/docker-compose.yml" DOCKER_NIX_CMD: >- {{ .DOCKER_COMPOSE_CMD }} run --rm -v "{{ .ROOT_DIR }}:/workspace" nixos # ----------------------------------------------------------------- # Docker management # ----------------------------------------------------------------- tasks: docker-build: desc: Build the Docker container for Nix cmds: - "{{ .DOCKER_COMPOSE_CMD }} build" dir: "{{ .ROOT_DIR }}" # --------------------------------------------------------------- # NOTE: we now use DOCKER_NIX_CMD (which already has the bind‑mount) # --------------------------------------------------------------- docker-shell: desc: Enter a shell in the Docker container (repo is mounted) cmds: - "{{ .DOCKER_NIX_CMD }} bash" dir: "{{ .ROOT_DIR }}" docker-up: desc: Start the Docker container (detached) cmds: - "{{ .DOCKER_COMPOSE_CMD }} up -d" dir: "{{ .ROOT_DIR }}" docker-down: desc: Stop the Docker container cmds: - "{{ .DOCKER_COMPOSE_CMD }} down" dir: "{{ .ROOT_DIR }}" # ----------------------------------------------------------------- # Age‑key handling (SOPS) # ----------------------------------------------------------------- generate-age-key: desc: Generate a new age key for a host (default - all hosts without a key) vars: # If HOST is supplied we generate only for that host; otherwise we # iterate over every directory under `hosts/` that lacks an age.key. TARGET_HOSTS: | {{- if .HOST -}} {{ .HOST }} {{- else -}} {{- range $dir := (files (printf "%s/hosts/*" .ROOT_DIR)) -}} {{- $name := base $dir -}} {{- if not (fileExists (printf "%s/age.key" $dir)) -}} {{ $name }} {{- end -}} {{- end -}} {{- end }} cmds: - | {{- $list := splitList "\n" .TARGET_HOSTS -}} {{- range $h := $list }} echo "🔑 Generating age.key for host '{{ $h }}' ..." {{ .DOCKER_NIX_CMD }} -v "{{ .ROOT_DIR }}/hosts/{{ $h }}:/workspace/hosts/{{ $h }}" \ nixos age-keygen -o /workspace/hosts/{{ $h }}/age.key {{- end }} dir: "{{ .ROOT_DIR }}" # ----------------------------------------------------------------- # Encrypt / Decrypt (loop over hosts) # ----------------------------------------------------------------- encrypt-secrets: desc: | Encrypt `secrets.yaml` for every host that has an age.key. Pass HOST= to limit to a single host. vars: # Space‑separated list of hosts that have both age.key & secrets.yaml TARGET_HOSTS: | {{- if .HOST -}} {{ .HOST }} {{- else -}} {{- $list := slice -}} {{- range $dir := (files (printf "%s/hosts/*" .ROOT_DIR)) -}} {{- $name := base $dir -}} {{- $age := printf "%s/age.key" $dir -}} {{- $sec := printf "%s/secrets.yaml" $dir -}} {{- if (and (fileExists $age) (fileExists $sec)) -}} {{- $list = append $list $name -}} {{- end -}} {{- end -}} {{- join " " $list -}} {{- end }} preconditions: - sh: test -n "{{ .TARGET_HOSTS }}" msg: "No hosts with both age.key and secrets.yaml were found." cmds: - | for host in {{ .TARGET_HOSTS }}; do echo "🔐 Encrypting secrets for host '$host' ..." {{ .DOCKER_NIX_CMD }} -v "{{ .ROOT_DIR }}/hosts/$host:/workspace/hosts/$host" \ nixos sh -c "\ pub=\$(grep 'public key' /workspace/hosts/$host/age.key | awk '{print \$NF}') && \ sops --encrypt --age=\$pub --in-place /workspace/hosts/$host/secrets.yaml" done dir: "{{ .ROOT_DIR }}" decrypt-secrets: desc: | Decrypt `secrets.yaml` for every host that has an age.key. Decrypted output is written to `secrets.yaml.decrypted`. Pass HOST= to limit to a single host. vars: TARGET_HOSTS: | {{- if .HOST -}} {{ .HOST }} {{- else -}} {{- $list := slice -}} {{- range $dir := (files (printf "%s/hosts/*" .ROOT_DIR)) -}} {{- $name := base $dir -}} {{- $age := printf "%s/age.key" $dir -}} {{- $sec := printf "%s/secrets.yaml" $dir -}} {{- if (and (fileExists $age) (fileExists $sec)) -}} {{- $list = append $list $name -}} {{- end -}} {{- end -}} {{- join " " $list -}} {{- end }} preconditions: - sh: test -n "{{ .TARGET_HOSTS }}" msg: "No hosts with both age.key and secrets.yaml were found." cmds: - | for host in {{ .TARGET_HOSTS }}; do echo "🔓 Decrypting secrets for host '$host' ..." {{ .DOCKER_NIX_CMD }} -v "{{ .ROOT_DIR }}/hosts/$host:/workspace/hosts/$host" \ nixos sh -c "\ pub=\$(grep '# public key' /workspace/hosts/$host/age.key | awk '{print \$NF}') && \ sops --decrypt --age=\$pub /workspace/hosts/$host/secrets.yaml \ > /workspace/hosts/$host/secrets.yaml.decrypted" done dir: "{{ .ROOT_DIR }}" # ----------------------------------------------------------------- # Nix flake & build # ----------------------------------------------------------------- flake-check: desc: Run `nix flake check` on the repository cmds: - "{{ .DOCKER_NIX_CMD }} nix flake check --flake /workspace" dir: "{{ .ROOT_DIR }}" build-blade: desc: Build the NixOS configuration for the *blade* host cmds: - "{{ .DOCKER_NIX_CMD }} nix build /workspace#nixosConfigurations.blade" dir: "{{ .ROOT_DIR }}" # ----------------------------------------------------------------- # Colmena deployment # ----------------------------------------------------------------- deploy: desc: Deploy to an arbitrary node (usage - task deploy NODE=blade) cmds: - "{{ .DOCKER_NIX_CMD }} colmena apply --on {{ .NODE }}" dir: "{{ .ROOT_DIR }}" preconditions: - sh: test -n "{{ .NODE }}" msg: "NODE variable is required. Example: task deploy NODE=blade" - sh: test -f "{{ .ROOT_DIR }}/age.key" - sh: test -f "{{ .ROOT_DIR }}/hosts/{{ .NODE }}/secrets.yaml" deploy-blade: desc: Deploy the configuration to the *blade* host cmds: - "{{ .DOCKER_NIX_CMD }} colmena apply --on blade" dir: "{{ .ROOT_DIR }}" preconditions: - sh: test -f "{{ .ROOT_DIR }}/age.key" - sh: test -f "{{ .ROOT_DIR }}/hosts/blade/secrets.yaml" # ----------------------------------------------------------------- # Helper: add a new node # ----------------------------------------------------------------- add-node: desc: Create a new host directory with skeleton Nix files prompt: "Enter the name of the new node (e.g., node2):" cmds: - mkdir -p "{{ .FLAKE_DIR }}/hosts/{{ .CLI_ARGS }}" - | cat > "{{ .FLAKE_DIR }}/hosts/{{ .CLI_ARGS }}/configuration.nix" <<'EOF' { config, lib, pkgs, ... }: { imports = [ ./hardware-configuration.nix ../../common.nix ]; networking.hostName = "{{ .CLI_ARGS }}"; } EOF - | cat > "{{ .FLAKE_DIR }}/hosts/{{ .CLI_ARGS }}/hardware-configuration.nix" <<'EOF' # Add hardware‑specific configurations here { config, lib, pkgs, modulesPath, ... }: { imports = [ (modulesPath + "/installer/scan/not-detected.nix") ]; } EOF - | cat > "{{ .FLAKE_DIR }}/hosts/{{ .CLI_ARGS }}/secrets.yaml" <<'EOF' # Add node‑specific secrets here EOF - | cat > "{{ .FLAKE_DIR }}/hosts/{{ .CLI_ARGS }}/default.nix" <<'EOF' { ... }: { imports = [ ./configuration.nix ./hardware-configuration.nix ../../common.nix ]; } EOF dir: "{{ .ROOT_DIR }}" # ----------------------------------------------------------------- # House‑keeping # ----------------------------------------------------------------- clean: desc: Remove temporary/decrypted secret files for all hosts cmds: - | find "{{ .ROOT_DIR }}/hosts" -name 'secrets.yaml.decrypted' -delete silent: true