From 806c5afbfb0bc3858c673c71ea80e8dda7d0bb6f Mon Sep 17 00:00:00 2001 From: Gennady Azarenkov Date: Tue, 21 May 2024 14:42:07 +0300 Subject: [PATCH] Recreate Backstage Pod on changes of ConfigMaps/Secrets configured in CR (#351) * watch and reconcile config Signed-off-by: gazarenkov * example Signed-off-by: gazarenkov * recreate by secret Signed-off-by: gazarenkov * add auto-recreate labels/annotations by operator by default and configure Signed-off-by: gazarenkov * Regenerate bundle manifests Co-authored-by: gazarenkov * fic lint Signed-off-by: gazarenkov * small fixes Signed-off-by: gazarenkov * add comment Signed-off-by: gazarenkov * fix manifest Signed-off-by: gazarenkov * test preprocessor Signed-off-by: gazarenkov * remove db config from default app-config Signed-off-by: gazarenkov * Regenerate bundle manifests Co-authored-by: gazarenkov * Sync cluster permissions in RHDH CSV with upstream CSV --------- Signed-off-by: gazarenkov Co-authored-by: github-actions[bot] Co-authored-by: gazarenkov Co-authored-by: Armel Soro --- .rhdh/bundle/manifests/rhdh-operator.csv.yaml | 21 +-- Makefile | 2 +- ...backstage-default-config_v1_configmap.yaml | 4 - ...kstage-operator.clusterserviceversion.yaml | 23 +-- config/manager/default-config/app-config.yaml | 4 - config/rbac/role.yaml | 21 +-- controllers/backstage_controller.go | 131 ++++++++++---- controllers/mock_client.go | 143 +++++++++++++++ controllers/preprocessor_test.go | 100 +++++++++++ controllers/spec_preprocessor.go | 127 ++++++++++---- go.mod | 3 + go.sum | 38 +--- integration_tests/README.md | 26 ++- integration_tests/config-refresh_test.go | 164 ++++++++++++++++++ integration_tests/cr-config_test.go | 157 +++++------------ integration_tests/db_test.go | 146 ++++++++++++++++ integration_tests/default-config_test.go | 42 +++-- integration_tests/rhdh-config_test.go | 2 +- integration_tests/route_test.go | 2 +- integration_tests/suite_test.go | 28 ++- integration_tests/utils.go | 74 +++++++- pkg/model/appconfig_test.go | 8 - pkg/model/db-service.go | 4 +- pkg/model/db-statefulset.go | 4 +- pkg/model/deployment.go | 11 +- pkg/model/externalconfig.go | 75 ++++++++ pkg/model/runtime.go | 16 +- pkg/model/runtime_test.go | 10 +- pkg/model/service.go | 2 +- pkg/utils/pod-mutator.go | 6 +- pkg/utils/utils.go | 9 +- 31 files changed, 1065 insertions(+), 338 deletions(-) create mode 100644 controllers/mock_client.go create mode 100644 controllers/preprocessor_test.go create mode 100644 integration_tests/config-refresh_test.go create mode 100644 integration_tests/db_test.go create mode 100644 pkg/model/externalconfig.go diff --git a/.rhdh/bundle/manifests/rhdh-operator.csv.yaml b/.rhdh/bundle/manifests/rhdh-operator.csv.yaml index 5e44b456..3ce38dc1 100644 --- a/.rhdh/bundle/manifests/rhdh-operator.csv.yaml +++ b/.rhdh/bundle/manifests/rhdh-operator.csv.yaml @@ -81,6 +81,7 @@ spec: - "" resources: - configmaps + - secrets - services verbs: - create @@ -99,30 +100,10 @@ spec: - get - list - watch - - apiGroups: - - "" - resources: - - secrets - verbs: - - create - - delete - - patch - - update - apiGroups: - apps resources: - deployments - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - apps - resources: - statefulsets verbs: - create diff --git a/Makefile b/Makefile index 2ee5b04a..c8bfbebe 100644 --- a/Makefile +++ b/Makefile @@ -134,7 +134,7 @@ test: manifests generate fmt vet envtest ## Run tests. We need LOCALBIN=$(LOCALB .PHONY: integration-test integration-test: ginkgo manifests generate fmt vet envtest ## Run integration_tests. We need LOCALBIN=$(LOCALBIN) to get correct default-config path mkdir -p $(LOCALBIN)/default-config && cp config/manager/$(CONF_DIR)/* $(LOCALBIN)/default-config - LOCALBIN=$(LOCALBIN) KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" $(GINKGO) -v -r integration_tests + LOCALBIN=$(LOCALBIN) KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" $(GINKGO) -v -r $(ARGS) integration_tests ##@ Build diff --git a/bundle/manifests/backstage-default-config_v1_configmap.yaml b/bundle/manifests/backstage-default-config_v1_configmap.yaml index 3f3e7b5e..979b0682 100644 --- a/bundle/manifests/backstage-default-config_v1_configmap.yaml +++ b/bundle/manifests/backstage-default-config_v1_configmap.yaml @@ -8,10 +8,6 @@ data: data: default.app-config.yaml: | backend: - database: - connection: - password: ${POSTGRES_PASSWORD} - user: ${POSTGRES_USER} auth: keys: # This is a default value, which you should change by providing your own app-config diff --git a/bundle/manifests/backstage-operator.clusterserviceversion.yaml b/bundle/manifests/backstage-operator.clusterserviceversion.yaml index a7d72242..d0ab1148 100644 --- a/bundle/manifests/backstage-operator.clusterserviceversion.yaml +++ b/bundle/manifests/backstage-operator.clusterserviceversion.yaml @@ -21,7 +21,7 @@ metadata: } ] capabilities: Seamless Upgrades - createdAt: "2024-04-01T18:15:06Z" + createdAt: "2024-05-21T10:47:18Z" operatorframework.io/suggested-namespace: backstage-system operators.operatorframework.io/builder: operator-sdk-v1.33.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 @@ -50,6 +50,7 @@ spec: - "" resources: - configmaps + - secrets - services verbs: - create @@ -68,30 +69,10 @@ spec: - get - list - watch - - apiGroups: - - "" - resources: - - secrets - verbs: - - create - - delete - - patch - - update - apiGroups: - apps resources: - deployments - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - apps - resources: - statefulsets verbs: - create diff --git a/config/manager/default-config/app-config.yaml b/config/manager/default-config/app-config.yaml index ccfe93e8..3a2a88f6 100644 --- a/config/manager/default-config/app-config.yaml +++ b/config/manager/default-config/app-config.yaml @@ -5,10 +5,6 @@ metadata: data: default.app-config.yaml: | backend: - database: - connection: - password: ${POSTGRES_PASSWORD} - user: ${POSTGRES_USER} auth: keys: # This is a default value, which you should change by providing your own app-config diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 78c50678..48610f5b 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -9,6 +9,7 @@ rules: - "" resources: - configmaps + - secrets - services verbs: - create @@ -27,30 +28,10 @@ rules: - get - list - watch -- apiGroups: - - "" - resources: - - secrets - verbs: - - create - - delete - - patch - - update - apiGroups: - apps resources: - deployments - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - apps - resources: - statefulsets verbs: - create diff --git a/controllers/backstage_controller.go b/controllers/backstage_controller.go index 5352f6ed..52ad5617 100644 --- a/controllers/backstage_controller.go +++ b/controllers/backstage_controller.go @@ -19,6 +19,12 @@ import ( "fmt" "reflect" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" @@ -44,7 +50,15 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" ) -var recNumber = 0 +var watchedConfigSelector = metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: model.ExtConfigSyncLabel, + Values: []string{"true"}, + Operator: metav1.LabelSelectorOpIn, + }, + }, +} // BackstageReconciler reconciles a Backstage object type BackstageReconciler struct { @@ -53,23 +67,16 @@ type BackstageReconciler struct { // If true, Backstage Controller always sync the state of runtime objects created // otherwise, runtime objects can be re-configured independently OwnsRuntime bool - - // Namespace allows to restrict the reconciliation to this particular namespace, - // and ignore requests from other namespaces. - // This is mostly useful for our tests, to overcome a limitation of EnvTest about namespace deletion. - Namespace string - + // indicates if current cluster is Openshift IsOpenShift bool } //+kubebuilder:rbac:groups=rhdh.redhat.com,resources=backstages,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=rhdh.redhat.com,resources=backstages/status,verbs=get;update;patch //+kubebuilder:rbac:groups=rhdh.redhat.com,resources=backstages/finalizers,verbs=update -//+kubebuilder:rbac:groups="",resources=configmaps;services,verbs=get;watch;create;update;list;delete;patch +//+kubebuilder:rbac:groups="",resources=configmaps;secrets;services,verbs=get;watch;create;update;list;delete;patch //+kubebuilder:rbac:groups="",resources=persistentvolumes;persistentvolumeclaims,verbs=get;list;watch -//+kubebuilder:rbac:groups="",resources=secrets,verbs=create;delete;patch;update -//+kubebuilder:rbac:groups="apps",resources=deployments,verbs=get;watch;create;update;list;delete;patch -//+kubebuilder:rbac:groups="apps",resources=statefulsets,verbs=get;watch;create;update;list;delete;patch +//+kubebuilder:rbac:groups="apps",resources=deployments;statefulsets,verbs=get;watch;create;update;list;delete;patch //+kubebuilder:rbac:groups="route.openshift.io",resources=routes;routes/custom-host,verbs=get;watch;create;update;list;delete;patch // Reconcile is part of the main kubernetes reconciliation loop which aims to @@ -79,16 +86,6 @@ type BackstageReconciler struct { func (r *BackstageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { lg := log.FromContext(ctx) - recNumber = recNumber + 1 - lg.V(1).Info(fmt.Sprintf("starting reconciliation (namespace: %q), number %d", req.NamespacedName, recNumber)) - - // Ignore requests for other namespaces, if specified. - // This is mostly useful for our tests, to overcome a limitation of EnvTest about namespace deletion. - // More details on https://book.kubebuilder.io/reference/envtest.html#namespace-usage-limitation - if r.Namespace != "" && req.Namespace != r.Namespace { - return ctrl.Result{}, nil - } - backstage := bs.Backstage{} if err := r.Get(ctx, req.NamespacedName, &backstage); err != nil { if errors.IsNotFound(err) { @@ -282,18 +279,90 @@ func setStatusCondition(backstage *bs.Backstage, condType bs.BackstageConditionT }) } +// requestByLabel returns a request with current Namespace and Backstage Object name taken from label +// or empty request object if label not found +func (r *BackstageReconciler) requestByLabel(ctx context.Context, object client.Object) []reconcile.Request { + + lg := log.FromContext(ctx) + + backstageName := object.GetAnnotations()[model.BackstageNameAnnotation] + if backstageName == "" { + lg.V(1).Info(fmt.Sprintf("warning: %s annotation is not defined for %s, Backstage instances will not be reconciled in this loop", model.BackstageNameAnnotation, object.GetName())) + return []reconcile.Request{} + } + + nn := types.NamespacedName{ + Namespace: object.GetNamespace(), + Name: backstageName, + } + + backstage := bs.Backstage{} + if err := r.Get(ctx, nn, &backstage); err != nil { + if !errors.IsNotFound(err) { + lg.Error(err, "request by label failed, get Backstage ") + } + return []reconcile.Request{} + } + + ec, err := r.preprocessSpec(ctx, backstage) + if err != nil { + lg.Error(err, "request by label failed, preprocess Backstage ") + return []reconcile.Request{} + } + + deploy := &appsv1.Deployment{} + if err := r.Get(ctx, types.NamespacedName{Name: model.DeploymentName(backstage.Name), Namespace: object.GetNamespace()}, deploy); err != nil { + if errors.IsNotFound(err) { + lg.V(1).Info("request by label, deployment not found", "name", model.DeploymentName(backstage.Name)) + } else { + lg.Error(err, "request by label failed, get Deployment ", "error ", err) + } + return []reconcile.Request{} + } + + newHash := ec.GetHash() + oldHash := deploy.Spec.Template.ObjectMeta.GetAnnotations()[model.ExtConfigHashAnnotation] + if newHash == oldHash { + lg.V(1).Info("request by label, hash are equal", "hash", newHash) + return []reconcile.Request{} + } + + lg.V(1).Info("enqueuing reconcile for", object.GetObjectKind().GroupVersionKind().Kind, object.GetName(), "new hash: ", newHash, "old hash: ", oldHash) + return []reconcile.Request{{NamespacedName: types.NamespacedName{Name: backstage.Name, Namespace: object.GetNamespace()}}} + +} + // SetupWithManager sets up the controller with the Manager. func (r *BackstageReconciler) SetupWithManager(mgr ctrl.Manager) error { - builder := ctrl.NewControllerManagedBy(mgr). - For(&bs.Backstage{}) - - // [GA] do not remove it - //if r.OwnsRuntime { - // builder.Owns(&appsv1.Deployment{}). - // Owns(&corev1.Service{}). - // Owns(&appsv1.StatefulSet{}) - //} + pred, err := predicate.LabelSelectorPredicate(watchedConfigSelector) + if err != nil { + return fmt.Errorf("failed to construct the predicate for matching secrets. This should not happen: %w", err) + } - return builder.Complete(r) + b := ctrl.NewControllerManagedBy(mgr). + For(&bs.Backstage{}). + Watches( + &corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request { + return r.requestByLabel(ctx, o) + }), + builder.WithPredicates(pred, predicate.Funcs{ + DeleteFunc: func(e event.DeleteEvent) bool { return true }, + UpdateFunc: func(e event.UpdateEvent) bool { return true }, + //CreateFunc: func(e event.CreateEvent) bool { return true }, + }), + ). + Watches( + &corev1.ConfigMap{}, + handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request { + return r.requestByLabel(ctx, o) + }), + builder.WithPredicates(pred, predicate.Funcs{ + DeleteFunc: func(e event.DeleteEvent) bool { return true }, + UpdateFunc: func(e event.UpdateEvent) bool { return true }, + //CreateFunc: func(e event.CreateEvent) bool { return true }, + })) + + return b.Complete(r) } diff --git a/controllers/mock_client.go b/controllers/mock_client.go new file mode 100644 index 00000000..722e2caf --- /dev/null +++ b/controllers/mock_client.go @@ -0,0 +1,143 @@ +// +// Copyright (c) 2023 Red Hat, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controller + +import ( + "context" + "encoding/json" + "fmt" + "reflect" + "strings" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const implementMe = "implement me if needed" + +// Mock K8s go-client with very basic implementation of (some) methods +// to be able to simply test controller logic +type MockClient struct { + objects map[NameKind][]byte +} + +func NewMockClient() MockClient { + return MockClient{ + objects: map[NameKind][]byte{}, + } +} + +type NameKind struct { + Name string + Kind string +} + +func kind(obj runtime.Object) string { + str := reflect.TypeOf(obj).String() + return str[strings.LastIndex(str, ".")+1:] + //return reflect.TypeOf(obj).String() +} + +func (m MockClient) Get(_ context.Context, key client.ObjectKey, obj client.Object, _ ...client.GetOption) error { + + if key.Name == "" { + return fmt.Errorf("get: name should not be empty") + } + uobj := m.objects[NameKind{Name: key.Name, Kind: kind(obj)}] + if uobj == nil { + return errors.NewNotFound(schema.GroupResource{Group: "", Resource: kind(obj)}, key.Name) + } + err := json.Unmarshal(uobj, obj) + if err != nil { + return err + } + return nil +} + +func (m MockClient) List(_ context.Context, _ client.ObjectList, _ ...client.ListOption) error { + panic(implementMe) +} + +func (m MockClient) Create(_ context.Context, obj client.Object, _ ...client.CreateOption) error { + if obj.GetName() == "" { + return fmt.Errorf("update: object Name should not be empty") + } + uobj := m.objects[NameKind{Name: obj.GetName(), Kind: kind(obj)}] + if uobj != nil { + return errors.NewAlreadyExists(schema.GroupResource{Group: "", Resource: kind(obj)}, obj.GetName()) + } + dat, err := json.Marshal(obj) + if err != nil { + return err + } + m.objects[NameKind{Name: obj.GetName(), Kind: kind(obj)}] = dat + return nil +} + +func (m MockClient) Delete(_ context.Context, _ client.Object, _ ...client.DeleteOption) error { + panic(implementMe) +} + +func (m MockClient) Update(_ context.Context, obj client.Object, _ ...client.UpdateOption) error { + + if obj.GetName() == "" { + return fmt.Errorf("update: object Name should not be empty") + } + uobj := m.objects[NameKind{Name: obj.GetName(), Kind: kind(obj)}] + if uobj == nil { + return errors.NewNotFound(schema.GroupResource{Group: "", Resource: kind(obj)}, obj.GetName()) + } + dat, err := json.Marshal(obj) + if err != nil { + return err + } + m.objects[NameKind{Name: obj.GetName(), Kind: kind(obj)}] = dat + return nil +} + +func (m MockClient) Patch(_ context.Context, _ client.Object, _ client.Patch, _ ...client.PatchOption) error { + panic(implementMe) +} + +func (m MockClient) DeleteAllOf(_ context.Context, _ client.Object, _ ...client.DeleteAllOfOption) error { + panic(implementMe) +} + +func (m MockClient) Status() client.SubResourceWriter { + panic(implementMe) +} + +func (m MockClient) SubResource(_ string) client.SubResourceClient { + panic(implementMe) +} + +func (m MockClient) Scheme() *runtime.Scheme { + panic(implementMe) +} + +func (m MockClient) RESTMapper() meta.RESTMapper { + panic(implementMe) +} + +func (m MockClient) GroupVersionKindFor(_ runtime.Object) (schema.GroupVersionKind, error) { + panic(implementMe) +} + +func (m MockClient) IsObjectNamespaced(_ runtime.Object) (bool, error) { + panic(implementMe) +} diff --git a/controllers/preprocessor_test.go b/controllers/preprocessor_test.go new file mode 100644 index 00000000..51e2db3a --- /dev/null +++ b/controllers/preprocessor_test.go @@ -0,0 +1,100 @@ +// +// 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" + "os" + "redhat-developer/red-hat-developer-hub-operator/api/v1alpha1" + "redhat-developer/red-hat-developer-hub-operator/pkg/model" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +func updateConfigMap(t *testing.T) BackstageReconciler { + ctx := context.TODO() + + bs := v1alpha1.Backstage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bs1", + Namespace: "ns1", + }, + Spec: v1alpha1.BackstageSpec{ + Application: &v1alpha1.Application{ + AppConfig: &v1alpha1.AppConfig{ + ConfigMaps: []v1alpha1.ObjectKeyRef{{Name: "cm1"}}, + }, + }, + }, + } + + cm := corev1.ConfigMap{} + cm.Name = "cm1" + + rc := BackstageReconciler{ + Client: NewMockClient(), + } + + assert.NoError(t, rc.Create(ctx, &cm)) + + // reconcile + extConf, err := rc.preprocessSpec(ctx, bs) + assert.NoError(t, err) + + assert.NotNil(t, extConf.AppConfigs["cm1"].Labels) + assert.Equal(t, 1, len(extConf.AppConfigs["cm1"].Labels)) + oldHash := extConf.GetHash() + + // Update ConfigMap with new data + err = rc.Get(ctx, types.NamespacedName{Namespace: "ns1", Name: "cm1"}, &cm) + assert.NoError(t, err) + cm.Data = map[string]string{"key": "value"} + err = rc.Update(ctx, &cm) + assert.NoError(t, err) + + // reconcile again + extConf, err = rc.preprocessSpec(ctx, bs) + assert.NoError(t, err) + + assert.NotEqual(t, oldHash, extConf.GetHash()) + + return rc +} + +func TestExtConfigChanged(t *testing.T) { + + ctx := context.TODO() + cm := corev1.ConfigMap{} + + rc := updateConfigMap(t) + err := rc.Get(ctx, types.NamespacedName{Namespace: "ns1", Name: "cm1"}, &cm) + assert.NoError(t, err) + // true : Backstage will be reconciled + assert.Equal(t, "true", cm.Labels[model.ExtConfigSyncLabel]) + + err = os.Setenv(AutoSyncEnvVar, "false") + assert.NoError(t, err) + + rc = updateConfigMap(t) + err = rc.Get(ctx, types.NamespacedName{Namespace: "ns1", Name: "cm1"}, &cm) + assert.NoError(t, err) + // false : Backstage will not be reconciled + assert.Equal(t, "false", cm.Labels[model.ExtConfigSyncLabel]) + +} diff --git a/controllers/spec_preprocessor.go b/controllers/spec_preprocessor.go index 37c5fbcc..12b33099 100644 --- a/controllers/spec_preprocessor.go +++ b/controllers/spec_preprocessor.go @@ -17,6 +17,14 @@ package controller import ( "context" "fmt" + "os" + "strconv" + + "k8s.io/apimachinery/pkg/api/errors" + + "sigs.k8s.io/controller-runtime/pkg/log" + + "sigs.k8s.io/controller-runtime/pkg/client" bs "redhat-developer/red-hat-developer-hub-operator/api/v1alpha1" @@ -26,6 +34,8 @@ import ( "k8s.io/apimachinery/pkg/types" ) +const AutoSyncEnvVar = "EXT_CONF_SYNC_backstage" + // Add additional details to the Backstage Spec helping in making Backstage RuntimeObjects Model // Validates Backstage Spec and fails fast if something not correct func (r *BackstageReconciler) preprocessSpec(ctx context.Context, backstage bs.Backstage) (model.ExternalConfig, error) { @@ -34,28 +44,23 @@ func (r *BackstageReconciler) preprocessSpec(ctx context.Context, backstage bs.B bsSpec := backstage.Spec ns := backstage.Namespace - result := model.ExternalConfig{ - RawConfig: map[string]string{}, - AppConfigs: map[string]corev1.ConfigMap{}, - ExtraFileConfigMaps: map[string]corev1.ConfigMap{}, - ExtraEnvConfigMaps: map[string]corev1.ConfigMap{}, - } + result := model.NewExternalConfig() // Process RawConfig if bsSpec.RawRuntimeConfig != nil { if bsSpec.RawRuntimeConfig.BackstageConfigName != "" { - cm := corev1.ConfigMap{} - if err := r.Get(ctx, types.NamespacedName{Name: bsSpec.RawRuntimeConfig.BackstageConfigName, Namespace: ns}, &cm); err != nil { - return result, fmt.Errorf("failed to load rawConfig %s: %w", bsSpec.RawRuntimeConfig.BackstageConfigName, err) + cm := &corev1.ConfigMap{} + if err := r.addExtConfig(&result, ctx, cm, backstage.Name, bsSpec.RawRuntimeConfig.BackstageConfigName, ns); err != nil { + return result, err } for key, value := range cm.Data { result.RawConfig[key] = value } } if bsSpec.RawRuntimeConfig.LocalDbConfigName != "" { - cm := corev1.ConfigMap{} - if err := r.Get(ctx, types.NamespacedName{Name: bsSpec.RawRuntimeConfig.LocalDbConfigName, Namespace: ns}, &cm); err != nil { - return result, fmt.Errorf("failed to load rawConfig %s: %w", bsSpec.RawRuntimeConfig.LocalDbConfigName, err) + cm := &corev1.ConfigMap{} + if err := r.addExtConfig(&result, ctx, cm, backstage.Name, bsSpec.RawRuntimeConfig.LocalDbConfigName, ns); err != nil { + return result, err } for key, value := range cm.Data { result.RawConfig[key] = value @@ -69,47 +74,109 @@ func (r *BackstageReconciler) preprocessSpec(ctx context.Context, backstage bs.B // Process AppConfigs if bsSpec.Application.AppConfig != nil { - //mountPath := bsSpec.Application.AppConfig.MountPath for _, ac := range bsSpec.Application.AppConfig.ConfigMaps { - cm := corev1.ConfigMap{} - if err := r.Get(ctx, types.NamespacedName{Name: ac.Name, Namespace: ns}, &cm); err != nil { - return result, fmt.Errorf("failed to get configMap %s: %w", ac.Name, err) + cm := &corev1.ConfigMap{} + if err := r.addExtConfig(&result, ctx, cm, backstage.Name, ac.Name, ns); err != nil { + return result, err } - result.AppConfigs[cm.Name] = cm + result.AppConfigs[ac.Name] = *cm } } // Process ConfigMapFiles if bsSpec.Application.ExtraFiles != nil && bsSpec.Application.ExtraFiles.ConfigMaps != nil { for _, ef := range bsSpec.Application.ExtraFiles.ConfigMaps { - cm := corev1.ConfigMap{} - if err := r.Get(ctx, types.NamespacedName{Name: ef.Name, Namespace: ns}, &cm); err != nil { - return result, fmt.Errorf("failed to get ConfigMap %s: %w", ef.Name, err) + cm := &corev1.ConfigMap{} + if err := r.addExtConfig(&result, ctx, cm, backstage.Name, ef.Name, ns); err != nil { + return result, err + } + result.ExtraFileConfigMaps[cm.Name] = *cm + } + } + + // Process SecretFiles + if bsSpec.Application.ExtraFiles != nil && bsSpec.Application.ExtraFiles.Secrets != nil { + for _, ef := range bsSpec.Application.ExtraFiles.Secrets { + secret := &corev1.Secret{} + if err := r.addExtConfig(&result, ctx, secret, backstage.Name, ef.Name, ns); err != nil { + return result, err } - result.ExtraFileConfigMaps[cm.Name] = cm + result.ExtraFileSecrets[secret.Name] = *secret } } // Process ConfigMapEnvs if bsSpec.Application.ExtraEnvs != nil && bsSpec.Application.ExtraEnvs.ConfigMaps != nil { for _, ee := range bsSpec.Application.ExtraEnvs.ConfigMaps { - cm := corev1.ConfigMap{} - if err := r.Get(ctx, types.NamespacedName{Name: ee.Name, Namespace: ns}, &cm); err != nil { - return result, fmt.Errorf("failed to get configMap %s: %w", ee.Name, err) + cm := &corev1.ConfigMap{} + if err := r.addExtConfig(&result, ctx, cm, backstage.Name, ee.Name, ns); err != nil { + return result, err + } + result.ExtraEnvConfigMaps[cm.Name] = *cm + } + } + + // Process SecretEnvs + if bsSpec.Application.ExtraEnvs != nil && bsSpec.Application.ExtraEnvs.Secrets != nil { + for _, ee := range bsSpec.Application.ExtraEnvs.Secrets { + secret := &corev1.Secret{} + if err := r.addExtConfig(&result, ctx, secret, backstage.Name, ee.Name, ns); err != nil { + return result, err } - result.ExtraEnvConfigMaps[cm.Name] = cm + result.ExtraEnvSecrets[secret.Name] = *secret } } // Process DynamicPlugins if bsSpec.Application.DynamicPluginsConfigMapName != "" { - cm := corev1.ConfigMap{} - if err := r.Get(ctx, types.NamespacedName{Name: bsSpec.Application.DynamicPluginsConfigMapName, - Namespace: ns}, &cm); err != nil { - return result, fmt.Errorf("failed to get ConfigMap %v: %w", cm, err) + cm := &corev1.ConfigMap{} + if err := r.addExtConfig(&result, ctx, cm, backstage.Name, bsSpec.Application.DynamicPluginsConfigMapName, ns); err != nil { + return result, err } - result.DynamicPlugins = cm + result.DynamicPlugins = *cm } return result, nil } + +func (r *BackstageReconciler) addExtConfig(config *model.ExternalConfig, ctx context.Context, obj client.Object, backstageName, objectName, ns string) error { + + lg := log.FromContext(ctx) + + if err := r.Get(ctx, types.NamespacedName{Name: objectName, Namespace: ns}, obj); err != nil { + if _, ok := obj.(*corev1.Secret); ok && errors.IsForbidden(err) { + return fmt.Errorf("warning: Secrets GET is forbidden, updating Secrets may not cause Pod recreating") + } + return fmt.Errorf("failed to get external config from %s: %s", objectName, err) + } + + if err := config.AddToSyncedConfig(obj); err != nil { + return fmt.Errorf("failed to add to synced %s: %s", obj.GetName(), err) + } + + if obj.GetLabels() == nil { + obj.SetLabels(map[string]string{}) + } + if obj.GetAnnotations() == nil { + obj.SetAnnotations(map[string]string{}) + } + + autoSync := true + autoSyncStr, ok := os.LookupEnv(AutoSyncEnvVar) + if ok { + autoSync, _ = strconv.ParseBool(autoSyncStr) + } + + if obj.GetLabels()[model.ExtConfigSyncLabel] == "" || obj.GetAnnotations()[model.BackstageNameAnnotation] == "" || + obj.GetLabels()[model.ExtConfigSyncLabel] != strconv.FormatBool(autoSync) { + + obj.GetLabels()[model.ExtConfigSyncLabel] = strconv.FormatBool(autoSync) + obj.GetAnnotations()[model.BackstageNameAnnotation] = backstageName + if err := r.Update(ctx, obj); err != nil { + return fmt.Errorf("failed to update external config %s: %s", objectName, err) + } + lg.V(1).Info(fmt.Sprintf("update external config %s with label %s=%s and annotation %s=%s", obj.GetName(), model.ExtConfigSyncLabel, strconv.FormatBool(autoSync), model.BackstageNameAnnotation, backstageName)) + } + + return nil +} diff --git a/go.mod b/go.mod index f79e2697..d19a2966 100644 --- a/go.mod +++ b/go.mod @@ -35,14 +35,17 @@ require ( github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/moby/spdystream v0.2.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.18.0 // indirect diff --git a/go.sum b/go.sum index 329f0a41..23524f21 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= @@ -16,7 +17,6 @@ github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1 github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -49,15 +49,16 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJY github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -70,6 +71,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -77,17 +80,12 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8= github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk= github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg= -github.com/openshift/api v0.0.0-20240328182048-8bef56a2e295 h1:Fv47GtZvL6XvM/eHdRyb9NJezy/wY/0YtisbZyir58E= -github.com/openshift/api v0.0.0-20240328182048-8bef56a2e295/go.mod h1:CxgbWAlvu2iQB0UmKTtRu1YfepRg1/vJ64n2DlIEVz4= -github.com/openshift/api v0.0.0-20240412130237-e2b0b690b638 h1://6BunjFcTaoaWD9IXRC2BynmIW9ag7k2ekGrUYJbzY= -github.com/openshift/api v0.0.0-20240412130237-e2b0b690b638/go.mod h1:CxgbWAlvu2iQB0UmKTtRu1YfepRg1/vJ64n2DlIEVz4= -github.com/openshift/api v0.0.0-20240415140253-c0feb35ae9fb h1:VXw3qKECkLeZFJaNw5XPnAgwn8nCeLe3OeXgGHSzRsU= -github.com/openshift/api v0.0.0-20240415140253-c0feb35ae9fb/go.mod h1:CxgbWAlvu2iQB0UmKTtRu1YfepRg1/vJ64n2DlIEVz4= github.com/openshift/api v0.0.0-20240418150331-2449d07abb86 h1:m/w2kof5rKYG0O+Xx19mmvDenen7LQWgRdV46/iVud0= github.com/openshift/api v0.0.0-20240418150331-2449d07abb86/go.mod h1:CxgbWAlvu2iQB0UmKTtRu1YfepRg1/vJ64n2DlIEVz4= github.com/openshift/api v0.0.0-20240419172957-f39cf2ef93fd h1:DztdAsKaNJjfL12LyBCxL2ELPXn4NdWE/IxLCUpL7AY= @@ -115,8 +113,6 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -138,8 +134,6 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= @@ -151,12 +145,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -194,24 +184,14 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw= -k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80= k8s.io/api v0.29.4 h1:WEnF/XdxuCxdG3ayHNRR8yH3cI1B/llkWBma6bq4R3w= k8s.io/api v0.29.4/go.mod h1:DetSv0t4FBTcEpfA84NJV3g9a7+rSzlUHk5ADAYHUv0= -k8s.io/apiextensions-apiserver v0.29.0 h1:0VuspFG7Hj+SxyF/Z/2T0uFbI5gb5LRgEyUVE3Q4lV0= -k8s.io/apiextensions-apiserver v0.29.0/go.mod h1:TKmpy3bTS0mr9pylH0nOt/QzQRrW7/h7yLdRForMZwc= k8s.io/apiextensions-apiserver v0.29.2 h1:UK3xB5lOWSnhaCk0RFZ0LUacPZz9RY4wi/yt2Iu+btg= k8s.io/apiextensions-apiserver v0.29.2/go.mod h1:aLfYjpA5p3OwtqNXQFkhJ56TB+spV8Gc4wfMhUA3/b8= -k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU= -k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU= k8s.io/apimachinery v0.29.4 h1:RaFdJiDmuKs/8cm1M6Dh1Kvyh59YQFDcFuFTSmXes6Q= k8s.io/apimachinery v0.29.4/go.mod h1:i3FJVwhvSp/6n8Fl4K97PJEP8C+MM+aoDq4+ZJBf70Y= -k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg= -k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0= k8s.io/client-go v0.29.4 h1:79ytIedxVfyXV8rpH3jCBW0u+un0fxHDwX5F9K8dPR8= k8s.io/client-go v0.29.4/go.mod h1:kC1thZQ4zQWYwldsfI088BbK6RkxK+aF5ebV8y9Q4tk= -k8s.io/component-base v0.29.0 h1:T7rjd5wvLnPBV1vC4zWd/iWRbV8Mdxs+nGaoaFzGw3s= -k8s.io/component-base v0.29.0/go.mod h1:sADonFTQ9Zc9yFLghpDpmNXEdHyQmFIGbiuZbqAXQ1M= k8s.io/component-base v0.29.2 h1:lpiLyuvPA9yV1aQwGLENYyK7n/8t6l3nn3zAtFTJYe8= k8s.io/component-base v0.29.2/go.mod h1:BfB3SLrefbZXiBfbM+2H1dlat21Uewg/5qtKOl8degM= k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= @@ -220,8 +200,6 @@ k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/A k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= k8s.io/utils v0.0.0-20240310230437-4693a0247e57 h1:gbqbevonBh57eILzModw6mrkbwM0gQBEuevE/AaBsHY= k8s.io/utils v0.0.0-20240310230437-4693a0247e57/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.17.2 h1:FwHwD1CTUemg0pW2otk7/U5/i5m2ymzvOXdbeGOUvw0= -sigs.k8s.io/controller-runtime v0.17.2/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s= sigs.k8s.io/controller-runtime v0.17.3 h1:65QmN7r3FWgTxDMz9fvGnO1kbf2nu+acg9p2R9oYYYk= sigs.k8s.io/controller-runtime v0.17.3/go.mod h1:N0jpP5Lo7lMTF9aL56Z/B2oWBJjey6StQM0jRbKQXtY= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= diff --git a/integration_tests/README.md b/integration_tests/README.md index 7a2cff99..2d284472 100644 --- a/integration_tests/README.md +++ b/integration_tests/README.md @@ -13,7 +13,7 @@ How to run Integration Tests There are 2 environment variables to use with `make` command - `USE_EXISTING_CLUSTER=true` tells test suite to use externally running cluster (from the current .kube/config context) instead of envtest. - `USE_EXISTING_CONTROLLER=true` tells test suite to use operator controller manager either deployed to the cluster OR (prevails if both) running locally with `make [install] run` command. Works only with `USE_EXISTING_CLUSTER=true` - + So, in most of the cases - Make sure you test desirable version of Operator image, that's what `make image-build image-push` does. See Makefile what version `` has. @@ -21,4 +21,26 @@ How to run Integration Tests - `make install deploy` this will install CR and deploy Controller to `backstage-system` - `make integration-test USE_EXISTING_CLUSTER=true USE_EXISTING_CONTROLLER=true` - \ No newline at end of file +To run GINKGO with command line arguments (see https://onsi.github.io/ginkgo/#running-specs) +use 'ARGS' environment variable. +For example to run specific test(s) you can use something like: + +`make integration-test ARGS='--focus "my favorite test"'` + +NOTE: + +Some tests are Openshift specific only and skipped in a local envtest and bare k8s cluster. + +` +if !isOpenshiftCluster() { +Skip("Skipped for non-Openshift cluster") +} +` + +Some tests are workable only in real (EXISTING) cluster and skipped in envtest. + +` +if !*testEnv.UseExistingCluster { +Skip("Skipped for not real cluster") +} +` \ No newline at end of file diff --git a/integration_tests/config-refresh_test.go b/integration_tests/config-refresh_test.go new file mode 100644 index 00000000..d8feb801 --- /dev/null +++ b/integration_tests/config-refresh_test.go @@ -0,0 +1,164 @@ +// +// 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 integration_tests + +import ( + "context" + "fmt" + "redhat-developer/red-hat-developer-hub-operator/pkg/utils" + "strings" + "time" + + "sigs.k8s.io/controller-runtime/pkg/client" + + corev1 "k8s.io/api/core/v1" + + appsv1 "k8s.io/api/apps/v1" + + "redhat-developer/red-hat-developer-hub-operator/pkg/model" + + bsv1alpha1 "redhat-developer/red-hat-developer-hub-operator/api/v1alpha1" + + "k8s.io/apimachinery/pkg/types" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = When("create backstage with external configuration", func() { + + var ( + ctx context.Context + ns string + ) + + BeforeEach(func() { + ctx = context.Background() + ns = createNamespace(ctx) + }) + + AfterEach(func() { + deleteNamespace(ctx, ns) + }) + + It("refresh config", func() { + + if !*testEnv.UseExistingCluster { + Skip("Skipped for not real cluster") + } + + appConfig1 := "app-config1" + secretEnv1 := "secret-env1" + + backstageName := generateRandName("") + + generateConfigMap(ctx, k8sClient, appConfig1, ns, map[string]string{"key11": "app:", "key12": "app:"}, nil, nil) + generateSecret(ctx, k8sClient, secretEnv1, ns, map[string]string{"sec11": "val11"}, nil, nil) + + bs := bsv1alpha1.BackstageSpec{ + Application: &bsv1alpha1.Application{ + AppConfig: &bsv1alpha1.AppConfig{ + MountPath: "/my/mount/path", + ConfigMaps: []bsv1alpha1.ObjectKeyRef{ + {Name: appConfig1}, + }, + }, + ExtraEnvs: &bsv1alpha1.ExtraEnvs{ + Secrets: []bsv1alpha1.ObjectKeyRef{ + {Name: secretEnv1, Key: "sec11"}, + }, + }, + }, + } + + createAndReconcileBackstage(ctx, ns, bs, backstageName) + + Eventually(func(g Gomega) { + deploy := &appsv1.Deployment{} + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: model.DeploymentName(backstageName)}, deploy) + g.Expect(err).ShouldNot(HaveOccurred()) + + podList := &corev1.PodList{} + err = k8sClient.List(ctx, podList, client.InNamespace(ns), client.MatchingLabels{model.BackstageAppLabel: utils.BackstageAppLabelValue(backstageName)}) + g.Expect(err).ShouldNot(HaveOccurred()) + + g.Expect(len(podList.Items)).To(Equal(1)) + podName := podList.Items[0].Name + out, _, err := executeRemoteCommand(ctx, ns, podName, "backstage-backend", "cat /my/mount/path/key11") + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(out).To(Equal("app:")) + + out, _, err = executeRemoteCommand(ctx, ns, podName, "backstage-backend", "echo $sec11") + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect("val11\r\n").To(Equal(out)) + + }, 10*time.Minute, 10*time.Second).Should(Succeed(), controllerMessage()) + + cm := &corev1.ConfigMap{} + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: appConfig1}, cm) + Expect(err).ShouldNot(HaveOccurred()) + + newData := "app:\n backend:" + cm.Data = map[string]string{"key11": newData} + err = k8sClient.Update(ctx, cm) + Expect(err).ShouldNot(HaveOccurred()) + + Eventually(func(g Gomega) { + err = k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: appConfig1}, cm) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(cm.Data["key11"]).To(Equal(newData)) + + // Pod replaced so have to re-ask + podList := &corev1.PodList{} + err = k8sClient.List(ctx, podList, client.InNamespace(ns), client.MatchingLabels{model.BackstageAppLabel: utils.BackstageAppLabelValue(backstageName)}) + g.Expect(err).ShouldNot(HaveOccurred()) + + podName := podList.Items[0].Name + out, _, err := executeRemoteCommand(ctx, ns, podName, "backstage-backend", "cat /my/mount/path/key11") + g.Expect(err).ShouldNot(HaveOccurred()) + // TODO nicer method to compare file content with added '\r' + g.Expect(strings.ReplaceAll(out, "\r", "")).To(Equal(newData)) + + _, _, err = executeRemoteCommand(ctx, ns, podName, "backstage-backend", "cat /my/mount/path/key12") + g.Expect(err).Should(HaveOccurred()) + + }, 10*time.Minute, 10*time.Second).Should(Succeed(), controllerMessage()) + + sec := &corev1.Secret{} + err = k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: secretEnv1}, sec) + Expect(err).ShouldNot(HaveOccurred()) + newEnv := "val22" + sec.StringData = map[string]string{"sec11": newEnv} + err = k8sClient.Update(ctx, sec) + Expect(err).ShouldNot(HaveOccurred()) + + Eventually(func(g Gomega) { + + // Pod replaced so have to re-ask + podList := &corev1.PodList{} + err = k8sClient.List(ctx, podList, client.InNamespace(ns), client.MatchingLabels{model.BackstageAppLabel: utils.BackstageAppLabelValue(backstageName)}) + g.Expect(err).ShouldNot(HaveOccurred()) + + podName := podList.Items[0].Name + + out, _, err := executeRemoteCommand(ctx, ns, podName, "backstage-backend", "echo $sec11") + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(fmt.Sprintf("%s%s", newEnv, "\r\n")).To(Equal(out)) + + }, 10*time.Minute, 10*time.Second).Should(Succeed(), controllerMessage()) + + }) + +}) diff --git a/integration_tests/cr-config_test.go b/integration_tests/cr-config_test.go index 622c0790..8dd3d1c8 100644 --- a/integration_tests/cr-config_test.go +++ b/integration_tests/cr-config_test.go @@ -18,12 +18,10 @@ import ( "context" "time" - "k8s.io/apimachinery/pkg/api/errors" + corev1 "k8s.io/api/core/v1" "k8s.io/utils/ptr" - corev1 "k8s.io/api/core/v1" - "redhat-developer/red-hat-developer-hub-operator/pkg/utils" appsv1 "k8s.io/api/apps/v1" @@ -58,20 +56,20 @@ var _ = When("create backstage with CR configured", func() { It("creates Backstage with configuration ", func() { - appConfig1 := generateConfigMap(ctx, k8sClient, "app-config1", ns, map[string]string{"key11": "app:", "key12": "app:"}) - appConfig2 := generateConfigMap(ctx, k8sClient, "app-config2", ns, map[string]string{"key21": "app:", "key22": "app:"}) + appConfig1 := generateConfigMap(ctx, k8sClient, "app-config1", ns, map[string]string{"key11": "app:", "key12": "app:"}, nil, nil) + appConfig2 := generateConfigMap(ctx, k8sClient, "app-config2", ns, map[string]string{"key21": "app:", "key22": "app:"}, nil, nil) - cmFile1 := generateConfigMap(ctx, k8sClient, "cm-file1", ns, map[string]string{"cm11": "11", "cm12": "12"}) - cmFile2 := generateConfigMap(ctx, k8sClient, "cm-file2", ns, map[string]string{"cm21": "21", "cm22": "22"}) + cmFile1 := generateConfigMap(ctx, k8sClient, "cm-file1", ns, map[string]string{"cm11": "11", "cm12": "12"}, nil, nil) + cmFile2 := generateConfigMap(ctx, k8sClient, "cm-file2", ns, map[string]string{"cm21": "21", "cm22": "22"}, nil, nil) - secretFile1 := generateSecret(ctx, k8sClient, "secret-file1", ns, []string{"sec11", "sec12"}) - secretFile2 := generateSecret(ctx, k8sClient, "secret-file2", ns, []string{"sec21", "sec22"}) + secretFile1 := generateSecret(ctx, k8sClient, "secret-file1", ns, map[string]string{"sec11": "val11", "sec12": "val12"}, nil, nil) + secretFile2 := generateSecret(ctx, k8sClient, "secret-file2", ns, map[string]string{"sec21": "val21", "sec22": "val22"}, nil, nil) - cmEnv1 := generateConfigMap(ctx, k8sClient, "cm-env1", ns, map[string]string{"cm11": "11", "cm12": "12"}) - cmEnv2 := generateConfigMap(ctx, k8sClient, "cm-env2", ns, map[string]string{"cm21": "21", "cm22": "22"}) + cmEnv1 := generateConfigMap(ctx, k8sClient, "cm-env1", ns, map[string]string{"cm11": "11", "cm12": "12"}, nil, nil) + cmEnv2 := generateConfigMap(ctx, k8sClient, "cm-env2", ns, map[string]string{"cm21": "21", "cm22": "22"}, nil, nil) - secretEnv1 := generateSecret(ctx, k8sClient, "secret-env1", ns, []string{"sec11", "sec12"}) - _ = generateSecret(ctx, k8sClient, "secret-env2", ns, []string{"sec21", "sec22"}) + secretEnv1 := generateSecret(ctx, k8sClient, "secret-env1", ns, map[string]string{"sec11": "val11", "sec12": "val12"}, nil, nil) + _ = generateSecret(ctx, k8sClient, "secret-env2", ns, map[string]string{"sec21": "val21", "sec22": "val22"}, nil, nil) bs := bsv1alpha1.BackstageSpec{ Application: &bsv1alpha1.Application{ @@ -82,7 +80,6 @@ var _ = When("create backstage with CR configured", func() { {Name: appConfig2, Key: "key21"}, }, }, - //DynamicPluginsConfigMapName: "", ExtraFiles: &bsv1alpha1.ExtraFiles{ MountPath: "/my/file/path", ConfigMaps: []bsv1alpha1.ObjectKeyRef{ @@ -108,7 +105,7 @@ var _ = When("create backstage with CR configured", func() { }, }, } - backstageName := createAndReconcileBackstage(ctx, ns, bs) + backstageName := createAndReconcileBackstage(ctx, ns, bs, "") Eventually(func(g Gomega) { deploy := &appsv1.Deployment{} @@ -162,17 +159,43 @@ var _ = When("create backstage with CR configured", func() { g.Expect("cm21").To(BeEnvVarForContainer(c)) g.Expect("sec11").To(BeEnvVarForContainer(c)) - for _, cond := range deploy.Status.Conditions { - if cond.Type == "Available" { - g.Expect(cond.Status).To(Equal(corev1.ConditionTrue)) - } - } - }, 5*time.Minute, time.Second).Should(Succeed(), controllerMessage()) + }, time.Minute, time.Second).Should(Succeed(), controllerMessage()) + }) + + It("generates label and annotation", func() { + + appConfig := generateConfigMap(ctx, k8sClient, "app-config1", ns, map[string]string{"key11": "app:", "key12": "app:"}, nil, nil) + + bs := bsv1alpha1.BackstageSpec{ + Application: &bsv1alpha1.Application{ + AppConfig: &bsv1alpha1.AppConfig{ + ConfigMaps: []bsv1alpha1.ObjectKeyRef{ + {Name: appConfig}, + }, + }, + }, + } + + backstageName := createAndReconcileBackstage(ctx, ns, bs, "") + Eventually(func(g Gomega) { + + cm := &corev1.ConfigMap{} + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: appConfig}, cm) + g.Expect(err).ShouldNot(HaveOccurred()) + + g.Expect(cm.Labels).To(HaveLen(1)) + g.Expect(cm.Labels[model.ExtConfigSyncLabel]).To(Equal("true")) + + g.Expect(cm.Annotations).To(HaveLen(1)) + g.Expect(cm.Annotations[model.BackstageNameAnnotation]).To(Equal(backstageName)) + + }, 10*time.Second, time.Second).Should(Succeed()) + }) It("creates default Backstage and then update CR ", func() { - backstageName := createAndReconcileBackstage(ctx, ns, bsv1alpha1.BackstageSpec{}) + backstageName := createAndReconcileBackstage(ctx, ns, bsv1alpha1.BackstageSpec{}, "") Eventually(func(g Gomega) { By("creating Deployment with replicas=1 by default") @@ -214,96 +237,6 @@ var _ = When("create backstage with CR configured", func() { }, time.Minute, time.Second).Should(Succeed()) }) - - It("creates default Backstage and then update CR to not to use local DB", func() { - backstageName := createAndReconcileBackstage(ctx, ns, bsv1alpha1.BackstageSpec{}) - - Eventually(func(g Gomega) { - By("creating Deployment with database.enableLocalDb=true by default") - - err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: model.DbStatefulSetName(backstageName)}, &appsv1.StatefulSet{}) - g.Expect(err).To(Not(HaveOccurred())) - - err = k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: model.DbStatefulSetName(backstageName)}, &corev1.Service{}) - g.Expect(err).To(Not(HaveOccurred())) - - err = k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: model.DbStatefulSetName(backstageName)}, &corev1.Secret{}) - g.Expect(err).To(Not(HaveOccurred())) - - }, time.Minute, time.Second).Should(Succeed()) - - By("updating Backstage") - update := &bsv1alpha1.Backstage{} - err := k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, update) - Expect(err).To(Not(HaveOccurred())) - update.Spec.Database = &bsv1alpha1.Database{} - update.Spec.Database.EnableLocalDb = ptr.To(false) - err = k8sClient.Update(ctx, update) - Expect(err).To(Not(HaveOccurred())) - _, err = NewTestBackstageReconciler(ns).ReconcileAny(ctx, reconcile.Request{ - NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, - }) - Expect(err).To(Not(HaveOccurred())) - - Eventually(func(g Gomega) { - By("deleting Local Db StatefulSet, Service and Secret") - err = k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: model.DbStatefulSetName(backstageName)}, &appsv1.StatefulSet{}) - g.Expect(err).To(HaveOccurred()) - g.Expect(errors.IsNotFound(err)) - - err = k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: model.DbServiceName(backstageName)}, &corev1.Service{}) - g.Expect(err).To(HaveOccurred()) - g.Expect(errors.IsNotFound(err)) - - err = k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: model.DbSecretDefaultName(backstageName)}, &corev1.Secret{}) - g.Expect(err).To(HaveOccurred()) - g.Expect(errors.IsNotFound(err)) - }, time.Minute, time.Second).Should(Succeed()) - - }) - - It("creates Backstage with disabled local DB and secret", func() { - backstageName := createAndReconcileBackstage(ctx, ns, bsv1alpha1.BackstageSpec{ - Database: &bsv1alpha1.Database{ - EnableLocalDb: ptr.To(false), - AuthSecretName: "existing-secret", - }, - }) - - Eventually(func(g Gomega) { - By("not creating a StatefulSet for the Database") - err := k8sClient.Get(ctx, - types.NamespacedName{Namespace: ns, Name: model.DbStatefulSetName(backstageName)}, - &appsv1.StatefulSet{}) - g.Expect(err).Should(HaveOccurred()) - g.Expect(errors.IsNotFound(err)) - - By("Checking if Deployment was successfully created in the reconciliation") - err = k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: model.DeploymentName(backstageName)}, &appsv1.Deployment{}) - g.Expect(err).Should(Not(HaveOccurred())) - }, time.Minute, time.Second).Should(Succeed()) - }) - - It("creates Backstage with disabled local DB no secret", func() { - backstageName := createAndReconcileBackstage(ctx, ns, bsv1alpha1.BackstageSpec{ - Database: &bsv1alpha1.Database{ - EnableLocalDb: ptr.To(false), - }, - }) - - Eventually(func(g Gomega) { - By("not creating a StatefulSet for the Database") - err := k8sClient.Get(ctx, - types.NamespacedName{Namespace: ns, Name: model.DbStatefulSetName(backstageName)}, - &appsv1.StatefulSet{}) - g.Expect(err).Should(HaveOccurred()) - g.Expect(errors.IsNotFound(err)) - - By("Checking if Deployment was successfully created in the reconciliation") - err = k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: model.DeploymentName(backstageName)}, &appsv1.Deployment{}) - g.Expect(err).Should(Not(HaveOccurred())) - }, time.Minute, time.Second).Should(Succeed()) - }) }) // Duplicated files in different CMs diff --git a/integration_tests/db_test.go b/integration_tests/db_test.go new file mode 100644 index 00000000..7e8ec69b --- /dev/null +++ b/integration_tests/db_test.go @@ -0,0 +1,146 @@ +// +// 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 integration_tests + +import ( + "context" + "time" + + "k8s.io/apimachinery/pkg/api/errors" + + "k8s.io/utils/ptr" + + corev1 "k8s.io/api/core/v1" + + appsv1 "k8s.io/api/apps/v1" + + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "redhat-developer/red-hat-developer-hub-operator/pkg/model" + + bsv1alpha1 "redhat-developer/red-hat-developer-hub-operator/api/v1alpha1" + + "k8s.io/apimachinery/pkg/types" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = When("create backstage with CR configured", func() { + + var ( + ctx context.Context + ns string + ) + + BeforeEach(func() { + ctx = context.Background() + ns = createNamespace(ctx) + }) + + AfterEach(func() { + deleteNamespace(ctx, ns) + }) + + It("creates default Backstage and then update CR to not to use local DB", func() { + backstageName := createAndReconcileBackstage(ctx, ns, bsv1alpha1.BackstageSpec{}, "") + + Eventually(func(g Gomega) { + By("creating Deployment with database.enableLocalDb=true by default") + + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: model.DbStatefulSetName(backstageName)}, &appsv1.StatefulSet{}) + g.Expect(err).To(Not(HaveOccurred())) + + err = k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: model.DbStatefulSetName(backstageName)}, &corev1.Service{}) + g.Expect(err).To(Not(HaveOccurred())) + + err = k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: model.DbStatefulSetName(backstageName)}, &corev1.Secret{}) + g.Expect(err).To(Not(HaveOccurred())) + + }, time.Minute, time.Second).Should(Succeed()) + + By("updating Backstage") + update := &bsv1alpha1.Backstage{} + err := k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, update) + Expect(err).To(Not(HaveOccurred())) + update.Spec.Database = &bsv1alpha1.Database{} + update.Spec.Database.EnableLocalDb = ptr.To(false) + err = k8sClient.Update(ctx, update) + Expect(err).To(Not(HaveOccurred())) + _, err = NewTestBackstageReconciler(ns).ReconcileAny(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, + }) + Expect(err).To(Not(HaveOccurred())) + + Eventually(func(g Gomega) { + By("deleting Local Db StatefulSet, Service and Secret") + err = k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: model.DbStatefulSetName(backstageName)}, &appsv1.StatefulSet{}) + g.Expect(err).To(HaveOccurred()) + g.Expect(errors.IsNotFound(err)) + + err = k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: model.DbServiceName(backstageName)}, &corev1.Service{}) + g.Expect(err).To(HaveOccurred()) + g.Expect(errors.IsNotFound(err)) + + err = k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: model.DbSecretDefaultName(backstageName)}, &corev1.Secret{}) + g.Expect(err).To(HaveOccurred()) + g.Expect(errors.IsNotFound(err)) + }, time.Minute, time.Second).Should(Succeed()) + + }) + + It("creates Backstage with disabled local DB and secret", func() { + backstageName := createAndReconcileBackstage(ctx, ns, bsv1alpha1.BackstageSpec{ + Database: &bsv1alpha1.Database{ + EnableLocalDb: ptr.To(false), + AuthSecretName: "existing-secret", + }, + }, "") + + Eventually(func(g Gomega) { + By("not creating a StatefulSet for the Database") + err := k8sClient.Get(ctx, + types.NamespacedName{Namespace: ns, Name: model.DbStatefulSetName(backstageName)}, + &appsv1.StatefulSet{}) + g.Expect(err).Should(HaveOccurred()) + g.Expect(errors.IsNotFound(err)) + + By("Checking if Deployment was successfully created in the reconciliation") + err = k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: model.DeploymentName(backstageName)}, &appsv1.Deployment{}) + g.Expect(err).Should(Not(HaveOccurred())) + }, time.Minute, time.Second).Should(Succeed()) + }) + + It("creates Backstage with disabled local DB no secret", func() { + backstageName := createAndReconcileBackstage(ctx, ns, bsv1alpha1.BackstageSpec{ + Database: &bsv1alpha1.Database{ + EnableLocalDb: ptr.To(false), + }, + }, "") + + Eventually(func(g Gomega) { + By("not creating a StatefulSet for the Database") + err := k8sClient.Get(ctx, + types.NamespacedName{Namespace: ns, Name: model.DbStatefulSetName(backstageName)}, + &appsv1.StatefulSet{}) + g.Expect(err).Should(HaveOccurred()) + g.Expect(errors.IsNotFound(err)) + + By("Checking if Deployment was successfully created in the reconciliation") + err = k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: model.DeploymentName(backstageName)}, &appsv1.Deployment{}) + g.Expect(err).Should(Not(HaveOccurred())) + }, time.Minute, time.Second).Should(Succeed()) + }) +}) diff --git a/integration_tests/default-config_test.go b/integration_tests/default-config_test.go index d97e713e..b5f3e1f7 100644 --- a/integration_tests/default-config_test.go +++ b/integration_tests/default-config_test.go @@ -51,7 +51,7 @@ var _ = When("create default backstage", func() { It("creates runtime objects", func() { - backstageName := createAndReconcileBackstage(ctx, ns, bsv1alpha1.BackstageSpec{}) + backstageName := createAndReconcileBackstage(ctx, ns, bsv1alpha1.BackstageSpec{}, "") Eventually(func(g Gomega) { By("creating a secret for accessing the Database") @@ -95,20 +95,30 @@ var _ = When("create default backstage", func() { g.Expect(utils.GenerateVolumeNameFromCmOrSecret(model.AppConfigDefaultName(backstageName))). To(BeAddedAsVolumeToPodSpec(deploy.Spec.Template.Spec)) - By("setting Backstage status") - bs := &bsv1alpha1.Backstage{} - err = k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: backstageName}, bs) - g.Expect(err).ShouldNot(HaveOccurred()) - // TODO better matcher for Conditions - g.Expect(bs.Status.Conditions[0].Reason).To(Equal("Deployed")) + }, 5*time.Minute, time.Second).Should(Succeed()) - for _, cond := range deploy.Status.Conditions { - if cond.Type == "Available" { - g.Expect(cond.Status).To(Equal(corev1.ConditionTrue)) - } - } + if *testEnv.UseExistingCluster { + By("setting Backstage status (real cluster only)") + Eventually(func(g Gomega) { - }, 5*time.Minute, time.Second).Should(Succeed()) + bs := &bsv1alpha1.Backstage{} + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: backstageName}, bs) + g.Expect(err).ShouldNot(HaveOccurred()) + + deploy := &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: model.DeploymentName(backstageName)}, deploy) + g.Expect(err).ShouldNot(HaveOccurred()) + + // TODO better matcher for Conditions + g.Expect(bs.Status.Conditions[0].Reason).To(Equal("Deployed")) + + for _, cond := range deploy.Status.Conditions { + if cond.Type == "Available" { + g.Expect(cond.Status).To(Equal(corev1.ConditionTrue)) + } + } + }, 5*time.Minute, time.Second).Should(Succeed()) + } }) It("creates runtime object using raw configuration ", func() { @@ -116,15 +126,15 @@ var _ = When("create default backstage", func() { bsConf := map[string]string{"deployment.yaml": readTestYamlFile("raw-deployment.yaml")} dbConf := map[string]string{"db-statefulset.yaml": readTestYamlFile("raw-statefulset.yaml")} - bsRaw := generateConfigMap(ctx, k8sClient, "bsraw", ns, bsConf) - dbRaw := generateConfigMap(ctx, k8sClient, "dbraw", ns, dbConf) + bsRaw := generateConfigMap(ctx, k8sClient, "bsraw", ns, bsConf, nil, nil) + dbRaw := generateConfigMap(ctx, k8sClient, "dbraw", ns, dbConf, nil, nil) backstageName := createAndReconcileBackstage(ctx, ns, bsv1alpha1.BackstageSpec{ RawRuntimeConfig: &bsv1alpha1.RuntimeConfig{ BackstageConfigName: bsRaw, LocalDbConfigName: dbRaw, }, - }) + }, "") Eventually(func(g Gomega) { By("creating Deployment") diff --git a/integration_tests/rhdh-config_test.go b/integration_tests/rhdh-config_test.go index eabcdf65..bb702e46 100644 --- a/integration_tests/rhdh-config_test.go +++ b/integration_tests/rhdh-config_test.go @@ -39,7 +39,7 @@ var _ = When("create default backstage", func() { ctx := context.Background() ns := createNamespace(ctx) - backstageName := createAndReconcileBackstage(ctx, ns, bsv1alpha1.BackstageSpec{}) + backstageName := createAndReconcileBackstage(ctx, ns, bsv1alpha1.BackstageSpec{}, "") Eventually(func(g Gomega) { deploy := &appsv1.Deployment{} diff --git a/integration_tests/route_test.go b/integration_tests/route_test.go index 278a63e0..404c0e5d 100644 --- a/integration_tests/route_test.go +++ b/integration_tests/route_test.go @@ -61,7 +61,7 @@ var _ = When("create default backstage", func() { Subdomain: "test", }, }, - }) + }, "") Eventually(func() error { found := &bsv1alpha1.Backstage{} diff --git a/integration_tests/suite_test.go b/integration_tests/suite_test.go index e3f52ef0..8d708b37 100644 --- a/integration_tests/suite_test.go +++ b/integration_tests/suite_test.go @@ -20,6 +20,8 @@ import ( "os" "strconv" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -141,8 +143,18 @@ func randString(n int) string { return string(b) } -func createBackstage(ctx context.Context, spec bsv1alpha1.BackstageSpec, ns string) string { - backstageName := "test-backstage-" + randString(5) +// generateRandName return random name if name is empty or name itself otherwise +func generateRandName(name string) string { + if name != "" { + return name + } + return "test-backstage-" + randString(5) +} + +func createBackstage(ctx context.Context, spec bsv1alpha1.BackstageSpec, ns string, name string) string { + + backstageName := generateRandName(name) + err := k8sClient.Create(ctx, &bsv1alpha1.Backstage{ ObjectMeta: metav1.ObjectMeta{ Name: backstageName, @@ -154,8 +166,8 @@ func createBackstage(ctx context.Context, spec bsv1alpha1.BackstageSpec, ns stri return backstageName } -func createAndReconcileBackstage(ctx context.Context, ns string, spec bsv1alpha1.BackstageSpec) string { - backstageName := createBackstage(ctx, spec, ns) +func createAndReconcileBackstage(ctx context.Context, ns string, spec bsv1alpha1.BackstageSpec, name string) string { + backstageName := createBackstage(ctx, spec, ns, name) Eventually(func() error { found := &bsv1alpha1.Backstage{} @@ -165,6 +177,14 @@ func createAndReconcileBackstage(ctx context.Context, ns string, spec bsv1alpha1 _, err := NewTestBackstageReconciler(ns).ReconcileAny(ctx, reconcile.Request{ NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, }) + + if err != nil { + GinkgoWriter.Printf("===> Error detected on Backstage reconcile: %s \n", err.Error()) + if errors.IsAlreadyExists(err) || errors.IsConflict(err) { + return backstageName + } + } + Expect(err).To(Not(HaveOccurred())) return backstageName diff --git a/integration_tests/utils.go b/integration_tests/utils.go index 6dcfd858..1d99443e 100644 --- a/integration_tests/utils.go +++ b/integration_tests/utils.go @@ -15,11 +15,17 @@ package integration_tests import ( + "bytes" "context" "fmt" "os" "path/filepath" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/remotecommand" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -28,11 +34,13 @@ import ( . "github.com/onsi/gomega" ) -func generateConfigMap(ctx context.Context, k8sClient client.Client, name, namespace string, data map[string]string) string { +func generateConfigMap(ctx context.Context, k8sClient client.Client, name string, namespace string, data, labels map[string]string, annotations map[string]string) string { Expect(k8sClient.Create(ctx, &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, + Name: name, + Namespace: namespace, + Labels: labels, + Annotations: annotations, }, Data: data, })).To(Not(HaveOccurred())) @@ -40,15 +48,20 @@ func generateConfigMap(ctx context.Context, k8sClient client.Client, name, names return name } -func generateSecret(ctx context.Context, k8sClient client.Client, name, namespace string, keys []string) string { - data := map[string]string{} - for _, v := range keys { +func generateSecret(ctx context.Context, k8sClient client.Client, name, namespace string, data, labels, annotations map[string]string) string { + if data == nil { + data = map[string]string{} + } + + for _, v := range data { data[v] = fmt.Sprintf("value-%s", v) } Expect(k8sClient.Create(ctx, &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, + Name: name, + Namespace: namespace, + Labels: labels, + Annotations: annotations, }, StringData: data, })).To(Not(HaveOccurred())) @@ -62,3 +75,48 @@ func readTestYamlFile(name string) string { Expect(err).NotTo(HaveOccurred()) return string(b) } + +func executeRemoteCommand(ctx context.Context, podNamespace, podName, container, command string) (string, string, error) { + kubeCfg := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + clientcmd.NewDefaultClientConfigLoadingRules(), + &clientcmd.ConfigOverrides{}, + ) + restCfg, err := kubeCfg.ClientConfig() + if err != nil { + return "", "", err + } + coreClient, err := kubernetes.NewForConfig(restCfg) + if err != nil { + return "", "", err + } + + buf := &bytes.Buffer{} + errBuf := &bytes.Buffer{} + request := coreClient.CoreV1().RESTClient(). + Post(). + Namespace(podNamespace). + Resource("pods"). + Name(podName). + SubResource("exec"). + VersionedParams(&corev1.PodExecOptions{ + Command: []string{"/bin/sh", "-c", command}, + Container: container, + Stdin: false, + Stdout: true, + Stderr: true, + TTY: true, + }, scheme.ParameterCodec) + exec, err := remotecommand.NewSPDYExecutor(restCfg, "POST", request.URL()) + if err != nil { + return "", "", fmt.Errorf("%w failed creating executor %s on %v/%v", err, command, podNamespace, podName) + } + err = exec.StreamWithContext(ctx, remotecommand.StreamOptions{ + Stdout: buf, + Stderr: errBuf, + }) + if err != nil { + return "", "", fmt.Errorf("%w Failed executing command %s on %v/%v", err, command, podNamespace, podName) + } + + return buf.String(), errBuf.String(), nil +} diff --git a/pkg/model/appconfig_test.go b/pkg/model/appconfig_test.go index 45ac7ead..835a9931 100644 --- a/pkg/model/appconfig_test.go +++ b/pkg/model/appconfig_test.go @@ -146,16 +146,8 @@ func TestDefaultAndSpecifiedAppConfig(t *testing.T) { assert.Equal(t, 4, len(deployment.deployment.Spec.Template.Spec.Containers[0].Args)) assert.Equal(t, 2, len(deployment.deployment.Spec.Template.Spec.Volumes)) - //assert.Equal(t, filepath.Dir(deployment.deployment.Spec.Template.Spec.Containers[0].Args[1]), - // deployment.deployment.Spec.Template.Spec.Containers[0].VolumeMounts[0].MountPath) - - // it should be valid assertion using Volumes and VolumeMounts indexes since the order of adding is from default to specified - //assert.Equal(t, utils.GenerateVolumeNameFromCmOrSecret()deployment.deployment.Spec.Template.Spec.Volumes[0].Name assert.Equal(t, deployment.deployment.Spec.Template.Spec.Volumes[0].Name, deployment.deployment.Spec.Template.Spec.Containers[0].VolumeMounts[0].Name) - //t.Log(">>>>>>>>>>>>>>>>", ) - //t.Log(">>>>>>>>>>>>>>>>", ) - } diff --git a/pkg/model/db-service.go b/pkg/model/db-service.go index d6988565..0e405130 100644 --- a/pkg/model/db-service.go +++ b/pkg/model/db-service.go @@ -27,7 +27,7 @@ import ( type DbServiceFactory struct{} func (f DbServiceFactory) newBackstageObject() RuntimeObject { - return &DbService{ /*service: &corev1.Service{}*/ } + return &DbService{} } type DbService struct { @@ -85,5 +85,5 @@ func (b *DbService) validate(_ *BackstageModel, _ bsv1alpha1.Backstage) error { func (b *DbService) setMetaInfo(backstageName string) { b.service.SetName(DbServiceName(backstageName)) - utils.GenerateLabel(&b.service.Spec.Selector, BackstageAppLabel, fmt.Sprintf("backstage-db-%s", backstageName)) + utils.GenerateLabel(&b.service.Spec.Selector, BackstageAppLabel, utils.BackstageDbAppLabelValue(backstageName)) } diff --git a/pkg/model/db-statefulset.go b/pkg/model/db-statefulset.go index 4a5a733b..fd7b144f 100644 --- a/pkg/model/db-statefulset.go +++ b/pkg/model/db-statefulset.go @@ -105,8 +105,8 @@ func (b *DbStatefulSet) validate(model *BackstageModel, backstage bsv1alpha1.Bac func (b *DbStatefulSet) setMetaInfo(backstageName string) { b.statefulSet.SetName(DbStatefulSetName(backstageName)) - utils.GenerateLabel(&b.statefulSet.Spec.Template.ObjectMeta.Labels, BackstageAppLabel, fmt.Sprintf("backstage-db-%s", backstageName)) - utils.GenerateLabel(&b.statefulSet.Spec.Selector.MatchLabels, BackstageAppLabel, fmt.Sprintf("backstage-db-%s", backstageName)) + utils.GenerateLabel(&b.statefulSet.Spec.Template.ObjectMeta.Labels, BackstageAppLabel, utils.BackstageDbAppLabelValue(backstageName)) + utils.GenerateLabel(&b.statefulSet.Spec.Selector.MatchLabels, BackstageAppLabel, utils.BackstageDbAppLabelValue(backstageName)) } // returns DB container diff --git a/pkg/model/deployment.go b/pkg/model/deployment.go index 67c5be79..39ef9d98 100644 --- a/pkg/model/deployment.go +++ b/pkg/model/deployment.go @@ -32,6 +32,7 @@ import ( const BackstageImageEnvVar = "RELATED_IMAGE_backstage" const defaultMountDir = "/opt/app-root/src" +const ExtConfigHashAnnotation = "rhdh.redhat.com/ext-config-hash" type BackstageDeploymentFactory struct{} @@ -73,6 +74,12 @@ func (b *BackstageDeployment) addToModel(model *BackstageModel, _ bsv1alpha1.Bac if b.deployment == nil { return false, fmt.Errorf("Backstage Deployment is not initialized, make sure there is deployment.yaml in default or raw configuration") } + + if b.deployment.Spec.Template.ObjectMeta.Annotations == nil { + b.deployment.Spec.Template.ObjectMeta.Annotations = map[string]string{} + } + b.deployment.Spec.Template.ObjectMeta.Annotations[ExtConfigHashAnnotation] = model.ExternalConfig.GetHash() + model.backstageDeployment = b model.setRuntimeObject(b) @@ -130,8 +137,8 @@ func (b *BackstageDeployment) validate(model *BackstageModel, backstage bsv1alph func (b *BackstageDeployment) setMetaInfo(backstageName string) { b.deployment.SetName(DeploymentName(backstageName)) - utils.GenerateLabel(&b.deployment.Spec.Template.ObjectMeta.Labels, BackstageAppLabel, fmt.Sprintf("backstage-%s", backstageName)) - utils.GenerateLabel(&b.deployment.Spec.Selector.MatchLabels, BackstageAppLabel, fmt.Sprintf("backstage-%s", backstageName)) + utils.GenerateLabel(&b.deployment.Spec.Template.ObjectMeta.Labels, BackstageAppLabel, utils.BackstageAppLabelValue(backstageName)) + utils.GenerateLabel(&b.deployment.Spec.Selector.MatchLabels, BackstageAppLabel, utils.BackstageAppLabelValue(backstageName)) } func (b *BackstageDeployment) container() *corev1.Container { diff --git a/pkg/model/externalconfig.go b/pkg/model/externalconfig.go new file mode 100644 index 00000000..13843d76 --- /dev/null +++ b/pkg/model/externalconfig.go @@ -0,0 +1,75 @@ +// +// 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 model + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ExtConfigSyncLabel = "rhdh.redhat.com/ext-config-sync" +const BackstageNameAnnotation = "rhdh.redhat.com/backstage-name" + +type ExternalConfig struct { + RawConfig map[string]string + AppConfigs map[string]corev1.ConfigMap + ExtraFileConfigMaps map[string]corev1.ConfigMap + ExtraFileSecrets map[string]corev1.Secret + ExtraEnvConfigMaps map[string]corev1.ConfigMap + ExtraEnvSecrets map[string]corev1.Secret + DynamicPlugins corev1.ConfigMap + + syncedContent []byte +} + +func NewExternalConfig() ExternalConfig { + + return ExternalConfig{ + RawConfig: map[string]string{}, + AppConfigs: map[string]corev1.ConfigMap{}, + ExtraFileConfigMaps: map[string]corev1.ConfigMap{}, + ExtraFileSecrets: map[string]corev1.Secret{}, + ExtraEnvConfigMaps: map[string]corev1.ConfigMap{}, + ExtraEnvSecrets: map[string]corev1.Secret{}, + DynamicPlugins: corev1.ConfigMap{}, + + syncedContent: []byte{}, + } +} + +func (e *ExternalConfig) GetHash() string { + h := sha256.New() + h.Write([]byte(e.syncedContent)) + return fmt.Sprintf("%x", h.Sum(nil)) +} + +func (e *ExternalConfig) AddToSyncedConfig(content client.Object) error { + + if content.GetLabels()[ExtConfigSyncLabel] == "" || content.GetAnnotations()[BackstageNameAnnotation] == "" { + return nil + } + + d, err := json.Marshal(content) + if err != nil { + return err + } + + e.syncedContent = append(e.syncedContent, d...) + return nil +} diff --git a/pkg/model/runtime.go b/pkg/model/runtime.go index 671288ed..82f3f3db 100644 --- a/pkg/model/runtime.go +++ b/pkg/model/runtime.go @@ -37,11 +37,7 @@ import ( const BackstageAppLabel = "rhdh.redhat.com/app" // Backstage configuration scaffolding with empty BackstageObjects. -// There are all possible objects for configuration, can be: -// Mandatory - Backstage Deployment (Pod), Service -// Optional - mostly (but not only) Backstage Pod configuration objects (AppConfig, ExtraConfig) -// ForLocalDatabase - mandatory if EnabledLocalDb, ignored otherwise -// ForOpenshift - if configured, used for Openshift deployment, ignored otherwise +// There are all possible objects for configuration var runtimeConfig []ObjectConfig // BackstageModel represents internal object model @@ -61,8 +57,6 @@ type BackstageModel struct { RuntimeObjects []RuntimeObject ExternalConfig ExternalConfig - - //appConfigs []SpecifiedConfigMap } type SpecifiedConfigMap struct { @@ -70,14 +64,6 @@ type SpecifiedConfigMap struct { Key string } -type ExternalConfig struct { - RawConfig map[string]string - AppConfigs map[string]corev1.ConfigMap - ExtraFileConfigMaps map[string]corev1.ConfigMap - ExtraEnvConfigMaps map[string]corev1.ConfigMap - DynamicPlugins corev1.ConfigMap -} - func (m *BackstageModel) setRuntimeObject(object RuntimeObject) { for i, obj := range m.RuntimeObjects { if reflect.TypeOf(obj) == reflect.TypeOf(object) { diff --git a/pkg/model/runtime_test.go b/pkg/model/runtime_test.go index 5d41d301..3f5354cb 100644 --- a/pkg/model/runtime_test.go +++ b/pkg/model/runtime_test.go @@ -17,6 +17,7 @@ package model import ( "context" "fmt" + "testing" "k8s.io/utils/ptr" @@ -29,7 +30,14 @@ import ( "github.com/stretchr/testify/assert" ) -//const backstageContainerName = "backstage-backend" +func TestIfEmptyObjectsContainTypeinfo(t *testing.T) { + for _, cfg := range runtimeConfig { + obj := cfg.ObjectFactory.newBackstageObject() + assert.NotNil(t, obj.EmptyObject()) + // TODO uncomment when Kind is available + //assert.NotEmpty(t, obj.EmptyObject().GetObjectKind().GroupVersionKind().Kind) + } +} // NOTE: to make it work locally env var LOCALBIN should point to the directory where default-config folder located func TestInitDefaultDeploy(t *testing.T) { diff --git a/pkg/model/service.go b/pkg/model/service.go index da1b3a1b..519a77d6 100644 --- a/pkg/model/service.go +++ b/pkg/model/service.go @@ -78,5 +78,5 @@ func (b *BackstageService) validate(_ *BackstageModel, _ bsv1alpha1.Backstage) e func (b *BackstageService) setMetaInfo(backstageName string) { b.service.SetName(ServiceName(backstageName)) - utils.GenerateLabel(&b.service.Spec.Selector, BackstageAppLabel, fmt.Sprintf("backstage-%s", backstageName)) + utils.GenerateLabel(&b.service.Spec.Selector, BackstageAppLabel, utils.BackstageAppLabelValue(backstageName)) } diff --git a/pkg/utils/pod-mutator.go b/pkg/utils/pod-mutator.go index d7c9e606..c6c62e4d 100644 --- a/pkg/utils/pod-mutator.go +++ b/pkg/utils/pod-mutator.go @@ -39,7 +39,7 @@ type PodMutator struct { // podSpec - PodSpec to add Volume to // container - container to add VolumeMount(s) to // kind - kind of source, can be ConfigMap or Secret -// object name - name of source object +// objectName - name of source object // mountPath - mount path, default one or as it specified in BackstageCR.spec.Application.AppConfig|ExtraFiles // fileName - file name which fits one of the object's key, otherwise error will be returned. // data - key:value pairs from the object. should be specified if fileName specified @@ -77,6 +77,10 @@ func MountFilesFrom(podSpec *corev1.PodSpec, container *corev1.Container, kind O } +// AddEnvVarsFrom adds environment variable to specified container +// kind - kind of source, can be ConfigMap or Secret +// objectName - name of source object +// varName - name of env variable func AddEnvVarsFrom(container *corev1.Container, kind ObjectKind, objectName, varName string) { if varName == "" { diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 92dc8c98..fa109274 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -53,10 +53,17 @@ func GenerateRuntimeObjectName(backstageCRName string, objectType string) string // GenerateVolumeNameFromCmOrSecret generates volume name for mounting ConfigMap or Secret func GenerateVolumeNameFromCmOrSecret(cmOrSecretName string) string { - //return fmt.Sprintf("vol-%s", cmOrSecretName) return cmOrSecretName } +func BackstageAppLabelValue(backstageName string) string { + return fmt.Sprintf("backstage-%s", backstageName) +} + +func BackstageDbAppLabelValue(backstageName string) string { + return fmt.Sprintf("backstage-db-%s", backstageName) +} + func ReadYaml(manifest []byte, object interface{}) error { dec := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(manifest), 1000) if err := dec.Decode(object); err != nil {