Skip to content

Commit 24c4dfa

Browse files
author
Per G. da Silva
committed
Add OLMv0-to-OLMv1 migration CLI and library
Add a CLI tool (hack/tools/migrate/) and supporting migration library (internal/operator-controller/migration/) to migrate operators managed by OLMv0 (Subscription/CSV) to OLMv1 (ClusterExtension/ClusterExtensionRevision). CLI subcommands: - migrate (root): Full interactive migration of a single operator - migrate all: Scan cluster, check eligibility, and migrate all eligible operators - migrate check: Run readiness/compatibility checks without modifying resources - migrate gather: Collect and display migration info (dry-run) Migration library capabilities: - Operator profiling: read Subscription, CSV, InstallPlan state - Readiness checks: Subscription state, CSV health, uniqueness, dependencies - Compatibility checks: AllNamespaces mode, dependencies, APIServices, OperatorConditions - Resource collection: 4 strategies (CRD labels, owner labels, ownerRefs, InstallPlan steps) - ClusterCatalog resolution: match OLMv0 CatalogSource to OLMv1 ClusterCatalog - Zero-downtime migration with backup/recovery at each phase - Server-side apply with field manager olm.operatorframework.io/migration Signed-off-by: Per G. da Silva <pegoncal@redhat.com>
1 parent 09d32fb commit 24c4dfa

17 files changed

Lines changed: 3705 additions & 0 deletions

File tree

hack/tools/migrate/all.go

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"errors"
7+
"fmt"
8+
"os"
9+
"strings"
10+
11+
"github.com/spf13/cobra"
12+
"k8s.io/client-go/rest"
13+
14+
"github.com/operator-framework/operator-controller/internal/operator-controller/migration"
15+
)
16+
17+
var allCmd = &cobra.Command{
18+
Use: "all",
19+
Short: "Discover and migrate all eligible OLMv0 operators to OLMv1",
20+
Long: `Scans the cluster for all OLMv0 Subscriptions, checks each for migration
21+
eligibility, presents a summary, and migrates the eligible operators one by one.
22+
23+
Examples:
24+
# Interactive — review and approve each operator
25+
migrate all
26+
27+
# Non-interactive — migrate all eligible operators
28+
migrate all -y`,
29+
RunE: runAll,
30+
}
31+
32+
func runAll(cmd *cobra.Command, _ []string) error {
33+
c, restCfg, err := newClient()
34+
if err != nil {
35+
return err
36+
}
37+
38+
ctx := cmd.Context()
39+
m := migration.NewMigrator(c, restCfg)
40+
m.Progress = progressFunc
41+
42+
// Phase 1: Scan
43+
fmt.Printf("\n%s%s🔎 Scanning cluster for OLMv0 Subscriptions...%s\n", colorBold, colorCyan, colorReset)
44+
startProgress()
45+
results, err := m.ScanAllSubscriptions(ctx)
46+
clearProgress()
47+
if err != nil {
48+
fail(fmt.Sprintf("Failed to scan Subscriptions: %v", err))
49+
return err
50+
}
51+
52+
if len(results) == 0 {
53+
info("No Subscriptions found on the cluster.")
54+
return nil
55+
}
56+
57+
// Phase 2: Display results
58+
var eligible, ineligible []migration.OperatorScanResult
59+
for _, r := range results {
60+
if r.Eligible {
61+
eligible = append(eligible, r)
62+
} else {
63+
ineligible = append(ineligible, r)
64+
}
65+
}
66+
67+
sectionHeader(fmt.Sprintf("Scan Results (%d Subscriptions found)", len(results)))
68+
69+
if len(eligible) > 0 {
70+
fmt.Printf("\n %s%sEligible for migration (%d):%s\n", colorBold, colorGreen, len(eligible), colorReset)
71+
for i, r := range eligible {
72+
fmt.Printf(" %s%d)%s %s%s%s/%s%s (package: %s, version: %s)\n",
73+
colorGreen, i+1, colorReset,
74+
colorBold, r.SubscriptionNamespace, colorReset,
75+
r.SubscriptionName, colorReset,
76+
r.PackageName, r.Version)
77+
}
78+
}
79+
80+
if len(ineligible) > 0 {
81+
fmt.Printf("\n %s%sNot eligible (%d):%s\n", colorBold, colorRed, len(ineligible), colorReset)
82+
for _, r := range ineligible {
83+
reason := summarizeIneligibility(r)
84+
fmt.Printf(" %s✗%s %s/%s (package: %s) — %s%s%s\n",
85+
colorRed, colorReset,
86+
r.SubscriptionNamespace, r.SubscriptionName,
87+
r.PackageName,
88+
colorDim, reason, colorReset)
89+
}
90+
}
91+
92+
if len(eligible) == 0 {
93+
fmt.Println()
94+
warn("No operators are eligible for migration.")
95+
return nil
96+
}
97+
98+
// Phase 3: Confirmation
99+
if !autoApprove {
100+
fmt.Printf("\n%s🔄 Migrate %d eligible operator(s) to OLMv1? [y/N]: %s", colorYellow, len(eligible), colorReset)
101+
reader := bufio.NewReader(os.Stdin)
102+
answer, _ := reader.ReadString('\n')
103+
answer = strings.TrimSpace(strings.ToLower(answer))
104+
if answer != "y" && answer != "yes" {
105+
warn("Migration cancelled by user")
106+
return nil
107+
}
108+
}
109+
110+
// Phase 4: Migrate each operator
111+
var succeeded, failed int
112+
for i, r := range eligible {
113+
fmt.Printf("\n%s%s════════════════════════════════════════════════════════════%s\n",
114+
colorBold, colorCyan, colorReset)
115+
fmt.Printf("%s%s [%d/%d] Migrating %s/%s (%s@%s)%s\n",
116+
colorBold, colorCyan, i+1, len(eligible),
117+
r.SubscriptionNamespace, r.SubscriptionName,
118+
r.PackageName, r.Version, colorReset)
119+
fmt.Printf("%s%s════════════════════════════════════════════════════════════%s\n",
120+
colorBold, colorCyan, colorReset)
121+
122+
if err := migrateSingle(ctx, m, r, restCfg); err != nil {
123+
fail(fmt.Sprintf("Migration failed: %v", err))
124+
failed++
125+
126+
if !autoApprove && i < len(eligible)-1 {
127+
fmt.Printf("\n %sContinue with remaining operators? [y/N]: %s", colorYellow, colorReset)
128+
reader := bufio.NewReader(os.Stdin)
129+
answer, _ := reader.ReadString('\n')
130+
answer = strings.TrimSpace(strings.ToLower(answer))
131+
if answer != "y" && answer != "yes" {
132+
warn("Remaining migrations cancelled")
133+
break
134+
}
135+
}
136+
} else {
137+
succeeded++
138+
}
139+
}
140+
141+
// Phase 5: Summary
142+
fmt.Printf("\n%s%s════════════════════════════════════════════════════════════%s\n",
143+
colorBold, colorCyan, colorReset)
144+
sectionHeader("Migration Summary")
145+
if succeeded > 0 {
146+
success(fmt.Sprintf("%d operator(s) migrated successfully", succeeded))
147+
}
148+
if failed > 0 {
149+
fail(fmt.Sprintf("%d operator(s) failed to migrate", failed))
150+
}
151+
if len(ineligible) > 0 {
152+
info(fmt.Sprintf("%d operator(s) were not eligible", len(ineligible)))
153+
}
154+
fmt.Println()
155+
156+
if failed > 0 {
157+
return fmt.Errorf("%d migration(s) failed", failed)
158+
}
159+
return nil
160+
}
161+
162+
func migrateSingle(ctx context.Context, m *migration.Migrator, r migration.OperatorScanResult, restCfg *rest.Config) error {
163+
opts := migration.Options{
164+
SubscriptionName: r.SubscriptionName,
165+
SubscriptionNamespace: r.SubscriptionNamespace,
166+
}
167+
opts.ApplyDefaults()
168+
169+
// Profile
170+
sub, csv, ip, err := m.GetCSVAndInstallPlan(ctx, opts)
171+
if err != nil {
172+
return fmt.Errorf("profiling failed: %w", err)
173+
}
174+
175+
bundleInfo, err := m.GetBundleInfo(ctx, opts, csv, ip)
176+
if err != nil {
177+
return fmt.Errorf("bundle info failed: %w", err)
178+
}
179+
_ = sub // already checked in scan
180+
181+
detail("Package:", bundleInfo.PackageName)
182+
detail("Version:", bundleInfo.Version)
183+
184+
// Catalog resolution
185+
info("Resolving ClusterCatalog...")
186+
csImage, _ := m.GetCatalogSourceImage(ctx, bundleInfo.CatalogSourceRef)
187+
if csImage != "" {
188+
bundleInfo.CatalogSourceImage = csImage
189+
}
190+
191+
startProgress()
192+
catalogName, err := m.ResolveClusterCatalog(ctx, bundleInfo, restCfg)
193+
clearProgress()
194+
if err != nil {
195+
var notFound *migration.PackageNotFoundError
196+
if errors.As(err, &notFound) && bundleInfo.CatalogSourceImage != "" {
197+
warn(err.Error())
198+
catalogName, err = promptCreateCatalog(ctx, m, bundleInfo, restCfg)
199+
if err != nil {
200+
return err
201+
}
202+
} else {
203+
return fmt.Errorf("catalog resolution failed: %w", err)
204+
}
205+
}
206+
bundleInfo.ResolvedCatalogName = catalogName
207+
success(fmt.Sprintf("Using catalog: %s", catalogName))
208+
209+
// Collect
210+
info("Collecting resources...")
211+
objects, err := m.CollectResources(ctx, opts, csv, ip, bundleInfo.PackageName)
212+
if err != nil {
213+
return fmt.Errorf("resource collection failed: %w", err)
214+
}
215+
bundleInfo.CollectedObjects = objects
216+
success(fmt.Sprintf("Collected %d resources", len(objects)))
217+
218+
// Backup
219+
backup, err := m.BackupResources(ctx, opts, csv)
220+
if err != nil {
221+
return fmt.Errorf("backup failed: %w", err)
222+
}
223+
if err := backup.SaveToDisk("."); err != nil {
224+
warn(fmt.Sprintf("Could not save backup to disk: %v", err))
225+
} else {
226+
info(fmt.Sprintf("Backup: %s", backup.Dir))
227+
}
228+
229+
// Prepare
230+
info("Removing OLMv0 management (orphan cascade)...")
231+
if err := m.PrepareForMigration(ctx, opts, csv); err != nil {
232+
fail("Preparation failed, recovering...")
233+
startProgress()
234+
if recoverErr := m.RecoverFromBackup(ctx, opts, backup); recoverErr != nil {
235+
clearProgress()
236+
return fmt.Errorf("preparation failed: %w; recovery also failed: %v", err, recoverErr)
237+
}
238+
clearProgress()
239+
return fmt.Errorf("preparation failed (recovered): %w", err)
240+
}
241+
success("OLMv0 management removed")
242+
243+
// CER
244+
info("Creating ClusterExtensionRevision...")
245+
startProgress()
246+
if err := m.CreateClusterExtensionRevision(ctx, opts, bundleInfo); err != nil {
247+
clearProgress()
248+
fail("CER failed, recovering...")
249+
startProgress()
250+
if recoverErr := m.RecoverBeforeCE(ctx, opts, backup); recoverErr != nil {
251+
clearProgress()
252+
return fmt.Errorf("CER creation failed: %w; recovery also failed: %v", err, recoverErr)
253+
}
254+
clearProgress()
255+
return fmt.Errorf("CER creation failed (recovered): %w", err)
256+
}
257+
clearProgress()
258+
success(fmt.Sprintf("CER %s-1 available", opts.ClusterExtensionName))
259+
260+
// CE
261+
info("Creating ClusterExtension...")
262+
startProgress()
263+
if err := m.CreateClusterExtension(ctx, opts, bundleInfo); err != nil {
264+
clearProgress()
265+
return fmt.Errorf("CE creation failed: %w", err)
266+
}
267+
clearProgress()
268+
success(fmt.Sprintf("CE %s installed", opts.ClusterExtensionName))
269+
270+
// Cleanup
271+
info("Cleaning up OLMv0 resources...")
272+
cleanupResult := m.CleanupOLMv0Resources(ctx, opts, bundleInfo.PackageName, csv.Name)
273+
for _, action := range cleanupResult.Actions {
274+
switch {
275+
case action.Skipped:
276+
info(fmt.Sprintf("⏭️ %s", action.Description))
277+
case action.Error != nil:
278+
warn(fmt.Sprintf("%s: %v", action.Description, action.Error))
279+
case action.Succeeded:
280+
success(action.Description)
281+
}
282+
}
283+
284+
banner(fmt.Sprintf("%s migrated successfully!", bundleInfo.PackageName))
285+
return nil
286+
}
287+
288+
func summarizeIneligibility(r migration.OperatorScanResult) string {
289+
if r.Error != nil {
290+
return r.Error.Error()
291+
}
292+
if len(r.FailedChecks) > 0 {
293+
reasons := make([]string, len(r.FailedChecks))
294+
for i, c := range r.FailedChecks {
295+
reasons[i] = fmt.Sprintf("%s: %s", c.Name, c.Message)
296+
}
297+
return strings.Join(reasons, "; ")
298+
}
299+
return "unknown"
300+
}

0 commit comments

Comments
 (0)