diff --git a/scripts/codex-maintenance.sh b/scripts/codex-maintenance.sh new file mode 100755 index 0000000..a5cd11a --- /dev/null +++ b/scripts/codex-maintenance.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "== Refresh Python tools ==" +python3 -m pip install --break-system-packages --upgrade \ + pip \ + yamllint \ + ansible \ + ansible-lint + +echo "== Sanity check installed tools ==" +for cmd in docker terraform tflint ansible-lint yamllint yq jq shellcheck; do + if command -v "$cmd" >/dev/null 2>&1; then + echo "OK: $cmd -> $(command -v "$cmd")" + else + echo "MISSING: $cmd" + fi +done + +echo "== Reconcile dummy secret material ==" + +REPO_ROOT="${CODEX_REPO_DIR:-$PWD}" +SECRETS_DIR="$REPO_ROOT/secrets" +INVENTORY_JSON="$SECRETS_DIR/inventory.json" +EXAMPLE_ENV="$SECRETS_DIR/.env.secrets.example" +STACK_ENV="$SECRETS_DIR/stack-secrets.env" + +if [[ ! -f "$INVENTORY_JSON" ]]; then + echo "Missing inventory file: $INVENTORY_JSON" + exit 1 +fi + +if [[ ! -f "$EXAMPLE_ENV" ]]; then + echo "Missing example env file: $EXAMPLE_ENV" + exit 1 +fi + +mkdir -p "$SECRETS_DIR" + +dummy_value_for_key() { + local key="$1" + case "$key" in + *EMAIL* ) echo "dummy@example.com" ;; + *USER*|*USERNAME* ) echo "dummy-user" ;; + *DOMAIN* ) echo "example.lan.ddnsgeek.com" ;; + *TZ ) echo "Australia/Brisbane" ;; + *URL* ) echo "https://example.lan.ddnsgeek.com" ;; + *PORT* ) echo "1234" ;; + *PASSWORD*|*PASS*|*TOKEN*|*SECRET*|*KEY*|*JWT* ) echo "dummy-${key,,}" ;; + *FINGERPRINT* ) echo "0000000000000000000000000000000000000000" ;; + *DB_NAME* ) echo "dummydb" ;; + *DB_USER* ) echo "dummyuser" ;; + *NAME* ) echo "dummy-name" ;; + *ADDRESS* ) echo "dummy" ;; + * ) echo "dummy-value" ;; + esac +} + +rebuild_dummy_stack_env() { + local tmp + tmp="$(mktemp)" + + cp "$EXAMPLE_ENV" "$tmp" + + while IFS= read -r var; do + [[ -z "$var" ]] && continue + dummy="$(dummy_value_for_key "$var")" + + if grep -Eq "^[[:space:]]*${var}=" "$tmp"; then + sed -i "s|^[[:space:]]*${var}=.*|${var}=${dummy}|" "$tmp" + else + printf '%s=%s\n' "$var" "$dummy" >> "$tmp" + fi + done < <(jq -r '.env_template_variables[].variable' "$INVENTORY_JSON") + + mv "$tmp" "$STACK_ENV" + chmod 600 "$STACK_ENV" || true + echo "Updated $STACK_ENV" +} + +reconcile_file_based_secrets() { + local wanted existing relpath abspath + wanted="$(mktemp)" + existing="$(mktemp)" + + jq -r '.file_based_secrets[].path' "$INVENTORY_JSON" | sort -u > "$wanted" + + find "$SECRETS_DIR" -maxdepth 1 -type f -name '*.txt' -printf '%P\n' \ + | sed "s#^#secrets/#" \ + | sort -u > "$existing" + + # Create missing listed files + while IFS= read -r relpath; do + [[ -z "$relpath" ]] && continue + abspath="$REPO_ROOT/$relpath" + if [[ ! -f "$abspath" ]]; then + mkdir -p "$(dirname "$abspath")" + printf 'dummy-secret\n' > "$abspath" + chmod 600 "$abspath" || true + echo "Created $relpath" + fi + done < <(comm -23 "$wanted" "$existing") + + # Remove stale files no longer listed in inventory.json + while IFS= read -r relpath; do + [[ -z "$relpath" ]] && continue + abspath="$REPO_ROOT/$relpath" + if [[ -f "$abspath" ]]; then + rm -f "$abspath" + echo "Removed stale $relpath" + fi + done < <(comm -13 "$wanted" "$existing") + + rm -f "$wanted" "$existing" +} + +rebuild_dummy_stack_env +reconcile_file_based_secrets + +echo "== Dummy secret reconciliation complete ==" +echo "stack env: $STACK_ENV" +jq -r '.file_based_secrets[].path' "$INVENTORY_JSON" | sed 's/^/file secret: /' diff --git a/scripts/codex-setup.sh b/scripts/codex-setup.sh new file mode 100755 index 0000000..8c90f8a --- /dev/null +++ b/scripts/codex-setup.sh @@ -0,0 +1,174 @@ +#!/usr/bin/env bash +set -euo pipefail + +export DEBIAN_FRONTEND=noninteractive + +echo "== Base packages ==" +if command -v apt-get >/dev/null 2>&1; then + apt-get update + apt-get install -y \ + bash \ + ca-certificates \ + curl \ + git \ + jq \ + unzip \ + wget \ + python3 \ + python3-pip \ + python3-venv \ + shellcheck +else + echo "This script currently expects an apt-based environment." + exit 1 +fi + +echo "== yq ==" +if ! command -v yq >/dev/null 2>&1; then + YQ_VERSION="v4.44.3" + ARCH="$(dpkg --print-architecture)" + case "$ARCH" in + amd64) YQ_ARCH="amd64" ;; + arm64) YQ_ARCH="arm64" ;; + *) echo "Unsupported architecture: $ARCH"; exit 1 ;; + esac + wget -qO /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_${YQ_ARCH}" + chmod +x /usr/local/bin/yq +fi + +echo "== Docker CLI + Compose plugin ==" +if ! command -v docker >/dev/null 2>&1; then + apt-get install -y docker.io docker-compose-v2 || true +fi + +echo "== Python tooling ==" +python3 -m pip install --break-system-packages --upgrade pip +python3 -m pip install --break-system-packages \ + yamllint \ + ansible \ + ansible-lint + +echo "== Terraform ==" +if ! command -v terraform >/dev/null 2>&1; then + TF_VERSION="1.8.5" + ARCH="$(dpkg --print-architecture)" + case "$ARCH" in + amd64) TF_ARCH="amd64" ;; + arm64) TF_ARCH="arm64" ;; + *) echo "Unsupported architecture: $ARCH"; exit 1 ;; + esac + + wget -qO /tmp/terraform.zip \ + "https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_${TF_ARCH}.zip" + unzip -o /tmp/terraform.zip -d /tmp + install /tmp/terraform /usr/local/bin/terraform +fi + +echo "== TFLint ==" +if ! command -v tflint >/dev/null 2>&1; then + TFLINT_VERSION="v0.56.0" + ARCH="$(dpkg --print-architecture)" + case "$ARCH" in + amd64) TFLINT_ARCH="amd64" ;; + arm64) TFLINT_ARCH="arm64" ;; + *) echo "Unsupported architecture: $ARCH"; exit 1 ;; + esac + + wget -qO /tmp/tflint.zip \ + "https://github.com/terraform-linters/tflint/releases/download/${TFLINT_VERSION}/tflint_linux_${TFLINT_ARCH}.zip" + unzip -o /tmp/tflint.zip -d /tmp + install /tmp/tflint /usr/local/bin/tflint +fi + +echo "== Dummy secret material for compose validation ==" + +REPO_ROOT="${CODEX_REPO_DIR:-$PWD}" +SECRETS_DIR="$REPO_ROOT/secrets" +INVENTORY_JSON="$SECRETS_DIR/inventory.json" +EXAMPLE_ENV="$SECRETS_DIR/.env.secrets.example" +STACK_ENV="$SECRETS_DIR/stack-secrets.env" + +if [[ ! -f "$INVENTORY_JSON" ]]; then + echo "Missing inventory file: $INVENTORY_JSON" + exit 1 +fi + +if [[ ! -f "$EXAMPLE_ENV" ]]; then + echo "Missing example env file: $EXAMPLE_ENV" + exit 1 +fi + +mkdir -p "$SECRETS_DIR" + +dummy_value_for_key() { + local key="$1" + case "$key" in + *EMAIL* ) echo "dummy@example.com" ;; + *USER*|*USERNAME* ) echo "dummy-user" ;; + *DOMAIN* ) echo "example.lan.ddnsgeek.com" ;; + *TZ ) echo "Australia/Brisbane" ;; + *URL* ) echo "https://example.lan.ddnsgeek.com" ;; + *PORT* ) echo "1234" ;; + *PASSWORD*|*PASS*|*TOKEN*|*SECRET*|*KEY*|*JWT* ) echo "dummy-${key,,}" ;; + *FINGERPRINT* ) echo "0000000000000000000000000000000000000000" ;; + *DB_NAME* ) echo "dummydb" ;; + *DB_USER* ) echo "dummyuser" ;; + *NAME* ) echo "dummy-name" ;; + *ADDRESS* ) echo "dummy" ;; + * ) echo "dummy-value" ;; + esac +} + +render_dummy_stack_env() { + cp "$EXAMPLE_ENV" "$STACK_ENV.tmp" + + while IFS= read -r var; do + [[ -z "$var" ]] && continue + dummy="$(dummy_value_for_key "$var")" + + if grep -Eq "^[[:space:]]*${var}=" "$STACK_ENV.tmp"; then + sed -i "s|^[[:space:]]*${var}=.*|${var}=${dummy}|" "$STACK_ENV.tmp" + else + printf '%s=%s\n' "$var" "$dummy" >> "$STACK_ENV.tmp" + fi + done < <(jq -r '.env_template_variables[].variable' "$INVENTORY_JSON") + + mv "$STACK_ENV.tmp" "$STACK_ENV" + chmod 600 "$STACK_ENV" || true +} + +ensure_dummy_secret_files() { + jq -r '.file_based_secrets[].path' "$INVENTORY_JSON" | while IFS= read -r relpath; do + [[ -z "$relpath" ]] && continue + abspath="$REPO_ROOT/$relpath" + mkdir -p "$(dirname "$abspath")" + if [[ ! -f "$abspath" ]]; then + printf 'dummy-secret\n' > "$abspath" + fi + chmod 600 "$abspath" || true + done +} + +render_dummy_stack_env +ensure_dummy_secret_files + +echo +echo "== Installed versions ==" +bash --version | head -n 1 || true +git --version || true +docker --version || true +docker compose version || true +python3 --version || true +ansible --version | head -n 1 || true +ansible-lint --version || true +terraform version | head -n 1 || true +tflint --version || true +shellcheck --version | head -n 1 || true +yamllint --version || true +yq --version || true +jq --version || true + +echo +echo "== Dummy secret files prepared ==" +echo "$STACK_ENV" +jq -r '.file_based_secrets[].path' "$INVENTORY_JSON" || true diff --git a/update-containers.log b/update-containers.log index 438066b..4b93d3b 100644 --- a/update-containers.log +++ b/update-containers.log @@ -1,13 +1,13 @@ -22:10:52 INFO: === Update started: 2026-04-20 22:10:52 === -22:10:52 WARNING: Skipping traefik (directory does not exist) -22:10:52 WARNING: Skipping nextcloud (directory does not exist) -22:10:52 WARNING: Skipping passbolt (directory does not exist) -22:10:52 WARNING: Skipping searxng (directory does not exist) -22:10:52 WARNING: Skipping gitea (directory does not exist) -22:10:52 WARNING: Skipping gotify (directory does not exist) -22:10:52 WARNING: Skipping grafana (directory does not exist) -22:10:52 WARNING: Skipping gramps (directory does not exist) -22:10:52 WARNING: Skipping portainer (directory does not exist) -22:10:52 WARNING: Skipping prometheus (directory does not exist) -22:10:52 WARNING: Skipping uptime-kuma (directory does not exist) -22:10:52 INFO: Pruning unused containers, images, networks, and volumes... +10:17:35 INFO: === Update started: 2026-04-21 10:17:35 === +10:17:35 WARNING: Skipping traefik (directory does not exist) +10:17:35 WARNING: Skipping nextcloud (directory does not exist) +10:17:35 WARNING: Skipping passbolt (directory does not exist) +10:17:35 WARNING: Skipping searxng (directory does not exist) +10:17:35 WARNING: Skipping gitea (directory does not exist) +10:17:35 WARNING: Skipping gotify (directory does not exist) +10:17:35 WARNING: Skipping grafana (directory does not exist) +10:17:35 WARNING: Skipping gramps (directory does not exist) +10:17:35 WARNING: Skipping portainer (directory does not exist) +10:17:35 WARNING: Skipping prometheus (directory does not exist) +10:17:35 WARNING: Skipping uptime-kuma (directory does not exist) +10:17:35 INFO: Pruning unused containers, images, networks, and volumes...