Skip to content

Commit 0990317

Browse files
authored
feat!: support setting the username for the relational database (#891)
* Support setting the username for the relational database fixes #889 * update crd+documentation
1 parent 382d327 commit 0990317

File tree

7 files changed

+71
-22
lines changed

7 files changed

+71
-22
lines changed

api/v1alpha1/tenantcontrolplane_types.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ type AddonsSpec struct {
290290
// TenantControlPlaneSpec defines the desired state of TenantControlPlane.
291291
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.dataStore) || has(self.dataStore)", message="unsetting the dataStore is not supported"
292292
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.dataStoreSchema) || has(self.dataStoreSchema)", message="unsetting the dataStoreSchema is not supported"
293+
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.dataStoreUsername) || has(self.dataStoreUsername)", message="unsetting the dataStoreUsername is not supported"
293294
// +kubebuilder:validation:XValidation:rule="!has(self.networkProfile.loadBalancerSourceRanges) || (size(self.networkProfile.loadBalancerSourceRanges) == 0 || self.controlPlane.service.serviceType == 'LoadBalancer')", message="LoadBalancer source ranges are supported only with LoadBalancer service type"
294295
// +kubebuilder:validation:XValidation:rule="!has(self.networkProfile.loadBalancerClass) || self.controlPlane.service.serviceType == 'LoadBalancer'", message="LoadBalancerClass is supported only with LoadBalancer service type"
295296
// +kubebuilder:validation:XValidation:rule="self.controlPlane.service.serviceType != 'LoadBalancer' || (oldSelf.controlPlane.service.serviceType != 'LoadBalancer' && self.controlPlane.service.serviceType == 'LoadBalancer') || has(self.networkProfile.loadBalancerClass) == has(oldSelf.networkProfile.loadBalancerClass)",message="LoadBalancerClass cannot be set or unset at runtime"
@@ -307,8 +308,14 @@ type TenantControlPlaneSpec struct {
307308
// to the user to avoid clashes between different TenantControlPlanes. If not set upon creation, Kamaji will default the
308309
// DataStoreSchema by concatenating the namespace and name of the TenantControlPlane.
309310
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="changing the dataStoreSchema is not supported"
310-
DataStoreSchema string `json:"dataStoreSchema,omitempty"`
311-
ControlPlane ControlPlane `json:"controlPlane"`
311+
DataStoreSchema string `json:"dataStoreSchema,omitempty"`
312+
// DataStoreUsername allows to specify the username of the database (for relational DataStores). This
313+
// value is optional and immutable. Note that Kamaji currently doesn't ensure that DataStoreUsername values are unique. It's up
314+
// to the user to avoid clashes between different TenantControlPlanes. If not set upon creation, Kamaji will default the
315+
// DataStoreUsername by concatenating the namespace and name of the TenantControlPlane.
316+
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="changing the dataStoreUsername is not supported"
317+
DataStoreUsername string `json:"dataStoreUsername,omitempty"`
318+
ControlPlane ControlPlane `json:"controlPlane"`
312319
// Kubernetes specification for tenant control plane
313320
Kubernetes KubernetesSpec `json:"kubernetes"`
314321
// NetworkProfile specifies how the network is

charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6486,6 +6486,16 @@ spec:
64866486
x-kubernetes-validations:
64876487
- message: changing the dataStoreSchema is not supported
64886488
rule: self == oldSelf
6489+
dataStoreUsername:
6490+
description: |-
6491+
DataStoreUsername allows to specify the username of the database (for relational DataStores). This
6492+
value is optional and immutable. Note that Kamaji currently doesn't ensure that DataStoreUsername values are unique. It's up
6493+
to the user to avoid clashes between different TenantControlPlanes. If not set upon creation, Kamaji will default the
6494+
DataStoreUsername by concatenating the namespace and name of the TenantControlPlane.
6495+
type: string
6496+
x-kubernetes-validations:
6497+
- message: changing the dataStoreUsername is not supported
6498+
rule: self == oldSelf
64896499
kubernetes:
64906500
description: Kubernetes specification for tenant control plane
64916501
properties:
@@ -6670,6 +6680,8 @@ spec:
66706680
rule: '!has(oldSelf.dataStore) || has(self.dataStore)'
66716681
- message: unsetting the dataStoreSchema is not supported
66726682
rule: '!has(oldSelf.dataStoreSchema) || has(self.dataStoreSchema)'
6683+
- message: unsetting the dataStoreUsername is not supported
6684+
rule: '!has(oldSelf.dataStoreUsername) || has(self.dataStoreUsername)'
66736685
- message: LoadBalancer source ranges are supported only with LoadBalancer service type
66746686
rule: '!has(self.networkProfile.loadBalancerSourceRanges) || (size(self.networkProfile.loadBalancerSourceRanges) == 0 || self.controlPlane.service.serviceType == ''LoadBalancer'')'
66756687
- message: LoadBalancerClass is supported only with LoadBalancer service type

docs/content/reference/api.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27049,6 +27049,16 @@ to the user to avoid clashes between different TenantControlPlanes. If not set u
2704927049
DataStoreSchema by concatenating the namespace and name of the TenantControlPlane.<br/>
2705027050
</td>
2705127051
<td>false</td>
27052+
</tr><tr>
27053+
<td><b>dataStoreUsername</b></td>
27054+
<td>string</td>
27055+
<td>
27056+
DataStoreUsername allows to specify the username of the database (for relational DataStores). This
27057+
value is optional and immutable. Note that Kamaji currently doesn't ensure that DataStoreUsername values are unique. It's up
27058+
to the user to avoid clashes between different TenantControlPlanes. If not set upon creation, Kamaji will default the
27059+
DataStoreUsername by concatenating the namespace and name of the TenantControlPlane.<br/>
27060+
</td>
27061+
<td>false</td>
2705227062
</tr><tr>
2705327063
<td><b><a href="#tenantcontrolplanespecnetworkprofile">networkProfile</a></b></td>
2705427064
<td>object</td>

internal/resources/datastore/datastore_storage_config.go

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package datastore
66
import (
77
"context"
88
"fmt"
9-
"strings"
109

1110
"github.com/google/uuid"
1211
"github.com/pkg/errors"
@@ -125,19 +124,6 @@ func (r *Config) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.
125124
default:
126125
password = []byte(uuid.New().String())
127126
}
128-
// the coalesce function prioritizes the return value stored in the TenantControlPlane status,
129-
// although this is going to be populated by the UpdateTenantControlPlaneStatus handler of the resource datastore-setup:
130-
// the default value will be used for fresh new configurations, and preserving a previous one:
131-
// this will keep us safe from naming changes cases as occurred with the following commit:
132-
// https://github.com/clastix/kamaji/pull/203/commits/09ce38f489cccca72ab728a259bc8fb2cf6e4770
133-
coalesceFn := func(fromStatus string) []byte {
134-
if len(fromStatus) > 0 {
135-
return []byte(fromStatus)
136-
}
137-
// The dash character (-) must be replaced with an underscore, PostgreSQL is complaining about it:
138-
// https://github.com/clastix/kamaji/issues/328
139-
return []byte(strings.ReplaceAll(fmt.Sprintf("%s_%s", tenantControlPlane.GetNamespace(), tenantControlPlane.GetName()), "-", "_"))
140-
}
141127

142128
finalizersList := sets.New[string](r.resource.GetFinalizers()...)
143129
finalizersList.Insert(finalizers.DatastoreSecretFinalizer)
@@ -161,7 +147,25 @@ func (r *Config) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.
161147
username = u
162148
password = p
163149
} else {
164-
username = coalesceFn(tenantControlPlane.Status.Storage.Setup.User)
150+
// prioritize the username stored in the TenantControlPlane status,
151+
// although this is going to be populated by the UpdateTenantControlPlaneStatus handler of the resource datastore-setup:
152+
// the default value will be used for fresh new configurations, and preserving a previous one:
153+
// this will keep us safe from naming changes cases as occurred with the following commit:
154+
// https://github.com/clastix/kamaji/pull/203/commits/09ce38f489cccca72ab728a259bc8fb2cf6e4770
155+
switch {
156+
case len(tenantControlPlane.Status.Storage.Setup.User) > 0:
157+
// for existing TCPs, the dataStoreSchema will be adopted from the status,
158+
// as the mutating webhook only takes care of TCP creations, not updates
159+
username = []byte(tenantControlPlane.Status.Storage.Setup.User)
160+
tenantControlPlane.Spec.DataStoreUsername = string(username)
161+
case len(tenantControlPlane.Spec.DataStoreUsername) > 0:
162+
// for new TCPs, the spec field will have been provided by the user
163+
// or defaulted by the defaulting webhook
164+
username = []byte(tenantControlPlane.Spec.DataStoreUsername)
165+
default:
166+
// this can only happen on TCP creations when the webhook is not installed
167+
return fmt.Errorf("cannot build datastore storage config, username must either exist in Spec or Status")
168+
}
165169
}
166170

167171
var dataStoreSchema string

internal/resources/datastore/datastore_storage_config_test.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,17 @@ var _ = Describe("DatastoreStorageConfig", func() {
6060
}
6161
})
6262

63-
When("TCP has no dataStoreSchema defined", func() {
63+
When("TCP has neither dataStoreSchema nor dataStoreUsername defined", func() {
6464
It("should return an error", func() {
6565
_, err := resources.Handle(ctx, dsc, tcp)
6666
Expect(err).To(HaveOccurred())
6767
})
6868
})
6969

70-
When("TCP has dataStoreSchema set in spec", func() {
70+
When("TCP has dataStoreSchema and dataStoreUsername set in spec", func() {
7171
BeforeEach(func() {
7272
tcp.Spec.DataStoreSchema = "custom-prefix"
73+
tcp.Spec.DataStoreUsername = "custom-user"
7374
})
7475

7576
It("should create the datastore secret with the schema name from the spec", func() {
@@ -81,17 +82,19 @@ var _ = Describe("DatastoreStorageConfig", func() {
8182
Expect(fakeClient.List(ctx, secrets)).To(Succeed())
8283
Expect(secrets.Items).To(HaveLen(1))
8384
Expect(secrets.Items[0].Data["DB_SCHEMA"]).To(Equal([]byte("custom-prefix")))
85+
Expect(secrets.Items[0].Data["DB_USER"]).To(Equal([]byte("custom-user")))
8486
})
8587
})
8688

87-
When("TCP has dataStoreSchema set in status, but not in spec", func() {
89+
When("TCP has dataStoreSchema and dataStoreUsername set in status, but not in spec", func() {
8890
// this test case ensures that existing TCPs (created in a CRD version without
8991
// the dataStoreSchema field) correctly adopt the spec field from the status.
9092

9193
It("should create the datastore secret with the correct schema name and update the TCP spec", func() {
9294
By("updating the TCP status")
9395
Expect(fakeClient.Get(ctx, client.ObjectKeyFromObject(tcp), tcp)).To(Succeed())
9496
tcp.Status.Storage.Setup.Schema = "existing-schema-name"
97+
tcp.Status.Storage.Setup.User = "existing-username"
9598
Expect(fakeClient.Status().Update(ctx, tcp)).To(Succeed())
9699

97100
By("handling the resource")
@@ -104,12 +107,14 @@ var _ = Describe("DatastoreStorageConfig", func() {
104107
Expect(fakeClient.List(ctx, secrets)).To(Succeed())
105108
Expect(secrets.Items).To(HaveLen(1))
106109
Expect(secrets.Items[0].Data["DB_SCHEMA"]).To(Equal([]byte("existing-schema-name")))
110+
Expect(secrets.Items[0].Data["DB_USER"]).To(Equal([]byte("existing-username")))
107111

108112
By("checking the TCP spec")
109113
// we have to check the modified struct here (instead of retrieving the object
110114
// via the fakeClient), as the TCP resource update is not done by the resources.
111115
// Instead, the TCP controller will handle TCP updates after handling all resources
112116
tcp.Spec.DataStoreSchema = "existing-schema-name"
117+
tcp.Spec.DataStoreUsername = "existing-username"
113118
})
114119
})
115120
})

internal/webhook/handlers/tcp_defaults.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,11 @@ func (t TenantControlPlaneDefaults) defaultUnsetFields(tcp *kamajiv1alpha1.Tenan
7676
dss := strings.ReplaceAll(fmt.Sprintf("%s_%s", tcp.GetNamespace(), tcp.GetName()), "-", "_")
7777
tcp.Spec.DataStoreSchema = dss
7878
}
79+
80+
if len(tcp.Spec.DataStoreUsername) == 0 {
81+
// The dash character (-) must be replaced with an underscore, PostgreSQL is complaining about it:
82+
// https://github.com/clastix/kamaji/issues/328
83+
username := strings.ReplaceAll(fmt.Sprintf("%s_%s", tcp.GetNamespace(), tcp.GetName()), "-", "_")
84+
tcp.Spec.DataStoreUsername = username
85+
}
7986
}

internal/webhook/handlers/tcp_defaults_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ var _ = Describe("TCP Defaulting Webhook", func() {
4949
It("should issue all required patches", func() {
5050
ops, err := t.OnCreate(tcp)(ctx, admission.Request{})
5151
Expect(err).ToNot(HaveOccurred())
52-
Expect(ops).To(HaveLen(3))
52+
Expect(ops).To(HaveLen(4))
5353
})
5454

5555
It("should default the dataStore", func() {
@@ -60,19 +60,23 @@ var _ = Describe("TCP Defaulting Webhook", func() {
6060
))
6161
})
6262

63-
It("should default the dataStoreSchema to the expected value", func() {
63+
It("should default the dataStoreSchema and dataStoreUsername to the expected value", func() {
6464
ops, err := t.OnCreate(tcp)(ctx, admission.Request{})
6565
Expect(err).ToNot(HaveOccurred())
6666
Expect(ops).To(ContainElement(
6767
jsonpatch.Operation{Operation: "add", Path: "/spec/dataStoreSchema", Value: "default_tcp"},
6868
))
69+
Expect(ops).To(ContainElement(
70+
jsonpatch.Operation{Operation: "add", Path: "/spec/dataStoreUsername", Value: "default_tcp"},
71+
))
6972
})
7073
})
7174

7275
Describe("fields are already set", func() {
7376
BeforeEach(func() {
7477
tcp.Spec.DataStore = "etcd"
7578
tcp.Spec.DataStoreSchema = "my_tcp"
79+
tcp.Spec.DataStoreUsername = "my_tcp"
7680
tcp.Spec.ControlPlane.Deployment.Replicas = ptr.To(int32(2))
7781
})
7882

0 commit comments

Comments
 (0)