From 3e9c44f00e0ee74e4d2dde1c250e79983ea4a036 Mon Sep 17 00:00:00 2001 From: Stephen Heywood Date: Tue, 2 Mar 2021 11:13:14 +1300 Subject: [PATCH] Add lifecycle e2e test for service status e2e test validates the following service status endpoints - patchCoreV1NamespacedServiceStatus - readCoreV1NamespacedServiceStatus - replaceCoreV1NamespacedServiceStatus Also includes untested service endpoint - patchCoreV1NamespacedService --- test/e2e/network/service.go | 222 ++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) diff --git a/test/e2e/network/service.go b/test/e2e/network/service.go index 101b491ea24..7040dbca8dc 100644 --- a/test/e2e/network/service.go +++ b/test/e2e/network/service.go @@ -30,6 +30,7 @@ import ( "time" utilnet "k8s.io/apimachinery/pkg/util/net" + utilrand "k8s.io/apimachinery/pkg/util/rand" "k8s.io/client-go/tools/cache" @@ -38,6 +39,7 @@ import ( discoveryv1beta1 "k8s.io/api/discovery/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/sets" @@ -45,6 +47,7 @@ import ( watch "k8s.io/apimachinery/pkg/watch" clientset "k8s.io/client-go/kubernetes" watchtools "k8s.io/client-go/tools/watch" + "k8s.io/client-go/util/retry" cloudprovider "k8s.io/cloud-provider" "k8s.io/kubernetes/test/e2e/framework" e2edeployment "k8s.io/kubernetes/test/e2e/framework/deployment" @@ -84,6 +87,8 @@ const ( clusterAddonLabelKey = "k8s-app" kubeAPIServerLabelName = "kube-apiserver" clusterComponentKey = "component" + + svcReadyTimeout = 1 * time.Minute ) var ( @@ -2199,6 +2204,223 @@ var _ = common.SIGDescribe("Services", func() { _, err = f.ClientSet.CoreV1().Endpoints(testNamespaceName).Get(context.TODO(), testEndpointName, metav1.GetOptions{}) framework.ExpectError(err, "should not be able to fetch Endpoint") }) + + ginkgo.It("should complete a service status lifecycle", func() { + + ns := f.Namespace.Name + svcResource := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "services"} + svcClient := f.ClientSet.CoreV1().Services(ns) + + testSvcName := "test-service-" + utilrand.String(5) + testSvcLabels := map[string]string{"test-service-static": "true"} + testSvcLabelsFlat := "test-service-static=true" + + w := &cache.ListWatch{ + WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { + options.LabelSelector = testSvcLabelsFlat + return cs.CoreV1().Services(ns).Watch(context.TODO(), options) + }, + } + + svcList, err := cs.CoreV1().Services("").List(context.TODO(), metav1.ListOptions{LabelSelector: testSvcLabelsFlat}) + framework.ExpectNoError(err, "failed to list Services") + + ginkgo.By("creating a Service") + testService := v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: testSvcName, + Labels: testSvcLabels, + }, + Spec: v1.ServiceSpec{ + Type: "ClusterIP", + Ports: []v1.ServicePort{{ + Name: "http", + Protocol: v1.ProtocolTCP, + Port: int32(80), + TargetPort: intstr.FromInt(80), + }}, + }, + } + _, err = cs.CoreV1().Services(ns).Create(context.TODO(), &testService, metav1.CreateOptions{}) + framework.ExpectNoError(err, "failed to create Service") + + ginkgo.By("watching for the Service to be added") + ctx, cancel := context.WithTimeout(context.Background(), svcReadyTimeout) + defer cancel() + _, err = watchtools.Until(ctx, svcList.ResourceVersion, w, func(event watch.Event) (bool, error) { + if svc, ok := event.Object.(*v1.Service); ok { + found := svc.ObjectMeta.Name == testService.ObjectMeta.Name && + svc.ObjectMeta.Namespace == ns && + svc.Labels["test-service-static"] == "true" + if !found { + framework.Logf("observed Service %v in namespace %v with labels: %v & ports %v", svc.ObjectMeta.Name, svc.ObjectMeta.Namespace, svc.Labels, svc.Spec.Ports) + return false, nil + } + framework.Logf("Found Service %v in namespace %v with labels: %v & ports %v", svc.ObjectMeta.Name, svc.ObjectMeta.Namespace, svc.Labels, svc.Spec.Ports) + return found, nil + } + framework.Logf("Observed event: %+v", event.Object) + return false, nil + }) + framework.ExpectNoError(err, "Failed to locate Service %v in namespace %v", testService.ObjectMeta.Name, ns) + framework.Logf("Service %s created", testSvcName) + + ginkgo.By("Getting /status") + svcStatusUnstructured, err := f.DynamicClient.Resource(svcResource).Namespace(ns).Get(context.TODO(), testSvcName, metav1.GetOptions{}, "status") + framework.ExpectNoError(err, "Failed to fetch ServiceStatus of Service %s in namespace %s", testSvcName, ns) + svcStatusBytes, err := json.Marshal(svcStatusUnstructured) + framework.ExpectNoError(err, "Failed to marshal unstructured response. %v", err) + + var svcStatus v1.Service + err = json.Unmarshal(svcStatusBytes, &svcStatus) + framework.ExpectNoError(err, "Failed to unmarshal JSON bytes to a Service object type") + framework.Logf("Service %s has LoadBalancer: %v", testSvcName, svcStatus.Status.LoadBalancer) + + ginkgo.By("patching the ServiceStatus") + lbStatus := v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{{IP: "203.0.113.1"}}, + } + lbStatusJSON, err := json.Marshal(lbStatus) + framework.ExpectNoError(err, "Failed to marshal JSON. %v", err) + _, err = svcClient.Patch(context.TODO(), testSvcName, types.MergePatchType, + []byte(`{"metadata":{"annotations":{"patchedstatus":"true"}},"status":{"loadBalancer":`+string(lbStatusJSON)+`}}`), + metav1.PatchOptions{}, "status") + framework.ExpectNoError(err, "Could not patch service status", err) + + ginkgo.By("watching for the Service to be patched") + ctx, cancel = context.WithTimeout(context.Background(), svcReadyTimeout) + defer cancel() + + _, err = watchtools.Until(ctx, svcList.ResourceVersion, w, func(event watch.Event) (bool, error) { + if svc, ok := event.Object.(*v1.Service); ok { + found := svc.ObjectMeta.Name == testService.ObjectMeta.Name && + svc.ObjectMeta.Namespace == ns && + svc.Annotations["patchedstatus"] == "true" + if !found { + framework.Logf("observed Service %v in namespace %v with annotations: %v & LoadBalancer: %v", svc.ObjectMeta.Name, svc.ObjectMeta.Namespace, svc.Annotations, svc.Status.LoadBalancer) + return false, nil + } + framework.Logf("Found Service %v in namespace %v with annotations: %v & LoadBalancer: %v", svc.ObjectMeta.Name, svc.ObjectMeta.Namespace, svc.Annotations, svc.Status.LoadBalancer) + return found, nil + } + framework.Logf("Observed event: %+v", event.Object) + return false, nil + }) + framework.ExpectNoError(err, "failed to locate Service %v in namespace %v", testService.ObjectMeta.Name, ns) + framework.Logf("Service %s has service status patched", testSvcName) + + ginkgo.By("updating the ServiceStatus") + + var statusToUpdate, updatedStatus *v1.Service + err = retry.RetryOnConflict(retry.DefaultRetry, func() error { + statusToUpdate, err = svcClient.Get(context.TODO(), testSvcName, metav1.GetOptions{}) + framework.ExpectNoError(err, "Unable to retrieve service %s", testSvcName) + + statusToUpdate.Status.Conditions = append(statusToUpdate.Status.Conditions, metav1.Condition{ + Type: "StatusUpdate", + Status: metav1.ConditionTrue, + Reason: "E2E", + Message: "Set from e2e test", + }) + + updatedStatus, err = svcClient.UpdateStatus(context.TODO(), statusToUpdate, metav1.UpdateOptions{}) + return err + }) + framework.ExpectNoError(err, "\n\n Failed to UpdateStatus. %v\n\n", err) + framework.Logf("updatedStatus.Conditions: %#v", updatedStatus.Status.Conditions) + + ginkgo.By("watching for the Service to be updated") + ctx, cancel = context.WithTimeout(context.Background(), svcReadyTimeout) + defer cancel() + _, err = watchtools.Until(ctx, svcList.ResourceVersion, w, func(event watch.Event) (bool, error) { + if svc, ok := event.Object.(*v1.Service); ok { + found := svc.ObjectMeta.Name == testService.ObjectMeta.Name && + svc.ObjectMeta.Namespace == ns && + svc.Annotations["patchedstatus"] == "true" + if !found { + framework.Logf("Observed Service %v in namespace %v with annotations: %v & Conditions: %v", svc.ObjectMeta.Name, svc.ObjectMeta.Namespace, svc.Annotations, svc.Status.LoadBalancer) + return false, nil + } + for _, cond := range svc.Status.Conditions { + if cond.Type == "StatusUpdate" && + cond.Reason == "E2E" && + cond.Message == "Set from e2e test" { + framework.Logf("Found Service %v in namespace %v with annotations: %v & Conditions: %v", svc.ObjectMeta.Name, svc.ObjectMeta.Namespace, svc.Annotations, svc.Status.Conditions) + return found, nil + } else { + framework.Logf("Observed Service %v in namespace %v with annotations: %v & Conditions: %v", svc.ObjectMeta.Name, svc.ObjectMeta.Namespace, svc.Annotations, svc.Status.LoadBalancer) + return false, nil + } + } + } + framework.Logf("Observed event: %+v", event.Object) + return false, nil + }) + framework.ExpectNoError(err, "failed to locate Service %v in namespace %v", testService.ObjectMeta.Name, ns) + framework.Logf("Service %s has service status updated", testSvcName) + + ginkgo.By("patching the service") + servicePatchPayload, err := json.Marshal(v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "test-service": "patched", + }, + }, + }) + + _, err = svcClient.Patch(context.TODO(), testSvcName, types.StrategicMergePatchType, []byte(servicePatchPayload), metav1.PatchOptions{}) + framework.ExpectNoError(err, "failed to patch service. %v", err) + + ginkgo.By("watching for the Service to be patched") + ctx, cancel = context.WithTimeout(context.Background(), svcReadyTimeout) + defer cancel() + _, err = watchtools.Until(ctx, svcList.ResourceVersion, w, func(event watch.Event) (bool, error) { + if svc, ok := event.Object.(*v1.Service); ok { + found := svc.ObjectMeta.Name == testService.ObjectMeta.Name && + svc.ObjectMeta.Namespace == ns && + svc.Labels["test-service"] == "patched" + if !found { + framework.Logf("observed Service %v in namespace %v with labels: %v", svc.ObjectMeta.Name, svc.ObjectMeta.Namespace, svc.Labels) + return false, nil + } + framework.Logf("Found Service %v in namespace %v with labels: %v", svc.ObjectMeta.Name, svc.ObjectMeta.Namespace, svc.Labels) + return found, nil + } + framework.Logf("Observed event: %+v", event.Object) + return false, nil + }) + framework.ExpectNoError(err, "failed to locate Service %v in namespace %v", testService.ObjectMeta.Name, ns) + framework.Logf("Service %s patched", testSvcName) + + ginkgo.By("deleting the service") + err = cs.CoreV1().Services(ns).Delete(context.TODO(), testSvcName, metav1.DeleteOptions{}) + framework.ExpectNoError(err, "failed to delete the Service. %v", err) + + ginkgo.By("watching for the Service to be deleted") + ctx, cancel = context.WithTimeout(context.Background(), svcReadyTimeout) + defer cancel() + _, err = watchtools.Until(ctx, svcList.ResourceVersion, w, func(event watch.Event) (bool, error) { + switch event.Type { + case watch.Deleted: + if svc, ok := event.Object.(*v1.Service); ok { + found := svc.ObjectMeta.Name == testService.ObjectMeta.Name && + svc.ObjectMeta.Namespace == ns && + svc.Labels["test-service-static"] == "true" + if !found { + framework.Logf("observed Service %v in namespace %v with labels: %v & annotations: %v", svc.ObjectMeta.Name, svc.ObjectMeta.Namespace, svc.Labels, svc.Annotations) + return false, nil + } + framework.Logf("Found Service %v in namespace %v with labels: %v & annotations: %v", svc.ObjectMeta.Name, svc.ObjectMeta.Namespace, svc.Labels, svc.Annotations) + return found, nil + } + default: + framework.Logf("Observed event: %+v", event.Type) + } + return false, nil + }) + framework.ExpectNoError(err, "failed to delete Service %v in namespace %v", testService.ObjectMeta.Name, ns) + framework.Logf("Service %s deleted", testSvcName) + }) }) // execAffinityTestForSessionAffinityTimeout is a helper function that wrap the logic of