89 lines
3.0 KiB
Go
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
|
|
}
|