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 }