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  }