diff --git a/test/extended/node/README.md b/test/extended/node/README.md index cd970bb3c49a..5b97d3c08806 100644 --- a/test/extended/node/README.md +++ b/test/extended/node/README.md @@ -8,9 +8,10 @@ This directory contains OpenShift end-to-end tests for node-related features. - **kubeletconfig_features.go** - Tests applying KubeletConfig to custom machine config pools, requires node reboots - **kubelet_secret_pulled_images.go** - Tests kubelet credential verification for image pulls (`KubeletEnsureSecretPulledImages` feature gate). Covers multi-tenancy isolation, credential rotation, ImagePullPolicy behavior, credential verification policy (NeverVerify/AlwaysVerify), and registry availability scenarios. Requires `TechPreviewNoUpgrade` or `CustomNoUpgrade` FeatureSet. -- **node_e2e/image_registry_config.go** - Container registry config change (OCP-44820) - Verifies search registry update triggers MCO rollout and lands on nodes [Disruptive] -- **node_e2e/pdb_drain.go** - PodDisruptionBudget drain blocking (OCP-67564) - Tests that node drain is blocked when PDB has minAvailable=100% with empty selector [Disruptive] [Lifecycle:informing] -- **node_e2e/container_runtime_config.go** - ContainerRuntimeConfig pidsLimit (OCP-45351) and overlaySize (OCP-46313) - Verifies CTRCFG settings are applied via MCO rollout and reflected on nodes [Disruptive] +- **node_e2e/container_runtime_config.go** - ContainerRuntimeConfig pidsLimit (OCP-45351) and overlaySize (OCP-46313) - Verifies CTRCFG settings are applied via MCO rollout and reflected on nodes \[Disruptive\] +- **node_e2e/image_registry_config.go** - Container registry config change (OCP-44820) - Verifies search registry update triggers MCO rollout and lands on nodes \[Disruptive\] +- **node_e2e/image_signature.go** - Image signature verification - Verifies image signature verification for Red Hat Container Registries \[Disruptive\]\[Serial\]\[OTP\] +- **node_e2e/pdb_drain.go** - PodDisruptionBudget drain blocking (OCP-67564) - Tests that node drain is blocked when PDB has minAvailable=100% with empty selector \[Disruptive\] \[Lifecycle:informing\] ### Suite: openshift/usernamespace diff --git a/test/extended/node/node_e2e/image_signature.go b/test/extended/node/node_e2e/image_signature.go new file mode 100644 index 000000000000..b3ee0ab59050 --- /dev/null +++ b/test/extended/node/node_e2e/image_signature.go @@ -0,0 +1,143 @@ +package node + +import ( + "context" + "path/filepath" + "strings" + "time" + + g "github.com/onsi/ginkgo/v2" + o "github.com/onsi/gomega" + ote "github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo" + + "k8s.io/apimachinery/pkg/util/wait" + e2e "k8s.io/kubernetes/test/e2e/framework" + + nodeutils "github.com/openshift/origin/test/extended/node" + exutil "github.com/openshift/origin/test/extended/util" +) + +var _ = g.Describe("[Suite:openshift/disruptive-longrunning][sig-node][Disruptive][Serial] Image signature verification", func() { + var ( + oc = exutil.NewCLIWithoutNamespace("image-sig") + nodeE2EBaseDir = exutil.FixturePath("testdata", "node", "node_e2e") + imgSignatureYAML = filepath.Join(nodeE2EBaseDir, "machineconfig-image-signature.yaml") + ) + + g.BeforeEach(func() { + isMicroShift, err := exutil.IsMicroShiftCluster(oc.AdminKubeClient()) + o.Expect(err).NotTo(o.HaveOccurred()) + if isMicroShift { + g.Skip("Skipping test on MicroShift cluster") + } + }) + + //author: bgudi@redhat.com + g.It("[OTP] Enable image signature verification for Red Hat Container Registries [OCP-59552]", ote.Informing(), func() { + ctx := context.Background() + + g.By("Check if mcp worker exists in current cluster") + machineCount, err := oc.AsAdmin().WithoutNamespace().Run("get").Args("mcp", "worker", "-o=jsonpath={.status.machineCount}").Output() + if err != nil || machineCount == "0" { + g.Skip("Skipping test: mcp worker does not exist in this cluster") + } + e2e.Logf("Worker MCP machine count: %s", machineCount) + + g.By("Apply a machine config to set image signature policy for worker nodes") + err = oc.AsAdmin().WithoutNamespace().Run("create").Args("-f", imgSignatureYAML).Execute() + o.Expect(err).NotTo(o.HaveOccurred(), "failed to create MachineConfig") + + g.DeferCleanup(func(ctx context.Context) { + g.By("Delete the MachineConfig") + oc.AsAdmin().WithoutNamespace().Run("delete").Args("-f", imgSignatureYAML, "--ignore-not-found").Execute() + + g.By("Wait for MCP to finish rolling back") + err := waitForMCPUpdate(ctx, oc, "worker", 30*time.Minute) + if err != nil { + e2e.Logf("Warning: MCP did not finish rolling back: %v", err) + } + }, ctx) + + g.By("Wait for MCP to finish updating") + err = waitForMCPUpdate(ctx, oc, "worker", 30*time.Minute) + o.Expect(err).NotTo(o.HaveOccurred(), "MCP worker did not finish updating") + + g.By("Verify the signature configuration in /etc/containers/policy.json") + err = checkImageSignature(oc) + o.Expect(err).NotTo(o.HaveOccurred(), "image signature configuration verification failed") + }) +}) + +// waitForMCPUpdate waits for the MachineConfigPool to finish updating. +// It checks the Updated condition to become True (which means update is complete). +// Returns nil when the MCP is updated, or an error if it times out. +// This is a helper function and does not contain assertions. +func waitForMCPUpdate(ctx context.Context, oc *exutil.CLI, mcpName string, timeout time.Duration) error { + g.GinkgoHelper() + return wait.PollUntilContextTimeout(ctx, 30*time.Second, timeout, false, func(ctx context.Context) (bool, error) { + // Check the Updated condition instead of Updating + // Updated=True means the MCP has finished updating + updatedStatus, err := oc.AsAdmin().WithoutNamespace().Run("get").Args("mcp", mcpName, "-o=jsonpath={.status.conditions[?(@.type=='Updated')].status}").Output() + if err != nil { + e2e.Logf("Error getting MCP Updated status: %v", err) + return false, nil + } + + // Check that machine counts match (all machines have the desired config) + machineCount, err := oc.AsAdmin().WithoutNamespace().Run("get").Args("mcp", mcpName, "-o=jsonpath={.status.machineCount}").Output() + if err != nil { + e2e.Logf("Error getting machine count: %v", err) + return false, nil + } + updatedMachineCount, err := oc.AsAdmin().WithoutNamespace().Run("get").Args("mcp", mcpName, "-o=jsonpath={.status.updatedMachineCount}").Output() + if err != nil { + e2e.Logf("Error getting updated machine count: %v", err) + return false, nil + } + + e2e.Logf("MCP %s: Updated=%s, machines=%s, updatedMachines=%s", mcpName, updatedStatus, machineCount, updatedMachineCount) + + if strings.Contains(updatedStatus, "True") && machineCount == updatedMachineCount { + e2e.Logf("MCP %s updated successfully", mcpName) + return true, nil + } + e2e.Logf("MCP %s is still updating", mcpName) + return false, nil + }) +} + +// checkImageSignature verifies that the image signature policy is correctly configured on worker nodes. +// It checks for required entries in /etc/containers/policy.json for Red Hat registries. +// This is a helper function and does not contain assertions. +func checkImageSignature(oc *exutil.CLI) error { + g.GinkgoHelper() + return wait.PollUntilContextTimeout(context.Background(), 10*time.Second, 30*time.Second, true, func(ctx context.Context) (bool, error) { + workerNode := nodeutils.GetFirstReadyWorkerNode(oc) + policyJSON, err := nodeutils.ExecOnNodeWithChroot(oc, workerNode, "cat", "/etc/containers/policy.json") + if err != nil { + e2e.Logf("Error reading policy.json: %v", err) + return false, nil + } + + e2e.Logf("Checking policy.json content from node %s", workerNode) + + // Check for required entries in the policy.json + requiredEntries := []string{ + "registry.access.redhat.com", + "signedBy", + "GPGKeys", + "/etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release", + "registry.redhat.io", + } + + for _, entry := range requiredEntries { + if !strings.Contains(policyJSON, entry) { + e2e.Logf("Missing required entry in policy.json: %s", entry) + return false, nil + } + } + + e2e.Logf("Image signature policy verified successfully") + return true, nil + }) +} diff --git a/test/extended/testdata/bindata.go b/test/extended/testdata/bindata.go index 2fe62f7cd555..00d5a737a6f2 100644 --- a/test/extended/testdata/bindata.go +++ b/test/extended/testdata/bindata.go @@ -459,6 +459,7 @@ // test/extended/testdata/node/nested_container/containers.conf // test/extended/testdata/node/nested_container/run_tests.sh // test/extended/testdata/node/nested_container/skip_tests.sh +// test/extended/testdata/node/node_e2e/machineconfig-image-signature.yaml // test/extended/testdata/node/node_e2e/pod-dev-fuse.yaml // test/extended/testdata/node_tuning/nto-stalld.yaml // test/extended/testdata/oauthserver/cabundle-cm.yaml @@ -50688,6 +50689,41 @@ func testExtendedTestdataNodeNested_containerSkip_testsSh() (*asset, error) { return a, nil } +var _testExtendedTestdataNodeNode_e2eMachineconfigImageSignatureYaml = []byte(`# Generated by Butane; do not edit +apiVersion: machineconfiguration.openshift.io/v1 +kind: MachineConfig +metadata: + labels: + machineconfiguration.openshift.io/role: worker + name: 51-worker-rh-registry-trust +spec: + config: + ignition: + version: 3.2.0 + storage: + files: + - contents: + source: data:;base64,ewogICJkZWZhdWx0IjogWwogICAgewogICAgICAidHlwZSI6ICJpbnNlY3VyZUFjY2VwdEFueXRoaW5nIgogICAgfQogIF0sCiAgInRyYW5zcG9ydHMiOiB7CiAgICAiZG9ja2VyIjogewogICAgICAicmVnaXN0cnkuYWNjZXNzLnJlZGhhdC5jb20iOiBbCiAgICAgICAgewogICAgICAgICAgInR5cGUiOiAic2lnbmVkQnkiLAogICAgICAgICAgImtleVR5cGUiOiAiR1BHS2V5cyIsCiAgICAgICAgICAia2V5UGF0aCI6ICIvZXRjL3BraS9ycG0tZ3BnL1JQTS1HUEctS0VZLXJlZGhhdC1yZWxlYXNlIgogICAgICAgIH0KICAgICAgXSwKICAgICAgInJlZ2lzdHJ5LnJlZGhhdC5pbyI6IFsKICAgICAgICB7CiAgICAgICAgICAidHlwZSI6ICJzaWduZWRCeSIsCiAgICAgICAgICAia2V5VHlwZSI6ICJHUEdLZXlzIiwKICAgICAgICAgICJrZXlQYXRoIjogIi9ldGMvcGtpL3JwbS1ncGcvUlBNLUdQRy1LRVktcmVkaGF0LXJlbGVhc2UiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImRvY2tlci1kYWVtb24iOiB7CiAgICAgICIiOiBbCiAgICAgICAgewogICAgICAgICAgInR5cGUiOiAiaW5zZWN1cmVBY2NlcHRBbnl0aGluZyIKICAgICAgICB9CiAgICAgIF0KICAgIH0KICB9Cn0K + mode: 420 + overwrite: true + path: /etc/containers/policy.json +`) + +func testExtendedTestdataNodeNode_e2eMachineconfigImageSignatureYamlBytes() ([]byte, error) { + return _testExtendedTestdataNodeNode_e2eMachineconfigImageSignatureYaml, nil +} + +func testExtendedTestdataNodeNode_e2eMachineconfigImageSignatureYaml() (*asset, error) { + bytes, err := testExtendedTestdataNodeNode_e2eMachineconfigImageSignatureYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "test/extended/testdata/node/node_e2e/machineconfig-image-signature.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + var _testExtendedTestdataNodeNode_e2ePodDevFuseYaml = []byte(`apiVersion: v1 kind: Pod metadata: @@ -56772,6 +56808,7 @@ var _bindata = map[string]func() (*asset, error){ "test/extended/testdata/node/nested_container/containers.conf": testExtendedTestdataNodeNested_containerContainersConf, "test/extended/testdata/node/nested_container/run_tests.sh": testExtendedTestdataNodeNested_containerRun_testsSh, "test/extended/testdata/node/nested_container/skip_tests.sh": testExtendedTestdataNodeNested_containerSkip_testsSh, + "test/extended/testdata/node/node_e2e/machineconfig-image-signature.yaml": testExtendedTestdataNodeNode_e2eMachineconfigImageSignatureYaml, "test/extended/testdata/node/node_e2e/pod-dev-fuse.yaml": testExtendedTestdataNodeNode_e2ePodDevFuseYaml, "test/extended/testdata/node_tuning/nto-stalld.yaml": testExtendedTestdataNode_tuningNtoStalldYaml, "test/extended/testdata/oauthserver/cabundle-cm.yaml": testExtendedTestdataOauthserverCabundleCmYaml, @@ -57576,7 +57613,8 @@ var _bintree = &bintree{nil, map[string]*bintree{ "skip_tests.sh": {testExtendedTestdataNodeNested_containerSkip_testsSh, map[string]*bintree{}}, }}, "node_e2e": {nil, map[string]*bintree{ - "pod-dev-fuse.yaml": {testExtendedTestdataNodeNode_e2ePodDevFuseYaml, map[string]*bintree{}}, + "machineconfig-image-signature.yaml": {testExtendedTestdataNodeNode_e2eMachineconfigImageSignatureYaml, map[string]*bintree{}}, + "pod-dev-fuse.yaml": {testExtendedTestdataNodeNode_e2ePodDevFuseYaml, map[string]*bintree{}}, }}, }}, "node_tuning": {nil, map[string]*bintree{ diff --git a/test/extended/testdata/node/node_e2e/machineconfig-image-signature.yaml b/test/extended/testdata/node/node_e2e/machineconfig-image-signature.yaml new file mode 100644 index 000000000000..66bf37132185 --- /dev/null +++ b/test/extended/testdata/node/node_e2e/machineconfig-image-signature.yaml @@ -0,0 +1,18 @@ +# Generated by Butane; do not edit +apiVersion: machineconfiguration.openshift.io/v1 +kind: MachineConfig +metadata: + labels: + machineconfiguration.openshift.io/role: worker + name: 51-worker-rh-registry-trust +spec: + config: + ignition: + version: 3.2.0 + storage: + files: + - contents: + source: data:;base64,ewogICJkZWZhdWx0IjogWwogICAgewogICAgICAidHlwZSI6ICJpbnNlY3VyZUFjY2VwdEFueXRoaW5nIgogICAgfQogIF0sCiAgInRyYW5zcG9ydHMiOiB7CiAgICAiZG9ja2VyIjogewogICAgICAicmVnaXN0cnkuYWNjZXNzLnJlZGhhdC5jb20iOiBbCiAgICAgICAgewogICAgICAgICAgInR5cGUiOiAic2lnbmVkQnkiLAogICAgICAgICAgImtleVR5cGUiOiAiR1BHS2V5cyIsCiAgICAgICAgICAia2V5UGF0aCI6ICIvZXRjL3BraS9ycG0tZ3BnL1JQTS1HUEctS0VZLXJlZGhhdC1yZWxlYXNlIgogICAgICAgIH0KICAgICAgXSwKICAgICAgInJlZ2lzdHJ5LnJlZGhhdC5pbyI6IFsKICAgICAgICB7CiAgICAgICAgICAidHlwZSI6ICJzaWduZWRCeSIsCiAgICAgICAgICAia2V5VHlwZSI6ICJHUEdLZXlzIiwKICAgICAgICAgICJrZXlQYXRoIjogIi9ldGMvcGtpL3JwbS1ncGcvUlBNLUdQRy1LRVktcmVkaGF0LXJlbGVhc2UiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImRvY2tlci1kYWVtb24iOiB7CiAgICAgICIiOiBbCiAgICAgICAgewogICAgICAgICAgInR5cGUiOiAiaW5zZWN1cmVBY2NlcHRBbnl0aGluZyIKICAgICAgICB9CiAgICAgIF0KICAgIH0KICB9Cn0K + mode: 420 + overwrite: true + path: /etc/containers/policy.json