istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/config/kube/crdclient/client_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 crdclient
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"reflect"
    21  	"testing"
    22  	"time"
    23  
    24  	"go.uber.org/atomic"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"k8s.io/apimachinery/pkg/types"
    27  	"sigs.k8s.io/gateway-api/pkg/consts"
    28  
    29  	"istio.io/api/meta/v1alpha1"
    30  	"istio.io/api/networking/v1alpha3"
    31  	"istio.io/api/networking/v1beta1"
    32  	clientnetworkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3"
    33  	apiistioioapinetworkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
    34  	"istio.io/istio/pilot/pkg/model"
    35  	"istio.io/istio/pkg/config"
    36  	"istio.io/istio/pkg/config/schema/collection"
    37  	"istio.io/istio/pkg/config/schema/collections"
    38  	"istio.io/istio/pkg/config/schema/gvk"
    39  	"istio.io/istio/pkg/config/schema/resource"
    40  	"istio.io/istio/pkg/kube"
    41  	"istio.io/istio/pkg/kube/controllers"
    42  	"istio.io/istio/pkg/kube/kclient/clienttest"
    43  	"istio.io/istio/pkg/kube/kubetypes"
    44  	"istio.io/istio/pkg/slices"
    45  	"istio.io/istio/pkg/test"
    46  	"istio.io/istio/pkg/test/util/assert"
    47  	"istio.io/istio/pkg/test/util/retry"
    48  )
    49  
    50  func makeClient(t *testing.T, schemas collection.Schemas, f kubetypes.DynamicObjectFilter) (model.ConfigStoreController, kube.CLIClient) {
    51  	fake := kube.NewFakeClient()
    52  	if f != nil {
    53  		kube.SetObjectFilter(fake, f)
    54  	}
    55  	for _, s := range schemas.All() {
    56  		var annotations map[string]string
    57  		if s.Group() == gvk.KubernetesGateway.Group {
    58  			annotations = map[string]string{
    59  				consts.BundleVersionAnnotation: consts.BundleVersion,
    60  			}
    61  		}
    62  		clienttest.MakeCRDWithAnnotations(t, fake, s.GroupVersionResource(), annotations)
    63  	}
    64  	stop := test.NewStop(t)
    65  	config := New(fake, Option{})
    66  	go config.Run(stop)
    67  	fake.RunAndWait(stop)
    68  	kube.WaitForCacheSync("test", stop, config.HasSynced)
    69  	return config, fake
    70  }
    71  
    72  func createResource(t *testing.T, store model.ConfigStoreController, r resource.Schema, configMeta config.Meta) config.Spec {
    73  	pb, err := r.NewInstance()
    74  	if err != nil {
    75  		t.Fatal(err)
    76  	}
    77  
    78  	if _, err := store.Create(config.Config{
    79  		Meta: configMeta,
    80  		Spec: pb,
    81  	}); err != nil {
    82  		t.Fatalf("Create => got %v", err)
    83  	}
    84  
    85  	return pb
    86  }
    87  
    88  // Ensure that the client can run without CRDs present
    89  func TestClientNoCRDs(t *testing.T) {
    90  	schema := collection.NewSchemasBuilder().MustAdd(collections.Sidecar).Build()
    91  	store, _ := makeClient(t, schema, nil)
    92  	retry.UntilOrFail(t, store.HasSynced, retry.Timeout(time.Second))
    93  	r := collections.VirtualService
    94  	configMeta := config.Meta{
    95  		Name:             "name",
    96  		Namespace:        "ns",
    97  		GroupVersionKind: r.GroupVersionKind(),
    98  	}
    99  	createResource(t, store, r, configMeta)
   100  
   101  	retry.UntilSuccessOrFail(t, func() error {
   102  		l := store.List(r.GroupVersionKind(), configMeta.Namespace)
   103  		if len(l) != 0 {
   104  			return fmt.Errorf("expected no items returned for unknown CRD, got %v", l)
   105  		}
   106  		return nil
   107  	}, retry.Timeout(time.Second*5), retry.Converge(5))
   108  	retry.UntilOrFail(t, func() bool {
   109  		return store.Get(r.GroupVersionKind(), configMeta.Name, configMeta.Namespace) == nil
   110  	}, retry.Message("expected no items returned for unknown CRD"), retry.Timeout(time.Second*5), retry.Converge(5))
   111  }
   112  
   113  // Ensure that the client can run without CRDs present, but then added later
   114  func TestClientDelayedCRDs(t *testing.T) {
   115  	// ns1 is allowed, ns2 is not
   116  	f := kubetypes.NewStaticObjectFilter(func(obj interface{}) bool {
   117  		// When an object is deleted, obj could be a DeletionFinalStateUnknown marker item.
   118  		object := controllers.ExtractObject(obj)
   119  		if object == nil {
   120  			return false
   121  		}
   122  		ns := object.GetNamespace()
   123  		return ns == "ns1"
   124  	})
   125  	schema := collection.NewSchemasBuilder().MustAdd(collections.Sidecar).Build()
   126  	store, fake := makeClient(t, schema, f)
   127  	retry.UntilOrFail(t, store.HasSynced, retry.Timeout(time.Second))
   128  	r := collections.VirtualService
   129  
   130  	// Create a virtual service
   131  	configMeta1 := config.Meta{
   132  		Name:             "name1",
   133  		Namespace:        "ns1",
   134  		GroupVersionKind: r.GroupVersionKind(),
   135  	}
   136  	createResource(t, store, r, configMeta1)
   137  
   138  	configMeta2 := config.Meta{
   139  		Name:             "name2",
   140  		Namespace:        "ns2",
   141  		GroupVersionKind: r.GroupVersionKind(),
   142  	}
   143  	createResource(t, store, r, configMeta2)
   144  
   145  	retry.UntilSuccessOrFail(t, func() error {
   146  		l := store.List(r.GroupVersionKind(), "")
   147  		if len(l) != 0 {
   148  			return fmt.Errorf("expected no items returned for unknown CRD")
   149  		}
   150  		return nil
   151  	}, retry.Timeout(time.Second*5), retry.Converge(5))
   152  
   153  	clienttest.MakeCRD(t, fake, r.GroupVersionResource())
   154  
   155  	retry.UntilSuccessOrFail(t, func() error {
   156  		l := store.List(r.GroupVersionKind(), "")
   157  		if len(l) != 1 {
   158  			return fmt.Errorf("expected items returned")
   159  		}
   160  		if l[0].Name != configMeta1.Name {
   161  			return fmt.Errorf("expected `name1` returned")
   162  		}
   163  		return nil
   164  	}, retry.Timeout(time.Second*10), retry.Converge(5))
   165  }
   166  
   167  // CheckIstioConfigTypes validates that an empty store can do CRUD operators on all given types
   168  func TestClient(t *testing.T) {
   169  	store, _ := makeClient(t, collections.PilotGatewayAPI().Union(collections.Kube), nil)
   170  	configName := "test"
   171  	configNamespace := "test-ns"
   172  	timeout := retry.Timeout(time.Millisecond * 200)
   173  	for _, r := range collections.PilotGatewayAPI().All() {
   174  		name := r.Kind()
   175  		t.Run(name, func(t *testing.T) {
   176  			configMeta := config.Meta{
   177  				GroupVersionKind: r.GroupVersionKind(),
   178  				Name:             configName,
   179  			}
   180  			if !r.IsClusterScoped() {
   181  				configMeta.Namespace = configNamespace
   182  			}
   183  			pb := createResource(t, store, r, configMeta)
   184  
   185  			// Kubernetes is eventually consistent, so we allow a short time to pass before we get
   186  			retry.UntilSuccessOrFail(t, func() error {
   187  				cfg := store.Get(r.GroupVersionKind(), configName, configMeta.Namespace)
   188  				if cfg == nil || !reflect.DeepEqual(cfg.Meta, configMeta) {
   189  					return fmt.Errorf("get(%v) => got unexpected object %v", name, cfg)
   190  				}
   191  				return nil
   192  			}, timeout)
   193  
   194  			// Validate it shows up in List
   195  			retry.UntilSuccessOrFail(t, func() error {
   196  				cfgs := store.List(r.GroupVersionKind(), configMeta.Namespace)
   197  				if len(cfgs) != 1 {
   198  					return fmt.Errorf("expected 1 config, got %v", len(cfgs))
   199  				}
   200  				for _, cfg := range cfgs {
   201  					if !reflect.DeepEqual(cfg.Meta, configMeta) {
   202  						return fmt.Errorf("get(%v) => got %v", name, cfg)
   203  					}
   204  				}
   205  				return nil
   206  			}, timeout)
   207  
   208  			// check we can update object metadata
   209  			annotations := map[string]string{
   210  				"foo": "bar",
   211  			}
   212  			configMeta.Annotations = annotations
   213  			if _, err := store.Update(config.Config{
   214  				Meta: configMeta,
   215  				Spec: pb,
   216  			}); err != nil {
   217  				t.Errorf("Unexpected Error in Update -> %v", err)
   218  			}
   219  			if r.StatusKind() != "" {
   220  				stat, err := r.Status()
   221  				if err != nil {
   222  					t.Fatal(err)
   223  				}
   224  				if _, err := store.UpdateStatus(config.Config{
   225  					Meta:   configMeta,
   226  					Status: stat,
   227  				}); err != nil {
   228  					t.Errorf("Unexpected Error in Update -> %v", err)
   229  				}
   230  			}
   231  			var cfg *config.Config
   232  			// validate it is updated
   233  			retry.UntilSuccessOrFail(t, func() error {
   234  				cfg = store.Get(r.GroupVersionKind(), configName, configMeta.Namespace)
   235  				if cfg == nil || !reflect.DeepEqual(cfg.Meta, configMeta) {
   236  					return fmt.Errorf("get(%v) => got unexpected object %v", name, cfg)
   237  				}
   238  				return nil
   239  			})
   240  
   241  			// check we can patch items
   242  			var patchedCfg config.Config
   243  			if _, err := store.(*Client).Patch(*cfg, func(cfg config.Config) (config.Config, types.PatchType) {
   244  				cfg.Annotations["fizz"] = "buzz"
   245  				patchedCfg = cfg
   246  				return cfg, types.JSONPatchType
   247  			}); err != nil {
   248  				t.Errorf("unexpected err in Patch: %v", err)
   249  			}
   250  			// validate it is updated
   251  			retry.UntilSuccessOrFail(t, func() error {
   252  				cfg := store.Get(r.GroupVersionKind(), configName, configMeta.Namespace)
   253  				if cfg == nil || !reflect.DeepEqual(cfg.Meta, patchedCfg.Meta) {
   254  					return fmt.Errorf("get(%v) => got unexpected object %v", name, cfg)
   255  				}
   256  				return nil
   257  			})
   258  
   259  			// Check we can remove items
   260  			if err := store.Delete(r.GroupVersionKind(), configName, configNamespace, nil); err != nil {
   261  				t.Fatalf("failed to delete: %v", err)
   262  			}
   263  			retry.UntilSuccessOrFail(t, func() error {
   264  				cfg := store.Get(r.GroupVersionKind(), configName, configNamespace)
   265  				if cfg != nil {
   266  					return fmt.Errorf("get(%v) => got %v, expected item to be deleted", name, cfg)
   267  				}
   268  				return nil
   269  			}, timeout)
   270  		})
   271  	}
   272  
   273  	t.Run("update status", func(t *testing.T) {
   274  		r := collections.WorkloadGroup
   275  		name := "name1"
   276  		namespace := "bar"
   277  		cfgMeta := config.Meta{
   278  			GroupVersionKind: r.GroupVersionKind(),
   279  			Name:             name,
   280  		}
   281  		if !r.IsClusterScoped() {
   282  			cfgMeta.Namespace = namespace
   283  		}
   284  		pb := &v1alpha3.WorkloadGroup{Probe: &v1alpha3.ReadinessProbe{PeriodSeconds: 6}}
   285  		if _, err := store.Create(config.Config{
   286  			Meta: cfgMeta,
   287  			Spec: config.Spec(pb),
   288  		}); err != nil {
   289  			t.Fatalf("Create bad: %v", err)
   290  		}
   291  
   292  		retry.UntilSuccessOrFail(t, func() error {
   293  			cfg := store.Get(r.GroupVersionKind(), name, cfgMeta.Namespace)
   294  			if cfg == nil {
   295  				return fmt.Errorf("cfg shouldn't be nil :(")
   296  			}
   297  			if !reflect.DeepEqual(cfg.Meta, cfgMeta) {
   298  				return fmt.Errorf("something is deeply wrong....., %v", cfg.Meta)
   299  			}
   300  			return nil
   301  		})
   302  
   303  		stat := &v1alpha1.IstioStatus{
   304  			Conditions: []*v1alpha1.IstioCondition{
   305  				{
   306  					Type:    "Health",
   307  					Message: "heath is badd",
   308  				},
   309  			},
   310  		}
   311  
   312  		if _, err := store.UpdateStatus(config.Config{
   313  			Meta:   cfgMeta,
   314  			Spec:   config.Spec(pb),
   315  			Status: config.Status(stat),
   316  		}); err != nil {
   317  			t.Errorf("bad: %v", err)
   318  		}
   319  
   320  		retry.UntilSuccessOrFail(t, func() error {
   321  			cfg := store.Get(r.GroupVersionKind(), name, cfgMeta.Namespace)
   322  			if cfg == nil {
   323  				return fmt.Errorf("cfg can't be nil")
   324  			}
   325  			if !reflect.DeepEqual(cfg.Status, stat) {
   326  				return fmt.Errorf("status %v does not match %v", cfg.Status, stat)
   327  			}
   328  			return nil
   329  		})
   330  	})
   331  }
   332  
   333  // TestClientInitialSyncSkipsOtherRevisions tests that the initial sync skips objects from other
   334  // revisions.
   335  func TestClientInitialSyncSkipsOtherRevisions(t *testing.T) {
   336  	fake := kube.NewFakeClient()
   337  	for _, s := range collections.Istio.All() {
   338  		clienttest.MakeCRD(t, fake, s.GroupVersionResource())
   339  	}
   340  
   341  	// Populate the client with some ServiceEntrys such that 1/3 are in the default revision and
   342  	// 2/3 are in different revisions.
   343  	labels := []map[string]string{
   344  		nil,
   345  		{"istio.io/rev": "canary"},
   346  		{"istio.io/rev": "prod"},
   347  	}
   348  	var expectedNoRevision []config.Config
   349  	var expectedCanary []config.Config
   350  	var expectedProd []config.Config
   351  	for i := 0; i < 9; i++ {
   352  		selectedLabels := labels[i%len(labels)]
   353  		obj := &clientnetworkingv1alpha3.ServiceEntry{
   354  			ObjectMeta: metav1.ObjectMeta{
   355  				Name:      fmt.Sprintf("test-service-entry-%d", i),
   356  				Namespace: "test",
   357  				Labels:    selectedLabels,
   358  			},
   359  			Spec: v1alpha3.ServiceEntry{},
   360  		}
   361  
   362  		clienttest.NewWriter[*clientnetworkingv1alpha3.ServiceEntry](t, fake).Create(obj)
   363  		// canary revision should receive only global objects and objects with the canary revision
   364  		if selectedLabels == nil || reflect.DeepEqual(selectedLabels, labels[1]) {
   365  			expectedCanary = append(expectedCanary, TranslateObject(obj, gvk.ServiceEntry, ""))
   366  		}
   367  		// prod revision should receive only global objects and objects with the prod revision
   368  		if selectedLabels == nil || reflect.DeepEqual(selectedLabels, labels[2]) {
   369  			expectedProd = append(expectedProd, TranslateObject(obj, gvk.ServiceEntry, ""))
   370  		}
   371  		// no revision should receive all objects
   372  		expectedNoRevision = append(expectedNoRevision, TranslateObject(obj, gvk.ServiceEntry, ""))
   373  	}
   374  
   375  	storeCases := map[string][]config.Config{
   376  		"":       expectedNoRevision, // No revision specified, should receive all events.
   377  		"canary": expectedCanary,     // Only SEs from the canary revision should be received.
   378  		"prod":   expectedProd,       // Only SEs from the prod revision should be received.
   379  	}
   380  	for rev, expected := range storeCases {
   381  		store := New(fake, Option{
   382  			Revision: rev,
   383  		})
   384  
   385  		var cfgsAdded []config.Config
   386  		store.RegisterEventHandler(
   387  			gvk.ServiceEntry,
   388  			func(old config.Config, curr config.Config, event model.Event) {
   389  				if event != model.EventAdd {
   390  					t.Fatalf("unexpected event: %v", event)
   391  				}
   392  				cfgsAdded = append(cfgsAdded, curr)
   393  			},
   394  		)
   395  
   396  		stop := test.NewStop(t)
   397  		fake.RunAndWait(stop)
   398  		go store.Run(stop)
   399  
   400  		kube.WaitForCacheSync("test", stop, store.HasSynced)
   401  
   402  		// The order of the events doesn't matter, so sort the two slices so the ordering is consistent
   403  		sortFunc := func(a config.Config) string {
   404  			return a.Key()
   405  		}
   406  		slices.SortBy(cfgsAdded, sortFunc)
   407  		slices.SortBy(expected, sortFunc)
   408  
   409  		assert.Equal(t, expected, cfgsAdded)
   410  	}
   411  }
   412  
   413  func TestClientSync(t *testing.T) {
   414  	obj := &clientnetworkingv1alpha3.ServiceEntry{
   415  		ObjectMeta: metav1.ObjectMeta{
   416  			Name:      "test-service-entry",
   417  			Namespace: "test",
   418  		},
   419  		Spec: v1alpha3.ServiceEntry{},
   420  	}
   421  	fake := kube.NewFakeClient()
   422  	clienttest.NewWriter[*clientnetworkingv1alpha3.ServiceEntry](t, fake).Create(obj)
   423  	for _, s := range collections.Pilot.All() {
   424  		clienttest.MakeCRD(t, fake, s.GroupVersionResource())
   425  	}
   426  	stop := test.NewStop(t)
   427  	c := New(fake, Option{})
   428  
   429  	events := atomic.NewInt64(0)
   430  	c.RegisterEventHandler(gvk.ServiceEntry, func(c config.Config, c2 config.Config, event model.Event) {
   431  		events.Inc()
   432  	})
   433  	go c.Run(stop)
   434  	fake.RunAndWait(stop)
   435  	kube.WaitForCacheSync("test", stop, c.HasSynced)
   436  	// This MUST have been called by the time HasSynced returns true
   437  	assert.Equal(t, events.Load(), 1)
   438  }
   439  
   440  func TestAlternativeVersions(t *testing.T) {
   441  	fake := kube.NewFakeClient()
   442  	fake.RunAndWait(test.NewStop(t))
   443  	vs := apiistioioapinetworkingv1beta1.VirtualService{
   444  		TypeMeta:   metav1.TypeMeta{},
   445  		ObjectMeta: metav1.ObjectMeta{Name: "oo"},
   446  		Spec:       v1beta1.VirtualService{Hosts: []string{"hello"}},
   447  		Status:     v1alpha1.IstioStatus{},
   448  	}
   449  	_, err := fake.Istio().NetworkingV1beta1().VirtualServices("test").Create(context.Background(), &vs, metav1.CreateOptions{})
   450  	assert.NoError(t, err)
   451  }