| /* |
| Copyright 2022 The Kubernetes 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 apps |
| |
| import ( |
| "context" |
| "fmt" |
| "time" |
| |
| "github.com/onsi/ginkgo/v2" |
| "github.com/onsi/gomega" |
| |
| appsv1 "k8s.io/api/apps/v1" |
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| "k8s.io/apimachinery/pkg/labels" |
| "k8s.io/apimachinery/pkg/runtime" |
| "k8s.io/apimachinery/pkg/types" |
| utilrand "k8s.io/apimachinery/pkg/util/rand" |
| "k8s.io/apimachinery/pkg/util/wait" |
| clientset "k8s.io/client-go/kubernetes" |
| "k8s.io/client-go/kubernetes/scheme" |
| "k8s.io/client-go/util/retry" |
| extensionsinternal "k8s.io/kubernetes/pkg/apis/extensions" |
| "k8s.io/kubernetes/pkg/controller" |
| labelsutil "k8s.io/kubernetes/pkg/util/labels" |
| "k8s.io/kubernetes/test/e2e/framework" |
| e2edaemonset "k8s.io/kubernetes/test/e2e/framework/daemonset" |
| e2eresource "k8s.io/kubernetes/test/e2e/framework/resource" |
| admissionapi "k8s.io/pod-security-admission/api" |
| "k8s.io/utils/pointer" |
| ) |
| |
| const ( |
| controllerRevisionRetryPeriod = 1 * time.Second |
| controllerRevisionRetryTimeout = 1 * time.Minute |
| ) |
| |
| // This test must be run in serial because it assumes the Daemon Set pods will |
| // always get scheduled. If we run other tests in parallel, this may not |
| // happen. In the future, running in parallel may work if we have an eviction |
| // model which lets the DS controller kick out other pods to make room. |
| // See https://issues.k8s.io/21767 for more details |
| var _ = SIGDescribe("ControllerRevision", framework.WithSerial(), func() { |
| var f *framework.Framework |
| |
| ginkgo.AfterEach(func(ctx context.Context) { |
| // Clean up |
| daemonsets, err := f.ClientSet.AppsV1().DaemonSets(f.Namespace.Name).List(ctx, metav1.ListOptions{}) |
| framework.ExpectNoError(err, "unable to dump DaemonSets") |
| if daemonsets != nil && len(daemonsets.Items) > 0 { |
| for _, ds := range daemonsets.Items { |
| ginkgo.By(fmt.Sprintf("Deleting DaemonSet %q", ds.Name)) |
| framework.ExpectNoError(e2eresource.DeleteResourceAndWaitForGC(ctx, f.ClientSet, extensionsinternal.Kind("DaemonSet"), f.Namespace.Name, ds.Name)) |
| err = wait.PollUntilContextTimeout(ctx, dsRetryPeriod, dsRetryTimeout, true, checkRunningOnNoNodes(f, &ds)) |
| framework.ExpectNoError(err, "error waiting for daemon pod to be reaped") |
| } |
| } |
| if daemonsets, err := f.ClientSet.AppsV1().DaemonSets(f.Namespace.Name).List(ctx, metav1.ListOptions{}); err == nil { |
| framework.Logf("daemonset: %s", runtime.EncodeOrDie(scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...), daemonsets)) |
| } else { |
| framework.Logf("unable to dump daemonsets: %v", err) |
| } |
| if pods, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).List(ctx, metav1.ListOptions{}); err == nil { |
| framework.Logf("pods: %s", runtime.EncodeOrDie(scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...), pods)) |
| } else { |
| framework.Logf("unable to dump pods: %v", err) |
| } |
| err = clearDaemonSetNodeLabels(ctx, f.ClientSet) |
| framework.ExpectNoError(err) |
| }) |
| |
| f = framework.NewDefaultFramework("controllerrevisions") |
| f.NamespacePodSecurityLevel = admissionapi.LevelBaseline |
| |
| image := WebserverImage |
| dsName := "e2e-" + utilrand.String(5) + "-daemon-set" |
| |
| var ns string |
| var c clientset.Interface |
| |
| ginkgo.BeforeEach(func(ctx context.Context) { |
| ns = f.Namespace.Name |
| |
| c = f.ClientSet |
| |
| updatedNS, err := patchNamespaceAnnotations(ctx, c, ns) |
| framework.ExpectNoError(err) |
| |
| ns = updatedNS.Name |
| |
| err = clearDaemonSetNodeLabels(ctx, c) |
| framework.ExpectNoError(err) |
| }) |
| |
| /* |
| Release: v1.25 |
| Testname: ControllerRevision, resource lifecycle |
| Description: Creating a DaemonSet MUST succeed. Listing all |
| ControllerRevisions with a label selector MUST find only one. |
| After patching the ControllerRevision with a new label, the label |
| MUST be found. Creating a new ControllerRevision for the DaemonSet |
| MUST succeed. Listing the ControllerRevisions by label selector |
| MUST find only two. Deleting a ControllerRevision MUST succeed. |
| Listing the ControllerRevisions by label selector MUST find only |
| one. After updating the ControllerRevision with a new label, the |
| label MUST be found. Patching the DaemonSet MUST succeed. Listing the |
| ControllerRevisions by label selector MUST find only two. Deleting |
| a collection of ControllerRevision via a label selector MUST succeed. |
| Listing the ControllerRevisions by label selector MUST find only one. |
| The current ControllerRevision revision MUST be 3. |
| */ |
| framework.ConformanceIt("should manage the lifecycle of a ControllerRevision", func(ctx context.Context) { |
| csAppsV1 := f.ClientSet.AppsV1() |
| |
| dsLabel := map[string]string{"daemonset-name": dsName} |
| dsLabelSelector := labels.SelectorFromSet(dsLabel).String() |
| |
| ginkgo.By(fmt.Sprintf("Creating DaemonSet %q", dsName)) |
| testDaemonset, err := csAppsV1.DaemonSets(ns).Create(ctx, newDaemonSetWithLabel(dsName, image, dsLabel), metav1.CreateOptions{}) |
| framework.ExpectNoError(err) |
| |
| ginkgo.By("Check that daemon pods launch on every node of the cluster.") |
| err = wait.PollUntilContextTimeout(ctx, dsRetryPeriod, dsRetryTimeout, true, checkRunningOnAllNodes(f, testDaemonset)) |
| framework.ExpectNoError(err, "error waiting for daemon pod to start") |
| err = e2edaemonset.CheckDaemonStatus(ctx, f, dsName) |
| framework.ExpectNoError(err) |
| |
| ginkgo.By(fmt.Sprintf("Confirm DaemonSet %q successfully created with %q label", dsName, dsLabelSelector)) |
| dsList, err := csAppsV1.DaemonSets("").List(ctx, metav1.ListOptions{LabelSelector: dsLabelSelector}) |
| framework.ExpectNoError(err, "failed to list Daemon Sets") |
| gomega.Expect(dsList.Items).To(gomega.HaveLen(1), "filtered list wasn't found") |
| |
| ds, err := c.AppsV1().DaemonSets(ns).Get(ctx, dsName, metav1.GetOptions{}) |
| framework.ExpectNoError(err) |
| |
| // Listing across all namespaces to verify api endpoint: listAppsV1ControllerRevisionForAllNamespaces |
| ginkgo.By(fmt.Sprintf("Listing all ControllerRevisions with label %q", dsLabelSelector)) |
| revs, err := csAppsV1.ControllerRevisions("").List(ctx, metav1.ListOptions{LabelSelector: dsLabelSelector}) |
| framework.ExpectNoError(err, "Failed to list ControllerRevision: %v", err) |
| gomega.Expect(revs.Items).To(gomega.HaveLen(1), "Failed to find any controllerRevisions") |
| |
| // Locate the current ControllerRevision from the list |
| var initialRevision *appsv1.ControllerRevision |
| |
| rev := revs.Items[0] |
| oref := rev.OwnerReferences[0] |
| if oref.Kind == "DaemonSet" && oref.UID == ds.UID { |
| framework.Logf("Located ControllerRevision: %q", rev.Name) |
| initialRevision, err = csAppsV1.ControllerRevisions(ns).Get(ctx, rev.Name, metav1.GetOptions{}) |
| framework.ExpectNoError(err, "failed to lookup ControllerRevision: %v", err) |
| gomega.Expect(initialRevision).NotTo(gomega.BeNil(), "failed to lookup ControllerRevision: %v", initialRevision) |
| } |
| |
| ginkgo.By(fmt.Sprintf("Patching ControllerRevision %q", initialRevision.Name)) |
| payload := "{\"metadata\":{\"labels\":{\"" + initialRevision.Name + "\":\"patched\"}}}" |
| patchedControllerRevision, err := csAppsV1.ControllerRevisions(ns).Patch(ctx, initialRevision.Name, types.StrategicMergePatchType, []byte(payload), metav1.PatchOptions{}) |
| framework.ExpectNoError(err, "failed to patch ControllerRevision %s in namespace %s", initialRevision.Name, ns) |
| gomega.Expect(patchedControllerRevision.Labels).To(gomega.HaveKeyWithValue(initialRevision.Name, "patched"), "Did not find 'patched' label for this ControllerRevision. Current labels: %v", patchedControllerRevision.Labels) |
| framework.Logf("%s has been patched", patchedControllerRevision.Name) |
| |
| ginkgo.By("Create a new ControllerRevision") |
| ds.Spec.Template.Spec.TerminationGracePeriodSeconds = pointer.Int64(1) |
| newHash, newName := hashAndNameForDaemonSet(ds) |
| newRevision := &appsv1.ControllerRevision{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: newName, |
| Namespace: ds.Namespace, |
| Labels: labelsutil.CloneAndAddLabel(ds.Spec.Template.Labels, appsv1.DefaultDaemonSetUniqueLabelKey, newHash), |
| Annotations: ds.Annotations, |
| OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(ds, appsv1.SchemeGroupVersion.WithKind("DaemonSet"))}, |
| }, |
| Data: initialRevision.Data, |
| Revision: initialRevision.Revision + 1, |
| } |
| newControllerRevision, err := csAppsV1.ControllerRevisions(ns).Create(ctx, newRevision, metav1.CreateOptions{}) |
| framework.ExpectNoError(err, "Failed to create ControllerRevision: %v", err) |
| framework.Logf("Created ControllerRevision: %s", newControllerRevision.Name) |
| |
| ginkgo.By("Confirm that there are two ControllerRevisions") |
| err = wait.PollUntilContextTimeout(ctx, controllerRevisionRetryPeriod, controllerRevisionRetryTimeout, true, checkControllerRevisionListQuantity(f, dsLabelSelector, 2)) |
| framework.ExpectNoError(err, "failed to count required ControllerRevisions") |
| |
| ginkgo.By(fmt.Sprintf("Deleting ControllerRevision %q", initialRevision.Name)) |
| err = csAppsV1.ControllerRevisions(ns).Delete(ctx, initialRevision.Name, metav1.DeleteOptions{}) |
| framework.ExpectNoError(err, "Failed to delete ControllerRevision: %v", err) |
| |
| ginkgo.By("Confirm that there is only one ControllerRevision") |
| err = wait.PollUntilContextTimeout(ctx, controllerRevisionRetryPeriod, controllerRevisionRetryTimeout, true, checkControllerRevisionListQuantity(f, dsLabelSelector, 1)) |
| framework.ExpectNoError(err, "failed to count required ControllerRevisions") |
| |
| listControllerRevisions, err := csAppsV1.ControllerRevisions(ns).List(ctx, metav1.ListOptions{}) |
| currentControllerRevision := listControllerRevisions.Items[0] |
| |
| ginkgo.By(fmt.Sprintf("Updating ControllerRevision %q", currentControllerRevision.Name)) |
| var updatedControllerRevision *appsv1.ControllerRevision |
| |
| err = retry.RetryOnConflict(retry.DefaultRetry, func() error { |
| updatedControllerRevision, err = csAppsV1.ControllerRevisions(ns).Get(ctx, currentControllerRevision.Name, metav1.GetOptions{}) |
| framework.ExpectNoError(err, "Unable to get ControllerRevision %s", currentControllerRevision.Name) |
| updatedControllerRevision.Labels[currentControllerRevision.Name] = "updated" |
| updatedControllerRevision, err = csAppsV1.ControllerRevisions(ns).Update(ctx, updatedControllerRevision, metav1.UpdateOptions{}) |
| return err |
| }) |
| framework.ExpectNoError(err, "failed to update ControllerRevision in namespace: %s", ns) |
| gomega.Expect(updatedControllerRevision.Labels).To(gomega.HaveKeyWithValue(currentControllerRevision.Name, "updated"), "Did not find 'updated' label for this ControllerRevision. Current labels: %v", updatedControllerRevision.Labels) |
| framework.Logf("%s has been updated", updatedControllerRevision.Name) |
| |
| ginkgo.By("Generate another ControllerRevision by patching the Daemonset") |
| patch := fmt.Sprintf(`{"spec":{"template":{"spec":{"terminationGracePeriodSeconds": %d}}},"updateStrategy":{"type":"RollingUpdate"}}`, 1) |
| |
| _, err = c.AppsV1().DaemonSets(ns).Patch(ctx, dsName, types.StrategicMergePatchType, []byte(patch), metav1.PatchOptions{}) |
| framework.ExpectNoError(err, "error patching daemon set") |
| |
| ginkgo.By("Confirm that there are two ControllerRevisions") |
| err = wait.PollUntilContextTimeout(ctx, controllerRevisionRetryPeriod, controllerRevisionRetryTimeout, true, checkControllerRevisionListQuantity(f, dsLabelSelector, 2)) |
| framework.ExpectNoError(err, "failed to count required ControllerRevisions") |
| |
| updatedLabel := map[string]string{updatedControllerRevision.Name: "updated"} |
| updatedLabelSelector := labels.SelectorFromSet(updatedLabel).String() |
| |
| ginkgo.By(fmt.Sprintf("Removing a ControllerRevision via 'DeleteCollection' with labelSelector: %q", updatedLabelSelector)) |
| err = csAppsV1.ControllerRevisions(ns).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: updatedLabelSelector}) |
| framework.ExpectNoError(err, "Failed to delete ControllerRevision: %v", err) |
| |
| ginkgo.By("Confirm that there is only one ControllerRevision") |
| err = wait.PollUntilContextTimeout(ctx, controllerRevisionRetryPeriod, controllerRevisionRetryTimeout, true, checkControllerRevisionListQuantity(f, dsLabelSelector, 1)) |
| framework.ExpectNoError(err, "failed to count required ControllerRevisions") |
| |
| list, err := csAppsV1.ControllerRevisions(ns).List(ctx, metav1.ListOptions{}) |
| framework.ExpectNoError(err, "failed to list ControllerRevision") |
| gomega.Expect(list.Items[0].Revision).To(gomega.Equal(int64(3)), "failed to find the expected revision for the Controller") |
| framework.Logf("ControllerRevision %q has revision %d", list.Items[0].Name, list.Items[0].Revision) |
| }) |
| }) |
| |
| func checkControllerRevisionListQuantity(f *framework.Framework, label string, quantity int) func(ctx context.Context) (bool, error) { |
| return func(ctx context.Context) (bool, error) { |
| var err error |
| |
| framework.Logf("Requesting list of ControllerRevisions to confirm quantity") |
| |
| list, err := f.ClientSet.AppsV1().ControllerRevisions(f.Namespace.Name).List(ctx, metav1.ListOptions{ |
| LabelSelector: label}) |
| if err != nil { |
| return false, err |
| } |
| |
| if len(list.Items) != quantity { |
| return false, nil |
| } |
| framework.Logf("Found %d ControllerRevisions", quantity) |
| return true, nil |
| } |
| } |
| |
| func hashAndNameForDaemonSet(ds *appsv1.DaemonSet) (string, string) { |
| hash := fmt.Sprint(controller.ComputeHash(&ds.Spec.Template, ds.Status.CollisionCount)) |
| name := ds.Name + "-" + hash |
| return hash, name |
| } |