mirror of
https://github.com/vee1e/runtimeclass-debugger.git
synced 2026-09-01 10:18:42 +00:00
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.
264 lines
9.8 KiB
Go
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()
|
|
}
|