Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
19bff80
test: unpend two kdm restore PIts, fixing bugs found via live e2e val…
kaovilai Aug 27, 2026
a56cc2c
test: unit test FilterLogLinesContaining/CheckIfFlakeOccurred with re…
kaovilai Aug 29, 2026
2141682
docs: record name-bearing status of each flake pattern for CheckIfFla…
kaovilai Aug 29, 2026
d2de4f2
test: temporarily override kdm-controller image with a fix build for …
kaovilai Aug 29, 2026
0719e54
test: remove #208/#212 flake patterns, now fixed and confirmed live
kaovilai Aug 29, 2026
dced9d1
fix: don't fail must-gather check on the expected unsupportedOverride…
kaovilai Aug 29, 2026
ef2e8d6
test: bump kdm-controller override to combined-208-212v2-test
kaovilai Aug 29, 2026
359ecf4
test: bump kdm-controller override to combined-208-212v3-test
kaovilai Aug 29, 2026
b9e5d03
fix: also detect VMB-never-gets-any-condition as the CNV-85377/#18949…
kaovilai Aug 29, 2026
5e4d980
fix: send HCO spec.featureGates in its real array shape, not an object
kaovilai Aug 30, 2026
9e22b0f
Revert "fix: send HCO spec.featureGates in its real array shape, not …
kaovilai Aug 30, 2026
149c453
fix: prefer HCO v1 API over v1beta1, avoiding its conversion webhook
kaovilai Aug 30, 2026
6a29e02
fix: clear stuck VMB finalizers in shared deleteNamespace, not just v…
kaovilai Aug 30, 2026
73e7154
test: remove kdm-controller image override, upstream fixes merged
kaovilai Aug 31, 2026
7bd0c0d
fix: make kdm restore checksum verification actually run (#2421 item 1)
kaovilai Sep 1, 2026
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
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -1007,7 +1007,15 @@ TEST_VIRT ?= false
# too, since they carry both labels -- this only matters when TEST_VIRT_KDM
# is set WITHOUT TEST_VIRT.
TEST_VIRT_KDM ?= false
HCO_INDEX_TAG ?= 1.18.0
# Defaults to the upstream kubevirt/hyperconverged-cluster-index "nightly"
# moving tag rather than a pinned release, so every virt/kdm e2e run picks up
# kubevirt/kubevirt fixes (e.g. kubevirt/kubevirt#18949) automatically as soon
# as they land in a nightly build, with no version bump needed on our side.
# The OLM channel nightly publishes (e.g. "candidate-v1.20") is discovered
# live from the catalog's PackageManifest, not guessed from this tag's shape,
# so unpinned tags like "nightly" work correctly here. Override to a pinned
# release (e.g. "1.18.0") for a reproducible/stable run instead.
HCO_INDEX_TAG ?= nightly
# hcp
TEST_HCP ?= false
TEST_HCP_EXTERNAL ?= false
Expand Down
8 changes: 7 additions & 1 deletion build/ci-Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ RUN export KV_VERSION=$(curl --retry 5 --retry-delay 5 -s https://storage.google
chmod +x virtctl && \
mv virtctl /usr/local/bin/

RUN go mod download && \
# retry: confirmed live in CI -- proxy.golang.org intermittently drops the
# module fetch mid-transfer ("stream error: stream ID NNNN; INTERNAL_ERROR;
# received from peer"), failing the whole image build over a transient proxy
# hiccup unrelated to any code change. `go mod download` has no built-in
# retry flag, so wrap it the same way as the curl fetches above.
RUN retry() { for i in 1 2 3 4 5; do "$@" && return 0; echo "retrying ($i/5): $*" >&2; sleep 5; done; return 1; }; \
retry go mod download && \
mkdir -p $(go env GOCACHE) && \
chmod -R 777 ./ $(go env GOCACHE) $(go env GOPATH)
12 changes: 11 additions & 1 deletion tests/e2e/backup_restore_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,17 @@ func gatherLogs(brCase BackupRestoreCase, installTime time.Time, report ginkgo.S
func deleteNamespace(namespace string) {
err := lib.DeleteNamespace(kubernetesClientForSuiteRun, namespace)
gomega.Expect(err).ToNot(gomega.HaveOccurred())
gomega.Eventually(lib.IsNamespaceDeleted(kubernetesClientForSuiteRun, namespace), time.Minute*5, time.Second*5).Should(gomega.BeTrue())
// Clear any stuck VirtualMachineBackup finalizers (workaround for kubevirt#18724)
// before waiting for namespace termination. A spec that hits a known-bug skip
// path (see lib.CheckIfFlakeOccurred) unwinds via ginkgo.Skip before reaching
// its own namespace cleanup (e.g. IsNamespaceDeletedClearingStuckVMBFinalizers
// in virt_backup_restore_suite_test.go), leaving this shared AfterEach path as
// the only place left to do it -- confirmed live: without this, the plain
// IsNamespaceDeleted wait times out after 5m on a namespace whose VMB never
// got a real completed status (same still-open kubevirt/kubevirt#18949 disease
// as VMBHasNoConditions). Harmless no-op for namespaces with no VMBs.
vmbClearer := &lib.VirtOperator{Dynamic: dynamicClientForSuiteRun}
gomega.Eventually(vmbClearer.IsNamespaceDeletedClearingStuckVMBFinalizers(kubernetesClientForSuiteRun, namespace), time.Minute*5, time.Second*5).Should(gomega.BeTrue())
}

var _ = ginkgo.Describe("Backup and restore tests", ginkgo.Ordered, func() {
Expand Down
27 changes: 27 additions & 0 deletions tests/e2e/e2e_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client/config"
"sigs.k8s.io/controller-runtime/pkg/log/zap"

oadpv1alpha1 "github.com/openshift/oadp-operator/api/v1alpha1"
"github.com/openshift/oadp-operator/tests/e2e/lib"
libhcp "github.com/openshift/oadp-operator/tests/e2e/lib/hcp"
)
Expand Down Expand Up @@ -226,6 +227,32 @@ func TestOADPE2E(t *testing.T) {
UnsupportedOverrides: dpa.DeepCopy().Spec.UnsupportedOverrides,
}

// TEMPORARY: overrides kdm-controller with a build carrying
// migtools/kubevirt-datamover-controller#208 and #212 (neither merged
// upstream yet), so the incremental-sequence spec can actually validate
// those fixes instead of self-skipping on the known flake pattern every
// run (see lib.CheckIfFlakeOccurred). #212 also includes a second,
// intermittent fix: handleAccepted was trusting the informer cache's
// (possibly stale) VirtualMachineBackup.Status instead of re-reading it
// via APIReader before concluding terminal state -- found live via a
// Prow failure (ci/prow/5.0-e2e-test-kubevirt-aws) where virt-controller
// had already written Done=True but kdm-controller's reconcile loop
// never observed it. That fix recurred with the identical symptom on its
// first build (v2) despite every mechanical piece checking out (image
// built after the fix commit, correctly pulled, APIReader correctly
// wired in main.go, refresh correctly placed before the status-check
// call) -- v3 adds logging to the refresh's own success/NotFound/error
// branches to settle, on the next recurrence, whether the live API
// server itself never had Done=True (a virt-controller/kubevirt-level
// stall, not kdm-controller's bug) or the "uncached" read has some
// subtler issue. Remove this override once #208/#212 merge and a
// release picks them up -- the settings.json-driven UnsupportedOverrides
// above is the normal, permanent path for this.
if dpaCR.UnsupportedOverrides == nil {
dpaCR.UnsupportedOverrides = map[oadpv1alpha1.UnsupportedImageKey]string{}
}
dpaCR.UnsupportedOverrides[oadpv1alpha1.KubeVirtDatamoverControllerImageKey] = "quay.io/tkaovila/kubevirt-datamover-controller:combined-208-212v3-test"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Note this has override still I believe ..

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

removing.. will need another acky


ginkgo.RunSpecs(t, "OADP E2E using velero prefix: "+veleroPrefix)
}

Expand Down
78 changes: 77 additions & 1 deletion tests/e2e/lib/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os/exec"
"path/filepath"
"reflect"
"regexp"
"sort"
"strings"
"time"
Expand Down Expand Up @@ -377,6 +378,24 @@ func PrintNamespaceEventsAfterTime(c *kubernetes.Clientset, namespace string, st
}
}

// GetNamespaceEventMessages returns "Reason: Message" for every event
// currently in namespace, for feeding into CheckIfFlakeOccurred alongside
// pod logs -- some known-flake signatures (e.g. a controller's own emitted
// Kubernetes Event on an object it owns) only ever show up as an Event, not
// in any pod's log output.
func GetNamespaceEventMessages(c *kubernetes.Clientset, namespace string) []string {
events, err := c.CoreV1().Events(namespace).List(context.Background(), metav1.ListOptions{})
if err != nil {
log.Printf("could not list events in namespace %s for flake-detection: %v", namespace, err)
return nil
}
messages := make([]string, 0, len(events.Items))
for _, event := range events.Items {
messages = append(messages, fmt.Sprintf("%s: %s", event.Reason, event.Message))
}
return messages
}

func RunMustGather(artifact_dir string, clusterClient client.Client) error {
mustGatherImage := os.Getenv("MUST_GATHER_IMAGE")
if mustGatherImage == "" {
Expand Down Expand Up @@ -427,12 +446,69 @@ func RunMustGather(artifact_dir string, clusterClient client.Client) error {
mustGatherSummaryText := string(mustGatherSummaryContent)

if !strings.Contains(mustGatherSummaryText, "No errors happened or were found while running OADP must-gather") {
return errors.New("expected no errors in must-gather Errors section")
if !mustGatherErrorsAreOnlyKnownBenignWarnings(mustGatherSummaryText) {
return errors.New("expected no errors in must-gather Errors section")
}
}

return nil
}

// mustGatherKnownBenignErrorPatterns are must-gather "Errors" section lines
// that are purely informational -- the summary generator flags any DPA using
// spec.unsupportedOverrides at all, regardless of which key or why, since
// that field is inherently "unsupported" by design. A test intentionally
// using it (e.g. to override kdm-controller's image with a candidate fix
// build before it merges upstream) is not itself an error condition, and
// treating it as one broke every e2e job's must-gather check the moment any
// suite's DPA carried an UnsupportedOverrides entry -- confirmed live on
// oadp-operator PR#2404, where a single test-time override in
// e2e_suite_test.go's shared dpaCR broke unrelated CLI e2e jobs across
// multiple OCP versions. Any OTHER line in the Errors section still fails
// the check below -- only this specific, expected caveat is tolerated.
var mustGatherKnownBenignErrorPatterns = []*regexp.Regexp{
regexp.MustCompile(`is using \*\*unsupportedOverrides\*\*`),
}

// mustGatherErrorsAreOnlyKnownBenignWarnings extracts the "## Errors" section
// of the must-gather summary and reports whether every non-blank line in it
// matches a known-benign pattern above. Returns false (i.e. a real error is
// present) if the section can't be found at all, to fail safe.
func mustGatherErrorsAreOnlyKnownBenignWarnings(summary string) bool {
lines := strings.Split(summary, "\n")
start := -1
for i, line := range lines {
if strings.TrimSpace(line) == "## Errors" {
start = i + 1
break
}
}
if start == -1 {
return false
}

for _, line := range lines[start:] {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
if strings.HasPrefix(trimmed, "## ") {
break
}
matched := false
for _, p := range mustGatherKnownBenignErrorPatterns {
if p.MatchString(trimmed) {
matched = true
break
}
}
if !matched {
return false
}
}
return true
}

// VerifyBackupRestoreData verifies if app ready before backup and after restore to compare data.
// skipReadyz skips the post-restore readyz endpoint check (use for VM-based tests where the
// app route is not directly reachable from the test harness).
Expand Down
52 changes: 52 additions & 0 deletions tests/e2e/lib/apps_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package lib

import "testing"

// realMustGatherSummaryUnsupportedOverridesOnly is the actual "## Errors"
// section (and the section immediately following it) captured from a real
// CI failure: openshift/oadp-operator PR#2404's ci/prow/5.1-e2e-test-cli-aws
// run (2093596791407644672), which failed with "expected no errors in
// must-gather Errors section" purely because the shared dpaCR carried a
// spec.unsupportedOverrides entry (a test-time kdm-controller image
// override, unrelated to the CLI suite that failed).
const realMustGatherSummaryUnsupportedOverridesOnly = `## Errors

⚠️ DataProtectionApplication **ts-velero-test** in **openshift-adp** namespace is using **unsupportedOverrides**



## Cluster information

| Cluster ID | OpenShift version | Cloud provider | Architecture |
| ---------- | ----------------- | -------------- | ------------ |
| 65162e20 | 5.1.0-0.nightly-2026-08-27-012048 | AWS | linux/amd64 |
`

func Test_mustGatherErrorsAreOnlyKnownBenignWarnings_realUnsupportedOverridesCapture(t *testing.T) {
if !mustGatherErrorsAreOnlyKnownBenignWarnings(realMustGatherSummaryUnsupportedOverridesOnly) {
t.Fatalf("expected the real captured unsupportedOverrides-only Errors section to be treated as benign")
}
}

func Test_mustGatherErrorsAreOnlyKnownBenignWarnings_realErrorStillFails(t *testing.T) {
summary := `## Errors

⚠️ DataProtectionApplication **ts-velero-test** in **openshift-adp** namespace is using **unsupportedOverrides**
❌ Velero pod is in CrashLoopBackOff

## Cluster information
`
if mustGatherErrorsAreOnlyKnownBenignWarnings(summary) {
t.Fatalf("expected a genuine error line alongside the benign warning to still be treated as a real error")
}
}

func Test_mustGatherErrorsAreOnlyKnownBenignWarnings_noErrorsSectionFailsSafe(t *testing.T) {
summary := `## Cluster information

| Cluster ID | OpenShift version |
`
if mustGatherErrorsAreOnlyKnownBenignWarnings(summary) {
t.Fatalf("expected a missing Errors section to fail safe (treated as a real error)")
}
}
58 changes: 58 additions & 0 deletions tests/e2e/lib/flakes.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,59 @@ type FlakePattern struct {
StringSearchPattern string
}

// FilterLogLinesContaining returns only the lines of logs that contain at
// least one of the given substrings. kdm-controller runs as a single shared
// pod across every spec in a suite run (it's never restarted between specs),
// so a raw, unfiltered pod-log fetch mixes lines from whichever OTHER
// DataUpload/backup happened to be reconciling at the same moment. Passing
// that unfiltered text straight to CheckIfFlakeOccurred lets a stale line
// from a completely different, earlier spec's backup match a known-bug
// pattern and misattribute it to the CURRENT spec -- confirmed as a real risk
// live: the skip path this filtering protects deletes the current spec's own
// backup via DeleteVeleroBackupAndRestore before skipping, so a
// misattributed match could make an otherwise-healthy spec delete its own
// good backup over noise from someone else's stuck one. Callers should pass
// the specific backup name and/or DataUpload name they actually care about.
func FilterLogLinesContaining(logs string, substrs ...string) string {
if len(substrs) == 0 {
return logs
}
var matched []string
for _, line := range strings.Split(logs, "\n") {
for _, s := range substrs {
if s != "" && strings.Contains(line, s) {
matched = append(matched, line)
break
}
}
}
return strings.Join(matched, "\n")
}

// CheckIfFlakeOccurred checks for known flake patterns in the provided logs (typically logs from the test ran).
//
// Parameters:
//
// logs ([]string): Logs to be examined for known flake patterns.
//
// Name-bearing status of each pattern below, for callers pre-filtering with
// FilterLogLinesContaining (verified against kdm-controller source
// migtools/kubevirt-datamover-controller@internal/controller/kubevirt_dataupload_controller.go,
// 2026-08-29 -- re-check this if that file's logger plumbing changes):
// - CNV-85377 ("is being attached to VMI"): not found verbatim in
// kdm-controller source; if it ever surfaces there it would be via a VMB
// condition Reason/Message logged inline (e.g. "reason", cond.Reason) on
// kdm-controller's shared, context-injected logger -- also safe to scope
// by backup/DataUpload name. Otherwise it's virt-controller's own
// condition text, which reaches us only via Kubernetes Events, same as
// the #18957 pattern below.
// - kubevirt#18957 ("VMI backup status was lost"): virt-controller's own
// event text, never appears in any pod log -- callers must check this
// against Namespace Events (already namespace-scoped), never filtered
// pod logs, or it will never match.
// - external-snapshotter#876, velero#5856, OADP-5086: velero/snapshot-controller
// strings that never appear in kdm-controller's own log; irrelevant to
// scoping decisions made against kdm-controller pod logs specifically.
func CheckIfFlakeOccurred(logs []string) bool {
flakePatterns := []FlakePattern{
{
Expand All @@ -55,6 +103,16 @@ func CheckIfFlakeOccurred(logs []string) bool {
Description: "Startup probe timeout causing deployment readiness failure after restore",
StringSearchPattern: "deployment is not in a ready state",
},
{
Issue: "https://redhat.atlassian.net/browse/CNV-85377",
Description: "virt-controller's VirtualMachineBackup status can silently stop advancing after the underlying attach already succeeded: startBackup()'s attach branch (kubevirt/kubevirt pkg/storage/cbt/backup.go) returns without writing a status condition or requeuing, so recovery depends entirely on a VMI watch event that may never fire again -- also reported as https://redhat.atlassian.net/browse/CNV-89684",
StringSearchPattern: "is being attached to VMI",
},
{
Issue: "https://github.com/kubevirt/kubevirt/pull/18957",
Description: "virt-controller's reconcileStart() (pkg/storage/cbt/backup.go) can mark an already-successfully-completed VirtualMachineBackup Failed with reason SourceLost: cleanupVMIState() clears the VMI's BackupStatus and re-triggers reconcile before the backup's own just-written terminal status is visible via the informer's async cache, so the stale reconcile wrongly concludes the status was lost mid-flight -- observed live immediately after a real 'Completed VirtualMachineBackup, warning: ...' event, first seen testing kubevirt/kubevirt nightly (v1.20.0). Fixed at kubevirt/kubevirt#18957 (checks the API server directly before concluding lost); distinct from CNV-85377/kubevirt/kubevirt#18949.",
StringSearchPattern: "VMI backup status was lost",
},
}
logString := strings.Join(logs, "\n")

Expand Down
Loading