From 70960792beba5954859bcb057e10b5482bd8b37f Mon Sep 17 00:00:00 2001 From: gangwgr Date: Fri, 19 Jun 2026 19:24:27 +0530 Subject: [PATCH] Migrate openshift-test-private kcm cases to OTE --- test/e2e/kcm_alerts.go | 72 ++++ test/e2e/kcm_controller.go | 281 ++++++++++++++ test/e2e/kcm_cronjob.go | 147 ++++++++ test/e2e/kcm_operator.go | 161 ++++++++ test/library/helpers.go | 352 ++++++++++++++++++ test/library/oc.go | 136 +++++++ test/testdata/testdata.go | 6 + test/testdata/workloads/cronjob54195.yaml | 37 ++ test/testdata/workloads/cronjob54195ic.yaml | 37 ++ test/testdata/workloads/cronjob54195notz.yaml | 36 ++ test/testdata/workloads/cronjob56176.yaml | 40 ++ test/testdata/workloads/cronjob75375.yaml | 41 ++ test/testdata/workloads/cronjob_54196.yaml | 49 +++ test/testdata/workloads/daemonset-origin.yaml | 59 +++ test/testdata/workloads/daemonset-update.yaml | 59 +++ .../workloads/deploy_duplicatepodsrs.yaml | 52 +++ test/testdata/workloads/deployment-69072.yaml | 77 ++++ test/testdata/workloads/deployment-73887.yaml | 37 ++ .../deployment-with-shutdown-gracefully.yaml | 75 ++++ test/testdata/workloads/project-69072.yaml | 12 + 20 files changed, 1766 insertions(+) create mode 100644 test/e2e/kcm_alerts.go create mode 100644 test/e2e/kcm_controller.go create mode 100644 test/e2e/kcm_cronjob.go create mode 100644 test/e2e/kcm_operator.go create mode 100644 test/library/helpers.go create mode 100644 test/library/oc.go create mode 100644 test/testdata/testdata.go create mode 100644 test/testdata/workloads/cronjob54195.yaml create mode 100644 test/testdata/workloads/cronjob54195ic.yaml create mode 100644 test/testdata/workloads/cronjob54195notz.yaml create mode 100644 test/testdata/workloads/cronjob56176.yaml create mode 100644 test/testdata/workloads/cronjob75375.yaml create mode 100644 test/testdata/workloads/cronjob_54196.yaml create mode 100644 test/testdata/workloads/daemonset-origin.yaml create mode 100644 test/testdata/workloads/daemonset-update.yaml create mode 100644 test/testdata/workloads/deploy_duplicatepodsrs.yaml create mode 100644 test/testdata/workloads/deployment-69072.yaml create mode 100644 test/testdata/workloads/deployment-73887.yaml create mode 100644 test/testdata/workloads/deployment-with-shutdown-gracefully.yaml create mode 100644 test/testdata/workloads/project-69072.yaml diff --git a/test/e2e/kcm_alerts.go b/test/e2e/kcm_alerts.go new file mode 100644 index 000000000..00d9ed1b2 --- /dev/null +++ b/test/e2e/kcm_alerts.go @@ -0,0 +1,72 @@ +package e2e + +import ( + "path/filepath" + + g "github.com/onsi/ginkgo/v2" + o "github.com/onsi/gomega" + + "github.com/openshift/cluster-kube-controller-manager-operator/pkg/operator/operatorclient" + testlib "github.com/openshift/cluster-kube-controller-manager-operator/test/library" +) + +var _ = g.Describe("kube-controller-manager alert workloads", func() { + g.It("[OTP][OCP-26247] Alert when pod has a PodDisruptionBudget with minAvailable 1 disruptionsAllowed 0", func() { + oc := testlib.NewOC() + oc.SetupProject() + testlib.SetNamespacePrivileged(oc, oc.Namespace()) + g.DeferCleanup(func() { + _ = oc.WithoutNamespace().Run("delete").Args("project", oc.Namespace()).Execute() + }) + ns := oc.Namespace() + + err := oc.WithoutNamespace().Run("create").Args("deployment", "deploy26247", "-n", ns, "--image", "quay.io/openshifttest/hello-openshift@sha256:4200f438cf2e9446f6bcff9d67ceea1f69ed07a2f83363b7fb52529f7ddd8a83").Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + + err = oc.WithoutNamespace().Run("create").Args("poddisruptionbudget", "pdb26247", "--selector=app=deploy26247", "--min-available=1", "-n", ns).Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + + token := testlib.GetSAToken(oc) + testlib.CheckMetric(oc, testlib.PrometheusURL+` --data-urlencode 'query=ALERTS{alertname="PodDisruptionBudgetAtLimit"}'`, token, "pdb26247", 600) + + err = oc.Run("scale").Args("deploy", "deploy26247", "--replicas=0").Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + testlib.CheckMetric(oc, testlib.PrometheusURL+` --data-urlencode 'query=ALERTS{alertname="PodDisruptionBudgetLimit"}'`, token, "pdb26247", 600) + }) + + g.It("[OTP][OCP-73887] validate for alert KubeDeploymentReplicasMismatch", func() { + oc := testlib.NewOC() + g.DeferCleanup(func() { + _ = oc.WithoutNamespace().Run("delete").Args("deployment", "replicas-mismatch", "-n", "default", "--ignore-not-found").Execute() + }) + + deployFile := filepath.Join(testlib.FixturePath("workloads"), "deployment-73887.yaml") + err := oc.WithoutNamespace().Run("create").Args("-f", deployFile, "-n", "default").Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + + token := testlib.GetSAToken(oc) + testlib.CheckMetric(oc, testlib.PrometheusURL+` --data-urlencode 'query=ALERTS{alertname="KubeDeploymentReplicasMismatch"}'`, token, "replicas-mismatch", 1200) + }) + + g.It("[OTP][OCP-73874] validate for alert KubeControllerManagerDown [Serial][Disruptive]", func() { + oc := testlib.NewOC() + masterNodes := testlib.GetClusterNodesBy(oc, "master") + if len(masterNodes) == 0 { + g.Skip("no master nodes available in cluster") + } + + g.DeferCleanup(func() { + for _, node := range masterNodes { + _ = oc.WithoutNamespace().Run("debug").Args("node/"+node, "-n", operatorclient.TargetNamespace, "--", "chroot", "/host", "mv", "/etc/kubernetes/kube-controller-manager-pod.yaml", "/etc/kubernetes/manifests/").Execute() + } + }) + + for _, node := range masterNodes { + err := oc.WithoutNamespace().Run("debug").Args("node/"+node, "-n", operatorclient.TargetNamespace, "--", "chroot", "/host", "mv", "/etc/kubernetes/manifests/kube-controller-manager-pod.yaml", "/etc/kubernetes/").Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + } + + token := testlib.GetSAToken(oc) + testlib.CheckMetric(oc, testlib.PrometheusURL+` --data-urlencode 'query=ALERTS{alertname="KubeControllerManagerDown"}'`, token, "openshift-kube-controller-manager", 1200) + }) +}) diff --git a/test/e2e/kcm_controller.go b/test/e2e/kcm_controller.go new file mode 100644 index 000000000..96effbcd1 --- /dev/null +++ b/test/e2e/kcm_controller.go @@ -0,0 +1,281 @@ +package e2e + +import ( + "context" + "fmt" + "path/filepath" + "regexp" + "strings" + "time" + + g "github.com/onsi/ginkgo/v2" + o "github.com/onsi/gomega" + + testlib "github.com/openshift/cluster-kube-controller-manager-operator/test/library" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" +) + +const ( + project43092Source = "p43092-1" + project43092Target = "p43092-2" + project43099 = "p43099" + clusterRole43099 = "foo43099" +) + +var _ = g.Describe("kube-controller-manager controller workloads", func() { + g.It("[OTP][OCP-43039] openshift-object-counts quota dynamically updating as the resource is deleted", func() { + oc := testlib.NewOC() + oc.SetupProject() + testlib.SetNamespacePrivileged(oc, oc.Namespace()) + g.DeferCleanup(func() { + _ = oc.WithoutNamespace().Run("delete").Args("project", oc.Namespace()).Execute() + }) + ns := oc.Namespace() + + err := oc.Run("create").Args("quota", "quota43039", "--hard=openshift.io/imagestreams=10").Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + + output, err := oc.WithoutNamespace().Run("describe").Args("quota", "quota43039", "-n", ns).Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(output).To(o.ContainSubstring("openshift.io/imagestreams 0 10")) + + err = oc.WithoutNamespace().Run("new-app").Args("quay.io/openshifttest/hello-openshift@sha256:4200f438cf2e9446f6bcff9d67ceea1f69ed07a2f83363b7fb52529f7ddd8a83", "-n", ns, "--import-mode=PreserveOriginal").Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + + output, err = oc.WithoutNamespace().Run("describe").Args("quota", "quota43039", "-n", ns).Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(output).To(o.ContainSubstring("openshift.io/imagestreams 1 10")) + + err = oc.WithoutNamespace().Run("delete").Args("is", "--all", "-n", ns).Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + err = wait.PollUntilContextCancel(ctx, 5*time.Second, true, func(ctx context.Context) (bool, error) { + output, err = oc.WithoutNamespace().Run("get").Args("is", "-n", ns).Output() + if err != nil { + return false, err + } + return strings.Contains(output, "No resources found"), nil + }) + testlib.AssertWaitPollNoErr(err, "ImageStream not deleted") + + output, err = oc.WithoutNamespace().Run("describe").Args("quota", "quota43039", "-n", ns).Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(output).To(o.ContainSubstring("openshift.io/imagestreams 0 10")) + }) + + g.It("[OTP][OCP-43092] Namespaced dependents try to use cross-namespace owner references will be deleted", func() { + oc := testlib.NewOC() + deployT := filepath.Join(testlib.FixturePath("workloads"), "deploy_duplicatepodsrs.yaml") + + _ = oc.WithoutNamespace().Run("delete").Args("project", project43092Source, "--ignore-not-found").Execute() + _ = oc.WithoutNamespace().Run("delete").Args("project", project43092Target, "--ignore-not-found").Execute() + + err := oc.WithoutNamespace().Run("new-project").Args(project43092Source).Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + g.DeferCleanup(func() { + _ = oc.WithoutNamespace().Run("delete").Args("project", project43092Source).Execute() + }) + + err = oc.WithoutNamespace().Run("new-app").Args("quay.io/openshifttest/hello-openshift@sha256:4200f438cf2e9446f6bcff9d67ceea1f69ed07a2f83363b7fb52529f7ddd8a83", "-n", project43092Source, "--import-mode=PreserveOriginal").Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + testlib.CheckPodStatus(oc, "deployment=hello-openshift", project43092Source, "Running") + + var reference string + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + err = wait.PollUntilContextCancel(ctx, 2*time.Second, true, func(ctx context.Context) (bool, error) { + reference, err = oc.WithoutNamespace().Run("get").Args("rs", "-o=jsonpath={.items[0].metadata.ownerReferences}", "-n", project43092Source).Output() + return err == nil && reference != "", nil + }) + testlib.AssertWaitPollNoErr(err, "RS ownerReferences not found") + + err = oc.WithoutNamespace().Run("new-project").Args(project43092Target).Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + g.DeferCleanup(func() { + _ = oc.WithoutNamespace().Run("delete").Args("project", project43092Target).Execute() + }) + + ctx3, cancel3 := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel3() + err = wait.PollUntilContextCancel(ctx3, 5*time.Second, true, func(ctx context.Context) (bool, error) { + err := testlib.ApplyResourceFromTemplate(oc, "--ignore-unknown-parameters=true", "-f", deployT, + "-p", "DNAME=hello-openshift", "NAMESPACE="+project43092Target, "REPLICASNUM=1") + return err == nil, nil + }) + testlib.AssertWaitPollNoErr(err, "create duplicate pods RS") + err = oc.WithoutNamespace().Run("patch").Args("rs/hello-openshift", "-n", project43092Target, "--type=json", "-p", "[{\"op\": \"add\" , \"path\" : \"/metadata/ownerReferences\", \"value\":"+reference+"}]").Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + + ctx2, cancel2 := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel2() + err = wait.PollUntilContextCancel(ctx2, 5*time.Second, true, func(ctx context.Context) (bool, error) { + output, err := oc.WithoutNamespace().Run("get").Args("rs", "-n", project43092Target).Output() + if err != nil { + return false, err + } + return strings.Contains(output, "No resources found"), nil + }) + testlib.AssertWaitPollNoErr(err, "RS not deleted") + + eve, err := oc.WithoutNamespace().Run("get").Args("events", "-n", project43092Target).Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(eve).To(o.ContainSubstring("OwnerRefInvalidNamespace")) + }) + + g.It("[OTP][OCP-43099] Cluster-scoped dependents with namespaced kind owner references will trigger warning Event", func() { + oc := testlib.NewOC() + _ = oc.WithoutNamespace().Run("delete").Args("project", project43099, "--ignore-not-found").Execute() + _ = oc.WithoutNamespace().Run("delete").Args("clusterrole", clusterRole43099, "--ignore-not-found").Execute() + + err := oc.WithoutNamespace().Run("new-project").Args(project43099).Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + g.DeferCleanup(func() { + _ = oc.WithoutNamespace().Run("delete").Args("project", project43099).Execute() + }) + + err = oc.WithoutNamespace().Run("new-app").Args("quay.io/openshifttest/hello-openshift@sha256:4200f438cf2e9446f6bcff9d67ceea1f69ed07a2f83363b7fb52529f7ddd8a83", "-n", project43099, "--import-mode=PreserveOriginal").Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + + var reference string + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + err = wait.PollUntilContextCancel(ctx, 2*time.Second, true, func(ctx context.Context) (bool, error) { + reference, err = oc.WithoutNamespace().Run("get").Args("rs", "-o=jsonpath={.items[0].metadata.ownerReferences}", "-n", project43099).Output() + return err == nil && reference != "", nil + }) + testlib.AssertWaitPollNoErr(err, "RS ownerReferences not found in "+project43099) + + err = oc.WithoutNamespace().Run("create").Args("clusterrole", clusterRole43099, "--verb=get,list,watch", "--resource=pods,pods/status").Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + g.DeferCleanup(func() { + _ = oc.WithoutNamespace().Run("delete").Args("clusterrole/" + clusterRole43099).Execute() + }) + err = oc.WithoutNamespace().Run("patch").Args("clusterrole/"+clusterRole43099, "--type=json", "-p", "[{\"op\": \"add\" , \"path\" : \"/metadata/ownerReferences\", \"value\":"+reference+"}]").Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + + ctx2, cancel2 := context.WithTimeout(context.Background(), 200*time.Second) + defer cancel2() + err = wait.PollUntilContextCancel(ctx2, 20*time.Second, true, func(ctx context.Context) (bool, error) { + output, err := oc.WithoutNamespace().Run("get").Args("events", "-n", "default").Output() + if err != nil { + return false, err + } + matched, _ := regexp.MatchString("Warning.*OwnerRefInvalidNamespace.*clusterrole/"+clusterRole43099, output) + return matched, nil + }) + testlib.AssertWaitPollNoErr(err, "OwnerRefInvalidNamespace event not found") + + output, err := oc.WithoutNamespace().Run("get").Args("clusterrole", clusterRole43099).Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(output).To(o.ContainSubstring(clusterRole43099)) + }) + + g.It("[OTP][OCP-67765] Make sure rolling update logic to exclude unsetting nodes [Serial]", func() { + if testlib.IsSNOCluster(testlib.NewOC()) { + g.Skip("requires multi-node cluster") + } + oc := testlib.NewOC() + oc.SetupProject() + testlib.SetNamespacePrivileged(oc, oc.Namespace()) + g.DeferCleanup(func() { + _ = oc.WithoutNamespace().Run("delete").Args("project", oc.Namespace()).Execute() + }) + ns := oc.Namespace() + + nodeList := testlib.GetClusterNodesBy(oc, "worker") + if len(nodeList) == 0 { + g.Skip("no worker nodes available in cluster") + } + g.DeferCleanup(func() { + _ = oc.WithoutNamespace().Run("adm").Args("taint", "node", nodeList[0], "dedicated:NoSchedule-").Execute() + }) + err := oc.WithoutNamespace().Run("adm").Args("taint", "node", nodeList[0], "dedicated=special-user:NoSchedule").Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + + dsOrigin := filepath.Join(testlib.FixturePath("workloads"), "daemonset-origin.yaml") + dsUpdate := filepath.Join(testlib.FixturePath("workloads"), "daemonset-update.yaml") + err = oc.Run("create").Args("-f", dsOrigin).Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + + kubeConfig, err := testlib.NewClientConfigForTest() + o.Expect(err).NotTo(o.HaveOccurred()) + kubeClient, err := kubernetes.NewForConfig(kubeConfig) + o.Expect(err).NotTo(o.HaveOccurred()) + + testlib.WaitForDaemonsetPodsToBeReady(kubeClient, ns, "hello-openshift") + o.Expect(testlib.GetDaemonsetDesiredNum(oc, ns, "hello-openshift")).To(o.Equal(len(nodeList))) + + err = oc.Run("replace").Args("-f", dsUpdate).Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + testlib.WaitForDaemonsetPodsToBeReady(kubeClient, ns, "hello-openshift") + o.Expect(testlib.GetDaemonsetDesiredNum(oc, ns, "hello-openshift")).To(o.Equal(len(nodeList) - 1)) + }) + + g.It("[OTP][OCP-69072] Infinite PODs loop creation with NodeAffinity status [Serial]", func() { + oc := testlib.NewOC() + projectFile := filepath.Join(testlib.FixturePath("workloads"), "project-69072.yaml") + deployFile := filepath.Join(testlib.FixturePath("workloads"), "deployment-69072.yaml") + + err := oc.WithoutNamespace().Run("create").Args("-f", projectFile).Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + g.DeferCleanup(func() { + _ = oc.WithoutNamespace().Run("delete").Args("-f", projectFile).Execute() + }) + err = oc.WithoutNamespace().Run("create").Args("-f", deployFile, "-n", "infinite-pod-creation-69072").Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + g.DeferCleanup(func() { + _ = oc.WithoutNamespace().Run("delete").Args("deployment", "infinite-pod-creation-69072", "-n", "infinite-pod-creation-69072").Execute() + }) + + testlib.CheckPodStatus(oc, "app=infinite-pod-creation-69072", "infinite-pod-creation-69072", "Running") + masters := testlib.GetClusterNodesBy(oc, "master") + if len(masters) == 0 { + g.Skip("no master nodes available in cluster") + } + patch := fmt.Sprintf(`{"spec":{"template":{"spec":{"nodeName": "%s"}}}}`, masters[0]) + _, err = oc.WithoutNamespace().Run("patch").Args("deployment", "-n", "infinite-pod-creation-69072", "infinite-pod-creation-69072", "--type=merge", "-p", patch).Output() + o.Expect(err).NotTo(o.HaveOccurred()) + + podStatus, err := oc.WithoutNamespace().Run("get").Args("pods", "-n", "infinite-pod-creation-69072").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + fmt.Fprintf(g.GinkgoWriter, "pod listing:\n%s\n", podStatus) + o.Expect(podStatus).NotTo(o.ContainSubstring("NodeAffinity")) + }) + + g.It("[OTP][OCP-19922] Terminating pod should be removed from endpoints list for service", func() { + oc := testlib.NewOC() + oc.SetupProject() + testlib.SetNamespacePrivileged(oc, oc.Namespace()) + g.DeferCleanup(func() { + _ = oc.WithoutNamespace().Run("delete").Args("project", oc.Namespace()).Execute() + }) + ns := oc.Namespace() + deployFile := filepath.Join(testlib.FixturePath("workloads"), "deployment-with-shutdown-gracefully.yaml") + + err := oc.Run("create").Args("-f", deployFile).Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + err = oc.Run("expose").Args("deployment", "hello-19922").Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(testlib.WaitForAvailableRsRunning(oc, "deployment", "hello-19922", ns, "1")).To(o.BeTrue()) + + podIP, err := oc.Run("get").Args("pod", "-l", "deployment=hello-19922", "-o=jsonpath={.items[*].status.podIP}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + + err = oc.Run("set").Args("env", "deployment", "hello-19922", "foo=bar").Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + + ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + defer cancel() + err = wait.PollUntilContextCancel(ctx, 10*time.Second, true, func(ctx context.Context) (bool, error) { + describeSVC, err := oc.Run("describe").Args("svc", "hello-19922").Output() + if err != nil { + return false, err + } + return !strings.Contains(describeSVC, podIP), nil + }) + testlib.AssertWaitPollNoErr(err, "terminating pod still in endpoints") + }) +}) diff --git a/test/e2e/kcm_cronjob.go b/test/e2e/kcm_cronjob.go new file mode 100644 index 000000000..95678e9cd --- /dev/null +++ b/test/e2e/kcm_cronjob.go @@ -0,0 +1,147 @@ +package e2e + +import ( + "context" + "path/filepath" + "regexp" + "time" + + g "github.com/onsi/ginkgo/v2" + o "github.com/onsi/gomega" + + testlib "github.com/openshift/cluster-kube-controller-manager-operator/test/library" + "k8s.io/apimachinery/pkg/util/wait" +) + +var _ = g.Describe("kube-controller-manager cronjob workloads", func() { + g.It("[OTP][OCP-56176] oc debug cronjob should fail with a meaningful error", func() { + oc := testlib.NewOC() + oc.SetupProject() + testlib.SetNamespacePrivileged(oc, oc.Namespace()) + g.DeferCleanup(func() { + _ = oc.WithoutNamespace().Run("delete").Args("project", oc.Namespace()).Execute() + }) + cronjobF := filepath.Join(testlib.FixturePath("workloads"), "cronjob56176.yaml") + + err := oc.Run("create").Args("-f", cronjobF).Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + output, err := oc.Run("get").Args("cronjob").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(output).To(o.ContainSubstring("cronjob56176")) + + debugErr, err := oc.Run("debug").Args("cronjob/cronjob56176").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(debugErr).NotTo(o.ContainSubstring("v1.CronJob is not supported by debug")) + }) + + g.It("[OTP][OCP-54195] Enable CronJobTimeZone feature and verify that it works fine", func() { + oc := testlib.NewOC() + oc.SetupProject() + testlib.SetNamespacePrivileged(oc, oc.Namespace()) + g.DeferCleanup(func() { + _ = oc.WithoutNamespace().Run("delete").Args("project", oc.Namespace()).Execute() + }) + base := testlib.FixturePath("workloads") + + err := oc.Run("create").Args("-f", filepath.Join(base, "cronjob54195.yaml")).Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) + defer cancel() + err = wait.PollUntilContextCancel(ctx, 10*time.Second, true, func(ctx context.Context) (bool, error) { + name, err := oc.WithoutNamespace().Run("get").Args("cronjob", "cronjob54195", "-n", oc.Namespace(), "-o=jsonpath={.metadata.name}").Output() + return err == nil && name == "cronjob54195", nil + }) + testlib.AssertWaitPollNoErr(err, "cronjob54195 not created") + + tz, err := oc.WithoutNamespace().Run("get").Args("cronjob", "cronjob54195", "-n", oc.Namespace(), "-o=jsonpath={.spec.timeZone}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(tz).To(o.ContainSubstring("Asia/Calcutta")) + + ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Second) + defer cancel2() + err = wait.PollUntilContextCancel(ctx2, 10*time.Second, true, func(ctx context.Context) (bool, error) { + jobs, err := oc.WithoutNamespace().Run("get").Args("job", "-n", oc.Namespace()).Output() + return err == nil && stringsContains(jobs, "cronjob54195"), nil + }) + testlib.AssertWaitPollNoErr(err, "job not created from cronjob54195") + + incorrect, err := oc.Run("create").Args("-f", filepath.Join(base, "cronjob54195ic.yaml")).Output() + o.Expect(err).To(o.HaveOccurred()) + o.Expect(incorrect).To(o.ContainSubstring("unknown time zone Asia/china")) + + err = oc.Run("create").Args("-f", filepath.Join(base, "cronjob54195notz.yaml")).Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + + ctx3, cancel3 := context.WithTimeout(context.Background(), 100*time.Second) + defer cancel3() + err = wait.PollUntilContextCancel(ctx3, 10*time.Second, true, func(ctx context.Context) (bool, error) { + name, err := oc.WithoutNamespace().Run("get").Args("cronjob", "cronjob54195notz", "-n", oc.Namespace(), "-o=jsonpath={.metadata.name}").Output() + return err == nil && name == "cronjob54195notz", nil + }) + testlib.AssertWaitPollNoErr(err, "cronjob54195notz not created") + }) + + g.It("[OTP][OCP-54196] Create cronjob by retreiving current time by its timezone", func() { + oc := testlib.NewOC() + oc.SetupProject() + testlib.SetNamespacePrivileged(oc, oc.Namespace()) + g.DeferCleanup(func() { + _ = oc.WithoutNamespace().Run("delete").Args("project", oc.Namespace()).Execute() + }) + schedule, timeZoneName := testlib.GetTimeFromTimezone() + o.Expect(schedule).NotTo(o.BeEmpty()) + o.Expect(timeZoneName).NotTo(o.BeEmpty()) + + template := filepath.Join(testlib.FixturePath("workloads"), "cronjob_54196.yaml") + err := testlib.ApplyResourceFromTemplate(oc, "--ignore-unknown-parameters=true", "-f", template, + "-p", "CNAME=cronjob54196", "NAMESPACE="+oc.Namespace(), "SCHEDULE="+schedule, "TIMEZONE="+timeZoneName) + o.Expect(err).NotTo(o.HaveOccurred()) + + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) + defer cancel() + err = wait.PollUntilContextCancel(ctx, 10*time.Second, true, func(ctx context.Context) (bool, error) { + name, err := oc.WithoutNamespace().Run("get").Args("cronjob", "cronjob54196", "-n", oc.Namespace(), "-o=jsonpath={.metadata.name}").Output() + return err == nil && name == "cronjob54196", nil + }) + testlib.AssertWaitPollNoErr(err, "cronjob54196 not created") + + tz, err := oc.WithoutNamespace().Run("get").Args("cronjob", "cronjob54196", "-n", oc.Namespace(), "-o=jsonpath={.spec.timeZone}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(tz).To(o.ContainSubstring(timeZoneName)) + + ctx2, cancel2 := context.WithTimeout(context.Background(), 180*time.Second) + defer cancel2() + err = wait.PollUntilContextCancel(ctx2, 10*time.Second, true, func(ctx context.Context) (bool, error) { + jobs, err := oc.WithoutNamespace().Run("get").Args("job", "-n", oc.Namespace()).Output() + return err == nil && stringsContains(jobs, "cronjob54196"), nil + }) + testlib.AssertWaitPollNoErr(err, "job not created from cronjob54196") + }) + + // OCP-75375 verifies that creating a Job from a CronJob via "oc create job --from=cronjob/..." + // succeeds without "cannot set blockOwnerDeletion" errors. + g.It("[OTP][OCP-75375] oc create job fails with cannot set blockOwnerDeletion", func() { + oc := testlib.NewOC() + oc.SetupProject() + testlib.SetNamespacePrivileged(oc, oc.Namespace()) + g.DeferCleanup(func() { + _ = oc.WithoutNamespace().Run("delete").Args("project", oc.Namespace()).Execute() + }) + cronjobF := filepath.Join(testlib.FixturePath("workloads"), "cronjob75375.yaml") + + err := oc.Run("create").Args("-f", cronjobF).Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + output, err := oc.Run("get").Args("cronjob").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(output).To(o.ContainSubstring("pi75375")) + + _, err = oc.Run("create").Args("job", "example-75375", "--from=cronjob/pi75375").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + }) +}) + +func stringsContains(s, sub string) bool { + matched, _ := regexp.MatchString(sub, s) + return matched +} diff --git a/test/e2e/kcm_operator.go b/test/e2e/kcm_operator.go new file mode 100644 index 000000000..d0af2b681 --- /dev/null +++ b/test/e2e/kcm_operator.go @@ -0,0 +1,161 @@ +package e2e + +import ( + "context" + "net/url" + "regexp" + "strings" + "time" + + g "github.com/onsi/ginkgo/v2" + o "github.com/onsi/gomega" + + configv1 "github.com/openshift/api/config/v1" + configclient "github.com/openshift/client-go/config/clientset/versioned/typed/config/v1" + "github.com/openshift/cluster-kube-controller-manager-operator/pkg/operator/operatorclient" + testlib "github.com/openshift/cluster-kube-controller-manager-operator/test/library" + "k8s.io/apimachinery/pkg/util/wait" +) + +var _ = g.Describe("kube-controller-manager-operator workloads", func() { + g.It("[OTP][OCP-28001] KCM should recover when its temporary secrets are deleted [Serial][Disruptive]", func() { + oc := testlib.NewOC() + namespace := operatorclient.TargetNamespace + + kubeConfig, err := testlib.NewClientConfigForTest() + o.Expect(err).NotTo(o.HaveOccurred()) + configClient, err := configclient.NewForConfig(kubeConfig) + o.Expect(err).NotTo(o.HaveOccurred()) + + // Ensure operator recovers to healthy state even if test fails mid-way + g.DeferCleanup(func() { + recoverCtx, recoverCancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer recoverCancel() + testlib.WaitForKubeControllerManagerClusterOperator(g.GinkgoTB(), recoverCtx, configClient, configv1.ConditionTrue, configv1.ConditionFalse, configv1.ConditionFalse) + }) + + var temporarySecrets []string + output, err := oc.Run("get").Args("secrets", "-n", namespace, "-o=jsonpath={.items[*].metadata.name}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + for _, name := range strings.Fields(output) { + annotations, err := oc.Run("get").Args("secrets", "-n", namespace, name, "-o=jsonpath={.metadata.annotations}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + if matched, _ := regexp.MatchString("kubernetes.io/service-account.name", annotations); matched { + continue + } + ownerKind, err := oc.Run("get").Args("secrets", "-n", namespace, name, "-o=jsonpath={.metadata.ownerReferences[0].kind}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + if ownerKind == "ConfigMap" { + continue + } + temporarySecrets = append(temporarySecrets, name) + } + + for _, secret := range temporarySecrets { + _, err = oc.Run("delete").Args("secrets", "-n", namespace, secret).Output() + o.Expect(err).NotTo(o.HaveOccurred()) + } + + err = testlib.WaitCoBecomes(oc, "kube-controller-manager", 100, map[string]string{"Progressing": "True"}) + testlib.AssertWaitPollNoErr(err, "kube-controller-manager operator did not start progressing") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + testlib.WaitForKubeControllerManagerClusterOperator(g.GinkgoTB(), ctx, configClient, configv1.ConditionTrue, configv1.ConditionFalse, configv1.ConditionFalse) + }) + + g.It("[OTP][OCP-43035] KCM use internal LB to avoid outages during kube-apiserver rollout [Serial][Disruptive]", func() { + oc := testlib.NewOC() + infra, err := oc.WithoutNamespace().Run("get").Args("infrastructures", "cluster", "-o=jsonpath={.status.infrastructureTopology}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + if infra == "SingleReplica" { + g.Skip("SNO cluster") + } + + output, err := oc.WithoutNamespace().Run("whoami").Args("--show-server").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + apiURL, err := url.Parse(strings.TrimSpace(output)) + o.Expect(err).NotTo(o.HaveOccurred()) + host := apiURL.Hostname() + o.Expect(host).To(o.HavePrefix("api.")) + internalLB := "server: https://api-int." + strings.TrimPrefix(host, "api.") + + output, err = oc.WithoutNamespace().Run("get").Args("configmap", "controller-manager-kubeconfig", "-n", operatorclient.TargetNamespace, "-oyaml").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(output).To(o.ContainSubstring(internalLB)) + + leaderKcm := testlib.GetLeaderKCM(oc) + g.DeferCleanup(func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + var lastErr error + err := wait.PollUntilContextCancel(ctx, 15*time.Second, true, func(ctx context.Context) (bool, error) { + _, lastErr = testlib.DebugNodeWithChroot(oc, leaderKcm, "mv", "/home/kube-apiserver-pod.yaml", "/etc/kubernetes/manifests/") + return lastErr == nil, nil + }) + if err != nil { + o.Expect(lastErr).NotTo(o.HaveOccurred()) + } + }) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + err = wait.PollUntilContextCancel(ctx, 15*time.Second, true, func(ctx context.Context) (bool, error) { + _, err := testlib.DebugNodeWithChroot(oc, leaderKcm, "mv", "/etc/kubernetes/manifests/kube-apiserver-pod.yaml", "/home/") + return err == nil, nil + }) + o.Expect(err).NotTo(o.HaveOccurred()) + err = oc.WithoutNamespace().Run("delete").Args("-n", "openshift-kube-apiserver", "pod/"+"kube-apiserver-"+leaderKcm).Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + + ctx2, cancel2 := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel2() + err = wait.PollUntilContextCancel(ctx2, 10*time.Second, true, func(ctx context.Context) (bool, error) { + output, err := oc.Run("get").Args("co", "kube-controller-manager").Output() + if err != nil { + return false, nil + } + matched, _ := regexp.MatchString("True.*False.*False", output) + return matched, nil + }) + testlib.AssertWaitPollNoErr(err, "kube-controller-manager operator unhealthy during KAS disruption") + }) + + g.It("[OTP][OCP-60194] Make sure KCM KS operator is rebased onto the latest version of Kubernetes", func() { + oc := testlib.NewOC() + ocVersion, err := oc.WithoutNamespace().Run("get").Args("node", "-o=jsonpath={.items[0].status.nodeInfo.kubeletVersion}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + kubeVersion := strings.Split(strings.Split(ocVersion, "+")[0], "v")[1] + kuberVersion := strings.Split(kubeVersion, ".")[0] + "." + strings.Split(kubeVersion, ".")[1] + + kcmPod, err := oc.WithoutNamespace().Run("describe").Args("pod", "-n", operatorclient.OperatorNamespace, "-l", "app=kube-controller-manager-operator").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(kcmPod).To(o.ContainSubstring(kuberVersion)) + + ksPod, err := oc.WithoutNamespace().Run("describe").Args("pod", "-n", "openshift-kube-scheduler-operator", "-l", "app=openshift-kube-scheduler-operator").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(ksPod).To(o.ContainSubstring(kuberVersion)) + }) + + g.It("63962 Verify MaxUnavailableStatefulSet feature is available via TechPreviewNoUpgrade", func() { + if !testlib.IsTechPreviewNoUpgrade() { + g.Skip("requires TechPreviewNoUpgrade feature set") + } + oc := testlib.NewOC() + kcmPodOut, err := oc.WithoutNamespace().Run("get").Args("pod", "-n", operatorclient.TargetNamespace, "-l", "app=kube-controller-manager", "-o=jsonpath={.items[0].spec.containers[0].args}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(kcmPodOut).To(o.ContainSubstring("--feature-gates=MaxUnavailableStatefulSet=true")) + }) + + g.It("[OTP][OCP-70877] Add annotation in the kube-controller-manager-guard static pod for workload partitioning", func() { + oc := testlib.NewOC() + testlib.SkipForSNOCluster(oc) + guardPods, err := oc.WithoutNamespace().Run("get").Args("po", "-n", operatorclient.TargetNamespace, "-l=app=guard", `-ojsonpath={.items[?(@.status.phase=="Running")].metadata.name}`).Output() + o.Expect(err).NotTo(o.HaveOccurred()) + names := strings.Fields(guardPods) + o.Expect(names).NotTo(o.BeEmpty()) + annotation, err := oc.WithoutNamespace().Run("get").Args("po", "-n", operatorclient.TargetNamespace, names[0], `-ojsonpath={.metadata.annotations}`).Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(annotation).To(o.ContainSubstring(`"target.workload.openshift.io/management":"{\"effect\": \"PreferredDuringScheduling\"}"`)) + }) + +}) diff --git a/test/library/helpers.go b/test/library/helpers.go new file mode 100644 index 000000000..6b34a60c2 --- /dev/null +++ b/test/library/helpers.go @@ -0,0 +1,352 @@ +package library + +import ( + "context" + "fmt" + "io/fs" + "os" + "os/exec" + "path/filepath" + "reflect" + "regexp" + "runtime" + "strconv" + "strings" + "sync" + "time" + + g "github.com/onsi/ginkgo/v2" + + configv1 "github.com/openshift/api/config/v1" + configclient "github.com/openshift/client-go/config/clientset/versioned/typed/config/v1" + testdataFS "github.com/openshift/cluster-kube-controller-manager-operator/test/testdata" + machineryerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" +) + +const ( + // coStabilityInterval is the time to wait after an operator reaches the + // desired status before re-checking, to confirm the status is stable. + coStabilityInterval = 100 * time.Second + + // PrometheusURL is the in-cluster Prometheus query endpoint. + PrometheusURL = "https://prometheus-k8s.openshift-monitoring.svc:9091/api/v1/query" +) + +var ( + fixtureDir string + fixtureOnce sync.Once +) + +// FixturePath returns an absolute path to a file under test/testdata. +// When running from source (go test), it resolves relative to the source tree. +// When running from a compiled binary (OTE), it extracts embedded fixtures to a +// temp directory and returns paths there. +func FixturePath(elem ...string) string { + _, filename, _, ok := runtime.Caller(0) + if ok { + base := filepath.Join(filepath.Dir(filename), "..", "testdata") + path := filepath.Join(append([]string{base}, elem...)...) + if _, err := os.Stat(path); err == nil { + return path + } + } + + fixtureOnce.Do(func() { + dir, err := os.MkdirTemp("", "kcm-e2e-fixtures-") + if err != nil { + g.Fail(fmt.Sprintf("create fixture temp dir: %v", err)) + } + if err := fs.WalkDir(testdataFS.Content, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + dest := filepath.Join(dir, path) + if d.IsDir() { + return os.MkdirAll(dest, 0o755) + } + data, err := testdataFS.Content.ReadFile(path) + if err != nil { + return err + } + return os.WriteFile(dest, data, 0o644) + }); err != nil { + g.Fail(fmt.Sprintf("extract embedded fixtures: %v", err)) + } + fixtureDir = dir + }) + + return filepath.Join(append([]string{fixtureDir}, elem...)...) +} + +func AssertWaitPollNoErr(err error, msg string) { + if err != nil { + g.Fail(fmt.Sprintf("%s: %v", msg, err)) + } +} + +func WaitCoBecomes(oc *OC, coName string, waitSec int, expectedStatus map[string]string) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(waitSec)*time.Second) + defer cancel() + return wait.PollUntilContextCancel(ctx, 5*time.Second, true, func(ctx context.Context) (bool, error) { + gotten := getCoStatus(oc, coName, expectedStatus) + if !reflect.DeepEqual(expectedStatus, gotten) { + return false, nil + } + if reflect.DeepEqual(expectedStatus, map[string]string{"Available": "True", "Progressing": "False", "Degraded": "False"}) { + time.Sleep(coStabilityInterval) + gotten = getCoStatus(oc, coName, expectedStatus) + if !reflect.DeepEqual(expectedStatus, gotten) { + return false, nil + } + } + fmt.Fprintf(g.GinkgoWriter, "operator %s reached status %v\n", coName, gotten) + return true, nil + }) +} + +func getCoStatus(oc *OC, coName string, statusToCompare map[string]string) map[string]string { + result := make(map[string]string) + for key := range statusToCompare { + args := fmt.Sprintf(`-o=jsonpath={.status.conditions[?(.type == '%s')].status}`, key) + status, err := oc.WithoutNamespace().Run("get").Args("co", args, coName).Output() + if err != nil { + g.Fail(fmt.Sprintf("get co status: %v", err)) + } + result[key] = status + } + return result +} + +func GetLeaderKCM(oc *OC) string { + output, err := oc.WithoutNamespace().Run("get").Args("lease/kube-controller-manager", "-n", "kube-system", "-o=jsonpath={.spec.holderIdentity}").Output() + if err != nil { + g.Fail(fmt.Sprintf("get KCM leader lease: %v", err)) + } + leaderIP := strings.Split(output, "_")[0] + out, err := oc.WithoutNamespace().Run("get").Args("node", "-l", "node-role.kubernetes.io/master=", "-o=jsonpath={.items[*].metadata.name}").Output() + if err != nil { + g.Fail(fmt.Sprintf("get master nodes: %v", err)) + } + for _, master := range strings.Fields(out) { + if matched, _ := regexp.MatchString(leaderIP, master); matched { + return master + } + } + g.Fail("KCM leader node not found") + return "" +} + +func GetClusterNodesBy(oc *OC, role string) []string { + label := "node-role.kubernetes.io/worker=" + if role == "master" { + label = "node-role.kubernetes.io/master=" + } + out, err := oc.WithoutNamespace().Run("get").Args("node", "-l", label, "-o=jsonpath={.items[*].metadata.name}").Output() + if err != nil { + g.Fail(fmt.Sprintf("get cluster nodes by %s: %v", role, err)) + } + return strings.Fields(out) +} + +func DebugNodeWithChroot(oc *OC, node string, args ...string) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + debugArgs := append([]string{"debug", "node/" + node, "-n", "openshift-kube-controller-manager", "--", "chroot", "/host"}, args...) + cmd := exec.CommandContext(ctx, "oc", debugArgs...) + cmd.Env = append(os.Environ(), "KUBECONFIG="+oc.kubeconfig) + out, err := cmd.CombinedOutput() + return string(out), err +} + +func GetSAToken(oc *OC) string { + token, err := oc.WithoutNamespace().Run("create").Args("token", "prometheus-k8s", "-n", "openshift-monitoring").Output() + if err != nil { + g.Fail(fmt.Sprintf("get SA token: %v", err)) + } + return strings.TrimSpace(token) +} + +func RemoteShPod(oc *OC, namespace, pod, shell string, args ...string) (string, error) { + execArgs := append([]string{"-n", namespace, "pod/" + pod, "--", shell}, args...) + return oc.WithoutNamespace().Run("exec").Args(execArgs...).Output() +} + +func CheckMetric(oc *OC, url, token, metricString string, timeoutSec int) { + getCmd := "curl -G -k -s -H 'Authorization:Bearer " + token + "' " + url + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second) + defer cancel() + err := wait.PollUntilContextCancel(ctx, 10*time.Second, true, func(ctx context.Context) (bool, error) { + metrics, err := RemoteShPod(oc, "openshift-monitoring", "prometheus-k8s-0", "sh", "-c", getCmd) + if err != nil || !strings.Contains(metrics, metricString) { + return false, nil + } + return true, nil + }) + AssertWaitPollNoErr(err, fmt.Sprintf("metric output should contain %q", metricString)) +} + +func WaitForAvailableRsRunning(oc *OC, rsKind, rsName, namespace, expected string) bool { + ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second) + defer cancel() + err := wait.PollUntilContextCancel(ctx, 20*time.Second, true, func(ctx context.Context) (bool, error) { + output, err := oc.WithoutNamespace().Run("get").Args(rsKind, rsName, "-n", namespace, "-o=jsonpath={.status.availableReplicas}").Output() + if err != nil { + return false, nil + } + return strings.TrimSpace(output) == expected, nil + }) + return err == nil +} + +func CheckPodStatus(oc *OC, podLabel, namespace, expected string) { + ctx, cancel := context.WithTimeout(context.Background(), 300*time.Second) + defer cancel() + err := wait.PollUntilContextCancel(ctx, 20*time.Second, true, func(ctx context.Context) (bool, error) { + output, err := oc.WithoutNamespace().Run("get").Args("pod", "-n", namespace, "-l", podLabel, "-o=jsonpath={.items[*].status.phase}").Output() + if err != nil { + return false, nil + } + return strings.Contains(output, expected), nil + }) + AssertWaitPollNoErr(err, fmt.Sprintf("pod %s not in expected state %s", podLabel, expected)) +} + +func IsTechPreviewNoUpgrade() bool { + kubeConfig, err := NewClientConfigForTest() + if err != nil { + g.Fail(fmt.Sprintf("get kube config: %v", err)) + } + client, err := configclient.NewForConfig(kubeConfig) + if err != nil { + g.Fail(fmt.Sprintf("create config client: %v", err)) + } + fg, err := client.FeatureGates().Get(context.Background(), "cluster", metav1.GetOptions{}) + if machineryerrors.IsNotFound(err) { + return false + } + if err != nil { + g.Fail(fmt.Sprintf("get feature gate: %v", err)) + } + return fg.Spec.FeatureSet == configv1.TechPreviewNoUpgrade +} + +func IsSNOCluster(oc *OC) bool { + masters := GetClusterNodesBy(oc, "master") + workers := GetClusterNodesBy(oc, "worker") + return len(masters) == 1 && len(workers) == 1 && masters[0] == workers[0] +} + +func SkipForSNOCluster(oc *OC) { + if IsSNOCluster(oc) { + g.Skip("SNO cluster — test requires multiple nodes") + } +} + +func GetTimeFromTimezone() (schedule, timeZone string) { + localZone, err := time.LoadLocation("") + if err != nil { + g.Fail(fmt.Sprintf("load timezone: %v", err)) + } + currentTime := time.Now().In(localZone) + hour, minu, _ := currentTime.Clock() + if minu >= 58 { + hour = (hour + 1) % 24 + minu = (minu + 2) % 60 + } else { + minu += 2 + } + return fmt.Sprintf("%d %d * * *", minu, hour), localZone.String() +} + +func ApplyResourceFromTemplate(oc *OC, parameters ...string) error { + var configFile string + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + err := wait.PollUntilContextCancel(ctx, 3*time.Second, true, func(ctx context.Context) (bool, error) { + out, err := oc.Run("process").Args(parameters...).Output() + if err != nil { + return false, nil + } + tmp, err := os.CreateTemp("", "kcm-workload-*.json") + if err != nil { + return false, err + } + if _, err := tmp.WriteString(out); err != nil { + return false, err + } + if err := tmp.Close(); err != nil { + return false, err + } + configFile = tmp.Name() + return true, nil + }) + if err != nil { + return err + } + defer os.Remove(configFile) + return oc.WithoutNamespace().Run("apply").Args("-f", configFile).Execute() +} + +func WaitForDaemonsetPodsToBeReady(kubeClient kubernetes.Interface, namespace, name string) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + err := wait.PollUntilContextCancel(ctx, 30*time.Second, true, func(ctx context.Context) (bool, error) { + ds, err := kubeClient.AppsV1().DaemonSets(namespace).Get(ctx, name, metav1.GetOptions{}) + if machineryerrors.IsNotFound(err) { + return false, nil + } + if err != nil { + return false, err + } + return ds.Status.DesiredNumberScheduled > 0 && + ds.Status.NumberReady == ds.Status.DesiredNumberScheduled && + ds.Status.NumberAvailable == ds.Status.DesiredNumberScheduled && + ds.Status.UpdatedNumberScheduled == ds.Status.DesiredNumberScheduled && + ds.Status.NumberMisscheduled == 0, nil + }) + AssertWaitPollNoErr(err, "daemonset not ready") +} + +func GetDaemonsetDesiredNum(oc *OC, namespace, name string) int { + out, err := oc.WithoutNamespace().Run("get").Args("daemonset", name, "-n", namespace, "-o=jsonpath={.status.desiredNumberScheduled}").Output() + if err != nil { + g.Fail(fmt.Sprintf("get daemonset desired number: %v", err)) + } + n, err := strconv.Atoi(out) + if err != nil { + g.Fail(fmt.Sprintf("parse daemonset desired number: %v", err)) + } + return n +} + +func CheckNodeUncordoned(oc *OC, node string) error { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + return wait.PollUntilContextCancel(ctx, 30*time.Second, true, func(ctx context.Context) (bool, error) { + status, err := oc.WithoutNamespace().Run("get").Args("nodes", node, "-o=jsonpath={.spec}").Output() + if err != nil { + return false, err + } + return !strings.Contains(status, "unschedulable"), nil + }) +} + +func SetNamespacePrivileged(oc *OC, namespace string) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + var lastErr error + err := wait.PollUntilContextCancel(ctx, 2*time.Second, true, func(ctx context.Context) (bool, error) { + lastErr = oc.WithoutNamespace().Run("label").Args("namespace", namespace, + "pod-security.kubernetes.io/enforce=privileged", + "pod-security.kubernetes.io/warn=privileged", + "pod-security.kubernetes.io/audit=privileged", + "--overwrite").Execute() + return lastErr == nil, nil + }) + if err != nil { + g.Fail(fmt.Sprintf("set namespace privileged: %v", lastErr)) + } +} diff --git a/test/library/oc.go b/test/library/oc.go new file mode 100644 index 000000000..1ef6b45a2 --- /dev/null +++ b/test/library/oc.go @@ -0,0 +1,136 @@ +package library + +import ( + "context" + "fmt" + "math/rand" + "os" + "os/exec" + "regexp" + "strings" + "time" + + g "github.com/onsi/ginkgo/v2" +) + +const ocCommandTimeout = 5 * time.Minute + +var bearerTokenRe = regexp.MustCompile(`Bearer\s+\S+`) + +// OC wraps oc CLI for e2e tests that still rely on openshift/cli behavior. +// Each instance gets its own kubeconfig copy to avoid corruption from +// concurrent oc commands modifying the same file. +type OC struct { + namespace string + noNS bool + kubeconfig string +} + +func NewOC() *OC { + oc := &OC{} + oc.kubeconfig = copyKubeconfig() + return oc +} + +func (c *OC) Namespace() string { + return c.namespace +} + +func (c *OC) SetupProject() { + name := randomString() + _, err := c.WithoutNamespace().Run("new-project").Args(name).Output() + if err != nil { + g.Fail(fmt.Sprintf("create project: %v", err)) + } + c.namespace = name +} + +func (c *OC) WithoutNamespace() *OC { + clone := *c + clone.noNS = true + return &clone +} + +type ocCommand struct { + oc *OC + args []string +} + +func (c *OC) Run(subcommand string) *ocCommand { + return &ocCommand{oc: c, args: []string{subcommand}} +} + +func (cmd *ocCommand) Args(args ...string) *ocCommand { + cmd.args = append(cmd.args, args...) + return cmd +} + +func (cmd *ocCommand) Output() (string, error) { + out, err := cmd.run() + return string(out), err +} + +func (cmd *ocCommand) Execute() error { + _, err := cmd.run() + return err +} + +func (cmd *ocCommand) Outputs() (string, string, error) { + out, err := cmd.run() + return string(out), string(out), err +} + +func (cmd *ocCommand) run() ([]byte, error) { + args := append([]string{}, cmd.args...) + if !cmd.oc.noNS && cmd.oc.namespace != "" { + args = append(args, "-n", cmd.oc.namespace) + } + full := append([]string{"oc"}, args...) + fmt.Fprintf(g.GinkgoWriter, "running: %s\n", redactSensitive(strings.Join(full, " "))) + ctx, cancel := context.WithTimeout(context.Background(), ocCommandTimeout) + defer cancel() + c := exec.CommandContext(ctx, "oc", args...) + c.Env = append(os.Environ(), "KUBECONFIG="+cmd.oc.kubeconfig) + out, err := c.CombinedOutput() + if err != nil { + return out, fmt.Errorf("%w: %s", err, strings.TrimSpace(string(out))) + } + return out, nil +} + +// copyKubeconfig creates a per-instance copy of the kubeconfig so that +// parallel tests don't corrupt a shared file when oc modifies contexts. +func copyKubeconfig() string { + src := os.Getenv("KUBECONFIG") + if src == "" { + src = os.ExpandEnv("$HOME/.kube/config") + } + data, err := os.ReadFile(src) + if err != nil { + g.Fail(fmt.Sprintf("read kubeconfig %s: %v", src, err)) + } + tmp, err := os.CreateTemp("", "kcm-e2e-kubeconfig-*") + if err != nil { + g.Fail(fmt.Sprintf("create temp kubeconfig: %v", err)) + } + if _, err := tmp.Write(data); err != nil { + tmp.Close() + g.Fail(fmt.Sprintf("write temp kubeconfig: %v", err)) + } + tmp.Close() + return tmp.Name() +} + +func randomString() string { + const chars = "abcdefghijklmnopqrstuvwxyz0123456789" + seed := rand.New(rand.NewSource(time.Now().UnixNano())) + b := make([]byte, 8) + for i := range b { + b[i] = chars[seed.Intn(len(chars))] + } + return "kcm-e2e-" + string(b) +} + +func redactSensitive(s string) string { + return bearerTokenRe.ReplaceAllString(s, "Bearer [REDACTED]") +} diff --git a/test/testdata/testdata.go b/test/testdata/testdata.go new file mode 100644 index 000000000..76f1c62e6 --- /dev/null +++ b/test/testdata/testdata.go @@ -0,0 +1,6 @@ +package testdata + +import "embed" + +//go:embed workloads/* +var Content embed.FS diff --git a/test/testdata/workloads/cronjob54195.yaml b/test/testdata/workloads/cronjob54195.yaml new file mode 100644 index 000000000..297314a74 --- /dev/null +++ b/test/testdata/workloads/cronjob54195.yaml @@ -0,0 +1,37 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: cronjob54195 +spec: + schedule: '* * * * *' + timeZone: Asia/Calcutta + jobTemplate: + spec: + template: + spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: hello + image: quay.io/openshifttest/busybox@sha256:c5439d7db88ab5423999530349d327b04279ad3161d7596d2126dfb5b02bfd1f + imagePullPolicy: IfNotPresent + command: + - /bin/sh + - -c + - date; echo Hello from the CronJob + resources: + requests: + cpu: 50m + memory: 32Mi + limits: + cpu: 100m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + restartPolicy: OnFailure diff --git a/test/testdata/workloads/cronjob54195ic.yaml b/test/testdata/workloads/cronjob54195ic.yaml new file mode 100644 index 000000000..6ccd06f61 --- /dev/null +++ b/test/testdata/workloads/cronjob54195ic.yaml @@ -0,0 +1,37 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: cronjob54195ic +spec: + schedule: '* * * * *' + timeZone: Asia/china + jobTemplate: + spec: + template: + spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: hello + image: quay.io/openshifttest/busybox@sha256:c5439d7db88ab5423999530349d327b04279ad3161d7596d2126dfb5b02bfd1f + imagePullPolicy: IfNotPresent + command: + - /bin/sh + - -c + - date; echo Hello from the CronJob + resources: + requests: + cpu: 50m + memory: 32Mi + limits: + cpu: 100m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + restartPolicy: OnFailure diff --git a/test/testdata/workloads/cronjob54195notz.yaml b/test/testdata/workloads/cronjob54195notz.yaml new file mode 100644 index 000000000..f40704441 --- /dev/null +++ b/test/testdata/workloads/cronjob54195notz.yaml @@ -0,0 +1,36 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: cronjob54195notz +spec: + schedule: '* * * * *' + jobTemplate: + spec: + template: + spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: hello + image: quay.io/openshifttest/busybox@sha256:c5439d7db88ab5423999530349d327b04279ad3161d7596d2126dfb5b02bfd1f + imagePullPolicy: IfNotPresent + command: + - /bin/sh + - -c + - date; echo Hello from the CronJob + resources: + requests: + cpu: 50m + memory: 32Mi + limits: + cpu: 100m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + restartPolicy: OnFailure diff --git a/test/testdata/workloads/cronjob56176.yaml b/test/testdata/workloads/cronjob56176.yaml new file mode 100644 index 000000000..4601967cc --- /dev/null +++ b/test/testdata/workloads/cronjob56176.yaml @@ -0,0 +1,40 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: cronjob56176 +spec: + schedule: "*/1 * * * *" + concurrencyPolicy: "Replace" + startingDeadlineSeconds: 200 + suspend: true + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 1 + jobTemplate: + spec: + template: + metadata: + labels: + parent: "cronjobpi" + spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: pi + image: quay.io/openshifttest/mysql@sha256:0c76fd1a2eb31b0a196c7c557e4e56a11a6a8b26d745289e75fc983602035ba5 + command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + restartPolicy: OnFailure diff --git a/test/testdata/workloads/cronjob75375.yaml b/test/testdata/workloads/cronjob75375.yaml new file mode 100644 index 000000000..25a4623c2 --- /dev/null +++ b/test/testdata/workloads/cronjob75375.yaml @@ -0,0 +1,41 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: pi75375 +spec: + schedule: "*/1 * * * *" + timeZone: Etc/UTC + concurrencyPolicy: "Replace" + startingDeadlineSeconds: 200 + suspend: true + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 1 + jobTemplate: + spec: + template: + metadata: + labels: + parent: "cronjobpi" + spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: pi + image: perl + command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + restartPolicy: OnFailure diff --git a/test/testdata/workloads/cronjob_54196.yaml b/test/testdata/workloads/cronjob_54196.yaml new file mode 100644 index 000000000..0cbab3820 --- /dev/null +++ b/test/testdata/workloads/cronjob_54196.yaml @@ -0,0 +1,49 @@ +apiVersion: template.openshift.io/v1 +kind: Template +metadata: + name: cronjob-with-timezone-template +objects: +- kind: CronJob + apiVersion: batch/v1 + metadata: + name: "${CNAME}" + namespace: "${NAMESPACE}" + spec: + schedule: "${SCHEDULE}" + timeZone: "${TIMEZONE}" + jobTemplate: + spec: + template: + spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: hello + image: quay.io/openshifttest/busybox@sha256:c5439d7db88ab5423999530349d327b04279ad3161d7596d2126dfb5b02bfd1f + imagePullPolicy: IfNotPresent + command: + - /bin/sh + - -c + - date; echo Hello from the CronJob + resources: + requests: + cpu: 50m + memory: 32Mi + limits: + cpu: 100m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + restartPolicy: OnFailure + +parameters: +- name: CNAME +- name: NAMESPACE +- name: SCHEDULE +- name: TIMEZONE diff --git a/test/testdata/workloads/daemonset-origin.yaml b/test/testdata/workloads/daemonset-origin.yaml new file mode 100644 index 000000000..7ca8da7d4 --- /dev/null +++ b/test/testdata/workloads/daemonset-origin.yaml @@ -0,0 +1,59 @@ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: hello-openshift + labels: + k8s-app: hello-openshift +spec: + updateStrategy: + rollingUpdate: + maxSurge: 10% + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + name: hello-openshift + template: + metadata: + labels: + name: hello-openshift + spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + tolerations: + - key: dedicated + operator: Exists + containers: + - name: hello-openshift + image: quay.io/openshifttest/hello-openshift@sha256:4200f438cf2e9446f6bcff9d67ceea1f69ed07a2f83363b7fb52529f7ddd8a83 + ports: + - containerPort: 8080 + protocol: TCP + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 100m + memory: 200Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + readinessProbe: + httpGet: + path: / + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 20 + terminationGracePeriodSeconds: 30 diff --git a/test/testdata/workloads/daemonset-update.yaml b/test/testdata/workloads/daemonset-update.yaml new file mode 100644 index 000000000..06122e0eb --- /dev/null +++ b/test/testdata/workloads/daemonset-update.yaml @@ -0,0 +1,59 @@ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: hello-openshift + labels: + k8s-app: hello-openshift +spec: + updateStrategy: + rollingUpdate: + maxSurge: 10% + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + name: hello-openshift + template: + metadata: + labels: + name: hello-openshift + spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + tolerations: + - key: test-nonexist + operator: Exists + containers: + - name: hello-openshift + image: quay.io/openshifttest/hello-openshift@sha256:4200f438cf2e9446f6bcff9d67ceea1f69ed07a2f83363b7fb52529f7ddd8a83 + ports: + - containerPort: 8080 + protocol: TCP + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 100m + memory: 200Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + readinessProbe: + httpGet: + path: / + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 20 + terminationGracePeriodSeconds: 30 diff --git a/test/testdata/workloads/deploy_duplicatepodsrs.yaml b/test/testdata/workloads/deploy_duplicatepodsrs.yaml new file mode 100644 index 000000000..6088a5beb --- /dev/null +++ b/test/testdata/workloads/deploy_duplicatepodsrs.yaml @@ -0,0 +1,52 @@ +apiVersion: template.openshift.io/v1 +kind: Template +metadata: + name: deploy-duplicatepodsrs-template +objects: +- kind: ReplicaSet + apiVersion: apps/v1 + metadata: + name: "${DNAME}" + namespace: "${NAMESPACE}" + spec: + replicas: ${{REPLICASNUM}} + selector: + matchLabels: + tier: "${DNAME}" + matchExpressions: + - {key: tier, operator: In, values: ["${DNAME}"]} + template: + metadata: + labels: + app: "${DNAME}" + tier: "${DNAME}" + spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: "${DNAME}" + image: quay.io/openshifttest/hello-openshift@sha256:4200f438cf2e9446f6bcff9d67ceea1f69ed07a2f83363b7fb52529f7ddd8a83 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 100m + memory: 128Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + env: + - name: GET_HOSTS_FROM + value: dns + ports: + - containerPort: 8080 +parameters: +- name: DNAME +- name: NAMESPACE +- name: REPLICASNUM diff --git a/test/testdata/workloads/deployment-69072.yaml b/test/testdata/workloads/deployment-69072.yaml new file mode 100644 index 000000000..a86015da4 --- /dev/null +++ b/test/testdata/workloads/deployment-69072.yaml @@ -0,0 +1,77 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: infinite-pod-creation-69072 + app.kubernetes.io/component: infinite-pod-creation-69072 + app.kubernetes.io/instance: infinite-pod-creation-69072 + name: infinite-pod-creation-69072 +spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + deployment: infinite-pod-creation-69072 + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + labels: + deployment: infinite-pod-creation-69072 + app: infinite-pod-creation-69072 + spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app: infinite-pod-creation-69072 + containers: + - image: quay.io/openshifttest/hello-openshift@sha256:4200f438cf2e9446f6bcff9d67ceea1f69ed07a2f83363b7fb52529f7ddd8a83 + imagePullPolicy: IfNotPresent + name: infinite-pod-creation + ports: + - containerPort: 8080 + protocol: TCP + - containerPort: 8888 + protocol: TCP + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 100m + memory: 128Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + readinessProbe: + httpGet: + path: / + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 20 + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + terminationGracePeriodSeconds: 30 diff --git a/test/testdata/workloads/deployment-73887.yaml b/test/testdata/workloads/deployment-73887.yaml new file mode 100644 index 000000000..bbf55482f --- /dev/null +++ b/test/testdata/workloads/deployment-73887.yaml @@ -0,0 +1,37 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: replicas-mismatch +spec: + replicas: 5 + selector: + matchLabels: + app: replicas-mismatch + template: + metadata: + labels: + app: replicas-mismatch + spec: + nodeSelector: + non-existent-label/kcm-e2e-73887: "true" + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: busybox + image: quay.io/openshifttest/busybox@sha256:c5439d7db88ab5423999530349d327b04279ad3161d7596d2126dfb5b02bfd1f + command: ["sh", "-c", "sleep 3600"] + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 100m + memory: 128Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL diff --git a/test/testdata/workloads/deployment-with-shutdown-gracefully.yaml b/test/testdata/workloads/deployment-with-shutdown-gracefully.yaml new file mode 100644 index 000000000..7381f902f --- /dev/null +++ b/test/testdata/workloads/deployment-with-shutdown-gracefully.yaml @@ -0,0 +1,75 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + labels: + app: hello-19922 + name: hello-19922 +spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + deployment: hello-19922 + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + labels: + deployment: hello-19922 + spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: hello-19922 + image: quay.io/openshifttest/hello-openshift@sha256:4200f438cf2e9446f6bcff9d67ceea1f69ed07a2f83363b7fb52529f7ddd8a83 + imagePullPolicy: IfNotPresent + lifecycle: + preStop: + exec: + command: + - /bin/sh + - -c + - sleep 150 + ports: + - containerPort: 8080 + protocol: TCP + - containerPort: 8888 + protocol: TCP + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 100m + memory: 128Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + readinessProbe: + httpGet: + path: / + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 20 + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + terminationGracePeriodSeconds: 600 diff --git a/test/testdata/workloads/project-69072.yaml b/test/testdata/workloads/project-69072.yaml new file mode 100644 index 000000000..3878e2a6d --- /dev/null +++ b/test/testdata/workloads/project-69072.yaml @@ -0,0 +1,12 @@ +apiVersion: project.openshift.io/v1 +kind: Project +metadata: + annotations: + openshift.io/node-selector: node-role.kubernetes.io/worker= + labels: + kubernetes.io/metadata.name: infinite-pod-creation + pod-security.kubernetes.io/audit: restricted + pod-security.kubernetes.io/audit-version: v1.24 + pod-security.kubernetes.io/warn: restricted + pod-security.kubernetes.io/warn-version: v1.24 + name: infinite-pod-creation-69072