package controller import ( "context" "errors" "fmt" "net/http" terdutv1alpha1 "git.ryuvia.com/niklas/terdut-operator/api/v1alpha1" "git.ryuvia.com/niklas/terdut-operator/internal/tdclient" ) // createOrAdoptTeam calls POST /api/teams with the TerdutServer's // instance-scoped credential, or, if an earlier interrupted attempt // already created this name (409), adopts it via GET /api/teams?name= // (TEAM-LOOKUP.md) rather than treating the conflict as an error -- // DESIGN.md §5's general adopt-on-conflict rule, the same shape // TerdutServer's own bootstrap flow uses for minting its instance account. func (r *TerdutTeamReconciler) createOrAdoptTeam(ctx context.Context, team *terdutv1alpha1.TerdutTeam, instanceClient *tdclient.Client) error { created, err := instanceClient.CreateTeam(ctx, team.Spec.DisplayName) if err == nil { team.Status.TeamID = created.ID return nil } statusErr, ok := errors.AsType[*tdclient.StatusError](err) if !ok || statusErr.Code != http.StatusConflict { return fmt.Errorf("POST /api/teams: %w", err) } found, err := instanceClient.GetTeamByName(ctx, team.Spec.DisplayName) if err != nil { return fmt.Errorf("GET /api/teams?name=%s (adopting after 409): %w", team.Spec.DisplayName, err) } if found == nil { // Genuinely pathological, not just a narrow crash window: the name // was taken a moment ago and isn't now. Surfaced as a plain error // (standard requeue-with-backoff) rather than a dedicated // condition -- there's no documented recovery to point at that // differs from "try again". return fmt.Errorf("POST /api/teams 409'd for %q but GET found nothing", team.Spec.DisplayName) } team.Status.TeamID = found.ID return nil } // mintTeamCredential mints this team's own team-scoped service account, // using the TerdutServer's instance-scoped credential (DESIGN.md §6 point // 3: an instance-scoped caller may do this against any team). Adopts via // GET+mint-new-key on a 409, the same pattern TerdutServer's own bootstrap // flow uses. func (r *TerdutTeamReconciler) mintTeamCredential(ctx context.Context, team *terdutv1alpha1.TerdutTeam, instanceClient *tdclient.Client) error { saName := teamServiceAccountName(team) result, err := instanceClient.CreateTeamServiceAccount(ctx, saName, team.Status.TeamID) var key string if err == nil { key = result.Key.Key } else { statusErr, ok := errors.AsType[*tdclient.StatusError](err) if !ok || statusErr.Code != http.StatusConflict { return fmt.Errorf("POST /api/service-accounts (team scope): %w", err) } sa, err := instanceClient.GetServiceAccountByName(ctx, saName) if err != nil { return fmt.Errorf("GET /api/service-accounts?name=%s (adopting after 409): %w", saName, err) } if sa == nil { return fmt.Errorf("POST /api/service-accounts 409'd for %q but GET found nothing", saName) } minted, err := instanceClient.CreateServiceAccountKey(ctx, sa.ID, "initial") if err != nil { return fmt.Errorf("POST /api/service-accounts/%d/keys (adopting after 409): %w", sa.ID, err) } key = minted.Key } credsName := teamCredentialsSecretName(team) if err := writeOperatorSecret(ctx, r.Client, r.OperatorNamespace, credsName, key); err != nil { return err } team.Status.CredentialsSecretRef = &terdutv1alpha1.SecretKeyRef{Name: credsName, Key: credentialsSecretDataKey} return nil }