Skip to content
Open
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
8 changes: 8 additions & 0 deletions cmd/cluster-config-operator-tests-ext/dependencymagnet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package main

// This file imports test packages to ensure they are registered with the OTE framework.
// The blank import causes the test's init() functions to run, which registers Ginkgo specs.

import (
_ "github.com/openshift/cluster-config-operator/test/e2e"
)
49 changes: 38 additions & 11 deletions cmd/cluster-config-operator-tests-ext/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,34 @@ https://github.com/openshift-eng/openshift-tests-extension/blob/main/cmd/example
package main

import (
"context"
"fmt"
"os"

"github.com/spf13/cobra"
"k8s.io/component-base/cli"
"k8s.io/klog/v2"

otecmd "github.com/openshift-eng/openshift-tests-extension/pkg/cmd"
oteextension "github.com/openshift-eng/openshift-tests-extension/pkg/extension"
oteginkgo "github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo"
"github.com/openshift/cluster-config-operator/pkg/version"

"k8s.io/klog/v2"
)

func main() {
command := newOperatorTestCommand(context.Background())
code := cli.Run(command)
cmd, err := newOperatorTestCommand()
if err != nil {
klog.Fatal(err)
}

code := cli.Run(cmd)
os.Exit(code)
}

func newOperatorTestCommand(ctx context.Context) *cobra.Command {
registry := prepareOperatorTestsRegistry()
func newOperatorTestCommand() (*cobra.Command, error) {
registry, err := prepareOperatorTestsRegistry()
if err != nil {
return nil, fmt.Errorf("failed to prepare test registry: %w", err)
}

cmd := &cobra.Command{
Use: "cluster-config-operator-tests-ext",
Expand All @@ -49,18 +56,38 @@ func newOperatorTestCommand(ctx context.Context) *cobra.Command {

cmd.AddCommand(otecmd.DefaultExtensionCommands(registry)...)

return cmd
return cmd, nil
}

// prepareOperatorTestsRegistry creates the OTE registry for this operator.
//
// Note:
//
// This method must be called before adding the registry to the OTE framework.
func prepareOperatorTestsRegistry() *oteextension.Registry {
registry := oteextension.NewRegistry()
func prepareOperatorTestsRegistry() (*oteextension.Registry, error) {
// Build test specs from Ginkgo
specs, err := oteginkgo.BuildExtensionTestSpecsFromOpenShiftGinkgoSuite()
if err != nil {
return nil, fmt.Errorf("failed to build test specs from ginkgo: %w", err)
}

// Create extension
extension := oteextension.NewExtension("openshift", "payload", "cluster-config-operator")

// Add test suites
extension.AddSuite(oteextension.Suite{
Name: "openshift/cluster-config-operator/Operator/Serial",
Qualifiers: []string{
`name.contains("[Operator]") && name.contains("[Serial]")`,
},
})

// Add specs to extension
extension.AddSpecs(specs)

// Register extension with registry
registry := oteextension.NewRegistry()
registry.Register(extension)
return registry

return registry, nil
}
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ toolchain go1.24.5
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/go-bindata/go-bindata v3.1.2+incompatible
github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.38.0
github.com/openshift-eng/openshift-tests-extension v0.0.0-20250804142706-7b3ab438a292
github.com/openshift/api v0.0.0-20250806102053-6a7223edb2fc
github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee
Expand Down Expand Up @@ -45,6 +47,7 @@ require (
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.3 // indirect
Expand All @@ -64,8 +67,6 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
github.com/onsi/gomega v1.38.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/profile v1.7.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
Expand Down Expand Up @@ -102,6 +103,7 @@ require (
golang.org/x/term v0.33.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/grpc v1.68.1 // indirect
Expand Down
1 change: 0 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
Expand Down
45 changes: 45 additions & 0 deletions test/e2e/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package e2e

import (
"context"
"time"

"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)

const (
operatorNamespace = "openshift-config-operator"
operatorName = "openshift-config-operator"
clusterOperatorName = "config-operator"
pollTimeout = 2 * time.Minute
pollInterval = 5 * time.Second
)

// getKubernetesClient returns a Kubernetes client for interacting with the cluster.
func getKubernetesClient() (kubernetes.Interface, error) {
config, err := getRestConfig()
if err != nil {
return nil, err
}
return kubernetes.NewForConfig(config)
}

// getRestConfig returns a REST config for the cluster.
func getRestConfig() (*rest.Config, error) {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
configOverrides := &clientcmd.ConfigOverrides{}
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
return kubeConfig.ClientConfig()
}

// testContext returns a context for test operations.
func testContext() context.Context {
return context.Background()
}

// int64Ptr returns a pointer to an int64 value.
func int64Ptr(i int64) *int64 {
return &i
}
88 changes: 88 additions & 0 deletions test/e2e/operator_health.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package e2e

import (
"bufio"
"fmt"
"strings"

g "github.com/onsi/ginkgo/v2"
o "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var _ = g.Describe("[Operator][Serial] Operator Health", func() {
var (
ctx = testContext()
)

g.Context("Deployment Verification", func() {
g.It("should have a running deployment with ready replicas", func() {
k8sClient, err := getKubernetesClient()
o.Expect(err).NotTo(o.HaveOccurred(), "failed to create kubernetes client")

o.Eventually(func() error {
deployment, err := k8sClient.AppsV1().Deployments(operatorNamespace).Get(ctx, operatorName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get deployment: %w", err)
}

if deployment.Status.ReadyReplicas < 1 {
return fmt.Errorf("deployment has %d ready replicas, expected at least 1", deployment.Status.ReadyReplicas)
}

return nil
}, pollTimeout, pollInterval).Should(o.Succeed())
})
})

g.Context("Pod Health", func() {
g.It("should have running pods without fatal errors in logs", func() {
k8sClient, err := getKubernetesClient()
o.Expect(err).NotTo(o.HaveOccurred(), "failed to create kubernetes client")

// First, verify pods are running
var pods *corev1.PodList
o.Eventually(func() error {
podList, err := k8sClient.CoreV1().Pods(operatorNamespace).List(ctx, metav1.ListOptions{
LabelSelector: "app=openshift-config-operator",
})
if err != nil {
return fmt.Errorf("failed to list pods: %w", err)
}

if len(podList.Items) == 0 {
return fmt.Errorf("no pods found with label app=openshift-config-operator")
}

for _, pod := range podList.Items {
if pod.Status.Phase != corev1.PodRunning {
return fmt.Errorf("pod %s is in phase %s, expected Running", pod.Name, pod.Status.Phase)
}
}

pods = podList
return nil
}, pollTimeout, pollInterval).Should(o.Succeed())

// Check pod logs for fatal errors
for _, pod := range pods.Items {
podLogs, err := k8sClient.CoreV1().Pods(operatorNamespace).GetLogs(pod.Name, &corev1.PodLogOptions{
Container: operatorName,
TailLines: int64Ptr(100),
}).DoRaw(ctx)
o.Expect(err).NotTo(o.HaveOccurred(), "failed to get logs for pod %s", pod.Name)

// Check for fatal errors in logs
scanner := bufio.NewScanner(strings.NewReader(string(podLogs)))
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(strings.ToLower(line), "fatal") ||
strings.Contains(strings.ToLower(line), "panic") {
g.Fail(fmt.Sprintf("found fatal error in pod %s logs: %s", pod.Name, line))
}
}
Comment on lines +76 to +84
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Log scanning may produce false positives.

The case-insensitive substring match for "fatal" and "panic" could flag benign log lines such as "no fatal errors found" or "panic recovery handler registered". Consider tightening the pattern—for example, matching common structured log prefixes like "F" or "level=fatal", or using a regex that checks for word boundaries.

🔎 Example of a tighter check
 				for scanner.Scan() {
 					line := scanner.Text()
-					if strings.Contains(strings.ToLower(line), "fatal") ||
-						strings.Contains(strings.ToLower(line), "panic") {
+					lower := strings.ToLower(line)
+					// Match klog fatal prefix or explicit panic indicators
+					if strings.HasPrefix(line, "F") || // klog fatal lines start with F
+						strings.Contains(lower, "level=fatal") ||
+						strings.Contains(lower, "panic:") {
 						g.Fail(fmt.Sprintf("found fatal error in pod %s logs: %s", pod.Name, line))
 					}
 				}
📝 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
// Check for fatal errors in logs
scanner := bufio.NewScanner(strings.NewReader(string(podLogs)))
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(strings.ToLower(line), "fatal") ||
strings.Contains(strings.ToLower(line), "panic") {
g.Fail(fmt.Sprintf("found fatal error in pod %s logs: %s", pod.Name, line))
}
}
// Check for fatal errors in logs
scanner := bufio.NewScanner(strings.NewReader(string(podLogs)))
for scanner.Scan() {
line := scanner.Text()
lower := strings.ToLower(line)
// Match klog fatal prefix or explicit panic indicators
if strings.HasPrefix(line, "F") || // klog fatal lines start with F
strings.Contains(lower, "level=fatal") ||
strings.Contains(lower, "panic:") {
g.Fail(fmt.Sprintf("found fatal error in pod %s logs: %s", pod.Name, line))
}
}
🤖 Prompt for AI Agents
In test/e2e/operator_health.go around lines 76 to 84, the current
case-insensitive substring checks for "fatal" and "panic" produce false
positives; replace them with a stricter regex-based check that matches log-level
patterns and whole words (e.g. \bpanic\b, \bfatal\b) and common structured keys
like level=fatal, "level":"fatal", or a single-letter prefix used by the logger
(e.g. ^F\d or ^P\d) using case-insensitive matching; compile the regex once,
test each log line against it, and only fail when the regex matches (optionally
also exclude obvious negations like lines starting with "no " or containing "no
fatal" before failing).

}
})
})
})
14 changes: 14 additions & 0 deletions vendor/github.com/go-task/slim-sprig/v3/.editorconfig

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions vendor/github.com/go-task/slim-sprig/v3/.gitattributes

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions vendor/github.com/go-task/slim-sprig/v3/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading