package cluster import ( "context" "encoding/json" "fmt" "strings" "time" ) // imagePullDNSBlockerReasons classifies image pull failures caused by DNS. // Signature: (o *Orchestrator) imagePullDNSBlockerReasons(ctx context.Context) (map[string]string, error). // Why: deleting an unchanged ImagePullBackOff pod does not repair registry DNS; // Ananke should report the blocker and wait for DNS/registry health to change. func (o *Orchestrator) imagePullDNSBlockerReasons(ctx context.Context) (map[string]string, error) { eventsOut, err := o.kubectl(ctx, 30*time.Second, "get", "events", "-A", "-o", "json") if err != nil { return nil, fmt.Errorf("query events for image-pull DNS scan: %w", err) } reasons := map[string]string{} if strings.TrimSpace(eventsOut) == "" { return reasons, nil } var events eventList if err := json.Unmarshal([]byte(eventsOut), &events); err != nil { return nil, fmt.Errorf("decode events for image-pull DNS scan: %w", err) } for _, event := range events.Items { if !strings.EqualFold(strings.TrimSpace(event.Type), "Warning") { continue } if !strings.EqualFold(strings.TrimSpace(event.InvolvedObject.Kind), "Pod") { continue } reason := strings.TrimSpace(event.Reason) if reason != "Failed" && reason != "FailedPull" && reason != "ErrImagePull" && reason != "ImagePullBackOff" { continue } message := strings.TrimSpace(event.Message) if !imagePullMessageHasDNSFailure(message) { continue } key := strings.TrimSpace(event.InvolvedObject.Namespace) + "/" + strings.TrimSpace(event.InvolvedObject.Name) if key == "/" { continue } reasons[key] = "ImagePullDNSBlocker:" + imagePullRegistryHost(message) } return reasons, nil } // imagePullMessageHasDNSFailure runs one orchestration or CLI step. // Signature: imagePullMessageHasDNSFailure(message string) bool. // Why: image pull recycling should be suppressed only for resolver failures, // leaving ordinary pull/auth errors on their existing paths. func imagePullMessageHasDNSFailure(message string) bool { lower := strings.ToLower(message) if !strings.Contains(lower, "lookup ") { return false } return strings.Contains(lower, "try again") || strings.Contains(lower, "no such host") || strings.Contains(lower, "server misbehaving") || strings.Contains(lower, "temporary failure") || strings.Contains(lower, "i/o timeout") } // imagePullRegistryHost runs one orchestration or CLI step. // Signature: imagePullRegistryHost(message string) string. // Why: operator status should group DNS blockers by registry host instead of // repeating full kubelet event text for every pod. func imagePullRegistryHost(message string) string { lower := strings.ToLower(message) idx := strings.Index(lower, "lookup ") if idx < 0 { return "unknown-registry" } rest := strings.TrimSpace(message[idx+len("lookup "):]) fields := strings.Fields(rest) if len(fields) == 0 { return "unknown-registry" } host := strings.Trim(fields[0], `"'[]():,`) if host == "" { return "unknown-registry" } return host }