prole/etc/init_knoe_auth.sh
chrisfu 903f84f200 feat: ship Junie #3 (image rename) + Phase 2 OIDC GKE deploy + k3d chrisfu seed
Three independent lines of work landing in one commit because they were all
on disk together end-of-session and the cross-file edits (TODO, knoe-system,
junie/README) interleave cleanly:

1. **Queue #3 — image rename `knoe-authority` → `knoe-auth`** (Junie).
   Closes drift R6. The Maven artifact has been `knoe-auth.jar` since
   commit b355855; the deploy manifests now match.
     authority/Dockerfile.app                                         (NEW)
     deploy/gcp/gke/knoe-auth-deployment.yaml                         (3 image tags renamed)
     deploy/opentofu/k3s/manifests/knoe/knoe-auth-deployment.yaml     (2 image tags renamed)
     Makefile                                                         (docker-build-auth +
                                                                       docker-push-auth + REGISTRY/KNOE_AUTH_VERSION
                                                                       defaults)
     docs/plans/junie/03-image-rename-knoe-authority-to-knoe-auth.md  (brief, kept as design record)

2. **Phase 2 OIDC provider — GKE deploy** (Junie). Source landed via the
   merge that brought claude/crazy-bose-fec256 back; the k3d sandbox
   shipped earlier today (commit 93157b0). This commit completes the GKE
   path: Kong route `/auth` → knoe-auth, default values added, signing
   key wired via `knoe-auth-oidc` secret per the brief.
     supabase/helm/knoe-supabase/templates/kong/config.yaml           (Kong route /auth, gated
                                                                       on .Values.knoeAuth.enabled)
     supabase/helm/knoe-supabase/values.yaml                          (knoeAuth.enabled=false default;
                                                                       studioIngress block — also
                                                                       fixes the helm lint issue noted
                                                                       in earlier commit 03bb731)
     docs/plans/junie/phase2-oidc-gke-deploy.md                       (brief, kept as design record)

3. **k3d dev-user seed: `chrisfu` + `knoe_developer`** (Claude). Closes
   "I want auth as chrisfu@knoey.com to my local cnpg database" for the
   local dev loop. `etc/init_knoe_auth.sh --mode k3d` now runs
   `seed_dev_users_k3d()` after the schema bootstrap, creating:
     - `knoe_developer` group role with R/W on `knoe`+`public` (mirrors
       the GKE production layout from docs/db-access.md; was hand-rolled
       in production per the 2026-04-30 onboarding work, never baked into
       postInitTemplateSQL).
     - `chrisfu` LOGIN role with password `chrisfu-dev` (idempotent —
       resets on every `make k3d-knoe-up`, so the rebuild loop is
       deterministic). Granted into `knoe_developer`.
   From the host with port-forward up:
     PGPASSWORD=chrisfu-dev psql "postgresql://chrisfu@localhost:5432/knoe-db?sslmode=require"

Cross-cutting doc updates:
  docs/TODO.md            — Phase 2 GKE entry removed from "In progress"
                            (now "(none — all items shipped or paused)");
                            queue #3 + reality table R6 removed; pg_oauth
                            paused note flipped to "OIDC issuer now
                            deployed to GKE — pg_oauth can resume"; Done
                            section gets new entries for #3, Phase 2 GKE,
                            and chrisfu seed.
  docs/knoe-system.md     — Phase 2 GKE row → Shipped; pg_oauth → Ready;
                            knoe-auth-deployment.yaml note "knoe-auth:latest"
                            instead of "knoe-authority:latest"; "One-time
                            cluster setup" mentions the chrisfu seed.
  docs/local-dev-knoe-auth.md — "Verify psql connectivity" rewritten to
                            connect as chrisfu (was `knoe`); new "Reset /
                            rebuild loop" section explaining idempotency.
  docs/plans/junie/README.md — two new rows under Shipped: brief 03 and
                            phase2-oidc-gke-deploy.

Verification (the bits I ran locally):
  - bash -n etc/init_knoe_auth.sh                                     OK
  - The seed SQL is idempotent (DO blocks with EXISTS checks +
    ALTER ROLE on the password reset path)
  - The Kong /auth route is gated on .Values.knoeAuth.enabled — default
    false, no behavior change until someone flips it on per Junie's brief

Out of scope for this commit:
  - Actually flipping knoeAuth.enabled=true on the live GKE chart and
    rolling out — Junie's brief covers the runbook; needs the
    knoe-auth-oidc K8s secret populated from 1Password first
  - Rebuilding + pushing the new knoe-auth:latest image — `make
    docker-push-auth` is wired, just hasn't been run yet
  - Round 1.5 OpenBao transit-key encryption (still queued)

Co-authored-by: Junie <junie@jetbrains.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:07:40 -07:00

574 lines
22 KiB
Bash
Executable File

#!/usr/bin/env bash
# init_knoe_auth.sh
# Provision knoe-auth (Kerberos KDC + Spring Boot enrollment service).
#
# Usage:
# ./etc/init_knoe_auth.sh [--context KUBECONTEXT] [--namespace NAMESPACE] [--project PROJECT_ID] [--mode MODE]
# ./etc/init_knoe_auth.sh initialize # full setup
# ./etc/init_knoe_auth.sh schema # schema only (idempotent)
# ./etc/init_knoe_auth.sh invite EMAIL # create first admin invite
# ./etc/init_knoe_auth.sh status # check pod + principal state
#
# Modes:
# (default / gke) GKE deploy — uses 1Password for secrets, GCP Workload Identity
# k3d Local k3d dev loop — uses hardcoded dev passwords, skips GCP steps
#
# Prerequisites:
# kubectl, op (1Password CLI — GKE mode only), psql (or kubectl exec fallback)
#
# Env vars (override args):
# APP_CLUSTER_KUBECONTEXT, KNOE_NAMESPACE, GCP_PROJECT_ID,
# KNOE_DB_HOST, KNOE_DB_PORT, KNOE_DB_NAME, KNOE_DB_SUPERUSER,
# KNOE_MODE (set to 'k3d' as alternative to --mode k3d)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
GKE_DIR="$REPO_ROOT/deploy/gcp/gke"
# ── Defaults ────────────────────────────────────────────────────────────────
MODE="${KNOE_MODE:-gke}" # gke (default) or k3d
APP_CTX="${APP_CLUSTER_KUBECONTEXT:-gke_plenary-truck-485623-p7_us-west3_knoe-dev-0}"
NAMESPACE="${KNOE_NAMESPACE:-knoe-system}"
GCP_PROJECT="${GCP_PROJECT_ID:-plenary-truck-485623-p7}"
DB_HOST="${KNOE_DB_HOST:-}" # resolved from CNPG svc if blank
DB_PORT="${KNOE_DB_PORT:-5432}"
DB_NAME="${KNOE_DB_NAME:-knoe}"
DB_SUPERUSER="${KNOE_DB_SUPERUSER:-postgres}"
REALM="KNOE.DEV"
AUTH_HOST="${KNOE_AUTH_HOST:-https://auth.knoe.dev}"
# k3d-specific defaults (overridden when --mode k3d is active)
K3D_CLUSTER_NAME="${K3D_CLUSTER_NAME:-k3d-knoe}"
K3D_CTX="k3d-${K3D_CLUSTER_NAME}"
K3D_DB_NS="${K3D_DB_NS:-knoe-db-0}"
# Dev-only passwords — NOT used in GKE mode; safe to commit
K3D_KDC_MASTER_PASSWORD="${K3D_KDC_MASTER_PASSWORD:-knoe-local-master-dev}"
K3D_KDC_ADMIN_PASSWORD="${K3D_KDC_ADMIN_PASSWORD:-knoe-local-admin-dev}"
log() { echo "[init_knoe_auth] $*"; }
info() { log "INFO $*"; }
warn() { log "WARN $*" >&2; }
die() { log "ERROR $*" >&2; exit 1; }
kube() { kubectl --context="$APP_CTX" "$@"; }
# ── Argument parsing ─────────────────────────────────────────────────────────
COMMAND="${1:-initialize}"
shift || true
while [[ $# -gt 0 ]]; do
case "$1" in
--context) APP_CTX="$2"; shift 2 ;;
--namespace) NAMESPACE="$2"; shift 2 ;;
--project) GCP_PROJECT="$2"; shift 2 ;;
--db-host) DB_HOST="$2"; shift 2 ;;
--mode) MODE="$2"; shift 2 ;;
*) break ;;
esac
done
# Apply k3d mode overrides after argument parsing
if [[ "$MODE" == "k3d" ]]; then
APP_CTX="${APP_CLUSTER_KUBECONTEXT:-${K3D_CTX}}"
REALM="KNOE.LOCAL"
AUTH_HOST="${KNOE_AUTH_HOST:-http://localhost:8080}"
# DB is accessed via port-forward (localhost:5432) in k3d mode
DB_HOST="${KNOE_DB_HOST:-localhost}"
fi
# ── Helpers ──────────────────────────────────────────────────────────────────
require_tool() {
command -v "$1" >/dev/null 2>&1 || die "Required tool not found: $1 — install it and retry."
}
wait_for_pods() {
local label="$1"
local timeout="${2:-180}"
info "Waiting up to ${timeout}s for pods with label ${label} in ${NAMESPACE}..."
kube -n "$NAMESPACE" wait pod \
-l "$label" \
--for=condition=Ready \
--timeout="${timeout}s"
}
op_secret() {
# Retrieve a 1Password secret; fall back to prompting if op isn't authed.
local item="$1" field="${2:-password}"
if command -v op >/dev/null 2>&1; then
op item get "$item" --fields "$field" 2>/dev/null || {
warn "1Password: could not read $item/$field — prompting."
read -rsp "Enter value for $item/$field: " val; echo
printf '%s' "$val"
}
else
read -rsp "Enter value for $item/$field: " val; echo
printf '%s' "$val"
fi
}
resolve_db_host() {
if [[ -n "$DB_HOST" ]]; then return; fi
if [[ "$MODE" == "k3d" ]]; then
# In k3d mode the engineer runs make k3d-knoe-pf first; DB is at localhost:5432
DB_HOST="localhost"
info "k3d mode: using DB host localhost (port-forward expected on :5432)"
return
fi
# Try to resolve CNPG primary service from the DB cluster context
DB_CTX="${DB_CLUSTER_KUBECONTEXT:-gke_plenary-truck-485623-p7_us-west3_knoe-cnpg-0}"
DB_HOST=$(kubectl --context="$DB_CTX" -n knoe-db-0 \
get svc knoe-db-rw -o jsonpath='{.spec.clusterIP}' 2>/dev/null || echo "")
[[ -z "$DB_HOST" ]] && die "Cannot resolve KNOE DB host. Set KNOE_DB_HOST or ensure knoe-db-rw svc exists."
info "Resolved DB host: $DB_HOST"
}
psql_file() {
local file="$1"
if [[ "$MODE" == "k3d" ]]; then
# In k3d mode: exec into the CNPG primary pod directly (no port-forward needed for schema)
local pod
pod=$(kubectl --context="$APP_CTX" -n "$K3D_DB_NS" \
get pod -l cnpg.io/cluster=knoe-db,role=primary \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
[[ -z "$pod" ]] && die "Cannot find CNPG primary pod in k3d. Is 'make k3d-knoe-up' complete?"
kubectl --context="$APP_CTX" -n "$K3D_DB_NS" cp "$file" "${pod}:/tmp/knoe_auth_schema.sql"
kubectl --context="$APP_CTX" -n "$K3D_DB_NS" exec "$pod" -- \
psql -U "$DB_SUPERUSER" -d "$DB_NAME" -f /tmp/knoe_auth_schema.sql
return
fi
if command -v psql >/dev/null 2>&1 && [[ -n "${PGPASSWORD:-}" ]]; then
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_SUPERUSER" -d "$DB_NAME" -f "$file"
else
# Fallback: exec into CNPG primary pod
DB_CTX="${DB_CLUSTER_KUBECONTEXT:-gke_plenary-truck-485623-p7_us-west3_knoe-cnpg-0}"
local pod
pod=$(kubectl --context="$DB_CTX" -n knoe-db-0 \
get pod -l cnpg.io/cluster=knoe-db,role=primary \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
[[ -z "$pod" ]] && die "Cannot find CNPG primary pod. Set PGPASSWORD and KNOE_DB_HOST for direct psql."
kubectl --context="$DB_CTX" -n knoe-db-0 cp "$file" "${pod}:/tmp/knoe_auth_schema.sql"
kubectl --context="$DB_CTX" -n knoe-db-0 exec "$pod" -- \
psql -U "$DB_SUPERUSER" -d "$DB_NAME" -f /tmp/knoe_auth_schema.sql
fi
}
# ── Schema ───────────────────────────────────────────────────────────────────
run_schema() {
info "Applying knoe-auth schema additions..."
resolve_db_host
local tmpfile
tmpfile=$(mktemp /tmp/knoe_auth_schema_XXXX.sql)
cat > "$tmpfile" <<'ENDSQL'
-- ── knoe-auth Round 1 schema additions ───────────────────────────────────────
-- Idempotent: all CREATE TABLE ... IF NOT EXISTS
-- Invite tokens (admin creates, single-use)
-- contact is the email/phone the invite was sent to — the OTP trust anchor.
-- knoe.dev starts with ZERO pre-knowledge of the developer's home org.
CREATE TABLE IF NOT EXISTS knoe.invitation (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
token TEXT NOT NULL UNIQUE,
contact TEXT NOT NULL,
contact_type TEXT NOT NULL DEFAULT 'email',
name_hint TEXT,
otp_hash TEXT NOT NULL,
otp_expires_at TIMESTAMPTZ NOT NULL,
otp_attempts INT NOT NULL DEFAULT 0,
otp_verified_at TIMESTAMPTZ,
created_by TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ,
used_by TEXT
);
CREATE INDEX IF NOT EXISTS idx_invitation_token ON knoe.invitation(token);
CREATE INDEX IF NOT EXISTS idx_invitation_contact ON knoe.invitation(contact);
-- External identity corroborations (Google sub → knoe user)
-- provider_hd records the developer's home domain (prole.org, gmail.com, etc.)
-- for audit purposes only — it is NOT used for access control.
CREATE TABLE IF NOT EXISTS knoe.identity (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES knoe.user(id) ON DELETE CASCADE,
provider TEXT NOT NULL,
provider_sub TEXT NOT NULL,
provider_email TEXT,
provider_hd TEXT,
verified_at TIMESTAMPTZ NOT NULL,
UNIQUE(provider, provider_sub)
);
CREATE INDEX IF NOT EXISTS idx_identity_user ON knoe.identity(user_id);
-- TOTP 2FA credentials (encrypted secret, backup codes)
CREATE TABLE IF NOT EXISTS knoe.totp_credential (
user_id INT PRIMARY KEY REFERENCES knoe.user(id) ON DELETE CASCADE,
secret TEXT NOT NULL,
verified_at TIMESTAMPTZ,
backup_codes TEXT[],
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Platform-managed resources (repos, db roles, policies, etc.)
CREATE TABLE IF NOT EXISTS knoe.knobject (
id SERIAL PRIMARY KEY,
type TEXT NOT NULL,
name TEXT NOT NULL,
platform_id TEXT,
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(type, name)
);
-- Access grants (user → knobject with role)
CREATE TABLE IF NOT EXISTS knoe.access_grant (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES knoe.user(id),
knobject_id INT NOT NULL REFERENCES knoe.knobject(id),
role TEXT NOT NULL,
granted_by TEXT NOT NULL,
granted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
revoked_at TIMESTAMPTZ,
UNIQUE(user_id, knobject_id)
);
-- Async provisioning job queue (GitLab user, Gitea user, CNPG role, etc.)
CREATE TABLE IF NOT EXISTS knoe.provisioning_job (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES knoe.user(id),
job_type TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
payload JSONB,
result JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_provisioning_job_status
ON knoe.provisioning_job(status, created_at);
-- Seed well-known knobjects
INSERT INTO knoe.knobject (type, name, metadata) VALUES
('gitea_org', 'knoey.com', '{"description": "Knoey.com Gitea organisation"}'),
('gitlab_group','knoey.com', '{"description": "Knoey.com GitLab group"}')
ON CONFLICT (type, name) DO NOTHING;
SELECT 'knoe-auth schema v1 applied.' AS status;
ENDSQL
psql_file "$tmpfile"
rm -f "$tmpfile"
info "Schema applied."
}
# ── Dev-user seed (k3d only) ─────────────────────────────────────────────────
#
# Creates the `knoe_developer` group role (the production GKE deploy uses it
# too via pg_hba.conf `+knoe_developer` rules; on GKE it was hand-rolled per
# 2026-04-30 onboarding work, never baked into postInitTemplateSQL — see
# docs/db-access.md). Then creates a `chrisfu` LOGIN role with a dev
# password and grants `knoe_developer` to it, so the engineer can connect
# from the host as chrisfu@knoey.com via the port-forward.
#
# Idempotent: re-runs on every `make k3d-knoe-up` and either creates or
# updates the role's password. This makes the rebuild loop deterministic —
# after `down && up`, chrisfu's password is always `chrisfu-dev`.
seed_dev_users_k3d() {
info "Seeding dev users (knoe_developer + chrisfu) for k3d ..."
local tmpfile
tmpfile=$(mktemp /tmp/knoe_auth_seed_XXXX.sql)
cat > "$tmpfile" <<'ENDSQL'
-- knoe_developer group role: R/W on knoe + public, R/O on auth/storage/extensions.
-- Mirrors the GKE production layout (docs/db-access.md). NOLOGIN — group only.
DO $do$ BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'knoe_developer') THEN
CREATE ROLE knoe_developer NOLOGIN;
END IF;
END $do$;
-- knoe + public — full R/W
GRANT USAGE ON SCHEMA knoe TO knoe_developer;
GRANT USAGE ON SCHEMA public TO knoe_developer;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA knoe TO knoe_developer;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO knoe_developer;
GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA knoe TO knoe_developer;
GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO knoe_developer;
ALTER DEFAULT PRIVILEGES IN SCHEMA knoe GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO knoe_developer;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO knoe_developer;
ALTER DEFAULT PRIVILEGES IN SCHEMA knoe GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO knoe_developer;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO knoe_developer;
-- Per-engineer LOGIN role for chrisfu (local-dev only; password is fixed dev value).
-- Creates if missing, otherwise resets the password — guarantees the rebuild loop
-- always produces the same credentials.
DO $do$ BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'chrisfu') THEN
CREATE ROLE chrisfu LOGIN INHERIT PASSWORD 'chrisfu-dev';
ELSE
ALTER ROLE chrisfu WITH LOGIN INHERIT PASSWORD 'chrisfu-dev';
END IF;
END $do$;
GRANT knoe_developer TO chrisfu;
-- Sanity check (visible in psql_file output).
SELECT 'chrisfu' AS role,
pg_has_role('chrisfu', 'knoe_developer', 'MEMBER') AS is_developer;
ENDSQL
psql_file "$tmpfile"
rm -f "$tmpfile"
info "Dev-user seed applied (chrisfu / chrisfu-dev, member of knoe_developer)."
}
# ── KDC secrets ──────────────────────────────────────────────────────────────
create_kdc_secrets() {
if kube -n "$NAMESPACE" get secret knoe-kdc-secrets >/dev/null 2>&1; then
info "knoe-kdc-secrets already exists — skipping."
return
fi
if [[ "$MODE" == "k3d" ]]; then
info "k3d mode: creating knoe-kdc-secrets with dev passwords (no 1Password)..."
kube -n "$NAMESPACE" create secret generic knoe-kdc-secrets \
--from-literal=master_password="$K3D_KDC_MASTER_PASSWORD" \
--from-literal=admin_password="$K3D_KDC_ADMIN_PASSWORD"
info "knoe-kdc-secrets created (dev passwords)."
return
fi
info "Creating knoe-kdc-secrets from 1Password..."
local master admin
master=$(op_secret "knoe-kdc-master" "password")
admin=$(op_secret "knoe-kdc-admin" "password")
kube -n "$NAMESPACE" create secret generic knoe-kdc-secrets \
--from-literal=master_password="$master" \
--from-literal=admin_password="$admin"
info "knoe-kdc-secrets created."
}
create_google_oidc_secret() {
if kube -n "$NAMESPACE" get secret knoe-auth-google-oidc >/dev/null 2>&1; then
info "knoe-auth-google-oidc already exists — skipping."
return
fi
info "Creating knoe-auth-google-oidc secret..."
local client_id client_secret
client_id=$(op_secret "knoe-google-oidc" "client_id")
client_secret=$(op_secret "knoe-google-oidc" "client_secret")
kube -n "$NAMESPACE" create secret generic knoe-auth-google-oidc \
--from-literal=client_id="$client_id" \
--from-literal=client_secret="$client_secret"
info "knoe-auth-google-oidc created."
}
create_session_secret() {
if kube -n "$NAMESPACE" get secret knoe-auth-secrets >/dev/null 2>&1; then
info "knoe-auth-secrets already exists — skipping."
return
fi
info "Creating knoe-auth-secrets (session HMAC key)..."
local session_secret
session_secret=$(op_secret "knoe-auth-session" "password")
kube -n "$NAMESPACE" create secret generic knoe-auth-secrets \
--from-literal=sessionSecret="$session_secret"
info "knoe-auth-secrets created."
}
create_oidc_path_b_secret() {
if kube -n "$NAMESPACE" get secret knoe-auth-oidc >/dev/null 2>&1; then
info "knoe-auth-oidc already exists — skipping."
return
fi
info "Creating knoe-auth-oidc secret for Path B..."
local client_id client_secret signing_key
client_id=$(op_secret "knoe-auth-oidc-gitlab" "client_id")
client_secret=$(op_secret "knoe-auth-oidc-gitlab" "client_secret")
signing_key=$(op_secret "knoe-auth-oidc-signing" "private_key")
kube -n "$NAMESPACE" create secret generic knoe-auth-oidc \
--from-literal=client-id="$client_id" \
--from-literal=client-secret="$client_secret" \
--from-literal=signing-key="$signing_key"
info "knoe-auth-oidc created."
}
# ── Manifests ────────────────────────────────────────────────────────────────
apply_manifests() {
if [[ "$MODE" == "k3d" ]]; then
local k3d_dir="$REPO_ROOT/k8s/knoe"
info "k3d mode: applying KDC manifests from $k3d_dir..."
kube apply -f "$k3d_dir/knoe-kdc-configmap.yaml"
kube apply -f "$k3d_dir/knoe-kdc-pvc.yaml"
kube apply -f "$k3d_dir/knoe-kdc-deployment.yaml"
kube apply -f "$k3d_dir/knoe-kdc-service.yaml"
info "k3d mode: skipping knoe-auth Deployment (runs on host via mvn spring-boot:run)."
return
fi
info "Applying KDC ConfigMap..."
kube apply -f "$GKE_DIR/knoe-kdc-configmap.yaml"
info "Applying knoe-auth Deployment..."
kube apply -f "$GKE_DIR/knoe-auth-deployment.yaml"
}
# ── Invite helper ─────────────────────────────────────────────────────────────
create_first_invite() {
local contact="${1:-}"
[[ -z "$contact" ]] && { read -rp "Invite contact (email or phone): " contact; }
local name_hint=""
read -rp "Display name hint (optional, press Enter to skip): " name_hint || true
info "Creating invite for: $contact"
local admin_token
admin_token=$(op_secret "knoe-admin-token" "credential" 2>/dev/null || \
{ read -rsp "Admin token: " t; echo; printf '%s' "$t"; })
local response
response=$(curl -sf -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $admin_token" \
-d "{\"contact\":\"$contact\",\"contactType\":\"email\",\"nameHint\":\"$name_hint\"}" \
"${AUTH_HOST}/auth/admin/invites") || {
warn "Admin API call failed. knoe-auth may not be ready yet."
warn "Retry: POST ${AUTH_HOST}/auth/admin/invites"
return 1
}
local invite_url
invite_url=$(printf '%s' "$response" | grep -o '"enrollUrl":"[^"]*"' | sed 's/"enrollUrl":"//;s/"//')
printf '\n\033[1;32mInvite URL:\033[0m %s\n\n' "$invite_url"
info "Send the above URL to: $contact"
}
# ── Status ────────────────────────────────────────────────────────────────────
show_status() {
if [[ "$MODE" == "k3d" ]]; then
info "=== KDC pod status ==="
kube -n "$NAMESPACE" get pods -l app=knoe-kdc 2>/dev/null || true
info "=== KDC secret ==="
kube -n "$NAMESPACE" get secret knoe-kdc-secrets 2>/dev/null || true
info "=== KDC service ==="
kube -n "$NAMESPACE" get svc knoe-kdc 2>/dev/null || true
info "=== knoe-auth runs on host ==="
info " export KRB5_CONFIG=\$PWD/etc/krb5.local.conf"
info " mvn -pl authority spring-boot:run"
return
fi
info "=== Pod status ==="
kube -n "$NAMESPACE" get pods -l app=knoe-auth 2>/dev/null || true
info "=== Secrets ==="
kube -n "$NAMESPACE" get secret \
knoe-kdc-secrets knoe-auth-google-oidc knoe-auth-secrets 2>/dev/null || true
info "=== Services ==="
kube -n "$NAMESPACE" get svc knoe-auth 2>/dev/null || true
info "=== Enrollment endpoint ==="
info "${AUTH_HOST}/auth/enroll"
}
# ── Full initialization ───────────────────────────────────────────────────────
cmd_initialize() {
require_tool kubectl
info "=== knoe-auth initialization ==="
info " Mode: $MODE"
info " Realm: $REALM"
info " Cluster: $APP_CTX"
info " NS: $NAMESPACE"
info ""
if [[ "$MODE" == "k3d" ]]; then
cmd_initialize_k3d
return
fi
# 1. Ensure namespace exists
kube get namespace "$NAMESPACE" >/dev/null 2>&1 || \
kube create namespace "$NAMESPACE"
# 2. Secrets
create_kdc_secrets
create_google_oidc_secret
create_session_secret
create_oidc_path_b_secret
# 3. Apply ConfigMap + Deployment
apply_manifests
# 4. Wait for pods
wait_for_pods "app=knoe-auth" 240
# 5. Schema
run_schema
# 6. Done
info ""
info "=== knoe-auth is ready ==="
info "Enrollment URL: ${AUTH_HOST}/auth/enroll?token=<invite_token>"
info ""
info "Next: create first admin invite:"
info " $0 invite chrisfu@knoey.com"
info ""
show_status
}
cmd_initialize_k3d() {
info "=== k3d mode: provisioning KDC + schema ==="
# 1. Ensure namespace exists
kube get namespace "$NAMESPACE" >/dev/null 2>&1 || \
kube create namespace "$NAMESPACE"
# 2. KDC secret (dev passwords, no 1Password)
create_kdc_secrets
# 3. Apply KDC manifests
apply_manifests
# 4. Wait for KDC pod
info "Waiting for KDC deployment to be ready..."
kube -n "$NAMESPACE" rollout status deployment/knoe-kdc --timeout=120s
# 5. Schema (via kubectl exec into CNPG primary)
run_schema
# 6. Dev-user seed: knoe_developer group + chrisfu role (k3d-only).
seed_dev_users_k3d
info ""
info "=== k3d knoe-auth stack is ready ==="
info "Run: make k3d-knoe-pf"
info "Then in another terminal:"
info " export KRB5_CONFIG=\$PWD/etc/krb5.local.conf"
info " export KNOE_AUTH_OIDC_SIGNING_KEY=\$(cat etc/secrets/knoe-auth-oidc-key.b64)"
info " mvn -pl authority spring-boot:run -Dspring-boot.run.profiles=k3d"
info ""
info "Dev DB access (from host, with port-forward up):"
info " PGPASSWORD=chrisfu-dev psql -h localhost -U chrisfu -d knoe-db"
info ""
show_status
}
# ── Dispatch ──────────────────────────────────────────────────────────────────
case "$COMMAND" in
initialize) cmd_initialize ;;
schema) run_schema ;;
invite) create_first_invite "${1:-}" ;;
status) show_status ;;
*)
echo "Usage: $0 {initialize|schema|invite EMAIL|status} [--context CTX] [--namespace NS]" >&2
exit 1
;;
esac