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 }