2026-04-17 05:15:58 +00:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# tools/vault-seed-woodpecker.sh — Idempotent seed for kv/disinto/shared/woodpecker
|
|
|
|
|
#
|
2026-04-17 05:36:15 +00:00
|
|
|
# Part of the Nomad+Vault migration (S3.1 + S3.3, issues #934 + #936). Populates
|
|
|
|
|
# the KV v2 path read by nomad/jobs/woodpecker-server.hcl:
|
|
|
|
|
# - agent_secret: pre-shared secret for woodpecker-server ↔ agent communication
|
|
|
|
|
# - forgejo_client + forgejo_secret: OAuth2 client credentials from Forgejo
|
2026-04-17 05:15:58 +00:00
|
|
|
#
|
2026-04-17 05:36:15 +00:00
|
|
|
# This script handles BOTH:
|
|
|
|
|
# 1. S3.1: seeds `agent_secret` if missing
|
|
|
|
|
# 2. S3.3: calls wp-oauth-register.sh to create Forgejo OAuth app + store
|
|
|
|
|
# forgejo_client/forgejo_secret in Vault
|
|
|
|
|
#
|
|
|
|
|
# Idempotency contract:
|
|
|
|
|
# - agent_secret: missing → generate and write; present → skip, log unchanged
|
|
|
|
|
# - OAuth app + credentials: handled by wp-oauth-register.sh (idempotent)
|
2026-04-17 05:15:58 +00:00
|
|
|
# This script preserves any existing keys it doesn't own.
|
|
|
|
|
#
|
|
|
|
|
# Idempotency contract (per key):
|
|
|
|
|
# - Key missing or empty in Vault → generate a random value, write it,
|
|
|
|
|
# log "agent_secret generated".
|
|
|
|
|
# - Key present with a non-empty value → leave untouched, log
|
|
|
|
|
# "agent_secret unchanged".
|
|
|
|
|
#
|
|
|
|
|
# Preconditions:
|
|
|
|
|
# - Vault reachable + unsealed at $VAULT_ADDR.
|
|
|
|
|
# - VAULT_TOKEN set (env) or /etc/vault.d/root.token readable.
|
|
|
|
|
# - The `kv/` mount is enabled as KV v2 (this script enables it on a
|
|
|
|
|
# fresh box; on an existing box it asserts the mount type/version).
|
|
|
|
|
#
|
|
|
|
|
# Requires:
|
|
|
|
|
# - VAULT_ADDR (e.g. http://127.0.0.1:8200)
|
|
|
|
|
# - VAULT_TOKEN (env OR /etc/vault.d/root.token, resolved by lib/hvault.sh)
|
|
|
|
|
# - curl, jq, openssl
|
|
|
|
|
#
|
|
|
|
|
# Usage:
|
|
|
|
|
# tools/vault-seed-woodpecker.sh
|
|
|
|
|
# tools/vault-seed-woodpecker.sh --dry-run
|
|
|
|
|
#
|
|
|
|
|
# Exit codes:
|
|
|
|
|
# 0 success (seed applied, or already applied)
|
|
|
|
|
# 1 precondition / API / mount-mismatch failure
|
|
|
|
|
# =============================================================================
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
2026-04-17 05:21:47 +00:00
|
|
|
SEED_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
|
REPO_ROOT="$(cd "${SEED_DIR}/.." && pwd)"
|
2026-04-17 05:36:15 +00:00
|
|
|
LIB_DIR="${REPO_ROOT}/lib/init/nomad"
|
2026-04-17 05:15:58 +00:00
|
|
|
# shellcheck source=../lib/hvault.sh
|
|
|
|
|
source "${REPO_ROOT}/lib/hvault.sh"
|
|
|
|
|
|
|
|
|
|
KV_MOUNT="kv"
|
|
|
|
|
KV_LOGICAL_PATH="disinto/shared/woodpecker"
|
|
|
|
|
KV_API_PATH="${KV_MOUNT}/data/${KV_LOGICAL_PATH}"
|
2026-04-17 05:21:47 +00:00
|
|
|
AGENT_SECRET_BYTES=32 # 32 bytes → 64 hex chars
|
2026-04-17 05:15:58 +00:00
|
|
|
|
2026-04-17 05:21:47 +00:00
|
|
|
LOG_TAG="[vault-seed-woodpecker]"
|
|
|
|
|
log() { printf '%s %s\n' "$LOG_TAG" "$*"; }
|
|
|
|
|
die() { printf '%s ERROR: %s\n' "$LOG_TAG" "$*" >&2; exit 1; }
|
2026-04-17 05:15:58 +00:00
|
|
|
|
|
|
|
|
# ── Flag parsing ─────────────────────────────────────────────────────────────
|
2026-04-17 05:21:47 +00:00
|
|
|
# for-over-"$@" loop — shape distinct from vault-seed-forgejo.sh (arity:value
|
|
|
|
|
# case) and vault-apply-roles.sh (if/elif).
|
2026-04-17 05:15:58 +00:00
|
|
|
DRY_RUN=0
|
|
|
|
|
for arg in "$@"; do
|
|
|
|
|
case "$arg" in
|
|
|
|
|
--dry-run) DRY_RUN=1 ;;
|
|
|
|
|
-h|--help)
|
|
|
|
|
printf 'Usage: %s [--dry-run]\n\n' "$(basename "$0")"
|
2026-04-17 05:36:15 +00:00
|
|
|
printf 'Seed kv/disinto/shared/woodpecker with secrets.\n\n'
|
|
|
|
|
printf 'Handles both S3.1 (agent_secret) and S3.3 (OAuth app + credentials):\n'
|
|
|
|
|
printf ' - agent_secret: generated if missing\n'
|
|
|
|
|
printf ' - forgejo_client/forgejo_secret: created via Forgejo API if missing\n\n'
|
|
|
|
|
printf ' --dry-run Print planned actions without writing.\n'
|
2026-04-17 05:15:58 +00:00
|
|
|
exit 0
|
|
|
|
|
;;
|
|
|
|
|
*) die "invalid argument: ${arg} (try --help)" ;;
|
|
|
|
|
esac
|
|
|
|
|
done
|
|
|
|
|
|
2026-04-17 05:21:47 +00:00
|
|
|
# ── Preconditions — binary + Vault connectivity checks ───────────────────────
|
|
|
|
|
required_bins=(curl jq openssl)
|
|
|
|
|
for bin in "${required_bins[@]}"; do
|
|
|
|
|
command -v "$bin" >/dev/null 2>&1 || die "required binary not found: ${bin}"
|
2026-04-17 05:15:58 +00:00
|
|
|
done
|
2026-04-17 05:21:47 +00:00
|
|
|
[ -n "${VAULT_ADDR:-}" ] || die "VAULT_ADDR unset — export VAULT_ADDR=http://127.0.0.1:8200"
|
|
|
|
|
hvault_token_lookup >/dev/null || die "Vault auth probe failed — check VAULT_ADDR + VAULT_TOKEN"
|
2026-04-17 05:15:58 +00:00
|
|
|
|
2026-04-17 05:36:15 +00:00
|
|
|
# ── Step 1/3: ensure kv/ mount exists and is KV v2 ───────────────────────────
|
|
|
|
|
log "── Step 1/3: ensure ${KV_MOUNT}/ is KV v2 ──"
|
2026-04-17 05:21:47 +00:00
|
|
|
export DRY_RUN
|
|
|
|
|
hvault_ensure_kv_v2 "$KV_MOUNT" "[vault-seed-woodpecker]" \
|
|
|
|
|
|| die "KV mount check failed"
|
2026-04-17 05:15:58 +00:00
|
|
|
|
2026-04-17 05:36:15 +00:00
|
|
|
# ── Step 2/3: seed agent_secret at kv/data/disinto/shared/woodpecker ─────────
|
|
|
|
|
log "── Step 2/3: seed agent_secret ──"
|
2026-04-17 05:15:58 +00:00
|
|
|
|
|
|
|
|
existing_raw="$(hvault_get_or_empty "${KV_API_PATH}")" \
|
|
|
|
|
|| die "failed to read ${KV_API_PATH}"
|
|
|
|
|
|
|
|
|
|
# Read all existing keys so we can preserve them on write (KV v2 replaces
|
|
|
|
|
# `.data` atomically). Missing path → empty object.
|
|
|
|
|
existing_data="{}"
|
|
|
|
|
existing_agent_secret=""
|
|
|
|
|
if [ -n "$existing_raw" ]; then
|
|
|
|
|
existing_data="$(printf '%s' "$existing_raw" | jq '.data.data // {}')"
|
|
|
|
|
existing_agent_secret="$(printf '%s' "$existing_raw" | jq -r '.data.data.agent_secret // ""')"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ -n "$existing_agent_secret" ]; then
|
|
|
|
|
log "agent_secret unchanged"
|
2026-04-17 05:36:15 +00:00
|
|
|
else
|
|
|
|
|
# agent_secret is missing — generate it.
|
|
|
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
|
|
|
log "[dry-run] would generate + write: agent_secret"
|
|
|
|
|
else
|
|
|
|
|
new_agent_secret="$(openssl rand -hex "$AGENT_SECRET_BYTES")"
|
2026-04-17 05:15:58 +00:00
|
|
|
|
2026-04-17 05:36:15 +00:00
|
|
|
# Merge the new key into existing data to preserve any keys written by
|
|
|
|
|
# other seeders (e.g. S3.3's forgejo_client/forgejo_secret).
|
|
|
|
|
payload="$(printf '%s' "$existing_data" \
|
|
|
|
|
| jq --arg as "$new_agent_secret" '{data: (. + {agent_secret: $as})}')"
|
2026-04-17 05:15:58 +00:00
|
|
|
|
2026-04-17 05:36:15 +00:00
|
|
|
_hvault_request POST "${KV_API_PATH}" "$payload" >/dev/null \
|
|
|
|
|
|| die "failed to write ${KV_API_PATH}"
|
2026-04-17 05:15:58 +00:00
|
|
|
|
2026-04-17 05:36:15 +00:00
|
|
|
log "agent_secret generated"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── Step 3/3: register Forgejo OAuth app and store credentials ───────────────
|
|
|
|
|
log "── Step 3/3: register Forgejo OAuth app ──"
|
2026-04-17 05:15:58 +00:00
|
|
|
|
2026-04-17 05:54:30 +00:00
|
|
|
# Export DRY_RUN for the OAuth script and call it
|
|
|
|
|
export DRY_RUN
|
|
|
|
|
if "${LIB_DIR}/wp-oauth-register.sh" || [ "$DRY_RUN" -eq 1 ]; then
|
|
|
|
|
:
|
|
|
|
|
elif [ -n "${FORGE_URL:-}" ]; then
|
|
|
|
|
# Forgejo was configured but unavailable
|
|
|
|
|
log "OAuth registration check failed (Forgejo may not be running)"
|
|
|
|
|
log "This is expected if Forgejo is not available"
|
2026-04-17 05:36:15 +00:00
|
|
|
fi
|
2026-04-17 05:15:58 +00:00
|
|
|
|
2026-04-17 05:36:15 +00:00
|
|
|
log "done — agent_secret + OAuth credentials seeded"
|