diff --git a/operators/multiclusterobservability/api/shared/multiclusterobservability_shared.go b/operators/multiclusterobservability/api/shared/multiclusterobservability_shared.go index 9bbf588cd..9d38df4fa 100644 --- a/operators/multiclusterobservability/api/shared/multiclusterobservability_shared.go +++ b/operators/multiclusterobservability/api/shared/multiclusterobservability_shared.go @@ -20,11 +20,22 @@ import ( // +kubebuilder:validation:MaxLength=2083 type URL string +// Validate validates the underlying URL. func (u URL) Validate() error { _, err := url.Parse(string(u)) return err } +// HostPath returns the URL's host together with its path. +// This also runs a validation of the underlying url. +func (u URL) HostPath() (string, error) { + parsedUrl, err := url.Parse(string(u)) + if err != nil { + return "", err + } + return parsedUrl.Host + parsedUrl.Path, nil +} + // ObservabilityAddonSpec is the spec of observability addon. type ObservabilityAddonSpec struct { // EnableMetrics indicates the observability addon push metrics to hub server. diff --git a/operators/multiclusterobservability/controllers/placementrule/hub_info_secret.go b/operators/multiclusterobservability/controllers/placementrule/hub_info_secret.go index 29f862681..a32a1bccf 100644 --- a/operators/multiclusterobservability/controllers/placementrule/hub_info_secret.go +++ b/operators/multiclusterobservability/controllers/placementrule/hub_info_secret.go @@ -23,15 +23,15 @@ import ( func generateHubInfoSecret(client client.Client, obsNamespace string, namespace string, ingressCtlCrdExists bool) (*corev1.Secret, error) { - obsApiRouteHost := "" + obsAPIHost := "" alertmanagerEndpoint := "" alertmanagerRouterCA := "" if ingressCtlCrdExists { var err error - obsApiRouteHost, err = config.GetObsAPIHost(context.TODO(), client, obsNamespace) + obsAPIHost, err = config.GetObsAPIExternalHost(context.TODO(), client, obsNamespace) if err != nil { - log.Error(err, "Failed to get the host for observatorium API route") + log.Error(err, "Failed to get the host for Observatorium API host URL") return nil, err } @@ -56,7 +56,7 @@ func generateHubInfoSecret(client client.Client, obsNamespace string, } else { // for KinD support, the managedcluster and hub cluster are assumed in the same cluster, the observatorium-api // will be accessed through k8s service FQDN + port - obsApiRouteHost = config.GetOperandNamePrefix() + "observatorium-api" + "." + config.GetDefaultNamespace() + ".svc.cluster.local:8080" + obsAPIHost = config.GetOperandNamePrefix() + "observatorium-api" + "." + config.GetDefaultNamespace() + ".svc.cluster.local:8080" // if alerting is disabled, do not set alertmanagerEndpoint if !config.IsAlertingDisabled() { alertmanagerEndpoint = config.AlertmanagerServiceName + "." + config.GetDefaultNamespace() + ".svc.cluster.local:9095" @@ -70,7 +70,7 @@ func generateHubInfoSecret(client client.Client, obsNamespace string, } obsApiURL := url.URL{ - Host: obsApiRouteHost, + Host: obsAPIHost, Path: operatorconfig.ObservatoriumAPIRemoteWritePath, } if !obsApiURL.IsAbs() { diff --git a/operators/multiclusterobservability/controllers/placementrule/hub_info_secret_test.go b/operators/multiclusterobservability/controllers/placementrule/hub_info_secret_test.go index 15eb94158..51fe9c501 100644 --- a/operators/multiclusterobservability/controllers/placementrule/hub_info_secret_test.go +++ b/operators/multiclusterobservability/controllers/placementrule/hub_info_secret_test.go @@ -10,6 +10,8 @@ import ( operatorv1 "github.com/openshift/api/operator/v1" routev1 "github.com/openshift/api/route/v1" + mcoshared "github.com/stolostron/multicluster-observability-operator/operators/multiclusterobservability/api/shared" + mcov1beta2 "github.com/stolostron/multicluster-observability-operator/operators/multiclusterobservability/api/v1beta2" "gopkg.in/yaml.v2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -120,10 +122,34 @@ func newTestAmDefaultCA() *corev1.ConfigMap { } } +func newMultiClusterObservability() *mcov1beta2.MultiClusterObservability { + return &mcov1beta2.MultiClusterObservability{ + TypeMeta: metav1.TypeMeta{Kind: "MultiClusterObservability"}, + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: mcov1beta2.MultiClusterObservabilitySpec{ + StorageConfig: &mcov1beta2.StorageConfig{ + MetricObjectStorage: &mcoshared.PreConfiguredStorage{ + Key: "test", + Name: "test", + }, + AlertmanagerStorageSize: "2Gi", + }, + }, + } +} + func TestNewSecret(t *testing.T) { initSchema(t) - objs := []runtime.Object{newTestObsApiRoute(), newTestAlertmanagerRoute(), newTestIngressController(), newTestRouteCASecret()} + mco := newMultiClusterObservability() + config.SetMonitoringCRName(mco.Name) + objs := []runtime.Object{ + newTestObsApiRoute(), + newTestAlertmanagerRoute(), + newTestIngressController(), + newTestRouteCASecret(), + mco, + } c := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() hubInfo, err := generateHubInfoSecret(c, mcoNamespace, namespace, true) @@ -138,6 +164,22 @@ func TestNewSecret(t *testing.T) { if !strings.HasPrefix(hub.ObservatoriumAPIEndpoint, "https://test-host") || hub.AlertmanagerEndpoint != "https://"+routeHost || hub.AlertmanagerRouterCA != routerCA { t.Fatalf("Wrong content in hub info secret: \ngot: "+hub.ObservatoriumAPIEndpoint+" "+hub.AlertmanagerEndpoint+" "+hub.AlertmanagerRouterCA, clusterName+" "+"https://test-host"+" "+"test-host"+" "+routerCA) } + + mco.Spec.AdvancedConfig = &mcov1beta2.AdvancedConfig{CustomObservabilityHubURL: "https://custom-obs", CustomAlertmanagerHubURL: "https://custom-am"} + c = fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() + hubInfo, err = generateHubInfoSecret(c, mcoNamespace, namespace, true) + if err != nil { + t.Fatalf("Failed to initial the hub info secret: (%v)", err) + } + hub = &operatorconfig.HubInfo{} + err = yaml.Unmarshal(hubInfo.Data[operatorconfig.HubInfoSecretKey], &hub) + if err != nil { + t.Fatalf("Failed to unmarshal data in hub info secret (%v)", err) + } + if !strings.HasPrefix(hub.ObservatoriumAPIEndpoint, "https://custom-obs") || !strings.HasPrefix(hub.AlertmanagerEndpoint, "https://custom-am") || hub.AlertmanagerRouterCA != routerCA { + t.Fatalf("Wrong content in hub info secret: \ngot: "+hub.ObservatoriumAPIEndpoint+" "+hub.AlertmanagerEndpoint+" "+hub.AlertmanagerRouterCA, clusterName+" "+"https://custom-obs"+" "+"custom-obs"+" "+routerCA) + } + } func TestNewBYOSecret(t *testing.T) { diff --git a/operators/multiclusterobservability/pkg/certificates/certificates.go b/operators/multiclusterobservability/pkg/certificates/certificates.go index d942ae099..ce374f439 100644 --- a/operators/multiclusterobservability/pkg/certificates/certificates.go +++ b/operators/multiclusterobservability/pkg/certificates/certificates.go @@ -461,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(context.TODO(), c, config.GetDefaultNamespace()) + url, err := config.GetObsAPIRouteHost(context.TODO(), c, config.GetDefaultNamespace()) if err != nil { log.Error(err, "Failed to get api route address") return nil, err diff --git a/operators/multiclusterobservability/pkg/config/config.go b/operators/multiclusterobservability/pkg/config/config.go index 9ad6ec534..0ce7a5b36 100644 --- a/operators/multiclusterobservability/pkg/config/config.go +++ b/operators/multiclusterobservability/pkg/config/config.go @@ -480,8 +480,23 @@ func GetDefaultTenantName() string { return defaultTenantName } -// GetObsAPIHost is used to get the URL for observartium api gateway. -func GetObsAPIHost(ctx context.Context, client client.Client, namespace string) (string, error) { +// GetObsAPIRouteHost is used to Route's host for Observatorium API. This doesn't take into consideration +// the `advanced.customObservabilityHubURL` configuration. +func GetObsAPIRouteHost(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 + } + return GetRouteHost(client, obsAPIGateway, namespace) +} + +// GetObsAPIExternalHost is used to get the frontend URL that should be used to reach the Observatorium API instance. +// This takes into consideration the `advanced.customObservabilityHubURL` configuration. +func GetObsAPIExternalHost(ctx context.Context, client client.Client, namespace string) (string, error) { mco := &observabilityv1beta2.MultiClusterObservability{} err := client.Get(ctx, types.NamespacedName{ @@ -492,11 +507,16 @@ func GetObsAPIHost(ctx context.Context, client client.Client, namespace string) } advancedConfig := mco.Spec.AdvancedConfig if advancedConfig != nil && advancedConfig.CustomObservabilityHubURL != "" { - err := advancedConfig.CustomObservabilityHubURL.Validate() + hubObsUrl := advancedConfig.CustomObservabilityHubURL + err := hubObsUrl.Validate() + if err != nil { + return "", err + } + obsHostPath, err := hubObsUrl.HostPath() if err != nil { return "", err } - return string(advancedConfig.CustomObservabilityHubURL), nil + return obsHostPath, nil } return GetRouteHost(client, obsAPIGateway, namespace) } diff --git a/operators/multiclusterobservability/pkg/config/config_test.go b/operators/multiclusterobservability/pkg/config/config_test.go index 407919465..353a9bc42 100644 --- a/operators/multiclusterobservability/pkg/config/config_test.go +++ b/operators/multiclusterobservability/pkg/config/config_test.go @@ -15,6 +15,7 @@ import ( routev1 "github.com/openshift/api/route/v1" fakeconfigclient "github.com/openshift/client-go/config/clientset/versioned/fake" observatoriumv1alpha1 "github.com/stolostron/observatorium-operator/api/v1alpha1" + "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -252,7 +253,7 @@ func TestGetClusterIDFailed(t *testing.T) { } } -func TestGetObsAPIHost(t *testing.T) { +func TestGetObsAPIRouteHost(t *testing.T) { route := &routev1.Route{ ObjectMeta: metav1.ObjectMeta{ Name: obsAPIGateway, @@ -267,12 +268,14 @@ func TestGetObsAPIHost(t *testing.T) { scheme.AddKnownTypes(mcov1beta2.GroupVersion, &mcov1beta2.MultiClusterObservability{}) client := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(route).Build() - host, _ := GetObsAPIHost(context.TODO(), client, "default") + host, err := GetObsAPIRouteHost(context.TODO(), client, "default") + assert.NoError(t, err) if host == apiServerURL { t.Errorf("Should not get route host in default namespace") } - host, _ = GetObsAPIHost(context.TODO(), client, "test") + host, err = GetObsAPIRouteHost(context.TODO(), client, "test") + assert.NoError(t, err) if host != apiServerURL { t.Errorf("Observatorium api (%v) is not the expected (%v)", host, apiServerURL) } @@ -289,14 +292,70 @@ func TestGetObsAPIHost(t *testing.T) { }, } client = fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(route, mco).Build() - host, _ = GetObsAPIHost(context.TODO(), client, "test") - if host != customBaseURL { + host, err = GetObsAPIRouteHost(context.TODO(), client, "test") + assert.NoError(t, err) + if host != apiServerURL { + t.Errorf("Observatorium api (%v) is not the expected (%v)", host, apiServerURL) + } + + mco.Spec.AdvancedConfig.CustomObservabilityHubURL = "httpa://foob ar.c" + client = fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(route, mco).Build() + host, err = GetObsAPIRouteHost(context.TODO(), client, "test") + assert.NoError(t, err) + if host != apiServerURL { + t.Errorf("Observatorium api (%v) is not the expected (%v)", host, apiServerURL) + } +} + +func TestGetObsAPIExternalHost(t *testing.T) { + route := &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: obsAPIGateway, + Namespace: "test", + }, + Spec: routev1.RouteSpec{ + Host: apiServerURL, + }, + } + scheme := runtime.NewScheme() + scheme.AddKnownTypes(routev1.GroupVersion, route) + scheme.AddKnownTypes(mcov1beta2.GroupVersion, &mcov1beta2.MultiClusterObservability{}) + client := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(route).Build() + + host, err := GetObsAPIExternalHost(context.TODO(), client, "default") + assert.NoError(t, err) + if host == apiServerURL { + t.Errorf("Should not get route host in default namespace") + } + + host, err = GetObsAPIExternalHost(context.TODO(), client, "test") + assert.NoError(t, err) + if host != apiServerURL { + t.Errorf("Observatorium api (%v) is not the expected (%v)", host, apiServerURL) + } + + customBaseURL := "https://custom.base/url" + expectedHost := "custom.base/url" + mco := &mcov1beta2.MultiClusterObservability{ + ObjectMeta: metav1.ObjectMeta{ + Name: GetMonitoringCRName(), + }, + Spec: mcov1beta2.MultiClusterObservabilitySpec{ + AdvancedConfig: &mcov1beta2.AdvancedConfig{ + CustomObservabilityHubURL: mcoshared.URL(customBaseURL), + }, + }, + } + client = fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(route, mco).Build() + host, err = GetObsAPIExternalHost(context.TODO(), client, "test") + assert.NoError(t, err) + if host != expectedHost { 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(context.TODO(), client, "test") + _, err = GetObsAPIExternalHost(context.TODO(), client, "test") if err == nil { t.Errorf("expected error when parsing URL '%v', but got none", mco.Spec.AdvancedConfig.CustomObservabilityHubURL) }