@@ -28,9 +28,9 @@ import (
2828 "k8s.io/apimachinery/pkg/runtime"
2929 "k8s.io/client-go/dynamic"
3030 "k8s.io/client-go/kubernetes"
31- "k8s.io/client-go/rest"
3231 clientgoscheme "k8s.io/client-go/kubernetes/scheme"
3332 _ "k8s.io/client-go/plugin/pkg/client/auth" // Import all auth providers
33+ "k8s.io/client-go/rest"
3434 "k8s.io/client-go/tools/clientcmd"
3535 "sigs.k8s.io/controller-runtime/pkg/client"
3636
@@ -186,24 +186,18 @@ func runMigration(ctx context.Context, cfg *Config, stdin io.Reader, stdout io.W
186186 return fmt .Errorf ("aborted" )
187187 }
188188
189- // Backup phase: every to-be-adopted cluster is snapshotted before any
190- // mutation; a failed backup excludes that cluster from the apply.
189+ var backup func () error
191190 if cfg .backupConfigured () {
192- if err := runBackups (ctx , cfg , restCfg , kube , ctrlClient , plans , d , stdout ); err != nil {
193- return err
194- }
191+ backup = func () error { return runBackups (ctx , cfg , restCfg , kube , ctrlClient , plans , d , stdout ) }
195192 } else {
196193 fmt .Fprintln (stdout , "! pre-adoption backup skipped (--skip-backup)" )
197194 }
198195
199- // Auth phase: the legacy NoPassword root cannot match a credentials
200- // Secret, so auth is switched off on the live etcd; the new operator
201- // re-enables it with the referenced Secret once it takes over.
202- if err := disableAuthForAdoptions (ctx , restCfg , kube , plans , d , facts , stdout ); err != nil {
203- return err
204- }
205-
206- stats , err := applyPlans (ctx , ctrlClient , dyn , plans , stdout )
196+ stats , err := runMutationPhases (
197+ func () error { return disableAuthForAdoptions (ctx , restCfg , kube , plans , d , facts , stdout ) },
198+ backup ,
199+ func () (applyStats , error ) { return applyPlans (ctx , ctrlClient , dyn , plans , stdout ) },
200+ )
207201 if err != nil {
208202 return err
209203 }
@@ -217,6 +211,33 @@ func runMigration(ctx context.Context, cfg *Config, stdin io.Reader, stdout io.W
217211 return errorIfPlanFailed (plans )
218212}
219213
214+ // runMutationPhases runs the post-confirmation adoption phases in the order
215+ // their inter-phase contracts REQUIRE, and returns the apply stats:
216+ //
217+ // 1. authDisable — switch auth off on every auth-enabled legacy etcd.
218+ // 2. backup — snapshot each to-be-adopted cluster (nil ⇒ --skip-backup).
219+ // 3. apply — re-own the data plane and create the new CRs.
220+ //
221+ // Auth-disable MUST precede backup: the snapshot Job dials etcd anonymously
222+ // (cert-only, no user), and etcd gates the Maintenance Snapshot RPC behind
223+ // auth when it is enabled — so for an auth-enabled cluster (the Cozystack/
224+ // Kamaji case) the backup can only succeed once auth is off. Running them in
225+ // the reverse order silently flips exactly those clusters to ActionError and
226+ // excludes them from adoption. A cluster whose auth-disable fails is itself
227+ // flipped to ActionError and then skipped by the backup and apply phases, so
228+ // an unprotected cluster is never adopted.
229+ func runMutationPhases (authDisable func () error , backup func () error , apply func () (applyStats , error )) (applyStats , error ) {
230+ if err := authDisable (); err != nil {
231+ return applyStats {}, err
232+ }
233+ if backup != nil {
234+ if err := backup (); err != nil {
235+ return applyStats {}, err
236+ }
237+ }
238+ return apply ()
239+ }
240+
220241// mustNamespace/mustName split a pre-validated namespace/name ref.
221242func mustNamespace (ref string ) string { ns , _ , _ := splitRef (ref ); return ns }
222243func mustName (ref string ) string { _ , n , _ := splitRef (ref ); return n }
0 commit comments