#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" source "${REPO_ROOT}/scripts/vault/dev/provider-lib.sh" usage() { cat <<'EOF' Usage: scripts/ci/reconcile-vault-dev.sh Commands: prepare-infra reconcile-transit reconcile-workload apply-apps EOF } log() { printf '[reconcile-vault-dev] %s\n' "$*" } require_cmd() { local cmd="$1" if ! command -v "$cmd" >/dev/null 2>&1; then echo "$cmd is required" >&2 exit 1 fi } require_env() { local name="$1" if [[ -z "${!name:-}" ]]; then echo "$name must be set" >&2 exit 1 fi } wait_http_ready() { local url="$1" local label="$2" if ! curl -fsS --retry 30 --retry-delay 2 --retry-connrefused "$url" >/dev/null 2>&1; then echo "${label} is not reachable at ${url}" >&2 return 1 fi } start_port_forward() { local namespace="$1" local service="$2" local local_port="$3" local remote_port="$4" local log_file="$5" kubectl -n "$namespace" port-forward "svc/${service}" "${local_port}:${remote_port}" >"$log_file" 2>&1 & local pf_pid=$! trap 'kill "$pf_pid" >/dev/null 2>&1 || true' EXIT printf '%s\n' "$pf_pid" } use_port_forward() { [[ "${RECONCILE_USE_PORT_FORWARD:-true}" == "true" ]] } transit_login() { VAULT_ADDR="${TRANSIT_VAULT_ADDR}" \ vault write -field=token auth/approle/login \ role_id="${VAULT_TRANSIT_DEV_ROLE_ID}" \ secret_id="${VAULT_TRANSIT_DEV_SECRET_ID}" } ensure_transit_state_resource() { local address="$1" local import_id="$2" if ! terraform -chdir="${REPO_ROOT}/terraform/vault-transit/reconcile" state show "$address" >/dev/null 2>&1; then log "Importing missing vault-transit state for ${address}" terraform -chdir="${REPO_ROOT}/terraform/vault-transit/reconcile" import "$address" "$import_id" fi } ensure_workload_state_resource() { local address="$1" local import_id="$2" if ! terraform -chdir="${REPO_ROOT}/terraform/vault/reconcile" state show "$address" >/dev/null 2>&1; then log "Importing missing workload-vault state for ${address}" terraform -chdir="${REPO_ROOT}/terraform/vault/reconcile" import "$address" "$import_id" fi } prepare_infra() { require_cmd kubectl require_env TF_STATE_DIR mkdir -p "${TF_STATE_DIR}" "${REPO_ROOT}/scripts/vault/dev/apply-argocd-dev-infra.sh" kubectl -n vault-transit wait --for=create deployment/vault-transit --timeout=300s kubectl -n vault-transit wait --for=condition=available deployment/vault-transit --timeout=300s kubectl -n vault wait --for=create deployment/vault --timeout=300s kubectl -n vault wait --for=condition=available deployment/vault --timeout=300s } reconcile_transit() { require_cmd kubectl require_cmd vault require_cmd terraform require_cmd curl require_cmd jq require_env TF_STATE_DIR require_env TRANSIT_VAULT_ADDR require_env VAULT_TRANSIT_DEV_ROLE_ID require_env VAULT_TRANSIT_DEV_SECRET_ID if use_port_forward; then start_port_forward vault-transit vault-transit 18200 8200 /tmp/vault-transit-port-forward.log >/dev/null fi wait_http_ready "${TRANSIT_VAULT_ADDR}/v1/sys/health" "Transit provider Vault API" VAULT_ADDR="${TRANSIT_VAULT_ADDR}" "${REPO_ROOT}/scripts/vault-transit/dev/ensure-unsealed.sh" local transit_tf_token transit_tf_token="$(transit_login)" VAULT_ADDR="${TRANSIT_VAULT_ADDR}" \ VAULT_TOKEN="${transit_tf_token}" \ vault policy write \ vault-transit-automation-dev \ "${REPO_ROOT}/runbooks/vault-transit/dev/policies/vault-transit-automation-dev.hcl" transit_tf_token="$(transit_login)" local transit_state_path="${TF_STATE_DIR}/vault-transit-reconcile.tfstate" TF_VAR_vault_addr="${TRANSIT_VAULT_ADDR}" \ TF_VAR_vault_token="${transit_tf_token}" \ terraform -chdir="${REPO_ROOT}/terraform/vault-transit/reconcile" init \ -input=false \ -reconfigure \ -backend-config="path=${transit_state_path}" if ! terraform -chdir="${REPO_ROOT}/terraform/vault-transit/reconcile" state list >/dev/null 2>&1; then echo "vault-transit Terraform state is unreadable. Re-seed ${transit_state_path}." >&2 exit 1 fi export TF_VAR_vault_addr="${TRANSIT_VAULT_ADDR}" export TF_VAR_vault_token="${transit_tf_token}" ensure_transit_state_resource vault_policy.workload_vault_transit_dev workload-vault-transit-dev ensure_transit_state_resource vault_policy.vault_transit_admin_dev vault-transit-admin-dev ensure_transit_state_resource vault_policy.vault_transit_automation_dev vault-transit-automation-dev ensure_transit_state_resource vault_approle_auth_backend_role.workflow auth/approle/role/vault-transit-dev-workflow TF_VAR_vault_addr="${TRANSIT_VAULT_ADDR}" \ TF_VAR_vault_token="${transit_tf_token}" \ terraform -chdir="${REPO_ROOT}/terraform/vault-transit/reconcile" apply -input=false -auto-approve } reconcile_workload() { require_cmd kubectl require_cmd vault require_cmd terraform require_cmd curl require_env TF_STATE_DIR require_env TRANSIT_VAULT_ADDR require_env WORKLOAD_VAULT_ADDR require_env VAULT_TRANSIT_DEV_ROLE_ID require_env VAULT_TRANSIT_DEV_SECRET_ID if use_port_forward; then start_port_forward vault vault 8200 8200 /tmp/vault-workload-port-forward.log >/dev/null fi wait_http_ready "${WORKLOAD_VAULT_ADDR}/v1/sys/health" "Workload Vault API" VAULT_ADDR="${WORKLOAD_VAULT_ADDR}" "${REPO_ROOT}/scripts/vault/dev/ensure-unsealed.sh" local transit_tf_token transit_tf_token="$(transit_login)" local workload_role_id local workload_secret_id workload_role_id="$( VAULT_ADDR="${TRANSIT_VAULT_ADDR}" \ VAULT_TOKEN="${transit_tf_token}" \ vault kv get -field=VAULT_WORKLOAD_DEV_ROLE_ID kv/dev/workload/bootstrap 2>/dev/null || true )" workload_secret_id="$( VAULT_ADDR="${TRANSIT_VAULT_ADDR}" \ VAULT_TOKEN="${transit_tf_token}" \ vault kv get -field=VAULT_WORKLOAD_DEV_SECRET_ID kv/dev/workload/bootstrap 2>/dev/null || true )" if [[ -z "${workload_role_id}" || -z "${workload_secret_id}" ]]; then echo "Workload Vault bootstrap AppRole is missing from provider Vault." >&2 echo "Execute scripts/vault/dev/bootstrap-runbook.sh manually with a privileged workload token to seed kv/dev/workload/bootstrap." >&2 exit 1 fi local workload_tf_token workload_tf_token="$( VAULT_ADDR="${WORKLOAD_VAULT_ADDR}" \ vault write -field=token auth/approle/login \ role_id="${workload_role_id}" \ secret_id="${workload_secret_id}" )" local workload_state_path="${TF_STATE_DIR}/vault-reconcile.tfstate" TF_VAR_workload_vault_addr="${WORKLOAD_VAULT_ADDR}" \ TF_VAR_workload_vault_token="${workload_tf_token}" \ TF_VAR_transit_vault_addr="${TRANSIT_VAULT_ADDR}" \ TF_VAR_transit_vault_token="${transit_tf_token}" \ terraform -chdir="${REPO_ROOT}/terraform/vault/reconcile" init \ -input=false \ -reconfigure \ -backend-config="path=${workload_state_path}" export TF_VAR_workload_vault_addr="${WORKLOAD_VAULT_ADDR}" export TF_VAR_workload_vault_token="${workload_tf_token}" export TF_VAR_transit_vault_addr="${TRANSIT_VAULT_ADDR}" export TF_VAR_transit_vault_token="${transit_tf_token}" ensure_workload_state_resource vault_policy.auth_server auth-server-dev ensure_workload_state_resource vault_policy.auth_db_migration auth-db-migration-dev ensure_workload_state_resource vault_policy.postgres postgres-dev ensure_workload_state_resource vault_policy.keycloak keycloak-dev ensure_workload_state_resource vault_policy.keycloak_client_sync keycloak-client-sync-dev ensure_workload_state_resource vault_policy.postgres_operator postgres-operator-dev ensure_workload_state_resource vault_policy.keycloak_operator keycloak-operator-dev ensure_workload_state_resource vault_policy.platform_admin platform-admin-dev ensure_workload_state_resource vault_policy.workload_automation workload-automation-dev ensure_workload_state_resource vault_kubernetes_auth_backend_role.auth_server auth/kubernetes/role/auth-server-dev ensure_workload_state_resource vault_kubernetes_auth_backend_role.auth_db_migration auth/kubernetes/role/auth-db-migration-dev ensure_workload_state_resource vault_kubernetes_auth_backend_role.postgres auth/kubernetes/role/postgres-dev ensure_workload_state_resource vault_kubernetes_auth_backend_role.keycloak auth/kubernetes/role/keycloak-dev ensure_workload_state_resource vault_kubernetes_auth_backend_role.keycloak_client_sync auth/kubernetes/role/keycloak-client-sync-dev ensure_workload_state_resource vault_approle_auth_backend_role.workflow auth/approle/role/workload-dev-workflow ensure_workload_state_resource vault_database_secret_backend_connection.platform_postgres database/config/platform-postgres-dev ensure_workload_state_resource vault_database_secret_backend_role.auth_db_migration database/roles/auth-db-migration-dev ensure_workload_state_resource vault_database_secret_backend_role.postgres_operator database/roles/postgres-operator-dev ensure_workload_state_resource vault_kv_secret_v2.platform_postgres_superuser kv/data/dev/platform/postgres/superuser ensure_workload_state_resource vault_kv_secret_v2.platform_postgres_auth_server kv/data/dev/platform/postgres/auth-server ensure_workload_state_resource vault_kv_secret_v2.platform_postgres_keycloak kv/data/dev/platform/postgres/keycloak ensure_workload_state_resource vault_kv_secret_v2.platform_keycloak_bootstrap_admin kv/data/dev/platform/keycloak/bootstrap-admin ensure_workload_state_resource vault_kv_secret_v2.platform_keycloak_client_auth_server kv/data/dev/platform/keycloak/client-auth-server TF_VAR_workload_vault_addr="${WORKLOAD_VAULT_ADDR}" \ TF_VAR_workload_vault_token="${workload_tf_token}" \ TF_VAR_transit_vault_addr="${TRANSIT_VAULT_ADDR}" \ TF_VAR_transit_vault_token="${transit_tf_token}" \ terraform -chdir="${REPO_ROOT}/terraform/vault/reconcile" apply -input=false -auto-approve if kubectl -n vault get deployment -l app.kubernetes.io/name=vault-agent-injector -o name | grep -q .; then kubectl -n vault wait --for=condition=available deployment -l app.kubernetes.io/name=vault-agent-injector --timeout=180s fi } apply_apps() { require_cmd kubectl "${REPO_ROOT}/scripts/vault/dev/apply-argocd-dev-apps.sh" } main() { require_cmd bash require_cmd kubectl case "${1:-}" in prepare-infra) prepare_infra ;; reconcile-transit) reconcile_transit ;; reconcile-workload) reconcile_workload ;; apply-apps) apply_apps ;; *) usage >&2 exit 1 ;; esac } main "$@"