From bde4d85a52c2e3f3b078c50c69c2b0f5569aa740 Mon Sep 17 00:00:00 2001 From: Bernd Worsch Date: Sat, 13 Sep 2025 23:34:27 +0200 Subject: [PATCH] chore: extended makefile with hooks target to set up pre-commit --- Makefile | 115 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 89 insertions(+), 26 deletions(-) diff --git a/Makefile b/Makefile index cf4b7de..2dad806 100644 --- a/Makefile +++ b/Makefile @@ -1,36 +1,99 @@ +# -------- RailianceHosts Make Utilities -------- SHELL := /usr/bin/env bash +.DEFAULT_GOAL := help -# Decrypt Hetzner token at runtime (requires your SOPS_AGE_KEY loaded locally) +# Set this to your Gitea host if you want 'remote-set' helper +GITEA ?= gitea.example.com +OWNER ?= coulomb +REPO ?= railiance-hosts + +# Decrypt Hetzner token at runtime (requires SOPS_AGE_KEY or keys.txt locally) HCLOUD_TOKEN := $(shell sops -d --extract '["ops"]["hcloud_token"]' inventory/group_vars/secrets.sops.yaml 2>/dev/null) -.PHONY: all apply tf-init tf-apply ansible destroy fmt check sops-edit sops-rotate +# ---- Help ---- +help: ## Show this help + @echo "RailianceHosts Commands"; \ grep -E '^[a-zA-Z0-9_-]+:.*?## ' $(MAKEFILE_LIST) | sort | sed 's/:.*##/: /' -all: apply +# ---- Git hooks ---- +hooks: ## Configure git to use repo-local hooks (.githooks) + git config core.hooksPath .githooks + @echo "✔ hooks enabled (core.hooksPath=.githooks)" -fmt: - terraform -chdir=terraform/hetzner fmt -recursive || true +hooks-test: ## Test secrets hook blocks plaintext in secrets/ + @mkdir -p secrets && echo 'PLAINTEXT_TEST=true' > secrets/_hook_test.yaml + @git add secrets/_hook_test.yaml || true + @if git commit -m "TEST: should be blocked" 2>/dev/null; then \ echo "❌ Hook did NOT block plaintext (check .githooks/pre-commit)"; \ git reset --soft HEAD~1; \ else \ echo "✔ Hook blocked plaintext as expected"; \ fi + @git restore --staged secrets/_hook_test.yaml || true + @rm -f secrets/_hook_test.yaml -tf-init: - terraform -chdir=terraform/hetzner init +# ---- SOPS / Age helpers ---- +sops-setup: ## Copy age key to SOPS default path (~/.config/sops/age/keys.txt) + mkdir -p ~/.config/sops/age + cp -n ~/.config/age/key.txt ~/.config/sops/age/keys.txt || true + chmod 600 ~/.config/sops/age/keys.txt + @echo "✔ SOPS key path set (~/.config/sops/age/keys.txt). Alternatively export SOPS_AGE_KEY." -tf-apply: tf-init - @if [ -z "$(HCLOUD_TOKEN)" ]; then echo "HCLOUD_TOKEN empty. Did you load your SOPS key and encrypt ops.hcloud_token?"; exit 1; fi - @export HCLOUD_TOKEN=$(HCLOUD_TOKEN); \ - terraform -chdir=terraform/hetzner apply -auto-approve - -ansible: - cd ansible && ansible-playbook playbooks/bootstrap.yaml -u admin - -apply: fmt tf-apply ansible - -destroy: - @if [ -z "$(HCLOUD_TOKEN)" ]; then echo "HCLOUD_TOKEN empty. Did you load your SOPS key?"; exit 1; fi - @export HCLOUD_TOKEN=$(HCLOUD_TOKEN); \ - terraform -chdir=terraform/hetzner destroy -auto-approve - -sops-edit: +sops-edit: ## Edit the global secrets with SOPS sops inventory/group_vars/secrets.sops.yaml -check: - terraform -chdir=terraform/hetzner plan >/dev/null || true - cd ansible && ansible-inventory --list >/dev/null +sops-encrypt: ## Encrypt a file in place: make sops-encrypt FILE=secrets/foo.yaml + @[ -n "$(FILE)" ] || (echo "Usage: make sops-encrypt FILE=secrets/xxx.yaml" && exit 1) + sops --encrypt --in-place $(FILE) + @echo "✔ Encrypted $(FILE)" + +sops-decrypt: ## Print decrypted file to stdout (for inspection) FILE=secrets/foo.sops.yaml + @[ -n "$(FILE)" ] || (echo "Usage: make sops-decrypt FILE=secrets/xxx.sops.yaml" && exit 1) + sops -d $(FILE) + +sops-rotate: ## Rotate recipients on a SOPS file (after updating .sops.yaml) + @[ -n "$(FILE)" ] || (echo "Usage: make sops-rotate FILE=secrets/xxx.sops.yaml" && exit 1) + sops --rotate --in-place $(FILE) + +check-secrets: ## Fail if any file in secrets/ is not SOPS-encrypted + @! (git ls-files secrets | xargs -r grep -L -E '(^sops:$$|\"sops\"[[:space:]]*:)' | tee /dev/stderr | read) \ || (echo "❌ Unencrypted secrets detected above. Encrypt with: sops --encrypt --in-place "; exit 1) + @echo "✔ All files in secrets/ appear SOPS-encrypted" + +# ---- Terraform (Hetzner) ---- +tf-fmt: ## Terraform fmt + @terraform -chdir=terraform/hetzner fmt -recursive || true + +tf-init: ## Terraform init + @terraform -chdir=terraform/hetzner init + +tf-plan: tf-init ## Terraform plan (requires decrypted HCLOUD_TOKEN) + @[ -n "$(HCLOUD_TOKEN)" ] || (echo "HCLOUD_TOKEN empty; export SOPS_AGE_KEY or set keys.txt & fill secrets.sops.yaml" && exit 1) + @export HCLOUD_TOKEN=$(HCLOUD_TOKEN); terraform -chdir=terraform/hetzner plan + +tf-apply: tf-init ## Terraform apply (provision) + @[ -n "$(HCLOUD_TOKEN)" ] || (echo "HCLOUD_TOKEN empty; export SOPS_AGE_KEY or set keys.txt & fill secrets.sops.yaml" && exit 1) + @export HCLOUD_TOKEN=$(HCLOUD_TOKEN); terraform -chdir=terraform/hetzner apply -auto-approve + +tf-destroy: tf-init ## Terraform destroy (tear down) + @[ -n "$(HCLOUD_TOKEN)" ] || (echo "HCLOUD_TOKEN empty; export SOPS_AGE_KEY or set keys.txt & fill secrets.sops.yaml" && exit 1) + @export HCLOUD_TOKEN=$(HCLOUD_TOKEN); terraform -chdir=terraform/hetzner destroy -auto-approve + +# ---- Ansible ---- +ansible-bootstrap: ## Run base bootstrap play (users, ssh, ufw, sops-agent) + cd ansible && ansible-playbook playbooks/bootstrap.yaml -u admin + +converge: ansible-bootstrap ## Alias for current bootstrap converge + @true + +# ---- Orchestration ---- +apply: tf-fmt tf-apply ansible-bootstrap ## Provision via Terraform then converge via Ansible + +# ---- Utilities ---- +doctor: ## Check tools and basic repo setup + @bash -ceu ' \ ok(){ printf "✔ %s\\n" "$$1"; }; fail(){ printf "❌ %s\\n" "$$1"; exit 1; }; \ command -v git >/dev/null && ok "git: $$(git --version)" || fail "git missing"; \ command -v terraform >/dev/null && ok "terraform: $$(terraform version | head -1)"; \ command -v ansible >/dev/null && ok "ansible: $$(ansible --version | head -1)"; \ command -v sops >/dev/null && ok "sops: $$(sops --version)"; \ command -v age >/dev/null && ok "age: $$(age --version)"; \ test -f keys/admin_ssh.pub && ok "keys/admin_ssh.pub present" || echo "ℹ add your SSH pubkey to keys/admin_ssh.pub"; \ test -f inventory/group_vars/secrets.sops.yaml && ok "secrets.sops.yaml present" || echo "ℹ create inventory/group_vars/secrets.sops.yaml"; \ grep -q "age1" .sops.yaml && ok ".sops.yaml has an age recipient" || echo "ℹ add your age public key to .sops.yaml"; \ git config --get core.hooksPath >/dev/null && ok "git hooksPath: $$(git config --get core.hooksPath)" || echo "ℹ run: make hooks"; \ ' + +new-host: ## Add a new host quickly: make new-host NAME=web-01 TYPE=cpx21 REGION=nbg1 ROLE=web + @[ -n "$(NAME)" ] || (echo "Usage: make new-host NAME=..." && exit 1) + @TYPE?=cpx21; REGION?=nbg1; ROLE?=generic; IMG?=ubuntu-24.04; USER?=admin; \ python3 - <<'PY' \import sys, yaml; \p="inventory/servers.yaml"; d=yaml.safe_load(open(p)) ; d['servers'].append({"name":"$(NAME)","labels":[],"role":"$(ROLE)","region":"$(REGION)","type":"$(TYPE)","image":"$(IMG)","ssh_user":"$(USER)"}); \yaml.safe_dump(d, open(p,"w"), sort_keys=False); \print("✔ Added host $(NAME) to inventory/servers.yaml") +PY + +remote-set: ## Set origin to your Gitea repo (GITEA/OWNER/REPO vars) + git remote remove origin 2>/dev/null || true + git remote add origin https://$(GITEA)/$(OWNER)/$(REPO).git + git branch -M main + git push -u origin main + @echo "✔ Remote set to https://$(GITEA)/$(OWNER)/$(REPO).git"