infra/.woodpecker/provision-user.yml
Viktor Barzin 704fa09185 fix: remove manual event from build-ci-image to fix issue automation
build-ci-image.yml had event:[push,manual] which caused it to run
on every manual pipeline trigger. Its registry_user/registry_password
secrets don't have the manual event, causing all manual pipelines to
error. Removed manual from its event list since it only needs push.

Reverted evaluate conditions (Woodpecker evaluates secrets before
conditions, so evaluate can't prevent missing-secret errors).

[ci skip]

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:31:25 +00:00

160 lines
7 KiB
YAML

when:
event: manual
clone:
git:
image: woodpeckerci/plugin-git
settings:
attempts: 5
backoff: 10s
steps:
- name: validate-inputs
image: alpine
commands:
- |
if [ -z "$USERNAME" ] || [ -z "$EMAIL" ]; then
echo "ERROR: USERNAME and EMAIL variables are required"
echo "Trigger with: POST /api/repos/1/pipelines {branch:master, variables:{USERNAME:x, EMAIL:y}}"
exit 1
fi
# Validate username: lowercase alphanumeric + dash/underscore, 2-63 chars
if ! echo "$USERNAME" | grep -qE '^[a-z0-9][a-z0-9_-]{0,61}[a-z0-9]$'; then
echo "ERROR: USERNAME must be 2-63 chars, lowercase alphanumeric/dash/underscore"
exit 1
fi
# Validate email: basic format check
if ! echo "$EMAIL" | grep -qE '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'; then
echo "ERROR: EMAIL must be a valid email address"
exit 1
fi
echo "Provisioning user: $USERNAME ($EMAIL)"
echo "export PROVISION_USERNAME='$USERNAME'" > .provision-env
echo "export PROVISION_EMAIL='$EMAIL'" >> .provision-env
- name: prepare
image: alpine
commands:
- "apk update && apk add jq curl git git-crypt"
# git-crypt for secrets/ directory
- |
curl -k https://10.0.20.100:6443/api/v1/namespaces/woodpecker/configmaps/git-crypt-key \
-H "Authorization:Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
| jq -r .data.key | base64 -d > /tmp/key
- "git-crypt unlock /tmp/key; rm -f /tmp/key"
# Vault: authenticate via K8s service account JWT
- |
SA_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
VAULT_TOKEN=$(curl -s -X POST http://vault-active.vault.svc.cluster.local:8200/v1/auth/kubernetes/login \
-d "{\"role\":\"ci\",\"jwt\":\"$SA_TOKEN\"}" | jq -r .auth.client_token)
echo "export VAULT_TOKEN=$VAULT_TOKEN" > .vault-env
echo "export VAULT_ADDR=http://vault-active.vault.svc.cluster.local:8200" >> .vault-env
- name: update-vault-kv
image: alpine
commands:
- "apk update && apk add jq curl"
# Read current platform secret
- |
. .provision-env && . .vault-env
CURRENT=$(curl -s -H "X-Vault-Token: $VAULT_TOKEN" \
"$VAULT_ADDR/v1/secret/data/platform" | jq -r '.data.data')
# Parse current k8s_users (stored as JSON string)
CURRENT_USERS=$(echo "$CURRENT" | jq -r '.k8s_users')
# Check if user already exists
if echo "$CURRENT_USERS" | jq -e --arg u "$PROVISION_USERNAME" '.[$u]' >/dev/null 2>&1; then
echo "User $PROVISION_USERNAME already exists in k8s_users — skipping Vault KV update"
exit 0
fi
# Add new user with convention defaults
UPDATED_USERS=$(echo "$CURRENT_USERS" | jq --arg u "$PROVISION_USERNAME" --arg e "$PROVISION_EMAIL" \
'. + {($u): {"role":"namespace-owner","email":$e,"namespaces":[$u],"domains":[],"quota":{"cpu_requests":"2","memory_requests":"4Gi","memory_limits":"8Gi","pods":"20"}}}')
# Write back full platform secret with updated k8s_users (as JSON string)
PAYLOAD=$(echo "$CURRENT" | jq --arg users "$UPDATED_USERS" '.k8s_users = $users')
curl -s -X POST -H "X-Vault-Token: $VAULT_TOKEN" \
"$VAULT_ADDR/v1/secret/data/platform" \
-d "{\"data\": $PAYLOAD}" | jq .
echo "Added $PROVISION_USERNAME to k8s_users in Vault"
- name: create-authentik-groups
image: alpine
commands:
- "apk update && apk add jq curl"
- |
source .provision-env && source .vault-env
# Get Authentik API token from Vault
AUTHENTIK_TOKEN=$(curl -s -H "X-Vault-Token: $VAULT_TOKEN" \
"$VAULT_ADDR/v1/secret/data/viktor" | jq -r '.data.data.authentik_api_token')
AUTHENTIK_URL="https://authentik.viktorbarzin.me"
# Create sops-USERNAME group if it doesn't exist
SOPS_GROUP="sops-$PROVISION_USERNAME"
EXISTING=$(curl -s -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
"$AUTHENTIK_URL/api/v3/core/groups/?name=$SOPS_GROUP" | jq -r '.results | length')
if [ "$EXISTING" = "0" ]; then
GROUP_PAYLOAD=$(jq -n --arg name "$SOPS_GROUP" '{"name": $name, "is_superuser": false}')
GROUP_PK=$(curl -s -X POST -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
-H "Content-Type: application/json" \
"$AUTHENTIK_URL/api/v3/core/groups/" \
-d "$GROUP_PAYLOAD" | jq -r '.pk')
echo "Created Authentik group $SOPS_GROUP (pk=$GROUP_PK)"
else
GROUP_PK=$(curl -s -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
"$AUTHENTIK_URL/api/v3/core/groups/?name=$SOPS_GROUP" | jq -r '.results[0].pk')
echo "Authentik group $SOPS_GROUP already exists (pk=$GROUP_PK)"
fi
# Find the user by username
USER_PK=$(curl -s -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
"$AUTHENTIK_URL/api/v3/core/users/?username=$PROVISION_USERNAME" | jq -r '.results[0].pk')
if [ "$USER_PK" = "null" ] || [ -z "$USER_PK" ]; then
echo "WARNING: User $PROVISION_USERNAME not found in Authentik — group assignment skipped"
echo "The user may not have signed up yet. Groups will need manual assignment."
exit 0
fi
# Add user to sops group
CURRENT_MEMBERS=$(curl -s -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
"$AUTHENTIK_URL/api/v3/core/groups/$GROUP_PK/" | jq -r '.users')
UPDATED_MEMBERS=$(echo "$CURRENT_MEMBERS" | jq --argjson uid "$USER_PK" '. + [$uid] | unique')
curl -s -X PATCH -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
-H "Content-Type: application/json" \
"$AUTHENTIK_URL/api/v3/core/groups/$GROUP_PK/" \
-d "{\"users\": $UPDATED_MEMBERS}" | jq .
echo "Added user $PROVISION_USERNAME (pk=$USER_PK) to group $SOPS_GROUP"
- name: notify-apply-needed
image: curlimages/curl
commands:
- |
. .provision-env
echo "User $PROVISION_USERNAME added to Vault KV and Authentik sops group."
echo "Manual step needed: apply vault + rbac + woodpecker stacks."
echo " cd stacks/vault && ../../scripts/tg apply --non-interactive"
echo " cd stacks/rbac && ../../scripts/tg apply --non-interactive"
echo " cd stacks/woodpecker && ../../scripts/tg apply --non-interactive"
- name: slack
image: curlimages/curl
commands:
- |
. .provision-env 2>/dev/null || true
curl -s -X POST -H 'Content-type: application/json' \
--data "{\"channel\":\"general\",\"text\":\"Woodpecker CI: User provisioned — $PROVISION_USERNAME added to Vault KV + Authentik. Run: cd stacks/vault && ../../scripts/tg apply --non-interactive && cd ../rbac && ../../scripts/tg apply --non-interactive\"}" \
"$SLACK_WEBHOOK" || true
environment:
SLACK_WEBHOOK:
from_secret: slack_webhook
when:
status: [success, failure]