Stage 2: TerdutTeam (create, mint team credential, rename/oidc-groups, delete)
CI / test (push) Successful in 1m41s

Implements ROADMAP.md Stage 2 against terdut-server's now-real
GET /api/teams?name= (TEAM-LOOKUP.md, landed in terdut-server just
before this commit) -- without it, the adopt-on-409 pattern this
controller depends on for team creation had no server-side lookup to
call, the same gap TerdutServer's own bootstrap flow hit and fixed in
Stage 1.

- api/v1alpha1: TerdutTeamSpec per DESIGN.md §4.2 (serverRef, displayName,
  oidc). status.credentialsSecretRef drops namespace for key, matching
  the fix already applied to TerdutServer's.
- internal/controller:
  - terdutteam_controller.go: resolves serverRef (same-namespace by
    default; cross-namespace gated by the target TerdutServer's
    spec.allowedTeams, DESIGN.md §4.6), waits for that TerdutServer to be
    Bootstrapped (no cross-controller RPC -- reads its
    status.credentialsSecretRef directly, DESIGN.md §5), creates the team
    and mints its team-scoped credential using the TerdutServer's
    instance-scoped one, then applies rename/oidc-groups with the
    team-scoped credential every reconcile (both are idempotent PUTs of
    the whole resource -- applied unconditionally rather than diffed
    against a stored last-applied value, same "cheap because it's small"
    reasoning §5 already gives the escalation policy's whole-policy PUT).
  - terdutteam_allowedteams.go: the §4.6 consent check in isolation from
    any client, unit-tested directly against hand-built inputs.
  - terdutteam_bootstrap.go: create-or-adopt-on-409 for the team itself
    (via TEAM-LOOKUP.md) and for its team-scoped service account (via the
    same GET-by-name+mint-new-key pattern Stage 1 already uses for the
    instance account).
  - secrets.go: extracted TerdutServer's write/read-credential-Secret
    helpers into free functions, now shared by both controllers rather
    than duplicated.
  - Finalizer deletes the team server-side (owner-gated, needs the
    team-scoped credential -- confirmed against source that an
    instance-scoped one does not satisfy requireTeamOwner, same finding
    as TEAM-LOOKUP.md's) and cleans up its credentials Secret. A team
    created but never fully reconciled to Ready (no team-scoped
    credential ever minted) is left orphaned server-side on delete -- a
    known, documented limitation (terdut-server has no delete path that
    doesn't require owner-equivalent access), not a silent gap.
- internal/tdclient: Team type, CreateTeam, GetTeamByName, RenameTeam,
  DeleteTeam, SetTeamOIDCGroups, CreateTeamServiceAccount -- matching
  terdut-server's real handlers' shapes field-for-field, same as Stage
  1's client additions.
- Tests: envtest covering the happy path, both not-ready reasons
  (ServerRefNotFound, WaitingForServer), cross-namespace allow/deny
  (default-closed and explicit All), both adopt-on-409 paths (team
  itself, team-scoped service account), and deletion. Extended the shared
  fakeTerdutServer (Stage 1) with team endpoints rather than writing a
  second, separately-drifting fake. 72.8%/30.6% coverage, 0 lint issues.

Verified locally: make fmt lint test build all clean.
This commit is contained in:
Niklas Ye
2026-10-01 11:09:40 +02:00
parent 72c979e3e8
commit c9c52af2f7
24 changed files with 1755 additions and 36 deletions
@@ -0,0 +1,187 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.22.0
name: terdutteams.terdut.ryuvia.com
spec:
group: terdut.ryuvia.com
names:
kind: TerdutTeam
listKind: TerdutTeamList
plural: terdutteams
singular: terdutteam
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .spec.serverRef.name
name: Server
type: string
- jsonPath: .status.teamID
name: TeamID
type: integer
- jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
type: string
- jsonPath: .status.conditions[?(@.type=="Ready")].reason
name: Reason
type: string
name: v1alpha1
schema:
openAPIV3Schema:
description: TerdutTeam is the Schema for the terdutteams API
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: spec defines the desired state of TerdutTeam
properties:
displayName:
description: |-
displayName is this team's name, both in terdut-server's own data
(POST /api/teams {"name": ...}) and as the identity POST /api/teams
and GET /api/teams?name= correlate on (DESIGN.md §5's idempotent-
create rule, via TEAM-LOOKUP.md).
minLength: 1
type: string
oidc:
description: |-
TerdutTeamOIDC binds which identity-provider groups grant membership and
ownership of this team (DESIGN.md §4.2). Both empty means no group grants
either role here — matches terdut-server's own NULLIF-on-empty-string
handling (internal/api/oidc_teams.go).
properties:
memberGroup:
type: string
ownerGroup:
type: string
type: object
serverRef:
description: serverRef names the TerdutServer this team belongs to.
properties:
name:
minLength: 1
type: string
namespace:
type: string
required:
- name
type: object
required:
- displayName
- serverRef
type: object
status:
description: status defines the observed state of TerdutTeam
properties:
conditions:
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
credentialsSecretRef:
description: |-
credentialsSecretRef is this team's own scoped credential
(DESIGN.md §6 point 3) -- pure output, always in the operator's own
namespace, under a fixed data key ("token").
properties:
key:
description: key is the data key inside the Secret holding the
raw value.
minLength: 1
type: string
name:
description: name is the Secret's name.
minLength: 1
type: string
required:
- key
- name
type: object
observedGeneration:
format: int64
type: integer
teamID:
description: |-
teamID is the server-side id -- needed by every child object's
controller (DESIGN.md §4.2).
format: int64
type: integer
type: object
required:
- spec
type: object
served: true
storage: true
subresources:
status: {}
+1
View File
@@ -3,6 +3,7 @@
# It should be run by config/default
resources:
- bases/terdut.ryuvia.com_terdutservers.yaml
- bases/terdut.ryuvia.com_terdutteams.yaml
# +kubebuilder:scaffold:crdkustomizeresource
patches: