package controller import ( "context" "errors" "fmt" "net/http" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" terdutv1alpha1 "git.ryuvia.com/niklas/terdut-operator/api/v1alpha1" "git.ryuvia.com/niklas/terdut-operator/internal/tdclient" ) // bootstrapStateLostError is DESIGN.md §6's one genuinely pathological // case: a checkpointed admin credential was used and then lost before the // lasting credential it was for could be persisted. Distinct from a plain // error so Reconcile can route it to a Ready: False condition (the // documented recovery is delete-and-recreate, not an automatic retry) rather // than treating it as a transient reconcile failure. type bootstrapStateLostError struct{ detail string } func (e *bootstrapStateLostError) Error() string { return fmt.Sprintf( "server reports already bootstrapped, but neither status.credentialsSecretRef nor a "+ "checkpointed admin credential exist here: %s. This TerdutServer cannot recover a "+ "credential on its own; delete and recreate it", e.detail) } // reconcileBootstrap implements DESIGN.md §6 point 1's self-registration // flow, checkpointed against the two real crash windows in it rather than // leaving them as theoretical gaps. Only called once // srv.Status.CredentialsSecretRef is nil and the Deployment has a ready // replica. func (r *TerdutServerReconciler) reconcileBootstrap(ctx context.Context, srv *terdutv1alpha1.TerdutServer) error { adminKey, err := r.getOrCreateCheckpointedAdminKey(ctx, srv) if err != nil { return err } bc := r.NewClient(serviceURL(srv)).WithToken(adminKey) instanceKey, err := r.getOrMintInstanceServiceAccountKey(ctx, bc) if err != nil { return err } credsName := credentialsSecretName(srv) if err := writeOperatorSecret(ctx, r.Client, r.OperatorNamespace, credsName, instanceKey); err != nil { return err } srv.Status.CredentialsSecretRef = &terdutv1alpha1.SecretKeyRef{Name: credsName, Key: credentialsSecretDataKey} // Best-effort: the checkpoint has done its job. Leaving it behind on a // delete failure here isn't a correctness problem (the next reconcile // finds status.CredentialsSecretRef already set and never looks at the // checkpoint again) — it would just be an unused Secret sitting around, // cleaned up for real by the finalizer on delete. checkpoint := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: checkpointSecretName(srv), Namespace: r.OperatorNamespace}} _ = r.Delete(ctx, checkpoint) return nil } // getOrCreateCheckpointedAdminKey returns a usable admin key: from the // checkpoint Secret if an earlier, interrupted attempt already got one, or // freshly from /api/bootstrap, immediately checkpointed before it's used // for anything else. func (r *TerdutServerReconciler) getOrCreateCheckpointedAdminKey(ctx context.Context, srv *terdutv1alpha1.TerdutServer) (string, error) { checkpointName := checkpointSecretName(srv) var checkpoint corev1.Secret err := r.Get(ctx, client.ObjectKey{Namespace: r.OperatorNamespace, Name: checkpointName}, &checkpoint) switch { case err == nil: return string(checkpoint.Data[credentialsSecretDataKey]), nil case !apierrors.IsNotFound(err): return "", err } bc := r.NewClient(serviceURL(srv)) result, err := bc.Bootstrap(ctx, bootstrapUsername, bootstrapEmail) if err != nil { if statusErr, ok := errors.AsType[*tdclient.StatusError](err); ok && statusErr.Code == http.StatusForbidden { // §1: this operator is the only thing that ever bootstraps a // server it created, so a 403 here (no checkpoint, no // status.credentialsSecretRef) means a prior reconcile already // won this exact race and its checkpoint was lost afterward -- // the one case §6 doesn't try to paper over. return "", &bootstrapStateLostError{detail: "/api/bootstrap returned 403"} } return "", fmt.Errorf("POST /api/bootstrap: %w", err) } if err := writeOperatorSecret(ctx, r.Client, r.OperatorNamespace, checkpointName, result.APIKey.Key); err != nil { return "", fmt.Errorf("checkpointing admin key: %w", err) } return result.APIKey.Key, nil } // getOrMintInstanceServiceAccountKey mints the operator's own instance- // scoped service account, or, if an earlier interrupted attempt already // created it (409), adopts it and mints a fresh key rather than treating // the conflict as an error (DESIGN.md §6 point 1, §5's general // adopt-on-conflict rule). func (r *TerdutServerReconciler) getOrMintInstanceServiceAccountKey(ctx context.Context, bc *tdclient.Client) (string, error) { result, err := bc.CreateInstanceServiceAccount(ctx, serviceAccountName) if err == nil { return result.Key.Key, nil } statusErr, ok := errors.AsType[*tdclient.StatusError](err) if !ok || statusErr.Code != http.StatusConflict { return "", fmt.Errorf("POST /api/service-accounts: %w", err) } sa, err := bc.GetServiceAccountByName(ctx, serviceAccountName) if err != nil { return "", fmt.Errorf("GET /api/service-accounts?name=%s (adopting after 409): %w", serviceAccountName, err) } if sa == nil { return "", fmt.Errorf("POST /api/service-accounts 409'd for %q but GET found nothing", serviceAccountName) } key, err := bc.CreateServiceAccountKey(ctx, sa.ID, "initial") if err != nil { return "", fmt.Errorf("POST /api/service-accounts/%d/keys (adopting after 409): %w", sa.ID, err) } return key.Key, nil }