diff --git a/.gitignore b/.gitignore index aaadf73..507246d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # # Binaries for programs and plugins +/bin/ *.exe *.exe~ *.dll @@ -18,7 +19,7 @@ coverage.* profile.cov # Dependency directories (remove the comment below to include it) -# vendor/ +vendor/ # Go workspace file go.work @@ -27,6 +28,11 @@ go.work.sum # env file .env -# Editor/IDE -# .idea/ -# .vscode/ +# Editor/IDE/OS + .idea/ + .vscode/ +*.swp +.DS_Store + +# Migration artifacts produced at runtime +olm-migration-backup-*/ diff --git a/README.md b/README.md index ee86ab3..3dd0713 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,25 @@ # library-olm -Libraries and other utilities for Operator Framework and OLM + +A `v0` collection of Go libraries and CLIs for Operator Lifecycle Manager (OLM). + +This repository is the basis for the eventual `operator-framework/library-olm`. It is +personal and pre-release: APIs may change without notice while at `v0`. + +## Contents + +- [`migration/`](migration/) — OLMv0 → OLMv1 migration library and CLIs + ([OCPSTRAT-2693](https://redhat.atlassian.net/browse/OCPSTRAT-2693)). Start with + [`specs/20260821-migration-v0-to-v1/README.md`](specs/20260821-migration-v0-to-v1/README.md). + +The migration work follows a Specification-Driven Design (SDD) layout: + +| Doc | Purpose | +|---|---| +| [specs/20260821-migration-v0-to-v1/README.md](specs/20260821-migration-v0-to-v1/README.md) | Overview of the feature | +| [specs/20260821-migration-v0-to-v1/requirements.md](specs/20260821-migration-v0-to-v1/requirements.md) | What must be built, field-by-field | +| [specs/20260821-migration-v0-to-v1/plan.md](specs/20260821-migration-v0-to-v1/plan.md) | How it will be built (phased) | +| [specs/20260821-migration-v0-to-v1/validation.md](specs/20260821-migration-v0-to-v1/validation.md) | How we prove it works | + +## License + +Apache 2.0 — see [LICENSE](LICENSE). diff --git a/specs/20260821-migration-v0-to-v1/README.md b/specs/20260821-migration-v0-to-v1/README.md new file mode 100644 index 0000000..e2d94c5 --- /dev/null +++ b/specs/20260821-migration-v0-to-v1/README.md @@ -0,0 +1,115 @@ +# OLMv0 → OLMv1 Migration + +A `v0` Go library plus two CLIs that migrate operators from **OLMv0** (`Subscription` / +`ClusterServiceVersion` / `CatalogSource`) to **OLMv1** (`ClusterExtension` / +`ClusterObjectSet` / `ClusterCatalog`) management — with minimal downtime and full +rollback at every step. + +> Status: upstream **prototype** for +> [OCPSTRAT-2693](https://redhat.atlassian.net/browse/OCPSTRAT-2693). The library is +> versioned `v0` — breaking API changes are expected. Binary packaging, Console UI, and +> Hosted Control Planes are out of scope (see [requirements.md](requirements.md) §R10). + +## Why + +With OLMv1 reaching general availability, cluster administrators need a supported path to +move operators currently managed by OLMv0 onto OLMv1. Without tooling, adoption means +manually decommissioning and reinstalling each operator — risky, error-prone, and likely +to cause downtime or data loss for workloads that depend on those operators. This tooling +automates the transition, reduces operational risk, and accelerates OLMv1 adoption. + +## The two CLIs + +Catalogs are migrated **first**, then operators — the operator tool refuses to migrate an +operator whose package is not served by a `ClusterCatalog`. + +### `migrate-catalogs-v0-to-v1` +Migrates OLMv0 `CatalogSource` resources to OLMv1 `ClusterCatalog` resources. + +| Flag | Purpose | +|---|---| +| `--dry-run` | Print what would be created without modifying the cluster | + +### `migrate-operators-v0-to-v1` +Migrates OLMv0 `Subscription`/`CSV` installations to OLMv1 `ClusterExtension`/`ClusterObjectSet`. + +Every action is a verb subcommand that takes either an operator name **or** `--all` +(kubectl/`oc`-style). Because the target is an argument, an operator literally named +`check` or `convert` is unambiguous (`convert check`). + +| Command | Target | Purpose | +|---|---|---| +| `check \| --all` | Subscription(s) | Report readiness, compatibility, and four-state classification (no changes). `--all` scans the whole cluster. | +| `convert \| --all` | Subscription(s) | Perform the migration. `--dry-run` previews without changing the cluster. `--all` prints the four-section summary and converts every Eligible operator. | +| `rollback \| --all` | ClusterExtension(s) | Restore an operator to OLMv0 management. | +| `cleanup \| --all` | ClusterExtension(s) | Finish a partial migration (Conflict state). | + +Common flags: `-n/--namespace` (Subscription namespace), `--all`, `--dry-run` (on +`convert`), `--continue-on-error` (on `convert --all`), and the `--acknowledge-*` override +flags. Note the target differs by phase: `check`/`convert` act on a `Subscription` +(name + `-n` namespace); `rollback`/`cleanup` act on the resulting `ClusterExtension`. + +The tool is **non-interactive**: there are no prompts. `convert --dry-run` is the preview +mechanism, and every risk override is an explicit `--acknowledge-*` flag (see +[requirements.md](requirements.md) §R3). + +## Operator states + +`migrate-operators-v0-to-v1 check --all` classifies every OLMv0 `Subscription` into one of four states (and `convert --all` prints the same summary before migrating the Eligible ones): + +| State | Meaning | Action | +|---|---|---| +| **Eligible** | Passes all readiness & compatibility checks; package available in a `ClusterCatalog`; no `ClusterExtension` yet | Migrate | +| **Ineligible** | Fails one or more checks | Report the specific reason; skip (override with the matching `--acknowledge-*` flag) | +| **AlreadyMigrated** | No `Subscription`, but a `ClusterExtension` annotated `migrated-from-subscription` exists | Report as done; skip | +| **Conflict** | Both a `Subscription` **and** an annotated `ClusterExtension` exist — indicates a failed cleanup | Warn prominently; block; resolve with `cleanup` or `rollback` | + +## High-level flow + +``` +migrate-catalogs-v0-to-v1 (prerequisite: CatalogSource -> ClusterCatalog) + │ + ▼ +check ──────────────► convert ─────────────────────────────────────► cleanup + (classify / compat; │ (--dry-run to preview) (delete Sub/CSV, + --all to scan) │ OperatorGroup if last) + ├─ profile Subscription/CSV/InstallPlan + ├─ resolve target ClusterCatalog (by image) + ├─ back up Subscription and OperatorGroup specs (CE annotations) + ├─ collect owned resources (5 sources, dedup) + ├─ create ClusterObjectSet (SecretPacker, IfNoController) + │ └─ wait for COS controller: Succeeded=True + └─ create ClusterExtension (adopts the COS) + ▲ + └── rollback: delete CE+COS (orphan cascade), restore Subscription +``` + +Workloads keep running throughout when the install namespace is unchanged — only the +management plane changes (**close-to-zero downtime**). When a namespace change is required, +some downtime is unavoidable while the deployment restarts in the new namespace, but it is +minimized. + +## Architecture at a glance + +- **Standalone `v0` module**, two binaries, prow + GitHub Actions CI. +- Targets OLMv1's `ClusterObjectSet` (COS) — the current name for the revision object + (formerly `ClusterExtensionRevision`). +- The migration tool **creates** the COS and **waits** for the COS controller to set + `Succeeded=True`; it never writes status on OLMv1 APIs. It then creates the + `ClusterExtension`, which adopts the COS via owner labels. +- Collected objects are stored via boxcutter's **SecretPacker** (not inline) to support + large bundles, with `CollisionProtection: IfNoController` so OLMv1 can adopt existing + resources (including CRDs) without conflict. + +## Prototype lineage + +- [joelanford/operator-controller `0-to-1`](https://github.com/joelanford/operator-controller/tree/0-to-1) — single-file proof of concept +- [perdasilva/operator-controller `migration`](https://github.com/perdasilva/operator-controller/tree/migration) — the base this work is ported from + +## Documents + +| Doc | Contents | +|---|---| +| [requirements.md](requirements.md) | Functional/architectural requirements, eligibility rules, per-field handling for every Subscription, OperatorGroup, and CatalogSource field, and the ClusterExtension/ClusterCatalog target mappings | +| [plan.md](plan.md) | The eight implementation phases | +| [validation.md](validation.md) | Verifiable acceptance criteria and a requirement-traceability table | diff --git a/specs/20260821-migration-v0-to-v1/plan.md b/specs/20260821-migration-v0-to-v1/plan.md new file mode 100644 index 0000000..936a5c4 --- /dev/null +++ b/specs/20260821-migration-v0-to-v1/plan.md @@ -0,0 +1,183 @@ +# Implementation Plan — OLMv0 → OLMv1 Migration + +Eight phases plus a cross-repo prerequisite. Each phase lists its goal, key files, +dependencies, a one-line exit criterion, and the tracking Jira story. Requirement +references (`Rn`) point at [requirements.md](requirements.md); validation references at +[validation.md](validation.md). + +Base for the port: [perdasilva/operator-controller `migration`](https://github.com/perdasilva/operator-controller/tree/migration) +(`internal/operator-controller/migration/` + `hack/tools/migrate/`). + +### CLI as prototype / library exercise + +The CLIs in this repo (`migrate-operators-v0-to-v1`, `migrate-catalogs-v0-to-v1`) are +**example consumers of the library**, not production delivery artifacts. Their purpose is to +exercise and test the library API during the prototype phase; binary packaging and production +delivery are deferred to the downstream TP ([OCPSTRAT-2692](https://redhat.atlassian.net/browse/OCPSTRAT-2692)). + +To make this intent obvious in the repo layout, CLIs live under **`migration/examples/cmd/`** +rather than `migration/cmd/`: + +``` +migration/ + pkg/migration/ ← the library (canonical; the real deliverable) + pkg/catalogmigration/ ← catalog migration library + examples/ + cmd/ + migrate-operators-v0-to-v1/ ← example CLI exercising pkg/migration + migrate-catalogs-v0-to-v1/ ← example CLI exercising pkg/catalogmigration +``` + +Downstream consumers (e.g., an `oc` plugin or the Console) will import `pkg/migration` +directly and build their own CLI surface. + +--- + +## Prerequisite (cross-repo, runs in parallel) — [OPRUN-4716](https://redhat.atlassian.net/browse/OPRUN-4716) + +**Verify COS adoption end-to-end in `operator-controller`.** No controller changes are +expected: the COS controller reconciles any COS regardless of origin, and the CE controller +discovers a pre-created COS via `olm.operatorframework.io/owner-name` and adopts it (SSA +patch) on first reconcile. Confirm a manually pre-created COS (owner labels, `revision: 1`, +correct bundle annotations) reaches `Succeeded=True` and is adopted by a subsequently created +CE **without** producing a duplicate COS. Track boxcutter phase 2 / `ClusterObjectDeployment` +(R2.7). *Exit:* documented confirmation the flow works with no operator-controller changes. +Verified during Phase 8 E2E. + +--- + +## Phase 1 — Repo bootstrap & prototype port — [OPRUN-4717](https://redhat.atlassian.net/browse/OPRUN-4717) +**Goal:** Stand up the `v0` module and port the prototype onto OLMv1's current APIs. +- Module `github.com/operator-framework/library-olm` (`v0.x`); GitHub Actions (build, test, + golangci-lint, go-apidiff); prow config (tide, lgtm/approve, hold) mirroring operator-controller. +- Port to `migration/pkg/migration/` and `migration/examples/cmd/migrate-operators-v0-to-v1/`. +- Rename `ClusterExtensionRevision*` → `ClusterObjectSet*` (`clusterobjectset.go` apply-config, + `ClusterObjectSetTypeSucceeded`) (R2.2). +- Replace inline COS objects with boxcutter **SecretPacker**; set `CollisionProtection: IfNoController` (R2.4). +- `go.mod` deps: `operator-framework/operator-controller` (`ocv1`), `operator-framework/api` (OLMv0). + +**Depends on:** prerequisite (for the adoption contract). **Exit:** `go build ./...` and +`go test ./...` pass; CI green on the skeleton. + +## Phase 2 — Scan & classification — [OPRUN-4718](https://redhat.atlassian.net/browse/OPRUN-4718) +**Goal:** Four-state classification with catalog availability at scan time (R1.3, R4, C7). +- `types.go`: `OperatorStatus` enum (`Eligible`/`Ineligible`/`AlreadyMigrated`/`Conflict`), + replacing `OperatorScanResult.Eligible bool`. +- `scan.go` `ScanAllSubscriptions`: detect `Conflict` (Sub + annotated CE) and + `AlreadyMigrated` (no Sub + annotated CE); call catalog resolution per operator. +- `migration.go`: set the `olm.operatorframework.io/migrated-from-subscription: /` + annotation on **both** the COS and the CE (replacing the prototype's `migrated-from-v0: "true"`). + Setting it on the COS makes migrated revisions discoverable by cluster admins independently of + the CE, and provides provenance even if the CE annotation is lost. + +**Depends on:** Phase 1. **Exit:** unit tests classify one fixture operator into each state. + +## Phase 3 — Compatibility checks & acknowledgment framework — [OPRUN-4719](https://redhat.atlassian.net/browse/OPRUN-4719) +**Goal:** All eligibility rules (R3) and the override mechanism (R2.5). +- `compatibility.go`: implement C1, C4, C5, C6, C8 as overridable (soft) checks; C2 and C3 + as hard (non-overridable) blocks. Add `checkNoOLMv0APIAccess` (C5, inspecting all installed + RBAC, **excluding** `operatorconditions`, flagging only if OLMv0 API access exists without + OLMv1 RBAC). Keep the OperatorCondition-**status** check (C4, R9). +- `types.go`: `Options` gains one `bool` per soft flag — `AcknowledgeWatchScopeChange`, + `AcknowledgeOperatorCondition`, `AcknowledgeOLMv0APIAccess`, `AcknowledgeScopedServiceAccount`, + `AcknowledgeNotSteadyState`, `AcknowledgeNamespaceDelete`, `AcknowledgeInstalled` — plus + `ContinueOnError`. +- On use, record `olm.operatorframework.io/acknowledged-: "true"` on the CE. +- Collector places all objects (incl. CRDs) into the COS with `IfNoController`. + +**Note:** Once [OPRUN-4723](https://redhat.atlassian.net/browse/OPRUN-4723) (Phase 7) merges, +**remove C3 entirely** — OLMv1 will manage APIService objects natively so operators with +APIService definitions become Eligible with no flag required. + +**Depends on:** Phases 1, 2. **Exit:** each soft check flips Ineligible→Eligible when its +flag is set; CE carries the matching annotation. + +## Phase 4 — Migration & recovery commands — [OPRUN-4720](https://redhat.atlassian.net/browse/OPRUN-4720) +**Goal:** The operator CLI surface (R1.1, R1.2, R1.4, R1.5, R1.8) — verb-plus-target +(kubectl/`oc`-style); each verb takes an operator name or `--all`. +- `check | --all`: readiness + compatibility + four-state classification; `--all` scans the cluster (calls `Check`/`ScanAll`). +- `convert | --all`: profile → resolve catalog → back up Subscription and OperatorGroup + specs to CE annotations (R2.5) → optional `--backup ` (R2.6) → collect + (primary: `Operator` CR `status.components.refs`; supplementary: `olm.owner` label query, + ownerRef query, InstallPlan steps; dedup by GVK+ns+name — see R5) → create COS (wait + `Succeeded=True`) → create CE → cleanup. + `--dry-run` previews via `Gather`. `--all` prints the four-section summary, then converts + each Eligible operator; `--continue-on-error` to keep going. +- `rollback | --all`: require `--acknowledge-installed` when CE is `Installed=True`; delete CE + then COS with orphan cascade (fallback: new COS revision → `Succeeded=True` → orphan delete); + restore Subscription from the backup annotation (`startingCSV` → `installedCSV`). +- `cleanup | --all`: for `Conflict`; delete Subscription (orphan) + `CleanupOLMv0Resources`. +- CLI files under `migration/examples/cmd/migrate-operators-v0-to-v1/` (one file per verb). + +**Depends on:** Phases 1–3. **Exit:** `check`, `convert` (single + `--all`), `rollback`, and +`cleanup` each pass their VALIDATION per-command checks on kind. + +## Phase 5 — Catalog migration CLI *(parallelizable with 2–4)* — [OPRUN-4722](https://redhat.atlassian.net/browse/OPRUN-4722) +**Goal:** `migrate-catalogs-v0-to-v1` (R7). +- `migration/pkg/catalogmigration/` + `migration/examples/cmd/migrate-catalogs-v0-to-v1/`. +- List CatalogSources; skip already-migrated (matching image); create `ClusterCatalog` from + the image and wait `Serving=True`; report per source; `--dry-run`. +- Report non-image sources (configmap/internal/address) as not migratable. + +**Depends on:** Phase 1. **Exit:** N CatalogSources → N serving ClusterCatalogs; operator scan +then reports catalog-available. + +## Phase 6 — Install-namespace change ⚠️ blocked — [OPRUN-4721](https://redhat.atlassian.net/browse/OPRUN-4721) +**Goal:** Support `--install-namespace` differing from the Subscription namespace (R6, R9). +- **Blocked on** [OCPSTRAT-2690](https://redhat.atlassian.net/browse/OCPSTRAT-2690) / + [OPRUN-4505](https://redhat.atlassian.net/browse/OPRUN-4505) / + [PR #2825](https://github.com/operator-framework/operator-controller/pull/2825) (making + `spec.namespace` optional / COS-managed). Once it lands, the tool may omit `spec.namespace`. +- Move namespace-scoped resources to the new namespace; copy PSA (`pod-security.kubernetes.io/*`) + and `security.openshift.io/scc.podSecurityLabelSync` labels; delete the old namespace only with + `--acknowledge-namespace-delete`. + +**Depends on:** Phases 1, 3 + PR #2825. **Exit:** resources land in the new namespace with PSA/SCC +labels copied; old namespace deleted only when acknowledged. + +## Phase 7 — OLMv1 APIService renderer support *(cross-repo, operator-controller)* — [OPRUN-4723](https://redhat.atlassian.net/browse/OPRUN-4723) +**Goal:** Remove C3 (APIService definitions) as a permanent hard block by adding +`apiregistration.k8s.io` support to the OLMv1 registry+v1 bundle renderer. + +The current `ResourceGenerators` list in `internal/operator-controller/rukpak/render/registryv1/registryv1.go` +has **no** generator for `APIService` objects — it generates ServiceAccounts, RBAC, CRDs, +Deployments, Webhooks, and CertProvider, but not `k8s.io/kube-aggregator` APIService +registrations. Until this is fixed, operators that own APIService definitions cannot be +migrated at all (C3 hard block). + +**Scope (in `operator-controller`):** +- Add a `BundleCSVAPIServiceGenerator` to `ResourceGenerators` that reads + `csv.spec.apiservicedefinitions.owned` and emits the corresponding `APIService` objects. +- Update the `BundleValidator` if APIService-specific validation rules are needed. +- Once merged, **remove C3 entirely** from the migration tool (Phase 3 / OPRUN-4719) — OLMv1 + manages APIService objects natively; operators with APIService definitions become Eligible + with no override flag needed. + +**Depends on:** nothing (can start immediately, runs in parallel). **Exit:** registry+v1 +renderer generates `APIService` objects; C3 removed from migration tool (Phase 3 / OPRUN-4719 +updated). + +## Phase 8 — Testing — *(testing stories auto-created per epic)* +**Goal:** Confidence across unit and E2E (R-wide). +- Unit: ≥80% of `migration/pkg/...` with `controller-runtime/pkg/client/fake` — readiness, + compatibility (each ack flag), scan (4 states), catalog parsing, collector (CRD/IfNoController, + namespace rewrite, dedup), rollback/cleanup. +- E2E on kind (OLMv0 + OLMv1): all four states, each acknowledgment override, rollback, cleanup, + catalog migration, and the COS-adoption prerequisite. + +**Depends on:** Phases 1–5 (via Phase 4; Phase 6 tests gated on that phase). **Exit:** unit coverage target met; +E2E scenarios in VALIDATION pass in CI. + +--- + +## Dependency summary + +``` +Prerequisite (OPRUN-4716, operator-controller) ──┐ (parallel; needed before Phase 8) + ▼ +Phase 1 (4717) ──► Phase 2 (4718) ──► Phase 3 (4719) ──► Phase 4 (4720) ──► Phase 8 + │ └─────────────────────────────────► Phase 6 (4721) BLOCKED + └──► Phase 5 (4722, parallel) ──────────────────────────────────────────► Phase 8 + +Phase 7 (4723, operator-controller, parallel) ──► removes C3 from Phase 3 (operators with APIService definitions become Eligible) +``` diff --git a/specs/20260821-migration-v0-to-v1/requirements.md b/specs/20260821-migration-v0-to-v1/requirements.md new file mode 100644 index 0000000..a3d011d --- /dev/null +++ b/specs/20260821-migration-v0-to-v1/requirements.md @@ -0,0 +1,293 @@ +# Requirements — OLMv0 → OLMv1 Migration + +Synthesized from the "OLMv0 to OLMv1 Migration" RFC, +[OCPSTRAT-2693](https://redhat.atlassian.net/browse/OCPSTRAT-2693), and design review. +Requirements are labeled `R1`–`R10`; [VALIDATION.md](VALIDATION.md) traces each to a +verifiable check. + +API groups referenced: +- OLMv0: `operators.coreos.com/v1alpha1` (`Subscription`, `CatalogSource`, `ClusterServiceVersion`, `InstallPlan`), `operators.coreos.com/v1` (`OperatorGroup`, `Operator`, `OperatorCondition`) +- OLMv1: `olm.operatorframework.io/v1` (`ClusterExtension`, `ClusterObjectSet`, `ClusterCatalog`) + +--- + +## R1. Functional requirements + +**R1.1 — Library API.** A Go package exposes, at minimum: +- `ScanAll(ctx)` → all OLMv0 `Subscription`s classified into the four `OperatorStatus` states (R1.3), including a per-operator catalog-availability check. +- `Check(ctx, opts)` → run all readiness & compatibility checks for one operator; no cluster mutations. +- `Gather(ctx, opts)` → collect and return everything that would be migrated; no cluster mutations (backs the CLI `convert --dry-run`). +- `Migrate(ctx, opts)` → perform the full migration (phased, with recovery); backs the CLI `convert`. +- `Rollback(ctx, opts)` → restore an operator to OLMv0 management. +- `Cleanup(ctx, opts)` → finish a partial migration (Conflict state). +- A separate catalog-migration API for `CatalogSource` → `ClusterCatalog`. + +**R1.2 — CLI command surface.** Two binaries. `migrate-catalogs-v0-to-v1` (with +`--dry-run`) migrates catalogs. `migrate-operators-v0-to-v1` follows a kubectl/`oc`-style +verb-plus-target model: every action is a verb subcommand taking either an operator name +**or** `--all`. Because the target is an argument, an operator named `check`/`convert`/etc. +is never ambiguous. + +| Command | Target | Library call | Mutating? | +|---|---|---|---| +| `check ` / `check --all` | Subscription(s) | `Check` / `ScanAll` | no | +| `convert ` / `convert --all` | Subscription(s) | `Migrate` (`Gather` when `--dry-run`) | yes (no when `--dry-run`) | +| `rollback ` / `rollback --all` | ClusterExtension(s) | `Rollback` | yes | +| `cleanup ` / `cleanup --all` | ClusterExtension(s) | `Cleanup` | yes | + +Flags: `-n/--namespace`, `--all`, `--dry-run` (on `convert`), `--backup ` (on +`convert`; writes OLM-related objects to disk before deletions — see R2.6), +`--delete-operatorgroup` (on `convert`; deletes the OperatorGroup when no Subscriptions +remain — both conditions required), `--continue-on-error` (on `convert --all`), +`--acknowledge-installed` (on `rollback`), and the eligibility-override flags (R3): +`--acknowledge-watch-scope-change`, `--acknowledge-operator-condition`, +`--acknowledge-olmv0-api-access`, `--acknowledge-scoped-serviceaccount`, +`--acknowledge-not-steady-state`. `check`/`convert` target a `Subscription` (name + `-n` +namespace); `rollback`/`cleanup` target the resulting `ClusterExtension`. + +For `migrate-catalogs-v0-to-v1`: `--delete-catalogsource` deletes the source `CatalogSource` +after creating the `ClusterCatalog`, but only when no `Subscription` references it — both +conditions required. Default: leave the `CatalogSource` in place. +`--acknowledge-priority-overflow` caps an out-of-range `spec.priority` at `math.MaxInt32` / +`math.MinInt32` and proceeds rather than skipping the CatalogSource. + +**R1.3 — Four-state classification.** Every `Subscription` is `Eligible`, `Ineligible`, +`AlreadyMigrated`, or `Conflict`, each with a specific human-readable reason. + +**R1.4 — `--all` output ordering.** For `check --all` and `convert --all`, sections are +printed in order: **Conflict** (warn prominently; never auto-migrate) → **Ineligible** +(reason per operator) → **AlreadyMigrated** → **Eligible**. `convert --all` then migrates +the Eligible operators sequentially. + +**R1.5 — Batch failure handling.** `convert --all` stops on the first failure by default; +pass `--continue-on-error` to log the failure and continue with the remaining operators. + +**R1.6 — Non-interactive.** No prompts. `convert --dry-run` is the preview mechanism; all +overrides are explicit `--acknowledge-*` flags. + +**R1.7 — Downtime.** Close-to-zero downtime when the install namespace is unchanged (only +the management plane changes; workloads keep running). When a namespace change is required, +downtime is unavoidable but must be minimized. + +**R1.8 — Recovery.** Every mutating phase has a recovery path. Deletions use orphan +cascading to preserve operator workloads. The `Subscription` and `OperatorGroup` specs are +backed up as CE annotations (R2.5) so `rollback` is self-contained. A `--backup ` +flag (R2.6) saves all OLM-related objects to disk for auditing and manual recovery. + +--- + +## R2. Architectural requirements + +**R2.1** — Standalone `v0` Go module (`github.com/operator-framework/library-olm`); +breaking API changes permitted while `v0`. Two binaries. Prow + GitHub Actions CI +(build, test, lint, api-diff). + +**R2.2** — Target OLMv1's `ClusterObjectSet` (COS). The `perdasilva` prototype uses the +old `ClusterExtensionRevision` name throughout; all references must be updated to +`ClusterObjectSet` / `ClusterObjectSetList` / `ClusterObjectSetTypeSucceeded`. + +**R2.3** — The migration tool **creates** the COS, then **waits** for the COS controller +to set `Succeeded=True`, then creates the `ClusterExtension` (which adopts the COS via +`olm.operatorframework.io/owner-kind` + `owner-name` labels). The tool **must not** write +status on any OLMv1 API. No operator-controller changes are required (verified — see +[PLAN.md](PLAN.md) prerequisite). + +**R2.4** — Collected objects are stored via boxcutter's **SecretPacker** (Secret-backed, +not inline in the COS spec) to support large bundles, with +`CollisionProtection: IfNoController` so OLMv1 can adopt pre-existing resources — including +CRDs — without conflict, while still refusing to stomp resources owned by another +controller. + +**R2.5 — Migration annotations.** The `migrated-from-subscription` annotation is set on **both** the `ClusterObjectSet` and the `ClusterExtension`: +- On the **COS**: `olm.operatorframework.io/migrated-from-subscription: /` — ties the revision to its OLMv0 origin; makes migrated COSes discoverable independently of the CE and provides provenance even if the CE annotation is lost. +- On the **CE**: `olm.operatorframework.io/migrated-from-subscription: /` — the key for `AlreadyMigrated`/`Conflict` detection during scan. + +The CE also carries: +- `olm.operatorframework.io/migration-subscription-backup: ` — the original `Subscription` spec (package, channel, source, sourceNamespace, installPlanApproval, startingCSV), so `rollback` is self-contained and machine-independent. The `Subscription` is deleted during migration, so it cannot be used as the backup. +- `olm.operatorframework.io/migration-operatorgroup-backup: ` — the original `OperatorGroup` spec (selector, targetNamespaces, serviceAccountName, upgradeStrategy) from the Subscription's namespace, so `rollback` can also restore the OperatorGroup if needed. +- `olm.operatorframework.io/acknowledged-: "true"` — one per acknowledgment flag used at migration time, for audit. + +**R2.6 — `--backup ` flag.** On `convert`, optionally write all OLM-related objects for the operator to YAML files under the specified directory before any deletions occur. Files written: +- `subscription.yaml` — full `Subscription` object +- `operatorgroup.yaml` — full `OperatorGroup` object from the Subscription's namespace +- `clusterserviceversion.yaml` — the installed `ClusterServiceVersion` +- `installplans/` — one YAML per `InstallPlan` associated with the installed CSV + +The directory is created if it does not exist. Backup does not gate migration — it is informational and aids manual recovery if the CE annotation backup is insufficient. If the directory write fails, `convert` warns and continues (the CE annotation backup is the authoritative recovery path). + +**R2.7 — Boxcutter phase 2.** Upcoming boxcutter changes may introduce a +`ClusterObjectDeployment` resource. The implementation must track this and be prepared to +adapt which OLMv1 objects it creates. + +--- + +## R3. Eligibility & compatibility rules + +Blocks are **soft** (overridable by an explicit `--acknowledge-*` flag that records a CE +annotation, R2.5) or **hard** (must be remediated first — no override). + +| # | Check | Ineligible when… | Override flag | +|---|---|---|---| +| C1 | AllNamespaces watch scope | OperatorGroup targets specific namespaces (Own/Single/Multi) | `--acknowledge-watch-scope-change` | +| C2 | No dependency resolution *(hard)* | CSV declares `olm.package.required` or `olm.gvk.required` | none — OLMv1 fundamentally does not resolve dependencies; migrating without them would leave the operator broken | +| C3 | No APIService definitions *(hard, temporary)* | CSV `spec.apiservicedefinitions.owned` is non-empty | none — OLMv1's registry+v1 renderer currently has **no** `apiregistration.k8s.io` generator; when [OPRUN-4723](https://redhat.atlassian.net/browse/OPRUN-4723) merges, OLMv1 will manage APIService objects natively and **C3 is removed entirely** (no override flag; operators with APIService definitions become Eligible) | +| C4 | No active OperatorCondition | `OperatorCondition.status.conditions` has entries (see R9) | `--acknowledge-operator-condition` | +| C5 | OLMv0-API RBAC without OLMv1 RBAC | The installed RBAC (from live cluster, sourced from bundle manifests or CSV) grants access to `operators.coreos.com` resources (`subscriptions`/`installplans`/`clusterserviceversions`/`catalogsources`, **excluding** `operatorconditions`) **and** does not also grant equivalent OLMv1 API access — operators updated for OLMv1 compatibility will carry both and pass | `--acknowledge-olmv0-api-access` | +| C6 | No scoped ServiceAccount | OperatorGroup `spec.serviceAccountName` is set | `--acknowledge-scoped-serviceaccount` | +| C7 | Catalog availability *(hard)* | Package not served by any `ClusterCatalog` | none — run `migrate-catalogs-v0-to-v1` first | +| C8 | Steady state | CSV not `Succeeded`, or Subscription state not `AtLatestKnown`/`UpgradePending` | `--acknowledge-not-steady-state` | +| C9 | Not an OLMv0-managed dependency *(hard)* | Subscription carries the `olm.generated-by` annotation — the operator was auto-installed as a dependency of another operator | none — operators that are OLMv0-managed dependencies must not be individually migrated; they are part of a dependency graph that OLMv0 owns and OLMv0 would attempt to reinstall them if their Subscription is removed | + +--- + +## R4. Subscription field handling (`operators.coreos.com/v1alpha1`) + +The on-wire JSON keys differ from the Go field names — a tool reading raw objects must key +off the JSON names shown. `spec` is a pointer and required. + +| Field (JSON) | Go name | Handling | +|---|---|---| +| `spec.source` | `CatalogSource` | Locate the `CatalogSource` (with `sourceNamespace`) to obtain its image; used to resolve the target `ClusterCatalog`. Not copied to the CE directly. | +| `spec.sourceNamespace` | `CatalogSourceNamespace` | Namespace of the `CatalogSource`; used with `source`. | +| `spec.name` | `Package` | → `CE.spec.source.catalog.packageName`. Also forms the `migrated-from-subscription` annotation and the `Operator` CR name `.`. | +| `spec.channel` | `Channel` | When set: → `CE.spec.source.catalog.channels` (single-element list). When **empty**: OLMv0 resolves via the catalog's `defaultChannel` — a concept OLMv1 does not carry forward. OLMv1 with no `channels` considers upgrade edges across *all* channels, which may differ from OLMv0's default. Mitigation: query the resolved `ClusterCatalog` for the package's declared default channel and set it explicitly on the CE. Warn the admin if the default channel cannot be determined. | +| `spec.startingCSV` | `StartingCSV` | Not carried to the CE. Preserved in the backup; on `rollback`, reset to `status.installedCSV`. | +| `spec.installPlanApproval` | `InstallPlanApproval` | `Manual` → pin `CE.spec.source.catalog.version` to the installed version (preserve manual upgrade control). `Automatic` (or empty, which defaults to `Automatic`) → leave version unset for channel-based auto-upgrade. | +| `spec.config` | `Config` (`SubscriptionConfig`) | **Maps directly** to `CE.spec.config.inline.deploymentConfig`. OLMv1's `DeploymentConfig` is a Go **type alias** of `SubscriptionConfig` (`internal/operator-controller/config/config.go`), and the registry+v1 renderer applies it to the operator Deployment on **every** render — installs *and* upgrades — so overrides persist. Sub-fields map 1:1 — `env`, `envFrom`, `volumes`, `volumeMounts`, `tolerations`, `resources`, `nodeSelector`, `affinity`, `annotations` — **except `selector`**, which OLMv1 omits (never honored in v0; drop it, harmless). | +| `status.installedCSV` | — | **Primary input.** The migration operates on the CSV actually installed. | +| `status.currentCSV` | — | May differ from `installedCSV` during `UpgradePending` (manual approval). Acceptable; ignore it and operate on `installedCSV`. | +| `status.state` | — | Readiness gate (C8): must be `AtLatestKnown` or `UpgradePending`. | +| `status.installPlanRef` | — | Used as a **supplementary** source for resource collection (see R5). Not the primary source; see note below. `status.install` is the deprecated equivalent — fall back if the ref is absent. | +| `status.installPlanGeneration`, `status.catalogHealth`, `status.conditions`, `status.reason`, `status.lastUpdated` | — | Controller-managed; read-only signals, not migrated as user intent. | +| annotation `olm.generated-by` | — | If present, the Subscription was auto-generated by OLMv0 to satisfy another operator's dependency → C9 hard block; do not migrate. | + +### R5. Resource collection strategy + +The migration tool must collect all resources that belong to the operator's installation so +they can be placed into the `ClusterObjectSet` for OLMv1 to manage. No single OLMv0 +mechanism is complete; the strategy combines multiple sources and deduplicates by +GVK+namespace+name. + +**Primary source — `Operator` CR `status.components.refs`:** The `Operator` CR +(`operators.coreos.com/v1`, named `.`) is maintained by OLMv0 via +label-based selection on `operators.coreos.com/.`. It represents the +**live cluster state** of everything OLMv0 currently associates with the operator, including +resources created or managed dynamically by the CSV controller after initial install. This is +more comprehensive than the InstallPlan (which is an install-time plan only) and should be +treated as the primary source of truth. + +**Supplementary sources** (add anything not already in the Operator CR refs): +1. **`olm.owner` label query** — list all resources of the eligible kinds matching + `olm.owner=` across the cluster; catches resources the Operator CR may have + missed if the label propagation lagged. +2. **OwnerReference query** — in the Subscription namespace, list namespace-scoped resources + with an ownerReference pointing to the CSV; catches resources (e.g., ServiceAccounts) that + may lack the `olm.owner` label. +3. **InstallPlan steps** — parse the InstallPlan's `status.plan[]` steps where `resolving` + matches the CSV name, and fetch each live object. The InstallPlan does **not** cover + resources managed by the CSV controller after install, so it is a fallback supplement + rather than a primary source. + +**Excluded from collection:** `ClusterServiceVersion`, `Subscription`, `InstallPlan`, +`Operator`, `OperatorGroup`, `OperatorCondition` — these are OLMv0 management resources +cleaned up separately. + +--- + +## R6. OperatorGroup field handling (`operators.coreos.com/v1`) + +| Field | Handling | +|---|---| +| `spec.targetNamespaces` | If set → not AllNamespaces (Single/Multi) → C1 watch-scope block. When set, `selector` is ignored. | +| `spec.selector` | If set/non-empty → selector-based targeting → C1 watch-scope block. Empty/nil `selector` **and** empty `targetNamespaces` ⇒ AllNamespaces (eligible). | +| `spec.serviceAccountName` | Scoped install SA → C6. OLMv1 runs via operator-controller's cluster-admin SA; a scoped SA cannot be represented. Override `--acknowledge-scoped-serviceaccount` (operator will run with OLMv1's privileges). There is **no** CE target field for this — CE `spec.serviceAccount` is deprecated and ignored (see R7). | +| `spec.upgradeStrategy` | `Default` → fine. `TechPreviewUnsafeFailForward` → not mapped and not equivalent to `SelfCertified`; informational warning only, ignored. | +| `spec.staticProvidedAPIs` | No OLMv1 equivalent (OLMv1 does not use the `olm.providedAPIs` annotation). Ignored (note only). | +| `status.namespaces` | Read to compute the effective install mode (AllNamespaces vs Own/Single). | +| `status.serviceAccountRef`, `status.conditions`, `status.lastUpdated` | Controller-managed; not migrated. | +| Cleanup | OperatorGroup deletion requires **both** `--delete-operatorgroup` **and** no other `Subscription`s remaining in the namespace. If either condition is not met, the OperatorGroup is left in place. When deleted: first strip `olm.owner` / `olm.owner.namespace` / `olm.owner.kind` / `olm.managed` labels from the OperatorGroup aggregation ClusterRoles (`olm.og..-`) so RBAC is retained without OLMv0 ownership. | + +--- + +## R7. ClusterExtension result — how each CE spec field is populated + +The migration produces one `ClusterExtension` (`olm.operatorframework.io/v1`) from the +Subscription + OperatorGroup inputs above. + +| CE field | Source | Notes | +|---|---|---| +| `metadata.name` | Subscription name (default; `--ce-name` override) | | +| `metadata.annotations` | migration metadata | See R2.5 (`migrated-from-subscription`, `migration-subscription-backup`, `acknowledged-*`). | +| `spec.namespace` | Subscription namespace (default) or `--install-namespace` | Required, immutable today. **Phase 6 / [PR #2825](https://github.com/operator-framework/operator-controller/pull/2825):** may become optional/omitted → OLMv1 resolves it from bundle metadata. | +| `spec.serviceAccount` | **do not set** | Deprecated and **ignored** in current OLMv1 (operator-controller uses its own cluster-admin SA). The RFC/prototype `-installer` SA concept is obsolete — do not create or set it. | +| `spec.source.sourceType` | constant `Catalog` | Only implemented source type. | +| `spec.source.catalog.packageName` | Subscription `spec.name` | Required, immutable. | +| `spec.source.catalog.channels` | Subscription `spec.channel` | Single-element list if set. If empty, resolve the package's `defaultChannel` from the `ClusterCatalog` content and set it explicitly (see R4 channel row). | +| `spec.source.catalog.version` | installed CSV version — only if `installPlanApproval == Manual` | Pin for manual control; unset for Automatic. | +| `spec.source.catalog.selector` | resolved `ClusterCatalog` | `matchLabels: {olm.operatorframework.io/metadata.name: }` — pins to the catalog resolved from the CatalogSource image (ties to R8). | +| `spec.source.catalog.upgradeConstraintPolicy` | always `CatalogProvided` | Hardcoded to `CatalogProvided` (or left unset to pick up the OLMv1 default). OperatorGroup `TechPreviewUnsafeFailForward` is not mapped — it is a different concept from `SelfCertified` and has no equivalent. | +| `spec.install.preflight.crdUpgradeSafety` | not mapped | No OLMv0 equivalent; leave default (`Strict`). | +| `spec.config.inline.deploymentConfig` | Subscription `spec.config` (`SubscriptionConfig`) | 1:1 — `DeploymentConfig` is a type alias of `SubscriptionConfig`; all sub-fields except `selector` (R4). Applied to the operator Deployment on every render. Requires the `NewOLMConfigAPI` feature on the target cluster. | + +--- + +## R8. CatalogSource → ClusterCatalog mapping (`migrate-catalogs-v0-to-v1`) + +For each eligible `CatalogSource` (`operators.coreos.com/v1alpha1`), the tool first checks +whether a suitable `ClusterCatalog` already exists by matching on `spec.source.image.ref`. +This handles the common case where Red Hat's default catalogs (e.g. `redhat-operators`, +`certified-operators`, `community-operators`) are already present as ClusterCatalogs. + +**ClusterCatalog resolution order for a given CatalogSource:** +1. **Existing ClusterCatalog, image matches** (by name or by image scan) → adopt it; report as already covered. +2. **No match** → create a new ClusterCatalog (per the naming strategy below) and wait for `Serving=True`. + +**Migration annotation:** When a ClusterCatalog is first created or first adopted by this +tool, set `olm.operatorframework.io/migrated-from-catalogsource: /` where +`/` is the CatalogSource being processed. The annotation is written **once +and never overwritten** — if it is already present (e.g. because a different CatalogSource +from another namespace already triggered adoption), leave it unchanged. This ensures the +annotation is idempotent even when multiple CatalogSources in different namespaces map to +the same ClusterCatalog. + +The `CatalogSource` is **left in place** by default — it is deleted only when +`--delete-catalogsource` is passed **and** no remaining `Subscription` references it. +Both conditions must be met. + +| CatalogSource field | ClusterCatalog target | Notes | +|---|---|---| +| `spec.sourceType` | `spec.source.type` | Only `grpc` **with** `spec.image` maps to OLMv1 `Image`. `configmap` / `internal` / address-only sources have **no** OLMv1 equivalent → report as **not migratable** and skip. | +| `spec.image` | `spec.source.image.ref` | Required for `Image` type. | +| `spec.updateStrategy.registryPoll.interval` | `spec.source.image.pollIntervalMinutes` | Duration string (default 15m) → integer minutes. **Forbidden with digest-based refs** → drop the poll if the ref is a digest. | +| `spec.priority` (`int`) | `spec.priority` (`int32`) | Validate that the value fits in `int32` range (−2,147,483,648 to 2,147,483,647) before casting. If it does not fit: report a warning and mark the CatalogSource as not migratable by default. Pass `--acknowledge-priority-overflow` to proceed anyway, capping the value at `math.MaxInt32` (positive overflow) or `math.MinInt32` (negative overflow). In practice OLMv0 documentation describes the range as positive-to-negative int32, but Go's `int` is 64-bit on 64-bit platforms so out-of-range values are theoretically possible. | +| `spec.secrets` | — | No equivalent (OLMv1 uses the cluster global pull secret). Note if present. | +| `spec.grpcPodConfig.*` | — | No equivalent (catalogd manages the serving pod). Note if present. | +| `spec.displayName` / `description` / `publisher` / `icon` | — | Metadata; dropped. | +| `spec.configMap`, `spec.address` | — | Non-image sources; not migratable (see `sourceType`). | +| *(n/a)* | `spec.availabilityMode` | OLMv1-only; default `Available`. | +| `metadata.name` | `metadata.name` (with deduplication — see below) | CatalogSource is namespace-scoped; ClusterCatalog is cluster-scoped. Naming strategy: (1) **Same name, same image across namespaces** → consolidate into a single ClusterCatalog using that name (they serve identical content; all Subscriptions referencing any of those CatalogSources will resolve to the same ClusterCatalog). (2) **Same name, different image across namespaces** → cannot consolidate; use `-` for each conflicting CatalogSource to avoid collision. (3) **Unique name** → use `metadata.name` directly. The tool must scan all CatalogSources across all namespaces before creating any ClusterCatalogs to determine which strategy applies to each name. The resulting ClusterCatalog name becomes the `olm.operatorframework.io/metadata.name` value the CE selector (R7) pins to. | + +--- + +## R9. Edge cases + +- **Multiple operators in one namespace** → OperatorGroup deletion requires `--delete-operatorgroup` AND no Subscriptions remaining; in a multi-operator namespace the second condition won't be met until all operators are migrated (R6). +- **Multiple operators sharing a cluster resource** (e.g. the same CRD across Own/Single installs) → `CollisionProtection: IfNoController` allows adoption without conflict. +- **Operator not at steady state** → C8 (soft; overridable with `--acknowledge-not-steady-state`). +- **Dependency relationships** → an operator that depends on others is blocked by C2; migrating an operator that *others depend on* proceeds but must warn about dependents. +- **OperatorCondition detection** → OLMv0 stamps OperatorCondition RBAC onto **every** operator's service account, so RBAC is **not** a usage signal. Usage is detected **only** via `OperatorCondition.status.conditions` (C4); C5 explicitly excludes `operatorconditions` from the OLMv0-API RBAC check. +- **Certificate handling** → OLMv0 manages TLS certs directly; OLMv1 delegates to cert-manager (upstream) / service-ca (downstream). Expect pod restarts across the pivot; document as known behavior. +- **Large bundles** → SecretPacker (R2.4) avoids Kubernetes object-size limits. +- **Namespace change** → copy PSA labels (`pod-security.kubernetes.io/*`) and the OpenShift SCC sync label (`security.openshift.io/scc.podSecurityLabelSync`) from the old namespace to the new one. Delete the old namespace **only** with `--acknowledge-namespace-delete` (it may contain non-operator resources). +- **Disconnected / mirrored** → catalogs must be migrated first; the operator tool never auto-creates catalogs. + +--- + +## R10. Non-goals / out of scope + +- Dependency resolution (operators declaring `olm.package.required` / `olm.gvk.required`). +- Operators relying on scoped service accounts (without explicit acknowledgment). +- OwnNamespace / SingleNamespace as a *permanent* mode (migration converts to AllNamespaces). +- Hosted Control Planes — OLMv1 is not supported on HCP yet; the design must not foreclose it (e.g. don't hardcode a single kubeconfig). +- Binary packaging / `oc` plugin / container images — deferred to the downstream TP phase ([OCPSTRAT-2692](https://redhat.atlassian.net/browse/OCPSTRAT-2692)). +- Console UI. diff --git a/specs/20260821-migration-v0-to-v1/validation.md b/specs/20260821-migration-v0-to-v1/validation.md new file mode 100644 index 0000000..ea80bbd --- /dev/null +++ b/specs/20260821-migration-v0-to-v1/validation.md @@ -0,0 +1,125 @@ +# Validation — OLMv0 → OLMv1 Migration + +Verifiable acceptance criteria drawn from the RFC, +[OCPSTRAT-2693](https://redhat.atlassian.net/browse/OCPSTRAT-2693), and +[requirements.md](requirements.md). Each item is phrased so it can be asserted in a unit or +E2E test. IDs (`V*`) are referenced by the traceability table at the end. + +Unit tests use `sigs.k8s.io/controller-runtime/pkg/client/fake`; E2E runs on a kind cluster +with both OLMv0 and OLMv1 installed. + +## V1. Per-command behavior + +- **V1.1** `check -n ` on a healthy AllNamespaces operator with an available catalog reports all checks green and exits 0; makes no cluster changes. +- **V1.2** `convert -n --dry-run` lists every resource that would be migrated (grouped by kind), including CRDs, and reports them as COS objects with `CollisionProtection: IfNoController`; makes no cluster changes. +- **V1.3** `convert -n ` results in a `ClusterExtension` reaching `Installed=True`; the `Subscription` and `CSV` are deleted; CRDs remain and are adopted by OLMv1; the COS reached `Succeeded=True` **before** the CE was created. +- **V1.4** `rollback --acknowledge-installed` deletes the CE and COS (orphan cascade), recreates the `Subscription` from the backup annotation, and the operator returns to OLMv0 management (`AtLatestKnown`/`UpgradePending`). Without `--acknowledge-installed` on an `Installed=True` CE, rollback refuses and exits non-zero. +- **V1.5** `cleanup ` on a Conflict state deletes the `Subscription` and OLMv0 artifacts (Operator CR, OperatorCondition, copied CSVs, OperatorGroup if last) and leaves the CE intact. +- **V1.6** `check --all` and `convert --all` print sections in order Conflict → Ineligible → AlreadyMigrated → Eligible; Conflicts are never auto-migrated; `convert --all` migrates only the Eligible operators. +- **V1.7** `convert --all` stops on the first failure by default; with `--continue-on-error` it logs the failure and continues, exiting non-zero if any operator failed. +- **V1.8** No command ever blocks on interactive input. An operator whose name collides with a verb (e.g. `check`) is migratable as a positional argument (`convert check -n `). + +## V2. Eligibility matrix (one case per check, R3) + +For each, a fixture operator produces the expected state + reason, and setting the override +flag flips it to `Eligible`: + +- **V2.1 (C1)** OperatorGroup with `targetNamespaces` → Ineligible "watch scope"; `--acknowledge-watch-scope-change` → Eligible (migrates to AllNamespaces). +- **V2.2 (C2, hard)** CSV with `olm.package.required` → Ineligible "dependencies"; no override. +- **V2.3 (C2, hard)** CSV with `olm.gvk.required` → Ineligible "dependencies"; no override. +- **V2.4 (C3, hard, temporary)** CSV with owned APIServices → Ineligible "apiservices"; no override. Removed entirely when OPRUN-4723 merges. +- **V2.5 (C4)** OperatorCondition with `status.conditions` entries → Ineligible "operator-condition"; `--acknowledge-operator-condition` → Eligible. +- **V2.6 (C5)** CSV `.clusterPermissions` granting `operators.coreos.com/subscriptions` → Ineligible "olmv0-api-access"; `--acknowledge-olmv0-api-access` → Eligible. +- **V2.7 (C6)** OperatorGroup with `serviceAccountName` → Ineligible "scoped serviceaccount"; `--acknowledge-scoped-serviceaccount` → Eligible. +- **V2.8 (C7, hard)** Package absent from all ClusterCatalogs → Ineligible "package not found; run migrate-catalogs-v0-to-v1 first"; no override. +- **V2.9 (C8)** CSV not `Succeeded` or Subscription not at `AtLatestKnown`/`UpgradePending` → Ineligible "not at steady state"; `--acknowledge-not-steady-state` → Eligible. +- **V2.10 (C9, hard)** Subscription carries `olm.generated-by` → Ineligible "OLMv0-managed dependency"; no override. + +## V3. Field-mapping assertions (R4/R6/R7/R8) + +- **V3.1** Manual approval → `CE.spec.source.catalog.version` pinned to the installed version. +- **V3.2** Automatic approval → CE version unset (channel-based upgrades allowed). +- **V3.3** Subscription `spec.channel` → single-element `CE.spec.source.catalog.channels`; empty channel → omitted. +- **V3.4** `CE.spec.source.catalog.selector` pins to the resolved catalog via `olm.operatorframework.io/metadata.name`. +- **V3.5** `CE.spec.serviceAccount` is never set (deprecated/ignored), even when the OperatorGroup had a `serviceAccountName`. +- **V3.6** CE carries `migrated-from-subscription`, `migration-subscription-backup`, `migration-operatorgroup-backup`, and one `acknowledged-` annotation per flag used. +- **V3.7** OperatorGroup deleted only when both `--delete-operatorgroup` is passed AND no other Subscriptions remain in the namespace; left in place otherwise. +- **V3.8** CatalogSource `spec.image` → `ClusterCatalog.spec.source.image.ref`; ClusterCatalog `metadata.name` follows the deduplication strategy (V3.13/V3.14). +- **V3.9** CatalogSource `registryPoll.interval` → `pollIntervalMinutes` (integer minutes); dropped when the image ref is a digest. +- **V3.10** CatalogSource `priority` carried to ClusterCatalog `priority`. +- **V3.11** A `configmap`/`internal`/address-only CatalogSource is reported not-migratable and skipped. +- **V3.12** Subscription `spec.config` maps to `CE.spec.config.inline.deploymentConfig` with all sub-fields except `selector`; the operator Deployment reflects env/envFrom/resources/tolerations/nodeSelector/affinity/volumes/volumeMounts/annotations, and still does after an OLMv1-driven upgrade. A `spec.config.selector`, if present, is dropped (with a warning). +- **V3.13** Two CatalogSources in different namespaces share a name **and** the same image → `migrate-catalogs-v0-to-v1` creates a single `ClusterCatalog` using that name; both Subscriptions' CE selectors resolve to the same ClusterCatalog. +- **V3.14** Two CatalogSources in different namespaces share a name **but** have different images → `migrate-catalogs-v0-to-v1` creates two ClusterCatalogs named `-` and `-`; each Subscription's CE selector resolves to the correct ClusterCatalog by image match. +- **V3.15** CatalogSource `spec.priority` value outside `int32` range → reported as not migratable and skipped by default; with `--acknowledge-priority-overflow` the value is capped at `math.MaxInt32` / `math.MinInt32` and migration proceeds. +- **V3.16** A pre-existing `ClusterCatalog` whose `spec.source.image.ref` matches the CatalogSource image is adopted (not duplicated); `migrate-catalogs-v0-to-v1` reports it as already covered and sets `olm.operatorframework.io/migrated-from-catalogsource: /` on the ClusterCatalog if the annotation is not already present. Running `migrate-catalogs-v0-to-v1` against the Red Hat default catalogs (which already exist as ClusterCatalogs) produces no new ClusterCatalogs and leaves the existing annotations unchanged. +- **V3.17** Two CatalogSources from different namespaces both map to the same ClusterCatalog (same image) → the `migrated-from-catalogsource` annotation is set exactly once by whichever CatalogSource is processed first; subsequent processing of the other CatalogSource leaves the annotation unchanged. +- **V3.18 (R2.6)** `convert -n --backup ` writes four files before any deletions: `subscription.yaml`, `operatorgroup.yaml`, `clusterserviceversion.yaml`, and `installplans/` (one YAML per InstallPlan); directory is created if it does not exist; migration proceeds if directory write fails (backup is informational only). +- **V3.19 (R1.2)** `migrate-catalogs-v0-to-v1 --delete-catalogsource` deletes the CatalogSource only when both conditions are met: the flag is passed **and** no Subscription references it; when one or both conditions are not met, the CatalogSource is left in place. + +## V4. Edge-case tests (R9) + +- **V4.1** Two operators in one namespace: migrating one leaves the OperatorGroup intact. +- **V4.2** Two operators sharing a CRD: `IfNoController` lets the second adopt without a collision error. +- **V4.3** Operator not at steady state → Ineligible (C8) with a clear reason. +- **V4.4** Dependency operator (`olm.generated-by` present / declares requirements) → flagged; an operator others depend on migrates but emits a dependents warning. +- **V4.5** OperatorCondition disambiguation: an operator with OLMv0-stamped OperatorCondition RBAC but **empty** `status.conditions` is **Eligible** (RBAC is not treated as usage). +- **V4.6** Large bundle exceeding inline size limits migrates successfully via SecretPacker. +- **V4.7** Namespace change copies `pod-security.kubernetes.io/*` and `security.openshift.io/scc.podSecurityLabelSync` to the new namespace; old namespace deleted only with `--acknowledge-namespace-delete`. + +## V5. End-to-end scenario (kind) + +- **V5.1** Bootstrap kind with OLMv0 + OLMv1 side-by-side. +- **V5.2** Install an AllNamespaces operator via an OLMv0 Subscription; confirm healthy. +- **V5.3** `migrate-catalogs-v0-to-v1` → CatalogSource becomes a serving ClusterCatalog. +- **V5.4** `check -n ` → all green (catalog now found). +- **V5.5** `convert -n --dry-run` → lists resources incl. CRDs (`IfNoController`). +- **V5.6** `convert -n ` → CE `Installed=True`; Subscription/CSV deleted; CRDs adopted (demonstrates close-to-zero downtime with namespace unchanged). +- **V5.7** Upgrade via OLMv1 → CRDs updated through the normal bundle lifecycle. +- **V5.8** `rollback --acknowledge-installed` → Subscription restored, CE deleted. +- **V5.9** Fresh cluster with operators in all four states → `check --all` → correct four-section output; `convert --all` warns on Conflicts and migrates only Eligible. + +## V6. Non-functional (Jira deployment considerations) + +- **V6.1** Works on all node topologies: SNO, compact (3-node), and multi-node. +- **V6.2** No architecture-specific behavior (x86_64, aarch64, ppc64le, s390x). +- **V6.3** Works connected and in restricted networks (catalog resolution by package name against pre-mirrored catalogs). +- **V6.4** Only AllNamespaces install mode is produced (Own/Single converted with acknowledgment). +- **V6.5** Self-managed, classic (standalone) clusters. HCP explicitly not covered (R10). + +## V7. Deliverable checks + +- **V7.1** Repo at `/home/tshort/git/operator-framework/library-olm` with a pushed public personal remote; `specs/20260821-migration-v0-to-v1/` holds README, requirements, plan, validation. +- **V7.2** `requirements.md` covers every Subscription (R4), OperatorGroup (R6), and CatalogSource (R8) spec field, plus the full ClusterExtension target mapping (R7). +- **V7.3** `plan.md` has 8 phases + the cross-repo prerequisite, each with an exit criterion. +- **V7.4** This traceability table has no requirement without at least one validation item. + +--- + +## Traceability + +| Requirement | Validated by | +|---|---| +| R1.1 Library API | V1.1–V1.7 (via library calls), V3.*, V4.* | +| R1.2 Two CLIs | V1.*, V3.8–V3.11, V3.13–V3.19, V5 | +| R1.3 Four-state classification | V1.6, V2.1–V2.10, V5 | +| R1.4 `--all` ordering | V1.6 | +| R1.5 Batch failure / `--continue-on-error` | V1.7 | +| R1.6 Non-interactive | V1.8 | +| R1.7 Downtime posture | V5.6 (namespace unchanged), V4.7 (namespace change) | +| R1.8 Recovery | V1.4, V1.5 | +| R2.1 v0 module + CI | V7.1, V7.3 | +| R2.2 ClusterObjectSet rename | V1.3 (COS created), V4.6 | +| R2.3 Wait for COS Succeeded; no status writes | V1.3 | +| R2.4 SecretPacker + IfNoController | V1.2, V4.2, V4.6 | +| R2.5 CE annotations | V3.6 | +| R2.6 `--backup ` flag | V3.18 | +| R2.7 Boxcutter phase 2 | Prerequisite note (PLAN) | +| R3 C1–C9 | V2.1–V2.10 | +| R4 Subscription fields | V3.1–V3.3, V3.12, V4.4 | +| R5 Resource collection strategy | V1.3, V4.2, V4.6 | +| R6 OperatorGroup fields | V2.1, V2.7, V3.5, V3.7 | +| R7 ClusterExtension mapping | V3.1–V3.6, V3.12 | +| R8 CatalogSource→ClusterCatalog | V3.8–V3.11, V3.13–V3.17, V3.19 | +| R9 Edge cases | V4.1–V4.7 | +| R10 Non-goals | V6.4, V6.5 |