TerdutTeam: mint and surface a real invite link (spec.invite)
The actual fix for the human-onboarding gap niklas/terdut-server#23 found -- not a terdut-server change at all. A team-scoped credential is already owner-equivalent for POST/GET/DELETE /api/teams/{teamID}/invites (requireTeamOwner's synthetic-membership mechanism, ratified not accidental per that repo's SERVICE-ACCOUNTS.md), and invite redemption bypasses signup_mode entirely -- this TerdutTeam controller just never grew a feature to use either fact. New spec.invite{enabled, role (member|owner, default member), maxUses (1-100, default 1)} and status.inviteSecretRef. The Secret lives in the TerdutTeam's OWN namespace, not the operator's: unlike status.credentialsSecretRef (a durable, high-privilege credential, kept operator-side per DESIGN.md §6), an invite is bounded and limited-use, meant for this namespace's own human operators to read and hand out -- same precedent as TerdutAlertSource's status.webhookURLSecretRef, same- namespace and OwnerReference'd so deleting the TerdutTeam garbage-collects it automatically. internal/controller/terdutteam_invite.go: mints on first spec.invite.enabled, refreshes a day ahead of terdut-server's fixed 7-day TTL (reading the Secret's own stored expiresAt, no extra server round-trip per reconcile), revokes server-side and deletes the Secret when flipped back to false. A lost invite Secret is silently re-minted rather than treated as unrecoverable the way TerdutAlertSource's webhook key is -- nothing external holds a durable dependency on one specific invite link staying stable, it's read once by one human and handed out. New tdclient.Invite/CreateInvite/RevokeInvite. New envtest coverage: mint into the team's own namespace, refresh-before-expiry, revoke-on-disable (internal/controller/terdutteam_controller_test.go's new "spec.invite" Describe block), plus the fake server growing invite support (terdutserver_controller_test.go) -- its handleTeamSubPath dispatcher was split further (deadman switches into their own handleDeadmanSubPath, matching the existing handleIntegrationSubPath precedent) to stay under golangci-lint's gocyclo threshold with the new route added. examples/demo updated to prove this end to end: 02-team-platform.yaml turns on spec.invite; run-demo.sh's bootstrap_login/join_demo_teams (the psql signup_mode flip + a direct team_members INSERT) are replaced by redeem_platform_invite (reads status.inviteSecretRef, a real POST /api/signup with the invite token) and join_payments_team (POST /api/teams/{teamID}/members using Payments' own credential and alice's user id resolved via GET /api/users, deliberately not given its own spec.invite, so the demo shows both onboarding paths this feature unlocks) -- zero kubectl exec/psql calls remain anywhere in the script. README.md's "First login" section rewritten to match; it no longer documents the admin-token curl call that 403s against current terdut-server (niklas/terdut-server#23). Depends on niklas/terdut-server#24 (the callerMayManageServiceAccount fix for terdut-operator#3) being released before this is deployed for real -- not required to build or test this change itself, since the envtest fake never modeled that authorization gap to begin with.
This commit is contained in:
+32
-20
@@ -10,6 +10,14 @@ This is a demo kit, not a reference deployment: `00-postgres.yaml` runs
|
||||
Postgres with `emptyDir` storage and a password committed in this
|
||||
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
|
||||
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.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- The operator and its CRDs installed and running (`make install
|
||||
@@ -74,30 +82,34 @@ exposing it for real (Gateway API, Istio, or a plain `Ingress`) instead.
|
||||
|
||||
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 — that account has no password, so there's nothing to sign in with yet.
|
||||
`signup_mode` also defaults to `invite_only`, so open signup needs turning
|
||||
on first, using the admin token the operator generated for itself:
|
||||
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:
|
||||
|
||||
```sh
|
||||
# Which namespace the operator itself runs in:
|
||||
kubectl get deploy -A -l control-plane=controller-manager
|
||||
|
||||
# The Secret holding the operator's own admin token for this TerdutServer
|
||||
# (cross-namespace from terdut-operator-demo, per DESIGN.md §7):
|
||||
secretname=$(kubectl -n terdut-operator-demo get terdutserver terdut-operator-demo \
|
||||
-o jsonpath='{.status.credentialsSecretRef.name}')
|
||||
token=$(kubectl -n <operator-namespace-from-above> get secret "$secretname" \
|
||||
-o jsonpath='{.data.token}' | base64 -d)
|
||||
|
||||
curl -X PUT http://localhost:8080/api/admin/settings \
|
||||
-H "Authorization: Bearer $token" -H 'Content-Type: application/json' \
|
||||
-d '{"signup_mode":"open"}'
|
||||
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=>", ...}
|
||||
```
|
||||
|
||||
Then sign up through the UI as a normal human account. `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.
|
||||
`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).
|
||||
|
||||
## Fire some alerts
|
||||
|
||||
|
||||
Reference in New Issue
Block a user