runtimeclass-debugger/pkg/diag/diag.go
lakshit verma 7b524cc47b
diag: correct the config key name in the events-check advice
The messages told users to enable edgeCore.reportEvent, but the key
lives under the edged section as edged.reportEvent. Searching for the
wrong key in edgecore.yaml would find nothing.
2026-08-08 01:04:50 +05:30

264 lines
9.8 KiB
Go

// Package diag orchestrates the four RuntimeClass path checks and renders
// the diagnosis report.
package diag
import (
"context"
"errors"
"fmt"
"strings"
"sync"
nodev1 "k8s.io/api/node/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"github.com/kubeedge/runtimeclass-debugger/pkg/bridge"
"github.com/kubeedge/runtimeclass-debugger/pkg/config"
"github.com/kubeedge/runtimeclass-debugger/pkg/cri"
"github.com/kubeedge/runtimeclass-debugger/pkg/metaserver"
)
// RuntimeClassLister is the MetaServer view of the transport check.
type RuntimeClassLister interface {
ListRuntimeClasses(ctx context.Context) ([]nodev1.RuntimeClass, error)
}
// ClassSource is the local SQLite store view of the bridge check.
type ClassSource interface {
ListRuntimeClasses() ([]nodev1.RuntimeClass, error)
}
// Diag runs the four checks. Its dependencies are interfaces so tests can
// substitute fakes.
type Diag struct {
MetaServer RuntimeClassLister
Store ClassSource
ContainerdPath string
EdgeCorePath string
// NewNaive and NewWired build the two bridge lookups. They default to
// the bridge package implementations; tests override them to simulate
// resolver states.
NewNaive func() (bridge.Resolver, error)
NewWired func([]nodev1.RuntimeClass) (bridge.Resolver, error)
}
// New builds a Diag from a MetaServer client and the local file paths.
// The local store is opened lazily by the first check that needs it.
func New(meta RuntimeClassLister, dbPath, containerdPath, edgecorePath string) *Diag {
return &Diag{
MetaServer: meta,
Store: &lazyStore{path: dbPath},
ContainerdPath: containerdPath,
EdgeCorePath: edgecorePath,
}
}
// Run executes all four checks in dependency order and returns the report.
// A non-empty scope limits the bridge and CRI checks to those class names;
// otherwise the classes served by MetaServer are used.
func (d *Diag) Run(ctx context.Context, scope []string) *Report {
transport, classes := d.CheckTransport(ctx)
checks := []CheckResult{transport}
if len(scope) == 0 {
for _, c := range classes {
scope = append(scope, c.Name)
}
}
checks = append(checks, d.CheckBridge(scope))
checks = append(checks, d.CheckCRI(scope))
checks = append(checks, d.CheckEvents())
report := &Report{Checks: checks}
report.ExitCode = report.ComputeExitCode()
return report
}
// CheckTransport: does the edge have RuntimeClass objects at all?
func (d *Diag) CheckTransport(ctx context.Context) (CheckResult, []nodev1.RuntimeClass) {
classes, err := d.MetaServer.ListRuntimeClasses(ctx)
if err != nil {
var authErr *metaserver.AuthRequiredError
var unreachErr *metaserver.UnreachableError
switch {
case errors.As(err, &authErr):
return CheckResult{ID: "transport", Status: StatusWarn, Detail: authErr.Reason}, nil
case errors.As(err, &unreachErr):
return CheckResult{ID: "transport", Status: StatusWarn, Detail: unreachErr.Reason + "; cannot inspect transport"}, nil
default:
return CheckResult{ID: "transport", Status: StatusWarn, Detail: err.Error() + "; cannot inspect transport"}, nil
}
}
if len(classes) == 0 {
return CheckResult{ID: "transport", Status: StatusFail, Detail: "MetaServer reachable but returns no RuntimeClass objects: objects never reached the edge (transport gap; matches the #7141 problem)"}, nil
}
names := make([]string, 0, len(classes))
for _, c := range classes {
names = append(names, c.Name)
}
return CheckResult{ID: "transport", Status: StatusPass, Detail: fmt.Sprintf("%d RuntimeClass object(s) served by MetaServer (%s)", len(classes), strings.Join(names, ", "))}, classes
}
// CheckBridge: can Edged resolve the class name to a handler?
func (d *Diag) CheckBridge(scope []string) CheckResult {
if len(scope) == 0 {
return CheckResult{ID: "bridge", Status: StatusSkip, Detail: "nothing to resolve; see the transport check"}
}
local, err := d.Store.ListRuntimeClasses()
if err != nil {
return CheckResult{ID: "bridge", Status: StatusWarn, Detail: fmt.Sprintf("cannot read the local store: %v", err)}
}
localByName := map[string]bool{}
for _, c := range local {
localByName[c.Name] = true
}
naive, err := d.naiveManager()
if err != nil {
return CheckResult{ID: "bridge", Status: StatusWarn, Detail: fmt.Sprintf("cannot build the naive lookup: %v", err)}
}
wired, err := d.wiredManager(local)
if err != nil {
return CheckResult{ID: "bridge", Status: StatusWarn, Detail: fmt.Sprintf("cannot build the wired lookup: %v", err)}
}
lines := []string{}
status := StatusPass
for _, name := range scope {
naiveHandler, naiveErr := bridge.ResolveHandler(naive, name)
handler, wiredErr := bridge.ResolveHandler(wired, name)
switch {
case wiredErr == nil && naiveErr == nil:
lines = append(lines, fmt.Sprintf("class %q: naive lookup -> handler %q; wired lookup -> handler %q", name, naiveHandler, handler))
case wiredErr == nil:
// The class reached the local store but Edged's actual lookup
// path (the kubelet bridge) cannot resolve it: pods stay
// Pending. This is the exact gap the tool exists to detect.
status = StatusFail
lines = append(lines, fmt.Sprintf("class %q is in the local store but the kubelet-bridge lookup cannot resolve it (%s): edge bridge gap (NodeV1 not wired to metaclient)", name, lookupOutcome("", naiveErr)))
case localByName[name]:
status = StatusFail
lines = append(lines, fmt.Sprintf("class %q exists in the local store but the resolution path fails: %v", name, wiredErr))
default:
// The class never reached the store: a transport/sync symptom,
// not evidence about NodeV1 wiring. The transport check covers
// this root cause.
status = StatusFail
lines = append(lines, fmt.Sprintf("class %q not present in the local store: cannot verify the bridge; see the transport check", name))
}
}
return CheckResult{ID: "bridge", Status: status, Detail: strings.Join(lines, "; ")}
}
// naiveManager builds the lookup that models current Edged behavior, or the
// override installed by tests.
func (d *Diag) naiveManager() (bridge.Resolver, error) {
if d.NewNaive != nil {
return d.NewNaive()
}
return bridge.NewNaiveManager()
}
// wiredManager builds the lookup seeded with the local store, or the
// override installed by tests.
func (d *Diag) wiredManager(local []nodev1.RuntimeClass) (bridge.Resolver, error) {
if d.NewWired != nil {
return d.NewWired(local)
}
return bridge.NewWiredManager(local)
}
// CheckCRI: is the resolved handler configured in the container runtime?
func (d *Diag) CheckCRI(scope []string) CheckResult {
cc, err := cri.Load(d.ContainerdPath)
if err != nil {
if errors.Is(err, cri.ErrConfigNotFound) {
return CheckResult{ID: "cri", Status: StatusWarn, Detail: fmt.Sprintf("containerd config not found at %s; pass --containerd-config", d.ContainerdPath)}
}
return CheckResult{ID: "cri", Status: StatusWarn, Detail: fmt.Sprintf("cannot read containerd config: %v", err)}
}
if len(scope) == 0 {
return CheckResult{ID: "cri", Status: StatusPass, Detail: "no RuntimeClass handlers to verify"}
}
missing := []string{}
configured := []string{}
for _, name := range scope {
handler, err := d.handlerFor(name)
if err != nil {
return CheckResult{ID: "cri", Status: StatusWarn, Detail: fmt.Sprintf("cannot read the local store: %v", err)}
}
if cc.HasHandler(handler) {
configured = append(configured, handler)
} else {
missing = append(missing, handler)
}
}
if len(missing) > 0 {
return CheckResult{ID: "cri", Status: StatusFail, Detail: fmt.Sprintf("%s not configured in containerd; pod will stay Pending (FailedCreatePodSandBox)", quoteHandlers("handler", missing))}
}
return CheckResult{ID: "cri", Status: StatusPass, Detail: fmt.Sprintf("%s configured in containerd", quoteHandlers("handler", configured))}
}
// CheckEvents: would a failure even be visible to the user?
func (d *Diag) CheckEvents() CheckResult {
report, present, err := config.ReportEvent(d.EdgeCorePath)
if err != nil {
if errors.Is(err, config.ErrConfigNotFound) {
return CheckResult{ID: "events", Status: StatusWarn, Detail: fmt.Sprintf("edgecore.yaml not found at %s", d.EdgeCorePath)}
}
return CheckResult{ID: "events", Status: StatusWarn, Detail: fmt.Sprintf("cannot read edgecore.yaml: %v", err)}
}
if report {
return CheckResult{ID: "events", Status: StatusPass, Detail: "reportEvent is true; pod events are reported to the cloud"}
}
if !present {
return CheckResult{ID: "events", Status: StatusWarn, Detail: "edged.reportEvent is absent (defaults to false): pod failures may not surface as events. Enable edged.reportEvent"}
}
return CheckResult{ID: "events", Status: StatusWarn, Detail: "reportEvent is false: pod failures may not surface as events. Enable edged.reportEvent"}
}
// handlerFor maps a class name to its handler from the local store. Classes
// absent from the store fall back to the class name (the common naming
// convention); a store read failure is returned instead of guessed at.
func (d *Diag) handlerFor(name string) (string, error) {
classes, err := d.Store.ListRuntimeClasses()
if err != nil {
return "", err
}
for _, c := range classes {
if c.Name == name && c.Handler != "" {
return c.Handler, nil
}
}
return name, nil
}
func lookupOutcome(handler string, err error) string {
if err == nil {
return fmt.Sprintf("handler %q", handler)
}
if apierrors.IsNotFound(err) {
return "NotFound"
}
return err.Error()
}
func quoteHandlers(singular string, names []string) string {
if len(names) == 1 {
return fmt.Sprintf("%s %q", singular, names[0])
}
return fmt.Sprintf("%ss %q", singular, strings.Join(names, "\", \""))
}
type lazyStore struct {
path string
once sync.Once
store *bridge.Store
err error
}
func (l *lazyStore) ListRuntimeClasses() ([]nodev1.RuntimeClass, error) {
l.once.Do(func() {
l.store, l.err = bridge.OpenStore(l.path)
})
if l.err != nil {
return nil, l.err
}
return l.store.ListRuntimeClasses()
}