google.golang.org/grpc@v1.62.1/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 set to maxMaxAge since the value is too large in the input.
    64  			// - staleAge is ignore 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:                 5 * time.Minute,  // This is max maxAge.
    92  				staleAge:               time.Duration(0), // StaleAge is ignore because it was higher than maxAge.
    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: "without transformations",
   105  			input: []byte(`{
   106  				"routeLookupConfig": {
   107  					"grpcKeybuilders": [{
   108  						"names": [{"service": "service", "method": "method"}],
   109  						"headers": [{"key": "k1", "names": ["v1"]}]
   110  					}],
   111  					"lookupService": "target",
   112  					"lookupServiceTimeout" : "100s",
   113  					"maxAge": "60s",
   114  					"staleAge" : "50s",
   115  					"cacheSizeBytes": 1000,
   116  					"defaultTarget": "passthrough:///default"
   117  				},
   118  				"routeLookupChannelServiceConfig": {"loadBalancingConfig": [{"grpclb": {"childPolicy": [{"pickfirst": {}}]}}]},
   119  				"childPolicy": [{"grpclb": {"childPolicy": [{"pickfirst": {}}]}}],
   120  				"childPolicyConfigTargetFieldName": "serviceName"
   121  			}`),
   122  			wantCfg: &lbConfig{
   123  				lookupService:               "target",
   124  				lookupServiceTimeout:        100 * time.Second,
   125  				maxAge:                      60 * time.Second,
   126  				staleAge:                    50 * time.Second,
   127  				cacheSizeBytes:              1000,
   128  				defaultTarget:               "passthrough:///default",
   129  				controlChannelServiceConfig: `{"loadBalancingConfig": [{"grpclb": {"childPolicy": [{"pickfirst": {}}]}}]}`,
   130  				childPolicyName:             "grpclb",
   131  				childPolicyTargetField:      "serviceName",
   132  				childPolicyConfig: map[string]json.RawMessage{
   133  					"childPolicy": json.RawMessage(`[{"pickfirst": {}}]`),
   134  					"serviceName": json.RawMessage(childPolicyTargetFieldVal),
   135  				},
   136  			},
   137  		},
   138  	}
   139  
   140  	builder := rlsBB{}
   141  	for _, test := range tests {
   142  		t.Run(test.desc, func(t *testing.T) {
   143  			lbCfg, err := builder.ParseConfig(test.input)
   144  			if err != nil || !testEqual(lbCfg.(*lbConfig), test.wantCfg) {
   145  				t.Errorf("ParseConfig(%s) = {%+v, %v}, want {%+v, nil}", string(test.input), lbCfg, err, test.wantCfg)
   146  			}
   147  		})
   148  	}
   149  }
   150  
   151  // TestParseConfigErrors verifies config parsing failure scenarios.
   152  func (s) TestParseConfigErrors(t *testing.T) {
   153  	tests := []struct {
   154  		desc    string
   155  		input   []byte
   156  		wantErr string
   157  	}{
   158  		{
   159  			desc:    "empty input",
   160  			input:   nil,
   161  			wantErr: "rls: json unmarshal failed for service config",
   162  		},
   163  		{
   164  			desc:    "bad json",
   165  			input:   []byte(`bad bad json`),
   166  			wantErr: "rls: json unmarshal failed for service config",
   167  		},
   168  		{
   169  			desc: "bad grpcKeyBuilder",
   170  			input: []byte(`{
   171  					"routeLookupConfig": {
   172  						"grpcKeybuilders": [{
   173  							"names": [{"service": "service", "method": "method"}],
   174  							"headers": [{"key": "k1", "requiredMatch": true, "names": ["v1"]}]
   175  						}]
   176  					}
   177  				}`),
   178  			wantErr: "rls: GrpcKeyBuilder in RouteLookupConfig has required_match field set",
   179  		},
   180  		{
   181  			desc: "empty lookup service",
   182  			input: []byte(`{
   183  					"routeLookupConfig": {
   184  						"grpcKeybuilders": [{
   185  							"names": [{"service": "service", "method": "method"}],
   186  							"headers": [{"key": "k1", "names": ["v1"]}]
   187  						}]
   188  					}
   189  				}`),
   190  			wantErr: "rls: empty lookup_service in route lookup config",
   191  		},
   192  		{
   193  			desc: "unregistered scheme in lookup service URI",
   194  			input: []byte(`{
   195  					"routeLookupConfig": {
   196  						"grpcKeybuilders": [{
   197  							"names": [{"service": "service", "method": "method"}],
   198  							"headers": [{"key": "k1", "names": ["v1"]}]
   199  						}],
   200  						"lookupService": "badScheme:///target"
   201  					}
   202  				}`),
   203  			wantErr: "rls: unregistered scheme in lookup_service",
   204  		},
   205  		{
   206  			desc: "invalid lookup service timeout",
   207  			input: []byte(`{
   208  				"routeLookupConfig": {
   209  					"grpcKeybuilders": [{
   210  						"names": [{"service": "service", "method": "method"}],
   211  						"headers": [{"key": "k1", "names": ["v1"]}]
   212  					}],
   213  					"lookupService": "passthrough:///target",
   214  					"lookupServiceTimeout" : "315576000001s"
   215  				}
   216  			}`),
   217  			wantErr: "google.protobuf.Duration value out of range",
   218  		},
   219  		{
   220  			desc: "invalid max age",
   221  			input: []byte(`{
   222  				"routeLookupConfig": {
   223  					"grpcKeybuilders": [{
   224  						"names": [{"service": "service", "method": "method"}],
   225  						"headers": [{"key": "k1", "names": ["v1"]}]
   226  					}],
   227  					"lookupService": "passthrough:///target",
   228  					"lookupServiceTimeout" : "10s",
   229  					"maxAge" : "315576000001s"
   230  				}
   231  			}`),
   232  			wantErr: "google.protobuf.Duration value out of range",
   233  		},
   234  		{
   235  			desc: "invalid stale age",
   236  			input: []byte(`{
   237  				"routeLookupConfig": {
   238  					"grpcKeybuilders": [{
   239  						"names": [{"service": "service", "method": "method"}],
   240  						"headers": [{"key": "k1", "names": ["v1"]}]
   241  					}],
   242  					"lookupService": "passthrough:///target",
   243  					"lookupServiceTimeout" : "10s",
   244  					"maxAge" : "10s",
   245  					"staleAge" : "315576000001s"
   246  				}
   247  			}`),
   248  			wantErr: "google.protobuf.Duration value out of range",
   249  		},
   250  		{
   251  			desc: "invalid max age stale age combo",
   252  			input: []byte(`{
   253  				"routeLookupConfig": {
   254  					"grpcKeybuilders": [{
   255  						"names": [{"service": "service", "method": "method"}],
   256  						"headers": [{"key": "k1", "names": ["v1"]}]
   257  					}],
   258  					"lookupService": "passthrough:///target",
   259  					"lookupServiceTimeout" : "10s",
   260  					"staleAge" : "10s"
   261  				}
   262  			}`),
   263  			wantErr: "rls: stale_age is set, but max_age is not in route lookup config",
   264  		},
   265  		{
   266  			desc: "cache_size_bytes field is not set",
   267  			input: []byte(`{
   268  				"routeLookupConfig": {
   269  					"grpcKeybuilders": [{
   270  						"names": [{"service": "service", "method": "method"}],
   271  						"headers": [{"key": "k1", "names": ["v1"]}]
   272  					}],
   273  					"lookupService": "passthrough:///target",
   274  					"lookupServiceTimeout" : "10s",
   275  					"maxAge": "30s",
   276  					"staleAge" : "25s",
   277  					"defaultTarget": "passthrough:///default"
   278  				},
   279  				"childPolicyConfigTargetFieldName": "serviceName"
   280  			}`),
   281  			wantErr: "rls: cache_size_bytes must be set to a non-zero value",
   282  		},
   283  		{
   284  			desc: "routeLookupChannelServiceConfig is not in service config format",
   285  			input: []byte(`{
   286  				"routeLookupConfig": {
   287  					"grpcKeybuilders": [{
   288  						"names": [{"service": "service", "method": "method"}],
   289  						"headers": [{"key": "k1", "names": ["v1"]}]
   290  					}],
   291  					"lookupService": "target",
   292  					"lookupServiceTimeout" : "100s",
   293  					"maxAge": "60s",
   294  					"staleAge" : "50s",
   295  					"cacheSizeBytes": 1000,
   296  					"defaultTarget": "passthrough:///default"
   297  				},
   298  				"routeLookupChannelServiceConfig": "unknown",
   299  				"childPolicy": [{"grpclb": {"childPolicy": [{"pickfirst": {}}]}}],
   300  				"childPolicyConfigTargetFieldName": "serviceName"
   301  			}`),
   302  			wantErr: "cannot unmarshal string into Go value of type grpc.jsonSC",
   303  		},
   304  		{
   305  			desc: "routeLookupChannelServiceConfig contains unknown LB policy",
   306  			input: []byte(`{
   307  				"routeLookupConfig": {
   308  					"grpcKeybuilders": [{
   309  						"names": [{"service": "service", "method": "method"}],
   310  						"headers": [{"key": "k1", "names": ["v1"]}]
   311  					}],
   312  					"lookupService": "target",
   313  					"lookupServiceTimeout" : "100s",
   314  					"maxAge": "60s",
   315  					"staleAge" : "50s",
   316  					"cacheSizeBytes": 1000,
   317  					"defaultTarget": "passthrough:///default"
   318  				},
   319  				"routeLookupChannelServiceConfig": {
   320  					"loadBalancingConfig": [{"not_a_balancer1": {} }, {"not_a_balancer2": {}}]
   321  				},
   322  				"childPolicy": [{"grpclb": {"childPolicy": [{"pickfirst": {}}]}}],
   323  				"childPolicyConfigTargetFieldName": "serviceName"
   324  			}`),
   325  			wantErr: "invalid loadBalancingConfig: no supported policies found",
   326  		},
   327  		{
   328  			desc: "no child policy",
   329  			input: []byte(`{
   330  				"routeLookupConfig": {
   331  					"grpcKeybuilders": [{
   332  						"names": [{"service": "service", "method": "method"}],
   333  						"headers": [{"key": "k1", "names": ["v1"]}]
   334  					}],
   335  					"lookupService": "passthrough:///target",
   336  					"lookupServiceTimeout" : "10s",
   337  					"maxAge": "30s",
   338  					"staleAge" : "25s",
   339  					"cacheSizeBytes": 1000,
   340  					"defaultTarget": "passthrough:///default"
   341  				},
   342  				"childPolicyConfigTargetFieldName": "serviceName"
   343  			}`),
   344  			wantErr: "rls: invalid childPolicy config: no supported policies found",
   345  		},
   346  		{
   347  			desc: "no known child policy",
   348  			input: []byte(`{
   349  				"routeLookupConfig": {
   350  					"grpcKeybuilders": [{
   351  						"names": [{"service": "service", "method": "method"}],
   352  						"headers": [{"key": "k1", "names": ["v1"]}]
   353  					}],
   354  					"lookupService": "passthrough:///target",
   355  					"lookupServiceTimeout" : "10s",
   356  					"maxAge": "30s",
   357  					"staleAge" : "25s",
   358  					"cacheSizeBytes": 1000,
   359  					"defaultTarget": "passthrough:///default"
   360  				},
   361  				"childPolicy": [
   362  					{"cds_experimental": {"Cluster": "my-fav-cluster"}},
   363  					{"unknown-policy": {"unknown-field": "unknown-value"}}
   364  				],
   365  				"childPolicyConfigTargetFieldName": "serviceName"
   366  			}`),
   367  			wantErr: "rls: invalid childPolicy config: no supported policies found",
   368  		},
   369  		{
   370  			desc: "invalid child policy config - more than one entry in map",
   371  			input: []byte(`{
   372  				"routeLookupConfig": {
   373  					"grpcKeybuilders": [{
   374  						"names": [{"service": "service", "method": "method"}],
   375  						"headers": [{"key": "k1", "names": ["v1"]}]
   376  					}],
   377  					"lookupService": "passthrough:///target",
   378  					"lookupServiceTimeout" : "10s",
   379  					"maxAge": "30s",
   380  					"staleAge" : "25s",
   381  					"cacheSizeBytes": 1000,
   382  					"defaultTarget": "passthrough:///default"
   383  				},
   384  				"childPolicy": [
   385  					{
   386  						"cds_experimental": {"Cluster": "my-fav-cluster"},
   387  						"unknown-policy": {"unknown-field": "unknown-value"}
   388  					}
   389  				],
   390  				"childPolicyConfigTargetFieldName": "serviceName"
   391  			}`),
   392  			wantErr: "does not contain exactly 1 policy/config pair",
   393  		},
   394  		{
   395  			desc: "no childPolicyConfigTargetFieldName",
   396  			input: []byte(`{
   397  				"routeLookupConfig": {
   398  					"grpcKeybuilders": [{
   399  						"names": [{"service": "service", "method": "method"}],
   400  						"headers": [{"key": "k1", "names": ["v1"]}]
   401  					}],
   402  					"lookupService": "passthrough:///target",
   403  					"lookupServiceTimeout" : "10s",
   404  					"maxAge": "30s",
   405  					"staleAge" : "25s",
   406  					"cacheSizeBytes": 1000,
   407  					"defaultTarget": "passthrough:///default"
   408  				},
   409  				"childPolicy": [
   410  					{"cds_experimental": {"Cluster": "my-fav-cluster"}},
   411  					{"unknown-policy": {"unknown-field": "unknown-value"}},
   412  					{"grpclb": {}}
   413  				]
   414  			}`),
   415  			wantErr: "rls: childPolicyConfigTargetFieldName field is not set in service config",
   416  		},
   417  		{
   418  			desc: "child policy config validation failure",
   419  			input: []byte(`{
   420  				"routeLookupConfig": {
   421  					"grpcKeybuilders": [{
   422  						"names": [{"service": "service", "method": "method"}],
   423  						"headers": [{"key": "k1", "names": ["v1"]}]
   424  					}],
   425  					"lookupService": "passthrough:///target",
   426  					"lookupServiceTimeout" : "10s",
   427  					"maxAge": "30s",
   428  					"staleAge" : "25s",
   429  					"cacheSizeBytes": 1000,
   430  					"defaultTarget": "passthrough:///default"
   431  				},
   432  				"childPolicy": [
   433  					{"cds_experimental": {"Cluster": "my-fav-cluster"}},
   434  					{"unknown-policy": {"unknown-field": "unknown-value"}},
   435  					{"grpclb": {"childPolicy": "not-an-array"}}
   436  				],
   437  				"childPolicyConfigTargetFieldName": "serviceName"
   438  			}`),
   439  			wantErr: "rls: childPolicy config validation failed",
   440  		},
   441  	}
   442  
   443  	builder := rlsBB{}
   444  	for _, test := range tests {
   445  		t.Run(test.desc, func(t *testing.T) {
   446  			lbCfg, err := builder.ParseConfig(test.input)
   447  			if lbCfg != nil || !strings.Contains(fmt.Sprint(err), test.wantErr) {
   448  				t.Errorf("ParseConfig(%s) = {%+v, %v}, want {nil, %s}", string(test.input), lbCfg, err, test.wantErr)
   449  			}
   450  		})
   451  	}
   452  }