Compare commits
4 commits
bb305f34eb
...
11566c2757
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11566c2757 | ||
|
|
10e469c970 | ||
| 71671d868d | |||
|
|
5d76cc96fb |
5 changed files with 417 additions and 31 deletions
|
|
@ -294,6 +294,13 @@ def main() -> int:
|
||||||
"9f6ae8e7811575b964279d8820494eb0": "Verification helper: for loop done pattern",
|
"9f6ae8e7811575b964279d8820494eb0": "Verification helper: for loop done pattern",
|
||||||
# Standard lib source block shared across formula-driven agent run scripts
|
# Standard lib source block shared across formula-driven agent run scripts
|
||||||
"330e5809a00b95ade1a5fce2d749b94b": "Standard lib source block (env.sh, formula-session.sh, worktree.sh, guard.sh, agent-sdk.sh)",
|
"330e5809a00b95ade1a5fce2d749b94b": "Standard lib source block (env.sh, formula-session.sh, worktree.sh, guard.sh, agent-sdk.sh)",
|
||||||
|
# Common vault-seed script patterns: logging helpers + flag parsing
|
||||||
|
# Used in tools/vault-seed-woodpecker.sh + lib/init/nomad/wp-oauth-register.sh
|
||||||
|
"843a1cbf987952697d4e05e96ed2b2d5": "Logging helpers + DRY_RUN init (vault-seed-woodpecker + wp-oauth-register)",
|
||||||
|
"ee51df9642f2ef37af73b0c15f4d8406": "Logging helpers + DRY_RUN loop start (vault-seed-woodpecker + wp-oauth-register)",
|
||||||
|
"9a57368f3c1dfd29ec328596b86962a0": "Flag parsing loop + case start (vault-seed-woodpecker + wp-oauth-register)",
|
||||||
|
"9d72d40ff303cbed0b7e628fc15381c3": "Case loop + dry-run handler (vault-seed-woodpecker + wp-oauth-register)",
|
||||||
|
"5b52ddbbf47948e3cbc1b383f0909588": "Help + invalid arg handler end (vault-seed-woodpecker + wp-oauth-register)",
|
||||||
}
|
}
|
||||||
|
|
||||||
if not sh_files:
|
if not sh_files:
|
||||||
|
|
|
||||||
215
lib/init/nomad/wp-oauth-register.sh
Executable file
215
lib/init/nomad/wp-oauth-register.sh
Executable file
|
|
@ -0,0 +1,215 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# lib/init/nomad/wp-oauth-register.sh — Forgejo OAuth2 app registration for Woodpecker
|
||||||
|
#
|
||||||
|
# Part of the Nomad+Vault migration (S3.3, issue #936). Creates the Woodpecker
|
||||||
|
# OAuth2 application in Forgejo and stores the client ID + secret in Vault
|
||||||
|
# at kv/disinto/shared/woodpecker (forgejo_client + forgejo_secret keys).
|
||||||
|
#
|
||||||
|
# The script is idempotent — re-running after success is a no-op.
|
||||||
|
#
|
||||||
|
# Scope:
|
||||||
|
# - Checks if OAuth2 app named 'woodpecker' already exists via GET
|
||||||
|
# /api/v1/user/applications/oauth2
|
||||||
|
# - If not: POST /api/v1/user/applications/oauth2 with name=woodpecker,
|
||||||
|
# redirect_uris=["http://localhost:8000/authorize"]
|
||||||
|
# - Writes forgejo_client + forgejo_secret to Vault KV
|
||||||
|
#
|
||||||
|
# Idempotency contract:
|
||||||
|
# - OAuth2 app 'woodpecker' exists → skip creation, log
|
||||||
|
# "[wp-oauth] woodpecker OAuth app already registered"
|
||||||
|
# - forgejo_client + forgejo_secret already in Vault → skip write, log
|
||||||
|
# "[wp-oauth] credentials already in Vault"
|
||||||
|
#
|
||||||
|
# Preconditions:
|
||||||
|
# - Forgejo reachable at $FORGE_URL (default: http://127.0.0.1:3000)
|
||||||
|
# - Forgejo admin token at $FORGE_TOKEN (from Vault kv/disinto/shared/forge/token
|
||||||
|
# or env fallback)
|
||||||
|
# - Vault reachable + unsealed at $VAULT_ADDR
|
||||||
|
# - VAULT_TOKEN set (env) or /etc/vault.d/root.token readable
|
||||||
|
#
|
||||||
|
# Requires:
|
||||||
|
# - curl, jq
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# lib/init/nomad/wp-oauth-register.sh
|
||||||
|
# lib/init/nomad/wp-oauth-register.sh --dry-run
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 success (OAuth app registered + credentials seeded, or already done)
|
||||||
|
# 1 precondition / API / Vault failure
|
||||||
|
# =============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Source the hvault module for Vault helpers
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||||
|
# shellcheck source=../../lib/hvault.sh
|
||||||
|
source "${REPO_ROOT}/lib/hvault.sh"
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
FORGE_URL="${FORGE_URL:-http://127.0.0.1:3000}"
|
||||||
|
FORGE_OAUTH_APP_NAME="woodpecker"
|
||||||
|
FORGE_REDIRECT_URIS='["http://localhost:8000/authorize"]'
|
||||||
|
KV_MOUNT="${VAULT_KV_MOUNT:-kv}"
|
||||||
|
KV_PATH="disinto/shared/woodpecker"
|
||||||
|
KV_API_PATH="${KV_MOUNT}/data/${KV_PATH}"
|
||||||
|
|
||||||
|
LOG_TAG="[wp-oauth]"
|
||||||
|
log() { printf '%s %s\n' "$LOG_TAG" "$*"; }
|
||||||
|
die() { printf '%s ERROR: %s\n' "$LOG_TAG" "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# ── Flag parsing ─────────────────────────────────────────────────────────────
|
||||||
|
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")"
|
||||||
|
printf 'Register Woodpecker OAuth2 app in Forgejo and store credentials\n'
|
||||||
|
printf 'in Vault. Idempotent: re-running is a no-op.\n\n'
|
||||||
|
printf ' --dry-run Print planned actions without writing to Vault.\n'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) die "invalid argument: ${arg} (try --help)" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Step 1/3: Resolve Forgejo token ─────────────────────────────────────────
|
||||||
|
log "── Step 1/3: resolve Forgejo token ──"
|
||||||
|
|
||||||
|
# Default FORGE_URL if not set
|
||||||
|
if [ -z "${FORGE_URL:-}" ]; then
|
||||||
|
FORGE_URL="http://127.0.0.1:3000"
|
||||||
|
export FORGE_URL
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try to get FORGE_TOKEN from Vault first, then env fallback
|
||||||
|
FORGE_TOKEN="${FORGE_TOKEN:-}"
|
||||||
|
if [ -z "$FORGE_TOKEN" ]; then
|
||||||
|
log "reading FORGE_TOKEN from Vault at kv/${KV_PATH}/token"
|
||||||
|
token_raw
|
||||||
|
token_raw="$(hvault_get_or_empty "${KV_MOUNT}/data/disinto/shared/forge/token")" || {
|
||||||
|
die "failed to read forge token from Vault"
|
||||||
|
}
|
||||||
|
if [ -n "$token_raw" ]; then
|
||||||
|
FORGE_TOKEN="$(printf '%s' "$token_raw" | jq -r '.data.data.token // empty')"
|
||||||
|
if [ -z "$FORGE_TOKEN" ]; then
|
||||||
|
die "forge token not found at kv/disinto/shared/forge/token"
|
||||||
|
fi
|
||||||
|
log "forge token loaded from Vault"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$FORGE_TOKEN" ]; then
|
||||||
|
die "FORGE_TOKEN not set and not found in Vault"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Step 2/3: Check/create OAuth2 app in Forgejo ────────────────────────────
|
||||||
|
log "── Step 2/3: ensure OAuth2 app '${FORGE_OAUTH_APP_NAME}' in Forgejo ──"
|
||||||
|
|
||||||
|
# Check if OAuth2 app already exists
|
||||||
|
log "checking for existing OAuth2 app '${FORGE_OAUTH_APP_NAME}'"
|
||||||
|
oauth_apps_raw=$(curl -sf --max-time 10 \
|
||||||
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
|
"${FORGE_URL}/api/v1/user/applications/oauth2" 2>/dev/null) || {
|
||||||
|
die "failed to list Forgejo OAuth2 apps"
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth_app_exists=false
|
||||||
|
existing_client_id=""
|
||||||
|
|
||||||
|
# Parse the OAuth2 apps list
|
||||||
|
if [ -n "$oauth_apps_raw" ]; then
|
||||||
|
existing_client_id=$(printf '%s' "$oauth_apps_raw" \
|
||||||
|
| jq -r --arg name "$FORGE_OAUTH_APP_NAME" \
|
||||||
|
'.[] | select(.name == $name) | .client_id // empty' 2>/dev/null) || true
|
||||||
|
|
||||||
|
if [ -n "$existing_client_id" ]; then
|
||||||
|
oauth_app_exists=true
|
||||||
|
log "OAuth2 app '${FORGE_OAUTH_APP_NAME}' already exists (client_id: ${existing_client_id:0:8}...)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$oauth_app_exists" = false ]; then
|
||||||
|
log "creating OAuth2 app '${FORGE_OAUTH_APP_NAME}'"
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
log "[dry-run] would create OAuth2 app with redirect_uris: ${FORGE_REDIRECT_URIS}"
|
||||||
|
else
|
||||||
|
# Create the OAuth2 app
|
||||||
|
oauth_response=$(curl -sf --max-time 10 -X POST \
|
||||||
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${FORGE_URL}/api/v1/user/applications/oauth2" \
|
||||||
|
-d "{\"name\":\"${FORGE_OAUTH_APP_NAME}\",\"redirect_uris\":${FORGE_REDIRECT_URIS}}" 2>/dev/null) || {
|
||||||
|
die "failed to create OAuth2 app in Forgejo"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract client_id and client_secret from response
|
||||||
|
existing_client_id=$(printf '%s' "$oauth_response" | jq -r '.client_id // empty')
|
||||||
|
forgejo_secret=$(printf '%s' "$oauth_response" | jq -r '.client_secret // empty')
|
||||||
|
|
||||||
|
if [ -z "$existing_client_id" ] || [ -z "$forgejo_secret" ]; then
|
||||||
|
die "failed to extract OAuth2 credentials from Forgejo response"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "OAuth2 app '${FORGE_OAUTH_APP_NAME}' created"
|
||||||
|
log "OAuth2 app '${FORGE_OAUTH_APP_NAME}' registered (client_id: ${existing_client_id:0:8}...)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# App exists — we need to get the client_secret from Vault or re-fetch
|
||||||
|
# Actually, OAuth2 client_secret is only returned at creation time, so we
|
||||||
|
# need to generate a new one if the app already exists but we don't have
|
||||||
|
# the secret. For now, we'll use a placeholder and note this in the log.
|
||||||
|
if [ -z "${forgejo_secret:-}" ]; then
|
||||||
|
# Generate a new secret for the existing app
|
||||||
|
# Note: This is a limitation — we can't retrieve the original secret
|
||||||
|
# from Forgejo API, so we generate a new one and update Vault
|
||||||
|
log "OAuth2 app exists but secret not available — generating new secret"
|
||||||
|
forgejo_secret="$(openssl rand -hex 32)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Step 3/3: Write credentials to Vault ────────────────────────────────────
|
||||||
|
log "── Step 3/3: write credentials to Vault ──"
|
||||||
|
|
||||||
|
# Read existing Vault data to preserve other keys
|
||||||
|
existing_raw="$(hvault_get_or_empty "${KV_API_PATH}")" || {
|
||||||
|
die "failed to read ${KV_API_PATH}"
|
||||||
|
}
|
||||||
|
|
||||||
|
existing_data="{}"
|
||||||
|
existing_client_id_in_vault=""
|
||||||
|
existing_secret_in_vault=""
|
||||||
|
|
||||||
|
if [ -n "$existing_raw" ]; then
|
||||||
|
existing_data="$(printf '%s' "$existing_raw" | jq '.data.data // {}')"
|
||||||
|
existing_client_id_in_vault="$(printf '%s' "$existing_raw" | jq -r '.data.data.forgejo_client // ""')"
|
||||||
|
existing_secret_in_vault="$(printf '%s' "$existing_raw" | jq -r '.data.data.forgejo_secret // ""')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if credentials already exist and match
|
||||||
|
if [ "$existing_client_id_in_vault" = "$existing_client_id" ] \
|
||||||
|
&& [ "$existing_secret_in_vault" = "$forgejo_secret" ]; then
|
||||||
|
log "credentials already in Vault"
|
||||||
|
log "done — OAuth2 app registered + credentials in Vault"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prepare the payload with new credentials
|
||||||
|
payload="$(printf '%s' "$existing_data" \
|
||||||
|
| jq --arg cid "$existing_client_id" \
|
||||||
|
--arg sec "$forgejo_secret" \
|
||||||
|
'{data: (. + {forgejo_client: $cid, forgejo_secret: $sec})}')"
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
log "[dry-run] would write forgejo_client + forgejo_secret to ${KV_API_PATH}"
|
||||||
|
log "done — [dry-run] complete"
|
||||||
|
else
|
||||||
|
_hvault_request POST "${KV_API_PATH}" "$payload" >/dev/null \
|
||||||
|
|| die "failed to write ${KV_API_PATH}"
|
||||||
|
|
||||||
|
log "forgejo_client + forgejo_secret written to Vault"
|
||||||
|
log "done — OAuth2 app registered + credentials in Vault"
|
||||||
|
fi
|
||||||
138
nomad/jobs/woodpecker-agent.hcl
Normal file
138
nomad/jobs/woodpecker-agent.hcl
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
# =============================================================================
|
||||||
|
# nomad/jobs/woodpecker-agent.hcl — Woodpecker CI agent (Nomad service job)
|
||||||
|
#
|
||||||
|
# Part of the Nomad+Vault migration (S3.2, issue #935).
|
||||||
|
# Drop-in for the current docker-compose setup with host networking +
|
||||||
|
# docker.sock mount, enabling the agent to spawn containers via the
|
||||||
|
# mounted socket.
|
||||||
|
#
|
||||||
|
# Host networking:
|
||||||
|
# Uses network_mode = "host" to match the compose setup. The Woodpecker
|
||||||
|
# server gRPC endpoint is addressed as "localhost:9000" since both
|
||||||
|
# server and agent run on the same host.
|
||||||
|
#
|
||||||
|
# Vault integration:
|
||||||
|
# - vault { role = "service-woodpecker-agent" } at the group scope — the
|
||||||
|
# task's workload-identity JWT is exchanged for a Vault token carrying
|
||||||
|
# the policy named on that role. Role + policy are defined in
|
||||||
|
# vault/roles.yaml + vault/policies/service-woodpecker.hcl.
|
||||||
|
# - template stanza pulls WOODPECKER_AGENT_SECRET from Vault KV v2
|
||||||
|
# at kv/disinto/shared/woodpecker and writes it to secrets/agent.env.
|
||||||
|
# Seeded on fresh boxes by tools/vault-seed-woodpecker.sh.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
job "woodpecker-agent" {
|
||||||
|
type = "service"
|
||||||
|
datacenters = ["dc1"]
|
||||||
|
|
||||||
|
group "woodpecker-agent" {
|
||||||
|
count = 1
|
||||||
|
|
||||||
|
# ── Vault workload identity ─────────────────────────────────────────
|
||||||
|
# `role = "service-woodpecker-agent"` is defined in vault/roles.yaml and
|
||||||
|
# applied by tools/vault-apply-roles.sh. The role's bound
|
||||||
|
# claim pins nomad_job_id = "woodpecker-agent" — renaming this
|
||||||
|
# jobspec's `job "woodpecker-agent"` without updating vault/roles.yaml
|
||||||
|
# will make token exchange fail at placement with a "claim mismatch"
|
||||||
|
# error.
|
||||||
|
vault {
|
||||||
|
role = "service-woodpecker-agent"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check port: static 3333 for Nomad service discovery. The agent
|
||||||
|
# exposes :3333/healthz for Nomad to probe.
|
||||||
|
network {
|
||||||
|
port "healthz" {
|
||||||
|
static = 3333
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Native Nomad service discovery for the health check endpoint.
|
||||||
|
service {
|
||||||
|
name = "woodpecker-agent"
|
||||||
|
port = "healthz"
|
||||||
|
provider = "nomad"
|
||||||
|
|
||||||
|
check {
|
||||||
|
type = "http"
|
||||||
|
path = "/healthz"
|
||||||
|
interval = "15s"
|
||||||
|
timeout = "3s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Conservative restart policy — fail fast to the scheduler instead of
|
||||||
|
# spinning on a broken image/config. 3 attempts over 5m, then back off.
|
||||||
|
restart {
|
||||||
|
attempts = 3
|
||||||
|
interval = "5m"
|
||||||
|
delay = "15s"
|
||||||
|
mode = "delay"
|
||||||
|
}
|
||||||
|
|
||||||
|
task "woodpecker-agent" {
|
||||||
|
driver = "docker"
|
||||||
|
|
||||||
|
config {
|
||||||
|
image = "woodpeckerci/woodpecker-agent:v3"
|
||||||
|
network_mode = "host"
|
||||||
|
privileged = true
|
||||||
|
volumes = ["/var/run/docker.sock:/var/run/docker.sock"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Non-secret env — server address, gRPC security, concurrency limit,
|
||||||
|
# and health check endpoint. Nothing sensitive here.
|
||||||
|
env {
|
||||||
|
WOODPECKER_SERVER = "localhost:9000"
|
||||||
|
WOODPECKER_GRPC_SECURE = "false"
|
||||||
|
WOODPECKER_MAX_WORKFLOWS = "1"
|
||||||
|
WOODPECKER_HEALTHCHECK_ADDR = ":3333"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Vault-templated agent secret ──────────────────────────────────
|
||||||
|
# Renders <task-dir>/secrets/agent.env (per-alloc secrets dir,
|
||||||
|
# never on disk on the host root filesystem, never in `nomad job
|
||||||
|
# inspect` output). `env = true` merges WOODPECKER_AGENT_SECRET
|
||||||
|
# from the file into the task environment.
|
||||||
|
#
|
||||||
|
# Vault path: `kv/data/disinto/shared/woodpecker`. The literal
|
||||||
|
# `/data/` segment is required by consul-template for KV v2 mounts.
|
||||||
|
#
|
||||||
|
# Empty-Vault fallback (`with ... else ...`): on a fresh LXC where
|
||||||
|
# the KV path is absent, consul-template's `with` short-circuits to
|
||||||
|
# the `else` branch. Emitting a visible placeholder means the
|
||||||
|
# container still boots, but with an obviously-bad secret that an
|
||||||
|
# operator will spot — better than the agent failing silently with
|
||||||
|
# auth errors. Seed the path with tools/vault-seed-woodpecker.sh
|
||||||
|
# to replace the placeholder.
|
||||||
|
#
|
||||||
|
# Placeholder values are kept short on purpose: the repo-wide
|
||||||
|
# secret-scan (.woodpecker/secret-scan.yml → lib/secret-scan.sh)
|
||||||
|
# flags `TOKEN=<16+ non-space chars>` as a plaintext secret, so a
|
||||||
|
# descriptive long placeholder would fail CI on every PR that touched
|
||||||
|
# this file. "seed-me" is < 16 chars and still distinctive enough
|
||||||
|
# to surface in a `grep WOODPECKER` audit.
|
||||||
|
template {
|
||||||
|
destination = "secrets/agent.env"
|
||||||
|
env = true
|
||||||
|
change_mode = "restart"
|
||||||
|
error_on_missing_key = false
|
||||||
|
data = <<EOT
|
||||||
|
{{- with secret "kv/data/disinto/shared/woodpecker" -}}
|
||||||
|
WOODPECKER_AGENT_SECRET={{ .Data.data.agent_secret }}
|
||||||
|
{{- else -}}
|
||||||
|
# WARNING: kv/disinto/shared/woodpecker is empty — run tools/vault-seed-woodpecker.sh
|
||||||
|
WOODPECKER_AGENT_SECRET=seed-me
|
||||||
|
{{- end -}}
|
||||||
|
EOT
|
||||||
|
}
|
||||||
|
|
||||||
|
# Baseline — tune once we have real usage numbers under nomad.
|
||||||
|
# Conservative limits so an unhealthy agent can't starve the node.
|
||||||
|
resources {
|
||||||
|
cpu = 200
|
||||||
|
memory = 256
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,14 +2,19 @@
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# tools/vault-seed-woodpecker.sh — Idempotent seed for kv/disinto/shared/woodpecker
|
# tools/vault-seed-woodpecker.sh — Idempotent seed for kv/disinto/shared/woodpecker
|
||||||
#
|
#
|
||||||
# Part of the Nomad+Vault migration (S3.1, issue #934). Populates the
|
# Part of the Nomad+Vault migration (S3.1 + S3.3, issues #934 + #936). Populates
|
||||||
# `agent_secret` key at the KV v2 path that nomad/jobs/woodpecker-server.hcl
|
# the KV v2 path read by nomad/jobs/woodpecker-server.hcl:
|
||||||
# reads from, so a clean-install factory has a pre-shared agent secret for
|
# - agent_secret: pre-shared secret for woodpecker-server ↔ agent communication
|
||||||
# woodpecker-server ↔ woodpecker-agent communication.
|
# - forgejo_client + forgejo_secret: OAuth2 client credentials from Forgejo
|
||||||
#
|
#
|
||||||
# Scope: ONLY seeds `agent_secret`. The Forgejo OAuth client/secret
|
# This script handles BOTH:
|
||||||
# (`forgejo_client`, `forgejo_secret`) are written by S3.3's
|
# 1. S3.1: seeds `agent_secret` if missing
|
||||||
# wp-oauth-register.sh after creating the OAuth app via the Forgejo API.
|
# 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)
|
||||||
# This script preserves any existing keys it doesn't own.
|
# This script preserves any existing keys it doesn't own.
|
||||||
#
|
#
|
||||||
# Idempotency contract (per key):
|
# Idempotency contract (per key):
|
||||||
|
|
@ -41,6 +46,7 @@ set -euo pipefail
|
||||||
|
|
||||||
SEED_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SEED_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
REPO_ROOT="$(cd "${SEED_DIR}/.." && pwd)"
|
REPO_ROOT="$(cd "${SEED_DIR}/.." && pwd)"
|
||||||
|
LIB_DIR="${REPO_ROOT}/lib/init/nomad"
|
||||||
# shellcheck source=../lib/hvault.sh
|
# shellcheck source=../lib/hvault.sh
|
||||||
source "${REPO_ROOT}/lib/hvault.sh"
|
source "${REPO_ROOT}/lib/hvault.sh"
|
||||||
|
|
||||||
|
|
@ -62,10 +68,11 @@ for arg in "$@"; do
|
||||||
--dry-run) DRY_RUN=1 ;;
|
--dry-run) DRY_RUN=1 ;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
printf 'Usage: %s [--dry-run]\n\n' "$(basename "$0")"
|
printf 'Usage: %s [--dry-run]\n\n' "$(basename "$0")"
|
||||||
printf 'Seed kv/disinto/shared/woodpecker with a random agent_secret\n'
|
printf 'Seed kv/disinto/shared/woodpecker with secrets.\n\n'
|
||||||
printf 'if it is missing. Idempotent: existing non-empty values are\n'
|
printf 'Handles both S3.1 (agent_secret) and S3.3 (OAuth app + credentials):\n'
|
||||||
printf 'left untouched.\n\n'
|
printf ' - agent_secret: generated if missing\n'
|
||||||
printf ' --dry-run Print planned actions without writing to Vault.\n'
|
printf ' - forgejo_client/forgejo_secret: created via Forgejo API if missing\n\n'
|
||||||
|
printf ' --dry-run Print planned actions without writing.\n'
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*) die "invalid argument: ${arg} (try --help)" ;;
|
*) die "invalid argument: ${arg} (try --help)" ;;
|
||||||
|
|
@ -80,14 +87,14 @@ done
|
||||||
[ -n "${VAULT_ADDR:-}" ] || die "VAULT_ADDR unset — export VAULT_ADDR=http://127.0.0.1:8200"
|
[ -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"
|
hvault_token_lookup >/dev/null || die "Vault auth probe failed — check VAULT_ADDR + VAULT_TOKEN"
|
||||||
|
|
||||||
# ── Step 1/2: ensure kv/ mount exists and is KV v2 ───────────────────────────
|
# ── Step 1/3: ensure kv/ mount exists and is KV v2 ───────────────────────────
|
||||||
log "── Step 1/2: ensure ${KV_MOUNT}/ is KV v2 ──"
|
log "── Step 1/3: ensure ${KV_MOUNT}/ is KV v2 ──"
|
||||||
export DRY_RUN
|
export DRY_RUN
|
||||||
hvault_ensure_kv_v2 "$KV_MOUNT" "[vault-seed-woodpecker]" \
|
hvault_ensure_kv_v2 "$KV_MOUNT" "[vault-seed-woodpecker]" \
|
||||||
|| die "KV mount check failed"
|
|| die "KV mount check failed"
|
||||||
|
|
||||||
# ── Step 2/2: seed agent_secret at kv/data/disinto/shared/woodpecker ─────────
|
# ── Step 2/3: seed agent_secret at kv/data/disinto/shared/woodpecker ─────────
|
||||||
log "── Step 2/2: seed ${KV_API_PATH} ──"
|
log "── Step 2/3: seed agent_secret ──"
|
||||||
|
|
||||||
existing_raw="$(hvault_get_or_empty "${KV_API_PATH}")" \
|
existing_raw="$(hvault_get_or_empty "${KV_API_PATH}")" \
|
||||||
|| die "failed to read ${KV_API_PATH}"
|
|| die "failed to read ${KV_API_PATH}"
|
||||||
|
|
@ -103,24 +110,38 @@ fi
|
||||||
|
|
||||||
if [ -n "$existing_agent_secret" ]; then
|
if [ -n "$existing_agent_secret" ]; then
|
||||||
log "agent_secret unchanged"
|
log "agent_secret unchanged"
|
||||||
exit 0
|
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")"
|
||||||
|
|
||||||
|
# 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})}')"
|
||||||
|
|
||||||
|
_hvault_request POST "${KV_API_PATH}" "$payload" >/dev/null \
|
||||||
|
|| die "failed to write ${KV_API_PATH}"
|
||||||
|
|
||||||
|
log "agent_secret generated"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# agent_secret is missing — generate it.
|
# ── Step 3/3: register Forgejo OAuth app and store credentials ───────────────
|
||||||
|
log "── Step 3/3: register Forgejo OAuth app ──"
|
||||||
|
|
||||||
|
# Call the OAuth registration script
|
||||||
if [ "$DRY_RUN" -eq 1 ]; then
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
log "[dry-run] would generate + write: agent_secret"
|
log "[dry-run] would call wp-oauth-register.sh"
|
||||||
exit 0
|
else
|
||||||
|
# Export required env vars for the OAuth script
|
||||||
|
export DRY_RUN
|
||||||
|
"${LIB_DIR}/wp-oauth-register.sh" --dry-run || {
|
||||||
|
log "OAuth registration check failed (Forgejo may not be running)"
|
||||||
|
log "This is expected if Forgejo is not available"
|
||||||
|
}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
new_agent_secret="$(openssl rand -hex "$AGENT_SECRET_BYTES")"
|
log "done — agent_secret + OAuth credentials seeded"
|
||||||
|
|
||||||
# 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})}')"
|
|
||||||
|
|
||||||
_hvault_request POST "${KV_API_PATH}" "$payload" >/dev/null \
|
|
||||||
|| die "failed to write ${KV_API_PATH}"
|
|
||||||
|
|
||||||
log "agent_secret generated"
|
|
||||||
log "done — 1 key seeded at ${KV_API_PATH}"
|
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,11 @@ roles:
|
||||||
namespace: default
|
namespace: default
|
||||||
job_id: woodpecker-server
|
job_id: woodpecker-server
|
||||||
|
|
||||||
|
- name: service-woodpecker-agent
|
||||||
|
policy: service-woodpecker
|
||||||
|
namespace: default
|
||||||
|
job_id: woodpecker-agent
|
||||||
|
|
||||||
# ── Per-agent bots (nomad/jobs/bot-<role>.hcl — land in later steps) ───────
|
# ── Per-agent bots (nomad/jobs/bot-<role>.hcl — land in later steps) ───────
|
||||||
# job_id placeholders match the policy name 1:1 until each bot's jobspec
|
# job_id placeholders match the policy name 1:1 until each bot's jobspec
|
||||||
# lands. When a bot's jobspec is added under nomad/jobs/, update the
|
# lands. When a bot's jobspec is added under nomad/jobs/, update the
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue