google.golang.org/grpc@v1.72.2/stats/opentelemetry/csm/pluginoption_test.go (about) 1 /* 2 * 3 * Copyright 2024 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19 package csm 20 21 import ( 22 "context" 23 "encoding/base64" 24 "fmt" 25 "net/url" 26 "os" 27 "testing" 28 "time" 29 30 "google.golang.org/grpc/internal/envconfig" 31 "google.golang.org/grpc/internal/grpctest" 32 "google.golang.org/grpc/metadata" 33 34 "github.com/google/go-cmp/cmp" 35 "go.opentelemetry.io/otel/attribute" 36 37 "google.golang.org/protobuf/proto" 38 "google.golang.org/protobuf/types/known/structpb" 39 ) 40 41 type s struct { 42 grpctest.Tester 43 } 44 45 func Test(t *testing.T) { 46 grpctest.RunSubTests(t, s{}) 47 } 48 49 var defaultTestTimeout = 5 * time.Second 50 51 // clearEnv unsets all the environment variables relevant to the csm 52 // pluginOption. 53 func clearEnv() { 54 os.Unsetenv(envconfig.XDSBootstrapFileContentEnv) 55 os.Unsetenv(envconfig.XDSBootstrapFileNameEnv) 56 57 os.Unsetenv("CSM_CANONICAL_SERVICE_NAME") 58 os.Unsetenv("CSM_WORKLOAD_NAME") 59 } 60 61 func (s) TestGetLabels(t *testing.T) { 62 clearEnv() 63 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 64 defer cancel() 65 cpo := newPluginOption(ctx) 66 67 tests := []struct { 68 name string 69 unsetHeader bool // Should trigger "unknown" labels 70 twoValues bool // Should trigger "unknown" labels 71 metadataExchangeLabels map[string]string 72 labelsWant map[string]string 73 }{ 74 { 75 name: "unset-labels", 76 metadataExchangeLabels: nil, 77 labelsWant: map[string]string{ 78 "csm.workload_canonical_service": "unknown", 79 "csm.mesh_id": "unknown", 80 81 "csm.remote_workload_type": "unknown", 82 "csm.remote_workload_canonical_service": "unknown", 83 }, 84 }, 85 { 86 name: "metadata-partially-set", 87 metadataExchangeLabels: map[string]string{ 88 "type": "not-gce-or-gke", 89 "ignore-this": "ignore-this", 90 }, 91 labelsWant: map[string]string{ 92 "csm.workload_canonical_service": "unknown", 93 "csm.mesh_id": "unknown", 94 95 "csm.remote_workload_type": "not-gce-or-gke", 96 "csm.remote_workload_canonical_service": "unknown", 97 }, 98 }, 99 { 100 name: "google-compute-engine", 101 metadataExchangeLabels: map[string]string{ // All of these labels get emitted when type is "gcp_compute_engine". 102 "type": "gcp_compute_engine", 103 "canonical_service": "canonical_service_val", 104 "project_id": "unique-id", 105 "location": "us-east", 106 "workload_name": "workload_name_val", 107 }, 108 labelsWant: map[string]string{ 109 "csm.workload_canonical_service": "unknown", 110 "csm.mesh_id": "unknown", 111 112 "csm.remote_workload_type": "gcp_compute_engine", 113 "csm.remote_workload_canonical_service": "canonical_service_val", 114 "csm.remote_workload_project_id": "unique-id", 115 "csm.remote_workload_location": "us-east", 116 "csm.remote_workload_name": "workload_name_val", 117 }, 118 }, 119 // unset should go to unknown, ignore GKE labels that are not relevant 120 // to GCE. 121 { 122 name: "google-compute-engine-labels-partially-set-with-extra", 123 metadataExchangeLabels: map[string]string{ 124 "type": "gcp_compute_engine", 125 "canonical_service": "canonical_service_val", 126 "project_id": "unique-id", 127 "location": "us-east", 128 // "workload_name": "", unset workload name - should become "unknown" 129 "namespace_name": "should-be-ignored", 130 "cluster_name": "should-be-ignored", 131 }, 132 labelsWant: map[string]string{ 133 "csm.workload_canonical_service": "unknown", 134 "csm.mesh_id": "unknown", 135 136 "csm.remote_workload_type": "gcp_compute_engine", 137 "csm.remote_workload_canonical_service": "canonical_service_val", 138 "csm.remote_workload_project_id": "unique-id", 139 "csm.remote_workload_location": "us-east", 140 "csm.remote_workload_name": "unknown", 141 }, 142 }, 143 { 144 name: "google-kubernetes-engine", 145 metadataExchangeLabels: map[string]string{ 146 "type": "gcp_kubernetes_engine", 147 "canonical_service": "canonical_service_val", 148 "project_id": "unique-id", 149 "namespace_name": "namespace_name_val", 150 "cluster_name": "cluster_name_val", 151 "location": "us-east", 152 "workload_name": "workload_name_val", 153 }, 154 labelsWant: map[string]string{ 155 "csm.workload_canonical_service": "unknown", 156 "csm.mesh_id": "unknown", 157 158 "csm.remote_workload_type": "gcp_kubernetes_engine", 159 "csm.remote_workload_canonical_service": "canonical_service_val", 160 "csm.remote_workload_project_id": "unique-id", 161 "csm.remote_workload_cluster_name": "cluster_name_val", 162 "csm.remote_workload_namespace_name": "namespace_name_val", 163 "csm.remote_workload_location": "us-east", 164 "csm.remote_workload_name": "workload_name_val", 165 }, 166 }, 167 { 168 name: "google-kubernetes-engine-labels-partially-set", 169 metadataExchangeLabels: map[string]string{ 170 "type": "gcp_kubernetes_engine", 171 "canonical_service": "canonical_service_val", 172 "project_id": "unique-id", 173 "namespace_name": "namespace_name_val", 174 // "cluster_name": "", cluster_name unset, should become "unknown" 175 "location": "us-east", 176 // "workload_name": "", workload_name unset, should become "unknown" 177 }, 178 labelsWant: map[string]string{ 179 "csm.workload_canonical_service": "unknown", 180 "csm.mesh_id": "unknown", 181 182 "csm.remote_workload_type": "gcp_kubernetes_engine", 183 "csm.remote_workload_canonical_service": "canonical_service_val", 184 "csm.remote_workload_project_id": "unique-id", 185 "csm.remote_workload_cluster_name": "unknown", 186 "csm.remote_workload_namespace_name": "namespace_name_val", 187 "csm.remote_workload_location": "us-east", 188 "csm.remote_workload_name": "unknown", 189 }, 190 }, 191 { 192 name: "unset-header", 193 metadataExchangeLabels: map[string]string{ 194 "type": "gcp_kubernetes_engine", 195 "canonical_service": "canonical_service_val", 196 "project_id": "unique-id", 197 "namespace_name": "namespace_name_val", 198 "cluster_name": "cluster_name_val", 199 "location": "us-east", 200 "workload_name": "workload_name_val", 201 }, 202 unsetHeader: true, 203 labelsWant: map[string]string{ 204 "csm.workload_canonical_service": "unknown", 205 "csm.mesh_id": "unknown", 206 207 "csm.remote_workload_type": "unknown", 208 "csm.remote_workload_canonical_service": "unknown", 209 }, 210 }, 211 { 212 name: "two-header-values", 213 metadataExchangeLabels: map[string]string{ 214 "type": "gcp_kubernetes_engine", 215 "canonical_service": "canonical_service_val", 216 "project_id": "unique-id", 217 "namespace_name": "namespace_name_val", 218 "cluster_name": "cluster_name_val", 219 "location": "us-east", 220 "workload_name": "workload_name_val", 221 }, 222 twoValues: true, 223 labelsWant: map[string]string{ 224 "csm.workload_canonical_service": "unknown", 225 "csm.mesh_id": "unknown", 226 227 "csm.remote_workload_type": "unknown", 228 "csm.remote_workload_canonical_service": "unknown", 229 }, 230 }, 231 } 232 for _, test := range tests { 233 t.Run(test.name, func(t *testing.T) { 234 pbLabels := &structpb.Struct{ 235 Fields: map[string]*structpb.Value{}, 236 } 237 for k, v := range test.metadataExchangeLabels { 238 pbLabels.Fields[k] = structpb.NewStringValue(v) 239 } 240 protoWireFormat, err := proto.Marshal(pbLabels) 241 if err != nil { 242 t.Fatalf("Error marshaling proto: %v", err) 243 } 244 metadataExchangeLabelsEncoded := base64.RawStdEncoding.EncodeToString(protoWireFormat) 245 md := metadata.New(map[string]string{ 246 metadataExchangeKey: metadataExchangeLabelsEncoded, 247 }) 248 if test.unsetHeader { 249 md.Delete(metadataExchangeKey) 250 } 251 if test.twoValues { 252 md.Append(metadataExchangeKey, "extra-val") 253 } 254 255 labelsGot := cpo.GetLabels(md) 256 if diff := cmp.Diff(labelsGot, test.labelsWant); diff != "" { 257 t.Fatalf("cpo.GetLabels returned unexpected value (-got, +want): %v", diff) 258 } 259 }) 260 } 261 } 262 263 // TestDetermineTargetCSM tests the helper function that determines whether a 264 // target is relevant to CSM or not, based off the rules outlined in design. 265 func (s) TestDetermineTargetCSM(t *testing.T) { 266 tests := []struct { 267 name string 268 target string 269 targetCSM bool 270 }{ 271 { 272 name: "dns:///localhost", 273 target: "normal-target-here", 274 targetCSM: false, 275 }, 276 { 277 name: "xds-no-authority", 278 target: "xds:///localhost", 279 targetCSM: true, 280 }, 281 { 282 name: "xds-traffic-director-authority", 283 target: "xds://traffic-director-global.xds.googleapis.com/localhost", 284 targetCSM: true, 285 }, 286 { 287 name: "xds-not-traffic-director-authority", 288 target: "xds://not-traffic-director-authority/localhost", 289 targetCSM: false, 290 }, 291 } 292 for _, test := range tests { 293 t.Run(test.name, func(t *testing.T) { 294 parsedTarget, err := url.Parse(test.target) 295 if err != nil { 296 t.Fatalf("test target %v failed to parse: %v", test.target, err) 297 } 298 if got := determineTargetCSM(parsedTarget); got != test.targetCSM { 299 t.Fatalf("cpo.determineTargetCSM(%v): got %v, want %v", test.target, got, test.targetCSM) 300 } 301 }) 302 } 303 } 304 305 // TestSetLabels tests the setting of labels, which snapshots the resource and 306 // environment. It mocks the resource and environment, and then calls into 307 // labels creation. It verifies to local labels created and metadata exchange 308 // labels emitted from the setLabels function. 309 func (s) TestSetLabels(t *testing.T) { 310 clearEnv() 311 tests := []struct { 312 name string 313 resourceKeyValues map[string]string 314 csmCanonicalServiceNamePopulated bool 315 csmWorkloadNamePopulated bool 316 meshIDPopulated bool 317 localLabelsWant map[string]string 318 metadataExchangeLabelsWant map[string]string 319 }{ 320 { 321 name: "no-type", 322 csmCanonicalServiceNamePopulated: true, 323 meshIDPopulated: true, 324 resourceKeyValues: map[string]string{}, 325 localLabelsWant: map[string]string{ 326 "csm.workload_canonical_service": "canonical_service_name_val", // env var populated so should be set. 327 "csm.mesh_id": "mesh_id", // env var populated so should be set. 328 }, 329 metadataExchangeLabelsWant: map[string]string{ 330 "type": "unknown", 331 "canonical_service": "canonical_service_name_val", // env var populated so should be set. 332 }, 333 }, 334 { 335 name: "gce", 336 csmWorkloadNamePopulated: true, 337 resourceKeyValues: map[string]string{ 338 "cloud.platform": "gcp_compute_engine", 339 // csm workload name is an env var 340 "cloud.availability_zone": "cloud_availability_zone_val", 341 "cloud.region": "should-be-ignored", // cloud.availability_zone takes precedence 342 "cloud.account.id": "cloud_account_id_val", 343 }, 344 localLabelsWant: map[string]string{ 345 "csm.workload_canonical_service": "unknown", 346 "csm.mesh_id": "unknown", 347 }, 348 metadataExchangeLabelsWant: map[string]string{ 349 "type": "gcp_compute_engine", 350 "canonical_service": "unknown", 351 "workload_name": "workload_name_val", 352 "location": "cloud_availability_zone_val", 353 "project_id": "cloud_account_id_val", 354 }, 355 }, 356 { 357 name: "gce-half-unset", 358 resourceKeyValues: map[string]string{ 359 "cloud.platform": "gcp_compute_engine", 360 // csm workload name is an env var 361 "cloud.availability_zone": "cloud_availability_zone_val", 362 "cloud.region": "should-be-ignored", // cloud.availability_zone takes precedence 363 }, 364 localLabelsWant: map[string]string{ 365 "csm.workload_canonical_service": "unknown", 366 "csm.mesh_id": "unknown", 367 }, 368 metadataExchangeLabelsWant: map[string]string{ 369 "type": "gcp_compute_engine", 370 "canonical_service": "unknown", 371 "workload_name": "unknown", 372 "location": "cloud_availability_zone_val", 373 "project_id": "unknown", 374 }, 375 }, 376 { 377 name: "gke", 378 resourceKeyValues: map[string]string{ 379 "cloud.platform": "gcp_kubernetes_engine", 380 // csm workload name is an env var 381 "cloud.region": "cloud_region_val", // availability_zone isn't present, so this should become location 382 "cloud.account.id": "cloud_account_id_val", 383 "k8s.namespace.name": "k8s_namespace_name_val", 384 "k8s.cluster.name": "k8s_cluster_name_val", 385 }, 386 localLabelsWant: map[string]string{ 387 "csm.workload_canonical_service": "unknown", 388 "csm.mesh_id": "unknown", 389 }, 390 metadataExchangeLabelsWant: map[string]string{ 391 "type": "gcp_kubernetes_engine", 392 "canonical_service": "unknown", 393 "workload_name": "unknown", 394 "location": "cloud_region_val", 395 "project_id": "cloud_account_id_val", 396 "namespace_name": "k8s_namespace_name_val", 397 "cluster_name": "k8s_cluster_name_val", 398 }, 399 }, 400 { 401 name: "gke-half-unset", 402 resourceKeyValues: map[string]string{ // unset should become unknown 403 "cloud.platform": "gcp_kubernetes_engine", 404 // csm workload name is an env var 405 "cloud.region": "cloud_region_val", // availability_zone isn't present, so this should become location 406 // "cloud.account.id": "", // unset - should become unknown 407 "k8s.namespace.name": "k8s_namespace_name_val", 408 // "k8s.cluster.name": "", // unset - should become unknown 409 }, 410 localLabelsWant: map[string]string{ 411 "csm.workload_canonical_service": "unknown", 412 "csm.mesh_id": "unknown", 413 }, 414 metadataExchangeLabelsWant: map[string]string{ 415 "type": "gcp_kubernetes_engine", 416 "canonical_service": "unknown", 417 "workload_name": "unknown", 418 "location": "cloud_region_val", 419 "project_id": "unknown", 420 "namespace_name": "k8s_namespace_name_val", 421 "cluster_name": "unknown", 422 }, 423 }, 424 } 425 for _, test := range tests { 426 t.Run(test.name, func(t *testing.T) { 427 func() { 428 if test.csmCanonicalServiceNamePopulated { 429 os.Setenv("CSM_CANONICAL_SERVICE_NAME", "canonical_service_name_val") 430 defer os.Unsetenv("CSM_CANONICAL_SERVICE_NAME") 431 } 432 if test.csmWorkloadNamePopulated { 433 os.Setenv("CSM_WORKLOAD_NAME", "workload_name_val") 434 defer os.Unsetenv("CSM_WORKLOAD_NAME") 435 } 436 if test.meshIDPopulated { 437 os.Setenv("CSM_MESH_ID", "mesh_id") 438 defer os.Unsetenv("CSM_MESH_ID") 439 } 440 var attributes []attribute.KeyValue 441 for k, v := range test.resourceKeyValues { 442 attributes = append(attributes, attribute.String(k, v)) 443 } 444 // Return the attributes configured as part of the test in place 445 // of reading from resource. 446 attrSet := attribute.NewSet(attributes...) 447 origGetAttrSet := getAttrSetFromResourceDetector 448 getAttrSetFromResourceDetector = func(context.Context) *attribute.Set { 449 return &attrSet 450 } 451 defer func() { getAttrSetFromResourceDetector = origGetAttrSet }() 452 453 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 454 defer cancel() 455 localLabelsGot, mdEncoded := constructMetadataFromEnv(ctx) 456 if diff := cmp.Diff(localLabelsGot, test.localLabelsWant); diff != "" { 457 t.Fatalf("constructMetadataFromEnv() want: %v, got %v", test.localLabelsWant, localLabelsGot) 458 } 459 460 verifyMetadataExchangeLabels(mdEncoded, test.metadataExchangeLabelsWant) 461 }() 462 }) 463 } 464 } 465 466 func verifyMetadataExchangeLabels(mdEncoded string, mdLabelsWant map[string]string) error { 467 protoWireFormat, err := base64.RawStdEncoding.DecodeString(mdEncoded) 468 if err != nil { 469 return fmt.Errorf("error base 64 decoding metadata val: %v", err) 470 } 471 spb := &structpb.Struct{} 472 if err := proto.Unmarshal(protoWireFormat, spb); err != nil { 473 return fmt.Errorf("error unmarshaling proto wire format: %v", err) 474 } 475 fields := spb.GetFields() 476 for k, v := range mdLabelsWant { 477 if val, ok := fields[k]; !ok { 478 if _, ok := val.GetKind().(*structpb.Value_StringValue); !ok { 479 return fmt.Errorf("struct value for key %v should be string type", k) 480 } 481 if val.GetStringValue() != v { 482 return fmt.Errorf("struct value for key %v got: %v, want %v", k, val.GetStringValue(), v) 483 } 484 } 485 } 486 if len(mdLabelsWant) != len(fields) { 487 return fmt.Errorf("len(mdLabelsWant) = %v, len(mdLabelsGot) = %v", len(mdLabelsWant), len(fields)) 488 } 489 return nil 490 }