From a9fbe5f9432bcbea20ac553c7217f503246b76b8 Mon Sep 17 00:00:00 2001 From: Douglas Camata <159076+douglascamata@users.noreply.github.com> Date: Thu, 2 May 2024 13:15:56 +0200 Subject: [PATCH] [ACM-10706] Add support for custom alertmanager url (#1419) * Add support for custom alertmanager hub URL Signed-off-by: Douglas Camata <159076+douglascamata@users.noreply.github.com> * Add some missing contexts args Signed-off-by: Douglas Camata <159076+douglascamata@users.noreply.github.com> * Add tests for `GetAlertmanagerEndpoint` Signed-off-by: Douglas Camata <159076+douglascamata@users.noreply.github.com> --------- Signed-off-by: Douglas Camata <159076+douglascamata@users.noreply.github.com> --- .../multiclusterobservability_types.go | 5 ++ ...gement.io_multiclusterobservabilities.yaml | 5 ++ ...gement.io_multiclusterobservabilities.yaml | 7 +++ .../placementrule/hub_info_secret.go | 5 +- .../pkg/certificates/certificates.go | 7 ++- .../pkg/config/config.go | 26 ++++++-- .../pkg/config/config_test.go | 59 +++++++++++++++++-- 7 files changed, 100 insertions(+), 14 deletions(-) diff --git a/operators/multiclusterobservability/api/v1beta2/multiclusterobservability_types.go b/operators/multiclusterobservability/api/v1beta2/multiclusterobservability_types.go index 20d257339..accea4da1 100644 --- a/operators/multiclusterobservability/api/v1beta2/multiclusterobservability_types.go +++ b/operators/multiclusterobservability/api/v1beta2/multiclusterobservability_types.go @@ -47,6 +47,11 @@ type AdvancedConfig struct { // For the metrics-collector that runs in the hub this setting has no effect. // +optional CustomObservabilityHubURL observabilityshared.URL `json:"customObservabilityHubURL,omitempty"` + // CustomAlertmanagerHubURL overrides the alertmanager URL to send alerts from the spoke + // to the hub server. + // For the alertmanager that runs in the hub this setting has no effect. + // +optional + CustomAlertmanagerHubURL observabilityshared.URL `json:"customAlertmanagerHubURL,omitempty"` // The spec of the data retention configurations // +optional RetentionConfig *RetentionConfig `json:"retentionConfig,omitempty"` diff --git a/operators/multiclusterobservability/bundle/manifests/observability.open-cluster-management.io_multiclusterobservabilities.yaml b/operators/multiclusterobservability/bundle/manifests/observability.open-cluster-management.io_multiclusterobservabilities.yaml index 004f881f9..473330a3e 100644 --- a/operators/multiclusterobservability/bundle/manifests/observability.open-cluster-management.io_multiclusterobservabilities.yaml +++ b/operators/multiclusterobservability/bundle/manifests/observability.open-cluster-management.io_multiclusterobservabilities.yaml @@ -1148,6 +1148,11 @@ spec: description: Annotations is an unstructured key value map stored with a service account type: object type: object + customAlertmanagerHubURL: + description: CustomAlertmanagerHubURL overrides the alertmanager URL to send alerts from the spoke to the hub server. For the alertmanager that runs in the hub this setting has no effect. + maxLength: 2083 + pattern: ^https?:\/\/ + type: string customObservabilityHubURL: description: CustomObservabilityHubURL overrides the endpoint used by the metrics-collector to send metrics to the hub server. For the metrics-collector that runs in the hub this setting has no effect. maxLength: 2083 diff --git a/operators/multiclusterobservability/config/crd/bases/observability.open-cluster-management.io_multiclusterobservabilities.yaml b/operators/multiclusterobservability/config/crd/bases/observability.open-cluster-management.io_multiclusterobservabilities.yaml index 409242278..7534e7de6 100644 --- a/operators/multiclusterobservability/config/crd/bases/observability.open-cluster-management.io_multiclusterobservabilities.yaml +++ b/operators/multiclusterobservability/config/crd/bases/observability.open-cluster-management.io_multiclusterobservabilities.yaml @@ -1761,6 +1761,13 @@ spec: stored with a service account type: object type: object + customAlertmanagerHubURL: + description: CustomAlertmanagerHubURL overrides the alertmanager + URL to send alerts from the spoke to the hub server. For the + alertmanager that runs in the hub this setting has no effect. + maxLength: 2083 + pattern: ^https?:\/\/ + type: string customObservabilityHubURL: description: CustomObservabilityHubURL overrides the endpoint used by the metrics-collector to send metrics to the hub server. diff --git a/operators/multiclusterobservability/controllers/placementrule/hub_info_secret.go b/operators/multiclusterobservability/controllers/placementrule/hub_info_secret.go index c36cefb0a..3ece1f486 100644 --- a/operators/multiclusterobservability/controllers/placementrule/hub_info_secret.go +++ b/operators/multiclusterobservability/controllers/placementrule/hub_info_secret.go @@ -5,6 +5,7 @@ package placementrule import ( + "context" "net/url" "gopkg.in/yaml.v2" @@ -27,7 +28,7 @@ func generateHubInfoSecret(client client.Client, obsNamespace string, if ingressCtlCrdExists { var err error - obsApiRouteHost, err = config.GetObsAPIHost(client, obsNamespace) + obsApiRouteHost, err = config.GetObsAPIHost(context.TODO(), client, obsNamespace) if err != nil { log.Error(err, "Failed to get the host for observatorium API route") return nil, err @@ -35,7 +36,7 @@ func generateHubInfoSecret(client client.Client, obsNamespace string, // if alerting is disabled, do not set alertmanagerEndpoint if !config.IsAlertingDisabled() { - alertmanagerEndpoint, err = config.GetAlertmanagerEndpoint(client, obsNamespace) + alertmanagerEndpoint, err = config.GetAlertmanagerEndpoint(context.TODO(), client, obsNamespace) if err != nil { log.Error(err, "Failed to get alertmanager endpoint") return nil, err diff --git a/operators/multiclusterobservability/pkg/certificates/certificates.go b/operators/multiclusterobservability/pkg/certificates/certificates.go index 6e80a48fd..d942ae099 100644 --- a/operators/multiclusterobservability/pkg/certificates/certificates.go +++ b/operators/multiclusterobservability/pkg/certificates/certificates.go @@ -16,9 +16,10 @@ import ( "net" "time" - operatorconfig "github.com/stolostron/multicluster-observability-operator/operators/pkg/config" certificatesv1 "k8s.io/api/certificates/v1" + operatorconfig "github.com/stolostron/multicluster-observability-operator/operators/pkg/config" + "golang.org/x/exp/slices" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -460,7 +461,7 @@ func pemEncode(cert []byte, key []byte) (*bytes.Buffer, *bytes.Buffer) { func getHosts(c client.Client, ingressCtlCrdExists bool) ([]string, error) { hosts := []string{config.GetObsAPISvc(config.GetOperandName(config.Observatorium))} if ingressCtlCrdExists { - url, err := config.GetObsAPIHost(c, config.GetDefaultNamespace()) + url, err := config.GetObsAPIHost(context.TODO(), c, config.GetDefaultNamespace()) if err != nil { log.Error(err, "Failed to get api route address") return nil, err @@ -515,7 +516,7 @@ func CreateUpdateMtlsCertSecretForHubCollector(c client.Client, updateMtlsCert b log.Error(nil, "failed to sign CSR") return errors.NewBadRequest("failed to sign CSR") } - //Create a secret + // Create a secret HubMtlsSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: operatorconfig.HubMetricsCollectorMtlsCert, diff --git a/operators/multiclusterobservability/pkg/config/config.go b/operators/multiclusterobservability/pkg/config/config.go index 3c0c5922c..9ad6ec534 100644 --- a/operators/multiclusterobservability/pkg/config/config.go +++ b/operators/multiclusterobservability/pkg/config/config.go @@ -481,9 +481,9 @@ func GetDefaultTenantName() string { } // GetObsAPIHost is used to get the URL for observartium api gateway. -func GetObsAPIHost(client client.Client, namespace string) (string, error) { +func GetObsAPIHost(ctx context.Context, client client.Client, namespace string) (string, error) { mco := &observabilityv1beta2.MultiClusterObservability{} - err := client.Get(context.TODO(), + err := client.Get(ctx, types.NamespacedName{ Name: GetMonitoringCRName(), }, mco) @@ -532,10 +532,26 @@ func GetMCONamespace() string { } // GetAlertmanagerEndpoint is used to get the URL for alertmanager. -func GetAlertmanagerEndpoint(client client.Client, namespace string) (string, error) { - found := &routev1.Route{} +func GetAlertmanagerEndpoint(ctx context.Context, client client.Client, namespace string) (string, error) { + mco := &observabilityv1beta2.MultiClusterObservability{} + err := client.Get(ctx, + types.NamespacedName{ + Name: GetMonitoringCRName(), + }, mco) + if err != nil && !errors.IsNotFound(err) { + return "", err + } + advancedConfig := mco.Spec.AdvancedConfig + if advancedConfig != nil && advancedConfig.CustomAlertmanagerHubURL != "" { + err := advancedConfig.CustomAlertmanagerHubURL.Validate() + if err != nil { + return "", err + } + return string(advancedConfig.CustomAlertmanagerHubURL), nil + } - err := client.Get(context.TODO(), types.NamespacedName{Name: AlertmanagerRouteName, Namespace: namespace}, found) + found := &routev1.Route{} + err = client.Get(ctx, types.NamespacedName{Name: AlertmanagerRouteName, Namespace: namespace}, found) if err != nil && errors.IsNotFound(err) { // if the alertmanager router is not created yet, fallback to get host from the domain of ingresscontroller domain, err := getDomainForIngressController( diff --git a/operators/multiclusterobservability/pkg/config/config_test.go b/operators/multiclusterobservability/pkg/config/config_test.go index 3ad4552a5..407919465 100644 --- a/operators/multiclusterobservability/pkg/config/config_test.go +++ b/operators/multiclusterobservability/pkg/config/config_test.go @@ -5,6 +5,7 @@ package config import ( + "context" "fmt" "os" "reflect" @@ -266,12 +267,12 @@ func TestGetObsAPIHost(t *testing.T) { scheme.AddKnownTypes(mcov1beta2.GroupVersion, &mcov1beta2.MultiClusterObservability{}) client := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(route).Build() - host, _ := GetObsAPIHost(client, "default") + host, _ := GetObsAPIHost(context.TODO(), client, "default") if host == apiServerURL { t.Errorf("Should not get route host in default namespace") } - host, _ = GetObsAPIHost(client, "test") + host, _ = GetObsAPIHost(context.TODO(), client, "test") if host != apiServerURL { t.Errorf("Observatorium api (%v) is not the expected (%v)", host, apiServerURL) } @@ -288,18 +289,68 @@ func TestGetObsAPIHost(t *testing.T) { }, } client = fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(route, mco).Build() - host, _ = GetObsAPIHost(client, "test") + host, _ = GetObsAPIHost(context.TODO(), client, "test") if host != customBaseURL { t.Errorf("Observatorium api (%v) is not the expected (%v)", host, customBaseURL) } mco.Spec.AdvancedConfig.CustomObservabilityHubURL = "httpa://foob ar.c" client = fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(route, mco).Build() - _, err := GetObsAPIHost(client, "test") + _, err := GetObsAPIHost(context.TODO(), client, "test") if err == nil { t.Errorf("expected error when parsing URL '%v', but got none", mco.Spec.AdvancedConfig.CustomObservabilityHubURL) } +} + +func TestGetAlertmanagerEndpoint(t *testing.T) { + routeURL := "http://route.example.com" + route := &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: AlertmanagerRouteName, + Namespace: "test", + }, + Spec: routev1.RouteSpec{ + Host: routeURL, + }, + } + scheme := runtime.NewScheme() + scheme.AddKnownTypes(routev1.GroupVersion, route) + scheme.AddKnownTypes(mcov1beta2.GroupVersion, &mcov1beta2.MultiClusterObservability{}) + client := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(route).Build() + + host, _ := GetAlertmanagerEndpoint(context.TODO(), client, "default") + if host == routeURL { + t.Errorf("Should not get route host in default namespace") + } + + host, _ = GetAlertmanagerEndpoint(context.TODO(), client, "test") + if host != routeURL { + t.Errorf("Alertmanager URL (%v) is not the expected (%v)", host, routeURL) + } + customBaseURL := "https://custom.base/url" + mco := &mcov1beta2.MultiClusterObservability{ + ObjectMeta: metav1.ObjectMeta{ + Name: GetMonitoringCRName(), + }, + Spec: mcov1beta2.MultiClusterObservabilitySpec{ + AdvancedConfig: &mcov1beta2.AdvancedConfig{ + CustomAlertmanagerHubURL: mcoshared.URL(customBaseURL), + }, + }, + } + client = fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(route, mco).Build() + host, _ = GetAlertmanagerEndpoint(context.TODO(), client, "test") + if host != customBaseURL { + t.Errorf("Alertmanager URL (%v) is not the expected (%v)", host, customBaseURL) + } + + mco.Spec.AdvancedConfig.CustomAlertmanagerHubURL = "httpa://foob ar.c" + client = fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(route, mco).Build() + _, err := GetAlertmanagerEndpoint(context.TODO(), client, "test") + if err == nil { + t.Errorf("expected error when parsing URL '%v', but got none", mco.Spec.AdvancedConfig.CustomObservabilityHubURL) + } } func TestIsPaused(t *testing.T) {