k8s.io/kubernetes@v1.29.3/test/e2e/apps/controller_revision.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package apps
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"time"
    23  
    24  	"github.com/onsi/ginkgo/v2"
    25  	"github.com/onsi/gomega"
    26  
    27  	appsv1 "k8s.io/api/apps/v1"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/labels"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apimachinery/pkg/types"
    32  	utilrand "k8s.io/apimachinery/pkg/util/rand"
    33  	"k8s.io/apimachinery/pkg/util/wait"
    34  	clientset "k8s.io/client-go/kubernetes"
    35  	"k8s.io/client-go/kubernetes/scheme"
    36  	"k8s.io/client-go/util/retry"
    37  	extensionsinternal "k8s.io/kubernetes/pkg/apis/extensions"
    38  	"k8s.io/kubernetes/pkg/controller"
    39  	labelsutil "k8s.io/kubernetes/pkg/util/labels"
    40  	"k8s.io/kubernetes/test/e2e/framework"
    41  	e2edaemonset "k8s.io/kubernetes/test/e2e/framework/daemonset"
    42  	e2eresource "k8s.io/kubernetes/test/e2e/framework/resource"
    43  	admissionapi "k8s.io/pod-security-admission/api"
    44  	"k8s.io/utils/pointer"
    45  )
    46  
    47  const (
    48  	controllerRevisionRetryPeriod  = 1 * time.Second
    49  	controllerRevisionRetryTimeout = 1 * time.Minute
    50  )
    51  
    52  // This test must be run in serial because it assumes the Daemon Set pods will
    53  // always get scheduled.  If we run other tests in parallel, this may not
    54  // happen.  In the future, running in parallel may work if we have an eviction
    55  // model which lets the DS controller kick out other pods to make room.
    56  // See https://issues.k8s.io/21767 for more details
    57  var _ = SIGDescribe("ControllerRevision", framework.WithSerial(), func() {
    58  	var f *framework.Framework
    59  
    60  	ginkgo.AfterEach(func(ctx context.Context) {
    61  		// Clean up
    62  		daemonsets, err := f.ClientSet.AppsV1().DaemonSets(f.Namespace.Name).List(ctx, metav1.ListOptions{})
    63  		framework.ExpectNoError(err, "unable to dump DaemonSets")
    64  		if daemonsets != nil && len(daemonsets.Items) > 0 {
    65  			for _, ds := range daemonsets.Items {
    66  				ginkgo.By(fmt.Sprintf("Deleting DaemonSet %q", ds.Name))
    67  				framework.ExpectNoError(e2eresource.DeleteResourceAndWaitForGC(ctx, f.ClientSet, extensionsinternal.Kind("DaemonSet"), f.Namespace.Name, ds.Name))
    68  				err = wait.PollUntilContextTimeout(ctx, dsRetryPeriod, dsRetryTimeout, true, checkRunningOnNoNodes(f, &ds))
    69  				framework.ExpectNoError(err, "error waiting for daemon pod to be reaped")
    70  			}
    71  		}
    72  		if daemonsets, err := f.ClientSet.AppsV1().DaemonSets(f.Namespace.Name).List(ctx, metav1.ListOptions{}); err == nil {
    73  			framework.Logf("daemonset: %s", runtime.EncodeOrDie(scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...), daemonsets))
    74  		} else {
    75  			framework.Logf("unable to dump daemonsets: %v", err)
    76  		}
    77  		if pods, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).List(ctx, metav1.ListOptions{}); err == nil {
    78  			framework.Logf("pods: %s", runtime.EncodeOrDie(scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...), pods))
    79  		} else {
    80  			framework.Logf("unable to dump pods: %v", err)
    81  		}
    82  		err = clearDaemonSetNodeLabels(ctx, f.ClientSet)
    83  		framework.ExpectNoError(err)
    84  	})
    85  
    86  	f = framework.NewDefaultFramework("controllerrevisions")
    87  	f.NamespacePodSecurityLevel = admissionapi.LevelBaseline
    88  
    89  	image := WebserverImage
    90  	dsName := "e2e-" + utilrand.String(5) + "-daemon-set"
    91  
    92  	var ns string
    93  	var c clientset.Interface
    94  
    95  	ginkgo.BeforeEach(func(ctx context.Context) {
    96  		ns = f.Namespace.Name
    97  
    98  		c = f.ClientSet
    99  
   100  		updatedNS, err := patchNamespaceAnnotations(ctx, c, ns)
   101  		framework.ExpectNoError(err)
   102  
   103  		ns = updatedNS.Name
   104  
   105  		err = clearDaemonSetNodeLabels(ctx, c)
   106  		framework.ExpectNoError(err)
   107  	})
   108  
   109  	/*
   110  		Release: v1.25
   111  		Testname: ControllerRevision, resource lifecycle
   112  		Description: Creating a DaemonSet MUST succeed. Listing all
   113  		ControllerRevisions with a label selector MUST find only one.
   114  		After patching the ControllerRevision with a new label, the label
   115  		MUST be found. Creating a new ControllerRevision for the DaemonSet
   116  		MUST succeed. Listing the ControllerRevisions by label selector
   117  		MUST find only two. Deleting a ControllerRevision MUST succeed.
   118  		Listing the ControllerRevisions by label selector MUST find only
   119  		one. After updating the ControllerRevision with a new label, the
   120  		label MUST be found. Patching the DaemonSet MUST succeed. Listing the
   121  		ControllerRevisions by label selector MUST find only two. Deleting
   122  		a collection of ControllerRevision via a label selector MUST succeed.
   123  		Listing the ControllerRevisions by label selector MUST find only one.
   124  		The current ControllerRevision revision MUST be 3.
   125  	*/
   126  	framework.ConformanceIt("should manage the lifecycle of a ControllerRevision", func(ctx context.Context) {
   127  		csAppsV1 := f.ClientSet.AppsV1()
   128  
   129  		dsLabel := map[string]string{"daemonset-name": dsName}
   130  		dsLabelSelector := labels.SelectorFromSet(dsLabel).String()
   131  
   132  		ginkgo.By(fmt.Sprintf("Creating DaemonSet %q", dsName))
   133  		testDaemonset, err := csAppsV1.DaemonSets(ns).Create(ctx, newDaemonSetWithLabel(dsName, image, dsLabel), metav1.CreateOptions{})
   134  		framework.ExpectNoError(err)
   135  
   136  		ginkgo.By("Check that daemon pods launch on every node of the cluster.")
   137  		err = wait.PollUntilContextTimeout(ctx, dsRetryPeriod, dsRetryTimeout, true, checkRunningOnAllNodes(f, testDaemonset))
   138  		framework.ExpectNoError(err, "error waiting for daemon pod to start")
   139  		err = e2edaemonset.CheckDaemonStatus(ctx, f, dsName)
   140  		framework.ExpectNoError(err)
   141  
   142  		ginkgo.By(fmt.Sprintf("Confirm DaemonSet %q successfully created with %q label", dsName, dsLabelSelector))
   143  		dsList, err := csAppsV1.DaemonSets("").List(ctx, metav1.ListOptions{LabelSelector: dsLabelSelector})
   144  		framework.ExpectNoError(err, "failed to list Daemon Sets")
   145  		gomega.Expect(dsList.Items).To(gomega.HaveLen(1), "filtered list wasn't found")
   146  
   147  		ds, err := c.AppsV1().DaemonSets(ns).Get(ctx, dsName, metav1.GetOptions{})
   148  		framework.ExpectNoError(err)
   149  
   150  		// Listing across all namespaces to verify api endpoint: listAppsV1ControllerRevisionForAllNamespaces
   151  		ginkgo.By(fmt.Sprintf("Listing all ControllerRevisions with label %q", dsLabelSelector))
   152  		revs, err := csAppsV1.ControllerRevisions("").List(ctx, metav1.ListOptions{LabelSelector: dsLabelSelector})
   153  		framework.ExpectNoError(err, "Failed to list ControllerRevision: %v", err)
   154  		gomega.Expect(revs.Items).To(gomega.HaveLen(1), "Failed to find any controllerRevisions")
   155  
   156  		// Locate the current ControllerRevision from the list
   157  		var initialRevision *appsv1.ControllerRevision
   158  
   159  		rev := revs.Items[0]
   160  		oref := rev.OwnerReferences[0]
   161  		if oref.Kind == "DaemonSet" && oref.UID == ds.UID {
   162  			framework.Logf("Located ControllerRevision: %q", rev.Name)
   163  			initialRevision, err = csAppsV1.ControllerRevisions(ns).Get(ctx, rev.Name, metav1.GetOptions{})
   164  			framework.ExpectNoError(err, "failed to lookup ControllerRevision: %v", err)
   165  			gomega.Expect(initialRevision).NotTo(gomega.BeNil(), "failed to lookup ControllerRevision: %v", initialRevision)
   166  		}
   167  
   168  		ginkgo.By(fmt.Sprintf("Patching ControllerRevision %q", initialRevision.Name))
   169  		payload := "{\"metadata\":{\"labels\":{\"" + initialRevision.Name + "\":\"patched\"}}}"
   170  		patchedControllerRevision, err := csAppsV1.ControllerRevisions(ns).Patch(ctx, initialRevision.Name, types.StrategicMergePatchType, []byte(payload), metav1.PatchOptions{})
   171  		framework.ExpectNoError(err, "failed to patch ControllerRevision %s in namespace %s", initialRevision.Name, ns)
   172  		gomega.Expect(patchedControllerRevision.Labels).To(gomega.HaveKeyWithValue(initialRevision.Name, "patched"), "Did not find 'patched' label for this ControllerRevision. Current labels: %v", patchedControllerRevision.Labels)
   173  		framework.Logf("%s has been patched", patchedControllerRevision.Name)
   174  
   175  		ginkgo.By("Create a new ControllerRevision")
   176  		ds.Spec.Template.Spec.TerminationGracePeriodSeconds = pointer.Int64(1)
   177  		newHash, newName := hashAndNameForDaemonSet(ds)
   178  		newRevision := &appsv1.ControllerRevision{
   179  			ObjectMeta: metav1.ObjectMeta{
   180  				Name:            newName,
   181  				Namespace:       ds.Namespace,
   182  				Labels:          labelsutil.CloneAndAddLabel(ds.Spec.Template.Labels, appsv1.DefaultDaemonSetUniqueLabelKey, newHash),
   183  				Annotations:     ds.Annotations,
   184  				OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(ds, appsv1.SchemeGroupVersion.WithKind("DaemonSet"))},
   185  			},
   186  			Data:     initialRevision.Data,
   187  			Revision: initialRevision.Revision + 1,
   188  		}
   189  		newControllerRevision, err := csAppsV1.ControllerRevisions(ns).Create(ctx, newRevision, metav1.CreateOptions{})
   190  		framework.ExpectNoError(err, "Failed to create ControllerRevision: %v", err)
   191  		framework.Logf("Created ControllerRevision: %s", newControllerRevision.Name)
   192  
   193  		ginkgo.By("Confirm that there are two ControllerRevisions")
   194  		err = wait.PollUntilContextTimeout(ctx, controllerRevisionRetryPeriod, controllerRevisionRetryTimeout, true, checkControllerRevisionListQuantity(f, dsLabelSelector, 2))
   195  		framework.ExpectNoError(err, "failed to count required ControllerRevisions")
   196  
   197  		ginkgo.By(fmt.Sprintf("Deleting ControllerRevision %q", initialRevision.Name))
   198  		err = csAppsV1.ControllerRevisions(ns).Delete(ctx, initialRevision.Name, metav1.DeleteOptions{})
   199  		framework.ExpectNoError(err, "Failed to delete ControllerRevision: %v", err)
   200  
   201  		ginkgo.By("Confirm that there is only one ControllerRevision")
   202  		err = wait.PollUntilContextTimeout(ctx, controllerRevisionRetryPeriod, controllerRevisionRetryTimeout, true, checkControllerRevisionListQuantity(f, dsLabelSelector, 1))
   203  		framework.ExpectNoError(err, "failed to count required ControllerRevisions")
   204  
   205  		listControllerRevisions, err := csAppsV1.ControllerRevisions(ns).List(ctx, metav1.ListOptions{})
   206  		currentControllerRevision := listControllerRevisions.Items[0]
   207  
   208  		ginkgo.By(fmt.Sprintf("Updating ControllerRevision %q", currentControllerRevision.Name))
   209  		var updatedControllerRevision *appsv1.ControllerRevision
   210  
   211  		err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
   212  			updatedControllerRevision, err = csAppsV1.ControllerRevisions(ns).Get(ctx, currentControllerRevision.Name, metav1.GetOptions{})
   213  			framework.ExpectNoError(err, "Unable to get ControllerRevision %s", currentControllerRevision.Name)
   214  			updatedControllerRevision.Labels[currentControllerRevision.Name] = "updated"
   215  			updatedControllerRevision, err = csAppsV1.ControllerRevisions(ns).Update(ctx, updatedControllerRevision, metav1.UpdateOptions{})
   216  			return err
   217  		})
   218  		framework.ExpectNoError(err, "failed to update ControllerRevision in namespace: %s", ns)
   219  		gomega.Expect(updatedControllerRevision.Labels).To(gomega.HaveKeyWithValue(currentControllerRevision.Name, "updated"), "Did not find 'updated' label for this ControllerRevision. Current labels: %v", updatedControllerRevision.Labels)
   220  		framework.Logf("%s has been updated", updatedControllerRevision.Name)
   221  
   222  		ginkgo.By("Generate another ControllerRevision by patching the Daemonset")
   223  		patch := fmt.Sprintf(`{"spec":{"template":{"spec":{"terminationGracePeriodSeconds": %d}}},"updateStrategy":{"type":"RollingUpdate"}}`, 1)
   224  
   225  		_, err = c.AppsV1().DaemonSets(ns).Patch(ctx, dsName, types.StrategicMergePatchType, []byte(patch), metav1.PatchOptions{})
   226  		framework.ExpectNoError(err, "error patching daemon set")
   227  
   228  		ginkgo.By("Confirm that there are two ControllerRevisions")
   229  		err = wait.PollUntilContextTimeout(ctx, controllerRevisionRetryPeriod, controllerRevisionRetryTimeout, true, checkControllerRevisionListQuantity(f, dsLabelSelector, 2))
   230  		framework.ExpectNoError(err, "failed to count required ControllerRevisions")
   231  
   232  		updatedLabel := map[string]string{updatedControllerRevision.Name: "updated"}
   233  		updatedLabelSelector := labels.SelectorFromSet(updatedLabel).String()
   234  
   235  		ginkgo.By(fmt.Sprintf("Removing a ControllerRevision via 'DeleteCollection' with labelSelector: %q", updatedLabelSelector))
   236  		err = csAppsV1.ControllerRevisions(ns).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: updatedLabelSelector})
   237  		framework.ExpectNoError(err, "Failed to delete ControllerRevision: %v", err)
   238  
   239  		ginkgo.By("Confirm that there is only one ControllerRevision")
   240  		err = wait.PollUntilContextTimeout(ctx, controllerRevisionRetryPeriod, controllerRevisionRetryTimeout, true, checkControllerRevisionListQuantity(f, dsLabelSelector, 1))
   241  		framework.ExpectNoError(err, "failed to count required ControllerRevisions")
   242  
   243  		list, err := csAppsV1.ControllerRevisions(ns).List(ctx, metav1.ListOptions{})
   244  		framework.ExpectNoError(err, "failed to list ControllerRevision")
   245  		gomega.Expect(list.Items[0].Revision).To(gomega.Equal(int64(3)), "failed to find the expected revision for the Controller")
   246  		framework.Logf("ControllerRevision %q has revision %d", list.Items[0].Name, list.Items[0].Revision)
   247  	})
   248  })
   249  
   250  func checkControllerRevisionListQuantity(f *framework.Framework, label string, quantity int) func(ctx context.Context) (bool, error) {
   251  	return func(ctx context.Context) (bool, error) {
   252  		var err error
   253  
   254  		framework.Logf("Requesting list of ControllerRevisions to confirm quantity")
   255  
   256  		list, err := f.ClientSet.AppsV1().ControllerRevisions(f.Namespace.Name).List(ctx, metav1.ListOptions{
   257  			LabelSelector: label})
   258  		if err != nil {
   259  			return false, err
   260  		}
   261  
   262  		if len(list.Items) != quantity {
   263  			return false, nil
   264  		}
   265  		framework.Logf("Found %d ControllerRevisions", quantity)
   266  		return true, nil
   267  	}
   268  }
   269  
   270  func hashAndNameForDaemonSet(ds *appsv1.DaemonSet) (string, string) {
   271  	hash := fmt.Sprint(controller.ComputeHash(&ds.Spec.Template, ds.Status.CollisionCount))
   272  	name := ds.Name + "-" + hash
   273  	return hash, name
   274  }