@@ -269,7 +269,8 @@ func isTrainerInstalled(ctx context.Context, dynamicClient dynamic.Interface) (t
269269 if ! ok {
270270 slog .Info ("Kubeflow Trainer incomplete: admission webhook missing" ,
271271 "configuration" , trainerMutatingWebhookConfig , "webhook" , trainerMutatingWebhookName )
272- return trainerInstall {Incomplete : "the validating admission webhook configuration is missing" }, false , nil
272+ return trainerInstall {Incomplete : fmt .Sprintf (
273+ "admission configuration %q is missing" , trainerMutatingWebhookConfig )}, false , nil
273274 }
274275
275276 // The controller Deployment is found by label: its name is release-derived on
@@ -313,13 +314,18 @@ func discoverTrainerInstall(ctx context.Context, dynamicClient dynamic.Interface
313314 gvr schema.GroupVersionResource , configName , webhookName string ) (trainerInstall , bool , error ) {
314315
315316 obj , found , err := getTrainerObject (ctx , dynamicClient , gvr , "" , configName )
316- if err != nil || ! found {
317- if err == nil {
318- slog .Info ("Kubeflow Trainer incomplete: admission configuration missing" ,
319- "configuration" , configName )
320- }
317+ if err != nil {
318+ // A failed read is not evidence of absence. Propagate it so the caller
319+ // classifies it as transport or timeout rather than as a deployment that
320+ // never happened.
321321 return trainerInstall {}, false , err
322322 }
323+ if ! found {
324+ slog .Info ("Kubeflow Trainer incomplete: admission configuration missing" ,
325+ "configuration" , configName )
326+ return trainerInstall {Incomplete : fmt .Sprintf (
327+ "admission configuration %q is missing" , configName )}, false , nil
328+ }
323329
324330 entries , _ , err := unstructured .NestedSlice (obj .Object , "webhooks" )
325331 if err != nil {
@@ -346,7 +352,9 @@ func discoverTrainerInstall(ctx context.Context, dynamicClient dynamic.Interface
346352
347353 slog .Info ("Kubeflow Trainer incomplete: admission webhook missing" ,
348354 "configuration" , configName , "webhook" , webhookName )
349- return trainerInstall {Incomplete : "the validating admission webhook configuration is missing" }, false , nil
355+ return trainerInstall {Incomplete : fmt .Sprintf (
356+ "admission configuration %q exists but does not contain the %q webhook" ,
357+ configName , webhookName )}, false , nil
350358}
351359
352360// getTrainerObject fetches one object. NotFound reports found=false with no
@@ -537,6 +545,42 @@ func ensureTrainerInstalled(ctx context.Context, dynamicClient dynamic.Interface
537545 return nil , awaitTrainerController (ctx , dynamicClient , install )
538546}
539547
548+ // classifyPollExpiry turns an expired poll context into the right verdict, or nil
549+ // when the poll context is still live and the caller should handle its own error.
550+ //
551+ // The two expiries mean opposite things. A canceled parent means the run was
552+ // aborted — catalog timeout, canceled phase, killed Job — which is not a customer
553+ // deployment defect. A local deadline means the delivered Trainer never became
554+ // complete, which is.
555+ //
556+ // Both paths route through here so the verdict does not depend on whether the clock
557+ // ran out during a probe or during a sleep: the same cluster state must not produce
558+ // two different codes.
559+ func classifyPollExpiry (ctx , pollCtx context.Context , last trainerInstall ) error {
560+ if ctx .Err () != nil {
561+ return aicrErrors .Wrap (aicrErrors .ErrCodeTimeout ,
562+ "canceled while waiting for the recipe-declared Kubeflow Trainer" , ctx .Err ())
563+ }
564+ if pollCtx .Err () == nil {
565+ return nil
566+ }
567+
568+ // ErrCodeNotFound, not Unavailable: the read succeeded and the answer was "not
569+ // deployed". Unavailable is this package's code for a transport failure — see
570+ // the decision table on validators.Require — and using it here would file a
571+ // product defect alongside apiserver hiccups, telling whoever triages it to
572+ // re-run rather than to fix their deployment.
573+ reason := last .Incomplete
574+ if reason == "" {
575+ reason = "no complete installation was found"
576+ }
577+ return aicrErrors .New (aicrErrors .ErrCodeNotFound , fmt .Sprintf (
578+ "the recipe declares the %s component but its Kubeflow Trainer installation " +
579+ "did not become complete within %s: %s. The benchmark will not self-install " +
580+ "over a delivered component that failed to deploy" ,
581+ kubeflowTrainerComponent , trainerInstallWaitTimeout , reason ))
582+ }
583+
540584// awaitTrainerController waits for a Trainer the benchmark does not own to finish
541585// starting. The probe confirms every object exists; the controller may still be
542586// rolling, and waiting is the alternative to reinstalling over a healthy Trainer.
@@ -573,13 +617,20 @@ func waitForDeclaredTrainer(ctx context.Context, dynamicClient dynamic.Interface
573617 defer cancel ()
574618
575619 for {
576- // Probe with the parent context, not pollCtx: getTrainerObject checks
577- // ctx.Err() at the top of every read and returns Timeout, so a deadline
578- // landing mid-probe would surface as a bare timeout instead of the
579- // NotFound-plus-diagnosis this function exists to produce. Letting the
580- // select below own the deadline keeps the two conditions separable.
581- install , ok , err := isTrainerInstalled (ctx , dynamicClient )
620+ // Probe under pollCtx so the rollout allowance actually bounds it. Each probe
621+ // makes several sequential reads, each with its own DiagnosticTimeout, so a
622+ // probe running on the parent context could cross the deadline and still
623+ // report success — the allowance would bound only the sleeps.
624+ //
625+ // The cost is that getTrainerObject checks ctx.Err() at the top of every read
626+ // and returns Timeout, so an expiring deadline surfaces here as a probe error
627+ // rather than through the select. Classify it below instead of propagating it
628+ // blind, so the verdict does not depend on where in the loop the clock ran out.
629+ install , ok , err := isTrainerInstalled (pollCtx , dynamicClient )
582630 if err != nil {
631+ if verdict := classifyPollExpiry (ctx , pollCtx , last ); verdict != nil {
632+ return trainerInstall {}, verdict
633+ }
583634 return trainerInstall {}, aicrErrors .PropagateOrWrap (err , aicrErrors .ErrCodeInternal ,
584635 "failed to check Kubeflow Trainer installation" )
585636 }
@@ -590,30 +641,11 @@ func waitForDeclaredTrainer(ctx context.Context, dynamicClient dynamic.Interface
590641
591642 select {
592643 case <- pollCtx .Done ():
593- // pollCtx expires for two different reasons and they mean opposite
594- // things. A canceled parent means the run was aborted — catalog timeout,
595- // phase cancellation, the Job killed — and reporting that as a customer
596- // deployment defect is the same misclassification the NotFound code
597- // above exists to avoid, one level down.
598- if ctx .Err () != nil {
599- return trainerInstall {}, aicrErrors .Wrap (aicrErrors .ErrCodeTimeout ,
600- "canceled while waiting for the recipe-declared Kubeflow Trainer" , ctx .Err ())
601- }
602-
603- // ErrCodeNotFound, not Unavailable: the read succeeded and the answer was
604- // "not deployed". Unavailable is this package's code for a transport
605- // failure — see the decision table on validators.Require — and using it
606- // here would file a product defect alongside apiserver hiccups, telling
607- // whoever triages it to re-run rather than to fix their deployment.
608- reason := last .Incomplete
609- if reason == "" {
610- reason = "no complete installation was found"
644+ if verdict := classifyPollExpiry (ctx , pollCtx , last ); verdict != nil {
645+ return trainerInstall {}, verdict
611646 }
612- return trainerInstall {}, aicrErrors .New (aicrErrors .ErrCodeNotFound , fmt .Sprintf (
613- "the recipe declares the %s component but its Kubeflow Trainer installation " +
614- "did not become complete within %s: %s. The benchmark will not self-install " +
615- "over a delivered component that failed to deploy" ,
616- kubeflowTrainerComponent , trainerInstallWaitTimeout , reason ))
647+ return trainerInstall {}, aicrErrors .New (aicrErrors .ErrCodeInternal ,
648+ "Kubeflow Trainer wait ended without a verdict" )
617649 case <- time .After (trainerInstallPollInterval ):
618650 }
619651 }
0 commit comments