istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/test/xds/fake.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 "context" 19 "fmt" 20 "net" 21 "strings" 22 "time" 23 24 endpoint "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" 25 discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 26 "google.golang.org/grpc" 27 "google.golang.org/grpc/credentials/insecure" 28 "google.golang.org/grpc/test/bufconn" 29 authorizationv1 "k8s.io/api/authorization/v1" 30 "k8s.io/apimachinery/pkg/runtime" 31 "k8s.io/apimachinery/pkg/runtime/schema" 32 "k8s.io/client-go/kubernetes/fake" 33 k8stesting "k8s.io/client-go/testing" 34 35 meshconfig "istio.io/api/mesh/v1alpha1" 36 "istio.io/istio/pilot/pkg/autoregistration" 37 "istio.io/istio/pilot/pkg/bootstrap" 38 "istio.io/istio/pilot/pkg/config/kube/gateway" 39 ingress "istio.io/istio/pilot/pkg/config/kube/ingress" 40 "istio.io/istio/pilot/pkg/config/memory" 41 kubesecrets "istio.io/istio/pilot/pkg/credentials/kube" 42 "istio.io/istio/pilot/pkg/features" 43 "istio.io/istio/pilot/pkg/model" 44 "istio.io/istio/pilot/pkg/networking/core" 45 "istio.io/istio/pilot/pkg/serviceregistry" 46 kube "istio.io/istio/pilot/pkg/serviceregistry/kube/controller" 47 memregistry "istio.io/istio/pilot/pkg/serviceregistry/memory" 48 "istio.io/istio/pilot/pkg/serviceregistry/util/xdsfake" 49 "istio.io/istio/pilot/pkg/xds" 50 "istio.io/istio/pilot/pkg/xds/endpoints" 51 v3 "istio.io/istio/pilot/pkg/xds/v3" 52 "istio.io/istio/pilot/test/xdstest" 53 "istio.io/istio/pkg/adsc" 54 "istio.io/istio/pkg/cluster" 55 "istio.io/istio/pkg/config" 56 "istio.io/istio/pkg/config/constants" 57 "istio.io/istio/pkg/config/mesh" 58 "istio.io/istio/pkg/config/schema/collections" 59 "istio.io/istio/pkg/config/schema/gvk" 60 "istio.io/istio/pkg/config/schema/gvr" 61 "istio.io/istio/pkg/config/schema/kind" 62 "istio.io/istio/pkg/keepalive" 63 kubelib "istio.io/istio/pkg/kube" 64 "istio.io/istio/pkg/kube/multicluster" 65 "istio.io/istio/pkg/test" 66 "istio.io/istio/pkg/test/util/retry" 67 "istio.io/istio/pkg/util/sets" 68 ) 69 70 type FakeOptions struct { 71 // If provided, sets the name of the "default" or local cluster to the similaed pilots. (Defaults to opts.DefaultClusterName) 72 DefaultClusterName cluster.ID 73 // If provided, the minor version will be overridden for calls to GetKubernetesVersion to 1.minor 74 KubernetesVersion string 75 // If provided, a service registry with the name of each map key will be created with the given objects. 76 KubernetesObjectsByCluster map[cluster.ID][]runtime.Object 77 // If provided, these objects will be used directly for the default cluster ("Kubernetes" or DefaultClusterName) 78 KubernetesObjects []runtime.Object 79 // If provided, a service registry with the name of each map key will be created with the given objects. 80 KubernetesObjectStringByCluster map[cluster.ID]string 81 // If provided, the yaml string will be parsed and used as objects for the default cluster ("Kubernetes" or DefaultClusterName) 82 KubernetesObjectString string 83 // If provided, these configs will be used directly 84 Configs []config.Config 85 // If provided, the yaml string will be parsed and used as configs 86 ConfigString string 87 // If provided, the ConfigString will be treated as a go template, with this as input params 88 ConfigTemplateInput any 89 // If provided, this mesh config will be used 90 MeshConfig *meshconfig.MeshConfig 91 NetworksWatcher mesh.NetworksWatcher 92 93 // Callback to modify the kube client before it is started 94 KubeClientModifier func(c kubelib.Client) 95 96 // Override the default kube client constructor 97 KubeClientBuilder func(objects ...runtime.Object) kubelib.Client 98 99 // ListenerBuilder, if specified, allows making the server use the given 100 // listener instead of a buffered conn. 101 ListenerBuilder func() (net.Listener, error) 102 103 // Time to debounce 104 // By default, set to 0s to speed up tests 105 DebounceTime time.Duration 106 107 // EnableFakeXDSUpdater will use a XDSUpdater that can be used to watch events 108 EnableFakeXDSUpdater bool 109 DisableSecretAuthorization bool 110 Services []*model.Service 111 Gateways []model.NetworkGateway 112 } 113 114 type FakeDiscoveryServer struct { 115 *core.ConfigGenTest 116 t test.Failer 117 Discovery *xds.DiscoveryServer 118 Listener net.Listener 119 BufListener *bufconn.Listener 120 kubeClient kubelib.Client 121 KubeRegistry *kube.FakeController 122 XdsUpdater model.XDSUpdater 123 MemRegistry *memregistry.ServiceDiscovery 124 } 125 126 func NewFakeDiscoveryServer(t test.Failer, opts FakeOptions) *FakeDiscoveryServer { 127 m := opts.MeshConfig 128 if m == nil { 129 m = mesh.DefaultMeshConfig() 130 } 131 132 // Init with a dummy environment, since we have a circular dependency with the env creation. 133 s := xds.NewDiscoveryServer(model.NewEnvironment(), map[string]string{}) 134 // Disable debounce to reduce test times 135 s.DebounceOptions.DebounceAfter = opts.DebounceTime 136 // Setup time to Now instead of process start to make logs not misleading 137 s.DiscoveryStartTime = time.Now() 138 t.Cleanup(s.Shutdown) 139 140 serviceHandler := func(_, curr *model.Service, _ model.Event) { 141 pushReq := &model.PushRequest{ 142 Full: true, 143 ConfigsUpdated: sets.New(model.ConfigKey{Kind: kind.ServiceEntry, Name: string(curr.Hostname), Namespace: curr.Attributes.Namespace}), 144 Reason: model.NewReasonStats(model.ServiceUpdate), 145 } 146 s.ConfigUpdate(pushReq) 147 } 148 149 if opts.DefaultClusterName == "" { 150 opts.DefaultClusterName = constants.DefaultClusterName 151 } 152 k8sObjects := getKubernetesObjects(t, opts) 153 var defaultKubeClient kubelib.Client 154 var defaultKubeController *kube.FakeController 155 var registries []serviceregistry.Instance 156 if opts.NetworksWatcher != nil { 157 opts.NetworksWatcher.AddNetworksHandler(func() { 158 s.ConfigUpdate(&model.PushRequest{ 159 Full: true, 160 Reason: model.NewReasonStats(model.NetworksTrigger), 161 }) 162 }) 163 } 164 var xdsUpdater model.XDSUpdater = s 165 if opts.EnableFakeXDSUpdater { 166 xdsUpdater = xdsfake.NewWithDelegate(s) 167 } 168 mc := multicluster.NewFakeController() 169 creds := kubesecrets.NewMulticluster(opts.DefaultClusterName, mc) 170 171 configController := memory.NewSyncController(memory.MakeSkipValidation(collections.PilotGatewayAPI())) 172 clientBuilder := opts.KubeClientBuilder 173 if clientBuilder == nil { 174 clientBuilder = func(objects ...runtime.Object) kubelib.Client { 175 return kubelib.NewFakeClientWithVersion(opts.KubernetesVersion, objects...) 176 } 177 } 178 for k8sCluster, objs := range k8sObjects { 179 client := clientBuilder(objs...) 180 if opts.KubeClientModifier != nil { 181 opts.KubeClientModifier(client) 182 } 183 k8s, _ := kube.NewFakeControllerWithOptions(t, kube.FakeControllerOptions{ 184 ServiceHandler: serviceHandler, 185 Client: client, 186 ClusterID: k8sCluster, 187 DomainSuffix: "cluster.local", 188 XDSUpdater: xdsUpdater, 189 NetworksWatcher: opts.NetworksWatcher, 190 SkipRun: true, 191 ConfigCluster: k8sCluster == opts.DefaultClusterName, 192 MeshWatcher: mesh.NewFixedWatcher(m), 193 CRDs: []schema.GroupVersionResource{ 194 gvr.AuthorizationPolicy, 195 gvr.PeerAuthentication, 196 gvr.KubernetesGateway, 197 gvr.WorkloadEntry, 198 gvr.ServiceEntry, 199 }, 200 }) 201 stop := test.NewStop(t) 202 // start default client informers after creating ingress/secret controllers 203 if defaultKubeClient == nil || k8sCluster == opts.DefaultClusterName { 204 defaultKubeClient = client 205 if opts.DisableSecretAuthorization { 206 DisableAuthorizationForSecret(defaultKubeClient.Kube().(*fake.Clientset)) 207 } 208 defaultKubeController = k8s 209 } else { 210 client.RunAndWait(stop) 211 } 212 registries = append(registries, k8s) 213 mc.Add(k8sCluster, client, stop) 214 } 215 216 stop := test.NewStop(t) 217 ingr := ingress.NewController(defaultKubeClient, mesh.NewFixedWatcher(m), kube.Options{ 218 DomainSuffix: "cluster.local", 219 }) 220 defaultKubeClient.RunAndWait(stop) 221 222 var gwc *gateway.Controller 223 cg := core.NewConfigGenTest(t, core.TestOptions{ 224 Configs: opts.Configs, 225 ConfigString: opts.ConfigString, 226 ConfigTemplateInput: opts.ConfigTemplateInput, 227 ConfigController: configController, 228 MeshConfig: m, 229 XDSUpdater: xdsUpdater, 230 NetworksWatcher: opts.NetworksWatcher, 231 ServiceRegistries: registries, 232 ConfigStoreCaches: []model.ConfigStoreController{ingr}, 233 CreateConfigStore: func(c model.ConfigStoreController) model.ConfigStoreController { 234 g := gateway.NewController(defaultKubeClient, c, func(class schema.GroupVersionResource, stop <-chan struct{}) bool { 235 return true 236 }, nil, kube.Options{ 237 DomainSuffix: "cluster.local", 238 }) 239 gwc = g 240 return gwc 241 }, 242 SkipRun: true, 243 ClusterID: opts.DefaultClusterName, 244 Services: opts.Services, 245 Gateways: opts.Gateways, 246 }) 247 cg.Registry.AppendServiceHandler(serviceHandler) 248 s.Env = cg.Env() 249 s.Env.GatewayAPIController = gwc 250 if err := s.Env.InitNetworksManager(s); err != nil { 251 t.Fatal(err) 252 } 253 254 bootstrap.InitGenerators(s, core.NewConfigGenerator(s.Cache), "istio-system", "", nil) 255 s.Generators[v3.SecretType] = xds.NewSecretGen(creds, s.Cache, opts.DefaultClusterName, nil) 256 s.Generators[v3.ExtensionConfigurationType].(*xds.EcdsGenerator).SetCredController(creds) 257 258 memRegistry := cg.MemRegistry 259 memRegistry.XdsUpdater = s 260 261 // Setup config handlers 262 // TODO code re-use from server.go 263 configHandler := func(_, curr config.Config, event model.Event) { 264 pushReq := &model.PushRequest{ 265 Full: true, 266 ConfigsUpdated: sets.New(model.ConfigKey{Kind: kind.MustFromGVK(curr.GroupVersionKind), Name: curr.Name, Namespace: curr.Namespace}), 267 Reason: model.NewReasonStats(model.ConfigUpdate), 268 } 269 s.ConfigUpdate(pushReq) 270 } 271 schemas := collections.Pilot.All() 272 if features.EnableGatewayAPI { 273 schemas = collections.PilotGatewayAPI().All() 274 } 275 for _, schema := range schemas { 276 // This resource type was handled in external/servicediscovery.go, no need to rehandle here. 277 if schema.GroupVersionKind() == gvk.ServiceEntry { 278 continue 279 } 280 if schema.GroupVersionKind() == gvk.WorkloadEntry { 281 continue 282 } 283 284 cg.Store().RegisterEventHandler(schema.GroupVersionKind(), configHandler) 285 } 286 for _, registry := range registries { 287 k8s, ok := registry.(*kube.FakeController) 288 // this closely matches what we do in serviceregistry/kube/controller/multicluster.go 289 if !ok || k8s.Cluster() != cg.ServiceEntryRegistry.Cluster() { 290 continue 291 } 292 cg.ServiceEntryRegistry.AppendWorkloadHandler(k8s.WorkloadInstanceHandler) 293 k8s.AppendWorkloadHandler(cg.ServiceEntryRegistry.WorkloadInstanceHandler) 294 } 295 s.WorkloadEntryController = autoregistration.NewController(cg.Store(), "test", keepalive.Infinity) 296 297 var listener net.Listener 298 if opts.ListenerBuilder != nil { 299 var err error 300 if listener, err = opts.ListenerBuilder(); err != nil { 301 t.Fatal(err) 302 } 303 } else { 304 // Start in memory gRPC listener 305 buffer := 1024 * 1024 306 listener = bufconn.Listen(buffer) 307 } 308 309 grpcServer := grpc.NewServer() 310 s.Register(grpcServer) 311 go func() { 312 if err := grpcServer.Serve(listener); err != nil && !(err == grpc.ErrServerStopped || err.Error() == "closed") { 313 t.Fatal(err) 314 } 315 }() 316 t.Cleanup(func() { 317 grpcServer.Stop() 318 _ = listener.Close() 319 }) 320 // Start the discovery server 321 s.Start(stop) 322 cg.ServiceEntryRegistry.XdsUpdater = s 323 // Now that handlers are added, get everything started 324 cg.Run() 325 kubelib.WaitForCacheSync("fake", stop, 326 cg.Registry.HasSynced, 327 cg.Store().HasSynced) 328 cg.ServiceEntryRegistry.ResyncEDS() 329 330 // Send an update. This ensures that even if there are no configs provided, the push context is 331 // initialized. 332 s.ConfigUpdate(&model.PushRequest{Full: true}) 333 334 // Wait until initial updates are committed 335 c := s.InboundUpdates.Load() 336 retry.UntilOrFail(t, func() bool { 337 return s.CommittedUpdates.Load() >= c 338 }, retry.Delay(time.Millisecond)) 339 340 // Mark ourselves ready 341 s.CachesSynced() 342 343 bufListener, _ := listener.(*bufconn.Listener) 344 fake := &FakeDiscoveryServer{ 345 t: t, 346 Discovery: s, 347 Listener: listener, 348 BufListener: bufListener, 349 ConfigGenTest: cg, 350 kubeClient: defaultKubeClient, 351 KubeRegistry: defaultKubeController, 352 XdsUpdater: xdsUpdater, 353 MemRegistry: memRegistry, 354 } 355 356 return fake 357 } 358 359 func (f *FakeDiscoveryServer) KubeClient() kubelib.Client { 360 return f.kubeClient 361 } 362 363 func (f *FakeDiscoveryServer) PushContext() *model.PushContext { 364 return f.Env().PushContext() 365 } 366 367 // ConnectADS starts an ADS connection to the server. It will automatically be cleaned up when the test ends 368 func (f *FakeDiscoveryServer) ConnectADS() *xds.AdsTest { 369 conn, err := grpc.Dial("buffcon", 370 grpc.WithTransportCredentials(insecure.NewCredentials()), 371 grpc.WithBlock(), 372 grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { 373 return f.BufListener.Dial() 374 })) 375 if err != nil { 376 f.t.Fatalf("failed to connect: %v", err) 377 } 378 return xds.NewAdsTest(f.t, conn) 379 } 380 381 // ConnectDeltaADS starts a Delta ADS connection to the server. It will automatically be cleaned up when the test ends 382 func (f *FakeDiscoveryServer) ConnectDeltaADS() *xds.DeltaAdsTest { 383 conn, err := grpc.Dial("buffcon", 384 grpc.WithTransportCredentials(insecure.NewCredentials()), 385 grpc.WithBlock(), 386 grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { 387 return f.BufListener.Dial() 388 })) 389 if err != nil { 390 f.t.Fatalf("failed to connect: %v", err) 391 } 392 return xds.NewDeltaAdsTest(f.t, conn) 393 } 394 395 func APIWatches() []string { 396 watches := []string{gvk.MeshConfig.String()} 397 for _, sch := range collections.Pilot.All() { 398 watches = append(watches, sch.GroupVersionKind().String()) 399 } 400 return watches 401 } 402 403 func (f *FakeDiscoveryServer) ConnectUnstarted(p *model.Proxy, watch []string) *adsc.ADSC { 404 f.t.Helper() 405 p = f.SetupProxy(p) 406 initialWatch := []*discovery.DiscoveryRequest{} 407 for _, typeURL := range watch { 408 initialWatch = append(initialWatch, &discovery.DiscoveryRequest{TypeUrl: typeURL}) 409 } 410 opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} 411 if f.BufListener != nil { 412 opts = append(opts, grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { 413 return f.BufListener.Dial() 414 })) 415 } 416 adscConn, err := adsc.New(f.Listener.Addr().String(), &adsc.ADSConfig{ 417 Config: adsc.Config{ 418 IP: p.IPAddresses[0], 419 NodeType: p.Type, 420 Meta: p.Metadata.ToStruct(), 421 Locality: p.Locality, 422 Namespace: p.ConfigNamespace, 423 GrpcOpts: opts, 424 }, 425 InitialDiscoveryRequests: initialWatch, 426 }) 427 if err != nil { 428 f.t.Fatalf("Error connecting: %v", err) 429 } 430 f.t.Cleanup(func() { 431 adscConn.Close() 432 }) 433 return adscConn 434 } 435 436 // Connect starts an ADS connection to the server using adsc. It will automatically be cleaned up when the test ends 437 // watch can be configured to determine the resources to watch initially, and wait can be configured to determine what 438 // resources we should initially wait for. 439 func (f *FakeDiscoveryServer) Connect(p *model.Proxy, watch []string, wait []string) *adsc.ADSC { 440 f.t.Helper() 441 if watch == nil { 442 watch = []string{v3.ClusterType} 443 } 444 adscConn := f.ConnectUnstarted(p, watch) 445 if err := adscConn.Run(); err != nil { 446 f.t.Fatalf("ADSC: failed running: %v", err) 447 } 448 if len(wait) > 0 { 449 _, err := adscConn.Wait(10*time.Second, wait...) 450 if err != nil { 451 f.t.Fatalf("Error getting initial for %v config: %v", wait, err) 452 } 453 } 454 return adscConn 455 } 456 457 func (f *FakeDiscoveryServer) Endpoints(p *model.Proxy) []*endpoint.ClusterLoadAssignment { 458 loadAssignments := make([]*endpoint.ClusterLoadAssignment, 0) 459 for _, c := range xdstest.ExtractEdsClusterNames(f.Clusters(p)) { 460 builder := endpoints.NewEndpointBuilder(c, p, f.PushContext()) 461 loadAssignments = append(loadAssignments, builder.BuildClusterLoadAssignment(f.Discovery.Env.EndpointIndex)) 462 } 463 return loadAssignments 464 } 465 466 func (f *FakeDiscoveryServer) T() test.Failer { 467 return f.t 468 } 469 470 // EnsureSynced checks that all ConfigUpdates sent have been established 471 // This does NOT ensure that the change has been sent to all proxies; only that PushContext is updated 472 // Typically, if trying to ensure changes are sent, its better to wait for the push event. 473 474 func (f *FakeDiscoveryServer) EnsureSynced(t test.Failer) { 475 c := f.Discovery.InboundUpdates.Load() 476 retry.UntilOrFail(t, func() bool { 477 return f.Discovery.CommittedUpdates.Load() >= c 478 }, retry.Delay(time.Millisecond)) 479 } 480 481 func getKubernetesObjects(t test.Failer, opts FakeOptions) map[cluster.ID][]runtime.Object { 482 objects := map[cluster.ID][]runtime.Object{} 483 484 if len(opts.KubernetesObjects) > 0 { 485 objects[opts.DefaultClusterName] = append(objects[opts.DefaultClusterName], opts.KubernetesObjects...) 486 } 487 if len(opts.KubernetesObjectString) > 0 { 488 parsed, err := kubernetesObjectsFromString(opts.KubernetesObjectString) 489 if err != nil { 490 t.Fatalf("failed parsing KubernetesObjectString: %v", err) 491 } 492 objects[opts.DefaultClusterName] = append(objects[opts.DefaultClusterName], parsed...) 493 } 494 for k8sCluster, objectStr := range opts.KubernetesObjectStringByCluster { 495 parsed, err := kubernetesObjectsFromString(objectStr) 496 if err != nil { 497 t.Fatalf("failed parsing KubernetesObjectStringByCluster for %s: %v", k8sCluster, err) 498 } 499 objects[k8sCluster] = append(objects[k8sCluster], parsed...) 500 } 501 for k8sCluster, clusterObjs := range opts.KubernetesObjectsByCluster { 502 objects[k8sCluster] = append(objects[k8sCluster], clusterObjs...) 503 } 504 505 if len(objects) == 0 { 506 return map[cluster.ID][]runtime.Object{opts.DefaultClusterName: {}} 507 } 508 509 return objects 510 } 511 512 func kubernetesObjectsFromString(s string) ([]runtime.Object, error) { 513 var objects []runtime.Object 514 decode := kubelib.IstioCodec.UniversalDeserializer().Decode 515 objectStrs := strings.Split(s, "---") 516 for _, s := range objectStrs { 517 if len(strings.TrimSpace(s)) == 0 { 518 continue 519 } 520 o, _, err := decode([]byte(s), nil, nil) 521 if err != nil { 522 return nil, fmt.Errorf("failed deserializing kubernetes object: %v (%v)", err, s) 523 } 524 objects = append(objects, o) 525 } 526 return objects, nil 527 } 528 529 // DisableAuthorizationForSecret makes the authorization check always pass. Should be used only for tests. 530 func DisableAuthorizationForSecret(fake *fake.Clientset) { 531 fake.Fake.PrependReactor("create", "subjectaccessreviews", func(action k8stesting.Action) (bool, runtime.Object, error) { 532 return true, &authorizationv1.SubjectAccessReview{ 533 Status: authorizationv1.SubjectAccessReviewStatus{ 534 Allowed: true, 535 }, 536 }, nil 537 }) 538 }