diff --git a/pkg/console/operator/sync_v400.go b/pkg/console/operator/sync_v400.go index 88b2e12b1..b0b1edb54 100644 --- a/pkg/console/operator/sync_v400.go +++ b/pkg/console/operator/sync_v400.go @@ -485,14 +485,13 @@ func (co *consoleOperator) GetTelemetryConfiguration(ctx context.Context, operat if err != nil { return telemetryConfig, err } - if !telemeterClientIsAvailable { - telemetryConfig["TELEMETER_CLIENT_DISABLED"] = "true" - return telemetryConfig, nil - } + telemetryConfig["TELEMETER_CLIENT_DISABLED"] = fmt.Sprintf("%t", !telemeterClientIsAvailable) - accessToken, err := telemetry.GetAccessToken(co.configNSSecretLister) + var accessToken string + accessToken, err = telemetry.GetAccessToken(co.configNSSecretLister) if err != nil { - return nil, err + klog.V(4).Infof("telemetry config: failed to get access token, proceeding without organization metadata: %v", err) + accessToken = "" } organizationID, accountMail, refreshCache := telemetry.GetOrganizationMeta(telemetryConfig, co.trackables.organizationID, co.trackables.accountMail, clusterID, accessToken) // cache fetched ORGANIZATION_ID and ACCOUNT_MAIL diff --git a/pkg/console/operator/sync_v400_test.go b/pkg/console/operator/sync_v400_test.go index 544322c07..985945f30 100644 --- a/pkg/console/operator/sync_v400_test.go +++ b/pkg/console/operator/sync_v400_test.go @@ -1,15 +1,25 @@ package operator import ( + "context" + "encoding/json" + "sort" "testing" "github.com/go-test/deep" + configv1 "github.com/openshift/api/config/v1" + operatorv1 "github.com/openshift/api/operator/v1" + configlistersv1 "github.com/openshift/client-go/config/listers/config/v1" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + appsv1listers "k8s.io/client-go/listers/apps/v1" + corev1listers "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" "github.com/openshift/console-operator/pkg/api" + "github.com/openshift/console-operator/pkg/console/telemetry" ) func TestGetNodeComputeEnvironments(t *testing.T) { @@ -286,3 +296,246 @@ func TestDeploymentProgressingByGeneration(t *testing.T) { }) } } + +func newIndexer(keyFunc cache.KeyFunc) cache.Indexer { + return cache.NewIndexer(keyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}) +} + +func newTestConsoleOperator(t *testing.T, telemeterAvailable bool, pullSecretHasCloudAuth bool) *consoleOperator { + t.Helper() + + // telemetry-config ConfigMap in operator namespace + telemetryConfigCM := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: telemetry.TelemetryConfigMapName, + Namespace: api.OpenShiftConsoleOperatorNamespace, + }, + Data: map[string]string{ + "SEGMENT_API_HOST": "https://segment.example.com", + "SEGMENT_JS_HOST": "https://segment-js.example.com", + "SEGMENT_PUBLIC_API_KEY": "test-key", + }, + } + operatorNSCMIndexer := newIndexer(cache.MetaNamespaceKeyFunc) + if err := operatorNSCMIndexer.Add(telemetryConfigCM); err != nil { + t.Fatalf("failed to add telemetry configmap to indexer: %v", err) + } + + // ClusterVersion + cv := &configv1.ClusterVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: api.VersionResourceName, + }, + Spec: configv1.ClusterVersionSpec{ + ClusterID: "test-cluster-id", + }, + } + cvIndexer := newIndexer(func(obj interface{}) (string, error) { + meta := obj.(metav1.ObjectMetaAccessor).GetObjectMeta() + return meta.GetName(), nil + }) + if err := cvIndexer.Add(cv); err != nil { + t.Fatalf("failed to add clusterversion to indexer: %v", err) + } + + // telemeter-client deployment + var replicas int32 + if telemeterAvailable { + replicas = 1 + } + telemeterDeploy := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: telemetry.TelemeterClientDeploymentName, + Namespace: telemetry.TelemeterClientDeploymentNamespace, + }, + Status: appsv1.DeploymentStatus{ + AvailableReplicas: replicas, + }, + } + deployIndexer := newIndexer(cache.MetaNamespaceKeyFunc) + if err := deployIndexer.Add(telemeterDeploy); err != nil { + t.Fatalf("failed to add telemeter deployment to indexer: %v", err) + } + + // pull-secret + pullSecret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: telemetry.PullSecretName, + Namespace: api.OpenShiftConfigNamespace, + }, + Data: map[string][]byte{}, + } + if pullSecretHasCloudAuth { + dockerConfig := telemetry.DockerConfig{ + Auths: map[string]telemetry.DockerAuthEntry{ + "cloud.openshift.com": {Auth: "test-token"}, + }, + } + configBytes, err := json.Marshal(dockerConfig) + if err != nil { + t.Fatalf("failed to marshal docker config: %v", err) + } + pullSecret.Data[".dockerconfigjson"] = configBytes + } else { + dockerConfig := telemetry.DockerConfig{ + Auths: map[string]telemetry.DockerAuthEntry{ + "quay.io": {Auth: "quay-token"}, + }, + } + configBytes, err := json.Marshal(dockerConfig) + if err != nil { + t.Fatalf("failed to marshal docker config: %v", err) + } + pullSecret.Data[".dockerconfigjson"] = configBytes + } + secretIndexer := newIndexer(cache.MetaNamespaceKeyFunc) + if err := secretIndexer.Add(pullSecret); err != nil { + t.Fatalf("failed to add pull-secret to indexer: %v", err) + } + + return &consoleOperator{ + operatorNSConfigMapLister: corev1listers.NewConfigMapLister(operatorNSCMIndexer), + clusterVersionLister: configlistersv1.NewClusterVersionLister(cvIndexer), + monitoringDeploymentLister: appsv1listers.NewDeploymentLister(deployIndexer), + configNSSecretLister: corev1listers.NewSecretLister(secretIndexer), + } +} + +func TestGetTelemetryConfiguration_StableKeySet(t *testing.T) { + tests := []struct { + name string + telemeterAvailable bool + hasCloudAuth bool + expectDisabledVal string + }{ + { + name: "telemeter available with cloud auth", + telemeterAvailable: true, + hasCloudAuth: true, + expectDisabledVal: "false", + }, + { + name: "telemeter unavailable with cloud auth", + telemeterAvailable: false, + hasCloudAuth: true, + expectDisabledVal: "true", + }, + { + name: "telemeter unavailable without cloud auth (disconnected)", + telemeterAvailable: false, + hasCloudAuth: false, + expectDisabledVal: "true", + }, + { + name: "telemeter available without cloud auth (disconnected)", + telemeterAvailable: true, + hasCloudAuth: false, + expectDisabledVal: "false", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + co := newTestConsoleOperator(t, tt.telemeterAvailable, tt.hasCloudAuth) + operatorConfig := &operatorv1.Console{ + ObjectMeta: metav1.ObjectMeta{Name: "cluster"}, + } + + config, err := co.GetTelemetryConfiguration(context.Background(), operatorConfig) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if _, ok := config["CLUSTER_ID"]; !ok { + t.Error("expected CLUSTER_ID key to be present") + } + if _, ok := config["ORGANIZATION_ID"]; !ok { + t.Error("expected ORGANIZATION_ID key to always be present") + } + if _, ok := config["ACCOUNT_MAIL"]; !ok { + t.Error("expected ACCOUNT_MAIL key to always be present") + } + + disabledVal, ok := config["TELEMETER_CLIENT_DISABLED"] + if !ok { + t.Error("expected TELEMETER_CLIENT_DISABLED key to always be present") + } + if disabledVal != tt.expectDisabledVal { + t.Errorf("expected TELEMETER_CLIENT_DISABLED=%q, got %q", tt.expectDisabledVal, disabledVal) + } + }) + } +} + +func TestGetTelemetryConfiguration_KeySetStableAcrossAvailabilityChange(t *testing.T) { + operatorConfig := &operatorv1.Console{ + ObjectMeta: metav1.ObjectMeta{Name: "cluster"}, + } + + coUnavailable := newTestConsoleOperator(t, false, true) + configUnavailable, err := coUnavailable.GetTelemetryConfiguration(context.Background(), operatorConfig) + if err != nil { + t.Fatalf("unexpected error (unavailable): %v", err) + } + + coAvailable := newTestConsoleOperator(t, true, true) + configAvailable, err := coAvailable.GetTelemetryConfiguration(context.Background(), operatorConfig) + if err != nil { + t.Fatalf("unexpected error (available): %v", err) + } + + keysUnavailable := sortedKeys(configUnavailable) + keysAvailable := sortedKeys(configAvailable) + + // All keys must be present in both states — no key-set difference allowed. + sharedKeys := []string{"CLUSTER_ID", "ORGANIZATION_ID", "ACCOUNT_MAIL", "TELEMETER_CLIENT_DISABLED", "SEGMENT_API_HOST", "SEGMENT_JS_HOST", "SEGMENT_PUBLIC_API_KEY"} + for _, key := range sharedKeys { + if _, ok := configUnavailable[key]; !ok { + t.Errorf("key %q missing from unavailable config, keys present: %v", key, keysUnavailable) + } + if _, ok := configAvailable[key]; !ok { + t.Errorf("key %q missing from available config, keys present: %v", key, keysAvailable) + } + } + + if len(keysUnavailable) != len(keysAvailable) { + t.Fatalf("key set sizes differ: unavailable=%v, available=%v", keysUnavailable, keysAvailable) + } + for i := range keysUnavailable { + if keysUnavailable[i] != keysAvailable[i] { + t.Errorf("key set mismatch at index %d: unavailable has %q, available has %q", i, keysUnavailable[i], keysAvailable[i]) + } + } +} + +func sortedKeys(m map[string]string) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +// Verify that GetAccessToken failure does not cause GetTelemetryConfiguration to error +func TestGetTelemetryConfiguration_DisconnectedClusterNoError(t *testing.T) { + co := newTestConsoleOperator(t, false, false) + operatorConfig := &operatorv1.Console{ + ObjectMeta: metav1.ObjectMeta{Name: "cluster"}, + } + + config, err := co.GetTelemetryConfiguration(context.Background(), operatorConfig) + if err != nil { + t.Fatalf("expected no error on disconnected cluster, got: %v", err) + } + + if config["TELEMETER_CLIENT_DISABLED"] != "true" { + t.Error("expected TELEMETER_CLIENT_DISABLED=true on disconnected cluster") + } + if _, ok := config["ORGANIZATION_ID"]; !ok { + t.Error("expected ORGANIZATION_ID key to be present even on disconnected cluster") + } + if _, ok := config["ACCOUNT_MAIL"]; !ok { + t.Error("expected ACCOUNT_MAIL key to be present even on disconnected cluster") + } +}