@@ -24,6 +24,10 @@ import (
2424// See https://book.kubebuilder.io/reference/markers/webhook for docs
2525//+kubebuilder:webhook:verbs=create;update;delete,path=/validate-vshn-appcat-vshn-io-v1-vshnpostgresql,mutating=false,failurePolicy=fail,groups=vshn.appcat.vshn.io,resources=vshnpostgresqls,versions=v1,name=postgresql.vshn.appcat.vshn.io,sideEffects=None,admissionReviewVersions=v1
2626
27+ // Protect the nested XVSHNPostgreSQL composite from having its compositionRef changed once set.
28+ // This prevents accidental backend migrations (e.g. StackGres → CNPG) on live instances.
29+ //+kubebuilder:webhook:verbs=update,path=/validate-vshn-appcat-vshn-io-v1-xvshnpostgresql,mutating=false,failurePolicy=fail,groups=vshn.appcat.vshn.io,resources=xvshnpostgresqls,versions=v1,name=xvshnpostgresql.vshn.appcat.vshn.io,sideEffects=None,admissionReviewVersions=v1
30+
2731//RBAC
2832//+kubebuilder:rbac:groups=vshn.appcat.vshn.io,resources=xvshnpostgresqls,verbs=get;list;watch;patch;update
2933//+kubebuilder:rbac:groups=vshn.appcat.vshn.io,resources=xvshnpostgresqls/status,verbs=get;list;watch;patch;update
@@ -38,10 +42,12 @@ const (
3842)
3943
4044var (
41- pgGK = schema.GroupKind {Group : "vshn.appcat.vshn.io" , Kind : "VSHNPostgreSQL" }
42- pgGR = schema.GroupResource {Group : pgGK .Group , Resource : "vshnpostgresqls" }
45+ pgGK = schema.GroupKind {Group : "vshn.appcat.vshn.io" , Kind : "VSHNPostgreSQL" }
46+ pgGR = schema.GroupResource {Group : pgGK .Group , Resource : "vshnpostgresqls" }
47+ xpgGK = schema.GroupKind {Group : "vshn.appcat.vshn.io" , Kind : "XVSHNPostgreSQL" }
4348
4449 _ webhook.CustomValidator = & PostgreSQLWebhookHandler {}
50+ _ webhook.CustomValidator = & XVSHNPostgreSQLWebhookHandler {}
4551
4652 blocklist = map [string ]string {
4753 "listen_addresses" : "" ,
@@ -84,6 +90,46 @@ func SetupPostgreSQLWebhookHandlerWithManager(mgr ctrl.Manager, withQuota bool)
8490 Complete ()
8591}
8692
93+ // XVSHNPostgreSQLWebhookHandler validates the nested XVSHNPostgreSQL composite resource.
94+ // Its sole purpose is to make compositionRef immutable once set, preventing accidental
95+ // backend migrations on live nested instances (e.g. those created by VSHNKeycloak).
96+ type XVSHNPostgreSQLWebhookHandler struct {}
97+
98+ // SetupXVSHNPostgreSQLWebhookHandlerWithManager registers the XVSHNPostgreSQL validation webhook.
99+ func SetupXVSHNPostgreSQLWebhookHandlerWithManager (mgr ctrl.Manager ) error {
100+ return ctrl .NewWebhookManagedBy (mgr ).
101+ For (& vshnv1.XVSHNPostgreSQL {}).
102+ WithValidator (& XVSHNPostgreSQLWebhookHandler {}).
103+ Complete ()
104+ }
105+
106+ func (x * XVSHNPostgreSQLWebhookHandler ) ValidateCreate (_ context.Context , _ runtime.Object ) (admission.Warnings , error ) {
107+ return nil , nil
108+ }
109+
110+ func (x * XVSHNPostgreSQLWebhookHandler ) ValidateUpdate (_ context.Context , oldObj , newObj runtime.Object ) (admission.Warnings , error ) {
111+ oldPg , ok := oldObj .(* vshnv1.XVSHNPostgreSQL )
112+ if ! ok {
113+ return nil , fmt .Errorf ("provided manifest is not a valid XVSHNPostgreSQL object" )
114+ }
115+ newPg , ok := newObj .(* vshnv1.XVSHNPostgreSQL )
116+ if ! ok {
117+ return nil , fmt .Errorf ("provided manifest is not a valid XVSHNPostgreSQL object" )
118+ }
119+
120+ if newPg .DeletionTimestamp != nil {
121+ return nil , nil
122+ }
123+
124+ allErrs := newFielErrors (newPg .Name , xpgGK )
125+ allErrs .Add (validateCompositionRefImmutability (oldPg .Spec .CompositionRef .Name , newPg .Spec .CompositionRef .Name ))
126+ return nil , allErrs .Get ()
127+ }
128+
129+ func (x * XVSHNPostgreSQLWebhookHandler ) ValidateDelete (_ context.Context , _ runtime.Object ) (admission.Warnings , error ) {
130+ return nil , nil
131+ }
132+
87133func (p * PostgreSQLWebhookHandler ) ValidateCreate (ctx context.Context , obj runtime.Object ) (admission.Warnings , error ) {
88134 return p .validatePostgreSQL (ctx , obj , nil , true )
89135}
@@ -151,9 +197,7 @@ func (p *PostgreSQLWebhookHandler) validatePostgreSQL(ctx context.Context, newOb
151197
152198 // Do not allow changing compositionRef if it has been set previously.
153199 // When creating a new VSHNPostgresQL, crossplane will automatically set this field if unset.
154- if oldPg .Spec .CompositionRef .Name != "" && newPg .Spec .CompositionRef .Name != oldPg .Spec .CompositionRef .Name {
155- allErrs .Add (field .Forbidden (field .NewPath ("spec" , "compositionRef" ), "compositionRef is immutable" ))
156- }
200+ allErrs .Add (validateCompositionRefImmutability (oldPg .Spec .CompositionRef .Name , newPg .Spec .CompositionRef .Name ))
157201
158202 // Check for disk downsizing
159203 if diskErr := p .DefaultWebhookHandler .ValidateDiskDownsizing (ctx , oldPg , newPg , p .gk .Kind ); diskErr != nil {
@@ -389,6 +433,14 @@ func validatePostgreSQLEncryptionChanges(newEncryption, oldEncryption *vshnv1.VS
389433 return nil
390434}
391435
436+ // validateCompositionRefImmutability returns a Forbidden error if the compositionRef name has changed after being set
437+ func validateCompositionRefImmutability (oldRef , newRef string ) * field.Error {
438+ if oldRef != "" && newRef != oldRef {
439+ return field .Forbidden (field .NewPath ("spec" , "compositionRef" ), "compositionRef is immutable" )
440+ }
441+ return nil
442+ }
443+
392444// validatePinImageTag validates that pinImageTag's major version matches the specified majorVersion
393445func validatePinImageTag (pinImageTag , majorVersion string ) * field.Error {
394446 if pinImageTag == "" {
0 commit comments