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
4 changes: 4 additions & 0 deletions .github/workflows/migration-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ jobs:
run: make migration/e2e-fixture-setup
- name: Run fixture migration tests
run: make migration/test-e2e-fixture-matrix
- name: Verify cross-namespace fixture migration
run: make migration/test-e2e-cross-namespace
- name: Tear down fixture cluster
if: always()
run: E2E_CLUSTER_NAME=library-olm-fixture-e2e make migration/e2e-teardown
Expand Down Expand Up @@ -150,6 +152,8 @@ jobs:
run: make migration/e2e-setup
- name: Run live-operator migration tests
run: make migration/test-e2e-live-matrix
- name: Verify acknowledged live source-namespace deletion
run: make migration/test-e2e-live-namespace-delete
- name: Tear down live cluster
if: always()
run: make migration/e2e-teardown
Expand Down
8 changes: 8 additions & 0 deletions migration.mk
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,18 @@ migration/e2e-install-fixture-v0: ## Replay one OLMv0 install snapshot, or all,
migration/test-e2e-live-matrix: migration/build ## Install and migrate all three operators from live OLMv0
@set -euo pipefail; while IFS=$$'\t' read -r package channel namespace; do [[ -z "$$package" || "$$package" == \#* ]] && continue; coverage_dir="$(E2E_COVERAGE_DIR)/live/$$package"; artifact_dir="$(E2E_ARTIFACTS)/live/$$package"; mkdir -p "$$coverage_dir"; KUBECONFIG="$(E2E_KUBECONFIG)" ./hack/e2e/migration/delete-v1.sh "$$package"; KUBECONFIG="$(E2E_KUBECONFIG)" ./hack/e2e/migration/install-v0.sh "$$package"; KUBECONFIG="$(E2E_KUBECONFIG)" GOCOVERDIR="$$coverage_dir" E2E_ARTIFACTS="$$artifact_dir" E2E_SUITE=real-operator E2E_NAMESPACE="$$namespace" E2E_SUBSCRIPTION="$$package" go test -count=1 -tags=e2e ./test/e2e/migration -timeout "$(E2E_TIMEOUT)"; KUBECONFIG="$(E2E_KUBECONFIG)" ./hack/e2e/migration/delete-v1.sh "$$package"; done < test/e2e/migration/operators.tsv

.PHONY: migration/test-e2e-live-namespace-delete
migration/test-e2e-live-namespace-delete: migration/build ## Verify acknowledged source-namespace deletion with live OLMv0
@set -euo pipefail; package=ecr-secret-operator; namespace=migration-e2e-ecr-secret; target="$$namespace-target"; coverage_dir="$(E2E_COVERAGE_DIR)/live/cross-namespace-delete"; artifact_dir="$(E2E_ARTIFACTS)/live/cross-namespace-delete"; mkdir -p "$$coverage_dir"; KUBECONFIG="$(E2E_KUBECONFIG)" ./hack/e2e/migration/delete-v1.sh "$$package"; KUBECONFIG="$(E2E_KUBECONFIG)" kubectl delete namespace "$$target" --ignore-not-found --wait=true; KUBECONFIG="$(E2E_KUBECONFIG)" ./hack/e2e/migration/install-v0.sh "$$package"; KUBECONFIG="$(E2E_KUBECONFIG)" GOCOVERDIR="$$coverage_dir" E2E_ARTIFACTS="$$artifact_dir" E2E_SUITE=real-operator E2E_LIVE_NAMESPACE_DELETE_TEST=true E2E_NAMESPACE="$$namespace" E2E_SUBSCRIPTION="$$package" go test -count=1 -tags=e2e ./test/e2e/migration -run '^TestLiveCrossNamespaceDeletionMigration$$' -timeout "$(E2E_TIMEOUT)"; KUBECONFIG="$(E2E_KUBECONFIG)" ./hack/e2e/migration/delete-v1.sh "$$package"; KUBECONFIG="$(E2E_KUBECONFIG)" kubectl delete namespace "$$target" --ignore-not-found --wait=true

.PHONY: migration/test-e2e-fixture-matrix
migration/test-e2e-fixture-matrix: migration/build ## Replay and migrate all three OLMv0 install snapshots
@set -euo pipefail; while IFS=$$'\t' read -r package channel namespace; do [[ -z "$$package" || "$$package" == \#* ]] && continue; coverage_dir="$(E2E_COVERAGE_DIR)/fixture/$$package"; artifact_dir="$(E2E_ARTIFACTS)/fixture/$$package"; mkdir -p "$$coverage_dir"; KUBECONFIG="$(E2E_FIXTURE_KUBECONFIG)" ./hack/e2e/migration/delete-v1.sh "$$package"; KUBECONFIG="$(E2E_FIXTURE_KUBECONFIG)" ./hack/e2e/migration/install-fixture-v0.sh "$$package"; KUBECONFIG="$(E2E_FIXTURE_KUBECONFIG)" GOCOVERDIR="$$coverage_dir" E2E_ARTIFACTS="$$artifact_dir" E2E_SUITE=fixture E2E_NAMESPACE="$$namespace" E2E_SUBSCRIPTION="$$package" go test -count=1 -tags=e2e ./test/e2e/migration -timeout "$(E2E_TIMEOUT)"; KUBECONFIG="$(E2E_FIXTURE_KUBECONFIG)" ./hack/e2e/migration/delete-v1.sh "$$package"; done < test/e2e/migration/operators.tsv

.PHONY: migration/test-e2e-cross-namespace
migration/test-e2e-cross-namespace: migration/build ## Verify fixture migration into a different install namespace
@set -euo pipefail; package=ecr-secret-operator; namespace=migration-e2e-ecr-secret; target="$$namespace-target"; coverage_dir="$(E2E_COVERAGE_DIR)/fixture/cross-namespace"; artifact_dir="$(E2E_ARTIFACTS)/fixture/cross-namespace"; mkdir -p "$$coverage_dir"; KUBECONFIG="$(E2E_FIXTURE_KUBECONFIG)" ./hack/e2e/migration/delete-v1.sh "$$package"; KUBECONFIG="$(E2E_FIXTURE_KUBECONFIG)" kubectl delete namespace "$$target" --ignore-not-found --wait=true; KUBECONFIG="$(E2E_FIXTURE_KUBECONFIG)" ./hack/e2e/migration/install-fixture-v0.sh "$$package"; KUBECONFIG="$(E2E_FIXTURE_KUBECONFIG)" GOCOVERDIR="$$coverage_dir" E2E_ARTIFACTS="$$artifact_dir" E2E_SUITE=fixture E2E_CROSS_NAMESPACE_TEST=true E2E_NAMESPACE="$$namespace" E2E_SUBSCRIPTION="$$package" go test -count=1 -tags=e2e ./test/e2e/migration -run '^TestCrossNamespaceMigration$$' -timeout "$(E2E_TIMEOUT)"; KUBECONFIG="$(E2E_FIXTURE_KUBECONFIG)" ./hack/e2e/migration/delete-v1.sh "$$package"; KUBECONFIG="$(E2E_FIXTURE_KUBECONFIG)" kubectl delete namespace "$$target" --ignore-not-found --wait=true; : 'The source fixture namespace contains captured OLMv0 finalizers. install-fixture-v0.sh releases them before the next replay; CI tears down the Kind cluster.'

.PHONY: migration/test-e2e-in-cluster-job
migration/test-e2e-in-cluster-job: migration/e2e-fixture-setup migration/build-e2e-image ## Run migration CLIs in a fixture-cluster Job (no coverage collection)
E2E_OPERATOR=ecr-secret-operator E2E_KUBECONFIG="$(E2E_FIXTURE_KUBECONFIG)" $(MAKE) migration/e2e-delete-v1
Expand Down
139 changes: 139 additions & 0 deletions test/e2e/migration/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,145 @@ func TestMigration(t *testing.T) {
}
}

// TestCrossNamespaceMigration proves the explicit install-namespace path using
// a committed OLMv0 fixture. It is a separate invocation because it leaves the
// source namespace in place long enough to verify that only migrated operator
// resources, rather than the whole namespace, were removed.
func TestCrossNamespaceMigration(t *testing.T) {
if os.Getenv("E2E_CROSS_NAMESPACE_TEST") != "true" {
t.Skip("set E2E_CROSS_NAMESPACE_TEST=true to run the cross-namespace migration scenario")
}
if os.Getenv("E2E_SUITE") != "fixture" {
t.Fatal("cross-namespace migration is exercised against the fixture suite")
}

namespace, subscription := os.Getenv("E2E_NAMESPACE"), os.Getenv("E2E_SUBSCRIPTION")
if namespace == "" || subscription == "" {
t.Fatal("E2E_NAMESPACE and E2E_SUBSCRIPTION are required")
}
targetNamespace := namespace + "-target"
t.Cleanup(func() {
collectArtifacts(t, namespace)
if t.Failed() {
collectArtifacts(t, targetNamespace)
}
})

// Use labels which must be copied before the controller renders the target
// bundle. The target namespace is deliberately absent at conversion start.
run(t, "kubectl", "delete", "namespace/"+targetNamespace, "--ignore-not-found", "--wait=true")
// Use audit rather than enforce: the fixture bundle is not restricted-PSA
// compliant, so enforce would correctly prevent its Deployment from being
// created and turn this namespace-label preservation test into an unrelated
// admission test.
run(t, "kubectl", "label", "namespace/"+namespace,
"pod-security.kubernetes.io/audit=restricted",
"security.openshift.io/scc.podSecurityLabelSync=true", "--overwrite")

sourceDeployments, err := output("kubectl", "get", "deployment", "-n", namespace, "-o", "name")
if err != nil || strings.TrimSpace(sourceDeployments) == "" {
t.Fatalf("list source operator deployments: %v\n%s", err, sourceDeployments)
}

// The fixture CatalogSource is intentionally present but not reconciled by
// OLMv0; create its ClusterCatalog before conversion to satisfy C7.
run(t, binary(t, "migrate-catalogs-v0-to-v1"), "--kubeconfig", os.Getenv("KUBECONFIG"))
run(t, binary(t, "migrate-operators-v0-to-v1"), "convert", subscription,
"-n", namespace, "--install-namespace", targetNamespace,
"--kubeconfig", os.Getenv("KUBECONFIG"))

run(t, "kubectl", "wait", "--for=jsonpath={.status.conditions[?(@.type=='Installed')].status}=True", "clusterextension/"+subscription, "--timeout=10m")
gotNamespace, err := output("kubectl", "get", "clusterextension/"+subscription, "-o", "jsonpath={.spec.namespace}")
if err != nil || strings.TrimSpace(gotNamespace) != targetNamespace {
t.Fatalf("ClusterExtension install namespace = %q, err=%v; want %q", gotNamespace, err, targetNamespace)
}
for key, want := range map[string]string{
"pod-security.kubernetes.io/audit": "restricted",
"security.openshift.io/scc.podSecurityLabelSync": "true",
} {
got, err := output("kubectl", "get", "namespace/"+targetNamespace, "-o", "jsonpath={.metadata.labels."+escapeJSONPathLabel(key)+"}")
if err != nil || strings.TrimSpace(got) != want {
t.Fatalf("target namespace label %s = %q, err=%v; want %q", key, got, err, want)
}
}

// The catalog-rendered target Deployment proves the new installation is
// present. The source Deployments must be gone: retaining them would leave
// two active operator copies when the source namespace is intentionally kept.
targetDeployments, err := output("kubectl", "get", "deployment", "-n", targetNamespace, "-o", "name")
if err != nil || strings.TrimSpace(targetDeployments) == "" {
t.Fatalf("list target operator deployments: %v\n%s", err, targetDeployments)
}
for _, deployment := range strings.Fields(sourceDeployments) {
if out, err := output("kubectl", "get", deployment, "-n", namespace); err == nil {
t.Fatalf("source operator resource %s remains after cross-namespace migration:\n%s", deployment, out)
}
}
sourceDeletionTimestamp, err := output("kubectl", "get", "namespace/"+namespace, "-o", "jsonpath={.metadata.deletionTimestamp}")
if err != nil || strings.TrimSpace(sourceDeletionTimestamp) != "" {
t.Fatalf("source namespace deletionTimestamp = %q, err=%v; want empty", sourceDeletionTimestamp, err)
}
}

// TestLiveCrossNamespaceDeletionMigration verifies the destructive namespace
// path against OLMv0 itself. Unlike fixture tests, OLMv0 is present to release
// the CSV cleanup finalizer, so Kubernetes can complete namespace deletion.
func TestLiveCrossNamespaceDeletionMigration(t *testing.T) {
if os.Getenv("E2E_LIVE_NAMESPACE_DELETE_TEST") != "true" {
t.Skip("set E2E_LIVE_NAMESPACE_DELETE_TEST=true to run live namespace deletion")
}
if os.Getenv("E2E_SUITE") != "real-operator" {
t.Fatal("acknowledged namespace deletion is exercised against live OLMv0")
}

namespace, subscription := os.Getenv("E2E_NAMESPACE"), os.Getenv("E2E_SUBSCRIPTION")
if namespace == "" || subscription == "" {
t.Fatal("E2E_NAMESPACE and E2E_SUBSCRIPTION are required")
}
targetNamespace := namespace + "-target"
t.Cleanup(func() {
collectArtifacts(t, namespace)
if t.Failed() {
collectArtifacts(t, targetNamespace)
}
})

run(t, "kubectl", "delete", "namespace/"+targetNamespace, "--ignore-not-found", "--wait=true")
// Audit mode proves PSA labels are transferred without turning this test
// into an admission-policy test for the catalog bundle.
run(t, "kubectl", "label", "namespace/"+namespace,
"pod-security.kubernetes.io/audit=restricted",
"security.openshift.io/scc.podSecurityLabelSync=true", "--overwrite")

// Migrate catalogs before converting the Subscription so C7 is satisfied.
run(t, binary(t, "migrate-catalogs-v0-to-v1"), "--kubeconfig", os.Getenv("KUBECONFIG"))
run(t, binary(t, "migrate-operators-v0-to-v1"), "convert", subscription,
"-n", namespace, "--install-namespace", targetNamespace,
"--acknowledge-namespace-delete", "--kubeconfig", os.Getenv("KUBECONFIG"))

run(t, "kubectl", "wait", "--for=jsonpath={.status.conditions[?(@.type=='Installed')].status}=True", "clusterextension/"+subscription, "--timeout=10m")
for key, want := range map[string]string{
"pod-security.kubernetes.io/audit": "restricted",
"security.openshift.io/scc.podSecurityLabelSync": "true",
} {
got, err := output("kubectl", "get", "namespace/"+targetNamespace, "-o", "jsonpath={.metadata.labels."+escapeJSONPathLabel(key)+"}")
if err != nil || strings.TrimSpace(got) != want {
t.Fatalf("target namespace label %s = %q, err=%v; want %q", key, got, err, want)
}
}
targetDeployments, err := output("kubectl", "get", "deployment", "-n", targetNamespace, "-o", "name")
if err != nil || strings.TrimSpace(targetDeployments) == "" {
t.Fatalf("list target operator deployments: %v\n%s", err, targetDeployments)
}
if out, err := output("kubectl", "wait", "--for=delete", "namespace/"+namespace, "--timeout=10m"); err != nil {
t.Fatalf("wait for acknowledged source namespace deletion: %v\n%s", err, out)
}
}

func escapeJSONPathLabel(label string) string {
return strings.ReplaceAll(label, ".", `\.`)
}

// restoreSubscriptionForConflict replays the pre-migration Subscription without
// its API-assigned state, creating the Conflict state exercised by cleanup.
func restoreSubscriptionForConflict(t *testing.T, raw string) {
Expand Down
Loading