istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/wasm/convert_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 wasm 16 17 import ( 18 "errors" 19 "fmt" 20 "net/url" 21 "reflect" 22 "testing" 23 24 udpa "github.com/cncf/xds/go/udpa/type/v1" 25 core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 26 rbac "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3" 27 wasm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/wasm/v3" 28 v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/wasm/v3" 29 "github.com/envoyproxy/go-control-plane/pkg/conversion" 30 resource "github.com/envoyproxy/go-control-plane/pkg/resource/v3" 31 "google.golang.org/protobuf/proto" 32 anypb "google.golang.org/protobuf/types/known/anypb" 33 "google.golang.org/protobuf/types/known/emptypb" 34 "google.golang.org/protobuf/types/known/structpb" 35 36 "istio.io/istio/pilot/pkg/model" 37 "istio.io/istio/pilot/pkg/util/protoconv" 38 "istio.io/istio/pkg/config/xds" 39 ) 40 41 type mockCache struct { 42 wantSecret []byte 43 wantPolicy PullPolicy 44 } 45 46 func (c *mockCache) Get(downloadURL string, opts GetOptions) (string, error) { 47 url, _ := url.Parse(downloadURL) 48 query := url.Query() 49 50 module := query.Get("module") 51 errMsg := query.Get("error") 52 var err error 53 if errMsg != "" { 54 err = errors.New(errMsg) 55 } 56 if c.wantSecret != nil && !reflect.DeepEqual(c.wantSecret, opts.PullSecret) { 57 return "", fmt.Errorf("wrong secret for %v, got %q want %q", downloadURL, string(opts.PullSecret), c.wantSecret) 58 } 59 if c.wantPolicy != opts.PullPolicy { 60 return "", fmt.Errorf("wrong pull policy for %v, got %v want %v", downloadURL, opts.PullPolicy, c.wantPolicy) 61 } 62 63 return module, err 64 } 65 func (c *mockCache) Cleanup() {} 66 67 func messageToStruct(t *testing.T, m proto.Message) *structpb.Struct { 68 st, err := conversion.MessageToStruct(m) 69 if err != nil { 70 t.Fatal(err) 71 } 72 return st 73 } 74 75 func newStruct(t *testing.T, m map[string]any) *structpb.Struct { 76 st, err := structpb.NewStruct(m) 77 if err != nil { 78 t.Fatal(err) 79 } 80 return st 81 } 82 83 func messageToAnyWithTypeURL(t *testing.T, msg proto.Message, typeURL string) *anypb.Any { 84 b, err := proto.MarshalOptions{Deterministic: true}.Marshal(msg) 85 if err != nil { 86 t.Fatal(err) 87 } 88 return &anypb.Any{ 89 // nolint: staticcheck 90 TypeUrl: typeURL, 91 Value: b, 92 } 93 } 94 95 func TestWasmConvertWithWrongMessages(t *testing.T) { 96 cases := []struct { 97 name string 98 input []*anypb.Any 99 wantNack bool 100 }{ 101 { 102 name: "wrong typed config", 103 input: []*anypb.Any{protoconv.MessageToAny(&emptypb.Empty{})}, 104 wantNack: true, 105 }, 106 { 107 name: "wrong value in typed struct", 108 input: []*anypb.Any{protoconv.MessageToAny(&core.TypedExtensionConfig{ 109 Name: "wrong-input", 110 TypedConfig: protoconv.MessageToAny( 111 &udpa.TypedStruct{ 112 TypeUrl: xds.WasmHTTPFilterType, 113 Value: newStruct(t, map[string]any{"wrong": "value"}), 114 }, 115 ), 116 })}, 117 wantNack: true, 118 }, 119 { 120 name: "empty wasm in typed struct", 121 input: []*anypb.Any{protoconv.MessageToAny(&core.TypedExtensionConfig{ 122 Name: "wrong-input", 123 TypedConfig: protoconv.MessageToAny( 124 &udpa.TypedStruct{ 125 TypeUrl: xds.WasmHTTPFilterType, 126 Value: messageToStruct(t, &wasm.Wasm{}), 127 }, 128 ), 129 })}, 130 wantNack: true, 131 }, 132 { 133 name: "no remote and local code in wasm", 134 input: []*anypb.Any{protoconv.MessageToAny(&core.TypedExtensionConfig{ 135 Name: "wrong-input", 136 TypedConfig: protoconv.MessageToAny( 137 &udpa.TypedStruct{ 138 TypeUrl: xds.WasmHTTPFilterType, 139 Value: messageToStruct(t, &wasm.Wasm{ 140 Config: &v3.PluginConfig{ 141 Vm: &v3.PluginConfig_VmConfig{ 142 VmConfig: &v3.VmConfig{ 143 Code: &core.AsyncDataSource{}, 144 }, 145 }, 146 }, 147 }), 148 }, 149 ), 150 })}, 151 wantNack: true, 152 }, 153 { 154 name: "empty wasm in typed extension config", 155 input: []*anypb.Any{protoconv.MessageToAny(&core.TypedExtensionConfig{ 156 Name: "wrong-input", 157 TypedConfig: protoconv.MessageToAny(&wasm.Wasm{}), 158 })}, 159 wantNack: true, 160 }, 161 { 162 name: "wrong wasm filter value", 163 input: []*anypb.Any{protoconv.MessageToAny(&core.TypedExtensionConfig{ 164 Name: "wrong-input", 165 TypedConfig: messageToAnyWithTypeURL(t, &v3.VmConfig{Runtime: "test", VmId: "test"}, xds.WasmHTTPFilterType), 166 })}, 167 wantNack: true, 168 }, 169 { 170 name: "wrong typed struct value", 171 input: []*anypb.Any{protoconv.MessageToAny(&core.TypedExtensionConfig{ 172 Name: "wrong-input", 173 TypedConfig: messageToAnyWithTypeURL(t, &v3.VmConfig{Runtime: "test", VmId: "test"}, xds.TypedStructType), 174 })}, 175 wantNack: true, 176 }, 177 } 178 179 for _, tc := range cases { 180 t.Run(tc.name, func(t *testing.T) { 181 mc := &mockCache{} 182 gotErr := MaybeConvertWasmExtensionConfig(tc.input, mc) 183 if gotErr == nil { 184 t.Errorf("wasm config conversion should return error, but did not") 185 } 186 }) 187 } 188 } 189 190 func TestWasmConvert(t *testing.T) { 191 cases := []struct { 192 name string 193 input []*core.TypedExtensionConfig 194 wantOutput []*core.TypedExtensionConfig 195 wantErr bool 196 }{ 197 { 198 name: "nil typed config ", 199 input: []*core.TypedExtensionConfig{ 200 extensionConfigMap["nil-typed-config"], 201 }, 202 wantOutput: []*core.TypedExtensionConfig{ 203 extensionConfigMap["nil-typed-config"], 204 }, 205 wantErr: true, 206 }, 207 { 208 name: "remote load success", 209 input: []*core.TypedExtensionConfig{ 210 extensionConfigMap["remote-load-success"], 211 }, 212 wantOutput: []*core.TypedExtensionConfig{ 213 extensionConfigMap["remote-load-success-local-file"], 214 }, 215 wantErr: false, 216 }, 217 { 218 name: "remote load success without typed struct", 219 input: []*core.TypedExtensionConfig{ 220 extensionConfigMap["remote-load-success-without-typed-struct"], 221 }, 222 wantOutput: []*core.TypedExtensionConfig{ 223 extensionConfigMap["remote-load-success-local-file"], 224 }, 225 wantErr: false, 226 }, 227 { 228 name: "remote load fail", 229 input: []*core.TypedExtensionConfig{ 230 extensionConfigMap["remote-load-fail"], 231 }, 232 wantOutput: []*core.TypedExtensionConfig{ 233 extensionConfigMap["remote-load-fail"], 234 }, 235 wantErr: true, 236 }, 237 { 238 name: "mix", 239 input: []*core.TypedExtensionConfig{ 240 extensionConfigMap["remote-load-fail"], 241 extensionConfigMap["remote-load-success"], 242 }, 243 wantOutput: []*core.TypedExtensionConfig{ 244 extensionConfigMap["remote-load-fail"], 245 extensionConfigMap["remote-load-success-local-file"], 246 }, 247 wantErr: true, 248 }, 249 { 250 name: "remote load fail open", 251 input: []*core.TypedExtensionConfig{ 252 extensionConfigMap["remote-load-fail-open"], 253 }, 254 wantOutput: []*core.TypedExtensionConfig{ 255 extensionConfigMap["remote-load-allow"], 256 }, 257 wantErr: false, 258 }, 259 { 260 name: "no typed struct", 261 input: []*core.TypedExtensionConfig{ 262 extensionConfigMap["empty"], 263 }, 264 wantOutput: []*core.TypedExtensionConfig{ 265 extensionConfigMap["empty"], 266 }, 267 wantErr: false, 268 }, 269 { 270 name: "no wasm", 271 input: []*core.TypedExtensionConfig{ 272 extensionConfigMap["no-wasm"], 273 }, 274 wantOutput: []*core.TypedExtensionConfig{ 275 extensionConfigMap["no-wasm"], 276 }, 277 wantErr: false, 278 }, 279 { 280 name: "no remote load", 281 input: []*core.TypedExtensionConfig{ 282 extensionConfigMap["no-remote-load"], 283 }, 284 wantOutput: []*core.TypedExtensionConfig{ 285 extensionConfigMap["no-remote-load"], 286 }, 287 wantErr: false, 288 }, 289 { 290 name: "no uri", 291 input: []*core.TypedExtensionConfig{ 292 extensionConfigMap["no-http-uri"], 293 }, 294 wantOutput: []*core.TypedExtensionConfig{ 295 extensionConfigMap["no-http-uri"], 296 }, 297 wantErr: true, 298 }, 299 { 300 name: "secret", 301 input: []*core.TypedExtensionConfig{ 302 extensionConfigMap["remote-load-secret"], 303 }, 304 wantOutput: []*core.TypedExtensionConfig{ 305 extensionConfigMap["remote-load-success-local-file"], 306 }, 307 wantErr: false, 308 }, 309 } 310 311 for _, c := range cases { 312 t.Run(c.name, func(t *testing.T) { 313 resources := make([]*anypb.Any, 0, len(c.input)) 314 for _, i := range c.input { 315 resources = append(resources, protoconv.MessageToAny(i)) 316 } 317 mc := &mockCache{} 318 gotErr := MaybeConvertWasmExtensionConfig(resources, mc) 319 if len(resources) != len(c.wantOutput) { 320 t.Fatalf("wasm config conversion number of configuration got %v want %v", len(resources), len(c.wantOutput)) 321 } 322 for i, output := range resources { 323 ec := &core.TypedExtensionConfig{} 324 if err := output.UnmarshalTo(ec); err != nil { 325 t.Errorf("wasm config conversion output %v failed to unmarshal", output) 326 continue 327 } 328 if !proto.Equal(ec, c.wantOutput[i]) { 329 t.Errorf("wasm config conversion output index %d got %v want %v", i, ec, c.wantOutput[i]) 330 } 331 } 332 if c.wantErr && gotErr == nil { 333 t.Error("wasm config conversion fails to raise an error") 334 } else if !c.wantErr && gotErr != nil { 335 t.Errorf("wasm config conversion got unexpected error: %v", gotErr) 336 } 337 }) 338 } 339 } 340 341 func buildTypedStructExtensionConfig(name string, wasm *wasm.Wasm) *core.TypedExtensionConfig { 342 ws, _ := conversion.MessageToStruct(wasm) 343 return &core.TypedExtensionConfig{ 344 Name: name, 345 TypedConfig: protoconv.MessageToAny( 346 &udpa.TypedStruct{ 347 TypeUrl: xds.WasmHTTPFilterType, 348 Value: ws, 349 }, 350 ), 351 } 352 } 353 354 func buildAnyExtensionConfig(name string, msg proto.Message) *core.TypedExtensionConfig { 355 return &core.TypedExtensionConfig{ 356 Name: name, 357 TypedConfig: protoconv.MessageToAny(msg), 358 } 359 } 360 361 var extensionConfigMap = map[string]*core.TypedExtensionConfig{ 362 "nil-typed-config": { 363 Name: "nil-typed-config", 364 TypedConfig: nil, 365 }, 366 "empty": { 367 Name: "empty", 368 TypedConfig: protoconv.MessageToAny( 369 &structpb.Struct{}, 370 ), 371 }, 372 "no-wasm": { 373 Name: "no-wasm", 374 TypedConfig: protoconv.MessageToAny( 375 &udpa.TypedStruct{TypeUrl: resource.APITypePrefix + "sometype"}, 376 ), 377 }, 378 "no-remote-load": buildTypedStructExtensionConfig("no-remote-load", &wasm.Wasm{ 379 Config: &v3.PluginConfig{ 380 Vm: &v3.PluginConfig_VmConfig{ 381 VmConfig: &v3.VmConfig{ 382 Runtime: "envoy.wasm.runtime.null", 383 Code: &core.AsyncDataSource{Specifier: &core.AsyncDataSource_Local{ 384 Local: &core.DataSource{ 385 Specifier: &core.DataSource_InlineString{ 386 InlineString: "envoy.wasm.metadata_exchange", 387 }, 388 }, 389 }}, 390 }, 391 }, 392 }, 393 }), 394 "no-http-uri": buildTypedStructExtensionConfig("no-remote-load", &wasm.Wasm{ 395 Config: &v3.PluginConfig{ 396 Vm: &v3.PluginConfig_VmConfig{ 397 VmConfig: &v3.VmConfig{ 398 Code: &core.AsyncDataSource{Specifier: &core.AsyncDataSource_Remote{ 399 Remote: &core.RemoteDataSource{}, 400 }}, 401 }, 402 }, 403 }, 404 }), 405 "remote-load-success": buildTypedStructExtensionConfig("remote-load-success", &wasm.Wasm{ 406 Config: &v3.PluginConfig{ 407 Vm: &v3.PluginConfig_VmConfig{ 408 VmConfig: &v3.VmConfig{ 409 Code: &core.AsyncDataSource{Specifier: &core.AsyncDataSource_Remote{ 410 Remote: &core.RemoteDataSource{ 411 HttpUri: &core.HttpUri{ 412 Uri: "http://test?module=test.wasm", 413 }, 414 }, 415 }}, 416 }, 417 }, 418 }, 419 }), 420 "remote-load-success-local-file": buildAnyExtensionConfig("remote-load-success", &wasm.Wasm{ 421 Config: &v3.PluginConfig{ 422 Vm: &v3.PluginConfig_VmConfig{ 423 VmConfig: &v3.VmConfig{ 424 Code: &core.AsyncDataSource{Specifier: &core.AsyncDataSource_Local{ 425 Local: &core.DataSource{ 426 Specifier: &core.DataSource_Filename{ 427 Filename: "test.wasm", 428 }, 429 }, 430 }}, 431 }, 432 }, 433 }, 434 }), 435 "remote-load-success-without-typed-struct": buildAnyExtensionConfig("remote-load-success", &wasm.Wasm{ 436 Config: &v3.PluginConfig{ 437 Vm: &v3.PluginConfig_VmConfig{ 438 VmConfig: &v3.VmConfig{ 439 Code: &core.AsyncDataSource{Specifier: &core.AsyncDataSource_Remote{ 440 Remote: &core.RemoteDataSource{ 441 HttpUri: &core.HttpUri{ 442 Uri: "http://test?module=test.wasm", 443 }, 444 }, 445 }}, 446 }, 447 }, 448 }, 449 }), 450 "remote-load-fail": buildTypedStructExtensionConfig("remote-load-fail", &wasm.Wasm{ 451 Config: &v3.PluginConfig{ 452 Vm: &v3.PluginConfig_VmConfig{ 453 VmConfig: &v3.VmConfig{ 454 Code: &core.AsyncDataSource{Specifier: &core.AsyncDataSource_Remote{ 455 Remote: &core.RemoteDataSource{ 456 HttpUri: &core.HttpUri{ 457 Uri: "http://test?module=test.wasm&error=download-error", 458 }, 459 }, 460 }}, 461 }, 462 }, 463 }, 464 }), 465 "remote-load-fail-open": buildTypedStructExtensionConfig("remote-load-fail", &wasm.Wasm{ 466 Config: &v3.PluginConfig{ 467 Vm: &v3.PluginConfig_VmConfig{ 468 VmConfig: &v3.VmConfig{ 469 Code: &core.AsyncDataSource{Specifier: &core.AsyncDataSource_Remote{ 470 Remote: &core.RemoteDataSource{ 471 HttpUri: &core.HttpUri{ 472 Uri: "http://test?module=test.wasm&error=download-error", 473 }, 474 }, 475 }}, 476 }, 477 }, 478 FailOpen: true, 479 }, 480 }), 481 "remote-load-allow": buildAnyExtensionConfig("remote-load-fail", &rbac.RBAC{}), 482 "remote-load-secret": buildTypedStructExtensionConfig("remote-load-success", &wasm.Wasm{ 483 Config: &v3.PluginConfig{ 484 Vm: &v3.PluginConfig_VmConfig{ 485 VmConfig: &v3.VmConfig{ 486 Code: &core.AsyncDataSource{Specifier: &core.AsyncDataSource_Remote{ 487 Remote: &core.RemoteDataSource{ 488 HttpUri: &core.HttpUri{ 489 Uri: "http://test?module=test.wasm", 490 }, 491 }, 492 }}, 493 EnvironmentVariables: &v3.EnvironmentVariables{ 494 KeyValues: map[string]string{ 495 model.WasmSecretEnv: "secret", 496 }, 497 }, 498 }, 499 }, 500 }, 501 }), 502 }