diff --git a/README.md b/README.md index 860f780f..a5ec2fb2 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,32 @@ Currently the blackbox exporter deployment is only using the default config file ## Configuration +### Dynamic Configuration (ConfigMap) + +For HostedCluster monitoring, the operator supports dynamic configuration via the `route-monitor-operator-config` ConfigMap in the `openshift-route-monitor-operator` namespace. This ConfigMap can be deployed per-region/per-sector using the template at `hack/olm-registry/rmo-config-template.yaml`. + +**Supported ConfigMap fields:** +- `probe-api-url`: RHOBS synthetics API URL +- `probe-tenant`: RHOBS tenant name (default: "hcp") +- `oidc-client-id`: OIDC client ID for RHOBS authentication +- `oidc-client-secret`: OIDC client secret for RHOBS authentication +- `oidc-issuer-url`: OIDC issuer URL for RHOBS authentication +- `only-public-clusters`: Set to "true" to only monitor public clusters +- `dynatrace-enabled`: Enable/disable Dynatrace synthetic monitoring (default: "false") + +**Note:** ConfigMap values override command-line flags when present. + +### Dynatrace Synthetic Monitoring + +Dynatrace monitoring is **disabled by default**. To enable Dynatrace for specific sectors or regions: + +```yaml +data: + dynatrace-enabled: "true" +``` + +This allows per-region/per-sector control of Dynatrace monitoring via the config template. + ### Probe API URL (Experimental) For RHOBS synthetics integration with HostedCluster monitoring, configure the probe API URL: diff --git a/controllers/hostedcontrolplane/hostedcontrolplane.go b/controllers/hostedcontrolplane/hostedcontrolplane.go index 73de3f54..2b7f1f6f 100644 --- a/controllers/hostedcontrolplane/hostedcontrolplane.go +++ b/controllers/hostedcontrolplane/hostedcontrolplane.go @@ -28,6 +28,7 @@ import ( hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/openshift/route-monitor-operator/api/v1alpha1" "github.com/openshift/route-monitor-operator/config" + "github.com/openshift/route-monitor-operator/pkg/dynatrace" "github.com/openshift/route-monitor-operator/pkg/rhobs" "github.com/openshift/route-monitor-operator/pkg/util/finalizer" utilreconcile "github.com/openshift/route-monitor-operator/pkg/util/reconcile" @@ -95,10 +96,10 @@ func NewHostedControlPlaneReconciler(mgr manager.Manager, rhobsConfig RHOBSConfi } } -// getRHOBSConfig reads RHOBS configuration from the ConfigMap at reconcile time. +// getRHOBSConfig reads RHOBS and Dynatrace configuration from the ConfigMap at reconcile time. // If the ConfigMap doesn't exist or has empty values, it falls back to the command-line // flags stored in r.RHOBSConfig. -func (r *HostedControlPlaneReconciler) getRHOBSConfig(ctx context.Context) RHOBSConfig { +func (r *HostedControlPlaneReconciler) getRHOBSConfig(ctx context.Context) (RHOBSConfig, DynatraceConfig) { configMap := &corev1.ConfigMap{} err := r.Get(ctx, types.NamespacedName{ Name: configMapName, @@ -110,7 +111,7 @@ func (r *HostedControlPlaneReconciler) getRHOBSConfig(ctx context.Context) RHOBS if !kerr.IsNotFound(err) { logger.V(2).Info("Failed to read ConfigMap, using fallback config", "error", err.Error()) } - return r.RHOBSConfig + return r.RHOBSConfig, DynatraceConfig{Enabled: false} } // Merge ConfigMap values with fallback defaults @@ -134,7 +135,13 @@ func (r *HostedControlPlaneReconciler) getRHOBSConfig(ctx context.Context) RHOBS cfg.OnlyPublicClusters = true } - return cfg + // Read Dynatrace configuration - defaults to disabled + dynatraceConfig := DynatraceConfig{Enabled: false} + if strings.TrimSpace(configMap.Data["dynatrace-enabled"]) == "true" { + dynatraceConfig.Enabled = true + } + + return cfg, dynatraceConfig } //+kubebuilder:rbac:groups=openshift.io,resources=hostedcontrolplanes,verbs=get;list;watch;create;update;patch;delete @@ -148,7 +155,8 @@ func (r *HostedControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R defer log.Info("Finished reconciling HostedControlPlane") // Get dynamic config from ConfigMap (with fallback to command-line flags) - rhobsConfig := r.getRHOBSConfig(ctx) + rhobsConfig, dynatraceConfig := r.getRHOBSConfig(ctx) + log.V(2).Info("Loaded configuration", "dynatrace_enabled", dynatraceConfig.Enabled) // Fetch the HostedControlPlane instance hostedcontrolplane := &hypershiftv1beta1.HostedControlPlane{} @@ -162,24 +170,30 @@ func (r *HostedControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R return utilreconcile.RequeueWith(err) } - //Create Dynatrace API client - dynatraceApiClient, err := r.NewDynatraceApiClient(ctx) - if err != nil { - // If RHOBS is configured, Dynatrace client creation failure is non-fatal - if rhobsConfig.ProbeAPIURL != "" { - log.Info("Dynatrace client creation failed, continuing with RHOBS-only monitoring", "error", err.Error()) - dynatraceApiClient = nil + // Create Dynatrace API client only if Dynatrace is enabled + var dynatraceApiClient *dynatrace.DynatraceApiClient + if dynatraceConfig.Enabled { + client, err := r.NewDynatraceApiClient(ctx) + if err != nil { + // If RHOBS is configured, Dynatrace client creation failure is non-fatal + if rhobsConfig.ProbeAPIURL != "" { + log.Info("Dynatrace client creation failed, continuing with RHOBS-only monitoring", "error", err.Error()) + } else { + log.Error(err, "failed to create dynatrace client") + return utilreconcile.RequeueWith(err) + } } else { - log.Error(err, "failed to create dynatrace client") - return utilreconcile.RequeueWith(err) + dynatraceApiClient = client } + } else { + log.V(2).Info("Dynatrace monitoring disabled via feature flag") } // If the HostedControlPlane is marked for deletion, clean up shouldDelete := finalizer.WasDeleteRequested(hostedcontrolplane) if shouldDelete { - // Only attempt Dynatrace deletion if client was successfully created - if dynatraceApiClient != nil { + // Only attempt Dynatrace deletion if Dynatrace is enabled and client was successfully created + if dynatraceConfig.Enabled && dynatraceApiClient != nil { err = r.deleteDynatraceHttpMonitorResources(dynatraceApiClient, log, hostedcontrolplane) if err != nil { // If RHOBS is configured, Dynatrace failures are non-fatal - log warning and continue @@ -289,8 +303,8 @@ func (r *HostedControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R return utilreconcile.RequeueWith(err) } - // Only attempt Dynatrace deployment if client was successfully created - if dynatraceApiClient != nil { + // Only attempt Dynatrace deployment if Dynatrace is enabled and client was successfully created + if dynatraceConfig.Enabled && dynatraceApiClient != nil { log.Info("Deploying HTTP Monitor Resources") err = r.deployDynatraceHttpMonitorResources(ctx, dynatraceApiClient, log, hostedcontrolplane) if err != nil { diff --git a/controllers/hostedcontrolplane/hostedcontrolplane_test.go b/controllers/hostedcontrolplane/hostedcontrolplane_test.go index 4c25f1e2..c6573707 100644 --- a/controllers/hostedcontrolplane/hostedcontrolplane_test.go +++ b/controllers/hostedcontrolplane/hostedcontrolplane_test.go @@ -856,16 +856,18 @@ func TestHostedControlPlaneReconciler_getRHOBSConfig(t *testing.T) { } tests := []struct { - name string - configMap *corev1.ConfigMap - fallbackConfig RHOBSConfig - expected RHOBSConfig + name string + configMap *corev1.ConfigMap + fallbackConfig RHOBSConfig + expectedRHOBS RHOBSConfig + expectedDynatrace DynatraceConfig }{ { - name: "ConfigMap not present - uses fallback config", - configMap: nil, - fallbackConfig: fallbackConfig, - expected: fallbackConfig, + name: "ConfigMap not present - uses fallback config", + configMap: nil, + fallbackConfig: fallbackConfig, + expectedRHOBS: fallbackConfig, + expectedDynatrace: DynatraceConfig{Enabled: false}, // Default is disabled }, { name: "ConfigMap present with all values - uses ConfigMap values", @@ -881,10 +883,11 @@ func TestHostedControlPlaneReconciler_getRHOBSConfig(t *testing.T) { "oidc-client-secret": "configmap-secret", "oidc-issuer-url": "https://configmap-issuer.example.com", "only-public-clusters": "true", + "dynatrace-enabled": "true", }, }, fallbackConfig: fallbackConfig, - expected: RHOBSConfig{ + expectedRHOBS: RHOBSConfig{ ProbeAPIURL: "https://configmap-api.example.com/probes", Tenant: "configmap-tenant", OIDCClientID: "configmap-client-id", @@ -892,6 +895,7 @@ func TestHostedControlPlaneReconciler_getRHOBSConfig(t *testing.T) { OIDCIssuerURL: "https://configmap-issuer.example.com", OnlyPublicClusters: true, }, + expectedDynatrace: DynatraceConfig{Enabled: true}, }, { name: "ConfigMap present with partial values - merges with fallback", @@ -907,7 +911,7 @@ func TestHostedControlPlaneReconciler_getRHOBSConfig(t *testing.T) { }, }, fallbackConfig: fallbackConfig, - expected: RHOBSConfig{ + expectedRHOBS: RHOBSConfig{ ProbeAPIURL: "https://partial-api.example.com/probes", Tenant: "partial-tenant", OIDCClientID: "fallback-client-id", @@ -915,6 +919,7 @@ func TestHostedControlPlaneReconciler_getRHOBSConfig(t *testing.T) { OIDCIssuerURL: "https://fallback-issuer.example.com", OnlyPublicClusters: false, }, + expectedDynatrace: DynatraceConfig{Enabled: false}, // Default is disabled when not specified }, { name: "ConfigMap present with empty values - uses fallback for empty fields", @@ -930,10 +935,11 @@ func TestHostedControlPlaneReconciler_getRHOBSConfig(t *testing.T) { "oidc-client-secret": "", "oidc-issuer-url": "", "only-public-clusters": "false", + "dynatrace-enabled": "", // empty defaults to enabled }, }, fallbackConfig: fallbackConfig, - expected: RHOBSConfig{ + expectedRHOBS: RHOBSConfig{ ProbeAPIURL: "fallback-api.example.com/probes", // wrong, should be fallback Tenant: "fallback-tenant", // empty/whitespace uses fallback OIDCClientID: "valid-id", @@ -941,6 +947,7 @@ func TestHostedControlPlaneReconciler_getRHOBSConfig(t *testing.T) { OIDCIssuerURL: "https://fallback-issuer.example.com", OnlyPublicClusters: false, }, + expectedDynatrace: DynatraceConfig{Enabled: false}, // Empty defaults to disabled }, { name: "ConfigMap in wrong namespace - uses fallback config", @@ -953,8 +960,9 @@ func TestHostedControlPlaneReconciler_getRHOBSConfig(t *testing.T) { "probe-api-url": "https://wrong-namespace.example.com/probes", }, }, - fallbackConfig: fallbackConfig, - expected: fallbackConfig, + fallbackConfig: fallbackConfig, + expectedRHOBS: fallbackConfig, + expectedDynatrace: DynatraceConfig{Enabled: false}, // Default is disabled }, { name: "ConfigMap with wrong name - uses fallback config", @@ -967,8 +975,54 @@ func TestHostedControlPlaneReconciler_getRHOBSConfig(t *testing.T) { "probe-api-url": "https://wrong-name.example.com/probes", }, }, - fallbackConfig: fallbackConfig, - expected: fallbackConfig, + fallbackConfig: fallbackConfig, + expectedRHOBS: fallbackConfig, + expectedDynatrace: DynatraceConfig{Enabled: false}, // Default is disabled + }, + { + name: "ConfigMap with Dynatrace enabled", + configMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Namespace: config.OperatorNamespace, + }, + Data: map[string]string{ + "dynatrace-enabled": "true", + }, + }, + fallbackConfig: fallbackConfig, + expectedRHOBS: fallbackConfig, + expectedDynatrace: DynatraceConfig{Enabled: true}, + }, + { + name: "ConfigMap with Dynatrace disabled explicitly", + configMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Namespace: config.OperatorNamespace, + }, + Data: map[string]string{ + "dynatrace-enabled": "false", + }, + }, + fallbackConfig: fallbackConfig, + expectedRHOBS: fallbackConfig, + expectedDynatrace: DynatraceConfig{Enabled: false}, + }, + { + name: "ConfigMap with Dynatrace set to invalid value - defaults to disabled", + configMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Namespace: config.OperatorNamespace, + }, + Data: map[string]string{ + "dynatrace-enabled": "maybe", + }, + }, + fallbackConfig: fallbackConfig, + expectedRHOBS: fallbackConfig, + expectedDynatrace: DynatraceConfig{Enabled: false}, // Invalid value defaults to disabled }, } @@ -982,15 +1036,19 @@ func TestHostedControlPlaneReconciler_getRHOBSConfig(t *testing.T) { r := newTestReconciler(t, objs...) r.RHOBSConfig = tt.fallbackConfig - result := r.getRHOBSConfig(ctx) + rhobsResult, dynatraceResult := r.getRHOBSConfig(ctx) // For the empty values test, we need to fix the expected value if tt.name == "ConfigMap present with empty values - uses fallback for empty fields" { - tt.expected.ProbeAPIURL = "https://fallback-api.example.com/probes" + tt.expectedRHOBS.ProbeAPIURL = "https://fallback-api.example.com/probes" + } + + if rhobsResult != tt.expectedRHOBS { + t.Errorf("getRHOBSConfig() RHOBS got = %+v, want = %+v", rhobsResult, tt.expectedRHOBS) } - if result != tt.expected { - t.Errorf("getRHOBSConfig() got = %+v, want = %+v", result, tt.expected) + if dynatraceResult != tt.expectedDynatrace { + t.Errorf("getRHOBSConfig() Dynatrace got = %+v, want = %+v", dynatraceResult, tt.expectedDynatrace) } }) } diff --git a/controllers/hostedcontrolplane/synthetics.go b/controllers/hostedcontrolplane/synthetics.go index 1b146438..2476992a 100644 --- a/controllers/hostedcontrolplane/synthetics.go +++ b/controllers/hostedcontrolplane/synthetics.go @@ -259,6 +259,13 @@ type RHOBSConfig struct { OnlyPublicClusters bool } +// DynatraceConfig holds Dynatrace feature flag configuration. +// Dynatrace monitoring is disabled by default and can be enabled per-sector/region +// by setting "dynatrace-enabled: true" in the ConfigMap. +type DynatraceConfig struct { + Enabled bool // Feature flag - defaults to false +} + // ensureRHOBSProbe ensures that a RHOBS probe exists for the HostedControlPlane func (r *HostedControlPlaneReconciler) ensureRHOBSProbe(ctx context.Context, log logr.Logger, hostedcontrolplane *hypershiftv1beta1.HostedControlPlane, cfg RHOBSConfig) error { clusterID := hostedcontrolplane.Spec.ClusterID diff --git a/hack/olm-registry/rmo-config-template.yaml b/hack/olm-registry/rmo-config-template.yaml index e14fb17d..ccd75804 100644 --- a/hack/olm-registry/rmo-config-template.yaml +++ b/hack/olm-registry/rmo-config-template.yaml @@ -17,6 +17,10 @@ metadata: # # For production traffic splitting in high-load regions (e.g., us-east-1 with # multiple RHOBS cells), see SREP-3225 for adding rhobs-cell labels to MCs. +# +# Configuration Parameters: +# - DYNATRACE_ENABLED: Controls Dynatrace synthetic monitoring (default: "false") +# Set to "true" to enable Dynatrace for specific sectors/regions ############################################################################### parameters: @@ -42,7 +46,11 @@ parameters: required: true - name: ONLY_PUBLIC_CLUSTERS value: "" - required: false + required: false +- name: DYNATRACE_ENABLED + value: "false" + required: false + description: "Enable Dynatrace synthetic monitoring for this sector" objects: - apiVersion: hive.openshift.io/v1 @@ -79,6 +87,7 @@ objects: oidc-client-secret: ${OIDC_CLIENT_SECRET} oidc-issuer-url: ${OIDC_ISSUER_URL} only-public-clusters: ${ONLY_PUBLIC_CLUSTERS} + dynatrace-enabled: ${DYNATRACE_ENABLED} # Label the RMO namespace so the RHOBS MonitoringStack discovers # ServiceMonitors in it and scrapes RMO metrics for the HCP tenant. - apiVersion: v1