diff --git a/DESIGN.md b/DESIGN.md index a78a92fd89..a10f1bab04 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -425,37 +425,32 @@ The `MeshSpec` implementation **has no awareness** of: ```go // MeshSpec is an interface declaring functions, which provide the specs for a service mesh declared with SMI. type MeshSpec interface { - // ListTrafficSplits lists TrafficSplit SMI resources. + // ListTrafficSplits lists SMI TrafficSplit resources ListTrafficSplits() []*split.TrafficSplit - // ListTrafficSplitServices fetches all services declared with SMI Spec. + // ListTrafficSplitServices lists WeightedServices for the services specified in TrafficSplit SMI resources ListTrafficSplitServices() []service.WeightedService - // ListServiceAccounts fetches all service accounts declared with SMI Spec. + // ListServiceAccounts lists ServiceAccount resources specified in SMI TrafficTarget resources ListServiceAccounts() []service.K8sServiceAccount - // GetService fetches a specific service declared in SMI. + // GetService fetches a Kubernetes Service resource for the given MeshService GetService(service.MeshService) *corev1.Service - // ListHTTPTrafficSpecs lists TrafficSpec SMI resources. + // ListServices Lists Kubernets Service resources that are part of monitored namespaces + ListServices() []*corev1.Service + + // ListHTTPTrafficSpecs lists SMI HTTPRouteGroup resources ListHTTPTrafficSpecs() []*spec.HTTPRouteGroup - // ListTrafficTargets lists TrafficTarget SMI resources. + // ListTrafficTargets lists SMI TrafficTarget resources ListTrafficTargets() []*target.TrafficTarget - // ListBackpressures lists Backpressure resources. - // This is an experimental feature, which will eventually - // in some shape or form make its way into SMI Spec. - ListBackpressures() []*backpressure.Backpressure - - // GetBackpressurePolicy gets the Backpressure policy corresponding to the MeshService + // GetBackpressurePolicy fetches the Backpressure policy for the MeshService GetBackpressurePolicy(service.MeshService) *backpressure.Backpressure - // GetAnnouncementsChannel returns the channel on which SMI makes announcements + // GetAnnouncementsChannel returns the channel on which SMI client makes announcements GetAnnouncementsChannel() <-chan interface{} - - // ListServices returns a list of services that are part of monitored namespaces - ListServices() []*corev1.Service } ``` diff --git a/go.sum b/go.sum index 0dee3db3a8..182bb53f75 100644 --- a/go.sum +++ b/go.sum @@ -1230,6 +1230,7 @@ k8s.io/cli-runtime v0.18.0 h1:jG8XpSqQ5TrV0N+EZ3PFz6+gqlCk71dkggWCCq9Mq34= k8s.io/cli-runtime v0.18.0/go.mod h1:1eXfmBsIJosjn9LjEBUd2WVPoPAY9XGTqTFcPMIBsUQ= k8s.io/client-go v0.18.0 h1:yqKw4cTUQraZK3fcVCMeSa+lqKwcjZ5wtcOIPnxQno4= k8s.io/client-go v0.18.0/go.mod h1:uQSYDYs4WhVZ9i6AIoEZuwUggLVEF64HOD37boKAtF8= +k8s.io/client-go v11.0.0+incompatible h1:LBbX2+lOwY9flffWlJM7f1Ct8V2SRNiMRDFeiwnJo9o= k8s.io/code-generator v0.18.0/go.mod h1:+UHX5rSbxmR8kzS+FAv7um6dtYrZokQvjHpDSYRVkTc= k8s.io/component-base v0.18.0 h1:I+lP0fNfsEdTDpHaL61bCAqTZLoiWjEEP304Mo5ZQgE= k8s.io/component-base v0.18.0/go.mod h1:u3BCg0z1uskkzrnAKFzulmYaEpZF7XC9Pf/uFyb1v2c= diff --git a/pkg/smi/client.go b/pkg/smi/client.go index d59ef9cc7b..4670304074 100644 --- a/pkg/smi/client.go +++ b/pkg/smi/client.go @@ -43,7 +43,7 @@ func NewMeshSpecClient(smiKubeConfig *rest.Config, kubeClient kubernetes.Interfa backpressureClientSet = backpressureClient.NewForConfigOrDie(smiKubeConfig) } - client := newSMIClient( + client, err := newSMIClient( kubeClient, smiTrafficSplitClientSet, smiTrafficSpecClientSet, @@ -52,13 +52,10 @@ func NewMeshSpecClient(smiKubeConfig *rest.Config, kubeClient kubernetes.Interfa osmNamespace, namespaceController, kubernetesClientName, + stop, ) - err := client.run(stop) - if err != nil { - return client, errors.Errorf("Could not start %s client", kubernetesClientName) - } - return client, nil + return client, err } func (c *Client) run(stop <-chan struct{}) error { @@ -104,18 +101,13 @@ func (c *Client) run(stop <-chan struct{}) error { return nil } -// GetID implements endpoints.Provider interface and returns a string descriptor / identifier of the compute provider. -func (c *Client) GetID() string { - return c.providerIdent -} - // GetAnnouncementsChannel returns the announcement channel for the SMI client. func (c *Client) GetAnnouncementsChannel() <-chan interface{} { return c.announcements } // newClient creates a provider based on a Kubernetes client instance. -func newSMIClient(kubeClient kubernetes.Interface, smiTrafficSplitClient *smiTrafficSplitClient.Clientset, smiTrafficSpecClient *smiTrafficSpecClient.Clientset, smiTrafficTargetClient *smiTrafficTargetClient.Clientset, backpressureClient *backpressureClient.Clientset, osmNamespace string, namespaceController namespace.Controller, providerIdent string) *Client { +func newSMIClient(kubeClient kubernetes.Interface, smiTrafficSplitClient smiTrafficSplitClient.Interface, smiTrafficSpecClient smiTrafficSpecClient.Interface, smiTrafficTargetClient smiTrafficTargetClient.Interface, backpressureClient backpressureClient.Interface, osmNamespace string, namespaceController namespace.Controller, providerIdent string, stop chan struct{}) (*Client, error) { informerFactory := informers.NewSharedInformerFactory(kubeClient, k8s.DefaultKubeEventResyncInterval) smiTrafficSplitInformerFactory := smiTrafficSplitInformers.NewSharedInformerFactory(smiTrafficSplitClient, k8s.DefaultKubeEventResyncInterval) smiTrafficSpecInformerFactory := smiTrafficSpecInformers.NewSharedInformerFactory(smiTrafficSpecClient, k8s.DefaultKubeEventResyncInterval) @@ -164,7 +156,12 @@ func newSMIClient(kubeClient kubernetes.Interface, smiTrafficSplitClient *smiTra informerCollection.Backpressure.AddEventHandler(k8s.GetKubernetesEventHandlers("Backpressure", "SMI", client.announcements, shouldObserve)) } - return &client + err := client.run(stop) + if err != nil { + return &client, errors.Errorf("Could not start %s client", kubernetesClientName) + } + + return &client, err } // ListTrafficSplits implements mesh.MeshSpec by returning the list of traffic splits. @@ -181,7 +178,7 @@ func (c *Client) ListTrafficSplits() []*split.TrafficSplit { return trafficSplits } -// ListHTTPTrafficSpecs implements mesh.Topology by returning the list of traffic specs. +// ListHTTPTrafficSpecs lists SMI HTTPRouteGroup resources func (c *Client) ListHTTPTrafficSpecs() []*spec.HTTPRouteGroup { var httpTrafficSpec []*spec.HTTPRouteGroup for _, specIface := range c.caches.TrafficSpec.List() { @@ -209,27 +206,6 @@ func (c *Client) ListTrafficTargets() []*target.TrafficTarget { return trafficTargets } -// ListBackpressures implements smi.MeshSpec and returns a list of backpressure policies. -func (c *Client) ListBackpressures() []*backpressure.Backpressure { - var backpressureList []*backpressure.Backpressure - - if !featureflags.IsBackpressureEnabled() { - log.Info().Msgf("Backpressure turned off!") - return backpressureList - } - - for _, pressureIface := range c.caches.Backpressure.List() { - bpressure := pressureIface.(*backpressure.Backpressure) - - if !c.namespaceController.IsMonitoredNamespace(bpressure.Namespace) { - continue - } - backpressureList = append(backpressureList, bpressure) - } - - return backpressureList -} - // GetBackpressurePolicy gets the Backpressure policy corresponding to the MeshService func (c *Client) GetBackpressurePolicy(svc service.MeshService) *backpressure.Backpressure { if !featureflags.IsBackpressureEnabled() { @@ -278,7 +254,7 @@ func (c *Client) ListTrafficSplitServices() []service.WeightedService { return services } -// ListServiceAccounts implements mesh.MeshSpec by returning the service accounts observed from the given compute provider +// ListServiceAccounts lists ServiceAccounts specified in SMI TrafficTarget resources func (c *Client) ListServiceAccounts() []service.K8sServiceAccount { var serviceAccounts []service.K8sServiceAccount for _, targetIface := range c.caches.TrafficTarget.List() { diff --git a/pkg/smi/client_test.go b/pkg/smi/client_test.go new file mode 100644 index 0000000000..73a5b2fc93 --- /dev/null +++ b/pkg/smi/client_test.go @@ -0,0 +1,585 @@ +package smi + +import ( + "context" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + osmPolicy "github.com/openservicemesh/osm/experimental/pkg/apis/policy/v1alpha1" + osmPolicyClient "github.com/openservicemesh/osm/experimental/pkg/client/clientset/versioned/fake" + smiAccess "github.com/servicemeshinterface/smi-sdk-go/pkg/apis/access/v1alpha2" + smiSpecs "github.com/servicemeshinterface/smi-sdk-go/pkg/apis/specs/v1alpha3" + smiSplit "github.com/servicemeshinterface/smi-sdk-go/pkg/apis/split/v1alpha2" + testTrafficTargetClient "github.com/servicemeshinterface/smi-sdk-go/pkg/gen/client/access/clientset/versioned/fake" + testTrafficSpecClient "github.com/servicemeshinterface/smi-sdk-go/pkg/gen/client/specs/clientset/versioned/fake" + testTrafficSplitClient "github.com/servicemeshinterface/smi-sdk-go/pkg/gen/client/split/clientset/versioned/fake" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + testclient "k8s.io/client-go/kubernetes/fake" + + "github.com/openservicemesh/osm/pkg/featureflags" + "github.com/openservicemesh/osm/pkg/namespace" + "github.com/openservicemesh/osm/pkg/service" + "github.com/openservicemesh/osm/pkg/tests" +) + +const ( + testNamespaceName = "test" +) + +type fakeKubeClientSet struct { + kubeClient *testclient.Clientset + smiTrafficSplitClientSet *testTrafficSplitClient.Clientset + smiTrafficSpecClientSet *testTrafficSpecClient.Clientset + smiTrafficTargetClientSet *testTrafficTargetClient.Clientset + osmPolicyClientSet *osmPolicyClient.Clientset +} + +func bootstrapClient() (MeshSpec, *fakeKubeClientSet, error) { + osmNamespace := "osm-system" + meshName := "osm" + stop := make(chan struct{}) + kubeClient := testclient.NewSimpleClientset() + smiTrafficSplitClientSet := testTrafficSplitClient.NewSimpleClientset() + smiTrafficSpecClientSet := testTrafficSpecClient.NewSimpleClientset() + smiTrafficTargetClientSet := testTrafficTargetClient.NewSimpleClientset() + osmPolicyClientSet := osmPolicyClient.NewSimpleClientset() + namespaceController := namespace.NewNamespaceController(kubeClient, meshName, stop) + + fakeClientSet := &fakeKubeClientSet{ + kubeClient: kubeClient, + smiTrafficSplitClientSet: smiTrafficSplitClientSet, + smiTrafficSpecClientSet: smiTrafficSpecClientSet, + smiTrafficTargetClientSet: smiTrafficTargetClientSet, + osmPolicyClientSet: osmPolicyClientSet, + } + + // Create a test namespace that is monitored + testNamespace := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespaceName, + Labels: map[string]string{namespace.MonitorLabel: meshName}, // Label selectors don't work with fake clients, only here to signify its importance + }, + } + if _, err := kubeClient.CoreV1().Namespaces().Create(context.TODO(), &testNamespace, metav1.CreateOptions{}); err != nil { + log.Fatal().Err(err).Msgf("Error creating Namespace %v", testNamespace) + } + <-namespaceController.GetAnnouncementsChannel() + + meshSpec, err := newSMIClient( + kubeClient, + smiTrafficSplitClientSet, + smiTrafficSpecClientSet, + smiTrafficTargetClientSet, + osmPolicyClientSet, + osmNamespace, + namespaceController, + kubernetesClientName, + stop, + ) + + return meshSpec, fakeClientSet, err +} + +var _ = Describe("When listing TrafficSplit", func() { + var ( + meshSpec MeshSpec + fakeClientSet *fakeKubeClientSet + err error + ) + BeforeEach(func() { + meshSpec, fakeClientSet, err = bootstrapClient() + Expect(err).ToNot(HaveOccurred()) + }) + + It("should return a list of traffic split resources", func() { + split := &smiSplit.TrafficSplit{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ListTrafficSplits", + Namespace: testNamespaceName, + }, + Spec: smiSplit.TrafficSplitSpec{ + Service: tests.BookstoreApexServiceName, + Backends: []smiSplit.TrafficSplitBackend{ + { + Service: tests.BookstoreServiceName, + Weight: tests.Weight, + }, + }, + }, + } + + _, err := fakeClientSet.smiTrafficSplitClientSet.SplitV1alpha2().TrafficSplits(testNamespaceName).Create(context.TODO(), split, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + <-meshSpec.GetAnnouncementsChannel() + + splits := meshSpec.ListTrafficSplits() + Expect(len(splits)).To(Equal(1)) + Expect(split).To(Equal(splits[0])) + + err = fakeClientSet.smiTrafficSplitClientSet.SplitV1alpha2().TrafficSplits(testNamespaceName).Delete(context.TODO(), split.Name, metav1.DeleteOptions{}) + Expect(err).ToNot(HaveOccurred()) + <-meshSpec.GetAnnouncementsChannel() + }) +}) + +var _ = Describe("When listing TrafficSplit services", func() { + var ( + meshSpec MeshSpec + fakeClientSet *fakeKubeClientSet + err error + ) + BeforeEach(func() { + meshSpec, fakeClientSet, err = bootstrapClient() + Expect(err).ToNot(HaveOccurred()) + }) + + It("should return a list of weighted services corresponding to the traffic split backends", func() { + split := &smiSplit.TrafficSplit{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ListTrafficSplitServices", + Namespace: testNamespaceName, + }, + Spec: smiSplit.TrafficSplitSpec{ + Service: tests.BookstoreApexServiceName, + Backends: []smiSplit.TrafficSplitBackend{ + { + Service: "bookstore-v1", + Weight: tests.Weight, + }, + { + Service: "bookstore-v2", + Weight: tests.Weight, + }, + }, + }, + } + + _, err := fakeClientSet.smiTrafficSplitClientSet.SplitV1alpha2().TrafficSplits(testNamespaceName).Create(context.TODO(), split, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + <-meshSpec.GetAnnouncementsChannel() + + weightedServices := meshSpec.ListTrafficSplitServices() + Expect(len(weightedServices)).To(Equal(len(split.Spec.Backends))) + for i, backend := range split.Spec.Backends { + Expect(weightedServices[i].Service).To(Equal(service.MeshService{Namespace: split.Namespace, Name: backend.Service})) + Expect(weightedServices[i].Weight).To(Equal(backend.Weight)) + Expect(weightedServices[i].RootService).To(Equal(split.Spec.Service)) + } + + err = fakeClientSet.smiTrafficSplitClientSet.SplitV1alpha2().TrafficSplits(testNamespaceName).Delete(context.TODO(), split.Name, metav1.DeleteOptions{}) + Expect(err).ToNot(HaveOccurred()) + <-meshSpec.GetAnnouncementsChannel() + }) +}) + +var _ = Describe("When listing ServiceAccounts", func() { + var ( + meshSpec MeshSpec + fakeClientSet *fakeKubeClientSet + err error + ) + BeforeEach(func() { + meshSpec, fakeClientSet, err = bootstrapClient() + Expect(err).ToNot(HaveOccurred()) + }) + + It("should return a list of service accounts specified in TrafficTarget resources", func() { + trafficTarget := &smiAccess.TrafficTarget{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "access.smi-spec.io/v1alpha2", + Kind: "TrafficTarget", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ListServiceAccounts", + Namespace: testNamespaceName, + }, + Spec: smiAccess.TrafficTargetSpec{ + Destination: smiAccess.IdentityBindingSubject{ + Kind: "Name", + Name: tests.BookstoreServiceAccountName, + Namespace: testNamespaceName, + }, + Sources: []smiAccess.IdentityBindingSubject{{ + Kind: "Name", + Name: tests.BookbuyerServiceAccountName, + Namespace: testNamespaceName, + }}, + Rules: []smiAccess.TrafficTargetRule{{ + Kind: "HTTPRouteGroup", + Name: tests.RouteGroupName, + Matches: []string{tests.BuyBooksMatchName}, + }}, + }, + } + + _, err := fakeClientSet.smiTrafficTargetClientSet.AccessV1alpha2().TrafficTargets(testNamespaceName).Create(context.TODO(), trafficTarget, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + <-meshSpec.GetAnnouncementsChannel() + + svcAccounts := meshSpec.ListServiceAccounts() + + numExpectedSvcAccounts := len(trafficTarget.Spec.Sources) + 1 // 1 for the destination ServiceAccount + Expect(len(svcAccounts)).To(Equal(numExpectedSvcAccounts)) + + err = fakeClientSet.smiTrafficTargetClientSet.AccessV1alpha2().TrafficTargets(testNamespaceName).Delete(context.TODO(), trafficTarget.Name, metav1.DeleteOptions{}) + Expect(err).ToNot(HaveOccurred()) + <-meshSpec.GetAnnouncementsChannel() + }) +}) + +var _ = Describe("When listing TrafficTargets", func() { + var ( + meshSpec MeshSpec + fakeClientSet *fakeKubeClientSet + err error + ) + BeforeEach(func() { + meshSpec, fakeClientSet, err = bootstrapClient() + Expect(err).ToNot(HaveOccurred()) + }) + + It("Returns a list of TrafficTarget resources", func() { + trafficTarget := &smiAccess.TrafficTarget{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "access.smi-spec.io/v1alpha2", + Kind: "TrafficTarget", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ListTrafficTargets", + Namespace: testNamespaceName, + }, + Spec: smiAccess.TrafficTargetSpec{ + Destination: smiAccess.IdentityBindingSubject{ + Kind: "Name", + Name: tests.BookstoreServiceAccountName, + Namespace: testNamespaceName, + }, + Sources: []smiAccess.IdentityBindingSubject{{ + Kind: "Name", + Name: tests.BookbuyerServiceAccountName, + Namespace: testNamespaceName, + }}, + Rules: []smiAccess.TrafficTargetRule{{ + Kind: "HTTPRouteGroup", + Name: tests.RouteGroupName, + Matches: []string{tests.BuyBooksMatchName}, + }}, + }, + } + + _, err := fakeClientSet.smiTrafficTargetClientSet.AccessV1alpha2().TrafficTargets(testNamespaceName).Create(context.TODO(), trafficTarget, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + <-meshSpec.GetAnnouncementsChannel() + + targets := meshSpec.ListTrafficTargets() + Expect(len(targets)).To(Equal(1)) + + err = fakeClientSet.smiTrafficTargetClientSet.AccessV1alpha2().TrafficTargets(testNamespaceName).Delete(context.TODO(), trafficTarget.Name, metav1.DeleteOptions{}) + Expect(err).ToNot(HaveOccurred()) + <-meshSpec.GetAnnouncementsChannel() + }) +}) + +var _ = Describe("When fetching a Service corresponding to a Meshservice", func() { + var ( + meshSpec MeshSpec + fakeClientSet *fakeKubeClientSet + err error + ) + BeforeEach(func() { + meshSpec, fakeClientSet, err = bootstrapClient() + Expect(err).ToNot(HaveOccurred()) + }) + + It("should return a Service resource corresponding to the given service", func() { + meshSvc := service.MeshService{ + Namespace: testNamespaceName, + Name: "test-GetService", + } + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: meshSvc.Name, + Namespace: meshSvc.Namespace, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{ + Name: "servicePort", + Protocol: corev1.ProtocolTCP, + Port: tests.ServicePort, + }}, + }, + } + + _, err := fakeClientSet.kubeClient.CoreV1().Services(testNamespaceName).Create(context.TODO(), svc, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + <-meshSpec.GetAnnouncementsChannel() + + svcIncache := meshSpec.GetService(meshSvc) + Expect(svcIncache).To(Equal(svc)) + + err = fakeClientSet.kubeClient.CoreV1().Services(testNamespaceName).Delete(context.TODO(), svc.Name, metav1.DeleteOptions{}) + Expect(err).ToNot(HaveOccurred()) + <-meshSpec.GetAnnouncementsChannel() + }) + + It("should return nil when the given MeshService is not found", func() { + meshSvc := service.MeshService{ + Namespace: testNamespaceName, + Name: "test-GetService", + } + + svcIncache := meshSpec.GetService(meshSvc) + Expect(svcIncache).To(BeNil()) + }) +}) + +var _ = Describe("When listing Services", func() { + var ( + meshSpec MeshSpec + fakeClientSet *fakeKubeClientSet + err error + ) + BeforeEach(func() { + meshSpec, fakeClientSet, err = bootstrapClient() + Expect(err).ToNot(HaveOccurred()) + }) + + It("should return an empty list when no services are found", func() { + services := meshSpec.ListServices() + Expect(len(services)).To(Equal(0)) // fixme cache sync not done yet + }) + + It("should return a list of Services", func() { + svc1 := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-1", + Namespace: testNamespaceName, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{ + Name: "servicePort", + Protocol: corev1.ProtocolTCP, + Port: tests.ServicePort, + }}, + }, + } + svc2 := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-2", + Namespace: testNamespaceName, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{ + Name: "servicePort", + Protocol: corev1.ProtocolTCP, + Port: tests.ServicePort, + }}, + }, + } + + _, err := fakeClientSet.kubeClient.CoreV1().Services(testNamespaceName).Create(context.TODO(), svc1, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + <-meshSpec.GetAnnouncementsChannel() + _, err = fakeClientSet.kubeClient.CoreV1().Services(testNamespaceName).Create(context.TODO(), svc2, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + <-meshSpec.GetAnnouncementsChannel() + + services := meshSpec.ListServices() + Expect(len(services)).To(Equal(2)) + + expectedServices := []string{"test-1", "test-2"} + Expect(services[0].Name).To(BeElementOf(expectedServices)) + Expect(services[1].Name).To(BeElementOf(expectedServices)) + + err = fakeClientSet.kubeClient.CoreV1().Services(testNamespaceName).Delete(context.TODO(), svc1.Name, metav1.DeleteOptions{}) + Expect(err).ToNot(HaveOccurred()) + <-meshSpec.GetAnnouncementsChannel() + err = fakeClientSet.kubeClient.CoreV1().Services(testNamespaceName).Delete(context.TODO(), svc2.Name, metav1.DeleteOptions{}) + Expect(err).ToNot(HaveOccurred()) + <-meshSpec.GetAnnouncementsChannel() + }) +}) + +var _ = Describe("When listing ListHTTPTrafficSpecs", func() { + var ( + meshSpec MeshSpec + fakeClientSet *fakeKubeClientSet + err error + ) + BeforeEach(func() { + meshSpec, fakeClientSet, err = bootstrapClient() + Expect(err).ToNot(HaveOccurred()) + }) + + It("Returns an empty list when no HTTPRouteGroup are found", func() { + services := meshSpec.ListHTTPTrafficSpecs() + Expect(len(services)).To(Equal(0)) + }) + + It("should return a list of ListHTTPTrafficSpecs resources", func() { + routeSpec := &smiSpecs.HTTPRouteGroup{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "specs.smi-spec.io/v1alpha3", + Kind: "HTTPRouteGroup", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespaceName, + Name: "test-ListHTTPTrafficSpecs", + }, + Spec: smiSpecs.HTTPRouteGroupSpec{ + Matches: []smiSpecs.HTTPMatch{ + { + Name: tests.BuyBooksMatchName, + PathRegex: tests.BookstoreBuyPath, + Methods: []string{"GET"}, + Headers: map[string]string{ + "user-agent": tests.HTTPUserAgent, + }, + }, + { + Name: tests.SellBooksMatchName, + PathRegex: tests.BookstoreSellPath, + Methods: []string{"GET"}, + }, + { + Name: tests.WildcardWithHeadersMatchName, + Headers: map[string]string{ + "user-agent": tests.HTTPUserAgent, + }, + }, + }, + }, + } + + _, err := fakeClientSet.smiTrafficSpecClientSet.SpecsV1alpha3().HTTPRouteGroups(testNamespaceName).Create(context.TODO(), routeSpec, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + <-meshSpec.GetAnnouncementsChannel() + + httpRoutes := meshSpec.ListHTTPTrafficSpecs() + Expect(len(httpRoutes)).To(Equal(1)) + Expect(httpRoutes[0].Name).To(Equal(routeSpec.Name)) + + err = fakeClientSet.smiTrafficSpecClientSet.SpecsV1alpha3().HTTPRouteGroups(testNamespaceName).Delete(context.TODO(), routeSpec.Name, metav1.DeleteOptions{}) + Expect(err).ToNot(HaveOccurred()) + <-meshSpec.GetAnnouncementsChannel() + }) +}) + +var _ = Describe("When fetching BackpressurePolicy for the given MeshService", func() { + var ( + meshSpec MeshSpec + fakeClientSet *fakeKubeClientSet + err error + ) + + It("should returns nil when a Backpressure feature is disabled", func() { + meshSvc := service.MeshService{ + Namespace: testNamespaceName, + Name: "test-GetBackpressurePolicy", + } + backpressure := meshSpec.GetBackpressurePolicy(meshSvc) + Expect(backpressure).To(BeNil()) + }) + + // Initialize feature for unit testing + optional := featureflags.OptionalFeatures{ + Backpressure: true, + } + featureflags.Initialize(optional) + + BeforeEach(func() { + meshSpec, fakeClientSet, err = bootstrapClient() + Expect(err).ToNot(HaveOccurred()) + }) + + It("should returns nil when a Backpressure policy does not exist for the given service", func() { + meshSvc := service.MeshService{ + Namespace: testNamespaceName, + Name: "test-GetBackpressurePolicy", + } + backpressure := meshSpec.GetBackpressurePolicy(meshSvc) + Expect(backpressure).To(BeNil()) + }) + + It("should return the Backpresure policy for the given service", func() { + meshSvc := service.MeshService{ + Namespace: testNamespaceName, + Name: "test-GetBackpressurePolicy", + } + backpressurePolicy := &osmPolicy.Backpressure{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "policy.openservicemesh.io/v1alpha1", + Kind: "Backpressure", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespaceName, + Name: "test-GetBackpressurePolicy", + Labels: map[string]string{"app": meshSvc.Name}, + }, + Spec: osmPolicy.BackpressureSpec{ + MaxConnections: 123, + }, + } + + _, err := fakeClientSet.osmPolicyClientSet.PolicyV1alpha1().Backpressures(testNamespaceName).Create(context.TODO(), backpressurePolicy, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + <-meshSpec.GetAnnouncementsChannel() + + backpressurePolicyInCache := meshSpec.GetBackpressurePolicy(meshSvc) + Expect(backpressurePolicyInCache).ToNot(BeNil()) + Expect(backpressurePolicyInCache.Name).To(Equal(backpressurePolicy.Name)) + + err = fakeClientSet.osmPolicyClientSet.PolicyV1alpha1().Backpressures(testNamespaceName).Delete(context.TODO(), backpressurePolicy.Name, metav1.DeleteOptions{}) + Expect(err).ToNot(HaveOccurred()) + <-meshSpec.GetAnnouncementsChannel() + }) + + It("should return nil when the app label is missing for the given service", func() { + meshSvc := service.MeshService{ + Namespace: testNamespaceName, + Name: "test-GetBackpressurePolicy", + } + backpressurePolicy := &osmPolicy.Backpressure{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "policy.openservicemesh.io/v1alpha1", + Kind: "Backpressure", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespaceName, + Name: "test-GetBackpressurePolicy", + }, + Spec: osmPolicy.BackpressureSpec{ + MaxConnections: 123, + }, + } + + _, err := fakeClientSet.osmPolicyClientSet.PolicyV1alpha1().Backpressures(testNamespaceName).Create(context.TODO(), backpressurePolicy, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + <-meshSpec.GetAnnouncementsChannel() + + backpressurePolicyInCache := meshSpec.GetBackpressurePolicy(meshSvc) + Expect(backpressurePolicyInCache).To(BeNil()) + + err = fakeClientSet.osmPolicyClientSet.PolicyV1alpha1().Backpressures(testNamespaceName).Delete(context.TODO(), backpressurePolicy.Name, metav1.DeleteOptions{}) + Expect(err).ToNot(HaveOccurred()) + <-meshSpec.GetAnnouncementsChannel() + }) +}) + +var _ = Describe("When fetching the announcement channel", func() { + var ( + meshSpec MeshSpec + err error + ) + + BeforeEach(func() { + meshSpec, _, err = bootstrapClient() + Expect(err).ToNot(HaveOccurred()) + }) + + It("should return an announcement channel on which events are notified", func() { + announcementChan := meshSpec.GetAnnouncementsChannel() + Expect(announcementChan).ToNot(BeNil()) + }) +}) diff --git a/pkg/smi/fake.go b/pkg/smi/fake.go index 163ffca24c..7c9d8b4cc1 100644 --- a/pkg/smi/fake.go +++ b/pkg/smi/fake.go @@ -67,7 +67,7 @@ func (f fakeMeshSpec) GetService(svc service.MeshService) *corev1.Service { return nil } -// ListHTTPTrafficSpecs lists TrafficSpec SMI resources for the fake Mesh Spec. +// ListHTTPTrafficSpecs lists SMI HTTPRouteGroup resources func (f fakeMeshSpec) ListHTTPTrafficSpecs() []*spec.HTTPRouteGroup { return f.routeGroups } @@ -77,21 +77,7 @@ func (f fakeMeshSpec) ListTrafficTargets() []*target.TrafficTarget { return f.trafficTargets } -// ListBackpressures lists Backpressure SMI resources for the fake Mesh Spec. -func (f fakeMeshSpec) ListBackpressures() []*backpressure.Backpressure { - return f.backpressures -} - func (f fakeMeshSpec) GetBackpressurePolicy(svc service.MeshService) *backpressure.Backpressure { - for _, backpressure := range f.backpressures { - app, ok := backpressure.Labels["app"] - if !ok { - continue - } - if svc.Namespace == backpressure.Namespace && svc.Name == app { - return backpressure - } - } return nil } diff --git a/pkg/smi/types.go b/pkg/smi/types.go index f2806233c4..0f7294b509 100644 --- a/pkg/smi/types.go +++ b/pkg/smi/types.go @@ -47,40 +47,32 @@ type Client struct { namespaceController namespace.Controller } -// ClientIdentity is the identity of an Envoy proxy connected to the Open Service Mesh. -type ClientIdentity string - // MeshSpec is an interface declaring functions, which provide the specs for a service mesh declared with SMI. type MeshSpec interface { - // ListTrafficSplits lists TrafficSplit SMI resources. + // ListTrafficSplits lists SMI TrafficSplit resources ListTrafficSplits() []*split.TrafficSplit - // ListTrafficSplitServices fetches all services declared with SMI Spec. + // ListTrafficSplitServices lists WeightedServices for the services specified in TrafficSplit SMI resources ListTrafficSplitServices() []service.WeightedService - // ListServiceAccounts fetches all service accounts declared with SMI Spec. + // ListServiceAccounts lists ServiceAccount resources specified in SMI TrafficTarget resources ListServiceAccounts() []service.K8sServiceAccount - // GetService fetches a specific service declared in SMI. + // GetService fetches a Kubernetes Service resource for the given MeshService GetService(service.MeshService) *corev1.Service - // ListHTTPTrafficSpecs lists TrafficSpec SMI resources. + // ListServices Lists Kubernets Service resources that are part of monitored namespaces + ListServices() []*corev1.Service + + // ListHTTPTrafficSpecs lists SMI HTTPRouteGroup resources ListHTTPTrafficSpecs() []*spec.HTTPRouteGroup - // ListTrafficTargets lists TrafficTarget SMI resources. + // ListTrafficTargets lists SMI TrafficTarget resources ListTrafficTargets() []*target.TrafficTarget - // ListBackpressures lists Backpressure resources. - // This is an experimental feature, which will eventually - // in some shape or form make its way into SMI Spec. - ListBackpressures() []*backpressure.Backpressure - - // GetBackpressurePolicy gets the Backpressure policy corresponding to the MeshService + // GetBackpressurePolicy fetches the Backpressure policy for the MeshService GetBackpressurePolicy(service.MeshService) *backpressure.Backpressure - // GetAnnouncementsChannel returns the channel on which SMI makes announcements + // GetAnnouncementsChannel returns the channel on which SMI client makes announcements GetAnnouncementsChannel() <-chan interface{} - - // ListServices returns a list of services that are part of monitored namespaces - ListServices() []*corev1.Service }