Authenticate with a seeded operator key; fold escalation and switches into TerdutTeam
Credentials: the TerdutServer controller generates <name>-operator-key in the server's own namespace (owned by it) and hands it to the pods as TERDUT_OPERATOR_KEY; the server creates its instance-scoped account from it at every start. A replaced Secret rolls the pods. The bootstrap handshake, the checkpoint Secret, per-team service accounts and credentials Secrets, BootstrapStateLost and credentials.deletionPolicy are gone. CRDs: TerdutServer, TerdutTeam and TerdutAlertSource. TerdutEscalationRule and TerdutDeadmanSwitch become spec.escalation and spec.deadmanSwitches[] on the team (matched by name, extras removed); team invites are removed. A team is created under the identity <namespace>/<name> (external_id), so a retry, a lost status or a deleted team heal by repeating the same call, and a display name owned by another team is TeamNameTaken instead of an adoption. The server resolves escalation usernames (UnknownUser condition). OIDC claim names and trustEmail are spec fields. Fixes: query values are URL-escaped; every delete treats 404 as success; deleting a team no longer depends on allowedTeams consent; a switch or integration deleted on the server is recreated; unnamed switches take the CR's name. Cleanup: scaffold e2e test, AGENTS.md, devcontainer, unused config/ pieces and Client.Version() removed; DESIGN.md, README, ROADMAP and the demo (run-demo.sh, manifests) rewritten for the new design. Secret RBAC stays cluster-wide, now stated in DESIGN.md section 9. Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# The one TerdutServer this whole demo runs against. Everything else in
|
||||
# this directory (teams, escalation rules, dead man's switches, alert
|
||||
# sources) references it by name.
|
||||
# this directory (teams with their escalation and dead man's switches,
|
||||
# alert sources) references it by name.
|
||||
#
|
||||
# The operator never creates any ingress/HTTPRoute for this TerdutServer --
|
||||
# that's a permanent non-goal (DESIGN.md §1, NetworkingSpec's own doc
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# Two teams (this one and 03-team-payments.yaml) so the demo shows
|
||||
# per-team isolation -- separate incident lists, separate escalation
|
||||
# ladders, separate alert sources -- rather than one team standing in for
|
||||
# everything.
|
||||
# everything. A team carries its own escalation ladder and dead man's
|
||||
# switches; alert sources are separate objects (04-alertsource-*.yaml)
|
||||
# because each owns a webhook Secret.
|
||||
apiVersion: terdut.ryuvia.com/v1alpha1
|
||||
kind: TerdutTeam
|
||||
metadata:
|
||||
@@ -15,9 +17,29 @@ spec:
|
||||
name: terdut-operator-demo
|
||||
displayName: Platform
|
||||
# No oidc block: this demo is password-login only (01-server.yaml).
|
||||
# A real invite link, minted via this team's own credential, is how
|
||||
# run-demo.sh's alice actually gets in -- no signup_mode flip, no admin
|
||||
# token (see niklas/terdut-server#23's resolution). Role/maxUses left at
|
||||
# their defaults (member, 1): one link for one person.
|
||||
invite:
|
||||
enabled: true
|
||||
|
||||
# The whole ladder, replaced as one unit. username is required iff kind is
|
||||
# "user" (CEL validation at apply time). The server resolves it, so a
|
||||
# username it does not know yet leaves this team at Reason: UnknownUser
|
||||
# until that person exists -- run-demo.sh creates alice for exactly that.
|
||||
escalation:
|
||||
repeatCount: 2
|
||||
fallbackTopic: platform-fallback
|
||||
levels:
|
||||
- timeout: 5m
|
||||
targets:
|
||||
- kind: user
|
||||
username: alice
|
||||
- timeout: 10m
|
||||
targets:
|
||||
- kind: oncall
|
||||
|
||||
# Dead man's switches, by name. fire-alerts.sh's "heartbeat" scenario sends
|
||||
# a matching alert; stop sending it and terdut-server itself opens an
|
||||
# incident once `timeout` passes with no heartbeat. A switch on the server
|
||||
# that is not listed here is removed.
|
||||
deadmanSwitches:
|
||||
- name: platform-watchdog
|
||||
matcher: "alertname=PlatformWatchdog"
|
||||
timeout: 15m
|
||||
severity: critical
|
||||
|
||||
@@ -6,9 +6,15 @@ spec:
|
||||
serverRef:
|
||||
name: terdut-operator-demo
|
||||
displayName: Payments
|
||||
# No spec.invite here, deliberately: run-demo.sh joins alice to this team
|
||||
# through POST /api/teams/{teamID}/members instead (this team's own
|
||||
# credential, same owner-equivalent reach spec.invite relies on, plus her
|
||||
# user id resolved via GET /api/users), once she already has an account
|
||||
# from Platform's invite -- showing both onboarding paths this feature
|
||||
# unlocks, not just the one.
|
||||
escalation:
|
||||
repeatCount: 1
|
||||
fallbackTopic: payments-fallback
|
||||
levels:
|
||||
- timeout: 5m
|
||||
targets:
|
||||
- kind: oncall
|
||||
deadmanSwitches:
|
||||
- name: payments-watchdog
|
||||
matcher: "alertname=PaymentsWatchdog"
|
||||
timeout: 15m
|
||||
severity: critical
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# One per team is the rule (DESIGN.md §4.3) -- a second TerdutEscalationRule
|
||||
# naming the same teamRef would just clobber this one on the next reconcile.
|
||||
apiVersion: terdut.ryuvia.com/v1alpha1
|
||||
kind: TerdutEscalationRule
|
||||
metadata:
|
||||
name: terdutescalationrule-platform
|
||||
spec:
|
||||
teamRef:
|
||||
name: terdutteam-platform
|
||||
repeatCount: 2
|
||||
fallbackTopic: platform-fallback
|
||||
levels:
|
||||
# username is required iff kind is "user", rejected otherwise -- CEL
|
||||
# validation at apply time (api/v1alpha1/terdutescalationrule_types.go).
|
||||
# alice won't exist on a fresh demo install -- see README.md for
|
||||
# creating a real user if you want this level to mean something, or
|
||||
# just watch it fall through to oncall after 5m.
|
||||
- timeout: 5m
|
||||
targets:
|
||||
- kind: user
|
||||
username: alice
|
||||
- timeout: 10m
|
||||
targets:
|
||||
- kind: oncall
|
||||
@@ -1,13 +0,0 @@
|
||||
apiVersion: terdut.ryuvia.com/v1alpha1
|
||||
kind: TerdutEscalationRule
|
||||
metadata:
|
||||
name: terdutescalationrule-payments
|
||||
spec:
|
||||
teamRef:
|
||||
name: terdutteam-payments
|
||||
repeatCount: 1
|
||||
fallbackTopic: payments-fallback
|
||||
levels:
|
||||
- timeout: 5m
|
||||
targets:
|
||||
- kind: oncall
|
||||
@@ -1,17 +0,0 @@
|
||||
# A per-team dead man's switch (DESIGN.md §4.4) -- a different thing from
|
||||
# 01-server.yaml's spec.deadman, which this demo leaves unset so this CRD
|
||||
# is what you're actually seeing reconcile. fire-alerts.sh's "heartbeat"
|
||||
# scenario sends a matching alert; stop sending it and terdut-server
|
||||
# itself opens an incident once `timeout` passes with no heartbeat.
|
||||
apiVersion: terdut.ryuvia.com/v1alpha1
|
||||
kind: TerdutDeadmanSwitch
|
||||
metadata:
|
||||
name: terdutdeadmanswitch-platform
|
||||
spec:
|
||||
teamRef:
|
||||
name: terdutteam-platform
|
||||
# name omitted -- terdut-server derives one from the matcher's own
|
||||
# canonical form (DESIGN.md §4.4).
|
||||
matcher: "alertname=PlatformWatchdog"
|
||||
timeout: 15m
|
||||
severity: critical
|
||||
@@ -1,10 +0,0 @@
|
||||
apiVersion: terdut.ryuvia.com/v1alpha1
|
||||
kind: TerdutDeadmanSwitch
|
||||
metadata:
|
||||
name: terdutdeadmanswitch-payments
|
||||
spec:
|
||||
teamRef:
|
||||
name: terdutteam-payments
|
||||
matcher: "alertname=PaymentsWatchdog"
|
||||
timeout: 15m
|
||||
severity: critical
|
||||
+29
-43
@@ -1,9 +1,9 @@
|
||||
# Demo
|
||||
|
||||
Every CRD this operator reconciles, wired into one working install: one
|
||||
`TerdutServer`, two `TerdutTeam`s (Platform and Payments) each with their
|
||||
own `TerdutEscalationRule`, `TerdutDeadmanSwitch` and `TerdutAlertSource`,
|
||||
plus a script that fires synthetic Alertmanager webhooks at it so you can
|
||||
`TerdutServer`, two `TerdutTeam`s (Platform and Payments), each carrying its
|
||||
own escalation ladder and dead man's switches, and a `TerdutAlertSource` per
|
||||
team, plus a script that fires synthetic Alertmanager webhooks at it so you can
|
||||
watch real incidents appear, escalate and resolve.
|
||||
|
||||
This is a demo kit, not a reference deployment: `00-postgres.yaml` runs
|
||||
@@ -13,7 +13,7 @@ directory. Throw the whole namespace away when you're done.
|
||||
**Want this fully automated instead of walking through it by hand?**
|
||||
`./run-demo.sh` does everything below itself, against a fresh (or
|
||||
already-set-up) `kind` cluster — creates the cluster, installs the
|
||||
operator, applies every CR here, signs `alice` in for real, and fires a
|
||||
operator, applies every CR here, creates `alice` as the first user, and fires a
|
||||
few alerts. `./run-demo.sh --help` for the knobs, `./run-demo.sh
|
||||
--teardown` to tear it back down. The rest of this file is the manual
|
||||
walkthrough it automates.
|
||||
@@ -44,16 +44,17 @@ kubectl create namespace terdut-operator-demo
|
||||
kubectl apply -n terdut-operator-demo -k .
|
||||
```
|
||||
|
||||
Listed and applied in dependency order (server → team → everything that
|
||||
`teamRef`s it), but you don't have to preserve that order yourself:
|
||||
every controller here re-queues and waits rather than failing when a ref
|
||||
isn't resolvable yet (`kubectl describe` shows `Reason: TeamRefNotFound` /
|
||||
`WaitingForTeam` while that settles).
|
||||
Listed and applied in dependency order (server → team → alert source), but
|
||||
you don't have to preserve that order yourself: every controller here
|
||||
re-queues and waits rather than failing when a ref isn't resolvable yet
|
||||
(`kubectl describe` shows `Reason: WaitingForServer` / `WaitingForTeam` while
|
||||
that settles). Platform stays at `Reason: UnknownUser` until `alice` exists:
|
||||
its escalation ladder names her.
|
||||
|
||||
Watch it converge:
|
||||
|
||||
```sh
|
||||
kubectl get terdutservers,terdutteams,terdutescalationrules,terdutdeadmanswitches,terdutalertsources \
|
||||
kubectl get terdutservers,terdutteams,terdutalertsources \
|
||||
-n terdut-operator-demo
|
||||
```
|
||||
|
||||
@@ -80,36 +81,21 @@ exposing it for real (Gateway API, Istio, or a plain `Ingress`) instead.
|
||||
|
||||
### First login
|
||||
|
||||
The operator's own bootstrap (DESIGN.md §6) creates the first user through
|
||||
`/api/bootstrap` and immediately mints itself a service-account token from
|
||||
it, then discards the bootstrap user's own key — nobody ever signs in as
|
||||
that account, and `signup_mode` stays `invite_only` by default. **Don't try
|
||||
to flip it via the operator's own token**: that token is a service account,
|
||||
and `/api/admin/settings` is deliberately human-only on terdut-server
|
||||
(`niklas/terdut-server#23` has the full reasoning — widening that gate was
|
||||
the wrong fix).
|
||||
|
||||
The real path in: `02-team-platform.yaml` turns on `spec.invite`, so
|
||||
Platform's own `TerdutTeam` mints a real invite link with its own
|
||||
already-working team-scoped credential (the same reach that lets it manage
|
||||
its own escalation policy, dead man's switches and integrations — owner-
|
||||
equivalent, confirmed in terdut-server's `SERVICE-ACCOUNTS.md`). Invite
|
||||
redemption bypasses `signup_mode` entirely, so this needs no admin
|
||||
credential at all:
|
||||
The operator authenticates with a key of its own (the `TerdutServer`'s
|
||||
`<name>-operator-key` Secret, handed to the server as `TERDUT_OPERATOR_KEY`) and
|
||||
never creates a user. A person gets in the way anyone does on a fresh
|
||||
terdut-server: `/api/bootstrap` creates the first user, an administrator,
|
||||
while no user exists yet:
|
||||
|
||||
```sh
|
||||
secretname=$(kubectl -n terdut-operator-demo get terdutteam terdutteam-platform \
|
||||
-o jsonpath='{.status.inviteSecretRef.name}')
|
||||
url=$(kubectl -n terdut-operator-demo get secret "$secretname" -o jsonpath='{.data.url}' | base64 -d)
|
||||
echo "$url" # open this, or POST /api/signup with {"invite": "<the token after invite=>", ...}
|
||||
curl -sS -X POST http://localhost:8080/api/bootstrap -H 'Content-Type: application/json' \
|
||||
-d '{"username":"alice","email":"alice@example.com","password":"a-long-demo-password"}'
|
||||
```
|
||||
|
||||
`04-escalation-platform.yaml` names a user `alice` at its first escalation
|
||||
level — sign up as `alice` if you want that level to mean something rather
|
||||
than falling through to on-call after 5 minutes. `run-demo.sh` does exactly
|
||||
this automatically (and also joins `alice` to Payments, which deliberately
|
||||
has no `spec.invite` of its own — see that file's comment for the second
|
||||
onboarding path this demonstrates).
|
||||
The response carries an API key, shown once. An administrator can manage any
|
||||
team, so add `alice` to both teams with it (`POST /api/teams/{teamID}/members`;
|
||||
`kubectl get terdutteam -o jsonpath='{.status.teamID}'` gives the ids), or just
|
||||
use the UI's team pages. `run-demo.sh` does exactly this for you.
|
||||
|
||||
## Fire some alerts
|
||||
|
||||
@@ -122,8 +108,8 @@ export NAMESPACE=terdut-operator-demo
|
||||
./fire-alerts.sh platform disk-full
|
||||
./fire-alerts.sh payments pod-crash
|
||||
|
||||
# Watch it open an incident, escalate per 04/05-escalation-*.yaml's
|
||||
# levels, and show up on the Platform/Payments team's own incident list.
|
||||
# Watch it open an incident, escalate per the escalation ladders in
|
||||
# 02/03-team-*.yaml, and show up on the Platform/Payments team's own incident list.
|
||||
|
||||
./fire-alerts.sh platform high-cpu resolve
|
||||
```
|
||||
@@ -149,7 +135,7 @@ fired with. `./run-demo.sh` fires its alerts across `prod-eu` and `prod-us`.
|
||||
|
||||
### Dead man's switches
|
||||
|
||||
`06-deadman-platform.yaml` / `07-deadman-payments.yaml` expect a heartbeat
|
||||
The `deadmanSwitches` in `02-team-platform.yaml` / `03-team-payments.yaml` expect a heartbeat
|
||||
alert on a 15-minute timeout:
|
||||
|
||||
```sh
|
||||
@@ -167,7 +153,7 @@ the switch is watching for silence, not for a signal.
|
||||
kubectl delete namespace terdut-operator-demo
|
||||
```
|
||||
|
||||
The operator's own finalizers clean up everything cross-namespace
|
||||
(credentials Secrets in the operator's namespace, server-side team/rule/
|
||||
integration rows) before this namespace's objects actually disappear —
|
||||
give it a few seconds past the `kubectl delete` returning.
|
||||
The operator's finalizers delete each team on the server (with its
|
||||
escalation, switches and integrations) before this namespace's objects
|
||||
actually disappear — give it a few seconds past the `kubectl delete`
|
||||
returning. A team with open incidents is not deleted until they are resolved.
|
||||
|
||||
@@ -44,7 +44,7 @@ scenarios:
|
||||
disk-full critical -- disk usage above 95%
|
||||
pod-crash error -- a pod crash-looping
|
||||
heartbeat critical -- the team's dead man's switch heartbeat
|
||||
(matches the matcher in 06/07-deadman-*.yaml -- send this
|
||||
(matches the matcher in the deadmanSwitches in 02/03-team-*.yaml -- send this
|
||||
repeatedly to keep the switch alive, or stop sending it and
|
||||
watch terdut-server open an incident on its own once
|
||||
`timeout` passes with no heartbeat. "resolve" is not a valid
|
||||
@@ -74,7 +74,7 @@ case "$scenario" in
|
||||
disk-full) alertname=TerdutDemoDiskFull severity=critical summary="Disk usage above 95% on /data" ;;
|
||||
pod-crash) alertname=TerdutDemoPodCrashLooping severity=error summary="Pod web-7f8b9 is crash-looping (5 restarts in 10m)" ;;
|
||||
heartbeat)
|
||||
# Must match 06-deadman-platform.yaml / 07-deadman-payments.yaml's own
|
||||
# Must match 02-team-platform.yaml / 03-team-payments.yaml's own
|
||||
# matcher exactly -- that's what makes this a heartbeat rather than a
|
||||
# third ordinary alert.
|
||||
case "$team" in
|
||||
@@ -95,7 +95,7 @@ esac
|
||||
|
||||
secret_name="terdutalertsource-${team}-terdut-webhook"
|
||||
key="$(kubectl -n "$NAMESPACE" get secret "$secret_name" -o jsonpath='{.data.key}' | base64 -d)"
|
||||
[ -n "$key" ] || { echo "fire-alerts.sh: empty key read from Secret $secret_name -- has 08/09-alertsource-*.yaml reconciled yet?" >&2; exit 1; }
|
||||
[ -n "$key" ] || { echo "fire-alerts.sh: empty key read from Secret $secret_name -- has 04/05-alertsource-*.yaml reconciled yet?" >&2; exit 1; }
|
||||
|
||||
# Stable per (team, scenario) so a resolve targets the same alert a fire
|
||||
# created: terdut-server correlates on (team_id, fingerprint), not on
|
||||
|
||||
@@ -9,9 +9,5 @@ resources:
|
||||
- 01-server.yaml
|
||||
- 02-team-platform.yaml
|
||||
- 03-team-payments.yaml
|
||||
- 04-escalation-platform.yaml
|
||||
- 05-escalation-payments.yaml
|
||||
- 06-deadman-platform.yaml
|
||||
- 07-deadman-payments.yaml
|
||||
- 08-alertsource-platform.yaml
|
||||
- 09-alertsource-payments.yaml
|
||||
- 04-alertsource-platform.yaml
|
||||
- 05-alertsource-payments.yaml
|
||||
|
||||
+47
-95
@@ -103,7 +103,7 @@ install_operator() {
|
||||
apply_demo() {
|
||||
log "creating namespace $NAMESPACE"
|
||||
kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - >/dev/null
|
||||
log "applying demo CRs (00-09) into $NAMESPACE"
|
||||
log "applying demo CRs (00-05) into $NAMESPACE"
|
||||
kubectl apply -n "$NAMESPACE" -k "$SCRIPT_DIR" >/dev/null
|
||||
}
|
||||
|
||||
@@ -116,28 +116,18 @@ wait_for_objects() {
|
||||
done
|
||||
}
|
||||
|
||||
# Just the server and the two teams -- everything redeem_platform_invite and
|
||||
# join_payments_team need. Deliberately NOT the escalation rules here: this
|
||||
# demo kit's own terdutescalationrule-platform names alice as a level-1
|
||||
# target, and that CR cannot reach Ready until alice actually exists
|
||||
# (terdut-server resolves every named username at reconcile time, not just
|
||||
# at escalation time) -- a real dependency this script has to satisfy by
|
||||
# creating her first, not something kubectl wait can be told to ignore.
|
||||
wait_for_teams_ready() {
|
||||
wait_for_objects \
|
||||
"terdutserver/terdut-operator-demo" \
|
||||
"terdutteam/terdutteam-platform" \
|
||||
"terdutteam/terdutteam-payments"
|
||||
# Only the server: the teams cannot reach Ready until alice exists (their
|
||||
# escalation ladders name her, and terdut-server resolves usernames when the
|
||||
# ladder is applied), and alice is created below, through the server itself.
|
||||
wait_for_server_ready() {
|
||||
wait_for_objects "terdutserver/terdut-operator-demo"
|
||||
}
|
||||
|
||||
# Everything that was waiting on alice (or just on the teams above, now
|
||||
# already satisfied) to exist.
|
||||
# Everything that was waiting on alice to exist.
|
||||
wait_for_remaining_ready() {
|
||||
wait_for_objects \
|
||||
"terdutescalationrule/terdutescalationrule-platform" \
|
||||
"terdutescalationrule/terdutescalationrule-payments" \
|
||||
"terdutdeadmanswitch/terdutdeadmanswitch-platform" \
|
||||
"terdutdeadmanswitch/terdutdeadmanswitch-payments" \
|
||||
"terdutteam/terdutteam-platform" \
|
||||
"terdutteam/terdutteam-payments" \
|
||||
"terdutalertsource/terdutalertsource-platform" \
|
||||
"terdutalertsource/terdutalertsource-payments"
|
||||
}
|
||||
@@ -173,91 +163,54 @@ start_port_forward() {
|
||||
log "port-forward ready (pid $STARTED_PF_PID, log $PF_LOGFILE)"
|
||||
}
|
||||
|
||||
# Redeems Platform's own invite link -- minted by its TerdutTeam
|
||||
# (02-team-platform.yaml's spec.invite.enabled, reconciled through that
|
||||
# team's own already-working team-scoped credential, which requireTeamOwner
|
||||
# already treats as owner-equivalent for /invites -- ratified, not a
|
||||
# workaround, in terdut-server's SERVICE-ACCOUNTS.md) and redeemed through
|
||||
# the ordinary signup endpoint. Invite redemption bypasses signup_mode
|
||||
# entirely (terdut-server's internal/api/signup.go), so this needs no admin
|
||||
# credential, no signup_mode flip, and no direct Postgres access at all --
|
||||
# unlike an earlier version of this script, before terdut-operator grew
|
||||
# this feature (see niklas/terdut-server#23).
|
||||
redeem_platform_invite() {
|
||||
log "reading Platform's invite link"
|
||||
local secret_name invite_url invite_token tries=0
|
||||
until secret_name="$(kubectl -n "$NAMESPACE" get terdutteam terdutteam-platform \
|
||||
-o jsonpath='{.status.inviteSecretRef.name}' 2>/dev/null)" && [ -n "$secret_name" ]; do
|
||||
tries=$((tries + 1))
|
||||
[ "$tries" -lt 30 ] || die "terdutteam-platform never reported status.inviteSecretRef -- check spec.invite.enabled and kubectl describe it"
|
||||
sleep 1
|
||||
done
|
||||
invite_url="$(kubectl -n "$NAMESPACE" get secret "$secret_name" -o jsonpath='{.data.url}' | base64 -d)"
|
||||
invite_token="${invite_url##*invite=}"
|
||||
[ -n "$invite_token" ] || die "could not parse an invite token out of $invite_url"
|
||||
|
||||
# A re-run: the invite was spent by the first run, and the server answers a
|
||||
# spent invite with 403 before it ever looks at the username, so the 409
|
||||
# handled below never arrives. If alice can already sign in, she exists.
|
||||
# Creates alice as the install's first user and administrator through
|
||||
# /api/bootstrap, which is open until a first user exists -- the operator
|
||||
# authenticates with its own seeded key and never uses it, so this is how a
|
||||
# person gets in. An administrator may manage any team, so alice's own key is
|
||||
# enough to add her to both; the teams' own status.teamID is set as soon as
|
||||
# the operator has created each team, even while they still wait on her.
|
||||
create_alice_and_join_teams() {
|
||||
# A re-run: she exists already (and was added to the teams the first time).
|
||||
local login_code
|
||||
login_code="$(curl -sS -o /dev/null -w '%{http_code}' \
|
||||
-X POST "${BASE_URL}/api/login" -H 'Content-Type: application/json' \
|
||||
-d "$(jq -n --arg u "$ALICE_USERNAME" --arg p "$DEMO_PASSWORD" '{username: $u, password: $p}')")"
|
||||
if [ "$login_code" = "200" ]; then
|
||||
log "account ${ALICE_USERNAME} already exists and can sign in, skipping signup (re-run detected)"
|
||||
log "account ${ALICE_USERNAME} already exists and can sign in, skipping creation (re-run detected)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "signing up ${ALICE_USERNAME} via Platform's invite"
|
||||
local body resp_file code
|
||||
body="$(jq -n \
|
||||
--arg u "$ALICE_USERNAME" --arg e "$ALICE_EMAIL" \
|
||||
--arg p "$DEMO_PASSWORD" --arg i "$invite_token" \
|
||||
'{username: $u, email: $e, password: $p, invite: $i}')"
|
||||
log "creating ${ALICE_USERNAME} as the first user (administrator)"
|
||||
local resp_file code alice_key
|
||||
resp_file="$(mktemp)"
|
||||
code="$(curl -sS -o "$resp_file" -w '%{http_code}' \
|
||||
-X POST "${BASE_URL}/api/signup" -H 'Content-Type: application/json' -d "$body")"
|
||||
|
||||
case "$code" in
|
||||
201) log "created local account ${ALICE_USERNAME} (joined Platform)" ;;
|
||||
409) log "account ${ALICE_USERNAME} already exists, skipping (re-run detected)" ;;
|
||||
*) die "signup failed (HTTP $code): $(cat "$resp_file")" ;;
|
||||
esac
|
||||
-X POST "${BASE_URL}/api/bootstrap" -H 'Content-Type: application/json' \
|
||||
-d "$(jq -n --arg u "$ALICE_USERNAME" --arg e "$ALICE_EMAIL" --arg p "$DEMO_PASSWORD" \
|
||||
'{username: $u, email: $e, password: $p}')")"
|
||||
[ "$code" = "201" ] || die "bootstrap failed (HTTP $code): $(cat "$resp_file")"
|
||||
alice_key="$(jq -r '.api_key.key' "$resp_file")"
|
||||
rm -f "$resp_file"
|
||||
}
|
||||
|
||||
# redeem_platform_invite's signup already used up alice's one signup -- a
|
||||
# second POST /api/signup would just 409 on the taken username, it doesn't
|
||||
# join an existing account to another team. Payments is joined through the
|
||||
# ordinary team-scoped member endpoint instead, using that team's own
|
||||
# already-working team-scoped credential (owner-equivalent, same reach the
|
||||
# invite route above relies on) and alice's user id resolved via
|
||||
# GET /api/users -- the same lookup tdclient.GetUserByUsername does
|
||||
# operator-side. The endpoint upserts on (team_id, user_id), so this is
|
||||
# naturally idempotent across re-runs with no separate conflict handling
|
||||
# needed.
|
||||
join_payments_team() {
|
||||
log "adding ${ALICE_USERNAME} to Payments"
|
||||
local payments_id payments_secret payments_key alice_id resp_file code
|
||||
payments_id="$(kubectl -n "$NAMESPACE" get terdutteam terdutteam-payments -o jsonpath='{.status.teamID}')"
|
||||
[ -n "$payments_id" ] || die "could not read status.teamID off terdutteam-payments"
|
||||
payments_secret="$(kubectl -n "$NAMESPACE" get terdutteam terdutteam-payments \
|
||||
-o jsonpath='{.status.credentialsSecretRef.name}')"
|
||||
[ -n "$payments_secret" ] || die "terdutteam-payments has no status.credentialsSecretRef yet"
|
||||
payments_key="$(kubectl -n "$OPERATOR_NAMESPACE" get secret "$payments_secret" -o jsonpath='{.data.token}' | base64 -d)"
|
||||
|
||||
alice_id="$(curl -sS -H "Authorization: Bearer $payments_key" "${BASE_URL}/api/users" \
|
||||
| jq -r --arg u "$ALICE_USERNAME" '.[] | select(.username == $u) | .id')"
|
||||
[ -n "$alice_id" ] || die "could not resolve ${ALICE_USERNAME}'s user id via GET /api/users"
|
||||
|
||||
resp_file="$(mktemp)"
|
||||
code="$(curl -sS -o "$resp_file" -w '%{http_code}' \
|
||||
-X POST "${BASE_URL}/api/teams/${payments_id}/members" \
|
||||
-H "Authorization: Bearer $payments_key" -H 'Content-Type: application/json' \
|
||||
-d "$(jq -n --argjson id "$alice_id" '{user_id: $id, role: "member"}')")"
|
||||
[ "$code" = "204" ] || die "adding ${ALICE_USERNAME} to Payments failed (HTTP $code): $(cat "$resp_file")"
|
||||
log "added ${ALICE_USERNAME} to Payments"
|
||||
rm -f "$resp_file"
|
||||
local team team_id
|
||||
for team in terdutteam-platform terdutteam-payments; do
|
||||
team_id=""
|
||||
local tries=0
|
||||
until team_id="$(kubectl -n "$NAMESPACE" get terdutteam "$team" -o jsonpath='{.status.teamID}' 2>/dev/null)" \
|
||||
&& [ -n "$team_id" ]; do
|
||||
tries=$((tries + 1))
|
||||
[ "$tries" -lt 60 ] || die "$team never reported status.teamID -- kubectl describe it"
|
||||
sleep 1
|
||||
done
|
||||
local alice_id
|
||||
alice_id="$(curl -sS -H "Authorization: Bearer $alice_key" "${BASE_URL}/api/users" \
|
||||
| jq -r --arg u "$ALICE_USERNAME" '.[] | select(.username == $u) | .id')"
|
||||
code="$(curl -sS -o /dev/null -w '%{http_code}' \
|
||||
-X POST "${BASE_URL}/api/teams/${team_id}/members" \
|
||||
-H "Authorization: Bearer $alice_key" -H 'Content-Type: application/json' \
|
||||
-d "$(jq -n --argjson id "$alice_id" '{user_id: $id, role: "owner"}')")"
|
||||
[ "$code" = "204" ] || die "adding ${ALICE_USERNAME} to ${team} failed (HTTP $code)"
|
||||
log "added ${ALICE_USERNAME} to ${team}"
|
||||
done
|
||||
}
|
||||
|
||||
fire_demo_alerts() {
|
||||
@@ -350,10 +303,9 @@ main() {
|
||||
ensure_kind_cluster
|
||||
install_operator
|
||||
apply_demo
|
||||
wait_for_teams_ready
|
||||
wait_for_server_ready
|
||||
start_port_forward
|
||||
redeem_platform_invite
|
||||
join_payments_team
|
||||
create_alice_and_join_teams
|
||||
wait_for_remaining_ready
|
||||
fire_demo_alerts
|
||||
print_summary
|
||||
|
||||
Reference in New Issue
Block a user