diff --git a/internal/resources/datastore/datastore_storage_config.go b/internal/resources/datastore/datastore_storage_config.go index 1b7622cd..08cc884b 100644 --- a/internal/resources/datastore/datastore_storage_config.go +++ b/internal/resources/datastore/datastore_storage_config.go @@ -155,9 +155,25 @@ func (r *Config) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1. username = coalesceFn(tenantControlPlane.Status.Storage.Setup.User) } + var dataStoreSchema string + switch { + case len(tenantControlPlane.Spec.DataStoreSchema) > 0: + // for new TCPs, the spec field will have been provided by the user + // or defaulted by the defaulting webhook + dataStoreSchema = tenantControlPlane.Spec.DataStoreSchema + case len(tenantControlPlane.Status.Storage.Setup.Schema) > 0: + // for existing TCPs, the dataStoreSchema will be adopted from the status, + // as the mutating webhook only takes care of TCP creations, not updates + dataStoreSchema = tenantControlPlane.Status.Storage.Setup.Schema + tenantControlPlane.Spec.DataStoreSchema = dataStoreSchema + default: + // this can only happen on TCP creations when the webhook is not installed + return fmt.Errorf("cannot build datastore storage config, schema name must either exist in Spec or Status") + } + r.resource.Data = map[string][]byte{ "DB_CONNECTION_STRING": []byte(r.ConnString), - "DB_SCHEMA": []byte(tenantControlPlane.Spec.DataStoreSchema), + "DB_SCHEMA": []byte(dataStoreSchema), "DB_USER": username, "DB_PASSWORD": password, } diff --git a/internal/resources/datastore/datastore_storage_config_test.go b/internal/resources/datastore/datastore_storage_config_test.go new file mode 100644 index 00000000..84754c08 --- /dev/null +++ b/internal/resources/datastore/datastore_storage_config_test.go @@ -0,0 +1,115 @@ +// Copyright 2022 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package datastore_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1" + "github.com/clastix/kamaji/internal/resources" + "github.com/clastix/kamaji/internal/resources/datastore" +) + +var _ = Describe("DatastoreStorageConfig", func() { + var ( + ctx context.Context + dsc *datastore.Config + tcp *kamajiv1alpha1.TenantControlPlane + ds *kamajiv1alpha1.DataStore + ) + + BeforeEach(func() { + ctx = context.Background() + + tcp = &kamajiv1alpha1.TenantControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tcp", + Namespace: "default", + }, + Spec: kamajiv1alpha1.TenantControlPlaneSpec{}, + } + + ds = &kamajiv1alpha1.DataStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: "datastore", + Namespace: "default", + }, + } + + Expect(kamajiv1alpha1.AddToScheme(scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme)).To(Succeed()) + }) + + JustBeforeEach(func() { + fakeClient = fake.NewClientBuilder(). + WithScheme(scheme).WithObjects(tcp).WithStatusSubresource(tcp).Build() + + dsc = &datastore.Config{ + Client: fakeClient, + ConnString: "", + DataStore: *ds, + } + }) + + When("TCP has no dataStoreSchema defined", func() { + It("should return an error", func() { + _, err := resources.Handle(ctx, dsc, tcp) + Expect(err).To(HaveOccurred()) + }) + }) + + When("TCP has dataStoreSchema set in spec", func() { + BeforeEach(func() { + tcp.Spec.DataStoreSchema = "custom-prefix" + }) + + It("should create the datastore secret with the schema name from the spec", func() { + op, err := resources.Handle(ctx, dsc, tcp) + Expect(err).ToNot(HaveOccurred()) + Expect(op).To(Equal(controllerutil.OperationResultCreated)) + + secrets := &corev1.SecretList{} + Expect(fakeClient.List(ctx, secrets)).To(Succeed()) + Expect(secrets.Items).To(HaveLen(1)) + Expect(secrets.Items[0].Data["DB_SCHEMA"]).To(Equal([]byte("custom-prefix"))) + }) + }) + + When("TCP has dataStoreSchema set in status, but not in spec", func() { + // this test case ensures that existing TCPs (created in a CRD version without + // the dataStoreSchema field) correctly adopt the spec field from the status. + + It("should create the datastore secret with the correct schema name and update the TCP spec", func() { + By("updating the TCP status") + Expect(fakeClient.Get(ctx, client.ObjectKeyFromObject(tcp), tcp)).To(Succeed()) + tcp.Status.Storage.Setup.Schema = "existing-schema-name" + Expect(fakeClient.Status().Update(ctx, tcp)).To(Succeed()) + + By("handling the resource") + op, err := resources.Handle(ctx, dsc, tcp) + Expect(err).ToNot(HaveOccurred()) + Expect(op).To(Equal(controllerutil.OperationResultCreated)) + + By("checking the secret") + secrets := &corev1.SecretList{} + Expect(fakeClient.List(ctx, secrets)).To(Succeed()) + Expect(secrets.Items).To(HaveLen(1)) + Expect(secrets.Items[0].Data["DB_SCHEMA"]).To(Equal([]byte("existing-schema-name"))) + + By("checking the TCP spec") + // we have to check the modified struct here (instead of retrieving the object + // via the fakeClient), as the TCP resource update is not done by the resources. + // Instead, the TCP controller will handle TCP updates after handling all resources + tcp.Spec.DataStoreSchema = "existing-schema-name" + }) + }) +}) diff --git a/internal/resources/datastore/datastore_suite_test.go b/internal/resources/datastore/datastore_suite_test.go new file mode 100644 index 00000000..5cff8155 --- /dev/null +++ b/internal/resources/datastore/datastore_suite_test.go @@ -0,0 +1,23 @@ +// Copyright 2022 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package datastore_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + fakeClient client.Client + scheme *runtime.Scheme = runtime.NewScheme() +) + +func TestDatastore(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Datastore Suite") +}