diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index d055a79c..6e9d2098 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -16,19 +16,15 @@ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/pointer" ) -type BackstageConditionReason string - -type BackstageConditionType string - +// Constants for status conditions const ( - BackstageConditionTypeDeployed BackstageConditionType = "Deployed" - - BackstageConditionReasonDeployed BackstageConditionReason = "Deployed" - BackstageConditionReasonFailed BackstageConditionReason = "DeployFailed" - BackstageConditionReasonInProgress BackstageConditionReason = "DeployInProgress" + // TODO: RuntimeConditionRunning string = "RuntimeRunning" + ConditionDeployed string = "Deployed" + DeployOK string = "DeployOK" + DeployFailed string = "DeployFailed" + DeployInProgress string = "DeployInProgress" ) // BackstageSpec defines the desired state of Backstage @@ -36,20 +32,11 @@ type BackstageSpec struct { // Configuration for Backstage. Optional. Application *Application `json:"application,omitempty"` - // Raw Runtime RuntimeObjects configuration. For Advanced scenarios. - //RawConfig string `json:"rawConfig,omitempty"` - - RawRuntimeConfig *RuntimeConfig `json:"rawRuntimeConfig,omitempty"` + // Raw Runtime Objects configuration. For Advanced scenarios. + RawRuntimeConfig RuntimeConfig `json:"rawRuntimeConfig,omitempty"` // Configuration for database access. Optional. - Database *Database `json:"database,omitempty"` -} - -type RuntimeConfig struct { - // Name of ConfigMap containing Backstage runtime objects configuration - BackstageConfigName string `json:"backstageConfig,omitempty"` - // Name of ConfigMap containing LocalDb (PostgreSQL) runtime objects configuration - LocalDbConfigName string `json:"localDbConfig,omitempty"` + Database Database `json:"database,omitempty"` } type Database struct { @@ -58,8 +45,8 @@ type Database struct { //+kubebuilder:default=true EnableLocalDb *bool `json:"enableLocalDb,omitempty"` - // Name of the secret for database authentication. Required for external database access. - // Optional for a local database (EnableLocalDb=true) and if absent a secret will be auto generated. + // Name of the secret for database authentication. Optional. + // For a local database deployment (EnableLocalDb=true), a secret will be auto generated if it does not exist. // The secret shall include information used for the database access. // An example for PostgreSQL DB access: // "POSTGRES_PASSWORD": "rl4s3Fh4ng3M4" @@ -111,7 +98,7 @@ type Application struct { // Image Pull Secrets to use in all containers (including Init Containers) // +optional - ImagePullSecrets []string `json:"imagePullSecrets,omitempty"` + ImagePullSecrets *[]string `json:"imagePullSecrets,omitempty"` // Route configuration. Used for OpenShift only. Route *Route `json:"route,omitempty"` @@ -190,6 +177,13 @@ type Env struct { Value string `json:"value"` } +type RuntimeConfig struct { + // Name of ConfigMap containing Backstage runtime objects configuration + BackstageConfigName string `json:"backstageConfig,omitempty"` + // Name of ConfigMap containing LocalDb (P|ostgreSQL) runtime objects configuration + LocalDbConfigName string `json:"localDbConfig,omitempty"` +} + // BackstageStatus defines the observed state of Backstage type BackstageStatus struct { // Conditions is the list of conditions describing the state of the runtime @@ -274,29 +268,3 @@ type TLS struct { func init() { SchemeBuilder.Register(&Backstage{}, &BackstageList{}) } - -func (s *BackstageSpec) IsLocalDbEnabled() bool { - if s.Database == nil { - return true - } - return pointer.BoolDeref(s.Database.EnableLocalDb, true) -} - -func (s *BackstageSpec) IsRouteEnabled() bool { - if s.Application == nil || s.Application.Route == nil { - return false - } - return pointer.BoolDeref(s.Application.Route.Enabled, true) -} - -func (s *BackstageSpec) IsRouteEmpty() bool { - route := s.Application.Route - if route.Host != "" && route.Subdomain != "" && route.TLS != nil && *route.TLS != (TLS{}) { - return true - } - return false -} - -func (s *BackstageSpec) IsAuthSecretSpecified() bool { - return s.Database != nil && s.Database.AuthSecretName != "" -} diff --git a/bundle/manifests/backstage-default-config_v1_configmap.yaml b/bundle/manifests/backstage-default-config_v1_configmap.yaml index 9edceaf8..5e6056f0 100644 --- a/bundle/manifests/backstage-default-config_v1_configmap.yaml +++ b/bundle/manifests/backstage-default-config_v1_configmap.yaml @@ -1,34 +1,28 @@ apiVersion: v1 data: - app-config.yaml: |- + backend-auth-configmap.yaml: | apiVersion: v1 kind: ConfigMap metadata: - name: my-backstage-config-cm1 # placeholder for -default-appconfig + name: # placeholder for '-backend-auth' data: - default.app-config.yaml: | + "app-config.backend-auth.default.yaml": | backend: - database: - connection: - password: ${POSTGRES_PASSWORD} - user: ${POSTGRES_USER} auth: keys: # This is a default value, which you should change by providing your own app-config - secret: "pl4s3Ch4ng3M3" - db-secret.yaml: |- + db-secret.yaml: | apiVersion: v1 kind: Secret metadata: - name: postgres-secrets # will be replaced - namespace: backstage - type: Opaque + name: # placeholder for 'backstage-psql-secret-' stringData: - POSTGRES_PASSWORD: #wrgd5688 #admin123 # leave it empty to make it autogenerated - POSTGRES_PORT: "5432" - POSTGRES_USER: postgres - POSTGRESQL_ADMIN_PASSWORD: admin123 - POSTGRES_HOST: bs1-db-service #placeholder -db-service + "POSTGRES_PASSWORD": "rl4s3Fh4ng3M4" # default value, change to your own value + "POSTGRES_PORT": "5432" + "POSTGRES_USER": "postgres" + "POSTGRESQL_ADMIN_PASSWORD": "rl4s3Fh4ng3M4" # default value, change to your own value + "POSTGRES_HOST": "" # set to your Postgres DB host. If the local DB is deployed, set to 'backstage-psql-' db-service-hl.yaml: | apiVersion: v1 kind: Service @@ -50,7 +44,7 @@ data: janus-idp.io/app: backstage-psql-cr1 # placeholder for 'backstage-psql-' ports: - port: 5432 - db-statefulset.yaml: |- + db-statefulset.yaml: | apiVersion: apps/v1 kind: StatefulSet metadata: @@ -68,9 +62,13 @@ data: janus-idp.io/app: backstage-psql-cr1 # placeholder for 'backstage-psql-' name: backstage-db-cr1 # placeholder for 'backstage-psql-' spec: - persistentVolumeClaimRetentionPolicy: - whenDeleted: Retain - whenScaled: Retain + automountServiceAccountToken: false + ## https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/ + ## The optional .spec.persistentVolumeClaimRetentionPolicy field controls if and how PVCs are deleted during the lifecycle of a StatefulSet. + ## You must enable the StatefulSetAutoDeletePVC feature gate on the API server and the controller manager to use this field. + # persistentVolumeClaimRetentionPolicy: + # whenDeleted: Retain + # whenScaled: Retain containers: - env: - name: POSTGRESQL_PORT_NUMBER @@ -80,10 +78,10 @@ data: - name: PGDATA value: /var/lib/pgsql/data/userdata envFrom: - # - secretRef: - # name: # will be replaced with 'backstage-psql-secrets-' - # image: quay.io/fedora/postgresql-15:latest - image: # will be replaced with the actual image + - secretRef: + name: # will be replaced with 'backstage-psql-secrets-' + # image will be replaced by the value of the `RELATED_IMAGE_postgresql` env var, if set + image: quay.io/fedora/postgresql-15:latest imagePullPolicy: IfNotPresent securityContext: runAsNonRoot: true @@ -127,7 +125,9 @@ data: cpu: 250m memory: 256Mi limits: + cpu: 250m memory: 1024Mi + ephemeral-storage: 20Mi volumeMounts: - mountPath: /dev/shm name: dshm @@ -171,7 +171,7 @@ data: labels: janus-idp.io/app: # placeholder for 'backstage-' spec: - # serviceAccountName: default + automountServiceAccountToken: false volumes: - ephemeral: volumeClaimTemplate: @@ -187,13 +187,6 @@ data: defaultMode: 420 optional: true secretName: dynamic-plugins-npmrc - - name: dynamic-plugins-conf - configMap: - name: default-dynamic-plugins - optional: true - items: - - key: dynamic-plugins.yaml - path: dynamic-plugins.yaml initContainers: - command: @@ -202,7 +195,8 @@ data: env: - name: NPM_CONFIG_USERCONFIG value: /opt/app-root/src/.npmrc.dynamic-plugins - image: # will be replaced with the actual image quay.io/janus-idp/backstage-showcase:next + # image will be replaced by the value of the `RELATED_IMAGE_backstage` env var, if set + image: quay.io/janus-idp/backstage-showcase:latest imagePullPolicy: IfNotPresent name: install-dynamic-plugins volumeMounts: @@ -212,15 +206,16 @@ data: name: dynamic-plugins-npmrc readOnly: true subPath: .npmrc - - mountPath: /opt/app-root/src/dynamic-plugins.yaml - subPath: dynamic-plugins.yaml - name: dynamic-plugins-conf - readOnly: true workingDir: /opt/app-root/src - + resources: + limits: + cpu: 1000m + memory: 2.5Gi + ephemeral-storage: 5Gi containers: - name: backstage-backend - image: # will be replaced with the actual image quay.io/janus-idp/backstage-showcase:next + # image will be replaced by the value of the `RELATED_IMAGE_backstage` env var, if set + image: quay.io/janus-idp/backstage-showcase:latest imagePullPolicy: IfNotPresent args: - "--config" @@ -251,14 +246,24 @@ data: env: - name: APP_CONFIG_backend_listen_port value: "7007" + envFrom: + - secretRef: + name: # will be replaced with 'backstage-psql-secrets-' + # - secretRef: + # name: backstage-secrets volumeMounts: - mountPath: /opt/app-root/src/dynamic-plugins-root name: dynamic-plugins-root - dynamic-plugins.yaml: |- + resources: + limits: + cpu: 1000m + memory: 2.5Gi + ephemeral-storage: 5Gi + dynamic-plugins-configmap.yaml: |- apiVersion: v1 kind: ConfigMap metadata: - name: default-dynamic-plugins # must be the same as (deployment.yaml).spec.template.spec.volumes.name.dynamic-plugins-conf.configMap.name + name: # placeholder for '-dynamic-plugins' data: "dynamic-plugins.yaml": | includes: @@ -279,15 +284,6 @@ data: to: kind: Service name: # placeholder for 'backstage-' - secret-envs.yaml: | - apiVersion: v1 - kind: Secret - metadata: - name: backend-auth-secret - stringData: - # generated with the command below (from https://janus-idp.io/docs/auth/service-to-service-auth/#setup): - # node -p 'require("crypto").randomBytes(24).toString("base64")' - BACKEND_SECRET: "R2FxRVNrcmwzYzhhN3l0V1VRcnQ3L1pLT09WaVhDNUEK" # notsecret service.yaml: |- apiVersion: v1 kind: Service diff --git a/bundle/manifests/backstage-operator.clusterserviceversion.yaml b/bundle/manifests/backstage-operator.clusterserviceversion.yaml index 6ccfb951..bc3be853 100644 --- a/bundle/manifests/backstage-operator.clusterserviceversion.yaml +++ b/bundle/manifests/backstage-operator.clusterserviceversion.yaml @@ -21,7 +21,7 @@ metadata: } ] capabilities: Seamless Upgrades - createdAt: "2024-01-29T20:18:14Z" + createdAt: "2024-02-13T07:11:47Z" operatorframework.io/suggested-namespace: backstage-system operators.operatorframework.io/builder: operator-sdk-v1.33.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 @@ -185,6 +185,7 @@ spec: operator: In values: - linux + automountServiceAccountToken: true containers: - args: - --secure-listen-address=0.0.0.0:8443 @@ -219,7 +220,7 @@ spec: - name: RELATED_IMAGE_postgresql value: quay.io/fedora/postgresql-15:latest - name: RELATED_IMAGE_backstage - value: quay.io/janus-idp/backstage-showcase:next + value: quay.io/janus-idp/backstage-showcase:latest image: quay.io/janus-idp/operator:0.0.1 livenessProbe: httpGet: @@ -237,6 +238,7 @@ spec: resources: limits: cpu: 500m + ephemeral-storage: 20Mi memory: 128Mi requests: cpu: 10m @@ -322,6 +324,6 @@ spec: relatedImages: - image: quay.io/fedora/postgresql-15:latest name: postgresql - - image: quay.io/janus-idp/backstage-showcase:next + - image: quay.io/janus-idp/backstage-showcase:latest name: backstage version: 0.0.1 diff --git a/config/manager/default-config/db-statefulset.yaml b/config/manager/default-config/db-statefulset.yaml index 6b283994..f1da07e1 100644 --- a/config/manager/default-config/db-statefulset.yaml +++ b/config/manager/default-config/db-statefulset.yaml @@ -33,7 +33,8 @@ spec: envFrom: - secretRef: name: # will be replaced with 'backstage-psql-secrets-' - image: # will be replaced with the actual image + # image will be replaced by the value of the `RELATED_IMAGE_postgresql` env var, if set + image: quay.io/fedora/postgresql-15:latest imagePullPolicy: IfNotPresent securityContext: runAsNonRoot: true diff --git a/config/manager/default-config/deployment.yaml b/config/manager/default-config/deployment.yaml index c6b9d23a..8056da50 100644 --- a/config/manager/default-config/deployment.yaml +++ b/config/manager/default-config/deployment.yaml @@ -36,7 +36,8 @@ spec: env: - name: NPM_CONFIG_USERCONFIG value: /opt/app-root/src/.npmrc.dynamic-plugins - image: # will be replaced with the actual image quay.io/janus-idp/backstage-showcase:next + # image will be replaced by the value of the `RELATED_IMAGE_backstage` env var, if set + image: quay.io/janus-idp/backstage-showcase:latest imagePullPolicy: IfNotPresent name: install-dynamic-plugins volumeMounts: @@ -54,7 +55,8 @@ spec: ephemeral-storage: 5Gi containers: - name: backstage-backend - image: # will be replaced with the actual image quay.io/janus-idp/backstage-showcase:next + # image will be replaced by the value of the `RELATED_IMAGE_backstage` env var, if set + image: quay.io/janus-idp/backstage-showcase:latest imagePullPolicy: IfNotPresent args: - "--config" diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 914f0aa8..f11e35fb 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -76,7 +76,7 @@ spec: - name: RELATED_IMAGE_postgresql value: quay.io/fedora/postgresql-15:latest - name: RELATED_IMAGE_backstage - value: quay.io/janus-idp/backstage-showcase:next + value: quay.io/janus-idp/backstage-showcase:latest image: controller:latest name: manager securityContext: diff --git a/controllers/backstage_controller.go b/controllers/backstage_controller.go index d52322af..e3c2a65c 100644 --- a/controllers/backstage_controller.go +++ b/controllers/backstage_controller.go @@ -41,6 +41,11 @@ const ( BackstageAppLabel = "janus-idp.io/app" ) +var ( + envPostgresImage string + envBackstageImage string +) + // BackstageReconciler reconciles a Backstage object type BackstageReconciler struct { client.Client @@ -56,10 +61,6 @@ type BackstageReconciler struct { Namespace string IsOpenShift bool - - PsqlImage string - - BackstageImage string } //+kubebuilder:rbac:groups=janus-idp.io,resources=backstages,verbs=get;list;watch;create;update;patch;delete @@ -295,14 +296,13 @@ func (r *BackstageReconciler) labels(meta *v1.ObjectMeta, backstage bs.Backstage // SetupWithManager sets up the controller with the Manager. func (r *BackstageReconciler) SetupWithManager(mgr ctrl.Manager, log logr.Logger) error { - if len(r.PsqlImage) == 0 { - r.PsqlImage = "quay.io/fedora/postgresql-15:latest" - log.Info("Enviroment variable is not set, default is used", bs.EnvPostGresImage, r.PsqlImage) - } - if len(r.BackstageImage) == 0 { - r.BackstageImage = "quay.io/janus-idp/backstage-showcase:next" - log.Info("Enviroment variable is not set, default is used", bs.EnvBackstageImage, r.BackstageImage) + var ok bool + if envPostgresImage, ok = os.LookupEnv("RELATED_IMAGE_postgresql"); !ok { + log.Info("RELATED_IMAGE_postgresql environment variable is not set, default will be used") + } + if envBackstageImage, ok = os.LookupEnv("RELATED_IMAGE_backstage"); !ok { + log.Info("RELATED_IMAGE_backstage environment variable is not set, default will be used") } builder := ctrl.NewControllerManagedBy(mgr). diff --git a/controllers/backstage_controller_test.go b/controllers/backstage_controller_test.go index efd34ed2..c759fc9a 100644 --- a/controllers/backstage_controller_test.go +++ b/controllers/backstage_controller_test.go @@ -61,12 +61,12 @@ var _ = Describe("Backstage controller", func() { Expect(err).To(Not(HaveOccurred())) backstageReconciler = &BackstageReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - Namespace: ns, - OwnsRuntime: true, - PsqlImage: "test-postgresql-15:latest", - BackstageImage: "test-backstage-showcase:next", + Client: k8sClient, + Scheme: k8sClient.Scheme(), + Namespace: ns, + OwnsRuntime: true, + //PsqlImage: "test-postgresql-15:latest", + //BackstageImage: "test-backstage-showcase:next", } }) diff --git a/controllers/backstage_deployment.go b/controllers/backstage_deployment.go new file mode 100644 index 00000000..e8080ef8 --- /dev/null +++ b/controllers/backstage_deployment.go @@ -0,0 +1,238 @@ +// +// 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" + + 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" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + bs "janus-idp.io/backstage-operator/api/v1alpha1" +) + +const ( + _defaultBackstageInitContainerName = "install-dynamic-plugins" + _defaultBackstageMainContainerName = "backstage-backend" + _containersWorkingDir = "/opt/app-root/src" +) + +// ContainerVisitor is called with each container +type ContainerVisitor func(container *v1.Container) + +// visitContainers invokes the visitor function for every container in the given pod template spec +func visitContainers(podTemplateSpec *v1.PodTemplateSpec, visitor ContainerVisitor) { + for i := range podTemplateSpec.Spec.InitContainers { + visitor(&podTemplateSpec.Spec.InitContainers[i]) + } + for i := range podTemplateSpec.Spec.Containers { + visitor(&podTemplateSpec.Spec.Containers[i]) + } + for i := range podTemplateSpec.Spec.EphemeralContainers { + visitor((*v1.Container)(&podTemplateSpec.Spec.EphemeralContainers[i].EphemeralContainerCommon)) + } +} + +func (r *BackstageReconciler) reconcileBackstageDeployment(ctx context.Context, backstage *bs.Backstage, ns string) error { + deployment := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{ + Name: getDefaultObjName(*backstage), + Namespace: ns}, + } + if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, deployment, r.deploymentObjectMutFun(ctx, deployment, *backstage, ns)); err != nil { + if errors.IsConflict(err) { + return retryReconciliation(err) + } + msg := fmt.Sprintf("failed to deploy Backstage Deployment: %s", err) + setStatusCondition(backstage, bs.ConditionDeployed, metav1.ConditionFalse, bs.DeployFailed, msg) + return fmt.Errorf(msg) + } + return nil +} + +func (r *BackstageReconciler) deploymentObjectMutFun(ctx context.Context, targetDeployment *appsv1.Deployment, backstage bs.Backstage, ns string) controllerutil.MutateFn { + return func() error { + deployment := &appsv1.Deployment{} + targetDeployment.ObjectMeta.DeepCopyInto(&deployment.ObjectMeta) + + err := r.readConfigMapOrDefault(ctx, backstage.Spec.RawRuntimeConfig.BackstageConfigName, "deployment.yaml", ns, deployment) + if err != nil { + return fmt.Errorf("failed to read config: %s", err) + } + + // Override deployment name + deployment.Name = getDefaultObjName(backstage) + + r.setDefaultDeploymentImage(deployment) + + r.applyBackstageLabels(backstage, deployment) + + if err = r.addParams(ctx, backstage, ns, deployment); err != nil { + return err + } + + r.applyApplicationParamsFromCR(backstage, deployment) + + if err = r.validateAndUpdatePsqlSecretRef(backstage, deployment); err != nil { + return fmt.Errorf("failed to validate database secret, reason: %s", err) + } + + if r.OwnsRuntime { + if err = controllerutil.SetControllerReference(&backstage, deployment, r.Scheme); err != nil { + return fmt.Errorf("failed to set owner reference: %s", err) + } + } + + deployment.ObjectMeta.DeepCopyInto(&targetDeployment.ObjectMeta) + deployment.Spec.DeepCopyInto(&targetDeployment.Spec) + return nil + } +} + +func (r *BackstageReconciler) addParams(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { + if err := r.addVolumes(ctx, backstage, ns, deployment); err != nil { + return fmt.Errorf("failed to add volumes to Backstage deployment, reason: %s", err) + } + + if err := r.addVolumeMounts(ctx, backstage, ns, deployment); err != nil { + return fmt.Errorf("failed to add volume mounts to Backstage deployment, reason: %s", err) + } + + if err := r.addContainerArgs(ctx, backstage, ns, deployment); err != nil { + return fmt.Errorf("failed to add container args to Backstage deployment, reason: %s", err) + } + + if err := r.addEnvVars(backstage, ns, deployment); err != nil { + return fmt.Errorf("failed to add env vars to Backstage deployment, reason: %s", err) + } + return nil +} + +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 { + return err + } + if dpConfVol != nil { + deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, *dpConfVol) + } + + backendAuthAppConfig, err := r.getBackendAuthAppConfig(ctx, backstage, ns) + if err != nil { + return err + } + + deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, r.appConfigsToVolumes(backstage, backendAuthAppConfig)...) + deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, r.extraFilesToVolumes(backstage)...) + return nil +} + +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 + } + backendAuthAppConfig, err := r.getBackendAuthAppConfig(ctx, backstage, ns) + if err != nil { + return err + } + err = r.addAppConfigsVolumeMounts(ctx, backstage, ns, deployment, backendAuthAppConfig) + if err != nil { + return err + } + return r.addExtraFilesVolumeMounts(ctx, backstage, ns, deployment) +} + +func (r *BackstageReconciler) addContainerArgs(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { + backendAuthAppConfig, err := r.getBackendAuthAppConfig(ctx, backstage, ns) + if err != nil { + return err + } + return r.addAppConfigsContainerArgs(ctx, backstage, ns, deployment, backendAuthAppConfig) +} + +func (r *BackstageReconciler) addEnvVars(backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { + r.addExtraEnvs(backstage, deployment) + return nil +} + +func (r *BackstageReconciler) validateAndUpdatePsqlSecretRef(backstage bs.Backstage, deployment *appsv1.Deployment) error { + for i, c := range deployment.Spec.Template.Spec.Containers { + if c.Name != _defaultBackstageMainContainerName { + continue + } + for k, from := range deployment.Spec.Template.Spec.Containers[i].EnvFrom { + if from.SecretRef.Name != postGresSecret { + continue + } + if len(backstage.Spec.Database.AuthSecretName) == 0 { + from.SecretRef.Name = getDefaultPsqlSecretName(&backstage) + } else { + from.SecretRef.Name = backstage.Spec.Database.AuthSecretName + } + deployment.Spec.Template.Spec.Containers[i].EnvFrom[k] = from + break + } + } + + return nil +} + +func (r *BackstageReconciler) setDefaultDeploymentImage(deployment *appsv1.Deployment) { + if envBackstageImage != "" { + visitContainers(&deployment.Spec.Template, func(container *v1.Container) { + container.Image = envBackstageImage + + }) + } +} + +func (r *BackstageReconciler) applyBackstageLabels(backstage bs.Backstage, deployment *appsv1.Deployment) { + setBackstageAppLabel(&deployment.Spec.Template.ObjectMeta.Labels, backstage) + setBackstageAppLabel(&deployment.Spec.Selector.MatchLabels, backstage) + r.labels(&deployment.ObjectMeta, backstage) +} + +func (r *BackstageReconciler) applyApplicationParamsFromCR(backstage bs.Backstage, deployment *appsv1.Deployment) { + if backstage.Spec.Application != nil { + deployment.Spec.Replicas = backstage.Spec.Application.Replicas + if backstage.Spec.Application.Image != nil { + visitContainers(&deployment.Spec.Template, func(container *v1.Container) { + container.Image = *backstage.Spec.Application.Image + }) + } + if backstage.Spec.Application.ImagePullSecrets != nil { // use image pull secrets from the CR spec + deployment.Spec.Template.Spec.ImagePullSecrets = nil + if len(*backstage.Spec.Application.ImagePullSecrets) > 0 { + for _, imagePullSecret := range *backstage.Spec.Application.ImagePullSecrets { + deployment.Spec.Template.Spec.ImagePullSecrets = append(deployment.Spec.Template.Spec.ImagePullSecrets, v1.LocalObjectReference{ + Name: imagePullSecret, + }) + } + } + } + } +} + +func getDefaultObjName(backstage bs.Backstage) string { + return fmt.Sprintf("backstage-%s", backstage.Name) +} + +func getDefaultDbObjName(backstage bs.Backstage) string { + return fmt.Sprintf("backstage-psql-%s", backstage.Name) +} diff --git a/controllers/local_db_statefulset.go b/controllers/local_db_statefulset.go new file mode 100644 index 00000000..327f63ce --- /dev/null +++ b/controllers/local_db_statefulset.go @@ -0,0 +1,164 @@ +/* +Copyright 2023. + +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" + + 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" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + bs "janus-idp.io/backstage-operator/api/v1alpha1" +) + +const ( + ownerRefFmt = "failed to set owner reference: %s" +) + +func (r *BackstageReconciler) reconcileLocalDbStatefulSet(ctx context.Context, backstage *bs.Backstage, ns string) error { + statefulSet := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: getDefaultDbObjName(*backstage), + Namespace: ns, + }, + } + if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, statefulSet, r.localDBStatefulSetMutFun(ctx, statefulSet, *backstage, ns)); err != nil { + if errors.IsConflict(err) { + return retryReconciliation(err) + } + msg := fmt.Sprintf("failed to deploy Database StatefulSet: %s", err) + setStatusCondition(backstage, bs.ConditionDeployed, metav1.ConditionFalse, bs.DeployFailed, msg) + return fmt.Errorf(msg) + } + return nil +} + +func (r *BackstageReconciler) localDBStatefulSetMutFun(ctx context.Context, targetStatefulSet *appsv1.StatefulSet, backstage bs.Backstage, ns string) controllerutil.MutateFn { + return func() error { + statefulSet := &appsv1.StatefulSet{} + targetStatefulSet.ObjectMeta.DeepCopyInto(&statefulSet.ObjectMeta) + err := r.readConfigMapOrDefault(ctx, backstage.Spec.RawRuntimeConfig.LocalDbConfigName, "db-statefulset.yaml", ns, statefulSet) + if err != nil { + return err + } + + // Override the name + statefulSet.Name = getDefaultDbObjName(backstage) + if err = r.patchLocalDbStatefulSetObj(statefulSet, backstage); err != nil { + return err + } + r.labels(&statefulSet.ObjectMeta, backstage) + if err = r.patchLocalDbStatefulSetObj(statefulSet, backstage); err != nil { + return err + } + + r.setDefaultStatefulSetImage(statefulSet) + + _, err = r.handlePsqlSecret(ctx, statefulSet, &backstage) + if err != nil { + return err + } + + if r.OwnsRuntime { + // Set the ownerreferences for the statefulset so that when the backstage CR is deleted, + // the statefulset is automatically deleted + // Note that the PVCs associated with the statefulset are not deleted automatically + // to prevent data loss. However OpenShift v4.14 and Kubernetes v1.27 introduced an optional + // parameter persistentVolumeClaimRetentionPolicy in the statefulset spec: + // spec: + // persistentVolumeClaimRetentionPolicy: + // whenDeleted: Delete + // whenScaled: Retain + // This will allow the PVCs to get automatically deleted when the statefulset is deleted if + // the StatefulSetAutoDeletePVC feature gate is enabled on the API server. + // For more information, see https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/ + if err := controllerutil.SetControllerReference(&backstage, statefulSet, r.Scheme); err != nil { + return fmt.Errorf(ownerRefFmt, err) + } + } + + statefulSet.ObjectMeta.DeepCopyInto(&targetStatefulSet.ObjectMeta) + statefulSet.Spec.DeepCopyInto(&targetStatefulSet.Spec) + return nil + } +} + +func (r *BackstageReconciler) patchLocalDbStatefulSetObj(statefulSet *appsv1.StatefulSet, backstage bs.Backstage) error { + name := getDefaultDbObjName(backstage) + statefulSet.SetName(name) + statefulSet.Spec.Template.SetName(name) + statefulSet.Spec.ServiceName = fmt.Sprintf("%s-hl", name) + + setLabel(&statefulSet.Spec.Template.ObjectMeta.Labels, name) + setLabel(&statefulSet.Spec.Selector.MatchLabels, name) + + return nil +} + +func (r *BackstageReconciler) setDefaultStatefulSetImage(statefulSet *appsv1.StatefulSet) { + if envPostgresImage != "" { + visitContainers(&statefulSet.Spec.Template, func(container *v1.Container) { + container.Image = envPostgresImage + }) + } +} + +// cleanupLocalDbResources removes all local db related resources, including statefulset, services and generated secret. +func (r *BackstageReconciler) cleanupLocalDbResources(ctx context.Context, backstage bs.Backstage) error { + statefulSet := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: getDefaultDbObjName(backstage), + Namespace: backstage.Namespace, + }, + } + if _, err := r.cleanupResource(ctx, statefulSet, backstage); err != nil { + return fmt.Errorf("failed to delete database statefulset, reason: %s", err) + } + + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: getDefaultDbObjName(backstage), + Namespace: backstage.Namespace, + }, + } + if _, err := r.cleanupResource(ctx, service, backstage); err != nil { + return fmt.Errorf("failed to delete database service, reason: %s", err) + } + serviceHL := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("backstage-psql-%s-hl", backstage.Name), + Namespace: backstage.Namespace, + }, + } + if _, err := r.cleanupResource(ctx, serviceHL, backstage); err != nil { + return fmt.Errorf("failed to delete headless database service, reason: %s", err) + } + + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: getDefaultPsqlSecretName(&backstage), + Namespace: backstage.Namespace, + }, + } + if _, err := r.cleanupResource(ctx, secret, backstage); err != nil { + return fmt.Errorf("failed to delete generated database secret, reason: %s", err) + } + return nil +} diff --git a/examples/janus-cr-with-app-configs.yaml b/examples/janus-cr-with-app-configs.yaml index 1f08edad..2ddaf4ba 100644 --- a/examples/janus-cr-with-app-configs.yaml +++ b/examples/janus-cr-with-app-configs.yaml @@ -80,6 +80,7 @@ data: target: https://github.com/ododev/odo-backstage-software-template/blob/main/template.yaml rules: - allow: [Template] + # # catalog.providers.githubOrg.default.orgUrl --- apiVersion: v1 @@ -132,7 +133,7 @@ data: endpoints: /explore-backend-completed: target: 'http://localhost:7017' - + --- apiVersion: v1 kind: ConfigMap diff --git a/main.go b/main.go index 47635ea5..63ba642f 100644 --- a/main.go +++ b/main.go @@ -32,10 +32,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" - backstageiov1alpha1 "redhat-developer/red-hat-developer-hub-operator/api/v1alpha1" - controller "redhat-developer/red-hat-developer-hub-operator/controllers" - openshift "github.com/openshift/api/route/v1" + backstageiov1alpha1 "janus-idp.io/backstage-operator/api/v1alpha1" + controller "janus-idp.io/backstage-operator/controllers" //+kubebuilder:scaffold:imports ) @@ -109,7 +108,7 @@ func main() { Scheme: mgr.GetScheme(), OwnsRuntime: ownRuntime, IsOpenShift: isOpenShift, - }).SetupWithManager(mgr); err != nil { + }).SetupWithManager(mgr, setupLog); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Backstage") os.Exit(1) }