Compare commits

...

2 commits

Author SHA1 Message Date
67f2aba9d8 STATUS: record dev_env (built+applied) and working deploy path
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:21:36 +02:00
aea4f8c3d6 dev_env: install Node.js from pinned tarball, drop npm bloat
Debian's npm package pulls a ~400-package node-* tree (the first deploy
installed 527 packages). Replace apt nodejs+npm with a pinned upstream Node
tarball (v20.19.2) installed to /opt + symlinked, mirroring the nvim install
pattern (ADR-014 pinning). npm/npx come bundled. Molecule verifies node/npm
on PATH; lint + idempotent converge green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:21:33 +02:00
6 changed files with 86 additions and 19 deletions

View file

@ -26,19 +26,23 @@ _Last reviewed: 2026-06-11._
| ADR-002 security strategy + `docs/security/{accepted-risks,service-checklist}.md` | Present — threat model, principles, governance frame; checklist + risk register are docs, enforced manually in review |
| Service-role standard + per-service `SECURITY.md` convention | Defined (ADR-004 + `docs/security/service-security-template.md`); not yet applied — no service roles exist |
| Tag standard + enforcement (ADR-019) | Works — `tests/tags.yml` (closed vocabulary) + `scripts/check-tags.py` (run by `make lint`, unit-tested): enforces the tag vocabulary and that each role import in a play's `roles:` block carries its role-name tag. Governs mostly-unbuilt roles, but the linter is live now. Proxmox VM tag convention (`<env>`, group, `managed-by=terraform`) is in the Terraform HCL but unprovisioned. |
| `ubongo` — physical control / AI-worker host (ADR-015) | **Built (partial).** Debian 13.5 on a Lenovo M70q (i3-10100T, 16 GB, 256 GB SSD; no disk encryption — accepted risk). Full toolchain installed + pinned to `fisi` (Docker 29.5.3, rbw 1.15.0, Claude Code 2.1.173, ansible-core 2.17.14 + molecule via `make setup`/`make collections`). Repo cloned under a dedicated `claude` user (docker group, no sudo). Vault works via rbw (offline-cache decryption verified). SSH key-only (password + root login disabled). In the production inventory `control` group at 10.20.10.151. **Pending:** NetBird mesh enrollment (so SSH is LAN-only); full `base` hardening (only the `firewall` concern exists, and it is NOT applied here — applying default-deny with no mesh would lock out inbound SSH on the physical NIC); OPNsense DHCP reservation for 10.20.10.151 (MAC `88:a4:c2:e0:ee:da`); Terraform state backup (no TF state yet). |
| `roles/dev_env/` — interactive developer environment | **Built + applied.** zsh + oh-my-zsh + oh-my-posh, tmux + TPM plugins, neovim; dotfiles deployed via GNU stow (re-derived from V4/fisi per ADR-013). Node.js from a pinned upstream tarball (not Debian's npm). Lint + Molecule (idempotent) green. **Applied to `ubongo`** for users `sjat` + `claude` (verified: zsh login shells, stow-symlinked `.zshrc`/`.tmux.conf` + nvim config, oh-my-zsh, tmux plugins; nvim v0.12.2, oh-my-posh 29.0.1). Run via `playbooks/workstation.yml` against the `control` group (no dedicated `workstations` group yet). |
| `make check` / `make deploy PLAYBOOK=<name>` | **Works.** First end-to-end run (applying `dev_env`) surfaced + fixed latent bugs: Makefile `PLAYBOOK` var collision (binary path vs playbook-name arg) meant the targets never ran; `ansible.cfg` referenced uninstalled community.general callbacks (now built-in `default` + `ansible.posix.profile_tasks`); `acl` package added so Ansible can `become_user` an unprivileged user. The make targets now function — though `site`/`base`/`docker_host` content is still incomplete (see below). |
| `ubongo` — physical control / AI-worker host (ADR-015) | **Built (partial).** Debian 13.5 on a Lenovo M70q (i3-10100T, 16 GB, 256 GB SSD; no disk encryption — accepted risk). Full toolchain installed + pinned to `fisi` (Docker 29.5.3, rbw 1.15.0, Claude Code 2.1.173, ansible-core 2.17.14 + molecule via `make setup`/`make collections`). Repo cloned under a dedicated `claude` user (docker group, no sudo). Vault works via rbw (offline-cache decryption verified). SSH key-only (password + root login disabled). In the production inventory `control` group at 10.20.10.151. **`dev_env` now applied here** (zsh/tmux/nvim for `sjat` + `claude`, via `playbooks/workstation.yml`). Managed as the operator account `sjat` (`group_vars/control` sets `ansible_user: sjat`), not the `ansible` service user `group_vars/all` assumes — ubongo has no bootstrapped `ansible` user. **Pending:** NetBird mesh enrollment (so SSH is LAN-only); full `base` hardening (only the `firewall` concern exists, and it is NOT applied here — applying default-deny with no mesh would lock out inbound SSH on the physical NIC); proper `ansible`-user bootstrap (currently managed as `sjat`); OPNsense DHCP reservation for 10.20.10.151 (MAC `88:a4:c2:e0:ee:da`); Terraform state backup (no TF state yet). |
## Scaffolded but empty — NOT implemented
| Thing | State |
|---|---|
| `roles/base/` | **Partially built.** The `firewall` concern is implemented (nftables: catalog-driven default-deny + east-west allowlist + auto-rollback apply; ADR-020) with pytest + Molecule render/syntax tests. Other concerns (SSH hardening, fail2ban, auditd, packages, users) are **not** built yet, so `make deploy PLAYBOOK=site` is still incomplete. |
| `roles/base/` | **Partially built.** The `firewall` concern is implemented (nftables: catalog-driven default-deny + east-west allowlist + auto-rollback apply; ADR-020) with pytest + Molecule render/syntax tests. Other concerns (SSH hardening, fail2ban, auditd, packages, users) are **not** built yet, so `make deploy PLAYBOOK=site` has no real content to apply (the make target itself now works — see "Real and working today"). |
| `roles/docker_host/` | Not in git. Same. |
| `inventories/*/hosts.yml` | Structured stubs with empty host maps (`hosts: {}`); regenerated by `make tf-inventory` once Terraform has hosts |
| `inventories/production/group_vars/{docker_hosts,proxmox_hosts}/` | Empty dirs |
So `make deploy PLAYBOOK=site` is still incomplete — `base` is only partially built (its
`firewall` concern only) and the `docker_host` role does not exist yet.
So `make deploy PLAYBOOK=site` has no real content to apply — `base` is only partially
built (its `firewall` concern only) and the `docker_host` role does not exist yet. (The
`make check`/`deploy` machinery itself now works — first proven by applying `dev_env` via
`playbooks/workstation.yml`.)
## Designed but not built

View file

@ -10,11 +10,12 @@ or service VMs.
## What it does
- Installs packages: `zsh, tmux, git, stow, build-essential, curl, ca-certificates,
fzf, ripgrep, direnv, nodejs, npm` (`dev_env__packages`).
- Installs **pinned** neovim (`dev_env__nvim_version`) and oh-my-posh
(`dev_env__omp_version`) from GitHub releases, and the system-wide oh-my-posh theme
`/etc/oh-my-posh/zen.toml`.
- Installs packages: `zsh, tmux, git, stow, acl, build-essential, curl,
ca-certificates, fzf, ripgrep, direnv` (`dev_env__packages`).
- Installs **pinned** neovim (`dev_env__nvim_version`), oh-my-posh
(`dev_env__omp_version`) and Node.js (`dev_env__node_version`) from upstream releases
(Node from the nodejs.org tarball — not Debian's `npm`, which pulls a ~400-package
tree), plus the system-wide oh-my-posh theme `/etc/oh-my-posh/zen.toml`.
- For each user in `dev_env__users`: sets the login shell to zsh, clones oh-my-zsh +
custom plugins and the tmux/TPM plugins, and **stows** the dotfiles into `~`.
@ -32,6 +33,7 @@ LSPs/formatters self-install via mason (no system LSP packages needed).
| `dev_env__users` | `[]` | Users to configure. Set per group, e.g. `group_vars/control → [sjat, claude]`. Empty = no per-user work. |
| `dev_env__nvim_version` | `v0.12.2` | Pinned neovim release. |
| `dev_env__omp_version` | `29.0.1` | Pinned oh-my-posh release. |
| `dev_env__node_version` | `v20.19.2` | Pinned Node.js release (nodejs.org tarball; npm bundled). |
| `dev_env__packages` | see defaults | APT packages. |
| `dev_env__omz_custom_plugins` | autosuggestions, syntax-highlighting | Cloned into `~/.oh-my-zsh/custom/plugins`. |
| `dev_env__tmux_plugins` | tpm, tmux-sensible, vim-tmux-navigator, catppuccin@v1.0.3 | Cloned into `~/.tmux/plugins`. |

View file

@ -6,27 +6,28 @@
# workstation-class group (e.g. group_vars/control → [sjat, claude]).
dev_env__users: []
# APT packages. nvim + oh-my-posh are installed separately from pinned releases.
# (nvim uses mason internally for LSPs, so no system LSP packages are needed; node is
# present so mason's node-based servers work. direnv is referenced by the .zshrc.)
# APT packages. nvim, oh-my-posh and Node.js are installed separately from pinned
# releases — Debian's `npm` pulls a ~400-package node-* tree, so we use the upstream
# Node tarball instead (npm bundled). nvim uses mason internally for LSPs; node is
# present so mason's node-based servers work. direnv is referenced by the .zshrc;
# acl lets Ansible become_user an unprivileged user (sjat -> claude) for file copies.
dev_env__packages:
- zsh
- tmux
- git
- stow
- acl # lets Ansible become_user an unprivileged user (sjat -> claude) for file copies
- acl
- build-essential
- curl
- ca-certificates
- fzf
- ripgrep
- direnv
- nodejs
- npm
# Pinned tool versions (ADR-014 — pin, don't track "latest").
dev_env__nvim_version: "v0.12.2"
dev_env__omp_version: "29.0.1"
dev_env__node_version: "v20.19.2"
# oh-my-zsh custom plugins (cloned per user into ~/.oh-my-zsh/custom/plugins).
dev_env__omz_custom_plugins:

View file

@ -23,6 +23,8 @@
loop:
- /usr/local/bin/nvim
- /usr/local/bin/oh-my-posh
- /usr/local/bin/node
- /usr/local/bin/npm
- /etc/oh-my-posh/zen.toml
register: dev_env__sys
loop_control:
@ -31,10 +33,8 @@
- name: Assert system tools are installed
ansible.builtin.assert:
that:
- dev_env__sys.results[0].stat.exists
- dev_env__sys.results[1].stat.exists
- dev_env__sys.results[2].stat.exists
fail_msg: nvim/oh-my-posh/zen.toml missing
- dev_env__sys.results | map(attribute='stat.exists') | min
fail_msg: a system tool (nvim/oh-my-posh/node/npm/zen.toml) is missing
- name: Look up the test user
ansible.builtin.getent:

View file

@ -15,6 +15,10 @@
ansible.builtin.include_tasks: oh_my_posh.yml
tags: [packages]
- name: Install Node.js (pinned release)
ansible.builtin.include_tasks: nodejs.yml
tags: [packages]
- name: Configure each developer user
ansible.builtin.include_tasks: per_user.yml
loop: "{{ dev_env__users }}"

View file

@ -0,0 +1,56 @@
---
- name: Node.js | Read installed-version sentinel
ansible.builtin.slurp:
src: /etc/node_installed_version
register: dev_env__node_sentinel
failed_when: false
- name: Node.js | Determine installed version
ansible.builtin.set_fact:
dev_env__node_installed: >-
{{ (dev_env__node_sentinel.content | b64decode | trim)
if dev_env__node_sentinel.content is defined else '' }}
- name: Node.js | Install pinned release
when: dev_env__node_installed != dev_env__node_version
block:
- name: Node.js | Download release tarball
ansible.builtin.get_url:
url: "https://nodejs.org/dist/{{ dev_env__node_version }}/node-{{ dev_env__node_version }}-linux-x64.tar.gz"
dest: "/tmp/node-{{ dev_env__node_version }}.tar.gz"
mode: "0644"
- name: Node.js | Create versioned install directory
ansible.builtin.file:
path: "/opt/node-{{ dev_env__node_version }}"
state: directory
mode: "0755"
- name: Node.js | Extract tarball
ansible.builtin.unarchive:
src: "/tmp/node-{{ dev_env__node_version }}.tar.gz"
dest: "/opt/node-{{ dev_env__node_version }}"
remote_src: true
extra_opts: ["--strip-components=1"]
- name: Node.js | Symlink node, npm, npx into PATH
ansible.builtin.file:
src: "/opt/node-{{ dev_env__node_version }}/bin/{{ item }}"
dest: "/usr/local/bin/{{ item }}"
state: link
force: true
loop:
- node
- npm
- npx
- name: Node.js | Write version sentinel
ansible.builtin.copy:
content: "{{ dev_env__node_version }}"
dest: /etc/node_installed_version
mode: "0644"
- name: Node.js | Remove downloaded tarball
ansible.builtin.file:
path: "/tmp/node-{{ dev_env__node_version }}.tar.gz"
state: absent