istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/model/extensions_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 model 16 17 import ( 18 "net/url" 19 "testing" 20 "time" 21 22 core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 23 wasmextensions "github.com/envoyproxy/go-control-plane/envoy/extensions/wasm/v3" 24 "google.golang.org/protobuf/types/known/durationpb" 25 26 extensions "istio.io/api/extensions/v1alpha1" 27 "istio.io/api/type/v1beta1" 28 "istio.io/istio/pilot/pkg/model/credentials" 29 "istio.io/istio/pilot/pkg/networking" 30 "istio.io/istio/pkg/cluster" 31 "istio.io/istio/pkg/config" 32 "istio.io/istio/pkg/test/util/assert" 33 ) 34 35 func TestBuildDataSource(t *testing.T) { 36 cases := []struct { 37 url string 38 wasmPlugin *extensions.WasmPlugin 39 40 expected *core.AsyncDataSource 41 }{ 42 { 43 url: "file://fake.wasm", 44 wasmPlugin: &extensions.WasmPlugin{ 45 Url: "file://fake.wasm", 46 }, 47 expected: &core.AsyncDataSource{ 48 Specifier: &core.AsyncDataSource_Local{ 49 Local: &core.DataSource{ 50 Specifier: &core.DataSource_Filename{ 51 Filename: "fake.wasm", 52 }, 53 }, 54 }, 55 }, 56 }, 57 { 58 url: "oci://ghcr.io/istio/fake-wasm:latest", 59 wasmPlugin: &extensions.WasmPlugin{ 60 Sha256: "fake-sha256", 61 }, 62 expected: &core.AsyncDataSource{ 63 Specifier: &core.AsyncDataSource_Remote{ 64 Remote: &core.RemoteDataSource{ 65 HttpUri: &core.HttpUri{ 66 Uri: "oci://ghcr.io/istio/fake-wasm:latest", 67 Timeout: durationpb.New(30 * time.Second), 68 HttpUpstreamType: &core.HttpUri_Cluster{ 69 Cluster: "_", 70 }, 71 }, 72 Sha256: "fake-sha256", 73 }, 74 }, 75 }, 76 }, 77 } 78 79 for _, tc := range cases { 80 t.Run("", func(t *testing.T) { 81 u, err := url.Parse(tc.url) 82 assert.NoError(t, err) 83 got := buildDataSource(u, tc.wasmPlugin) 84 assert.Equal(t, tc.expected, got) 85 }) 86 } 87 } 88 89 func TestBuildVMConfig(t *testing.T) { 90 cases := []struct { 91 desc string 92 vm *extensions.VmConfig 93 policy extensions.PullPolicy 94 expected *wasmextensions.PluginConfig_VmConfig 95 }{ 96 { 97 desc: "Build VMConfig without a base VMConfig", 98 vm: nil, 99 policy: extensions.PullPolicy_UNSPECIFIED_POLICY, 100 expected: &wasmextensions.PluginConfig_VmConfig{ 101 VmConfig: &wasmextensions.VmConfig{ 102 Runtime: defaultRuntime, 103 EnvironmentVariables: &wasmextensions.EnvironmentVariables{ 104 KeyValues: map[string]string{ 105 WasmSecretEnv: "secret-name", 106 WasmResourceVersionEnv: "dummy-resource-version", 107 }, 108 }, 109 }, 110 }, 111 }, 112 { 113 desc: "Build VMConfig on top of a base VMConfig", 114 vm: &extensions.VmConfig{ 115 Env: []*extensions.EnvVar{ 116 { 117 Name: "POD_NAME", 118 ValueFrom: extensions.EnvValueSource_HOST, 119 }, 120 { 121 Name: "ENV1", 122 Value: "VAL1", 123 }, 124 }, 125 }, 126 policy: extensions.PullPolicy_UNSPECIFIED_POLICY, 127 expected: &wasmextensions.PluginConfig_VmConfig{ 128 VmConfig: &wasmextensions.VmConfig{ 129 Runtime: defaultRuntime, 130 EnvironmentVariables: &wasmextensions.EnvironmentVariables{ 131 HostEnvKeys: []string{"POD_NAME"}, 132 KeyValues: map[string]string{ 133 "ENV1": "VAL1", 134 WasmSecretEnv: "secret-name", 135 WasmResourceVersionEnv: "dummy-resource-version", 136 }, 137 }, 138 }, 139 }, 140 }, 141 { 142 desc: "Build VMConfig with if-not-present pull policy", 143 vm: nil, 144 policy: extensions.PullPolicy_IfNotPresent, 145 expected: &wasmextensions.PluginConfig_VmConfig{ 146 VmConfig: &wasmextensions.VmConfig{ 147 Runtime: defaultRuntime, 148 EnvironmentVariables: &wasmextensions.EnvironmentVariables{ 149 KeyValues: map[string]string{ 150 WasmSecretEnv: "secret-name", 151 WasmPolicyEnv: extensions.PullPolicy_name[int32(extensions.PullPolicy_IfNotPresent)], 152 WasmResourceVersionEnv: "dummy-resource-version", 153 }, 154 }, 155 }, 156 }, 157 }, 158 } 159 160 for _, tc := range cases { 161 t.Run(tc.desc, func(t *testing.T) { 162 got := buildVMConfig(nil, "dummy-resource-version", &extensions.WasmPlugin{ 163 VmConfig: tc.vm, 164 ImagePullSecret: "secret-name", 165 ImagePullPolicy: tc.policy, 166 }) 167 assert.Equal(t, tc.expected, got) 168 }) 169 } 170 } 171 172 func TestToSecretName(t *testing.T) { 173 cases := []struct { 174 name string 175 namespace string 176 want string 177 wantResourceName string 178 wantResourceNamespace string 179 }{ 180 { 181 name: "sec", 182 namespace: "nm", 183 want: credentials.KubernetesSecretTypeURI + "nm/sec", 184 wantResourceName: "sec", 185 wantResourceNamespace: "nm", 186 }, 187 { 188 name: "nm/sec", 189 namespace: "nm", 190 want: credentials.KubernetesSecretTypeURI + "nm/sec", 191 wantResourceName: "sec", 192 wantResourceNamespace: "nm", 193 }, 194 { 195 name: "nm2/sec", 196 namespace: "nm", 197 // Makes sure we won't search namespace outside of nm (which is the WasmPlugin namespace). 198 want: credentials.KubernetesSecretTypeURI + "nm/sec", 199 wantResourceName: "sec", 200 wantResourceNamespace: "nm", 201 }, 202 { 203 name: credentials.KubernetesSecretTypeURI + "nm/sec", 204 namespace: "nm", 205 want: credentials.KubernetesSecretTypeURI + "nm/sec", 206 wantResourceName: "sec", 207 wantResourceNamespace: "nm", 208 }, 209 { 210 name: "kubernetes://nm2/sec", 211 namespace: "nm", 212 want: credentials.KubernetesSecretTypeURI + "nm/sec", 213 wantResourceName: "sec", 214 wantResourceNamespace: "nm", 215 }, 216 { 217 name: "kubernetes://sec", 218 namespace: "nm", 219 want: credentials.KubernetesSecretTypeURI + "nm/sec", 220 wantResourceName: "sec", 221 wantResourceNamespace: "nm", 222 }, 223 } 224 225 for _, tt := range cases { 226 t.Run(tt.name, func(t *testing.T) { 227 got := toSecretResourceName(tt.name, tt.namespace) 228 if got != tt.want { 229 t.Errorf("got secret name %q, want %q", got, tt.want) 230 } 231 sr, err := credentials.ParseResourceName(got, tt.namespace, cluster.ID("cluster"), cluster.ID("cluster")) 232 if err != nil { 233 t.Error(err) 234 } 235 if sr.Name != tt.wantResourceName { 236 t.Errorf("parse secret name got %v want %v", sr.Name, tt.name) 237 } 238 if sr.Namespace != tt.wantResourceNamespace { 239 t.Errorf("parse secret name got %v want %v", sr.Name, tt.name) 240 } 241 }) 242 } 243 } 244 245 func TestFailStrategy(t *testing.T) { 246 cases := []struct { 247 desc string 248 in *extensions.WasmPlugin 249 out bool 250 }{ 251 { 252 desc: "close", 253 in: &extensions.WasmPlugin{ 254 Url: "file://fake.wasm", 255 FailStrategy: extensions.FailStrategy_FAIL_CLOSE, 256 }, 257 out: false, 258 }, 259 { 260 desc: "open", 261 in: &extensions.WasmPlugin{ 262 Url: "file://fake.wasm", 263 FailStrategy: extensions.FailStrategy_FAIL_OPEN, 264 }, 265 out: true, 266 }, 267 } 268 for _, tc := range cases { 269 t.Run(tc.desc, func(t *testing.T) { 270 out := convertToWasmPluginWrapper(config.Config{Spec: tc.in}) 271 if out == nil { 272 t.Fatalf("must not get nil") 273 } 274 filter := out.BuildHTTPWasmFilter() 275 if out == nil { 276 t.Fatalf("filter can not be nil") 277 } 278 if got := filter.Config.FailOpen; got != tc.out { 279 t.Errorf("got %t, want %t", got, tc.out) 280 } 281 }) 282 } 283 } 284 285 func TestMatchListener(t *testing.T) { 286 cases := []struct { 287 desc string 288 wasmPlugin *WasmPluginWrapper 289 proxyLabels map[string]string 290 listenerInfo WasmPluginListenerInfo 291 want bool 292 }{ 293 { 294 desc: "match and selector are nil", 295 wasmPlugin: &WasmPluginWrapper{WasmPlugin: &extensions.WasmPlugin{Selector: nil, Match: nil}}, 296 proxyLabels: map[string]string{"a": "b", "c": "d"}, 297 listenerInfo: WasmPluginListenerInfo{ 298 Port: 1234, 299 Class: networking.ListenerClassSidecarInbound, 300 }, 301 want: true, 302 }, 303 { 304 desc: "only the workload selector is given", 305 wasmPlugin: &WasmPluginWrapper{WasmPlugin: &extensions.WasmPlugin{ 306 Selector: &v1beta1.WorkloadSelector{ 307 MatchLabels: map[string]string{"a": "b"}, 308 }, 309 Match: nil, 310 }}, 311 proxyLabels: map[string]string{"a": "b", "c": "d"}, 312 listenerInfo: WasmPluginListenerInfo{ 313 Port: 1234, 314 Class: networking.ListenerClassSidecarInbound, 315 }, 316 want: true, 317 }, 318 { 319 desc: "mismatched selector", 320 wasmPlugin: &WasmPluginWrapper{WasmPlugin: &extensions.WasmPlugin{ 321 Selector: &v1beta1.WorkloadSelector{ 322 MatchLabels: map[string]string{"e": "f"}, 323 }, 324 Match: nil, 325 }}, 326 proxyLabels: map[string]string{"a": "b", "c": "d"}, 327 listenerInfo: WasmPluginListenerInfo{ 328 Port: 1234, 329 Class: networking.ListenerClassSidecarInbound, 330 }, 331 want: false, 332 }, 333 { 334 desc: "default traffic selector value is matched with all the traffics", 335 wasmPlugin: &WasmPluginWrapper{WasmPlugin: &extensions.WasmPlugin{ 336 Selector: nil, 337 Match: []*extensions.WasmPlugin_TrafficSelector{ 338 {}, 339 }, 340 }}, 341 proxyLabels: map[string]string{"a": "b", "c": "d"}, 342 listenerInfo: WasmPluginListenerInfo{ 343 Port: 1234, 344 Class: networking.ListenerClassSidecarInbound, 345 }, 346 want: true, 347 }, 348 { 349 desc: "only workloadMode of the traffic selector is given", 350 wasmPlugin: &WasmPluginWrapper{WasmPlugin: &extensions.WasmPlugin{ 351 Selector: nil, 352 Match: []*extensions.WasmPlugin_TrafficSelector{ 353 { 354 Mode: v1beta1.WorkloadMode_SERVER, 355 Ports: nil, 356 }, 357 }, 358 }}, 359 proxyLabels: map[string]string{"a": "b", "c": "d"}, 360 listenerInfo: WasmPluginListenerInfo{ 361 Port: 1234, 362 Class: networking.ListenerClassSidecarInbound, 363 }, 364 want: true, 365 }, 366 { 367 desc: "workloadMode of the traffic selector and empty list of ports are given", 368 wasmPlugin: &WasmPluginWrapper{WasmPlugin: &extensions.WasmPlugin{ 369 Selector: nil, 370 Match: []*extensions.WasmPlugin_TrafficSelector{ 371 { 372 Mode: v1beta1.WorkloadMode_SERVER, 373 Ports: []*v1beta1.PortSelector{}, 374 }, 375 }, 376 }}, 377 proxyLabels: map[string]string{"a": "b", "c": "d"}, 378 listenerInfo: WasmPluginListenerInfo{ 379 Port: 1234, 380 Class: networking.ListenerClassSidecarInbound, 381 }, 382 want: true, 383 }, 384 { 385 desc: "workloadMode of the traffic selector and numbered port are given", 386 wasmPlugin: &WasmPluginWrapper{WasmPlugin: &extensions.WasmPlugin{ 387 Selector: nil, 388 Match: []*extensions.WasmPlugin_TrafficSelector{ 389 { 390 Mode: v1beta1.WorkloadMode_SERVER, 391 Ports: []*v1beta1.PortSelector{{Number: 1234}}, 392 }, 393 }, 394 }}, 395 proxyLabels: map[string]string{"a": "b", "c": "d"}, 396 listenerInfo: WasmPluginListenerInfo{ 397 Port: 1234, 398 Class: networking.ListenerClassSidecarInbound, 399 }, 400 want: true, 401 }, 402 { 403 desc: "workloadMode of the traffic selector and mismatched ports are given", 404 wasmPlugin: &WasmPluginWrapper{WasmPlugin: &extensions.WasmPlugin{ 405 Selector: nil, 406 Match: []*extensions.WasmPlugin_TrafficSelector{ 407 { 408 Mode: v1beta1.WorkloadMode_SERVER, 409 Ports: []*v1beta1.PortSelector{{Number: 1235}}, 410 }, 411 }, 412 }}, 413 proxyLabels: map[string]string{"a": "b", "c": "d"}, 414 listenerInfo: WasmPluginListenerInfo{ 415 Port: 1234, 416 Class: networking.ListenerClassSidecarInbound, 417 }, 418 want: false, 419 }, 420 { 421 desc: "traffic selector is matched, but workload selector is not matched", 422 wasmPlugin: &WasmPluginWrapper{WasmPlugin: &extensions.WasmPlugin{ 423 Selector: &v1beta1.WorkloadSelector{ 424 MatchLabels: map[string]string{"e": "f"}, 425 }, 426 Match: []*extensions.WasmPlugin_TrafficSelector{ 427 { 428 Mode: v1beta1.WorkloadMode_SERVER, 429 Ports: []*v1beta1.PortSelector{{Number: 1234}}, 430 }, 431 }, 432 }}, 433 proxyLabels: map[string]string{"a": "b", "c": "d"}, 434 listenerInfo: WasmPluginListenerInfo{ 435 Port: 1234, 436 Class: networking.ListenerClassSidecarInbound, 437 }, 438 want: false, 439 }, 440 { 441 desc: "outbound traffic is matched with workloadMode CLIENT", 442 wasmPlugin: &WasmPluginWrapper{WasmPlugin: &extensions.WasmPlugin{ 443 Selector: nil, 444 Match: []*extensions.WasmPlugin_TrafficSelector{ 445 { 446 Mode: v1beta1.WorkloadMode_CLIENT, 447 Ports: []*v1beta1.PortSelector{{Number: 1234}}, 448 }, 449 }, 450 }}, 451 proxyLabels: map[string]string{"a": "b", "c": "d"}, 452 listenerInfo: WasmPluginListenerInfo{ 453 Port: 1234, 454 Class: networking.ListenerClassSidecarOutbound, 455 }, 456 want: true, 457 }, 458 { 459 desc: "any traffic is matched with workloadMode CLIENT_AND_SERVER", 460 wasmPlugin: &WasmPluginWrapper{WasmPlugin: &extensions.WasmPlugin{ 461 Selector: nil, 462 Match: []*extensions.WasmPlugin_TrafficSelector{ 463 { 464 Mode: v1beta1.WorkloadMode_CLIENT_AND_SERVER, 465 Ports: []*v1beta1.PortSelector{{Number: 1234}}, 466 }, 467 }, 468 }}, 469 proxyLabels: map[string]string{"a": "b", "c": "d"}, 470 listenerInfo: WasmPluginListenerInfo{ 471 Port: 1234, 472 Class: networking.ListenerClassUndefined, 473 }, 474 want: true, 475 }, 476 { 477 desc: "gateway is matched with workloadMode CLIENT", 478 wasmPlugin: &WasmPluginWrapper{WasmPlugin: &extensions.WasmPlugin{ 479 Selector: nil, 480 Match: []*extensions.WasmPlugin_TrafficSelector{ 481 { 482 Mode: v1beta1.WorkloadMode_CLIENT, 483 Ports: []*v1beta1.PortSelector{{Number: 1234}}, 484 }, 485 }, 486 }}, 487 proxyLabels: map[string]string{"a": "b", "c": "d"}, 488 listenerInfo: WasmPluginListenerInfo{ 489 Port: 1234, 490 Class: networking.ListenerClassGateway, 491 }, 492 want: true, 493 }, 494 { 495 desc: "gateway is not matched with workloadMode SERVER", 496 wasmPlugin: &WasmPluginWrapper{WasmPlugin: &extensions.WasmPlugin{ 497 Selector: nil, 498 Match: []*extensions.WasmPlugin_TrafficSelector{ 499 { 500 Mode: v1beta1.WorkloadMode_SERVER, 501 Ports: []*v1beta1.PortSelector{{Number: 1234}}, 502 }, 503 }, 504 }}, 505 proxyLabels: map[string]string{"a": "b", "c": "d"}, 506 listenerInfo: WasmPluginListenerInfo{ 507 Port: 1234, 508 Class: networking.ListenerClassGateway, 509 }, 510 want: false, 511 }, 512 } 513 514 for _, tc := range cases { 515 t.Run(tc.desc, func(t *testing.T) { 516 opts := WorkloadPolicyMatcher{ 517 Namespace: "ns", 518 WorkloadLabels: tc.proxyLabels, 519 IsWaypoint: false, 520 } 521 got := tc.wasmPlugin.MatchListener(opts, tc.listenerInfo) 522 if tc.want != got { 523 t.Errorf("MatchListener got %v want %v", got, tc.want) 524 } 525 }) 526 } 527 }