-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Fix TNF recovery test stability with AfterEach cleanup and migration-threshold #31530
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
563946c
16c8b13
36ded9a
3f68468
c20e7b3
e3c9f31
d140484
63ef4f6
51b731c
5d4183a
9af1036
8742bf8
88a1c7f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,8 @@ import ( | |
| "strings" | ||
| "time" | ||
|
|
||
| configv1 "github.com/openshift/api/config/v1" | ||
| configv1client "github.com/openshift/client-go/config/clientset/versioned" | ||
| "github.com/openshift/origin/pkg/monitortests/testframework/watchnamespaces" | ||
|
|
||
| "github.com/openshift/origin/pkg/monitor" | ||
|
|
@@ -17,12 +19,17 @@ import ( | |
| corev1 "k8s.io/api/core/v1" | ||
| apierrors "k8s.io/apimachinery/pkg/api/errors" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| utilnet "k8s.io/apimachinery/pkg/util/net" | ||
| "k8s.io/apimachinery/pkg/util/wait" | ||
| "k8s.io/client-go/kubernetes" | ||
| "k8s.io/client-go/rest" | ||
| "k8s.io/kubernetes/test/e2e/framework" | ||
| ) | ||
|
|
||
| type operatorLogAnalyzer struct { | ||
| kubeClient kubernetes.Interface | ||
| kubeClient kubernetes.Interface | ||
| adminRESTConfig *rest.Config | ||
| reducedTopology bool | ||
| } | ||
|
|
||
| func InitialAndFinalOperatorLogScraper() monitortestframework.MonitorTest { | ||
|
|
@@ -34,41 +41,148 @@ func (w *operatorLogAnalyzer) PrepareCollection(ctx context.Context, adminRESTCo | |
| } | ||
|
|
||
| func (w *operatorLogAnalyzer) StartCollection(ctx context.Context, adminRESTConfig *rest.Config, recorder monitorapi.RecorderWriter) error { | ||
| w.adminRESTConfig = adminRESTConfig | ||
| w.reducedTopology = isReducedTopology(ctx, adminRESTConfig) | ||
| var err error | ||
| w.kubeClient, err = kubernetes.NewForConfig(adminRESTConfig) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if err := scanAllOperatorPods(ctx, w.kubeClient, newOperatorLogHandler(recorder)); err != nil { | ||
| if err := scanAllOperatorPods(ctx, w.kubeClient, w.reducedTopology, newOperatorLogHandler(recorder)); err != nil { | ||
| if w.reducedTopology && isTransientScrapeError(err) { | ||
| framework.Logf("operator-log-scraper: transient error on reduced topology during StartCollection, flaking: %v", err) | ||
| return &monitortestframework.FlakeError{Err: fmt.Errorf("unable to scan operator logs: %w", err)} | ||
| } | ||
| return fmt.Errorf("unable to scan operator logs: %w", err) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func scanAllOperatorPods(ctx context.Context, kubeClient kubernetes.Interface, logHandlers ...podaccess.LogHandler) error { | ||
| pods, err := kubeClient.CoreV1().Pods("").List(ctx, metav1.ListOptions{}) | ||
| // isReducedTopology returns true for DualReplica (TNF) or SingleReplica (SNO) topologies | ||
| // where transient API errors during recovery are expected. | ||
| func isReducedTopology(ctx context.Context, adminRESTConfig *rest.Config) bool { | ||
| configClient, err := configv1client.NewForConfig(adminRESTConfig) | ||
| if err != nil { | ||
| framework.Logf("operator-log-scraper: failed to create config client: %v", err) | ||
| return false | ||
| } | ||
|
|
||
| infrastructure, err := configClient.ConfigV1().Infrastructures().Get(ctx, "cluster", metav1.GetOptions{}) | ||
| if err != nil { | ||
| return fmt.Errorf("couldn't list pods: %w", err) | ||
| framework.Logf("operator-log-scraper: failed to get infrastructure: %v", err) | ||
| return false | ||
|
Comment on lines
+65
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Resolve and retain topology before scan failures.
Detect and store the topology before scanning. Preserve strict handling when topology detection is unknown. 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| topology := infrastructure.Status.ControlPlaneTopology | ||
| return topology == configv1.DualReplicaTopologyMode || topology == configv1.SingleReplicaTopologyMode | ||
| } | ||
|
|
||
| // isTransientScrapeError classifies errors that are expected during node recovery: | ||
| // API server 503s, NotFound for pods being recreated, connection refused/reset | ||
| // during kubelet restarts, and terminated container errors. | ||
| func isTransientScrapeError(err error) bool { | ||
| if err == nil { | ||
| return false | ||
| } | ||
|
|
||
| if apierrors.IsServiceUnavailable(err) || apierrors.IsServerTimeout(err) || | ||
| apierrors.IsTimeout(err) || apierrors.IsNotFound(err) || | ||
| apierrors.IsTooManyRequests(err) { | ||
| return true | ||
| } | ||
|
|
||
| if utilnet.IsConnectionRefused(err) || utilnet.IsConnectionReset(err) { | ||
| return true | ||
| } | ||
|
|
||
| msg := err.Error() | ||
| transientSubstrings := []string{ | ||
| "connection refused", | ||
| "connect: connection refused", | ||
| "Service Unavailable", | ||
| "the server is currently unable to handle the request", | ||
| "TLS handshake timeout", | ||
| "kubelet was down or unresponsive", | ||
| "container not found", | ||
| "ContainerNotFound", | ||
| "is terminated", | ||
| "is waiting to start", | ||
| "is not available", | ||
| } | ||
| for _, s := range transientSubstrings { | ||
| if strings.Contains(msg, s) { | ||
| return true | ||
| } | ||
| } | ||
|
|
||
| var joined interface{ Unwrap() []error } | ||
| if errors.As(err, &joined) { | ||
| for _, inner := range joined.Unwrap() { | ||
| if isTransientScrapeError(inner) { | ||
| return true | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return false | ||
| } | ||
|
|
||
| func scanAllOperatorPods(ctx context.Context, kubeClient kubernetes.Interface, reducedTopology bool, logHandlers ...podaccess.LogHandler) error { | ||
| var pods *corev1.PodList | ||
| var lastListErr error | ||
| backoff := wait.Backoff{ | ||
| Duration: 1 * time.Second, | ||
| Factor: 2.0, | ||
| Jitter: 0.1, | ||
| Steps: 4, | ||
| } | ||
| listErr := wait.ExponentialBackoffWithContext(ctx, backoff, func(ctx context.Context) (bool, error) { | ||
| var err error | ||
| pods, err = kubeClient.CoreV1().Pods("").List(ctx, metav1.ListOptions{}) | ||
| if err != nil { | ||
| lastListErr = err | ||
| if isTransientScrapeError(err) { | ||
| framework.Logf("operator-log-scraper: transient error listing pods, retrying: %v", err) | ||
| return false, nil | ||
| } | ||
| return false, err | ||
| } | ||
| return true, nil | ||
| }) | ||
| if listErr != nil { | ||
| if pods == nil { | ||
| if lastListErr != nil { | ||
| return fmt.Errorf("couldn't list pods: %w", lastListErr) | ||
| } | ||
| return fmt.Errorf("couldn't list pods: %w", listErr) | ||
| } | ||
|
Comment on lines
+154
to
+160
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
target=$(git ls-files | rg '^pkg/monitortests/testframework/operatorloganalyzer/operator_log_scraper\.go$')
cat -n "$target" | sed -n '110,185p'
printf '\n--- dependency declarations ---\n'
rg -n 'k8s\.io/(client-go|/apimachinery)|client-go|apimachinery' --glob 'go.mod' --glob 'go.sum' --glob 'vendor/**' .
printf '\n--- relevant symbols and call sites ---\n'
rg -n 'ExponentialBackoffWithContext|ErrWaitTimeout|scanAllOperatorPods|\.List\(' "$target" pkg go.mod go.sum 2>/dev/null | head -200Repository: openshift/origin Length of output: 50373 🏁 Script executed: #!/bin/bash
set -eu
target='pkg/monitortests/testframework/operatorloganalyzer/operator_log_scraper.go'
cat -n "$target" | sed -n '1,185p'
printf '\n--- module versions ---\n'
rg -n 'k8s\.io/(client-go|apimachinery)' go.mod vendor/modules.txt | head -40
printf '\n--- exact retry and list definitions ---\n'
rg -l 'func ExponentialBackoffWithContext|type PodInterface interface|func \(.*\) List\(ctx context\.Context' vendor/k8s.io/apimachinery vendor/k8s.io/client-go |
while IFS= read -r f; do
printf '\n--- %s ---\n' "$f"
rg -n -A35 -B8 'func ExponentialBackoffWithContext|type PodInterface interface|func \(.*\) List\(ctx context\.Context' "$f"
doneRepository: openshift/origin Length of output: 25287 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
scraper = Path("pkg/monitortests/testframework/operatorloganalyzer/operator_log_scraper.go").read_text()
lister = Path("vendor/k8s.io/client-go/gentype/type.go").read_text()
backoff = Path("vendor/k8s.io/apimachinery/pkg/util/wait/backoff.go").read_text()
assert "pods, err = kubeClient.CoreV1().Pods(\"\").List(ctx, metav1.ListOptions{})" in scraper
assert "if listErr != nil {\n\t\tif pods == nil {" in scraper
assert "list := l.newList()" in lister
assert "err := l.client.client.Get()" in lister
assert "return list, err" in lister
assert "return ctx.Err()" in backoff
assert "return ErrWaitTimeout" in backoff
# Model the result shape returned by the vendored List implementation.
def current_outcome(list_err, pods, last_list_err):
if list_err is not None and pods is None:
return ("error", last_list_err if last_list_err is not None else list_err)
# The remaining scanner sees the returned list. An empty list produces no scan error.
return ("success", pods)
def proposed_outcome(list_err, pods, last_list_err):
if list_err == "wait.ErrWaitTimeout" and last_list_err is not None:
return ("error", last_list_err)
if list_err is not None:
return ("error", list_err)
return ("success", pods)
cases = [
("immediate non-transient request error", "api-error", {}, "api-error"),
("exhausted transient retries", "wait.ErrWaitTimeout", {}, "transient-api-error"),
("context cancellation", "context.Canceled", {}, "transient-api-error"),
]
for name, list_err, pods, last_err in cases:
current = current_outcome(list_err, pods, last_err)
proposed = proposed_outcome(list_err, pods, last_err)
print(f"{name}: current={current[0]}, proposed={proposed[0]}")
assert current[0] == "success"
assert proposed[0] == "error"
print("All modeled failure paths demonstrate the current error suppression.")
PYRepository: openshift/origin Length of output: 409 Propagate
🤖 Prompt for AI AgentsSource: MCP tools |
||
| } | ||
|
|
||
| errs := []error{} | ||
| for _, pod := range pods.Items { | ||
| if !strings.HasPrefix(pod.Namespace, "openshift-") { | ||
| continue | ||
| } | ||
| if !strings.Contains(pod.Name, "operator") { | ||
| if !strings.Contains(pod.Name, "-operator-") { | ||
| continue | ||
| } | ||
| // this is just a basic check to see if we can expect logs to be present. Unready, unhealthy, and failed pods all still have logs. | ||
| if pod.Status.Phase == corev1.PodPending || pod.Status.Phase == corev1.PodUnknown { | ||
| continue | ||
| } | ||
|
|
||
| for _, container := range pod.Spec.Containers { | ||
| streamer := podaccess.NewOneTimePodStreamer(kubeClient, pod.Namespace, pod.Name, container.Name, logHandlers...) | ||
| if err := streamer.ReadLog(ctx); err != nil && !apierrors.IsNotFound(err) { | ||
| if err := streamer.ReadLog(ctx); err != nil { | ||
| if apierrors.IsNotFound(err) { | ||
| continue | ||
| } | ||
| if reducedTopology && isTransientScrapeError(err) { | ||
| framework.Logf("operator-log-scraper: skipping transient error reading log for pods/%s -n %s -c %s: %v", | ||
| pod.Name, pod.Namespace, container.Name, err) | ||
| continue | ||
| } | ||
| errs = append(errs, fmt.Errorf("error reading log for pods/%s -n %s -c %s: %w", pod.Name, pod.Namespace, container.Name, err)) | ||
| } | ||
| } | ||
|
|
@@ -79,7 +193,12 @@ func scanAllOperatorPods(ctx context.Context, kubeClient kubernetes.Interface, l | |
|
|
||
| func (w *operatorLogAnalyzer) CollectData(ctx context.Context, storageDir string, beginning, end time.Time) (monitorapi.Intervals, []*junitapi.JUnitTestCase, error) { | ||
| localRecorder := monitor.NewRecorder() | ||
| if err := scanAllOperatorPods(ctx, w.kubeClient, newOperatorLogHandlerAfterTime(localRecorder, beginning)); err != nil { | ||
| if err := scanAllOperatorPods(ctx, w.kubeClient, w.reducedTopology, newOperatorLogHandlerAfterTime(localRecorder, beginning)); err != nil { | ||
| if w.reducedTopology && isTransientScrapeError(err) { | ||
| framework.Logf("operator-log-scraper: transient error on reduced topology during CollectData, flaking: %v", err) | ||
| return localRecorder.Intervals(time.Time{}, time.Time{}), nil, | ||
| &monitortestframework.FlakeError{Err: fmt.Errorf("unable to scan operator logs: %w", err)} | ||
| } | ||
| return nil, nil, fmt.Errorf("unable to scan operator logs: %w", err) | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.