google.golang.org/grpc@v1.72.2/balancer/rls/config_test.go (about)

     1  /*
     2   *
     3   * Copyright 2020 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 rls
    20  
    21  import (
    22  	"encoding/json"
    23  	"fmt"
    24  	"strings"
    25  	"testing"
    26  	"time"
    27  
    28  	_ "google.golang.org/grpc/balancer/grpclb"               // grpclb for config parsing.
    29  	_ "google.golang.org/grpc/internal/resolver/passthrough" // passthrough resolver.
    30  )
    31  
    32  // testEqual reports whether the lbCfgs a and b are equal. This is to be used
    33  // only from tests. This ignores the keyBuilderMap field because its internals
    34  // are not exported, and hence not possible to specify in the want section of
    35  // the test. This is fine because we already have tests to make sure that the
    36  // keyBuilder is parsed properly from the service config.
    37  func testEqual(a, b *lbConfig) bool {
    38  	return a.lookupService == b.lookupService &&
    39  		a.lookupServiceTimeout == b.lookupServiceTimeout &&
    40  		a.maxAge == b.maxAge &&
    41  		a.staleAge == b.staleAge &&
    42  		a.cacheSizeBytes == b.cacheSizeBytes &&
    43  		a.defaultTarget == b.defaultTarget &&
    44  		a.controlChannelServiceConfig == b.controlChannelServiceConfig &&
    45  		a.childPolicyName == b.childPolicyName &&
    46  		a.childPolicyTargetField == b.childPolicyTargetField &&
    47  		childPolicyConfigEqual(a.childPolicyConfig, b.childPolicyConfig)
    48  }
    49  
    50  // TestParseConfig verifies successful config parsing scenarios.
    51  func (s) TestParseConfig(t *testing.T) {
    52  	childPolicyTargetFieldVal, _ := json.Marshal(dummyChildPolicyTarget)
    53  	tests := []struct {
    54  		desc    string
    55  		input   []byte
    56  		wantCfg *lbConfig
    57  	}{
    58  		{
    59  			// This input validates a few cases:
    60  			// - A top-level unknown field should not fail.
    61  			// - An unknown field in routeLookupConfig proto should not fail.
    62  			// - lookupServiceTimeout is set to its default value, since it is not specified in the input.
    63  			// - maxAge is clamped to maxMaxAge if staleAge is not set.
    64  			// - staleAge is ignored because it is higher than maxAge in the input.
    65  			// - cacheSizeBytes is greater than the hard upper limit of 5MB
    66  			desc: "with transformations 1",
    67  			input: []byte(`{
    68  				"top-level-unknown-field": "unknown-value",
    69  				"routeLookupConfig": {
    70  					"unknown-field": "unknown-value",
    71  					"grpcKeybuilders": [{
    72  						"names": [{"service": "service", "method": "method"}],
    73  						"headers": [{"key": "k1", "names": ["v1"]}]
    74  					}],
    75  					"lookupService": ":///target",
    76  					"maxAge" : "500s",
    77  					"staleAge": "600s",
    78  					"cacheSizeBytes": 100000000,
    79  					"defaultTarget": "passthrough:///default"
    80  				},
    81  				"childPolicy": [
    82  					{"cds_experimental": {"Cluster": "my-fav-cluster"}},
    83  					{"unknown-policy": {"unknown-field": "unknown-value"}},
    84  					{"grpclb": {"childPolicy": [{"pickfirst": {}}]}}
    85  				],
    86  				"childPolicyConfigTargetFieldName": "serviceName"
    87  			}`),
    88  			wantCfg: &lbConfig{
    89  				lookupService:          ":///target",
    90  				lookupServiceTimeout:   10 * time.Second,  // This is the default value.
    91  				maxAge:                 500 * time.Second, // Max age is not clamped when stale age is set.
    92  				staleAge:               300 * time.Second, // StaleAge is clamped because it was higher than maxMaxAge.
    93  				cacheSizeBytes:         maxCacheSize,
    94  				defaultTarget:          "passthrough:///default",
    95  				childPolicyName:        "grpclb",
    96  				childPolicyTargetField: "serviceName",
    97  				childPolicyConfig: map[string]json.RawMessage{
    98  					"childPolicy": json.RawMessage(`[{"pickfirst": {}}]`),
    99  					"serviceName": json.RawMessage(childPolicyTargetFieldVal),
   100  				},
   101  			},
   102  		},
   103  		{
   104  			desc: "maxAge not clamped when staleAge is set",
   105  			input: []byte(`{
   106  				"routeLookupConfig": {
   107  					"grpcKeybuilders": [{
   108  						"names": [{"service": "service", "method": "method"}],
   109  						"headers": [{"key": "k1", "names": ["v1"]}]
   110  					}],
   111  					"lookupService": ":///target",
   112  					"maxAge" : "500s",
   113  					"staleAge": "200s",
   114  					"cacheSizeBytes": 100000000
   115  				},
   116  				"childPolicy": [
   117  					{"grpclb": {"childPolicy": [{"pickfirst": {}}]}}
   118  				],
   119  				"childPolicyConfigTargetFieldName": "serviceName"
   120  			}`),
   121  			wantCfg: &lbConfig{
   122  				lookupService:          ":///target",
   123  				lookupServiceTimeout:   10 * time.Second,  // This is the default value.
   124  				maxAge:                 500 * time.Second, // Max age is not clamped when stale age is set.
   125  				staleAge:               200 * time.Second, // This is stale age within maxMaxAge.
   126  				cacheSizeBytes:         maxCacheSize,
   127  				childPolicyName:        "grpclb",
   128  				childPolicyTargetField: "serviceName",
   129  				childPolicyConfig: map[string]json.RawMessage{
   130  					"childPolicy": json.RawMessage(`[{"pickfirst": {}}]`),
   131  					"serviceName": json.RawMessage(childPolicyTargetFieldVal),
   132  				},
   133  			},
   134  		},
   135  		{
   136  			desc: "maxAge clamped when staleAge is not set",
   137  			input: []byte(`{
   138  				"routeLookupConfig": {
   139  					"grpcKeybuilders": [{
   140  						"names": [{"service": "service", "method": "method"}],
   141  						"headers": [{"key": "k1", "names": ["v1"]}]
   142  					}],
   143  					"lookupService": ":///target",
   144  					"maxAge" : "500s",
   145  					"cacheSizeBytes": 100000000
   146  				},
   147  				"childPolicy": [
   148  					{"grpclb": {"childPolicy": [{"pickfirst": {}}]}}
   149  				],
   150  				"childPolicyConfigTargetFieldName": "serviceName"
   151  			}`),
   152  			wantCfg: &lbConfig{
   153  				lookupService:          ":///target",
   154  				lookupServiceTimeout:   10 * time.Second,  // This is the default value.
   155  				maxAge:                 300 * time.Second, // Max age is clamped when stale age is not set.
   156  				staleAge:               300 * time.Second,
   157  				cacheSizeBytes:         maxCacheSize,
   158  				childPolicyName:        "grpclb",
   159  				childPolicyTargetField: "serviceName",
   160  				childPolicyConfig: map[string]json.RawMessage{
   161  					"childPolicy": json.RawMessage(`[{"pickfirst": {}}]`),
   162  					"serviceName": json.RawMessage(childPolicyTargetFieldVal),
   163  				},
   164  			},
   165  		},
   166  		{
   167  			desc: "without transformations",
   168  			input: []byte(`{
   169  				"routeLookupConfig": {
   170  					"grpcKeybuilders": [{
   171  						"names": [{"service": "service", "method": "method"}],
   172  						"headers": [{"key": "k1", "names": ["v1"]}]
   173  					}],
   174  					"lookupService": "target",
   175  					"lookupServiceTimeout" : "100s",
   176  					"maxAge": "60s",
   177  					"staleAge" : "50s",
   178  					"cacheSizeBytes": 1000,
   179  					"defaultTarget": "passthrough:///default"
   180  				},
   181  				"routeLookupChannelServiceConfig": {"loadBalancingConfig": [{"grpclb": {"childPolicy": [{"pickfirst": {}}]}}]},
   182  				"childPolicy": [{"grpclb": {"childPolicy": [{"pickfirst": {}}]}}],
   183  				"childPolicyConfigTargetFieldName": "serviceName"
   184  			}`),
   185  			wantCfg: &lbConfig{
   186  				lookupService:               "target",
   187  				lookupServiceTimeout:        100 * time.Second,
   188  				maxAge:                      60 * time.Second,
   189  				staleAge:                    50 * time.Second,
   190  				cacheSizeBytes:              1000,
   191  				defaultTarget:               "passthrough:///default",
   192  				controlChannelServiceConfig: `{"loadBalancingConfig": [{"grpclb": {"childPolicy": [{"pickfirst": {}}]}}]}`,
   193  				childPolicyName:             "grpclb",
   194  				childPolicyTargetField:      "serviceName",
   195  				childPolicyConfig: map[string]json.RawMessage{
   196  					"childPolicy": json.RawMessage(`[{"pickfirst": {}}]`),
   197  					"serviceName": json.RawMessage(childPolicyTargetFieldVal),
   198  				},
   199  			},
   200  		},
   201  	}
   202  
   203  	builder := rlsBB{}
   204  	for _, test := range tests {
   205  		t.Run(test.desc, func(t *testing.T) {
   206  			lbCfg, err := builder.ParseConfig(test.input)
   207  			if err != nil || !testEqual(lbCfg.(*lbConfig), test.wantCfg) {
   208  				t.Errorf("ParseConfig(%s) = {%+v, %v}, want {%+v, nil}", string(test.input), lbCfg, err, test.wantCfg)
   209  			}
   210  		})
   211  	}
   212  }
   213  
   214  // TestParseConfigErrors verifies config parsing failure scenarios.
   215  func (s) TestParseConfigErrors(t *testing.T) {
   216  	tests := []struct {
   217  		desc    string
   218  		input   []byte
   219  		wantErr string
   220  	}{
   221  		{
   222  			desc:    "empty input",
   223  			input:   nil,
   224  			wantErr: "rls: json unmarshal failed for service config",
   225  		},
   226  		{
   227  			desc:    "bad json",
   228  			input:   []byte(`bad bad json`),
   229  			wantErr: "rls: json unmarshal failed for service config",
   230  		},
   231  		{
   232  			desc: "bad grpcKeyBuilder",
   233  			input: []byte(`{
   234  					"routeLookupConfig": {
   235  						"grpcKeybuilders": [{
   236  							"names": [{"service": "service", "method": "method"}],
   237  							"headers": [{"key": "k1", "requiredMatch": true, "names": ["v1"]}]
   238  						}]
   239  					}
   240  				}`),
   241  			wantErr: "rls: GrpcKeyBuilder in RouteLookupConfig has required_match field set",
   242  		},
   243  		{
   244  			desc: "empty lookup service",
   245  			input: []byte(`{
   246  					"routeLookupConfig": {
   247  						"grpcKeybuilders": [{
   248  							"names": [{"service": "service", "method": "method"}],
   249  							"headers": [{"key": "k1", "names": ["v1"]}]
   250  						}]
   251  					}
   252  				}`),
   253  			wantErr: "rls: empty lookup_service in route lookup config",
   254  		},
   255  		{
   256  			desc: "unregistered scheme in lookup service URI",
   257  			input: []byte(`{
   258  					"routeLookupConfig": {
   259  						"grpcKeybuilders": [{
   260  							"names": [{"service": "service", "method": "method"}],
   261  							"headers": [{"key": "k1", "names": ["v1"]}]
   262  						}],
   263  						"lookupService": "badScheme:///target"
   264  					}
   265  				}`),
   266  			wantErr: "rls: unregistered scheme in lookup_service",
   267  		},
   268  		{
   269  			desc: "invalid lookup service timeout",
   270  			input: []byte(`{
   271  				"routeLookupConfig": {
   272  					"grpcKeybuilders": [{
   273  						"names": [{"service": "service", "method": "method"}],
   274  						"headers": [{"key": "k1", "names": ["v1"]}]
   275  					}],
   276  					"lookupService": "passthrough:///target",
   277  					"lookupServiceTimeout" : "315576000001s"
   278  				}
   279  			}`),
   280  			wantErr: "google.protobuf.Duration value out of range",
   281  		},
   282  		{
   283  			desc: "invalid max age",
   284  			input: []byte(`{
   285  				"routeLookupConfig": {
   286  					"grpcKeybuilders": [{
   287  						"names": [{"service": "service", "method": "method"}],
   288  						"headers": [{"key": "k1", "names": ["v1"]}]
   289  					}],
   290  					"lookupService": "passthrough:///target",
   291  					"lookupServiceTimeout" : "10s",
   292  					"maxAge" : "315576000001s"
   293  				}
   294  			}`),
   295  			wantErr: "google.protobuf.Duration value out of range",
   296  		},
   297  		{
   298  			desc: "invalid stale age",
   299  			input: []byte(`{
   300  				"routeLookupConfig": {
   301  					"grpcKeybuilders": [{
   302  						"names": [{"service": "service", "method": "method"}],
   303  						"headers": [{"key": "k1", "names": ["v1"]}]
   304  					}],
   305  					"lookupService": "passthrough:///target",
   306  					"lookupServiceTimeout" : "10s",
   307  					"maxAge" : "10s",
   308  					"staleAge" : "315576000001s"
   309  				}
   310  			}`),
   311  			wantErr: "google.protobuf.Duration value out of range",
   312  		},
   313  		{
   314  			desc: "invalid max age stale age combo",
   315  			input: []byte(`{
   316  				"routeLookupConfig": {
   317  					"grpcKeybuilders": [{
   318  						"names": [{"service": "service", "method": "method"}],
   319  						"headers": [{"key": "k1", "names": ["v1"]}]
   320  					}],
   321  					"lookupService": "passthrough:///target",
   322  					"lookupServiceTimeout" : "10s",
   323  					"staleAge" : "10s"
   324  				}
   325  			}`),
   326  			wantErr: "rls: stale_age is set, but max_age is not in route lookup config",
   327  		},
   328  		{
   329  			desc: "cache_size_bytes field is not set",
   330  			input: []byte(`{
   331  				"routeLookupConfig": {
   332  					"grpcKeybuilders": [{
   333  						"names": [{"service": "service", "method": "method"}],
   334  						"headers": [{"key": "k1", "names": ["v1"]}]
   335  					}],
   336  					"lookupService": "passthrough:///target",
   337  					"lookupServiceTimeout" : "10s",
   338  					"maxAge": "30s",
   339  					"staleAge" : "25s",
   340  					"defaultTarget": "passthrough:///default"
   341  				},
   342  				"childPolicyConfigTargetFieldName": "serviceName"
   343  			}`),
   344  			wantErr: "rls: cache_size_bytes must be set to a non-zero value",
   345  		},
   346  		{
   347  			desc: "routeLookupChannelServiceConfig is not in service config format",
   348  			input: []byte(`{
   349  				"routeLookupConfig": {
   350  					"grpcKeybuilders": [{
   351  						"names": [{"service": "service", "method": "method"}],
   352  						"headers": [{"key": "k1", "names": ["v1"]}]
   353  					}],
   354  					"lookupService": "target",
   355  					"lookupServiceTimeout" : "100s",
   356  					"maxAge": "60s",
   357  					"staleAge" : "50s",
   358  					"cacheSizeBytes": 1000,
   359  					"defaultTarget": "passthrough:///default"
   360  				},
   361  				"routeLookupChannelServiceConfig": "unknown",
   362  				"childPolicy": [{"grpclb": {"childPolicy": [{"pickfirst": {}}]}}],
   363  				"childPolicyConfigTargetFieldName": "serviceName"
   364  			}`),
   365  			wantErr: "cannot unmarshal string into Go value of type grpc.jsonSC",
   366  		},
   367  		{
   368  			desc: "routeLookupChannelServiceConfig contains unknown LB policy",
   369  			input: []byte(`{
   370  				"routeLookupConfig": {
   371  					"grpcKeybuilders": [{
   372  						"names": [{"service": "service", "method": "method"}],
   373  						"headers": [{"key": "k1", "names": ["v1"]}]
   374  					}],
   375  					"lookupService": "target",
   376  					"lookupServiceTimeout" : "100s",
   377  					"maxAge": "60s",
   378  					"staleAge" : "50s",
   379  					"cacheSizeBytes": 1000,
   380  					"defaultTarget": "passthrough:///default"
   381  				},
   382  				"routeLookupChannelServiceConfig": {
   383  					"loadBalancingConfig": [{"not_a_balancer1": {} }, {"not_a_balancer2": {}}]
   384  				},
   385  				"childPolicy": [{"grpclb": {"childPolicy": [{"pickfirst": {}}]}}],
   386  				"childPolicyConfigTargetFieldName": "serviceName"
   387  			}`),
   388  			wantErr: "no supported policies found in config",
   389  		},
   390  		{
   391  			desc: "no child policy",
   392  			input: []byte(`{
   393  				"routeLookupConfig": {
   394  					"grpcKeybuilders": [{
   395  						"names": [{"service": "service", "method": "method"}],
   396  						"headers": [{"key": "k1", "names": ["v1"]}]
   397  					}],
   398  					"lookupService": "passthrough:///target",
   399  					"lookupServiceTimeout" : "10s",
   400  					"maxAge": "30s",
   401  					"staleAge" : "25s",
   402  					"cacheSizeBytes": 1000,
   403  					"defaultTarget": "passthrough:///default"
   404  				},
   405  				"childPolicyConfigTargetFieldName": "serviceName"
   406  			}`),
   407  			wantErr: "rls: invalid childPolicy config: no supported policies found",
   408  		},
   409  		{
   410  			desc: "no known child policy",
   411  			input: []byte(`{
   412  				"routeLookupConfig": {
   413  					"grpcKeybuilders": [{
   414  						"names": [{"service": "service", "method": "method"}],
   415  						"headers": [{"key": "k1", "names": ["v1"]}]
   416  					}],
   417  					"lookupService": "passthrough:///target",
   418  					"lookupServiceTimeout" : "10s",
   419  					"maxAge": "30s",
   420  					"staleAge" : "25s",
   421  					"cacheSizeBytes": 1000,
   422  					"defaultTarget": "passthrough:///default"
   423  				},
   424  				"childPolicy": [
   425  					{"cds_experimental": {"Cluster": "my-fav-cluster"}},
   426  					{"unknown-policy": {"unknown-field": "unknown-value"}}
   427  				],
   428  				"childPolicyConfigTargetFieldName": "serviceName"
   429  			}`),
   430  			wantErr: "rls: invalid childPolicy config: no supported policies found",
   431  		},
   432  		{
   433  			desc: "invalid child policy config - more than one entry in map",
   434  			input: []byte(`{
   435  				"routeLookupConfig": {
   436  					"grpcKeybuilders": [{
   437  						"names": [{"service": "service", "method": "method"}],
   438  						"headers": [{"key": "k1", "names": ["v1"]}]
   439  					}],
   440  					"lookupService": "passthrough:///target",
   441  					"lookupServiceTimeout" : "10s",
   442  					"maxAge": "30s",
   443  					"staleAge" : "25s",
   444  					"cacheSizeBytes": 1000,
   445  					"defaultTarget": "passthrough:///default"
   446  				},
   447  				"childPolicy": [
   448  					{
   449  						"cds_experimental": {"Cluster": "my-fav-cluster"},
   450  						"unknown-policy": {"unknown-field": "unknown-value"}
   451  					}
   452  				],
   453  				"childPolicyConfigTargetFieldName": "serviceName"
   454  			}`),
   455  			wantErr: "does not contain exactly 1 policy/config pair",
   456  		},
   457  		{
   458  			desc: "no childPolicyConfigTargetFieldName",
   459  			input: []byte(`{
   460  				"routeLookupConfig": {
   461  					"grpcKeybuilders": [{
   462  						"names": [{"service": "service", "method": "method"}],
   463  						"headers": [{"key": "k1", "names": ["v1"]}]
   464  					}],
   465  					"lookupService": "passthrough:///target",
   466  					"lookupServiceTimeout" : "10s",
   467  					"maxAge": "30s",
   468  					"staleAge" : "25s",
   469  					"cacheSizeBytes": 1000,
   470  					"defaultTarget": "passthrough:///default"
   471  				},
   472  				"childPolicy": [
   473  					{"cds_experimental": {"Cluster": "my-fav-cluster"}},
   474  					{"unknown-policy": {"unknown-field": "unknown-value"}},
   475  					{"grpclb": {}}
   476  				]
   477  			}`),
   478  			wantErr: "rls: childPolicyConfigTargetFieldName field is not set in service config",
   479  		},
   480  		{
   481  			desc: "child policy config validation failure",
   482  			input: []byte(`{
   483  				"routeLookupConfig": {
   484  					"grpcKeybuilders": [{
   485  						"names": [{"service": "service", "method": "method"}],
   486  						"headers": [{"key": "k1", "names": ["v1"]}]
   487  					}],
   488  					"lookupService": "passthrough:///target",
   489  					"lookupServiceTimeout" : "10s",
   490  					"maxAge": "30s",
   491  					"staleAge" : "25s",
   492  					"cacheSizeBytes": 1000,
   493  					"defaultTarget": "passthrough:///default"
   494  				},
   495  				"childPolicy": [
   496  					{"cds_experimental": {"Cluster": "my-fav-cluster"}},
   497  					{"unknown-policy": {"unknown-field": "unknown-value"}},
   498  					{"grpclb": {"childPolicy": "not-an-array"}}
   499  				],
   500  				"childPolicyConfigTargetFieldName": "serviceName"
   501  			}`),
   502  			wantErr: "rls: childPolicy config validation failed",
   503  		},
   504  	}
   505  
   506  	builder := rlsBB{}
   507  	for _, test := range tests {
   508  		t.Run(test.desc, func(t *testing.T) {
   509  			lbCfg, err := builder.ParseConfig(test.input)
   510  			if lbCfg != nil || !strings.Contains(fmt.Sprint(err), test.wantErr) {
   511  				t.Errorf("ParseConfig(%s) = {%+v, %v}, want {nil, %s}", string(test.input), lbCfg, err, test.wantErr)
   512  			}
   513  		})
   514  	}
   515  }