ananke/internal/cluster/orchestrator_image_pull_dns.go
2026-07-07 18:29:51 -03:00

89 lines
3.0 KiB
Go

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
}