Skip to main content

Home Lab

Overview repository for the CS_PROJECT_CODE home-lab platform. It holds the architecture record and design notes tying the child repos together, plus iac-ctl, an interactive CLI tool that drives the platform's deployment stages end to end.

Quickstart​

git clone <this-repo-url>
uv sync
uv run iac-ctl

Every child repo (vault-deployer, devops-server-deployer, devops-server-onboarding, application-deployer, dashboard, and optionally mcp-server and documentation) is delivered to clients as a directory under the controller directory that iac-ctl asks for on first launch. See Controller tool for what every menu action does.

Controller tool​

What each of iac-ctl's menu actions does.

Every prompt has buttons, so Enter and Ctrl+S are optional. When an action walks over several repos, its yes/no prompts show Yes, Skip, and Cancel. Skip moves on to the next repo; Cancel drops the whole action back to the main menu. The last confirmation of an action, and every prompt with no next item to move on to, shows only its primary button (Yes, Submit, or Confirm) and Cancel. Host and tag checklists add a filter box and Select All and Clear All buttons. Large text areas also get Copy, Paste, and Clean. Ctrl+C quits at any time.

Environment variables​

VariableRequiredDefaultUsed for
IAC_CTL_BASE_CONFIG_JSON_FILENo~/.iac-ctl/config.jsonWhere the persisted config (project code, PyPI index, Ansible requirements source, extra vars, uv, pnpm and rsync paths, controller directory) is read from and written to.
DEVOPS_SERVER_INVENTORY_HOSTNAME"Enable MCP", "Devops Server Onboarding", "Application Deployer"NoneThe DevOps server host's inventory hostname, set in the config's extra_vars (not the process environment). Locates the DevOps server's registry in Vault for the Docker login, and reaches the devops-server-onboarding scripts and the application-deployer playbook through the subprocess environment; without it the action fails immediately, and the footer shows a warning banner.

The config's extra_vars are the environment iac-ctl hands to the subprocesses it runs, so they are where the platform's own variables go:

KeyRequired byUsed for
VAULT_API_ENDPOINT, VAULT_API_KEY, VAULT_CA_CERT_BASE64, VAULT_CLIENT_CERT_BASE64, VAULT_CLIENT_KEY_BASE64The Vault pre-checkConnecting to Vault. For a key missing from extra_vars, the Vault client falls back to the process environment variable of the same name. "Enable MCP" copies them from extra_vars into mcp-server's config and ignores the process environment.

CS_PROJECT_CODE comes from the config's cs_project_code, and PATH from iac-ctl's own process. Those two, DEBUG, and every key starting with IAC_CTL are reserved in any letter case, so extra_vars can't set them. Keys that differ only in case count as duplicates.

Screenshots​

Main​

iac-ctl-main.png

Application Deployer​

iac-ctl-application-deployer.png

Shared checks​

Vault pre-check​

"Devops Server Deployer", "Application Deployer", "Devops Server Onboarding", "Vault Secrets", and "Enable MCP" run this check as soon as they're selected, before any prompt. It:

  • Logs a warning and stops if any of the five Vault connection fields is missing from both the config's extra_vars and the process environment.
  • Connects and lists the config's cs_project_code bucket, logging a warning and stopping if that fails.

On failure, the reason also appears as the action's status message.

"Vault Deployer" skips this check. It deploys Vault itself, so it can't rely on Vault being up. "Dashboard Deployer" and "Documentation" also skip it because they read from the process environment rather than Vault. The other deployers ("Devops Server Deployer", "Application Deployer") and the onboarding scripts read their secrets and per-host config from Vault.

Write .env files​

Walks the controller repo and every repo in ChildRepository, and writes .env from the config's extra_vars in each one that exists locally, as export KEY='value' lines. A missing .env is created without asking. An existing one (file, symlink, or directory) is deleted and rewritten only after the user confirms. extra_vars can't contain a reserved key, including CS_PROJECT_CODE (matched case-insensitively), because iac-ctl manages those values.

Apply repository templates​

Walks the controller repo and every repo in ChildRepository that exists locally. For each one it checks whether the repo is the directory iac-ctl is running from. If it is, an extra prompt warns that applying the templates there could affect the running process, and declining skips that repo. It then asks once whether to apply the repository templates. On confirmation it writes:

FilesWhen
An empty README.mdEvery repo that doesn't have one
A WTFPL LICENSE, overwrittenEvery repo
.yamllint, overwritten with the baseline configEvery repo
.git-hooks/pre-commit, executable (see below)Every repo
.gitleaks.toml, required keys merged inEvery repo
.venv, uv.lock, pyproject.toml (see below)Only if the repo has pyproject.toml
package.json, node_modules, lock filesOnly if the repo has package.json
requirements.yml, ansible.cfg, .ansible-lintOnly if the repo has requirements.yml

.git-hooks/pre-commit is the gitleaks hook and the repo's only pre-commit hook: any other pre-commit* file in .git-hooks is deleted.

.gitleaks.toml is parsed and only its title (Gitleaks development) and [extend] useDefault = true are set; the file is created if missing, and every other key, table, and comment stays as it was.

For a repo with a pyproject.toml, it:

  • Deletes .venv and uv.lock.
  • Rewrites pyproject.toml against the config's pypi_remote_source, and sets [project]'s description (from the repo's entry in ChildRepository, or CONTROLLER_DESCRIPTION for the controller) and version (the controller's own version, shown in the TUI header). rewrite_pyproject_toml() defines exactly what changes.
  • Runs uv lock using the config's uv_path.

For a repo with a package.json, it:

  • Sets name (the repo's name), version (the controller's own version), and description (as for pyproject.toml), leaving every other key as it was.
  • Deletes node_modules, package-lock.json, yarn.lock, and pnpm-lock.yaml.
  • Runs pnpm install --reporter=append-only using the config's pnpm_path.

For a repo with a requirements.yml at its root, it points that file at the config's ansible_requirements_source: INNER (the devops-server mirror) or EXTERNAL (upstream GitHub). It also overwrites ansible.cfg with the INI form of ANSIBLE_CONFIG and .ansible-lint with the YAML form of ANSIBLE_LINT_CONFIG. ansible-galaxy install belongs to "Install dependencies", which runs it after uv sync sets up the repo's .venv (and after pnpm install, for a repo that also has a package.json).

Install dependencies​

Walks the controller repo and every repo in ChildRepository that has a pyproject.toml or a package.json, asking per repo whether to "install all dependencies". The prompt doesn't name a tool because the action covers more than one. On confirmation, a repo with a pyproject.toml has its .venv deleted and uv sync --locked --managed-python --all-extras --all-packages --no-progress run using the config's uv_path. A repo with a package.json then gets pnpm install --frozen-lockfile --reporter=append-only run using the config's pnpm_path; --frozen-lockfile fails rather than rewriting the lock file.

--locked fails rather than rewriting uv.lock, so an out-of-sync checkout has nothing to lose. When the repo is the directory iac-ctl is running from, an extra prompt warns that installing there could affect the running process.

If the repo has a requirements.yml, it then runs uv --offline run --no-sync ansible-galaxy install -r requirements.yml --force -vvv. uv sync has just put Ansible in .venv, so a failure here is a real error: a warning names the repo and the walk moves on. The "Running..." overlay's status line shows which repo is in progress.

Enable MCP​

Rotates the platform's mcp-server config. It fails immediately if DEVOPS_SERVER_INVENTORY_HOSTNAME is missing from the config's extra_vars; the footer shows a warning banner while it is missing. After the Vault pre-check and one Yes/Cancel prompt, it logs the Docker SDK client into the DevOps server's container registry (registry address and admin API token from Vault's <project_code>/devops-server/hosts/<DEVOPS_SERVER_INVENTORY_HOSTNAME>/apps/scm/generated, admin user name from the DevOps server API, called with the Root CA from Vault's <project_code>/certificate-authority/root-ca as the trusted certificate), then:

  • Generates a new bearer secret and a random port.
  • Builds a JSON value from the config's cs_project_code, mcp_transport=streamable-http, mcp_server_host=0.0.0.0, the port and secret, and the Vault connection fields in the config's extra_vars.
  • Removes any existing controller-mcp-server container, rebuilds the image from mcp-server's Dockerfile, and runs it on 127.0.0.1:<port> with the JSON value passed straight into the container. No config file is written or mounted. If the image build fails, its output is logged as errors before the action stops. It then waits, for up to 90 seconds, for the container to report healthy.
  • Writes or updates a controller-mcp-server entry in the .mcp.json of the controller repo and of every repo in ChildRepository that exists locally, pointing at http://127.0.0.1:<port>/mcp with an Authorization: Bearer <secret> header. Other entries in each file stay as they are, and a file that isn't a valid JSON object is left untouched with a warning.

.mcp.json is a local file.

Vault Deployer​

Runs vault-deployer's playbook.yml against hosts and tags picked at run time:

  • If the repo has no playbook.yml, it logs a warning and stops.
  • It runs ansible-playbook --list-hosts and --list-tags through the config's uv_path and parses the output into two lists. Tags containing danger, cleanup, or never are left out of the tag list. If either command fails or either list is empty, it logs a warning and stops.
  • The user checks off hosts, then tags. Nothing starts checked. Confirm shows an inline error until at least one item is checked. Cancel or Escape stops the whole action.
  • A final Yes/Cancel prompt lists the chosen hosts and tags.
  • The playbook runs with the hosts joined into -l host1,host2 and the tags into --tags tag1,tag2.

vault-deployer's playbook fails when its inventory holds more than one remote host, whatever hosts are checked. That check runs in the playbook, not in iac-ctl.

Devops Server Deployer​

Runs the Vault pre-check, then devops-server-deployer's playbook.yml with the same steps as "Vault Deployer". The tag list covers every service on the DevOps server host, one prefix each (for example pypi_mirror_*, database_*, scm_*).

Devops Server Onboarding​

Runs one of devops-server-onboarding's console scripts. Each script reads its config from Vault on startup.

  • It fails immediately if DEVOPS_SERVER_INVENTORY_HOSTNAME is missing from the config's extra_vars, before any prompt or Vault call. The footer shows a warning banner while it is missing.
  • It runs the Vault pre-check.
  • It lists every command under [project.scripts] in the repo's pyproject.toml. If the file is missing or the table is empty, it logs a warning and stops.
  • The user picks one command from a single-choice list and confirms with Yes/Cancel.
  • It runs uv --offline run --no-sync --no-progress <command> through the config's uv_path in devops-server-onboarding, with the config's extra_vars, CS_PROJECT_CODE, and PATH as the process's environment.

"Install dependencies" installs these scripts; this action runs no playbook.

Application Deployer​

Fails immediately if DEVOPS_SERVER_INVENTORY_HOSTNAME is missing from the config's extra_vars, since the playbook reads the DevOps server's registry details from Vault under that hostname. Then runs the Vault pre-check and application-deployer's playbook.yml with the same steps as "Vault Deployer". Its inventory spans several hosts and the playbook accepts more than one, so checking several hosts is a normal run.

Dashboard Deployer​

Runs one of dashboard's console scripts (currently dashboard-deployer, which uploads the stack over SSH and starts it with Docker Compose). dashboard reads its settings from the process environment, plus its own .env for any key the environment doesn't set, not Vault, so this action skips the Vault pre-check. Otherwise it follows the same steps as "Devops Server Onboarding": list the [project.scripts] commands in the repo's pyproject.toml (a missing file or empty table logs a warning and stops), pick one, confirm with Yes/Cancel, then run uv --offline run --no-sync --no-progress <command> in dashboard with the config's extra_vars, CS_PROJECT_CODE, and PATH as the process's environment.

Documentation​

Runs one of documentation's package.json scripts (for example start, build, diagram, or deploy:preview). documentation is a Docusaurus site published separately from the platform's services. It reads its settings from the process environment, not Vault, so this action skips the Vault pre-check.

  • It lists every entry under scripts in the repo's package.json. If the file is missing or the object is empty, it logs a warning and stops.
  • The user picks one script from a single-choice list and confirms with Yes/Cancel.
  • It runs pnpm run <script> through the config's pnpm_path in documentation, with the config's extra_vars, CS_PROJECT_CODE, and PATH as the process's environment.

"Install dependencies" installs the site's packages; this action does not.

Vault Secrets​

Reads, writes, lists, or deletes one Vault secret. After the Vault pre-check, it asks for a key as a bucket/key path, prefilled with the config's cs_project_code. The key must be non-blank and start with that code. It then asks which operation to run. Cancel or Escape on either prompt backs out before any operation runs.

read/write need a full bucket/key path. A bare bucket fails when the operation runs, like any other Vault error. list accepts a bare bucket and lists its root, and delete accepts one too (see below).

  • read/write: fetches the current value and opens it, pretty-printed, in the same large text area "Recreate config" uses for extra_vars. A missing or empty secret starts as {}. Cancel backs out without validating. Submit re-prompts until the text parses as a JSON object, then asks for a final Yes/Cancel before writing. The write replaces the whole previous value. Since this shows the current value first, there's no separate read operation.
  • list: logs the child keys directly under the path.
  • delete: asks for a final Yes/Cancel, then deletes the key's full history and, recursively, every child key under it. A bare bucket is accepted here and deletes the bucket's whole KV mount: every secret and all history in it. The key prompt is prefilled with the bare cs_project_code, so deleting without extending the path targets the project's entire bucket. This can't be undone.

Recreate config​

Re-runs the prompts create_config asks on first launch (project code, PyPI index, Ansible requirements source, extra vars, uv, pnpm and rsync paths, controller directory) and overwrites the saved config with the answers. These are the only prompts with no Cancel button and no Escape. On a first run there's no main menu to fall back to, so once started they can't be backed out of. Each still has a Submit button.

It then runs the Vault pre-check against the new config, so the footer's Vault warning reflects the new config right away.

Toggle session status​

Shows or hides the session-status bar under the header. The bar lists the config file path, project code, PyPI and Ansible sources, uv, pnpm and rsync paths, the Python interpreter, the controller directory, how many extra_vars keys are set (never their values), and runtime tool versions (uv, pnpm, rsync, Python). It starts hidden and keeps the last choice until toggled again.

Clear console​

Empties the log panel on the right.

Exit​

Quits iac-ctl. Ctrl+C does the same from anywhere, including an open prompt.

Documentation​

The platform's child repos, deployment order, and architecture diagram are in documentation/README.md. The documentation repo also holds the prerequisites for the platform, the OpenWrt router's configuration and rebuild checklist, and the documentation site.