Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pkg/defaultmonitortests/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/openshift/origin/pkg/monitortests/kubelet/containerfailures"
"github.com/openshift/origin/pkg/monitortests/machines/watchmachines"
"github.com/openshift/origin/pkg/monitortests/monitoring/disruptionmetricsapi"
"github.com/openshift/origin/pkg/monitortests/monitoring/hapolicymanagement"
"github.com/openshift/origin/pkg/monitortests/monitoring/statefulsetsrecreation"
"github.com/openshift/origin/pkg/monitortests/network/disruptioningress"
"github.com/openshift/origin/pkg/monitortests/network/disruptionpodnetwork"
Expand Down Expand Up @@ -182,6 +183,7 @@ func newUniversalMonitorTests(info monitortestframework.MonitorTestInitializatio
monitorTestRegistry.AddMonitorTestOrDie("operator-state-analyzer", "Cluster Version Operator", operatorstateanalyzer.NewAnalyzer())
monitorTestRegistry.AddMonitorTestOrDie("required-scc-annotation-checker", "Cluster Version Operator", requiredsccmonitortests.NewAnalyzer())
monitorTestRegistry.AddMonitorTestOrDie("cluster-version-checker", "Cluster Version Operator", clusterversionchecker.NewClusterVersionChecker())
monitorTestRegistry.AddMonitorTestOrDie("ha-policy-management-checker", "Cluster Version Operator", hapolicymanagement.NewAnalyzer())

monitorTestRegistry.AddMonitorTestOrDie("etcd-log-analyzer", "etcd", etcdloganalyzer.NewEtcdLogAnalyzer())
monitorTestRegistry.AddMonitorTestOrDie("legacy-etcd-invariants", "etcd", legacyetcdmonitortests.NewLegacyTests())
Expand Down
315 changes: 315 additions & 0 deletions pkg/monitortests/monitoring/hapolicymanagement/monitortest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
package hapolicymanagement

import (
"context"
"fmt"
"strings"
"time"

"github.com/openshift/origin/pkg/monitor/monitorapi"
"github.com/openshift/origin/pkg/monitortestframework"
"github.com/openshift/origin/pkg/test/ginkgo/junitapi"
exutil "github.com/openshift/origin/test/extended/util"

corev1 "k8s.io/api/core/v1"
policyv1 "k8s.io/api/policy/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)

type haPolicyManagementChecker struct {
kubeClient kubernetes.Interface
}

func NewAnalyzer() monitortestframework.MonitorTest {
return &haPolicyManagementChecker{}
}

func (w *haPolicyManagementChecker) PrepareCollection(ctx context.Context, adminRESTConfig *rest.Config, recorder monitorapi.RecorderWriter) error {
return nil
}

func (w *haPolicyManagementChecker) StartCollection(ctx context.Context, adminRESTConfig *rest.Config, recorder monitorapi.RecorderWriter) error {
var err error
w.kubeClient, err = kubernetes.NewForConfig(adminRESTConfig)
if err != nil {
return err
}

return nil
}

func checkNonMetricsPorts(container corev1.Container) bool {
for _, port := range container.Ports {
if port.Name != "metrics" {
return true
}
}
return false
}

func checkPodReadinessLivenessProbes(namespace string, initOrSidecar bool, container corev1.Container) error {
if initOrSidecar {
return nil
}

if container.LivenessProbe == nil {
return fmt.Errorf("container %s in ns/%s should have livenessProbe", container.Name, namespace)
}

if !checkNonMetricsPorts(container) {
return nil
}

if container.ReadinessProbe == nil {
return fmt.Errorf("container %s in ns/%s should have readinessProbe", container.Name, namespace)
}

return nil
}
Comment on lines +53 to +71

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

initOrSidecar boolean is passed inverted at every call site — probe checks never run on real containers.

checkPodReadinessLivenessProbes and checkPodStartupProbes both skip validation (return nil) when initOrSidecar is true. But every caller passes true for the main Containers loop and false for the InitContainers loop (Deployments: lines 180/183 vs 188/191; StatefulSets: 211/214 vs 219/222; DaemonSets: 242/245 vs 250/253; Pods: 267/270 vs 275/278). This means the checks are skipped for regular application containers — the actual target of this policy — and instead incorrectly enforced on init containers, which typically don't need liveness/readiness probes. The monitor as written will never catch a missing probe on a real workload container.

🐛 Proposed fix (apply to all 8 call sites)
-			for _, container := range dep.Spec.Template.Spec.Containers {
-				if err := checkPodReadinessLivenessProbes(ns.Name, true, container); err != nil {
+			for _, container := range dep.Spec.Template.Spec.Containers {
+				if err := checkPodReadinessLivenessProbes(ns.Name, false, container); err != nil {
 					failures = append(failures, err.Error())
 				}
-				if err := checkPodStartupProbes(ns.Name, true, container); err != nil {
+				if err := checkPodStartupProbes(ns.Name, false, container); err != nil {
 					failures = append(failures, err.Error())
 				}
 			}
 			for _, container := range dep.Spec.Template.Spec.InitContainers {
-				if err := checkPodReadinessLivenessProbes(ns.Name, false, container); err != nil {
+				if err := checkPodReadinessLivenessProbes(ns.Name, true, container); err != nil {
 					failures = append(failures, err.Error())
 				}
-				if err := checkPodStartupProbes(ns.Name, false, container); err != nil {
+				if err := checkPodStartupProbes(ns.Name, true, container); err != nil {
 					failures = append(failures, err.Error())
 				}
 			}

Apply the same swap to the StatefulSets, DaemonSets, and Pods loops.

Also applies to: 73-83, 178-282

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/monitortests/monitoring/hapolicymanagement/monitortest.go` around lines
53 - 71, Correct the boolean argument at all call sites of
checkPodReadinessLivenessProbes and checkPodStartupProbes: pass false for
regular Containers and true for InitContainers so probe validation runs only on
workload containers. Apply this consistently in the Deployment, StatefulSet,
DaemonSet, and Pod loops, preserving the existing skip behavior for init or
sidecar containers.


func checkPodStartupProbes(namespace string, initOrSidecar bool, container corev1.Container) error {
if initOrSidecar {
return nil
}

if container.StartupProbe == nil && checkNonMetricsPorts(container) {
return fmt.Errorf("container %s in ns/%s should have startupProbe", container.Name, namespace)
}

return nil
}

func checkPodDisruptionBudget(namespace string, name string, podTemplate corev1.PodTemplateSpec, selector *metav1.LabelSelector, pdbList *policyv1.PodDisruptionBudgetList) error {
depSelector, err := metav1.LabelSelectorAsSelector(selector)
if err != nil {
return fmt.Errorf("invalid label selector: %w", err)
}

podLabels := labels.Set(podTemplate.Labels)

var matchedPDB *policyv1.PodDisruptionBudget
for _, pdb := range pdbList.Items {
if pdb.Spec.Selector == nil {
continue
}

pdbSelector, err := metav1.LabelSelectorAsSelector(pdb.Spec.Selector)
if err != nil {
continue
}

// Perform bidirectional selector matching: verify that both the PDB selector
// and the Deployment selector successfully target the same underlying Pod labels.
if pdbSelector.Matches(podLabels) && depSelector.Matches(podLabels) {
matchedPDB = &pdb
break
}
}

if matchedPDB == nil {
return fmt.Errorf("workload %s in ns/%s should have PodDisruptionBudget", name, namespace)
}

return nil
}

func checkPodAntiAffinity(namespace string, name string, podTemplate corev1.PodTemplateSpec) error {
podSpec := podTemplate.Spec

if podSpec.Affinity == nil || podSpec.Affinity.PodAntiAffinity == nil {
return nil
}

antiAffinity := podSpec.Affinity.PodAntiAffinity
hasRequired := len(antiAffinity.RequiredDuringSchedulingIgnoredDuringExecution) > 0
hasPreferred := len(antiAffinity.PreferredDuringSchedulingIgnoredDuringExecution) > 0

if !hasRequired && !hasPreferred {
return fmt.Errorf("workload %s in ns/%s should have podAntiAffinity", name, namespace)
}

return nil
}
Comment on lines +119 to +135

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

checkPodAntiAffinity logic is inverted — workloads without any anti-affinity pass silently.

If podSpec.Affinity or PodAntiAffinity is nil (the common case for a workload that never configured anti-affinity), the function returns nil (no violation). Only when the struct exists but both RequiredDuringSchedulingIgnoredDuringExecution and PreferredDuringSchedulingIgnoredDuringExecution are empty does it report an error — an unusual, rare configuration. As written, this check will almost never fire for the actual non-compliant case it's meant to detect.

🐛 Proposed fix
 func checkPodAntiAffinity(namespace string, name string, podTemplate corev1.PodTemplateSpec) error {
 	podSpec := podTemplate.Spec
 
-	if podSpec.Affinity == nil || podSpec.Affinity.PodAntiAffinity == nil {
-		return nil
-	}
+	if podSpec.Affinity == nil || podSpec.Affinity.PodAntiAffinity == nil {
+		return fmt.Errorf("workload %s in ns/%s should have podAntiAffinity", name, namespace)
+	}
 
 	antiAffinity := podSpec.Affinity.PodAntiAffinity
 	hasRequired := len(antiAffinity.RequiredDuringSchedulingIgnoredDuringExecution) > 0
 	hasPreferred := len(antiAffinity.PreferredDuringSchedulingIgnoredDuringExecution) > 0
 
 	if !hasRequired && !hasPreferred {
 		return fmt.Errorf("workload %s in ns/%s should have podAntiAffinity", name, namespace)
 	}
 
 	return nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func checkPodAntiAffinity(namespace string, name string, podTemplate corev1.PodTemplateSpec) error {
podSpec := podTemplate.Spec
if podSpec.Affinity == nil || podSpec.Affinity.PodAntiAffinity == nil {
return nil
}
antiAffinity := podSpec.Affinity.PodAntiAffinity
hasRequired := len(antiAffinity.RequiredDuringSchedulingIgnoredDuringExecution) > 0
hasPreferred := len(antiAffinity.PreferredDuringSchedulingIgnoredDuringExecution) > 0
if !hasRequired && !hasPreferred {
return fmt.Errorf("workload %s in ns/%s should have podAntiAffinity", name, namespace)
}
return nil
}
func checkPodAntiAffinity(namespace string, name string, podTemplate corev1.PodTemplateSpec) error {
podSpec := podTemplate.Spec
if podSpec.Affinity == nil || podSpec.Affinity.PodAntiAffinity == nil {
return fmt.Errorf("workload %s in ns/%s should have podAntiAffinity", name, namespace)
}
antiAffinity := podSpec.Affinity.PodAntiAffinity
hasRequired := len(antiAffinity.RequiredDuringSchedulingIgnoredDuringExecution) > 0
hasPreferred := len(antiAffinity.PreferredDuringSchedulingIgnoredDuringExecution) > 0
if !hasRequired && !hasPreferred {
return fmt.Errorf("workload %s in ns/%s should have podAntiAffinity", name, namespace)
}
return nil
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/monitortests/monitoring/hapolicymanagement/monitortest.go` around lines
119 - 135, Update checkPodAntiAffinity so a nil Affinity or PodAntiAffinity is
reported as a missing anti-affinity violation instead of returning nil. Preserve
the existing error message and continue rejecting configurations where both
required and preferred anti-affinity lists are empty.


func hasControllerOwner(pod *corev1.Pod) bool {
if len(pod.OwnerReferences) == 0 {
return false
}

for _, ref := range pod.OwnerReferences {
if ref.Controller != nil && *ref.Controller {
return true
}
}

return false
}

func (w *haPolicyManagementChecker) CollectData(ctx context.Context, storageDir string, beginning, end time.Time) (monitorapi.Intervals, []*junitapi.JUnitTestCase, error) {
if w.kubeClient == nil {
return nil, nil, nil
}

namespaces, err := w.kubeClient.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
if err != nil {
return nil, nil, err
}

junits := []*junitapi.JUnitTestCase{}
for _, ns := range namespaces.Items {
// skip managed service namespaces
if exutil.ManagedServiceNamespaces.Has(ns.Name) {
continue
}

// Filter OpenShift namespaces
isOpenShiftNamespace := ns.Name == "openshift" || strings.HasPrefix(ns.Name, "openshift-")
if !(strings.HasPrefix(ns.Name, "kube-") || ns.Name == "default" || isOpenShiftNamespace) {
continue
}

var failures []string

pdbList, _ := w.kubeClient.PolicyV1().PodDisruptionBudgets(ns.Name).List(ctx, metav1.ListOptions{})
deployments, _ := w.kubeClient.AppsV1().Deployments(ns.Name).List(ctx, metav1.ListOptions{})
for _, dep := range deployments.Items {
for _, container := range dep.Spec.Template.Spec.Containers {
if err := checkPodReadinessLivenessProbes(ns.Name, true, container); err != nil {
failures = append(failures, err.Error())
}
if err := checkPodStartupProbes(ns.Name, true, container); err != nil {
failures = append(failures, err.Error())
}
}
for _, container := range dep.Spec.Template.Spec.InitContainers {
if err := checkPodReadinessLivenessProbes(ns.Name, false, container); err != nil {
failures = append(failures, err.Error())
}
if err := checkPodStartupProbes(ns.Name, false, container); err != nil {
failures = append(failures, err.Error())
}
}

if dep.Spec.Replicas != nil && *dep.Spec.Replicas <= 1 {
continue
}

if err := checkPodDisruptionBudget(ns.Name, dep.Name, dep.Spec.Template, dep.Spec.Selector, pdbList); err != nil {
failures = append(failures, err.Error())
}
if err := checkPodAntiAffinity(ns.Name, dep.Name, dep.Spec.Template); err != nil {
failures = append(failures, err.Error())
}
}

statefulsets, _ := w.kubeClient.AppsV1().StatefulSets(ns.Name).List(ctx, metav1.ListOptions{})
for _, sts := range statefulsets.Items {
for _, container := range sts.Spec.Template.Spec.Containers {
if err := checkPodReadinessLivenessProbes(ns.Name, true, container); err != nil {
failures = append(failures, err.Error())
}
if err := checkPodStartupProbes(ns.Name, true, container); err != nil {
failures = append(failures, err.Error())
}
}
for _, container := range sts.Spec.Template.Spec.InitContainers {
if err := checkPodReadinessLivenessProbes(ns.Name, false, container); err != nil {
failures = append(failures, err.Error())
}
if err := checkPodStartupProbes(ns.Name, false, container); err != nil {
failures = append(failures, err.Error())
}
}

if sts.Spec.Replicas != nil && *sts.Spec.Replicas <= 1 {
continue
}

if err := checkPodDisruptionBudget(ns.Name, sts.Name, sts.Spec.Template, sts.Spec.Selector, pdbList); err != nil {
failures = append(failures, err.Error())
}
if err := checkPodAntiAffinity(ns.Name, sts.Name, sts.Spec.Template); err != nil {
failures = append(failures, err.Error())
}
}

daemonsets, _ := w.kubeClient.AppsV1().DaemonSets(ns.Name).List(ctx, metav1.ListOptions{})
for _, dms := range daemonsets.Items {
for _, container := range dms.Spec.Template.Spec.Containers {
if err := checkPodReadinessLivenessProbes(ns.Name, true, container); err != nil {
failures = append(failures, err.Error())
}
if err := checkPodStartupProbes(ns.Name, true, container); err != nil {
failures = append(failures, err.Error())
}
}
for _, container := range dms.Spec.Template.Spec.InitContainers {
if err := checkPodReadinessLivenessProbes(ns.Name, false, container); err != nil {
failures = append(failures, err.Error())
}
if err := checkPodStartupProbes(ns.Name, false, container); err != nil {
failures = append(failures, err.Error())
}
}
}

pods, _ := w.kubeClient.CoreV1().Pods(ns.Name).List(ctx, metav1.ListOptions{})
Comment on lines +176 to +259

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Kubernetes API list errors are silently discarded.

PodDisruptionBudgets/Deployments/StatefulSets/DaemonSets/Pods .List(...) calls all discard the error via _ (lines 176, 177, 208, 239, 259). A transient API failure or permission issue will be treated as "no resources found" rather than surfacing a failure, producing a false "compliant" JUnit result for that namespace instead of flagging the collection error.

As per path instructions for **/*.go, "Never ignore error returns."

🛡️ Example fix pattern (repeat for each List call)
-		pdbList, _ := w.kubeClient.PolicyV1().PodDisruptionBudgets(ns.Name).List(ctx, metav1.ListOptions{})
-		deployments, _ := w.kubeClient.AppsV1().Deployments(ns.Name).List(ctx, metav1.ListOptions{})
+		pdbList, err := w.kubeClient.PolicyV1().PodDisruptionBudgets(ns.Name).List(ctx, metav1.ListOptions{})
+		if err != nil {
+			failures = append(failures, fmt.Sprintf("failed to list PodDisruptionBudgets in ns/%s: %v", ns.Name, err))
+		}
+		deployments, err := w.kubeClient.AppsV1().Deployments(ns.Name).List(ctx, metav1.ListOptions{})
+		if err != nil {
+			failures = append(failures, fmt.Sprintf("failed to list Deployments in ns/%s: %v", ns.Name, err))
+		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/monitortests/monitoring/hapolicymanagement/monitortest.go` around lines
176 - 259, Handle and report errors from every Kubernetes List call in the
namespace monitoring flow, including PodDisruptionBudgets, Deployments,
StatefulSets, DaemonSets, and Pods. Replace each discarded error with explicit
handling that appends the collection failure to failures and prevents the
corresponding resource checks from treating an unavailable collection as empty;
preserve normal iteration when listing succeeds.

Source: Path instructions

for _, pod := range pods.Items {
// Filter bare pods
if hasControllerOwner(&pod) {
continue
}

for _, container := range pod.Spec.Containers {
if err := checkPodReadinessLivenessProbes(ns.Name, true, container); err != nil {
failures = append(failures, err.Error())
}
if err := checkPodStartupProbes(ns.Name, true, container); err != nil {
failures = append(failures, err.Error())
}
}
for _, container := range pod.Spec.InitContainers {
if err := checkPodReadinessLivenessProbes(ns.Name, false, container); err != nil {
failures = append(failures, err.Error())
}
if err := checkPodStartupProbes(ns.Name, false, container); err != nil {
failures = append(failures, err.Error())
}
}
}

testName := fmt.Sprintf("[Monitor:ha-compliance][sig-arch] workload in ns/%s should comply with HA policy", ns.Name)

if len(failures) > 0 {
junits = append(junits, &junitapi.JUnitTestCase{
Name: testName,
SystemOut: strings.Join(failures, "\n"),
})
} else {
junits = append(junits, &junitapi.JUnitTestCase{
Name: testName,
})
}
}

return nil, junits, nil
}

func (w *haPolicyManagementChecker) ConstructComputedIntervals(ctx context.Context, startingIntervals monitorapi.Intervals, recordedResources monitorapi.ResourcesMap, beginning, end time.Time) (monitorapi.Intervals, error) {
return nil, nil
}

func (w *haPolicyManagementChecker) EvaluateTestsFromConstructedIntervals(ctx context.Context, finalIntervals monitorapi.Intervals) ([]*junitapi.JUnitTestCase, error) {
return nil, nil
}

func (w *haPolicyManagementChecker) WriteContentToStorage(ctx context.Context, storageDir, timeSuffix string, finalIntervals monitorapi.Intervals, finalResourceState monitorapi.ResourcesMap) error {
return nil
}

func (w *haPolicyManagementChecker) Cleanup(ctx context.Context) error {
return nil
}