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 }