From 9a77e34f0acd7cf5c26fce434a8c23a99bd3bdb5 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 4 Dec 2023 16:03:54 +0100 Subject: [PATCH 01/31] Add tests highlighting the expectations --- controllers/backstage_controller_test.go | 1058 +++++++++++++++++----- 1 file changed, 813 insertions(+), 245 deletions(-) diff --git a/controllers/backstage_controller_test.go b/controllers/backstage_controller_test.go index 9ec82f08..3a42f08b 100644 --- a/controllers/backstage_controller_test.go +++ b/controllers/backstage_controller_test.go @@ -27,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -124,19 +125,44 @@ var _ = Describe("Backstage controller", func() { } findEnvVar := func(envVars []corev1.EnvVar, key string) (corev1.EnvVar, bool) { - return findElementByPredicate(envVars, func(envVar corev1.EnvVar) bool { + list := findElementsByPredicate(envVars, func(envVar corev1.EnvVar) bool { return envVar.Name == key }) + if len(list) == 0 { + return corev1.EnvVar{}, false + } + return list[0], true + } + + findEnvVarFrom := func(envVars []corev1.EnvFromSource, key string) (corev1.EnvFromSource, bool) { + list := findElementsByPredicate(envVars, func(envVar corev1.EnvFromSource) bool { + var n string + switch { + case envVar.ConfigMapRef != nil: + n = envVar.ConfigMapRef.Name + case envVar.SecretRef != nil: + n = envVar.SecretRef.Name + } + return n == key + }) + if len(list) == 0 { + return corev1.EnvFromSource{}, false + } + return list[0], true } findVolume := func(vols []corev1.Volume, name string) (corev1.Volume, bool) { - return findElementByPredicate(vols, func(vol corev1.Volume) bool { + list := findElementsByPredicate(vols, func(vol corev1.Volume) bool { return vol.Name == name }) + if len(list) == 0 { + return corev1.Volume{}, false + } + return list[0], true } - findVolumeMount := func(mounts []corev1.VolumeMount, name string) (corev1.VolumeMount, bool) { - return findElementByPredicate(mounts, func(mount corev1.VolumeMount) bool { + findVolumeMounts := func(mounts []corev1.VolumeMount, name string) []corev1.VolumeMount { + return findElementsByPredicate(mounts, func(mount corev1.VolumeMount) bool { return mount.Name == name }) } @@ -162,6 +188,14 @@ var _ = Describe("Backstage controller", func() { }) Expect(err).To(Not(HaveOccurred())) + By("creating a StatefulSet for the Database") + Eventually(func(g Gomega) { + found := &appsv1.StatefulSet{} + name := fmt.Sprintf("backstage-psql-%s", backstage.Name) + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, found) + g.Expect(err).ShouldNot(HaveOccurred()) + }, time.Minute, time.Second).Should(Succeed()) + By("Generating a value for backend auth secret key") Eventually(func(g Gomega) { found := &corev1.Secret{} @@ -192,6 +226,9 @@ var _ = Describe("Backstage controller", func() { return k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, found) }, time.Minute, time.Second).Should(Succeed()) + By("checking the number of replicas") + Expect(found.Spec.Replicas).To(HaveValue(BeEquivalentTo(1))) + By("Checking that the Deployment is configured with a random backend auth secret") backendSecretEnvVar, ok := findEnvVar(found.Spec.Template.Spec.Containers[0].Env, "BACKEND_SECRET") Expect(ok).To(BeTrue(), "env var BACKEND_SECRET not found in main container") @@ -235,25 +272,23 @@ var _ = Describe("Backstage controller", func() { By("Checking the Init Container Volume Mounts in the Backstage Deployment", func() { Expect(initCont.VolumeMounts).To(HaveLen(3)) - dpRoot, ok := findVolumeMount(initCont.VolumeMounts, "dynamic-plugins-root") - Expect(ok).To(BeTrue(), - "No volume mount found with name: dynamic-plugins-root") - Expect(dpRoot.MountPath).To(Equal("/dynamic-plugins-root")) - Expect(dpRoot.ReadOnly).To(BeFalse()) - Expect(dpRoot.SubPath).To(BeEmpty()) - - dpNpmrc, ok := findVolumeMount(initCont.VolumeMounts, "dynamic-plugins-npmrc") - Expect(ok).To(BeTrue(), - "No volume mount found with name: dynamic-plugins-npmrc") - Expect(dpNpmrc.MountPath).To(Equal("/opt/app-root/src/.npmrc.dynamic-plugins")) - Expect(dpNpmrc.ReadOnly).To(BeTrue()) - Expect(dpNpmrc.SubPath).To(Equal(".npmrc")) - - dp, ok := findVolumeMount(initCont.VolumeMounts, dynamicPluginsConfigName) - Expect(ok).To(BeTrue(), "No volume mount found with name: %s", dynamicPluginsConfigName) - Expect(dp.MountPath).To(Equal("/opt/app-root/src/dynamic-plugins.yaml")) - Expect(dp.SubPath).To(Equal("dynamic-plugins.yaml")) - Expect(dp.ReadOnly).To(BeTrue()) + dpRoot := findVolumeMounts(initCont.VolumeMounts, "dynamic-plugins-root") + Expect(dpRoot).To(HaveLen(1), "No volume mount found with name: dynamic-plugins-root") + Expect(dpRoot[0].MountPath).To(Equal("/dynamic-plugins-root")) + Expect(dpRoot[0].ReadOnly).To(BeFalse()) + Expect(dpRoot[0].SubPath).To(BeEmpty()) + + dpNpmrc := findVolumeMounts(initCont.VolumeMounts, "dynamic-plugins-npmrc") + Expect(dpNpmrc).To(HaveLen(1), "No volume mount found with name: dynamic-plugins-npmrc") + Expect(dpNpmrc[0].MountPath).To(Equal("/opt/app-root/src/.npmrc.dynamic-plugins")) + Expect(dpNpmrc[0].ReadOnly).To(BeTrue()) + Expect(dpNpmrc[0].SubPath).To(Equal(".npmrc")) + + dp := findVolumeMounts(initCont.VolumeMounts, dynamicPluginsConfigName) + Expect(dp).To(HaveLen(1), "No volume mount found with name: %s", dynamicPluginsConfigName) + Expect(dp[0].MountPath).To(Equal("/opt/app-root/src/dynamic-plugins.yaml")) + Expect(dp[0].SubPath).To(Equal("dynamic-plugins.yaml")) + Expect(dp[0].ReadOnly).To(BeTrue()) }) By("Checking the Number of main containers in the Backstage Deployment") @@ -269,10 +304,10 @@ var _ = Describe("Backstage controller", func() { By("Checking the main container Volume Mounts in the Backstage Deployment", func() { Expect(mainCont.VolumeMounts).To(HaveLen(1)) - dpRoot, ok := findVolumeMount(mainCont.VolumeMounts, "dynamic-plugins-root") - Expect(ok).To(BeTrue(), "No volume mount found with name: dynamic-plugins-root") - Expect(dpRoot.MountPath).To(Equal("/opt/app-root/src/dynamic-plugins-root")) - Expect(dpRoot.SubPath).To(BeEmpty()) + dpRoot := findVolumeMounts(mainCont.VolumeMounts, "dynamic-plugins-root") + Expect(dpRoot).To(HaveLen(1), "No volume mount found with name: dynamic-plugins-root") + Expect(dpRoot[0].MountPath).To(Equal("/opt/app-root/src/dynamic-plugins-root")) + Expect(dpRoot[0].SubPath).To(BeEmpty()) }) By("Checking the latest Status added to the Backstage instance") @@ -429,11 +464,22 @@ spec: var backstage *bsv1alpha1.Backstage BeforeEach(func() { + var item bsv1alpha1.AppConfigItem + name := "a-non-existing-" + strings.ToLower(kind) + switch kind { + case "ConfigMap": + item = bsv1alpha1.AppConfigItem{ + ConfigMapRef: &bsv1alpha1.Ref{Name: name}, + } + case "Secret": + item = bsv1alpha1.AppConfigItem{ + SecretRef: &bsv1alpha1.Ref{Name: name}, + } + } backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - AppConfigs: []bsv1alpha1.AppConfigRef{ - { - Name: "a-non-existing-" + strings.ToLower(kind), - Kind: kind, + Backstage: &bsv1alpha1.BackstageSpecBackstage{ + AppConfig: &bsv1alpha1.AppConfig{ + Items: []bsv1alpha1.AppConfigItem{item}, }, }, }) @@ -463,231 +509,271 @@ spec: }) } - for _, dynamicPluginsConfigKind := range []string{"ConfigMap", "Secret"} { - dynamicPluginsConfigKind := dynamicPluginsConfigKind - When("referencing ConfigMaps and Secrets for app-configs and dynamic plugins config as "+dynamicPluginsConfigKind, func() { - const ( - appConfig1CmName = "my-app-config-1-cm" - appConfig2SecretName = "my-app-config-2-secret" - dynamicPluginsConfigName = "my-dynamic-plugins-config" - ) - - var backstage *bsv1alpha1.Backstage - - BeforeEach(func() { - appConfig1Cm := buildConfigMap(appConfig1CmName, map[string]string{ - "my-app-config-11.yaml": ` + for _, mountPath := range []string{"", "/some/path/for/app-config"} { + mountPath := mountPath + for _, dynamicPluginsConfigKind := range []string{"ConfigMap", "Secret"} { + dynamicPluginsConfigKind := dynamicPluginsConfigKind + When(fmt.Sprintf("referencing ConfigMaps and Secrets for app-configs (mountPath=%q) and dynamic plugins config as %s", mountPath, dynamicPluginsConfigKind), + func() { + const ( + appConfig1CmName = "my-app-config-1-cm" + appConfig2SecretName = "my-app-config-2-secret" + dynamicPluginsConfigName = "my-dynamic-plugins-config" + ) + + var backstage *bsv1alpha1.Backstage + + BeforeEach(func() { + appConfig1Cm := buildConfigMap(appConfig1CmName, map[string]string{ + "my-app-config-11.yaml": ` # my-app-config-11.yaml `, - "my-app-config-12.yaml": ` + "my-app-config-12.yaml": ` # my-app-config-12.yaml `, - }) - err := k8sClient.Create(ctx, appConfig1Cm) - Expect(err).To(Not(HaveOccurred())) + }) + err := k8sClient.Create(ctx, appConfig1Cm) + Expect(err).To(Not(HaveOccurred())) - appConfig2Secret := buildSecret(appConfig2SecretName, map[string][]byte{ - "my-app-config-21.yaml": []byte(` + appConfig2Secret := buildSecret(appConfig2SecretName, map[string][]byte{ + "my-app-config-21.yaml": []byte(` # my-app-config-21.yaml `), - "my-app-config-22.yaml": []byte(` + "my-app-config-22.yaml": []byte(` # my-app-config-22.yaml `), - }) - err = k8sClient.Create(ctx, appConfig2Secret) - Expect(err).To(Not(HaveOccurred())) - - var dynamicPluginsObject client.Object - switch dynamicPluginsConfigKind { - case "ConfigMap": - dynamicPluginsObject = buildConfigMap(dynamicPluginsConfigName, map[string]string{ - "dynamic-plugins.yaml": ` + }) + err = k8sClient.Create(ctx, appConfig2Secret) + Expect(err).To(Not(HaveOccurred())) + + var ( + dynamicPluginsObject client.Object + cmRef *bsv1alpha1.Ref + secRef *bsv1alpha1.Ref + ) + switch dynamicPluginsConfigKind { + case "ConfigMap": + cmRef = &bsv1alpha1.Ref{ + Name: dynamicPluginsConfigName, + } + dynamicPluginsObject = buildConfigMap(dynamicPluginsConfigName, map[string]string{ + "dynamic-plugins.yaml": ` # dynamic-plugins.yaml (configmap) includes: [dynamic-plugins.default.yaml] plugins: [] `, - }) - case "Secret": - dynamicPluginsObject = buildSecret(dynamicPluginsConfigName, map[string][]byte{ - "dynamic-plugins.yaml": []byte(` + }) + case "Secret": + secRef = &bsv1alpha1.Ref{ + Name: dynamicPluginsConfigName, + } + dynamicPluginsObject = buildSecret(dynamicPluginsConfigName, map[string][]byte{ + "dynamic-plugins.yaml": []byte(` # dynamic-plugins.yaml (secret) includes: [dynamic-plugins.default.yaml] plugins: [] `), + }) + default: + Fail(fmt.Sprintf("unsupported kind for dynamic plugins object: %q", dynamicPluginsConfigKind)) + } + err = k8sClient.Create(ctx, dynamicPluginsObject) + Expect(err).To(Not(HaveOccurred())) + + backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ + Backstage: &bsv1alpha1.BackstageSpecBackstage{ + AppConfig: &bsv1alpha1.AppConfig{ + MountPath: mountPath, + Items: []bsv1alpha1.AppConfigItem{ + { + ConfigMapRef: &bsv1alpha1.Ref{Name: appConfig1CmName}, + }, + { + SecretRef: &bsv1alpha1.Ref{Name: appConfig2SecretName}, + }, + }, + }, + DynamicPluginsConfig: &bsv1alpha1.DynamicPluginsConfig{ + ConfigMapRef: cmRef, + SecretRef: secRef, + }, + }, + }) + err = k8sClient.Create(ctx, backstage) + Expect(err).To(Not(HaveOccurred())) }) - default: - Fail(fmt.Sprintf("unsupported kind for dynamic plugins object: %q", dynamicPluginsConfigKind)) - } - err = k8sClient.Create(ctx, dynamicPluginsObject) - Expect(err).To(Not(HaveOccurred())) - backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - AppConfigs: []bsv1alpha1.AppConfigRef{ - { - Name: appConfig1CmName, - Kind: "ConfigMap", - }, - { - Name: appConfig2SecretName, - Kind: "Secret", - }, - }, - DynamicPluginsConfig: &bsv1alpha1.DynamicPluginsConfigRef{ - Name: dynamicPluginsConfigName, - Kind: dynamicPluginsConfigKind, - }, - }) - err = k8sClient.Create(ctx, backstage) - Expect(err).To(Not(HaveOccurred())) - }) + It("should reconcile", func() { + By("Checking if the custom resource was successfully created") + Eventually(func() error { + found := &bsv1alpha1.Backstage{} + return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Reconciling the custom resource created") + _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Checking that the Deployment was successfully created in the reconciliation") + found := &appsv1.Deployment{} + Eventually(func(g Gomega) { + // TODO to get name from default + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, found) + g.Expect(err).To(Not(HaveOccurred())) + }, time.Minute, time.Second).Should(Succeed()) + + By("Checking the Volumes in the Backstage Deployment", func() { + Expect(found.Spec.Template.Spec.Volumes).To(HaveLen(5)) + + _, ok := findVolume(found.Spec.Template.Spec.Volumes, "dynamic-plugins-root") + Expect(ok).To(BeTrue(), "No volume found with name: dynamic-plugins-root") + + _, ok = findVolume(found.Spec.Template.Spec.Volumes, "dynamic-plugins-npmrc") + Expect(ok).To(BeTrue(), "No volume found with name: dynamic-plugins-npmrc") + + appConfig1CmVol, ok := findVolume(found.Spec.Template.Spec.Volumes, appConfig1CmName) + Expect(ok).To(BeTrue(), "No volume found with name: %s", appConfig1CmName) + Expect(appConfig1CmVol.VolumeSource.Secret).To(BeNil()) + Expect(appConfig1CmVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) + Expect(appConfig1CmVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(appConfig1CmName)) + + appConfig2SecretVol, ok := findVolume(found.Spec.Template.Spec.Volumes, appConfig2SecretName) + Expect(ok).To(BeTrue(), "No volume found with name: %s", appConfig2SecretName) + Expect(appConfig2SecretVol.VolumeSource.ConfigMap).To(BeNil()) + Expect(appConfig2SecretVol.VolumeSource.Secret.DefaultMode).To(HaveValue(Equal(int32(420)))) + Expect(appConfig2SecretVol.VolumeSource.Secret.SecretName).To(Equal(appConfig2SecretName)) + + dynamicPluginsConfigVol, ok := findVolume(found.Spec.Template.Spec.Volumes, dynamicPluginsConfigName) + Expect(ok).To(BeTrue(), "No volume found with name: %s", dynamicPluginsConfigName) + switch dynamicPluginsConfigKind { + case "ConfigMap": + Expect(dynamicPluginsConfigVol.VolumeSource.Secret).To(BeNil()) + Expect(dynamicPluginsConfigVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) + Expect(dynamicPluginsConfigVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(dynamicPluginsConfigName)) + case "Secret": + Expect(dynamicPluginsConfigVol.VolumeSource.ConfigMap).To(BeNil()) + Expect(dynamicPluginsConfigVol.VolumeSource.Secret.DefaultMode).To(HaveValue(Equal(int32(420)))) + Expect(dynamicPluginsConfigVol.VolumeSource.Secret.SecretName).To(Equal(dynamicPluginsConfigName)) + } + }) + + By("Checking the Number of init containers in the Backstage Deployment") + Expect(found.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + initCont := found.Spec.Template.Spec.InitContainers[0] + + By("Checking the Init Container Env Vars in the Backstage Deployment", func() { + Expect(initCont.Env).To(HaveLen(1)) + Expect(initCont.Env[0].Name).To(Equal("NPM_CONFIG_USERCONFIG")) + Expect(initCont.Env[0].Value).To(Equal("/opt/app-root/src/.npmrc.dynamic-plugins")) + }) + + By("Checking the Init Container Volume Mounts in the Backstage Deployment", func() { + Expect(initCont.VolumeMounts).To(HaveLen(3)) + + dpRoot := findVolumeMounts(initCont.VolumeMounts, "dynamic-plugins-root") + Expect(dpRoot).To(HaveLen(1), + "No volume mount found with name: dynamic-plugins-root") + Expect(dpRoot[0].MountPath).To(Equal("/dynamic-plugins-root")) + Expect(dpRoot[0].ReadOnly).To(BeFalse()) + Expect(dpRoot[0].SubPath).To(BeEmpty()) + + dpNpmrc := findVolumeMounts(initCont.VolumeMounts, "dynamic-plugins-npmrc") + Expect(dpNpmrc).To(HaveLen(1), + "No volume mount found with name: dynamic-plugins-npmrc") + Expect(dpNpmrc[0].MountPath).To(Equal("/opt/app-root/src/.npmrc.dynamic-plugins")) + Expect(dpNpmrc[0].ReadOnly).To(BeTrue()) + Expect(dpNpmrc[0].SubPath).To(Equal(".npmrc")) + + dp := findVolumeMounts(initCont.VolumeMounts, dynamicPluginsConfigName) + Expect(dp).To(HaveLen(1), "No volume mount found with name: %s", dynamicPluginsConfigName) + Expect(dp[0].MountPath).To(Equal("/opt/app-root/src/dynamic-plugins.yaml")) + Expect(dp[0].SubPath).To(Equal("dynamic-plugins.yaml")) + Expect(dp[0].ReadOnly).To(BeTrue()) + }) + + By("Checking the Number of main containers in the Backstage Deployment") + Expect(found.Spec.Template.Spec.Containers).To(HaveLen(1)) + mainCont := found.Spec.Template.Spec.Containers[0] + + expectedMountPath := mountPath + if expectedMountPath == "" { + expectedMountPath = "/opt/app-root/src" + } + + By("Checking the main container Args in the Backstage Deployment", func() { + Expect(mainCont.Args).To(HaveLen(10)) + Expect(mainCont.Args[1]).To(Equal("dynamic-plugins-root/app-config.dynamic-plugins.yaml")) + for i := 0; i <= 8; i += 2 { + Expect(mainCont.Args[i]).To(Equal("--config")) + } + //TODO(rm3l): the order of the rest of the --config args should be the same as the order in + // which the keys are listed in the ConfigMap/Secrets + // But as this is returned as a map, Go does not provide any guarantee on the iteration order. + Expect(mainCont.Args[3]).To(SatisfyAny( + Equal(expectedMountPath+"/my-app-config-11.yaml"), + Equal(expectedMountPath+"/my-app-config-12.yaml"), + )) + Expect(mainCont.Args[5]).To(SatisfyAny( + Equal(expectedMountPath+"/my-app-config-11.yaml"), + Equal(expectedMountPath+"/my-app-config-12.yaml"), + )) + Expect(mainCont.Args[3]).To(Not(Equal(mainCont.Args[5]))) + Expect(mainCont.Args[7]).To(SatisfyAny( + Equal(expectedMountPath+"/my-app-config-21.yaml"), + Equal(expectedMountPath+"/my-app-config-22.yaml"), + )) + Expect(mainCont.Args[9]).To(SatisfyAny( + Equal(expectedMountPath+"/my-app-config-21.yaml"), + Equal(expectedMountPath+"/my-app-config-22.yaml"), + )) + Expect(mainCont.Args[7]).To(Not(Equal(mainCont.Args[9]))) + }) + + By("Checking the main container Volume Mounts in the Backstage Deployment", func() { + Expect(mainCont.VolumeMounts).To(HaveLen(5)) + + dpRoot := findVolumeMounts(mainCont.VolumeMounts, "dynamic-plugins-root") + Expect(dpRoot).To(HaveLen(1), "No volume mount found with name: dynamic-plugins-root") + Expect(dpRoot[0].MountPath).To(Equal("/opt/app-root/src/dynamic-plugins-root")) + Expect(dpRoot[0].SubPath).To(BeEmpty()) + + appConfig1CmMounts := findVolumeMounts(mainCont.VolumeMounts, appConfig1CmName) + Expect(appConfig1CmMounts).To(HaveLen(2), "No volume mounts found with name: %s", appConfig1CmName) + Expect(appConfig1CmMounts[0].MountPath).ToNot(Equal(appConfig1CmMounts[1].MountPath)) + for i := 0; i <= 1; i++ { + Expect(appConfig1CmMounts[i].MountPath).To( + SatisfyAny( + Equal(expectedMountPath+"/my-app-config-11.yaml"), + Equal(expectedMountPath+"/my-app-config-12.yaml"))) + Expect(appConfig1CmMounts[i].SubPath).To( + SatisfyAny( + Equal("my-app-config-11.yaml"), + Equal("my-app-config-12.yaml"))) + } + + appConfig2SecretMounts := findVolumeMounts(mainCont.VolumeMounts, appConfig2SecretName) + Expect(appConfig2SecretMounts).To(HaveLen(2), "No volume mounts found with name: %s", appConfig2SecretName) + Expect(appConfig2SecretMounts[0].MountPath).ToNot(Equal(appConfig2SecretMounts[1].MountPath)) + for i := 0; i <= 1; i++ { + Expect(appConfig2SecretMounts[i].MountPath).To( + SatisfyAny( + Equal(expectedMountPath+"/my-app-config-21.yaml"), + Equal(expectedMountPath+"/my-app-config-22.yaml"))) + Expect(appConfig2SecretMounts[i].SubPath).To( + SatisfyAny( + Equal("my-app-config-21.yaml"), + Equal("my-app-config-22.yaml"))) + } + }) + + By("Checking the latest Status added to the Backstage instance") + verifyBackstageInstance(ctx) - It("should reconcile", func() { - By("Checking if the custom resource was successfully created") - Eventually(func() error { - found := &bsv1alpha1.Backstage{} - return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) - }, time.Minute, time.Second).Should(Succeed()) - - By("Reconciling the custom resource created") - _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, - }) - Expect(err).To(Not(HaveOccurred())) - - By("Checking that the Deployment was successfully created in the reconciliation") - found := &appsv1.Deployment{} - Eventually(func(g Gomega) { - // TODO to get name from default - err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, found) - g.Expect(err).To(Not(HaveOccurred())) - }, time.Minute, time.Second).Should(Succeed()) - - By("Checking the Volumes in the Backstage Deployment", func() { - Expect(found.Spec.Template.Spec.Volumes).To(HaveLen(5)) - - _, ok := findVolume(found.Spec.Template.Spec.Volumes, "dynamic-plugins-root") - Expect(ok).To(BeTrue(), "No volume found with name: dynamic-plugins-root") - - _, ok = findVolume(found.Spec.Template.Spec.Volumes, "dynamic-plugins-npmrc") - Expect(ok).To(BeTrue(), "No volume found with name: dynamic-plugins-npmrc") - - appConfig1CmVol, ok := findVolume(found.Spec.Template.Spec.Volumes, appConfig1CmName) - Expect(ok).To(BeTrue(), "No volume found with name: %s", appConfig1CmName) - Expect(appConfig1CmVol.VolumeSource.Secret).To(BeNil()) - Expect(appConfig1CmVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) - Expect(appConfig1CmVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(appConfig1CmName)) - - appConfig2SecretVol, ok := findVolume(found.Spec.Template.Spec.Volumes, appConfig2SecretName) - Expect(ok).To(BeTrue(), "No volume found with name: %s", appConfig2SecretName) - Expect(appConfig2SecretVol.VolumeSource.ConfigMap).To(BeNil()) - Expect(appConfig2SecretVol.VolumeSource.Secret.DefaultMode).To(HaveValue(Equal(int32(420)))) - Expect(appConfig2SecretVol.VolumeSource.Secret.SecretName).To(Equal(appConfig2SecretName)) - - dynamicPluginsConfigVol, ok := findVolume(found.Spec.Template.Spec.Volumes, dynamicPluginsConfigName) - Expect(ok).To(BeTrue(), "No volume found with name: %s", dynamicPluginsConfigName) - switch dynamicPluginsConfigKind { - case "ConfigMap": - Expect(dynamicPluginsConfigVol.VolumeSource.Secret).To(BeNil()) - Expect(dynamicPluginsConfigVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) - Expect(dynamicPluginsConfigVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(dynamicPluginsConfigName)) - case "Secret": - Expect(dynamicPluginsConfigVol.VolumeSource.ConfigMap).To(BeNil()) - Expect(dynamicPluginsConfigVol.VolumeSource.Secret.DefaultMode).To(HaveValue(Equal(int32(420)))) - Expect(dynamicPluginsConfigVol.VolumeSource.Secret.SecretName).To(Equal(dynamicPluginsConfigName)) - } - }) - - By("Checking the Number of init containers in the Backstage Deployment") - Expect(found.Spec.Template.Spec.InitContainers).To(HaveLen(1)) - initCont := found.Spec.Template.Spec.InitContainers[0] - - By("Checking the Init Container Env Vars in the Backstage Deployment", func() { - Expect(initCont.Env).To(HaveLen(1)) - Expect(initCont.Env[0].Name).To(Equal("NPM_CONFIG_USERCONFIG")) - Expect(initCont.Env[0].Value).To(Equal("/opt/app-root/src/.npmrc.dynamic-plugins")) - }) - - By("Checking the Init Container Volume Mounts in the Backstage Deployment", func() { - Expect(initCont.VolumeMounts).To(HaveLen(3)) - - dpRoot, ok := findVolumeMount(initCont.VolumeMounts, "dynamic-plugins-root") - Expect(ok).To(BeTrue(), - "No volume mount found with name: dynamic-plugins-root") - Expect(dpRoot.MountPath).To(Equal("/dynamic-plugins-root")) - Expect(dpRoot.ReadOnly).To(BeFalse()) - Expect(dpRoot.SubPath).To(BeEmpty()) - - dpNpmrc, ok := findVolumeMount(initCont.VolumeMounts, "dynamic-plugins-npmrc") - Expect(ok).To(BeTrue(), - "No volume mount found with name: dynamic-plugins-npmrc") - Expect(dpNpmrc.MountPath).To(Equal("/opt/app-root/src/.npmrc.dynamic-plugins")) - Expect(dpNpmrc.ReadOnly).To(BeTrue()) - Expect(dpNpmrc.SubPath).To(Equal(".npmrc")) - - dp, ok := findVolumeMount(initCont.VolumeMounts, dynamicPluginsConfigName) - Expect(ok).To(BeTrue(), "No volume mount found with name: %s", dynamicPluginsConfigName) - Expect(dp.MountPath).To(Equal("/opt/app-root/src/dynamic-plugins.yaml")) - Expect(dp.SubPath).To(Equal("dynamic-plugins.yaml")) - Expect(dp.ReadOnly).To(BeTrue()) - }) - - By("Checking the Number of main containers in the Backstage Deployment") - Expect(found.Spec.Template.Spec.Containers).To(HaveLen(1)) - mainCont := found.Spec.Template.Spec.Containers[0] - - By("Checking the main container Args in the Backstage Deployment", func() { - Expect(mainCont.Args).To(HaveLen(10)) - Expect(mainCont.Args[1]).To(Equal("dynamic-plugins-root/app-config.dynamic-plugins.yaml")) - for i := 0; i <= 8; i += 2 { - Expect(mainCont.Args[i]).To(Equal("--config")) - } - //TODO(rm3l): the order of the rest of the --config args should be the same as the order in - // which the keys are listed in the ConfigMap/Secrets - // But as this is returned as a map, Go does not provide any guarantee on the iteration order. - Expect(mainCont.Args[3]).To(SatisfyAny( - Equal("/opt/app-root/src/my-app-config-1-cm/my-app-config-11.yaml"), - Equal("/opt/app-root/src/my-app-config-1-cm/my-app-config-12.yaml"), - )) - Expect(mainCont.Args[5]).To(SatisfyAny( - Equal("/opt/app-root/src/my-app-config-1-cm/my-app-config-11.yaml"), - Equal("/opt/app-root/src/my-app-config-1-cm/my-app-config-12.yaml"), - )) - Expect(mainCont.Args[3]).To(Not(Equal(mainCont.Args[5]))) - Expect(mainCont.Args[7]).To(SatisfyAny( - Equal("/opt/app-root/src/my-app-config-2-secret/my-app-config-21.yaml"), - Equal("/opt/app-root/src/my-app-config-2-secret/my-app-config-22.yaml"), - )) - Expect(mainCont.Args[9]).To(SatisfyAny( - Equal("/opt/app-root/src/my-app-config-2-secret/my-app-config-21.yaml"), - Equal("/opt/app-root/src/my-app-config-2-secret/my-app-config-22.yaml"), - )) - Expect(mainCont.Args[7]).To(Not(Equal(mainCont.Args[9]))) - }) - - By("Checking the main container Volume Mounts in the Backstage Deployment", func() { - Expect(mainCont.VolumeMounts).To(HaveLen(3)) - - dpRoot, ok := findVolumeMount(mainCont.VolumeMounts, "dynamic-plugins-root") - Expect(ok).To(BeTrue(), "No volume mount found with name: dynamic-plugins-root") - Expect(dpRoot.MountPath).To(Equal("/opt/app-root/src/dynamic-plugins-root")) - Expect(dpRoot.SubPath).To(BeEmpty()) - - appConfig1CmMount, ok := findVolumeMount(mainCont.VolumeMounts, appConfig1CmName) - Expect(ok).To(BeTrue(), "No volume mount found with name: %s", appConfig1CmName) - Expect(appConfig1CmMount.MountPath).To(Equal("/opt/app-root/src/my-app-config-1-cm")) - Expect(appConfig1CmMount.SubPath).To(BeEmpty()) - - appConfig2SecretMount, ok := findVolumeMount(mainCont.VolumeMounts, appConfig2SecretName) - Expect(ok).To(BeTrue(), "No volume mount found with name: %s", appConfig2SecretName) - Expect(appConfig2SecretMount.MountPath).To(Equal("/opt/app-root/src/my-app-config-2-secret")) - Expect(appConfig2SecretMount.SubPath).To(BeEmpty()) + }) }) - - By("Checking the latest Status added to the Backstage instance") - verifyBackstageInstance(ctx) - - }) - }) + } } }) @@ -698,9 +784,11 @@ plugins: [] var backstage *bsv1alpha1.Backstage BeforeEach(func() { backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - BackendAuthSecretRef: &bsv1alpha1.BackendAuthSecretRef{ - Name: "non-existing-secret", - Key: key, + Backstage: &bsv1alpha1.BackstageSpecBackstage{ + BackendAuthSecretRef: &bsv1alpha1.BackendAuthSecretRef{ + Name: "non-existing-secret", + Key: key, + }, }, }) err := k8sClient.Create(ctx, backstage) @@ -773,9 +861,11 @@ plugins: [] err := k8sClient.Create(ctx, backendAuthSecret) Expect(err).To(Not(HaveOccurred())) backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - BackendAuthSecretRef: &bsv1alpha1.BackendAuthSecretRef{ - Name: backendAuthSecretName, - Key: key, + Backstage: &bsv1alpha1.BackstageSpecBackstage{ + BackendAuthSecretRef: &bsv1alpha1.BackendAuthSecretRef{ + Name: backendAuthSecretName, + Key: key, + }, }, }) err = k8sClient.Create(ctx, backstage) @@ -835,14 +925,492 @@ plugins: [] }) } }) + + Context("Extra Configs", func() { + for _, kind := range []string{"ConfigMap", "Secret"} { + kind := kind + When(fmt.Sprintf("referencing non-existing %s as extra-config", kind), func() { + var backstage *bsv1alpha1.Backstage + + BeforeEach(func() { + var item bsv1alpha1.ExtraConfigItem + name := "a-non-existing-" + strings.ToLower(kind) + switch kind { + case "ConfigMap": + item = bsv1alpha1.ExtraConfigItem{ + ConfigMapRef: &bsv1alpha1.Ref{Name: name}, + } + case "Secret": + item = bsv1alpha1.ExtraConfigItem{ + SecretRef: &bsv1alpha1.Ref{Name: name}, + } + } + backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ + Backstage: &bsv1alpha1.BackstageSpecBackstage{ + ExtraConfig: &bsv1alpha1.ExtraConfig{ + Items: []bsv1alpha1.ExtraConfigItem{item}, + }, + }, + }) + err := k8sClient.Create(ctx, backstage) + Expect(err).To(Not(HaveOccurred())) + }) + + It("should fail to reconcile", func() { + By("Checking if the custom resource was successfully created") + Eventually(func() error { + found := &bsv1alpha1.Backstage{} + return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Not reconciling the custom resource created") + _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, + }) + Expect(err).To(HaveOccurred()) + + By("Not creating a Backstage Deployment") + Consistently(func() error { + // TODO to get name from default + return k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, &appsv1.Deployment{}) + }, 5*time.Second, time.Second).Should(Not(Succeed())) + }) + }) + } + + for _, mountPath := range []string{"", "/some/path/for/extra/config"} { + mountPath := mountPath + When("referencing ConfigMaps and Secrets for extra config files - mountPath="+mountPath, func() { + const ( + extraConfig1CmName = "my-extra-config-1-cm" + extraConfig2SecretName = "my-extra-config-2-secret" + ) + + var backstage *bsv1alpha1.Backstage + + BeforeEach(func() { + extraConfig1Cm := buildConfigMap(extraConfig1CmName, map[string]string{ + "my-extra-config-11.yaml": ` +# my-extra-config-11.yaml +`, + "my-extra-config-12.yaml": ` +# my-extra-config-12.yaml +`, + }) + err := k8sClient.Create(ctx, extraConfig1Cm) + Expect(err).To(Not(HaveOccurred())) + + extraConfig2Secret := buildSecret(extraConfig2SecretName, map[string][]byte{ + "my-extra-config-21.yaml": []byte(` +# my-extra-config-21.yaml +`), + "my-extra-config-22.yaml": []byte(` +# my-extra-config-22.yaml +`), + }) + err = k8sClient.Create(ctx, extraConfig2Secret) + Expect(err).To(Not(HaveOccurred())) + + backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ + Backstage: &bsv1alpha1.BackstageSpecBackstage{ + ExtraConfig: &bsv1alpha1.ExtraConfig{ + MountPath: mountPath, + Items: []bsv1alpha1.ExtraConfigItem{ + { + ConfigMapRef: &bsv1alpha1.Ref{Name: extraConfig1CmName}, + }, + { + SecretRef: &bsv1alpha1.Ref{Name: extraConfig2SecretName}, + }, + }, + }, + }, + }) + err = k8sClient.Create(ctx, backstage) + Expect(err).To(Not(HaveOccurred())) + }) + + It("should reconcile", func() { + By("Checking if the custom resource was successfully created") + Eventually(func() error { + found := &bsv1alpha1.Backstage{} + return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Reconciling the custom resource created") + _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Checking that the Deployment was successfully created in the reconciliation") + found := &appsv1.Deployment{} + Eventually(func(g Gomega) { + // TODO to get name from default + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, found) + g.Expect(err).To(Not(HaveOccurred())) + }, time.Minute, time.Second).Should(Succeed()) + + By("Checking the Volumes in the Backstage Deployment", func() { + Expect(found.Spec.Template.Spec.Volumes).To(HaveLen(5)) + + extraConfig1CmVol, ok := findVolume(found.Spec.Template.Spec.Volumes, extraConfig1CmName) + Expect(ok).To(BeTrue(), "No volume found with name: %s", extraConfig1CmName) + Expect(extraConfig1CmVol.VolumeSource.Secret).To(BeNil()) + Expect(extraConfig1CmVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) + Expect(extraConfig1CmVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(extraConfig1CmName)) + + extraConfig2SecretVol, ok := findVolume(found.Spec.Template.Spec.Volumes, extraConfig2SecretName) + Expect(ok).To(BeTrue(), "No volume found with name: %s", extraConfig2SecretName) + Expect(extraConfig2SecretVol.VolumeSource.ConfigMap).To(BeNil()) + Expect(extraConfig2SecretVol.VolumeSource.Secret.DefaultMode).To(HaveValue(Equal(int32(420)))) + Expect(extraConfig2SecretVol.VolumeSource.Secret.SecretName).To(Equal(extraConfig2SecretName)) + }) + + initCont := found.Spec.Template.Spec.InitContainers[0] + By("Checking the Init Container Volume Mounts in the Backstage Deployment", func() { + Expect(initCont.VolumeMounts).To(HaveLen(3)) + + // Extra config mounted in the main container + Expect(findVolumeMounts(initCont.VolumeMounts, extraConfig1CmName)).Should(HaveLen(0)) + Expect(findVolumeMounts(initCont.VolumeMounts, extraConfig2SecretName)).Should(HaveLen(0)) + }) + + mainCont := found.Spec.Template.Spec.Containers[0] + + By("Checking the main container Volume Mounts in the Backstage Deployment", func() { + Expect(mainCont.VolumeMounts).To(HaveLen(5)) + + expectedMountPath := mountPath + if expectedMountPath == "" { + expectedMountPath = "/opt/app-root/src" + } + + extraConfig1CmMounts := findVolumeMounts(mainCont.VolumeMounts, extraConfig1CmName) + Expect(extraConfig1CmMounts).To(HaveLen(2), "No volume mounts found with name: %s", extraConfig1CmName) + Expect(extraConfig1CmMounts[0].MountPath).ToNot(Equal(extraConfig1CmMounts[1].MountPath)) + for i := 0; i <= 1; i++ { + Expect(extraConfig1CmMounts[i].MountPath).To( + SatisfyAny( + Equal(expectedMountPath+"/my-extra-config-11.yaml"), + Equal(expectedMountPath+"/my-extra-config-12.yaml"))) + Expect(extraConfig1CmMounts[i].SubPath).To( + SatisfyAny( + Equal("my-extra-config-11.yaml"), + Equal("my-extra-config-12.yaml"))) + } + + extraConfig2SecretMounts := findVolumeMounts(mainCont.VolumeMounts, extraConfig2SecretName) + Expect(extraConfig2SecretMounts).To(HaveLen(2), "No volume mounts found with name: %s", extraConfig2SecretName) + Expect(extraConfig2SecretMounts[0].MountPath).ToNot(Equal(extraConfig2SecretMounts[1].MountPath)) + for i := 0; i <= 1; i++ { + Expect(extraConfig2SecretMounts[i].MountPath).To( + SatisfyAny( + Equal(expectedMountPath+"/my-extra-config-21.yaml"), + Equal(expectedMountPath+"/my-extra-config-22.yaml"))) + Expect(extraConfig2SecretMounts[i].SubPath).To( + SatisfyAny( + Equal("my-extra-config-21.yaml"), + Equal("my-extra-config-22.yaml"))) + } + }) + + By("Checking the latest Status added to the Backstage instance") + verifyBackstageInstance(ctx) + }) + }) + } + }) + + Context("Env and EnvFrom", func() { + When("setting environment variables either directly or via references to ConfigMap or Secret", func() { + const ( + envConfig1CmName = "my-env-config-1-cm" + envConfig2SecretName = "my-env-config-2-secret" + ) + + var backstage *bsv1alpha1.Backstage + + BeforeEach(func() { + envConfig1Cm := buildConfigMap(envConfig1CmName, map[string]string{ + "MY_ENV_VAR_1_FROM_CM": "value 11", + "MY_ENV_VAR_2_FROM_CM": "value 12", + }) + err := k8sClient.Create(ctx, envConfig1Cm) + Expect(err).To(Not(HaveOccurred())) + + envConfig2Secret := buildSecret(envConfig2SecretName, map[string][]byte{ + "MY_ENV_VAR_1_FROM_SECRET": []byte("value 21"), + "MY_ENV_VAR_2_FROM_SECRET": []byte("value 22"), + }) + err = k8sClient.Create(ctx, envConfig2Secret) + Expect(err).To(Not(HaveOccurred())) + + backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ + Backstage: &bsv1alpha1.BackstageSpecBackstage{ + Env: []bsv1alpha1.Env{ + {Name: "MY_ENV_VAR_1", Value: "value 10"}, + {Name: "MY_ENV_VAR_2", Value: "value 20"}, + }, + EnvFrom: []bsv1alpha1.EnvFrom{ + { + ConfigMapRef: &bsv1alpha1.Ref{Name: envConfig1CmName}, + }, + { + SecretRef: &bsv1alpha1.Ref{Name: envConfig2SecretName}, + }, + }, + }, + }) + err = k8sClient.Create(ctx, backstage) + Expect(err).To(Not(HaveOccurred())) + }) + + It("should reconcile", func() { + By("Checking if the custom resource was successfully created") + Eventually(func() error { + found := &bsv1alpha1.Backstage{} + return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Reconciling the custom resource created") + _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Checking that the Deployment was successfully created in the reconciliation") + found := &appsv1.Deployment{} + Eventually(func(g Gomega) { + // TODO to get name from default + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, found) + g.Expect(err).To(Not(HaveOccurred())) + }, time.Minute, time.Second).Should(Succeed()) + + visitContainers(&found.Spec.Template, func(container *corev1.Container) { + By(fmt.Sprintf("Checking Env in the Backstage Deployment - container: %q", container.Name), func() { + Expect(len(container.Env)).To(BeNumerically(">=", 2), + "Expected at least 2 items in Env for container %q, fot %d", container.Name, len(container.Env)) + envVar, ok := findEnvVar(container.Env, "MY_ENV_VAR_1") + Expect(ok).To(BeTrue(), "No env var with name MY_ENV_VAR_1 in init container") + Expect(envVar.Value).Should(Equal("value 10")) + envVar, ok = findEnvVar(container.Env, "MY_ENV_VAR_2") + Expect(ok).To(BeTrue(), "No env var with name MY_ENV_VAR_2 in init container") + Expect(envVar.Value).Should(Equal("value 20")) + }) + By(fmt.Sprintf("Checking EnvFrom in the Backstage Deployment - container: %q", container.Name), func() { + Expect(len(container.EnvFrom)).To(BeNumerically(">=", 2), + "Expected at least 2 items in EnvFrom for container %q, fot %d", container.Name, len(container.EnvFrom)) + envVar, ok := findEnvVarFrom(container.EnvFrom, envConfig1CmName) + Expect(ok).To(BeTrue(), "No ConfigMap-backed envFrom in init container: %s", envConfig1CmName) + Expect(envVar.ConfigMapRef).ShouldNot(BeNil()) + envVar, ok = findEnvVarFrom(container.EnvFrom, envConfig2SecretName) + Expect(ok).To(BeTrue(), "No ConfigMap-backed envFrom in init container: %s", envConfig2SecretName) + Expect(envVar.SecretRef).ShouldNot(BeNil()) + }) + }) + + By("Checking the latest Status added to the Backstage instance") + verifyBackstageInstance(ctx) + }) + }) + }) + + When("setting image", func() { + var imageName = "quay.io/my-org/my-awesome-image:1.2.3" + + var backstage *bsv1alpha1.Backstage + + BeforeEach(func() { + backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ + Backstage: &bsv1alpha1.BackstageSpecBackstage{ + Image: &imageName, + }, + }) + err := k8sClient.Create(ctx, backstage) + Expect(err).To(Not(HaveOccurred())) + }) + + It("should reconcile", func() { + By("Checking if the custom resource was successfully created") + Eventually(func() error { + found := &bsv1alpha1.Backstage{} + return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Reconciling the custom resource created") + _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Checking that the Deployment was successfully created in the reconciliation") + found := &appsv1.Deployment{} + Eventually(func(g Gomega) { + // TODO to get name from default + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, found) + g.Expect(err).To(Not(HaveOccurred())) + }, time.Minute, time.Second).Should(Succeed()) + + visitContainers(&found.Spec.Template, func(container *corev1.Container) { + By(fmt.Sprintf("Checking Image in the Backstage Deployment - container: %q", container.Name), func() { + Expect(container.Image).Should(Equal(imageName)) + }) + }) + + By("Checking the latest Status added to the Backstage instance") + verifyBackstageInstance(ctx) + }) + }) + + When("setting image pull secret", func() { + var imagePullSecretName = "some-image-pull-secret" + + var backstage *bsv1alpha1.Backstage + + BeforeEach(func() { + backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ + Backstage: &bsv1alpha1.BackstageSpecBackstage{ + ImagePullSecret: &imagePullSecretName, + }, + }) + err := k8sClient.Create(ctx, backstage) + Expect(err).To(Not(HaveOccurred())) + }) + + It("should reconcile", func() { + By("Checking if the custom resource was successfully created") + Eventually(func() error { + found := &bsv1alpha1.Backstage{} + return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Reconciling the custom resource created") + _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Checking that the Deployment was successfully created in the reconciliation") + found := &appsv1.Deployment{} + Eventually(func(g Gomega) { + // TODO to get name from default + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, found) + g.Expect(err).To(Not(HaveOccurred())) + }, time.Minute, time.Second).Should(Succeed()) + + By("Checking the image pull secret is included in the pod spec of Backstage", func() { + var list []string + for _, v := range found.Spec.Template.Spec.ImagePullSecrets { + list = append(list, v.Name) + } + Expect(list).Should(ContainElement(imagePullSecretName)) + }) + + By("Checking the latest Status added to the Backstage instance") + verifyBackstageInstance(ctx) + }) + }) + + When("setting the number of replicas", func() { + var nbReplicas int32 = 5 + + var backstage *bsv1alpha1.Backstage + + BeforeEach(func() { + backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ + Backstage: &bsv1alpha1.BackstageSpecBackstage{ + Replicas: &nbReplicas, + }, + }) + err := k8sClient.Create(ctx, backstage) + Expect(err).To(Not(HaveOccurred())) + }) + + It("should reconcile", func() { + By("Checking if the custom resource was successfully created") + Eventually(func() error { + found := &bsv1alpha1.Backstage{} + return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Reconciling the custom resource created") + _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Checking that the Deployment was successfully created in the reconciliation") + found := &appsv1.Deployment{} + Eventually(func(g Gomega) { + // TODO to get name from default + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, found) + g.Expect(err).To(Not(HaveOccurred())) + }, time.Minute, time.Second).Should(Succeed()) + + By("Checking the number of replicas of the Backstage Instance") + Expect(found.Spec.Replicas).Should(HaveValue(BeEquivalentTo(nbReplicas))) + + By("Checking the latest Status added to the Backstage instance") + verifyBackstageInstance(ctx) + }) + }) + + Context("PostgreSQL", func() { + // Other cases covered in the tests above + + When("disabling PostgreSQL in the CR", func() { + var backstage *bsv1alpha1.Backstage + BeforeEach(func() { + backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ + Postgresql: &bsv1alpha1.BackstageSpecPostgresql{ + Enabled: pointer.Bool(false), + }, + }) + err := k8sClient.Create(ctx, backstage) + Expect(err).To(Not(HaveOccurred())) + }) + + It("should successfully reconcile a custom resource for default Backstage", func() { + By("Checking if the custom resource was successfully created") + Eventually(func() error { + found := &bsv1alpha1.Backstage{} + return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Reconciling the custom resource created") + _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, + }) + Expect(err).To(Not(HaveOccurred())) + + By("not creating a StatefulSet for the Database") + Consistently(func(g Gomega) { + err := k8sClient.Get(ctx, + types.NamespacedName{Namespace: ns, Name: fmt.Sprintf("backstage-psql-%s", backstage.Name)}, + &appsv1.StatefulSet{}) + g.Expect(err).Should(HaveOccurred()) + g.Expect(errors.IsNotFound(err)).Should(BeTrue(), "Expected error to be a not-found one, but got %v", err) + }, 10*time.Second, time.Second).Should(Succeed()) + + By("Checking if Deployment was successfully created in the reconciliation") + Eventually(func() error { + // TODO to get name from default + return k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, &appsv1.Deployment{}) + }, time.Minute, time.Second).Should(Succeed()) + + By("Checking the latest Status added to the Backstage instance") + verifyBackstageInstance(ctx) + }) + }) + }) }) -func findElementByPredicate[T any](l []T, predicate func(t T) bool) (T, bool) { +func findElementsByPredicate[T any](l []T, predicate func(t T) bool) (result []T) { for _, v := range l { if predicate(v) { - return v, true + result = append(result, v) } } - var zero T - return zero, false + return result } From 7e2526710f7258e0773c2c1ff89b1953f5f496d1 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 4 Dec 2023 16:06:33 +0100 Subject: [PATCH 02/31] Update CRD with the structure discussed in [1] [1] https://github.com/janus-idp/operator/issues/21#issuecomment-1834055506 --- api/v1alpha1/backstage_types.go | 178 +++++++++--- api/v1alpha1/zz_generated.deepcopy.go | 255 ++++++++++++++-- bundle.Dockerfile | 2 +- ...kstage-operator.clusterserviceversion.yaml | 3 +- bundle/manifests/janus-idp.io_backstages.yaml | 271 +++++++++++++----- bundle/metadata/annotations.yaml | 2 +- config/crd/bases/janus-idp.io_backstages.yaml | 271 +++++++++++++----- 7 files changed, 795 insertions(+), 187 deletions(-) diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index 42fa4b2f..4435106d 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -25,65 +25,173 @@ const ( // BackstageSpec defines the desired state of Backstage type BackstageSpec struct { - // References to existing app-configs Config objects. - // Each element can be a reference to any ConfigMap or Secret, - // and will be mounted inside the main application container under a dedicated directory containing the ConfigMap - // or Secret name. Additionally, each file will be passed as a `--config /path/to/secret_or_configmap/key` to the - // main container args in the order of the entries defined in the AppConfigs list. - // But bear in mind that for a single AppConfig element containing several files, - // the order in which those files will be appended to the container args, the main container args cannot be guaranteed. - // So if you want to pass multiple app-config files, it is recommended to pass one ConfigMap/Secret per app-config file. - AppConfigs []AppConfigRef `json:"appConfigs,omitempty"` + // Configuration for Backstage. Optional. + Backstage *BackstageSpecBackstage `json:"backstage,omitempty"` + + // Configuration for the local database. Optional. + Postgresql *BackstageSpecPostgresql `json:"postgresql,omitempty"` - // Optional Backend Auth Secret Name. A new one will be generated if not set. + // Raw Runtime Objects configuration. For Advanced scenarios. + RawRuntimeConfig RuntimeConfig `json:"rawRuntimeConfig,omitempty"` +} + +type BackstageSpecBackstage struct { + + // Optional Reference to a Secret to use for Backend Auth. A new one will be generated if not set. // This Secret is used to set an environment variable named 'APP_CONFIG_backend_auth_keys' in the // main container, which takes precedence over any 'backend.auth.keys' field defined // in default or custom application configuration files. // This is required for service-to-service auth and is shared by all backend plugins. + //+optional BackendAuthSecretRef *BackendAuthSecretRef `json:"backendAuthSecretRef,omitempty"` + // References to existing app-configs Config objects, that will be mounted as files in the specified mount path. + // Each element can be a reference to any ConfigMap or Secret, + // and will be mounted inside the main application container under a dedicated directory containing the ConfigMap + // or Secret name (relative to the specified mount path). + // Additionally, each file will be passed as a `--config /path/to/secret_or_configmap/key` to the + // main container args in the order of the entries defined in the AppConfigs list. + // But bear in mind that for a single AppConfig element containing several files, + // the order in which those files will be appended to the container args, the main container args cannot be guaranteed. + // So if you want to pass multiple app-config files, it is recommended to pass one ConfigMap/Secret per app-config file. + //+optional + AppConfig *AppConfig `json:"appConfig,omitempty"` + // Reference to an existing configuration object for Dynamic Plugins. // This can be a reference to any ConfigMap or Secret, // but the object must have an existing key named: 'dynamic-plugins.yaml' - DynamicPluginsConfig *DynamicPluginsConfigRef `json:"dynamicPluginsConfig,omitempty"` - - // Raw Runtime Objects configuration - RawRuntimeConfig RuntimeConfig `json:"rawRuntimeConfig,omitempty"` - - //+kubebuilder:default=false - SkipLocalDb bool `json:"skipLocalDb,omitempty"` + //+optional + DynamicPluginsConfig *DynamicPluginsConfig `json:"dynamicPlugins,omitempty"` + + // References to existing Config objects to use as extra config files. + // They will be mounted as files in the specified mount path. + // Each element can be a reference to any ConfigMap or Secret. + //+optional + ExtraConfig *ExtraConfig `json:"extraConfig,omitempty"` + + // Environment variables to inject into all the Backstage containers. + // Bear in mind not to put sensitive data here. Use EnvFrom instead. + //+optional + Env []Env `json:"env,omitempty"` + + // Environment variables to inject into all the Backstage containers, as references to existing ConfigMap or Secret objects. + //+optional + EnvFrom []EnvFrom `json:"envFrom,omitempty"` + + // Number of desired replicas to set in the Backstage Deployment. + // Defaults to 1. + //+optional + //+kubebuilder:default=1 + Replicas *int32 `json:"replicas,omitempty"` + + // Image to use in all containers (including Init Containers) + //+optional + Image *string `json:"image,omitempty"` + + // Image Pull Secret to use in all containers (including Init Containers) + //+optional + ImagePullSecret *string `json:"imagePullSecret,omitempty"` } -type AppConfigRef struct { - // Name of an existing App Config object +type BackendAuthSecretRef struct { + // Name of the secret to use for the backend auth //+kubebuilder:validation:Required Name string `json:"name"` - // Type of the existing App Config object, either ConfigMap or Secret - //+kubebuilder:validation:Required - //+kubebuilder:validation:Enum=ConfigMap;Secret - Kind string `json:"kind"` + // Key in the secret to use for the backend auth. Default value is: backend-secret + // +optional + //+kubebuilder:default=backend-secret + Key string `json:"key,omitempty"` } -type DynamicPluginsConfigRef struct { - // Name of the Dynamic Plugins config object - // +kubebuilder:validation:Required - Name string `json:"name"` +type AppConfig struct { + // Mount path for all app-config files listed in the Items field + // +optional + // +kubebuilder:default=/opt/app-root/src + MountPath string `json:"mountPath,omitempty"` + + // List of references to app-config Config objects. + // +optional + Items []AppConfigItem `json:"items,omitempty"` +} + +type AppConfigItem struct { + // ConfigMap containing one or more app-config files + // +optional + ConfigMapRef *Ref `json:"configMapRef,omitempty"` + + // Secret containing one or more app-config files + // +optional + SecretRef *Ref `json:"secretRef,omitempty"` +} + +type ExtraConfig struct { + // Mount path for all extra configuration files listed in the Items field + // +optional + // +kubebuilder:default=/opt/app-root/src + MountPath string `json:"mountPath,omitempty"` + + // List of references to extra config Config objects. + // +optional + Items []ExtraConfigItem `json:"items,omitempty"` +} + +type ExtraConfigItem struct { + // ConfigMap containing one or more extra config files + // +optional + ConfigMapRef *Ref `json:"configMapRef,omitempty"` + + // Secret containing one or more extra config files + // +optional + SecretRef *Ref `json:"secretRef,omitempty"` +} + +type DynamicPluginsConfig struct { + // ConfigMap containing the dynamic plugins' configuration. It needs to have a key named: "dynamic-plugins.yaml". + // ConfigMapRef will be used if both ConfigMapRef and SecretRef are provided. + // +optional + ConfigMapRef *Ref `json:"configMapRef,omitempty"` + + // Secret containing the dynamic plugins' configuration. It needs to have a key named: "dynamic-plugins.yaml". + // ConfigMapRef will be used if both ConfigMapRef and SecretRef are provided. + // +optional + SecretRef *Ref `json:"secretRef,omitempty"` +} - // Type of the Dynamic Plugins config object, either ConfigMap or Secret +type Ref struct { + // Name of the object referenced. //+kubebuilder:validation:Required - //+kubebuilder:validation:Enum=ConfigMap;Secret - Kind string `json:"kind"` + Name string `json:"name"` } -type BackendAuthSecretRef struct { - // Name of the secret to use for the backend auth +type Env struct { + // Name of the environment variable //+kubebuilder:validation:Required Name string `json:"name"` - // Key in the secret to use for the backend auth. Default value is: backend-secret - //+kubebuilder:default=backend-secret - Key string `json:"key,omitempty"` + // Value of the environment variable + //+kubebuilder:validation:Required + Value string `json:"value"` +} + +type EnvFrom struct { + // ConfigMap containing the environment variables to inject + // +optional + ConfigMapRef *Ref `json:"configMapRef,omitempty"` + + // Secret containing the environment variables to inject + // +optional + SecretRef *Ref `json:"secretRef,omitempty"` +} + +type BackstageSpecPostgresql struct { + // Control the creation of a local PostgreSQL DB. Set to false if using for example an external Database for Backstage. + // To use an external Database, you can provide your own app-config file (see the AppConfig field) containing references + // to the Database connection information, which might be supplied as environment variables (see the Env field) or + // extra-configuration files (see the ExtraConfig field in the BackstageSpecBackstage structure). + // +optional + //+kubebuilder:default=true + Enabled *bool `json:"disabled,omitempty"` } type RuntimeConfig struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 891bee7f..2fcde668 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -27,16 +27,48 @@ import ( ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AppConfigRef) DeepCopyInto(out *AppConfigRef) { +func (in *AppConfig) DeepCopyInto(out *AppConfig) { *out = *in + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AppConfigItem, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppConfig. +func (in *AppConfig) DeepCopy() *AppConfig { + if in == nil { + return nil + } + out := new(AppConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AppConfigItem) DeepCopyInto(out *AppConfigItem) { + *out = *in + if in.ConfigMapRef != nil { + in, out := &in.ConfigMapRef, &out.ConfigMapRef + *out = new(Ref) + **out = **in + } + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(Ref) + **out = **in + } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppConfigRef. -func (in *AppConfigRef) DeepCopy() *AppConfigRef { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppConfigItem. +func (in *AppConfigItem) DeepCopy() *AppConfigItem { if in == nil { return nil } - out := new(AppConfigRef) + out := new(AppConfigItem) in.DeepCopyInto(out) return out } @@ -118,30 +150,107 @@ func (in *BackstageList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BackstageSpec) DeepCopyInto(out *BackstageSpec) { *out = *in - if in.AppConfigs != nil { - in, out := &in.AppConfigs, &out.AppConfigs - *out = make([]AppConfigRef, len(*in)) - copy(*out, *in) + if in.Backstage != nil { + in, out := &in.Backstage, &out.Backstage + *out = new(BackstageSpecBackstage) + (*in).DeepCopyInto(*out) + } + if in.Postgresql != nil { + in, out := &in.Postgresql, &out.Postgresql + *out = new(BackstageSpecPostgresql) + (*in).DeepCopyInto(*out) + } + out.RawRuntimeConfig = in.RawRuntimeConfig +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackstageSpec. +func (in *BackstageSpec) DeepCopy() *BackstageSpec { + if in == nil { + return nil } + out := new(BackstageSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackstageSpecBackstage) DeepCopyInto(out *BackstageSpecBackstage) { + *out = *in if in.BackendAuthSecretRef != nil { in, out := &in.BackendAuthSecretRef, &out.BackendAuthSecretRef *out = new(BackendAuthSecretRef) **out = **in } + if in.AppConfig != nil { + in, out := &in.AppConfig, &out.AppConfig + *out = new(AppConfig) + (*in).DeepCopyInto(*out) + } if in.DynamicPluginsConfig != nil { in, out := &in.DynamicPluginsConfig, &out.DynamicPluginsConfig - *out = new(DynamicPluginsConfigRef) + *out = new(DynamicPluginsConfig) + (*in).DeepCopyInto(*out) + } + if in.ExtraConfig != nil { + in, out := &in.ExtraConfig, &out.ExtraConfig + *out = new(ExtraConfig) + (*in).DeepCopyInto(*out) + } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]Env, len(*in)) + copy(*out, *in) + } + if in.EnvFrom != nil { + in, out := &in.EnvFrom, &out.EnvFrom + *out = make([]EnvFrom, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + if in.Image != nil { + in, out := &in.Image, &out.Image + *out = new(string) + **out = **in + } + if in.ImagePullSecret != nil { + in, out := &in.ImagePullSecret, &out.ImagePullSecret + *out = new(string) **out = **in } - out.RawRuntimeConfig = in.RawRuntimeConfig } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackstageSpec. -func (in *BackstageSpec) DeepCopy() *BackstageSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackstageSpecBackstage. +func (in *BackstageSpecBackstage) DeepCopy() *BackstageSpecBackstage { if in == nil { return nil } - out := new(BackstageSpec) + out := new(BackstageSpecBackstage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackstageSpecPostgresql) DeepCopyInto(out *BackstageSpecPostgresql) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackstageSpecPostgresql. +func (in *BackstageSpecPostgresql) DeepCopy() *BackstageSpecPostgresql { + if in == nil { + return nil + } + out := new(BackstageSpecPostgresql) in.DeepCopyInto(out) return out } @@ -169,16 +278,128 @@ func (in *BackstageStatus) DeepCopy() *BackstageStatus { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DynamicPluginsConfigRef) DeepCopyInto(out *DynamicPluginsConfigRef) { +func (in *DynamicPluginsConfig) DeepCopyInto(out *DynamicPluginsConfig) { + *out = *in + if in.ConfigMapRef != nil { + in, out := &in.ConfigMapRef, &out.ConfigMapRef + *out = new(Ref) + **out = **in + } + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(Ref) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DynamicPluginsConfig. +func (in *DynamicPluginsConfig) DeepCopy() *DynamicPluginsConfig { + if in == nil { + return nil + } + out := new(DynamicPluginsConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Env) DeepCopyInto(out *Env) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Env. +func (in *Env) DeepCopy() *Env { + if in == nil { + return nil + } + out := new(Env) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EnvFrom) DeepCopyInto(out *EnvFrom) { + *out = *in + if in.ConfigMapRef != nil { + in, out := &in.ConfigMapRef, &out.ConfigMapRef + *out = new(Ref) + **out = **in + } + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(Ref) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvFrom. +func (in *EnvFrom) DeepCopy() *EnvFrom { + if in == nil { + return nil + } + out := new(EnvFrom) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExtraConfig) DeepCopyInto(out *ExtraConfig) { + *out = *in + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ExtraConfigItem, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtraConfig. +func (in *ExtraConfig) DeepCopy() *ExtraConfig { + if in == nil { + return nil + } + out := new(ExtraConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExtraConfigItem) DeepCopyInto(out *ExtraConfigItem) { + *out = *in + if in.ConfigMapRef != nil { + in, out := &in.ConfigMapRef, &out.ConfigMapRef + *out = new(Ref) + **out = **in + } + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(Ref) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtraConfigItem. +func (in *ExtraConfigItem) DeepCopy() *ExtraConfigItem { + if in == nil { + return nil + } + out := new(ExtraConfigItem) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Ref) DeepCopyInto(out *Ref) { *out = *in } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DynamicPluginsConfigRef. -func (in *DynamicPluginsConfigRef) DeepCopy() *DynamicPluginsConfigRef { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Ref. +func (in *Ref) DeepCopy() *Ref { if in == nil { return nil } - out := new(DynamicPluginsConfigRef) + out := new(Ref) in.DeepCopyInto(out) return out } diff --git a/bundle.Dockerfile b/bundle.Dockerfile index 513284c3..7887d751 100644 --- a/bundle.Dockerfile +++ b/bundle.Dockerfile @@ -6,7 +6,7 @@ LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ LABEL operators.operatorframework.io.bundle.package.v1=backstage-operator LABEL operators.operatorframework.io.bundle.channels.v1=alpha -LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.25.0 +LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.32.0 LABEL operators.operatorframework.io.metrics.mediatype.v1=metrics+v1 LABEL operators.operatorframework.io.metrics.project_layout=go.kubebuilder.io/v3 diff --git a/bundle/manifests/backstage-operator.clusterserviceversion.yaml b/bundle/manifests/backstage-operator.clusterserviceversion.yaml index 15ccac06..90b623bd 100644 --- a/bundle/manifests/backstage-operator.clusterserviceversion.yaml +++ b/bundle/manifests/backstage-operator.clusterserviceversion.yaml @@ -21,7 +21,8 @@ metadata: } ] capabilities: Basic Install - operators.operatorframework.io/builder: operator-sdk-v1.25.0 + createdAt: "2023-12-04T14:55:40Z" + operators.operatorframework.io/builder: operator-sdk-v1.32.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 name: backstage-operator.v0.0.1 namespace: placeholder diff --git a/bundle/manifests/janus-idp.io_backstages.yaml b/bundle/manifests/janus-idp.io_backstages.yaml index 803f96a7..12311035 100644 --- a/bundle/manifests/janus-idp.io_backstages.yaml +++ b/bundle/manifests/janus-idp.io_backstages.yaml @@ -34,76 +34,218 @@ spec: spec: description: BackstageSpec defines the desired state of Backstage properties: - appConfigs: - description: References to existing app-configs Config objects. Each - element can be a reference to any ConfigMap or Secret, and will - be mounted inside the main application container under a dedicated - directory containing the ConfigMap or Secret name. Additionally, - each file will be passed as a `--config /path/to/secret_or_configmap/key` - to the main container args in the order of the entries defined in - the AppConfigs list. But bear in mind that for a single AppConfig - element containing several files, the order in which those files - will be appended to the container args, the main container args - cannot be guaranteed. So if you want to pass multiple app-config - files, it is recommended to pass one ConfigMap/Secret per app-config - file. - items: - properties: - kind: - description: Type of the existing App Config object, either - ConfigMap or Secret - enum: - - ConfigMap - - Secret - type: string - name: - description: Name of an existing App Config object - type: string - required: - - kind - - name - type: object - type: array - backendAuthSecretRef: - description: Optional Backend Auth Secret Name. A new one will be - generated if not set. This Secret is used to set an environment - variable named 'APP_CONFIG_backend_auth_keys' in the main container, - which takes precedence over any 'backend.auth.keys' field defined - in default or custom application configuration files. This is required - for service-to-service auth and is shared by all backend plugins. + backstage: + description: Configuration for Backstage. Optional. properties: - key: - default: backend-secret - description: 'Key in the secret to use for the backend auth. Default - value is: backend-secret' + appConfig: + description: References to existing app-configs Config objects, + that will be mounted as files in the specified mount path. Each + element can be a reference to any ConfigMap or Secret, and will + be mounted inside the main application container under a dedicated + directory containing the ConfigMap or Secret name (relative + to the specified mount path). Additionally, each file will be + passed as a `--config /path/to/secret_or_configmap/key` to the + main container args in the order of the entries defined in the + AppConfigs list. But bear in mind that for a single AppConfig + element containing several files, the order in which those files + will be appended to the container args, the main container args + cannot be guaranteed. So if you want to pass multiple app-config + files, it is recommended to pass one ConfigMap/Secret per app-config + file. + properties: + items: + description: List of references to app-config Config objects. + items: + properties: + configMapRef: + description: ConfigMap containing one or more app-config + files + properties: + name: + description: Name of the object referenced. + type: string + required: + - name + type: object + secretRef: + description: Secret containing one or more app-config + files + properties: + name: + description: Name of the object referenced. + type: string + required: + - name + type: object + type: object + type: array + mountPath: + default: /opt/app-root/src + description: Mount path for all app-config files listed in + the Items field + type: string + type: object + backendAuthSecretRef: + description: Optional Reference to a Secret to use for Backend + Auth. A new one will be generated if not set. This Secret is + used to set an environment variable named 'APP_CONFIG_backend_auth_keys' + in the main container, which takes precedence over any 'backend.auth.keys' + field defined in default or custom application configuration + files. This is required for service-to-service auth and is shared + by all backend plugins. + properties: + key: + default: backend-secret + description: 'Key in the secret to use for the backend auth. + Default value is: backend-secret' + type: string + name: + description: Name of the secret to use for the backend auth + type: string + required: + - name + type: object + dynamicPlugins: + description: 'Reference to an existing configuration object for + Dynamic Plugins. This can be a reference to any ConfigMap or + Secret, but the object must have an existing key named: ''dynamic-plugins.yaml''' + properties: + configMapRef: + description: 'ConfigMap containing the dynamic plugins'' configuration. + It needs to have a key named: "dynamic-plugins.yaml". ConfigMapRef + will be used if both ConfigMapRef and SecretRef are provided.' + properties: + name: + description: Name of the object referenced. + type: string + required: + - name + type: object + secretRef: + description: 'Secret containing the dynamic plugins'' configuration. + It needs to have a key named: "dynamic-plugins.yaml". ConfigMapRef + will be used if both ConfigMapRef and SecretRef are provided.' + properties: + name: + description: Name of the object referenced. + type: string + required: + - name + type: object + type: object + env: + description: Environment variables to inject into all the Backstage + containers. Bear in mind not to put sensitive data here. Use + EnvFrom instead. + items: + properties: + name: + description: Name of the environment variable + type: string + value: + description: Value of the environment variable + type: string + required: + - name + - value + type: object + type: array + envFrom: + description: Environment variables to inject into all the Backstage + containers, as references to existing ConfigMap or Secret objects. + items: + properties: + configMapRef: + description: ConfigMap containing the environment variables + to inject + properties: + name: + description: Name of the object referenced. + type: string + required: + - name + type: object + secretRef: + description: Secret containing the environment variables + to inject + properties: + name: + description: Name of the object referenced. + type: string + required: + - name + type: object + type: object + type: array + extraConfig: + description: References to existing Config objects to use as extra + config files. They will be mounted as files in the specified + mount path. Each element can be a reference to any ConfigMap + or Secret. + properties: + items: + description: List of references to extra config Config objects. + items: + properties: + configMapRef: + description: ConfigMap containing one or more extra + config files + properties: + name: + description: Name of the object referenced. + type: string + required: + - name + type: object + secretRef: + description: Secret containing one or more extra config + files + properties: + name: + description: Name of the object referenced. + type: string + required: + - name + type: object + type: object + type: array + mountPath: + default: /opt/app-root/src + description: Mount path for all extra configuration files + listed in the Items field + type: string + type: object + image: + description: Image to use in all containers (including Init Containers) type: string - name: - description: Name of the secret to use for the backend auth + imagePullSecret: + description: Image Pull Secret to use in all containers (including + Init Containers) type: string - required: - - name + replicas: + default: 1 + description: Number of desired replicas to set in the Backstage + Deployment. Defaults to 1. + format: int32 + type: integer type: object - dynamicPluginsConfig: - description: 'Reference to an existing configuration object for Dynamic - Plugins. This can be a reference to any ConfigMap or Secret, but - the object must have an existing key named: ''dynamic-plugins.yaml''' + postgresql: + description: Configuration for the local database. Optional. properties: - kind: - description: Type of the Dynamic Plugins config object, either - ConfigMap or Secret - enum: - - ConfigMap - - Secret - type: string - name: - description: Name of the Dynamic Plugins config object - type: string - required: - - kind - - name + disabled: + default: true + description: Control the creation of a local PostgreSQL DB. Set + to false if using for example an external Database for Backstage. + To use an external Database, you can provide your own app-config + file (see the AppConfig field) containing references to the + Database connection information, which might be supplied as + environment variables (see the Env field) or extra-configuration + files (see the ExtraConfig field in the BackstageSpecBackstage + structure). + type: boolean type: object rawRuntimeConfig: - description: Raw Runtime Objects configuration + description: Raw Runtime Objects configuration. For Advanced scenarios. properties: backstageConfig: description: Name of ConfigMap containing Backstage runtime objects @@ -114,9 +256,6 @@ spec: runtime objects configuration type: string type: object - skipLocalDb: - default: false - type: boolean type: object status: description: BackstageStatus defines the observed state of Backstage diff --git a/bundle/metadata/annotations.yaml b/bundle/metadata/annotations.yaml index c877e0fb..a6b3d099 100644 --- a/bundle/metadata/annotations.yaml +++ b/bundle/metadata/annotations.yaml @@ -5,7 +5,7 @@ annotations: operators.operatorframework.io.bundle.metadata.v1: metadata/ operators.operatorframework.io.bundle.package.v1: backstage-operator operators.operatorframework.io.bundle.channels.v1: alpha - operators.operatorframework.io.metrics.builder: operator-sdk-v1.25.0 + operators.operatorframework.io.metrics.builder: operator-sdk-v1.32.0 operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 operators.operatorframework.io.metrics.project_layout: go.kubebuilder.io/v3 diff --git a/config/crd/bases/janus-idp.io_backstages.yaml b/config/crd/bases/janus-idp.io_backstages.yaml index ce2ae5b5..f58ce8d4 100644 --- a/config/crd/bases/janus-idp.io_backstages.yaml +++ b/config/crd/bases/janus-idp.io_backstages.yaml @@ -35,76 +35,218 @@ spec: spec: description: BackstageSpec defines the desired state of Backstage properties: - appConfigs: - description: References to existing app-configs Config objects. Each - element can be a reference to any ConfigMap or Secret, and will - be mounted inside the main application container under a dedicated - directory containing the ConfigMap or Secret name. Additionally, - each file will be passed as a `--config /path/to/secret_or_configmap/key` - to the main container args in the order of the entries defined in - the AppConfigs list. But bear in mind that for a single AppConfig - element containing several files, the order in which those files - will be appended to the container args, the main container args - cannot be guaranteed. So if you want to pass multiple app-config - files, it is recommended to pass one ConfigMap/Secret per app-config - file. - items: - properties: - kind: - description: Type of the existing App Config object, either - ConfigMap or Secret - enum: - - ConfigMap - - Secret - type: string - name: - description: Name of an existing App Config object - type: string - required: - - kind - - name - type: object - type: array - backendAuthSecretRef: - description: Optional Backend Auth Secret Name. A new one will be - generated if not set. This Secret is used to set an environment - variable named 'APP_CONFIG_backend_auth_keys' in the main container, - which takes precedence over any 'backend.auth.keys' field defined - in default or custom application configuration files. This is required - for service-to-service auth and is shared by all backend plugins. + backstage: + description: Configuration for Backstage. Optional. properties: - key: - default: backend-secret - description: 'Key in the secret to use for the backend auth. Default - value is: backend-secret' + appConfig: + description: References to existing app-configs Config objects, + that will be mounted as files in the specified mount path. Each + element can be a reference to any ConfigMap or Secret, and will + be mounted inside the main application container under a dedicated + directory containing the ConfigMap or Secret name (relative + to the specified mount path). Additionally, each file will be + passed as a `--config /path/to/secret_or_configmap/key` to the + main container args in the order of the entries defined in the + AppConfigs list. But bear in mind that for a single AppConfig + element containing several files, the order in which those files + will be appended to the container args, the main container args + cannot be guaranteed. So if you want to pass multiple app-config + files, it is recommended to pass one ConfigMap/Secret per app-config + file. + properties: + items: + description: List of references to app-config Config objects. + items: + properties: + configMapRef: + description: ConfigMap containing one or more app-config + files + properties: + name: + description: Name of the object referenced. + type: string + required: + - name + type: object + secretRef: + description: Secret containing one or more app-config + files + properties: + name: + description: Name of the object referenced. + type: string + required: + - name + type: object + type: object + type: array + mountPath: + default: /opt/app-root/src + description: Mount path for all app-config files listed in + the Items field + type: string + type: object + backendAuthSecretRef: + description: Optional Reference to a Secret to use for Backend + Auth. A new one will be generated if not set. This Secret is + used to set an environment variable named 'APP_CONFIG_backend_auth_keys' + in the main container, which takes precedence over any 'backend.auth.keys' + field defined in default or custom application configuration + files. This is required for service-to-service auth and is shared + by all backend plugins. + properties: + key: + default: backend-secret + description: 'Key in the secret to use for the backend auth. + Default value is: backend-secret' + type: string + name: + description: Name of the secret to use for the backend auth + type: string + required: + - name + type: object + dynamicPlugins: + description: 'Reference to an existing configuration object for + Dynamic Plugins. This can be a reference to any ConfigMap or + Secret, but the object must have an existing key named: ''dynamic-plugins.yaml''' + properties: + configMapRef: + description: 'ConfigMap containing the dynamic plugins'' configuration. + It needs to have a key named: "dynamic-plugins.yaml". ConfigMapRef + will be used if both ConfigMapRef and SecretRef are provided.' + properties: + name: + description: Name of the object referenced. + type: string + required: + - name + type: object + secretRef: + description: 'Secret containing the dynamic plugins'' configuration. + It needs to have a key named: "dynamic-plugins.yaml". ConfigMapRef + will be used if both ConfigMapRef and SecretRef are provided.' + properties: + name: + description: Name of the object referenced. + type: string + required: + - name + type: object + type: object + env: + description: Environment variables to inject into all the Backstage + containers. Bear in mind not to put sensitive data here. Use + EnvFrom instead. + items: + properties: + name: + description: Name of the environment variable + type: string + value: + description: Value of the environment variable + type: string + required: + - name + - value + type: object + type: array + envFrom: + description: Environment variables to inject into all the Backstage + containers, as references to existing ConfigMap or Secret objects. + items: + properties: + configMapRef: + description: ConfigMap containing the environment variables + to inject + properties: + name: + description: Name of the object referenced. + type: string + required: + - name + type: object + secretRef: + description: Secret containing the environment variables + to inject + properties: + name: + description: Name of the object referenced. + type: string + required: + - name + type: object + type: object + type: array + extraConfig: + description: References to existing Config objects to use as extra + config files. They will be mounted as files in the specified + mount path. Each element can be a reference to any ConfigMap + or Secret. + properties: + items: + description: List of references to extra config Config objects. + items: + properties: + configMapRef: + description: ConfigMap containing one or more extra + config files + properties: + name: + description: Name of the object referenced. + type: string + required: + - name + type: object + secretRef: + description: Secret containing one or more extra config + files + properties: + name: + description: Name of the object referenced. + type: string + required: + - name + type: object + type: object + type: array + mountPath: + default: /opt/app-root/src + description: Mount path for all extra configuration files + listed in the Items field + type: string + type: object + image: + description: Image to use in all containers (including Init Containers) type: string - name: - description: Name of the secret to use for the backend auth + imagePullSecret: + description: Image Pull Secret to use in all containers (including + Init Containers) type: string - required: - - name + replicas: + default: 1 + description: Number of desired replicas to set in the Backstage + Deployment. Defaults to 1. + format: int32 + type: integer type: object - dynamicPluginsConfig: - description: 'Reference to an existing configuration object for Dynamic - Plugins. This can be a reference to any ConfigMap or Secret, but - the object must have an existing key named: ''dynamic-plugins.yaml''' + postgresql: + description: Configuration for the local database. Optional. properties: - kind: - description: Type of the Dynamic Plugins config object, either - ConfigMap or Secret - enum: - - ConfigMap - - Secret - type: string - name: - description: Name of the Dynamic Plugins config object - type: string - required: - - kind - - name + disabled: + default: true + description: Control the creation of a local PostgreSQL DB. Set + to false if using for example an external Database for Backstage. + To use an external Database, you can provide your own app-config + file (see the AppConfig field) containing references to the + Database connection information, which might be supplied as + environment variables (see the Env field) or extra-configuration + files (see the ExtraConfig field in the BackstageSpecBackstage + structure). + type: boolean type: object rawRuntimeConfig: - description: Raw Runtime Objects configuration + description: Raw Runtime Objects configuration. For Advanced scenarios. properties: backstageConfig: description: Name of ConfigMap containing Backstage runtime objects @@ -115,9 +257,6 @@ spec: runtime objects configuration type: string type: object - skipLocalDb: - default: false - type: boolean type: object status: description: BackstageStatus defines the observed state of Backstage From a6a79b67c87c6007cdb6680a86705cdc01028d0e Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 4 Dec 2023 16:08:27 +0100 Subject: [PATCH 03/31] Handle Number of Replicas, Image and Image Pull Secret fields --- controllers/backstage_deployment.go | 33 +++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/controllers/backstage_deployment.go b/controllers/backstage_deployment.go index 20e3ae28..bf678275 100644 --- a/controllers/backstage_deployment.go +++ b/controllers/backstage_deployment.go @@ -20,6 +20,7 @@ import ( bs "janus-idp.io/backstage-operator/api/v1alpha1" appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -128,6 +129,22 @@ const ( //`, _defaultBackstageInitContainerName, _defaultBackstageMainContainerName, _containersWorkingDir) //) +// ContainerVisitor is called with each container +type ContainerVisitor func(container *v1.Container) + +// visitContainers invokes the visitor function for every container in the given pod template spec +func visitContainers(podTemplateSpec *v1.PodTemplateSpec, visitor ContainerVisitor) { + for i := range podTemplateSpec.Spec.InitContainers { + visitor(&podTemplateSpec.Spec.InitContainers[i]) + } + for i := range podTemplateSpec.Spec.Containers { + visitor(&podTemplateSpec.Spec.Containers[i]) + } + for i := range podTemplateSpec.Spec.EphemeralContainers { + visitor((*v1.Container)(&podTemplateSpec.Spec.EphemeralContainers[i].EphemeralContainerCommon)) + } +} + func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, backstage bs.Backstage, ns string) error { //lg := log.FromContext(ctx) @@ -173,6 +190,22 @@ func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, back return fmt.Errorf("failed to add env vars to Backstage deployment, reason: %s", err) } + if backstage.Spec.Backstage != nil { + deployment.Spec.Replicas = backstage.Spec.Backstage.Replicas + + if backstage.Spec.Backstage.Image != nil { + visitContainers(&deployment.Spec.Template, func(container *v1.Container) { + container.Image = *backstage.Spec.Backstage.Image + }) + } + + if backstage.Spec.Backstage.ImagePullSecret != nil { + deployment.Spec.Template.Spec.ImagePullSecrets = append(deployment.Spec.Template.Spec.ImagePullSecrets, v1.LocalObjectReference{ + Name: *backstage.Spec.Backstage.ImagePullSecret, + }) + } + } + err = r.Create(ctx, deployment) if err != nil { return fmt.Errorf("failed to create backstage deployment, reason: %s", err) From 7a7d527a2c2c838d853869eecab905a0a26cca8f Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 4 Dec 2023 16:09:07 +0100 Subject: [PATCH 04/31] Handle Env and EnvFrom fields to set environment variables --- controllers/backstage_deployment.go | 49 ++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/controllers/backstage_deployment.go b/controllers/backstage_deployment.go index bf678275..12a4b140 100644 --- a/controllers/backstage_deployment.go +++ b/controllers/backstage_deployment.go @@ -250,5 +250,52 @@ func (r *BackstageReconciler) addContainerArgs(ctx context.Context, backstage bs } func (r *BackstageReconciler) addEnvVars(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { - return r.addBackendAuthEnvVar(ctx, backstage, ns, deployment) + err := r.addBackendAuthEnvVar(ctx, backstage, ns, deployment) + if err != nil { + return err + } + if backstage.Spec.Backstage == nil { + return nil + } + + for _, env := range backstage.Spec.Backstage.Env { + visitContainers(&deployment.Spec.Template, func(container *v1.Container) { + container.Env = append(container.Env, v1.EnvVar{ + Name: env.Name, + Value: env.Value, + }) + }) + } + + for _, envFrom := range backstage.Spec.Backstage.EnvFrom { + var ( + name string + cmSrc *v1.ConfigMapEnvSource + secSrc *v1.SecretEnvSource + ) + switch { + case envFrom.ConfigMapRef != nil: + name = envFrom.ConfigMapRef.Name + cmSrc = &v1.ConfigMapEnvSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: name, + }, + } + case envFrom.SecretRef != nil: + name = envFrom.SecretRef.Name + secSrc = &v1.SecretEnvSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: name, + }, + } + } + visitContainers(&deployment.Spec.Template, func(container *v1.Container) { + container.EnvFrom = append(container.EnvFrom, v1.EnvFromSource{ + ConfigMapRef: cmSrc, + SecretRef: secSrc, + }) + }) + } + + return nil } From 6a6959ff4534c6a236361f7886df73a8899341ef Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 4 Dec 2023 16:10:57 +0100 Subject: [PATCH 05/31] Handle Volumes and Volume Mounts for app-configs, dynamic plugins config and extra config files --- controllers/backstage_app_config.go | 69 ++++++++---- controllers/backstage_deployment.go | 7 +- controllers/backstage_dynamic_plugins.go | 48 +++++---- controllers/backstage_extra_config.go | 131 +++++++++++++++++++++++ 4 files changed, 213 insertions(+), 42 deletions(-) create mode 100644 controllers/backstage_extra_config.go diff --git a/controllers/backstage_app_config.go b/controllers/backstage_app_config.go index fde78bb2..962b42c0 100644 --- a/controllers/backstage_app_config.go +++ b/controllers/backstage_app_config.go @@ -31,23 +31,29 @@ type appConfigData struct { } func (r *BackstageReconciler) appConfigsToVolumes(backstage bs.Backstage) (result []v1.Volume) { - for _, appConfig := range backstage.Spec.AppConfigs { + if backstage.Spec.Backstage == nil || backstage.Spec.Backstage.AppConfig == nil { + return nil + } + for _, appConfig := range backstage.Spec.Backstage.AppConfig.Items { var volumeSource v1.VolumeSource - switch appConfig.Kind { - case "ConfigMap": + var name string + switch { + case appConfig.ConfigMapRef != nil: + name = appConfig.ConfigMapRef.Name volumeSource.ConfigMap = &v1.ConfigMapVolumeSource{ DefaultMode: pointer.Int32(420), - LocalObjectReference: v1.LocalObjectReference{Name: appConfig.Name}, + LocalObjectReference: v1.LocalObjectReference{Name: name}, } - case "Secret": + case appConfig.SecretRef != nil: + name = appConfig.SecretRef.Name volumeSource.Secret = &v1.SecretVolumeSource{ DefaultMode: pointer.Int32(420), - SecretName: appConfig.Name, + SecretName: name, } } result = append(result, v1.Volume{ - Name: appConfig.Name, + Name: name, VolumeSource: volumeSource, }, ) @@ -57,6 +63,10 @@ func (r *BackstageReconciler) appConfigsToVolumes(backstage bs.Backstage) (resul } func (r *BackstageReconciler) addAppConfigsVolumeMounts(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { + if backstage.Spec.Backstage == nil || backstage.Spec.Backstage.AppConfig == nil { + return nil + } + appConfigFilenamesList, err := r.extractAppConfigFileNames(ctx, backstage, ns) if err != nil { return err @@ -65,11 +75,14 @@ func (r *BackstageReconciler) addAppConfigsVolumeMounts(ctx context.Context, bac for i, c := range deployment.Spec.Template.Spec.Containers { if c.Name == _defaultBackstageMainContainerName { for _, appConfigFilenames := range appConfigFilenamesList { - deployment.Spec.Template.Spec.Containers[i].VolumeMounts = append(deployment.Spec.Template.Spec.Containers[i].VolumeMounts, - v1.VolumeMount{ - Name: appConfigFilenames.ref, - MountPath: fmt.Sprintf("%s/%s", _containersWorkingDir, appConfigFilenames.ref), - }) + for _, f := range appConfigFilenames.files { + deployment.Spec.Template.Spec.Containers[i].VolumeMounts = append(deployment.Spec.Template.Spec.Containers[i].VolumeMounts, + v1.VolumeMount{ + Name: appConfigFilenames.ref, + MountPath: fmt.Sprintf("%s/%s", backstage.Spec.Backstage.AppConfig.MountPath, f), + SubPath: f, + }) + } } break } @@ -78,6 +91,10 @@ func (r *BackstageReconciler) addAppConfigsVolumeMounts(ctx context.Context, bac } func (r *BackstageReconciler) addAppConfigsContainerArgs(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { + if backstage.Spec.Backstage == nil || backstage.Spec.Backstage.AppConfig == nil { + return nil + } + appConfigFilenamesList, err := r.extractAppConfigFileNames(ctx, backstage, ns) if err != nil { return err @@ -88,9 +105,9 @@ func (r *BackstageReconciler) addAppConfigsContainerArgs(ctx context.Context, ba for _, appConfigFilenames := range appConfigFilenamesList { // Args for _, fileName := range appConfigFilenames.files { + appConfigPath := fmt.Sprintf("%s/%s", backstage.Spec.Backstage.AppConfig.MountPath, fileName) deployment.Spec.Template.Spec.Containers[i].Args = - append(deployment.Spec.Template.Spec.Containers[i].Args, "--config", - fmt.Sprintf("%s/%s/%s", _containersWorkingDir, appConfigFilenames.ref, fileName)) + append(deployment.Spec.Template.Spec.Containers[i].Args, "--config", appConfigPath) } } break @@ -102,14 +119,19 @@ func (r *BackstageReconciler) addAppConfigsContainerArgs(ctx context.Context, ba // extractAppConfigFileNames returns a mapping of app-config object name and the list of files in it. // We intentionally do not return a Map, to preserve the iteration order of the AppConfigs in the Custom Resource, // even though we can't guarantee the iteration order of the files listed inside each ConfigMap or Secret. -func (r *BackstageReconciler) extractAppConfigFileNames(ctx context.Context, backstage bs.Backstage, ns string) ([]appConfigData, error) { - var result []appConfigData - for _, appConfig := range backstage.Spec.AppConfigs { +func (r *BackstageReconciler) extractAppConfigFileNames(ctx context.Context, backstage bs.Backstage, ns string) (result []appConfigData, err error) { + if backstage.Spec.Backstage == nil || backstage.Spec.Backstage.AppConfig == nil { + return nil, nil + } + + for _, appConfig := range backstage.Spec.Backstage.AppConfig.Items { var files []string - switch appConfig.Kind { - case "ConfigMap": + var name string + switch { + case appConfig.ConfigMapRef != nil: + name = appConfig.ConfigMapRef.Name cm := v1.ConfigMap{} - if err := r.Get(ctx, types.NamespacedName{Name: appConfig.Name, Namespace: ns}, &cm); err != nil { + if err = r.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, &cm); err != nil { return nil, err } for filename := range cm.Data { @@ -120,9 +142,10 @@ func (r *BackstageReconciler) extractAppConfigFileNames(ctx context.Context, bac // Bear in mind that iteration order over this map is not guaranteed by Go files = append(files, filename) } - case "Secret": + case appConfig.SecretRef != nil: + name = appConfig.SecretRef.Name sec := v1.Secret{} - if err := r.Get(ctx, types.NamespacedName{Name: appConfig.Name, Namespace: ns}, &sec); err != nil { + if err = r.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, &sec); err != nil { return nil, err } for filename := range sec.Data { @@ -131,7 +154,7 @@ func (r *BackstageReconciler) extractAppConfigFileNames(ctx context.Context, bac } } result = append(result, appConfigData{ - ref: appConfig.Name, + ref: name, files: files, }) } diff --git a/controllers/backstage_deployment.go b/controllers/backstage_deployment.go index 12a4b140..cb3d7037 100644 --- a/controllers/backstage_deployment.go +++ b/controllers/backstage_deployment.go @@ -234,6 +234,7 @@ func (r *BackstageReconciler) addVolumes(ctx context.Context, backstage bs.Backs } deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, r.appConfigsToVolumes(backstage)...) + deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, r.extraConfigsToVolumes(backstage)...) return nil } @@ -242,7 +243,11 @@ func (r *BackstageReconciler) addVolumeMounts(ctx context.Context, backstage bs. if err != nil { return err } - return r.addAppConfigsVolumeMounts(ctx, backstage, ns, deployment) + err = r.addAppConfigsVolumeMounts(ctx, backstage, ns, deployment) + if err != nil { + return err + } + return r.addExtraConfigsVolumeMounts(ctx, backstage, ns, deployment) } func (r *BackstageReconciler) addContainerArgs(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { diff --git a/controllers/backstage_dynamic_plugins.go b/controllers/backstage_dynamic_plugins.go index af28fc34..d0ebb327 100644 --- a/controllers/backstage_dynamic_plugins.go +++ b/controllers/backstage_dynamic_plugins.go @@ -40,16 +40,16 @@ import ( //` //) -func (r *BackstageReconciler) getOrGenerateDynamicPluginsConf(ctx context.Context, backstage bs.Backstage, ns string) (config bs.DynamicPluginsConfigRef, err error) { - if backstage.Spec.DynamicPluginsConfig != nil { - return *backstage.Spec.DynamicPluginsConfig, nil +func (r *BackstageReconciler) getOrGenerateDynamicPluginsConf(ctx context.Context, backstage bs.Backstage, ns string) (config bs.DynamicPluginsConfig, err error) { + if backstage.Spec.Backstage != nil && backstage.Spec.Backstage.DynamicPluginsConfig != nil { + return *backstage.Spec.Backstage.DynamicPluginsConfig, nil } //Create default ConfigMap for dynamic plugins var cm v1.ConfigMap err = r.readConfigMapOrDefault(ctx, backstage.Spec.RawRuntimeConfig.BackstageConfigName, "dynamic-plugins-configmap.yaml", ns, &cm) if err != nil { - return bs.DynamicPluginsConfigRef{}, fmt.Errorf("failed to read config: %s", err) + return bs.DynamicPluginsConfig{}, fmt.Errorf("failed to read config: %s", err) } dpConfigName := fmt.Sprintf("%s-dynamic-plugins", backstage.Name) @@ -57,17 +57,18 @@ func (r *BackstageReconciler) getOrGenerateDynamicPluginsConf(ctx context.Contex err = r.Get(ctx, types.NamespacedName{Name: dpConfigName, Namespace: ns}, &cm) if err != nil { if !errors.IsNotFound(err) { - return bs.DynamicPluginsConfigRef{}, fmt.Errorf("failed to get config map for dynamic plugins (%q), reason: %s", dpConfigName, err) + return bs.DynamicPluginsConfig{}, fmt.Errorf("failed to get config map for dynamic plugins (%q), reason: %s", dpConfigName, err) } err = r.Create(ctx, &cm) if err != nil { - return bs.DynamicPluginsConfigRef{}, fmt.Errorf("failed to create config map for dynamic plugins, reason: %s", err) + return bs.DynamicPluginsConfig{}, fmt.Errorf("failed to create config map for dynamic plugins, reason: %s", err) } } - return bs.DynamicPluginsConfigRef{ - Name: dpConfigName, - Kind: "ConfigMap", + return bs.DynamicPluginsConfig{ + ConfigMapRef: &bs.Ref{ + Name: dpConfigName, + }, }, nil } @@ -77,26 +78,29 @@ func (r *BackstageReconciler) getDynamicPluginsConfVolume(ctx context.Context, b return nil, err } - if dpConf.Name == "" { + if dpConf.ConfigMapRef == nil && dpConf.SecretRef == nil { return nil, nil } var volumeSource v1.VolumeSource - switch dpConf.Kind { - case "ConfigMap": + var name string + switch { + case dpConf.ConfigMapRef != nil: + name = dpConf.ConfigMapRef.Name volumeSource.ConfigMap = &v1.ConfigMapVolumeSource{ DefaultMode: pointer.Int32(420), - LocalObjectReference: v1.LocalObjectReference{Name: dpConf.Name}, + LocalObjectReference: v1.LocalObjectReference{Name: name}, } - case "Secret": + case dpConf.SecretRef != nil: + name = dpConf.SecretRef.Name volumeSource.Secret = &v1.SecretVolumeSource{ DefaultMode: pointer.Int32(420), - SecretName: dpConf.Name, + SecretName: name, } } return &v1.Volume{ - Name: dpConf.Name, + Name: name, VolumeSource: volumeSource, }, nil } @@ -107,15 +111,23 @@ func (r *BackstageReconciler) addDynamicPluginsConfVolumeMount(ctx context.Conte return err } - if dpConf.Name == "" { + if dpConf.ConfigMapRef == nil && dpConf.SecretRef == nil { return nil } + var name string + switch { + case dpConf.ConfigMapRef != nil: + name = dpConf.ConfigMapRef.Name + case dpConf.SecretRef != nil: + name = dpConf.SecretRef.Name + } + for i, c := range deployment.Spec.Template.Spec.InitContainers { if c.Name == _defaultBackstageInitContainerName { deployment.Spec.Template.Spec.InitContainers[i].VolumeMounts = append(deployment.Spec.Template.Spec.InitContainers[i].VolumeMounts, v1.VolumeMount{ - Name: dpConf.Name, + Name: name, MountPath: fmt.Sprintf("%s/dynamic-plugins.yaml", _containersWorkingDir), ReadOnly: true, SubPath: "dynamic-plugins.yaml", diff --git a/controllers/backstage_extra_config.go b/controllers/backstage_extra_config.go new file mode 100644 index 00000000..694a7f30 --- /dev/null +++ b/controllers/backstage_extra_config.go @@ -0,0 +1,131 @@ +// +// Copyright (c) 2023 Red Hat, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controller + +import ( + "context" + "fmt" + + bs "janus-idp.io/backstage-operator/api/v1alpha1" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/pointer" +) + +func (r *BackstageReconciler) extraConfigsToVolumes(backstage bs.Backstage) (result []v1.Volume) { + if backstage.Spec.Backstage == nil || backstage.Spec.Backstage.ExtraConfig == nil { + return nil + } + for _, extraConfig := range backstage.Spec.Backstage.ExtraConfig.Items { + var volumeSource v1.VolumeSource + var name string + switch { + case extraConfig.ConfigMapRef != nil: + name = extraConfig.ConfigMapRef.Name + volumeSource.ConfigMap = &v1.ConfigMapVolumeSource{ + DefaultMode: pointer.Int32(420), + LocalObjectReference: v1.LocalObjectReference{Name: name}, + } + case extraConfig.SecretRef != nil: + name = extraConfig.SecretRef.Name + volumeSource.Secret = &v1.SecretVolumeSource{ + DefaultMode: pointer.Int32(420), + SecretName: name, + } + } + result = append(result, + v1.Volume{ + Name: name, + VolumeSource: volumeSource, + }, + ) + } + + return result +} + +func (r *BackstageReconciler) addExtraConfigsVolumeMounts(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { + if backstage.Spec.Backstage == nil || backstage.Spec.Backstage.ExtraConfig == nil { + return nil + } + + appConfigFilenamesList, err := r.extractExtraConfigFileNames(ctx, backstage, ns) + if err != nil { + return err + } + + for i, c := range deployment.Spec.Template.Spec.Containers { + if c.Name == _defaultBackstageMainContainerName { + for _, appConfigFilenames := range appConfigFilenamesList { + for _, f := range appConfigFilenames.files { + deployment.Spec.Template.Spec.Containers[i].VolumeMounts = append(deployment.Spec.Template.Spec.Containers[i].VolumeMounts, + v1.VolumeMount{ + Name: appConfigFilenames.ref, + MountPath: fmt.Sprintf("%s/%s", backstage.Spec.Backstage.ExtraConfig.MountPath, f), + SubPath: f, + }) + } + } + break + } + } + return nil +} + +// extractExtraConfigFileNames returns a mapping of extra-config object name and the list of files in it. +// We intentionally do not return a Map, to preserve the iteration order of the ExtraConfigs in the Custom Resource, +// even though we can't guarantee the iteration order of the files listed inside each ConfigMap or Secret. +func (r *BackstageReconciler) extractExtraConfigFileNames(ctx context.Context, backstage bs.Backstage, ns string) (result []appConfigData, err error) { + if backstage.Spec.Backstage == nil || backstage.Spec.Backstage.ExtraConfig == nil { + return nil, nil + } + + for _, appConfig := range backstage.Spec.Backstage.ExtraConfig.Items { + var files []string + var name string + switch { + case appConfig.ConfigMapRef != nil: + name = appConfig.ConfigMapRef.Name + cm := v1.ConfigMap{} + if err = r.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, &cm); err != nil { + return nil, err + } + for filename := range cm.Data { + // Bear in mind that iteration order over this map is not guaranteed by Go + files = append(files, filename) + } + for filename := range cm.BinaryData { + // Bear in mind that iteration order over this map is not guaranteed by Go + files = append(files, filename) + } + case appConfig.SecretRef != nil: + name = appConfig.SecretRef.Name + sec := v1.Secret{} + if err = r.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, &sec); err != nil { + return nil, err + } + for filename := range sec.Data { + // Bear in mind that iteration order over this map is not guaranteed by Go + files = append(files, filename) + } + } + result = append(result, appConfigData{ + ref: name, + files: files, + }) + } + return result, nil +} From b7947b7094fd64c06fc723312f4028ce269b20a1 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 4 Dec 2023 16:11:35 +0100 Subject: [PATCH 06/31] Fix Backend Auth Secret handling --- controllers/backstage_backend_auth.go | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/controllers/backstage_backend_auth.go b/controllers/backstage_backend_auth.go index 6cc532c9..30d70550 100644 --- a/controllers/backstage_backend_auth.go +++ b/controllers/backstage_backend_auth.go @@ -41,8 +41,8 @@ var ( ) func (r *BackstageReconciler) handleBackendAuthSecret(ctx context.Context, backstage bs.Backstage, ns string) (secretName string, err error) { - if backstage.Spec.BackendAuthSecretRef != nil { - return backstage.Spec.BackendAuthSecretRef.Name, nil + if backstage.Spec.Backstage != nil && backstage.Spec.Backstage.BackendAuthSecretRef != nil { + return backstage.Spec.Backstage.BackendAuthSecretRef.Name, nil } //Create default Secret for backend auth @@ -60,15 +60,6 @@ func (r *BackstageReconciler) handleBackendAuthSecret(ctx context.Context, backs if !errors.IsNotFound(err) { return "", fmt.Errorf("failed to get secret for backend auth (%q), reason: %s", backendAuthSecretName, err) } - var k string - if backstage.Spec.BackendAuthSecretRef != nil { - k = backstage.Spec.BackendAuthSecretRef.Key - } - if k == "" { - //TODO(rm3l): why kubebuilder default values do not work - k = "backend-secret" - } - // there should not be any difference between default and not default // if isDefault { // Create a secret with a random value @@ -81,7 +72,7 @@ func (r *BackstageReconciler) handleBackendAuthSecret(ctx context.Context, backs return base64.StdEncoding.EncodeToString(bytes) }(24) sec.Data = map[string][]byte{ - k: []byte(authVal), + "backend-secret": []byte(authVal), } // } err = r.Create(ctx, &sec) @@ -105,8 +96,8 @@ func (r *BackstageReconciler) addBackendAuthEnvVar(ctx context.Context, backstag for i, c := range deployment.Spec.Template.Spec.Containers { if c.Name == _defaultBackstageMainContainerName { var k string - if backstage.Spec.BackendAuthSecretRef != nil { - k = backstage.Spec.BackendAuthSecretRef.Key + if backstage.Spec.Backstage != nil && backstage.Spec.Backstage.BackendAuthSecretRef != nil { + k = backstage.Spec.Backstage.BackendAuthSecretRef.Key } if k == "" { //TODO(rm3l): why kubebuilder default values do not work From d07e568d8a644b387b9aadd86340b5dff2154339 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 4 Dec 2023 16:12:11 +0100 Subject: [PATCH 07/31] handle PostgreSQL field --- controllers/backstage_controller.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/controllers/backstage_controller.go b/controllers/backstage_controller.go index 58262bf1..57adc539 100644 --- a/controllers/backstage_controller.go +++ b/controllers/backstage_controller.go @@ -30,6 +30,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/utils/pointer" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" @@ -90,7 +91,7 @@ func (r *BackstageReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return ctrl.Result{}, fmt.Errorf("failed to load backstage deployment from the cluster: %w", err) } - if !backstage.Spec.SkipLocalDb { + if backstage.Spec.Postgresql == nil || pointer.BoolDeref(backstage.Spec.Postgresql.Enabled, true) { /* We use default strogeclass currently, and no PV is needed in that case. If we decide later on to support user provided storageclass we can enable pv creation. From 1cfa417debb0fe27e32bf3e0cb427c41101c65b0 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 4 Dec 2023 16:12:42 +0100 Subject: [PATCH 08/31] Update sample YAML manifest --- examples/janus-cr-with-app-configs.yaml | 122 +++++++++++++++++++----- 1 file changed, 99 insertions(+), 23 deletions(-) diff --git a/examples/janus-cr-with-app-configs.yaml b/examples/janus-cr-with-app-configs.yaml index 9fba835b..6fadd3ac 100644 --- a/examples/janus-cr-with-app-configs.yaml +++ b/examples/janus-cr-with-app-configs.yaml @@ -3,17 +3,38 @@ kind: Backstage metadata: name: my-backstage-app-with-app-config spec: - appConfigs: - - name: "my-backstage-config-cm1" - kind: ConfigMap - - name: "my-backstage-config-secret1" - kind: Secret - dynamicPluginsConfig: - name: my-dynamic-plugins-config-cm - kind: ConfigMap - backendAuthSecretRef: - name: "my-backstage-backend-auth-secret" - key: "my-auth-key" + backstage: + appConfig: + #mountPath: /opt/app-root/src + items: + - configMapRef: + name: "my-backstage-config-cm1" + - secretRef: + name: "my-backstage-config-secret1" + dynamicPlugins: + configMapRef: + name: my-dynamic-plugins-config-cm + env: + - name: MY_ENV_VAR_1 + value: my-value-1 + - name: MY_ENV_VAR_2 + value: my-value-2 + envFrom: + - configMapRef: + name: my-env-cm-1 + - secretRef: + name: my-env-secret1 + backendAuthSecretRef: + name: "my-backstage-backend-auth-secret" + key: "my-auth-key" + replicas: 2 + extraConfig: + mountPath: /tmp/my-extra-files + items: + - configMapRef: + name: "my-backstage-extra-config-cm1" + - secretRef: + name: "my-backstage-extra-config-secret1" --- apiVersion: v1 @@ -54,18 +75,19 @@ apiVersion: v1 kind: Secret metadata: name: my-backstage-config-secret1 -data: - # auth: - # # see https://janus-idp.io/docs/auth/ to learn about auth providers - # environment: development - # providers: - # github: - # development: - # clientId: 'xxx' - # clientSecret: 'yyy' - my-app-config-1.secret.yaml: YXV0aDoKICAjIHNlZSBodHRwczovL2JhY2tzdGFnZS5pby9kb2NzL2F1dGgvIHRvIGxlYXJuIGFib3V0IGF1dGggcHJvdmlkZXJzCiAgZW52aXJvbm1lbnQ6IGRldmVsb3BtZW50CiAgcHJvdmlkZXJzOgogICAgZ2l0aHViOgogICAgICBkZXZlbG9wbWVudDoKICAgICAgICBjbGllbnRJZDogJ3h4eCcKICAgICAgICBjbGllbnRTZWNyZXQ6ICd5eXknCg== +stringData: + my-app-config-1.secret.yaml: | + auth: + # see https://backstage.io/docs/auth/ to learn about auth providers + environment: development + providers: + github: + development: + clientId: '${MY_ENV_VAR_1}' + clientSecret: '${MY_ENV_VAR_2}' # # a comment - my-app-config-2.secret.yaml: IyBhIGNvbW1lbnQK + my-app-config-2.secret.yaml: | + # a comment --- apiVersion: v1 @@ -85,4 +107,58 @@ data: proxy: endpoints: /explore-backend-completed: - target: 'http://localhost:7017' \ No newline at end of file + target: 'http://localhost:7017' + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-env-cm-1 +data: + CM_ENV1: "cm env 1" + CM_ENV2: "cm env 2" + +--- +apiVersion: v1 +kind: Secret +metadata: + name: my-env-secret1 +stringData: + SEC_ENV1: "secret env 1" + SEC_ENV2: "secret env 2" + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-backstage-extra-config-cm1 +data: + cm_file1.txt: | + # From ConfigMap + Lorem Ipsum + Dolor Sit Amet + cm_file2.properties: | + conf.x=y + conf.y=z + +--- +apiVersion: v1 +kind: Secret +metadata: + name: my-backstage-extra-config-secret1 +stringData: + secret_file1.txt: | + # From Secret + Lorem Ipsum + Dolor Sit Amet + secret_file2.properties: | + sec.a=b + sec.b=c + secrets.prod.yaml: | + appId: 1 + webhookUrl: https://smee.io/foo + clientId: someGithubAppClientId + clientSecret: someGithubAppClientSecret + webhookSecret: someWebhookSecret + privateKey: | + SomeRsaPrivateKey From f1b43045857f13e592a728b34cce2f4d95e3c46d Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Tue, 5 Dec 2023 12:20:22 +0100 Subject: [PATCH 09/31] Simplify types to make them more legible Co-authored-by: Gennady Azarenkov --- api/v1alpha1/backstage_types.go | 10 +- api/v1alpha1/zz_generated.deepcopy.go | 174 +++++++++--------- config/crd/bases/janus-idp.io_backstages.yaml | 3 +- controllers/backstage_app_config.go | 16 +- controllers/backstage_backend_auth.go | 8 +- controllers/backstage_controller_test.go | 22 +-- controllers/backstage_deployment.go | 18 +- controllers/backstage_dynamic_plugins.go | 4 +- controllers/backstage_extra_config.go | 12 +- 9 files changed, 133 insertions(+), 134 deletions(-) diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index 4435106d..373015f1 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -26,16 +26,16 @@ const ( // BackstageSpec defines the desired state of Backstage type BackstageSpec struct { // Configuration for Backstage. Optional. - Backstage *BackstageSpecBackstage `json:"backstage,omitempty"` + Application *Application `json:"backstage,omitempty"` // Configuration for the local database. Optional. - Postgresql *BackstageSpecPostgresql `json:"postgresql,omitempty"` + Postgresql *Postgresql `json:"postgresql,omitempty"` // Raw Runtime Objects configuration. For Advanced scenarios. RawRuntimeConfig RuntimeConfig `json:"rawRuntimeConfig,omitempty"` } -type BackstageSpecBackstage struct { +type Application struct { // Optional Reference to a Secret to use for Backend Auth. A new one will be generated if not set. // This Secret is used to set an environment variable named 'APP_CONFIG_backend_auth_keys' in the @@ -184,11 +184,11 @@ type EnvFrom struct { SecretRef *Ref `json:"secretRef,omitempty"` } -type BackstageSpecPostgresql struct { +type Postgresql struct { // Control the creation of a local PostgreSQL DB. Set to false if using for example an external Database for Backstage. // To use an external Database, you can provide your own app-config file (see the AppConfig field) containing references // to the Database connection information, which might be supplied as environment variables (see the Env field) or - // extra-configuration files (see the ExtraConfig field in the BackstageSpecBackstage structure). + // extra-configuration files (see the ExtraConfig field in the Application structure). // +optional //+kubebuilder:default=true Enabled *bool `json:"disabled,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 2fcde668..9dd25fe5 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -22,7 +22,7 @@ limitations under the License. package v1alpha1 import ( - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -73,6 +73,68 @@ func (in *AppConfigItem) DeepCopy() *AppConfigItem { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Application) DeepCopyInto(out *Application) { + *out = *in + if in.BackendAuthSecretRef != nil { + in, out := &in.BackendAuthSecretRef, &out.BackendAuthSecretRef + *out = new(BackendAuthSecretRef) + **out = **in + } + if in.AppConfig != nil { + in, out := &in.AppConfig, &out.AppConfig + *out = new(AppConfig) + (*in).DeepCopyInto(*out) + } + if in.DynamicPluginsConfig != nil { + in, out := &in.DynamicPluginsConfig, &out.DynamicPluginsConfig + *out = new(DynamicPluginsConfig) + (*in).DeepCopyInto(*out) + } + if in.ExtraConfig != nil { + in, out := &in.ExtraConfig, &out.ExtraConfig + *out = new(ExtraConfig) + (*in).DeepCopyInto(*out) + } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]Env, len(*in)) + copy(*out, *in) + } + if in.EnvFrom != nil { + in, out := &in.EnvFrom, &out.EnvFrom + *out = make([]EnvFrom, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + if in.Image != nil { + in, out := &in.Image, &out.Image + *out = new(string) + **out = **in + } + if in.ImagePullSecret != nil { + in, out := &in.ImagePullSecret, &out.ImagePullSecret + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Application. +func (in *Application) DeepCopy() *Application { + if in == nil { + return nil + } + out := new(Application) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BackendAuthSecretRef) DeepCopyInto(out *BackendAuthSecretRef) { *out = *in @@ -150,14 +212,14 @@ func (in *BackstageList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BackstageSpec) DeepCopyInto(out *BackstageSpec) { *out = *in - if in.Backstage != nil { - in, out := &in.Backstage, &out.Backstage - *out = new(BackstageSpecBackstage) + if in.Application != nil { + in, out := &in.Application, &out.Application + *out = new(Application) (*in).DeepCopyInto(*out) } if in.Postgresql != nil { in, out := &in.Postgresql, &out.Postgresql - *out = new(BackstageSpecPostgresql) + *out = new(Postgresql) (*in).DeepCopyInto(*out) } out.RawRuntimeConfig = in.RawRuntimeConfig @@ -173,88 +235,6 @@ func (in *BackstageSpec) DeepCopy() *BackstageSpec { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BackstageSpecBackstage) DeepCopyInto(out *BackstageSpecBackstage) { - *out = *in - if in.BackendAuthSecretRef != nil { - in, out := &in.BackendAuthSecretRef, &out.BackendAuthSecretRef - *out = new(BackendAuthSecretRef) - **out = **in - } - if in.AppConfig != nil { - in, out := &in.AppConfig, &out.AppConfig - *out = new(AppConfig) - (*in).DeepCopyInto(*out) - } - if in.DynamicPluginsConfig != nil { - in, out := &in.DynamicPluginsConfig, &out.DynamicPluginsConfig - *out = new(DynamicPluginsConfig) - (*in).DeepCopyInto(*out) - } - if in.ExtraConfig != nil { - in, out := &in.ExtraConfig, &out.ExtraConfig - *out = new(ExtraConfig) - (*in).DeepCopyInto(*out) - } - if in.Env != nil { - in, out := &in.Env, &out.Env - *out = make([]Env, len(*in)) - copy(*out, *in) - } - if in.EnvFrom != nil { - in, out := &in.EnvFrom, &out.EnvFrom - *out = make([]EnvFrom, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.Replicas != nil { - in, out := &in.Replicas, &out.Replicas - *out = new(int32) - **out = **in - } - if in.Image != nil { - in, out := &in.Image, &out.Image - *out = new(string) - **out = **in - } - if in.ImagePullSecret != nil { - in, out := &in.ImagePullSecret, &out.ImagePullSecret - *out = new(string) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackstageSpecBackstage. -func (in *BackstageSpecBackstage) DeepCopy() *BackstageSpecBackstage { - if in == nil { - return nil - } - out := new(BackstageSpecBackstage) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BackstageSpecPostgresql) DeepCopyInto(out *BackstageSpecPostgresql) { - *out = *in - if in.Enabled != nil { - in, out := &in.Enabled, &out.Enabled - *out = new(bool) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackstageSpecPostgresql. -func (in *BackstageSpecPostgresql) DeepCopy() *BackstageSpecPostgresql { - if in == nil { - return nil - } - out := new(BackstageSpecPostgresql) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BackstageStatus) DeepCopyInto(out *BackstageStatus) { *out = *in @@ -389,6 +369,26 @@ func (in *ExtraConfigItem) DeepCopy() *ExtraConfigItem { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Postgresql) DeepCopyInto(out *Postgresql) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Postgresql. +func (in *Postgresql) DeepCopy() *Postgresql { + if in == nil { + return nil + } + out := new(Postgresql) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Ref) DeepCopyInto(out *Ref) { *out = *in diff --git a/config/crd/bases/janus-idp.io_backstages.yaml b/config/crd/bases/janus-idp.io_backstages.yaml index f58ce8d4..43972dc7 100644 --- a/config/crd/bases/janus-idp.io_backstages.yaml +++ b/config/crd/bases/janus-idp.io_backstages.yaml @@ -241,8 +241,7 @@ spec: file (see the AppConfig field) containing references to the Database connection information, which might be supplied as environment variables (see the Env field) or extra-configuration - files (see the ExtraConfig field in the BackstageSpecBackstage - structure). + files (see the ExtraConfig field in the Application structure). type: boolean type: object rawRuntimeConfig: diff --git a/controllers/backstage_app_config.go b/controllers/backstage_app_config.go index 962b42c0..b3c78959 100644 --- a/controllers/backstage_app_config.go +++ b/controllers/backstage_app_config.go @@ -31,10 +31,10 @@ type appConfigData struct { } func (r *BackstageReconciler) appConfigsToVolumes(backstage bs.Backstage) (result []v1.Volume) { - if backstage.Spec.Backstage == nil || backstage.Spec.Backstage.AppConfig == nil { + if backstage.Spec.Application == nil || backstage.Spec.Application.AppConfig == nil { return nil } - for _, appConfig := range backstage.Spec.Backstage.AppConfig.Items { + for _, appConfig := range backstage.Spec.Application.AppConfig.Items { var volumeSource v1.VolumeSource var name string switch { @@ -63,7 +63,7 @@ func (r *BackstageReconciler) appConfigsToVolumes(backstage bs.Backstage) (resul } func (r *BackstageReconciler) addAppConfigsVolumeMounts(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { - if backstage.Spec.Backstage == nil || backstage.Spec.Backstage.AppConfig == nil { + if backstage.Spec.Application == nil || backstage.Spec.Application.AppConfig == nil { return nil } @@ -79,7 +79,7 @@ func (r *BackstageReconciler) addAppConfigsVolumeMounts(ctx context.Context, bac deployment.Spec.Template.Spec.Containers[i].VolumeMounts = append(deployment.Spec.Template.Spec.Containers[i].VolumeMounts, v1.VolumeMount{ Name: appConfigFilenames.ref, - MountPath: fmt.Sprintf("%s/%s", backstage.Spec.Backstage.AppConfig.MountPath, f), + MountPath: fmt.Sprintf("%s/%s", backstage.Spec.Application.AppConfig.MountPath, f), SubPath: f, }) } @@ -91,7 +91,7 @@ func (r *BackstageReconciler) addAppConfigsVolumeMounts(ctx context.Context, bac } func (r *BackstageReconciler) addAppConfigsContainerArgs(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { - if backstage.Spec.Backstage == nil || backstage.Spec.Backstage.AppConfig == nil { + if backstage.Spec.Application == nil || backstage.Spec.Application.AppConfig == nil { return nil } @@ -105,7 +105,7 @@ func (r *BackstageReconciler) addAppConfigsContainerArgs(ctx context.Context, ba for _, appConfigFilenames := range appConfigFilenamesList { // Args for _, fileName := range appConfigFilenames.files { - appConfigPath := fmt.Sprintf("%s/%s", backstage.Spec.Backstage.AppConfig.MountPath, fileName) + appConfigPath := fmt.Sprintf("%s/%s", backstage.Spec.Application.AppConfig.MountPath, fileName) deployment.Spec.Template.Spec.Containers[i].Args = append(deployment.Spec.Template.Spec.Containers[i].Args, "--config", appConfigPath) } @@ -120,11 +120,11 @@ func (r *BackstageReconciler) addAppConfigsContainerArgs(ctx context.Context, ba // We intentionally do not return a Map, to preserve the iteration order of the AppConfigs in the Custom Resource, // even though we can't guarantee the iteration order of the files listed inside each ConfigMap or Secret. func (r *BackstageReconciler) extractAppConfigFileNames(ctx context.Context, backstage bs.Backstage, ns string) (result []appConfigData, err error) { - if backstage.Spec.Backstage == nil || backstage.Spec.Backstage.AppConfig == nil { + if backstage.Spec.Application == nil || backstage.Spec.Application.AppConfig == nil { return nil, nil } - for _, appConfig := range backstage.Spec.Backstage.AppConfig.Items { + for _, appConfig := range backstage.Spec.Application.AppConfig.Items { var files []string var name string switch { diff --git a/controllers/backstage_backend_auth.go b/controllers/backstage_backend_auth.go index 30d70550..cac81e12 100644 --- a/controllers/backstage_backend_auth.go +++ b/controllers/backstage_backend_auth.go @@ -41,8 +41,8 @@ var ( ) func (r *BackstageReconciler) handleBackendAuthSecret(ctx context.Context, backstage bs.Backstage, ns string) (secretName string, err error) { - if backstage.Spec.Backstage != nil && backstage.Spec.Backstage.BackendAuthSecretRef != nil { - return backstage.Spec.Backstage.BackendAuthSecretRef.Name, nil + if backstage.Spec.Application != nil && backstage.Spec.Application.BackendAuthSecretRef != nil { + return backstage.Spec.Application.BackendAuthSecretRef.Name, nil } //Create default Secret for backend auth @@ -96,8 +96,8 @@ func (r *BackstageReconciler) addBackendAuthEnvVar(ctx context.Context, backstag for i, c := range deployment.Spec.Template.Spec.Containers { if c.Name == _defaultBackstageMainContainerName { var k string - if backstage.Spec.Backstage != nil && backstage.Spec.Backstage.BackendAuthSecretRef != nil { - k = backstage.Spec.Backstage.BackendAuthSecretRef.Key + if backstage.Spec.Application != nil && backstage.Spec.Application.BackendAuthSecretRef != nil { + k = backstage.Spec.Application.BackendAuthSecretRef.Key } if k == "" { //TODO(rm3l): why kubebuilder default values do not work diff --git a/controllers/backstage_controller_test.go b/controllers/backstage_controller_test.go index 3a42f08b..1f0e0a09 100644 --- a/controllers/backstage_controller_test.go +++ b/controllers/backstage_controller_test.go @@ -477,7 +477,7 @@ spec: } } backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - Backstage: &bsv1alpha1.BackstageSpecBackstage{ + Application: &bsv1alpha1.Application{ AppConfig: &bsv1alpha1.AppConfig{ Items: []bsv1alpha1.AppConfigItem{item}, }, @@ -581,7 +581,7 @@ plugins: [] Expect(err).To(Not(HaveOccurred())) backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - Backstage: &bsv1alpha1.BackstageSpecBackstage{ + Application: &bsv1alpha1.Application{ AppConfig: &bsv1alpha1.AppConfig{ MountPath: mountPath, Items: []bsv1alpha1.AppConfigItem{ @@ -784,7 +784,7 @@ plugins: [] var backstage *bsv1alpha1.Backstage BeforeEach(func() { backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - Backstage: &bsv1alpha1.BackstageSpecBackstage{ + Application: &bsv1alpha1.Application{ BackendAuthSecretRef: &bsv1alpha1.BackendAuthSecretRef{ Name: "non-existing-secret", Key: key, @@ -861,7 +861,7 @@ plugins: [] err := k8sClient.Create(ctx, backendAuthSecret) Expect(err).To(Not(HaveOccurred())) backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - Backstage: &bsv1alpha1.BackstageSpecBackstage{ + Application: &bsv1alpha1.Application{ BackendAuthSecretRef: &bsv1alpha1.BackendAuthSecretRef{ Name: backendAuthSecretName, Key: key, @@ -946,7 +946,7 @@ plugins: [] } } backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - Backstage: &bsv1alpha1.BackstageSpecBackstage{ + Application: &bsv1alpha1.Application{ ExtraConfig: &bsv1alpha1.ExtraConfig{ Items: []bsv1alpha1.ExtraConfigItem{item}, }, @@ -1012,7 +1012,7 @@ plugins: [] Expect(err).To(Not(HaveOccurred())) backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - Backstage: &bsv1alpha1.BackstageSpecBackstage{ + Application: &bsv1alpha1.Application{ ExtraConfig: &bsv1alpha1.ExtraConfig{ MountPath: mountPath, Items: []bsv1alpha1.ExtraConfigItem{ @@ -1147,7 +1147,7 @@ plugins: [] Expect(err).To(Not(HaveOccurred())) backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - Backstage: &bsv1alpha1.BackstageSpecBackstage{ + Application: &bsv1alpha1.Application{ Env: []bsv1alpha1.Env{ {Name: "MY_ENV_VAR_1", Value: "value 10"}, {Name: "MY_ENV_VAR_2", Value: "value 20"}, @@ -1223,7 +1223,7 @@ plugins: [] BeforeEach(func() { backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - Backstage: &bsv1alpha1.BackstageSpecBackstage{ + Application: &bsv1alpha1.Application{ Image: &imageName, }, }) @@ -1270,7 +1270,7 @@ plugins: [] BeforeEach(func() { backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - Backstage: &bsv1alpha1.BackstageSpecBackstage{ + Application: &bsv1alpha1.Application{ ImagePullSecret: &imagePullSecretName, }, }) @@ -1319,7 +1319,7 @@ plugins: [] BeforeEach(func() { backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - Backstage: &bsv1alpha1.BackstageSpecBackstage{ + Application: &bsv1alpha1.Application{ Replicas: &nbReplicas, }, }) @@ -1363,7 +1363,7 @@ plugins: [] var backstage *bsv1alpha1.Backstage BeforeEach(func() { backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - Postgresql: &bsv1alpha1.BackstageSpecPostgresql{ + Postgresql: &bsv1alpha1.Postgresql{ Enabled: pointer.Bool(false), }, }) diff --git a/controllers/backstage_deployment.go b/controllers/backstage_deployment.go index cb3d7037..b26ad66d 100644 --- a/controllers/backstage_deployment.go +++ b/controllers/backstage_deployment.go @@ -190,18 +190,18 @@ func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, back return fmt.Errorf("failed to add env vars to Backstage deployment, reason: %s", err) } - if backstage.Spec.Backstage != nil { - deployment.Spec.Replicas = backstage.Spec.Backstage.Replicas + if backstage.Spec.Application != nil { + deployment.Spec.Replicas = backstage.Spec.Application.Replicas - if backstage.Spec.Backstage.Image != nil { + if backstage.Spec.Application.Image != nil { visitContainers(&deployment.Spec.Template, func(container *v1.Container) { - container.Image = *backstage.Spec.Backstage.Image + container.Image = *backstage.Spec.Application.Image }) } - if backstage.Spec.Backstage.ImagePullSecret != nil { + if backstage.Spec.Application.ImagePullSecret != nil { deployment.Spec.Template.Spec.ImagePullSecrets = append(deployment.Spec.Template.Spec.ImagePullSecrets, v1.LocalObjectReference{ - Name: *backstage.Spec.Backstage.ImagePullSecret, + Name: *backstage.Spec.Application.ImagePullSecret, }) } } @@ -259,11 +259,11 @@ func (r *BackstageReconciler) addEnvVars(ctx context.Context, backstage bs.Backs if err != nil { return err } - if backstage.Spec.Backstage == nil { + if backstage.Spec.Application == nil { return nil } - for _, env := range backstage.Spec.Backstage.Env { + for _, env := range backstage.Spec.Application.Env { visitContainers(&deployment.Spec.Template, func(container *v1.Container) { container.Env = append(container.Env, v1.EnvVar{ Name: env.Name, @@ -272,7 +272,7 @@ func (r *BackstageReconciler) addEnvVars(ctx context.Context, backstage bs.Backs }) } - for _, envFrom := range backstage.Spec.Backstage.EnvFrom { + for _, envFrom := range backstage.Spec.Application.EnvFrom { var ( name string cmSrc *v1.ConfigMapEnvSource diff --git a/controllers/backstage_dynamic_plugins.go b/controllers/backstage_dynamic_plugins.go index d0ebb327..c5cd86b1 100644 --- a/controllers/backstage_dynamic_plugins.go +++ b/controllers/backstage_dynamic_plugins.go @@ -41,8 +41,8 @@ import ( //) func (r *BackstageReconciler) getOrGenerateDynamicPluginsConf(ctx context.Context, backstage bs.Backstage, ns string) (config bs.DynamicPluginsConfig, err error) { - if backstage.Spec.Backstage != nil && backstage.Spec.Backstage.DynamicPluginsConfig != nil { - return *backstage.Spec.Backstage.DynamicPluginsConfig, nil + if backstage.Spec.Application != nil && backstage.Spec.Application.DynamicPluginsConfig != nil { + return *backstage.Spec.Application.DynamicPluginsConfig, nil } //Create default ConfigMap for dynamic plugins diff --git a/controllers/backstage_extra_config.go b/controllers/backstage_extra_config.go index 694a7f30..66e2f86c 100644 --- a/controllers/backstage_extra_config.go +++ b/controllers/backstage_extra_config.go @@ -26,10 +26,10 @@ import ( ) func (r *BackstageReconciler) extraConfigsToVolumes(backstage bs.Backstage) (result []v1.Volume) { - if backstage.Spec.Backstage == nil || backstage.Spec.Backstage.ExtraConfig == nil { + if backstage.Spec.Application == nil || backstage.Spec.Application.ExtraConfig == nil { return nil } - for _, extraConfig := range backstage.Spec.Backstage.ExtraConfig.Items { + for _, extraConfig := range backstage.Spec.Application.ExtraConfig.Items { var volumeSource v1.VolumeSource var name string switch { @@ -58,7 +58,7 @@ func (r *BackstageReconciler) extraConfigsToVolumes(backstage bs.Backstage) (res } func (r *BackstageReconciler) addExtraConfigsVolumeMounts(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { - if backstage.Spec.Backstage == nil || backstage.Spec.Backstage.ExtraConfig == nil { + if backstage.Spec.Application == nil || backstage.Spec.Application.ExtraConfig == nil { return nil } @@ -74,7 +74,7 @@ func (r *BackstageReconciler) addExtraConfigsVolumeMounts(ctx context.Context, b deployment.Spec.Template.Spec.Containers[i].VolumeMounts = append(deployment.Spec.Template.Spec.Containers[i].VolumeMounts, v1.VolumeMount{ Name: appConfigFilenames.ref, - MountPath: fmt.Sprintf("%s/%s", backstage.Spec.Backstage.ExtraConfig.MountPath, f), + MountPath: fmt.Sprintf("%s/%s", backstage.Spec.Application.ExtraConfig.MountPath, f), SubPath: f, }) } @@ -89,11 +89,11 @@ func (r *BackstageReconciler) addExtraConfigsVolumeMounts(ctx context.Context, b // We intentionally do not return a Map, to preserve the iteration order of the ExtraConfigs in the Custom Resource, // even though we can't guarantee the iteration order of the files listed inside each ConfigMap or Secret. func (r *BackstageReconciler) extractExtraConfigFileNames(ctx context.Context, backstage bs.Backstage, ns string) (result []appConfigData, err error) { - if backstage.Spec.Backstage == nil || backstage.Spec.Backstage.ExtraConfig == nil { + if backstage.Spec.Application == nil || backstage.Spec.Application.ExtraConfig == nil { return nil, nil } - for _, appConfig := range backstage.Spec.Backstage.ExtraConfig.Items { + for _, appConfig := range backstage.Spec.Application.ExtraConfig.Items { var files []string var name string switch { From 97d7234c31047c805731bc862cd4c5ce4a32597b Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Tue, 5 Dec 2023 12:32:56 +0100 Subject: [PATCH 10/31] Inject Env and EnvFrom into the main Deployment containers, not init or ephemeral containers Co-authored-by: Gennady Azarenkov --- api/v1alpha1/backstage_types.go | 4 +- config/crd/bases/janus-idp.io_backstages.yaml | 4 +- controllers/backstage_controller_test.go | 55 ++++++++++++------- controllers/backstage_deployment.go | 19 ++++--- 4 files changed, 48 insertions(+), 34 deletions(-) diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index 373015f1..0d0c91aa 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -69,12 +69,12 @@ type Application struct { //+optional ExtraConfig *ExtraConfig `json:"extraConfig,omitempty"` - // Environment variables to inject into all the Backstage containers. + // Environment variables to inject into the application containers. // Bear in mind not to put sensitive data here. Use EnvFrom instead. //+optional Env []Env `json:"env,omitempty"` - // Environment variables to inject into all the Backstage containers, as references to existing ConfigMap or Secret objects. + // Environment variables to inject into the application containers, as references to existing ConfigMap or Secret objects. //+optional EnvFrom []EnvFrom `json:"envFrom,omitempty"` diff --git a/config/crd/bases/janus-idp.io_backstages.yaml b/config/crd/bases/janus-idp.io_backstages.yaml index 43972dc7..5f8a4441 100644 --- a/config/crd/bases/janus-idp.io_backstages.yaml +++ b/config/crd/bases/janus-idp.io_backstages.yaml @@ -135,7 +135,7 @@ spec: type: object type: object env: - description: Environment variables to inject into all the Backstage + description: Environment variables to inject into the application containers. Bear in mind not to put sensitive data here. Use EnvFrom instead. items: @@ -152,7 +152,7 @@ spec: type: object type: array envFrom: - description: Environment variables to inject into all the Backstage + description: Environment variables to inject into the application containers, as references to existing ConfigMap or Secret objects. items: properties: diff --git a/controllers/backstage_controller_test.go b/controllers/backstage_controller_test.go index 1f0e0a09..4f66d02f 100644 --- a/controllers/backstage_controller_test.go +++ b/controllers/backstage_controller_test.go @@ -1187,27 +1187,40 @@ plugins: [] g.Expect(err).To(Not(HaveOccurred())) }, time.Minute, time.Second).Should(Succeed()) - visitContainers(&found.Spec.Template, func(container *corev1.Container) { - By(fmt.Sprintf("Checking Env in the Backstage Deployment - container: %q", container.Name), func() { - Expect(len(container.Env)).To(BeNumerically(">=", 2), - "Expected at least 2 items in Env for container %q, fot %d", container.Name, len(container.Env)) - envVar, ok := findEnvVar(container.Env, "MY_ENV_VAR_1") - Expect(ok).To(BeTrue(), "No env var with name MY_ENV_VAR_1 in init container") - Expect(envVar.Value).Should(Equal("value 10")) - envVar, ok = findEnvVar(container.Env, "MY_ENV_VAR_2") - Expect(ok).To(BeTrue(), "No env var with name MY_ENV_VAR_2 in init container") - Expect(envVar.Value).Should(Equal("value 20")) - }) - By(fmt.Sprintf("Checking EnvFrom in the Backstage Deployment - container: %q", container.Name), func() { - Expect(len(container.EnvFrom)).To(BeNumerically(">=", 2), - "Expected at least 2 items in EnvFrom for container %q, fot %d", container.Name, len(container.EnvFrom)) - envVar, ok := findEnvVarFrom(container.EnvFrom, envConfig1CmName) - Expect(ok).To(BeTrue(), "No ConfigMap-backed envFrom in init container: %s", envConfig1CmName) - Expect(envVar.ConfigMapRef).ShouldNot(BeNil()) - envVar, ok = findEnvVarFrom(container.EnvFrom, envConfig2SecretName) - Expect(ok).To(BeTrue(), "No ConfigMap-backed envFrom in init container: %s", envConfig2SecretName) - Expect(envVar.SecretRef).ShouldNot(BeNil()) - }) + mainCont := found.Spec.Template.Spec.Containers[0] + By(fmt.Sprintf("Checking Env in the Backstage Deployment - container: %q", mainCont.Name), func() { + Expect(len(mainCont.Env)).To(BeNumerically(">=", 2), + "Expected at least 2 items in Env for container %q, fot %d", mainCont.Name, len(mainCont.Env)) + envVar, ok := findEnvVar(mainCont.Env, "MY_ENV_VAR_1") + Expect(ok).To(BeTrue(), "No env var with name MY_ENV_VAR_1 in init container") + Expect(envVar.Value).Should(Equal("value 10")) + envVar, ok = findEnvVar(mainCont.Env, "MY_ENV_VAR_2") + Expect(ok).To(BeTrue(), "No env var with name MY_ENV_VAR_2 in init container") + Expect(envVar.Value).Should(Equal("value 20")) + }) + By(fmt.Sprintf("Checking EnvFrom in the Backstage Deployment - container: %q", mainCont.Name), func() { + Expect(len(mainCont.EnvFrom)).To(BeNumerically(">=", 2), + "Expected at least 2 items in EnvFrom for container %q, fot %d", mainCont.Name, len(mainCont.EnvFrom)) + envVar, ok := findEnvVarFrom(mainCont.EnvFrom, envConfig1CmName) + Expect(ok).To(BeTrue(), "No ConfigMap-backed envFrom in init container: %s", envConfig1CmName) + Expect(envVar.ConfigMapRef).ShouldNot(BeNil()) + envVar, ok = findEnvVarFrom(mainCont.EnvFrom, envConfig2SecretName) + Expect(ok).To(BeTrue(), "No Secret-backed envFrom in init container: %s", envConfig2SecretName) + Expect(envVar.SecretRef).ShouldNot(BeNil()) + }) + + initCont := found.Spec.Template.Spec.InitContainers[0] + By("not injecting Env set in CR into the Backstage Deployment Init Container", func() { + _, ok := findEnvVar(initCont.Env, "MY_ENV_VAR_1") + Expect(ok).To(BeFalse(), "Env var with name MY_ENV_VAR_1 should not be injected into init container") + _, ok = findEnvVar(initCont.Env, "MY_ENV_VAR_2") + Expect(ok).To(BeFalse(), "Env var with name MY_ENV_VAR_2 should not be injected into init container") + }) + By("not injecting EnvFrom set in CR into the Backstage Deployment Init Container", func() { + _, ok := findEnvVarFrom(initCont.EnvFrom, envConfig1CmName) + Expect(ok).To(BeFalse(), "ConfigMap-backed envFrom should not be added to init container: %s", envConfig1CmName) + _, ok = findEnvVarFrom(initCont.EnvFrom, envConfig2SecretName) + Expect(ok).To(BeFalse(), "Secret-backed envFrom should not be added to init container: %s", envConfig2SecretName) }) By("Checking the latest Status added to the Backstage instance") diff --git a/controllers/backstage_deployment.go b/controllers/backstage_deployment.go index b26ad66d..952e5603 100644 --- a/controllers/backstage_deployment.go +++ b/controllers/backstage_deployment.go @@ -264,12 +264,12 @@ func (r *BackstageReconciler) addEnvVars(ctx context.Context, backstage bs.Backs } for _, env := range backstage.Spec.Application.Env { - visitContainers(&deployment.Spec.Template, func(container *v1.Container) { - container.Env = append(container.Env, v1.EnvVar{ + for i := range deployment.Spec.Template.Spec.Containers { + deployment.Spec.Template.Spec.Containers[i].Env = append(deployment.Spec.Template.Spec.Containers[i].Env, v1.EnvVar{ Name: env.Name, Value: env.Value, }) - }) + } } for _, envFrom := range backstage.Spec.Application.EnvFrom { @@ -294,12 +294,13 @@ func (r *BackstageReconciler) addEnvVars(ctx context.Context, backstage bs.Backs }, } } - visitContainers(&deployment.Spec.Template, func(container *v1.Container) { - container.EnvFrom = append(container.EnvFrom, v1.EnvFromSource{ - ConfigMapRef: cmSrc, - SecretRef: secSrc, - }) - }) + for i := range deployment.Spec.Template.Spec.Containers { + deployment.Spec.Template.Spec.Containers[i].EnvFrom = append(deployment.Spec.Template.Spec.Containers[i].EnvFrom, + v1.EnvFromSource{ + ConfigMapRef: cmSrc, + SecretRef: secSrc, + }) + } } return nil From 524712aa1740dc34d19eb1cbc17699f3e6cf3496 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Tue, 5 Dec 2023 15:49:16 +0100 Subject: [PATCH 11/31] Fix JSON tag of `postgresql.enabled field Co-authored-by: Jianrong Zhang --- api/v1alpha1/backstage_types.go | 2 +- config/crd/bases/janus-idp.io_backstages.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index 0d0c91aa..d91226cb 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -191,7 +191,7 @@ type Postgresql struct { // extra-configuration files (see the ExtraConfig field in the Application structure). // +optional //+kubebuilder:default=true - Enabled *bool `json:"disabled,omitempty"` + Enabled *bool `json:"enabled,omitempty"` } type RuntimeConfig struct { diff --git a/config/crd/bases/janus-idp.io_backstages.yaml b/config/crd/bases/janus-idp.io_backstages.yaml index 5f8a4441..5037a9ac 100644 --- a/config/crd/bases/janus-idp.io_backstages.yaml +++ b/config/crd/bases/janus-idp.io_backstages.yaml @@ -233,7 +233,7 @@ spec: postgresql: description: Configuration for the local database. Optional. properties: - disabled: + enabled: default: true description: Control the creation of a local PostgreSQL DB. Set to false if using for example an external Database for Backstage. From df83d842e1c962903696560124a5b82fa4f0b15b Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Tue, 5 Dec 2023 15:52:35 +0100 Subject: [PATCH 12/31] Rename Ref struct into ObjectRef Co-authored-by: Jianrong Zhang --- api/v1alpha1/backstage_types.go | 18 +++++----- api/v1alpha1/zz_generated.deepcopy.go | 42 ++++++++++++------------ controllers/backstage_controller_test.go | 28 ++++++++-------- controllers/backstage_dynamic_plugins.go | 2 +- 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index d91226cb..98b7ba73 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -118,11 +118,11 @@ type AppConfig struct { type AppConfigItem struct { // ConfigMap containing one or more app-config files // +optional - ConfigMapRef *Ref `json:"configMapRef,omitempty"` + ConfigMapRef *ObjectRef `json:"configMapRef,omitempty"` // Secret containing one or more app-config files // +optional - SecretRef *Ref `json:"secretRef,omitempty"` + SecretRef *ObjectRef `json:"secretRef,omitempty"` } type ExtraConfig struct { @@ -139,26 +139,26 @@ type ExtraConfig struct { type ExtraConfigItem struct { // ConfigMap containing one or more extra config files // +optional - ConfigMapRef *Ref `json:"configMapRef,omitempty"` + ConfigMapRef *ObjectRef `json:"configMapRef,omitempty"` // Secret containing one or more extra config files // +optional - SecretRef *Ref `json:"secretRef,omitempty"` + SecretRef *ObjectRef `json:"secretRef,omitempty"` } type DynamicPluginsConfig struct { // ConfigMap containing the dynamic plugins' configuration. It needs to have a key named: "dynamic-plugins.yaml". // ConfigMapRef will be used if both ConfigMapRef and SecretRef are provided. // +optional - ConfigMapRef *Ref `json:"configMapRef,omitempty"` + ConfigMapRef *ObjectRef `json:"configMapRef,omitempty"` // Secret containing the dynamic plugins' configuration. It needs to have a key named: "dynamic-plugins.yaml". // ConfigMapRef will be used if both ConfigMapRef and SecretRef are provided. // +optional - SecretRef *Ref `json:"secretRef,omitempty"` + SecretRef *ObjectRef `json:"secretRef,omitempty"` } -type Ref struct { +type ObjectRef struct { // Name of the object referenced. //+kubebuilder:validation:Required Name string `json:"name"` @@ -177,11 +177,11 @@ type Env struct { type EnvFrom struct { // ConfigMap containing the environment variables to inject // +optional - ConfigMapRef *Ref `json:"configMapRef,omitempty"` + ConfigMapRef *ObjectRef `json:"configMapRef,omitempty"` // Secret containing the environment variables to inject // +optional - SecretRef *Ref `json:"secretRef,omitempty"` + SecretRef *ObjectRef `json:"secretRef,omitempty"` } type Postgresql struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 9dd25fe5..4cebcc12 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -53,12 +53,12 @@ func (in *AppConfigItem) DeepCopyInto(out *AppConfigItem) { *out = *in if in.ConfigMapRef != nil { in, out := &in.ConfigMapRef, &out.ConfigMapRef - *out = new(Ref) + *out = new(ObjectRef) **out = **in } if in.SecretRef != nil { in, out := &in.SecretRef, &out.SecretRef - *out = new(Ref) + *out = new(ObjectRef) **out = **in } } @@ -262,12 +262,12 @@ func (in *DynamicPluginsConfig) DeepCopyInto(out *DynamicPluginsConfig) { *out = *in if in.ConfigMapRef != nil { in, out := &in.ConfigMapRef, &out.ConfigMapRef - *out = new(Ref) + *out = new(ObjectRef) **out = **in } if in.SecretRef != nil { in, out := &in.SecretRef, &out.SecretRef - *out = new(Ref) + *out = new(ObjectRef) **out = **in } } @@ -302,12 +302,12 @@ func (in *EnvFrom) DeepCopyInto(out *EnvFrom) { *out = *in if in.ConfigMapRef != nil { in, out := &in.ConfigMapRef, &out.ConfigMapRef - *out = new(Ref) + *out = new(ObjectRef) **out = **in } if in.SecretRef != nil { in, out := &in.SecretRef, &out.SecretRef - *out = new(Ref) + *out = new(ObjectRef) **out = **in } } @@ -349,12 +349,12 @@ func (in *ExtraConfigItem) DeepCopyInto(out *ExtraConfigItem) { *out = *in if in.ConfigMapRef != nil { in, out := &in.ConfigMapRef, &out.ConfigMapRef - *out = new(Ref) + *out = new(ObjectRef) **out = **in } if in.SecretRef != nil { in, out := &in.SecretRef, &out.SecretRef - *out = new(Ref) + *out = new(ObjectRef) **out = **in } } @@ -370,36 +370,36 @@ func (in *ExtraConfigItem) DeepCopy() *ExtraConfigItem { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Postgresql) DeepCopyInto(out *Postgresql) { +func (in *ObjectRef) DeepCopyInto(out *ObjectRef) { *out = *in - if in.Enabled != nil { - in, out := &in.Enabled, &out.Enabled - *out = new(bool) - **out = **in - } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Postgresql. -func (in *Postgresql) DeepCopy() *Postgresql { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectRef. +func (in *ObjectRef) DeepCopy() *ObjectRef { if in == nil { return nil } - out := new(Postgresql) + out := new(ObjectRef) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Ref) DeepCopyInto(out *Ref) { +func (in *Postgresql) DeepCopyInto(out *Postgresql) { *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Ref. -func (in *Ref) DeepCopy() *Ref { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Postgresql. +func (in *Postgresql) DeepCopy() *Postgresql { if in == nil { return nil } - out := new(Ref) + out := new(Postgresql) in.DeepCopyInto(out) return out } diff --git a/controllers/backstage_controller_test.go b/controllers/backstage_controller_test.go index 4f66d02f..ae6e61cb 100644 --- a/controllers/backstage_controller_test.go +++ b/controllers/backstage_controller_test.go @@ -469,11 +469,11 @@ spec: switch kind { case "ConfigMap": item = bsv1alpha1.AppConfigItem{ - ConfigMapRef: &bsv1alpha1.Ref{Name: name}, + ConfigMapRef: &bsv1alpha1.ObjectRef{Name: name}, } case "Secret": item = bsv1alpha1.AppConfigItem{ - SecretRef: &bsv1alpha1.Ref{Name: name}, + SecretRef: &bsv1alpha1.ObjectRef{Name: name}, } } backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ @@ -548,12 +548,12 @@ spec: var ( dynamicPluginsObject client.Object - cmRef *bsv1alpha1.Ref - secRef *bsv1alpha1.Ref + cmRef *bsv1alpha1.ObjectRef + secRef *bsv1alpha1.ObjectRef ) switch dynamicPluginsConfigKind { case "ConfigMap": - cmRef = &bsv1alpha1.Ref{ + cmRef = &bsv1alpha1.ObjectRef{ Name: dynamicPluginsConfigName, } dynamicPluginsObject = buildConfigMap(dynamicPluginsConfigName, map[string]string{ @@ -564,7 +564,7 @@ plugins: [] `, }) case "Secret": - secRef = &bsv1alpha1.Ref{ + secRef = &bsv1alpha1.ObjectRef{ Name: dynamicPluginsConfigName, } dynamicPluginsObject = buildSecret(dynamicPluginsConfigName, map[string][]byte{ @@ -586,10 +586,10 @@ plugins: [] MountPath: mountPath, Items: []bsv1alpha1.AppConfigItem{ { - ConfigMapRef: &bsv1alpha1.Ref{Name: appConfig1CmName}, + ConfigMapRef: &bsv1alpha1.ObjectRef{Name: appConfig1CmName}, }, { - SecretRef: &bsv1alpha1.Ref{Name: appConfig2SecretName}, + SecretRef: &bsv1alpha1.ObjectRef{Name: appConfig2SecretName}, }, }, }, @@ -938,11 +938,11 @@ plugins: [] switch kind { case "ConfigMap": item = bsv1alpha1.ExtraConfigItem{ - ConfigMapRef: &bsv1alpha1.Ref{Name: name}, + ConfigMapRef: &bsv1alpha1.ObjectRef{Name: name}, } case "Secret": item = bsv1alpha1.ExtraConfigItem{ - SecretRef: &bsv1alpha1.Ref{Name: name}, + SecretRef: &bsv1alpha1.ObjectRef{Name: name}, } } backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ @@ -1017,10 +1017,10 @@ plugins: [] MountPath: mountPath, Items: []bsv1alpha1.ExtraConfigItem{ { - ConfigMapRef: &bsv1alpha1.Ref{Name: extraConfig1CmName}, + ConfigMapRef: &bsv1alpha1.ObjectRef{Name: extraConfig1CmName}, }, { - SecretRef: &bsv1alpha1.Ref{Name: extraConfig2SecretName}, + SecretRef: &bsv1alpha1.ObjectRef{Name: extraConfig2SecretName}, }, }, }, @@ -1154,10 +1154,10 @@ plugins: [] }, EnvFrom: []bsv1alpha1.EnvFrom{ { - ConfigMapRef: &bsv1alpha1.Ref{Name: envConfig1CmName}, + ConfigMapRef: &bsv1alpha1.ObjectRef{Name: envConfig1CmName}, }, { - SecretRef: &bsv1alpha1.Ref{Name: envConfig2SecretName}, + SecretRef: &bsv1alpha1.ObjectRef{Name: envConfig2SecretName}, }, }, }, diff --git a/controllers/backstage_dynamic_plugins.go b/controllers/backstage_dynamic_plugins.go index c5cd86b1..928b8256 100644 --- a/controllers/backstage_dynamic_plugins.go +++ b/controllers/backstage_dynamic_plugins.go @@ -66,7 +66,7 @@ func (r *BackstageReconciler) getOrGenerateDynamicPluginsConf(ctx context.Contex } return bs.DynamicPluginsConfig{ - ConfigMapRef: &bs.Ref{ + ConfigMapRef: &bs.ObjectRef{ Name: dpConfigName, }, }, nil From 32fdcc6a563360375f925603f1b1f7e42c7e95f7 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Wed, 6 Dec 2023 10:51:26 +0100 Subject: [PATCH 13/31] Use simple boolean over struct for controlling the creation of the local DB Co-authored-by: Gennady Azarenkov --- api/v1alpha1/backstage_types.go | 12 +++++++--- api/v1alpha1/zz_generated.deepcopy.go | 10 ++++---- config/crd/bases/janus-idp.io_backstages.yaml | 24 ++++++++----------- controllers/backstage_controller.go | 2 +- controllers/backstage_controller_test.go | 4 +--- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index 98b7ba73..3736edb2 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -28,11 +28,17 @@ type BackstageSpec struct { // Configuration for Backstage. Optional. Application *Application `json:"backstage,omitempty"` - // Configuration for the local database. Optional. - Postgresql *Postgresql `json:"postgresql,omitempty"` - // Raw Runtime Objects configuration. For Advanced scenarios. RawRuntimeConfig RuntimeConfig `json:"rawRuntimeConfig,omitempty"` + + // Control the creation of a local PostgreSQL DB. Set to true if using for example an external Database for Backstage. + // To use an external Database, you can provide your own app-config file (see the AppConfig field in the Application structure) + // containing references to the Database connection information, + // which might be supplied as environment variables (see the Env field) or extra-configuration files + // (see the ExtraConfig field in the Application structure). + // +optional + //+kubebuilder:default=false + SkipLocalDb *bool `json:"skipLocalDb,omitempty"` } type Application struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 4cebcc12..207626e2 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -217,12 +217,12 @@ func (in *BackstageSpec) DeepCopyInto(out *BackstageSpec) { *out = new(Application) (*in).DeepCopyInto(*out) } - if in.Postgresql != nil { - in, out := &in.Postgresql, &out.Postgresql - *out = new(Postgresql) - (*in).DeepCopyInto(*out) - } out.RawRuntimeConfig = in.RawRuntimeConfig + if in.SkipLocalDb != nil { + in, out := &in.SkipLocalDb, &out.SkipLocalDb + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackstageSpec. diff --git a/config/crd/bases/janus-idp.io_backstages.yaml b/config/crd/bases/janus-idp.io_backstages.yaml index 5037a9ac..ac4b1acd 100644 --- a/config/crd/bases/janus-idp.io_backstages.yaml +++ b/config/crd/bases/janus-idp.io_backstages.yaml @@ -230,20 +230,6 @@ spec: format: int32 type: integer type: object - postgresql: - description: Configuration for the local database. Optional. - properties: - enabled: - default: true - description: Control the creation of a local PostgreSQL DB. Set - to false if using for example an external Database for Backstage. - To use an external Database, you can provide your own app-config - file (see the AppConfig field) containing references to the - Database connection information, which might be supplied as - environment variables (see the Env field) or extra-configuration - files (see the ExtraConfig field in the Application structure). - type: boolean - type: object rawRuntimeConfig: description: Raw Runtime Objects configuration. For Advanced scenarios. properties: @@ -256,6 +242,16 @@ spec: runtime objects configuration type: string type: object + skipLocalDb: + default: false + description: Control the creation of a local PostgreSQL DB. Set to + true if using for example an external Database for Backstage. To + use an external Database, you can provide your own app-config file + (see the AppConfig field in the Application structure) containing + references to the Database connection information, which might be + supplied as environment variables (see the Env field) or extra-configuration + files (see the ExtraConfig field in the Application structure). + type: boolean type: object status: description: BackstageStatus defines the observed state of Backstage diff --git a/controllers/backstage_controller.go b/controllers/backstage_controller.go index 57adc539..804134d7 100644 --- a/controllers/backstage_controller.go +++ b/controllers/backstage_controller.go @@ -91,7 +91,7 @@ func (r *BackstageReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return ctrl.Result{}, fmt.Errorf("failed to load backstage deployment from the cluster: %w", err) } - if backstage.Spec.Postgresql == nil || pointer.BoolDeref(backstage.Spec.Postgresql.Enabled, true) { + if !pointer.BoolDeref(backstage.Spec.SkipLocalDb, false) { /* We use default strogeclass currently, and no PV is needed in that case. If we decide later on to support user provided storageclass we can enable pv creation. diff --git a/controllers/backstage_controller_test.go b/controllers/backstage_controller_test.go index ae6e61cb..4f8a16c5 100644 --- a/controllers/backstage_controller_test.go +++ b/controllers/backstage_controller_test.go @@ -1376,9 +1376,7 @@ plugins: [] var backstage *bsv1alpha1.Backstage BeforeEach(func() { backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - Postgresql: &bsv1alpha1.Postgresql{ - Enabled: pointer.Bool(false), - }, + SkipLocalDb: pointer.Bool(true), }) err := k8sClient.Create(ctx, backstage) Expect(err).To(Not(HaveOccurred())) From 175a0013405c079efd37f9578d5c4224e82f8e6b Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Wed, 6 Dec 2023 11:39:25 +0100 Subject: [PATCH 14/31] Allow only ConfigMaps for app-configs Now that we have support for environment variables and mounting extra files, we can simplify the logic by not allowing using Secrets. This is also more aligned with the Backstage way of writing configuration files [1] [1] https://backstage.io/docs/conf/writing/ Co-authored-by: Gennady Azarenkov --- api/v1alpha1/backstage_types.go | 9 +- api/v1alpha1/zz_generated.deepcopy.go | 10 +- config/crd/bases/janus-idp.io_backstages.yaml | 35 ++--- controllers/backstage_app_config.go | 63 +++------ controllers/backstage_controller_test.go | 131 +++++------------- examples/janus-cr-with-app-configs.yaml | 27 ++-- 6 files changed, 85 insertions(+), 190 deletions(-) diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index 3736edb2..527e5c3f 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -111,14 +111,17 @@ type BackendAuthSecretRef struct { } type AppConfig struct { - // Mount path for all app-config files listed in the Items field + // Mount path for all app-config files listed in the ConfigMapNames field // +optional // +kubebuilder:default=/opt/app-root/src MountPath string `json:"mountPath,omitempty"` - // List of references to app-config Config objects. + // Names of ConfigMaps storing the app-config files. Will be mounted as files under the MountPath specified. + // Bear in mind not to put sensitive data in those ConfigMaps. Instead, your app-config content can reference + // environment variables (which you can set with the Env or EnvFrom fields) and/or include extra files (see the ExtraConfig field). + // More details on https://backstage.io/docs/conf/writing/. // +optional - Items []AppConfigItem `json:"items,omitempty"` + ConfigMapNames []string `json:"configMapNames,omitempty"` } type AppConfigItem struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 207626e2..e12a41eb 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -29,12 +29,10 @@ import ( // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AppConfig) DeepCopyInto(out *AppConfig) { *out = *in - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]AppConfigItem, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } + if in.ConfigMapNames != nil { + in, out := &in.ConfigMapNames, &out.ConfigMapNames + *out = make([]string, len(*in)) + copy(*out, *in) } } diff --git a/config/crd/bases/janus-idp.io_backstages.yaml b/config/crd/bases/janus-idp.io_backstages.yaml index ac4b1acd..192a6240 100644 --- a/config/crd/bases/janus-idp.io_backstages.yaml +++ b/config/crd/bases/janus-idp.io_backstages.yaml @@ -54,36 +54,21 @@ spec: files, it is recommended to pass one ConfigMap/Secret per app-config file. properties: - items: - description: List of references to app-config Config objects. + configMapNames: + description: Names of ConfigMaps storing the app-config files. + Will be mounted as files under the MountPath specified. + Bear in mind not to put sensitive data in those ConfigMaps. + Instead, your app-config content can reference environment + variables (which you can set with the Env or EnvFrom fields) + and/or include extra files (see the ExtraConfig field). + More details on https://backstage.io/docs/conf/writing/. items: - properties: - configMapRef: - description: ConfigMap containing one or more app-config - files - properties: - name: - description: Name of the object referenced. - type: string - required: - - name - type: object - secretRef: - description: Secret containing one or more app-config - files - properties: - name: - description: Name of the object referenced. - type: string - required: - - name - type: object - type: object + type: string type: array mountPath: default: /opt/app-root/src description: Mount path for all app-config files listed in - the Items field + the ConfigMapNames field type: string type: object backendAuthSecretRef: diff --git a/controllers/backstage_app_config.go b/controllers/backstage_app_config.go index b3c78959..c4091857 100644 --- a/controllers/backstage_app_config.go +++ b/controllers/backstage_app_config.go @@ -34,26 +34,16 @@ func (r *BackstageReconciler) appConfigsToVolumes(backstage bs.Backstage) (resul if backstage.Spec.Application == nil || backstage.Spec.Application.AppConfig == nil { return nil } - for _, appConfig := range backstage.Spec.Application.AppConfig.Items { - var volumeSource v1.VolumeSource - var name string - switch { - case appConfig.ConfigMapRef != nil: - name = appConfig.ConfigMapRef.Name - volumeSource.ConfigMap = &v1.ConfigMapVolumeSource{ + for _, cmName := range backstage.Spec.Application.AppConfig.ConfigMapNames { + volumeSource := v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ DefaultMode: pointer.Int32(420), - LocalObjectReference: v1.LocalObjectReference{Name: name}, - } - case appConfig.SecretRef != nil: - name = appConfig.SecretRef.Name - volumeSource.Secret = &v1.SecretVolumeSource{ - DefaultMode: pointer.Int32(420), - SecretName: name, - } + LocalObjectReference: v1.LocalObjectReference{Name: cmName}, + }, } result = append(result, v1.Volume{ - Name: name, + Name: cmName, VolumeSource: volumeSource, }, ) @@ -124,37 +114,22 @@ func (r *BackstageReconciler) extractAppConfigFileNames(ctx context.Context, bac return nil, nil } - for _, appConfig := range backstage.Spec.Application.AppConfig.Items { + for _, cmName := range backstage.Spec.Application.AppConfig.ConfigMapNames { + cm := v1.ConfigMap{} + if err = r.Get(ctx, types.NamespacedName{Name: cmName, Namespace: ns}, &cm); err != nil { + return nil, err + } var files []string - var name string - switch { - case appConfig.ConfigMapRef != nil: - name = appConfig.ConfigMapRef.Name - cm := v1.ConfigMap{} - if err = r.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, &cm); err != nil { - return nil, err - } - for filename := range cm.Data { - // Bear in mind that iteration order over this map is not guaranteed by Go - files = append(files, filename) - } - for filename := range cm.BinaryData { - // Bear in mind that iteration order over this map is not guaranteed by Go - files = append(files, filename) - } - case appConfig.SecretRef != nil: - name = appConfig.SecretRef.Name - sec := v1.Secret{} - if err = r.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, &sec); err != nil { - return nil, err - } - for filename := range sec.Data { - // Bear in mind that iteration order over this map is not guaranteed by Go - files = append(files, filename) - } + for filename := range cm.Data { + // Bear in mind that iteration order over this map is not guaranteed by Go + files = append(files, filename) + } + for filename := range cm.BinaryData { + // Bear in mind that iteration order over this map is not guaranteed by Go + files = append(files, filename) } result = append(result, appConfigData{ - ref: name, + ref: cmName, files: files, }) } diff --git a/controllers/backstage_controller_test.go b/controllers/backstage_controller_test.go index 4f8a16c5..e17105a5 100644 --- a/controllers/backstage_controller_test.go +++ b/controllers/backstage_controller_test.go @@ -458,56 +458,41 @@ spec: }) Context("App Configs", func() { - for _, kind := range []string{"ConfigMap", "Secret"} { - kind := kind - When(fmt.Sprintf("referencing non-existing %s as app-config", kind), func() { - var backstage *bsv1alpha1.Backstage + When(fmt.Sprintf("referencing non-existing ConfigMap as app-config"), func() { + var backstage *bsv1alpha1.Backstage - BeforeEach(func() { - var item bsv1alpha1.AppConfigItem - name := "a-non-existing-" + strings.ToLower(kind) - switch kind { - case "ConfigMap": - item = bsv1alpha1.AppConfigItem{ - ConfigMapRef: &bsv1alpha1.ObjectRef{Name: name}, - } - case "Secret": - item = bsv1alpha1.AppConfigItem{ - SecretRef: &bsv1alpha1.ObjectRef{Name: name}, - } - } - backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - Application: &bsv1alpha1.Application{ - AppConfig: &bsv1alpha1.AppConfig{ - Items: []bsv1alpha1.AppConfigItem{item}, - }, + BeforeEach(func() { + backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ + Application: &bsv1alpha1.Application{ + AppConfig: &bsv1alpha1.AppConfig{ + ConfigMapNames: []string{"a-non-existing-cm"}, }, - }) - err := k8sClient.Create(ctx, backstage) - Expect(err).To(Not(HaveOccurred())) + }, }) + err := k8sClient.Create(ctx, backstage) + Expect(err).To(Not(HaveOccurred())) + }) - It("should fail to reconcile", func() { - By("Checking if the custom resource was successfully created") - Eventually(func() error { - found := &bsv1alpha1.Backstage{} - return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) - }, time.Minute, time.Second).Should(Succeed()) - - By("Not reconciling the custom resource created") - _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, - }) - Expect(err).To(HaveOccurred()) + It("should fail to reconcile", func() { + By("Checking if the custom resource was successfully created") + Eventually(func() error { + found := &bsv1alpha1.Backstage{} + return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) + }, time.Minute, time.Second).Should(Succeed()) - By("Not creating a Backstage Deployment") - Consistently(func() error { - // TODO to get name from default - return k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, &appsv1.Deployment{}) - }, 5*time.Second, time.Second).Should(Not(Succeed())) + By("Not reconciling the custom resource created") + _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, }) + Expect(err).To(HaveOccurred()) + + By("Not creating a Backstage Deployment") + Consistently(func() error { + // TODO to get name from default + return k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, &appsv1.Deployment{}) + }, 5*time.Second, time.Second).Should(Not(Succeed())) }) - } + }) for _, mountPath := range []string{"", "/some/path/for/app-config"} { mountPath := mountPath @@ -517,7 +502,6 @@ spec: func() { const ( appConfig1CmName = "my-app-config-1-cm" - appConfig2SecretName = "my-app-config-2-secret" dynamicPluginsConfigName = "my-dynamic-plugins-config" ) @@ -535,17 +519,6 @@ spec: err := k8sClient.Create(ctx, appConfig1Cm) Expect(err).To(Not(HaveOccurred())) - appConfig2Secret := buildSecret(appConfig2SecretName, map[string][]byte{ - "my-app-config-21.yaml": []byte(` -# my-app-config-21.yaml -`), - "my-app-config-22.yaml": []byte(` -# my-app-config-22.yaml -`), - }) - err = k8sClient.Create(ctx, appConfig2Secret) - Expect(err).To(Not(HaveOccurred())) - var ( dynamicPluginsObject client.Object cmRef *bsv1alpha1.ObjectRef @@ -583,15 +556,8 @@ plugins: [] backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ Application: &bsv1alpha1.Application{ AppConfig: &bsv1alpha1.AppConfig{ - MountPath: mountPath, - Items: []bsv1alpha1.AppConfigItem{ - { - ConfigMapRef: &bsv1alpha1.ObjectRef{Name: appConfig1CmName}, - }, - { - SecretRef: &bsv1alpha1.ObjectRef{Name: appConfig2SecretName}, - }, - }, + MountPath: mountPath, + ConfigMapNames: []string{appConfig1CmName}, }, DynamicPluginsConfig: &bsv1alpha1.DynamicPluginsConfig{ ConfigMapRef: cmRef, @@ -625,7 +591,7 @@ plugins: [] }, time.Minute, time.Second).Should(Succeed()) By("Checking the Volumes in the Backstage Deployment", func() { - Expect(found.Spec.Template.Spec.Volumes).To(HaveLen(5)) + Expect(found.Spec.Template.Spec.Volumes).To(HaveLen(4)) _, ok := findVolume(found.Spec.Template.Spec.Volumes, "dynamic-plugins-root") Expect(ok).To(BeTrue(), "No volume found with name: dynamic-plugins-root") @@ -639,12 +605,6 @@ plugins: [] Expect(appConfig1CmVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) Expect(appConfig1CmVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(appConfig1CmName)) - appConfig2SecretVol, ok := findVolume(found.Spec.Template.Spec.Volumes, appConfig2SecretName) - Expect(ok).To(BeTrue(), "No volume found with name: %s", appConfig2SecretName) - Expect(appConfig2SecretVol.VolumeSource.ConfigMap).To(BeNil()) - Expect(appConfig2SecretVol.VolumeSource.Secret.DefaultMode).To(HaveValue(Equal(int32(420)))) - Expect(appConfig2SecretVol.VolumeSource.Secret.SecretName).To(Equal(appConfig2SecretName)) - dynamicPluginsConfigVol, ok := findVolume(found.Spec.Template.Spec.Volumes, dynamicPluginsConfigName) Expect(ok).To(BeTrue(), "No volume found with name: %s", dynamicPluginsConfigName) switch dynamicPluginsConfigKind { @@ -703,9 +663,9 @@ plugins: [] } By("Checking the main container Args in the Backstage Deployment", func() { - Expect(mainCont.Args).To(HaveLen(10)) + Expect(mainCont.Args).To(HaveLen(6)) Expect(mainCont.Args[1]).To(Equal("dynamic-plugins-root/app-config.dynamic-plugins.yaml")) - for i := 0; i <= 8; i += 2 { + for i := 0; i <= 4; i += 2 { Expect(mainCont.Args[i]).To(Equal("--config")) } //TODO(rm3l): the order of the rest of the --config args should be the same as the order in @@ -720,19 +680,10 @@ plugins: [] Equal(expectedMountPath+"/my-app-config-12.yaml"), )) Expect(mainCont.Args[3]).To(Not(Equal(mainCont.Args[5]))) - Expect(mainCont.Args[7]).To(SatisfyAny( - Equal(expectedMountPath+"/my-app-config-21.yaml"), - Equal(expectedMountPath+"/my-app-config-22.yaml"), - )) - Expect(mainCont.Args[9]).To(SatisfyAny( - Equal(expectedMountPath+"/my-app-config-21.yaml"), - Equal(expectedMountPath+"/my-app-config-22.yaml"), - )) - Expect(mainCont.Args[7]).To(Not(Equal(mainCont.Args[9]))) }) By("Checking the main container Volume Mounts in the Backstage Deployment", func() { - Expect(mainCont.VolumeMounts).To(HaveLen(5)) + Expect(mainCont.VolumeMounts).To(HaveLen(3)) dpRoot := findVolumeMounts(mainCont.VolumeMounts, "dynamic-plugins-root") Expect(dpRoot).To(HaveLen(1), "No volume mount found with name: dynamic-plugins-root") @@ -752,20 +703,6 @@ plugins: [] Equal("my-app-config-11.yaml"), Equal("my-app-config-12.yaml"))) } - - appConfig2SecretMounts := findVolumeMounts(mainCont.VolumeMounts, appConfig2SecretName) - Expect(appConfig2SecretMounts).To(HaveLen(2), "No volume mounts found with name: %s", appConfig2SecretName) - Expect(appConfig2SecretMounts[0].MountPath).ToNot(Equal(appConfig2SecretMounts[1].MountPath)) - for i := 0; i <= 1; i++ { - Expect(appConfig2SecretMounts[i].MountPath).To( - SatisfyAny( - Equal(expectedMountPath+"/my-app-config-21.yaml"), - Equal(expectedMountPath+"/my-app-config-22.yaml"))) - Expect(appConfig2SecretMounts[i].SubPath).To( - SatisfyAny( - Equal("my-app-config-21.yaml"), - Equal("my-app-config-22.yaml"))) - } }) By("Checking the latest Status added to the Backstage instance") diff --git a/examples/janus-cr-with-app-configs.yaml b/examples/janus-cr-with-app-configs.yaml index 6fadd3ac..8e65785e 100644 --- a/examples/janus-cr-with-app-configs.yaml +++ b/examples/janus-cr-with-app-configs.yaml @@ -6,11 +6,9 @@ spec: backstage: appConfig: #mountPath: /opt/app-root/src - items: - - configMapRef: - name: "my-backstage-config-cm1" - - secretRef: - name: "my-backstage-config-secret1" + configMapNames: + - "my-backstage-config-cm1" + - "my-backstage-config-cm2" dynamicPlugins: configMapRef: name: my-dynamic-plugins-config-cm @@ -53,30 +51,30 @@ kind: ConfigMap metadata: name: my-backstage-config-cm1 data: - my-app-config.prod.yaml: | + app-config1-cm1.db.yaml: | backend: database: connection: password: ${POSTGRESQL_PASSWORD} user: ${POSTGRESQL_USER} - my-app-config-2.yaml: | + app-config2-cm1.yaml: | # Some comment in this file - my-app-config.odo.yaml: | + app-config3-cm1.odo.yaml: | catalog: locations: # [...] - type: url - target: https://github.com/rm3l/odo-backstage-golden-path-template/blob/main/template.yaml + target: https://github.com/ododev/odo-backstage-software-template/blob/main/template.yaml rules: - allow: [Template] --- apiVersion: v1 -kind: Secret +kind: ConfigMap metadata: - name: my-backstage-config-secret1 -stringData: - my-app-config-1.secret.yaml: | + name: my-backstage-config-cm2 +data: + app-config1-cm2.gh.yaml: | auth: # see https://backstage.io/docs/auth/ to learn about auth providers environment: development @@ -85,8 +83,7 @@ stringData: development: clientId: '${MY_ENV_VAR_1}' clientSecret: '${MY_ENV_VAR_2}' - # # a comment - my-app-config-2.secret.yaml: | + app-config2-cm2.yaml: | # a comment --- From 5c51402cd8926d5dfcc158e0ea8acc8a28417750 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Wed, 6 Dec 2023 12:07:44 +0100 Subject: [PATCH 15/31] Similar to app-configs, allow only a ConfigMap for dynamic plugins configuration Now that we have support for environment variables and mounting extra files, we can simplify the logic by not allowing using Secrets. This is also more aligned with the Backstage way of writing configuration files [1] [1] https://backstage.io/docs/conf/writing/ Co-authored-by: Gennady Azarenkov --- api/v1alpha1/backstage_types.go | 8 +- api/v1alpha1/zz_generated.deepcopy.go | 5 - config/crd/bases/janus-idp.io_backstages.yaml | 33 +- controllers/backstage_controller_test.go | 364 ++++++++---------- controllers/backstage_dynamic_plugins.go | 58 +-- examples/janus-cr-with-app-configs.yaml | 4 +- 6 files changed, 190 insertions(+), 282 deletions(-) diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index 527e5c3f..32b9d457 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -63,11 +63,11 @@ type Application struct { //+optional AppConfig *AppConfig `json:"appConfig,omitempty"` - // Reference to an existing configuration object for Dynamic Plugins. - // This can be a reference to any ConfigMap or Secret, - // but the object must have an existing key named: 'dynamic-plugins.yaml' + // Reference to an existing ConfigMap for Dynamic Plugins. + // A new one will be generated with the default config if not set. + // The ConfigMap object must have an existing key named: 'dynamic-plugins.yaml'. //+optional - DynamicPluginsConfig *DynamicPluginsConfig `json:"dynamicPlugins,omitempty"` + DynamicPluginsConfigMapRef string `json:"dynamicPluginsConfigMapRef,omitempty"` // References to existing Config objects to use as extra config files. // They will be mounted as files in the specified mount path. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index e12a41eb..918ea996 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -84,11 +84,6 @@ func (in *Application) DeepCopyInto(out *Application) { *out = new(AppConfig) (*in).DeepCopyInto(*out) } - if in.DynamicPluginsConfig != nil { - in, out := &in.DynamicPluginsConfig, &out.DynamicPluginsConfig - *out = new(DynamicPluginsConfig) - (*in).DeepCopyInto(*out) - } if in.ExtraConfig != nil { in, out := &in.ExtraConfig, &out.ExtraConfig *out = new(ExtraConfig) diff --git a/config/crd/bases/janus-idp.io_backstages.yaml b/config/crd/bases/janus-idp.io_backstages.yaml index 192a6240..192c458e 100644 --- a/config/crd/bases/janus-idp.io_backstages.yaml +++ b/config/crd/bases/janus-idp.io_backstages.yaml @@ -91,34 +91,11 @@ spec: required: - name type: object - dynamicPlugins: - description: 'Reference to an existing configuration object for - Dynamic Plugins. This can be a reference to any ConfigMap or - Secret, but the object must have an existing key named: ''dynamic-plugins.yaml''' - properties: - configMapRef: - description: 'ConfigMap containing the dynamic plugins'' configuration. - It needs to have a key named: "dynamic-plugins.yaml". ConfigMapRef - will be used if both ConfigMapRef and SecretRef are provided.' - properties: - name: - description: Name of the object referenced. - type: string - required: - - name - type: object - secretRef: - description: 'Secret containing the dynamic plugins'' configuration. - It needs to have a key named: "dynamic-plugins.yaml". ConfigMapRef - will be used if both ConfigMapRef and SecretRef are provided.' - properties: - name: - description: Name of the object referenced. - type: string - required: - - name - type: object - type: object + dynamicPluginsConfigMapRef: + description: 'Reference to an existing ConfigMap for Dynamic Plugins. + A new one will be generated with the default config if not set. + The ConfigMap object must have an existing key named: ''dynamic-plugins.yaml''.' + type: string env: description: Environment variables to inject into the application containers. Bear in mind not to put sensitive data here. Use diff --git a/controllers/backstage_controller_test.go b/controllers/backstage_controller_test.go index e17105a5..e722db24 100644 --- a/controllers/backstage_controller_test.go +++ b/controllers/backstage_controller_test.go @@ -28,7 +28,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/pointer" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" bsv1alpha1 "janus-idp.io/backstage-operator/api/v1alpha1" @@ -496,221 +495,184 @@ spec: for _, mountPath := range []string{"", "/some/path/for/app-config"} { mountPath := mountPath - for _, dynamicPluginsConfigKind := range []string{"ConfigMap", "Secret"} { - dynamicPluginsConfigKind := dynamicPluginsConfigKind - When(fmt.Sprintf("referencing ConfigMaps and Secrets for app-configs (mountPath=%q) and dynamic plugins config as %s", mountPath, dynamicPluginsConfigKind), - func() { - const ( - appConfig1CmName = "my-app-config-1-cm" - dynamicPluginsConfigName = "my-dynamic-plugins-config" - ) - - var backstage *bsv1alpha1.Backstage - - BeforeEach(func() { - appConfig1Cm := buildConfigMap(appConfig1CmName, map[string]string{ - "my-app-config-11.yaml": ` + When(fmt.Sprintf("referencing ConfigMaps and Secrets for app-configs (mountPath=%q) and dynamic plugins config as ConfigMap", mountPath), + func() { + const ( + appConfig1CmName = "my-app-config-1-cm" + dynamicPluginsConfigName = "my-dynamic-plugins-config" + ) + + var backstage *bsv1alpha1.Backstage + + BeforeEach(func() { + appConfig1Cm := buildConfigMap(appConfig1CmName, map[string]string{ + "my-app-config-11.yaml": ` # my-app-config-11.yaml `, - "my-app-config-12.yaml": ` + "my-app-config-12.yaml": ` # my-app-config-12.yaml `, - }) - err := k8sClient.Create(ctx, appConfig1Cm) - Expect(err).To(Not(HaveOccurred())) - - var ( - dynamicPluginsObject client.Object - cmRef *bsv1alpha1.ObjectRef - secRef *bsv1alpha1.ObjectRef - ) - switch dynamicPluginsConfigKind { - case "ConfigMap": - cmRef = &bsv1alpha1.ObjectRef{ - Name: dynamicPluginsConfigName, - } - dynamicPluginsObject = buildConfigMap(dynamicPluginsConfigName, map[string]string{ - "dynamic-plugins.yaml": ` + }) + err := k8sClient.Create(ctx, appConfig1Cm) + Expect(err).To(Not(HaveOccurred())) + + dynamicPluginsCm := buildConfigMap(dynamicPluginsConfigName, map[string]string{ + "dynamic-plugins.yaml": ` # dynamic-plugins.yaml (configmap) includes: [dynamic-plugins.default.yaml] plugins: [] `, - }) - case "Secret": - secRef = &bsv1alpha1.ObjectRef{ - Name: dynamicPluginsConfigName, - } - dynamicPluginsObject = buildSecret(dynamicPluginsConfigName, map[string][]byte{ - "dynamic-plugins.yaml": []byte(` -# dynamic-plugins.yaml (secret) -includes: [dynamic-plugins.default.yaml] -plugins: [] -`), - }) - default: - Fail(fmt.Sprintf("unsupported kind for dynamic plugins object: %q", dynamicPluginsConfigKind)) - } - err = k8sClient.Create(ctx, dynamicPluginsObject) - Expect(err).To(Not(HaveOccurred())) - - backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - Application: &bsv1alpha1.Application{ - AppConfig: &bsv1alpha1.AppConfig{ - MountPath: mountPath, - ConfigMapNames: []string{appConfig1CmName}, - }, - DynamicPluginsConfig: &bsv1alpha1.DynamicPluginsConfig{ - ConfigMapRef: cmRef, - SecretRef: secRef, - }, + }) + err = k8sClient.Create(ctx, dynamicPluginsCm) + Expect(err).To(Not(HaveOccurred())) + + backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ + Application: &bsv1alpha1.Application{ + AppConfig: &bsv1alpha1.AppConfig{ + MountPath: mountPath, + ConfigMapNames: []string{appConfig1CmName}, }, - }) - err = k8sClient.Create(ctx, backstage) - Expect(err).To(Not(HaveOccurred())) + DynamicPluginsConfigMapRef: dynamicPluginsConfigName, + }, }) + err = k8sClient.Create(ctx, backstage) + Expect(err).To(Not(HaveOccurred())) + }) - It("should reconcile", func() { - By("Checking if the custom resource was successfully created") - Eventually(func() error { - found := &bsv1alpha1.Backstage{} - return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) - }, time.Minute, time.Second).Should(Succeed()) - - By("Reconciling the custom resource created") - _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, - }) - Expect(err).To(Not(HaveOccurred())) - - By("Checking that the Deployment was successfully created in the reconciliation") - found := &appsv1.Deployment{} - Eventually(func(g Gomega) { - // TODO to get name from default - err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, found) - g.Expect(err).To(Not(HaveOccurred())) - }, time.Minute, time.Second).Should(Succeed()) - - By("Checking the Volumes in the Backstage Deployment", func() { - Expect(found.Spec.Template.Spec.Volumes).To(HaveLen(4)) - - _, ok := findVolume(found.Spec.Template.Spec.Volumes, "dynamic-plugins-root") - Expect(ok).To(BeTrue(), "No volume found with name: dynamic-plugins-root") - - _, ok = findVolume(found.Spec.Template.Spec.Volumes, "dynamic-plugins-npmrc") - Expect(ok).To(BeTrue(), "No volume found with name: dynamic-plugins-npmrc") - - appConfig1CmVol, ok := findVolume(found.Spec.Template.Spec.Volumes, appConfig1CmName) - Expect(ok).To(BeTrue(), "No volume found with name: %s", appConfig1CmName) - Expect(appConfig1CmVol.VolumeSource.Secret).To(BeNil()) - Expect(appConfig1CmVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) - Expect(appConfig1CmVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(appConfig1CmName)) - - dynamicPluginsConfigVol, ok := findVolume(found.Spec.Template.Spec.Volumes, dynamicPluginsConfigName) - Expect(ok).To(BeTrue(), "No volume found with name: %s", dynamicPluginsConfigName) - switch dynamicPluginsConfigKind { - case "ConfigMap": - Expect(dynamicPluginsConfigVol.VolumeSource.Secret).To(BeNil()) - Expect(dynamicPluginsConfigVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) - Expect(dynamicPluginsConfigVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(dynamicPluginsConfigName)) - case "Secret": - Expect(dynamicPluginsConfigVol.VolumeSource.ConfigMap).To(BeNil()) - Expect(dynamicPluginsConfigVol.VolumeSource.Secret.DefaultMode).To(HaveValue(Equal(int32(420)))) - Expect(dynamicPluginsConfigVol.VolumeSource.Secret.SecretName).To(Equal(dynamicPluginsConfigName)) - } - }) - - By("Checking the Number of init containers in the Backstage Deployment") - Expect(found.Spec.Template.Spec.InitContainers).To(HaveLen(1)) - initCont := found.Spec.Template.Spec.InitContainers[0] - - By("Checking the Init Container Env Vars in the Backstage Deployment", func() { - Expect(initCont.Env).To(HaveLen(1)) - Expect(initCont.Env[0].Name).To(Equal("NPM_CONFIG_USERCONFIG")) - Expect(initCont.Env[0].Value).To(Equal("/opt/app-root/src/.npmrc.dynamic-plugins")) - }) - - By("Checking the Init Container Volume Mounts in the Backstage Deployment", func() { - Expect(initCont.VolumeMounts).To(HaveLen(3)) - - dpRoot := findVolumeMounts(initCont.VolumeMounts, "dynamic-plugins-root") - Expect(dpRoot).To(HaveLen(1), - "No volume mount found with name: dynamic-plugins-root") - Expect(dpRoot[0].MountPath).To(Equal("/dynamic-plugins-root")) - Expect(dpRoot[0].ReadOnly).To(BeFalse()) - Expect(dpRoot[0].SubPath).To(BeEmpty()) - - dpNpmrc := findVolumeMounts(initCont.VolumeMounts, "dynamic-plugins-npmrc") - Expect(dpNpmrc).To(HaveLen(1), - "No volume mount found with name: dynamic-plugins-npmrc") - Expect(dpNpmrc[0].MountPath).To(Equal("/opt/app-root/src/.npmrc.dynamic-plugins")) - Expect(dpNpmrc[0].ReadOnly).To(BeTrue()) - Expect(dpNpmrc[0].SubPath).To(Equal(".npmrc")) - - dp := findVolumeMounts(initCont.VolumeMounts, dynamicPluginsConfigName) - Expect(dp).To(HaveLen(1), "No volume mount found with name: %s", dynamicPluginsConfigName) - Expect(dp[0].MountPath).To(Equal("/opt/app-root/src/dynamic-plugins.yaml")) - Expect(dp[0].SubPath).To(Equal("dynamic-plugins.yaml")) - Expect(dp[0].ReadOnly).To(BeTrue()) - }) - - By("Checking the Number of main containers in the Backstage Deployment") - Expect(found.Spec.Template.Spec.Containers).To(HaveLen(1)) - mainCont := found.Spec.Template.Spec.Containers[0] - - expectedMountPath := mountPath - if expectedMountPath == "" { - expectedMountPath = "/opt/app-root/src" - } + It("should reconcile", func() { + By("Checking if the custom resource was successfully created") + Eventually(func() error { + found := &bsv1alpha1.Backstage{} + return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Reconciling the custom resource created") + _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Checking that the Deployment was successfully created in the reconciliation") + found := &appsv1.Deployment{} + Eventually(func(g Gomega) { + // TODO to get name from default + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, found) + g.Expect(err).To(Not(HaveOccurred())) + }, time.Minute, time.Second).Should(Succeed()) + + By("Checking the Volumes in the Backstage Deployment", func() { + Expect(found.Spec.Template.Spec.Volumes).To(HaveLen(4)) + + _, ok := findVolume(found.Spec.Template.Spec.Volumes, "dynamic-plugins-root") + Expect(ok).To(BeTrue(), "No volume found with name: dynamic-plugins-root") + + _, ok = findVolume(found.Spec.Template.Spec.Volumes, "dynamic-plugins-npmrc") + Expect(ok).To(BeTrue(), "No volume found with name: dynamic-plugins-npmrc") + + appConfig1CmVol, ok := findVolume(found.Spec.Template.Spec.Volumes, appConfig1CmName) + Expect(ok).To(BeTrue(), "No volume found with name: %s", appConfig1CmName) + Expect(appConfig1CmVol.VolumeSource.Secret).To(BeNil()) + Expect(appConfig1CmVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) + Expect(appConfig1CmVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(appConfig1CmName)) + + dynamicPluginsConfigVol, ok := findVolume(found.Spec.Template.Spec.Volumes, dynamicPluginsConfigName) + Expect(ok).To(BeTrue(), "No volume found with name: %s", dynamicPluginsConfigName) + Expect(dynamicPluginsConfigVol.VolumeSource.Secret).To(BeNil()) + Expect(dynamicPluginsConfigVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) + Expect(dynamicPluginsConfigVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(dynamicPluginsConfigName)) + }) - By("Checking the main container Args in the Backstage Deployment", func() { - Expect(mainCont.Args).To(HaveLen(6)) - Expect(mainCont.Args[1]).To(Equal("dynamic-plugins-root/app-config.dynamic-plugins.yaml")) - for i := 0; i <= 4; i += 2 { - Expect(mainCont.Args[i]).To(Equal("--config")) - } - //TODO(rm3l): the order of the rest of the --config args should be the same as the order in - // which the keys are listed in the ConfigMap/Secrets - // But as this is returned as a map, Go does not provide any guarantee on the iteration order. - Expect(mainCont.Args[3]).To(SatisfyAny( - Equal(expectedMountPath+"/my-app-config-11.yaml"), - Equal(expectedMountPath+"/my-app-config-12.yaml"), - )) - Expect(mainCont.Args[5]).To(SatisfyAny( - Equal(expectedMountPath+"/my-app-config-11.yaml"), - Equal(expectedMountPath+"/my-app-config-12.yaml"), - )) - Expect(mainCont.Args[3]).To(Not(Equal(mainCont.Args[5]))) - }) - - By("Checking the main container Volume Mounts in the Backstage Deployment", func() { - Expect(mainCont.VolumeMounts).To(HaveLen(3)) - - dpRoot := findVolumeMounts(mainCont.VolumeMounts, "dynamic-plugins-root") - Expect(dpRoot).To(HaveLen(1), "No volume mount found with name: dynamic-plugins-root") - Expect(dpRoot[0].MountPath).To(Equal("/opt/app-root/src/dynamic-plugins-root")) - Expect(dpRoot[0].SubPath).To(BeEmpty()) - - appConfig1CmMounts := findVolumeMounts(mainCont.VolumeMounts, appConfig1CmName) - Expect(appConfig1CmMounts).To(HaveLen(2), "No volume mounts found with name: %s", appConfig1CmName) - Expect(appConfig1CmMounts[0].MountPath).ToNot(Equal(appConfig1CmMounts[1].MountPath)) - for i := 0; i <= 1; i++ { - Expect(appConfig1CmMounts[i].MountPath).To( - SatisfyAny( - Equal(expectedMountPath+"/my-app-config-11.yaml"), - Equal(expectedMountPath+"/my-app-config-12.yaml"))) - Expect(appConfig1CmMounts[i].SubPath).To( - SatisfyAny( - Equal("my-app-config-11.yaml"), - Equal("my-app-config-12.yaml"))) - } - }) - - By("Checking the latest Status added to the Backstage instance") - verifyBackstageInstance(ctx) + By("Checking the Number of init containers in the Backstage Deployment") + Expect(found.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + initCont := found.Spec.Template.Spec.InitContainers[0] + + By("Checking the Init Container Env Vars in the Backstage Deployment", func() { + Expect(initCont.Env).To(HaveLen(1)) + Expect(initCont.Env[0].Name).To(Equal("NPM_CONFIG_USERCONFIG")) + Expect(initCont.Env[0].Value).To(Equal("/opt/app-root/src/.npmrc.dynamic-plugins")) + }) + By("Checking the Init Container Volume Mounts in the Backstage Deployment", func() { + Expect(initCont.VolumeMounts).To(HaveLen(3)) + + dpRoot := findVolumeMounts(initCont.VolumeMounts, "dynamic-plugins-root") + Expect(dpRoot).To(HaveLen(1), + "No volume mount found with name: dynamic-plugins-root") + Expect(dpRoot[0].MountPath).To(Equal("/dynamic-plugins-root")) + Expect(dpRoot[0].ReadOnly).To(BeFalse()) + Expect(dpRoot[0].SubPath).To(BeEmpty()) + + dpNpmrc := findVolumeMounts(initCont.VolumeMounts, "dynamic-plugins-npmrc") + Expect(dpNpmrc).To(HaveLen(1), + "No volume mount found with name: dynamic-plugins-npmrc") + Expect(dpNpmrc[0].MountPath).To(Equal("/opt/app-root/src/.npmrc.dynamic-plugins")) + Expect(dpNpmrc[0].ReadOnly).To(BeTrue()) + Expect(dpNpmrc[0].SubPath).To(Equal(".npmrc")) + + dp := findVolumeMounts(initCont.VolumeMounts, dynamicPluginsConfigName) + Expect(dp).To(HaveLen(1), "No volume mount found with name: %s", dynamicPluginsConfigName) + Expect(dp[0].MountPath).To(Equal("/opt/app-root/src/dynamic-plugins.yaml")) + Expect(dp[0].SubPath).To(Equal("dynamic-plugins.yaml")) + Expect(dp[0].ReadOnly).To(BeTrue()) }) + + By("Checking the Number of main containers in the Backstage Deployment") + Expect(found.Spec.Template.Spec.Containers).To(HaveLen(1)) + mainCont := found.Spec.Template.Spec.Containers[0] + + expectedMountPath := mountPath + if expectedMountPath == "" { + expectedMountPath = "/opt/app-root/src" + } + + By("Checking the main container Args in the Backstage Deployment", func() { + Expect(mainCont.Args).To(HaveLen(6)) + Expect(mainCont.Args[1]).To(Equal("dynamic-plugins-root/app-config.dynamic-plugins.yaml")) + for i := 0; i <= 4; i += 2 { + Expect(mainCont.Args[i]).To(Equal("--config")) + } + //TODO(rm3l): the order of the rest of the --config args should be the same as the order in + // which the keys are listed in the ConfigMap/Secrets + // But as this is returned as a map, Go does not provide any guarantee on the iteration order. + Expect(mainCont.Args[3]).To(SatisfyAny( + Equal(expectedMountPath+"/my-app-config-11.yaml"), + Equal(expectedMountPath+"/my-app-config-12.yaml"), + )) + Expect(mainCont.Args[5]).To(SatisfyAny( + Equal(expectedMountPath+"/my-app-config-11.yaml"), + Equal(expectedMountPath+"/my-app-config-12.yaml"), + )) + Expect(mainCont.Args[3]).To(Not(Equal(mainCont.Args[5]))) + }) + + By("Checking the main container Volume Mounts in the Backstage Deployment", func() { + Expect(mainCont.VolumeMounts).To(HaveLen(3)) + + dpRoot := findVolumeMounts(mainCont.VolumeMounts, "dynamic-plugins-root") + Expect(dpRoot).To(HaveLen(1), "No volume mount found with name: dynamic-plugins-root") + Expect(dpRoot[0].MountPath).To(Equal("/opt/app-root/src/dynamic-plugins-root")) + Expect(dpRoot[0].SubPath).To(BeEmpty()) + + appConfig1CmMounts := findVolumeMounts(mainCont.VolumeMounts, appConfig1CmName) + Expect(appConfig1CmMounts).To(HaveLen(2), "No volume mounts found with name: %s", appConfig1CmName) + Expect(appConfig1CmMounts[0].MountPath).ToNot(Equal(appConfig1CmMounts[1].MountPath)) + for i := 0; i <= 1; i++ { + Expect(appConfig1CmMounts[i].MountPath).To( + SatisfyAny( + Equal(expectedMountPath+"/my-app-config-11.yaml"), + Equal(expectedMountPath+"/my-app-config-12.yaml"))) + Expect(appConfig1CmMounts[i].SubPath).To( + SatisfyAny( + Equal("my-app-config-11.yaml"), + Equal("my-app-config-12.yaml"))) + } + }) + + By("Checking the latest Status added to the Backstage instance") + verifyBackstageInstance(ctx) + }) - } + }) } }) diff --git a/controllers/backstage_dynamic_plugins.go b/controllers/backstage_dynamic_plugins.go index 928b8256..67d9c42b 100644 --- a/controllers/backstage_dynamic_plugins.go +++ b/controllers/backstage_dynamic_plugins.go @@ -40,16 +40,16 @@ import ( //` //) -func (r *BackstageReconciler) getOrGenerateDynamicPluginsConf(ctx context.Context, backstage bs.Backstage, ns string) (config bs.DynamicPluginsConfig, err error) { - if backstage.Spec.Application != nil && backstage.Spec.Application.DynamicPluginsConfig != nil { - return *backstage.Spec.Application.DynamicPluginsConfig, nil +func (r *BackstageReconciler) getOrGenerateDynamicPluginsConf(ctx context.Context, backstage bs.Backstage, ns string) (configMap string, err error) { + if backstage.Spec.Application != nil && backstage.Spec.Application.DynamicPluginsConfigMapRef != "" { + return backstage.Spec.Application.DynamicPluginsConfigMapRef, nil } //Create default ConfigMap for dynamic plugins var cm v1.ConfigMap err = r.readConfigMapOrDefault(ctx, backstage.Spec.RawRuntimeConfig.BackstageConfigName, "dynamic-plugins-configmap.yaml", ns, &cm) if err != nil { - return bs.DynamicPluginsConfig{}, fmt.Errorf("failed to read config: %s", err) + return "", fmt.Errorf("failed to read config: %s", err) } dpConfigName := fmt.Sprintf("%s-dynamic-plugins", backstage.Name) @@ -57,19 +57,15 @@ func (r *BackstageReconciler) getOrGenerateDynamicPluginsConf(ctx context.Contex err = r.Get(ctx, types.NamespacedName{Name: dpConfigName, Namespace: ns}, &cm) if err != nil { if !errors.IsNotFound(err) { - return bs.DynamicPluginsConfig{}, fmt.Errorf("failed to get config map for dynamic plugins (%q), reason: %s", dpConfigName, err) + return "", fmt.Errorf("failed to get config map for dynamic plugins (%q), reason: %s", dpConfigName, err) } err = r.Create(ctx, &cm) if err != nil { - return bs.DynamicPluginsConfig{}, fmt.Errorf("failed to create config map for dynamic plugins, reason: %s", err) + return "", fmt.Errorf("failed to create config map for dynamic plugins, reason: %s", err) } } - return bs.DynamicPluginsConfig{ - ConfigMapRef: &bs.ObjectRef{ - Name: dpConfigName, - }, - }, nil + return dpConfigName, nil } func (r *BackstageReconciler) getDynamicPluginsConfVolume(ctx context.Context, backstage bs.Backstage, ns string) (*v1.Volume, error) { @@ -78,30 +74,18 @@ func (r *BackstageReconciler) getDynamicPluginsConfVolume(ctx context.Context, b return nil, err } - if dpConf.ConfigMapRef == nil && dpConf.SecretRef == nil { + if dpConf == "" { return nil, nil } - var volumeSource v1.VolumeSource - var name string - switch { - case dpConf.ConfigMapRef != nil: - name = dpConf.ConfigMapRef.Name - volumeSource.ConfigMap = &v1.ConfigMapVolumeSource{ - DefaultMode: pointer.Int32(420), - LocalObjectReference: v1.LocalObjectReference{Name: name}, - } - case dpConf.SecretRef != nil: - name = dpConf.SecretRef.Name - volumeSource.Secret = &v1.SecretVolumeSource{ - DefaultMode: pointer.Int32(420), - SecretName: name, - } - } - return &v1.Volume{ - Name: name, - VolumeSource: volumeSource, + Name: dpConf, + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + DefaultMode: pointer.Int32(420), + LocalObjectReference: v1.LocalObjectReference{Name: dpConf}, + }, + }, }, nil } @@ -111,23 +95,15 @@ func (r *BackstageReconciler) addDynamicPluginsConfVolumeMount(ctx context.Conte return err } - if dpConf.ConfigMapRef == nil && dpConf.SecretRef == nil { + if dpConf == "" { return nil } - var name string - switch { - case dpConf.ConfigMapRef != nil: - name = dpConf.ConfigMapRef.Name - case dpConf.SecretRef != nil: - name = dpConf.SecretRef.Name - } - for i, c := range deployment.Spec.Template.Spec.InitContainers { if c.Name == _defaultBackstageInitContainerName { deployment.Spec.Template.Spec.InitContainers[i].VolumeMounts = append(deployment.Spec.Template.Spec.InitContainers[i].VolumeMounts, v1.VolumeMount{ - Name: name, + Name: dpConf, MountPath: fmt.Sprintf("%s/dynamic-plugins.yaml", _containersWorkingDir), ReadOnly: true, SubPath: "dynamic-plugins.yaml", diff --git a/examples/janus-cr-with-app-configs.yaml b/examples/janus-cr-with-app-configs.yaml index 8e65785e..5bb484af 100644 --- a/examples/janus-cr-with-app-configs.yaml +++ b/examples/janus-cr-with-app-configs.yaml @@ -9,9 +9,7 @@ spec: configMapNames: - "my-backstage-config-cm1" - "my-backstage-config-cm2" - dynamicPlugins: - configMapRef: - name: my-dynamic-plugins-config-cm + dynamicPluginsConfigMapRef: "my-dynamic-plugins-config-cm" env: - name: MY_ENV_VAR_1 value: my-value-1 From 2508e3743121eca8e927752f9f14c8dfc479fc53 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Wed, 6 Dec 2023 15:10:38 +0100 Subject: [PATCH 16/31] Rename ConfigMapNames into ConfigMapRefs This makes it more aligned with the other fields --- api/v1alpha1/backstage_types.go | 14 ++------- api/v1alpha1/zz_generated.deepcopy.go | 29 ++----------------- config/crd/bases/janus-idp.io_backstages.yaml | 4 +-- controllers/backstage_app_config.go | 12 ++++---- controllers/backstage_controller_test.go | 6 ++-- examples/janus-cr-with-app-configs.yaml | 2 +- 6 files changed, 16 insertions(+), 51 deletions(-) diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index 32b9d457..05ac58d7 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -111,7 +111,7 @@ type BackendAuthSecretRef struct { } type AppConfig struct { - // Mount path for all app-config files listed in the ConfigMapNames field + // Mount path for all app-config files listed in the ConfigMapRefs field // +optional // +kubebuilder:default=/opt/app-root/src MountPath string `json:"mountPath,omitempty"` @@ -121,17 +121,7 @@ type AppConfig struct { // environment variables (which you can set with the Env or EnvFrom fields) and/or include extra files (see the ExtraConfig field). // More details on https://backstage.io/docs/conf/writing/. // +optional - ConfigMapNames []string `json:"configMapNames,omitempty"` -} - -type AppConfigItem struct { - // ConfigMap containing one or more app-config files - // +optional - ConfigMapRef *ObjectRef `json:"configMapRef,omitempty"` - - // Secret containing one or more app-config files - // +optional - SecretRef *ObjectRef `json:"secretRef,omitempty"` + ConfigMapRefs []string `json:"configMapRefs,omitempty"` } type ExtraConfig struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 918ea996..33bf2e3e 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -29,8 +29,8 @@ import ( // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AppConfig) DeepCopyInto(out *AppConfig) { *out = *in - if in.ConfigMapNames != nil { - in, out := &in.ConfigMapNames, &out.ConfigMapNames + if in.ConfigMapRefs != nil { + in, out := &in.ConfigMapRefs, &out.ConfigMapRefs *out = make([]string, len(*in)) copy(*out, *in) } @@ -46,31 +46,6 @@ func (in *AppConfig) DeepCopy() *AppConfig { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AppConfigItem) DeepCopyInto(out *AppConfigItem) { - *out = *in - if in.ConfigMapRef != nil { - in, out := &in.ConfigMapRef, &out.ConfigMapRef - *out = new(ObjectRef) - **out = **in - } - if in.SecretRef != nil { - in, out := &in.SecretRef, &out.SecretRef - *out = new(ObjectRef) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppConfigItem. -func (in *AppConfigItem) DeepCopy() *AppConfigItem { - if in == nil { - return nil - } - out := new(AppConfigItem) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Application) DeepCopyInto(out *Application) { *out = *in diff --git a/config/crd/bases/janus-idp.io_backstages.yaml b/config/crd/bases/janus-idp.io_backstages.yaml index 192c458e..ef8335cd 100644 --- a/config/crd/bases/janus-idp.io_backstages.yaml +++ b/config/crd/bases/janus-idp.io_backstages.yaml @@ -54,7 +54,7 @@ spec: files, it is recommended to pass one ConfigMap/Secret per app-config file. properties: - configMapNames: + configMapRefs: description: Names of ConfigMaps storing the app-config files. Will be mounted as files under the MountPath specified. Bear in mind not to put sensitive data in those ConfigMaps. @@ -68,7 +68,7 @@ spec: mountPath: default: /opt/app-root/src description: Mount path for all app-config files listed in - the ConfigMapNames field + the ConfigMapRefs field type: string type: object backendAuthSecretRef: diff --git a/controllers/backstage_app_config.go b/controllers/backstage_app_config.go index c4091857..1a137a33 100644 --- a/controllers/backstage_app_config.go +++ b/controllers/backstage_app_config.go @@ -34,16 +34,16 @@ func (r *BackstageReconciler) appConfigsToVolumes(backstage bs.Backstage) (resul if backstage.Spec.Application == nil || backstage.Spec.Application.AppConfig == nil { return nil } - for _, cmName := range backstage.Spec.Application.AppConfig.ConfigMapNames { + for _, cmRef := range backstage.Spec.Application.AppConfig.ConfigMapRefs { volumeSource := v1.VolumeSource{ ConfigMap: &v1.ConfigMapVolumeSource{ DefaultMode: pointer.Int32(420), - LocalObjectReference: v1.LocalObjectReference{Name: cmName}, + LocalObjectReference: v1.LocalObjectReference{Name: cmRef}, }, } result = append(result, v1.Volume{ - Name: cmName, + Name: cmRef, VolumeSource: volumeSource, }, ) @@ -114,9 +114,9 @@ func (r *BackstageReconciler) extractAppConfigFileNames(ctx context.Context, bac return nil, nil } - for _, cmName := range backstage.Spec.Application.AppConfig.ConfigMapNames { + for _, cmRef := range backstage.Spec.Application.AppConfig.ConfigMapRefs { cm := v1.ConfigMap{} - if err = r.Get(ctx, types.NamespacedName{Name: cmName, Namespace: ns}, &cm); err != nil { + if err = r.Get(ctx, types.NamespacedName{Name: cmRef, Namespace: ns}, &cm); err != nil { return nil, err } var files []string @@ -129,7 +129,7 @@ func (r *BackstageReconciler) extractAppConfigFileNames(ctx context.Context, bac files = append(files, filename) } result = append(result, appConfigData{ - ref: cmName, + ref: cmRef, files: files, }) } diff --git a/controllers/backstage_controller_test.go b/controllers/backstage_controller_test.go index e722db24..1be38ae6 100644 --- a/controllers/backstage_controller_test.go +++ b/controllers/backstage_controller_test.go @@ -464,7 +464,7 @@ spec: backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ Application: &bsv1alpha1.Application{ AppConfig: &bsv1alpha1.AppConfig{ - ConfigMapNames: []string{"a-non-existing-cm"}, + ConfigMapRefs: []string{"a-non-existing-cm"}, }, }, }) @@ -529,8 +529,8 @@ plugins: [] backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ Application: &bsv1alpha1.Application{ AppConfig: &bsv1alpha1.AppConfig{ - MountPath: mountPath, - ConfigMapNames: []string{appConfig1CmName}, + MountPath: mountPath, + ConfigMapRefs: []string{appConfig1CmName}, }, DynamicPluginsConfigMapRef: dynamicPluginsConfigName, }, diff --git a/examples/janus-cr-with-app-configs.yaml b/examples/janus-cr-with-app-configs.yaml index 5bb484af..985b29a7 100644 --- a/examples/janus-cr-with-app-configs.yaml +++ b/examples/janus-cr-with-app-configs.yaml @@ -6,7 +6,7 @@ spec: backstage: appConfig: #mountPath: /opt/app-root/src - configMapNames: + configMapRefs: - "my-backstage-config-cm1" - "my-backstage-config-cm2" dynamicPluginsConfigMapRef: "my-dynamic-plugins-config-cm" From edaedb8434c3fe0a6998c9d65329fdb954d3c32f Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Wed, 6 Dec 2023 15:15:48 +0100 Subject: [PATCH 17/31] Rename backendAuthSecretRef into backendAuthSecretKeyRef This makes it more aligned with how references to secret keys are named generally in K8s --- api/v1alpha1/backstage_types.go | 4 ++-- api/v1alpha1/zz_generated.deepcopy.go | 14 +++++++------- bundle/manifests/janus-idp.io_backstages.yaml | 2 +- config/crd/bases/janus-idp.io_backstages.yaml | 2 +- controllers/backstage_backend_auth.go | 8 ++++---- controllers/backstage_controller_test.go | 4 ++-- examples/janus-cr-with-app-configs.yaml | 2 +- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index 05ac58d7..72ed2d17 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -49,7 +49,7 @@ type Application struct { // in default or custom application configuration files. // This is required for service-to-service auth and is shared by all backend plugins. //+optional - BackendAuthSecretRef *BackendAuthSecretRef `json:"backendAuthSecretRef,omitempty"` + BackendAuthSecretKeyRef *BackendAuthSecretKeyRef `json:"backendAuthSecretKeyRef,omitempty"` // References to existing app-configs Config objects, that will be mounted as files in the specified mount path. // Each element can be a reference to any ConfigMap or Secret, @@ -99,7 +99,7 @@ type Application struct { ImagePullSecret *string `json:"imagePullSecret,omitempty"` } -type BackendAuthSecretRef struct { +type BackendAuthSecretKeyRef struct { // Name of the secret to use for the backend auth //+kubebuilder:validation:Required Name string `json:"name"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 33bf2e3e..4b29996e 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -49,9 +49,9 @@ func (in *AppConfig) DeepCopy() *AppConfig { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Application) DeepCopyInto(out *Application) { *out = *in - if in.BackendAuthSecretRef != nil { - in, out := &in.BackendAuthSecretRef, &out.BackendAuthSecretRef - *out = new(BackendAuthSecretRef) + if in.BackendAuthSecretKeyRef != nil { + in, out := &in.BackendAuthSecretKeyRef, &out.BackendAuthSecretKeyRef + *out = new(BackendAuthSecretKeyRef) **out = **in } if in.AppConfig != nil { @@ -104,16 +104,16 @@ func (in *Application) DeepCopy() *Application { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BackendAuthSecretRef) DeepCopyInto(out *BackendAuthSecretRef) { +func (in *BackendAuthSecretKeyRef) DeepCopyInto(out *BackendAuthSecretKeyRef) { *out = *in } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendAuthSecretRef. -func (in *BackendAuthSecretRef) DeepCopy() *BackendAuthSecretRef { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendAuthSecretKeyRef. +func (in *BackendAuthSecretKeyRef) DeepCopy() *BackendAuthSecretKeyRef { if in == nil { return nil } - out := new(BackendAuthSecretRef) + out := new(BackendAuthSecretKeyRef) in.DeepCopyInto(out) return out } diff --git a/bundle/manifests/janus-idp.io_backstages.yaml b/bundle/manifests/janus-idp.io_backstages.yaml index 12311035..84652c2d 100644 --- a/bundle/manifests/janus-idp.io_backstages.yaml +++ b/bundle/manifests/janus-idp.io_backstages.yaml @@ -85,7 +85,7 @@ spec: the Items field type: string type: object - backendAuthSecretRef: + backendAuthSecretKeyRef: description: Optional Reference to a Secret to use for Backend Auth. A new one will be generated if not set. This Secret is used to set an environment variable named 'APP_CONFIG_backend_auth_keys' diff --git a/config/crd/bases/janus-idp.io_backstages.yaml b/config/crd/bases/janus-idp.io_backstages.yaml index ef8335cd..2208ff43 100644 --- a/config/crd/bases/janus-idp.io_backstages.yaml +++ b/config/crd/bases/janus-idp.io_backstages.yaml @@ -71,7 +71,7 @@ spec: the ConfigMapRefs field type: string type: object - backendAuthSecretRef: + backendAuthSecretKeyRef: description: Optional Reference to a Secret to use for Backend Auth. A new one will be generated if not set. This Secret is used to set an environment variable named 'APP_CONFIG_backend_auth_keys' diff --git a/controllers/backstage_backend_auth.go b/controllers/backstage_backend_auth.go index cac81e12..ee52f2df 100644 --- a/controllers/backstage_backend_auth.go +++ b/controllers/backstage_backend_auth.go @@ -41,8 +41,8 @@ var ( ) func (r *BackstageReconciler) handleBackendAuthSecret(ctx context.Context, backstage bs.Backstage, ns string) (secretName string, err error) { - if backstage.Spec.Application != nil && backstage.Spec.Application.BackendAuthSecretRef != nil { - return backstage.Spec.Application.BackendAuthSecretRef.Name, nil + if backstage.Spec.Application != nil && backstage.Spec.Application.BackendAuthSecretKeyRef != nil { + return backstage.Spec.Application.BackendAuthSecretKeyRef.Name, nil } //Create default Secret for backend auth @@ -96,8 +96,8 @@ func (r *BackstageReconciler) addBackendAuthEnvVar(ctx context.Context, backstag for i, c := range deployment.Spec.Template.Spec.Containers { if c.Name == _defaultBackstageMainContainerName { var k string - if backstage.Spec.Application != nil && backstage.Spec.Application.BackendAuthSecretRef != nil { - k = backstage.Spec.Application.BackendAuthSecretRef.Key + if backstage.Spec.Application != nil && backstage.Spec.Application.BackendAuthSecretKeyRef != nil { + k = backstage.Spec.Application.BackendAuthSecretKeyRef.Key } if k == "" { //TODO(rm3l): why kubebuilder default values do not work diff --git a/controllers/backstage_controller_test.go b/controllers/backstage_controller_test.go index 1be38ae6..57b53ceb 100644 --- a/controllers/backstage_controller_test.go +++ b/controllers/backstage_controller_test.go @@ -684,7 +684,7 @@ plugins: [] BeforeEach(func() { backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ Application: &bsv1alpha1.Application{ - BackendAuthSecretRef: &bsv1alpha1.BackendAuthSecretRef{ + BackendAuthSecretKeyRef: &bsv1alpha1.BackendAuthSecretKeyRef{ Name: "non-existing-secret", Key: key, }, @@ -761,7 +761,7 @@ plugins: [] Expect(err).To(Not(HaveOccurred())) backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ Application: &bsv1alpha1.Application{ - BackendAuthSecretRef: &bsv1alpha1.BackendAuthSecretRef{ + BackendAuthSecretKeyRef: &bsv1alpha1.BackendAuthSecretKeyRef{ Name: backendAuthSecretName, Key: key, }, diff --git a/examples/janus-cr-with-app-configs.yaml b/examples/janus-cr-with-app-configs.yaml index 985b29a7..a80d8f8e 100644 --- a/examples/janus-cr-with-app-configs.yaml +++ b/examples/janus-cr-with-app-configs.yaml @@ -20,7 +20,7 @@ spec: name: my-env-cm-1 - secretRef: name: my-env-secret1 - backendAuthSecretRef: + backendAuthSecretKeyRef: name: "my-backstage-backend-auth-secret" key: "my-auth-key" replicas: 2 From d7f0986284a9c0e73a82f64a62131a5c732908ae Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Wed, 6 Dec 2023 17:38:06 +0100 Subject: [PATCH 18/31] Use standard Kubernetes types for Env and EnvFrom Co-authored-by: Jianrong Zhang --- api/v1alpha1/backstage_types.go | 5 +- api/v1alpha1/zz_generated.deepcopy.go | 13 +- config/crd/bases/janus-idp.io_backstages.yaml | 135 ++++++++++++++++-- controllers/backstage_controller_test.go | 8 +- controllers/backstage_deployment.go | 41 +----- examples/janus-cr-with-app-configs.yaml | 8 +- 6 files changed, 147 insertions(+), 63 deletions(-) diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index 72ed2d17..b437408b 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -15,6 +15,7 @@ package v1alpha1 import ( + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -78,11 +79,11 @@ type Application struct { // Environment variables to inject into the application containers. // Bear in mind not to put sensitive data here. Use EnvFrom instead. //+optional - Env []Env `json:"env,omitempty"` + Env []v1.EnvVar `json:"env,omitempty"` // Environment variables to inject into the application containers, as references to existing ConfigMap or Secret objects. //+optional - EnvFrom []EnvFrom `json:"envFrom,omitempty"` + EnvFrom []v1.EnvFromSource `json:"envFrom,omitempty"` // Number of desired replicas to set in the Backstage Deployment. // Defaults to 1. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 4b29996e..719fc095 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -22,7 +22,8 @@ limitations under the License. package v1alpha1 import ( - "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -66,12 +67,14 @@ func (in *Application) DeepCopyInto(out *Application) { } if in.Env != nil { in, out := &in.Env, &out.Env - *out = make([]Env, len(*in)) - copy(*out, *in) + *out = make([]v1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.EnvFrom != nil { in, out := &in.EnvFrom, &out.EnvFrom - *out = make([]EnvFrom, len(*in)) + *out = make([]v1.EnvFromSource, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -208,7 +211,7 @@ func (in *BackstageStatus) DeepCopyInto(out *BackstageStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } diff --git a/config/crd/bases/janus-idp.io_backstages.yaml b/config/crd/bases/janus-idp.io_backstages.yaml index 2208ff43..158580ab 100644 --- a/config/crd/bases/janus-idp.io_backstages.yaml +++ b/config/crd/bases/janus-idp.io_backstages.yaml @@ -101,43 +101,152 @@ spec: containers. Bear in mind not to put sensitive data here. Use EnvFrom instead. items: + description: EnvVar represents an environment variable present + in a Container. properties: name: - description: Name of the environment variable + description: Name of the environment variable. Must be a + C_IDENTIFIER. type: string value: - description: Value of the environment variable + description: 'Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in + the container and any service environment variables. If + a variable cannot be resolved, the reference in the input + string will be unchanged. Double $$ are reduced to a single + $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless + of whether the variable exists or not. Defaults to "".' type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, + uid?' + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.labels['''']`, + `metadata.annotations['''']`, spec.nodeName, + spec.serviceAccountName, status.hostIP, status.podIP, + status.podIPs.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: 'Selects a resource of the container: only + resources limits and requests (limits.cpu, limits.memory, + limits.ephemeral-storage, requests.cpu, requests.memory + and requests.ephemeral-storage) are currently supported.' + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, + uid?' + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object required: - name - - value type: object type: array envFrom: description: Environment variables to inject into the application containers, as references to existing ConfigMap or Secret objects. items: + description: EnvFromSource represents the source of a set of + ConfigMaps properties: configMapRef: - description: ConfigMap containing the environment variables - to inject + description: The ConfigMap to select from properties: name: - description: Name of the object referenced. + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' type: string - required: - - name + optional: + description: Specify whether the ConfigMap must be defined + type: boolean type: object + x-kubernetes-map-type: atomic + prefix: + description: An optional identifier to prepend to each key + in the ConfigMap. Must be a C_IDENTIFIER. + type: string secretRef: - description: Secret containing the environment variables - to inject + description: The Secret to select from properties: name: - description: Name of the object referenced. + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' type: string - required: - - name + optional: + description: Specify whether the Secret must be defined + type: boolean type: object + x-kubernetes-map-type: atomic type: object type: array extraConfig: diff --git a/controllers/backstage_controller_test.go b/controllers/backstage_controller_test.go index 57b53ceb..eb816def 100644 --- a/controllers/backstage_controller_test.go +++ b/controllers/backstage_controller_test.go @@ -1047,16 +1047,16 @@ plugins: [] backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ Application: &bsv1alpha1.Application{ - Env: []bsv1alpha1.Env{ + Env: []corev1.EnvVar{ {Name: "MY_ENV_VAR_1", Value: "value 10"}, {Name: "MY_ENV_VAR_2", Value: "value 20"}, }, - EnvFrom: []bsv1alpha1.EnvFrom{ + EnvFrom: []corev1.EnvFromSource{ { - ConfigMapRef: &bsv1alpha1.ObjectRef{Name: envConfig1CmName}, + ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: envConfig1CmName}}, }, { - SecretRef: &bsv1alpha1.ObjectRef{Name: envConfig2SecretName}, + SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: envConfig2SecretName}}, }, }, }, diff --git a/controllers/backstage_deployment.go b/controllers/backstage_deployment.go index 952e5603..67a0f773 100644 --- a/controllers/backstage_deployment.go +++ b/controllers/backstage_deployment.go @@ -263,44 +263,9 @@ func (r *BackstageReconciler) addEnvVars(ctx context.Context, backstage bs.Backs return nil } - for _, env := range backstage.Spec.Application.Env { - for i := range deployment.Spec.Template.Spec.Containers { - deployment.Spec.Template.Spec.Containers[i].Env = append(deployment.Spec.Template.Spec.Containers[i].Env, v1.EnvVar{ - Name: env.Name, - Value: env.Value, - }) - } - } - - for _, envFrom := range backstage.Spec.Application.EnvFrom { - var ( - name string - cmSrc *v1.ConfigMapEnvSource - secSrc *v1.SecretEnvSource - ) - switch { - case envFrom.ConfigMapRef != nil: - name = envFrom.ConfigMapRef.Name - cmSrc = &v1.ConfigMapEnvSource{ - LocalObjectReference: v1.LocalObjectReference{ - Name: name, - }, - } - case envFrom.SecretRef != nil: - name = envFrom.SecretRef.Name - secSrc = &v1.SecretEnvSource{ - LocalObjectReference: v1.LocalObjectReference{ - Name: name, - }, - } - } - for i := range deployment.Spec.Template.Spec.Containers { - deployment.Spec.Template.Spec.Containers[i].EnvFrom = append(deployment.Spec.Template.Spec.Containers[i].EnvFrom, - v1.EnvFromSource{ - ConfigMapRef: cmSrc, - SecretRef: secSrc, - }) - } + for i := range deployment.Spec.Template.Spec.Containers { + deployment.Spec.Template.Spec.Containers[i].Env = append(deployment.Spec.Template.Spec.Containers[i].Env, backstage.Spec.Application.Env...) + deployment.Spec.Template.Spec.Containers[i].EnvFrom = append(deployment.Spec.Template.Spec.Containers[i].EnvFrom, backstage.Spec.Application.EnvFrom...) } return nil diff --git a/examples/janus-cr-with-app-configs.yaml b/examples/janus-cr-with-app-configs.yaml index a80d8f8e..cea973ca 100644 --- a/examples/janus-cr-with-app-configs.yaml +++ b/examples/janus-cr-with-app-configs.yaml @@ -15,10 +15,16 @@ spec: value: my-value-1 - name: MY_ENV_VAR_2 value: my-value-2 + - name: MY_ENV_VAR1_FROM_CM1 + valueFrom: + configMapKeyRef: + name: my-env-cm-1 + key: CM_ENV1 envFrom: - configMapRef: name: my-env-cm-1 - - secretRef: + - prefix: MY_ENV_SECRET1_ + secretRef: name: my-env-secret1 backendAuthSecretKeyRef: name: "my-backstage-backend-auth-secret" From 32897923af8888a84c68a5a887ba216c964002d6 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Wed, 6 Dec 2023 17:42:52 +0100 Subject: [PATCH 19/31] fixup! Allow only ConfigMaps for app-configs --- controllers/backstage_controller_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controllers/backstage_controller_test.go b/controllers/backstage_controller_test.go index eb816def..b5293477 100644 --- a/controllers/backstage_controller_test.go +++ b/controllers/backstage_controller_test.go @@ -457,7 +457,7 @@ spec: }) Context("App Configs", func() { - When(fmt.Sprintf("referencing non-existing ConfigMap as app-config"), func() { + When("referencing non-existing ConfigMap as app-config", func() { var backstage *bsv1alpha1.Backstage BeforeEach(func() { From ef2082aa891e66ababbf45302ef3f78e84d234f8 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Fri, 8 Dec 2023 13:37:14 +0100 Subject: [PATCH 20/31] Update CRD based on our latest discussions in [1] [1] https://github.com/janus-idp/operator/issues/26#issuecomment-1847091483 Co-authored-by: Gennady Azarenkov Co-authored-by: Jianrong Zhang --- api/v1alpha1/backstage_types.go | 145 ++++---- api/v1alpha1/zz_generated.deepcopy.go | 193 +++-------- config/crd/bases/janus-idp.io_backstages.yaml | 326 +++++++----------- controllers/backstage_app_config.go | 38 +- controllers/backstage_backend_auth.go | 8 +- controllers/backstage_controller_test.go | 81 +++-- controllers/backstage_deployment.go | 13 +- controllers/backstage_dynamic_plugins.go | 4 +- controllers/backstage_extra_envs.go | 64 ++++ ...tra_config.go => backstage_extra_files.go} | 89 +++-- examples/janus-cr-with-app-configs.yaml | 72 ++-- examples/janus-cr.yaml | 2 +- 12 files changed, 477 insertions(+), 558 deletions(-) create mode 100644 controllers/backstage_extra_envs.go rename controllers/{backstage_extra_config.go => backstage_extra_files.go} (56%) diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index b437408b..af226e5c 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -15,7 +15,6 @@ package v1alpha1 import ( - v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -27,7 +26,7 @@ const ( // BackstageSpec defines the desired state of Backstage type BackstageSpec struct { // Configuration for Backstage. Optional. - Application *Application `json:"backstage,omitempty"` + Application *Application `json:"application,omitempty"` // Raw Runtime Objects configuration. For Advanced scenarios. RawRuntimeConfig RuntimeConfig `json:"rawRuntimeConfig,omitempty"` @@ -36,79 +35,62 @@ type BackstageSpec struct { // To use an external Database, you can provide your own app-config file (see the AppConfig field in the Application structure) // containing references to the Database connection information, // which might be supplied as environment variables (see the Env field) or extra-configuration files - // (see the ExtraConfig field in the Application structure). + // (see the ExtraFiles field in the Application structure). // +optional //+kubebuilder:default=false SkipLocalDb *bool `json:"skipLocalDb,omitempty"` } type Application struct { + // References to existing app-configs ConfigMap objects, that will be mounted as files in the specified mount path. + // Each element can be a reference to any ConfigMap or Secret, + // and will be mounted inside the main application container under a specified mount directory. + // Additionally, each file will be passed as a `--config /mount/path/to/configmap/key` to the + // main container args in the order of the entries defined in the AppConfigs list. + // But bear in mind that for a single ConfigMap element containing several filenames, + // the order in which those files will be appended to the main container args cannot be guaranteed. + // So if you want to pass multiple app-config files, it is recommended to pass one ConfigMap per app-config file. + // +optional + AppConfig *AppConfig `json:"appConfig,omitempty"` // Optional Reference to a Secret to use for Backend Auth. A new one will be generated if not set. // This Secret is used to set an environment variable named 'APP_CONFIG_backend_auth_keys' in the // main container, which takes precedence over any 'backend.auth.keys' field defined // in default or custom application configuration files. // This is required for service-to-service auth and is shared by all backend plugins. - //+optional - BackendAuthSecretKeyRef *BackendAuthSecretKeyRef `json:"backendAuthSecretKeyRef,omitempty"` - - // References to existing app-configs Config objects, that will be mounted as files in the specified mount path. - // Each element can be a reference to any ConfigMap or Secret, - // and will be mounted inside the main application container under a dedicated directory containing the ConfigMap - // or Secret name (relative to the specified mount path). - // Additionally, each file will be passed as a `--config /path/to/secret_or_configmap/key` to the - // main container args in the order of the entries defined in the AppConfigs list. - // But bear in mind that for a single AppConfig element containing several files, - // the order in which those files will be appended to the container args, the main container args cannot be guaranteed. - // So if you want to pass multiple app-config files, it is recommended to pass one ConfigMap/Secret per app-config file. - //+optional - AppConfig *AppConfig `json:"appConfig,omitempty"` + // Default value for the key in the secret is 'backend-secret. + // +optional + BackendAuthSecret *ObjectKeyRef `json:"backendAuthSecret,omitempty"` // Reference to an existing ConfigMap for Dynamic Plugins. // A new one will be generated with the default config if not set. // The ConfigMap object must have an existing key named: 'dynamic-plugins.yaml'. - //+optional - DynamicPluginsConfigMapRef string `json:"dynamicPluginsConfigMapRef,omitempty"` + // +optional + DynamicPluginsConfigMapName string `json:"dynamicPluginsConfigMapName,omitempty"` // References to existing Config objects to use as extra config files. // They will be mounted as files in the specified mount path. // Each element can be a reference to any ConfigMap or Secret. - //+optional - ExtraConfig *ExtraConfig `json:"extraConfig,omitempty"` - - // Environment variables to inject into the application containers. - // Bear in mind not to put sensitive data here. Use EnvFrom instead. - //+optional - Env []v1.EnvVar `json:"env,omitempty"` + // +optional + ExtraFiles *ExtraFiles `json:"extraFiles,omitempty"` - // Environment variables to inject into the application containers, as references to existing ConfigMap or Secret objects. - //+optional - EnvFrom []v1.EnvFromSource `json:"envFrom,omitempty"` + // Extra environment variables + // +optional + ExtraEnvs *ExtraEnvs `json:"extraEnvs,omitempty"` // Number of desired replicas to set in the Backstage Deployment. // Defaults to 1. - //+optional + // +optional //+kubebuilder:default=1 Replicas *int32 `json:"replicas,omitempty"` // Image to use in all containers (including Init Containers) - //+optional + // +optional Image *string `json:"image,omitempty"` - // Image Pull Secret to use in all containers (including Init Containers) - //+optional - ImagePullSecret *string `json:"imagePullSecret,omitempty"` -} - -type BackendAuthSecretKeyRef struct { - // Name of the secret to use for the backend auth - //+kubebuilder:validation:Required - Name string `json:"name"` - - // Key in the secret to use for the backend auth. Default value is: backend-secret + // Image Pull Secrets to use in all containers (including Init Containers) // +optional - //+kubebuilder:default=backend-secret - Key string `json:"key,omitempty"` + ImagePullSecrets []string `json:"imagePullSecrets,omitempty"` } type AppConfig struct { @@ -117,51 +99,62 @@ type AppConfig struct { // +kubebuilder:default=/opt/app-root/src MountPath string `json:"mountPath,omitempty"` - // Names of ConfigMaps storing the app-config files. Will be mounted as files under the MountPath specified. + // List of ConfigMaps storing the app-config files. Will be mounted as files under the MountPath specified. + // For each item in this array, if a key is not specified, it means that all keys in the ConfigMap will be mounted as files. + // Otherwise, only the specified key will be mounted as a file. // Bear in mind not to put sensitive data in those ConfigMaps. Instead, your app-config content can reference - // environment variables (which you can set with the Env or EnvFrom fields) and/or include extra files (see the ExtraConfig field). + // environment variables (which you can set with the ExtraEnvs field) and/or include extra files (see the ExtraFiles field). // More details on https://backstage.io/docs/conf/writing/. // +optional - ConfigMapRefs []string `json:"configMapRefs,omitempty"` + ConfigMaps []ObjectKeyRef `json:"configMaps,omitempty"` } -type ExtraConfig struct { +type ExtraFiles struct { // Mount path for all extra configuration files listed in the Items field // +optional // +kubebuilder:default=/opt/app-root/src MountPath string `json:"mountPath,omitempty"` - // List of references to extra config Config objects. + // List of references to ConfigMaps objects mounted as extra files under the MountPath specified. + // For each item in this array, if a key is not specified, it means that all keys in the ConfigMap will be mounted as files. + // Otherwise, only the specified key will be mounted as a file. // +optional - Items []ExtraConfigItem `json:"items,omitempty"` -} + ConfigMaps []ObjectKeyRef `json:"configMaps,omitempty"` -type ExtraConfigItem struct { - // ConfigMap containing one or more extra config files + // List of references to Secrets objects mounted as extra files under the MountPath specified. + // For each item in this array, if a key is not specified, it means that all keys in the Secret will be mounted as files. + // Otherwise, only the specified key will be mounted as a file. // +optional - ConfigMapRef *ObjectRef `json:"configMapRef,omitempty"` + Secrets []ObjectKeyRef `json:"secrets,omitempty"` +} - // Secret containing one or more extra config files +type ExtraEnvs struct { + // List of references to ConfigMaps objects to inject as additional environment variables. + // For each item in this array, if a key is not specified, it means that all keys in the ConfigMap will be injected as additional environment variables. + // Otherwise, only the specified key will be injected as an additional environment variable. // +optional - SecretRef *ObjectRef `json:"secretRef,omitempty"` -} + ConfigMaps []ObjectKeyRef `json:"configMaps,omitempty"` -type DynamicPluginsConfig struct { - // ConfigMap containing the dynamic plugins' configuration. It needs to have a key named: "dynamic-plugins.yaml". - // ConfigMapRef will be used if both ConfigMapRef and SecretRef are provided. + // List of references to Secrets objects to inject as additional environment variables. + // For each item in this array, if a key is not specified, it means that all keys in the Secret will be injected as additional environment variables. + // Otherwise, only the specified key will be injected as environment variable. // +optional - ConfigMapRef *ObjectRef `json:"configMapRef,omitempty"` + Secrets []ObjectKeyRef `json:"secrets,omitempty"` - // Secret containing the dynamic plugins' configuration. It needs to have a key named: "dynamic-plugins.yaml". - // ConfigMapRef will be used if both ConfigMapRef and SecretRef are provided. + // List of name and value pairs to add as environment variables. // +optional - SecretRef *ObjectRef `json:"secretRef,omitempty"` + Envs []Env `json:"envs,omitempty"` } -type ObjectRef struct { - // Name of the object referenced. +type ObjectKeyRef struct { + // Name of the object + // We support only ConfigMaps and Secrets. //+kubebuilder:validation:Required Name string `json:"name"` + + // Key in the object + // +optional + Key string `json:"key,omitempty"` } type Env struct { @@ -174,26 +167,6 @@ type Env struct { Value string `json:"value"` } -type EnvFrom struct { - // ConfigMap containing the environment variables to inject - // +optional - ConfigMapRef *ObjectRef `json:"configMapRef,omitempty"` - - // Secret containing the environment variables to inject - // +optional - SecretRef *ObjectRef `json:"secretRef,omitempty"` -} - -type Postgresql struct { - // Control the creation of a local PostgreSQL DB. Set to false if using for example an external Database for Backstage. - // To use an external Database, you can provide your own app-config file (see the AppConfig field) containing references - // to the Database connection information, which might be supplied as environment variables (see the Env field) or - // extra-configuration files (see the ExtraConfig field in the Application structure). - // +optional - //+kubebuilder:default=true - Enabled *bool `json:"enabled,omitempty"` -} - type RuntimeConfig struct { // Name of ConfigMap containing Backstage runtime objects configuration BackstageConfigName string `json:"backstageConfig,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 719fc095..b4fdac77 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -22,17 +22,16 @@ limitations under the License. package v1alpha1 import ( - "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AppConfig) DeepCopyInto(out *AppConfig) { *out = *in - if in.ConfigMapRefs != nil { - in, out := &in.ConfigMapRefs, &out.ConfigMapRefs - *out = make([]string, len(*in)) + if in.ConfigMaps != nil { + in, out := &in.ConfigMaps, &out.ConfigMaps + *out = make([]ObjectKeyRef, len(*in)) copy(*out, *in) } } @@ -50,34 +49,25 @@ func (in *AppConfig) DeepCopy() *AppConfig { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Application) DeepCopyInto(out *Application) { *out = *in - if in.BackendAuthSecretKeyRef != nil { - in, out := &in.BackendAuthSecretKeyRef, &out.BackendAuthSecretKeyRef - *out = new(BackendAuthSecretKeyRef) - **out = **in - } if in.AppConfig != nil { in, out := &in.AppConfig, &out.AppConfig *out = new(AppConfig) (*in).DeepCopyInto(*out) } - if in.ExtraConfig != nil { - in, out := &in.ExtraConfig, &out.ExtraConfig - *out = new(ExtraConfig) - (*in).DeepCopyInto(*out) + if in.BackendAuthSecret != nil { + in, out := &in.BackendAuthSecret, &out.BackendAuthSecret + *out = new(ObjectKeyRef) + **out = **in } - if in.Env != nil { - in, out := &in.Env, &out.Env - *out = make([]v1.EnvVar, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } + if in.ExtraFiles != nil { + in, out := &in.ExtraFiles, &out.ExtraFiles + *out = new(ExtraFiles) + (*in).DeepCopyInto(*out) } - if in.EnvFrom != nil { - in, out := &in.EnvFrom, &out.EnvFrom - *out = make([]v1.EnvFromSource, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } + if in.ExtraEnvs != nil { + in, out := &in.ExtraEnvs, &out.ExtraEnvs + *out = new(ExtraEnvs) + (*in).DeepCopyInto(*out) } if in.Replicas != nil { in, out := &in.Replicas, &out.Replicas @@ -89,10 +79,10 @@ func (in *Application) DeepCopyInto(out *Application) { *out = new(string) **out = **in } - if in.ImagePullSecret != nil { - in, out := &in.ImagePullSecret, &out.ImagePullSecret - *out = new(string) - **out = **in + if in.ImagePullSecrets != nil { + in, out := &in.ImagePullSecrets, &out.ImagePullSecrets + *out = make([]string, len(*in)) + copy(*out, *in) } } @@ -106,21 +96,6 @@ func (in *Application) DeepCopy() *Application { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BackendAuthSecretKeyRef) DeepCopyInto(out *BackendAuthSecretKeyRef) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendAuthSecretKeyRef. -func (in *BackendAuthSecretKeyRef) DeepCopy() *BackendAuthSecretKeyRef { - if in == nil { - return nil - } - out := new(BackendAuthSecretKeyRef) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Backstage) DeepCopyInto(out *Backstage) { *out = *in @@ -211,7 +186,7 @@ func (in *BackstageStatus) DeepCopyInto(out *BackstageStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -228,31 +203,6 @@ func (in *BackstageStatus) DeepCopy() *BackstageStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DynamicPluginsConfig) DeepCopyInto(out *DynamicPluginsConfig) { - *out = *in - if in.ConfigMapRef != nil { - in, out := &in.ConfigMapRef, &out.ConfigMapRef - *out = new(ObjectRef) - **out = **in - } - if in.SecretRef != nil { - in, out := &in.SecretRef, &out.SecretRef - *out = new(ObjectRef) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DynamicPluginsConfig. -func (in *DynamicPluginsConfig) DeepCopy() *DynamicPluginsConfig { - if in == nil { - return nil - } - out := new(DynamicPluginsConfig) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Env) DeepCopyInto(out *Env) { *out = *in @@ -269,108 +219,71 @@ func (in *Env) DeepCopy() *Env { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *EnvFrom) DeepCopyInto(out *EnvFrom) { +func (in *ExtraEnvs) DeepCopyInto(out *ExtraEnvs) { *out = *in - if in.ConfigMapRef != nil { - in, out := &in.ConfigMapRef, &out.ConfigMapRef - *out = new(ObjectRef) - **out = **in - } - if in.SecretRef != nil { - in, out := &in.SecretRef, &out.SecretRef - *out = new(ObjectRef) - **out = **in + if in.ConfigMaps != nil { + in, out := &in.ConfigMaps, &out.ConfigMaps + *out = make([]ObjectKeyRef, len(*in)) + copy(*out, *in) } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvFrom. -func (in *EnvFrom) DeepCopy() *EnvFrom { - if in == nil { - return nil + if in.Secrets != nil { + in, out := &in.Secrets, &out.Secrets + *out = make([]ObjectKeyRef, len(*in)) + copy(*out, *in) } - out := new(EnvFrom) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ExtraConfig) DeepCopyInto(out *ExtraConfig) { - *out = *in - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]ExtraConfigItem, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } + if in.Envs != nil { + in, out := &in.Envs, &out.Envs + *out = make([]Env, len(*in)) + copy(*out, *in) } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtraConfig. -func (in *ExtraConfig) DeepCopy() *ExtraConfig { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtraEnvs. +func (in *ExtraEnvs) DeepCopy() *ExtraEnvs { if in == nil { return nil } - out := new(ExtraConfig) + out := new(ExtraEnvs) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ExtraConfigItem) DeepCopyInto(out *ExtraConfigItem) { +func (in *ExtraFiles) DeepCopyInto(out *ExtraFiles) { *out = *in - if in.ConfigMapRef != nil { - in, out := &in.ConfigMapRef, &out.ConfigMapRef - *out = new(ObjectRef) - **out = **in - } - if in.SecretRef != nil { - in, out := &in.SecretRef, &out.SecretRef - *out = new(ObjectRef) - **out = **in + if in.ConfigMaps != nil { + in, out := &in.ConfigMaps, &out.ConfigMaps + *out = make([]ObjectKeyRef, len(*in)) + copy(*out, *in) } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtraConfigItem. -func (in *ExtraConfigItem) DeepCopy() *ExtraConfigItem { - if in == nil { - return nil + if in.Secrets != nil { + in, out := &in.Secrets, &out.Secrets + *out = make([]ObjectKeyRef, len(*in)) + copy(*out, *in) } - out := new(ExtraConfigItem) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ObjectRef) DeepCopyInto(out *ObjectRef) { - *out = *in } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectRef. -func (in *ObjectRef) DeepCopy() *ObjectRef { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtraFiles. +func (in *ExtraFiles) DeepCopy() *ExtraFiles { if in == nil { return nil } - out := new(ObjectRef) + out := new(ExtraFiles) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Postgresql) DeepCopyInto(out *Postgresql) { +func (in *ObjectKeyRef) DeepCopyInto(out *ObjectKeyRef) { *out = *in - if in.Enabled != nil { - in, out := &in.Enabled, &out.Enabled - *out = new(bool) - **out = **in - } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Postgresql. -func (in *Postgresql) DeepCopy() *Postgresql { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectKeyRef. +func (in *ObjectKeyRef) DeepCopy() *ObjectKeyRef { if in == nil { return nil } - out := new(Postgresql) + out := new(ObjectKeyRef) in.DeepCopyInto(out) return out } diff --git a/config/crd/bases/janus-idp.io_backstages.yaml b/config/crd/bases/janus-idp.io_backstages.yaml index 158580ab..1d4a4c9c 100644 --- a/config/crd/bases/janus-idp.io_backstages.yaml +++ b/config/crd/bases/janus-idp.io_backstages.yaml @@ -35,35 +35,46 @@ spec: spec: description: BackstageSpec defines the desired state of Backstage properties: - backstage: + application: description: Configuration for Backstage. Optional. properties: appConfig: - description: References to existing app-configs Config objects, + description: References to existing app-configs ConfigMap objects, that will be mounted as files in the specified mount path. Each element can be a reference to any ConfigMap or Secret, and will - be mounted inside the main application container under a dedicated - directory containing the ConfigMap or Secret name (relative - to the specified mount path). Additionally, each file will be - passed as a `--config /path/to/secret_or_configmap/key` to the - main container args in the order of the entries defined in the - AppConfigs list. But bear in mind that for a single AppConfig - element containing several files, the order in which those files - will be appended to the container args, the main container args - cannot be guaranteed. So if you want to pass multiple app-config - files, it is recommended to pass one ConfigMap/Secret per app-config - file. + be mounted inside the main application container under a specified + mount directory. Additionally, each file will be passed as a + `--config /mount/path/to/configmap/key` to the main container + args in the order of the entries defined in the AppConfigs list. + But bear in mind that for a single ConfigMap element containing + several filenames, the order in which those files will be appended + to the main container args cannot be guaranteed. So if you want + to pass multiple app-config files, it is recommended to pass + one ConfigMap per app-config file. properties: - configMapRefs: - description: Names of ConfigMaps storing the app-config files. + configMaps: + description: List of ConfigMaps storing the app-config files. Will be mounted as files under the MountPath specified. - Bear in mind not to put sensitive data in those ConfigMaps. - Instead, your app-config content can reference environment - variables (which you can set with the Env or EnvFrom fields) - and/or include extra files (see the ExtraConfig field). + For each item in this array, if a key is not specified, + it means that all keys in the ConfigMap will be mounted + as files. Otherwise, only the specified key will be mounted + as a file. Bear in mind not to put sensitive data in those + ConfigMaps. Instead, your app-config content can reference + environment variables (which you can set with the ExtraEnvs + field) and/or include extra files (see the ExtraFiles field). More details on https://backstage.io/docs/conf/writing/. items: - type: string + properties: + key: + description: Key in the object + type: string + name: + description: Name of the object We support only ConfigMaps + and Secrets. + type: string + required: + - name + type: object type: array mountPath: default: /opt/app-root/src @@ -71,214 +82,114 @@ spec: the ConfigMapRefs field type: string type: object - backendAuthSecretKeyRef: + backendAuthSecret: description: Optional Reference to a Secret to use for Backend Auth. A new one will be generated if not set. This Secret is used to set an environment variable named 'APP_CONFIG_backend_auth_keys' in the main container, which takes precedence over any 'backend.auth.keys' field defined in default or custom application configuration files. This is required for service-to-service auth and is shared - by all backend plugins. + by all backend plugins. Default value for the key in the secret + is 'backend-secret. properties: key: - default: backend-secret - description: 'Key in the secret to use for the backend auth. - Default value is: backend-secret' + description: Key in the object type: string name: - description: Name of the secret to use for the backend auth + description: Name of the object We support only ConfigMaps + and Secrets. type: string required: - name type: object - dynamicPluginsConfigMapRef: + dynamicPluginsConfigMapName: description: 'Reference to an existing ConfigMap for Dynamic Plugins. A new one will be generated with the default config if not set. The ConfigMap object must have an existing key named: ''dynamic-plugins.yaml''.' type: string - env: - description: Environment variables to inject into the application - containers. Bear in mind not to put sensitive data here. Use - EnvFrom instead. - items: - description: EnvVar represents an environment variable present - in a Container. - properties: - name: - description: Name of the environment variable. Must be a - C_IDENTIFIER. - type: string - value: - description: 'Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in - the container and any service environment variables. If - a variable cannot be resolved, the reference in the input - string will be unchanged. Double $$ are reduced to a single - $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless - of whether the variable exists or not. Defaults to "".' - type: string - valueFrom: - description: Source for the environment variable's value. - Cannot be used if value is not empty. + extraEnvs: + description: Extra environment variables + properties: + configMaps: + description: List of references to ConfigMaps objects to inject + as additional environment variables. For each item in this + array, if a key is not specified, it means that all keys + in the ConfigMap will be injected as additional environment + variables. Otherwise, only the specified key will be injected + as an additional environment variable. + items: properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - type: string - optional: - description: Specify whether the ConfigMap or its - key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: 'Selects a field of the pod: supports metadata.name, - metadata.namespace, `metadata.labels['''']`, - `metadata.annotations['''']`, spec.nodeName, - spec.serviceAccountName, status.hostIP, status.podIP, - status.podIPs.' - properties: - apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select in the - specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: - description: 'Selects a resource of the container: only - resources limits and requests (limits.cpu, limits.memory, - limits.ephemeral-storage, requests.cpu, requests.memory - and requests.ephemeral-storage) are currently supported.' - properties: - containerName: - description: 'Container name: required for volumes, - optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of the - exposed resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the pod's - namespace - properties: - key: - description: The key of the secret to select from. Must - be a valid secret key. - type: string - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - type: string - optional: - description: Specify whether the Secret or its key - must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + key: + description: Key in the object + type: string + name: + description: Name of the object We support only ConfigMaps + and Secrets. + type: string + required: + - name type: object - required: - - name - type: object - type: array - envFrom: - description: Environment variables to inject into the application - containers, as references to existing ConfigMap or Secret objects. - items: - description: EnvFromSource represents the source of a set of - ConfigMaps - properties: - configMapRef: - description: The ConfigMap to select from + type: array + envs: + description: List of name and value pairs to add as environment + variables. + items: properties: name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' + description: Name of the environment variable + type: string + value: + description: Value of the environment variable type: string - optional: - description: Specify whether the ConfigMap must be defined - type: boolean + required: + - name + - value type: object - x-kubernetes-map-type: atomic - prefix: - description: An optional identifier to prepend to each key - in the ConfigMap. Must be a C_IDENTIFIER. - type: string - secretRef: - description: The Secret to select from + type: array + secrets: + description: List of references to Secrets objects to inject + as additional environment variables. For each item in this + array, if a key is not specified, it means that all keys + in the Secret will be injected as additional environment + variables. Otherwise, only the specified key will be injected + as environment variable. + items: properties: + key: + description: Key in the object + type: string name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' + description: Name of the object We support only ConfigMaps + and Secrets. type: string - optional: - description: Specify whether the Secret must be defined - type: boolean + required: + - name type: object - x-kubernetes-map-type: atomic - type: object - type: array - extraConfig: + type: array + type: object + extraFiles: description: References to existing Config objects to use as extra config files. They will be mounted as files in the specified mount path. Each element can be a reference to any ConfigMap or Secret. properties: - items: - description: List of references to extra config Config objects. + configMaps: + description: List of references to ConfigMaps objects mounted + as extra files under the MountPath specified. For each item + in this array, if a key is not specified, it means that + all keys in the ConfigMap will be mounted as files. Otherwise, + only the specified key will be mounted as a file. items: properties: - configMapRef: - description: ConfigMap containing one or more extra - config files - properties: - name: - description: Name of the object referenced. - type: string - required: - - name - type: object - secretRef: - description: Secret containing one or more extra config - files - properties: - name: - description: Name of the object referenced. - type: string - required: - - name - type: object + key: + description: Key in the object + type: string + name: + description: Name of the object We support only ConfigMaps + and Secrets. + type: string + required: + - name type: object type: array mountPath: @@ -286,14 +197,35 @@ spec: description: Mount path for all extra configuration files listed in the Items field type: string + secrets: + description: List of references to Secrets objects mounted + as extra files under the MountPath specified. For each item + in this array, if a key is not specified, it means that + all keys in the Secret will be mounted as files. Otherwise, + only the specified key will be mounted as a file. + items: + properties: + key: + description: Key in the object + type: string + name: + description: Name of the object We support only ConfigMaps + and Secrets. + type: string + required: + - name + type: object + type: array type: object image: description: Image to use in all containers (including Init Containers) type: string - imagePullSecret: - description: Image Pull Secret to use in all containers (including + imagePullSecrets: + description: Image Pull Secrets to use in all containers (including Init Containers) - type: string + items: + type: string + type: array replicas: default: 1 description: Number of desired replicas to set in the Backstage @@ -321,7 +253,7 @@ spec: (see the AppConfig field in the Application structure) containing references to the Database connection information, which might be supplied as environment variables (see the Env field) or extra-configuration - files (see the ExtraConfig field in the Application structure). + files (see the ExtraFiles field in the Application structure). type: boolean type: object status: diff --git a/controllers/backstage_app_config.go b/controllers/backstage_app_config.go index 1a137a33..7fd532ae 100644 --- a/controllers/backstage_app_config.go +++ b/controllers/backstage_app_config.go @@ -34,16 +34,16 @@ func (r *BackstageReconciler) appConfigsToVolumes(backstage bs.Backstage) (resul if backstage.Spec.Application == nil || backstage.Spec.Application.AppConfig == nil { return nil } - for _, cmRef := range backstage.Spec.Application.AppConfig.ConfigMapRefs { + for _, cm := range backstage.Spec.Application.AppConfig.ConfigMaps { volumeSource := v1.VolumeSource{ ConfigMap: &v1.ConfigMapVolumeSource{ DefaultMode: pointer.Int32(420), - LocalObjectReference: v1.LocalObjectReference{Name: cmRef}, + LocalObjectReference: v1.LocalObjectReference{Name: cm.Name}, }, } result = append(result, v1.Volume{ - Name: cmRef, + Name: cm.Name, VolumeSource: volumeSource, }, ) @@ -114,22 +114,28 @@ func (r *BackstageReconciler) extractAppConfigFileNames(ctx context.Context, bac return nil, nil } - for _, cmRef := range backstage.Spec.Application.AppConfig.ConfigMapRefs { - cm := v1.ConfigMap{} - if err = r.Get(ctx, types.NamespacedName{Name: cmRef, Namespace: ns}, &cm); err != nil { - return nil, err - } + for _, cmRef := range backstage.Spec.Application.AppConfig.ConfigMaps { var files []string - for filename := range cm.Data { - // Bear in mind that iteration order over this map is not guaranteed by Go - files = append(files, filename) - } - for filename := range cm.BinaryData { - // Bear in mind that iteration order over this map is not guaranteed by Go - files = append(files, filename) + if cmRef.Key != "" { + // Limit to that file only + files = append(files, cmRef.Key) + } else { + // All keys + cm := v1.ConfigMap{} + if err = r.Get(ctx, types.NamespacedName{Name: cmRef.Name, Namespace: ns}, &cm); err != nil { + return nil, err + } + for filename := range cm.Data { + // Bear in mind that iteration order over this map is not guaranteed by Go + files = append(files, filename) + } + for filename := range cm.BinaryData { + // Bear in mind that iteration order over this map is not guaranteed by Go + files = append(files, filename) + } } result = append(result, appConfigData{ - ref: cmRef, + ref: cmRef.Name, files: files, }) } diff --git a/controllers/backstage_backend_auth.go b/controllers/backstage_backend_auth.go index ee52f2df..a2569c34 100644 --- a/controllers/backstage_backend_auth.go +++ b/controllers/backstage_backend_auth.go @@ -41,8 +41,8 @@ var ( ) func (r *BackstageReconciler) handleBackendAuthSecret(ctx context.Context, backstage bs.Backstage, ns string) (secretName string, err error) { - if backstage.Spec.Application != nil && backstage.Spec.Application.BackendAuthSecretKeyRef != nil { - return backstage.Spec.Application.BackendAuthSecretKeyRef.Name, nil + if backstage.Spec.Application != nil && backstage.Spec.Application.BackendAuthSecret != nil { + return backstage.Spec.Application.BackendAuthSecret.Name, nil } //Create default Secret for backend auth @@ -96,8 +96,8 @@ func (r *BackstageReconciler) addBackendAuthEnvVar(ctx context.Context, backstag for i, c := range deployment.Spec.Template.Spec.Containers { if c.Name == _defaultBackstageMainContainerName { var k string - if backstage.Spec.Application != nil && backstage.Spec.Application.BackendAuthSecretKeyRef != nil { - k = backstage.Spec.Application.BackendAuthSecretKeyRef.Key + if backstage.Spec.Application != nil && backstage.Spec.Application.BackendAuthSecret != nil { + k = backstage.Spec.Application.BackendAuthSecret.Key } if k == "" { //TODO(rm3l): why kubebuilder default values do not work diff --git a/controllers/backstage_controller_test.go b/controllers/backstage_controller_test.go index b5293477..f5d07132 100644 --- a/controllers/backstage_controller_test.go +++ b/controllers/backstage_controller_test.go @@ -464,7 +464,9 @@ spec: backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ Application: &bsv1alpha1.Application{ AppConfig: &bsv1alpha1.AppConfig{ - ConfigMapRefs: []string{"a-non-existing-cm"}, + ConfigMaps: []bsv1alpha1.ObjectKeyRef{ + {Name: "a-non-existing-cm"}, + }, }, }, }) @@ -529,10 +531,12 @@ plugins: [] backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ Application: &bsv1alpha1.Application{ AppConfig: &bsv1alpha1.AppConfig{ - MountPath: mountPath, - ConfigMapRefs: []string{appConfig1CmName}, + MountPath: mountPath, + ConfigMaps: []bsv1alpha1.ObjectKeyRef{ + {Name: appConfig1CmName}, + }, }, - DynamicPluginsConfigMapRef: dynamicPluginsConfigName, + DynamicPluginsConfigMapName: dynamicPluginsConfigName, }, }) err = k8sClient.Create(ctx, backstage) @@ -684,7 +688,7 @@ plugins: [] BeforeEach(func() { backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ Application: &bsv1alpha1.Application{ - BackendAuthSecretKeyRef: &bsv1alpha1.BackendAuthSecretKeyRef{ + BackendAuthSecret: &bsv1alpha1.ObjectKeyRef{ Name: "non-existing-secret", Key: key, }, @@ -761,7 +765,7 @@ plugins: [] Expect(err).To(Not(HaveOccurred())) backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ Application: &bsv1alpha1.Application{ - BackendAuthSecretKeyRef: &bsv1alpha1.BackendAuthSecretKeyRef{ + BackendAuthSecret: &bsv1alpha1.ObjectKeyRef{ Name: backendAuthSecretName, Key: key, }, @@ -825,29 +829,29 @@ plugins: [] } }) - Context("Extra Configs", func() { + Context("Extra Files", func() { for _, kind := range []string{"ConfigMap", "Secret"} { kind := kind - When(fmt.Sprintf("referencing non-existing %s as extra-config", kind), func() { + When(fmt.Sprintf("referencing non-existing %s as extra-file", kind), func() { var backstage *bsv1alpha1.Backstage BeforeEach(func() { - var item bsv1alpha1.ExtraConfigItem + var ( + cmExtraFiles []bsv1alpha1.ObjectKeyRef + secExtraFiles []bsv1alpha1.ObjectKeyRef + ) name := "a-non-existing-" + strings.ToLower(kind) switch kind { case "ConfigMap": - item = bsv1alpha1.ExtraConfigItem{ - ConfigMapRef: &bsv1alpha1.ObjectRef{Name: name}, - } + cmExtraFiles = append(cmExtraFiles, bsv1alpha1.ObjectKeyRef{Name: name}) case "Secret": - item = bsv1alpha1.ExtraConfigItem{ - SecretRef: &bsv1alpha1.ObjectRef{Name: name}, - } + secExtraFiles = append(secExtraFiles, bsv1alpha1.ObjectKeyRef{Name: name}) } backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ Application: &bsv1alpha1.Application{ - ExtraConfig: &bsv1alpha1.ExtraConfig{ - Items: []bsv1alpha1.ExtraConfigItem{item}, + ExtraFiles: &bsv1alpha1.ExtraFiles{ + ConfigMaps: cmExtraFiles, + Secrets: secExtraFiles, }, }, }) @@ -912,15 +916,13 @@ plugins: [] backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ Application: &bsv1alpha1.Application{ - ExtraConfig: &bsv1alpha1.ExtraConfig{ + ExtraFiles: &bsv1alpha1.ExtraFiles{ MountPath: mountPath, - Items: []bsv1alpha1.ExtraConfigItem{ - { - ConfigMapRef: &bsv1alpha1.ObjectRef{Name: extraConfig1CmName}, - }, - { - SecretRef: &bsv1alpha1.ObjectRef{Name: extraConfig2SecretName}, - }, + ConfigMaps: []bsv1alpha1.ObjectKeyRef{ + {Name: extraConfig1CmName}, + }, + Secrets: []bsv1alpha1.ObjectKeyRef{ + {Name: extraConfig2SecretName}, }, }, }, @@ -1047,16 +1049,16 @@ plugins: [] backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ Application: &bsv1alpha1.Application{ - Env: []corev1.EnvVar{ - {Name: "MY_ENV_VAR_1", Value: "value 10"}, - {Name: "MY_ENV_VAR_2", Value: "value 20"}, - }, - EnvFrom: []corev1.EnvFromSource{ - { - ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: envConfig1CmName}}, + ExtraEnvs: &bsv1alpha1.ExtraEnvs{ + Envs: []bsv1alpha1.Env{ + {Name: "MY_ENV_VAR_1", Value: "value 10"}, + {Name: "MY_ENV_VAR_2", Value: "value 20"}, + }, + ConfigMaps: []bsv1alpha1.ObjectKeyRef{ + {Name: envConfig1CmName}, }, - { - SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: envConfig2SecretName}}, + Secrets: []bsv1alpha1.ObjectKeyRef{ + {Name: envConfig2SecretName}, }, }, }, @@ -1175,15 +1177,18 @@ plugins: [] }) }) - When("setting image pull secret", func() { - var imagePullSecretName = "some-image-pull-secret" + When("setting image pull secrets", func() { + const ( + ips1 = "some-image-pull-secret-1" + ips2 = "some-image-pull-secret-2" + ) var backstage *bsv1alpha1.Backstage BeforeEach(func() { backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ Application: &bsv1alpha1.Application{ - ImagePullSecret: &imagePullSecretName, + ImagePullSecrets: []string{ips1, ips2}, }, }) err := k8sClient.Create(ctx, backstage) @@ -1216,7 +1221,7 @@ plugins: [] for _, v := range found.Spec.Template.Spec.ImagePullSecrets { list = append(list, v.Name) } - Expect(list).Should(ContainElement(imagePullSecretName)) + Expect(list).Should(HaveExactElements(ips1, ips2)) }) By("Checking the latest Status added to the Backstage instance") diff --git a/controllers/backstage_deployment.go b/controllers/backstage_deployment.go index 67a0f773..e27dc87b 100644 --- a/controllers/backstage_deployment.go +++ b/controllers/backstage_deployment.go @@ -199,9 +199,9 @@ func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, back }) } - if backstage.Spec.Application.ImagePullSecret != nil { + for _, imagePullSecret := range backstage.Spec.Application.ImagePullSecrets { deployment.Spec.Template.Spec.ImagePullSecrets = append(deployment.Spec.Template.Spec.ImagePullSecrets, v1.LocalObjectReference{ - Name: *backstage.Spec.Application.ImagePullSecret, + Name: imagePullSecret, }) } } @@ -234,7 +234,7 @@ func (r *BackstageReconciler) addVolumes(ctx context.Context, backstage bs.Backs } deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, r.appConfigsToVolumes(backstage)...) - deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, r.extraConfigsToVolumes(backstage)...) + deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, r.extraFilesToVolumes(backstage)...) return nil } @@ -247,7 +247,7 @@ func (r *BackstageReconciler) addVolumeMounts(ctx context.Context, backstage bs. if err != nil { return err } - return r.addExtraConfigsVolumeMounts(ctx, backstage, ns, deployment) + return r.addExtraFilesVolumeMounts(ctx, backstage, ns, deployment) } func (r *BackstageReconciler) addContainerArgs(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { @@ -263,10 +263,7 @@ func (r *BackstageReconciler) addEnvVars(ctx context.Context, backstage bs.Backs return nil } - for i := range deployment.Spec.Template.Spec.Containers { - deployment.Spec.Template.Spec.Containers[i].Env = append(deployment.Spec.Template.Spec.Containers[i].Env, backstage.Spec.Application.Env...) - deployment.Spec.Template.Spec.Containers[i].EnvFrom = append(deployment.Spec.Template.Spec.Containers[i].EnvFrom, backstage.Spec.Application.EnvFrom...) - } + r.addExtraEnvs(backstage, deployment) return nil } diff --git a/controllers/backstage_dynamic_plugins.go b/controllers/backstage_dynamic_plugins.go index 67d9c42b..4f9be317 100644 --- a/controllers/backstage_dynamic_plugins.go +++ b/controllers/backstage_dynamic_plugins.go @@ -41,8 +41,8 @@ import ( //) func (r *BackstageReconciler) getOrGenerateDynamicPluginsConf(ctx context.Context, backstage bs.Backstage, ns string) (configMap string, err error) { - if backstage.Spec.Application != nil && backstage.Spec.Application.DynamicPluginsConfigMapRef != "" { - return backstage.Spec.Application.DynamicPluginsConfigMapRef, nil + if backstage.Spec.Application != nil && backstage.Spec.Application.DynamicPluginsConfigMapName != "" { + return backstage.Spec.Application.DynamicPluginsConfigMapName, nil } //Create default ConfigMap for dynamic plugins diff --git a/controllers/backstage_extra_envs.go b/controllers/backstage_extra_envs.go new file mode 100644 index 00000000..ca62bb45 --- /dev/null +++ b/controllers/backstage_extra_envs.go @@ -0,0 +1,64 @@ +package controller + +import ( + bs "janus-idp.io/backstage-operator/api/v1alpha1" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" +) + +func (r *BackstageReconciler) addExtraEnvs(backstage bs.Backstage, deployment *appsv1.Deployment) { + if backstage.Spec.Application.ExtraEnvs != nil { + for _, env := range backstage.Spec.Application.ExtraEnvs.Envs { + for i := range deployment.Spec.Template.Spec.Containers { + deployment.Spec.Template.Spec.Containers[i].Env = append(deployment.Spec.Template.Spec.Containers[i].Env, v1.EnvVar{ + Name: env.Name, + Value: env.Value, + }) + } + } + + for _, cmRef := range backstage.Spec.Application.ExtraEnvs.ConfigMaps { + for i := range deployment.Spec.Template.Spec.Containers { + if cmRef.Key != "" { + deployment.Spec.Template.Spec.Containers[i].Env = append(deployment.Spec.Template.Spec.Containers[i].Env, v1.EnvVar{ + Name: cmRef.Key, + ValueFrom: &v1.EnvVarSource{ + ConfigMapKeyRef: &v1.ConfigMapKeySelector{ + LocalObjectReference: v1.LocalObjectReference{Name: cmRef.Name}, + Key: cmRef.Key, + }, + }, + }) + } else { + deployment.Spec.Template.Spec.Containers[i].EnvFrom = append(deployment.Spec.Template.Spec.Containers[i].EnvFrom, v1.EnvFromSource{ + ConfigMapRef: &v1.ConfigMapEnvSource{ + LocalObjectReference: v1.LocalObjectReference{Name: cmRef.Name}, + }, + }) + } + } + } + + for _, secRef := range backstage.Spec.Application.ExtraEnvs.Secrets { + for i := range deployment.Spec.Template.Spec.Containers { + if secRef.Key != "" { + deployment.Spec.Template.Spec.Containers[i].Env = append(deployment.Spec.Template.Spec.Containers[i].Env, v1.EnvVar{ + Name: secRef.Key, + ValueFrom: &v1.EnvVarSource{ + SecretKeyRef: &v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{Name: secRef.Name}, + Key: secRef.Key, + }, + }, + }) + } else { + deployment.Spec.Template.Spec.Containers[i].EnvFrom = append(deployment.Spec.Template.Spec.Containers[i].EnvFrom, v1.EnvFromSource{ + SecretRef: &v1.SecretEnvSource{ + LocalObjectReference: v1.LocalObjectReference{Name: secRef.Name}, + }, + }) + } + } + } + } +} diff --git a/controllers/backstage_extra_config.go b/controllers/backstage_extra_files.go similarity index 56% rename from controllers/backstage_extra_config.go rename to controllers/backstage_extra_files.go index 66e2f86c..8e834b3d 100644 --- a/controllers/backstage_extra_config.go +++ b/controllers/backstage_extra_files.go @@ -25,31 +25,33 @@ import ( "k8s.io/utils/pointer" ) -func (r *BackstageReconciler) extraConfigsToVolumes(backstage bs.Backstage) (result []v1.Volume) { - if backstage.Spec.Application == nil || backstage.Spec.Application.ExtraConfig == nil { +func (r *BackstageReconciler) extraFilesToVolumes(backstage bs.Backstage) (result []v1.Volume) { + if backstage.Spec.Application == nil || backstage.Spec.Application.ExtraFiles == nil { return nil } - for _, extraConfig := range backstage.Spec.Application.ExtraConfig.Items { - var volumeSource v1.VolumeSource - var name string - switch { - case extraConfig.ConfigMapRef != nil: - name = extraConfig.ConfigMapRef.Name - volumeSource.ConfigMap = &v1.ConfigMapVolumeSource{ - DefaultMode: pointer.Int32(420), - LocalObjectReference: v1.LocalObjectReference{Name: name}, - } - case extraConfig.SecretRef != nil: - name = extraConfig.SecretRef.Name - volumeSource.Secret = &v1.SecretVolumeSource{ - DefaultMode: pointer.Int32(420), - SecretName: name, - } - } + for _, cmExtraFile := range backstage.Spec.Application.ExtraFiles.ConfigMaps { + result = append(result, + v1.Volume{ + Name: cmExtraFile.Name, + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + DefaultMode: pointer.Int32(420), + LocalObjectReference: v1.LocalObjectReference{Name: cmExtraFile.Name}, + }, + }, + }, + ) + } + for _, secExtraFile := range backstage.Spec.Application.ExtraFiles.Secrets { result = append(result, v1.Volume{ - Name: name, - VolumeSource: volumeSource, + Name: secExtraFile.Name, + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + DefaultMode: pointer.Int32(420), + SecretName: secExtraFile.Name, + }, + }, }, ) } @@ -57,12 +59,12 @@ func (r *BackstageReconciler) extraConfigsToVolumes(backstage bs.Backstage) (res return result } -func (r *BackstageReconciler) addExtraConfigsVolumeMounts(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { - if backstage.Spec.Application == nil || backstage.Spec.Application.ExtraConfig == nil { +func (r *BackstageReconciler) addExtraFilesVolumeMounts(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { + if backstage.Spec.Application == nil || backstage.Spec.Application.ExtraFiles == nil { return nil } - appConfigFilenamesList, err := r.extractExtraConfigFileNames(ctx, backstage, ns) + appConfigFilenamesList, err := r.extractExtraFileNames(ctx, backstage, ns) if err != nil { return err } @@ -74,7 +76,7 @@ func (r *BackstageReconciler) addExtraConfigsVolumeMounts(ctx context.Context, b deployment.Spec.Template.Spec.Containers[i].VolumeMounts = append(deployment.Spec.Template.Spec.Containers[i].VolumeMounts, v1.VolumeMount{ Name: appConfigFilenames.ref, - MountPath: fmt.Sprintf("%s/%s", backstage.Spec.Application.ExtraConfig.MountPath, f), + MountPath: fmt.Sprintf("%s/%s", backstage.Spec.Application.ExtraFiles.MountPath, f), SubPath: f, }) } @@ -85,22 +87,22 @@ func (r *BackstageReconciler) addExtraConfigsVolumeMounts(ctx context.Context, b return nil } -// extractExtraConfigFileNames returns a mapping of extra-config object name and the list of files in it. +// extractExtraFileNames returns a mapping of extra-config object name and the list of files in it. // We intentionally do not return a Map, to preserve the iteration order of the ExtraConfigs in the Custom Resource, // even though we can't guarantee the iteration order of the files listed inside each ConfigMap or Secret. -func (r *BackstageReconciler) extractExtraConfigFileNames(ctx context.Context, backstage bs.Backstage, ns string) (result []appConfigData, err error) { - if backstage.Spec.Application == nil || backstage.Spec.Application.ExtraConfig == nil { +func (r *BackstageReconciler) extractExtraFileNames(ctx context.Context, backstage bs.Backstage, ns string) (result []appConfigData, err error) { + if backstage.Spec.Application == nil || backstage.Spec.Application.ExtraFiles == nil { return nil, nil } - for _, appConfig := range backstage.Spec.Application.ExtraConfig.Items { + for _, cmExtraFile := range backstage.Spec.Application.ExtraFiles.ConfigMaps { var files []string - var name string - switch { - case appConfig.ConfigMapRef != nil: - name = appConfig.ConfigMapRef.Name + if cmExtraFile.Key != "" { + // Limit to that file only + files = append(files, cmExtraFile.Key) + } else { cm := v1.ConfigMap{} - if err = r.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, &cm); err != nil { + if err = r.Get(ctx, types.NamespacedName{Name: cmExtraFile.Name, Namespace: ns}, &cm); err != nil { return nil, err } for filename := range cm.Data { @@ -111,10 +113,21 @@ func (r *BackstageReconciler) extractExtraConfigFileNames(ctx context.Context, b // Bear in mind that iteration order over this map is not guaranteed by Go files = append(files, filename) } - case appConfig.SecretRef != nil: - name = appConfig.SecretRef.Name + } + result = append(result, appConfigData{ + ref: cmExtraFile.Name, + files: files, + }) + } + + for _, secExtraFile := range backstage.Spec.Application.ExtraFiles.Secrets { + var files []string + if secExtraFile.Key != "" { + // Limit to that file only + files = append(files, secExtraFile.Key) + } else { sec := v1.Secret{} - if err = r.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, &sec); err != nil { + if err = r.Get(ctx, types.NamespacedName{Name: secExtraFile.Name, Namespace: ns}, &sec); err != nil { return nil, err } for filename := range sec.Data { @@ -123,7 +136,7 @@ func (r *BackstageReconciler) extractExtraConfigFileNames(ctx context.Context, b } } result = append(result, appConfigData{ - ref: name, + ref: secExtraFile.Name, files: files, }) } diff --git a/examples/janus-cr-with-app-configs.yaml b/examples/janus-cr-with-app-configs.yaml index cea973ca..f53961b1 100644 --- a/examples/janus-cr-with-app-configs.yaml +++ b/examples/janus-cr-with-app-configs.yaml @@ -3,40 +3,38 @@ kind: Backstage metadata: name: my-backstage-app-with-app-config spec: - backstage: + skipLocalDb: false + application: appConfig: #mountPath: /opt/app-root/src - configMapRefs: - - "my-backstage-config-cm1" - - "my-backstage-config-cm2" - dynamicPluginsConfigMapRef: "my-dynamic-plugins-config-cm" - env: - - name: MY_ENV_VAR_1 - value: my-value-1 - - name: MY_ENV_VAR_2 - value: my-value-2 - - name: MY_ENV_VAR1_FROM_CM1 - valueFrom: - configMapKeyRef: - name: my-env-cm-1 - key: CM_ENV1 - envFrom: - - configMapRef: - name: my-env-cm-1 - - prefix: MY_ENV_SECRET1_ - secretRef: - name: my-env-secret1 - backendAuthSecretKeyRef: + configMaps: + - name: "my-backstage-config-cm1" + - name: "my-backstage-config-cm2" + dynamicPluginsConfigMapName: "my-dynamic-plugins-config-cm" + extraEnvs: + envs: + - name: MY_ENV_VAR_1 + value: my-value-1 + - name: MY_ENV_VAR_2 + value: my-value-2 + configMaps: + - name: my-env-cm-1 + - name: my-env-cm-11 + key: CM_ENV11 + secrets: + - name: my-env-secret1 + - name: my-env-secret11 + key: SEC_ENV11 + backendAuthSecret: name: "my-backstage-backend-auth-secret" key: "my-auth-key" replicas: 2 - extraConfig: + extraFiles: mountPath: /tmp/my-extra-files - items: - - configMapRef: - name: "my-backstage-extra-config-cm1" - - secretRef: - name: "my-backstage-extra-config-secret1" + configMaps: + - name: "my-backstage-extra-config-cm1" + secrets: + - name: "my-backstage-extra-config-secret1" --- apiVersion: v1 @@ -119,6 +117,15 @@ data: CM_ENV1: "cm env 1" CM_ENV2: "cm env 2" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-env-cm-11 +data: + CM_ENV11: "cm env 11" + CM_ENV12: "cm env 12" + --- apiVersion: v1 kind: Secret @@ -128,6 +135,15 @@ stringData: SEC_ENV1: "secret env 1" SEC_ENV2: "secret env 2" +--- +apiVersion: v1 +kind: Secret +metadata: + name: my-env-secret11 +stringData: + SEC_ENV11: "secret env 11" + SEC_ENV12: "secret env 12" + --- apiVersion: v1 kind: ConfigMap diff --git a/examples/janus-cr.yaml b/examples/janus-cr.yaml index b5d2a771..8d113d1c 100644 --- a/examples/janus-cr.yaml +++ b/examples/janus-cr.yaml @@ -4,6 +4,6 @@ metadata: name: bs-janus namespace: backstage spec: - runtimeConfig: + rawRuntimeConfig: backstageConfig: janus-config # dryRun: true From 255545269d5a2742e3cd43e812374a71981a3152 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Fri, 8 Dec 2023 18:12:50 +0100 Subject: [PATCH 21/31] Update test cases --- controllers/backstage_controller_test.go | 534 ++++++++++++++--------- 1 file changed, 329 insertions(+), 205 deletions(-) diff --git a/controllers/backstage_controller_test.go b/controllers/backstage_controller_test.go index f5d07132..cb9fd728 100644 --- a/controllers/backstage_controller_test.go +++ b/controllers/backstage_controller_test.go @@ -497,186 +497,211 @@ spec: for _, mountPath := range []string{"", "/some/path/for/app-config"} { mountPath := mountPath - When(fmt.Sprintf("referencing ConfigMaps and Secrets for app-configs (mountPath=%q) and dynamic plugins config as ConfigMap", mountPath), - func() { - const ( - appConfig1CmName = "my-app-config-1-cm" - dynamicPluginsConfigName = "my-dynamic-plugins-config" - ) - - var backstage *bsv1alpha1.Backstage - - BeforeEach(func() { - appConfig1Cm := buildConfigMap(appConfig1CmName, map[string]string{ - "my-app-config-11.yaml": ` + for _, key := range []string{"", "my-app-config-12.yaml"} { + key := key + When(fmt.Sprintf("referencing ConfigMaps for app-configs (mountPath=%q, key=%q) and dynamic plugins config ConfigMap", mountPath, key), + func() { + const ( + appConfig1CmName = "my-app-config-1-cm" + dynamicPluginsConfigName = "my-dynamic-plugins-config" + ) + + var backstage *bsv1alpha1.Backstage + + BeforeEach(func() { + appConfig1Cm := buildConfigMap(appConfig1CmName, map[string]string{ + "my-app-config-11.yaml": ` # my-app-config-11.yaml `, - "my-app-config-12.yaml": ` + "my-app-config-12.yaml": ` # my-app-config-12.yaml `, - }) - err := k8sClient.Create(ctx, appConfig1Cm) - Expect(err).To(Not(HaveOccurred())) + }) + err := k8sClient.Create(ctx, appConfig1Cm) + Expect(err).To(Not(HaveOccurred())) - dynamicPluginsCm := buildConfigMap(dynamicPluginsConfigName, map[string]string{ - "dynamic-plugins.yaml": ` + dynamicPluginsCm := buildConfigMap(dynamicPluginsConfigName, map[string]string{ + "dynamic-plugins.yaml": ` # dynamic-plugins.yaml (configmap) includes: [dynamic-plugins.default.yaml] plugins: [] `, - }) - err = k8sClient.Create(ctx, dynamicPluginsCm) - Expect(err).To(Not(HaveOccurred())) - - backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - Application: &bsv1alpha1.Application{ - AppConfig: &bsv1alpha1.AppConfig{ - MountPath: mountPath, - ConfigMaps: []bsv1alpha1.ObjectKeyRef{ - {Name: appConfig1CmName}, + }) + err = k8sClient.Create(ctx, dynamicPluginsCm) + Expect(err).To(Not(HaveOccurred())) + + backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ + Application: &bsv1alpha1.Application{ + AppConfig: &bsv1alpha1.AppConfig{ + MountPath: mountPath, + ConfigMaps: []bsv1alpha1.ObjectKeyRef{ + { + Name: appConfig1CmName, + Key: key, + }, + }, }, + DynamicPluginsConfigMapName: dynamicPluginsConfigName, }, - DynamicPluginsConfigMapName: dynamicPluginsConfigName, - }, - }) - err = k8sClient.Create(ctx, backstage) - Expect(err).To(Not(HaveOccurred())) - }) - - It("should reconcile", func() { - By("Checking if the custom resource was successfully created") - Eventually(func() error { - found := &bsv1alpha1.Backstage{} - return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) - }, time.Minute, time.Second).Should(Succeed()) - - By("Reconciling the custom resource created") - _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, - }) - Expect(err).To(Not(HaveOccurred())) - - By("Checking that the Deployment was successfully created in the reconciliation") - found := &appsv1.Deployment{} - Eventually(func(g Gomega) { - // TODO to get name from default - err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, found) - g.Expect(err).To(Not(HaveOccurred())) - }, time.Minute, time.Second).Should(Succeed()) - - By("Checking the Volumes in the Backstage Deployment", func() { - Expect(found.Spec.Template.Spec.Volumes).To(HaveLen(4)) - - _, ok := findVolume(found.Spec.Template.Spec.Volumes, "dynamic-plugins-root") - Expect(ok).To(BeTrue(), "No volume found with name: dynamic-plugins-root") - - _, ok = findVolume(found.Spec.Template.Spec.Volumes, "dynamic-plugins-npmrc") - Expect(ok).To(BeTrue(), "No volume found with name: dynamic-plugins-npmrc") - - appConfig1CmVol, ok := findVolume(found.Spec.Template.Spec.Volumes, appConfig1CmName) - Expect(ok).To(BeTrue(), "No volume found with name: %s", appConfig1CmName) - Expect(appConfig1CmVol.VolumeSource.Secret).To(BeNil()) - Expect(appConfig1CmVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) - Expect(appConfig1CmVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(appConfig1CmName)) - - dynamicPluginsConfigVol, ok := findVolume(found.Spec.Template.Spec.Volumes, dynamicPluginsConfigName) - Expect(ok).To(BeTrue(), "No volume found with name: %s", dynamicPluginsConfigName) - Expect(dynamicPluginsConfigVol.VolumeSource.Secret).To(BeNil()) - Expect(dynamicPluginsConfigVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) - Expect(dynamicPluginsConfigVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(dynamicPluginsConfigName)) - }) - - By("Checking the Number of init containers in the Backstage Deployment") - Expect(found.Spec.Template.Spec.InitContainers).To(HaveLen(1)) - initCont := found.Spec.Template.Spec.InitContainers[0] - - By("Checking the Init Container Env Vars in the Backstage Deployment", func() { - Expect(initCont.Env).To(HaveLen(1)) - Expect(initCont.Env[0].Name).To(Equal("NPM_CONFIG_USERCONFIG")) - Expect(initCont.Env[0].Value).To(Equal("/opt/app-root/src/.npmrc.dynamic-plugins")) + }) + err = k8sClient.Create(ctx, backstage) + Expect(err).To(Not(HaveOccurred())) }) - By("Checking the Init Container Volume Mounts in the Backstage Deployment", func() { - Expect(initCont.VolumeMounts).To(HaveLen(3)) - - dpRoot := findVolumeMounts(initCont.VolumeMounts, "dynamic-plugins-root") - Expect(dpRoot).To(HaveLen(1), - "No volume mount found with name: dynamic-plugins-root") - Expect(dpRoot[0].MountPath).To(Equal("/dynamic-plugins-root")) - Expect(dpRoot[0].ReadOnly).To(BeFalse()) - Expect(dpRoot[0].SubPath).To(BeEmpty()) - - dpNpmrc := findVolumeMounts(initCont.VolumeMounts, "dynamic-plugins-npmrc") - Expect(dpNpmrc).To(HaveLen(1), - "No volume mount found with name: dynamic-plugins-npmrc") - Expect(dpNpmrc[0].MountPath).To(Equal("/opt/app-root/src/.npmrc.dynamic-plugins")) - Expect(dpNpmrc[0].ReadOnly).To(BeTrue()) - Expect(dpNpmrc[0].SubPath).To(Equal(".npmrc")) - - dp := findVolumeMounts(initCont.VolumeMounts, dynamicPluginsConfigName) - Expect(dp).To(HaveLen(1), "No volume mount found with name: %s", dynamicPluginsConfigName) - Expect(dp[0].MountPath).To(Equal("/opt/app-root/src/dynamic-plugins.yaml")) - Expect(dp[0].SubPath).To(Equal("dynamic-plugins.yaml")) - Expect(dp[0].ReadOnly).To(BeTrue()) - }) - - By("Checking the Number of main containers in the Backstage Deployment") - Expect(found.Spec.Template.Spec.Containers).To(HaveLen(1)) - mainCont := found.Spec.Template.Spec.Containers[0] - - expectedMountPath := mountPath - if expectedMountPath == "" { - expectedMountPath = "/opt/app-root/src" - } - - By("Checking the main container Args in the Backstage Deployment", func() { - Expect(mainCont.Args).To(HaveLen(6)) - Expect(mainCont.Args[1]).To(Equal("dynamic-plugins-root/app-config.dynamic-plugins.yaml")) - for i := 0; i <= 4; i += 2 { - Expect(mainCont.Args[i]).To(Equal("--config")) + It("should reconcile", func() { + By("Checking if the custom resource was successfully created") + Eventually(func() error { + found := &bsv1alpha1.Backstage{} + return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Reconciling the custom resource created") + _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Checking that the Deployment was successfully created in the reconciliation") + found := &appsv1.Deployment{} + Eventually(func(g Gomega) { + // TODO to get name from default + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, found) + g.Expect(err).To(Not(HaveOccurred())) + }, time.Minute, time.Second).Should(Succeed()) + + By("Checking the Volumes in the Backstage Deployment", func() { + Expect(found.Spec.Template.Spec.Volumes).To(HaveLen(4)) + + _, ok := findVolume(found.Spec.Template.Spec.Volumes, "dynamic-plugins-root") + Expect(ok).To(BeTrue(), "No volume found with name: dynamic-plugins-root") + + _, ok = findVolume(found.Spec.Template.Spec.Volumes, "dynamic-plugins-npmrc") + Expect(ok).To(BeTrue(), "No volume found with name: dynamic-plugins-npmrc") + + appConfig1CmVol, ok := findVolume(found.Spec.Template.Spec.Volumes, appConfig1CmName) + Expect(ok).To(BeTrue(), "No volume found with name: %s", appConfig1CmName) + Expect(appConfig1CmVol.VolumeSource.Secret).To(BeNil()) + Expect(appConfig1CmVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) + Expect(appConfig1CmVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(appConfig1CmName)) + + dynamicPluginsConfigVol, ok := findVolume(found.Spec.Template.Spec.Volumes, dynamicPluginsConfigName) + Expect(ok).To(BeTrue(), "No volume found with name: %s", dynamicPluginsConfigName) + Expect(dynamicPluginsConfigVol.VolumeSource.Secret).To(BeNil()) + Expect(dynamicPluginsConfigVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) + Expect(dynamicPluginsConfigVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(dynamicPluginsConfigName)) + }) + + By("Checking the Number of init containers in the Backstage Deployment") + Expect(found.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + initCont := found.Spec.Template.Spec.InitContainers[0] + + By("Checking the Init Container Env Vars in the Backstage Deployment", func() { + Expect(initCont.Env).To(HaveLen(1)) + Expect(initCont.Env[0].Name).To(Equal("NPM_CONFIG_USERCONFIG")) + Expect(initCont.Env[0].Value).To(Equal("/opt/app-root/src/.npmrc.dynamic-plugins")) + }) + + By("Checking the Init Container Volume Mounts in the Backstage Deployment", func() { + Expect(initCont.VolumeMounts).To(HaveLen(3)) + + dpRoot := findVolumeMounts(initCont.VolumeMounts, "dynamic-plugins-root") + Expect(dpRoot).To(HaveLen(1), + "No volume mount found with name: dynamic-plugins-root") + Expect(dpRoot[0].MountPath).To(Equal("/dynamic-plugins-root")) + Expect(dpRoot[0].ReadOnly).To(BeFalse()) + Expect(dpRoot[0].SubPath).To(BeEmpty()) + + dpNpmrc := findVolumeMounts(initCont.VolumeMounts, "dynamic-plugins-npmrc") + Expect(dpNpmrc).To(HaveLen(1), + "No volume mount found with name: dynamic-plugins-npmrc") + Expect(dpNpmrc[0].MountPath).To(Equal("/opt/app-root/src/.npmrc.dynamic-plugins")) + Expect(dpNpmrc[0].ReadOnly).To(BeTrue()) + Expect(dpNpmrc[0].SubPath).To(Equal(".npmrc")) + + dp := findVolumeMounts(initCont.VolumeMounts, dynamicPluginsConfigName) + Expect(dp).To(HaveLen(1), "No volume mount found with name: %s", dynamicPluginsConfigName) + Expect(dp[0].MountPath).To(Equal("/opt/app-root/src/dynamic-plugins.yaml")) + Expect(dp[0].SubPath).To(Equal("dynamic-plugins.yaml")) + Expect(dp[0].ReadOnly).To(BeTrue()) + }) + + By("Checking the Number of main containers in the Backstage Deployment") + Expect(found.Spec.Template.Spec.Containers).To(HaveLen(1)) + mainCont := found.Spec.Template.Spec.Containers[0] + + expectedMountPath := mountPath + if expectedMountPath == "" { + expectedMountPath = "/opt/app-root/src" } - //TODO(rm3l): the order of the rest of the --config args should be the same as the order in - // which the keys are listed in the ConfigMap/Secrets - // But as this is returned as a map, Go does not provide any guarantee on the iteration order. - Expect(mainCont.Args[3]).To(SatisfyAny( - Equal(expectedMountPath+"/my-app-config-11.yaml"), - Equal(expectedMountPath+"/my-app-config-12.yaml"), - )) - Expect(mainCont.Args[5]).To(SatisfyAny( - Equal(expectedMountPath+"/my-app-config-11.yaml"), - Equal(expectedMountPath+"/my-app-config-12.yaml"), - )) - Expect(mainCont.Args[3]).To(Not(Equal(mainCont.Args[5]))) - }) - - By("Checking the main container Volume Mounts in the Backstage Deployment", func() { - Expect(mainCont.VolumeMounts).To(HaveLen(3)) - - dpRoot := findVolumeMounts(mainCont.VolumeMounts, "dynamic-plugins-root") - Expect(dpRoot).To(HaveLen(1), "No volume mount found with name: dynamic-plugins-root") - Expect(dpRoot[0].MountPath).To(Equal("/opt/app-root/src/dynamic-plugins-root")) - Expect(dpRoot[0].SubPath).To(BeEmpty()) - appConfig1CmMounts := findVolumeMounts(mainCont.VolumeMounts, appConfig1CmName) - Expect(appConfig1CmMounts).To(HaveLen(2), "No volume mounts found with name: %s", appConfig1CmName) - Expect(appConfig1CmMounts[0].MountPath).ToNot(Equal(appConfig1CmMounts[1].MountPath)) - for i := 0; i <= 1; i++ { - Expect(appConfig1CmMounts[i].MountPath).To( - SatisfyAny( + By("Checking the main container Args in the Backstage Deployment", func() { + nbArgs := 6 + if key != "" { + nbArgs = 4 + } + Expect(mainCont.Args).To(HaveLen(nbArgs)) + Expect(mainCont.Args[1]).To(Equal("dynamic-plugins-root/app-config.dynamic-plugins.yaml")) + for i := 0; i <= nbArgs-2; i += 2 { + Expect(mainCont.Args[i]).To(Equal("--config")) + } + if key == "" { + //TODO(rm3l): the order of the rest of the --config args should be the same as the order in + // which the keys are listed in the ConfigMap/Secrets + // But as this is returned as a map, Go does not provide any guarantee on the iteration order. + Expect(mainCont.Args[3]).To(SatisfyAny( Equal(expectedMountPath+"/my-app-config-11.yaml"), - Equal(expectedMountPath+"/my-app-config-12.yaml"))) - Expect(appConfig1CmMounts[i].SubPath).To( - SatisfyAny( - Equal("my-app-config-11.yaml"), - Equal("my-app-config-12.yaml"))) - } - }) - - By("Checking the latest Status added to the Backstage instance") - verifyBackstageInstance(ctx) + Equal(expectedMountPath+"/my-app-config-12.yaml"), + )) + Expect(mainCont.Args[5]).To(SatisfyAny( + Equal(expectedMountPath+"/my-app-config-11.yaml"), + Equal(expectedMountPath+"/my-app-config-12.yaml"), + )) + Expect(mainCont.Args[3]).To(Not(Equal(mainCont.Args[5]))) + } else { + Expect(mainCont.Args[3]).To(Equal(fmt.Sprintf("%s/%s", expectedMountPath, key))) + } + }) + + By("Checking the main container Volume Mounts in the Backstage Deployment", func() { + nbMounts := 3 + if key != "" { + nbMounts = 2 + } + Expect(mainCont.VolumeMounts).To(HaveLen(nbMounts)) + + dpRoot := findVolumeMounts(mainCont.VolumeMounts, "dynamic-plugins-root") + Expect(dpRoot).To(HaveLen(1), "No volume mount found with name: dynamic-plugins-root") + Expect(dpRoot[0].MountPath).To(Equal("/opt/app-root/src/dynamic-plugins-root")) + Expect(dpRoot[0].SubPath).To(BeEmpty()) + + appConfig1CmMounts := findVolumeMounts(mainCont.VolumeMounts, appConfig1CmName) + Expect(appConfig1CmMounts).To(HaveLen(nbMounts-1), "Wrong number of volume mounts found with name: %s", appConfig1CmName) + if key != "" { + Expect(appConfig1CmMounts).To(HaveLen(1), "Wrong number of volume mounts found with name: %s", appConfig1CmName) + Expect(appConfig1CmMounts[0].MountPath).To(Equal(fmt.Sprintf("%s/%s", expectedMountPath, key))) + Expect(appConfig1CmMounts[0].SubPath).To(Equal(key)) + } else { + Expect(appConfig1CmMounts).To(HaveLen(2), "Wrong number of volume mounts found with name: %s", appConfig1CmName) + Expect(appConfig1CmMounts[0].MountPath).ToNot(Equal(appConfig1CmMounts[1].MountPath)) + for i := 0; i <= 1; i++ { + Expect(appConfig1CmMounts[i].MountPath).To( + SatisfyAny( + Equal(expectedMountPath+"/my-app-config-11.yaml"), + Equal(expectedMountPath+"/my-app-config-12.yaml"))) + Expect(appConfig1CmMounts[i].SubPath).To( + SatisfyAny( + Equal("my-app-config-11.yaml"), + Equal("my-app-config-12.yaml"))) + } + } + }) + + By("Checking the latest Status added to the Backstage instance") + verifyBackstageInstance(ctx) + }) }) - }) + } } }) @@ -883,16 +908,18 @@ plugins: [] for _, mountPath := range []string{"", "/some/path/for/extra/config"} { mountPath := mountPath - When("referencing ConfigMaps and Secrets for extra config files - mountPath="+mountPath, func() { + When("referencing ConfigMaps and Secrets for extra files - mountPath="+mountPath, func() { const ( - extraConfig1CmName = "my-extra-config-1-cm" - extraConfig2SecretName = "my-extra-config-2-secret" + extraConfig1CmNameAll = "my-extra-config-1-cm-all" + extraConfig2SecretNameAll = "my-extra-config-2-secret-all" + extraConfig1CmNameSingle = "my-extra-config-1-cm-single" + extraConfig2SecretNameSingle = "my-extra-config-2-secret-single" ) var backstage *bsv1alpha1.Backstage BeforeEach(func() { - extraConfig1Cm := buildConfigMap(extraConfig1CmName, map[string]string{ + extraConfig1CmAll := buildConfigMap(extraConfig1CmNameAll, map[string]string{ "my-extra-config-11.yaml": ` # my-extra-config-11.yaml `, @@ -900,10 +927,10 @@ plugins: [] # my-extra-config-12.yaml `, }) - err := k8sClient.Create(ctx, extraConfig1Cm) + err := k8sClient.Create(ctx, extraConfig1CmAll) Expect(err).To(Not(HaveOccurred())) - extraConfig2Secret := buildSecret(extraConfig2SecretName, map[string][]byte{ + extraConfig2SecretAll := buildSecret(extraConfig2SecretNameAll, map[string][]byte{ "my-extra-config-21.yaml": []byte(` # my-extra-config-21.yaml `), @@ -911,7 +938,29 @@ plugins: [] # my-extra-config-22.yaml `), }) - err = k8sClient.Create(ctx, extraConfig2Secret) + err = k8sClient.Create(ctx, extraConfig2SecretAll) + Expect(err).To(Not(HaveOccurred())) + + extraConfig1CmSingle := buildConfigMap(extraConfig1CmNameSingle, map[string]string{ + "my-extra-file-11-single.yaml": ` +# my-extra-file-11-single.yaml +`, + "my-extra-file-12-single.yaml": ` +# my-extra-file-12-single.yaml +`, + }) + err = k8sClient.Create(ctx, extraConfig1CmSingle) + Expect(err).To(Not(HaveOccurred())) + + extraConfig2SecretSingle := buildSecret(extraConfig2SecretNameSingle, map[string][]byte{ + "my-extra-file-21-single.yaml": []byte(` +# my-extra-file-21-single.yaml +`), + "my-extra-file-22-single.yaml": []byte(` +# my-extra-file-22-single.yaml +`), + }) + err = k8sClient.Create(ctx, extraConfig2SecretSingle) Expect(err).To(Not(HaveOccurred())) backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ @@ -919,10 +968,12 @@ plugins: [] ExtraFiles: &bsv1alpha1.ExtraFiles{ MountPath: mountPath, ConfigMaps: []bsv1alpha1.ObjectKeyRef{ - {Name: extraConfig1CmName}, + {Name: extraConfig1CmNameAll}, + {Name: extraConfig1CmNameSingle, Key: "my-extra-file-12-single.yaml"}, }, Secrets: []bsv1alpha1.ObjectKeyRef{ - {Name: extraConfig2SecretName}, + {Name: extraConfig2SecretNameAll}, + {Name: extraConfig2SecretNameSingle, Key: "my-extra-file-22-single.yaml"}, }, }, }, @@ -953,19 +1004,31 @@ plugins: [] }, time.Minute, time.Second).Should(Succeed()) By("Checking the Volumes in the Backstage Deployment", func() { - Expect(found.Spec.Template.Spec.Volumes).To(HaveLen(5)) + Expect(found.Spec.Template.Spec.Volumes).To(HaveLen(7)) - extraConfig1CmVol, ok := findVolume(found.Spec.Template.Spec.Volumes, extraConfig1CmName) - Expect(ok).To(BeTrue(), "No volume found with name: %s", extraConfig1CmName) + extraConfig1CmVol, ok := findVolume(found.Spec.Template.Spec.Volumes, extraConfig1CmNameAll) + Expect(ok).To(BeTrue(), "No volume found with name: %s", extraConfig1CmNameAll) Expect(extraConfig1CmVol.VolumeSource.Secret).To(BeNil()) Expect(extraConfig1CmVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) - Expect(extraConfig1CmVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(extraConfig1CmName)) + Expect(extraConfig1CmVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(extraConfig1CmNameAll)) - extraConfig2SecretVol, ok := findVolume(found.Spec.Template.Spec.Volumes, extraConfig2SecretName) - Expect(ok).To(BeTrue(), "No volume found with name: %s", extraConfig2SecretName) + extraConfig2SecretVol, ok := findVolume(found.Spec.Template.Spec.Volumes, extraConfig2SecretNameAll) + Expect(ok).To(BeTrue(), "No volume found with name: %s", extraConfig2SecretNameAll) Expect(extraConfig2SecretVol.VolumeSource.ConfigMap).To(BeNil()) Expect(extraConfig2SecretVol.VolumeSource.Secret.DefaultMode).To(HaveValue(Equal(int32(420)))) - Expect(extraConfig2SecretVol.VolumeSource.Secret.SecretName).To(Equal(extraConfig2SecretName)) + Expect(extraConfig2SecretVol.VolumeSource.Secret.SecretName).To(Equal(extraConfig2SecretNameAll)) + + extraConfig1SingleCmVol, ok := findVolume(found.Spec.Template.Spec.Volumes, extraConfig1CmNameSingle) + Expect(ok).To(BeTrue(), "No volume found with name: %s", extraConfig1CmNameSingle) + Expect(extraConfig1SingleCmVol.VolumeSource.Secret).To(BeNil()) + Expect(extraConfig1SingleCmVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) + Expect(extraConfig1SingleCmVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(extraConfig1CmNameSingle)) + + extraConfig2SingleSecretVol, ok := findVolume(found.Spec.Template.Spec.Volumes, extraConfig2SecretNameSingle) + Expect(ok).To(BeTrue(), "No volume found with name: %s", extraConfig2SecretNameSingle) + Expect(extraConfig2SingleSecretVol.VolumeSource.ConfigMap).To(BeNil()) + Expect(extraConfig2SingleSecretVol.VolumeSource.Secret.DefaultMode).To(HaveValue(Equal(int32(420)))) + Expect(extraConfig2SingleSecretVol.VolumeSource.Secret.SecretName).To(Equal(extraConfig2SecretNameSingle)) }) initCont := found.Spec.Template.Spec.InitContainers[0] @@ -973,22 +1036,22 @@ plugins: [] Expect(initCont.VolumeMounts).To(HaveLen(3)) // Extra config mounted in the main container - Expect(findVolumeMounts(initCont.VolumeMounts, extraConfig1CmName)).Should(HaveLen(0)) - Expect(findVolumeMounts(initCont.VolumeMounts, extraConfig2SecretName)).Should(HaveLen(0)) + Expect(findVolumeMounts(initCont.VolumeMounts, extraConfig1CmNameAll)).Should(HaveLen(0)) + Expect(findVolumeMounts(initCont.VolumeMounts, extraConfig2SecretNameAll)).Should(HaveLen(0)) }) mainCont := found.Spec.Template.Spec.Containers[0] By("Checking the main container Volume Mounts in the Backstage Deployment", func() { - Expect(mainCont.VolumeMounts).To(HaveLen(5)) + Expect(mainCont.VolumeMounts).To(HaveLen(7)) expectedMountPath := mountPath if expectedMountPath == "" { expectedMountPath = "/opt/app-root/src" } - extraConfig1CmMounts := findVolumeMounts(mainCont.VolumeMounts, extraConfig1CmName) - Expect(extraConfig1CmMounts).To(HaveLen(2), "No volume mounts found with name: %s", extraConfig1CmName) + extraConfig1CmMounts := findVolumeMounts(mainCont.VolumeMounts, extraConfig1CmNameAll) + Expect(extraConfig1CmMounts).To(HaveLen(2), "No volume mounts found with name: %s", extraConfig1CmNameAll) Expect(extraConfig1CmMounts[0].MountPath).ToNot(Equal(extraConfig1CmMounts[1].MountPath)) for i := 0; i <= 1; i++ { Expect(extraConfig1CmMounts[i].MountPath).To( @@ -1001,8 +1064,8 @@ plugins: [] Equal("my-extra-config-12.yaml"))) } - extraConfig2SecretMounts := findVolumeMounts(mainCont.VolumeMounts, extraConfig2SecretName) - Expect(extraConfig2SecretMounts).To(HaveLen(2), "No volume mounts found with name: %s", extraConfig2SecretName) + extraConfig2SecretMounts := findVolumeMounts(mainCont.VolumeMounts, extraConfig2SecretNameAll) + Expect(extraConfig2SecretMounts).To(HaveLen(2), "No volume mounts found with name: %s", extraConfig2SecretNameAll) Expect(extraConfig2SecretMounts[0].MountPath).ToNot(Equal(extraConfig2SecretMounts[1].MountPath)) for i := 0; i <= 1; i++ { Expect(extraConfig2SecretMounts[i].MountPath).To( @@ -1014,6 +1077,16 @@ plugins: [] Equal("my-extra-config-21.yaml"), Equal("my-extra-config-22.yaml"))) } + + extraConfig1CmSingleMounts := findVolumeMounts(mainCont.VolumeMounts, extraConfig1CmNameSingle) + Expect(extraConfig1CmSingleMounts).To(HaveLen(1), "No volume mounts found with name: %s", extraConfig1CmNameSingle) + Expect(extraConfig1CmSingleMounts[0].MountPath).To(Equal(expectedMountPath + "/my-extra-file-12-single.yaml")) + Expect(extraConfig1CmSingleMounts[0].SubPath).To(Equal("my-extra-file-12-single.yaml")) + + extraConfig2SecretSingleMounts := findVolumeMounts(mainCont.VolumeMounts, extraConfig2SecretNameSingle) + Expect(extraConfig2SecretSingleMounts).To(HaveLen(1), "No volume mounts found with name: %s", extraConfig2SecretNameSingle) + Expect(extraConfig2SecretSingleMounts[0].MountPath).To(Equal(expectedMountPath + "/my-extra-file-22-single.yaml")) + Expect(extraConfig2SecretSingleMounts[0].SubPath).To(Equal("my-extra-file-22-single.yaml")) }) By("Checking the latest Status added to the Backstage instance") @@ -1023,30 +1096,46 @@ plugins: [] } }) - Context("Env and EnvFrom", func() { + Context("Extra Env Vars", func() { When("setting environment variables either directly or via references to ConfigMap or Secret", func() { const ( - envConfig1CmName = "my-env-config-1-cm" - envConfig2SecretName = "my-env-config-2-secret" + envConfig1CmNameAll = "my-env-config-1-cm-all" + envConfig2SecretNameAll = "my-env-config-2-secret-all" + envConfig1CmNameSingle = "my-env-config-1-cm-single" + envConfig2SecretNameSingle = "my-env-config-2-secret-single" ) var backstage *bsv1alpha1.Backstage BeforeEach(func() { - envConfig1Cm := buildConfigMap(envConfig1CmName, map[string]string{ + envConfig1Cm := buildConfigMap(envConfig1CmNameAll, map[string]string{ "MY_ENV_VAR_1_FROM_CM": "value 11", "MY_ENV_VAR_2_FROM_CM": "value 12", }) err := k8sClient.Create(ctx, envConfig1Cm) Expect(err).To(Not(HaveOccurred())) - envConfig2Secret := buildSecret(envConfig2SecretName, map[string][]byte{ + envConfig2Secret := buildSecret(envConfig2SecretNameAll, map[string][]byte{ "MY_ENV_VAR_1_FROM_SECRET": []byte("value 21"), "MY_ENV_VAR_2_FROM_SECRET": []byte("value 22"), }) err = k8sClient.Create(ctx, envConfig2Secret) Expect(err).To(Not(HaveOccurred())) + envConfig1CmSingle := buildConfigMap(envConfig1CmNameSingle, map[string]string{ + "MY_ENV_VAR_1_FROM_CM_SINGLE": "value 11 single", + "MY_ENV_VAR_2_FROM_CM_SINGLE": "value 12 single", + }) + err = k8sClient.Create(ctx, envConfig1CmSingle) + Expect(err).To(Not(HaveOccurred())) + + envConfig2SecretSingle := buildSecret(envConfig2SecretNameSingle, map[string][]byte{ + "MY_ENV_VAR_1_FROM_SECRET_SINGLE": []byte("value 21 single"), + "MY_ENV_VAR_2_FROM_SECRET_SINGLE": []byte("value 22 single"), + }) + err = k8sClient.Create(ctx, envConfig2SecretSingle) + Expect(err).To(Not(HaveOccurred())) + backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ Application: &bsv1alpha1.Application{ ExtraEnvs: &bsv1alpha1.ExtraEnvs{ @@ -1055,10 +1144,12 @@ plugins: [] {Name: "MY_ENV_VAR_2", Value: "value 20"}, }, ConfigMaps: []bsv1alpha1.ObjectKeyRef{ - {Name: envConfig1CmName}, + {Name: envConfig1CmNameAll}, + {Name: envConfig1CmNameSingle, Key: "MY_ENV_VAR_2_FROM_CM_SINGLE"}, }, Secrets: []bsv1alpha1.ObjectKeyRef{ - {Name: envConfig2SecretName}, + {Name: envConfig2SecretNameAll}, + {Name: envConfig2SecretNameSingle, Key: "MY_ENV_VAR_2_FROM_SECRET_SINGLE"}, }, }, }, @@ -1090,23 +1181,51 @@ plugins: [] mainCont := found.Spec.Template.Spec.Containers[0] By(fmt.Sprintf("Checking Env in the Backstage Deployment - container: %q", mainCont.Name), func() { - Expect(len(mainCont.Env)).To(BeNumerically(">=", 2), - "Expected at least 2 items in Env for container %q, fot %d", mainCont.Name, len(mainCont.Env)) + Expect(len(mainCont.Env)).To(BeNumerically(">=", 4), + "Expected at least 4 items in Env for container %q, fot %d", mainCont.Name, len(mainCont.Env)) + envVar, ok := findEnvVar(mainCont.Env, "MY_ENV_VAR_1") - Expect(ok).To(BeTrue(), "No env var with name MY_ENV_VAR_1 in init container") + Expect(ok).To(BeTrue(), "No env var with name MY_ENV_VAR_1 in main container") Expect(envVar.Value).Should(Equal("value 10")) + envVar, ok = findEnvVar(mainCont.Env, "MY_ENV_VAR_2") - Expect(ok).To(BeTrue(), "No env var with name MY_ENV_VAR_2 in init container") + Expect(ok).To(BeTrue(), "No env var with name MY_ENV_VAR_2 in main container") Expect(envVar.Value).Should(Equal("value 20")) + + envVar, ok = findEnvVar(mainCont.Env, "MY_ENV_VAR_2_FROM_CM_SINGLE") + Expect(ok).To(BeTrue(), "No env var with name MY_ENV_VAR_2_FROM_CM_SINGLE in main container") + Expect(envVar.Value).Should(BeEmpty()) + Expect(envVar.ValueFrom).ShouldNot(BeNil()) + Expect(envVar.ValueFrom.FieldRef).Should(BeNil()) + Expect(envVar.ValueFrom.ResourceFieldRef).Should(BeNil()) + Expect(envVar.ValueFrom.SecretKeyRef).Should(BeNil()) + Expect(envVar.ValueFrom.ConfigMapKeyRef).ShouldNot(BeNil()) + Expect(envVar.ValueFrom.ConfigMapKeyRef.Key).Should(Equal("MY_ENV_VAR_2_FROM_CM_SINGLE")) + Expect(envVar.ValueFrom.ConfigMapKeyRef.LocalObjectReference.Name).Should(Equal(envConfig1CmNameSingle)) + + envVar, ok = findEnvVar(mainCont.Env, "MY_ENV_VAR_2_FROM_SECRET_SINGLE") + Expect(ok).To(BeTrue(), "No env var with name MY_ENV_VAR_2_FROM_SECRET_SINGLE in main container") + Expect(envVar.Value).Should(BeEmpty()) + Expect(envVar.ValueFrom).ShouldNot(BeNil()) + Expect(envVar.ValueFrom.FieldRef).Should(BeNil()) + Expect(envVar.ValueFrom.ResourceFieldRef).Should(BeNil()) + Expect(envVar.ValueFrom.ConfigMapKeyRef).Should(BeNil()) + Expect(envVar.ValueFrom.SecretKeyRef).ShouldNot(BeNil()) + Expect(envVar.ValueFrom.SecretKeyRef.Key).Should(Equal("MY_ENV_VAR_2_FROM_SECRET_SINGLE")) + Expect(envVar.ValueFrom.SecretKeyRef.LocalObjectReference.Name).Should(Equal(envConfig2SecretNameSingle)) }) By(fmt.Sprintf("Checking EnvFrom in the Backstage Deployment - container: %q", mainCont.Name), func() { Expect(len(mainCont.EnvFrom)).To(BeNumerically(">=", 2), "Expected at least 2 items in EnvFrom for container %q, fot %d", mainCont.Name, len(mainCont.EnvFrom)) - envVar, ok := findEnvVarFrom(mainCont.EnvFrom, envConfig1CmName) - Expect(ok).To(BeTrue(), "No ConfigMap-backed envFrom in init container: %s", envConfig1CmName) + + envVar, ok := findEnvVarFrom(mainCont.EnvFrom, envConfig1CmNameAll) + Expect(ok).To(BeTrue(), "No ConfigMap-backed envFrom in main container: %s", envConfig1CmNameAll) + Expect(envVar.SecretRef).Should(BeNil()) Expect(envVar.ConfigMapRef).ShouldNot(BeNil()) - envVar, ok = findEnvVarFrom(mainCont.EnvFrom, envConfig2SecretName) - Expect(ok).To(BeTrue(), "No Secret-backed envFrom in init container: %s", envConfig2SecretName) + + envVar, ok = findEnvVarFrom(mainCont.EnvFrom, envConfig2SecretNameAll) + Expect(ok).To(BeTrue(), "No Secret-backed envFrom in main container: %s", envConfig2SecretNameAll) + Expect(envVar.ConfigMapRef).Should(BeNil()) Expect(envVar.SecretRef).ShouldNot(BeNil()) }) @@ -1116,12 +1235,16 @@ plugins: [] Expect(ok).To(BeFalse(), "Env var with name MY_ENV_VAR_1 should not be injected into init container") _, ok = findEnvVar(initCont.Env, "MY_ENV_VAR_2") Expect(ok).To(BeFalse(), "Env var with name MY_ENV_VAR_2 should not be injected into init container") + _, ok = findEnvVar(initCont.Env, "MY_ENV_VAR_2_FROM_CM_SINGLE") + Expect(ok).To(BeFalse(), "Env var with name MY_ENV_VAR_2_FROM_CM_SINGLE should not be injected into init container") + _, ok = findEnvVar(initCont.Env, "MY_ENV_VAR_2_FROM_SECRET_SINGLE") + Expect(ok).To(BeFalse(), "Env var with name MY_ENV_VAR_2_FROM_SECRET_SINGLE should not be injected into init container") }) By("not injecting EnvFrom set in CR into the Backstage Deployment Init Container", func() { - _, ok := findEnvVarFrom(initCont.EnvFrom, envConfig1CmName) - Expect(ok).To(BeFalse(), "ConfigMap-backed envFrom should not be added to init container: %s", envConfig1CmName) - _, ok = findEnvVarFrom(initCont.EnvFrom, envConfig2SecretName) - Expect(ok).To(BeFalse(), "Secret-backed envFrom should not be added to init container: %s", envConfig2SecretName) + _, ok := findEnvVarFrom(initCont.EnvFrom, envConfig1CmNameAll) + Expect(ok).To(BeFalse(), "ConfigMap-backed envFrom should not be added to init container: %s", envConfig1CmNameAll) + _, ok = findEnvVarFrom(initCont.EnvFrom, envConfig2SecretNameAll) + Expect(ok).To(BeFalse(), "Secret-backed envFrom should not be added to init container: %s", envConfig2SecretNameAll) }) By("Checking the latest Status added to the Backstage instance") @@ -1166,6 +1289,7 @@ plugins: [] g.Expect(err).To(Not(HaveOccurred())) }, time.Minute, time.Second).Should(Succeed()) + By("Checking that the image was set on all containers in the Pod Spec") visitContainers(&found.Spec.Template, func(container *corev1.Container) { By(fmt.Sprintf("Checking Image in the Backstage Deployment - container: %q", container.Name), func() { Expect(container.Image).Should(Equal(imageName)) @@ -1216,7 +1340,7 @@ plugins: [] g.Expect(err).To(Not(HaveOccurred())) }, time.Minute, time.Second).Should(Succeed()) - By("Checking the image pull secret is included in the pod spec of Backstage", func() { + By("Checking the image pull secrets are included in the pod spec of Backstage", func() { var list []string for _, v := range found.Spec.Template.Spec.ImagePullSecrets { list = append(list, v.Name) From 52518929acc1d453a1234a0521c9c921546183a7 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Fri, 8 Dec 2023 18:18:20 +0100 Subject: [PATCH 22/31] Update bundle manifests --- ...kstage-operator.clusterserviceversion.yaml | 2 +- bundle/manifests/janus-idp.io_backstages.yaml | 282 +++++++++--------- 2 files changed, 141 insertions(+), 143 deletions(-) diff --git a/bundle/manifests/backstage-operator.clusterserviceversion.yaml b/bundle/manifests/backstage-operator.clusterserviceversion.yaml index 90b623bd..a151665b 100644 --- a/bundle/manifests/backstage-operator.clusterserviceversion.yaml +++ b/bundle/manifests/backstage-operator.clusterserviceversion.yaml @@ -21,7 +21,7 @@ metadata: } ] capabilities: Basic Install - createdAt: "2023-12-04T14:55:40Z" + createdAt: "2023-12-08T17:17:14Z" operators.operatorframework.io/builder: operator-sdk-v1.32.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 name: backstage-operator.v0.0.1 diff --git a/bundle/manifests/janus-idp.io_backstages.yaml b/bundle/manifests/janus-idp.io_backstages.yaml index 84652c2d..6691ceaf 100644 --- a/bundle/manifests/janus-idp.io_backstages.yaml +++ b/bundle/manifests/janus-idp.io_backstages.yaml @@ -34,179 +34,161 @@ spec: spec: description: BackstageSpec defines the desired state of Backstage properties: - backstage: + application: description: Configuration for Backstage. Optional. properties: appConfig: - description: References to existing app-configs Config objects, + description: References to existing app-configs ConfigMap objects, that will be mounted as files in the specified mount path. Each element can be a reference to any ConfigMap or Secret, and will - be mounted inside the main application container under a dedicated - directory containing the ConfigMap or Secret name (relative - to the specified mount path). Additionally, each file will be - passed as a `--config /path/to/secret_or_configmap/key` to the - main container args in the order of the entries defined in the - AppConfigs list. But bear in mind that for a single AppConfig - element containing several files, the order in which those files - will be appended to the container args, the main container args - cannot be guaranteed. So if you want to pass multiple app-config - files, it is recommended to pass one ConfigMap/Secret per app-config - file. + be mounted inside the main application container under a specified + mount directory. Additionally, each file will be passed as a + `--config /mount/path/to/configmap/key` to the main container + args in the order of the entries defined in the AppConfigs list. + But bear in mind that for a single ConfigMap element containing + several filenames, the order in which those files will be appended + to the main container args cannot be guaranteed. So if you want + to pass multiple app-config files, it is recommended to pass + one ConfigMap per app-config file. properties: - items: - description: List of references to app-config Config objects. + configMaps: + description: List of ConfigMaps storing the app-config files. + Will be mounted as files under the MountPath specified. + For each item in this array, if a key is not specified, + it means that all keys in the ConfigMap will be mounted + as files. Otherwise, only the specified key will be mounted + as a file. Bear in mind not to put sensitive data in those + ConfigMaps. Instead, your app-config content can reference + environment variables (which you can set with the ExtraEnvs + field) and/or include extra files (see the ExtraFiles field). + More details on https://backstage.io/docs/conf/writing/. items: properties: - configMapRef: - description: ConfigMap containing one or more app-config - files - properties: - name: - description: Name of the object referenced. - type: string - required: - - name - type: object - secretRef: - description: Secret containing one or more app-config - files - properties: - name: - description: Name of the object referenced. - type: string - required: - - name - type: object + key: + description: Key in the object + type: string + name: + description: Name of the object We support only ConfigMaps + and Secrets. + type: string + required: + - name type: object type: array mountPath: default: /opt/app-root/src description: Mount path for all app-config files listed in - the Items field + the ConfigMapRefs field type: string type: object - backendAuthSecretKeyRef: + backendAuthSecret: description: Optional Reference to a Secret to use for Backend Auth. A new one will be generated if not set. This Secret is used to set an environment variable named 'APP_CONFIG_backend_auth_keys' in the main container, which takes precedence over any 'backend.auth.keys' field defined in default or custom application configuration files. This is required for service-to-service auth and is shared - by all backend plugins. + by all backend plugins. Default value for the key in the secret + is 'backend-secret. properties: key: - default: backend-secret - description: 'Key in the secret to use for the backend auth. - Default value is: backend-secret' + description: Key in the object type: string name: - description: Name of the secret to use for the backend auth + description: Name of the object We support only ConfigMaps + and Secrets. type: string required: - name type: object - dynamicPlugins: - description: 'Reference to an existing configuration object for - Dynamic Plugins. This can be a reference to any ConfigMap or - Secret, but the object must have an existing key named: ''dynamic-plugins.yaml''' + dynamicPluginsConfigMapName: + description: 'Reference to an existing ConfigMap for Dynamic Plugins. + A new one will be generated with the default config if not set. + The ConfigMap object must have an existing key named: ''dynamic-plugins.yaml''.' + type: string + extraEnvs: + description: Extra environment variables properties: - configMapRef: - description: 'ConfigMap containing the dynamic plugins'' configuration. - It needs to have a key named: "dynamic-plugins.yaml". ConfigMapRef - will be used if both ConfigMapRef and SecretRef are provided.' - properties: - name: - description: Name of the object referenced. - type: string - required: - - name - type: object - secretRef: - description: 'Secret containing the dynamic plugins'' configuration. - It needs to have a key named: "dynamic-plugins.yaml". ConfigMapRef - will be used if both ConfigMapRef and SecretRef are provided.' - properties: - name: - description: Name of the object referenced. - type: string - required: - - name - type: object - type: object - env: - description: Environment variables to inject into all the Backstage - containers. Bear in mind not to put sensitive data here. Use - EnvFrom instead. - items: - properties: - name: - description: Name of the environment variable - type: string - value: - description: Value of the environment variable - type: string - required: - - name - - value - type: object - type: array - envFrom: - description: Environment variables to inject into all the Backstage - containers, as references to existing ConfigMap or Secret objects. - items: - properties: - configMapRef: - description: ConfigMap containing the environment variables - to inject + configMaps: + description: List of references to ConfigMaps objects to inject + as additional environment variables. For each item in this + array, if a key is not specified, it means that all keys + in the ConfigMap will be injected as additional environment + variables. Otherwise, only the specified key will be injected + as an additional environment variable. + items: properties: + key: + description: Key in the object + type: string name: - description: Name of the object referenced. + description: Name of the object We support only ConfigMaps + and Secrets. type: string required: - name type: object - secretRef: - description: Secret containing the environment variables - to inject + type: array + envs: + description: List of name and value pairs to add as environment + variables. + items: properties: name: - description: Name of the object referenced. + description: Name of the environment variable + type: string + value: + description: Value of the environment variable type: string required: - name + - value type: object - type: object - type: array - extraConfig: + type: array + secrets: + description: List of references to Secrets objects to inject + as additional environment variables. For each item in this + array, if a key is not specified, it means that all keys + in the Secret will be injected as additional environment + variables. Otherwise, only the specified key will be injected + as environment variable. + items: + properties: + key: + description: Key in the object + type: string + name: + description: Name of the object We support only ConfigMaps + and Secrets. + type: string + required: + - name + type: object + type: array + type: object + extraFiles: description: References to existing Config objects to use as extra config files. They will be mounted as files in the specified mount path. Each element can be a reference to any ConfigMap or Secret. properties: - items: - description: List of references to extra config Config objects. + configMaps: + description: List of references to ConfigMaps objects mounted + as extra files under the MountPath specified. For each item + in this array, if a key is not specified, it means that + all keys in the ConfigMap will be mounted as files. Otherwise, + only the specified key will be mounted as a file. items: properties: - configMapRef: - description: ConfigMap containing one or more extra - config files - properties: - name: - description: Name of the object referenced. - type: string - required: - - name - type: object - secretRef: - description: Secret containing one or more extra config - files - properties: - name: - description: Name of the object referenced. - type: string - required: - - name - type: object + key: + description: Key in the object + type: string + name: + description: Name of the object We support only ConfigMaps + and Secrets. + type: string + required: + - name type: object type: array mountPath: @@ -214,14 +196,35 @@ spec: description: Mount path for all extra configuration files listed in the Items field type: string + secrets: + description: List of references to Secrets objects mounted + as extra files under the MountPath specified. For each item + in this array, if a key is not specified, it means that + all keys in the Secret will be mounted as files. Otherwise, + only the specified key will be mounted as a file. + items: + properties: + key: + description: Key in the object + type: string + name: + description: Name of the object We support only ConfigMaps + and Secrets. + type: string + required: + - name + type: object + type: array type: object image: description: Image to use in all containers (including Init Containers) type: string - imagePullSecret: - description: Image Pull Secret to use in all containers (including + imagePullSecrets: + description: Image Pull Secrets to use in all containers (including Init Containers) - type: string + items: + type: string + type: array replicas: default: 1 description: Number of desired replicas to set in the Backstage @@ -229,21 +232,6 @@ spec: format: int32 type: integer type: object - postgresql: - description: Configuration for the local database. Optional. - properties: - disabled: - default: true - description: Control the creation of a local PostgreSQL DB. Set - to false if using for example an external Database for Backstage. - To use an external Database, you can provide your own app-config - file (see the AppConfig field) containing references to the - Database connection information, which might be supplied as - environment variables (see the Env field) or extra-configuration - files (see the ExtraConfig field in the BackstageSpecBackstage - structure). - type: boolean - type: object rawRuntimeConfig: description: Raw Runtime Objects configuration. For Advanced scenarios. properties: @@ -256,6 +244,16 @@ spec: runtime objects configuration type: string type: object + skipLocalDb: + default: false + description: Control the creation of a local PostgreSQL DB. Set to + true if using for example an external Database for Backstage. To + use an external Database, you can provide your own app-config file + (see the AppConfig field in the Application structure) containing + references to the Database connection information, which might be + supplied as environment variables (see the Env field) or extra-configuration + files (see the ExtraFiles field in the Application structure). + type: boolean type: object status: description: BackstageStatus defines the observed state of Backstage From faf43335440f5a4e768bf2a72f764337e5500677 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 11 Dec 2023 00:15:14 +0100 Subject: [PATCH 23/31] Remove the need for the backendAuthSecret CRD field --- api/v1alpha1/backstage_types.go | 16 +-- api/v1alpha1/zz_generated.deepcopy.go | 5 - config/crd/bases/janus-idp.io_backstages.yaml | 20 --- .../backend-auth-configmap.yaml | 11 ++ .../default-config/backend-auth-secret.yaml | 2 +- controllers/backstage_app_config.go | 69 ++++++++--- controllers/backstage_backend_auth.go | 116 ++++++++++-------- controllers/backstage_deployment.go | 19 ++- examples/janus-cr-with-app-configs.yaml | 1 + 9 files changed, 149 insertions(+), 110 deletions(-) create mode 100644 config/manager/default-config/backend-auth-configmap.yaml diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index af226e5c..37207bca 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -53,14 +53,14 @@ type Application struct { // +optional AppConfig *AppConfig `json:"appConfig,omitempty"` - // Optional Reference to a Secret to use for Backend Auth. A new one will be generated if not set. - // This Secret is used to set an environment variable named 'APP_CONFIG_backend_auth_keys' in the - // main container, which takes precedence over any 'backend.auth.keys' field defined - // in default or custom application configuration files. - // This is required for service-to-service auth and is shared by all backend plugins. - // Default value for the key in the secret is 'backend-secret. - // +optional - BackendAuthSecret *ObjectKeyRef `json:"backendAuthSecret,omitempty"` + //// Optional Reference to a Secret to use for Backend Auth. A new one will be generated if not set. + //// This Secret is used to set an environment variable named 'APP_CONFIG_backend_auth_keys' in the + //// main container, which takes precedence over any 'backend.auth.keys' field defined + //// in default or custom application configuration files. + //// This is required for service-to-service auth and is shared by all backend plugins. + //// Default value for the key in the secret is 'backend-secret. + //// +optional + //BackendAuthSecret *ObjectKeyRef `json:"backendAuthSecret,omitempty"` // Reference to an existing ConfigMap for Dynamic Plugins. // A new one will be generated with the default config if not set. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index b4fdac77..e27a122d 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -54,11 +54,6 @@ func (in *Application) DeepCopyInto(out *Application) { *out = new(AppConfig) (*in).DeepCopyInto(*out) } - if in.BackendAuthSecret != nil { - in, out := &in.BackendAuthSecret, &out.BackendAuthSecret - *out = new(ObjectKeyRef) - **out = **in - } if in.ExtraFiles != nil { in, out := &in.ExtraFiles, &out.ExtraFiles *out = new(ExtraFiles) diff --git a/config/crd/bases/janus-idp.io_backstages.yaml b/config/crd/bases/janus-idp.io_backstages.yaml index 1d4a4c9c..1dd20cd0 100644 --- a/config/crd/bases/janus-idp.io_backstages.yaml +++ b/config/crd/bases/janus-idp.io_backstages.yaml @@ -82,26 +82,6 @@ spec: the ConfigMapRefs field type: string type: object - backendAuthSecret: - description: Optional Reference to a Secret to use for Backend - Auth. A new one will be generated if not set. This Secret is - used to set an environment variable named 'APP_CONFIG_backend_auth_keys' - in the main container, which takes precedence over any 'backend.auth.keys' - field defined in default or custom application configuration - files. This is required for service-to-service auth and is shared - by all backend plugins. Default value for the key in the secret - is 'backend-secret. - properties: - key: - description: Key in the object - type: string - name: - description: Name of the object We support only ConfigMaps - and Secrets. - type: string - required: - - name - type: object dynamicPluginsConfigMapName: description: 'Reference to an existing ConfigMap for Dynamic Plugins. A new one will be generated with the default config if not set. diff --git a/config/manager/default-config/backend-auth-configmap.yaml b/config/manager/default-config/backend-auth-configmap.yaml new file mode 100644 index 00000000..b862592d --- /dev/null +++ b/config/manager/default-config/backend-auth-configmap.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: # placeholder for '-backend-auth' +data: + "app-config.backend-auth.default.yaml": | + backend: + auth: + keys: + # This is a default value, which you should change by providing your own app-config + - secret: "pl4s3Ch4ng3M3" diff --git a/config/manager/default-config/backend-auth-secret.yaml b/config/manager/default-config/backend-auth-secret.yaml index 34e04f9a..b7f673b2 100644 --- a/config/manager/default-config/backend-auth-secret.yaml +++ b/config/manager/default-config/backend-auth-secret.yaml @@ -3,4 +3,4 @@ kind: Secret metadata: name: # placeholder for '-auth' data: -# A random value will be generated for the backend-secret key + backend-secret: # placeholder for random value that will be generated for the backend-secret key \ No newline at end of file diff --git a/controllers/backstage_app_config.go b/controllers/backstage_app_config.go index 7fd532ae..bcfaa953 100644 --- a/controllers/backstage_app_config.go +++ b/controllers/backstage_app_config.go @@ -30,11 +30,16 @@ type appConfigData struct { files []string } -func (r *BackstageReconciler) appConfigsToVolumes(backstage bs.Backstage) (result []v1.Volume) { - if backstage.Spec.Application == nil || backstage.Spec.Application.AppConfig == nil { - return nil +func (r *BackstageReconciler) appConfigsToVolumes(backstage bs.Backstage, backendAuthAppConfig *bs.ObjectKeyRef) (result []v1.Volume) { + var cms []bs.ObjectKeyRef + if backendAuthAppConfig != nil { + cms = append(cms, *backendAuthAppConfig) } - for _, cm := range backstage.Spec.Application.AppConfig.ConfigMaps { + if backstage.Spec.Application != nil && backstage.Spec.Application.AppConfig != nil { + cms = append(cms, backstage.Spec.Application.AppConfig.ConfigMaps...) + } + + for _, cm := range cms { volumeSource := v1.VolumeSource{ ConfigMap: &v1.ConfigMapVolumeSource{ DefaultMode: pointer.Int32(420), @@ -52,12 +57,26 @@ func (r *BackstageReconciler) appConfigsToVolumes(backstage bs.Backstage) (resul return result } -func (r *BackstageReconciler) addAppConfigsVolumeMounts(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { - if backstage.Spec.Application == nil || backstage.Spec.Application.AppConfig == nil { - return nil +func (r *BackstageReconciler) addAppConfigsVolumeMounts( + ctx context.Context, + backstage bs.Backstage, + ns string, + deployment *appsv1.Deployment, + backendAuthAppConfig *bs.ObjectKeyRef, +) error { + var ( + mountPath = _containersWorkingDir + cms []bs.ObjectKeyRef + ) + if backendAuthAppConfig != nil { + cms = append(cms, *backendAuthAppConfig) + } + if backstage.Spec.Application != nil && backstage.Spec.Application.AppConfig != nil { + cms = append(cms, backstage.Spec.Application.AppConfig.ConfigMaps...) + mountPath = backstage.Spec.Application.AppConfig.MountPath } - appConfigFilenamesList, err := r.extractAppConfigFileNames(ctx, backstage, ns) + appConfigFilenamesList, err := r.extractAppConfigFileNames(ctx, ns, cms) if err != nil { return err } @@ -69,7 +88,7 @@ func (r *BackstageReconciler) addAppConfigsVolumeMounts(ctx context.Context, bac deployment.Spec.Template.Spec.Containers[i].VolumeMounts = append(deployment.Spec.Template.Spec.Containers[i].VolumeMounts, v1.VolumeMount{ Name: appConfigFilenames.ref, - MountPath: fmt.Sprintf("%s/%s", backstage.Spec.Application.AppConfig.MountPath, f), + MountPath: fmt.Sprintf("%s/%s", mountPath, f), SubPath: f, }) } @@ -80,12 +99,26 @@ func (r *BackstageReconciler) addAppConfigsVolumeMounts(ctx context.Context, bac return nil } -func (r *BackstageReconciler) addAppConfigsContainerArgs(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { - if backstage.Spec.Application == nil || backstage.Spec.Application.AppConfig == nil { - return nil +func (r *BackstageReconciler) addAppConfigsContainerArgs( + ctx context.Context, + backstage bs.Backstage, + ns string, + deployment *appsv1.Deployment, + backendAuthAppConfig *bs.ObjectKeyRef, +) error { + var ( + mountPath = _containersWorkingDir + cms []bs.ObjectKeyRef + ) + if backendAuthAppConfig != nil { + cms = append(cms, *backendAuthAppConfig) + } + if backstage.Spec.Application != nil && backstage.Spec.Application.AppConfig != nil { + cms = append(cms, backstage.Spec.Application.AppConfig.ConfigMaps...) + mountPath = backstage.Spec.Application.AppConfig.MountPath } - appConfigFilenamesList, err := r.extractAppConfigFileNames(ctx, backstage, ns) + appConfigFilenamesList, err := r.extractAppConfigFileNames(ctx, ns, cms) if err != nil { return err } @@ -95,7 +128,7 @@ func (r *BackstageReconciler) addAppConfigsContainerArgs(ctx context.Context, ba for _, appConfigFilenames := range appConfigFilenamesList { // Args for _, fileName := range appConfigFilenames.files { - appConfigPath := fmt.Sprintf("%s/%s", backstage.Spec.Application.AppConfig.MountPath, fileName) + appConfigPath := fmt.Sprintf("%s/%s", mountPath, fileName) deployment.Spec.Template.Spec.Containers[i].Args = append(deployment.Spec.Template.Spec.Containers[i].Args, "--config", appConfigPath) } @@ -109,12 +142,8 @@ func (r *BackstageReconciler) addAppConfigsContainerArgs(ctx context.Context, ba // extractAppConfigFileNames returns a mapping of app-config object name and the list of files in it. // We intentionally do not return a Map, to preserve the iteration order of the AppConfigs in the Custom Resource, // even though we can't guarantee the iteration order of the files listed inside each ConfigMap or Secret. -func (r *BackstageReconciler) extractAppConfigFileNames(ctx context.Context, backstage bs.Backstage, ns string) (result []appConfigData, err error) { - if backstage.Spec.Application == nil || backstage.Spec.Application.AppConfig == nil { - return nil, nil - } - - for _, cmRef := range backstage.Spec.Application.AppConfig.ConfigMaps { +func (r *BackstageReconciler) extractAppConfigFileNames(ctx context.Context, ns string, cms []bs.ObjectKeyRef) (result []appConfigData, err error) { + for _, cmRef := range cms { var files []string if cmRef.Key != "" { // Limit to that file only diff --git a/controllers/backstage_backend_auth.go b/controllers/backstage_backend_auth.go index a2569c34..f785d61f 100644 --- a/controllers/backstage_backend_auth.go +++ b/controllers/backstage_backend_auth.go @@ -30,39 +30,49 @@ import ( var ( _defaultBackendAuthSecretValue = "pl4s3Ch4ng3M3" - // defaultBackstageBackendAuthSecret = ` - //apiVersion: v1 - //kind: Secret - //metadata: - // name: # placeholder for '-auth' - //data: - // # A random value will be generated for the backend-secret key - //` + _backendSecretKey = "backend-secret" ) -func (r *BackstageReconciler) handleBackendAuthSecret(ctx context.Context, backstage bs.Backstage, ns string) (secretName string, err error) { - if backstage.Spec.Application != nil && backstage.Spec.Application.BackendAuthSecret != nil { - return backstage.Spec.Application.BackendAuthSecret.Name, nil +func (r *BackstageReconciler) getBackendAuthAppConfig( + ctx context.Context, + backstage bs.Backstage, + ns string, +) (backendAuthAppConfig *bs.ObjectKeyRef, err error) { + if backstage.Spec.Application != nil && backstage.Spec.Application.AppConfig != nil { + // Users are expected to provide their own app-configs with the right backend auth secret + return nil, nil + } + + var cm v1.ConfigMap + err = r.readConfigMapOrDefault(ctx, backstage.Spec.RawRuntimeConfig.BackstageConfigName, "backend-auth-configmap.yaml", ns, &cm) + if err != nil { + return nil, fmt.Errorf("failed to read config: %s", err) + } + // Create ConfigMap + backendAuthCmName := fmt.Sprintf("%s-auth", backstage.Name) + cm.SetName(backendAuthCmName) + err = r.Get(ctx, types.NamespacedName{Name: backendAuthCmName, Namespace: ns}, &cm) + if err != nil { + if !errors.IsNotFound(err) { + return nil, fmt.Errorf("failed to get ConfigMap for backend auth (%q), reason: %s", backendAuthCmName, err) + } + err = r.Create(ctx, &cm) + if err != nil { + return nil, fmt.Errorf("failed to create ConfigMap for backend auth, reason: %s", err) + } } - //Create default Secret for backend auth var sec v1.Secret - //var isDefault bool err = r.readConfigMapOrDefault(ctx, backstage.Spec.RawRuntimeConfig.BackstageConfigName, "backend-auth-secret.yaml", ns, &sec) if err != nil { - return "", fmt.Errorf("failed to read config: %s", err) + return nil, fmt.Errorf("failed to read config: %s", err) } - //Generate a secret if it does not exist - backendAuthSecretName := fmt.Sprintf("%s-auth", backstage.Name) - sec.SetName(backendAuthSecretName) - err = r.Get(ctx, types.NamespacedName{Name: backendAuthSecretName, Namespace: ns}, &sec) + sec.SetName(backendAuthCmName) + err = r.Get(ctx, types.NamespacedName{Name: backendAuthCmName, Namespace: ns}, &sec) if err != nil { if !errors.IsNotFound(err) { - return "", fmt.Errorf("failed to get secret for backend auth (%q), reason: %s", backendAuthSecretName, err) + return nil, fmt.Errorf("failed to get Secret for backend auth (%q), reason: %s", backendAuthCmName, err) } - // there should not be any difference between default and not default - // if isDefault { - // Create a secret with a random value authVal := func(length int) string { bytes := make([]byte, length) if _, randErr := rand.Read(bytes); randErr != nil { @@ -71,57 +81,57 @@ func (r *BackstageReconciler) handleBackendAuthSecret(ctx context.Context, backs } return base64.StdEncoding.EncodeToString(bytes) }(24) - sec.Data = map[string][]byte{ - "backend-secret": []byte(authVal), + sec.StringData = map[string]string{ + _backendSecretKey: authVal, } - // } err = r.Create(ctx, &sec) if err != nil { - return "", fmt.Errorf("failed to create secret for backend auth, reason: %s", err) + return nil, fmt.Errorf("failed to create Secret for backend auth, reason: %s", err) } } - return backendAuthSecretName, nil + + return &bs.ObjectKeyRef{Name: backendAuthCmName}, nil } func (r *BackstageReconciler) addBackendAuthEnvVar(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { - backendAuthSecretName, err := r.handleBackendAuthSecret(ctx, backstage, ns) + backendAuthAppConfig, err := r.getBackendAuthAppConfig(ctx, backstage, ns) if err != nil { return err } - - if backendAuthSecretName == "" { + if backendAuthAppConfig == nil { return nil } + hasEnvVar := func(c v1.Container, name string) bool { + for _, envVar := range c.Env { + if envVar.Name == name { + return true + } + } + return false + } + for i, c := range deployment.Spec.Template.Spec.Containers { if c.Name == _defaultBackstageMainContainerName { - var k string - if backstage.Spec.Application != nil && backstage.Spec.Application.BackendAuthSecret != nil { - k = backstage.Spec.Application.BackendAuthSecret.Key - } - if k == "" { - //TODO(rm3l): why kubebuilder default values do not work - k = "backend-secret" - } - deployment.Spec.Template.Spec.Containers[i].Env = append(deployment.Spec.Template.Spec.Containers[i].Env, - v1.EnvVar{ - Name: "BACKEND_SECRET", - ValueFrom: &v1.EnvVarSource{ - SecretKeyRef: &v1.SecretKeySelector{ - LocalObjectReference: v1.LocalObjectReference{ - Name: backendAuthSecretName, + if !hasEnvVar(c, "BACKEND_SECRET") { + deployment.Spec.Template.Spec.Containers[i].Env = append(deployment.Spec.Template.Spec.Containers[i].Env, + v1.EnvVar{ + Name: "BACKEND_SECRET", + ValueFrom: &v1.EnvVarSource{ + SecretKeyRef: &v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{ + // Secret has the same name as the backend auth ConfigMap + Name: backendAuthAppConfig.Name, + }, + Key: _backendSecretKey, + Optional: pointer.Bool(false), }, - Key: k, - Optional: pointer.Bool(false), }, - }, - }, - v1.EnvVar{ - Name: "APP_CONFIG_backend_auth_keys", - Value: `[{"secret": "$(BACKEND_SECRET)"}]`, - }) + }) + } break } } + return nil } diff --git a/controllers/backstage_deployment.go b/controllers/backstage_deployment.go index e27dc87b..c3278c59 100644 --- a/controllers/backstage_deployment.go +++ b/controllers/backstage_deployment.go @@ -233,7 +233,12 @@ func (r *BackstageReconciler) addVolumes(ctx context.Context, backstage bs.Backs deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, *dpConfVol) } - deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, r.appConfigsToVolumes(backstage)...) + backendAuthAppConfig, err := r.getBackendAuthAppConfig(ctx, backstage, ns) + if err != nil { + return err + } + + deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, r.appConfigsToVolumes(backstage, backendAuthAppConfig)...) deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, r.extraFilesToVolumes(backstage)...) return nil } @@ -243,7 +248,11 @@ func (r *BackstageReconciler) addVolumeMounts(ctx context.Context, backstage bs. if err != nil { return err } - err = r.addAppConfigsVolumeMounts(ctx, backstage, ns, deployment) + backendAuthAppConfig, err := r.getBackendAuthAppConfig(ctx, backstage, ns) + if err != nil { + return err + } + err = r.addAppConfigsVolumeMounts(ctx, backstage, ns, deployment, backendAuthAppConfig) if err != nil { return err } @@ -251,7 +260,11 @@ func (r *BackstageReconciler) addVolumeMounts(ctx context.Context, backstage bs. } func (r *BackstageReconciler) addContainerArgs(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { - return r.addAppConfigsContainerArgs(ctx, backstage, ns, deployment) + backendAuthAppConfig, err := r.getBackendAuthAppConfig(ctx, backstage, ns) + if err != nil { + return err + } + return r.addAppConfigsContainerArgs(ctx, backstage, ns, deployment, backendAuthAppConfig) } func (r *BackstageReconciler) addEnvVars(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { diff --git a/examples/janus-cr-with-app-configs.yaml b/examples/janus-cr-with-app-configs.yaml index f53961b1..dc08fd30 100644 --- a/examples/janus-cr-with-app-configs.yaml +++ b/examples/janus-cr-with-app-configs.yaml @@ -10,6 +10,7 @@ spec: configMaps: - name: "my-backstage-config-cm1" - name: "my-backstage-config-cm2" + key: "app-config1-cm2.gh.yaml" dynamicPluginsConfigMapName: "my-dynamic-plugins-config-cm" extraEnvs: envs: From 7d63241da0bd6547dee4cedb2c2b2f7ad85ea608 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 11 Dec 2023 09:59:35 +0100 Subject: [PATCH 24/31] Drop backendAuthSecret field in CRD This sounds too low-level and might sound confusing. Instead, if no AppConfig, ExtraFile or ExtraEnvVar is set, we would create a default app-config with a default value for this backend auth secret. Otherwise, we would make it clear in the doc that users need to additional define this in their own app-config or extra-env vars. --- api/v1alpha1/backstage_types.go | 9 - .../default-config/backend-auth-secret.yaml | 6 - config/manager/kustomization.yaml | 2 +- controllers/backstage_backend_auth.go | 94 ++------ controllers/backstage_controller_test.go | 201 +++--------------- controllers/backstage_deployment.go | 9 - controllers/backstage_dynamic_plugins.go | 9 + controllers/backstage_extra_envs.go | 86 ++++---- 8 files changed, 91 insertions(+), 325 deletions(-) delete mode 100644 config/manager/default-config/backend-auth-secret.yaml diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index 37207bca..9054bc58 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -53,15 +53,6 @@ type Application struct { // +optional AppConfig *AppConfig `json:"appConfig,omitempty"` - //// Optional Reference to a Secret to use for Backend Auth. A new one will be generated if not set. - //// This Secret is used to set an environment variable named 'APP_CONFIG_backend_auth_keys' in the - //// main container, which takes precedence over any 'backend.auth.keys' field defined - //// in default or custom application configuration files. - //// This is required for service-to-service auth and is shared by all backend plugins. - //// Default value for the key in the secret is 'backend-secret. - //// +optional - //BackendAuthSecret *ObjectKeyRef `json:"backendAuthSecret,omitempty"` - // Reference to an existing ConfigMap for Dynamic Plugins. // A new one will be generated with the default config if not set. // The ConfigMap object must have an existing key named: 'dynamic-plugins.yaml'. diff --git a/config/manager/default-config/backend-auth-secret.yaml b/config/manager/default-config/backend-auth-secret.yaml deleted file mode 100644 index b7f673b2..00000000 --- a/config/manager/default-config/backend-auth-secret.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: # placeholder for '-auth' -data: - backend-secret: # placeholder for random value that will be generated for the backend-secret key \ No newline at end of file diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 3dd75881..627bca60 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -17,6 +17,6 @@ configMapGenerator: - default-config/db-statefulset.yaml - default-config/db-service.yaml - default-config/db-service-hl.yaml - - default-config/backend-auth-secret.yaml + - default-config/backend-auth-configmap.yaml - default-config/dynamic-plugins-configmap.yaml name: default-config diff --git a/controllers/backstage_backend_auth.go b/controllers/backstage_backend_auth.go index f785d61f..d90b1990 100644 --- a/controllers/backstage_backend_auth.go +++ b/controllers/backstage_backend_auth.go @@ -16,21 +16,13 @@ package controller import ( "context" - "crypto/rand" - "encoding/base64" "fmt" bs "janus-idp.io/backstage-operator/api/v1alpha1" - appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" - "k8s.io/utils/pointer" -) - -var ( - _defaultBackendAuthSecretValue = "pl4s3Ch4ng3M3" - _backendSecretKey = "backend-secret" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) func (r *BackstageReconciler) getBackendAuthAppConfig( @@ -38,8 +30,9 @@ func (r *BackstageReconciler) getBackendAuthAppConfig( backstage bs.Backstage, ns string, ) (backendAuthAppConfig *bs.ObjectKeyRef, err error) { - if backstage.Spec.Application != nil && backstage.Spec.Application.AppConfig != nil { - // Users are expected to provide their own app-configs with the right backend auth secret + if backstage.Spec.Application != nil && + (backstage.Spec.Application.AppConfig != nil || backstage.Spec.Application.ExtraFiles != nil || backstage.Spec.Application.ExtraEnvs != nil) { + // Users are expected to fill their app-config(s) with their own backend auth key return nil, nil } @@ -49,89 +42,26 @@ func (r *BackstageReconciler) getBackendAuthAppConfig( return nil, fmt.Errorf("failed to read config: %s", err) } // Create ConfigMap - backendAuthCmName := fmt.Sprintf("%s-auth", backstage.Name) + backendAuthCmName := fmt.Sprintf("%s-auth-app-config", backstage.Name) cm.SetName(backendAuthCmName) err = r.Get(ctx, types.NamespacedName{Name: backendAuthCmName, Namespace: ns}, &cm) if err != nil { if !errors.IsNotFound(err) { return nil, fmt.Errorf("failed to get ConfigMap for backend auth (%q), reason: %s", backendAuthCmName, err) } - err = r.Create(ctx, &cm) - if err != nil { - return nil, fmt.Errorf("failed to create ConfigMap for backend auth, reason: %s", err) - } - } + setBackstageAppLabel(&cm.ObjectMeta.Labels, backstage) + r.labels(&cm.ObjectMeta, backstage) - var sec v1.Secret - err = r.readConfigMapOrDefault(ctx, backstage.Spec.RawRuntimeConfig.BackstageConfigName, "backend-auth-secret.yaml", ns, &sec) - if err != nil { - return nil, fmt.Errorf("failed to read config: %s", err) - } - sec.SetName(backendAuthCmName) - err = r.Get(ctx, types.NamespacedName{Name: backendAuthCmName, Namespace: ns}, &sec) - if err != nil { - if !errors.IsNotFound(err) { - return nil, fmt.Errorf("failed to get Secret for backend auth (%q), reason: %s", backendAuthCmName, err) - } - authVal := func(length int) string { - bytes := make([]byte, length) - if _, randErr := rand.Read(bytes); randErr != nil { - // Do not fail, but use a fallback value - return _defaultBackendAuthSecretValue + if r.OwnsRuntime { + if err = controllerutil.SetControllerReference(&backstage, &cm, r.Scheme); err != nil { + return nil, fmt.Errorf("failed to set owner reference: %s", err) } - return base64.StdEncoding.EncodeToString(bytes) - }(24) - sec.StringData = map[string]string{ - _backendSecretKey: authVal, } - err = r.Create(ctx, &sec) + err = r.Create(ctx, &cm) if err != nil { - return nil, fmt.Errorf("failed to create Secret for backend auth, reason: %s", err) + return nil, fmt.Errorf("failed to create ConfigMap for backend auth, reason: %s", err) } } return &bs.ObjectKeyRef{Name: backendAuthCmName}, nil } - -func (r *BackstageReconciler) addBackendAuthEnvVar(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { - backendAuthAppConfig, err := r.getBackendAuthAppConfig(ctx, backstage, ns) - if err != nil { - return err - } - if backendAuthAppConfig == nil { - return nil - } - - hasEnvVar := func(c v1.Container, name string) bool { - for _, envVar := range c.Env { - if envVar.Name == name { - return true - } - } - return false - } - - for i, c := range deployment.Spec.Template.Spec.Containers { - if c.Name == _defaultBackstageMainContainerName { - if !hasEnvVar(c, "BACKEND_SECRET") { - deployment.Spec.Template.Spec.Containers[i].Env = append(deployment.Spec.Template.Spec.Containers[i].Env, - v1.EnvVar{ - Name: "BACKEND_SECRET", - ValueFrom: &v1.EnvVarSource{ - SecretKeyRef: &v1.SecretKeySelector{ - LocalObjectReference: v1.LocalObjectReference{ - // Secret has the same name as the backend auth ConfigMap - Name: backendAuthAppConfig.Name, - }, - Key: _backendSecretKey, - Optional: pointer.Bool(false), - }, - }, - }) - } - break - } - } - - return nil -} diff --git a/controllers/backstage_controller_test.go b/controllers/backstage_controller_test.go index cb9fd728..0ec8c852 100644 --- a/controllers/backstage_controller_test.go +++ b/controllers/backstage_controller_test.go @@ -195,16 +195,15 @@ var _ = Describe("Backstage controller", func() { g.Expect(err).ShouldNot(HaveOccurred()) }, time.Minute, time.Second).Should(Succeed()) - By("Generating a value for backend auth secret key") - Eventually(func(g Gomega) { - found := &corev1.Secret{} - err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: backstageName + "-auth"}, found) - g.Expect(err).ShouldNot(HaveOccurred()) - - g.Expect(found.Data).To(HaveKey("backend-secret")) - g.Expect(found.Data["backend-secret"]).To(Not(BeEmpty()), - "backend auth secret should contain a non-empty 'backend-secret' in its data") - }, time.Minute, time.Second).Should(Succeed()) + backendAuthConfigName := fmt.Sprintf("%s-auth-app-config", backstageName) + By("Creating a ConfigMap for default backend auth key", func() { + Eventually(func(g Gomega) { + found := &corev1.ConfigMap{} + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: backendAuthConfigName}, found) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(found.Data).ToNot(BeEmpty(), "backend auth secret should contain non-empty data") + }, time.Minute, time.Second).Should(Succeed()) + }) By("Generating a ConfigMap for default config for dynamic plugins") dynamicPluginsConfigName := fmt.Sprintf("%s-dynamic-plugins", backstageName) @@ -228,22 +227,8 @@ var _ = Describe("Backstage controller", func() { By("checking the number of replicas") Expect(found.Spec.Replicas).To(HaveValue(BeEquivalentTo(1))) - By("Checking that the Deployment is configured with a random backend auth secret") - backendSecretEnvVar, ok := findEnvVar(found.Spec.Template.Spec.Containers[0].Env, "BACKEND_SECRET") - Expect(ok).To(BeTrue(), "env var BACKEND_SECRET not found in main container") - Expect(backendSecretEnvVar.ValueFrom.SecretKeyRef.Name).To( - Not(BeEmpty()), "'name' for backend auth secret ref should not be empty") - Expect(backendSecretEnvVar.ValueFrom.SecretKeyRef.Key).To( - Equal("backend-secret"), "Unexpected secret key ref for backend secret") - Expect(backendSecretEnvVar.ValueFrom.SecretKeyRef.Optional).To(HaveValue(BeFalse()), - "'optional' for backend auth secret ref should be 'false'") - - backendAuthAppConfigEnvVar, ok := findEnvVar(found.Spec.Template.Spec.Containers[0].Env, "APP_CONFIG_backend_auth_keys") - Expect(ok).To(BeTrue(), "env var APP_CONFIG_backend_auth_keys not found in main container") - Expect(backendAuthAppConfigEnvVar.Value).To(Equal(`[{"secret": "$(BACKEND_SECRET)"}]`)) - By("Checking the Volumes in the Backstage Deployment", func() { - Expect(found.Spec.Template.Spec.Volumes).To(HaveLen(3)) + Expect(found.Spec.Template.Spec.Volumes).To(HaveLen(4)) _, ok := findVolume(found.Spec.Template.Spec.Volumes, "dynamic-plugins-root") Expect(ok).To(BeTrue(), "No volume found with name: dynamic-plugins-root") @@ -256,6 +241,12 @@ var _ = Describe("Backstage controller", func() { Expect(dynamicPluginsConfigVol.VolumeSource.Secret).To(BeNil()) Expect(dynamicPluginsConfigVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) Expect(dynamicPluginsConfigVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(dynamicPluginsConfigName)) + + backendAuthAppConfigVol, ok := findVolume(found.Spec.Template.Spec.Volumes, backendAuthConfigName) + Expect(ok).To(BeTrue(), "No volume found with name: %s", backendAuthConfigName) + Expect(backendAuthAppConfigVol.VolumeSource.Secret).To(BeNil()) + Expect(backendAuthAppConfigVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) + Expect(backendAuthAppConfigVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(backendAuthConfigName)) }) By("Checking the Number of init containers in the Backstage Deployment") @@ -295,18 +286,25 @@ var _ = Describe("Backstage controller", func() { mainCont := found.Spec.Template.Spec.Containers[0] By("Checking the main container Args in the Backstage Deployment", func() { - Expect(mainCont.Args).To(HaveLen(2)) + Expect(mainCont.Args).To(HaveLen(4)) Expect(mainCont.Args[0]).To(Equal("--config")) Expect(mainCont.Args[1]).To(Equal("dynamic-plugins-root/app-config.dynamic-plugins.yaml")) + Expect(mainCont.Args[2]).To(Equal("--config")) + Expect(mainCont.Args[3]).To(Equal("/opt/app-root/src/app-config.backend-auth.default.yaml")) }) By("Checking the main container Volume Mounts in the Backstage Deployment", func() { - Expect(mainCont.VolumeMounts).To(HaveLen(1)) + Expect(mainCont.VolumeMounts).To(HaveLen(2)) dpRoot := findVolumeMounts(mainCont.VolumeMounts, "dynamic-plugins-root") Expect(dpRoot).To(HaveLen(1), "No volume mount found with name: dynamic-plugins-root") Expect(dpRoot[0].MountPath).To(Equal("/opt/app-root/src/dynamic-plugins-root")) Expect(dpRoot[0].SubPath).To(BeEmpty()) + + bsAuth := findVolumeMounts(mainCont.VolumeMounts, backendAuthConfigName) + Expect(bsAuth).To(HaveLen(1), "No volume mount found with name: %s", backendAuthConfigName) + Expect(bsAuth[0].MountPath).To(Equal("/opt/app-root/src/app-config.backend-auth.default.yaml")) + Expect(bsAuth[0].SubPath).To(Equal("app-config.backend-auth.default.yaml")) }) By("Checking the latest Status added to the Backstage instance") @@ -705,155 +703,6 @@ plugins: [] } }) - Context("Backend Auth Secret", func() { - for _, key := range []string{"", "some-key"} { - key := key - When("creating CR with a non existing backend secret ref and key="+key, func() { - var backstage *bsv1alpha1.Backstage - BeforeEach(func() { - backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - Application: &bsv1alpha1.Application{ - BackendAuthSecret: &bsv1alpha1.ObjectKeyRef{ - Name: "non-existing-secret", - Key: key, - }, - }, - }) - err := k8sClient.Create(ctx, backstage) - Expect(err).To(Not(HaveOccurred())) - }) - - It("should reconcile", func() { - By("Checking if the custom resource was successfully created") - Eventually(func() error { - found := &bsv1alpha1.Backstage{} - return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) - }, time.Minute, time.Second).Should(Succeed()) - - By("Reconciling the custom resource created") - _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, - }) - Expect(err).To(Not(HaveOccurred())) - - By("Not generating a value for backend auth secret key") - Consistently(func(g Gomega) { - found := &corev1.Secret{} - err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: backstageName + "-auth"}, found) - g.Expect(err).Should(HaveOccurred()) - g.Expect(errors.IsNotFound(err)).To(BeTrue(), - fmt.Sprintf("error must be a not-found error, but is %v", err)) - }, 5*time.Second, time.Second).Should(Succeed()) - - By("Checking that the Deployment was successfully created in the reconciliation") - found := &appsv1.Deployment{} - Eventually(func() error { - // TODO to get name from default - return k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, found) - }, time.Minute, time.Second).Should(Succeed()) - - By("Checking that the Deployment is configured with the specified secret", func() { - expectedKey := key - if key == "" { - expectedKey = "backend-secret" - } - backendSecretEnvVar, ok := findEnvVar(found.Spec.Template.Spec.Containers[0].Env, "BACKEND_SECRET") - Expect(ok).To(BeTrue(), "env var BACKEND_SECRET not found in main container") - Expect(backendSecretEnvVar.ValueFrom.SecretKeyRef.Name).To( - Equal("non-existing-secret"), "'name' for backend auth secret ref should not be empty") - Expect(backendSecretEnvVar.ValueFrom.SecretKeyRef.Key).To( - Equal(expectedKey), "Unexpected secret key ref for backend secret") - Expect(backendSecretEnvVar.ValueFrom.SecretKeyRef.Optional).To(HaveValue(BeFalse()), - "'optional' for backend auth secret ref should be 'false'") - - backendAuthAppConfigEnvVar, ok := findEnvVar(found.Spec.Template.Spec.Containers[0].Env, "APP_CONFIG_backend_auth_keys") - Expect(ok).To(BeTrue(), "env var APP_CONFIG_backend_auth_keys not found in main container") - Expect(backendAuthAppConfigEnvVar.Value).To(Equal(`[{"secret": "$(BACKEND_SECRET)"}]`)) - }) - - By("Checking the latest Status added to the Backstage instance") - verifyBackstageInstance(ctx) - }) - }) - - When("creating CR with an existing backend secret ref and key="+key, func() { - const backendAuthSecretName = "my-backend-auth-secret" - var backstage *bsv1alpha1.Backstage - - BeforeEach(func() { - d := make(map[string][]byte) - if key != "" { - d[key] = []byte("lorem-ipsum-dolor-sit-amet") - } - backendAuthSecret := buildSecret(backendAuthSecretName, d) - err := k8sClient.Create(ctx, backendAuthSecret) - Expect(err).To(Not(HaveOccurred())) - backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - Application: &bsv1alpha1.Application{ - BackendAuthSecret: &bsv1alpha1.ObjectKeyRef{ - Name: backendAuthSecretName, - Key: key, - }, - }, - }) - err = k8sClient.Create(ctx, backstage) - Expect(err).To(Not(HaveOccurred())) - }) - - It("should reconcile", func() { - By("Checking if the custom resource was successfully created") - Eventually(func() error { - found := &bsv1alpha1.Backstage{} - return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) - }, time.Minute, time.Second).Should(Succeed()) - - By("Reconciling the custom resource created") - _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, - }) - Expect(err).To(Not(HaveOccurred())) - - By("Not generating a value for backend auth secret key") - Consistently(func(g Gomega) { - found := &corev1.Secret{} - err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: backstageName + "-auth"}, found) - g.Expect(err).Should(HaveOccurred()) - g.Expect(errors.IsNotFound(err)).To(BeTrue(), - fmt.Sprintf("error must be a not-found error, but is %v", err)) - }, 5*time.Second, time.Second).Should(Succeed()) - - By("Checking that the Deployment was successfully created in the reconciliation") - found := &appsv1.Deployment{} - Eventually(func() error { - // TODO to get name from default - return k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, found) - }, time.Minute, time.Second).Should(Succeed()) - - By("Checking that the Deployment is configured with the specified secret", func() { - expectedKey := key - if key == "" { - expectedKey = "backend-secret" - } - backendSecretEnvVar, ok := findEnvVar(found.Spec.Template.Spec.Containers[0].Env, "BACKEND_SECRET") - Expect(ok).To(BeTrue(), "env var BACKEND_SECRET not found in main container") - Expect(backendSecretEnvVar.ValueFrom.SecretKeyRef.Name).To(Equal(backendAuthSecretName)) - Expect(backendSecretEnvVar.ValueFrom.SecretKeyRef.Key).To( - Equal(expectedKey), "Unexpected secret key ref for backend secret") - Expect(backendSecretEnvVar.ValueFrom.SecretKeyRef.Optional).To(HaveValue(BeFalse()), - "'optional' for backend auth secret ref should be 'false'") - - backendAuthAppConfigEnvVar, ok := findEnvVar(found.Spec.Template.Spec.Containers[0].Env, "APP_CONFIG_backend_auth_keys") - Expect(ok).To(BeTrue(), "env var APP_CONFIG_backend_auth_keys not found in main container") - Expect(backendAuthAppConfigEnvVar.Value).To(Equal(`[{"secret": "$(BACKEND_SECRET)"}]`)) - }) - - By("Checking the latest Status added to the Backstage instance") - verifyBackstageInstance(ctx) - }) - }) - } - }) - Context("Extra Files", func() { for _, kind := range []string{"ConfigMap", "Secret"} { kind := kind diff --git a/controllers/backstage_deployment.go b/controllers/backstage_deployment.go index c3278c59..3536b74f 100644 --- a/controllers/backstage_deployment.go +++ b/controllers/backstage_deployment.go @@ -268,15 +268,6 @@ func (r *BackstageReconciler) addContainerArgs(ctx context.Context, backstage bs } func (r *BackstageReconciler) addEnvVars(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { - err := r.addBackendAuthEnvVar(ctx, backstage, ns, deployment) - if err != nil { - return err - } - if backstage.Spec.Application == nil { - return nil - } - r.addExtraEnvs(backstage, deployment) - return nil } diff --git a/controllers/backstage_dynamic_plugins.go b/controllers/backstage_dynamic_plugins.go index 4f9be317..293e14ae 100644 --- a/controllers/backstage_dynamic_plugins.go +++ b/controllers/backstage_dynamic_plugins.go @@ -24,6 +24,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) //var ( @@ -59,6 +60,14 @@ func (r *BackstageReconciler) getOrGenerateDynamicPluginsConf(ctx context.Contex if !errors.IsNotFound(err) { return "", fmt.Errorf("failed to get config map for dynamic plugins (%q), reason: %s", dpConfigName, err) } + setBackstageAppLabel(&cm.ObjectMeta.Labels, backstage) + r.labels(&cm.ObjectMeta, backstage) + + if r.OwnsRuntime { + if err = controllerutil.SetControllerReference(&backstage, &cm, r.Scheme); err != nil { + return "", fmt.Errorf("failed to set owner reference: %s", err) + } + } err = r.Create(ctx, &cm) if err != nil { return "", fmt.Errorf("failed to create config map for dynamic plugins, reason: %s", err) diff --git a/controllers/backstage_extra_envs.go b/controllers/backstage_extra_envs.go index ca62bb45..2912e754 100644 --- a/controllers/backstage_extra_envs.go +++ b/controllers/backstage_extra_envs.go @@ -7,57 +7,59 @@ import ( ) func (r *BackstageReconciler) addExtraEnvs(backstage bs.Backstage, deployment *appsv1.Deployment) { - if backstage.Spec.Application.ExtraEnvs != nil { - for _, env := range backstage.Spec.Application.ExtraEnvs.Envs { - for i := range deployment.Spec.Template.Spec.Containers { - deployment.Spec.Template.Spec.Containers[i].Env = append(deployment.Spec.Template.Spec.Containers[i].Env, v1.EnvVar{ - Name: env.Name, - Value: env.Value, - }) - } + if backstage.Spec.Application == nil || backstage.Spec.Application.ExtraEnvs == nil { + return + } + + for _, env := range backstage.Spec.Application.ExtraEnvs.Envs { + for i := range deployment.Spec.Template.Spec.Containers { + deployment.Spec.Template.Spec.Containers[i].Env = append(deployment.Spec.Template.Spec.Containers[i].Env, v1.EnvVar{ + Name: env.Name, + Value: env.Value, + }) } + } - for _, cmRef := range backstage.Spec.Application.ExtraEnvs.ConfigMaps { - for i := range deployment.Spec.Template.Spec.Containers { - if cmRef.Key != "" { - deployment.Spec.Template.Spec.Containers[i].Env = append(deployment.Spec.Template.Spec.Containers[i].Env, v1.EnvVar{ - Name: cmRef.Key, - ValueFrom: &v1.EnvVarSource{ - ConfigMapKeyRef: &v1.ConfigMapKeySelector{ - LocalObjectReference: v1.LocalObjectReference{Name: cmRef.Name}, - Key: cmRef.Key, - }, - }, - }) - } else { - deployment.Spec.Template.Spec.Containers[i].EnvFrom = append(deployment.Spec.Template.Spec.Containers[i].EnvFrom, v1.EnvFromSource{ - ConfigMapRef: &v1.ConfigMapEnvSource{ + for _, cmRef := range backstage.Spec.Application.ExtraEnvs.ConfigMaps { + for i := range deployment.Spec.Template.Spec.Containers { + if cmRef.Key != "" { + deployment.Spec.Template.Spec.Containers[i].Env = append(deployment.Spec.Template.Spec.Containers[i].Env, v1.EnvVar{ + Name: cmRef.Key, + ValueFrom: &v1.EnvVarSource{ + ConfigMapKeyRef: &v1.ConfigMapKeySelector{ LocalObjectReference: v1.LocalObjectReference{Name: cmRef.Name}, + Key: cmRef.Key, }, - }) - } + }, + }) + } else { + deployment.Spec.Template.Spec.Containers[i].EnvFrom = append(deployment.Spec.Template.Spec.Containers[i].EnvFrom, v1.EnvFromSource{ + ConfigMapRef: &v1.ConfigMapEnvSource{ + LocalObjectReference: v1.LocalObjectReference{Name: cmRef.Name}, + }, + }) } } + } - for _, secRef := range backstage.Spec.Application.ExtraEnvs.Secrets { - for i := range deployment.Spec.Template.Spec.Containers { - if secRef.Key != "" { - deployment.Spec.Template.Spec.Containers[i].Env = append(deployment.Spec.Template.Spec.Containers[i].Env, v1.EnvVar{ - Name: secRef.Key, - ValueFrom: &v1.EnvVarSource{ - SecretKeyRef: &v1.SecretKeySelector{ - LocalObjectReference: v1.LocalObjectReference{Name: secRef.Name}, - Key: secRef.Key, - }, - }, - }) - } else { - deployment.Spec.Template.Spec.Containers[i].EnvFrom = append(deployment.Spec.Template.Spec.Containers[i].EnvFrom, v1.EnvFromSource{ - SecretRef: &v1.SecretEnvSource{ + for _, secRef := range backstage.Spec.Application.ExtraEnvs.Secrets { + for i := range deployment.Spec.Template.Spec.Containers { + if secRef.Key != "" { + deployment.Spec.Template.Spec.Containers[i].Env = append(deployment.Spec.Template.Spec.Containers[i].Env, v1.EnvVar{ + Name: secRef.Key, + ValueFrom: &v1.EnvVarSource{ + SecretKeyRef: &v1.SecretKeySelector{ LocalObjectReference: v1.LocalObjectReference{Name: secRef.Name}, + Key: secRef.Key, }, - }) - } + }, + }) + } else { + deployment.Spec.Template.Spec.Containers[i].EnvFrom = append(deployment.Spec.Template.Spec.Containers[i].EnvFrom, v1.EnvFromSource{ + SecretRef: &v1.SecretEnvSource{ + LocalObjectReference: v1.LocalObjectReference{Name: secRef.Name}, + }, + }) } } } From 6e879a4ccbc431b48a3780c411f5d009a7a6ad05 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 11 Dec 2023 09:59:54 +0100 Subject: [PATCH 25/31] Update example CR with app-configs --- examples/janus-cr-with-app-configs.yaml | 83 ++++++++++++++----------- 1 file changed, 48 insertions(+), 35 deletions(-) diff --git a/examples/janus-cr-with-app-configs.yaml b/examples/janus-cr-with-app-configs.yaml index dc08fd30..b947cf4f 100644 --- a/examples/janus-cr-with-app-configs.yaml +++ b/examples/janus-cr-with-app-configs.yaml @@ -5,17 +5,25 @@ metadata: spec: skipLocalDb: false application: + replicas: 2 appConfig: #mountPath: /opt/app-root/src configMaps: + - name: "my-backstage-config-backend-auth" - name: "my-backstage-config-cm1" - name: "my-backstage-config-cm2" key: "app-config1-cm2.gh.yaml" dynamicPluginsConfigMapName: "my-dynamic-plugins-config-cm" + extraFiles: + mountPath: /tmp/my-extra-files + configMaps: + - name: "my-backstage-extra-files-cm1" + secrets: + - name: "my-backstage-extra-files-secret1" extraEnvs: envs: - - name: MY_ENV_VAR_1 - value: my-value-1 + - name: GITHUB_ORG + value: 'my-gh-org' - name: MY_ENV_VAR_2 value: my-value-2 configMaps: @@ -23,30 +31,31 @@ spec: - name: my-env-cm-11 key: CM_ENV11 secrets: - - name: my-env-secret1 - - name: my-env-secret11 - key: SEC_ENV11 - backendAuthSecret: - name: "my-backstage-backend-auth-secret" - key: "my-auth-key" - replicas: 2 - extraFiles: - mountPath: /tmp/my-extra-files - configMaps: - - name: "my-backstage-extra-config-cm1" - secrets: - - name: "my-backstage-extra-config-secret1" + - name: "my-backstage-backend-auth-secret" + key: BACKEND_SECRET + - name: my-gh-auth-secret + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-backstage-config-backend-auth +data: + "app-config.backend-auth.yaml": | + backend: + auth: + keys: + - secret: "${BACKEND_SECRET}" --- apiVersion: v1 kind: Secret metadata: name: my-backstage-backend-auth-secret -data: +stringData: # generated with the command below (from https://janus-idp.io/docs/auth/service-to-service-auth/#setup): # node -p 'require("crypto").randomBytes(24).toString("base64")' - backend-secret: TDRORDFRa2JxaFJhNTBzOGFDc1FWUEJ4ekFtRUw4UEU= - my-auth-key: TDRORDFRa2JxaFJhNTBzOGFDc1FWUEJ4ekFtRUw4UEU= + BACKEND_SECRET: "R2FxRVNrcmwzYzhhN3l0V1VRcnQ3L1pLT09WaVhDNUEK" --- apiVersion: v1 @@ -65,7 +74,6 @@ data: app-config3-cm1.odo.yaml: | catalog: locations: - # [...] - type: url target: https://github.com/ododev/odo-backstage-software-template/blob/main/template.yaml rules: @@ -84,8 +92,8 @@ data: providers: github: development: - clientId: '${MY_ENV_VAR_1}' - clientSecret: '${MY_ENV_VAR_2}' + clientId: '${GH_CLIENT_ID}' + clientSecret: '${GH_CLIENT_SECRET}' app-config2-cm2.yaml: | # a comment @@ -99,6 +107,20 @@ data: includes: - dynamic-plugins.default.yaml plugins: + - package: './dynamic-plugins/dist/backstage-plugin-catalog-backend-module-github-dynamic' + disabled: false + pluginConfig: + catalog: + providers: + github: + myorg: + organization: '${GITHUB_ORG}' + schedule: + # supports cron, ISO duration, "human duration" (used below) + frequency: { minutes: 30} + # supports ISO duration, "human duration (used below) + timeout: { minutes: 3} + initialDelay: { seconds: 15} - package: '@dfatwork-pkgs/scaffolder-backend-module-http-request-wrapped-dynamic@4.0.9-0' integrity: 'sha512-+YYESzHdg1hsk2XN+zrtXPnsQnfbzmWIvcOM0oQLS4hf8F4iGTtOXKjWnZsR/14/khGsPrzy0oq1ytJ1/4ORkQ==' - package: '@dfatwork-pkgs/explore-backend-wrapped-dynamic@0.0.9-next.11' @@ -131,25 +153,16 @@ data: apiVersion: v1 kind: Secret metadata: - name: my-env-secret1 -stringData: - SEC_ENV1: "secret env 1" - SEC_ENV2: "secret env 2" - ---- -apiVersion: v1 -kind: Secret -metadata: - name: my-env-secret11 + name: my-gh-auth-secret stringData: - SEC_ENV11: "secret env 11" - SEC_ENV12: "secret env 12" + GH_CLIENT_ID: "my GH client ID" + GH_CLIENT_SECRET: "my GH client secret" --- apiVersion: v1 kind: ConfigMap metadata: - name: my-backstage-extra-config-cm1 + name: my-backstage-extra-files-cm1 data: cm_file1.txt: | # From ConfigMap @@ -163,7 +176,7 @@ data: apiVersion: v1 kind: Secret metadata: - name: my-backstage-extra-config-secret1 + name: my-backstage-extra-files-secret1 stringData: secret_file1.txt: | # From Secret From b76cee1f6d6421ad32a8cb1461404e660d4b0dc0 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 11 Dec 2023 10:02:13 +0100 Subject: [PATCH 26/31] Update bundle manifests --- ...backstage-default-config_v1_configmap.yaml | 13 ++++++++---- ...kstage-operator.clusterserviceversion.yaml | 2 +- bundle/manifests/janus-idp.io_backstages.yaml | 20 ------------------- 3 files changed, 10 insertions(+), 25 deletions(-) diff --git a/bundle/manifests/backstage-default-config_v1_configmap.yaml b/bundle/manifests/backstage-default-config_v1_configmap.yaml index f1ef5a24..c40fdb63 100644 --- a/bundle/manifests/backstage-default-config_v1_configmap.yaml +++ b/bundle/manifests/backstage-default-config_v1_configmap.yaml @@ -1,12 +1,17 @@ apiVersion: v1 data: - backend-auth-secret.yaml: | + backend-auth-configmap.yaml: | apiVersion: v1 - kind: Secret + kind: ConfigMap metadata: - name: # placeholder for '-auth' + name: # placeholder for '-backend-auth' data: - # A random value will be generated for the backend-secret key + "app-config.backend-auth.default.yaml": | + backend: + auth: + keys: + # This is a default value, which you should change by providing your own app-config + - secret: "pl4s3Ch4ng3M3" db-service-hl.yaml: |- apiVersion: v1 kind: Service diff --git a/bundle/manifests/backstage-operator.clusterserviceversion.yaml b/bundle/manifests/backstage-operator.clusterserviceversion.yaml index a151665b..11fe7f85 100644 --- a/bundle/manifests/backstage-operator.clusterserviceversion.yaml +++ b/bundle/manifests/backstage-operator.clusterserviceversion.yaml @@ -21,7 +21,7 @@ metadata: } ] capabilities: Basic Install - createdAt: "2023-12-08T17:17:14Z" + createdAt: "2023-12-11T09:00:15Z" operators.operatorframework.io/builder: operator-sdk-v1.32.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 name: backstage-operator.v0.0.1 diff --git a/bundle/manifests/janus-idp.io_backstages.yaml b/bundle/manifests/janus-idp.io_backstages.yaml index 6691ceaf..a2478e17 100644 --- a/bundle/manifests/janus-idp.io_backstages.yaml +++ b/bundle/manifests/janus-idp.io_backstages.yaml @@ -81,26 +81,6 @@ spec: the ConfigMapRefs field type: string type: object - backendAuthSecret: - description: Optional Reference to a Secret to use for Backend - Auth. A new one will be generated if not set. This Secret is - used to set an environment variable named 'APP_CONFIG_backend_auth_keys' - in the main container, which takes precedence over any 'backend.auth.keys' - field defined in default or custom application configuration - files. This is required for service-to-service auth and is shared - by all backend plugins. Default value for the key in the secret - is 'backend-secret. - properties: - key: - description: Key in the object - type: string - name: - description: Name of the object We support only ConfigMaps - and Secrets. - type: string - required: - - name - type: object dynamicPluginsConfigMapName: description: 'Reference to an existing ConfigMap for Dynamic Plugins. A new one will be generated with the default config if not set. From 2de73671d7706a2c41e6ac81c54d51abca86c04f Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 11 Dec 2023 11:51:54 +0100 Subject: [PATCH 27/31] Add example using RHDH image --- examples/janus-cr-with-app-configs.yaml | 2 +- examples/rhdh-cr.yaml | 92 +++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 examples/rhdh-cr.yaml diff --git a/examples/janus-cr-with-app-configs.yaml b/examples/janus-cr-with-app-configs.yaml index b947cf4f..0dd51bec 100644 --- a/examples/janus-cr-with-app-configs.yaml +++ b/examples/janus-cr-with-app-configs.yaml @@ -55,7 +55,7 @@ metadata: stringData: # generated with the command below (from https://janus-idp.io/docs/auth/service-to-service-auth/#setup): # node -p 'require("crypto").randomBytes(24).toString("base64")' - BACKEND_SECRET: "R2FxRVNrcmwzYzhhN3l0V1VRcnQ3L1pLT09WaVhDNUEK" + BACKEND_SECRET: "R2FxRVNrcmwzYzhhN3l0V1VRcnQ3L1pLT09WaVhDNUEK" # notsecret --- apiVersion: v1 diff --git a/examples/rhdh-cr.yaml b/examples/rhdh-cr.yaml new file mode 100644 index 00000000..a8cdfb62 --- /dev/null +++ b/examples/rhdh-cr.yaml @@ -0,0 +1,92 @@ +apiVersion: janus-idp.io/v1alpha1 +kind: Backstage +metadata: + name: my-rhdh +spec: + application: + image: quay.io/rhdh/rhdh-hub-rhel9:1.0-200 + imagePullSecrets: + - rhdh-pull-secret + appConfig: + configMaps: + - name: app-config-rhdh-auth + - name: app-config-rhdh-gh + dynamicPluginsConfigMapName: dynamic-plugins-rhdh + extraEnvs: + secrets: + - name: backend-auth-rhdh + key: BACKEND_SECRET + - name: gh-auth-rhdh + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config-rhdh-auth +data: + "app-config-rhdh-auth.yaml": | + backend: + auth: + keys: + - secret: "${BACKEND_SECRET}" + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config-rhdh-gh +data: + "app-config-rhdh-gh.yaml": | + auth: + # see https://backstage.io/docs/auth/ to learn about auth providers + environment: development + providers: + github: + development: + clientId: '${GH_CLIENT_ID}' + clientSecret: '${GH_CLIENT_SECRET}' + +--- +apiVersion: v1 +kind: Secret +metadata: + name: backend-auth-rhdh +stringData: + # generated with the command below (from https://janus-idp.io/docs/auth/service-to-service-auth/#setup): + # node -p 'require("crypto").randomBytes(24).toString("base64")' + BACKEND_SECRET: "R2FxRVNrcmwzYzhhN3l0V1VRcnQ3L1pLT09WaVhDNUEK" # notsecret + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: dynamic-plugins-rhdh +data: + dynamic-plugins.yaml: | + includes: + - dynamic-plugins.default.yaml + plugins: + - package: './dynamic-plugins/dist/backstage-plugin-catalog-backend-module-github-dynamic' + disabled: false + pluginConfig: + catalog: + providers: + github: + myorg: + organization: '${GITHUB_ORG}' + schedule: + # supports cron, ISO duration, "human duration" (used below) + frequency: { minutes: 30} + # supports ISO duration, "human duration (used below) + timeout: { minutes: 3} + initialDelay: { seconds: 15} + +--- +apiVersion: v1 +kind: Secret +metadata: + name: gh-auth-rhdh +stringData: + GITHUB_ORG: "my-gh-org" + GH_CLIENT_ID: "my GH client ID" + GH_CLIENT_SECRET: "my GH client secret" From 8d90c3d83b79b4fd72c6f2b8b4d514dae0503a0d Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 11 Dec 2023 13:34:04 +0100 Subject: [PATCH 28/31] Do not use pointer for SkipLocalDb field We use pointers generally to be able to determine if a field was purposely not set. But it should not matter here as the default value is 'false'. --- api/v1alpha1/backstage_types.go | 2 +- api/v1alpha1/zz_generated.deepcopy.go | 7 +------ controllers/backstage_controller.go | 3 +-- controllers/backstage_controller_test.go | 3 +-- 4 files changed, 4 insertions(+), 11 deletions(-) diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index 9054bc58..1fec6a36 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -38,7 +38,7 @@ type BackstageSpec struct { // (see the ExtraFiles field in the Application structure). // +optional //+kubebuilder:default=false - SkipLocalDb *bool `json:"skipLocalDb,omitempty"` + SkipLocalDb bool `json:"skipLocalDb,omitempty"` } type Application struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index e27a122d..9bdac919 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -22,7 +22,7 @@ limitations under the License. package v1alpha1 import ( - "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -159,11 +159,6 @@ func (in *BackstageSpec) DeepCopyInto(out *BackstageSpec) { (*in).DeepCopyInto(*out) } out.RawRuntimeConfig = in.RawRuntimeConfig - if in.SkipLocalDb != nil { - in, out := &in.SkipLocalDb, &out.SkipLocalDb - *out = new(bool) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackstageSpec. diff --git a/controllers/backstage_controller.go b/controllers/backstage_controller.go index 804134d7..58262bf1 100644 --- a/controllers/backstage_controller.go +++ b/controllers/backstage_controller.go @@ -30,7 +30,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/yaml" - "k8s.io/utils/pointer" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" @@ -91,7 +90,7 @@ func (r *BackstageReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return ctrl.Result{}, fmt.Errorf("failed to load backstage deployment from the cluster: %w", err) } - if !pointer.BoolDeref(backstage.Spec.SkipLocalDb, false) { + if !backstage.Spec.SkipLocalDb { /* We use default strogeclass currently, and no PV is needed in that case. If we decide later on to support user provided storageclass we can enable pv creation. diff --git a/controllers/backstage_controller_test.go b/controllers/backstage_controller_test.go index 0ec8c852..7c53c9ae 100644 --- a/controllers/backstage_controller_test.go +++ b/controllers/backstage_controller_test.go @@ -27,7 +27,6 @@ import ( "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/reconcile" bsv1alpha1 "janus-idp.io/backstage-operator/api/v1alpha1" @@ -1253,7 +1252,7 @@ plugins: [] var backstage *bsv1alpha1.Backstage BeforeEach(func() { backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - SkipLocalDb: pointer.Bool(true), + SkipLocalDb: true, }) err := k8sClient.Create(ctx, backstage) Expect(err).To(Not(HaveOccurred())) From 4091c05df37e60b08046af9852fd333141aaefad Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 11 Dec 2023 14:10:28 +0100 Subject: [PATCH 29/31] Simplify example for RHDH as much as possible --- examples/rhdh-cr.yaml | 36 +++++++++--------------------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/examples/rhdh-cr.yaml b/examples/rhdh-cr.yaml index a8cdfb62..7bffbe4c 100644 --- a/examples/rhdh-cr.yaml +++ b/examples/rhdh-cr.yaml @@ -9,34 +9,23 @@ spec: - rhdh-pull-secret appConfig: configMaps: - - name: app-config-rhdh-auth - - name: app-config-rhdh-gh + - name: app-config-rhdh dynamicPluginsConfigMapName: dynamic-plugins-rhdh extraEnvs: secrets: - - name: backend-auth-rhdh - key: BACKEND_SECRET - - name: gh-auth-rhdh + - name: secrets-rhdh --- apiVersion: v1 kind: ConfigMap metadata: - name: app-config-rhdh-auth + name: app-config-rhdh data: - "app-config-rhdh-auth.yaml": | + "app-config-rhdh.yaml": | backend: auth: keys: - secret: "${BACKEND_SECRET}" - ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: app-config-rhdh-gh -data: - "app-config-rhdh-gh.yaml": | auth: # see https://backstage.io/docs/auth/ to learn about auth providers environment: development @@ -50,11 +39,14 @@ data: apiVersion: v1 kind: Secret metadata: - name: backend-auth-rhdh + name: secrets-rhdh stringData: # generated with the command below (from https://janus-idp.io/docs/auth/service-to-service-auth/#setup): # node -p 'require("crypto").randomBytes(24).toString("base64")' BACKEND_SECRET: "R2FxRVNrcmwzYzhhN3l0V1VRcnQ3L1pLT09WaVhDNUEK" # notsecret + GH_ORG: "my-gh-org" + GH_CLIENT_ID: "my GH client ID" + GH_CLIENT_SECRET: "my GH client secret" --- apiVersion: v1 @@ -73,20 +65,10 @@ data: providers: github: myorg: - organization: '${GITHUB_ORG}' + organization: '${GH_ORG}' schedule: # supports cron, ISO duration, "human duration" (used below) frequency: { minutes: 30} # supports ISO duration, "human duration (used below) timeout: { minutes: 3} initialDelay: { seconds: 15} - ---- -apiVersion: v1 -kind: Secret -metadata: - name: gh-auth-rhdh -stringData: - GITHUB_ORG: "my-gh-org" - GH_CLIENT_ID: "my GH client ID" - GH_CLIENT_SECRET: "my GH client secret" From ff0060f80a9b3a2a1a61e81105a9fade5d6969e1 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 11 Dec 2023 15:55:15 +0100 Subject: [PATCH 30/31] Rename `SkipLocalDb` into `EnableLocalDb` Co-authored-by: Jianrong Zhang --- api/v1alpha1/backstage_types.go | 8 ++++---- api/v1alpha1/zz_generated.deepcopy.go | 5 +++++ config/crd/bases/janus-idp.io_backstages.yaml | 20 +++++++++---------- controllers/backstage_controller.go | 3 ++- controllers/backstage_controller_test.go | 3 ++- examples/janus-cr-with-app-configs.yaml | 2 +- 6 files changed, 24 insertions(+), 17 deletions(-) diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index 1fec6a36..cd9b0a63 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -31,14 +31,14 @@ type BackstageSpec struct { // Raw Runtime Objects configuration. For Advanced scenarios. RawRuntimeConfig RuntimeConfig `json:"rawRuntimeConfig,omitempty"` - // Control the creation of a local PostgreSQL DB. Set to true if using for example an external Database for Backstage. + // Control the creation of a local PostgreSQL DB. Set to false if using for example an external Database for Backstage. // To use an external Database, you can provide your own app-config file (see the AppConfig field in the Application structure) // containing references to the Database connection information, - // which might be supplied as environment variables (see the Env field) or extra-configuration files + // which might be supplied as environment variables (see the ExtraEnvs field) or extra-configuration files // (see the ExtraFiles field in the Application structure). // +optional - //+kubebuilder:default=false - SkipLocalDb bool `json:"skipLocalDb,omitempty"` + //+kubebuilder:default=true + EnableLocalDb *bool `json:"enableLocalDb,omitempty"` } type Application struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 9bdac919..e32d262a 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -159,6 +159,11 @@ func (in *BackstageSpec) DeepCopyInto(out *BackstageSpec) { (*in).DeepCopyInto(*out) } out.RawRuntimeConfig = in.RawRuntimeConfig + if in.EnableLocalDb != nil { + in, out := &in.EnableLocalDb, &out.EnableLocalDb + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackstageSpec. diff --git a/config/crd/bases/janus-idp.io_backstages.yaml b/config/crd/bases/janus-idp.io_backstages.yaml index 1dd20cd0..a28cac0f 100644 --- a/config/crd/bases/janus-idp.io_backstages.yaml +++ b/config/crd/bases/janus-idp.io_backstages.yaml @@ -213,6 +213,16 @@ spec: format: int32 type: integer type: object + enableLocalDb: + default: true + description: Control the creation of a local PostgreSQL DB. Set to + false if using for example an external Database for Backstage. To + use an external Database, you can provide your own app-config file + (see the AppConfig field in the Application structure) containing + references to the Database connection information, which might be + supplied as environment variables (see the ExtraEnvs field) or extra-configuration + files (see the ExtraFiles field in the Application structure). + type: boolean rawRuntimeConfig: description: Raw Runtime Objects configuration. For Advanced scenarios. properties: @@ -225,16 +235,6 @@ spec: runtime objects configuration type: string type: object - skipLocalDb: - default: false - description: Control the creation of a local PostgreSQL DB. Set to - true if using for example an external Database for Backstage. To - use an external Database, you can provide your own app-config file - (see the AppConfig field in the Application structure) containing - references to the Database connection information, which might be - supplied as environment variables (see the Env field) or extra-configuration - files (see the ExtraFiles field in the Application structure). - type: boolean type: object status: description: BackstageStatus defines the observed state of Backstage diff --git a/controllers/backstage_controller.go b/controllers/backstage_controller.go index 58262bf1..c195833a 100644 --- a/controllers/backstage_controller.go +++ b/controllers/backstage_controller.go @@ -30,6 +30,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/utils/pointer" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" @@ -90,7 +91,7 @@ func (r *BackstageReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return ctrl.Result{}, fmt.Errorf("failed to load backstage deployment from the cluster: %w", err) } - if !backstage.Spec.SkipLocalDb { + if pointer.BoolDeref(backstage.Spec.EnableLocalDb, true) { /* We use default strogeclass currently, and no PV is needed in that case. If we decide later on to support user provided storageclass we can enable pv creation. diff --git a/controllers/backstage_controller_test.go b/controllers/backstage_controller_test.go index 7c53c9ae..23275973 100644 --- a/controllers/backstage_controller_test.go +++ b/controllers/backstage_controller_test.go @@ -27,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/reconcile" bsv1alpha1 "janus-idp.io/backstage-operator/api/v1alpha1" @@ -1252,7 +1253,7 @@ plugins: [] var backstage *bsv1alpha1.Backstage BeforeEach(func() { backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - SkipLocalDb: true, + EnableLocalDb: pointer.Bool(false), }) err := k8sClient.Create(ctx, backstage) Expect(err).To(Not(HaveOccurred())) diff --git a/examples/janus-cr-with-app-configs.yaml b/examples/janus-cr-with-app-configs.yaml index 0dd51bec..0cac8f91 100644 --- a/examples/janus-cr-with-app-configs.yaml +++ b/examples/janus-cr-with-app-configs.yaml @@ -3,7 +3,7 @@ kind: Backstage metadata: name: my-backstage-app-with-app-config spec: - skipLocalDb: false + enableLocalDb: true application: replicas: 2 appConfig: From 2b18848397cb515b8ec9a505f6dd65cc2325505a Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 11 Dec 2023 16:41:54 +0100 Subject: [PATCH 31/31] Update bundle manifests --- ...kstage-operator.clusterserviceversion.yaml | 2 +- bundle/manifests/janus-idp.io_backstages.yaml | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/bundle/manifests/backstage-operator.clusterserviceversion.yaml b/bundle/manifests/backstage-operator.clusterserviceversion.yaml index 11fe7f85..a2f1c5ff 100644 --- a/bundle/manifests/backstage-operator.clusterserviceversion.yaml +++ b/bundle/manifests/backstage-operator.clusterserviceversion.yaml @@ -21,7 +21,7 @@ metadata: } ] capabilities: Basic Install - createdAt: "2023-12-11T09:00:15Z" + createdAt: "2023-12-11T15:41:14Z" operators.operatorframework.io/builder: operator-sdk-v1.32.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 name: backstage-operator.v0.0.1 diff --git a/bundle/manifests/janus-idp.io_backstages.yaml b/bundle/manifests/janus-idp.io_backstages.yaml index a2478e17..2135f30e 100644 --- a/bundle/manifests/janus-idp.io_backstages.yaml +++ b/bundle/manifests/janus-idp.io_backstages.yaml @@ -212,6 +212,16 @@ spec: format: int32 type: integer type: object + enableLocalDb: + default: true + description: Control the creation of a local PostgreSQL DB. Set to + false if using for example an external Database for Backstage. To + use an external Database, you can provide your own app-config file + (see the AppConfig field in the Application structure) containing + references to the Database connection information, which might be + supplied as environment variables (see the ExtraEnvs field) or extra-configuration + files (see the ExtraFiles field in the Application structure). + type: boolean rawRuntimeConfig: description: Raw Runtime Objects configuration. For Advanced scenarios. properties: @@ -224,16 +234,6 @@ spec: runtime objects configuration type: string type: object - skipLocalDb: - default: false - description: Control the creation of a local PostgreSQL DB. Set to - true if using for example an external Database for Backstage. To - use an external Database, you can provide your own app-config file - (see the AppConfig field in the Application structure) containing - references to the Database connection information, which might be - supplied as environment variables (see the Env field) or extra-configuration - files (see the ExtraFiles field in the Application structure). - type: boolean type: object status: description: BackstageStatus defines the observed state of Backstage