package controller import ( "context" "fmt" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/controller-runtime/pkg/client" terdutv1alpha1 "git.ryuvia.com/niklas/terdut-operator/api/v1alpha1" ) // databaseError carries a condition reason/message out of resolveDatabaseEnv // — distinct from a plain error, since "the database isn't ready yet" is an // expected, requeue-and-retry outcome (DESIGN.md §8), not a reconcile // failure. type databaseError struct { reason string message string } func (e *databaseError) Error() string { return e.message } // classifyClusterGetError turns a failed Get of the postgresql.acid.zalan.do // CR into the right condition — distinct from a plain reconcile error, and // its own function (not inlined) so it's unit-testable against a // hand-constructed error without needing a real client at all: a // *meta.NoKindMatchError means the CRD itself isn't installed // (ReasonPostgresOperatorCRDNotInstalled, §8/§9's graceful-degradation // case), apierrors.IsNotFound means the CRD exists but this particular // object doesn't (yet) (ReasonPostgresClusterNotFound). func classifyClusterGetError(err error, clusterName, namespace string) *databaseError { switch { case meta.IsNoMatchError(err): return &databaseError{ reason: terdutv1alpha1.ReasonPostgresOperatorCRDNotInstalled, message: "spec.database.postgresClusterRef is set but the postgresql.acid.zalan.do CRD isn't installed in this cluster", } case apierrors.IsNotFound(err): return &databaseError{ reason: terdutv1alpha1.ReasonPostgresClusterNotFound, message: fmt.Sprintf("postgresql.acid.zalan.do %q not found in namespace %q", clusterName, namespace), } default: // Any other error (RBAC, transient API failure) is a real // reconcile error, not a condition to report and wait out -- // callers still surface it as a databaseError so resolveDatabaseEnv // has one return shape, but Reconcile treats every databaseError as // a wait-and-retry outcome today. Revisit if that ever matters in // practice (a persistent RBAC misconfiguration looks identical to // "not ready yet" until someone reads the condition message). return &databaseError{reason: terdutv1alpha1.ReasonPostgresClusterNotFound, message: err.Error()} } } // resolveDatabaseEnv implements DESIGN.md §8's two Postgres paths. It never // reads a password's value — only ever wires a secretKeyRef into the // Deployment's env, the same way the chart does — so it needs no RBAC on // Secret *data* for this, only on the postgresql.acid.zalan.do CR itself // (the XValidation rule on TerdutServerSpec.Database guarantees exactly one // of the two fields below is set, so there's no third case to handle). func (r *TerdutServerReconciler) resolveDatabaseEnv(ctx context.Context, srv *terdutv1alpha1.TerdutServer) ([]corev1.EnvVar, *databaseError) { db := srv.Spec.Database if db.PostgresClusterRef != nil { return r.resolveZalandoDatabaseEnv(ctx, srv, db.PostgresClusterRef.Name) } env := []corev1.EnvVar{{Name: "TERDUT_DB_DSN", Value: db.DSN}} if db.PasswordSecretRef != nil { env = append(env, corev1.EnvVar{ Name: "PGPASSWORD", ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{Name: db.PasswordSecretRef.Name}, Key: db.PasswordSecretRef.Key, }, }, }) } return env, nil } // resolveZalandoDatabaseEnv resolves a Zalando postgres-operator // `postgresql` CR into a DSN + PGPASSWORD secretKeyRef, without reading the // generated credentials Secret's value — only confirming it exists, the // same "exists, don't read" posture the DSN path takes toward its own // password Secret. // // By convention (DESIGN.md §8, PostgresClusterRef's own doc comment) the // database and role are both named "terdut", and the primary Service is // named after the cluster CR itself — Zalando's own naming convention, not // something its status exposes as a field to read. func (r *TerdutServerReconciler) resolveZalandoDatabaseEnv( ctx context.Context, srv *terdutv1alpha1.TerdutServer, clusterName string, ) ([]corev1.EnvVar, *databaseError) { cluster := &unstructured.Unstructured{} cluster.SetGroupVersionKind(postgresqlGVK) if err := r.Get(ctx, client.ObjectKey{Namespace: srv.Namespace, Name: clusterName}, cluster); err != nil { return nil, classifyClusterGetError(err, clusterName, srv.Namespace) } credsSecretName := fmt.Sprintf("terdut.%s.credentials.postgresql.acid.zalan.do", clusterName) var secret corev1.Secret if err := r.Get(ctx, client.ObjectKey{Namespace: srv.Namespace, Name: credsSecretName}, &secret); err != nil { if apierrors.IsNotFound(err) { return nil, &databaseError{ reason: terdutv1alpha1.ReasonPostgresClusterNotFound, message: fmt.Sprintf( "postgresql.acid.zalan.do %q found, but its generated credentials Secret %q doesn't exist yet", clusterName, credsSecretName), } } return nil, &databaseError{reason: terdutv1alpha1.ReasonPostgresClusterNotFound, message: err.Error()} } host := fmt.Sprintf("%s.%s.svc", clusterName, srv.Namespace) dsn := fmt.Sprintf("postgres://terdut@%s:5432/terdut?sslmode=require", host) return []corev1.EnvVar{ {Name: "TERDUT_DB_DSN", Value: dsn}, { Name: "PGPASSWORD", ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{Name: credsSecretName}, Key: "password", }, }, }, }, nil }