|
| 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, ¬Found) && 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