istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/xds/ecds_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  package xds_test
    15  
    16  import (
    17  	"reflect"
    18  	"testing"
    19  	"time"
    20  
    21  	core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    22  	wasm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/wasm/v3"
    23  	discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
    24  	corev1 "k8s.io/api/core/v1"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"k8s.io/apimachinery/pkg/runtime"
    27  
    28  	extensions "istio.io/api/extensions/v1alpha1"
    29  	"istio.io/istio/pilot/pkg/model"
    30  	v3 "istio.io/istio/pilot/pkg/xds/v3"
    31  	"istio.io/istio/pilot/test/xds"
    32  	"istio.io/istio/pkg/config"
    33  	"istio.io/istio/pkg/config/constants"
    34  	"istio.io/istio/pkg/config/schema/gvk"
    35  	"istio.io/istio/pkg/config/schema/kind"
    36  	"istio.io/istio/pkg/spiffe"
    37  	"istio.io/istio/pkg/util/sets"
    38  )
    39  
    40  func TestECDS(t *testing.T) {
    41  	s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{
    42  		ConfigString: mustReadFile(t, "./testdata/ecds.yaml"),
    43  	})
    44  
    45  	ads := s.ConnectADS().WithType(v3.ExtensionConfigurationType)
    46  	wantExtensionConfigName := "extension-config"
    47  	md := model.NodeMetadata{
    48  		ClusterID: constants.DefaultClusterName,
    49  	}
    50  	res := ads.RequestResponseAck(t, &discovery.DiscoveryRequest{
    51  		Node: &core.Node{
    52  			Id:       ads.ID,
    53  			Metadata: md.ToStruct(),
    54  		},
    55  		ResourceNames: []string{wantExtensionConfigName},
    56  	})
    57  
    58  	var ec core.TypedExtensionConfig
    59  	err := res.Resources[0].UnmarshalTo(&ec)
    60  	if err != nil {
    61  		t.Fatal("Failed to unmarshal extension config", err)
    62  		return
    63  	}
    64  	if ec.Name != wantExtensionConfigName {
    65  		t.Errorf("extension config name got %v want %v", ec.Name, wantExtensionConfigName)
    66  	}
    67  }
    68  
    69  func makeDockerCredentials(name, namespace string, data map[string]string, secretType corev1.SecretType) *corev1.Secret {
    70  	bdata := map[string][]byte{}
    71  	for k, v := range data {
    72  		bdata[k] = []byte(v)
    73  	}
    74  	return &corev1.Secret{
    75  		ObjectMeta: metav1.ObjectMeta{
    76  			Name:      name,
    77  			Namespace: namespace,
    78  		},
    79  		Data: bdata,
    80  		Type: secretType,
    81  	}
    82  }
    83  
    84  func makeWasmPlugin(name, namespace, secret string) config.Config {
    85  	spec := &extensions.WasmPlugin{
    86  		Phase: extensions.PluginPhase_AUTHN,
    87  	}
    88  	if secret != "" {
    89  		spec.ImagePullSecret = secret
    90  	}
    91  	return config.Config{
    92  		Meta: config.Meta{
    93  			Name:             name,
    94  			Namespace:        namespace,
    95  			GroupVersionKind: gvk.WasmPlugin,
    96  		},
    97  		Spec: spec,
    98  	}
    99  }
   100  
   101  var (
   102  	// Secrets
   103  	defaultPullSecret = makeDockerCredentials("default-pull-secret", "default", map[string]string{
   104  		corev1.DockerConfigJsonKey: "default-docker-credential",
   105  	}, corev1.SecretTypeDockerConfigJson)
   106  	rootPullSecret = makeDockerCredentials("root-pull-secret", "istio-system", map[string]string{
   107  		corev1.DockerConfigJsonKey: "root-docker-credential",
   108  	}, corev1.SecretTypeDockerConfigJson)
   109  	wrongTypeSecret = makeDockerCredentials("wrong-type-pull-secret", "default", map[string]string{
   110  		corev1.DockerConfigJsonKey: "wrong-type-docker-credential",
   111  	}, corev1.SecretTypeTLS)
   112  
   113  	wasmPlugin             = makeWasmPlugin("default-plugin", "default", "")
   114  	wasmPluginWithSec      = makeWasmPlugin("default-plugin-with-sec", "default", "default-pull-secret")
   115  	wasmPluginWrongSec     = makeWasmPlugin("default-plugin-wrong-sec", "default", "wrong-secret")
   116  	wasmPluginWrongSecType = makeWasmPlugin("default-plugin-wrong-sec-type", "default", "wrong-type-pull-secret")
   117  	rootWasmPluginWithSec  = makeWasmPlugin("root-plugin", "istio-system", "root-pull-secret")
   118  )
   119  
   120  func TestECDSGenerate(t *testing.T) {
   121  	cases := []struct {
   122  		name             string
   123  		proxyNamespace   string
   124  		request          *model.PushRequest
   125  		watchedResources []string
   126  		wantExtensions   sets.String
   127  		wantSecrets      sets.String
   128  	}{
   129  		{
   130  			name:             "simple",
   131  			proxyNamespace:   "default",
   132  			request:          &model.PushRequest{Full: true},
   133  			watchedResources: []string{"extenstions.istio.io/wasmplugin/default.default-plugin"},
   134  			wantExtensions:   sets.String{"extenstions.istio.io/wasmplugin/default.default-plugin": {}},
   135  			wantSecrets:      sets.String{},
   136  		},
   137  		{
   138  			name:             "simple_with_secret",
   139  			proxyNamespace:   "default",
   140  			request:          &model.PushRequest{Full: true},
   141  			watchedResources: []string{"extenstions.istio.io/wasmplugin/default.default-plugin-with-sec"},
   142  			wantExtensions:   sets.String{"extenstions.istio.io/wasmplugin/default.default-plugin-with-sec": {}},
   143  			wantSecrets:      sets.String{"default-docker-credential": {}},
   144  		},
   145  		{
   146  			name:             "miss_secret",
   147  			proxyNamespace:   "default",
   148  			request:          &model.PushRequest{Full: true},
   149  			watchedResources: []string{"extenstions.istio.io/wasmplugin/default.default-plugin-wrong-sec"},
   150  			wantExtensions:   sets.String{"extenstions.istio.io/wasmplugin/default.default-plugin-wrong-sec": {}},
   151  			wantSecrets:      sets.String{},
   152  		},
   153  		{
   154  			name:             "wrong_secret_type",
   155  			proxyNamespace:   "default",
   156  			request:          &model.PushRequest{Full: true},
   157  			watchedResources: []string{"extenstions.istio.io/wasmplugin/default.default-plugin-wrong-sec-type"},
   158  			wantExtensions:   sets.String{"extenstions.istio.io/wasmplugin/default.default-plugin-wrong-sec-type": {}},
   159  			wantSecrets:      sets.String{},
   160  		},
   161  		{
   162  			name:           "root_and_default",
   163  			proxyNamespace: "default",
   164  			request:        &model.PushRequest{Full: true},
   165  			watchedResources: []string{
   166  				"extenstions.istio.io/wasmplugin/default.default-plugin-with-sec",
   167  				"extenstions.istio.io/wasmplugin/istio-system.root-plugin",
   168  			},
   169  			wantExtensions: sets.String{
   170  				"extenstions.istio.io/wasmplugin/default.default-plugin-with-sec": {},
   171  				"extenstions.istio.io/wasmplugin/istio-system.root-plugin":        {},
   172  			},
   173  			wantSecrets: sets.String{"default-docker-credential": {}, "root-docker-credential": {}},
   174  		},
   175  		{
   176  			name:             "only_root",
   177  			proxyNamespace:   "somenamespace",
   178  			request:          &model.PushRequest{Full: true},
   179  			watchedResources: []string{"extenstions.istio.io/wasmplugin/istio-system.root-plugin"},
   180  			wantExtensions:   sets.String{"extenstions.istio.io/wasmplugin/istio-system.root-plugin": {}},
   181  			wantSecrets:      sets.String{"root-docker-credential": {}},
   182  		},
   183  		{
   184  			name:           "no_relevant_config_update",
   185  			proxyNamespace: "default",
   186  			request: &model.PushRequest{
   187  				Full:           true,
   188  				ConfigsUpdated: sets.New(model.ConfigKey{Kind: kind.AuthorizationPolicy}),
   189  			},
   190  			watchedResources: []string{"extenstions.istio.io/wasmplugin/default.default-plugin-with-sec", "extenstions.istio.io/wasmplugin/istio-system.root-plugin"},
   191  			wantExtensions:   sets.String{},
   192  			wantSecrets:      sets.String{},
   193  		},
   194  		{
   195  			name:           "has_relevant_config_update",
   196  			proxyNamespace: "default",
   197  			request: &model.PushRequest{
   198  				Full:           true,
   199  				ConfigsUpdated: sets.New(model.ConfigKey{Kind: kind.AuthorizationPolicy}, model.ConfigKey{Kind: kind.WasmPlugin}),
   200  			},
   201  			watchedResources: []string{"extenstions.istio.io/wasmplugin/default.default-plugin-with-sec"},
   202  			wantExtensions:   sets.String{"extenstions.istio.io/wasmplugin/default.default-plugin-with-sec": {}},
   203  			wantSecrets:      sets.String{"default-docker-credential": {}},
   204  		},
   205  		{
   206  			name:           "non_relevant_secret_update",
   207  			proxyNamespace: "default",
   208  			request: &model.PushRequest{
   209  				Full:           true,
   210  				ConfigsUpdated: sets.New(model.ConfigKey{Kind: kind.AuthorizationPolicy}, model.ConfigKey{Kind: kind.Secret}),
   211  			},
   212  			watchedResources: []string{"extenstions.istio.io/wasmplugin/default.default-plugin-with-sec"},
   213  			wantExtensions:   sets.String{},
   214  			wantSecrets:      sets.String{},
   215  		},
   216  		{
   217  			name:           "non_relevant_secret_update_and_wasm_plugin",
   218  			proxyNamespace: "default",
   219  			request: &model.PushRequest{
   220  				Full:           true,
   221  				ConfigsUpdated: sets.New(model.ConfigKey{Kind: kind.WasmPlugin}, model.ConfigKey{Kind: kind.Secret}),
   222  			},
   223  			watchedResources: []string{"extenstions.istio.io/wasmplugin/default.default-plugin-with-sec"},
   224  			wantExtensions:   sets.String{"extenstions.istio.io/wasmplugin/default.default-plugin-with-sec": {}},
   225  			wantSecrets:      sets.String{"default-docker-credential": {}},
   226  		},
   227  		{
   228  			name:           "relevant_secret_update",
   229  			proxyNamespace: "default",
   230  			request: &model.PushRequest{
   231  				Full:           true,
   232  				ConfigsUpdated: sets.New(model.ConfigKey{Kind: kind.Secret, Name: "default-pull-secret", Namespace: "default"}),
   233  			},
   234  			watchedResources: []string{"extenstions.istio.io/wasmplugin/default.default-plugin-with-sec"},
   235  			wantExtensions:   sets.String{"extenstions.istio.io/wasmplugin/default.default-plugin-with-sec": {}},
   236  			wantSecrets:      sets.String{"default-docker-credential": {}},
   237  		},
   238  		{
   239  			name:           "relevant_secret_update_non_full_push",
   240  			proxyNamespace: "default",
   241  			request: &model.PushRequest{
   242  				Full:           false,
   243  				ConfigsUpdated: sets.New(model.ConfigKey{Kind: kind.Secret, Name: "default-pull-secret", Namespace: "default"}),
   244  			},
   245  			watchedResources: []string{"extenstions.istio.io/wasmplugin/default.default-plugin-with-sec"},
   246  			wantExtensions:   sets.String{"extenstions.istio.io/wasmplugin/default.default-plugin-with-sec": {}},
   247  			wantSecrets:      sets.String{"default-docker-credential": {}},
   248  		},
   249  		// All the credentials should be sent to istio-agent even if one of them is only updated,
   250  		// because `istio-agent` does not keep the credentials.
   251  		{
   252  			name:           "multi_wasmplugin_update_secret",
   253  			proxyNamespace: "default",
   254  			request: &model.PushRequest{
   255  				Full:           false,
   256  				ConfigsUpdated: sets.New(model.ConfigKey{Kind: kind.Secret, Name: "default-pull-secret", Namespace: "default"}),
   257  			},
   258  			watchedResources: []string{
   259  				"extenstions.istio.io/wasmplugin/default.default-plugin-with-sec",
   260  				"extenstions.istio.io/wasmplugin/istio-system.root-plugin",
   261  			},
   262  			wantExtensions: sets.String{
   263  				"extenstions.istio.io/wasmplugin/default.default-plugin-with-sec": {},
   264  				"extenstions.istio.io/wasmplugin/istio-system.root-plugin":        {},
   265  			},
   266  			wantSecrets: sets.String{"default-docker-credential": {}, "root-docker-credential": {}},
   267  		},
   268  	}
   269  
   270  	for _, tt := range cases {
   271  		t.Run(tt.name, func(t *testing.T) {
   272  			s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{
   273  				KubernetesObjects: []runtime.Object{defaultPullSecret, rootPullSecret, wrongTypeSecret},
   274  				Configs:           []config.Config{wasmPlugin, wasmPluginWithSec, wasmPluginWrongSec, wasmPluginWrongSecType, rootWasmPluginWithSec},
   275  			})
   276  
   277  			gen := s.Discovery.Generators[v3.ExtensionConfigurationType]
   278  			tt.request.Start = time.Now()
   279  			proxy := &model.Proxy{
   280  				VerifiedIdentity: &spiffe.Identity{Namespace: tt.proxyNamespace},
   281  				Type:             model.Router,
   282  				Metadata: &model.NodeMetadata{
   283  					ClusterID: constants.DefaultClusterName,
   284  				},
   285  			}
   286  			tt.request.Push = s.PushContext()
   287  			tt.request.Push.Mesh.RootNamespace = "istio-system"
   288  			resources, _, _ := gen.Generate(s.SetupProxy(proxy),
   289  				&model.WatchedResource{ResourceNames: tt.watchedResources}, tt.request)
   290  			gotExtensions := sets.String{}
   291  			gotSecrets := sets.String{}
   292  			for _, res := range resources {
   293  				gotExtensions.Insert(res.Name)
   294  				ec := &core.TypedExtensionConfig{}
   295  				res.Resource.UnmarshalTo(ec)
   296  				wasm := &wasm.Wasm{}
   297  				ec.TypedConfig.UnmarshalTo(wasm)
   298  				gotsecret := wasm.GetConfig().GetVmConfig().GetEnvironmentVariables().GetKeyValues()[model.WasmSecretEnv]
   299  				if gotsecret != "" {
   300  					gotSecrets.Insert(gotsecret)
   301  				}
   302  			}
   303  			if !reflect.DeepEqual(gotSecrets, tt.wantSecrets) {
   304  				t.Errorf("got secrets %v, want secrets %v", gotSecrets, tt.wantSecrets)
   305  			}
   306  			if !reflect.DeepEqual(gotExtensions, tt.wantExtensions) {
   307  				t.Errorf("got extensions %v, want extensions %v", gotExtensions, tt.wantExtensions)
   308  			}
   309  		})
   310  	}
   311  }