Skip to content

Commit 90b2c37

Browse files
Merge pull request #592 from vshn/feat/keycloak-nested-cnpg
Use CNPG as nested service for Keycloak
2 parents 4c4e289 + fe99d41 commit 90b2c37

File tree

10 files changed

+441
-138
lines changed

10 files changed

+441
-138
lines changed

cmd/controller.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,10 @@ func setupWebhooks(mgr manager.Manager, withQuota bool, withAppcatWebhooks bool,
196196
if err != nil {
197197
return err
198198
}
199+
err = webhooks.SetupXVSHNPostgreSQLWebhookHandlerWithManager(mgr)
200+
if err != nil {
201+
return err
202+
}
199203
err = webhooks.SetupRedisWebhookHandlerWithManager(mgr, withQuota)
200204
if err != nil {
201205
return err

config/controller/webhooks.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,3 +324,22 @@ webhooks:
324324
resources:
325325
- xobjectbuckets
326326
sideEffects: None
327+
- admissionReviewVersions:
328+
- v1
329+
clientConfig:
330+
service:
331+
name: webhook-service
332+
namespace: system
333+
path: /validate-vshn-appcat-vshn-io-v1-xvshnpostgresql
334+
failurePolicy: Fail
335+
name: xvshnpostgresql.vshn.appcat.vshn.io
336+
rules:
337+
- apiGroups:
338+
- vshn.appcat.vshn.io
339+
apiVersions:
340+
- v1
341+
operations:
342+
- UPDATE
343+
resources:
344+
- xvshnpostgresqls
345+
sideEffects: None

pkg/comp-functions/functions/common/postgresql.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,20 @@ func (a *PostgreSQLDependencyBuilder) CreateDependency() (string, error) {
162162
// and would therefore override any value we set before the merge.
163163
params.Instances = a.comp.GetInstances()
164164

165+
// Preserve the compositionRef of an already-existing nested PostgreSQL composite.
166+
compositionName := a.svc.Config.Data["defaultPGComposition"]
167+
observedPg := &vshnv1.XVSHNPostgreSQL{}
168+
err := a.svc.GetObservedComposedResource(observedPg, a.comp.GetName()+PgInstanceNameSuffix)
169+
if err != nil && err != runtime.ErrNotFound {
170+
return "", fmt.Errorf("couldn't read observed nested postgresql composite: %w", err)
171+
}
172+
if err == nil {
173+
if observedPg.Spec.CompositionRef.Name == "" {
174+
return "", fmt.Errorf("observed nested postgresql composite %q has no compositionRef, refusing to overwrite with default", observedPg.GetName())
175+
}
176+
compositionName = observedPg.Spec.CompositionRef.Name
177+
}
178+
165179
// We have to ignore the providerconfig on the composite itself.
166180
pg := &vshnv1.XVSHNPostgreSQL{
167181
ObjectMeta: metav1.ObjectMeta{
@@ -173,7 +187,7 @@ func (a *PostgreSQLDependencyBuilder) CreateDependency() (string, error) {
173187
Spec: vshnv1.XVSHNPostgreSQLSpec{
174188
Parameters: *params,
175189
CompositionRef: v1.CompositionReference{
176-
Name: a.svc.Config.Data["defaultPGComposition"],
190+
Name: compositionName,
177191
},
178192
ResourceSpec: xpv1.ResourceSpec{
179193
WriteConnectionSecretToReference: &xpv1.SecretReference{
@@ -189,7 +203,7 @@ func (a *PostgreSQLDependencyBuilder) CreateDependency() (string, error) {
189203
pg.Labels[runtime.ProviderConfigLabel] = v
190204
}
191205

192-
err := CustomCreateNetworkPolicy([]string{a.comp.GetInstanceNamespace()}, pg.GetInstanceNamespace(), pg.GetName()+"-"+a.comp.GetServiceName(), "", false, a.svc)
206+
err = CustomCreateNetworkPolicy([]string{a.comp.GetInstanceNamespace()}, pg.GetInstanceNamespace(), pg.GetName()+"-"+a.comp.GetServiceName(), "", false, a.svc)
193207
if err != nil {
194208
return "", err
195209
}

pkg/comp-functions/functions/vshnpostgrescnpg/connection_details.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,21 @@ func AddConnectionSecrets(ctx context.Context, comp *vshnv1.VSHNPostgreSQL, svc
4040
return runtime.NewWarningResult(fmt.Sprintf("Couldn't set connection details: %v", err))
4141
}
4242

43+
host := fmt.Sprintf("postgresql-rw.%s.svc.cluster.local", comp.GetInstanceNamespace())
44+
svc.SetConnectionDetail(PostgresqlHost, []byte(host))
45+
46+
// CNPG's uri field uses a bare hostname and a wildcard '*' database, which most
47+
// clients reject. Reconstruct the URL from the already-correct individual fields.
48+
cd := svc.GetConnectionDetails()
49+
user := string(cd[PostgresqlUser])
50+
password := string(cd[PostgresqlPassword])
51+
port := string(cd[PostgresqlPort])
52+
db := string(cd[PostgresqlDb])
53+
if user != "" && password != "" && port != "" {
54+
svc.SetConnectionDetail(PostgresqlURL, []byte(fmt.Sprintf("postgresql://%s:%s@%s:%s/%s",
55+
user, password, host, port, db)))
56+
}
57+
4358
return nil
4459
}
4560

pkg/controller/webhooks/postgresql.go

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ 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 XVSHNPostgreSQL composite from having its compositionRef changed once set.
28+
//+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
29+
2730
//RBAC
2831
//+kubebuilder:rbac:groups=vshn.appcat.vshn.io,resources=xvshnpostgresqls,verbs=get;list;watch;patch;update
2932
//+kubebuilder:rbac:groups=vshn.appcat.vshn.io,resources=xvshnpostgresqls/status,verbs=get;list;watch;patch;update
@@ -38,10 +41,12 @@ const (
3841
)
3942

4043
var (
41-
pgGK = schema.GroupKind{Group: "vshn.appcat.vshn.io", Kind: "VSHNPostgreSQL"}
42-
pgGR = schema.GroupResource{Group: pgGK.Group, Resource: "vshnpostgresqls"}
44+
pgGK = schema.GroupKind{Group: "vshn.appcat.vshn.io", Kind: "VSHNPostgreSQL"}
45+
pgGR = schema.GroupResource{Group: pgGK.Group, Resource: "vshnpostgresqls"}
46+
xpgGK = schema.GroupKind{Group: "vshn.appcat.vshn.io", Kind: "XVSHNPostgreSQL"}
4347

4448
_ webhook.CustomValidator = &PostgreSQLWebhookHandler{}
49+
_ webhook.CustomValidator = &XVSHNPostgreSQLWebhookHandler{}
4550

4651
blocklist = map[string]string{
4752
"listen_addresses": "",
@@ -84,6 +89,46 @@ func SetupPostgreSQLWebhookHandlerWithManager(mgr ctrl.Manager, withQuota bool)
8489
Complete()
8590
}
8691

92+
// XVSHNPostgreSQLWebhookHandler validates the nested XVSHNPostgreSQL composite resource.
93+
type XVSHNPostgreSQLWebhookHandler struct{}
94+
95+
// SetupXVSHNPostgreSQLWebhookHandlerWithManager registers the XVSHNPostgreSQL validation webhook.
96+
func SetupXVSHNPostgreSQLWebhookHandlerWithManager(mgr ctrl.Manager) error {
97+
return ctrl.NewWebhookManagedBy(mgr).
98+
For(&vshnv1.XVSHNPostgreSQL{}).
99+
WithValidator(&XVSHNPostgreSQLWebhookHandler{}).
100+
Complete()
101+
}
102+
103+
func (x *XVSHNPostgreSQLWebhookHandler) ValidateCreate(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
104+
return nil, nil
105+
}
106+
107+
func (x *XVSHNPostgreSQLWebhookHandler) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
108+
oldPg, ok := oldObj.(*vshnv1.XVSHNPostgreSQL)
109+
if !ok {
110+
return nil, fmt.Errorf("provided manifest is not a valid XVSHNPostgreSQL object")
111+
}
112+
newPg, ok := newObj.(*vshnv1.XVSHNPostgreSQL)
113+
if !ok {
114+
return nil, fmt.Errorf("provided manifest is not a valid XVSHNPostgreSQL object")
115+
}
116+
117+
if newPg.DeletionTimestamp != nil {
118+
return nil, nil
119+
}
120+
121+
allErrs := newFielErrors(newPg.Name, xpgGK)
122+
if err := validateCompositionRefImmutability(oldPg.Spec.CompositionRef.Name, newPg.Spec.CompositionRef.Name); err != nil {
123+
allErrs.Add(err)
124+
}
125+
return nil, allErrs.Get()
126+
}
127+
128+
func (x *XVSHNPostgreSQLWebhookHandler) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
129+
return nil, nil
130+
}
131+
87132
func (p *PostgreSQLWebhookHandler) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
88133
return p.validatePostgreSQL(ctx, obj, nil, true)
89134
}
@@ -151,8 +196,8 @@ func (p *PostgreSQLWebhookHandler) validatePostgreSQL(ctx context.Context, newOb
151196

152197
// Do not allow changing compositionRef if it has been set previously.
153198
// 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"))
199+
if err := validateCompositionRefImmutability(oldPg.Spec.CompositionRef.Name, newPg.Spec.CompositionRef.Name); err != nil {
200+
allErrs.Add(err)
156201
}
157202

158203
// Check for disk downsizing
@@ -389,6 +434,14 @@ func validatePostgreSQLEncryptionChanges(newEncryption, oldEncryption *vshnv1.VS
389434
return nil
390435
}
391436

437+
// validateCompositionRefImmutability returns a Forbidden error if the compositionRef name has changed after being set
438+
func validateCompositionRefImmutability(oldRef, newRef string) *field.Error {
439+
if oldRef != "" && newRef != oldRef {
440+
return field.Forbidden(field.NewPath("spec", "compositionRef"), "compositionRef is immutable")
441+
}
442+
return nil
443+
}
444+
392445
// validatePinImageTag validates that pinImageTag's major version matches the specified majorVersion
393446
func validatePinImageTag(pinImageTag, majorVersion string) *field.Error {
394447
if pinImageTag == "" {

pkg/controller/webhooks/postgresql_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,97 @@ func TestPostgreSQLWebhookHandler_ValidateCreateWithPinImageTag(t *testing.T) {
732732
assert.NoError(t, err)
733733
}
734734

735+
func TestValidateCompositionRefImmutability(t *testing.T) {
736+
tests := []struct {
737+
name string
738+
oldRef string
739+
newRef string
740+
expectErr bool
741+
}{
742+
{
743+
name: "GivenBothEmpty_ThenNoError",
744+
oldRef: "",
745+
newRef: "",
746+
expectErr: false,
747+
},
748+
{
749+
name: "GivenOldEmptyNewSet_ThenNoError",
750+
oldRef: "",
751+
newRef: "vshnpostgrescnpg.vshn.appcat.vshn.io",
752+
expectErr: false,
753+
},
754+
{
755+
name: "GivenSameRef_ThenNoError",
756+
oldRef: "vshnpostgres.vshn.appcat.vshn.io",
757+
newRef: "vshnpostgres.vshn.appcat.vshn.io",
758+
expectErr: false,
759+
},
760+
{
761+
name: "GivenOldSetNewDifferent_ThenError",
762+
oldRef: "vshnpostgres.vshn.appcat.vshn.io",
763+
newRef: "vshnpostgrescnpg.vshn.appcat.vshn.io",
764+
expectErr: true,
765+
},
766+
{
767+
name: "GivenOldSetNewEmpty_ThenError",
768+
oldRef: "vshnpostgres.vshn.appcat.vshn.io",
769+
newRef: "",
770+
expectErr: true,
771+
},
772+
}
773+
for _, tt := range tests {
774+
t.Run(tt.name, func(t *testing.T) {
775+
err := validateCompositionRefImmutability(tt.oldRef, tt.newRef)
776+
if tt.expectErr {
777+
assert.NotNil(t, err)
778+
assert.Contains(t, err.Error(), "compositionRef is immutable")
779+
} else {
780+
assert.Nil(t, err)
781+
}
782+
})
783+
}
784+
}
785+
786+
func TestXVSHNPostgreSQLWebhookHandler_ValidateUpdate(t *testing.T) {
787+
ctx := context.TODO()
788+
handler := XVSHNPostgreSQLWebhookHandler{}
789+
790+
base := &vshnv1.XVSHNPostgreSQL{
791+
ObjectMeta: metav1.ObjectMeta{
792+
Name: "keycloak-pg",
793+
Namespace: "vshn-keycloak-myinstance",
794+
},
795+
}
796+
797+
// Allow: compositionRef not yet set on either side
798+
_, err := handler.ValidateUpdate(ctx, base, base.DeepCopy())
799+
assert.NoError(t, err)
800+
801+
// Allow: first assignment (old empty, new set)
802+
withRef := base.DeepCopy()
803+
withRef.Spec.CompositionRef.Name = "vshnpostgrescnpg.vshn.appcat.vshn.io"
804+
_, err = handler.ValidateUpdate(ctx, base, withRef)
805+
assert.NoError(t, err)
806+
807+
// Allow: compositionRef unchanged
808+
_, err = handler.ValidateUpdate(ctx, withRef, withRef.DeepCopy())
809+
assert.NoError(t, err)
810+
811+
// Reject: compositionRef changed on existing instance
812+
changedRef := base.DeepCopy()
813+
changedRef.Spec.CompositionRef.Name = "vshnpostgres.vshn.appcat.vshn.io"
814+
_, err = handler.ValidateUpdate(ctx, withRef, changedRef)
815+
assert.Error(t, err)
816+
assert.Contains(t, err.Error(), "compositionRef is immutable")
817+
818+
// Allow: object is being deleted even if compositionRef would change
819+
deletingObj := changedRef.DeepCopy()
820+
now := metav1.Now()
821+
deletingObj.DeletionTimestamp = &now
822+
_, err = handler.ValidateUpdate(ctx, withRef, deletingObj)
823+
assert.NoError(t, err)
824+
}
825+
735826
// disabling this temporarily
736827
// func TestPostgreSQLWebhookHandler_ValidateMajorVersionUpgrade(t *testing.T) {
737828
// tests := []struct {

pkg/maintenance/postgresqlcnpg.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
cnpgv1 "github.com/vshn/appcat/v4/apis/cnpg/v1"
2020
vshnv1 "github.com/vshn/appcat/v4/apis/vshn/v1"
2121
corev1 "k8s.io/api/core/v1"
22+
apierrors "k8s.io/apimachinery/pkg/api/errors"
2223
"sigs.k8s.io/controller-runtime/pkg/client"
2324
)
2425

@@ -67,17 +68,17 @@ func (p *PostgreSQLCNPG) RunBackup(ctx context.Context) error {
6768
func (p *PostgreSQLCNPG) DoMaintenance(ctx context.Context) error {
6869
p.log.Info("Starting maintenance on postgresql instance")
6970

70-
claim := &vshnv1.VSHNPostgreSQL{}
71-
if err := p.k8sClient.Get(ctx, client.ObjectKey{Name: p.claimName, Namespace: p.claimNamespace}, claim); err != nil {
72-
return fmt.Errorf("couldn't get claim: %w", err)
71+
comp := &vshnv1.XVSHNPostgreSQL{}
72+
if err := p.k8sClient.Get(ctx, client.ObjectKey{Name: p.compName}, comp); err != nil {
73+
return fmt.Errorf("couldn't get composite: %w", err)
7374
}
7475

7576
instanceCluster, err := p.getCompositeCluster(ctx)
7677
if err != nil {
7778
return fmt.Errorf("couldn't get instance cluster: %w", err)
7879
}
7980

80-
version, err := strconv.Atoi(claim.Spec.Parameters.Service.MajorVersion)
81+
version, err := strconv.Atoi(comp.Spec.Parameters.Service.MajorVersion)
8182
if err != nil {
8283
return fmt.Errorf("cannot parse postgresql major version: %w", err)
8384
}
@@ -102,7 +103,6 @@ func (p *PostgreSQLCNPG) DoMaintenance(ctx context.Context) error {
102103
// EOL handling
103104

104105
if isEol := p.isEOL(version, latestCatalog); isEol {
105-
p.log.Info("Setting EOL on claim")
106106
if err := p.setEOLStatus(ctx); err != nil {
107107
return fmt.Errorf("couldn't set EOL status on claim: %w", err)
108108
}
@@ -270,11 +270,15 @@ func (p *PostgreSQLCNPG) isEOL(currentVersion int, imageCatalog *cnpgv1.ImageCat
270270
func (p *PostgreSQLCNPG) setEOLStatus(ctx context.Context) error {
271271
claim := &vshnv1.VSHNPostgreSQL{}
272272
err := p.k8sClient.Get(ctx, client.ObjectKey{Name: p.claimName, Namespace: p.claimNamespace}, claim)
273+
if apierrors.IsNotFound(err) {
274+
// There's no claim for nested services
275+
p.log.Info("VSHNPostgreSQL claim not found, skipping EOL status update")
276+
return nil
277+
}
273278
if err != nil {
274279
return err
275280
}
276281

277282
claim.Status.IsEOL = true
278-
279-
return p.k8sClient.Update(ctx, claim)
283+
return p.k8sClient.Status().Update(ctx, claim)
280284
}

0 commit comments

Comments
 (0)