@@ -31,6 +31,7 @@ import (
3131 corev1 "k8s.io/api/core/v1"
3232 apierrors "k8s.io/apimachinery/pkg/api/errors"
3333 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3435 "k8s.io/client-go/dynamic"
3536 "k8s.io/client-go/kubernetes"
3637)
@@ -381,6 +382,7 @@ func (v *k8sValidator) ValidateDeployment(ctx context.Context, functionID, versi
381382
382383 var match map [string ]interface {}
383384 var matchVerID string
385+ var matchNamespace string
384386 for i := range items {
385387 fid , vid := functionIdentity (items [i ].Object )
386388 if fid != functionID {
@@ -391,6 +393,7 @@ func (v *k8sValidator) ValidateDeployment(ctx context.Context, functionID, versi
391393 }
392394 match = items [i ].Object
393395 matchVerID = vid
396+ matchNamespace = items [i ].GetNamespace ()
394397 break
395398 }
396399 if match == nil {
@@ -406,34 +409,187 @@ func (v *k8sValidator) ValidateDeployment(ctx context.Context, functionID, versi
406409 }
407410 instances := extractInstances (match )
408411 out .Checks = append (out .Checks ,
409- checkPodReadiness (instances ),
412+ checkPodReadiness (ctx , v . cs , v . dc , matchNamespace , instances ),
410413 checkQueueHealth (match , instances ),
411414 checkGPUUtilization (cc .backend ),
412415 )
413416 return out , nil
414417}
415418
416- // checkPodReadiness fails when no instances are running or any instance reports
417- // an error/failed status.
418- func checkPodReadiness (instances []Instance ) CheckResult {
419+ // podReadinessRestartThreshold matches NVCA's own RestartCountToFailInstance
420+ // (internal/util/k8sutil/pod.go): the restart count past which a container's
421+ // last crash, not just its current state, is the relevant signal.
422+ const podReadinessRestartThreshold = int32 (3 )
423+
424+ // utilsPodName is the fixed name NVCA gives the sidecar Pod it uses as the
425+ // health signal for an entire MiniService (Helm) release: NVCA's own status
426+ // reconciler reduces "is this MiniService healthy" to this Pod's readiness
427+ // the same way checkPodReadiness does for a plain container function's Pod.
428+ const utilsPodName = "utils"
429+
430+ // checkPodReadiness reads each instance's real Pod (PodReady condition,
431+ // container state) instead of the ICMSRequest CR's mirrored status string,
432+ // which can lag or omit a crash NVCA hasn't reconciled forward yet. A
433+ // container function's Pod is named after the instance ID; a MiniService
434+ // (Helm) instance resolves to the "utils" Pod in its own namespace.
435+ func checkPodReadiness (ctx context.Context , cs kubernetes.Interface , dc dynamic.Interface , namespace string , instances []Instance ) CheckResult {
419436 res := CheckResult {Name : "pod-readiness" }
420437 if len (instances ) == 0 {
421438 res .Status = CheckFailed
422439 res .Message = "no instances running for this function version"
423440 return res
424441 }
442+ var warnMsg string
425443 for _ , in := range instances {
426- if instanceUnhealthy (in ) {
444+ var pod * corev1.Pod
445+ var err error
446+ switch {
447+ case isPodBackedInstance (in ):
448+ pod , err = cs .CoreV1 ().Pods (namespace ).Get (ctx , in .ID , metav1.GetOptions {})
449+ case in .Type == "MiniService" :
450+ pod , err = getMiniServiceUtilsPod (ctx , dc , cs , in .ID )
451+ default :
452+ // Unrecognized type: don't guess the resource kind, fall back
453+ // to the CR-reported status instead of silently skipping.
454+ if instanceUnhealthy (in ) {
455+ res .Status = CheckFailed
456+ res .Message = fmt .Sprintf ("instance %s is unhealthy (status=%s lastReported=%s)" , in .ID , orUnknown (in .Status ), orUnknown (in .LastReportedStatus ))
457+ return res
458+ }
459+ continue
460+ }
461+ if err != nil {
462+ res .Status = CheckFailed
463+ if apierrors .IsNotFound (err ) {
464+ res .Message = fmt .Sprintf ("instance %s: pod not found" , in .ID )
465+ } else {
466+ res .Message = fmt .Sprintf ("instance %s: failed to read pod: %v" , in .ID , err )
467+ }
468+ return res
469+ }
470+ reason , severity := podReadinessSeverity (pod )
471+ switch severity {
472+ case podSeverityFailed :
427473 res .Status = CheckFailed
428- res .Message = fmt .Sprintf ("instance %s is unhealthy (status=%s lastReported=%s) " , in .ID , orUnknown ( in . Status ), orUnknown ( in . LastReportedStatus ) )
474+ res .Message = fmt .Sprintf ("instance %s is unhealthy: %s " , in .ID , reason )
429475 return res
476+ case podSeverityWarning :
477+ // Keep checking the rest instead of failing fast: a later
478+ // instance could still be a genuine failure.
479+ if warnMsg == "" {
480+ warnMsg = fmt .Sprintf ("instance %s not yet ready: %s" , in .ID , reason )
481+ }
430482 }
431483 }
484+ if warnMsg != "" {
485+ res .Status = CheckWarning
486+ res .Message = warnMsg
487+ return res
488+ }
432489 res .Status = CheckPassed
433490 res .Message = fmt .Sprintf ("%s healthy" , pluralize (len (instances ), "instance" ))
434491 return res
435492}
436493
494+ // isPodBackedInstance reports whether an instance is backed by a single Pod
495+ // (the empty type defaults to Pod, matching the legacy field the read-only
496+ // inspector already tolerates) as opposed to a MiniService (Helm) release.
497+ func isPodBackedInstance (in Instance ) bool {
498+ switch in .Type {
499+ case "" , "Pod" :
500+ return true
501+ default :
502+ return false
503+ }
504+ }
505+
506+ // getMiniServiceUtilsPod resolves a MiniService instance to its utils Pod:
507+ // the MiniService CR (cluster-scoped, same lookup evictInstances uses to
508+ // delete it) names the release's dedicated namespace, where the utils Pod
509+ // lives under the fixed name NVCA itself relies on.
510+ func getMiniServiceUtilsPod (ctx context.Context , dc dynamic.Interface , cs kubernetes.Interface , instanceID string ) (* corev1.Pod , error ) {
511+ ms , err := dc .Resource (miniServiceGVR ).Get (ctx , instanceID , metav1.GetOptions {})
512+ if err != nil {
513+ return nil , fmt .Errorf ("reading MiniService %s: %w" , instanceID , err )
514+ }
515+ msNamespace , found , err := unstructured .NestedString (ms .Object , "spec" , "namespace" )
516+ if err != nil {
517+ return nil , fmt .Errorf ("MiniService %s spec.namespace: %w" , instanceID , err )
518+ }
519+ if ! found || msNamespace == "" {
520+ return nil , fmt .Errorf ("MiniService %s has no spec.namespace" , instanceID )
521+ }
522+ return cs .CoreV1 ().Pods (msNamespace ).Get (ctx , utilsPodName , metav1.GetOptions {})
523+ }
524+
525+ // podReadinessSeverityLevel classifies a not-ready Pod: ordinary startup is
526+ // a warning, and only a positively-identified problem (image pull failure,
527+ // a crash, a restart loop) is a failure.
528+ type podReadinessSeverityLevel int
529+
530+ const (
531+ podSeverityHealthy podReadinessSeverityLevel = iota
532+ podSeverityWarning
533+ podSeverityFailed
534+ )
535+
536+ // podReadinessSeverity reports why a Pod isn't ready and how severe that is:
537+ // image pull problems, then a container terminated with a non-zero exit
538+ // code, then a crash loop (restarts past the threshold, judged by the last
539+ // crash since the container may be cycling through Waiting again by now).
540+ // Mirrors NVCA's own IsPodReady/IsPodStuckInitializing (internal/util/k8sutil/pod.go)
541+ // without its time-threshold gating, since this is a point-in-time snapshot.
542+ // Ready=False/Unknown with none of the above (Pending, ContainerCreating,
543+ // an early probe not yet passing) is ordinary rollout, so it's a warning,
544+ // not a failure -- consistent with queue-health's DEPLOYING handling.
545+ func podReadinessSeverity (pod * corev1.Pod ) (string , podReadinessSeverityLevel ) {
546+ if isPodReadyConditionTrue (pod .Status ) {
547+ return "" , podSeverityHealthy
548+ }
549+
550+ allContainers := make ([]corev1.ContainerStatus , 0 , len (pod .Status .InitContainerStatuses )+ len (pod .Status .ContainerStatuses ))
551+ allContainers = append (allContainers , pod .Status .InitContainerStatuses ... )
552+ allContainers = append (allContainers , pod .Status .ContainerStatuses ... )
553+ for _ , cs := range allContainers {
554+ if w := cs .State .Waiting ; w != nil && (w .Reason == "ErrImagePull" || w .Reason == "ImagePullBackOff" ) {
555+ return fmt .Sprintf ("container %s: %s (%s)" , cs .Name , w .Reason , w .Message ), podSeverityFailed
556+ }
557+ }
558+
559+ for _ , cs := range allContainers {
560+ if t := cs .State .Terminated ; t != nil && t .ExitCode != 0 {
561+ return fmt .Sprintf ("container %s terminated: %s (exit code %d)" , cs .Name , t .Reason , t .ExitCode ), podSeverityFailed
562+ }
563+ }
564+
565+ for _ , cs := range allContainers {
566+ if cs .RestartCount < podReadinessRestartThreshold {
567+ continue
568+ }
569+ if t := cs .LastTerminationState .Terminated ; t != nil {
570+ return fmt .Sprintf ("container %s restarted %d times, last exit: %s (exit code %d)" , cs .Name , cs .RestartCount , t .Reason , t .ExitCode ), podSeverityFailed
571+ }
572+ if w := cs .LastTerminationState .Waiting ; w != nil {
573+ return fmt .Sprintf ("container %s restarted %d times, last state: %s" , cs .Name , cs .RestartCount , w .Reason ), podSeverityFailed
574+ }
575+ }
576+
577+ return fmt .Sprintf ("pod condition Ready=%s" , podReadyConditionStatus (pod .Status )), podSeverityWarning
578+ }
579+
580+ func isPodReadyConditionTrue (status corev1.PodStatus ) bool {
581+ return podReadyConditionStatus (status ) == corev1 .ConditionTrue
582+ }
583+
584+ func podReadyConditionStatus (status corev1.PodStatus ) corev1.ConditionStatus {
585+ for _ , c := range status .Conditions {
586+ if c .Type == corev1 .PodReady {
587+ return c .Status
588+ }
589+ }
590+ return corev1 .ConditionUnknown
591+ }
592+
437593// checkQueueHealth maps the collapsed request phase to a verdict: ACTIVE passes,
438594// DEPLOYING/DRAINING warn (transient), FAILED fails.
439595func checkQueueHealth (obj map [string ]interface {}, instances []Instance ) CheckResult {
0 commit comments