diff --git a/operators/multiclusterobservability/controllers/multiclusterobservability/multiclusterobservability_controller_test.go b/operators/multiclusterobservability/controllers/multiclusterobservability/multiclusterobservability_controller_test.go index 6faaa0c82..6b49103cc 100644 --- a/operators/multiclusterobservability/controllers/multiclusterobservability/multiclusterobservability_controller_test.go +++ b/operators/multiclusterobservability/controllers/multiclusterobservability/multiclusterobservability_controller_test.go @@ -34,12 +34,13 @@ import ( mchv1 "github.com/stolostron/multiclusterhub-operator/api/v1" + addonv1alpha1 "open-cluster-management.io/api/addon/v1alpha1" + clusterv1 "open-cluster-management.io/api/cluster/v1" + mcoshared "github.com/stolostron/multicluster-observability-operator/operators/multiclusterobservability/api/shared" mcov1beta2 "github.com/stolostron/multicluster-observability-operator/operators/multiclusterobservability/api/v1beta2" "github.com/stolostron/multicluster-observability-operator/operators/multiclusterobservability/pkg/config" "github.com/stolostron/multicluster-observability-operator/operators/multiclusterobservability/pkg/rendering/templates" - addonv1alpha1 "open-cluster-management.io/api/addon/v1alpha1" - clusterv1 "open-cluster-management.io/api/cluster/v1" ) func init() { @@ -58,7 +59,7 @@ func setupTest(t *testing.T) func() { manifestsPath := path.Join(wd, "../../manifests") os.Setenv("TEMPLATES_PATH", testManifestsPath) templates.ResetTemplates() - //clean up the manifest path if left over from previous test + // clean up the manifest path if left over from previous test if fi, err := os.Lstat(testManifestsPath); err == nil && fi.Mode()&os.ModeSymlink != 0 { if err = os.Remove(testManifestsPath); err != nil { t.Logf("Failed to delete symlink(%s) for the test manifests: (%v)", testManifestsPath, err) @@ -309,7 +310,7 @@ func TestMultiClusterMonitoringCRUpdate(t *testing.T) { clientCACerts := newTestCert(config.ClientCACerts, namespace) grafanaCert := newTestCert(config.GrafanaCerts, namespace) serverCert := newTestCert(config.ServerCerts, namespace) - //byo case for proxy + // byo case for proxy proxyRouteBYOCACerts := newTestCert(config.ProxyRouteBYOCAName, namespace) proxyRouteBYOCert := newTestCert(config.ProxyRouteBYOCERTName, namespace) // byo case for the alertmanager route @@ -319,6 +320,7 @@ func TestMultiClusterMonitoringCRUpdate(t *testing.T) { objs := []runtime.Object{mco, svc, serverCACerts, clientCACerts, proxyRouteBYOCACerts, grafanaCert, serverCert, testAmRouteBYOCaSecret, testAmRouteBYOCertSecret, proxyRouteBYOCert, clustermgmtAddon} + // Create a fake client to mock API calls. cl := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() @@ -339,10 +341,10 @@ func TestMultiClusterMonitoringCRUpdate(t *testing.T) { if err != nil { t.Fatalf("reconcile: (%v)", err) } - //wait for update status + // wait for update status time.Sleep(1 * time.Second) - //verify openshiftcluster monitoring label is set to true in namespace + // verify openshiftcluster monitoring label is set to true in namespace updatedNS := &corev1.Namespace{} err = cl.Get(context.TODO(), types.NamespacedName{ Name: namespace, @@ -398,7 +400,7 @@ func TestMultiClusterMonitoringCRUpdate(t *testing.T) { if err != nil { t.Fatalf("reconcile: (%v)", err) } - //wait for update status + // wait for update status time.Sleep(1 * time.Second) updatedObjectStoreSecret := &corev1.Secret{} @@ -434,7 +436,7 @@ func TestMultiClusterMonitoringCRUpdate(t *testing.T) { if err != nil { t.Fatalf("reconcile: (%v)", err) } - //wait for update status + // wait for update status time.Sleep(1 * time.Second) updatedConfigmap := &corev1.ConfigMap{} @@ -473,7 +475,7 @@ func TestMultiClusterMonitoringCRUpdate(t *testing.T) { if err != nil { t.Fatalf("reconcile: (%v)", err) } - //wait for update status + // wait for update status time.Sleep(1 * time.Second) updatedMCO = &mcov1beta2.MultiClusterObservability{} @@ -509,7 +511,7 @@ func TestMultiClusterMonitoringCRUpdate(t *testing.T) { t.Fatalf("reconcile: (%v)", err) } } - //wait for update status + // wait for update status time.Sleep(1 * time.Second) updatedMCO = &mcov1beta2.MultiClusterObservability{} @@ -552,7 +554,7 @@ func TestMultiClusterMonitoringCRUpdate(t *testing.T) { if err != nil { t.Fatalf("reconcile: (%v)", err) } - //wait for update status + // wait for update status time.Sleep(1 * time.Second) updatedMCO = &mcov1beta2.MultiClusterObservability{} @@ -586,7 +588,7 @@ func TestMultiClusterMonitoringCRUpdate(t *testing.T) { if err != nil { t.Fatalf("reconcile: (%v)", err) } - //wait for update status + // wait for update status time.Sleep(1 * time.Second) updatedMCO = &mcov1beta2.MultiClusterObservability{} @@ -626,7 +628,7 @@ func TestMultiClusterMonitoringCRUpdate(t *testing.T) { if err != nil { t.Fatalf("reconcile: (%v)", err) } - //wait for update status + // wait for update status time.Sleep(1 * time.Second) updatedMCO = &mcov1beta2.MultiClusterObservability{} @@ -640,7 +642,7 @@ func TestMultiClusterMonitoringCRUpdate(t *testing.T) { t.Errorf("Failed to get correct MCO status, expect Ready") } - //Test finalizer + // Test finalizer mco.ObjectMeta.DeletionTimestamp = &metav1.Time{Time: time.Now()} mco.ObjectMeta.Finalizers = []string{resFinalizer, "test-finalizerr"} mco.ObjectMeta.ResourceVersion = updatedMCO.ObjectMeta.ResourceVersion @@ -738,7 +740,7 @@ func TestImageReplaceForMCO(t *testing.T) { t.Fatalf("reconcile: (%v)", err) } - //wait for update status + // wait for update status time.Sleep(1 * time.Second) expectedDeploymentNames := []string{ @@ -811,7 +813,7 @@ func TestImageReplaceForMCO(t *testing.T) { // stop update status routine stopStatusUpdate <- struct{}{} - //wait for update status + // wait for update status time.Sleep(1 * time.Second) } @@ -1013,7 +1015,7 @@ func TestPrometheusRulesRemovedFromOpenshiftMonitoringNamespace(t *testing.T) { Name: "acm-observability-alert-rules", Namespace: "openshift-monitoring", }, - //Sample rules + // Sample rules Spec: monitoringv1.PrometheusRuleSpec{ Groups: []monitoringv1.RuleGroup{ { diff --git a/operators/multiclusterobservability/controllers/multiclusterobservability/observatorium.go b/operators/multiclusterobservability/controllers/multiclusterobservability/observatorium.go index 24838fd6f..ede2641e3 100644 --- a/operators/multiclusterobservability/controllers/multiclusterobservability/observatorium.go +++ b/operators/multiclusterobservability/controllers/multiclusterobservability/observatorium.go @@ -7,6 +7,12 @@ package multiclusterobservability import ( "bytes" "context" + + // The import of crypto/md5 below is not for cryptographic use. It is used to hash the contents of files to track + // changes and thus it's not a security issue. + // nolint:gosec + "crypto/md5" // #nosec G401 G501 + "encoding/hex" "errors" "fmt" "os" @@ -49,6 +55,58 @@ const ( endpointsRestartLabel = "endpoints/time-restarted" ) +// Fetch contents of the secrets: observability-server-certs, observability-client-ca-certs, +// observability-observatorium-api. +// Fetch contents of the configmap: observability-observatorium-api. +// Concatenate all of the above and hash their contents. +// If any of the secrets or configmaps aren't found, an empty struct of the respective type is used for the hash. +func hashObservatoriumCRConfig(cl client.Client) (string, error) { + secretsToQuery := []metav1.ObjectMeta{ + {Name: mcoconfig.ServerCerts, Namespace: mcoconfig.GetDefaultNamespace()}, + {Name: mcoconfig.ClientCACerts, Namespace: mcoconfig.GetDefaultNamespace()}, + {Name: mcoconfig.GetOperandNamePrefix() + mcoconfig.ObservatoriumAPI, Namespace: mcoconfig.GetDefaultNamespace()}, + } + configMapToQuery := metav1.ObjectMeta{ + Name: mcoconfig.GetOperandNamePrefix() + mcoconfig.ObservatoriumAPI, Namespace: mcoconfig.GetDefaultNamespace(), + } + + // The usage of crypto/md5 below is not for cryptographic use. It is used to hash the contents of files to track + // changes and thus it's not a security issue. + // nolint:gosec + hasher := md5.New() // #nosec G401 G501 + for _, secret := range secretsToQuery { + resultSecret := &v1.Secret{} + err := cl.Get(context.TODO(), types.NamespacedName{ + Name: secret.Name, + Namespace: secret.Namespace, + }, resultSecret) + if err != nil && !k8serrors.IsNotFound(err) { + return "", err + } + secretData, err := yaml.Marshal(resultSecret.Data) + if err != nil { + return "", err + } + hasher.Write(secretData) + } + + resultConfigMap := &v1.ConfigMap{} + err := cl.Get(context.TODO(), types.NamespacedName{ + Name: configMapToQuery.Name, + Namespace: configMapToQuery.Namespace, + }, resultConfigMap) + if err != nil && !k8serrors.IsNotFound(err) { + return "", err + } + configMapData, err := yaml.Marshal(resultConfigMap.Data) + if err != nil { + return "", err + } + hasher.Write(configMapData) + + return hex.EncodeToString(hasher.Sum(nil)), nil +} + // GenerateObservatoriumCR returns Observatorium cr defined in MultiClusterObservability func GenerateObservatoriumCR( cl client.Client, scheme *runtime.Scheme, @@ -75,6 +133,13 @@ func GenerateObservatoriumCR( if err != nil { return &ctrl.Result{}, err } + + hash, err := hashObservatoriumCRConfig(cl) + if err != nil { + return &ctrl.Result{}, err + } + labels["config-hash"] = hash + observatoriumCR := &obsv1alpha1.Observatorium{ ObjectMeta: metav1.ObjectMeta{ Name: mcoconfig.GetOperandName(mcoconfig.Observatorium), @@ -439,7 +504,7 @@ func newAPISpec(c client.Client, mco *mcov1beta2.MultiClusterObservability) (obs if !mcoconfig.WithoutResourcesRequests(mco.GetAnnotations()) { apiSpec.Resources = mcoconfig.GetResources(mcoconfig.ObservatoriumAPI, mco.Spec.AdvancedConfig) } - //set the default observatorium components' image + // set the default observatorium components' image apiSpec.Image = mcoconfig.DefaultImgRepository + "/" + mcoconfig.ObservatoriumAPIImgName + ":" + mcoconfig.DefaultImgTagSuffix replace, image := mcoconfig.ReplaceImage(mco.Annotations, apiSpec.Image, mcoconfig.ObservatoriumAPIImgName) @@ -601,8 +666,8 @@ func newRuleSpec(mco *mcov1beta2.MultiClusterObservability, scSelected string) o mco.Spec.StorageConfig.RuleStorageSize, scSelected) - //configure alertmanager in ruler - //ruleSpec.AlertmanagerURLs = []string{mcoconfig.AlertmanagerURL} + // configure alertmanager in ruler + // ruleSpec.AlertmanagerURLs = []string{mcoconfig.AlertmanagerURL} ruleSpec.AlertmanagerConfigFile = obsv1alpha1.AlertmanagerConfigFile{ Name: mcoconfig.AlertmanagersDefaultConfigMapName, Key: mcoconfig.AlertmanagersDefaultConfigFileKey, @@ -826,8 +891,8 @@ func newReceiverControllerSpec(mco *mcov1beta2.MultiClusterObservability) obsv1a func newCompactSpec(mco *mcov1beta2.MultiClusterObservability, scSelected string) obsv1alpha1.CompactSpec { compactSpec := obsv1alpha1.CompactSpec{} - //Compactor, generally, does not need to be highly available. - //Compactions are needed from time to time, only when new blocks appear. + // Compactor, generally, does not need to be highly available. + // Compactions are needed from time to time, only when new blocks appear. compactSpec.Replicas = &mcoconfig.Replicas1 if !mcoconfig.WithoutResourcesRequests(mco.GetAnnotations()) { compactSpec.Resources = mcoconfig.GetResources(mcoconfig.ThanosCompact, mco.Spec.AdvancedConfig) diff --git a/operators/multiclusterobservability/controllers/multiclusterobservability/observatorium_test.go b/operators/multiclusterobservability/controllers/multiclusterobservability/observatorium_test.go index 0895f5750..113f034b0 100644 --- a/operators/multiclusterobservability/controllers/multiclusterobservability/observatorium_test.go +++ b/operators/multiclusterobservability/controllers/multiclusterobservability/observatorium_test.go @@ -7,6 +7,7 @@ package multiclusterobservability import ( "bytes" "context" + "errors" "reflect" "testing" @@ -22,9 +23,10 @@ import ( mcoshared "github.com/stolostron/multicluster-observability-operator/operators/multiclusterobservability/api/shared" mcov1beta2 "github.com/stolostron/multicluster-observability-operator/operators/multiclusterobservability/api/v1beta2" + observatoriumv1alpha1 "github.com/stolostron/observatorium-operator/api/v1alpha1" + mcoconfig "github.com/stolostron/multicluster-observability-operator/operators/multiclusterobservability/pkg/config" mcoutil "github.com/stolostron/multicluster-observability-operator/operators/multiclusterobservability/pkg/util" - observatoriumv1alpha1 "github.com/stolostron/observatorium-operator/api/v1alpha1" ) var ( @@ -173,6 +175,38 @@ func TestNoUpdateObservatoriumCR(t *testing.T) { observatoriumv1alpha1.AddToScheme(s) objs := []runtime.Object{mco} + objs = append(objs, []runtime.Object{ + &corev1.Secret{ + TypeMeta: metav1.TypeMeta{Kind: "Secret"}, + ObjectMeta: metav1.ObjectMeta{Name: mcoconfig.ServerCerts, Namespace: namespace}, + Data: map[string][]byte{ + "tls.crt": []byte("server-cert"), + "tls.key": []byte("server-key"), + }, + }, + &corev1.Secret{ + TypeMeta: metav1.TypeMeta{Kind: "Secret"}, + ObjectMeta: metav1.ObjectMeta{Name: mcoconfig.ClientCACerts, Namespace: namespace}, + Data: map[string][]byte{ + "tls.crt": []byte("client-ca-cert"), + }, + }, + &corev1.Secret{ + TypeMeta: metav1.TypeMeta{Kind: "Secret"}, + ObjectMeta: metav1.ObjectMeta{Name: mcoconfig.GetOperandNamePrefix() + mcoconfig.ObservatoriumAPI, Namespace: namespace}, + Data: map[string][]byte{ + "tls.crt": []byte("test"), + }, + }, + &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{Kind: "ConfigMap"}, + ObjectMeta: metav1.ObjectMeta{Name: mcoconfig.GetOperandNamePrefix() + mcoconfig.ObservatoriumAPI, Namespace: namespace}, + Data: map[string]string{ + "config.yaml": "test", + }, + }, + }...) + // Create a fake client to mock API calls. cl := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() mcoconfig.SetOperandNames(cl) @@ -198,6 +232,15 @@ func TestNoUpdateObservatoriumCR(t *testing.T) { oldSpecBytes, _ := yaml.Marshal(oldSpec) newSpecBytes, _ := yaml.Marshal(newSpec) + hash, configHashFound := observatoriumCRFound.Labels["config-hash"] + if !configHashFound { + t.Errorf("config-hash label not found in Observatorium CR") + } + const expectedConfigHash = "06869d277adcef9eb22f81d61c964393" + if hash != expectedConfigHash { + t.Errorf("config-hash label contains unexpected hash. Want: '%s', got '%s'", expectedConfigHash, hash) + } + if res := bytes.Compare(newSpecBytes, oldSpecBytes); res != 0 { t.Errorf("%v should be equal to %v", string(oldSpecBytes), string(newSpecBytes)) } @@ -208,6 +251,76 @@ func TestNoUpdateObservatoriumCR(t *testing.T) { } } +func TestHashObservatoriumCRWithConfig(t *testing.T) { + namespace := mcoconfig.GetDefaultNamespace() + + tt := []struct { + name string + objs []runtime.Object + expectedHash string + expectedErr error + }{ + { + name: "With Observatorium's secrets and configmap present", + expectedHash: "06869d277adcef9eb22f81d61c964393", + objs: []runtime.Object{ + &corev1.Secret{ + TypeMeta: metav1.TypeMeta{Kind: "Secret"}, + ObjectMeta: metav1.ObjectMeta{Name: mcoconfig.ServerCerts, Namespace: namespace}, + Data: map[string][]byte{ + "tls.crt": []byte("server-cert"), + "tls.key": []byte("server-key"), + }, + }, + &corev1.Secret{ + TypeMeta: metav1.TypeMeta{Kind: "Secret"}, + ObjectMeta: metav1.ObjectMeta{Name: mcoconfig.ClientCACerts, Namespace: namespace}, + Data: map[string][]byte{ + "tls.crt": []byte("client-ca-cert"), + }, + }, + &corev1.Secret{ + TypeMeta: metav1.TypeMeta{Kind: "Secret"}, + ObjectMeta: metav1.ObjectMeta{Name: mcoconfig.GetOperandNamePrefix() + mcoconfig.ObservatoriumAPI, Namespace: namespace}, + Data: map[string][]byte{ + "tls.crt": []byte("test"), + }, + }, + &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{Kind: "ConfigMap"}, + ObjectMeta: metav1.ObjectMeta{Name: mcoconfig.GetOperandNamePrefix() + mcoconfig.ObservatoriumAPI, Namespace: namespace}, + Data: map[string]string{ + "config.yaml": "test", + }, + }, + }, + }, + { + name: "Without Observatorium's secrets and configmap present", + // The hash is still calculated when the configmaps and secrets aren't presented, because the implementation + // is hashing an empty object for each one of them if they aren't found. + expectedHash: "1b7799225be7c98c78387c6aff5b0eed", + objs: []runtime.Object{}, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + // Create a fake client to mock API calls. + cl := fake.NewClientBuilder().WithRuntimeObjects(tc.objs...).Build() + hash, err := hashObservatoriumCRConfig(cl) + + if !errors.Is(err, tc.expectedErr) { + t.Errorf("unexpected error: %v\nwant: %v", err, tc.expectedErr) + } + + if hash != tc.expectedHash { + t.Errorf("config-hash label contains unexpected hash. Want: '%s', got '%s'", tc.expectedHash, hash) + } + }) + } +} + func TestGetTLSSecretMountPath(t *testing.T) { testCaseList := []struct {