···
11
11
@printf '\033[1;33mUsage:\033[0m just <recipe> [args...]\n\n'
12
12
@just --list --list-heading $'Available recipes:\n\n'
13
13
14
14
-
# Generate {ci,edconfig} files.
14
14
+
# Regenerate editor config (.zed/settings.json).
15
15
[group('flake')]
16
16
-
gen target:
17
17
-
nix run .#{{ if target == "ci" { "render-workflows" } else if target == "edconfig" { "gen-files" } else { error("unknown target: " + target) } }}
16
16
+
gen:
17
17
+
nix run .#gen-files
18
18
19
19
# Update flake inputs.
20
20
[group('flake')]
···
183
183
sed -i "s|@${CURRENT_DIGEST}|@${UPSTREAM}|" "$TEMPLATE"
184
184
git add "$TEMPLATE"
185
185
git commit -m "k8s/tranquil-pds: ${FLOAT_TAG}@${CURRENT_DIGEST:7:12} → ${FLOAT_TAG}@${UPSTREAM:7:12}"
186
186
-
187
187
-
# Scaffold a new app chart under k8s/charts/<name>. After running, edit the
188
188
-
# values.yaml and add a release block to k8s/helmfile.yaml. See k8s/charts/README.md.
189
189
-
[group('kubes')]
190
190
-
new-app NAME:
191
191
-
#!/usr/bin/env bash
192
192
-
set -euo pipefail
193
193
-
DIR="k8s/charts/{{ NAME }}"
194
194
-
if [[ -e "$DIR" ]]; then
195
195
-
echo "$DIR already exists; aborting." >&2
196
196
-
exit 1
197
197
-
fi
198
198
-
mkdir -p "$DIR/templates"
199
199
-
cat > "$DIR/Chart.yaml" <<EOF
200
200
-
apiVersion: v2
201
201
-
name: {{ NAME }}
202
202
-
description: TODO
203
203
-
type: application
204
204
-
version: 0.1.0
205
205
-
appVersion: "1.0.0"
206
206
-
dependencies:
207
207
-
- name: common
208
208
-
version: 0.1.0
209
209
-
repository: file://../common
210
210
-
EOF
211
211
-
cat > "$DIR/values.yaml" <<'EOF'
212
212
-
replicaCount: 1
213
213
-
strategy: Recreate
214
214
-
215
215
-
image:
216
216
-
repository: TODO
217
217
-
tag: TODO@sha256:TODO
218
218
-
pullPolicy: IfNotPresent
219
219
-
220
220
-
resources:
221
221
-
requests: { cpu: 50m, memory: 64Mi }
222
222
-
limits: { cpu: "1", memory: 256Mi }
223
223
-
224
224
-
ports:
225
225
-
- name: http
226
226
-
containerPort: 80
227
227
-
228
228
-
service:
229
229
-
type: ClusterIP
230
230
-
ports:
231
231
-
- name: http
232
232
-
port: 80
233
233
-
targetPort: http
234
234
-
235
235
-
probes:
236
236
-
readiness:
237
237
-
httpGet: { path: /, port: http }
238
238
-
periodSeconds: 10
239
239
-
240
240
-
ingress:
241
241
-
enabled: true
242
242
-
className: traefik
243
243
-
routes:
244
244
-
- host: TODO.cute.haus
245
245
-
tlsSecret: cute-haus-tls
246
246
-
EOF
247
247
-
for kind in deployment service ingress; do
248
248
-
echo '{{{{- include "common.'"$kind"'" . }}}}' > "$DIR/templates/$kind.yaml"
249
249
-
done
250
250
-
helm dependency update "$DIR" >/dev/null
251
251
-
echo "scaffolded $DIR. next: edit values.yaml and add a release to k8s/helmfile.yaml."
···
1
1
+
# AGENTS.md
2
2
+
3
3
+
NixOS homelab + k3s cluster IaC for cute.haus. Single operator. Flake-parts
4
4
+
NixOS configs on bare-metal/VPS hosts; an in-tree helm chart per app on k3s;
5
5
+
SOPS+age secrets; Cloudflare/B2 via Terraform.
6
6
+
7
7
+
## Environment
8
8
+
9
9
+
- `direnv allow` loads the flake (`use flake . --impure`, sets
10
10
+
`NIXPKGS_ALLOW_UNFREE=1`) and decrypts `CLOUDFLARE_API_TOKEN`,
11
11
+
`HCLOUD_TOKEN`, and B2 keys into env from `secrets/*.yaml` (used by
12
12
+
terraform). The devShell runs `gen-files` on entry.
13
13
+
- Everything runs through the Nix flake; there is no global toolchain. The
14
14
+
`.justfile` is the command source of truth — run `just` to list recipes.
15
15
+
16
16
+
## Commands
17
17
+
18
18
+
| Task | Command |
19
19
+
| --- | --- |
20
20
+
| Local pre-push gate | `just check` → `bun scripts/check.ts` + `nix flake check --impure` |
21
21
+
| Build a NixOS host (no deploy) | `just build <host>` (`eterna` `jubilife` `pastoria` `snowpoint`) |
22
22
+
| Deploy all hosts | `just deploy` (nynx `--operation switch`) |
23
23
+
| Offline deploy + reboot | `just deploy-offline` |
24
24
+
| Update flake inputs | `just update [inputs]` / `just update-nixpkgs` |
25
25
+
| Regenerate editor config | `just gen` (writes `.zed/settings.json`) |
26
26
+
| Edit a secret | `just sops-edit <file>` (`secrets/<file>`) |
27
27
+
| Rekey after key change | `just sops-rekey` (after adding/removing `keys/*.pub`) |
28
28
+
| Bump pinned image digests | `just bump <chart>` / `--all` / `--check` |
29
29
+
| Bump private-registry image | `just bump-tranquil` (atcr.io, needs sops creds) |
30
30
+
31
31
+
`scripts/` is Bun + TypeScript (`bun scripts/<file>.ts`); `scripts/check.ts`
32
32
+
runs three consistency checks (forward-auth, release-names, pinned-images).
33
33
+
34
34
+
## CI (`.github/workflows/`)
35
35
+
36
36
+
- `check-config`: bun checks, `helm template` each chart, ansible
37
37
+
`--syntax-check`, terraform `fmt -check` + `init -backend=false` + `validate`.
38
38
+
- `check-nix`: `nix -Lv flake check`.
39
39
+
- `build-nixos`: builds all 4 host toplevels.
40
40
+
- `build-nix`: builds `devShells.default` + `packages.gen-files` (only on
41
41
+
`flake.lock`/`flake.nix` changes).
42
42
+
- PR + push to `master`; `**/*.md`, `.github/**`, `_img/**` ignored (except
43
43
+
build-nix). Renovate auto-merges minor/patch/digest on a schedule.
44
44
+
45
45
+
## Formatting
46
46
+
47
47
+
Defined in `nix/treefmt.nix` (no checked-in `treefmt.toml`; generated by
48
48
+
treefmt-nix). Enforced via `nix flake check` in `just check`.
49
49
+
50
50
+
- Nix: **alejandra** (not nixpkgs-fmt) + deadnix + statix.
51
51
+
- YAML/JSON/MD: prettier. Shell: shfmt (2-space) + shellcheck.
52
52
+
- TOML: taplo. HCL: terraform.
53
53
+
54
54
+
## Architecture
55
55
+
56
56
+
- `nix/` — flake-parts + `import-tree ./nix` (modules auto-load; just add a
57
57
+
file). Hosts live in `nix/hosts/<host>.nix` and set
58
58
+
`flake.nixosConfigurations.<host>`. NixOS modules register via
59
59
+
`flake.modules.nixos.<name>` (see `nix/nixos/base/default.nix`). Disk
60
60
+
profiles in `nix/disko.nix`. k3s node config in `nix/nixos/profiles/k3s.nix`
61
61
+
(flannel on `tailscale0`; startup blocks until tailscale0 has an IP).
62
62
+
- `k8s/` — `helmfile.yaml` is the ordered release graph (uses `needs:`).
63
63
+
`charts/<name>/` are in-tree charts with **explicit manifests, no shared
64
64
+
helpers** (see `k8s/charts/README.md`). `values/global.yaml` holds shared
65
65
+
image pins + constants; `values/secrets/<name>.yaml` holds `ref+sops://`
66
66
+
vals refs into `secrets/`. Secret flow: `secrets/*.yaml` → vals → helmfile
67
67
+
→ chart `templates/secret.yaml` → app `envFrom`.
68
68
+
- `secrets/` — SOPS, multi-recipient age. `.sops.yaml` is generated; never
69
69
+
hand-edit either.
70
70
+
- `keys/` — `aly_<host>.pub` (user key) + `root_<host>.pub` (host key) age
71
71
+
recipients. Add/remove a `.pub` then `just sops-rekey`.
72
72
+
- `terraform/` — Cloudflare DNS + B2 buckets. Remote state in B2 with **no
73
73
+
locking** (don't run from two places at once).
74
74
+
- `ansible/` — `inventory.ini` (the 4 deployable hosts).
75
75
+
`playbooks/vars/oidc-clients.yml` is the OIDC client source of truth.
76
76
+
77
77
+
## Conventions & gotchas
78
78
+
79
79
+
- **Every deployed chart must sha256-pin its container images**
80
80
+
(`image: repo:tag@sha256:...` or `image: {repository, tag: ...@sha256:...}`),
81
81
+
enforced by `check-pinned-images.ts`. Shared images (alpine, rclone) live
82
82
+
in `k8s/values/global.yaml`. Renovate keeps digests fresh (skips
83
83
+
`tranquil`, which is on private atcr.io — use `just bump-tranquil`).
84
84
+
- **forward-auth OIDC slugs** must be `forward-auth-<app>` and match a key in
85
85
+
`k8s/values/secrets/forward-auth.yaml` (enforced by `check-forward-auth.ts`).
86
86
+
- k3s node labels: `topology.kubernetes.io/zone` (cloud/home),
87
87
+
`cute.haus/ingress=true`, and `cute.haus/intel-gpu=true` (NixOS module).
88
88
+
For GPU pods request the `gpu.intel.com/i915` resource, not a nodeSelector.
···
1
1
{
2
2
-
description = "Aly's NixOS homelab flake with flake-parts";
2
2
+
description = "cute.haus infra";
3
3
4
4
inputs = {
5
5
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
···
1
1
apiVersion: v2
2
2
name: external-routes
3
3
-
description: Ingress routes for non-cluster services (homelab hosts via tailscale)
3
3
+
description: Ingress routes for non-cluster services
4
4
type: application
5
5
version: 0.1.0
···
1
1
apiVersion: v2
2
2
name: watsup
3
3
-
description: Watsup homelab dashboard
3
3
+
description: Watsup dashboard
4
4
type: application
5
5
version: 0.1.0