|
| 1 | +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +package scenarios |
| 5 | + |
| 6 | +import ( |
| 7 | + "context" |
| 8 | + "encoding/json" |
| 9 | + "testing" |
| 10 | + |
| 11 | + "github.com/aws-observability/helm-charts/integration-tests/amazon-cloudwatch-observability/util" |
| 12 | + "github.com/aws-observability/helm-charts/integration-tests/amazon-cloudwatch-observability/validations/minikube" |
| 13 | + "github.com/stretchr/testify/assert" |
| 14 | + "github.com/stretchr/testify/require" |
| 15 | + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| 16 | + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" |
| 17 | + "k8s.io/apimachinery/pkg/runtime/schema" |
| 18 | +) |
| 19 | + |
| 20 | +// TestDefaultOtel validates the config: "default:otel" value, which merges an OTLP receiver onto |
| 21 | +// the default CW Agent config. Asserts on the rendered CR's JSON spec.config, like the other scenarios. |
| 22 | +func TestDefaultOtel(t *testing.T) { |
| 23 | + k8sClient, err := util.NewK8sClient() |
| 24 | + require.NoError(t, err, "failed to create k8s client") |
| 25 | + |
| 26 | + ns, err := k8sClient.GetNamespace(minikube.Namespace) |
| 27 | + assert.NoError(t, err) |
| 28 | + assert.Equal(t, minikube.Namespace, ns.Name) |
| 29 | + |
| 30 | + exists, err := k8sClient.ValidateDeploymentExists(minikube.Namespace, "amazon-cloudwatch-observability-controller-manager") |
| 31 | + assert.NoError(t, err) |
| 32 | + assert.True(t, exists, "operator deployment should exist") |
| 33 | + |
| 34 | + dynamicClient, err := k8sClient.GetDynamicClient() |
| 35 | + require.NoError(t, err, "failed to get dynamic client") |
| 36 | + |
| 37 | + gvr := schema.GroupVersionResource{ |
| 38 | + Group: "cloudwatch.aws.amazon.com", |
| 39 | + Version: "v1alpha1", |
| 40 | + Resource: "amazoncloudwatchagents", |
| 41 | + } |
| 42 | + agentList, err := dynamicClient.Resource(gvr).Namespace(minikube.Namespace).List( |
| 43 | + context.Background(), metav1.ListOptions{}, |
| 44 | + ) |
| 45 | + require.NoError(t, err, "failed to list AmazonCloudWatchAgent CRs") |
| 46 | + |
| 47 | + agentMap := make(map[string]unstructured.Unstructured) |
| 48 | + for _, agent := range agentList.Items { |
| 49 | + agentMap[agent.GetName()] = agent |
| 50 | + } |
| 51 | + |
| 52 | + t.Run("NodeAgentHasOtlpReceiver", func(t *testing.T) { |
| 53 | + validateNodeAgentOtlpReceiver(t, agentMap) |
| 54 | + }) |
| 55 | + t.Run("NodeAgentDefaultConfigSurvives", func(t *testing.T) { |
| 56 | + validateNodeAgentDefaultConfigSurvives(t, agentMap) |
| 57 | + }) |
| 58 | + t.Run("ClusterScraperHasNoOtlpReceiver", func(t *testing.T) { |
| 59 | + validateClusterScraperNoOtlpReceiver(t, agentMap) |
| 60 | + }) |
| 61 | + |
| 62 | + t.Log("default:otel config scenario validation passed") |
| 63 | +} |
| 64 | + |
| 65 | +// configJSONOf parses a CR's spec.config (a JSON string) into a map, failing the test on any issue. |
| 66 | +func configJSONOf(t *testing.T, agentMap map[string]unstructured.Unstructured, name string) map[string]interface{} { |
| 67 | + t.Helper() |
| 68 | + agent, exists := agentMap[name] |
| 69 | + if !assert.True(t, exists, "%s CR should exist", name) { |
| 70 | + return nil |
| 71 | + } |
| 72 | + spec, ok := agent.Object["spec"].(map[string]interface{}) |
| 73 | + if !assert.True(t, ok, "%s spec should be a map", name) { |
| 74 | + return nil |
| 75 | + } |
| 76 | + configStr, ok := spec["config"].(string) |
| 77 | + if !assert.True(t, ok, "%s config should be a string", name) { |
| 78 | + return nil |
| 79 | + } |
| 80 | + var config map[string]interface{} |
| 81 | + if !assert.NoError(t, json.Unmarshal([]byte(configStr), &config), "%s config should be valid JSON", name) { |
| 82 | + return nil |
| 83 | + } |
| 84 | + return config |
| 85 | +} |
| 86 | + |
| 87 | +// otlpCollectBlockOf returns opentelemetry.collect.otlp from a parsed config, or nil if absent. |
| 88 | +func otlpCollectBlockOf(config map[string]interface{}) map[string]interface{} { |
| 89 | + otel, ok := config["opentelemetry"].(map[string]interface{}) |
| 90 | + if !ok { |
| 91 | + return nil |
| 92 | + } |
| 93 | + collect, ok := otel["collect"].(map[string]interface{}) |
| 94 | + if !ok { |
| 95 | + return nil |
| 96 | + } |
| 97 | + otlp, _ := collect["otlp"].(map[string]interface{}) |
| 98 | + return otlp |
| 99 | +} |
| 100 | + |
| 101 | +// validateNodeAgentOtlpReceiver checks the node agent's "default:otel" config carries the OTLP receiver. |
| 102 | +func validateNodeAgentOtlpReceiver(t *testing.T, agentMap map[string]unstructured.Unstructured) { |
| 103 | + config := configJSONOf(t, agentMap, "cloudwatch-agent") |
| 104 | + if config == nil { |
| 105 | + return |
| 106 | + } |
| 107 | + otlp := otlpCollectBlockOf(config) |
| 108 | + if !assert.NotNil(t, otlp, "node agent config should have opentelemetry.collect.otlp for default:otel") { |
| 109 | + return |
| 110 | + } |
| 111 | + // Endpoints are pinned to 0.0.0.0 so the receiver accepts traffic from other pods (the agent |
| 112 | + // default binds loopback only). |
| 113 | + assert.Equal(t, "0.0.0.0:4317", otlp["grpc_endpoint"], "otlp grpc_endpoint should be 0.0.0.0:4317") |
| 114 | + assert.Equal(t, "0.0.0.0:4318", otlp["http_endpoint"], "otlp http_endpoint should be 0.0.0.0:4318") |
| 115 | + assert.Equal(t, true, otlp["span_metrics_enabled"], "otlp span_metrics_enabled should be true") |
| 116 | +} |
| 117 | + |
| 118 | +// validateNodeAgentDefaultConfigSurvives checks the OTLP receiver was MERGED onto the default config, |
| 119 | +// not substituted for it: the default Container Insights block (logs.metrics_collected.kubernetes) |
| 120 | +// must remain alongside it. |
| 121 | +func validateNodeAgentDefaultConfigSurvives(t *testing.T, agentMap map[string]unstructured.Unstructured) { |
| 122 | + config := configJSONOf(t, agentMap, "cloudwatch-agent") |
| 123 | + if config == nil { |
| 124 | + return |
| 125 | + } |
| 126 | + logs, ok := config["logs"].(map[string]interface{}) |
| 127 | + if !assert.True(t, ok, "node agent config should retain the default logs section") { |
| 128 | + return |
| 129 | + } |
| 130 | + metricsCollected, ok := logs["metrics_collected"].(map[string]interface{}) |
| 131 | + if !assert.True(t, ok, "node agent config should retain logs.metrics_collected") { |
| 132 | + return |
| 133 | + } |
| 134 | + _, hasKubernetes := metricsCollected["kubernetes"] |
| 135 | + assert.True(t, hasKubernetes, |
| 136 | + "default Container Insights (logs.metrics_collected.kubernetes) should survive the OTLP merge") |
| 137 | +} |
| 138 | + |
| 139 | +// validateClusterScraperNoOtlpReceiver checks the cluster-scraper (config: "default", not |
| 140 | +// "default:otel") does NOT get the OTLP receiver; it describes other workloads, not local ingest. |
| 141 | +func validateClusterScraperNoOtlpReceiver(t *testing.T, agentMap map[string]unstructured.Unstructured) { |
| 142 | + config := configJSONOf(t, agentMap, "cloudwatch-agent-cluster-scraper") |
| 143 | + if config == nil { |
| 144 | + return |
| 145 | + } |
| 146 | + assert.Nil(t, otlpCollectBlockOf(config), |
| 147 | + "cluster-scraper config should NOT have an OTLP receiver (config is plain default)") |
| 148 | +} |
0 commit comments