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  }