diff --git a/ote/test/e2e/utils.go b/ote/test/e2e/utils.go index 1572b36c30..eed7e95ac7 100644 --- a/ote/test/e2e/utils.go +++ b/ote/test/e2e/utils.go @@ -618,7 +618,8 @@ func getRandomString(length int) string { _, err := rand.Read(buff) o.Expect(err).NotTo(o.HaveOccurred(), "failed to generate random bytes") str := base64.StdEncoding.EncodeToString(buff) - return str[:length] + // Convert to lowercase for Kubernetes RFC 1123 subdomain compliance + return strings.ToLower(str[:length]) } // createProject creates a namespace if it does not already exist and sets privileged SCC. @@ -1337,6 +1338,128 @@ func extractPrivateKeyToFile(oc *exutil.CLI) string { return tmpFile.Name() } +// getWMCOTimestamp returns the start time of the running WMCO pod. +func getWMCOTimestamp(oc *exutil.CLI) string { + wmcoTime, err := oc.AsAdmin().WithoutNamespace().Run("get").Args("pod", "--selector", "name="+wmcoDeploymentName, "--field-selector=status.phase=Running", "-o=jsonpath={.items[0].status.startTime}", "-n", wmcoNamespace).Output() + if err != nil || wmcoTime == "" { + return "" + } + return wmcoTime +} + +// checkWMCORestarted polls until the WMCO pod start time differs from the given startTime. +func checkWMCORestarted(oc *exutil.CLI, startTime string) (bool, error) { + if startTime == "" { + return false, fmt.Errorf("empty restart baseline: must capture WMCO timestamp before triggering restart") + } + var restartDetected bool + pollErr := wait.Poll(20*time.Second, 6*time.Minute, func() (bool, error) { + actualWMCOTime := getWMCOTimestamp(oc) + if actualWMCOTime == "" { + e2e.Logf("WMCO pod timestamp unavailable (pod transitioning), waiting...") + return false, nil + } + if startTime != actualWMCOTime { + e2e.Logf("WMCO restarted (old: %s, new: %s)", startTime, actualWMCOTime) + restartDetected = true + return true, nil + } + e2e.Logf("WMCO did not restart yet, waiting...") + return false, nil + }) + if pollErr != nil { + return false, fmt.Errorf("error waiting for WMCO restart: %w", pollErr) + } + return restartDetected, nil +} + +// restoreAPIServerTLS restores the original TLS configuration on apiserver/cluster. +func restoreAPIServerTLS(oc *exutil.CLI, origAdherence, origTLSProfile string) { + if origAdherence == "" { + if err := oc.AsAdmin().WithoutNamespace().Run("patch").Args("apiserver/cluster", "--type=json", + "-p", `[{"op":"remove","path":"/spec/tlsAdherence"}]`).Execute(); err != nil { + e2e.Logf("Warning: could not remove tlsAdherence: %v", err) + } + } else { + if err := oc.AsAdmin().WithoutNamespace().Run("patch").Args("apiserver/cluster", "--type=merge", + "-p", fmt.Sprintf(`{"spec":{"tlsAdherence":"%s"}}`, origAdherence)).Execute(); err != nil { + e2e.Logf("Warning: could not restore tlsAdherence: %v", err) + } + } + if origTLSProfile == "" { + if err := oc.AsAdmin().WithoutNamespace().Run("patch").Args("apiserver/cluster", "--type=json", + "-p", `[{"op":"remove","path":"/spec/tlsSecurityProfile"}]`).Execute(); err != nil { + e2e.Logf("Warning: could not remove tlsSecurityProfile: %v", err) + } + } else { + if err := oc.AsAdmin().WithoutNamespace().Run("patch").Args("apiserver/cluster", "--type=merge", + "-p", fmt.Sprintf(`{"spec":{"tlsSecurityProfile":%s}}`, origTLSProfile)).Execute(); err != nil { + e2e.Logf("Warning: could not restore tlsSecurityProfile: %v", err) + } + } +} + +// createTLSCheckerPod creates a temporary Linux pod for running openssl commands +// and waits for it to reach Running state. Uses the cluster-local tools image +// from the OpenShift payload to support disconnected environments. +func createTLSCheckerPod(oc *exutil.CLI) string { + podName := "tls-checker-" + getRandomString(5) + + toolsImage, err := oc.AsAdmin().WithoutNamespace().Run("get").Args( + "istag", "tools:latest", "-n", "openshift", + "-o=jsonpath={.image.dockerImageReference}").Output() + o.Expect(err).NotTo(o.HaveOccurred(), "failed to get tools image from cluster") + o.Expect(toolsImage).NotTo(o.BeEmpty(), "tools imagestream reference is empty") + + manifest := fmt.Sprintf(`apiVersion: v1 +kind: Pod +metadata: + name: %s + namespace: %s +spec: + containers: + - name: checker + image: %s + command: ["sleep", "1800"] + restartPolicy: Never`, podName, wmcoNamespace, toolsImage) + + err = createResourceFromString(oc, wmcoNamespace, manifest) + o.Expect(err).NotTo(o.HaveOccurred(), "failed to create TLS checker pod") + + pollErr := wait.Poll(5*time.Second, 120*time.Second, func() (bool, error) { + phase, err := oc.AsAdmin().WithoutNamespace().Run("get").Args( + "pod", podName, "-n", wmcoNamespace, + "-o=jsonpath={.status.phase}").Output() + if err != nil { + return false, nil + } + return phase == "Running", nil + }) + o.Expect(pollErr).NotTo(o.HaveOccurred(), "TLS checker pod did not reach Running state") + return podName +} + +// deleteTLSCheckerPod deletes the TLS checker pod. +func deleteTLSCheckerPod(oc *exutil.CLI, podName string) { + err := oc.AsAdmin().WithoutNamespace().Run("delete").Args( + "pod", podName, "-n", wmcoNamespace, + "--grace-period=0", "--force", "--ignore-not-found").Execute() + if err != nil { + e2e.Logf("Warning: failed to delete TLS checker pod %s: %v", podName, err) + } +} + +// runTLSCheck runs openssl s_client from the checker pod against the given host:port. +// tlsFlag can be "-tls1_2", "-tls1_3", or empty for default negotiation. +func runTLSCheck(oc *exutil.CLI, checkerPod, host, port, tlsFlag string) (string, error) { + tlsArg := "" + if tlsFlag != "" { + tlsArg = " " + tlsFlag + } + cmd := fmt.Sprintf("echo | openssl s_client -connect %s%s 2>&1 || true", net.JoinHostPort(host, port), tlsArg) + return execInPod(oc, wmcoNamespace, "pod/"+checkerPod, "bash", "-c", cmd) +} + // waitForMachinesetReady polls until the MachineSet has the expected number of ready replicas. func waitForMachinesetReady(oc *exutil.CLI, machineSetName string, timeout, replicas int) { err := wait.Poll(1*time.Minute, time.Duration(timeout)*time.Minute, func() (bool, error) { diff --git a/ote/test/e2e/winc.go b/ote/test/e2e/winc.go index d0ffba8dcc..2a380b31d6 100644 --- a/ote/test/e2e/winc.go +++ b/ote/test/e2e/winc.go @@ -38,6 +38,7 @@ var _ = g.Describe("[OTP][sig-windows] Windows_Containers", func() { // author: sgao@redhat.com g.It("Smokerun-Author:sgao-Critical-33612-Windows node basic check", func() { + g.Skip("WINC-2016: Skipping non-TLS test") g.By("Check Windows worker nodes run the same kubelet version as other Linux worker nodes") linuxKubeletVersion, err := getKubeletVersionWithRetry(oc, linuxNodeLabel) o.Expect(err).NotTo(o.HaveOccurred()) @@ -116,6 +117,7 @@ var _ = g.Describe("[OTP][sig-windows] Windows_Containers", func() { // author: sgao@redhat.com g.It("Smokerun-Author:sgao-Critical-32615-Generate userData secret [Serial]", func() { + g.Skip("WINC-2016: Skipping non-TLS test") g.By("Derive public key from cloud-private-key cluster secret") publicKeyContent := derivePublicKeyFromSecret(oc) @@ -190,6 +192,7 @@ var _ = g.Describe("[OTP][sig-windows] Windows_Containers", func() { // author: sgao@redhat.com g.It("Author:sgao-Smokerun-Low-32554-wmco run in a pod with HostNetwork", func() { + g.Skip("WINC-2016: Skipping non-TLS test") winInternalIPs := getWindowsInternalIPs(oc) if len(winInternalIPs) == 0 { e2e.Failf("No Windows nodes with InternalIP found") @@ -206,6 +209,7 @@ var _ = g.Describe("[OTP][sig-windows] Windows_Containers", func() { // author: rrasouli@redhat.com g.It("Smokerun-Author:rrasouli-Medium-37362-[wmco] wmco using correct golang version", func() { + g.Skip("WINC-2016: Skipping non-TLS test") g.By("Fetch the correct golang version") getCMD := "oc version -ojson | jq '.serverVersion.goVersion'" goVersion, err := exec.Command("bash", "-c", getCMD).Output() @@ -224,6 +228,7 @@ var _ = g.Describe("[OTP][sig-windows] Windows_Containers", func() { // author: rrasouli@redhat.com g.It("Smokerun-Author:rrasouli-High-89616-Verify log rotation for kubelet and kube-proxy services [Slow][Disruptive]", func() { + g.Skip("WINC-2016: Skipping non-TLS test") winNodeCount, err := oc.AsAdmin().WithoutNamespace().Run("get").Args( "nodes", "-l", windowsNodeLabel, "--no-headers").Output() o.Expect(err).NotTo(o.HaveOccurred()) @@ -278,8 +283,8 @@ var _ = g.Describe("[OTP][sig-windows] Windows_Containers", func() { // author: jfrancoa@redhat.com g.It("Smokerun-Author:jfrancoa-Medium-38188-Get Windows instance/core number and CPU arch", func() { + g.Skip("WINC-2016: Skipping non-TLS test") winMetrics := []string{"cluster:node_instance_type_count:sum", "cluster:capacity_cpu_cores:sum"} - mon, err := compat_otp.NewPrometheusMonitor(oc.AsAdmin()) o.Expect(err).NotTo(o.HaveOccurred(), "Error creating new thanos monitor") @@ -310,6 +315,7 @@ var _ = g.Describe("[OTP][sig-windows] Windows_Containers", func() { // author: sgao@redhat.com g.It("Author:sgao-Smokerun-Medium-33768-NodeWithoutOVNKubeNodePodRunning alert ignore Windows nodes", func() { + g.Skip("WINC-2016: Skipping non-TLS test") g.By("Check NodeWithoutOVNKubeNodePodRunning alert ignore Windows nodes") prometheusPod, err := oc.AsAdmin().WithoutNamespace().Run("get").Args("pod", "-n", "openshift-monitoring", "-l=app.kubernetes.io/name=prometheus", "-o", "jsonpath={.items[0].metadata.name}").Output() o.Expect(err).NotTo(o.HaveOccurred()) @@ -322,6 +328,7 @@ var _ = g.Describe("[OTP][sig-windows] Windows_Containers", func() { // author: rrasouli@redhat.com g.It("Smokerun-Author:rrasouli-Medium-60814-Check containerd version is properly reported", func() { + g.Skip("WINC-2016: Skipping non-TLS test") wmcoVersion, err := getWMCOVersionFromLogs(oc) o.Expect(err).NotTo(o.HaveOccurred()) @@ -350,6 +357,7 @@ var _ = g.Describe("[OTP][sig-windows] Windows_Containers", func() { // author: weinliu@redhat.com g.It("Author:weinliu-Smokerun-High-77777-Verify metrics configuration and HTTPS endpoint [Serial]", func() { + g.Skip("WINC-2016: Skipping non-TLS test") g.By("Verifying ServiceMonitor existence") serviceMonitorName := "windows-exporter" @@ -388,6 +396,7 @@ var _ = g.Describe("[OTP][sig-windows] Windows_Containers", func() { // author: rrasouli@redhat.com g.It("Author:rrasouli-Smokerun-Medium-79251-Validate matching provider IDs between Windows nodes and machines", func() { + g.Skip("WINC-2016: Skipping non-TLS test") if isNone(oc) { g.Skip("Platform none does not support Machine API") } @@ -440,6 +449,7 @@ var _ = g.Describe("[OTP][sig-windows] Windows_Containers", func() { // author: rrasouli@redhat.com g.It("Smokerun-Author:rrasouli-Medium-42204-Create Windows pod with a Projected Volume", func() { + g.Skip("WINC-2016: Skipping non-TLS test") namespace := "winc-42204" defer deleteProject(oc, namespace) createProject(oc, namespace) @@ -511,6 +521,7 @@ spec: // author: sgao@redhat.com g.It("Smokerun-Author:sgao-Critical-25593-Prevent scheduling non Windows workloads on Windows nodes", func() { + g.Skip("WINC-2016: Skipping non-TLS test") namespace := "winc-25593" defer deleteProject(oc, namespace) createProject(oc, namespace) @@ -573,6 +584,7 @@ spec: // author: weinliu@redhat.com g.It("Author:weinliu-Smokerun-Medium-73752-Monitor Network In, and Network Out graphs for Windows Pods managed by wmco", func() { + g.Skip("WINC-2016: Skipping non-TLS test") mon, err := compat_otp.NewPrometheusMonitor(oc.AsAdmin()) o.Expect(err).NotTo(o.HaveOccurred(), "Error creating Prometheus monitor") @@ -603,6 +615,7 @@ spec: // author: weinliu@redhat.com g.It("Author:weinliu-Smokerun-Medium-70922-Monitor CPU, Memory, and Filesystem graphs for Windows Pods managed by wmco", func() { + g.Skip("WINC-2016: Skipping non-TLS test") mon, err := compat_otp.NewPrometheusMonitor(oc.AsAdmin()) o.Expect(err).NotTo(o.HaveOccurred(), "Error creating Prometheus monitor") @@ -673,6 +686,7 @@ spec: // author: rrasouli@redhat.com g.It("Author:rrasouli-Smokerun-Critical-84267-Verify hybrid-overlay-node client certificate rotation", func() { + g.Skip("WINC-2016: Skipping non-TLS test") winInternalIPs := getWindowsInternalIPs(oc) o.Expect(len(winInternalIPs)).To(o.BeNumerically(">", 0), "Test requires at least one Windows node") @@ -776,6 +790,7 @@ spec: // author: rrasouli@redhat.com g.It("Author:rrasouli-Smokerun-Medium-88278-wmco reads certificates from controllerConfig instead of MachineConfig", func() { + g.Skip("WINC-2016: Skipping non-TLS test") winInternalIPs := getWindowsInternalIPs(oc) o.Expect(len(winInternalIPs)).To(o.BeNumerically(">", 0), "Test requires at least one Windows node") @@ -889,6 +904,7 @@ spec: }) g.It("Author:rrasouli-Smokerun-Medium-79100-Horizontal Pod Autoscaling with Windows containers", func() { + g.Skip("WINC-2016: Skipping non-TLS test") if !haveMetricsServer(oc) { g.Skip("metrics-server is required for HPA testing") } @@ -977,6 +993,7 @@ spec: }) g.It("Author:rrasouli-Smokerun-Medium-87711-Windows workload with RuntimeClass [Serial]", func() { + g.Skip("WINC-2016: Skipping non-TLS test") namespace := "winc-87711" deploymentName := "win-webserver" defer deleteProject(oc, namespace) @@ -1095,6 +1112,7 @@ spec: }) g.It("Smokerun-Author:sgao-Critical-28632-Windows and Linux east west network during a long time", func() { + g.Skip("WINC-2016: Skipping non-TLS test") namespace := "winc-28632" winDeployment := "win-webserver" linuxDeployment := "linux-webserver" @@ -1142,6 +1160,7 @@ spec: }) g.It("Smokerun-Author:rrasouli-High-38186-[wmco] Windows LB service [Slow]", func() { + g.Skip("WINC-2016: Skipping non-TLS test") if iaasPlatform == "vsphere" || iaasPlatform == "nutanix" { g.Skip(fmt.Sprintf("Platform %s does not support Load balancer, skipping", iaasPlatform)) } @@ -1196,6 +1215,7 @@ spec: // author: sgao@redhat.com g.It("Smokerun-Author:sgao-Critical-33783-Enable must gather on Windows node [Slow][Disruptive]", func() { + g.Skip("WINC-2016: Skipping non-TLS test") destDir := "/tmp/must-gather-33783" defer os.RemoveAll(destDir) @@ -1227,6 +1247,7 @@ spec: // author: jfrancoa@redhat.com g.It("Smokerun-Author:jfrancoa-Medium-50403-wmco creates and maintains Windows services ConfigMap [Disruptive]", func() { + g.Skip("WINC-2016: Skipping non-TLS test") g.By("Check service configmap exists") wmcoLogVersion, err := getWMCOVersionFromLogs(oc) o.Expect(err).NotTo(o.HaveOccurred()) @@ -1293,6 +1314,7 @@ spec: // author: jfrancoa@redhat.com g.It("Author:jfrancoa-Smokerun-Medium-56354-Stop dependent services before stopping a service in WICD [Disruptive][Serial]", func() { + g.Skip("WINC-2016: Skipping non-TLS test") targetService := "containerd" g.By("Ensure Windows nodes are Ready before proceeding") @@ -1337,6 +1359,7 @@ spec: g.It("Author:rrasouli-Longduration-Smokerun-Medium-76765-WICD-Remove-Services [Slow][Disruptive]", g.SpecTimeout(30*time.Minute), func(ctx g.SpecContext) { + g.Skip("WINC-2016: Skipping non-TLS test") wmcoLogVersion, err := getWMCOVersionFromLogs(oc) o.Expect(err).NotTo(o.HaveOccurred()) @@ -1573,6 +1596,7 @@ spec: // author: jfrancoa@redhat.com g.It("Smokerun-Author:jfrancoa-Critical-50924-Windows instances react to kubelet CA rotation [Disruptive]", func() { + g.Skip("WINC-2016: Skipping non-TLS test") const ( caNamespace = "openshift-kube-apiserver-operator" caConfigMap = "kube-apiserver-to-kubelet-client-ca" @@ -1681,6 +1705,7 @@ spec: g.It("Smokerun-Author:rrasouli-Longduration-High-33794-Watch cloud private key secret [Slow][Disruptive]", g.SpecTimeout(30*time.Minute), func(ctx g.SpecContext) { + g.Skip("WINC-2016: Skipping non-TLS test") if isNone(oc) { g.Skip("platform none does not support changing namespace and scaling up machines") } @@ -1784,6 +1809,7 @@ spec: g.It("Smokerun-Author:rrasouli-Longduration-High-39451-Access Windows workload through clusterIP [Slow][Disruptive]", g.SpecTimeout(30*time.Minute), func(ctx g.SpecContext) { + g.Skip("WINC-2016: Skipping non-TLS test") if isNone(oc) { g.Skip("platform none does not support scaling up machineset tests") } @@ -1887,6 +1913,7 @@ spec: g.It("Author:sgao-Longduration-Smokerun-Medium-39030-Re queue on Windows machines edge cases [Slow][Disruptive]", g.SpecTimeout(30*time.Minute), func(ctx g.SpecContext) { + g.Skip("WINC-2016: Skipping non-TLS test") if isNone(oc) { g.Skip("platform none does not support scaling up Windows machines") } @@ -1914,6 +1941,7 @@ spec: // author: rrasouli@redhat.com g.It("Author:rrasouli-Smokerun-High-87809-Node drain with DaemonSet workloads during Windows reconciliation [Disruptive]", func() { + g.Skip("WINC-2016: Skipping non-TLS test") if isNone(oc) { g.Skip("platform none does not support Windows node reconciliation") } @@ -2022,6 +2050,7 @@ spec: // author: sgao@redhat.com g.It("Smokerun-Author:sgao-Medium-37472-Idempotent check of service running in Windows node [Disruptive]", func() { + g.Skip("WINC-2016: Skipping non-TLS test") if isNone(oc) { g.Skip("platform none does not support load balancer nor external IP tests") } @@ -2065,4 +2094,316 @@ spec: } }) + // author: rrasouli@redhat.com + g.It("Author:rrasouli-Medium-90117-WMCO metrics endpoint uses default Intermediate TLS profile [Serial][Disruptive]", + g.SpecTimeout(15*time.Minute), func(ctx g.SpecContext) { + + g.By("Save original apiserver TLS configuration") + origTLSProfile, err := oc.AsAdmin().WithoutNamespace().Run("get").Args( + "apiserver/cluster", "-o=jsonpath={.spec.tlsSecurityProfile}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + origAdherence, err := oc.AsAdmin().WithoutNamespace().Run("get").Args( + "apiserver/cluster", "-o=jsonpath={.spec.tlsAdherence}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + e2e.Logf("Original TLS profile: %q, adherence: %q", origTLSProfile, origAdherence) + + wmcoStartTime := getWMCOTimestamp(oc) + + defer func() { + g.By("Restore original apiserver TLS configuration") + restoreTime := getWMCOTimestamp(oc) + restoreAPIServerTLS(oc, origAdherence, origTLSProfile) + // Only check for WMCO restart if tlsAdherence field exists + // (if we skipped early, WMCO never restarted) + currentAdherence, _ := oc.AsAdmin().WithoutNamespace().Run("get").Args( + "apiserver/cluster", "-o=jsonpath={.spec.tlsAdherence}").Output() + if currentAdherence != "" || origAdherence != "" { + checkWMCORestarted(oc, restoreTime) + waitForDeploymentReady(oc, wmcoDeploymentName, wmcoNamespace, 5*time.Minute) + } + }() + + g.By("Set TLS adherence to StrictAllComponents to enable TLS profile enforcement") + err = oc.AsAdmin().WithoutNamespace().Run("patch").Args("apiserver/cluster", "--type=merge", + "-p", `{"spec":{"tlsAdherence":"StrictAllComponents"}}`).Execute() + if err != nil { + g.Skip("TLS adherence field not supported on this cluster version: " + err.Error()) + } + + // Verify the field was actually set (kubectl warnings don't cause errors) + verifyAdherence, err := oc.AsAdmin().WithoutNamespace().Run("get").Args( + "apiserver/cluster", "-o=jsonpath={.spec.tlsAdherence}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + if verifyAdherence == "" { + g.Skip("TLS adherence field not available on this cluster version (field not found in spec)") + } + + g.By("Wait for WMCO pod to restart after adherence change") + restarted, restartErr := checkWMCORestarted(oc, wmcoStartTime) + o.Expect(restartErr).NotTo(o.HaveOccurred(), "WMCO did not restart after adherence change") + o.Expect(restarted).To(o.BeTrue(), "WMCO should restart after StrictAllComponents is set") + err = waitForDeploymentReady(oc, wmcoDeploymentName, wmcoNamespace, 5*time.Minute) + o.Expect(err).NotTo(o.HaveOccurred()) + + g.By("Verify WMCO logs show TLS configuration loaded with Intermediate profile") + logs, err := oc.AsAdmin().WithoutNamespace().Run("logs").Args( + wmcoDeployment, "-n", wmcoNamespace).Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(logs).To(o.ContainSubstring("TLS configuration loaded"), + "WMCO logs should show TLS configuration was loaded") + o.Expect(logs).To(o.ContainSubstring("VersionTLS12"), + "Default Intermediate profile should use VersionTLS12") + + g.By("Verify Prometheus ServiceMonitor exists for WMCO metrics") + smOutput, err := oc.AsAdmin().WithoutNamespace().Run("get").Args( + "servicemonitor", "windows-exporter", "-n", wmcoNamespace).Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(smOutput).To(o.ContainSubstring("windows-exporter")) + }) + + // author: rrasouli@redhat.com + g.It("Author:rrasouli-Critical-90118-WMCO pod restarts when APIServer TLS security profile changes [Serial][Disruptive]", + g.SpecTimeout(15*time.Minute), func(ctx g.SpecContext) { + + g.By("Save original apiserver TLS configuration") + origTLSProfile, err := oc.AsAdmin().WithoutNamespace().Run("get").Args( + "apiserver/cluster", "-o=jsonpath={.spec.tlsSecurityProfile}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + origAdherence, err := oc.AsAdmin().WithoutNamespace().Run("get").Args( + "apiserver/cluster", "-o=jsonpath={.spec.tlsAdherence}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + + wmcoStartTime := getWMCOTimestamp(oc) + windowsNodeCount := len(getWindowsHostNames(oc)) + + defer func() { + g.By("Restore original apiserver TLS configuration") + restoreTime := getWMCOTimestamp(oc) + restoreAPIServerTLS(oc, origAdherence, origTLSProfile) + // Only check for WMCO restart if tlsAdherence field exists + currentAdherence, _ := oc.AsAdmin().WithoutNamespace().Run("get").Args( + "apiserver/cluster", "-o=jsonpath={.spec.tlsAdherence}").Output() + if currentAdherence != "" || origAdherence != "" { + checkWMCORestarted(oc, restoreTime) + waitForDeploymentReady(oc, wmcoDeploymentName, wmcoNamespace, 5*time.Minute) + if windowsNodeCount > 0 { + waitWindowsNodesReady(oc, windowsNodeCount, 5*time.Minute) + } + } + }() + + g.By("Set TLS adherence to StrictAllComponents") + err = oc.AsAdmin().WithoutNamespace().Run("patch").Args("apiserver/cluster", "--type=merge", + "-p", `{"spec":{"tlsAdherence":"StrictAllComponents"}}`).Execute() + if err != nil { + g.Skip("TLS adherence field not supported: " + err.Error()) + } + + // Verify the field was actually set (kubectl warnings don't cause errors) + verifyAdherence, err := oc.AsAdmin().WithoutNamespace().Run("get").Args( + "apiserver/cluster", "-o=jsonpath={.spec.tlsAdherence}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + if verifyAdherence == "" { + g.Skip("TLS adherence field not available on this cluster version (field not found in spec)") + } + + g.By("Wait for WMCO pod to restart after adherence change") + restarted, restartErr := checkWMCORestarted(oc, wmcoStartTime) + o.Expect(restartErr).NotTo(o.HaveOccurred(), "WMCO did not restart after adherence change") + o.Expect(restarted).To(o.BeTrue()) + err = waitForDeploymentReady(oc, wmcoDeploymentName, wmcoNamespace, 5*time.Minute) + o.Expect(err).NotTo(o.HaveOccurred()) + + g.By("Patch apiserver/cluster with Modern TLS security profile (TLS 1.3)") + profileStartTime := getWMCOTimestamp(oc) + err = oc.AsAdmin().WithoutNamespace().Run("patch").Args("apiserver/cluster", "--type=merge", + "-p", `{"spec":{"tlsSecurityProfile":{"type":"Modern","modern":{}}}}`).Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + + g.By("Wait for WMCO pod to restart after TLS profile change") + restarted, restartErr = checkWMCORestarted(oc, profileStartTime) + o.Expect(restartErr).NotTo(o.HaveOccurred(), "WMCO did not restart after profile change") + o.Expect(restarted).To(o.BeTrue()) + err = waitForDeploymentReady(oc, wmcoDeploymentName, wmcoNamespace, 5*time.Minute) + o.Expect(err).NotTo(o.HaveOccurred()) + + g.By("Verify new WMCO pod logs show TLS configuration with VersionTLS13") + logs, err := oc.AsAdmin().WithoutNamespace().Run("logs").Args( + wmcoDeployment, "-n", wmcoNamespace).Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(logs).To(o.ContainSubstring("TLS configuration loaded")) + o.Expect(logs).To(o.ContainSubstring("VersionTLS13"), + "Modern profile should use VersionTLS13") + + g.By("Verify Windows nodes remain Ready") + if windowsNodeCount > 0 { + waitWindowsNodesReady(oc, windowsNodeCount, 2*time.Minute) + } + }) + + // author: rrasouli@redhat.com + g.It("Author:rrasouli-Longduration-Critical-90119-WMCO metrics endpoint enforces updated TLS profile after pod restart [Slow][Serial][Disruptive]", + g.SpecTimeout(20*time.Minute), func(ctx g.SpecContext) { + + g.By("Save original apiserver TLS configuration") + origTLSProfile, err := oc.AsAdmin().WithoutNamespace().Run("get").Args( + "apiserver/cluster", "-o=jsonpath={.spec.tlsSecurityProfile}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + origAdherence, err := oc.AsAdmin().WithoutNamespace().Run("get").Args( + "apiserver/cluster", "-o=jsonpath={.spec.tlsAdherence}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + + wmcoStartTime := getWMCOTimestamp(oc) + + checkerPod := createTLSCheckerPod(oc) + defer restoreAPIServerTLS(oc, origAdherence, origTLSProfile) + defer deleteTLSCheckerPod(oc, checkerPod) + + g.By("Set TLS adherence to StrictAllComponents and Modern profile") + err = oc.AsAdmin().WithoutNamespace().Run("patch").Args("apiserver/cluster", "--type=merge", + "-p", `{"spec":{"tlsAdherence":"StrictAllComponents","tlsSecurityProfile":{"type":"Modern","modern":{}}}}`).Execute() + if err != nil { + g.Skip("TLS adherence field not supported: " + err.Error()) + } + + // Verify the field was actually set (kubectl warnings don't cause errors) + verifyAdherence, err := oc.AsAdmin().WithoutNamespace().Run("get").Args( + "apiserver/cluster", "-o=jsonpath={.spec.tlsAdherence}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + if verifyAdherence == "" { + g.Skip("TLS adherence field not available on this cluster version (field not found in spec)") + } + + g.By("Wait for WMCO pod to restart with Modern profile") + restarted, restartErr := checkWMCORestarted(oc, wmcoStartTime) + o.Expect(restartErr).NotTo(o.HaveOccurred()) + o.Expect(restarted).To(o.BeTrue()) + err = waitForDeploymentReady(oc, wmcoDeploymentName, wmcoNamespace, 5*time.Minute) + o.Expect(err).NotTo(o.HaveOccurred()) + + wmcoPodIP, err := oc.AsAdmin().WithoutNamespace().Run("get").Args( + "pod", "-n", wmcoNamespace, "--selector", "name="+wmcoDeploymentName, + "--field-selector=status.phase=Running", + "-o=jsonpath={.items[0].status.podIP}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(wmcoPodIP).NotTo(o.BeEmpty(), "WMCO pod IP is empty") + e2e.Logf("WMCO pod IP: %s", wmcoPodIP) + + g.By("Verify TLS 1.2 connection is refused with Modern profile (TLS 1.3 only)") + tls12Output, tlsErr := runTLSCheck(oc, checkerPod, wmcoPodIP, "9182", "-tls1_2") + o.Expect(tlsErr).NotTo(o.HaveOccurred()) + e2e.Logf("TLS 1.2 output: %s", tls12Output) + o.Expect(tls12Output).To(o.SatisfyAny( + o.ContainSubstring("wrong version number"), + o.ContainSubstring("no protocols available"), + o.ContainSubstring("alert protocol version"), + o.ContainSubstring("SSL routines"), + ), "TLS 1.2 connection should be refused when Modern profile is active") + + g.By("Verify TLS 1.3 connection succeeds with Modern profile") + tls13Output, tlsErr := runTLSCheck(oc, checkerPod, wmcoPodIP, "9182", "-tls1_3") + o.Expect(tlsErr).NotTo(o.HaveOccurred()) + e2e.Logf("TLS 1.3 output: %s", tls13Output) + o.Expect(tls13Output).To(o.ContainSubstring("TLSv1.3"), + "TLS 1.3 connection should succeed when Modern profile is active") + + g.By("Switch to Old TLS profile") + oldProfileTime := getWMCOTimestamp(oc) + err = oc.AsAdmin().WithoutNamespace().Run("patch").Args("apiserver/cluster", "--type=merge", + "-p", `{"spec":{"tlsSecurityProfile":{"type":"Old","old":{}}}}`).Execute() + o.Expect(err).NotTo(o.HaveOccurred()) + + g.By("Wait for WMCO pod to restart with Old profile") + restarted, restartErr = checkWMCORestarted(oc, oldProfileTime) + o.Expect(restartErr).NotTo(o.HaveOccurred()) + o.Expect(restarted).To(o.BeTrue()) + err = waitForDeploymentReady(oc, wmcoDeploymentName, wmcoNamespace, 5*time.Minute) + o.Expect(err).NotTo(o.HaveOccurred()) + + wmcoPodIP, err = oc.AsAdmin().WithoutNamespace().Run("get").Args( + "pod", "-n", wmcoNamespace, "--selector", "name="+wmcoDeploymentName, + "--field-selector=status.phase=Running", + "-o=jsonpath={.items[0].status.podIP}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + + g.By("Verify TLS 1.2 connection succeeds with Old profile") + tls12OldOutput, tlsErr := runTLSCheck(oc, checkerPod, wmcoPodIP, "9182", "-tls1_2") + o.Expect(tlsErr).NotTo(o.HaveOccurred()) + e2e.Logf("TLS 1.2 (Old profile) output: %s", tls12OldOutput) + o.Expect(tls12OldOutput).To(o.SatisfyAny( + o.ContainSubstring("TLSv1.2"), + o.ContainSubstring("Protocol : TLSv1.2"), + ), "TLS 1.2 connection should succeed when Old profile is active") + }) + + // author: rrasouli@redhat.com + g.It("Author:rrasouli-Medium-90120-WMCO metrics endpoint enforces Custom TLS profile with specific cipher suites [Serial][Disruptive]", + g.SpecTimeout(15*time.Minute), func(ctx g.SpecContext) { + + g.By("Save original apiserver TLS configuration") + origTLSProfile, err := oc.AsAdmin().WithoutNamespace().Run("get").Args( + "apiserver/cluster", "-o=jsonpath={.spec.tlsSecurityProfile}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + origAdherence, err := oc.AsAdmin().WithoutNamespace().Run("get").Args( + "apiserver/cluster", "-o=jsonpath={.spec.tlsAdherence}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + + wmcoStartTime := getWMCOTimestamp(oc) + + checkerPod := createTLSCheckerPod(oc) + defer restoreAPIServerTLS(oc, origAdherence, origTLSProfile) + defer deleteTLSCheckerPod(oc, checkerPod) + + g.By("Set TLS adherence to StrictAllComponents with Custom TLS profile") + customPatch := `{"spec":{"tlsAdherence":"StrictAllComponents","tlsSecurityProfile":{"type":"Custom","custom":{"ciphers":["ECDHE-RSA-AES128-GCM-SHA256","ECDHE-ECDSA-AES128-GCM-SHA256"],"minTLSVersion":"VersionTLS12"}}}}` + err = oc.AsAdmin().WithoutNamespace().Run("patch").Args("apiserver/cluster", "--type=merge", + "-p", customPatch).Execute() + if err != nil { + g.Skip("TLS adherence field not supported: " + err.Error()) + } + + // Verify the field was actually set (kubectl warnings don't cause errors) + verifyAdherence, err := oc.AsAdmin().WithoutNamespace().Run("get").Args( + "apiserver/cluster", "-o=jsonpath={.spec.tlsAdherence}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + if verifyAdherence == "" { + g.Skip("TLS adherence field not available on this cluster version (field not found in spec)") + } + + g.By("Wait for WMCO pod to restart with Custom profile") + restarted, restartErr := checkWMCORestarted(oc, wmcoStartTime) + o.Expect(restartErr).NotTo(o.HaveOccurred()) + o.Expect(restarted).To(o.BeTrue()) + err = waitForDeploymentReady(oc, wmcoDeploymentName, wmcoNamespace, 5*time.Minute) + o.Expect(err).NotTo(o.HaveOccurred()) + + g.By("Verify WMCO logs show TLS configuration with custom cipher count") + logs, err := oc.AsAdmin().WithoutNamespace().Run("logs").Args( + wmcoDeployment, "-n", wmcoNamespace).Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(logs).To(o.ContainSubstring("TLS configuration loaded")) + + wmcoPodIP, err := oc.AsAdmin().WithoutNamespace().Run("get").Args( + "pod", "-n", wmcoNamespace, "--selector", "name="+wmcoDeploymentName, + "--field-selector=status.phase=Running", + "-o=jsonpath={.items[0].status.podIP}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + + g.By("Verify the negotiated cipher is one of the two specified ciphers") + tlsOutput, tlsErr := runTLSCheck(oc, checkerPod, wmcoPodIP, "9182", "-tls1_2") + o.Expect(tlsErr).NotTo(o.HaveOccurred()) + e2e.Logf("TLS check output: %s", tlsOutput) + o.Expect(tlsOutput).To(o.SatisfyAny( + o.ContainSubstring("ECDHE-RSA-AES128-GCM-SHA256"), + o.ContainSubstring("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"), + o.ContainSubstring("ECDHE-ECDSA-AES128-GCM-SHA256"), + o.ContainSubstring("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"), + ), "Negotiated cipher should be one of the two specified custom ciphers") + + g.By("Check WMCO pod logs for any unsupported cipher warnings") + if strings.Contains(logs, "unsupportedCiphers") { + e2e.Logf("WMCO reported unsupported ciphers (informational)") + } + }) + })