From f4fd26b313d5afba7e7027606ad4cb22d945977f Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 20 Nov 2023 17:58:31 +0100 Subject: [PATCH 01/18] Handle app-configs --- api/v1alpha1/backstage_types.go | 8 +- api/v1alpha1/zz_generated.deepcopy.go | 17 +- config/crd/bases/backstage.io_backstages.yaml | 10 +- controllers/backstage_deployment.go | 206 +++++++++++++++++- examples/janus-cr-with-app-configs.yaml | 52 +++++ 5 files changed, 286 insertions(+), 7 deletions(-) create mode 100644 examples/janus-cr-with-app-configs.yaml diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index 80c6e45b..d3eeaece 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -26,7 +26,7 @@ const ( // BackstageSpec defines the desired state of Backstage type BackstageSpec struct { // Backstage application AppConfigs - AppConfigs []string `json:"appConfigs,omitempty"` + AppConfigs []AppConfig `json:"appConfigs,omitempty"` // Raw Runtime Objects configuration RawRuntimeConfig RuntimeConfig `json:"rawRuntimeConfig,omitempty"` @@ -34,6 +34,12 @@ type BackstageSpec struct { SkipLocalDb bool `json:"skipLocalDb,omitempty"` } +type AppConfig struct { + Name string `json:"name,omitempty"` + //+kubebuilder:validation:Enum=ConfigMap;Secret + Kind string `json:"kind,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 702ff746..51c6f1b8 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -26,6 +26,21 @@ import ( 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 +} + +// 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 *Backstage) DeepCopyInto(out *Backstage) { *out = *in @@ -90,7 +105,7 @@ func (in *BackstageSpec) DeepCopyInto(out *BackstageSpec) { *out = *in if in.AppConfigs != nil { in, out := &in.AppConfigs, &out.AppConfigs - *out = make([]string, len(*in)) + *out = make([]AppConfig, len(*in)) copy(*out, *in) } out.RawRuntimeConfig = in.RawRuntimeConfig diff --git a/config/crd/bases/backstage.io_backstages.yaml b/config/crd/bases/backstage.io_backstages.yaml index 5feab6ca..505bad5e 100644 --- a/config/crd/bases/backstage.io_backstages.yaml +++ b/config/crd/bases/backstage.io_backstages.yaml @@ -38,7 +38,15 @@ spec: appConfigs: description: Backstage application AppConfigs items: - type: string + properties: + kind: + enum: + - ConfigMap + - Secret + type: string + name: + type: string + type: object type: array rawRuntimeConfig: description: Raw Runtime Objects configuration diff --git a/controllers/backstage_deployment.go b/controllers/backstage_deployment.go index 1cd919a7..916102a2 100644 --- a/controllers/backstage_deployment.go +++ b/controllers/backstage_deployment.go @@ -20,13 +20,19 @@ import ( bs "backstage.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" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) +const ( + _defaultBackstageMainContainerName = "backstage-backend" +) + var ( - DefaultBackstageDeployment = ` + DefaultBackstageDeployment = fmt.Sprintf(` apiVersion: apps/v1 kind: Deployment metadata: @@ -41,19 +47,106 @@ spec: labels: backstage.io/app: # placeholder for 'backstage-' spec: +# serviceAccountName: default + + volumes: + - ephemeral: + volumeClaimTemplate: + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + name: dynamic-plugins-root + - configMap: + defaultMode: 420 + name: dynamic-plugins + optional: true + name: dynamic-plugins + - name: dynamic-plugins-npmrc + secret: + defaultMode: 420 + optional: true + secretName: dynamic-plugins-npmrc +# TODO(rm3l): to mount if value set in CR + #- name: backstage-app-config + # configMap: + # name: my-backstage-from-helm-app-config + + initContainers: + - command: + - ./install-dynamic-plugins.sh + - /dynamic-plugins-root + env: + - name: NPM_CONFIG_USERCONFIG + value: /opt/app-root/src/.npmrc.dynamic-plugins + image: 'quay.io/janus-idp/backstage-showcase:next' + imagePullPolicy: IfNotPresent + name: install-dynamic-plugins + volumeMounts: + - mountPath: /dynamic-plugins-root + name: dynamic-plugins-root + - mountPath: /opt/app-root/src/dynamic-plugins.yaml + name: dynamic-plugins + readOnly: true + subPath: dynamic-plugins.yaml + - mountPath: /opt/app-root/src/.npmrc.dynamic-plugins + name: dynamic-plugins-npmrc + readOnly: true + subPath: .npmrc + workingDir: /opt/app-root/src + containers: - - name: backstage - image: ghcr.io/backstage/backstage + - name: %s + image: quay.io/janus-idp/backstage-showcase:next imagePullPolicy: IfNotPresent + args: + - "--config" + - "dynamic-plugins-root/app-config.dynamic-plugins.yaml" +# TODO(rm3l): to mount if value set in CR + #- "--config" + #- "/opt/app-root/src/app-config-from-configmap.yaml" + readinessProbe: + failureThreshold: 3 + httpGet: + path: /healthcheck + port: 7007 + scheme: HTTP + initialDelaySeconds: 30 + periodSeconds: 10 + successThreshold: 2 + timeoutSeconds: 2 + livenessProbe: + failureThreshold: 3 + httpGet: + path: /healthcheck + port: 7007 + scheme: HTTP + initialDelaySeconds: 60 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 2 ports: - name: http containerPort: 7007 +# TODO Handle user-defined env vars + env: + - name: APP_CONFIG_backend_listen_port + value: "7007" envFrom: - secretRef: name: postgres-secrets # - secretRef: # name: backstage-secrets -` + volumeMounts: +# TODO(rm3l): to mount if value set in CR + #- name: backstage-app-config + # mountPath: "/opt/app-root/src/app-config-from-configmap.yaml" + # subPath: app-config.yaml + - mountPath: /opt/app-root/src/dynamic-plugins-root + name: dynamic-plugins-root +`, _defaultBackstageMainContainerName) ) func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, backstage bs.Backstage, ns string) error { @@ -81,6 +174,19 @@ func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, back } } + r.addVolumes(backstage, deployment) + + if backstage.Spec.RawRuntimeConfig.BackstageConfigName == "" { + var appConfigFileNamesMap map[string][]string + appConfigFileNamesMap, err = r.extractAppConfigFileNames(ctx, backstage, ns) + if err != nil { + return err + } + r.addVolumeMounts(deployment, appConfigFileNamesMap) + r.addContainerArgs(deployment, appConfigFileNamesMap) + r.addEnvVars(deployment) + } + err = r.Create(ctx, deployment) if err != nil { return fmt.Errorf("failed to create backstage deplyment, reason: %s", err) @@ -98,3 +204,95 @@ func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, back } return nil } + +func (r *BackstageReconciler) addVolumes(backstage bs.Backstage, deployment *appsv1.Deployment) { + for _, appConfig := range backstage.Spec.AppConfigs { + var volumeSource v1.VolumeSource + switch appConfig.Kind { + case "ConfigMap": + volumeSource.ConfigMap = &v1.ConfigMapVolumeSource{ + DefaultMode: pointer.Int32(420), + LocalObjectReference: v1.LocalObjectReference{Name: appConfig.Name}, + } + case "Secret": + volumeSource.Secret = &v1.SecretVolumeSource{ + DefaultMode: pointer.Int32(420), + SecretName: appConfig.Name, + } + } + deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, + v1.Volume{ + Name: appConfig.Name, + VolumeSource: volumeSource, + }) + } +} + +func (r *BackstageReconciler) addVolumeMounts(deployment *appsv1.Deployment, appConfigFileNamesMap map[string][]string) { + for i, c := range deployment.Spec.Template.Spec.Containers { + if c.Name == _defaultBackstageMainContainerName { + for appConfigName := range appConfigFileNamesMap { + deployment.Spec.Template.Spec.Containers[i].VolumeMounts = append(deployment.Spec.Template.Spec.Containers[i].VolumeMounts, + v1.VolumeMount{ + Name: appConfigName, + MountPath: "/opt/app-root/src/" + appConfigName, + }) + } + break + } + } +} + +func (r *BackstageReconciler) addContainerArgs(deployment *appsv1.Deployment, appConfigFileNamesMap map[string][]string) { + for i, c := range deployment.Spec.Template.Spec.Containers { + if c.Name == _defaultBackstageMainContainerName { + for appConfigName, fileNames := range appConfigFileNamesMap { + // Args + for _, fileName := range fileNames { + deployment.Spec.Template.Spec.Containers[i].Args = + append(deployment.Spec.Template.Spec.Containers[i].Args, "--config", + fmt.Sprintf("/opt/app-root/src/%s/%s", appConfigName, fileName)) + } + } + break + } + } +} + +func (r *BackstageReconciler) addEnvVars(deployment *appsv1.Deployment) { + for i, c := range deployment.Spec.Template.Spec.Containers { + if c.Name == _defaultBackstageMainContainerName { + // FIXME(rm3l): Hack to set the 'BACKEND_SECRET' env var + deployment.Spec.Template.Spec.Containers[i].Env = append(deployment.Spec.Template.Spec.Containers[i].Env, v1.EnvVar{ + Name: "BACKEND_SECRET", + Value: "ch4ng3M3", + }) + break + } + } +} + +func (r *BackstageReconciler) extractAppConfigFileNames(ctx context.Context, backstage bs.Backstage, ns string) (map[string][]string, error) { + m := make(map[string][]string) + for _, appConfig := range backstage.Spec.AppConfigs { + switch appConfig.Kind { + case "ConfigMap": + cm := v1.ConfigMap{} + if err := r.Get(ctx, types.NamespacedName{Name: appConfig.Name, Namespace: ns}, &cm); err != nil { + return nil, err + } + for filename := range cm.Data { + m[appConfig.Name] = append(m[appConfig.Name], filename) + } + case "Secret": + sec := v1.Secret{} + if err := r.Get(ctx, types.NamespacedName{Name: appConfig.Name, Namespace: ns}, &sec); err != nil { + return nil, err + } + for filename := range sec.Data { + m[appConfig.Name] = append(m[appConfig.Name], filename) + } + } + } + return m, nil +} diff --git a/examples/janus-cr-with-app-configs.yaml b/examples/janus-cr-with-app-configs.yaml new file mode 100644 index 00000000..fe94261a --- /dev/null +++ b/examples/janus-cr-with-app-configs.yaml @@ -0,0 +1,52 @@ +apiVersion: backstage.io/v1alpha1 +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 + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-backstage-config-cm1 +data: + my-app-config.prod.yaml: | + backend: + auth: + keys: + - secret: ${BACKEND_SECRET} + database: + connection: + password: ${POSTGRESQL_PASSWORD} + user: ${POSTGRESQL_USER} + # Backstage override configuration for your prod development environment + auth: + # see https://backstage.io/docs/auth/ to learn about auth providers + environment: development + providers: + github: + development: + clientId: 'xxx' + clientSecret: 'yyy' + my-app-config-2.yaml: | + # Some comment in this file + dynamic-plugins.yaml: | + includes: + - dynamic-plugins.default.yaml + plugins: [] + +--- +apiVersion: v1 +kind: Secret +metadata: + name: my-backstage-config-secret1 +data: + # {} + my-app-config-1.secret.yaml: e30K + # # a comment + my-app-config-2.secret.yaml: IyBhIGNvbW1lbnQK From c2c1fa85ad725c69c97e8659219021b314fe2527 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Wed, 22 Nov 2023 13:54:52 +0100 Subject: [PATCH 02/18] Handle dynamic plugins config differently from app-configs They do not seem to be handled the same way. --- api/v1alpha1/backstage_types.go | 8 ++- api/v1alpha1/zz_generated.deepcopy.go | 33 +++++----- config/crd/bases/backstage.io_backstages.yaml | 11 ++++ controllers/backstage_deployment.go | 64 ++++++++++++------- examples/janus-cr-with-app-configs.yaml | 47 +++++++++----- 5 files changed, 106 insertions(+), 57 deletions(-) diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index d3eeaece..c15ce103 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -26,7 +26,11 @@ const ( // BackstageSpec defines the desired state of Backstage type BackstageSpec struct { // Backstage application AppConfigs - AppConfigs []AppConfig `json:"appConfigs,omitempty"` + AppConfigs []Config `json:"appConfigs,omitempty"` + + // Dynamic Plugins configuration + DynamicPluginsConfig Config `json:"dynamicPluginsConfig,omitempty"` + // Raw Runtime Objects configuration RawRuntimeConfig RuntimeConfig `json:"rawRuntimeConfig,omitempty"` @@ -34,7 +38,7 @@ type BackstageSpec struct { SkipLocalDb bool `json:"skipLocalDb,omitempty"` } -type AppConfig struct { +type Config struct { Name string `json:"name,omitempty"` //+kubebuilder:validation:Enum=ConfigMap;Secret Kind string `json:"kind,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 51c6f1b8..f93f31b7 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -26,21 +26,6 @@ import ( 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 -} - -// 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 *Backstage) DeepCopyInto(out *Backstage) { *out = *in @@ -105,9 +90,10 @@ func (in *BackstageSpec) DeepCopyInto(out *BackstageSpec) { *out = *in if in.AppConfigs != nil { in, out := &in.AppConfigs, &out.AppConfigs - *out = make([]AppConfig, len(*in)) + *out = make([]Config, len(*in)) copy(*out, *in) } + out.DynamicPluginsConfig = in.DynamicPluginsConfig out.RawRuntimeConfig = in.RawRuntimeConfig } @@ -143,6 +129,21 @@ 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 *Config) DeepCopyInto(out *Config) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Config. +func (in *Config) DeepCopy() *Config { + if in == nil { + return nil + } + out := new(Config) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RuntimeConfig) DeepCopyInto(out *RuntimeConfig) { *out = *in diff --git a/config/crd/bases/backstage.io_backstages.yaml b/config/crd/bases/backstage.io_backstages.yaml index 505bad5e..7bf0b2da 100644 --- a/config/crd/bases/backstage.io_backstages.yaml +++ b/config/crd/bases/backstage.io_backstages.yaml @@ -48,6 +48,17 @@ spec: type: string type: object type: array + dynamicPluginsConfig: + description: Dynamic Plugins configuration + properties: + kind: + enum: + - ConfigMap + - Secret + type: string + name: + type: string + type: object rawRuntimeConfig: description: Raw Runtime Objects configuration properties: diff --git a/controllers/backstage_deployment.go b/controllers/backstage_deployment.go index 916102a2..f32f9aba 100644 --- a/controllers/backstage_deployment.go +++ b/controllers/backstage_deployment.go @@ -28,6 +28,7 @@ import ( ) const ( + _defaultBackstageInitContainerName = "install-dynamic-plugins" _defaultBackstageMainContainerName = "backstage-backend" ) @@ -59,25 +60,16 @@ spec: requests: storage: 1Gi name: dynamic-plugins-root - - configMap: - defaultMode: 420 - name: dynamic-plugins - optional: true - name: dynamic-plugins - name: dynamic-plugins-npmrc secret: defaultMode: 420 optional: true secretName: dynamic-plugins-npmrc -# TODO(rm3l): to mount if value set in CR - #- name: backstage-app-config - # configMap: - # name: my-backstage-from-helm-app-config initContainers: - command: - ./install-dynamic-plugins.sh - - /dynamic-plugins-root + - dynamic-plugins-root env: - name: NPM_CONFIG_USERCONFIG value: /opt/app-root/src/.npmrc.dynamic-plugins @@ -85,12 +77,8 @@ spec: imagePullPolicy: IfNotPresent name: install-dynamic-plugins volumeMounts: - - mountPath: /dynamic-plugins-root + - mountPath: /opt/app-root/src/dynamic-plugins-root name: dynamic-plugins-root - - mountPath: /opt/app-root/src/dynamic-plugins.yaml - name: dynamic-plugins - readOnly: true - subPath: dynamic-plugins.yaml - mountPath: /opt/app-root/src/.npmrc.dynamic-plugins name: dynamic-plugins-npmrc readOnly: true @@ -104,9 +92,6 @@ spec: args: - "--config" - "dynamic-plugins-root/app-config.dynamic-plugins.yaml" -# TODO(rm3l): to mount if value set in CR - #- "--config" - #- "/opt/app-root/src/app-config-from-configmap.yaml" readinessProbe: failureThreshold: 3 httpGet: @@ -140,10 +125,6 @@ spec: # - secretRef: # name: backstage-secrets volumeMounts: -# TODO(rm3l): to mount if value set in CR - #- name: backstage-app-config - # mountPath: "/opt/app-root/src/app-config-from-configmap.yaml" - # subPath: app-config.yaml - mountPath: /opt/app-root/src/dynamic-plugins-root name: dynamic-plugins-root `, _defaultBackstageMainContainerName) @@ -182,7 +163,7 @@ func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, back if err != nil { return err } - r.addVolumeMounts(deployment, appConfigFileNamesMap) + r.addVolumeMounts(backstage, deployment, appConfigFileNamesMap) r.addContainerArgs(deployment, appConfigFileNamesMap) r.addEnvVars(deployment) } @@ -226,9 +207,44 @@ func (r *BackstageReconciler) addVolumes(backstage bs.Backstage, deployment *app VolumeSource: volumeSource, }) } + if backstage.Spec.DynamicPluginsConfig.Name != "" { + var volumeSource v1.VolumeSource + switch backstage.Spec.DynamicPluginsConfig.Kind { + case "ConfigMap": + volumeSource.ConfigMap = &v1.ConfigMapVolumeSource{ + DefaultMode: pointer.Int32(420), + LocalObjectReference: v1.LocalObjectReference{Name: backstage.Spec.DynamicPluginsConfig.Name}, + } + case "Secret": + volumeSource.Secret = &v1.SecretVolumeSource{ + DefaultMode: pointer.Int32(420), + SecretName: backstage.Spec.DynamicPluginsConfig.Name, + } + } + deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, + v1.Volume{ + Name: backstage.Spec.DynamicPluginsConfig.Name, + VolumeSource: volumeSource, + }) + } } -func (r *BackstageReconciler) addVolumeMounts(deployment *appsv1.Deployment, appConfigFileNamesMap map[string][]string) { +func (r *BackstageReconciler) addVolumeMounts(backstage bs.Backstage, deployment *appsv1.Deployment, appConfigFileNamesMap map[string][]string) { + if backstage.Spec.DynamicPluginsConfig.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: backstage.Spec.DynamicPluginsConfig.Name, + MountPath: "/opt/app-root/src/dynamic-plugins.yaml", + ReadOnly: true, + SubPath: "dynamic-plugins.yaml", + }) + break + } + } + } + for i, c := range deployment.Spec.Template.Spec.Containers { if c.Name == _defaultBackstageMainContainerName { for appConfigName := range appConfigFileNamesMap { diff --git a/examples/janus-cr-with-app-configs.yaml b/examples/janus-cr-with-app-configs.yaml index fe94261a..593751a2 100644 --- a/examples/janus-cr-with-app-configs.yaml +++ b/examples/janus-cr-with-app-configs.yaml @@ -8,6 +8,9 @@ spec: kind: ConfigMap - name: "my-backstage-config-secret1" kind: Secret + dynamicPluginsConfig: + name: my-dynamic-plugins-config-cm + kind: ConfigMap --- apiVersion: v1 @@ -24,21 +27,8 @@ data: connection: password: ${POSTGRESQL_PASSWORD} user: ${POSTGRESQL_USER} - # Backstage override configuration for your prod development environment - auth: - # see https://backstage.io/docs/auth/ to learn about auth providers - environment: development - providers: - github: - development: - clientId: 'xxx' - clientSecret: 'yyy' my-app-config-2.yaml: | # Some comment in this file - dynamic-plugins.yaml: | - includes: - - dynamic-plugins.default.yaml - plugins: [] --- apiVersion: v1 @@ -46,7 +36,34 @@ kind: Secret metadata: name: my-backstage-config-secret1 data: - # {} - my-app-config-1.secret.yaml: e30K + # auth: + # # see https://backstage.io/docs/auth/ to learn about auth providers + # environment: development + # providers: + # github: + # development: + # clientId: 'xxx' + # clientSecret: 'yyy' + my-app-config-1.secret.yaml: YXV0aDoKICAjIHNlZSBodHRwczovL2JhY2tzdGFnZS5pby9kb2NzL2F1dGgvIHRvIGxlYXJuIGFib3V0IGF1dGggcHJvdmlkZXJzCiAgZW52aXJvbm1lbnQ6IGRldmVsb3BtZW50CiAgcHJvdmlkZXJzOgogICAgZ2l0aHViOgogICAgICBkZXZlbG9wbWVudDoKICAgICAgICBjbGllbnRJZDogJ3h4eCcKICAgICAgICBjbGllbnRTZWNyZXQ6ICd5eXknCg== # # a comment my-app-config-2.secret.yaml: IyBhIGNvbW1lbnQK + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-dynamic-plugins-config-cm +data: + dynamic-plugins.yaml: | + includes: + - dynamic-plugins.default.yaml + plugins: + - 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' + integrity: 'sha512-/qUxjSedxQ0dmYqMWsZ2+OLGeovaaigRRrX1aTOz0GJMwSjOAauUUD1bMs56VPX74qWL1rf3Xr4nViiKo8rlIA==' + pluginConfig: + proxy: + endpoints: + /explore-backend-completed: + target: 'http://localhost:7017' \ No newline at end of file From cec19187b90e5fa423139206682d4e030d013e7f Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Wed, 22 Nov 2023 17:25:14 +0100 Subject: [PATCH 03/18] Set a default value for the 'backend.auth.keys[0].secret' config This is required for in the showcase image, for service-to-service communication [1] [1] https://backstage.io/docs/auth/service-to-service-auth/#setup --- controllers/backstage_deployment.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/controllers/backstage_deployment.go b/controllers/backstage_deployment.go index f32f9aba..c429c2a7 100644 --- a/controllers/backstage_deployment.go +++ b/controllers/backstage_deployment.go @@ -30,6 +30,7 @@ import ( const ( _defaultBackstageInitContainerName = "install-dynamic-plugins" _defaultBackstageMainContainerName = "backstage-backend" + _defaultBackendAuthSecretValue = "pl4s3Ch4ng3M3" ) var ( @@ -278,11 +279,13 @@ func (r *BackstageReconciler) addContainerArgs(deployment *appsv1.Deployment, ap func (r *BackstageReconciler) addEnvVars(deployment *appsv1.Deployment) { for i, c := range deployment.Spec.Template.Spec.Containers { if c.Name == _defaultBackstageMainContainerName { - // FIXME(rm3l): Hack to set the 'BACKEND_SECRET' env var - deployment.Spec.Template.Spec.Containers[i].Env = append(deployment.Spec.Template.Spec.Containers[i].Env, v1.EnvVar{ - Name: "BACKEND_SECRET", - Value: "ch4ng3M3", - }) + deployment.Spec.Template.Spec.Containers[i].Env = append(deployment.Spec.Template.Spec.Containers[i].Env, + // TODO(rm3l): backend.auth.keys[0].secret is required for service-to-service communication. + // This is a default value that will be modifiable by the user using environment variables. + v1.EnvVar{ + Name: "APP_CONFIG_backend_auth_keys", + Value: fmt.Sprintf(`[{"secret": "%s"}]`, _defaultBackendAuthSecret), + }) break } } From 87b1bba9c6b89e53c5ae0b20218486aac0b96d5c Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Wed, 22 Nov 2023 22:09:32 +0100 Subject: [PATCH 04/18] Allow to specify a secret for the backend auth key If not defined, the operator will create a dedicate secret. --- api/v1alpha1/backstage_types.go | 12 +++ api/v1alpha1/zz_generated.deepcopy.go | 16 ++++ config/crd/bases/backstage.io_backstages.yaml | 13 +++ controllers/backstage_deployment.go | 82 +++++++++++++++++-- examples/janus-cr-with-app-configs.yaml | 26 +++++- 5 files changed, 140 insertions(+), 9 deletions(-) diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index c15ce103..30a832dd 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -28,6 +28,9 @@ type BackstageSpec struct { // Backstage application AppConfigs AppConfigs []Config `json:"appConfigs,omitempty"` + // Optional Backend Auth Secret Name. A new one will be generated if not set. + BackendAuthSecretRef BackendAuthSecretRef `json:"backendAuthSecretRef,omitempty"` + // Dynamic Plugins configuration DynamicPluginsConfig Config `json:"dynamicPluginsConfig,omitempty"` @@ -44,6 +47,15 @@ type Config struct { Kind string `json:"kind,omitempty"` } +type BackendAuthSecretRef struct { + // Name of the secret to use for the backend auth + Name string `json:"name,omitempty"` + + // Key in the secret to use for the backend auth. Default value is: backend-secret + //+kubebuilder:default=backend-secret + Key string `json:"key,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 f93f31b7..64bc2cc5 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -26,6 +26,21 @@ import ( 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 *BackendAuthSecretRef) DeepCopyInto(out *BackendAuthSecretRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendAuthSecretRef. +func (in *BackendAuthSecretRef) DeepCopy() *BackendAuthSecretRef { + if in == nil { + return nil + } + out := new(BackendAuthSecretRef) + 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 @@ -93,6 +108,7 @@ func (in *BackstageSpec) DeepCopyInto(out *BackstageSpec) { *out = make([]Config, len(*in)) copy(*out, *in) } + out.BackendAuthSecretRef = in.BackendAuthSecretRef out.DynamicPluginsConfig = in.DynamicPluginsConfig out.RawRuntimeConfig = in.RawRuntimeConfig } diff --git a/config/crd/bases/backstage.io_backstages.yaml b/config/crd/bases/backstage.io_backstages.yaml index 7bf0b2da..8d5267c1 100644 --- a/config/crd/bases/backstage.io_backstages.yaml +++ b/config/crd/bases/backstage.io_backstages.yaml @@ -48,6 +48,19 @@ spec: type: string type: object type: array + backendAuthSecretRef: + description: Optional Backend Auth Secret Name. A new one will be + generated if not set. + 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 + type: object dynamicPluginsConfig: description: Dynamic Plugins configuration properties: diff --git a/controllers/backstage_deployment.go b/controllers/backstage_deployment.go index c429c2a7..87ce7aa4 100644 --- a/controllers/backstage_deployment.go +++ b/controllers/backstage_deployment.go @@ -16,12 +16,15 @@ package controller import ( "context" + "crypto/rand" + "encoding/base64" "fmt" bs "backstage.io/backstage-operator/api/v1alpha1" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" "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/controller/controllerutil" @@ -156,6 +159,11 @@ func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, back } } + backendAuthSecretName, err := r.handleBackendAuthSecret(ctx, backstage, ns) + if err != nil { + return err + } + r.addVolumes(backstage, deployment) if backstage.Spec.RawRuntimeConfig.BackstageConfigName == "" { @@ -166,7 +174,7 @@ func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, back } r.addVolumeMounts(backstage, deployment, appConfigFileNamesMap) r.addContainerArgs(deployment, appConfigFileNamesMap) - r.addEnvVars(deployment) + r.addEnvVars(backstage, deployment, backendAuthSecretName) } err = r.Create(ctx, deployment) @@ -187,6 +195,52 @@ func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, back return nil } +func (r *BackstageReconciler) handleBackendAuthSecret(ctx context.Context, backstage bs.Backstage, ns string) (secretName string, err error) { + backendAuthSecretName := backstage.Spec.BackendAuthSecretRef.Name + if backendAuthSecretName == "" { + //Generate a secret if it does not exist + backendAuthSecretName = fmt.Sprintf("%s-auth", backstage.Name) + sec := &v1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "v1", + APIVersion: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: backendAuthSecretName, + Namespace: ns, + }, + } + err = r.Get(ctx, types.NamespacedName{Name: backendAuthSecretName, 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) + } + // Create a secret with a random value + 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 + } + return base64.StdEncoding.EncodeToString(bytes) + }(24) + k := backstage.Spec.BackendAuthSecretRef.Key + if k == "" { + //TODO(rm3l): why kubebuilder default values do not work + k = "backend-secret" + } + sec.Data = map[string][]byte{ + k: []byte(authVal), + } + err = r.Create(ctx, sec) + if err != nil { + return "", fmt.Errorf("failed to create secret for backend auth, reason: %s", err) + } + } + } + return backendAuthSecretName, nil +} + func (r *BackstageReconciler) addVolumes(backstage bs.Backstage, deployment *appsv1.Deployment) { for _, appConfig := range backstage.Spec.AppConfigs { var volumeSource v1.VolumeSource @@ -276,15 +330,33 @@ func (r *BackstageReconciler) addContainerArgs(deployment *appsv1.Deployment, ap } } -func (r *BackstageReconciler) addEnvVars(deployment *appsv1.Deployment) { +func (r *BackstageReconciler) addEnvVars(backstage bs.Backstage, deployment *appsv1.Deployment, backendAuthSecretName string) { + if backendAuthSecretName == "" { + return + } for i, c := range deployment.Spec.Template.Spec.Containers { if c.Name == _defaultBackstageMainContainerName { + k := backstage.Spec.BackendAuthSecretRef.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, - // TODO(rm3l): backend.auth.keys[0].secret is required for service-to-service communication. - // This is a default value that will be modifiable by the user using environment variables. + v1.EnvVar{ + Name: "BACKEND_SECRET", + ValueFrom: &v1.EnvVarSource{ + SecretKeyRef: &v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{ + Name: backendAuthSecretName, + }, + Key: k, + Optional: pointer.Bool(false), + }, + }, + }, v1.EnvVar{ Name: "APP_CONFIG_backend_auth_keys", - Value: fmt.Sprintf(`[{"secret": "%s"}]`, _defaultBackendAuthSecret), + Value: `[{"secret": "$(BACKEND_SECRET)"}]`, }) break } diff --git a/examples/janus-cr-with-app-configs.yaml b/examples/janus-cr-with-app-configs.yaml index 593751a2..ae6f6611 100644 --- a/examples/janus-cr-with-app-configs.yaml +++ b/examples/janus-cr-with-app-configs.yaml @@ -11,6 +11,20 @@ spec: dynamicPluginsConfig: name: my-dynamic-plugins-config-cm kind: ConfigMap + backendAuthSecretRef: + name: "my-backstage-backend-auth-secret" + key: "my-auth-key" + +--- +apiVersion: v1 +kind: Secret +metadata: + name: my-backstage-backend-auth-secret +data: + # generated with the command below (from https://backstage.io/docs/auth/service-to-service-auth/#setup): + # node -p 'require("crypto").randomBytes(24).toString("base64")' + backend-secret: TDRORDFRa2JxaFJhNTBzOGFDc1FWUEJ4ekFtRUw4UEU= + my-auth-key: TDRORDFRa2JxaFJhNTBzOGFDc1FWUEJ4ekFtRUw4UEU= --- apiVersion: v1 @@ -19,16 +33,20 @@ metadata: name: my-backstage-config-cm1 data: my-app-config.prod.yaml: | - backend: - auth: - keys: - - secret: ${BACKEND_SECRET} database: connection: password: ${POSTGRESQL_PASSWORD} user: ${POSTGRESQL_USER} my-app-config-2.yaml: | # Some comment in this file + my-app-config.odo.yaml: | + catalog: + locations: + # [...] + - type: url + target: https://github.com/rm3l/odo-backstage-golden-path-template/blob/main/template.yaml + rules: + - allow: [Template] --- apiVersion: v1 From 06309ddfaf38211c4b25627a950768d7fe5f444d Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Thu, 23 Nov 2023 10:20:56 +0100 Subject: [PATCH 05/18] Add tests for handling app-configs and dynamic plugins configs --- controllers/backstage_controller_test.go | 579 ++++++++++++++++++++--- 1 file changed, 524 insertions(+), 55 deletions(-) diff --git a/controllers/backstage_controller_test.go b/controllers/backstage_controller_test.go index 9c16337c..73cbae78 100644 --- a/controllers/backstage_controller_test.go +++ b/controllers/backstage_controller_test.go @@ -17,6 +17,7 @@ package controller import ( "context" "fmt" + "strings" "time" bsv1alphav1 "backstage.io/backstage-operator/api/v1alpha1" @@ -24,8 +25,10 @@ import ( . "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -70,6 +73,44 @@ var _ = Describe("Backstage controller", func() { }) }) + buildBackstageCR := func(spec bsv1alphav1.BackstageSpec) *bsv1alphav1.Backstage { + return &bsv1alphav1.Backstage{ + ObjectMeta: metav1.ObjectMeta{ + Name: backstageName, + Namespace: ns, + }, + Spec: spec, + } + } + + buildConfigMap := func(name string, data map[string]string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + }, + Data: data, + } + } + + buildSecret := func(name string, data map[string][]byte) *corev1.Secret { + return &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + }, + Data: data, + } + } + verifyBackstageInstance := func(ctx context.Context) { Eventually(func(g Gomega) { var backstage bsv1alphav1.Backstage @@ -80,17 +121,28 @@ var _ = Describe("Backstage controller", func() { }, time.Minute, time.Second).Should(Succeed()) } + findEnvVar := func(envVars []corev1.EnvVar, key string) (corev1.EnvVar, bool) { + return findElementByPredicate(envVars, func(envVar corev1.EnvVar) bool { + return envVar.Name == key + }) + } + + findVolume := func(vols []corev1.Volume, name string) (corev1.Volume, bool) { + return findElementByPredicate(vols, func(vol corev1.Volume) bool { + return vol.Name == name + }) + } + + findVolumeMount := func(mounts []corev1.VolumeMount, name string) (corev1.VolumeMount, bool) { + return findElementByPredicate(mounts, func(mount corev1.VolumeMount) bool { + return mount.Name == name + }) + } + When("creating default CR with no spec", func() { var backstage *bsv1alphav1.Backstage BeforeEach(func() { - backstage = &bsv1alphav1.Backstage{ - ObjectMeta: metav1.ObjectMeta{ - Name: backstageName, - Namespace: ns, - }, - Spec: bsv1alphav1.BackstageSpec{}, - } - + backstage = buildBackstageCR(bsv1alphav1.BackstageSpec{}) err := k8sClient.Create(ctx, backstage) Expect(err).To(Not(HaveOccurred())) }) @@ -108,15 +160,39 @@ var _ = Describe("Backstage controller", func() { }) Expect(err).To(Not(HaveOccurred())) + 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()) + By("Checking if Deployment was successfully created in the reconciliation") + found := &appsv1.Deployment{} Eventually(func() error { - found := &appsv1.Deployment{} // 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 the latest Status added to the Backstage instance") + 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 latest Status added to the Backstage instance") verifyBackstageInstance(ctx) }) }) @@ -126,16 +202,8 @@ var _ = Describe("Backstage controller", func() { var backstage *bsv1alphav1.Backstage BeforeEach(func() { - backstageConfigMap := &corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "ConfigMap", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "my-bs-config", - Namespace: ns, - }, - Data: map[string]string{ + backstageConfigMap := buildConfigMap("my-bs-config", + map[string]string{ "deploy": ` apiVersion: apps/v1 kind: Deployment @@ -157,22 +225,15 @@ spec: - name: bs1 image: busybox `, - }, - } + }) err := k8sClient.Create(ctx, backstageConfigMap) Expect(err).To(Not(HaveOccurred())) - backstage = &bsv1alphav1.Backstage{ - ObjectMeta: metav1.ObjectMeta{ - Name: backstageName, - Namespace: ns, - }, - Spec: bsv1alphav1.BackstageSpec{ - RawRuntimeConfig: bsv1alphav1.RuntimeConfig{ - BackstageConfigName: backstageConfigMap.Name, - }, + backstage = buildBackstageCR(bsv1alphav1.BackstageSpec{ + RawRuntimeConfig: bsv1alphav1.RuntimeConfig{ + BackstageConfigName: backstageConfigMap.Name, }, - } + }) err = k8sClient.Create(ctx, backstage) Expect(err).To(Not(HaveOccurred())) @@ -206,17 +267,8 @@ spec: var backstage *bsv1alphav1.Backstage BeforeEach(func() { - localDbConfigMap := &corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "ConfigMap", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "my-db-config", - Namespace: ns, - }, - Data: map[string]string{ - "statefulset": ` + localDbConfigMap := buildConfigMap("my-db-config", map[string]string{ + "statefulset": ` apiVersion: apps/v1 kind: StatefulSet metadata: @@ -235,22 +287,15 @@ spec: - name: db image: busybox `, - }, - } + }) err := k8sClient.Create(ctx, localDbConfigMap) Expect(err).To(Not(HaveOccurred())) - backstage = &bsv1alphav1.Backstage{ - ObjectMeta: metav1.ObjectMeta{ - Name: backstageName, - Namespace: ns, + backstage = buildBackstageCR(bsv1alphav1.BackstageSpec{ + RawRuntimeConfig: bsv1alphav1.RuntimeConfig{ + LocalDbConfigName: localDbConfigMap.Name, }, - Spec: bsv1alphav1.BackstageSpec{ - RawRuntimeConfig: bsv1alphav1.RuntimeConfig{ - LocalDbConfigName: localDbConfigMap.Name, - }, - }, - } + }) err = k8sClient.Create(ctx, backstage) Expect(err).To(Not(HaveOccurred())) @@ -283,4 +328,428 @@ 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 *bsv1alphav1.Backstage + + BeforeEach(func() { + backstage = buildBackstageCR(bsv1alphav1.BackstageSpec{ + AppConfigs: []bsv1alphav1.Config{ + { + Name: "a-non-existing-" + strings.ToLower(kind), + Kind: kind, + }, + }, + }) + 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 := &bsv1alphav1.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 _, 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 *bsv1alphav1.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 +`, + }) + 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 + switch dynamicPluginsConfigKind { + case "ConfigMap": + 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(` +# 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(bsv1alphav1.BackstageSpec{ + AppConfigs: []bsv1alphav1.Config{ + { + Name: appConfig1CmName, + Kind: "ConfigMap", + }, + { + Name: appConfig2SecretName, + Kind: "Secret", + }, + }, + DynamicPluginsConfig: bsv1alphav1.Config{ + 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 := &bsv1alphav1.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("/opt/app-root/src/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) + + }) + }) + } + }) + + 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 *bsv1alphav1.Backstage + BeforeEach(func() { + backstage = buildBackstageCR(bsv1alphav1.BackstageSpec{ + BackendAuthSecretRef: bsv1alphav1.BackendAuthSecretRef{ + 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 := &bsv1alphav1.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 *bsv1alphav1.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(bsv1alphav1.BackstageSpec{ + BackendAuthSecretRef: bsv1alphav1.BackendAuthSecretRef{ + 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 := &bsv1alphav1.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) + }) + }) + } + }) }) + +func findElementByPredicate[T any](l []T, predicate func(t T) bool) (T, bool) { + for _, v := range l { + if predicate(v) { + return v, true + } + } + var zero T + return zero, false +} From 0b72dc7fd293cbeb949e5c8b0cb4d244d11ce74d Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Thu, 23 Nov 2023 22:21:32 +0100 Subject: [PATCH 06/18] Preserve the iteration order of the AppConfigs in the Custom Resource This is useful for the order of the `--config ...` flags in the main container args. Note however that by design with Golang Maps, we can't guarantee the iteration order of the files listed inside each ConfigMap or Secret. --- controllers/backstage_deployment.go | 57 ++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/controllers/backstage_deployment.go b/controllers/backstage_deployment.go index 87ce7aa4..542f9b16 100644 --- a/controllers/backstage_deployment.go +++ b/controllers/backstage_deployment.go @@ -134,6 +134,11 @@ spec: `, _defaultBackstageMainContainerName) ) +type appConfigData struct { + ref string + files []string +} + func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, backstage bs.Backstage, ns string) error { //lg := log.FromContext(ctx) @@ -167,13 +172,13 @@ func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, back r.addVolumes(backstage, deployment) if backstage.Spec.RawRuntimeConfig.BackstageConfigName == "" { - var appConfigFileNamesMap map[string][]string - appConfigFileNamesMap, err = r.extractAppConfigFileNames(ctx, backstage, ns) + var appConfigFilenamesList []appConfigData + appConfigFilenamesList, err = r.extractAppConfigFileNames(ctx, backstage, ns) if err != nil { return err } - r.addVolumeMounts(backstage, deployment, appConfigFileNamesMap) - r.addContainerArgs(deployment, appConfigFileNamesMap) + r.addVolumeMounts(backstage, deployment, appConfigFilenamesList) + r.addContainerArgs(deployment, appConfigFilenamesList) r.addEnvVars(backstage, deployment, backendAuthSecretName) } @@ -284,7 +289,7 @@ func (r *BackstageReconciler) addVolumes(backstage bs.Backstage, deployment *app } } -func (r *BackstageReconciler) addVolumeMounts(backstage bs.Backstage, deployment *appsv1.Deployment, appConfigFileNamesMap map[string][]string) { +func (r *BackstageReconciler) addVolumeMounts(backstage bs.Backstage, deployment *appsv1.Deployment, appConfigFilenamesList []appConfigData) { if backstage.Spec.DynamicPluginsConfig.Name != "" { for i, c := range deployment.Spec.Template.Spec.InitContainers { if c.Name == _defaultBackstageInitContainerName { @@ -302,11 +307,11 @@ func (r *BackstageReconciler) addVolumeMounts(backstage bs.Backstage, deployment for i, c := range deployment.Spec.Template.Spec.Containers { if c.Name == _defaultBackstageMainContainerName { - for appConfigName := range appConfigFileNamesMap { + for _, appConfigFilenames := range appConfigFilenamesList { deployment.Spec.Template.Spec.Containers[i].VolumeMounts = append(deployment.Spec.Template.Spec.Containers[i].VolumeMounts, v1.VolumeMount{ - Name: appConfigName, - MountPath: "/opt/app-root/src/" + appConfigName, + Name: appConfigFilenames.ref, + MountPath: "/opt/app-root/src/" + appConfigFilenames.ref, }) } break @@ -314,15 +319,15 @@ func (r *BackstageReconciler) addVolumeMounts(backstage bs.Backstage, deployment } } -func (r *BackstageReconciler) addContainerArgs(deployment *appsv1.Deployment, appConfigFileNamesMap map[string][]string) { +func (r *BackstageReconciler) addContainerArgs(deployment *appsv1.Deployment, appConfigFilenamesList []appConfigData) { for i, c := range deployment.Spec.Template.Spec.Containers { if c.Name == _defaultBackstageMainContainerName { - for appConfigName, fileNames := range appConfigFileNamesMap { + for _, appConfigFilenames := range appConfigFilenamesList { // Args - for _, fileName := range fileNames { + for _, fileName := range appConfigFilenames.files { deployment.Spec.Template.Spec.Containers[i].Args = append(deployment.Spec.Template.Spec.Containers[i].Args, "--config", - fmt.Sprintf("/opt/app-root/src/%s/%s", appConfigName, fileName)) + fmt.Sprintf("/opt/app-root/src/%s/%s", appConfigFilenames.ref, fileName)) } } break @@ -363,9 +368,13 @@ func (r *BackstageReconciler) addEnvVars(backstage bs.Backstage, deployment *app } } -func (r *BackstageReconciler) extractAppConfigFileNames(ctx context.Context, backstage bs.Backstage, ns string) (map[string][]string, error) { - m := make(map[string][]string) +// 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 { + var files []string switch appConfig.Kind { case "ConfigMap": cm := v1.ConfigMap{} @@ -373,7 +382,12 @@ func (r *BackstageReconciler) extractAppConfigFileNames(ctx context.Context, bac return nil, err } for filename := range cm.Data { - m[appConfig.Name] = append(m[appConfig.Name], filename) + // 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 "Secret": sec := v1.Secret{} @@ -381,9 +395,18 @@ func (r *BackstageReconciler) extractAppConfigFileNames(ctx context.Context, bac return nil, err } for filename := range sec.Data { - m[appConfig.Name] = append(m[appConfig.Name], filename) + // Bear in mind that iteration order over this map is not guaranteed by Go + files = append(files, filename) + } + for filename := range sec.StringData { + // Bear in mind that iteration order over this map is not guaranteed by Go + files = append(files, filename) } } + result = append(result, appConfigData{ + ref: appConfig.Name, + files: files, + }) } - return m, nil + return result, nil } From 9789b6923bb96318b3be64ae67fda1e91ab0c0a2 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Fri, 24 Nov 2023 11:05:48 +0100 Subject: [PATCH 07/18] Similar to what the Helm Chart does, generate a ConfigMap for default config for dynamic plugins Otherwise, some plugins might not be enabled when using the basic CR --- controllers/backstage_controller_test.go | 83 +++++++++++++++++++++++- controllers/backstage_deployment.go | 81 ++++++++++++++++++----- 2 files changed, 145 insertions(+), 19 deletions(-) diff --git a/controllers/backstage_controller_test.go b/controllers/backstage_controller_test.go index 73cbae78..76cfb612 100644 --- a/controllers/backstage_controller_test.go +++ b/controllers/backstage_controller_test.go @@ -171,6 +171,18 @@ var _ = Describe("Backstage controller", func() { "backend auth secret should contain a non-empty 'backend-secret' in its data") }, time.Minute, time.Second).Should(Succeed()) + By("Generating a ConfigMap for default config for dynamic plugins") + dynamicPluginsConfigName := fmt.Sprintf("%s-dynamic-plugins", backstageName) + Eventually(func(g Gomega) { + found := &corev1.ConfigMap{} + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: dynamicPluginsConfigName}, found) + g.Expect(err).ShouldNot(HaveOccurred()) + + g.Expect(found.Data).To(HaveKey("dynamic-plugins.yaml")) + g.Expect(found.Data["dynamic-plugins.yaml"]).To(Not(BeEmpty()), + "default ConfigMap for dynamic plugins should contain a non-empty 'dynamic-plugins.yaml' in its data") + }, time.Minute, time.Second).Should(Succeed()) + By("Checking if Deployment was successfully created in the reconciliation") found := &appsv1.Deployment{} Eventually(func() error { @@ -192,6 +204,75 @@ var _ = Describe("Backstage controller", func() { 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)) + + _, 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") + + 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, 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(2)) + Expect(mainCont.Args[0]).To(Equal("--config")) + Expect(mainCont.Args[1]).To(Equal("dynamic-plugins-root/app-config.dynamic-plugins.yaml")) + }) + + 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()) + }) + By("Checking the latest Status added to the Backstage instance") verifyBackstageInstance(ctx) }) @@ -520,7 +601,7 @@ plugins: [] 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("/opt/app-root/src/dynamic-plugins-root")) + Expect(dpRoot.MountPath).To(Equal("/dynamic-plugins-root")) Expect(dpRoot.ReadOnly).To(BeFalse()) Expect(dpRoot.SubPath).To(BeEmpty()) diff --git a/controllers/backstage_deployment.go b/controllers/backstage_deployment.go index 542f9b16..3eae678b 100644 --- a/controllers/backstage_deployment.go +++ b/controllers/backstage_deployment.go @@ -73,7 +73,7 @@ spec: initContainers: - command: - ./install-dynamic-plugins.sh - - dynamic-plugins-root + - /dynamic-plugins-root env: - name: NPM_CONFIG_USERCONFIG value: /opt/app-root/src/.npmrc.dynamic-plugins @@ -81,7 +81,7 @@ spec: imagePullPolicy: IfNotPresent name: install-dynamic-plugins volumeMounts: - - mountPath: /opt/app-root/src/dynamic-plugins-root + - mountPath: /dynamic-plugins-root name: dynamic-plugins-root - mountPath: /opt/app-root/src/.npmrc.dynamic-plugins name: dynamic-plugins-npmrc @@ -164,20 +164,25 @@ func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, back } } - backendAuthSecretName, err := r.handleBackendAuthSecret(ctx, backstage, ns) - if err != nil { - return err - } + if backstage.Spec.RawRuntimeConfig.BackstageConfigName == "" { + backendAuthSecretName, err := r.handleBackendAuthSecret(ctx, backstage, ns) + if err != nil { + return err + } - r.addVolumes(backstage, deployment) + dpConf, err := r.getOrGenerateDynamicPluginsConf(ctx, backstage, ns) + if err != nil { + return err + } + + r.addVolumes(backstage, dpConf, deployment) - if backstage.Spec.RawRuntimeConfig.BackstageConfigName == "" { var appConfigFilenamesList []appConfigData appConfigFilenamesList, err = r.extractAppConfigFileNames(ctx, backstage, ns) if err != nil { return err } - r.addVolumeMounts(backstage, deployment, appConfigFilenamesList) + r.addVolumeMounts(deployment, dpConf, appConfigFilenamesList) r.addContainerArgs(deployment, appConfigFilenamesList) r.addEnvVars(backstage, deployment, backendAuthSecretName) } @@ -200,6 +205,46 @@ func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, back return nil } +func (r *BackstageReconciler) getOrGenerateDynamicPluginsConf(ctx context.Context, backstage bs.Backstage, ns string) (config bs.Config, err error) { + if backstage.Spec.DynamicPluginsConfig.Name != "" { + return backstage.Spec.DynamicPluginsConfig, nil + } + //Generate a default ConfigMap for dynamic plugins + dpConfigName := fmt.Sprintf("%s-dynamic-plugins", backstage.Name) + conf := bs.Config{ + Name: dpConfigName, + Kind: "ConfigMap", + } + cm := &v1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "v1", + APIVersion: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: dpConfigName, + Namespace: ns, + }, + } + err = r.Get(ctx, types.NamespacedName{Name: dpConfigName, Namespace: ns}, cm) + if err != nil { + if !errors.IsNotFound(err) { + return bs.Config{}, fmt.Errorf("failed to get config map for dynamic plugins (%q), reason: %s", dpConfigName, err) + } + cm.Data = map[string]string{ + "dynamic-plugins.yaml": ` +includes: +- dynamic-plugins.default.yaml +plugins: [] +`, + } + err = r.Create(ctx, cm) + if err != nil { + return bs.Config{}, fmt.Errorf("failed to create config map for dynamic plugins, reason: %s", err) + } + } + return conf, nil +} + func (r *BackstageReconciler) handleBackendAuthSecret(ctx context.Context, backstage bs.Backstage, ns string) (secretName string, err error) { backendAuthSecretName := backstage.Spec.BackendAuthSecretRef.Name if backendAuthSecretName == "" { @@ -246,7 +291,7 @@ func (r *BackstageReconciler) handleBackendAuthSecret(ctx context.Context, backs return backendAuthSecretName, nil } -func (r *BackstageReconciler) addVolumes(backstage bs.Backstage, deployment *appsv1.Deployment) { +func (r *BackstageReconciler) addVolumes(backstage bs.Backstage, dynamicPluginsConf bs.Config, deployment *appsv1.Deployment) { for _, appConfig := range backstage.Spec.AppConfigs { var volumeSource v1.VolumeSource switch appConfig.Kind { @@ -267,35 +312,35 @@ func (r *BackstageReconciler) addVolumes(backstage bs.Backstage, deployment *app VolumeSource: volumeSource, }) } - if backstage.Spec.DynamicPluginsConfig.Name != "" { + if dynamicPluginsConf.Name != "" { var volumeSource v1.VolumeSource - switch backstage.Spec.DynamicPluginsConfig.Kind { + switch dynamicPluginsConf.Kind { case "ConfigMap": volumeSource.ConfigMap = &v1.ConfigMapVolumeSource{ DefaultMode: pointer.Int32(420), - LocalObjectReference: v1.LocalObjectReference{Name: backstage.Spec.DynamicPluginsConfig.Name}, + LocalObjectReference: v1.LocalObjectReference{Name: dynamicPluginsConf.Name}, } case "Secret": volumeSource.Secret = &v1.SecretVolumeSource{ DefaultMode: pointer.Int32(420), - SecretName: backstage.Spec.DynamicPluginsConfig.Name, + SecretName: dynamicPluginsConf.Name, } } deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, v1.Volume{ - Name: backstage.Spec.DynamicPluginsConfig.Name, + Name: dynamicPluginsConf.Name, VolumeSource: volumeSource, }) } } -func (r *BackstageReconciler) addVolumeMounts(backstage bs.Backstage, deployment *appsv1.Deployment, appConfigFilenamesList []appConfigData) { - if backstage.Spec.DynamicPluginsConfig.Name != "" { +func (r *BackstageReconciler) addVolumeMounts(deployment *appsv1.Deployment, dynamicPluginsConf bs.Config, appConfigFilenamesList []appConfigData) { + if dynamicPluginsConf.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: backstage.Spec.DynamicPluginsConfig.Name, + Name: dynamicPluginsConf.Name, MountPath: "/opt/app-root/src/dynamic-plugins.yaml", ReadOnly: true, SubPath: "dynamic-plugins.yaml", From a1a3f64b99662b12db3bd19b00e80417c988c489 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Fri, 24 Nov 2023 14:22:02 +0100 Subject: [PATCH 08/18] fixup! Handle app-configs --- examples/janus-cr-with-app-configs.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/janus-cr-with-app-configs.yaml b/examples/janus-cr-with-app-configs.yaml index ae6f6611..8e9c8ea5 100644 --- a/examples/janus-cr-with-app-configs.yaml +++ b/examples/janus-cr-with-app-configs.yaml @@ -33,6 +33,7 @@ metadata: name: my-backstage-config-cm1 data: my-app-config.prod.yaml: | + backend: database: connection: password: ${POSTGRESQL_PASSWORD} From 6a84a40e707bda67f84340975e981501bc6b5fae Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Fri, 24 Nov 2023 14:23:55 +0100 Subject: [PATCH 09/18] Add more comment on how and where the optional backend auth secret is being used Co-authored-by: Tomas Kral --- api/v1alpha1/backstage_types.go | 4 ++++ config/crd/bases/backstage.io_backstages.yaml | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index 30a832dd..1a602d4d 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -29,6 +29,10 @@ type BackstageSpec struct { AppConfigs []Config `json:"appConfigs,omitempty"` // 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. BackendAuthSecretRef BackendAuthSecretRef `json:"backendAuthSecretRef,omitempty"` // Dynamic Plugins configuration diff --git a/config/crd/bases/backstage.io_backstages.yaml b/config/crd/bases/backstage.io_backstages.yaml index 8d5267c1..fac33fd8 100644 --- a/config/crd/bases/backstage.io_backstages.yaml +++ b/config/crd/bases/backstage.io_backstages.yaml @@ -50,7 +50,11 @@ spec: type: array backendAuthSecretRef: description: Optional Backend Auth Secret Name. A new one will be - generated if not set. + 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 From 5c49c661d257cd44409dffa66ae2d2c2265ab2f6 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 27 Nov 2023 16:42:19 +0100 Subject: [PATCH 10/18] Break Config struct into more specialized structs and add more documentation about the behavior Co-authored-by: Gennady Azarenkov --- api/v1alpha1/backstage_types.go | 31 ++++++++++++++++--- api/v1alpha1/zz_generated.deepcopy.go | 25 ++++++++++++--- config/crd/bases/backstage.io_backstages.yaml | 23 ++++++++++++-- controllers/backstage_controller_test.go | 6 ++-- controllers/backstage_deployment.go | 12 +++---- 5 files changed, 76 insertions(+), 21 deletions(-) diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index 1a602d4d..ad5e8510 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -25,8 +25,15 @@ const ( // BackstageSpec defines the desired state of Backstage type BackstageSpec struct { - // Backstage application AppConfigs - AppConfigs []Config `json:"appConfigs,omitempty"` + // 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"` // 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 @@ -35,8 +42,10 @@ type BackstageSpec struct { // This is required for service-to-service auth and is shared by all backend plugins. BackendAuthSecretRef BackendAuthSecretRef `json:"backendAuthSecretRef,omitempty"` - // Dynamic Plugins configuration - DynamicPluginsConfig Config `json:"dynamicPluginsConfig,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"` @@ -45,8 +54,20 @@ type BackstageSpec struct { SkipLocalDb bool `json:"skipLocalDb,omitempty"` } -type Config struct { +type AppConfigRef struct { + // Name of an existing App Config object Name string `json:"name,omitempty"` + + // Type of the existing App Config object, either ConfigMap or Secret + //+kubebuilder:validation:Enum=ConfigMap;Secret + Kind string `json:"kind,omitempty"` +} + +type DynamicPluginsConfigRef struct { + // Name of the Dynamic Plugins config object + Name string `json:"name,omitempty"` + + // Type of the Dynamic Plugins config object, either ConfigMap or Secret //+kubebuilder:validation:Enum=ConfigMap;Secret Kind string `json:"kind,omitempty"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 64bc2cc5..b6de138b 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -26,6 +26,21 @@ import ( 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 *AppConfigRef) DeepCopyInto(out *AppConfigRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppConfigRef. +func (in *AppConfigRef) DeepCopy() *AppConfigRef { + if in == nil { + return nil + } + out := new(AppConfigRef) + 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 @@ -105,7 +120,7 @@ func (in *BackstageSpec) DeepCopyInto(out *BackstageSpec) { *out = *in if in.AppConfigs != nil { in, out := &in.AppConfigs, &out.AppConfigs - *out = make([]Config, len(*in)) + *out = make([]AppConfigRef, len(*in)) copy(*out, *in) } out.BackendAuthSecretRef = in.BackendAuthSecretRef @@ -146,16 +161,16 @@ func (in *BackstageStatus) DeepCopy() *BackstageStatus { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Config) DeepCopyInto(out *Config) { +func (in *DynamicPluginsConfigRef) DeepCopyInto(out *DynamicPluginsConfigRef) { *out = *in } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Config. -func (in *Config) DeepCopy() *Config { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DynamicPluginsConfigRef. +func (in *DynamicPluginsConfigRef) DeepCopy() *DynamicPluginsConfigRef { if in == nil { return nil } - out := new(Config) + out := new(DynamicPluginsConfigRef) in.DeepCopyInto(out) return out } diff --git a/config/crd/bases/backstage.io_backstages.yaml b/config/crd/bases/backstage.io_backstages.yaml index fac33fd8..e2a2b82b 100644 --- a/config/crd/bases/backstage.io_backstages.yaml +++ b/config/crd/bases/backstage.io_backstages.yaml @@ -36,15 +36,29 @@ spec: description: BackstageSpec defines the desired state of Backstage properties: appConfigs: - description: Backstage application 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 type: object type: array @@ -66,14 +80,19 @@ spec: type: string type: object dynamicPluginsConfig: - description: Dynamic Plugins configuration + 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: 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 type: object rawRuntimeConfig: diff --git a/controllers/backstage_controller_test.go b/controllers/backstage_controller_test.go index 76cfb612..a1e85f67 100644 --- a/controllers/backstage_controller_test.go +++ b/controllers/backstage_controller_test.go @@ -418,7 +418,7 @@ spec: BeforeEach(func() { backstage = buildBackstageCR(bsv1alphav1.BackstageSpec{ - AppConfigs: []bsv1alphav1.Config{ + AppConfigs: []bsv1alphav1.AppConfigRef{ { Name: "a-non-existing-" + strings.ToLower(kind), Kind: kind, @@ -510,7 +510,7 @@ plugins: [] Expect(err).To(Not(HaveOccurred())) backstage = buildBackstageCR(bsv1alphav1.BackstageSpec{ - AppConfigs: []bsv1alphav1.Config{ + AppConfigs: []bsv1alphav1.AppConfigRef{ { Name: appConfig1CmName, Kind: "ConfigMap", @@ -520,7 +520,7 @@ plugins: [] Kind: "Secret", }, }, - DynamicPluginsConfig: bsv1alphav1.Config{ + DynamicPluginsConfig: bsv1alphav1.DynamicPluginsConfigRef{ Name: dynamicPluginsConfigName, Kind: dynamicPluginsConfigKind, }, diff --git a/controllers/backstage_deployment.go b/controllers/backstage_deployment.go index 3eae678b..64b127e8 100644 --- a/controllers/backstage_deployment.go +++ b/controllers/backstage_deployment.go @@ -205,13 +205,13 @@ func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, back return nil } -func (r *BackstageReconciler) getOrGenerateDynamicPluginsConf(ctx context.Context, backstage bs.Backstage, ns string) (config bs.Config, err error) { +func (r *BackstageReconciler) getOrGenerateDynamicPluginsConf(ctx context.Context, backstage bs.Backstage, ns string) (config bs.DynamicPluginsConfigRef, err error) { if backstage.Spec.DynamicPluginsConfig.Name != "" { return backstage.Spec.DynamicPluginsConfig, nil } //Generate a default ConfigMap for dynamic plugins dpConfigName := fmt.Sprintf("%s-dynamic-plugins", backstage.Name) - conf := bs.Config{ + conf := bs.DynamicPluginsConfigRef{ Name: dpConfigName, Kind: "ConfigMap", } @@ -228,7 +228,7 @@ 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.Config{}, fmt.Errorf("failed to get config map for dynamic plugins (%q), reason: %s", dpConfigName, err) + return bs.DynamicPluginsConfigRef{}, fmt.Errorf("failed to get config map for dynamic plugins (%q), reason: %s", dpConfigName, err) } cm.Data = map[string]string{ "dynamic-plugins.yaml": ` @@ -239,7 +239,7 @@ plugins: [] } err = r.Create(ctx, cm) if err != nil { - return bs.Config{}, fmt.Errorf("failed to create config map for dynamic plugins, reason: %s", err) + return bs.DynamicPluginsConfigRef{}, fmt.Errorf("failed to create config map for dynamic plugins, reason: %s", err) } } return conf, nil @@ -291,7 +291,7 @@ func (r *BackstageReconciler) handleBackendAuthSecret(ctx context.Context, backs return backendAuthSecretName, nil } -func (r *BackstageReconciler) addVolumes(backstage bs.Backstage, dynamicPluginsConf bs.Config, deployment *appsv1.Deployment) { +func (r *BackstageReconciler) addVolumes(backstage bs.Backstage, dynamicPluginsConf bs.DynamicPluginsConfigRef, deployment *appsv1.Deployment) { for _, appConfig := range backstage.Spec.AppConfigs { var volumeSource v1.VolumeSource switch appConfig.Kind { @@ -334,7 +334,7 @@ func (r *BackstageReconciler) addVolumes(backstage bs.Backstage, dynamicPluginsC } } -func (r *BackstageReconciler) addVolumeMounts(deployment *appsv1.Deployment, dynamicPluginsConf bs.Config, appConfigFilenamesList []appConfigData) { +func (r *BackstageReconciler) addVolumeMounts(deployment *appsv1.Deployment, dynamicPluginsConf bs.DynamicPluginsConfigRef, appConfigFilenamesList []appConfigData) { if dynamicPluginsConf.Name != "" { for i, c := range deployment.Spec.Template.Spec.InitContainers { if c.Name == _defaultBackstageInitContainerName { From efd324ee34f2aa605a9171504d6271aedf0bf4a9 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 27 Nov 2023 16:56:46 +0100 Subject: [PATCH 11/18] Do not check if a Backstage ``rawRuntimeConfig` config name has been provided before checking if we needed to create default backend auth secret or dynamic plugins configmap The goal is to validate/refine the default (or raw) deployment with additional application-specific fields specified in the CR. Co-authored-by: Gennady Azarenkov --- controllers/backstage_deployment.go | 40 +++++++++++++++-------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/controllers/backstage_deployment.go b/controllers/backstage_deployment.go index 64b127e8..b99855fe 100644 --- a/controllers/backstage_deployment.go +++ b/controllers/backstage_deployment.go @@ -159,33 +159,35 @@ func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, back r.labels(&deployment.ObjectMeta, backstage) if r.OwnsRuntime { - if err := controllerutil.SetControllerReference(&backstage, deployment, r.Scheme); err != nil { + if err = controllerutil.SetControllerReference(&backstage, deployment, r.Scheme); err != nil { return fmt.Errorf("failed to set owner reference: %s", err) } } - if backstage.Spec.RawRuntimeConfig.BackstageConfigName == "" { - backendAuthSecretName, err := r.handleBackendAuthSecret(ctx, backstage, ns) - if err != nil { - return err - } + var ( + backendAuthSecretName string + dpConf bs.DynamicPluginsConfigRef + appConfigFilenamesList []appConfigData + ) + backendAuthSecretName, err = r.handleBackendAuthSecret(ctx, backstage, ns) + if err != nil { + return err + } - dpConf, err := r.getOrGenerateDynamicPluginsConf(ctx, backstage, ns) - if err != nil { - return err - } + dpConf, err = r.getOrGenerateDynamicPluginsConf(ctx, backstage, ns) + if err != nil { + return err + } - r.addVolumes(backstage, dpConf, deployment) + r.addVolumes(backstage, dpConf, deployment) - var appConfigFilenamesList []appConfigData - appConfigFilenamesList, err = r.extractAppConfigFileNames(ctx, backstage, ns) - if err != nil { - return err - } - r.addVolumeMounts(deployment, dpConf, appConfigFilenamesList) - r.addContainerArgs(deployment, appConfigFilenamesList) - r.addEnvVars(backstage, deployment, backendAuthSecretName) + appConfigFilenamesList, err = r.extractAppConfigFileNames(ctx, backstage, ns) + if err != nil { + return err } + r.addVolumeMounts(deployment, dpConf, appConfigFilenamesList) + r.addContainerArgs(deployment, appConfigFilenamesList) + r.addEnvVars(backstage, deployment, backendAuthSecretName) err = r.Create(ctx, deployment) if err != nil { From 0b1ad2764bf49a6724bd20a9d1e913206697b218 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 27 Nov 2023 18:06:27 +0100 Subject: [PATCH 12/18] Improve readability by moving specialized methods and functions in dedicated files Co-authored-by: Gennady Azarenkov --- controllers/backstage_app_config.go | 143 +++++++++++ controllers/backstage_backend_auth.go | 116 +++++++++ controllers/backstage_deployment.go | 307 +++-------------------- controllers/backstage_dynamic_plugins.go | 123 +++++++++ 4 files changed, 417 insertions(+), 272 deletions(-) create mode 100644 controllers/backstage_app_config.go create mode 100644 controllers/backstage_backend_auth.go create mode 100644 controllers/backstage_dynamic_plugins.go diff --git a/controllers/backstage_app_config.go b/controllers/backstage_app_config.go new file mode 100644 index 00000000..23ddb553 --- /dev/null +++ b/controllers/backstage_app_config.go @@ -0,0 +1,143 @@ +// +// 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 "backstage.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" +) + +type appConfigData struct { + ref string + files []string +} + +func (r *BackstageReconciler) appConfigsToVolumes(backstage bs.Backstage) (result []v1.Volume) { + for _, appConfig := range backstage.Spec.AppConfigs { + var volumeSource v1.VolumeSource + switch appConfig.Kind { + case "ConfigMap": + volumeSource.ConfigMap = &v1.ConfigMapVolumeSource{ + DefaultMode: pointer.Int32(420), + LocalObjectReference: v1.LocalObjectReference{Name: appConfig.Name}, + } + case "Secret": + volumeSource.Secret = &v1.SecretVolumeSource{ + DefaultMode: pointer.Int32(420), + SecretName: appConfig.Name, + } + } + result = append(result, + v1.Volume{ + Name: appConfig.Name, + VolumeSource: volumeSource, + }, + ) + } + + return result +} + +func (r *BackstageReconciler) addAppConfigsVolumeMounts(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { + appConfigFilenamesList, err := r.extractAppConfigFileNames(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 { + 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), + }) + } + break + } + } + return nil +} + +func (r *BackstageReconciler) addAppConfigsContainerArgs(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { + appConfigFilenamesList, err := r.extractAppConfigFileNames(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 { + // Args + for _, fileName := range appConfigFilenames.files { + 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)) + } + } + break + } + } + return nil +} + +// 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 { + var files []string + switch appConfig.Kind { + case "ConfigMap": + cm := v1.ConfigMap{} + if err := r.Get(ctx, types.NamespacedName{Name: appConfig.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 "Secret": + sec := v1.Secret{} + if err := r.Get(ctx, types.NamespacedName{Name: appConfig.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 sec.StringData { + // Bear in mind that iteration order over this map is not guaranteed by Go + files = append(files, filename) + } + } + result = append(result, appConfigData{ + ref: appConfig.Name, + files: files, + }) + } + return result, nil +} diff --git a/controllers/backstage_backend_auth.go b/controllers/backstage_backend_auth.go new file mode 100644 index 00000000..b6e25d0b --- /dev/null +++ b/controllers/backstage_backend_auth.go @@ -0,0 +1,116 @@ +// +// 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" + "crypto/rand" + "encoding/base64" + "fmt" + + bs "backstage.io/backstage-operator/api/v1alpha1" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/pointer" +) + +func (r *BackstageReconciler) handleBackendAuthSecret(ctx context.Context, backstage bs.Backstage, ns string) (secretName string, err error) { + backendAuthSecretName := backstage.Spec.BackendAuthSecretRef.Name + if backendAuthSecretName == "" { + //Generate a secret if it does not exist + backendAuthSecretName = fmt.Sprintf("%s-auth", backstage.Name) + sec := &v1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "v1", + APIVersion: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: backendAuthSecretName, + Namespace: ns, + }, + } + err = r.Get(ctx, types.NamespacedName{Name: backendAuthSecretName, 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) + } + // Create a secret with a random value + 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 + } + return base64.StdEncoding.EncodeToString(bytes) + }(24) + k := backstage.Spec.BackendAuthSecretRef.Key + if k == "" { + //TODO(rm3l): why kubebuilder default values do not work + k = "backend-secret" + } + sec.Data = map[string][]byte{ + k: []byte(authVal), + } + err = r.Create(ctx, sec) + if err != nil { + return "", fmt.Errorf("failed to create secret for backend auth, reason: %s", err) + } + } + } + return backendAuthSecretName, nil +} + +func (r *BackstageReconciler) addBackendAuthEnvVar(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { + backendAuthSecretName, err := r.handleBackendAuthSecret(ctx, backstage, ns) + if err != nil { + return err + } + + if backendAuthSecretName == "" { + return nil + } + + for i, c := range deployment.Spec.Template.Spec.Containers { + if c.Name == _defaultBackstageMainContainerName { + k := backstage.Spec.BackendAuthSecretRef.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, + }, + 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 b99855fe..0992308b 100644 --- a/controllers/backstage_deployment.go +++ b/controllers/backstage_deployment.go @@ -16,17 +16,12 @@ package controller import ( "context" - "crypto/rand" - "encoding/base64" "fmt" bs "backstage.io/backstage-operator/api/v1alpha1" appsv1 "k8s.io/api/apps/v1" - v1 "k8s.io/api/core/v1" "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/controller/controllerutil" ) @@ -34,6 +29,7 @@ const ( _defaultBackstageInitContainerName = "install-dynamic-plugins" _defaultBackstageMainContainerName = "backstage-backend" _defaultBackendAuthSecretValue = "pl4s3Ch4ng3M3" + _containersWorkingDir = "/opt/app-root/src" ) var ( @@ -76,21 +72,21 @@ spec: - /dynamic-plugins-root env: - name: NPM_CONFIG_USERCONFIG - value: /opt/app-root/src/.npmrc.dynamic-plugins + value: %[3]s/.npmrc.dynamic-plugins image: 'quay.io/janus-idp/backstage-showcase:next' imagePullPolicy: IfNotPresent - name: install-dynamic-plugins + name: %[1]s volumeMounts: - mountPath: /dynamic-plugins-root name: dynamic-plugins-root - - mountPath: /opt/app-root/src/.npmrc.dynamic-plugins + - mountPath: %[3]s/.npmrc.dynamic-plugins name: dynamic-plugins-npmrc readOnly: true subPath: .npmrc - workingDir: /opt/app-root/src + workingDir: %[3]s containers: - - name: %s + - name: %[2]s image: quay.io/janus-idp/backstage-showcase:next imagePullPolicy: IfNotPresent args: @@ -119,7 +115,6 @@ spec: ports: - name: http containerPort: 7007 -# TODO Handle user-defined env vars env: - name: APP_CONFIG_backend_listen_port value: "7007" @@ -129,16 +124,11 @@ spec: # - secretRef: # name: backstage-secrets volumeMounts: - - mountPath: /opt/app-root/src/dynamic-plugins-root + - mountPath: %[3]s/dynamic-plugins-root name: dynamic-plugins-root -`, _defaultBackstageMainContainerName) +`, _defaultBackstageInitContainerName, _defaultBackstageMainContainerName, _containersWorkingDir) ) -type appConfigData struct { - ref string - files []string -} - func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, backstage bs.Backstage, ns string) error { //lg := log.FromContext(ctx) @@ -164,34 +154,29 @@ func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, back } } - var ( - backendAuthSecretName string - dpConf bs.DynamicPluginsConfigRef - appConfigFilenamesList []appConfigData - ) - backendAuthSecretName, err = r.handleBackendAuthSecret(ctx, backstage, ns) + err = r.addVolumes(ctx, backstage, ns, deployment) if err != nil { - return err + return fmt.Errorf("failed to add volumes to Backstage deployment, reason: %s", err) } - dpConf, err = r.getOrGenerateDynamicPluginsConf(ctx, backstage, ns) + err = r.addVolumeMounts(ctx, backstage, ns, deployment) if err != nil { - return err + return fmt.Errorf("failed to add volume mounts to Backstage deployment, reason: %s", err) } - r.addVolumes(backstage, dpConf, deployment) + err = r.addContainerArgs(ctx, backstage, ns, deployment) + if err != nil { + return fmt.Errorf("failed to add container args to Backstage deployment, reason: %s", err) + } - appConfigFilenamesList, err = r.extractAppConfigFileNames(ctx, backstage, ns) + err = r.addEnvVars(ctx, backstage, ns, deployment) if err != nil { - return err + return fmt.Errorf("failed to add env vars to Backstage deployment, reason: %s", err) } - r.addVolumeMounts(deployment, dpConf, appConfigFilenamesList) - r.addContainerArgs(deployment, appConfigFilenamesList) - r.addEnvVars(backstage, deployment, backendAuthSecretName) err = r.Create(ctx, deployment) if err != nil { - return fmt.Errorf("failed to create backstage deplyment, reason: %s", err) + return fmt.Errorf("failed to create backstage deployment, reason: %s", err) } } else { @@ -207,253 +192,31 @@ func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, back return nil } -func (r *BackstageReconciler) getOrGenerateDynamicPluginsConf(ctx context.Context, backstage bs.Backstage, ns string) (config bs.DynamicPluginsConfigRef, err error) { - if backstage.Spec.DynamicPluginsConfig.Name != "" { - return backstage.Spec.DynamicPluginsConfig, nil - } - //Generate a default ConfigMap for dynamic plugins - dpConfigName := fmt.Sprintf("%s-dynamic-plugins", backstage.Name) - conf := bs.DynamicPluginsConfigRef{ - Name: dpConfigName, - Kind: "ConfigMap", - } - cm := &v1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - Kind: "v1", - APIVersion: "ConfigMap", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: dpConfigName, - Namespace: ns, - }, - } - err = r.Get(ctx, types.NamespacedName{Name: dpConfigName, Namespace: ns}, cm) +func (r *BackstageReconciler) addVolumes(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { + dpConfVol, err := r.getDynamicPluginsConfVolume(ctx, backstage, ns) 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) - } - cm.Data = map[string]string{ - "dynamic-plugins.yaml": ` -includes: -- dynamic-plugins.default.yaml -plugins: [] -`, - } - 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 err } - return conf, nil -} - -func (r *BackstageReconciler) handleBackendAuthSecret(ctx context.Context, backstage bs.Backstage, ns string) (secretName string, err error) { - backendAuthSecretName := backstage.Spec.BackendAuthSecretRef.Name - if backendAuthSecretName == "" { - //Generate a secret if it does not exist - backendAuthSecretName = fmt.Sprintf("%s-auth", backstage.Name) - sec := &v1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "v1", - APIVersion: "Secret", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: backendAuthSecretName, - Namespace: ns, - }, - } - err = r.Get(ctx, types.NamespacedName{Name: backendAuthSecretName, 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) - } - // Create a secret with a random value - 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 - } - return base64.StdEncoding.EncodeToString(bytes) - }(24) - k := backstage.Spec.BackendAuthSecretRef.Key - if k == "" { - //TODO(rm3l): why kubebuilder default values do not work - k = "backend-secret" - } - sec.Data = map[string][]byte{ - k: []byte(authVal), - } - err = r.Create(ctx, sec) - if err != nil { - return "", fmt.Errorf("failed to create secret for backend auth, reason: %s", err) - } - } + if dpConfVol != nil { + deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, *dpConfVol) } - return backendAuthSecretName, nil -} -func (r *BackstageReconciler) addVolumes(backstage bs.Backstage, dynamicPluginsConf bs.DynamicPluginsConfigRef, deployment *appsv1.Deployment) { - for _, appConfig := range backstage.Spec.AppConfigs { - var volumeSource v1.VolumeSource - switch appConfig.Kind { - case "ConfigMap": - volumeSource.ConfigMap = &v1.ConfigMapVolumeSource{ - DefaultMode: pointer.Int32(420), - LocalObjectReference: v1.LocalObjectReference{Name: appConfig.Name}, - } - case "Secret": - volumeSource.Secret = &v1.SecretVolumeSource{ - DefaultMode: pointer.Int32(420), - SecretName: appConfig.Name, - } - } - deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, - v1.Volume{ - Name: appConfig.Name, - VolumeSource: volumeSource, - }) - } - if dynamicPluginsConf.Name != "" { - var volumeSource v1.VolumeSource - switch dynamicPluginsConf.Kind { - case "ConfigMap": - volumeSource.ConfigMap = &v1.ConfigMapVolumeSource{ - DefaultMode: pointer.Int32(420), - LocalObjectReference: v1.LocalObjectReference{Name: dynamicPluginsConf.Name}, - } - case "Secret": - volumeSource.Secret = &v1.SecretVolumeSource{ - DefaultMode: pointer.Int32(420), - SecretName: dynamicPluginsConf.Name, - } - } - deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, - v1.Volume{ - Name: dynamicPluginsConf.Name, - VolumeSource: volumeSource, - }) - } -} - -func (r *BackstageReconciler) addVolumeMounts(deployment *appsv1.Deployment, dynamicPluginsConf bs.DynamicPluginsConfigRef, appConfigFilenamesList []appConfigData) { - if dynamicPluginsConf.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: dynamicPluginsConf.Name, - MountPath: "/opt/app-root/src/dynamic-plugins.yaml", - ReadOnly: true, - SubPath: "dynamic-plugins.yaml", - }) - break - } - } - } - - 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: "/opt/app-root/src/" + appConfigFilenames.ref, - }) - } - break - } - } + deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, r.appConfigsToVolumes(backstage)...) + return nil } -func (r *BackstageReconciler) addContainerArgs(deployment *appsv1.Deployment, appConfigFilenamesList []appConfigData) { - for i, c := range deployment.Spec.Template.Spec.Containers { - if c.Name == _defaultBackstageMainContainerName { - for _, appConfigFilenames := range appConfigFilenamesList { - // Args - for _, fileName := range appConfigFilenames.files { - deployment.Spec.Template.Spec.Containers[i].Args = - append(deployment.Spec.Template.Spec.Containers[i].Args, "--config", - fmt.Sprintf("/opt/app-root/src/%s/%s", appConfigFilenames.ref, fileName)) - } - } - break - } +func (r *BackstageReconciler) addVolumeMounts(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { + err := r.addDynamicPluginsConfVolumeMount(ctx, backstage, ns, deployment) + if err != nil { + return err } + return r.addAppConfigsVolumeMounts(ctx, backstage, ns, deployment) } -func (r *BackstageReconciler) addEnvVars(backstage bs.Backstage, deployment *appsv1.Deployment, backendAuthSecretName string) { - if backendAuthSecretName == "" { - return - } - for i, c := range deployment.Spec.Template.Spec.Containers { - if c.Name == _defaultBackstageMainContainerName { - k := backstage.Spec.BackendAuthSecretRef.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, - }, - Key: k, - Optional: pointer.Bool(false), - }, - }, - }, - v1.EnvVar{ - Name: "APP_CONFIG_backend_auth_keys", - Value: `[{"secret": "$(BACKEND_SECRET)"}]`, - }) - break - } - } +func (r *BackstageReconciler) addContainerArgs(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { + return r.addAppConfigsContainerArgs(ctx, backstage, ns, deployment) } -// 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 { - var files []string - switch appConfig.Kind { - case "ConfigMap": - cm := v1.ConfigMap{} - if err := r.Get(ctx, types.NamespacedName{Name: appConfig.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 "Secret": - sec := v1.Secret{} - if err := r.Get(ctx, types.NamespacedName{Name: appConfig.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 sec.StringData { - // Bear in mind that iteration order over this map is not guaranteed by Go - files = append(files, filename) - } - } - result = append(result, appConfigData{ - ref: appConfig.Name, - files: files, - }) - } - return result, nil +func (r *BackstageReconciler) addEnvVars(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { + return r.addBackendAuthEnvVar(ctx, backstage, ns, deployment) } diff --git a/controllers/backstage_dynamic_plugins.go b/controllers/backstage_dynamic_plugins.go new file mode 100644 index 00000000..849d9b31 --- /dev/null +++ b/controllers/backstage_dynamic_plugins.go @@ -0,0 +1,123 @@ +// +// 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 "backstage.io/backstage-operator/api/v1alpha1" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/pointer" +) + +func (r *BackstageReconciler) getOrGenerateDynamicPluginsConf(ctx context.Context, backstage bs.Backstage, ns string) (config bs.DynamicPluginsConfigRef, err error) { + if backstage.Spec.DynamicPluginsConfig.Name != "" { + return backstage.Spec.DynamicPluginsConfig, nil + } + //Generate a default ConfigMap for dynamic plugins + dpConfigName := fmt.Sprintf("%s-dynamic-plugins", backstage.Name) + conf := bs.DynamicPluginsConfigRef{ + Name: dpConfigName, + Kind: "ConfigMap", + } + cm := &v1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "v1", + APIVersion: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: dpConfigName, + Namespace: ns, + }, + } + 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) + } + cm.Data = map[string]string{ + "dynamic-plugins.yaml": ` +includes: +- dynamic-plugins.default.yaml +plugins: [] +`, + } + 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 conf, nil +} + +func (r *BackstageReconciler) getDynamicPluginsConfVolume(ctx context.Context, backstage bs.Backstage, ns string) (*v1.Volume, error) { + dpConf, err := r.getOrGenerateDynamicPluginsConf(ctx, backstage, ns) + if err != nil { + return nil, err + } + + if dpConf.Name == "" { + return nil, nil + } + + var volumeSource v1.VolumeSource + switch dpConf.Kind { + case "ConfigMap": + volumeSource.ConfigMap = &v1.ConfigMapVolumeSource{ + DefaultMode: pointer.Int32(420), + LocalObjectReference: v1.LocalObjectReference{Name: dpConf.Name}, + } + case "Secret": + volumeSource.Secret = &v1.SecretVolumeSource{ + DefaultMode: pointer.Int32(420), + SecretName: dpConf.Name, + } + } + + return &v1.Volume{ + Name: dpConf.Name, + VolumeSource: volumeSource, + }, nil +} + +func (r *BackstageReconciler) addDynamicPluginsConfVolumeMount(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { + dpConf, err := r.getOrGenerateDynamicPluginsConf(ctx, backstage, ns) + if err != nil { + return err + } + + if dpConf.Name == "" { + return nil + } + + 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, + MountPath: fmt.Sprintf("%s/dynamic-plugins.yaml", _containersWorkingDir), + ReadOnly: true, + SubPath: "dynamic-plugins.yaml", + }) + break + } + } + return nil +} From 19849fc0436d1013e747862b8f53d3369b10a4f7 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Wed, 29 Nov 2023 11:21:11 +0100 Subject: [PATCH 13/18] Add proper Kubebuilder markers for mandatory fields Co-authored-by: Jianrong Zhang --- api/v1alpha1/backstage_types.go | 11 ++++++++--- config/crd/bases/backstage.io_backstages.yaml | 6 ++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index ad5e8510..8fea1c08 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -56,25 +56,30 @@ type BackstageSpec struct { type AppConfigRef struct { // Name of an existing App Config object - Name string `json:"name,omitempty"` + //+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,omitempty"` } type DynamicPluginsConfigRef struct { // Name of the Dynamic Plugins config object - Name string `json:"name,omitempty"` + // +kubebuilder:validation:Required + Name string `json:"name"` // Type of the Dynamic Plugins config object, either ConfigMap or Secret + //+kubebuilder:validation:Required //+kubebuilder:validation:Enum=ConfigMap;Secret Kind string `json:"kind,omitempty"` } type BackendAuthSecretRef struct { // Name of the secret to use for the backend auth - Name string `json:"name,omitempty"` + //+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 diff --git a/config/crd/bases/backstage.io_backstages.yaml b/config/crd/bases/backstage.io_backstages.yaml index e2a2b82b..6d21a6d8 100644 --- a/config/crd/bases/backstage.io_backstages.yaml +++ b/config/crd/bases/backstage.io_backstages.yaml @@ -60,6 +60,8 @@ spec: name: description: Name of an existing App Config object type: string + required: + - name type: object type: array backendAuthSecretRef: @@ -78,6 +80,8 @@ spec: name: description: Name of the secret to use for the backend auth type: string + required: + - name type: object dynamicPluginsConfig: description: 'Reference to an existing configuration object for Dynamic @@ -94,6 +98,8 @@ spec: name: description: Name of the Dynamic Plugins config object type: string + required: + - name type: object rawRuntimeConfig: description: Raw Runtime Objects configuration From 10c8e1c6f8ca9f0e2f34bd02db309b92379ce3db Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Wed, 29 Nov 2023 12:05:19 +0100 Subject: [PATCH 14/18] Move default ConfigMap for Dynamic Plugins to a default configuration (overridable by the raw runtime ConfigMap) Co-authored-by: Gennady Azarenkov --- controllers/backstage_dynamic_plugins.go | 56 +++++++++++++----------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/controllers/backstage_dynamic_plugins.go b/controllers/backstage_dynamic_plugins.go index 849d9b31..a853bc09 100644 --- a/controllers/backstage_dynamic_plugins.go +++ b/controllers/backstage_dynamic_plugins.go @@ -22,49 +22,53 @@ import ( appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/pointer" ) +var ( + defaultDynamicPluginsConfigMap = ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: # placeholder for '-dynamic-plugins' +data: + "dynamic-plugins.yaml": | + includes: + - dynamic-plugins.default.yaml + plugins: [] +` +) + func (r *BackstageReconciler) getOrGenerateDynamicPluginsConf(ctx context.Context, backstage bs.Backstage, ns string) (config bs.DynamicPluginsConfigRef, err error) { if backstage.Spec.DynamicPluginsConfig.Name != "" { return backstage.Spec.DynamicPluginsConfig, nil } - //Generate a default ConfigMap for dynamic plugins - dpConfigName := fmt.Sprintf("%s-dynamic-plugins", backstage.Name) - conf := bs.DynamicPluginsConfigRef{ - Name: dpConfigName, - Kind: "ConfigMap", - } - cm := &v1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - Kind: "v1", - APIVersion: "ConfigMap", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: dpConfigName, - Namespace: ns, - }, + + //Create default ConfigMap for dynamic plugins + var cm v1.ConfigMap + err = r.readConfigMapOrDefault(ctx, backstage.Spec.RawRuntimeConfig.BackstageConfigName, "dynamic-plugins-configmap", ns, defaultDynamicPluginsConfigMap, &cm) + if err != nil { + return bs.DynamicPluginsConfigRef{}, fmt.Errorf("failed to read config: %s", err) } - err = r.Get(ctx, types.NamespacedName{Name: dpConfigName, Namespace: ns}, cm) + + dpConfigName := fmt.Sprintf("%s-dynamic-plugins", backstage.Name) + cm.SetName(dpConfigName) + 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) } - cm.Data = map[string]string{ - "dynamic-plugins.yaml": ` -includes: -- dynamic-plugins.default.yaml -plugins: [] -`, - } - err = r.Create(ctx, cm) + 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 conf, nil + + return bs.DynamicPluginsConfigRef{ + Name: dpConfigName, + Kind: "ConfigMap", + }, nil } func (r *BackstageReconciler) getDynamicPluginsConfVolume(ctx context.Context, backstage bs.Backstage, ns string) (*v1.Volume, error) { From 2d70feaccf49e09a0c5e5c00cb4a85aea06dee05 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Wed, 29 Nov 2023 17:09:30 +0100 Subject: [PATCH 15/18] Move default Backend Auth Secret to a default configuration (overridable by the raw runtime ConfigMap) Co-authored-by: Gennady Azarenkov --- controllers/backstage_backend_auth.go | 67 ++++++++++++++---------- controllers/backstage_controller.go | 22 ++++---- controllers/backstage_deployment.go | 3 +- controllers/backstage_dynamic_plugins.go | 2 +- controllers/backstage_service.go | 2 +- 5 files changed, 54 insertions(+), 42 deletions(-) diff --git a/controllers/backstage_backend_auth.go b/controllers/backstage_backend_auth.go index b6e25d0b..8b4c16a0 100644 --- a/controllers/backstage_backend_auth.go +++ b/controllers/backstage_backend_auth.go @@ -24,31 +24,49 @@ import ( appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/pointer" ) +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 +` +) + func (r *BackstageReconciler) handleBackendAuthSecret(ctx context.Context, backstage bs.Backstage, ns string) (secretName string, err error) { backendAuthSecretName := backstage.Spec.BackendAuthSecretRef.Name - if backendAuthSecretName == "" { - //Generate a secret if it does not exist - backendAuthSecretName = fmt.Sprintf("%s-auth", backstage.Name) - sec := &v1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "v1", - APIVersion: "Secret", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: backendAuthSecretName, - Namespace: ns, - }, + if backendAuthSecretName != "" { + return backendAuthSecretName, nil + } + + //Create default Secret for backend auth + var sec v1.Secret + var isDefault bool + isDefault, err = r.readConfigMapOrDefault(ctx, backstage.Spec.RawRuntimeConfig.BackstageConfigName, "backend-auth-secret", ns, defaultBackstageBackendAuthSecret, &sec) + if err != nil { + return "", 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) + if err != nil { + if !errors.IsNotFound(err) { + return "", fmt.Errorf("failed to get secret for backend auth (%q), reason: %s", backendAuthSecretName, err) } - err = r.Get(ctx, types.NamespacedName{Name: backendAuthSecretName, 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) - } + k := backstage.Spec.BackendAuthSecretRef.Key + if k == "" { + //TODO(rm3l): why kubebuilder default values do not work + k = "backend-secret" + } + if isDefault { // Create a secret with a random value authVal := func(length int) string { bytes := make([]byte, length) @@ -58,18 +76,13 @@ func (r *BackstageReconciler) handleBackendAuthSecret(ctx context.Context, backs } return base64.StdEncoding.EncodeToString(bytes) }(24) - k := backstage.Spec.BackendAuthSecretRef.Key - if k == "" { - //TODO(rm3l): why kubebuilder default values do not work - k = "backend-secret" - } sec.Data = map[string][]byte{ k: []byte(authVal), } - err = r.Create(ctx, sec) - if err != nil { - return "", fmt.Errorf("failed to create secret for backend auth, reason: %s", err) - } + } + err = r.Create(ctx, &sec) + if err != nil { + return "", fmt.Errorf("failed to create secret for backend auth, reason: %s", err) } } return backendAuthSecretName, nil diff --git a/controllers/backstage_controller.go b/controllers/backstage_controller.go index bfa0ecde..131bbe43 100644 --- a/controllers/backstage_controller.go +++ b/controllers/backstage_controller.go @@ -134,7 +134,7 @@ func (r *BackstageReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return ctrl.Result{}, nil } -func (r *BackstageReconciler) readConfigMapOrDefault(ctx context.Context, name string, key string, ns string, def string, object v1.Object) error { +func (r *BackstageReconciler) readConfigMapOrDefault(ctx context.Context, name string, key string, ns string, def string, object v1.Object) (isDefault bool, err error) { // ConfigMap name not set, default //lg := log.FromContext(ctx) @@ -142,34 +142,34 @@ func (r *BackstageReconciler) readConfigMapOrDefault(ctx context.Context, name s //lg.V(1).Info("readConfigMapOrDefault CM: ", "name", name) if name == "" { - err := readYaml(def, object) + err = readYaml(def, object) if err != nil { - return err + return true, err } object.SetNamespace(ns) - return nil + return true, nil } cm := corev1.ConfigMap{} - if err := r.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, &cm); err != nil { - return err + if err = r.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, &cm); err != nil { + return false, err } //lg.V(1).Info("readConfigMapOrDefault CM name found: ", "ConfigMap:", cm) val, ok := cm.Data[key] if !ok { // key not found, default - err := readYaml(def, object) + err = readYaml(def, object) if err != nil { - return err + return true, err } } else { - err := readYaml(val, object) + err = readYaml(val, object) if err != nil { - return err + return false, err } } object.SetNamespace(ns) - return nil + return !ok, nil } func readYaml(manifest string, object interface{}) error { diff --git a/controllers/backstage_deployment.go b/controllers/backstage_deployment.go index 0992308b..869a5a57 100644 --- a/controllers/backstage_deployment.go +++ b/controllers/backstage_deployment.go @@ -28,7 +28,6 @@ import ( const ( _defaultBackstageInitContainerName = "install-dynamic-plugins" _defaultBackstageMainContainerName = "backstage-backend" - _defaultBackendAuthSecretValue = "pl4s3Ch4ng3M3" _containersWorkingDir = "/opt/app-root/src" ) @@ -134,7 +133,7 @@ func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, back //lg := log.FromContext(ctx) deployment := &appsv1.Deployment{} - err := r.readConfigMapOrDefault(ctx, backstage.Spec.RawRuntimeConfig.BackstageConfigName, "deploy", ns, DefaultBackstageDeployment, deployment) + _, err := r.readConfigMapOrDefault(ctx, backstage.Spec.RawRuntimeConfig.BackstageConfigName, "deploy", ns, DefaultBackstageDeployment, deployment) if err != nil { return fmt.Errorf("failed to read config: %s", err) } diff --git a/controllers/backstage_dynamic_plugins.go b/controllers/backstage_dynamic_plugins.go index a853bc09..25d12040 100644 --- a/controllers/backstage_dynamic_plugins.go +++ b/controllers/backstage_dynamic_plugins.go @@ -47,7 +47,7 @@ func (r *BackstageReconciler) getOrGenerateDynamicPluginsConf(ctx context.Contex //Create default ConfigMap for dynamic plugins var cm v1.ConfigMap - err = r.readConfigMapOrDefault(ctx, backstage.Spec.RawRuntimeConfig.BackstageConfigName, "dynamic-plugins-configmap", ns, defaultDynamicPluginsConfigMap, &cm) + _, err = r.readConfigMapOrDefault(ctx, backstage.Spec.RawRuntimeConfig.BackstageConfigName, "dynamic-plugins-configmap", ns, defaultDynamicPluginsConfigMap, &cm) if err != nil { return bs.DynamicPluginsConfigRef{}, fmt.Errorf("failed to read config: %s", err) } diff --git a/controllers/backstage_service.go b/controllers/backstage_service.go index 63588a38..cd552861 100644 --- a/controllers/backstage_service.go +++ b/controllers/backstage_service.go @@ -50,7 +50,7 @@ func (r *BackstageReconciler) applyBackstageService(ctx context.Context, backsta //lg := log.FromContext(ctx) service := &corev1.Service{} - err := r.readConfigMapOrDefault(ctx, backstage.Spec.RawRuntimeConfig.BackstageConfigName, "service", ns, DefaultBackstageService, service) + _, err := r.readConfigMapOrDefault(ctx, backstage.Spec.RawRuntimeConfig.BackstageConfigName, "service", ns, DefaultBackstageService, service) if err != nil { return err } From 71c2b29b6ab5f6a8e9146eb9c705726ed4a01d45 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Wed, 29 Nov 2023 17:42:21 +0100 Subject: [PATCH 16/18] fixup! Add proper Kubebuilder markers for mandatory fields --- api/v1alpha1/backstage_types.go | 8 ++++---- api/v1alpha1/zz_generated.deepcopy.go | 12 ++++++++++-- config/crd/bases/backstage.io_backstages.yaml | 2 ++ controllers/backstage_backend_auth.go | 17 +++++++++++------ controllers/backstage_controller_test.go | 6 +++--- controllers/backstage_dynamic_plugins.go | 4 ++-- 6 files changed, 32 insertions(+), 17 deletions(-) diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index 8fea1c08..42fa4b2f 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -40,12 +40,12 @@ type BackstageSpec struct { // 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. - BackendAuthSecretRef BackendAuthSecretRef `json:"backendAuthSecretRef,omitempty"` + BackendAuthSecretRef *BackendAuthSecretRef `json:"backendAuthSecretRef,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"` + DynamicPluginsConfig *DynamicPluginsConfigRef `json:"dynamicPluginsConfig,omitempty"` // Raw Runtime Objects configuration RawRuntimeConfig RuntimeConfig `json:"rawRuntimeConfig,omitempty"` @@ -62,7 +62,7 @@ type AppConfigRef struct { // Type of the existing App Config object, either ConfigMap or Secret //+kubebuilder:validation:Required //+kubebuilder:validation:Enum=ConfigMap;Secret - Kind string `json:"kind,omitempty"` + Kind string `json:"kind"` } type DynamicPluginsConfigRef struct { @@ -73,7 +73,7 @@ type DynamicPluginsConfigRef struct { // Type of the Dynamic Plugins config object, either ConfigMap or Secret //+kubebuilder:validation:Required //+kubebuilder:validation:Enum=ConfigMap;Secret - Kind string `json:"kind,omitempty"` + Kind string `json:"kind"` } type BackendAuthSecretRef struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index b6de138b..9126a8bc 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -123,8 +123,16 @@ func (in *BackstageSpec) DeepCopyInto(out *BackstageSpec) { *out = make([]AppConfigRef, len(*in)) copy(*out, *in) } - out.BackendAuthSecretRef = in.BackendAuthSecretRef - out.DynamicPluginsConfig = in.DynamicPluginsConfig + if in.BackendAuthSecretRef != nil { + in, out := &in.BackendAuthSecretRef, &out.BackendAuthSecretRef + *out = new(BackendAuthSecretRef) + **out = **in + } + if in.DynamicPluginsConfig != nil { + in, out := &in.DynamicPluginsConfig, &out.DynamicPluginsConfig + *out = new(DynamicPluginsConfigRef) + **out = **in + } out.RawRuntimeConfig = in.RawRuntimeConfig } diff --git a/config/crd/bases/backstage.io_backstages.yaml b/config/crd/bases/backstage.io_backstages.yaml index 6d21a6d8..bd09ae1c 100644 --- a/config/crd/bases/backstage.io_backstages.yaml +++ b/config/crd/bases/backstage.io_backstages.yaml @@ -61,6 +61,7 @@ spec: description: Name of an existing App Config object type: string required: + - kind - name type: object type: array @@ -99,6 +100,7 @@ spec: description: Name of the Dynamic Plugins config object type: string required: + - kind - name type: object rawRuntimeConfig: diff --git a/controllers/backstage_backend_auth.go b/controllers/backstage_backend_auth.go index 8b4c16a0..1fef7cf5 100644 --- a/controllers/backstage_backend_auth.go +++ b/controllers/backstage_backend_auth.go @@ -41,9 +41,8 @@ data: ) func (r *BackstageReconciler) handleBackendAuthSecret(ctx context.Context, backstage bs.Backstage, ns string) (secretName string, err error) { - backendAuthSecretName := backstage.Spec.BackendAuthSecretRef.Name - if backendAuthSecretName != "" { - return backendAuthSecretName, nil + if backstage.Spec.BackendAuthSecretRef != nil { + return backstage.Spec.BackendAuthSecretRef.Name, nil } //Create default Secret for backend auth @@ -54,14 +53,17 @@ func (r *BackstageReconciler) handleBackendAuthSecret(ctx context.Context, backs return "", fmt.Errorf("failed to read config: %s", err) } //Generate a secret if it does not exist - backendAuthSecretName = fmt.Sprintf("%s-auth", backstage.Name) + backendAuthSecretName := fmt.Sprintf("%s-auth", backstage.Name) sec.SetName(backendAuthSecretName) err = r.Get(ctx, types.NamespacedName{Name: backendAuthSecretName, 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) } - k := backstage.Spec.BackendAuthSecretRef.Key + 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" @@ -100,7 +102,10 @@ func (r *BackstageReconciler) addBackendAuthEnvVar(ctx context.Context, backstag for i, c := range deployment.Spec.Template.Spec.Containers { if c.Name == _defaultBackstageMainContainerName { - k := backstage.Spec.BackendAuthSecretRef.Key + 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" diff --git a/controllers/backstage_controller_test.go b/controllers/backstage_controller_test.go index a1e85f67..3cb12734 100644 --- a/controllers/backstage_controller_test.go +++ b/controllers/backstage_controller_test.go @@ -520,7 +520,7 @@ plugins: [] Kind: "Secret", }, }, - DynamicPluginsConfig: bsv1alphav1.DynamicPluginsConfigRef{ + DynamicPluginsConfig: &bsv1alphav1.DynamicPluginsConfigRef{ Name: dynamicPluginsConfigName, Kind: dynamicPluginsConfigKind, }, @@ -686,7 +686,7 @@ plugins: [] var backstage *bsv1alphav1.Backstage BeforeEach(func() { backstage = buildBackstageCR(bsv1alphav1.BackstageSpec{ - BackendAuthSecretRef: bsv1alphav1.BackendAuthSecretRef{ + BackendAuthSecretRef: &bsv1alphav1.BackendAuthSecretRef{ Name: "non-existing-secret", Key: key, }, @@ -761,7 +761,7 @@ plugins: [] err := k8sClient.Create(ctx, backendAuthSecret) Expect(err).To(Not(HaveOccurred())) backstage = buildBackstageCR(bsv1alphav1.BackstageSpec{ - BackendAuthSecretRef: bsv1alphav1.BackendAuthSecretRef{ + BackendAuthSecretRef: &bsv1alphav1.BackendAuthSecretRef{ Name: backendAuthSecretName, Key: key, }, diff --git a/controllers/backstage_dynamic_plugins.go b/controllers/backstage_dynamic_plugins.go index 25d12040..3a90c782 100644 --- a/controllers/backstage_dynamic_plugins.go +++ b/controllers/backstage_dynamic_plugins.go @@ -41,8 +41,8 @@ data: ) func (r *BackstageReconciler) getOrGenerateDynamicPluginsConf(ctx context.Context, backstage bs.Backstage, ns string) (config bs.DynamicPluginsConfigRef, err error) { - if backstage.Spec.DynamicPluginsConfig.Name != "" { - return backstage.Spec.DynamicPluginsConfig, nil + if backstage.Spec.DynamicPluginsConfig != nil { + return *backstage.Spec.DynamicPluginsConfig, nil } //Create default ConfigMap for dynamic plugins From b4f919e782de5d563872bd5f434928f06f4ed718 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Wed, 29 Nov 2023 17:51:03 +0100 Subject: [PATCH 17/18] Do not read Secret's StringData field This is just write-only convenience field, and is never output when reading from the API. Co-authored-by: Gennady Azarenkov --- controllers/backstage_app_config.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/controllers/backstage_app_config.go b/controllers/backstage_app_config.go index 23ddb553..24ffa57d 100644 --- a/controllers/backstage_app_config.go +++ b/controllers/backstage_app_config.go @@ -129,10 +129,6 @@ 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) } - for filename := range sec.StringData { - // Bear in mind that iteration order over this map is not guaranteed by Go - files = append(files, filename) - } } result = append(result, appConfigData{ ref: appConfig.Name, From 1634d9561064d71105c544b7afff99b8d78ec8d2 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Wed, 29 Nov 2023 18:00:14 +0100 Subject: [PATCH 18/18] Fix conflicts upon rebase --- config/crd/bases/backstage.io_backstages.yaml | 2 +- controllers/local_db_statefulset.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/crd/bases/backstage.io_backstages.yaml b/config/crd/bases/backstage.io_backstages.yaml index bd09ae1c..7888bed1 100644 --- a/config/crd/bases/backstage.io_backstages.yaml +++ b/config/crd/bases/backstage.io_backstages.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.10.0 + controller-gen.kubebuilder.io/version: v0.11.3 creationTimestamp: null name: backstages.backstage.io spec: diff --git a/controllers/local_db_statefulset.go b/controllers/local_db_statefulset.go index 47067e88..a1bb8fe0 100644 --- a/controllers/local_db_statefulset.go +++ b/controllers/local_db_statefulset.go @@ -158,7 +158,7 @@ func (r *BackstageReconciler) applyLocalDbStatefulSet(ctx context.Context, backs lg := log.FromContext(ctx) statefulSet := &appsv1.StatefulSet{} - err := r.readConfigMapOrDefault(ctx, backstage.Spec.RawRuntimeConfig.LocalDbConfigName, "statefulset", ns, DefaultLocalDbDeployment, statefulSet) + _, err := r.readConfigMapOrDefault(ctx, backstage.Spec.RawRuntimeConfig.LocalDbConfigName, "statefulset", ns, DefaultLocalDbDeployment, statefulSet) if err != nil { return err } @@ -205,7 +205,7 @@ func (r *BackstageReconciler) applyPsqlService(ctx context.Context, backstage bs lg := log.FromContext(ctx) service := &corev1.Service{} - err := r.readConfigMapOrDefault(ctx, backstage.Spec.RawRuntimeConfig.LocalDbConfigName, "service", ns, defaultData, service) + _, err := r.readConfigMapOrDefault(ctx, backstage.Spec.RawRuntimeConfig.LocalDbConfigName, "service", ns, defaultData, service) if err != nil { return err }