istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/config/validation/envoyfilter/envoyfilter_test.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package envoyfilter
    16  
    17  import (
    18  	"strings"
    19  	"testing"
    20  
    21  	listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
    22  	hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
    23  	"google.golang.org/protobuf/proto"
    24  	"google.golang.org/protobuf/types/known/structpb"
    25  
    26  	networking "istio.io/api/networking/v1alpha3"
    27  	"istio.io/istio/pkg/config"
    28  	"istio.io/istio/pkg/test/util/assert"
    29  	"istio.io/istio/pkg/wellknown"
    30  )
    31  
    32  const (
    33  	// Config name for testing
    34  	someName = "foo"
    35  	// Config namespace for testing.
    36  	someNamespace = "bar"
    37  )
    38  
    39  func stringOrEmpty(v error) string {
    40  	if v == nil {
    41  		return ""
    42  	}
    43  	return v.Error()
    44  }
    45  
    46  func checkValidationMessage(t *testing.T, gotWarning Warning, gotError error, wantWarning string, wantError string) {
    47  	t.Helper()
    48  	if (gotError == nil) != (wantError == "") {
    49  		t.Fatalf("got err=%v but wanted err=%v", gotError, wantError)
    50  	}
    51  	if !strings.Contains(stringOrEmpty(gotError), wantError) {
    52  		t.Fatalf("got err=%v but wanted err=%v", gotError, wantError)
    53  	}
    54  
    55  	if (gotWarning == nil) != (wantWarning == "") {
    56  		t.Fatalf("got warning=%v but wanted warning=%v", gotWarning, wantWarning)
    57  	}
    58  	if !strings.Contains(stringOrEmpty(gotWarning), wantWarning) {
    59  		t.Fatalf("got warning=%v but wanted warning=%v", gotWarning, wantWarning)
    60  	}
    61  }
    62  
    63  func TestValidateEnvoyFilter(t *testing.T) {
    64  	tests := []struct {
    65  		name    string
    66  		in      proto.Message
    67  		error   string
    68  		warning string
    69  	}{
    70  		{name: "empty filters", in: &networking.EnvoyFilter{}, error: ""},
    71  		{name: "labels not defined in workload selector", in: &networking.EnvoyFilter{
    72  			WorkloadSelector: &networking.WorkloadSelector{},
    73  		}, error: "", warning: "Envoy filter: workload selector specified without labels, will be applied to all services in namespace"},
    74  		{name: "invalid applyTo", in: &networking.EnvoyFilter{
    75  			ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
    76  				{
    77  					ApplyTo: 0,
    78  				},
    79  			},
    80  		}, error: "Envoy filter: missing applyTo"},
    81  		{name: "nil patch", in: &networking.EnvoyFilter{
    82  			ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
    83  				{
    84  					ApplyTo: networking.EnvoyFilter_LISTENER,
    85  					Patch:   nil,
    86  				},
    87  			},
    88  		}, error: "Envoy filter: missing patch"},
    89  		{name: "invalid patch operation", in: &networking.EnvoyFilter{
    90  			ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
    91  				{
    92  					ApplyTo: networking.EnvoyFilter_LISTENER,
    93  					Patch:   &networking.EnvoyFilter_Patch{},
    94  				},
    95  			},
    96  		}, error: "Envoy filter: missing patch operation"},
    97  		{name: "nil patch value", in: &networking.EnvoyFilter{
    98  			ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
    99  				{
   100  					ApplyTo: networking.EnvoyFilter_LISTENER,
   101  					Patch: &networking.EnvoyFilter_Patch{
   102  						Operation: networking.EnvoyFilter_Patch_ADD,
   103  					},
   104  				},
   105  			},
   106  		}, error: "Envoy filter: missing patch value for non-remove operation"},
   107  		{name: "match with invalid regex", in: &networking.EnvoyFilter{
   108  			ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
   109  				{
   110  					ApplyTo: networking.EnvoyFilter_LISTENER,
   111  					Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
   112  						Proxy: &networking.EnvoyFilter_ProxyMatch{
   113  							ProxyVersion: "%#@~++==`24c234`",
   114  						},
   115  					},
   116  					Patch: &networking.EnvoyFilter_Patch{
   117  						Operation: networking.EnvoyFilter_Patch_REMOVE,
   118  					},
   119  				},
   120  			},
   121  		}, error: "Envoy filter: invalid regex for proxy version, [error parsing regexp: invalid nested repetition operator: `++`]"},
   122  		{name: "match with valid regex", in: &networking.EnvoyFilter{
   123  			ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
   124  				{
   125  					ApplyTo: networking.EnvoyFilter_LISTENER,
   126  					Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
   127  						Proxy: &networking.EnvoyFilter_ProxyMatch{
   128  							ProxyVersion: `release-1\.2-23434`,
   129  						},
   130  					},
   131  					Patch: &networking.EnvoyFilter_Patch{
   132  						Operation: networking.EnvoyFilter_Patch_REMOVE,
   133  					},
   134  				},
   135  			},
   136  		}, error: ""},
   137  		{name: "listener with invalid match", in: &networking.EnvoyFilter{
   138  			ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
   139  				{
   140  					ApplyTo: networking.EnvoyFilter_LISTENER,
   141  					Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
   142  						ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Cluster{
   143  							Cluster: &networking.EnvoyFilter_ClusterMatch{},
   144  						},
   145  					},
   146  					Patch: &networking.EnvoyFilter_Patch{
   147  						Operation: networking.EnvoyFilter_Patch_REMOVE,
   148  					},
   149  				},
   150  			},
   151  		}, error: "Envoy filter: applyTo for listener class objects cannot have non listener match"},
   152  		{name: "listener with invalid filter match", in: &networking.EnvoyFilter{
   153  			ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
   154  				{
   155  					ApplyTo: networking.EnvoyFilter_NETWORK_FILTER,
   156  					Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
   157  						ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
   158  							Listener: &networking.EnvoyFilter_ListenerMatch{
   159  								FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
   160  									Sni:    "124",
   161  									Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{},
   162  								},
   163  							},
   164  						},
   165  					},
   166  					Patch: &networking.EnvoyFilter_Patch{
   167  						Operation: networking.EnvoyFilter_Patch_REMOVE,
   168  					},
   169  				},
   170  			},
   171  		}, error: "Envoy filter: filter match has no name to match on"},
   172  		{name: "listener with sub filter match and invalid applyTo", in: &networking.EnvoyFilter{
   173  			ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
   174  				{
   175  					ApplyTo: networking.EnvoyFilter_NETWORK_FILTER,
   176  					Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
   177  						ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
   178  							Listener: &networking.EnvoyFilter_ListenerMatch{
   179  								FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
   180  									Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
   181  										Name:      "random",
   182  										SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{},
   183  									},
   184  								},
   185  							},
   186  						},
   187  					},
   188  					Patch: &networking.EnvoyFilter_Patch{
   189  						Operation: networking.EnvoyFilter_Patch_REMOVE,
   190  					},
   191  				},
   192  			},
   193  		}, error: "Envoy filter: subfilter match can be used with applyTo HTTP_FILTER only"},
   194  		{name: "listener with sub filter match and invalid filter name", in: &networking.EnvoyFilter{
   195  			ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
   196  				{
   197  					ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
   198  					Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
   199  						ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
   200  							Listener: &networking.EnvoyFilter_ListenerMatch{
   201  								FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
   202  									Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
   203  										Name:      "random",
   204  										SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{},
   205  									},
   206  								},
   207  							},
   208  						},
   209  					},
   210  					Patch: &networking.EnvoyFilter_Patch{
   211  						Operation: networking.EnvoyFilter_Patch_REMOVE,
   212  					},
   213  				},
   214  			},
   215  		}, error: "Envoy filter: subfilter match requires filter match with envoy.filters.network.http_connection_manager"},
   216  		{name: "listener with sub filter match and no sub filter name", in: &networking.EnvoyFilter{
   217  			ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
   218  				{
   219  					ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
   220  					Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
   221  						ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
   222  							Listener: &networking.EnvoyFilter_ListenerMatch{
   223  								FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
   224  									Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
   225  										Name:      wellknown.HTTPConnectionManager,
   226  										SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{},
   227  									},
   228  								},
   229  							},
   230  						},
   231  					},
   232  					Patch: &networking.EnvoyFilter_Patch{
   233  						Operation: networking.EnvoyFilter_Patch_REMOVE,
   234  					},
   235  				},
   236  			},
   237  		}, error: "Envoy filter: subfilter match has no name to match on"},
   238  		{name: "route configuration with invalid match", in: &networking.EnvoyFilter{
   239  			ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
   240  				{
   241  					ApplyTo: networking.EnvoyFilter_VIRTUAL_HOST,
   242  					Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
   243  						ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Cluster{
   244  							Cluster: &networking.EnvoyFilter_ClusterMatch{},
   245  						},
   246  					},
   247  					Patch: &networking.EnvoyFilter_Patch{
   248  						Operation: networking.EnvoyFilter_Patch_REMOVE,
   249  					},
   250  				},
   251  			},
   252  		}, error: "Envoy filter: applyTo for http route class objects cannot have non route configuration match"},
   253  		{name: "cluster with invalid match", in: &networking.EnvoyFilter{
   254  			ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
   255  				{
   256  					ApplyTo: networking.EnvoyFilter_CLUSTER,
   257  					Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
   258  						ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
   259  							Listener: &networking.EnvoyFilter_ListenerMatch{},
   260  						},
   261  					},
   262  					Patch: &networking.EnvoyFilter_Patch{
   263  						Operation: networking.EnvoyFilter_Patch_REMOVE,
   264  					},
   265  				},
   266  			},
   267  		}, error: "Envoy filter: applyTo for cluster class objects cannot have non cluster match"},
   268  		{name: "invalid patch value", in: &networking.EnvoyFilter{
   269  			ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
   270  				{
   271  					ApplyTo: networking.EnvoyFilter_CLUSTER,
   272  					Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
   273  						ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Cluster{
   274  							Cluster: &networking.EnvoyFilter_ClusterMatch{},
   275  						},
   276  					},
   277  					Patch: &networking.EnvoyFilter_Patch{
   278  						Operation: networking.EnvoyFilter_Patch_ADD,
   279  						Value: &structpb.Struct{
   280  							Fields: map[string]*structpb.Value{
   281  								"name": {
   282  									Kind: &structpb.Value_BoolValue{BoolValue: false},
   283  								},
   284  							},
   285  						},
   286  					},
   287  				},
   288  			},
   289  		}, error: `Envoy filter: json: cannot unmarshal bool into Go value of type string`},
   290  		{name: "happy config", in: &networking.EnvoyFilter{
   291  			ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
   292  				{
   293  					ApplyTo: networking.EnvoyFilter_CLUSTER,
   294  					Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
   295  						ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Cluster{
   296  							Cluster: &networking.EnvoyFilter_ClusterMatch{},
   297  						},
   298  					},
   299  					Patch: &networking.EnvoyFilter_Patch{
   300  						Operation: networking.EnvoyFilter_Patch_ADD,
   301  						Value: &structpb.Struct{
   302  							Fields: map[string]*structpb.Value{
   303  								"lb_policy": {
   304  									Kind: &structpb.Value_StringValue{StringValue: "RING_HASH"},
   305  								},
   306  							},
   307  						},
   308  					},
   309  				},
   310  				{
   311  					ApplyTo: networking.EnvoyFilter_NETWORK_FILTER,
   312  					Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
   313  						ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
   314  							Listener: &networking.EnvoyFilter_ListenerMatch{
   315  								FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
   316  									Name: "envoy.tcp_proxy",
   317  								},
   318  							},
   319  						},
   320  					},
   321  					Patch: &networking.EnvoyFilter_Patch{
   322  						Operation: networking.EnvoyFilter_Patch_INSERT_BEFORE,
   323  						Value: &structpb.Struct{
   324  							Fields: map[string]*structpb.Value{
   325  								"typed_config": {
   326  									Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{
   327  										Fields: map[string]*structpb.Value{
   328  											"@type": {
   329  												Kind: &structpb.Value_StringValue{
   330  													StringValue: "type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz",
   331  												},
   332  											},
   333  										},
   334  									}},
   335  								},
   336  							},
   337  						},
   338  					},
   339  				},
   340  				{
   341  					ApplyTo: networking.EnvoyFilter_NETWORK_FILTER,
   342  					Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
   343  						ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
   344  							Listener: &networking.EnvoyFilter_ListenerMatch{
   345  								FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
   346  									Name: "envoy.tcp_proxy",
   347  								},
   348  							},
   349  						},
   350  					},
   351  					Patch: &networking.EnvoyFilter_Patch{
   352  						Operation: networking.EnvoyFilter_Patch_INSERT_FIRST,
   353  						Value: &structpb.Struct{
   354  							Fields: map[string]*structpb.Value{
   355  								"typed_config": {
   356  									Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{
   357  										Fields: map[string]*structpb.Value{
   358  											"@type": {
   359  												Kind: &structpb.Value_StringValue{
   360  													StringValue: "type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz",
   361  												},
   362  											},
   363  										},
   364  									}},
   365  								},
   366  							},
   367  						},
   368  					},
   369  				},
   370  			},
   371  		}, error: ""},
   372  		{name: "deprecated config", in: &networking.EnvoyFilter{
   373  			ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
   374  				{
   375  					ApplyTo: networking.EnvoyFilter_NETWORK_FILTER,
   376  					Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
   377  						ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
   378  							Listener: &networking.EnvoyFilter_ListenerMatch{
   379  								FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
   380  									Name: "envoy.tcp_proxy",
   381  								},
   382  							},
   383  						},
   384  					},
   385  					Patch: &networking.EnvoyFilter_Patch{
   386  						Operation: networking.EnvoyFilter_Patch_INSERT_FIRST,
   387  						Value: &structpb.Struct{
   388  							Fields: map[string]*structpb.Value{
   389  								"typed_config": {
   390  									Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{
   391  										Fields: map[string]*structpb.Value{
   392  											"@type": {
   393  												Kind: &structpb.Value_StringValue{
   394  													StringValue: "type.googleapis.com/envoy.config.filter.network.ext_authz.v2.ExtAuthz",
   395  												},
   396  											},
   397  										},
   398  									}},
   399  								},
   400  							},
   401  						},
   402  					},
   403  				},
   404  			},
   405  		}, error: "referenced type unknown (hint: try using the v3 XDS API)"},
   406  		{name: "deprecated type", in: &networking.EnvoyFilter{
   407  			ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
   408  				{
   409  					ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
   410  					Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
   411  						ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
   412  							Listener: &networking.EnvoyFilter_ListenerMatch{
   413  								FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
   414  									Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
   415  										Name: "envoy.http_connection_manager",
   416  									},
   417  								},
   418  							},
   419  						},
   420  					},
   421  					Patch: &networking.EnvoyFilter_Patch{
   422  						Operation: networking.EnvoyFilter_Patch_INSERT_FIRST,
   423  						Value:     &structpb.Struct{},
   424  					},
   425  				},
   426  			},
   427  		}, error: "", warning: "using deprecated filter name"},
   428  		// Regression test for https://github.com/golang/protobuf/issues/1374
   429  		{name: "duration marshal", in: &networking.EnvoyFilter{
   430  			ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
   431  				{
   432  					ApplyTo: networking.EnvoyFilter_CLUSTER,
   433  					Patch: &networking.EnvoyFilter_Patch{
   434  						Operation: networking.EnvoyFilter_Patch_ADD,
   435  						Value: &structpb.Struct{
   436  							Fields: map[string]*structpb.Value{
   437  								"dns_refresh_rate": {
   438  									Kind: &structpb.Value_StringValue{
   439  										StringValue: "500ms",
   440  									},
   441  								},
   442  							},
   443  						},
   444  					},
   445  				},
   446  			},
   447  		}, error: "", warning: ""},
   448  	}
   449  	for _, tt := range tests {
   450  		t.Run(tt.name, func(t *testing.T) {
   451  			warn, err := validateEnvoyFilter(config.Config{
   452  				Meta: config.Meta{
   453  					Name:      someName,
   454  					Namespace: someNamespace,
   455  				},
   456  				Spec: tt.in,
   457  			}, Validation{})
   458  			checkValidationMessage(t, warn, err, tt.warning, tt.error)
   459  		})
   460  	}
   461  }
   462  
   463  func TestRecurseMissingTypedConfig(t *testing.T) {
   464  	good := &listener.Filter{
   465  		Name:       wellknown.TCPProxy,
   466  		ConfigType: &listener.Filter_TypedConfig{TypedConfig: nil},
   467  	}
   468  	ecds := &hcm.HttpFilter{
   469  		Name:       "something",
   470  		ConfigType: &hcm.HttpFilter_ConfigDiscovery{},
   471  	}
   472  	bad := &listener.Filter{
   473  		Name: wellknown.TCPProxy,
   474  	}
   475  	assert.Equal(t, recurseMissingTypedConfig(good.ProtoReflect()), []string{}, "typed config set")
   476  	assert.Equal(t, recurseMissingTypedConfig(ecds.ProtoReflect()), []string{}, "config discovery set")
   477  	assert.Equal(t, recurseMissingTypedConfig(bad.ProtoReflect()), []string{wellknown.TCPProxy}, "typed config not set")
   478  }