Files
kubernetes/test/e2e/dra/test-driver/app/server.go
John-Paul Sassine b7de71f9ce feat(kubelet): Add ResourceHealthStatus for DRA pods
This change introduces the ability for the Kubelet to monitor and report
the health of devices allocated via Dynamic Resource Allocation (DRA).
This addresses a key part of KEP-4680 by providing visibility into
device failures, which helps users and controllers diagnose pod failures.

The implementation includes:
- A new `v1alpha1.NodeHealth` gRPC service with a `WatchResources`
  stream that DRA plugins can optionally implement.
- A health information cache within the Kubelet's DRA manager to track
  the last known health of each device and handle plugin disconnections.
- An asynchronous update mechanism that triggers a pod sync when a
  device's health changes.
- A new `allocatedResourcesStatus` field in `v1.ContainerStatus` to
  expose the device health information to users via the Pod API.

Update vendor

KEP-4680: Fix lint, boilerplate, and codegen issues

Add another e2e test, add TODO for KEP4680 & update test infra helpers

Add Feature Gate e2e test

Fixing presubmits

Fix var names, feature gating, and nits

Fix DRA Health gRPC API according to review feedback
2025-07-24 23:23:18 +00:00

260 lines
9.4 KiB
Go

/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package app does all of the work necessary to configure and run a
// Kubernetes app process.
package app
import (
"errors"
"fmt"
"net"
"net/http"
"net/http/pprof"
"os"
"os/signal"
"path"
"path/filepath"
"strings"
"syscall"
"github.com/spf13/cobra"
"k8s.io/component-base/metrics"
resourceapi "k8s.io/api/resource/v1"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
cliflag "k8s.io/component-base/cli/flag"
"k8s.io/component-base/featuregate"
"k8s.io/component-base/logs"
logsapi "k8s.io/component-base/logs/api/v1"
"k8s.io/component-base/metrics/legacyregistry"
"k8s.io/component-base/term"
"k8s.io/dynamic-resource-allocation/kubeletplugin"
"k8s.io/dynamic-resource-allocation/resourceslice"
"k8s.io/klog/v2"
)
// NewCommand creates a *cobra.Command object with default parameters.
func NewCommand() *cobra.Command {
o := logsapi.NewLoggingConfiguration()
var clientset kubernetes.Interface
var config *rest.Config
logger := klog.Background()
cmd := &cobra.Command{
Use: "cdi-test-driver",
Long: "cdi-test-driver implements a resource driver controller and kubelet plugin.",
}
sharedFlagSets := cliflag.NamedFlagSets{}
fs := sharedFlagSets.FlagSet("logging")
logsapi.AddFlags(o, fs)
logs.AddFlags(fs, logs.SkipLoggingConfigurationFlags())
fs = sharedFlagSets.FlagSet("Kubernetes client")
kubeconfig := fs.String("kubeconfig", "", "Absolute path to the kube.config file. Either this or KUBECONFIG need to be set if the driver is being run out of cluster.")
kubeAPIQPS := fs.Float32("kube-api-qps", 50, "QPS to use while communicating with the kubernetes apiserver.")
kubeAPIBurst := fs.Int("kube-api-burst", 100, "Burst to use while communicating with the kubernetes apiserver.")
fs = sharedFlagSets.FlagSet("http server")
httpEndpoint := fs.String("http-endpoint", "",
"The TCP network address where the HTTP server for diagnostics, including pprof, metrics and (if applicable) leader election health check, will listen (example: `:8080`). The default is the empty string, which means the server is disabled.")
metricsPath := fs.String("metrics-path", "/metrics", "The HTTP path where Prometheus metrics will be exposed, disabled if empty.")
profilePath := fs.String("pprof-path", "", "The HTTP path where pprof profiling will be available, disabled if empty.")
fs = sharedFlagSets.FlagSet("CDI")
driverNameFlagName := "drivername"
driverName := fs.String(driverNameFlagName, "test-driver.cdi.k8s.io", "Resource driver name.")
fs = sharedFlagSets.FlagSet("other")
featureGate := featuregate.NewFeatureGate()
utilruntime.Must(logsapi.AddFeatureGates(featureGate))
featureGate.AddFlag(fs)
fs = cmd.PersistentFlags()
for _, f := range sharedFlagSets.FlagSets {
fs.AddFlagSet(f)
}
mux := http.NewServeMux()
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
// Activate logging as soon as possible, after that
// show flags with the final logging configuration.
if err := logsapi.ValidateAndApply(o, featureGate); err != nil {
return err
}
// get the KUBECONFIG from env if specified (useful for local/debug cluster)
kubeconfigEnv := os.Getenv("KUBECONFIG")
if kubeconfigEnv != "" {
logger.Info("Found KUBECONFIG environment variable set, using that..")
*kubeconfig = kubeconfigEnv
}
var err error
if *kubeconfig == "" {
config, err = rest.InClusterConfig()
if err != nil {
return fmt.Errorf("create in-cluster client configuration: %w", err)
}
} else {
config, err = clientcmd.BuildConfigFromFlags("", *kubeconfig)
if err != nil {
return fmt.Errorf("create out-of-cluster client configuration: %w", err)
}
}
config.QPS = *kubeAPIQPS
config.Burst = *kubeAPIBurst
clientset, err = kubernetes.NewForConfig(config)
if err != nil {
return fmt.Errorf("create client: %w", err)
}
if *httpEndpoint != "" {
if *metricsPath != "" {
// For workqueue and leader election metrics, set up via the anonymous imports of:
// https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/component-base/metrics/prometheus/workqueue/metrics.go
// https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/component-base/metrics/prometheus/clientgo/leaderelection/metrics.go
//
// Also to happens to include Go runtime and process metrics:
// https://github.com/kubernetes/kubernetes/blob/9780d88cb6a4b5b067256ecb4abf56892093ee87/staging/src/k8s.io/component-base/metrics/legacyregistry/registry.go#L46-L49
gatherer := legacyregistry.DefaultGatherer
actualPath := path.Join("/", *metricsPath)
logger.Info("Starting metrics", "path", actualPath)
mux.Handle(actualPath,
metrics.HandlerFor(gatherer, metrics.HandlerOpts{}))
}
if *profilePath != "" {
actualPath := path.Join("/", *profilePath)
logger.Info("Starting profiling", "path", actualPath)
mux.HandleFunc(path.Join("/", *profilePath), pprof.Index)
mux.HandleFunc(path.Join("/", *profilePath, "cmdline"), pprof.Cmdline)
mux.HandleFunc(path.Join("/", *profilePath, "profile"), pprof.Profile)
mux.HandleFunc(path.Join("/", *profilePath, "symbol"), pprof.Symbol)
mux.HandleFunc(path.Join("/", *profilePath, "trace"), pprof.Trace)
}
listener, err := net.Listen("tcp", *httpEndpoint)
if err != nil {
return fmt.Errorf("listen on HTTP endpoint: %w", err)
}
go func() {
logger.Info("Starting HTTP server", "endpoint", *httpEndpoint)
err := http.Serve(listener, mux)
if err != nil {
logger.Error(err, "HTTP server failed")
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
}
}()
}
return nil
}
kubeletPlugin := &cobra.Command{
Use: "kubelet-plugin",
Short: "run as kubelet plugin",
Long: "cdi-test-driver kubelet-plugin runs as a device plugin for kubelet that supports dynamic resource allocation.",
Args: cobra.ExactArgs(0),
}
kubeletPluginFlagSets := cliflag.NamedFlagSets{}
fs = kubeletPluginFlagSets.FlagSet("kubelet")
kubeletRegistryDir := fs.String("plugin-registration-path", kubeletplugin.KubeletRegistryDir, "The directory where kubelet looks for plugin registration sockets.")
kubeletPluginsDir := fs.String("datadir", kubeletplugin.KubeletPluginsDir, "The per-driver directory where the DRA Unix domain socket will be created.")
fs = kubeletPluginFlagSets.FlagSet("CDI")
cdiDir := fs.String("cdi-dir", "/var/run/cdi", "directory for dynamically created CDI JSON files")
nodeName := fs.String("node-name", "", "name of the node that the kubelet plugin is responsible for")
numDevices := fs.Int("num-devices", 4, "number of devices to simulate per node")
fs = kubeletPlugin.Flags()
for _, f := range kubeletPluginFlagSets.FlagSets {
fs.AddFlagSet(f)
}
kubeletPlugin.RunE = func(cmd *cobra.Command, args []string) error {
// Ensure that directories exist, creating them if necessary. We want
// to know early if there is a setup problem that would prevent
// creating those directories.
if err := os.MkdirAll(*cdiDir, os.FileMode(0750)); err != nil {
return fmt.Errorf("create CDI directory: %w", err)
}
datadir := path.Join(*kubeletPluginsDir, *driverName)
if err := os.MkdirAll(filepath.Dir(datadir), 0750); err != nil {
return fmt.Errorf("create socket directory: %w", err)
}
if *nodeName == "" {
return errors.New("--node-name not set")
}
devices := make([]resourceapi.Device, *numDevices)
for i := 0; i < *numDevices; i++ {
devices[i] = resourceapi.Device{
Name: fmt.Sprintf("device-%02d", i),
}
}
driverResources := resourceslice.DriverResources{
Pools: map[string]resourceslice.Pool{
*nodeName: {
Slices: []resourceslice.Slice{{
Devices: devices,
}},
},
},
}
plugin, err := StartPlugin(cmd.Context(), *cdiDir, *driverName, clientset, *nodeName, FileOperations{DriverResources: &driverResources},
Options{EnableHealthService: true},
kubeletplugin.PluginDataDirectoryPath(datadir),
kubeletplugin.RegistrarDirectoryPath(*kubeletRegistryDir),
)
if err != nil {
return fmt.Errorf("start example plugin: %w", err)
}
// Handle graceful shutdown. We need to delete Unix domain
// sockets.
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
logger.Info("Waiting for signal.")
sig := <-sigc
logger.Info("Received signal, shutting down.", "signal", sig)
plugin.Stop()
return nil
}
cmd.AddCommand(kubeletPlugin)
// SetUsageAndHelpFunc takes care of flag grouping. However,
// it doesn't support listing child commands. We add those
// to cmd.Use.
cols, _, _ := term.TerminalSize(cmd.OutOrStdout())
cliflag.SetUsageAndHelpFunc(cmd, sharedFlagSets, cols)
var children []string
for _, child := range cmd.Commands() {
children = append(children, child.Use)
}
cmd.Use += " [shared flags] " + strings.Join(children, "|")
cliflag.SetUsageAndHelpFunc(kubeletPlugin, kubeletPluginFlagSets, cols)
return cmd
}