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  }