| // Copyright Istio Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package mock |
| |
| import ( |
| "fmt" |
| "reflect" |
| "strconv" |
| "testing" |
| "time" |
| ) |
| |
| import ( |
| "go.uber.org/atomic" |
| networking "istio.io/api/networking/v1alpha3" |
| authz "istio.io/api/security/v1beta1" |
| api "istio.io/api/type/v1beta1" |
| "istio.io/pkg/log" |
| ) |
| |
| import ( |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/model" |
| config2 "github.com/apache/dubbo-go-pixiu/pkg/config" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/schema/collection" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/schema/collections" |
| "github.com/apache/dubbo-go-pixiu/pkg/test/config" |
| "github.com/apache/dubbo-go-pixiu/pkg/test/util/retry" |
| ) |
| |
| var ( |
| // ExampleVirtualService is an example V2 route rule |
| ExampleVirtualService = &networking.VirtualService{ |
| Hosts: []string{"prod", "test"}, |
| Http: []*networking.HTTPRoute{ |
| { |
| Route: []*networking.HTTPRouteDestination{ |
| { |
| Destination: &networking.Destination{ |
| Host: "job", |
| }, |
| Weight: 80, |
| }, |
| }, |
| }, |
| }, |
| } |
| |
| ExampleServiceEntry = &networking.ServiceEntry{ |
| Hosts: []string{"*.google.com"}, |
| Resolution: networking.ServiceEntry_NONE, |
| Ports: []*networking.Port{ |
| {Number: 80, Name: "http-name", Protocol: "http"}, |
| {Number: 8080, Name: "http2-name", Protocol: "http2"}, |
| }, |
| } |
| |
| ExampleGateway = &networking.Gateway{ |
| Servers: []*networking.Server{ |
| { |
| Hosts: []string{"google.com"}, |
| Port: &networking.Port{Name: "http", Protocol: "http", Number: 10080}, |
| }, |
| }, |
| } |
| |
| // ExampleDestinationRule is an example destination rule |
| ExampleDestinationRule = &networking.DestinationRule{ |
| Host: "ratings", |
| TrafficPolicy: &networking.TrafficPolicy{ |
| LoadBalancer: &networking.LoadBalancerSettings{ |
| LbPolicy: new(networking.LoadBalancerSettings_Simple), |
| }, |
| }, |
| } |
| |
| // ExampleAuthorizationPolicy is an example AuthorizationPolicy |
| ExampleAuthorizationPolicy = &authz.AuthorizationPolicy{ |
| Selector: &api.WorkloadSelector{ |
| MatchLabels: map[string]string{ |
| "app": "httpbin", |
| "version": "v1", |
| }, |
| }, |
| } |
| |
| mockGvk = collections.Mock.Resource().GroupVersionKind() |
| ) |
| |
| // Make creates a mock config indexed by a number |
| func Make(namespace string, i int) config2.Config { |
| name := fmt.Sprintf("%s%d", "mock-config", i) |
| return config2.Config{ |
| Meta: config2.Meta{ |
| GroupVersionKind: mockGvk, |
| Name: name, |
| Namespace: namespace, |
| Labels: map[string]string{ |
| "key": name, |
| }, |
| Annotations: map[string]string{ |
| "annotationkey": name, |
| }, |
| }, |
| Spec: &config.MockConfig{ |
| Key: name, |
| Pairs: []*config.ConfigPair{ |
| {Key: "key", Value: strconv.Itoa(i)}, |
| }, |
| }, |
| } |
| } |
| |
| // Compare checks two configs ignoring revisions and creation time |
| func Compare(a, b config2.Config) bool { |
| a.ResourceVersion = "" |
| b.ResourceVersion = "" |
| a.CreationTimestamp = time.Time{} |
| b.CreationTimestamp = time.Time{} |
| return reflect.DeepEqual(a, b) |
| } |
| |
| // CheckMapInvariant validates operational invariants of an empty config registry |
| func CheckMapInvariant(r model.ConfigStore, t *testing.T, namespace string, n int) { |
| // check that the config descriptor is the mock config descriptor |
| _, contains := r.Schemas().FindByGroupVersionKind(mockGvk) |
| if !contains { |
| t.Fatal("expected config mock types") |
| } |
| log.Info("Created mock descriptor") |
| |
| // create configuration objects |
| elts := make(map[int]config2.Config) |
| for i := 0; i < n; i++ { |
| elts[i] = Make(namespace, i) |
| } |
| log.Info("Make mock objects") |
| |
| // post all elements |
| for _, elt := range elts { |
| if _, err := r.Create(elt); err != nil { |
| t.Error(err) |
| } |
| } |
| log.Info("Created mock objects") |
| |
| revs := make(map[int]string) |
| |
| // check that elements are stored |
| for i, elt := range elts { |
| v1 := r.Get(mockGvk, elt.Name, elt.Namespace) |
| if v1 == nil || !Compare(elt, *v1) { |
| t.Errorf("wanted %v, got %v", elt, v1) |
| } else { |
| revs[i] = v1.ResourceVersion |
| } |
| } |
| |
| log.Info("Got stored elements") |
| |
| if _, err := r.Create(elts[0]); err == nil { |
| t.Error("expected error posting twice") |
| } |
| |
| invalid := config2.Config{ |
| Meta: config2.Meta{ |
| GroupVersionKind: mockGvk, |
| Name: "invalid", |
| ResourceVersion: revs[0], |
| }, |
| Spec: &config.MockConfig{}, |
| } |
| |
| missing := config2.Config{ |
| Meta: config2.Meta{ |
| GroupVersionKind: mockGvk, |
| Name: "missing", |
| ResourceVersion: revs[0], |
| }, |
| Spec: &config.MockConfig{Key: "missing"}, |
| } |
| |
| if _, err := r.Create(config2.Config{}); err == nil { |
| t.Error("expected error posting empty object") |
| } |
| |
| if _, err := r.Create(invalid); err == nil { |
| t.Error("expected error posting invalid object") |
| } |
| |
| if _, err := r.Update(config2.Config{}); err == nil { |
| t.Error("expected error updating empty object") |
| } |
| |
| if _, err := r.Update(invalid); err == nil { |
| t.Error("expected error putting invalid object") |
| } |
| |
| if _, err := r.Update(missing); err == nil { |
| t.Error("expected error putting missing object with a missing key") |
| } |
| |
| // check for missing type |
| if l, _ := r.List(config2.GroupVersionKind{}, namespace); len(l) > 0 { |
| t.Errorf("unexpected objects for missing type") |
| } |
| |
| // check for missing element |
| if cfg := r.Get(mockGvk, "missing", ""); cfg != nil { |
| t.Error("unexpected configuration object found") |
| } |
| |
| // check for missing element |
| if cfg := r.Get(config2.GroupVersionKind{}, "missing", ""); cfg != nil { |
| t.Error("unexpected configuration object found") |
| } |
| |
| // delete missing elements |
| if err := r.Delete(config2.GroupVersionKind{}, "missing", "", nil); err == nil { |
| t.Error("expected error on deletion of missing type") |
| } |
| |
| // delete missing elements |
| if err := r.Delete(mockGvk, "missing", "", nil); err == nil { |
| t.Error("expected error on deletion of missing element") |
| } |
| if err := r.Delete(mockGvk, "missing", "unknown", nil); err == nil { |
| t.Error("expected error on deletion of missing element in unknown namespace") |
| } |
| |
| // list elements |
| l, err := r.List(mockGvk, namespace) |
| if err != nil { |
| t.Errorf("List error %#v, %v", l, err) |
| } |
| if len(l) != n { |
| t.Errorf("wanted %d element(s), got %d in %v", n, len(l), l) |
| } |
| |
| // update all elements |
| for i := 0; i < n; i++ { |
| elt := Make(namespace, i) |
| elt.Spec.(*config.MockConfig).Pairs[0].Value += "(updated)" |
| elt.ResourceVersion = revs[i] |
| elts[i] = elt |
| if _, err = r.Update(elt); err != nil { |
| t.Error(err) |
| } |
| } |
| |
| // check that elements are stored |
| for i, elt := range elts { |
| v1 := r.Get(mockGvk, elts[i].Name, elts[i].Namespace) |
| if v1 == nil || !Compare(elt, *v1) { |
| t.Errorf("wanted %v, got %v", elt, v1) |
| } |
| } |
| |
| // delete all elements |
| for i := range elts { |
| if err = r.Delete(mockGvk, elts[i].Name, elts[i].Namespace, nil); err != nil { |
| t.Error(err) |
| } |
| } |
| log.Info("Delete elements") |
| |
| l, err = r.List(mockGvk, namespace) |
| if err != nil { |
| t.Error(err) |
| } |
| if len(l) != 0 { |
| t.Errorf("wanted 0 element(s), got %d in %v", len(l), l) |
| } |
| log.Info("Test done, deleting namespace") |
| } |
| |
| // CheckIstioConfigTypes validates that an empty store can ingest Istio config objects |
| func CheckIstioConfigTypes(store model.ConfigStore, namespace string, t *testing.T) { |
| configName := "example" |
| // Global scoped policies like MeshPolicy are not isolated, can't be |
| // run as part of the normal test suites - if needed they should |
| // be run in separate environment. The test suites are setting cluster |
| // scoped policies that may interfere and would require serialization |
| |
| cases := []struct { |
| name string |
| configName string |
| schema collection.Schema |
| spec config2.Spec |
| }{ |
| {"VirtualService", configName, collections.IstioNetworkingV1Alpha3Virtualservices, ExampleVirtualService}, |
| {"DestinationRule", configName, collections.IstioNetworkingV1Alpha3Destinationrules, ExampleDestinationRule}, |
| {"ServiceEntry", configName, collections.IstioNetworkingV1Alpha3Serviceentries, ExampleServiceEntry}, |
| {"Gateway", configName, collections.IstioNetworkingV1Alpha3Gateways, ExampleGateway}, |
| {"AuthorizationPolicy", configName, collections.IstioSecurityV1Beta1Authorizationpolicies, ExampleAuthorizationPolicy}, |
| } |
| |
| for _, c := range cases { |
| t.Run(c.name, func(t *testing.T) { |
| configMeta := config2.Meta{ |
| GroupVersionKind: c.schema.Resource().GroupVersionKind(), |
| Name: c.configName, |
| } |
| if !c.schema.Resource().IsClusterScoped() { |
| configMeta.Namespace = namespace |
| } |
| |
| if _, err := store.Create(config2.Config{ |
| Meta: configMeta, |
| Spec: c.spec, |
| }); err != nil { |
| t.Errorf("Post(%v) => got %v", c.name, err) |
| } |
| }) |
| } |
| } |
| |
| // CheckCacheEvents validates operational invariants of a cache |
| func CheckCacheEvents(store model.ConfigStore, cache model.ConfigStoreController, namespace string, n int, t *testing.T) { |
| n64 := int64(n) |
| stop := make(chan struct{}) |
| defer close(stop) |
| added, deleted := atomic.NewInt64(0), atomic.NewInt64(0) |
| cache.RegisterEventHandler(mockGvk, func(_, _ config2.Config, ev model.Event) { |
| switch ev { |
| case model.EventAdd: |
| if deleted.Load() != 0 { |
| t.Errorf("Events are not serialized (add)") |
| } |
| added.Inc() |
| case model.EventDelete: |
| if added.Load() != n64 { |
| t.Errorf("Events are not serialized (delete)") |
| } |
| deleted.Inc() |
| } |
| log.Infof("Added %d, deleted %d", added.Load(), deleted.Load()) |
| }) |
| go cache.Run(stop) |
| |
| // run map invariant sequence |
| CheckMapInvariant(store, t, namespace, n) |
| |
| log.Infof("Waiting till all events are received") |
| retry.UntilOrFail(t, func() bool { |
| return added.Load() == n64 && deleted.Load() == n64 |
| }, retry.Message("receive events"), retry.Delay(time.Millisecond*500), retry.Timeout(time.Minute)) |
| } |
| |
| // CheckCacheFreshness validates operational invariants of a cache |
| func CheckCacheFreshness(cache model.ConfigStoreController, namespace string, t *testing.T) { |
| stop := make(chan struct{}) |
| done := make(chan bool) |
| o := Make(namespace, 0) |
| |
| // validate cache consistency |
| cache.RegisterEventHandler(mockGvk, func(_, config config2.Config, ev model.Event) { |
| elts, _ := cache.List(mockGvk, namespace) |
| elt := cache.Get(o.GroupVersionKind, o.Name, o.Namespace) |
| switch ev { |
| case model.EventAdd: |
| if len(elts) != 1 { |
| t.Errorf("Got %#v, expected %d element(s) on Add event", elts, 1) |
| } |
| if elt == nil || !reflect.DeepEqual(elt.Spec, o.Spec) { |
| t.Errorf("Got %#v, expected %#v", elt, o) |
| } |
| |
| log.Infof("Calling Update(%s)", config.Key()) |
| revised := Make(namespace, 1) |
| revised.Meta = elt.Meta |
| if _, err := cache.Update(revised); err != nil { |
| t.Error(err) |
| } |
| case model.EventUpdate: |
| if len(elts) != 1 { |
| t.Errorf("Got %#v, expected %d element(s) on Update event", elts, 1) |
| } |
| if elt == nil { |
| t.Errorf("Got %#v, expected nonempty", elt) |
| } |
| |
| log.Infof("Calling Delete(%s)", config.Key()) |
| if err := cache.Delete(mockGvk, config.Name, config.Namespace, nil); err != nil { |
| t.Error(err) |
| } |
| case model.EventDelete: |
| if len(elts) != 0 { |
| t.Errorf("Got %#v, expected zero elements on Delete event", elts) |
| } |
| log.Infof("Stopping channel for (%#v)", config.Key()) |
| close(stop) |
| done <- true |
| } |
| }) |
| |
| go cache.Run(stop) |
| |
| // try warm-up with empty Get |
| if cfg := cache.Get(config2.GroupVersionKind{}, "example", namespace); cfg != nil { |
| t.Error("unexpected result for unknown type") |
| } |
| |
| // add and remove |
| log.Infof("Calling Create(%#v)", o) |
| if _, err := cache.Create(o); err != nil { |
| t.Error(err) |
| } |
| |
| timeout := time.After(10 * time.Second) |
| select { |
| case <-timeout: |
| t.Fatalf("timeout waiting to be done") |
| case <-done: |
| return |
| } |
| } |
| |
| // CheckCacheSync validates operational invariants of a cache against the |
| // non-cached client. |
| func CheckCacheSync(store model.ConfigStore, cache model.ConfigStoreController, namespace string, n int, t *testing.T) { |
| keys := make(map[int]config2.Config) |
| // add elements directly through client |
| for i := 0; i < n; i++ { |
| keys[i] = Make(namespace, i) |
| if _, err := store.Create(keys[i]); err != nil { |
| t.Error(err) |
| } |
| } |
| |
| // check in the controller cache |
| stop := make(chan struct{}) |
| defer close(stop) |
| go cache.Run(stop) |
| retry.UntilOrFail(t, cache.HasSynced, retry.Message("HasSynced")) |
| os, _ := cache.List(mockGvk, namespace) |
| if len(os) != n { |
| t.Errorf("cache.List => Got %d, expected %d", len(os), n) |
| } |
| |
| // remove elements directly through client |
| for i := 0; i < n; i++ { |
| if err := store.Delete(mockGvk, keys[i].Name, keys[i].Namespace, nil); err != nil { |
| t.Error(err) |
| } |
| } |
| |
| // check again in the controller cache |
| retry.UntilOrFail(t, func() bool { |
| os, _ = cache.List(mockGvk, namespace) |
| log.Infof("cache.List => Got %d, expected %d", len(os), 0) |
| return len(os) == 0 |
| }, retry.Message("no elements in cache")) |
| |
| // now add through the controller |
| for i := 0; i < n; i++ { |
| if _, err := cache.Create(Make(namespace, i)); err != nil { |
| t.Error(err) |
| } |
| } |
| |
| // check directly through the client |
| retry.UntilOrFail(t, func() bool { |
| cs, _ := cache.List(mockGvk, namespace) |
| os, _ := store.List(mockGvk, namespace) |
| log.Infof("cache.List => Got %d, expected %d", len(cs), n) |
| log.Infof("store.List => Got %d, expected %d", len(os), n) |
| return len(os) == n && len(cs) == n |
| }, retry.Message("cache and backing store match")) |
| |
| // remove elements directly through the client |
| for i := 0; i < n; i++ { |
| if err := store.Delete(mockGvk, keys[i].Name, keys[i].Namespace, nil); err != nil { |
| t.Error(err) |
| } |
| } |
| } |