istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/xds/proxy_dependencies_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 xds
    16  
    17  import (
    18  	"fmt"
    19  	"testing"
    20  
    21  	mesh "istio.io/api/mesh/v1alpha1"
    22  	networking "istio.io/api/networking/v1alpha3"
    23  	security "istio.io/api/security/v1beta1"
    24  	"istio.io/api/type/v1beta1"
    25  	"istio.io/istio/pilot/pkg/features"
    26  	"istio.io/istio/pilot/pkg/model"
    27  	"istio.io/istio/pilot/pkg/networking/core"
    28  	"istio.io/istio/pkg/config"
    29  	"istio.io/istio/pkg/config/schema/gvk"
    30  	"istio.io/istio/pkg/config/schema/kind"
    31  	"istio.io/istio/pkg/config/visibility"
    32  	"istio.io/istio/pkg/jwt"
    33  	"istio.io/istio/pkg/spiffe"
    34  	"istio.io/istio/pkg/test"
    35  	"istio.io/istio/pkg/util/sets"
    36  )
    37  
    38  func TestProxyNeedsPush(t *testing.T) {
    39  	const (
    40  		svcName        = "svc1.com"
    41  		privateSvcName = "private.com"
    42  		drName         = "dr1"
    43  		vsName         = "vs1"
    44  		scName         = "sc1"
    45  		nsName         = "ns1"
    46  		nsRoot         = "rootns"
    47  		generalName    = "name1"
    48  
    49  		invalidNameSuffix = "invalid"
    50  	)
    51  
    52  	type Case struct {
    53  		name    string
    54  		proxy   *model.Proxy
    55  		configs sets.Set[model.ConfigKey]
    56  		want    bool
    57  	}
    58  
    59  	sidecar := &model.Proxy{
    60  		Type: model.SidecarProxy, IPAddresses: []string{"127.0.0.1"}, Metadata: &model.NodeMetadata{},
    61  		SidecarScope: &model.SidecarScope{Name: generalName, Namespace: nsName},
    62  	}
    63  	gateway := &model.Proxy{
    64  		Type:            model.Router,
    65  		ConfigNamespace: nsName,
    66  		Metadata:        &model.NodeMetadata{Namespace: nsName},
    67  		Labels:          map[string]string{"gateway": "gateway"},
    68  	}
    69  
    70  	sidecarScopeKindNames := map[kind.Kind]string{
    71  		kind.ServiceEntry: svcName, kind.VirtualService: vsName, kind.DestinationRule: drName, kind.Sidecar: scName,
    72  	}
    73  	for kind, name := range sidecarScopeKindNames {
    74  		sidecar.SidecarScope.AddConfigDependencies(model.ConfigKey{Kind: kind, Name: name, Namespace: nsName}.HashCode())
    75  	}
    76  	for kind := range UnAffectedConfigKinds[model.SidecarProxy] {
    77  		sidecar.SidecarScope.AddConfigDependencies(model.ConfigKey{
    78  			Kind:      kind,
    79  			Name:      generalName,
    80  			Namespace: nsName,
    81  		}.HashCode())
    82  	}
    83  
    84  	cases := []Case{
    85  		{"no namespace or configs", sidecar, nil, true},
    86  		{
    87  			"gateway config for sidecar", sidecar, sets.New(model.ConfigKey{Kind: kind.Gateway, Name: generalName, Namespace: nsName}),
    88  
    89  			false,
    90  		},
    91  		{
    92  			"gateway config for gateway", gateway, sets.New(model.ConfigKey{Kind: kind.Gateway, Name: generalName, Namespace: nsName}),
    93  
    94  			true,
    95  		},
    96  		{
    97  			"sidecar config for gateway", gateway, sets.New(model.ConfigKey{Kind: kind.Sidecar, Name: scName, Namespace: nsName}),
    98  
    99  			false,
   100  		},
   101  		{
   102  			"invalid config for sidecar", sidecar,
   103  			sets.New(model.ConfigKey{Kind: kind.Kind(255), Name: generalName, Namespace: nsName}),
   104  
   105  			true,
   106  		},
   107  		{"mixture matched and unmatched config for sidecar", sidecar, sets.New(
   108  			model.ConfigKey{Kind: kind.DestinationRule, Name: drName, Namespace: nsName},
   109  			model.ConfigKey{Kind: kind.ServiceEntry, Name: svcName + invalidNameSuffix, Namespace: nsName},
   110  		), true},
   111  		{"mixture unmatched and unmatched config for sidecar", sidecar, sets.New(
   112  			model.ConfigKey{Kind: kind.DestinationRule, Name: drName + invalidNameSuffix, Namespace: nsName},
   113  			model.ConfigKey{Kind: kind.ServiceEntry, Name: svcName + invalidNameSuffix, Namespace: nsName},
   114  		), false},
   115  		{"empty configsUpdated for sidecar", sidecar, nil, true},
   116  	}
   117  
   118  	for k, name := range sidecarScopeKindNames {
   119  		cases = append(cases, Case{ // valid name
   120  			name:    fmt.Sprintf("%s config for sidecar", k.String()),
   121  			proxy:   sidecar,
   122  			configs: sets.New(model.ConfigKey{Kind: k, Name: name, Namespace: nsName}),
   123  			want:    true,
   124  		}, Case{ // invalid name
   125  			name:    fmt.Sprintf("%s unmatched config for sidecar", k.String()),
   126  			proxy:   sidecar,
   127  			configs: sets.New(model.ConfigKey{Kind: k, Name: name + invalidNameSuffix, Namespace: nsName}),
   128  			want:    false,
   129  		})
   130  	}
   131  
   132  	sidecarNamespaceScopeTypes := []kind.Kind{
   133  		kind.EnvoyFilter, kind.AuthorizationPolicy, kind.RequestAuthentication, kind.WasmPlugin,
   134  	}
   135  	for _, k := range sidecarNamespaceScopeTypes {
   136  		cases = append(cases,
   137  			Case{
   138  				name:    fmt.Sprintf("%s config for sidecar in same namespace", k.String()),
   139  				proxy:   sidecar,
   140  				configs: sets.New(model.ConfigKey{Kind: k, Name: generalName, Namespace: nsName}),
   141  				want:    true,
   142  			},
   143  			Case{
   144  				name:    fmt.Sprintf("%s config for sidecar in different namespace", k.String()),
   145  				proxy:   sidecar,
   146  				configs: sets.New(model.ConfigKey{Kind: k, Name: generalName, Namespace: "invalid-namespace"}),
   147  				want:    false,
   148  			},
   149  			Case{
   150  				name:    fmt.Sprintf("%s config in the root namespace", k.String()),
   151  				proxy:   sidecar,
   152  				configs: sets.New(model.ConfigKey{Kind: k, Name: generalName, Namespace: nsRoot}),
   153  				want:    true,
   154  			},
   155  		)
   156  	}
   157  
   158  	// tests for kind-affect-proxy.
   159  	for _, nodeType := range []model.NodeType{model.Router, model.SidecarProxy} {
   160  		proxy := gateway
   161  		if nodeType == model.SidecarProxy {
   162  			proxy = sidecar
   163  		}
   164  		for k := range UnAffectedConfigKinds[proxy.Type] {
   165  			cases = append(cases, Case{
   166  				name:    fmt.Sprintf("kind %s not affect %s", k.String(), nodeType),
   167  				proxy:   proxy,
   168  				configs: sets.New(model.ConfigKey{Kind: k, Name: generalName + invalidNameSuffix, Namespace: nsName}),
   169  
   170  				want: false,
   171  			})
   172  		}
   173  	}
   174  
   175  	// test for gateway proxy dependencies.
   176  	cg := core.NewConfigGenTest(t, core.TestOptions{
   177  		Services: []*model.Service{
   178  			{
   179  				Hostname: svcName,
   180  				Attributes: model.ServiceAttributes{
   181  					ExportTo:  sets.New(visibility.Public),
   182  					Namespace: nsName,
   183  				},
   184  			},
   185  			{
   186  				Hostname: privateSvcName,
   187  				Attributes: model.ServiceAttributes{
   188  					ExportTo:  sets.New(visibility.None),
   189  					Namespace: nsName,
   190  				},
   191  			},
   192  			{
   193  				Hostname: "foo",
   194  				Attributes: model.ServiceAttributes{
   195  					ExportTo:  sets.New(visibility.Public),
   196  					Namespace: nsName,
   197  				},
   198  			},
   199  		},
   200  	})
   201  	gateway.SetSidecarScope(cg.PushContext())
   202  
   203  	// service visibility updated
   204  	cg = core.NewConfigGenTest(t, core.TestOptions{
   205  		Services: []*model.Service{
   206  			{
   207  				Hostname: svcName,
   208  				Attributes: model.ServiceAttributes{
   209  					ExportTo:  sets.New(visibility.Public),
   210  					Namespace: nsName,
   211  				},
   212  			},
   213  			{
   214  				Hostname: privateSvcName,
   215  				Attributes: model.ServiceAttributes{
   216  					ExportTo:  sets.New(visibility.None),
   217  					Namespace: nsName,
   218  				},
   219  			},
   220  			{
   221  				Hostname: "foo",
   222  				Attributes: model.ServiceAttributes{
   223  					// service visibility changed from public to none
   224  					ExportTo:  sets.New(visibility.None),
   225  					Namespace: nsName,
   226  				},
   227  			},
   228  		},
   229  	})
   230  	gateway.SetSidecarScope(cg.PushContext())
   231  
   232  	cases = append(cases,
   233  		Case{
   234  			name:    "service with public visibility for gateway",
   235  			proxy:   gateway,
   236  			configs: sets.New(model.ConfigKey{Kind: kind.ServiceEntry, Name: svcName, Namespace: nsName}),
   237  			want:    true,
   238  		},
   239  		Case{
   240  			name:    "service with none visibility for gateway",
   241  			proxy:   gateway,
   242  			configs: sets.New(model.ConfigKey{Kind: kind.ServiceEntry, Name: privateSvcName, Namespace: nsName}),
   243  			want:    false,
   244  		},
   245  		Case{
   246  			name:    "service visibility changed from public to none",
   247  			proxy:   gateway,
   248  			configs: sets.New(model.ConfigKey{Kind: kind.ServiceEntry, Name: "foo", Namespace: nsName}),
   249  			want:    true,
   250  		},
   251  	)
   252  
   253  	for _, tt := range cases {
   254  		t.Run(tt.name, func(t *testing.T) {
   255  			cg.PushContext().Mesh.RootNamespace = nsRoot
   256  			got := DefaultProxyNeedsPush(tt.proxy, &model.PushRequest{ConfigsUpdated: tt.configs, Push: cg.PushContext()})
   257  			if got != tt.want {
   258  				t.Fatalf("Got needs push = %v, expected %v", got, tt.want)
   259  			}
   260  		})
   261  	}
   262  
   263  	// test for gateway proxy dependencies with PILOT_FILTER_GATEWAY_CLUSTER_CONFIG enabled.
   264  	test.SetForTest(t, &features.FilterGatewayClusterConfig, true)
   265  	test.SetForTest(t, &features.JwksFetchMode, jwt.Envoy)
   266  
   267  	const (
   268  		fooSvc       = "foo"
   269  		extensionSvc = "extension"
   270  		jwksSvc      = "jwks"
   271  	)
   272  
   273  	cg = core.NewConfigGenTest(t, core.TestOptions{
   274  		Services: []*model.Service{
   275  			{
   276  				Hostname: fooSvc,
   277  				Attributes: model.ServiceAttributes{
   278  					ExportTo:  sets.New(visibility.Public),
   279  					Namespace: nsName,
   280  				},
   281  			},
   282  			{
   283  				Hostname: svcName,
   284  				Attributes: model.ServiceAttributes{
   285  					ExportTo:  sets.New(visibility.Public),
   286  					Namespace: nsName,
   287  				},
   288  			},
   289  			{
   290  				Hostname: extensionSvc,
   291  				Attributes: model.ServiceAttributes{
   292  					ExportTo:  sets.New(visibility.Public),
   293  					Namespace: nsName,
   294  				},
   295  			},
   296  			{
   297  				Hostname: jwksSvc,
   298  				Attributes: model.ServiceAttributes{
   299  					ExportTo:  sets.New(visibility.Public),
   300  					Namespace: nsName,
   301  				},
   302  			},
   303  		},
   304  		Configs: []config.Config{
   305  			{
   306  				Meta: config.Meta{
   307  					GroupVersionKind: gvk.VirtualService,
   308  					Name:             svcName,
   309  					Namespace:        nsName,
   310  				},
   311  				Spec: &networking.VirtualService{
   312  					Hosts:    []string{"*"},
   313  					Gateways: []string{generalName},
   314  					Http: []*networking.HTTPRoute{
   315  						{
   316  							Route: []*networking.HTTPRouteDestination{
   317  								{
   318  									Destination: &networking.Destination{
   319  										Host: svcName,
   320  									},
   321  								},
   322  							},
   323  						},
   324  					},
   325  				},
   326  			},
   327  			{
   328  				Meta: config.Meta{
   329  					GroupVersionKind: gvk.RequestAuthentication,
   330  					Name:             jwksSvc,
   331  					Namespace:        nsName,
   332  				},
   333  				Spec: &security.RequestAuthentication{
   334  					Selector: &v1beta1.WorkloadSelector{MatchLabels: gateway.Labels},
   335  					JwtRules: []*security.JWTRule{{JwksUri: "https://" + jwksSvc}},
   336  				},
   337  			},
   338  			{
   339  				Meta: config.Meta{
   340  					GroupVersionKind: gvk.RequestAuthentication,
   341  					Name:             fooSvc,
   342  					Namespace:        nsName,
   343  				},
   344  				Spec: &security.RequestAuthentication{
   345  					// not matching the gateway
   346  					Selector: &v1beta1.WorkloadSelector{MatchLabels: map[string]string{"foo": "bar"}},
   347  					JwtRules: []*security.JWTRule{{JwksUri: "https://" + fooSvc}},
   348  				},
   349  			},
   350  		},
   351  		MeshConfig: &mesh.MeshConfig{
   352  			ExtensionProviders: []*mesh.MeshConfig_ExtensionProvider{
   353  				{
   354  					Provider: &mesh.MeshConfig_ExtensionProvider_EnvoyExtAuthzHttp{
   355  						EnvoyExtAuthzHttp: &mesh.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationHttpProvider{
   356  							Service: extensionSvc,
   357  						},
   358  					},
   359  				},
   360  			},
   361  		},
   362  	})
   363  
   364  	gateway.MergedGateway = &model.MergedGateway{
   365  		GatewayNameForServer: map[*networking.Server]string{
   366  			{}: nsName + "/" + generalName,
   367  		},
   368  	}
   369  	gateway.SetSidecarScope(cg.PushContext())
   370  
   371  	cases = []Case{
   372  		{
   373  			name:    "service without vs attached to gateway",
   374  			proxy:   gateway,
   375  			configs: sets.New(model.ConfigKey{Kind: kind.ServiceEntry, Name: fooSvc, Namespace: nsName}),
   376  			want:    false,
   377  		},
   378  		{
   379  			name:    "service with vs attached to gateway",
   380  			proxy:   gateway,
   381  			configs: sets.New(model.ConfigKey{Kind: kind.ServiceEntry, Name: svcName, Namespace: nsName}),
   382  			want:    true,
   383  		},
   384  		{
   385  			name:    "mesh config extensions",
   386  			proxy:   gateway,
   387  			configs: sets.New(model.ConfigKey{Kind: kind.ServiceEntry, Name: extensionSvc, Namespace: nsName}),
   388  			want:    true,
   389  		},
   390  		{
   391  			name:    "jwks servers",
   392  			proxy:   gateway,
   393  			configs: sets.New(model.ConfigKey{Kind: kind.ServiceEntry, Name: jwksSvc, Namespace: nsName}),
   394  			want:    true,
   395  		},
   396  	}
   397  
   398  	for _, tt := range cases {
   399  		t.Run(tt.name, func(t *testing.T) {
   400  			got := DefaultProxyNeedsPush(tt.proxy, &model.PushRequest{ConfigsUpdated: tt.configs, Push: cg.PushContext()})
   401  			if got != tt.want {
   402  				t.Fatalf("Got needs push = %v, expected %v", got, tt.want)
   403  			}
   404  		})
   405  	}
   406  
   407  	gateway.MergedGateway.ContainsAutoPassthroughGateways = true
   408  	for _, tt := range cases {
   409  		t.Run(tt.name, func(t *testing.T) {
   410  			push := DefaultProxyNeedsPush(tt.proxy, &model.PushRequest{ConfigsUpdated: tt.configs, Push: cg.PushContext()})
   411  			if !push {
   412  				t.Fatalf("Got needs push = %v, expected %v", push, true)
   413  			}
   414  		})
   415  	}
   416  }
   417  
   418  func TestCheckConnectionIdentity(t *testing.T) {
   419  	cases := []struct {
   420  		name      string
   421  		identity  []string
   422  		sa        string
   423  		namespace string
   424  		success   bool
   425  	}{
   426  		{
   427  			name:      "single match",
   428  			identity:  []string{spiffe.Identity{TrustDomain: "cluster.local", Namespace: "namespace", ServiceAccount: "serviceaccount"}.String()},
   429  			sa:        "serviceaccount",
   430  			namespace: "namespace",
   431  			success:   true,
   432  		},
   433  		{
   434  			name: "second match",
   435  			identity: []string{
   436  				spiffe.Identity{TrustDomain: "cluster.local", Namespace: "bad", ServiceAccount: "serviceaccount"}.String(),
   437  				spiffe.Identity{TrustDomain: "cluster.local", Namespace: "namespace", ServiceAccount: "serviceaccount"}.String(),
   438  			},
   439  			sa:        "serviceaccount",
   440  			namespace: "namespace",
   441  			success:   true,
   442  		},
   443  		{
   444  			name: "no match namespace",
   445  			identity: []string{
   446  				spiffe.Identity{TrustDomain: "cluster.local", Namespace: "bad", ServiceAccount: "serviceaccount"}.String(),
   447  			},
   448  			sa:        "serviceaccount",
   449  			namespace: "namespace",
   450  			success:   false,
   451  		},
   452  		{
   453  			name: "no match service account",
   454  			identity: []string{
   455  				spiffe.Identity{TrustDomain: "cluster.local", Namespace: "namespace", ServiceAccount: "bad"}.String(),
   456  			},
   457  			sa:        "serviceaccount",
   458  			namespace: "namespace",
   459  			success:   false,
   460  		},
   461  	}
   462  	for _, tt := range cases {
   463  		t.Run(tt.name, func(t *testing.T) {
   464  			proxy := &model.Proxy{ConfigNamespace: tt.namespace, Metadata: &model.NodeMetadata{ServiceAccount: tt.sa}}
   465  			if _, err := checkConnectionIdentity(proxy, tt.identity); (err == nil) != tt.success {
   466  				t.Fatalf("expected success=%v, got err=%v", tt.success, err)
   467  			}
   468  		})
   469  	}
   470  }