diff --git a/api/v1alpha1/backstage_types.go b/api/v1alpha1/backstage_types.go index 42fa4b2f..cd9b0a63 100644 --- a/api/v1alpha1/backstage_types.go +++ b/api/v1alpha1/backstage_types.go @@ -25,67 +25,139 @@ const ( // BackstageSpec defines the desired state of Backstage type BackstageSpec struct { - // References to existing app-configs Config objects. + // Configuration for Backstage. Optional. + Application *Application `json:"application,omitempty"` + + // Raw Runtime Objects configuration. For Advanced scenarios. + RawRuntimeConfig RuntimeConfig `json:"rawRuntimeConfig,omitempty"` + + // Control the creation of a local PostgreSQL DB. Set to false if using for example an external Database for Backstage. + // To use an external Database, you can provide your own app-config file (see the AppConfig field in the Application structure) + // containing references to the Database connection information, + // which might be supplied as environment variables (see the ExtraEnvs field) or extra-configuration files + // (see the ExtraFiles field in the Application structure). + // +optional + //+kubebuilder:default=true + EnableLocalDb *bool `json:"enableLocalDb,omitempty"` +} + +type Application struct { + // References to existing app-configs ConfigMap objects, that will be mounted as files in the specified mount path. // Each element can be a reference to any ConfigMap or Secret, - // and will be mounted inside the main application container under a 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 + // and will be mounted inside the main application container under a specified mount directory. + // Additionally, each file will be passed as a `--config /mount/path/to/configmap/key` to the // main container args in the order of the entries defined in the AppConfigs list. - // But bear in mind that for a single 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 - // 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"` - - // 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"` + // But bear in mind that for a single ConfigMap element containing several filenames, + // the order in which those files will be appended to the main container args cannot be guaranteed. + // So if you want to pass multiple app-config files, it is recommended to pass one ConfigMap per app-config file. + // +optional + AppConfig *AppConfig `json:"appConfig,omitempty"` + + // Reference to an existing ConfigMap for Dynamic Plugins. + // A new one will be generated with the default config if not set. + // The ConfigMap object must have an existing key named: 'dynamic-plugins.yaml'. + // +optional + DynamicPluginsConfigMapName string `json:"dynamicPluginsConfigMapName,omitempty"` + + // References to existing Config objects to use as extra config files. + // They will be mounted as files in the specified mount path. + // Each element can be a reference to any ConfigMap or Secret. + // +optional + ExtraFiles *ExtraFiles `json:"extraFiles,omitempty"` + + // Extra environment variables + // +optional + ExtraEnvs *ExtraEnvs `json:"extraEnvs,omitempty"` - //+kubebuilder:default=false - SkipLocalDb bool `json:"skipLocalDb,omitempty"` + // Number of desired replicas to set in the Backstage Deployment. + // Defaults to 1. + // +optional + //+kubebuilder:default=1 + Replicas *int32 `json:"replicas,omitempty"` + + // Image to use in all containers (including Init Containers) + // +optional + Image *string `json:"image,omitempty"` + + // Image Pull Secrets to use in all containers (including Init Containers) + // +optional + ImagePullSecrets []string `json:"imagePullSecrets,omitempty"` } -type AppConfigRef struct { - // Name of an existing App Config object - //+kubebuilder:validation:Required - Name string `json:"name"` +type AppConfig struct { + // Mount path for all app-config files listed in the ConfigMapRefs field + // +optional + // +kubebuilder:default=/opt/app-root/src + MountPath string `json:"mountPath,omitempty"` + + // List of ConfigMaps storing the app-config files. Will be mounted as files under the MountPath specified. + // For each item in this array, if a key is not specified, it means that all keys in the ConfigMap will be mounted as files. + // Otherwise, only the specified key will be mounted as a file. + // Bear in mind not to put sensitive data in those ConfigMaps. Instead, your app-config content can reference + // environment variables (which you can set with the ExtraEnvs field) and/or include extra files (see the ExtraFiles field). + // More details on https://backstage.io/docs/conf/writing/. + // +optional + ConfigMaps []ObjectKeyRef `json:"configMaps,omitempty"` +} - // Type of the existing App Config object, either ConfigMap or Secret - //+kubebuilder:validation:Required - //+kubebuilder:validation:Enum=ConfigMap;Secret - Kind string `json:"kind"` +type ExtraFiles struct { + // Mount path for all extra configuration files listed in the Items field + // +optional + // +kubebuilder:default=/opt/app-root/src + MountPath string `json:"mountPath,omitempty"` + + // List of references to ConfigMaps objects mounted as extra files under the MountPath specified. + // For each item in this array, if a key is not specified, it means that all keys in the ConfigMap will be mounted as files. + // Otherwise, only the specified key will be mounted as a file. + // +optional + ConfigMaps []ObjectKeyRef `json:"configMaps,omitempty"` + + // List of references to Secrets objects mounted as extra files under the MountPath specified. + // For each item in this array, if a key is not specified, it means that all keys in the Secret will be mounted as files. + // Otherwise, only the specified key will be mounted as a file. + // +optional + Secrets []ObjectKeyRef `json:"secrets,omitempty"` } -type DynamicPluginsConfigRef struct { - // Name of the Dynamic Plugins config object - // +kubebuilder:validation:Required - Name string `json:"name"` +type ExtraEnvs struct { + // List of references to ConfigMaps objects to inject as additional environment variables. + // For each item in this array, if a key is not specified, it means that all keys in the ConfigMap will be injected as additional environment variables. + // Otherwise, only the specified key will be injected as an additional environment variable. + // +optional + ConfigMaps []ObjectKeyRef `json:"configMaps,omitempty"` - // Type of the Dynamic Plugins config object, either ConfigMap or Secret - //+kubebuilder:validation:Required - //+kubebuilder:validation:Enum=ConfigMap;Secret - Kind string `json:"kind"` + // List of references to Secrets objects to inject as additional environment variables. + // For each item in this array, if a key is not specified, it means that all keys in the Secret will be injected as additional environment variables. + // Otherwise, only the specified key will be injected as environment variable. + // +optional + Secrets []ObjectKeyRef `json:"secrets,omitempty"` + + // List of name and value pairs to add as environment variables. + // +optional + Envs []Env `json:"envs,omitempty"` } -type BackendAuthSecretRef struct { - // Name of the secret to use for the backend auth +type ObjectKeyRef struct { + // Name of the object + // We support only ConfigMaps and Secrets. //+kubebuilder:validation:Required Name string `json:"name"` - // Key in the secret to use for the backend auth. Default value is: backend-secret - //+kubebuilder:default=backend-secret + // Key in the object + // +optional Key string `json:"key,omitempty"` } +type Env struct { + // Name of the environment variable + //+kubebuilder:validation:Required + Name string `json:"name"` + + // Value of the environment variable + //+kubebuilder:validation:Required + Value string `json:"value"` +} + 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 891bee7f..e32d262a 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -27,31 +27,66 @@ import ( ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AppConfigRef) DeepCopyInto(out *AppConfigRef) { +func (in *AppConfig) DeepCopyInto(out *AppConfig) { *out = *in + if in.ConfigMaps != nil { + in, out := &in.ConfigMaps, &out.ConfigMaps + *out = make([]ObjectKeyRef, len(*in)) + copy(*out, *in) + } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppConfigRef. -func (in *AppConfigRef) DeepCopy() *AppConfigRef { +// 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(AppConfigRef) + 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 *BackendAuthSecretRef) DeepCopyInto(out *BackendAuthSecretRef) { +func (in *Application) DeepCopyInto(out *Application) { *out = *in + if in.AppConfig != nil { + in, out := &in.AppConfig, &out.AppConfig + *out = new(AppConfig) + (*in).DeepCopyInto(*out) + } + if in.ExtraFiles != nil { + in, out := &in.ExtraFiles, &out.ExtraFiles + *out = new(ExtraFiles) + (*in).DeepCopyInto(*out) + } + if in.ExtraEnvs != nil { + in, out := &in.ExtraEnvs, &out.ExtraEnvs + *out = new(ExtraEnvs) + (*in).DeepCopyInto(*out) + } + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + if in.Image != nil { + in, out := &in.Image, &out.Image + *out = new(string) + **out = **in + } + if in.ImagePullSecrets != nil { + in, out := &in.ImagePullSecrets, &out.ImagePullSecrets + *out = make([]string, len(*in)) + copy(*out, *in) + } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendAuthSecretRef. -func (in *BackendAuthSecretRef) DeepCopy() *BackendAuthSecretRef { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Application. +func (in *Application) DeepCopy() *Application { if in == nil { return nil } - out := new(BackendAuthSecretRef) + out := new(Application) in.DeepCopyInto(out) return out } @@ -118,22 +153,17 @@ func (in *BackstageList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BackstageSpec) DeepCopyInto(out *BackstageSpec) { *out = *in - if in.AppConfigs != nil { - in, out := &in.AppConfigs, &out.AppConfigs - *out = make([]AppConfigRef, len(*in)) - copy(*out, *in) + if in.Application != nil { + in, out := &in.Application, &out.Application + *out = new(Application) + (*in).DeepCopyInto(*out) } - 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.RawRuntimeConfig = in.RawRuntimeConfig + if in.EnableLocalDb != nil { + in, out := &in.EnableLocalDb, &out.EnableLocalDb + *out = new(bool) **out = **in } - out.RawRuntimeConfig = in.RawRuntimeConfig } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackstageSpec. @@ -169,16 +199,86 @@ func (in *BackstageStatus) DeepCopy() *BackstageStatus { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DynamicPluginsConfigRef) DeepCopyInto(out *DynamicPluginsConfigRef) { +func (in *Env) DeepCopyInto(out *Env) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Env. +func (in *Env) DeepCopy() *Env { + if in == nil { + return nil + } + out := new(Env) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExtraEnvs) DeepCopyInto(out *ExtraEnvs) { + *out = *in + if in.ConfigMaps != nil { + in, out := &in.ConfigMaps, &out.ConfigMaps + *out = make([]ObjectKeyRef, len(*in)) + copy(*out, *in) + } + if in.Secrets != nil { + in, out := &in.Secrets, &out.Secrets + *out = make([]ObjectKeyRef, len(*in)) + copy(*out, *in) + } + if in.Envs != nil { + in, out := &in.Envs, &out.Envs + *out = make([]Env, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtraEnvs. +func (in *ExtraEnvs) DeepCopy() *ExtraEnvs { + if in == nil { + return nil + } + out := new(ExtraEnvs) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExtraFiles) DeepCopyInto(out *ExtraFiles) { + *out = *in + if in.ConfigMaps != nil { + in, out := &in.ConfigMaps, &out.ConfigMaps + *out = make([]ObjectKeyRef, len(*in)) + copy(*out, *in) + } + if in.Secrets != nil { + in, out := &in.Secrets, &out.Secrets + *out = make([]ObjectKeyRef, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtraFiles. +func (in *ExtraFiles) DeepCopy() *ExtraFiles { + if in == nil { + return nil + } + out := new(ExtraFiles) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObjectKeyRef) DeepCopyInto(out *ObjectKeyRef) { *out = *in } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DynamicPluginsConfigRef. -func (in *DynamicPluginsConfigRef) DeepCopy() *DynamicPluginsConfigRef { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectKeyRef. +func (in *ObjectKeyRef) DeepCopy() *ObjectKeyRef { if in == nil { return nil } - out := new(DynamicPluginsConfigRef) + out := new(ObjectKeyRef) in.DeepCopyInto(out) return out } diff --git a/bundle.Dockerfile b/bundle.Dockerfile index 513284c3..7887d751 100644 --- a/bundle.Dockerfile +++ b/bundle.Dockerfile @@ -6,7 +6,7 @@ LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ LABEL operators.operatorframework.io.bundle.package.v1=backstage-operator LABEL operators.operatorframework.io.bundle.channels.v1=alpha -LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.25.0 +LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.32.0 LABEL operators.operatorframework.io.metrics.mediatype.v1=metrics+v1 LABEL operators.operatorframework.io.metrics.project_layout=go.kubebuilder.io/v3 diff --git a/bundle/manifests/backstage-default-config_v1_configmap.yaml b/bundle/manifests/backstage-default-config_v1_configmap.yaml index f1ef5a24..c40fdb63 100644 --- a/bundle/manifests/backstage-default-config_v1_configmap.yaml +++ b/bundle/manifests/backstage-default-config_v1_configmap.yaml @@ -1,12 +1,17 @@ apiVersion: v1 data: - backend-auth-secret.yaml: | + backend-auth-configmap.yaml: | apiVersion: v1 - kind: Secret + kind: ConfigMap metadata: - name: # placeholder for '-auth' + name: # placeholder for '-backend-auth' data: - # A random value will be generated for the backend-secret key + "app-config.backend-auth.default.yaml": | + backend: + auth: + keys: + # This is a default value, which you should change by providing your own app-config + - secret: "pl4s3Ch4ng3M3" db-service-hl.yaml: |- apiVersion: v1 kind: Service diff --git a/bundle/manifests/backstage-operator.clusterserviceversion.yaml b/bundle/manifests/backstage-operator.clusterserviceversion.yaml index 15ccac06..a2f1c5ff 100644 --- a/bundle/manifests/backstage-operator.clusterserviceversion.yaml +++ b/bundle/manifests/backstage-operator.clusterserviceversion.yaml @@ -21,7 +21,8 @@ metadata: } ] capabilities: Basic Install - operators.operatorframework.io/builder: operator-sdk-v1.25.0 + createdAt: "2023-12-11T15:41:14Z" + operators.operatorframework.io/builder: operator-sdk-v1.32.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 name: backstage-operator.v0.0.1 namespace: placeholder diff --git a/bundle/manifests/janus-idp.io_backstages.yaml b/bundle/manifests/janus-idp.io_backstages.yaml index 803f96a7..2135f30e 100644 --- a/bundle/manifests/janus-idp.io_backstages.yaml +++ b/bundle/manifests/janus-idp.io_backstages.yaml @@ -34,76 +34,196 @@ spec: spec: description: BackstageSpec defines the desired state of Backstage properties: - appConfigs: - description: References to existing app-configs Config objects. Each - element can be a reference to any ConfigMap or Secret, and will - be mounted inside the main application container under a dedicated - directory containing the ConfigMap or Secret name. Additionally, - each file will be passed as a `--config /path/to/secret_or_configmap/key` - to the main container args in the order of the entries defined in - the AppConfigs list. But bear in mind that for a single AppConfig - element containing several files, the order in which those files - will be appended to the container args, the main container args - cannot be guaranteed. So if you want to pass multiple app-config - files, it is recommended to pass one ConfigMap/Secret per app-config - file. - items: - properties: - kind: - description: Type of the existing App Config object, either - ConfigMap or Secret - enum: - - ConfigMap - - Secret - type: string - name: - description: Name of an existing App Config object - type: string - required: - - kind - - name - type: object - type: array - backendAuthSecretRef: - description: Optional Backend Auth Secret Name. A new one will be - generated if not set. This Secret is used to set an environment - variable named 'APP_CONFIG_backend_auth_keys' in the main container, - which takes precedence over any 'backend.auth.keys' field defined - in default or custom application configuration files. This is required - for service-to-service auth and is shared by all backend plugins. + application: + description: Configuration for Backstage. Optional. properties: - key: - default: backend-secret - description: 'Key in the secret to use for the backend auth. Default - value is: backend-secret' + appConfig: + description: References to existing app-configs ConfigMap objects, + that will be mounted as files in the specified mount path. Each + element can be a reference to any ConfigMap or Secret, and will + be mounted inside the main application container under a specified + mount directory. Additionally, each file will be passed as a + `--config /mount/path/to/configmap/key` to the main container + args in the order of the entries defined in the AppConfigs list. + But bear in mind that for a single ConfigMap element containing + several filenames, the order in which those files will be appended + to the main container args cannot be guaranteed. So if you want + to pass multiple app-config files, it is recommended to pass + one ConfigMap per app-config file. + properties: + configMaps: + description: List of ConfigMaps storing the app-config files. + Will be mounted as files under the MountPath specified. + For each item in this array, if a key is not specified, + it means that all keys in the ConfigMap will be mounted + as files. Otherwise, only the specified key will be mounted + as a file. Bear in mind not to put sensitive data in those + ConfigMaps. Instead, your app-config content can reference + environment variables (which you can set with the ExtraEnvs + field) and/or include extra files (see the ExtraFiles field). + More details on https://backstage.io/docs/conf/writing/. + items: + properties: + key: + description: Key in the object + type: string + name: + description: Name of the object We support only ConfigMaps + and Secrets. + type: string + required: + - name + type: object + type: array + mountPath: + default: /opt/app-root/src + description: Mount path for all app-config files listed in + the ConfigMapRefs field + type: string + type: object + dynamicPluginsConfigMapName: + description: 'Reference to an existing ConfigMap for Dynamic Plugins. + A new one will be generated with the default config if not set. + The ConfigMap object must have an existing key named: ''dynamic-plugins.yaml''.' type: string - name: - description: Name of the secret to use for the backend auth + extraEnvs: + description: Extra environment variables + properties: + configMaps: + description: List of references to ConfigMaps objects to inject + as additional environment variables. For each item in this + array, if a key is not specified, it means that all keys + in the ConfigMap will be injected as additional environment + variables. Otherwise, only the specified key will be injected + as an additional environment variable. + items: + properties: + key: + description: Key in the object + type: string + name: + description: Name of the object We support only ConfigMaps + and Secrets. + type: string + required: + - name + type: object + type: array + envs: + description: List of name and value pairs to add as environment + variables. + items: + properties: + name: + description: Name of the environment variable + type: string + value: + description: Value of the environment variable + type: string + required: + - name + - value + type: object + type: array + secrets: + description: List of references to Secrets objects to inject + as additional environment variables. For each item in this + array, if a key is not specified, it means that all keys + in the Secret will be injected as additional environment + variables. Otherwise, only the specified key will be injected + as environment variable. + items: + properties: + key: + description: Key in the object + type: string + name: + description: Name of the object We support only ConfigMaps + and Secrets. + type: string + required: + - name + type: object + type: array + type: object + extraFiles: + description: References to existing Config objects to use as extra + config files. They will be mounted as files in the specified + mount path. Each element can be a reference to any ConfigMap + or Secret. + properties: + configMaps: + description: List of references to ConfigMaps objects mounted + as extra files under the MountPath specified. For each item + in this array, if a key is not specified, it means that + all keys in the ConfigMap will be mounted as files. Otherwise, + only the specified key will be mounted as a file. + items: + properties: + key: + description: Key in the object + type: string + name: + description: Name of the object We support only ConfigMaps + and Secrets. + type: string + required: + - name + type: object + type: array + mountPath: + default: /opt/app-root/src + description: Mount path for all extra configuration files + listed in the Items field + type: string + secrets: + description: List of references to Secrets objects mounted + as extra files under the MountPath specified. For each item + in this array, if a key is not specified, it means that + all keys in the Secret will be mounted as files. Otherwise, + only the specified key will be mounted as a file. + items: + properties: + key: + description: Key in the object + type: string + name: + description: Name of the object We support only ConfigMaps + and Secrets. + type: string + required: + - name + type: object + type: array + type: object + image: + description: Image to use in all containers (including Init Containers) type: string - required: - - name - type: object - dynamicPluginsConfig: - description: 'Reference to an existing configuration object for Dynamic - Plugins. This can be a reference to any ConfigMap or Secret, but - the object must have an existing key named: ''dynamic-plugins.yaml''' - properties: - kind: - description: Type of the Dynamic Plugins config object, either - ConfigMap or Secret - enum: - - ConfigMap - - Secret - type: string - name: - description: Name of the Dynamic Plugins config object - type: string - required: - - kind - - name + imagePullSecrets: + description: Image Pull Secrets to use in all containers (including + Init Containers) + items: + type: string + type: array + replicas: + default: 1 + description: Number of desired replicas to set in the Backstage + Deployment. Defaults to 1. + format: int32 + type: integer type: object + enableLocalDb: + default: true + description: Control the creation of a local PostgreSQL DB. Set to + false if using for example an external Database for Backstage. To + use an external Database, you can provide your own app-config file + (see the AppConfig field in the Application structure) containing + references to the Database connection information, which might be + supplied as environment variables (see the ExtraEnvs field) or extra-configuration + files (see the ExtraFiles field in the Application structure). + type: boolean rawRuntimeConfig: - description: Raw Runtime Objects configuration + description: Raw Runtime Objects configuration. For Advanced scenarios. properties: backstageConfig: description: Name of ConfigMap containing Backstage runtime objects @@ -114,9 +234,6 @@ spec: runtime objects configuration type: string type: object - skipLocalDb: - default: false - type: boolean type: object status: description: BackstageStatus defines the observed state of Backstage diff --git a/bundle/metadata/annotations.yaml b/bundle/metadata/annotations.yaml index c877e0fb..a6b3d099 100644 --- a/bundle/metadata/annotations.yaml +++ b/bundle/metadata/annotations.yaml @@ -5,7 +5,7 @@ annotations: operators.operatorframework.io.bundle.metadata.v1: metadata/ operators.operatorframework.io.bundle.package.v1: backstage-operator operators.operatorframework.io.bundle.channels.v1: alpha - operators.operatorframework.io.metrics.builder: operator-sdk-v1.25.0 + operators.operatorframework.io.metrics.builder: operator-sdk-v1.32.0 operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 operators.operatorframework.io.metrics.project_layout: go.kubebuilder.io/v3 diff --git a/config/crd/bases/janus-idp.io_backstages.yaml b/config/crd/bases/janus-idp.io_backstages.yaml index ce2ae5b5..a28cac0f 100644 --- a/config/crd/bases/janus-idp.io_backstages.yaml +++ b/config/crd/bases/janus-idp.io_backstages.yaml @@ -35,76 +35,196 @@ spec: spec: description: BackstageSpec defines the desired state of Backstage properties: - appConfigs: - description: References to existing app-configs Config objects. Each - element can be a reference to any ConfigMap or Secret, and will - be mounted inside the main application container under a dedicated - directory containing the ConfigMap or Secret name. Additionally, - each file will be passed as a `--config /path/to/secret_or_configmap/key` - to the main container args in the order of the entries defined in - the AppConfigs list. But bear in mind that for a single AppConfig - element containing several files, the order in which those files - will be appended to the container args, the main container args - cannot be guaranteed. So if you want to pass multiple app-config - files, it is recommended to pass one ConfigMap/Secret per app-config - file. - items: - properties: - kind: - description: Type of the existing App Config object, either - ConfigMap or Secret - enum: - - ConfigMap - - Secret - type: string - name: - description: Name of an existing App Config object - type: string - required: - - kind - - name - type: object - type: array - backendAuthSecretRef: - description: Optional Backend Auth Secret Name. A new one will be - generated if not set. This Secret is used to set an environment - variable named 'APP_CONFIG_backend_auth_keys' in the main container, - which takes precedence over any 'backend.auth.keys' field defined - in default or custom application configuration files. This is required - for service-to-service auth and is shared by all backend plugins. + application: + description: Configuration for Backstage. Optional. properties: - key: - default: backend-secret - description: 'Key in the secret to use for the backend auth. Default - value is: backend-secret' + appConfig: + description: References to existing app-configs ConfigMap objects, + that will be mounted as files in the specified mount path. Each + element can be a reference to any ConfigMap or Secret, and will + be mounted inside the main application container under a specified + mount directory. Additionally, each file will be passed as a + `--config /mount/path/to/configmap/key` to the main container + args in the order of the entries defined in the AppConfigs list. + But bear in mind that for a single ConfigMap element containing + several filenames, the order in which those files will be appended + to the main container args cannot be guaranteed. So if you want + to pass multiple app-config files, it is recommended to pass + one ConfigMap per app-config file. + properties: + configMaps: + description: List of ConfigMaps storing the app-config files. + Will be mounted as files under the MountPath specified. + For each item in this array, if a key is not specified, + it means that all keys in the ConfigMap will be mounted + as files. Otherwise, only the specified key will be mounted + as a file. Bear in mind not to put sensitive data in those + ConfigMaps. Instead, your app-config content can reference + environment variables (which you can set with the ExtraEnvs + field) and/or include extra files (see the ExtraFiles field). + More details on https://backstage.io/docs/conf/writing/. + items: + properties: + key: + description: Key in the object + type: string + name: + description: Name of the object We support only ConfigMaps + and Secrets. + type: string + required: + - name + type: object + type: array + mountPath: + default: /opt/app-root/src + description: Mount path for all app-config files listed in + the ConfigMapRefs field + type: string + type: object + dynamicPluginsConfigMapName: + description: 'Reference to an existing ConfigMap for Dynamic Plugins. + A new one will be generated with the default config if not set. + The ConfigMap object must have an existing key named: ''dynamic-plugins.yaml''.' type: string - name: - description: Name of the secret to use for the backend auth + extraEnvs: + description: Extra environment variables + properties: + configMaps: + description: List of references to ConfigMaps objects to inject + as additional environment variables. For each item in this + array, if a key is not specified, it means that all keys + in the ConfigMap will be injected as additional environment + variables. Otherwise, only the specified key will be injected + as an additional environment variable. + items: + properties: + key: + description: Key in the object + type: string + name: + description: Name of the object We support only ConfigMaps + and Secrets. + type: string + required: + - name + type: object + type: array + envs: + description: List of name and value pairs to add as environment + variables. + items: + properties: + name: + description: Name of the environment variable + type: string + value: + description: Value of the environment variable + type: string + required: + - name + - value + type: object + type: array + secrets: + description: List of references to Secrets objects to inject + as additional environment variables. For each item in this + array, if a key is not specified, it means that all keys + in the Secret will be injected as additional environment + variables. Otherwise, only the specified key will be injected + as environment variable. + items: + properties: + key: + description: Key in the object + type: string + name: + description: Name of the object We support only ConfigMaps + and Secrets. + type: string + required: + - name + type: object + type: array + type: object + extraFiles: + description: References to existing Config objects to use as extra + config files. They will be mounted as files in the specified + mount path. Each element can be a reference to any ConfigMap + or Secret. + properties: + configMaps: + description: List of references to ConfigMaps objects mounted + as extra files under the MountPath specified. For each item + in this array, if a key is not specified, it means that + all keys in the ConfigMap will be mounted as files. Otherwise, + only the specified key will be mounted as a file. + items: + properties: + key: + description: Key in the object + type: string + name: + description: Name of the object We support only ConfigMaps + and Secrets. + type: string + required: + - name + type: object + type: array + mountPath: + default: /opt/app-root/src + description: Mount path for all extra configuration files + listed in the Items field + type: string + secrets: + description: List of references to Secrets objects mounted + as extra files under the MountPath specified. For each item + in this array, if a key is not specified, it means that + all keys in the Secret will be mounted as files. Otherwise, + only the specified key will be mounted as a file. + items: + properties: + key: + description: Key in the object + type: string + name: + description: Name of the object We support only ConfigMaps + and Secrets. + type: string + required: + - name + type: object + type: array + type: object + image: + description: Image to use in all containers (including Init Containers) type: string - required: - - name - type: object - dynamicPluginsConfig: - description: 'Reference to an existing configuration object for Dynamic - Plugins. This can be a reference to any ConfigMap or Secret, but - the object must have an existing key named: ''dynamic-plugins.yaml''' - properties: - kind: - description: Type of the Dynamic Plugins config object, either - ConfigMap or Secret - enum: - - ConfigMap - - Secret - type: string - name: - description: Name of the Dynamic Plugins config object - type: string - required: - - kind - - name + imagePullSecrets: + description: Image Pull Secrets to use in all containers (including + Init Containers) + items: + type: string + type: array + replicas: + default: 1 + description: Number of desired replicas to set in the Backstage + Deployment. Defaults to 1. + format: int32 + type: integer type: object + enableLocalDb: + default: true + description: Control the creation of a local PostgreSQL DB. Set to + false if using for example an external Database for Backstage. To + use an external Database, you can provide your own app-config file + (see the AppConfig field in the Application structure) containing + references to the Database connection information, which might be + supplied as environment variables (see the ExtraEnvs field) or extra-configuration + files (see the ExtraFiles field in the Application structure). + type: boolean rawRuntimeConfig: - description: Raw Runtime Objects configuration + description: Raw Runtime Objects configuration. For Advanced scenarios. properties: backstageConfig: description: Name of ConfigMap containing Backstage runtime objects @@ -115,9 +235,6 @@ spec: runtime objects configuration type: string type: object - skipLocalDb: - default: false - type: boolean type: object status: description: BackstageStatus defines the observed state of Backstage diff --git a/config/manager/default-config/backend-auth-configmap.yaml b/config/manager/default-config/backend-auth-configmap.yaml new file mode 100644 index 00000000..b862592d --- /dev/null +++ b/config/manager/default-config/backend-auth-configmap.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: # placeholder for '-backend-auth' +data: + "app-config.backend-auth.default.yaml": | + backend: + auth: + keys: + # This is a default value, which you should change by providing your own app-config + - secret: "pl4s3Ch4ng3M3" diff --git a/config/manager/default-config/backend-auth-secret.yaml b/config/manager/default-config/backend-auth-secret.yaml deleted file mode 100644 index 34e04f9a..00000000 --- a/config/manager/default-config/backend-auth-secret.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: # placeholder for '-auth' -data: -# A random value will be generated for the backend-secret key diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 3dd75881..627bca60 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -17,6 +17,6 @@ configMapGenerator: - default-config/db-statefulset.yaml - default-config/db-service.yaml - default-config/db-service-hl.yaml - - default-config/backend-auth-secret.yaml + - default-config/backend-auth-configmap.yaml - default-config/dynamic-plugins-configmap.yaml name: default-config diff --git a/controllers/backstage_app_config.go b/controllers/backstage_app_config.go index fde78bb2..bcfaa953 100644 --- a/controllers/backstage_app_config.go +++ b/controllers/backstage_app_config.go @@ -30,24 +30,25 @@ type appConfigData struct { 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{ +func (r *BackstageReconciler) appConfigsToVolumes(backstage bs.Backstage, backendAuthAppConfig *bs.ObjectKeyRef) (result []v1.Volume) { + var cms []bs.ObjectKeyRef + if backendAuthAppConfig != nil { + cms = append(cms, *backendAuthAppConfig) + } + if backstage.Spec.Application != nil && backstage.Spec.Application.AppConfig != nil { + cms = append(cms, backstage.Spec.Application.AppConfig.ConfigMaps...) + } + + for _, cm := range cms { + volumeSource := v1.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, - } + LocalObjectReference: v1.LocalObjectReference{Name: cm.Name}, + }, } result = append(result, v1.Volume{ - Name: appConfig.Name, + Name: cm.Name, VolumeSource: volumeSource, }, ) @@ -56,8 +57,26 @@ func (r *BackstageReconciler) appConfigsToVolumes(backstage bs.Backstage) (resul return result } -func (r *BackstageReconciler) addAppConfigsVolumeMounts(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { - appConfigFilenamesList, err := r.extractAppConfigFileNames(ctx, backstage, ns) +func (r *BackstageReconciler) addAppConfigsVolumeMounts( + ctx context.Context, + backstage bs.Backstage, + ns string, + deployment *appsv1.Deployment, + backendAuthAppConfig *bs.ObjectKeyRef, +) error { + var ( + mountPath = _containersWorkingDir + cms []bs.ObjectKeyRef + ) + if backendAuthAppConfig != nil { + cms = append(cms, *backendAuthAppConfig) + } + if backstage.Spec.Application != nil && backstage.Spec.Application.AppConfig != nil { + cms = append(cms, backstage.Spec.Application.AppConfig.ConfigMaps...) + mountPath = backstage.Spec.Application.AppConfig.MountPath + } + + appConfigFilenamesList, err := r.extractAppConfigFileNames(ctx, ns, cms) if err != nil { return err } @@ -65,11 +84,14 @@ func (r *BackstageReconciler) addAppConfigsVolumeMounts(ctx context.Context, bac for i, c := range deployment.Spec.Template.Spec.Containers { if c.Name == _defaultBackstageMainContainerName { for _, appConfigFilenames := range appConfigFilenamesList { - deployment.Spec.Template.Spec.Containers[i].VolumeMounts = append(deployment.Spec.Template.Spec.Containers[i].VolumeMounts, - v1.VolumeMount{ - Name: appConfigFilenames.ref, - MountPath: fmt.Sprintf("%s/%s", _containersWorkingDir, appConfigFilenames.ref), - }) + for _, f := range appConfigFilenames.files { + deployment.Spec.Template.Spec.Containers[i].VolumeMounts = append(deployment.Spec.Template.Spec.Containers[i].VolumeMounts, + v1.VolumeMount{ + Name: appConfigFilenames.ref, + MountPath: fmt.Sprintf("%s/%s", mountPath, f), + SubPath: f, + }) + } } break } @@ -77,8 +99,26 @@ func (r *BackstageReconciler) addAppConfigsVolumeMounts(ctx context.Context, bac return nil } -func (r *BackstageReconciler) addAppConfigsContainerArgs(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { - appConfigFilenamesList, err := r.extractAppConfigFileNames(ctx, backstage, ns) +func (r *BackstageReconciler) addAppConfigsContainerArgs( + ctx context.Context, + backstage bs.Backstage, + ns string, + deployment *appsv1.Deployment, + backendAuthAppConfig *bs.ObjectKeyRef, +) error { + var ( + mountPath = _containersWorkingDir + cms []bs.ObjectKeyRef + ) + if backendAuthAppConfig != nil { + cms = append(cms, *backendAuthAppConfig) + } + if backstage.Spec.Application != nil && backstage.Spec.Application.AppConfig != nil { + cms = append(cms, backstage.Spec.Application.AppConfig.ConfigMaps...) + mountPath = backstage.Spec.Application.AppConfig.MountPath + } + + appConfigFilenamesList, err := r.extractAppConfigFileNames(ctx, ns, cms) if err != nil { return err } @@ -88,9 +128,9 @@ func (r *BackstageReconciler) addAppConfigsContainerArgs(ctx context.Context, ba for _, appConfigFilenames := range appConfigFilenamesList { // Args for _, fileName := range appConfigFilenames.files { + appConfigPath := fmt.Sprintf("%s/%s", mountPath, fileName) deployment.Spec.Template.Spec.Containers[i].Args = - append(deployment.Spec.Template.Spec.Containers[i].Args, "--config", - fmt.Sprintf("%s/%s/%s", _containersWorkingDir, appConfigFilenames.ref, fileName)) + append(deployment.Spec.Template.Spec.Containers[i].Args, "--config", appConfigPath) } } break @@ -102,14 +142,16 @@ func (r *BackstageReconciler) addAppConfigsContainerArgs(ctx context.Context, ba // extractAppConfigFileNames returns a mapping of app-config object name and the list of files in it. // We intentionally do not return a Map, to preserve the iteration order of the AppConfigs in the Custom Resource, // even though we can't guarantee the iteration order of the files listed inside each ConfigMap or Secret. -func (r *BackstageReconciler) extractAppConfigFileNames(ctx context.Context, backstage bs.Backstage, ns string) ([]appConfigData, error) { - var result []appConfigData - for _, appConfig := range backstage.Spec.AppConfigs { +func (r *BackstageReconciler) extractAppConfigFileNames(ctx context.Context, ns string, cms []bs.ObjectKeyRef) (result []appConfigData, err error) { + for _, cmRef := range cms { var files []string - switch appConfig.Kind { - case "ConfigMap": + if cmRef.Key != "" { + // Limit to that file only + files = append(files, cmRef.Key) + } else { + // All keys cm := v1.ConfigMap{} - if err := r.Get(ctx, types.NamespacedName{Name: appConfig.Name, Namespace: ns}, &cm); err != nil { + if err = r.Get(ctx, types.NamespacedName{Name: cmRef.Name, Namespace: ns}, &cm); err != nil { return nil, err } for filename := range cm.Data { @@ -120,18 +162,9 @@ func (r *BackstageReconciler) extractAppConfigFileNames(ctx context.Context, bac // Bear in mind that iteration order over this map is not guaranteed by Go files = append(files, filename) } - case "Secret": - 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) - } } result = append(result, appConfigData{ - ref: appConfig.Name, + ref: cmRef.Name, files: files, }) } diff --git a/controllers/backstage_backend_auth.go b/controllers/backstage_backend_auth.go index 6cc532c9..d90b1990 100644 --- a/controllers/backstage_backend_auth.go +++ b/controllers/backstage_backend_auth.go @@ -16,121 +16,52 @@ package controller import ( "context" - "crypto/rand" - "encoding/base64" "fmt" bs "janus-idp.io/backstage-operator/api/v1alpha1" - appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" - "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) -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) { - if backstage.Spec.BackendAuthSecretRef != nil { - return backstage.Spec.BackendAuthSecretRef.Name, nil +func (r *BackstageReconciler) getBackendAuthAppConfig( + ctx context.Context, + backstage bs.Backstage, + ns string, +) (backendAuthAppConfig *bs.ObjectKeyRef, err error) { + if backstage.Spec.Application != nil && + (backstage.Spec.Application.AppConfig != nil || backstage.Spec.Application.ExtraFiles != nil || backstage.Spec.Application.ExtraEnvs != nil) { + // Users are expected to fill their app-config(s) with their own backend auth key + return nil, nil } - //Create default Secret for backend auth - var sec v1.Secret - //var isDefault bool - err = r.readConfigMapOrDefault(ctx, backstage.Spec.RawRuntimeConfig.BackstageConfigName, "backend-auth-secret.yaml", ns, &sec) + var cm v1.ConfigMap + err = r.readConfigMapOrDefault(ctx, backstage.Spec.RawRuntimeConfig.BackstageConfigName, "backend-auth-configmap.yaml", ns, &cm) if err != nil { - return "", fmt.Errorf("failed to read config: %s", err) + return nil, fmt.Errorf("failed to read config: %s", err) } - //Generate a secret if it does not exist - backendAuthSecretName := fmt.Sprintf("%s-auth", backstage.Name) - sec.SetName(backendAuthSecretName) - err = r.Get(ctx, types.NamespacedName{Name: backendAuthSecretName, Namespace: ns}, &sec) + // Create ConfigMap + backendAuthCmName := fmt.Sprintf("%s-auth-app-config", backstage.Name) + cm.SetName(backendAuthCmName) + err = r.Get(ctx, types.NamespacedName{Name: backendAuthCmName, Namespace: ns}, &cm) if err != nil { if !errors.IsNotFound(err) { - return "", fmt.Errorf("failed to get secret for backend auth (%q), reason: %s", backendAuthSecretName, err) - } - var k string - if backstage.Spec.BackendAuthSecretRef != nil { - k = backstage.Spec.BackendAuthSecretRef.Key - } - if k == "" { - //TODO(rm3l): why kubebuilder default values do not work - k = "backend-secret" + return nil, fmt.Errorf("failed to get ConfigMap for backend auth (%q), reason: %s", backendAuthCmName, err) } + setBackstageAppLabel(&cm.ObjectMeta.Labels, backstage) + r.labels(&cm.ObjectMeta, backstage) - // there should not be any difference between default and not default - // if isDefault { - // Create a secret with a random value - authVal := func(length int) string { - bytes := make([]byte, length) - if _, randErr := rand.Read(bytes); randErr != nil { - // Do not fail, but use a fallback value - return _defaultBackendAuthSecretValue + if r.OwnsRuntime { + if err = controllerutil.SetControllerReference(&backstage, &cm, r.Scheme); err != nil { + return nil, fmt.Errorf("failed to set owner reference: %s", err) } - return base64.StdEncoding.EncodeToString(bytes) - }(24) - sec.Data = map[string][]byte{ - k: []byte(authVal), } - // } - err = r.Create(ctx, &sec) + err = r.Create(ctx, &cm) if err != nil { - return "", fmt.Errorf("failed to create secret for backend auth, reason: %s", err) + return nil, fmt.Errorf("failed to create ConfigMap for backend auth, reason: %s", err) } } - return 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 { - 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" - } - 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 + return &bs.ObjectKeyRef{Name: backendAuthCmName}, nil } diff --git a/controllers/backstage_controller.go b/controllers/backstage_controller.go index 58262bf1..c195833a 100644 --- a/controllers/backstage_controller.go +++ b/controllers/backstage_controller.go @@ -30,6 +30,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/utils/pointer" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" @@ -90,7 +91,7 @@ func (r *BackstageReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return ctrl.Result{}, fmt.Errorf("failed to load backstage deployment from the cluster: %w", err) } - if !backstage.Spec.SkipLocalDb { + if pointer.BoolDeref(backstage.Spec.EnableLocalDb, true) { /* We use default strogeclass currently, and no PV is needed in that case. If we decide later on to support user provided storageclass we can enable pv creation. diff --git a/controllers/backstage_controller_test.go b/controllers/backstage_controller_test.go index 9ec82f08..23275973 100644 --- a/controllers/backstage_controller_test.go +++ b/controllers/backstage_controller_test.go @@ -27,7 +27,7 @@ import ( "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" + "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/reconcile" bsv1alpha1 "janus-idp.io/backstage-operator/api/v1alpha1" @@ -124,19 +124,44 @@ var _ = Describe("Backstage controller", func() { } findEnvVar := func(envVars []corev1.EnvVar, key string) (corev1.EnvVar, bool) { - return findElementByPredicate(envVars, func(envVar corev1.EnvVar) bool { + list := findElementsByPredicate(envVars, func(envVar corev1.EnvVar) bool { return envVar.Name == key }) + if len(list) == 0 { + return corev1.EnvVar{}, false + } + return list[0], true + } + + findEnvVarFrom := func(envVars []corev1.EnvFromSource, key string) (corev1.EnvFromSource, bool) { + list := findElementsByPredicate(envVars, func(envVar corev1.EnvFromSource) bool { + var n string + switch { + case envVar.ConfigMapRef != nil: + n = envVar.ConfigMapRef.Name + case envVar.SecretRef != nil: + n = envVar.SecretRef.Name + } + return n == key + }) + if len(list) == 0 { + return corev1.EnvFromSource{}, false + } + return list[0], true } findVolume := func(vols []corev1.Volume, name string) (corev1.Volume, bool) { - return findElementByPredicate(vols, func(vol corev1.Volume) bool { + list := findElementsByPredicate(vols, func(vol corev1.Volume) bool { return vol.Name == name }) + if len(list) == 0 { + return corev1.Volume{}, false + } + return list[0], true } - findVolumeMount := func(mounts []corev1.VolumeMount, name string) (corev1.VolumeMount, bool) { - return findElementByPredicate(mounts, func(mount corev1.VolumeMount) bool { + findVolumeMounts := func(mounts []corev1.VolumeMount, name string) []corev1.VolumeMount { + return findElementsByPredicate(mounts, func(mount corev1.VolumeMount) bool { return mount.Name == name }) } @@ -162,17 +187,24 @@ var _ = Describe("Backstage controller", func() { }) Expect(err).To(Not(HaveOccurred())) - By("Generating a value for backend auth secret key") + By("creating a StatefulSet for the Database") Eventually(func(g Gomega) { - found := &corev1.Secret{} - err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: backstageName + "-auth"}, found) + found := &appsv1.StatefulSet{} + name := fmt.Sprintf("backstage-psql-%s", backstage.Name) + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, found) g.Expect(err).ShouldNot(HaveOccurred()) - - g.Expect(found.Data).To(HaveKey("backend-secret")) - g.Expect(found.Data["backend-secret"]).To(Not(BeEmpty()), - "backend auth secret should contain a non-empty 'backend-secret' in its data") }, time.Minute, time.Second).Should(Succeed()) + backendAuthConfigName := fmt.Sprintf("%s-auth-app-config", backstageName) + By("Creating a ConfigMap for default backend auth key", func() { + Eventually(func(g Gomega) { + found := &corev1.ConfigMap{} + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: backendAuthConfigName}, found) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(found.Data).ToNot(BeEmpty(), "backend auth secret should contain non-empty data") + }, time.Minute, time.Second).Should(Succeed()) + }) + By("Generating a ConfigMap for default config for dynamic plugins") dynamicPluginsConfigName := fmt.Sprintf("%s-dynamic-plugins", backstageName) Eventually(func(g Gomega) { @@ -192,22 +224,11 @@ var _ = Describe("Backstage controller", func() { 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 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 number of replicas") + Expect(found.Spec.Replicas).To(HaveValue(BeEquivalentTo(1))) By("Checking the Volumes in the Backstage Deployment", func() { - Expect(found.Spec.Template.Spec.Volumes).To(HaveLen(3)) + Expect(found.Spec.Template.Spec.Volumes).To(HaveLen(4)) _, ok := findVolume(found.Spec.Template.Spec.Volumes, "dynamic-plugins-root") Expect(ok).To(BeTrue(), "No volume found with name: dynamic-plugins-root") @@ -220,6 +241,12 @@ var _ = Describe("Backstage controller", func() { Expect(dynamicPluginsConfigVol.VolumeSource.Secret).To(BeNil()) Expect(dynamicPluginsConfigVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) Expect(dynamicPluginsConfigVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(dynamicPluginsConfigName)) + + backendAuthAppConfigVol, ok := findVolume(found.Spec.Template.Spec.Volumes, backendAuthConfigName) + Expect(ok).To(BeTrue(), "No volume found with name: %s", backendAuthConfigName) + Expect(backendAuthAppConfigVol.VolumeSource.Secret).To(BeNil()) + Expect(backendAuthAppConfigVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) + Expect(backendAuthAppConfigVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(backendAuthConfigName)) }) By("Checking the Number of init containers in the Backstage Deployment") @@ -235,25 +262,23 @@ var _ = Describe("Backstage controller", func() { By("Checking the Init Container Volume Mounts in the Backstage Deployment", func() { Expect(initCont.VolumeMounts).To(HaveLen(3)) - dpRoot, ok := findVolumeMount(initCont.VolumeMounts, "dynamic-plugins-root") - Expect(ok).To(BeTrue(), - "No volume mount found with name: dynamic-plugins-root") - Expect(dpRoot.MountPath).To(Equal("/dynamic-plugins-root")) - Expect(dpRoot.ReadOnly).To(BeFalse()) - Expect(dpRoot.SubPath).To(BeEmpty()) - - dpNpmrc, ok := findVolumeMount(initCont.VolumeMounts, "dynamic-plugins-npmrc") - Expect(ok).To(BeTrue(), - "No volume mount found with name: dynamic-plugins-npmrc") - Expect(dpNpmrc.MountPath).To(Equal("/opt/app-root/src/.npmrc.dynamic-plugins")) - Expect(dpNpmrc.ReadOnly).To(BeTrue()) - Expect(dpNpmrc.SubPath).To(Equal(".npmrc")) - - dp, ok := findVolumeMount(initCont.VolumeMounts, dynamicPluginsConfigName) - Expect(ok).To(BeTrue(), "No volume mount found with name: %s", dynamicPluginsConfigName) - Expect(dp.MountPath).To(Equal("/opt/app-root/src/dynamic-plugins.yaml")) - Expect(dp.SubPath).To(Equal("dynamic-plugins.yaml")) - Expect(dp.ReadOnly).To(BeTrue()) + dpRoot := findVolumeMounts(initCont.VolumeMounts, "dynamic-plugins-root") + Expect(dpRoot).To(HaveLen(1), "No volume mount found with name: dynamic-plugins-root") + Expect(dpRoot[0].MountPath).To(Equal("/dynamic-plugins-root")) + Expect(dpRoot[0].ReadOnly).To(BeFalse()) + Expect(dpRoot[0].SubPath).To(BeEmpty()) + + dpNpmrc := findVolumeMounts(initCont.VolumeMounts, "dynamic-plugins-npmrc") + Expect(dpNpmrc).To(HaveLen(1), "No volume mount found with name: dynamic-plugins-npmrc") + Expect(dpNpmrc[0].MountPath).To(Equal("/opt/app-root/src/.npmrc.dynamic-plugins")) + Expect(dpNpmrc[0].ReadOnly).To(BeTrue()) + Expect(dpNpmrc[0].SubPath).To(Equal(".npmrc")) + + dp := findVolumeMounts(initCont.VolumeMounts, dynamicPluginsConfigName) + Expect(dp).To(HaveLen(1), "No volume mount found with name: %s", dynamicPluginsConfigName) + Expect(dp[0].MountPath).To(Equal("/opt/app-root/src/dynamic-plugins.yaml")) + Expect(dp[0].SubPath).To(Equal("dynamic-plugins.yaml")) + Expect(dp[0].ReadOnly).To(BeTrue()) }) By("Checking the Number of main containers in the Backstage Deployment") @@ -261,18 +286,25 @@ var _ = Describe("Backstage controller", func() { mainCont := found.Spec.Template.Spec.Containers[0] By("Checking the main container Args in the Backstage Deployment", func() { - Expect(mainCont.Args).To(HaveLen(2)) + Expect(mainCont.Args).To(HaveLen(4)) Expect(mainCont.Args[0]).To(Equal("--config")) Expect(mainCont.Args[1]).To(Equal("dynamic-plugins-root/app-config.dynamic-plugins.yaml")) + Expect(mainCont.Args[2]).To(Equal("--config")) + Expect(mainCont.Args[3]).To(Equal("/opt/app-root/src/app-config.backend-auth.default.yaml")) }) By("Checking the main container Volume Mounts in the Backstage Deployment", func() { - Expect(mainCont.VolumeMounts).To(HaveLen(1)) + Expect(mainCont.VolumeMounts).To(HaveLen(2)) + + dpRoot := findVolumeMounts(mainCont.VolumeMounts, "dynamic-plugins-root") + Expect(dpRoot).To(HaveLen(1), "No volume mount found with name: dynamic-plugins-root") + Expect(dpRoot[0].MountPath).To(Equal("/opt/app-root/src/dynamic-plugins-root")) + Expect(dpRoot[0].SubPath).To(BeEmpty()) - 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()) + bsAuth := findVolumeMounts(mainCont.VolumeMounts, backendAuthConfigName) + Expect(bsAuth).To(HaveLen(1), "No volume mount found with name: %s", backendAuthConfigName) + Expect(bsAuth[0].MountPath).To(Equal("/opt/app-root/src/app-config.backend-auth.default.yaml")) + Expect(bsAuth[0].SubPath).To(Equal("app-config.backend-auth.default.yaml")) }) By("Checking the latest Status added to the Backstage instance") @@ -423,17 +455,277 @@ spec: }) Context("App Configs", func() { + When("referencing non-existing ConfigMap as app-config", func() { + var backstage *bsv1alpha1.Backstage + + BeforeEach(func() { + backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ + Application: &bsv1alpha1.Application{ + AppConfig: &bsv1alpha1.AppConfig{ + ConfigMaps: []bsv1alpha1.ObjectKeyRef{ + {Name: "a-non-existing-cm"}, + }, + }, + }, + }) + err := k8sClient.Create(ctx, backstage) + Expect(err).To(Not(HaveOccurred())) + }) + + It("should fail to reconcile", func() { + By("Checking if the custom resource was successfully created") + Eventually(func() error { + found := &bsv1alpha1.Backstage{} + return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Not reconciling the custom resource created") + _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, + }) + Expect(err).To(HaveOccurred()) + + By("Not creating a Backstage Deployment") + Consistently(func() error { + // TODO to get name from default + return k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, &appsv1.Deployment{}) + }, 5*time.Second, time.Second).Should(Not(Succeed())) + }) + }) + + for _, mountPath := range []string{"", "/some/path/for/app-config"} { + mountPath := mountPath + for _, key := range []string{"", "my-app-config-12.yaml"} { + key := key + When(fmt.Sprintf("referencing ConfigMaps for app-configs (mountPath=%q, key=%q) and dynamic plugins config ConfigMap", mountPath, key), + func() { + const ( + appConfig1CmName = "my-app-config-1-cm" + dynamicPluginsConfigName = "my-dynamic-plugins-config" + ) + + var backstage *bsv1alpha1.Backstage + + BeforeEach(func() { + appConfig1Cm := buildConfigMap(appConfig1CmName, map[string]string{ + "my-app-config-11.yaml": ` +# my-app-config-11.yaml +`, + "my-app-config-12.yaml": ` +# my-app-config-12.yaml +`, + }) + err := k8sClient.Create(ctx, appConfig1Cm) + Expect(err).To(Not(HaveOccurred())) + + dynamicPluginsCm := buildConfigMap(dynamicPluginsConfigName, map[string]string{ + "dynamic-plugins.yaml": ` +# dynamic-plugins.yaml (configmap) +includes: [dynamic-plugins.default.yaml] +plugins: [] +`, + }) + err = k8sClient.Create(ctx, dynamicPluginsCm) + Expect(err).To(Not(HaveOccurred())) + + backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ + Application: &bsv1alpha1.Application{ + AppConfig: &bsv1alpha1.AppConfig{ + MountPath: mountPath, + ConfigMaps: []bsv1alpha1.ObjectKeyRef{ + { + Name: appConfig1CmName, + Key: key, + }, + }, + }, + DynamicPluginsConfigMapName: dynamicPluginsConfigName, + }, + }) + err = k8sClient.Create(ctx, backstage) + Expect(err).To(Not(HaveOccurred())) + }) + + It("should reconcile", func() { + By("Checking if the custom resource was successfully created") + Eventually(func() error { + found := &bsv1alpha1.Backstage{} + return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Reconciling the custom resource created") + _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Checking that the Deployment was successfully created in the reconciliation") + found := &appsv1.Deployment{} + Eventually(func(g Gomega) { + // TODO to get name from default + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, found) + g.Expect(err).To(Not(HaveOccurred())) + }, time.Minute, time.Second).Should(Succeed()) + + By("Checking the Volumes in the Backstage Deployment", func() { + Expect(found.Spec.Template.Spec.Volumes).To(HaveLen(4)) + + _, ok := findVolume(found.Spec.Template.Spec.Volumes, "dynamic-plugins-root") + Expect(ok).To(BeTrue(), "No volume found with name: dynamic-plugins-root") + + _, ok = findVolume(found.Spec.Template.Spec.Volumes, "dynamic-plugins-npmrc") + Expect(ok).To(BeTrue(), "No volume found with name: dynamic-plugins-npmrc") + + appConfig1CmVol, ok := findVolume(found.Spec.Template.Spec.Volumes, appConfig1CmName) + Expect(ok).To(BeTrue(), "No volume found with name: %s", appConfig1CmName) + Expect(appConfig1CmVol.VolumeSource.Secret).To(BeNil()) + Expect(appConfig1CmVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) + Expect(appConfig1CmVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(appConfig1CmName)) + + dynamicPluginsConfigVol, ok := findVolume(found.Spec.Template.Spec.Volumes, dynamicPluginsConfigName) + Expect(ok).To(BeTrue(), "No volume found with name: %s", dynamicPluginsConfigName) + Expect(dynamicPluginsConfigVol.VolumeSource.Secret).To(BeNil()) + Expect(dynamicPluginsConfigVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) + Expect(dynamicPluginsConfigVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(dynamicPluginsConfigName)) + }) + + By("Checking the Number of init containers in the Backstage Deployment") + Expect(found.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + initCont := found.Spec.Template.Spec.InitContainers[0] + + By("Checking the Init Container Env Vars in the Backstage Deployment", func() { + Expect(initCont.Env).To(HaveLen(1)) + Expect(initCont.Env[0].Name).To(Equal("NPM_CONFIG_USERCONFIG")) + Expect(initCont.Env[0].Value).To(Equal("/opt/app-root/src/.npmrc.dynamic-plugins")) + }) + + By("Checking the Init Container Volume Mounts in the Backstage Deployment", func() { + Expect(initCont.VolumeMounts).To(HaveLen(3)) + + dpRoot := findVolumeMounts(initCont.VolumeMounts, "dynamic-plugins-root") + Expect(dpRoot).To(HaveLen(1), + "No volume mount found with name: dynamic-plugins-root") + Expect(dpRoot[0].MountPath).To(Equal("/dynamic-plugins-root")) + Expect(dpRoot[0].ReadOnly).To(BeFalse()) + Expect(dpRoot[0].SubPath).To(BeEmpty()) + + dpNpmrc := findVolumeMounts(initCont.VolumeMounts, "dynamic-plugins-npmrc") + Expect(dpNpmrc).To(HaveLen(1), + "No volume mount found with name: dynamic-plugins-npmrc") + Expect(dpNpmrc[0].MountPath).To(Equal("/opt/app-root/src/.npmrc.dynamic-plugins")) + Expect(dpNpmrc[0].ReadOnly).To(BeTrue()) + Expect(dpNpmrc[0].SubPath).To(Equal(".npmrc")) + + dp := findVolumeMounts(initCont.VolumeMounts, dynamicPluginsConfigName) + Expect(dp).To(HaveLen(1), "No volume mount found with name: %s", dynamicPluginsConfigName) + Expect(dp[0].MountPath).To(Equal("/opt/app-root/src/dynamic-plugins.yaml")) + Expect(dp[0].SubPath).To(Equal("dynamic-plugins.yaml")) + Expect(dp[0].ReadOnly).To(BeTrue()) + }) + + By("Checking the Number of main containers in the Backstage Deployment") + Expect(found.Spec.Template.Spec.Containers).To(HaveLen(1)) + mainCont := found.Spec.Template.Spec.Containers[0] + + expectedMountPath := mountPath + if expectedMountPath == "" { + expectedMountPath = "/opt/app-root/src" + } + + By("Checking the main container Args in the Backstage Deployment", func() { + nbArgs := 6 + if key != "" { + nbArgs = 4 + } + Expect(mainCont.Args).To(HaveLen(nbArgs)) + Expect(mainCont.Args[1]).To(Equal("dynamic-plugins-root/app-config.dynamic-plugins.yaml")) + for i := 0; i <= nbArgs-2; i += 2 { + Expect(mainCont.Args[i]).To(Equal("--config")) + } + if key == "" { + //TODO(rm3l): the order of the rest of the --config args should be the same as the order in + // which the keys are listed in the ConfigMap/Secrets + // But as this is returned as a map, Go does not provide any guarantee on the iteration order. + Expect(mainCont.Args[3]).To(SatisfyAny( + Equal(expectedMountPath+"/my-app-config-11.yaml"), + Equal(expectedMountPath+"/my-app-config-12.yaml"), + )) + Expect(mainCont.Args[5]).To(SatisfyAny( + Equal(expectedMountPath+"/my-app-config-11.yaml"), + Equal(expectedMountPath+"/my-app-config-12.yaml"), + )) + Expect(mainCont.Args[3]).To(Not(Equal(mainCont.Args[5]))) + } else { + Expect(mainCont.Args[3]).To(Equal(fmt.Sprintf("%s/%s", expectedMountPath, key))) + } + }) + + By("Checking the main container Volume Mounts in the Backstage Deployment", func() { + nbMounts := 3 + if key != "" { + nbMounts = 2 + } + Expect(mainCont.VolumeMounts).To(HaveLen(nbMounts)) + + dpRoot := findVolumeMounts(mainCont.VolumeMounts, "dynamic-plugins-root") + Expect(dpRoot).To(HaveLen(1), "No volume mount found with name: dynamic-plugins-root") + Expect(dpRoot[0].MountPath).To(Equal("/opt/app-root/src/dynamic-plugins-root")) + Expect(dpRoot[0].SubPath).To(BeEmpty()) + + appConfig1CmMounts := findVolumeMounts(mainCont.VolumeMounts, appConfig1CmName) + Expect(appConfig1CmMounts).To(HaveLen(nbMounts-1), "Wrong number of volume mounts found with name: %s", appConfig1CmName) + if key != "" { + Expect(appConfig1CmMounts).To(HaveLen(1), "Wrong number of volume mounts found with name: %s", appConfig1CmName) + Expect(appConfig1CmMounts[0].MountPath).To(Equal(fmt.Sprintf("%s/%s", expectedMountPath, key))) + Expect(appConfig1CmMounts[0].SubPath).To(Equal(key)) + } else { + Expect(appConfig1CmMounts).To(HaveLen(2), "Wrong number of volume mounts found with name: %s", appConfig1CmName) + Expect(appConfig1CmMounts[0].MountPath).ToNot(Equal(appConfig1CmMounts[1].MountPath)) + for i := 0; i <= 1; i++ { + Expect(appConfig1CmMounts[i].MountPath).To( + SatisfyAny( + Equal(expectedMountPath+"/my-app-config-11.yaml"), + Equal(expectedMountPath+"/my-app-config-12.yaml"))) + Expect(appConfig1CmMounts[i].SubPath).To( + SatisfyAny( + Equal("my-app-config-11.yaml"), + Equal("my-app-config-12.yaml"))) + } + } + }) + + By("Checking the latest Status added to the Backstage instance") + verifyBackstageInstance(ctx) + + }) + }) + } + } + }) + + Context("Extra Files", func() { for _, kind := range []string{"ConfigMap", "Secret"} { kind := kind - When(fmt.Sprintf("referencing non-existing %s as app-config", kind), func() { + When(fmt.Sprintf("referencing non-existing %s as extra-file", kind), func() { var backstage *bsv1alpha1.Backstage BeforeEach(func() { + var ( + cmExtraFiles []bsv1alpha1.ObjectKeyRef + secExtraFiles []bsv1alpha1.ObjectKeyRef + ) + name := "a-non-existing-" + strings.ToLower(kind) + switch kind { + case "ConfigMap": + cmExtraFiles = append(cmExtraFiles, bsv1alpha1.ObjectKeyRef{Name: name}) + case "Secret": + secExtraFiles = append(secExtraFiles, bsv1alpha1.ObjectKeyRef{Name: name}) + } backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - AppConfigs: []bsv1alpha1.AppConfigRef{ - { - Name: "a-non-existing-" + strings.ToLower(kind), - Kind: kind, + Application: &bsv1alpha1.Application{ + ExtraFiles: &bsv1alpha1.ExtraFiles{ + ConfigMaps: cmExtraFiles, + Secrets: secExtraFiles, }, }, }) @@ -463,79 +755,77 @@ spec: }) } - for _, dynamicPluginsConfigKind := range []string{"ConfigMap", "Secret"} { - dynamicPluginsConfigKind := dynamicPluginsConfigKind - When("referencing ConfigMaps and Secrets for app-configs and dynamic plugins config as "+dynamicPluginsConfigKind, func() { + for _, mountPath := range []string{"", "/some/path/for/extra/config"} { + mountPath := mountPath + When("referencing ConfigMaps and Secrets for extra files - mountPath="+mountPath, func() { const ( - appConfig1CmName = "my-app-config-1-cm" - appConfig2SecretName = "my-app-config-2-secret" - dynamicPluginsConfigName = "my-dynamic-plugins-config" + extraConfig1CmNameAll = "my-extra-config-1-cm-all" + extraConfig2SecretNameAll = "my-extra-config-2-secret-all" + extraConfig1CmNameSingle = "my-extra-config-1-cm-single" + extraConfig2SecretNameSingle = "my-extra-config-2-secret-single" ) var backstage *bsv1alpha1.Backstage BeforeEach(func() { - appConfig1Cm := buildConfigMap(appConfig1CmName, map[string]string{ - "my-app-config-11.yaml": ` -# my-app-config-11.yaml + extraConfig1CmAll := buildConfigMap(extraConfig1CmNameAll, map[string]string{ + "my-extra-config-11.yaml": ` +# my-extra-config-11.yaml `, - "my-app-config-12.yaml": ` -# my-app-config-12.yaml + "my-extra-config-12.yaml": ` +# my-extra-config-12.yaml `, }) - err := k8sClient.Create(ctx, appConfig1Cm) + err := k8sClient.Create(ctx, extraConfig1CmAll) Expect(err).To(Not(HaveOccurred())) - appConfig2Secret := buildSecret(appConfig2SecretName, map[string][]byte{ - "my-app-config-21.yaml": []byte(` -# my-app-config-21.yaml + extraConfig2SecretAll := buildSecret(extraConfig2SecretNameAll, map[string][]byte{ + "my-extra-config-21.yaml": []byte(` +# my-extra-config-21.yaml `), - "my-app-config-22.yaml": []byte(` -# my-app-config-22.yaml + "my-extra-config-22.yaml": []byte(` +# my-extra-config-22.yaml `), }) - err = k8sClient.Create(ctx, appConfig2Secret) + err = k8sClient.Create(ctx, extraConfig2SecretAll) 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: [] + extraConfig1CmSingle := buildConfigMap(extraConfig1CmNameSingle, map[string]string{ + "my-extra-file-11-single.yaml": ` +# my-extra-file-11-single.yaml `, - }) - case "Secret": - dynamicPluginsObject = buildSecret(dynamicPluginsConfigName, map[string][]byte{ - "dynamic-plugins.yaml": []byte(` -# dynamic-plugins.yaml (secret) -includes: [dynamic-plugins.default.yaml] -plugins: [] + "my-extra-file-12-single.yaml": ` +# my-extra-file-12-single.yaml +`, + }) + err = k8sClient.Create(ctx, extraConfig1CmSingle) + Expect(err).To(Not(HaveOccurred())) + + extraConfig2SecretSingle := buildSecret(extraConfig2SecretNameSingle, map[string][]byte{ + "my-extra-file-21-single.yaml": []byte(` +# my-extra-file-21-single.yaml `), - }) - default: - Fail(fmt.Sprintf("unsupported kind for dynamic plugins object: %q", dynamicPluginsConfigKind)) - } - err = k8sClient.Create(ctx, dynamicPluginsObject) + "my-extra-file-22-single.yaml": []byte(` +# my-extra-file-22-single.yaml +`), + }) + err = k8sClient.Create(ctx, extraConfig2SecretSingle) Expect(err).To(Not(HaveOccurred())) backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - AppConfigs: []bsv1alpha1.AppConfigRef{ - { - Name: appConfig1CmName, - Kind: "ConfigMap", - }, - { - Name: appConfig2SecretName, - Kind: "Secret", + Application: &bsv1alpha1.Application{ + ExtraFiles: &bsv1alpha1.ExtraFiles{ + MountPath: mountPath, + ConfigMaps: []bsv1alpha1.ObjectKeyRef{ + {Name: extraConfig1CmNameAll}, + {Name: extraConfig1CmNameSingle, Key: "my-extra-file-12-single.yaml"}, + }, + Secrets: []bsv1alpha1.ObjectKeyRef{ + {Name: extraConfig2SecretNameAll}, + {Name: extraConfig2SecretNameSingle, Key: "my-extra-file-22-single.yaml"}, + }, }, }, - DynamicPluginsConfig: &bsv1alpha1.DynamicPluginsConfigRef{ - Name: dynamicPluginsConfigName, - Kind: dynamicPluginsConfigKind, - }, }) err = k8sClient.Create(ctx, backstage) Expect(err).To(Not(HaveOccurred())) @@ -563,286 +853,452 @@ plugins: [] }, time.Minute, time.Second).Should(Succeed()) By("Checking the Volumes in the Backstage Deployment", func() { - Expect(found.Spec.Template.Spec.Volumes).To(HaveLen(5)) - - _, 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)) - } + Expect(found.Spec.Template.Spec.Volumes).To(HaveLen(7)) + + extraConfig1CmVol, ok := findVolume(found.Spec.Template.Spec.Volumes, extraConfig1CmNameAll) + Expect(ok).To(BeTrue(), "No volume found with name: %s", extraConfig1CmNameAll) + Expect(extraConfig1CmVol.VolumeSource.Secret).To(BeNil()) + Expect(extraConfig1CmVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) + Expect(extraConfig1CmVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(extraConfig1CmNameAll)) + + extraConfig2SecretVol, ok := findVolume(found.Spec.Template.Spec.Volumes, extraConfig2SecretNameAll) + Expect(ok).To(BeTrue(), "No volume found with name: %s", extraConfig2SecretNameAll) + Expect(extraConfig2SecretVol.VolumeSource.ConfigMap).To(BeNil()) + Expect(extraConfig2SecretVol.VolumeSource.Secret.DefaultMode).To(HaveValue(Equal(int32(420)))) + Expect(extraConfig2SecretVol.VolumeSource.Secret.SecretName).To(Equal(extraConfig2SecretNameAll)) + + extraConfig1SingleCmVol, ok := findVolume(found.Spec.Template.Spec.Volumes, extraConfig1CmNameSingle) + Expect(ok).To(BeTrue(), "No volume found with name: %s", extraConfig1CmNameSingle) + Expect(extraConfig1SingleCmVol.VolumeSource.Secret).To(BeNil()) + Expect(extraConfig1SingleCmVol.VolumeSource.ConfigMap.DefaultMode).To(HaveValue(Equal(int32(420)))) + Expect(extraConfig1SingleCmVol.VolumeSource.ConfigMap.LocalObjectReference.Name).To(Equal(extraConfig1CmNameSingle)) + + extraConfig2SingleSecretVol, ok := findVolume(found.Spec.Template.Spec.Volumes, extraConfig2SecretNameSingle) + Expect(ok).To(BeTrue(), "No volume found with name: %s", extraConfig2SecretNameSingle) + Expect(extraConfig2SingleSecretVol.VolumeSource.ConfigMap).To(BeNil()) + Expect(extraConfig2SingleSecretVol.VolumeSource.Secret.DefaultMode).To(HaveValue(Equal(int32(420)))) + Expect(extraConfig2SingleSecretVol.VolumeSource.Secret.SecretName).To(Equal(extraConfig2SecretNameSingle)) }) - 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()) + // Extra config mounted in the main container + Expect(findVolumeMounts(initCont.VolumeMounts, extraConfig1CmNameAll)).Should(HaveLen(0)) + Expect(findVolumeMounts(initCont.VolumeMounts, extraConfig2SecretNameAll)).Should(HaveLen(0)) }) - 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")) + By("Checking the main container Volume Mounts in the Backstage Deployment", func() { + Expect(mainCont.VolumeMounts).To(HaveLen(7)) + + expectedMountPath := mountPath + if expectedMountPath == "" { + expectedMountPath = "/opt/app-root/src" } - //TODO(rm3l): the order of the rest of the --config args should be the same as the order in - // which the keys are listed in the ConfigMap/Secrets - // But as this is returned as a map, Go does not provide any guarantee on the iteration order. - Expect(mainCont.Args[3]).To(SatisfyAny( - Equal("/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()) + extraConfig1CmMounts := findVolumeMounts(mainCont.VolumeMounts, extraConfig1CmNameAll) + Expect(extraConfig1CmMounts).To(HaveLen(2), "No volume mounts found with name: %s", extraConfig1CmNameAll) + Expect(extraConfig1CmMounts[0].MountPath).ToNot(Equal(extraConfig1CmMounts[1].MountPath)) + for i := 0; i <= 1; i++ { + Expect(extraConfig1CmMounts[i].MountPath).To( + SatisfyAny( + Equal(expectedMountPath+"/my-extra-config-11.yaml"), + Equal(expectedMountPath+"/my-extra-config-12.yaml"))) + Expect(extraConfig1CmMounts[i].SubPath).To( + SatisfyAny( + Equal("my-extra-config-11.yaml"), + Equal("my-extra-config-12.yaml"))) + } + + extraConfig2SecretMounts := findVolumeMounts(mainCont.VolumeMounts, extraConfig2SecretNameAll) + Expect(extraConfig2SecretMounts).To(HaveLen(2), "No volume mounts found with name: %s", extraConfig2SecretNameAll) + Expect(extraConfig2SecretMounts[0].MountPath).ToNot(Equal(extraConfig2SecretMounts[1].MountPath)) + for i := 0; i <= 1; i++ { + Expect(extraConfig2SecretMounts[i].MountPath).To( + SatisfyAny( + Equal(expectedMountPath+"/my-extra-config-21.yaml"), + Equal(expectedMountPath+"/my-extra-config-22.yaml"))) + Expect(extraConfig2SecretMounts[i].SubPath).To( + SatisfyAny( + Equal("my-extra-config-21.yaml"), + Equal("my-extra-config-22.yaml"))) + } + + extraConfig1CmSingleMounts := findVolumeMounts(mainCont.VolumeMounts, extraConfig1CmNameSingle) + Expect(extraConfig1CmSingleMounts).To(HaveLen(1), "No volume mounts found with name: %s", extraConfig1CmNameSingle) + Expect(extraConfig1CmSingleMounts[0].MountPath).To(Equal(expectedMountPath + "/my-extra-file-12-single.yaml")) + Expect(extraConfig1CmSingleMounts[0].SubPath).To(Equal("my-extra-file-12-single.yaml")) + + extraConfig2SecretSingleMounts := findVolumeMounts(mainCont.VolumeMounts, extraConfig2SecretNameSingle) + Expect(extraConfig2SecretSingleMounts).To(HaveLen(1), "No volume mounts found with name: %s", extraConfig2SecretNameSingle) + Expect(extraConfig2SecretSingleMounts[0].MountPath).To(Equal(expectedMountPath + "/my-extra-file-22-single.yaml")) + Expect(extraConfig2SecretSingleMounts[0].SubPath).To(Equal("my-extra-file-22-single.yaml")) }) By("Checking the latest Status added to the Backstage instance") 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 *bsv1alpha1.Backstage - BeforeEach(func() { - backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - BackendAuthSecretRef: &bsv1alpha1.BackendAuthSecretRef{ - Name: "non-existing-secret", - Key: key, - }, - }) - err := k8sClient.Create(ctx, backstage) - Expect(err).To(Not(HaveOccurred())) + Context("Extra Env Vars", func() { + When("setting environment variables either directly or via references to ConfigMap or Secret", func() { + const ( + envConfig1CmNameAll = "my-env-config-1-cm-all" + envConfig2SecretNameAll = "my-env-config-2-secret-all" + envConfig1CmNameSingle = "my-env-config-1-cm-single" + envConfig2SecretNameSingle = "my-env-config-2-secret-single" + ) + + var backstage *bsv1alpha1.Backstage + + BeforeEach(func() { + envConfig1Cm := buildConfigMap(envConfig1CmNameAll, map[string]string{ + "MY_ENV_VAR_1_FROM_CM": "value 11", + "MY_ENV_VAR_2_FROM_CM": "value 12", }) + err := k8sClient.Create(ctx, envConfig1Cm) + Expect(err).To(Not(HaveOccurred())) - It("should reconcile", func() { - By("Checking if the custom resource was successfully created") - Eventually(func() error { - found := &bsv1alpha1.Backstage{} - return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) - }, time.Minute, time.Second).Should(Succeed()) + envConfig2Secret := buildSecret(envConfig2SecretNameAll, map[string][]byte{ + "MY_ENV_VAR_1_FROM_SECRET": []byte("value 21"), + "MY_ENV_VAR_2_FROM_SECRET": []byte("value 22"), + }) + err = k8sClient.Create(ctx, envConfig2Secret) + Expect(err).To(Not(HaveOccurred())) - By("Reconciling the custom resource created") - _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, - }) - Expect(err).To(Not(HaveOccurred())) + envConfig1CmSingle := buildConfigMap(envConfig1CmNameSingle, map[string]string{ + "MY_ENV_VAR_1_FROM_CM_SINGLE": "value 11 single", + "MY_ENV_VAR_2_FROM_CM_SINGLE": "value 12 single", + }) + err = k8sClient.Create(ctx, envConfig1CmSingle) + Expect(err).To(Not(HaveOccurred())) - 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()) + envConfig2SecretSingle := buildSecret(envConfig2SecretNameSingle, map[string][]byte{ + "MY_ENV_VAR_1_FROM_SECRET_SINGLE": []byte("value 21 single"), + "MY_ENV_VAR_2_FROM_SECRET_SINGLE": []byte("value 22 single"), + }) + err = k8sClient.Create(ctx, envConfig2SecretSingle) + Expect(err).To(Not(HaveOccurred())) - 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()) + backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ + Application: &bsv1alpha1.Application{ + ExtraEnvs: &bsv1alpha1.ExtraEnvs{ + Envs: []bsv1alpha1.Env{ + {Name: "MY_ENV_VAR_1", Value: "value 10"}, + {Name: "MY_ENV_VAR_2", Value: "value 20"}, + }, + ConfigMaps: []bsv1alpha1.ObjectKeyRef{ + {Name: envConfig1CmNameAll}, + {Name: envConfig1CmNameSingle, Key: "MY_ENV_VAR_2_FROM_CM_SINGLE"}, + }, + Secrets: []bsv1alpha1.ObjectKeyRef{ + {Name: envConfig2SecretNameAll}, + {Name: envConfig2SecretNameSingle, Key: "MY_ENV_VAR_2_FROM_SECRET_SINGLE"}, + }, + }, + }, + }) + err = k8sClient.Create(ctx, backstage) + Expect(err).To(Not(HaveOccurred())) + }) - 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)"}]`)) - }) + It("should reconcile", func() { + By("Checking if the custom resource was successfully created") + Eventually(func() error { + found := &bsv1alpha1.Backstage{} + return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) + }, time.Minute, time.Second).Should(Succeed()) - By("Checking the latest Status added to the Backstage instance") - verifyBackstageInstance(ctx) + 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()) + + mainCont := found.Spec.Template.Spec.Containers[0] + By(fmt.Sprintf("Checking Env in the Backstage Deployment - container: %q", mainCont.Name), func() { + Expect(len(mainCont.Env)).To(BeNumerically(">=", 4), + "Expected at least 4 items in Env for container %q, fot %d", mainCont.Name, len(mainCont.Env)) + + envVar, ok := findEnvVar(mainCont.Env, "MY_ENV_VAR_1") + Expect(ok).To(BeTrue(), "No env var with name MY_ENV_VAR_1 in main container") + Expect(envVar.Value).Should(Equal("value 10")) + + envVar, ok = findEnvVar(mainCont.Env, "MY_ENV_VAR_2") + Expect(ok).To(BeTrue(), "No env var with name MY_ENV_VAR_2 in main container") + Expect(envVar.Value).Should(Equal("value 20")) + + envVar, ok = findEnvVar(mainCont.Env, "MY_ENV_VAR_2_FROM_CM_SINGLE") + Expect(ok).To(BeTrue(), "No env var with name MY_ENV_VAR_2_FROM_CM_SINGLE in main container") + Expect(envVar.Value).Should(BeEmpty()) + Expect(envVar.ValueFrom).ShouldNot(BeNil()) + Expect(envVar.ValueFrom.FieldRef).Should(BeNil()) + Expect(envVar.ValueFrom.ResourceFieldRef).Should(BeNil()) + Expect(envVar.ValueFrom.SecretKeyRef).Should(BeNil()) + Expect(envVar.ValueFrom.ConfigMapKeyRef).ShouldNot(BeNil()) + Expect(envVar.ValueFrom.ConfigMapKeyRef.Key).Should(Equal("MY_ENV_VAR_2_FROM_CM_SINGLE")) + Expect(envVar.ValueFrom.ConfigMapKeyRef.LocalObjectReference.Name).Should(Equal(envConfig1CmNameSingle)) + + envVar, ok = findEnvVar(mainCont.Env, "MY_ENV_VAR_2_FROM_SECRET_SINGLE") + Expect(ok).To(BeTrue(), "No env var with name MY_ENV_VAR_2_FROM_SECRET_SINGLE in main container") + Expect(envVar.Value).Should(BeEmpty()) + Expect(envVar.ValueFrom).ShouldNot(BeNil()) + Expect(envVar.ValueFrom.FieldRef).Should(BeNil()) + Expect(envVar.ValueFrom.ResourceFieldRef).Should(BeNil()) + Expect(envVar.ValueFrom.ConfigMapKeyRef).Should(BeNil()) + Expect(envVar.ValueFrom.SecretKeyRef).ShouldNot(BeNil()) + Expect(envVar.ValueFrom.SecretKeyRef.Key).Should(Equal("MY_ENV_VAR_2_FROM_SECRET_SINGLE")) + Expect(envVar.ValueFrom.SecretKeyRef.LocalObjectReference.Name).Should(Equal(envConfig2SecretNameSingle)) + }) + By(fmt.Sprintf("Checking EnvFrom in the Backstage Deployment - container: %q", mainCont.Name), func() { + Expect(len(mainCont.EnvFrom)).To(BeNumerically(">=", 2), + "Expected at least 2 items in EnvFrom for container %q, fot %d", mainCont.Name, len(mainCont.EnvFrom)) + + envVar, ok := findEnvVarFrom(mainCont.EnvFrom, envConfig1CmNameAll) + Expect(ok).To(BeTrue(), "No ConfigMap-backed envFrom in main container: %s", envConfig1CmNameAll) + Expect(envVar.SecretRef).Should(BeNil()) + Expect(envVar.ConfigMapRef).ShouldNot(BeNil()) + + envVar, ok = findEnvVarFrom(mainCont.EnvFrom, envConfig2SecretNameAll) + Expect(ok).To(BeTrue(), "No Secret-backed envFrom in main container: %s", envConfig2SecretNameAll) + Expect(envVar.ConfigMapRef).Should(BeNil()) + Expect(envVar.SecretRef).ShouldNot(BeNil()) }) + + initCont := found.Spec.Template.Spec.InitContainers[0] + By("not injecting Env set in CR into the Backstage Deployment Init Container", func() { + _, ok := findEnvVar(initCont.Env, "MY_ENV_VAR_1") + Expect(ok).To(BeFalse(), "Env var with name MY_ENV_VAR_1 should not be injected into init container") + _, ok = findEnvVar(initCont.Env, "MY_ENV_VAR_2") + Expect(ok).To(BeFalse(), "Env var with name MY_ENV_VAR_2 should not be injected into init container") + _, ok = findEnvVar(initCont.Env, "MY_ENV_VAR_2_FROM_CM_SINGLE") + Expect(ok).To(BeFalse(), "Env var with name MY_ENV_VAR_2_FROM_CM_SINGLE should not be injected into init container") + _, ok = findEnvVar(initCont.Env, "MY_ENV_VAR_2_FROM_SECRET_SINGLE") + Expect(ok).To(BeFalse(), "Env var with name MY_ENV_VAR_2_FROM_SECRET_SINGLE should not be injected into init container") + }) + By("not injecting EnvFrom set in CR into the Backstage Deployment Init Container", func() { + _, ok := findEnvVarFrom(initCont.EnvFrom, envConfig1CmNameAll) + Expect(ok).To(BeFalse(), "ConfigMap-backed envFrom should not be added to init container: %s", envConfig1CmNameAll) + _, ok = findEnvVarFrom(initCont.EnvFrom, envConfig2SecretNameAll) + Expect(ok).To(BeFalse(), "Secret-backed envFrom should not be added to init container: %s", envConfig2SecretNameAll) + }) + + By("Checking the latest Status added to the Backstage instance") + verifyBackstageInstance(ctx) }) + }) + }) - When("creating CR with an existing backend secret ref and key="+key, func() { - const backendAuthSecretName = "my-backend-auth-secret" - var backstage *bsv1alpha1.Backstage + When("setting image", func() { + var imageName = "quay.io/my-org/my-awesome-image:1.2.3" - BeforeEach(func() { - d := make(map[string][]byte) - if key != "" { - d[key] = []byte("lorem-ipsum-dolor-sit-amet") - } - backendAuthSecret := buildSecret(backendAuthSecretName, d) - err := k8sClient.Create(ctx, backendAuthSecret) - Expect(err).To(Not(HaveOccurred())) - backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ - BackendAuthSecretRef: &bsv1alpha1.BackendAuthSecretRef{ - Name: backendAuthSecretName, - Key: key, - }, - }) - err = k8sClient.Create(ctx, backstage) - Expect(err).To(Not(HaveOccurred())) + var backstage *bsv1alpha1.Backstage + + BeforeEach(func() { + backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ + Application: &bsv1alpha1.Application{ + Image: &imageName, + }, + }) + err := k8sClient.Create(ctx, backstage) + Expect(err).To(Not(HaveOccurred())) + }) + + It("should reconcile", func() { + By("Checking if the custom resource was successfully created") + Eventually(func() error { + found := &bsv1alpha1.Backstage{} + return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Reconciling the custom resource created") + _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Checking that the Deployment was successfully created in the reconciliation") + found := &appsv1.Deployment{} + Eventually(func(g Gomega) { + // TODO to get name from default + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, found) + g.Expect(err).To(Not(HaveOccurred())) + }, time.Minute, time.Second).Should(Succeed()) + + By("Checking that the image was set on all containers in the Pod Spec") + visitContainers(&found.Spec.Template, func(container *corev1.Container) { + By(fmt.Sprintf("Checking Image in the Backstage Deployment - container: %q", container.Name), func() { + Expect(container.Image).Should(Equal(imageName)) }) + }) - It("should reconcile", func() { - By("Checking if the custom resource was successfully created") - Eventually(func() error { - found := &bsv1alpha1.Backstage{} - return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) - }, time.Minute, time.Second).Should(Succeed()) + By("Checking the latest Status added to the Backstage instance") + verifyBackstageInstance(ctx) + }) + }) - By("Reconciling the custom resource created") - _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, - }) - Expect(err).To(Not(HaveOccurred())) + When("setting image pull secrets", func() { + const ( + ips1 = "some-image-pull-secret-1" + ips2 = "some-image-pull-secret-2" + ) - 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()) + var backstage *bsv1alpha1.Backstage - 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()) + BeforeEach(func() { + backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ + Application: &bsv1alpha1.Application{ + ImagePullSecrets: []string{ips1, ips2}, + }, + }) + err := k8sClient.Create(ctx, backstage) + Expect(err).To(Not(HaveOccurred())) + }) - 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)"}]`)) - }) + It("should reconcile", func() { + By("Checking if the custom resource was successfully created") + Eventually(func() error { + found := &bsv1alpha1.Backstage{} + return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) + }, time.Minute, time.Second).Should(Succeed()) - By("Checking the latest Status added to the Backstage instance") - verifyBackstageInstance(ctx) + By("Reconciling the custom resource created") + _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Checking that the Deployment was successfully created in the reconciliation") + found := &appsv1.Deployment{} + Eventually(func(g Gomega) { + // TODO to get name from default + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, found) + g.Expect(err).To(Not(HaveOccurred())) + }, time.Minute, time.Second).Should(Succeed()) + + By("Checking the image pull secrets are included in the pod spec of Backstage", func() { + var list []string + for _, v := range found.Spec.Template.Spec.ImagePullSecrets { + list = append(list, v.Name) + } + Expect(list).Should(HaveExactElements(ips1, ips2)) + }) + + By("Checking the latest Status added to the Backstage instance") + verifyBackstageInstance(ctx) + }) + }) + + When("setting the number of replicas", func() { + var nbReplicas int32 = 5 + + var backstage *bsv1alpha1.Backstage + + BeforeEach(func() { + backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ + Application: &bsv1alpha1.Application{ + Replicas: &nbReplicas, + }, + }) + err := k8sClient.Create(ctx, backstage) + Expect(err).To(Not(HaveOccurred())) + }) + + It("should reconcile", func() { + By("Checking if the custom resource was successfully created") + Eventually(func() error { + found := &bsv1alpha1.Backstage{} + return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Reconciling the custom resource created") + _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Checking that the Deployment was successfully created in the reconciliation") + found := &appsv1.Deployment{} + Eventually(func(g Gomega) { + // TODO to get name from default + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, found) + g.Expect(err).To(Not(HaveOccurred())) + }, time.Minute, time.Second).Should(Succeed()) + + By("Checking the number of replicas of the Backstage Instance") + Expect(found.Spec.Replicas).Should(HaveValue(BeEquivalentTo(nbReplicas))) + + By("Checking the latest Status added to the Backstage instance") + verifyBackstageInstance(ctx) + }) + }) + + Context("PostgreSQL", func() { + // Other cases covered in the tests above + + When("disabling PostgreSQL in the CR", func() { + var backstage *bsv1alpha1.Backstage + BeforeEach(func() { + backstage = buildBackstageCR(bsv1alpha1.BackstageSpec{ + EnableLocalDb: pointer.Bool(false), }) + err := k8sClient.Create(ctx, backstage) + Expect(err).To(Not(HaveOccurred())) }) - } + + It("should successfully reconcile a custom resource for default Backstage", func() { + By("Checking if the custom resource was successfully created") + Eventually(func() error { + found := &bsv1alpha1.Backstage{} + return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Reconciling the custom resource created") + _, err := backstageReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, + }) + Expect(err).To(Not(HaveOccurred())) + + By("not creating a StatefulSet for the Database") + Consistently(func(g Gomega) { + err := k8sClient.Get(ctx, + types.NamespacedName{Namespace: ns, Name: fmt.Sprintf("backstage-psql-%s", backstage.Name)}, + &appsv1.StatefulSet{}) + g.Expect(err).Should(HaveOccurred()) + g.Expect(errors.IsNotFound(err)).Should(BeTrue(), "Expected error to be a not-found one, but got %v", err) + }, 10*time.Second, time.Second).Should(Succeed()) + + By("Checking if Deployment was successfully created in the reconciliation") + Eventually(func() error { + // TODO to get name from default + return k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: "backstage"}, &appsv1.Deployment{}) + }, time.Minute, time.Second).Should(Succeed()) + + By("Checking the latest Status added to the Backstage instance") + verifyBackstageInstance(ctx) + }) + }) }) }) -func findElementByPredicate[T any](l []T, predicate func(t T) bool) (T, bool) { +func findElementsByPredicate[T any](l []T, predicate func(t T) bool) (result []T) { for _, v := range l { if predicate(v) { - return v, true + result = append(result, v) } } - var zero T - return zero, false + return result } diff --git a/controllers/backstage_deployment.go b/controllers/backstage_deployment.go index 20e3ae28..3536b74f 100644 --- a/controllers/backstage_deployment.go +++ b/controllers/backstage_deployment.go @@ -20,6 +20,7 @@ import ( bs "janus-idp.io/backstage-operator/api/v1alpha1" appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -128,6 +129,22 @@ const ( //`, _defaultBackstageInitContainerName, _defaultBackstageMainContainerName, _containersWorkingDir) //) +// ContainerVisitor is called with each container +type ContainerVisitor func(container *v1.Container) + +// visitContainers invokes the visitor function for every container in the given pod template spec +func visitContainers(podTemplateSpec *v1.PodTemplateSpec, visitor ContainerVisitor) { + for i := range podTemplateSpec.Spec.InitContainers { + visitor(&podTemplateSpec.Spec.InitContainers[i]) + } + for i := range podTemplateSpec.Spec.Containers { + visitor(&podTemplateSpec.Spec.Containers[i]) + } + for i := range podTemplateSpec.Spec.EphemeralContainers { + visitor((*v1.Container)(&podTemplateSpec.Spec.EphemeralContainers[i].EphemeralContainerCommon)) + } +} + func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, backstage bs.Backstage, ns string) error { //lg := log.FromContext(ctx) @@ -173,6 +190,22 @@ func (r *BackstageReconciler) applyBackstageDeployment(ctx context.Context, back return fmt.Errorf("failed to add env vars to Backstage deployment, reason: %s", err) } + if backstage.Spec.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 + }) + } + + for _, imagePullSecret := range backstage.Spec.Application.ImagePullSecrets { + deployment.Spec.Template.Spec.ImagePullSecrets = append(deployment.Spec.Template.Spec.ImagePullSecrets, v1.LocalObjectReference{ + Name: imagePullSecret, + }) + } + } + err = r.Create(ctx, deployment) if err != nil { return fmt.Errorf("failed to create backstage deployment, reason: %s", err) @@ -200,7 +233,13 @@ func (r *BackstageReconciler) addVolumes(ctx context.Context, backstage bs.Backs deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, *dpConfVol) } - deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, r.appConfigsToVolumes(backstage)...) + backendAuthAppConfig, err := r.getBackendAuthAppConfig(ctx, backstage, ns) + if err != nil { + return err + } + + deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, r.appConfigsToVolumes(backstage, backendAuthAppConfig)...) + deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, r.extraFilesToVolumes(backstage)...) return nil } @@ -209,13 +248,26 @@ func (r *BackstageReconciler) addVolumeMounts(ctx context.Context, backstage bs. if err != nil { return err } - return r.addAppConfigsVolumeMounts(ctx, backstage, ns, deployment) + backendAuthAppConfig, err := r.getBackendAuthAppConfig(ctx, backstage, ns) + if err != nil { + return err + } + err = r.addAppConfigsVolumeMounts(ctx, backstage, ns, deployment, backendAuthAppConfig) + if err != nil { + return err + } + return r.addExtraFilesVolumeMounts(ctx, backstage, ns, deployment) } func (r *BackstageReconciler) addContainerArgs(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { - return r.addAppConfigsContainerArgs(ctx, backstage, ns, deployment) + backendAuthAppConfig, err := r.getBackendAuthAppConfig(ctx, backstage, ns) + if err != nil { + return err + } + return r.addAppConfigsContainerArgs(ctx, backstage, ns, deployment, backendAuthAppConfig) } func (r *BackstageReconciler) addEnvVars(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { - return r.addBackendAuthEnvVar(ctx, backstage, ns, deployment) + r.addExtraEnvs(backstage, deployment) + return nil } diff --git a/controllers/backstage_dynamic_plugins.go b/controllers/backstage_dynamic_plugins.go index af28fc34..293e14ae 100644 --- a/controllers/backstage_dynamic_plugins.go +++ b/controllers/backstage_dynamic_plugins.go @@ -24,6 +24,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) //var ( @@ -40,16 +41,16 @@ import ( //` //) -func (r *BackstageReconciler) getOrGenerateDynamicPluginsConf(ctx context.Context, backstage bs.Backstage, ns string) (config bs.DynamicPluginsConfigRef, err error) { - if backstage.Spec.DynamicPluginsConfig != nil { - return *backstage.Spec.DynamicPluginsConfig, nil +func (r *BackstageReconciler) getOrGenerateDynamicPluginsConf(ctx context.Context, backstage bs.Backstage, ns string) (configMap string, err error) { + if backstage.Spec.Application != nil && backstage.Spec.Application.DynamicPluginsConfigMapName != "" { + return backstage.Spec.Application.DynamicPluginsConfigMapName, nil } //Create default ConfigMap for dynamic plugins var cm v1.ConfigMap err = r.readConfigMapOrDefault(ctx, backstage.Spec.RawRuntimeConfig.BackstageConfigName, "dynamic-plugins-configmap.yaml", ns, &cm) if err != nil { - return bs.DynamicPluginsConfigRef{}, fmt.Errorf("failed to read config: %s", err) + return "", fmt.Errorf("failed to read config: %s", err) } dpConfigName := fmt.Sprintf("%s-dynamic-plugins", backstage.Name) @@ -57,18 +58,23 @@ func (r *BackstageReconciler) getOrGenerateDynamicPluginsConf(ctx context.Contex err = r.Get(ctx, types.NamespacedName{Name: dpConfigName, Namespace: ns}, &cm) if err != nil { if !errors.IsNotFound(err) { - return bs.DynamicPluginsConfigRef{}, fmt.Errorf("failed to get config map for dynamic plugins (%q), reason: %s", dpConfigName, err) + return "", fmt.Errorf("failed to get config map for dynamic plugins (%q), reason: %s", dpConfigName, err) + } + setBackstageAppLabel(&cm.ObjectMeta.Labels, backstage) + r.labels(&cm.ObjectMeta, backstage) + + if r.OwnsRuntime { + if err = controllerutil.SetControllerReference(&backstage, &cm, r.Scheme); err != nil { + return "", fmt.Errorf("failed to set owner reference: %s", err) + } } err = r.Create(ctx, &cm) if err != nil { - return bs.DynamicPluginsConfigRef{}, fmt.Errorf("failed to create config map for dynamic plugins, reason: %s", err) + return "", fmt.Errorf("failed to create config map for dynamic plugins, reason: %s", err) } } - return bs.DynamicPluginsConfigRef{ - Name: dpConfigName, - Kind: "ConfigMap", - }, nil + return dpConfigName, nil } func (r *BackstageReconciler) getDynamicPluginsConfVolume(ctx context.Context, backstage bs.Backstage, ns string) (*v1.Volume, error) { @@ -77,27 +83,18 @@ func (r *BackstageReconciler) getDynamicPluginsConfVolume(ctx context.Context, b return nil, err } - if dpConf.Name == "" { + if dpConf == "" { 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, + Name: dpConf, + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + DefaultMode: pointer.Int32(420), + LocalObjectReference: v1.LocalObjectReference{Name: dpConf}, + }, + }, }, nil } @@ -107,7 +104,7 @@ func (r *BackstageReconciler) addDynamicPluginsConfVolumeMount(ctx context.Conte return err } - if dpConf.Name == "" { + if dpConf == "" { return nil } @@ -115,7 +112,7 @@ func (r *BackstageReconciler) addDynamicPluginsConfVolumeMount(ctx context.Conte if c.Name == _defaultBackstageInitContainerName { deployment.Spec.Template.Spec.InitContainers[i].VolumeMounts = append(deployment.Spec.Template.Spec.InitContainers[i].VolumeMounts, v1.VolumeMount{ - Name: dpConf.Name, + Name: dpConf, MountPath: fmt.Sprintf("%s/dynamic-plugins.yaml", _containersWorkingDir), ReadOnly: true, SubPath: "dynamic-plugins.yaml", diff --git a/controllers/backstage_extra_envs.go b/controllers/backstage_extra_envs.go new file mode 100644 index 00000000..2912e754 --- /dev/null +++ b/controllers/backstage_extra_envs.go @@ -0,0 +1,66 @@ +package controller + +import ( + bs "janus-idp.io/backstage-operator/api/v1alpha1" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" +) + +func (r *BackstageReconciler) addExtraEnvs(backstage bs.Backstage, deployment *appsv1.Deployment) { + if backstage.Spec.Application == nil || backstage.Spec.Application.ExtraEnvs == nil { + return + } + + for _, env := range backstage.Spec.Application.ExtraEnvs.Envs { + for i := range deployment.Spec.Template.Spec.Containers { + deployment.Spec.Template.Spec.Containers[i].Env = append(deployment.Spec.Template.Spec.Containers[i].Env, v1.EnvVar{ + Name: env.Name, + Value: env.Value, + }) + } + } + + for _, cmRef := range backstage.Spec.Application.ExtraEnvs.ConfigMaps { + for i := range deployment.Spec.Template.Spec.Containers { + if cmRef.Key != "" { + deployment.Spec.Template.Spec.Containers[i].Env = append(deployment.Spec.Template.Spec.Containers[i].Env, v1.EnvVar{ + Name: cmRef.Key, + ValueFrom: &v1.EnvVarSource{ + ConfigMapKeyRef: &v1.ConfigMapKeySelector{ + LocalObjectReference: v1.LocalObjectReference{Name: cmRef.Name}, + Key: cmRef.Key, + }, + }, + }) + } else { + deployment.Spec.Template.Spec.Containers[i].EnvFrom = append(deployment.Spec.Template.Spec.Containers[i].EnvFrom, v1.EnvFromSource{ + ConfigMapRef: &v1.ConfigMapEnvSource{ + LocalObjectReference: v1.LocalObjectReference{Name: cmRef.Name}, + }, + }) + } + } + } + + for _, secRef := range backstage.Spec.Application.ExtraEnvs.Secrets { + for i := range deployment.Spec.Template.Spec.Containers { + if secRef.Key != "" { + deployment.Spec.Template.Spec.Containers[i].Env = append(deployment.Spec.Template.Spec.Containers[i].Env, v1.EnvVar{ + Name: secRef.Key, + ValueFrom: &v1.EnvVarSource{ + SecretKeyRef: &v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{Name: secRef.Name}, + Key: secRef.Key, + }, + }, + }) + } else { + deployment.Spec.Template.Spec.Containers[i].EnvFrom = append(deployment.Spec.Template.Spec.Containers[i].EnvFrom, v1.EnvFromSource{ + SecretRef: &v1.SecretEnvSource{ + LocalObjectReference: v1.LocalObjectReference{Name: secRef.Name}, + }, + }) + } + } + } +} diff --git a/controllers/backstage_extra_files.go b/controllers/backstage_extra_files.go new file mode 100644 index 00000000..8e834b3d --- /dev/null +++ b/controllers/backstage_extra_files.go @@ -0,0 +1,144 @@ +// +// Copyright (c) 2023 Red Hat, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controller + +import ( + "context" + "fmt" + + bs "janus-idp.io/backstage-operator/api/v1alpha1" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/pointer" +) + +func (r *BackstageReconciler) extraFilesToVolumes(backstage bs.Backstage) (result []v1.Volume) { + if backstage.Spec.Application == nil || backstage.Spec.Application.ExtraFiles == nil { + return nil + } + for _, cmExtraFile := range backstage.Spec.Application.ExtraFiles.ConfigMaps { + result = append(result, + v1.Volume{ + Name: cmExtraFile.Name, + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + DefaultMode: pointer.Int32(420), + LocalObjectReference: v1.LocalObjectReference{Name: cmExtraFile.Name}, + }, + }, + }, + ) + } + for _, secExtraFile := range backstage.Spec.Application.ExtraFiles.Secrets { + result = append(result, + v1.Volume{ + Name: secExtraFile.Name, + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + DefaultMode: pointer.Int32(420), + SecretName: secExtraFile.Name, + }, + }, + }, + ) + } + + return result +} + +func (r *BackstageReconciler) addExtraFilesVolumeMounts(ctx context.Context, backstage bs.Backstage, ns string, deployment *appsv1.Deployment) error { + if backstage.Spec.Application == nil || backstage.Spec.Application.ExtraFiles == nil { + return nil + } + + appConfigFilenamesList, err := r.extractExtraFileNames(ctx, backstage, ns) + if err != nil { + return err + } + + for i, c := range deployment.Spec.Template.Spec.Containers { + if c.Name == _defaultBackstageMainContainerName { + for _, appConfigFilenames := range appConfigFilenamesList { + for _, f := range appConfigFilenames.files { + deployment.Spec.Template.Spec.Containers[i].VolumeMounts = append(deployment.Spec.Template.Spec.Containers[i].VolumeMounts, + v1.VolumeMount{ + Name: appConfigFilenames.ref, + MountPath: fmt.Sprintf("%s/%s", backstage.Spec.Application.ExtraFiles.MountPath, f), + SubPath: f, + }) + } + } + break + } + } + return nil +} + +// extractExtraFileNames returns a mapping of extra-config object name and the list of files in it. +// We intentionally do not return a Map, to preserve the iteration order of the ExtraConfigs in the Custom Resource, +// even though we can't guarantee the iteration order of the files listed inside each ConfigMap or Secret. +func (r *BackstageReconciler) extractExtraFileNames(ctx context.Context, backstage bs.Backstage, ns string) (result []appConfigData, err error) { + if backstage.Spec.Application == nil || backstage.Spec.Application.ExtraFiles == nil { + return nil, nil + } + + for _, cmExtraFile := range backstage.Spec.Application.ExtraFiles.ConfigMaps { + var files []string + if cmExtraFile.Key != "" { + // Limit to that file only + files = append(files, cmExtraFile.Key) + } else { + cm := v1.ConfigMap{} + if err = r.Get(ctx, types.NamespacedName{Name: cmExtraFile.Name, Namespace: ns}, &cm); err != nil { + return nil, err + } + for filename := range cm.Data { + // Bear in mind that iteration order over this map is not guaranteed by Go + files = append(files, filename) + } + for filename := range cm.BinaryData { + // Bear in mind that iteration order over this map is not guaranteed by Go + files = append(files, filename) + } + } + result = append(result, appConfigData{ + ref: cmExtraFile.Name, + files: files, + }) + } + + for _, secExtraFile := range backstage.Spec.Application.ExtraFiles.Secrets { + var files []string + if secExtraFile.Key != "" { + // Limit to that file only + files = append(files, secExtraFile.Key) + } else { + sec := v1.Secret{} + if err = r.Get(ctx, types.NamespacedName{Name: secExtraFile.Name, Namespace: ns}, &sec); err != nil { + return nil, err + } + for filename := range sec.Data { + // Bear in mind that iteration order over this map is not guaranteed by Go + files = append(files, filename) + } + } + result = append(result, appConfigData{ + ref: secExtraFile.Name, + files: files, + }) + } + return result, nil +} diff --git a/examples/janus-cr-with-app-configs.yaml b/examples/janus-cr-with-app-configs.yaml index 9fba835b..0cac8f91 100644 --- a/examples/janus-cr-with-app-configs.yaml +++ b/examples/janus-cr-with-app-configs.yaml @@ -3,28 +3,59 @@ kind: Backstage metadata: name: my-backstage-app-with-app-config spec: - appConfigs: - - name: "my-backstage-config-cm1" - kind: ConfigMap - - name: "my-backstage-config-secret1" - kind: Secret - dynamicPluginsConfig: - name: my-dynamic-plugins-config-cm - kind: ConfigMap - backendAuthSecretRef: - name: "my-backstage-backend-auth-secret" - key: "my-auth-key" + enableLocalDb: true + application: + replicas: 2 + appConfig: + #mountPath: /opt/app-root/src + configMaps: + - name: "my-backstage-config-backend-auth" + - name: "my-backstage-config-cm1" + - name: "my-backstage-config-cm2" + key: "app-config1-cm2.gh.yaml" + dynamicPluginsConfigMapName: "my-dynamic-plugins-config-cm" + extraFiles: + mountPath: /tmp/my-extra-files + configMaps: + - name: "my-backstage-extra-files-cm1" + secrets: + - name: "my-backstage-extra-files-secret1" + extraEnvs: + envs: + - name: GITHUB_ORG + value: 'my-gh-org' + - name: MY_ENV_VAR_2 + value: my-value-2 + configMaps: + - name: my-env-cm-1 + - name: my-env-cm-11 + key: CM_ENV11 + secrets: + - name: "my-backstage-backend-auth-secret" + key: BACKEND_SECRET + - name: my-gh-auth-secret + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-backstage-config-backend-auth +data: + "app-config.backend-auth.yaml": | + backend: + auth: + keys: + - secret: "${BACKEND_SECRET}" --- apiVersion: v1 kind: Secret metadata: name: my-backstage-backend-auth-secret -data: +stringData: # generated with the command below (from https://janus-idp.io/docs/auth/service-to-service-auth/#setup): # node -p 'require("crypto").randomBytes(24).toString("base64")' - backend-secret: TDRORDFRa2JxaFJhNTBzOGFDc1FWUEJ4ekFtRUw4UEU= - my-auth-key: TDRORDFRa2JxaFJhNTBzOGFDc1FWUEJ4ekFtRUw4UEU= + BACKEND_SECRET: "R2FxRVNrcmwzYzhhN3l0V1VRcnQ3L1pLT09WaVhDNUEK" # notsecret --- apiVersion: v1 @@ -32,40 +63,39 @@ kind: ConfigMap metadata: name: my-backstage-config-cm1 data: - my-app-config.prod.yaml: | + app-config1-cm1.db.yaml: | backend: database: connection: password: ${POSTGRESQL_PASSWORD} user: ${POSTGRESQL_USER} - my-app-config-2.yaml: | + app-config2-cm1.yaml: | # Some comment in this file - my-app-config.odo.yaml: | + app-config3-cm1.odo.yaml: | catalog: locations: - # [...] - type: url - target: https://github.com/rm3l/odo-backstage-golden-path-template/blob/main/template.yaml + target: https://github.com/ododev/odo-backstage-software-template/blob/main/template.yaml rules: - allow: [Template] --- apiVersion: v1 -kind: Secret +kind: ConfigMap metadata: - name: my-backstage-config-secret1 + name: my-backstage-config-cm2 data: - # auth: - # # see https://janus-idp.io/docs/auth/ to learn about auth providers - # environment: development - # providers: - # github: - # development: - # clientId: 'xxx' - # clientSecret: 'yyy' - my-app-config-1.secret.yaml: YXV0aDoKICAjIHNlZSBodHRwczovL2JhY2tzdGFnZS5pby9kb2NzL2F1dGgvIHRvIGxlYXJuIGFib3V0IGF1dGggcHJvdmlkZXJzCiAgZW52aXJvbm1lbnQ6IGRldmVsb3BtZW50CiAgcHJvdmlkZXJzOgogICAgZ2l0aHViOgogICAgICBkZXZlbG9wbWVudDoKICAgICAgICBjbGllbnRJZDogJ3h4eCcKICAgICAgICBjbGllbnRTZWNyZXQ6ICd5eXknCg== - # # a comment - my-app-config-2.secret.yaml: IyBhIGNvbW1lbnQK + app-config1-cm2.gh.yaml: | + auth: + # see https://backstage.io/docs/auth/ to learn about auth providers + environment: development + providers: + github: + development: + clientId: '${GH_CLIENT_ID}' + clientSecret: '${GH_CLIENT_SECRET}' + app-config2-cm2.yaml: | + # a comment --- apiVersion: v1 @@ -77,6 +107,20 @@ data: includes: - dynamic-plugins.default.yaml plugins: + - package: './dynamic-plugins/dist/backstage-plugin-catalog-backend-module-github-dynamic' + disabled: false + pluginConfig: + catalog: + providers: + github: + myorg: + organization: '${GITHUB_ORG}' + schedule: + # supports cron, ISO duration, "human duration" (used below) + frequency: { minutes: 30} + # supports ISO duration, "human duration (used below) + timeout: { minutes: 3} + initialDelay: { seconds: 15} - package: '@dfatwork-pkgs/scaffolder-backend-module-http-request-wrapped-dynamic@4.0.9-0' integrity: 'sha512-+YYESzHdg1hsk2XN+zrtXPnsQnfbzmWIvcOM0oQLS4hf8F4iGTtOXKjWnZsR/14/khGsPrzy0oq1ytJ1/4ORkQ==' - package: '@dfatwork-pkgs/explore-backend-wrapped-dynamic@0.0.9-next.11' @@ -85,4 +129,67 @@ data: proxy: endpoints: /explore-backend-completed: - target: 'http://localhost:7017' \ No newline at end of file + target: 'http://localhost:7017' + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-env-cm-1 +data: + CM_ENV1: "cm env 1" + CM_ENV2: "cm env 2" + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-env-cm-11 +data: + CM_ENV11: "cm env 11" + CM_ENV12: "cm env 12" + +--- +apiVersion: v1 +kind: Secret +metadata: + name: my-gh-auth-secret +stringData: + GH_CLIENT_ID: "my GH client ID" + GH_CLIENT_SECRET: "my GH client secret" + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-backstage-extra-files-cm1 +data: + cm_file1.txt: | + # From ConfigMap + Lorem Ipsum + Dolor Sit Amet + cm_file2.properties: | + conf.x=y + conf.y=z + +--- +apiVersion: v1 +kind: Secret +metadata: + name: my-backstage-extra-files-secret1 +stringData: + secret_file1.txt: | + # From Secret + Lorem Ipsum + Dolor Sit Amet + secret_file2.properties: | + sec.a=b + sec.b=c + secrets.prod.yaml: | + appId: 1 + webhookUrl: https://smee.io/foo + clientId: someGithubAppClientId + clientSecret: someGithubAppClientSecret + webhookSecret: someWebhookSecret + privateKey: | + SomeRsaPrivateKey diff --git a/examples/janus-cr.yaml b/examples/janus-cr.yaml index b5d2a771..8d113d1c 100644 --- a/examples/janus-cr.yaml +++ b/examples/janus-cr.yaml @@ -4,6 +4,6 @@ metadata: name: bs-janus namespace: backstage spec: - runtimeConfig: + rawRuntimeConfig: backstageConfig: janus-config # dryRun: true diff --git a/examples/rhdh-cr.yaml b/examples/rhdh-cr.yaml new file mode 100644 index 00000000..7bffbe4c --- /dev/null +++ b/examples/rhdh-cr.yaml @@ -0,0 +1,74 @@ +apiVersion: janus-idp.io/v1alpha1 +kind: Backstage +metadata: + name: my-rhdh +spec: + application: + image: quay.io/rhdh/rhdh-hub-rhel9:1.0-200 + imagePullSecrets: + - rhdh-pull-secret + appConfig: + configMaps: + - name: app-config-rhdh + dynamicPluginsConfigMapName: dynamic-plugins-rhdh + extraEnvs: + secrets: + - name: secrets-rhdh + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config-rhdh +data: + "app-config-rhdh.yaml": | + backend: + auth: + keys: + - secret: "${BACKEND_SECRET}" + auth: + # see https://backstage.io/docs/auth/ to learn about auth providers + environment: development + providers: + github: + development: + clientId: '${GH_CLIENT_ID}' + clientSecret: '${GH_CLIENT_SECRET}' + +--- +apiVersion: v1 +kind: Secret +metadata: + name: secrets-rhdh +stringData: + # generated with the command below (from https://janus-idp.io/docs/auth/service-to-service-auth/#setup): + # node -p 'require("crypto").randomBytes(24).toString("base64")' + BACKEND_SECRET: "R2FxRVNrcmwzYzhhN3l0V1VRcnQ3L1pLT09WaVhDNUEK" # notsecret + GH_ORG: "my-gh-org" + GH_CLIENT_ID: "my GH client ID" + GH_CLIENT_SECRET: "my GH client secret" + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: dynamic-plugins-rhdh +data: + dynamic-plugins.yaml: | + includes: + - dynamic-plugins.default.yaml + plugins: + - package: './dynamic-plugins/dist/backstage-plugin-catalog-backend-module-github-dynamic' + disabled: false + pluginConfig: + catalog: + providers: + github: + myorg: + organization: '${GH_ORG}' + schedule: + # supports cron, ISO duration, "human duration" (used below) + frequency: { minutes: 30} + # supports ISO duration, "human duration (used below) + timeout: { minutes: 3} + initialDelay: { seconds: 15}