mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/kube-vip/kube-vip.git
synced 2026-09-20 08:03:47 +08:00
* refactor(errors): centralize fatal error handling Detect wrapped PanicError values consistently and preserve their underlying causes when adding fatal context. Apply the helpers to manager, cluster, and IPVS error paths. Assisted-by: GitHub-Copilot:unspecified Signed-off-by: Marcel Fest <marcel.fest@telekom.de> * fix(watchers): restart after terminal watch failures Propagate fatal endpoint watcher failures through the owning service watcher so kube-vip releases leadership instead of remaining active with a stale watch. Treat terminal service, node, and annotation watch failures as errors while preserving clean context cancellation. Return exhausted authorization failures to RetryWatcher, safely decode watch error objects, and replace direct go-spew diagnostics with structured logging. Fixes #1685 Assisted-by: GitHub-Copilot:unspecified Signed-off-by: Marcel Fest <marcel.fest@telekom.de> * fix(services): replace state after traffic policy changes Recreate the service context and instance as one generation when a Service change requires teardown. Ignore delayed leadership cleanup from superseded contexts so it cannot remove replacement state. This prevents a stale Cluster-policy endpoint watcher from winning the service lease after externalTrafficPolicy changes to Local. Assisted-by: GitHub-Copilot:unspecified Signed-off-by: Marcel Fest <marcel.fest@telekom.de> * fix(cli): return command errors to container runtime Propagate manager and service command failures through Cobra so the process exits with status 1. Show usage for invocation errors while keeping runtime failures concise. Assisted-by: GitHub-Copilot:unspecified Signed-off-by: Marcel Fest <marcel.fest@telekom.de> * refactor(logging): use structured errors Replace direct stdout error output with slog records for command failures and traffic mirror qdisc lookup failures. Assisted-by: GitHub-Copilot:unspecified Signed-off-by: Marcel Fest <marcel.fest@telekom.de> * fix(watchers): continue after endpoint deletion Keep EndpointSlice watchers active when an individual endpoint object is deleted so replacement objects can be observed and service traffic can recover. Assisted-by: GitHub-Copilot:unspecified Signed-off-by: Marcel Fest <marcel.fest@telekom.de> --------- Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
461 lines
15 KiB
Go
461 lines
15 KiB
Go
package manager
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"syscall"
|
|
"time"
|
|
|
|
log "log/slog"
|
|
|
|
"github.com/kube-vip/kube-vip/pkg/arp"
|
|
"github.com/kube-vip/kube-vip/pkg/bgp"
|
|
"github.com/kube-vip/kube-vip/pkg/cluster"
|
|
"github.com/kube-vip/kube-vip/pkg/election"
|
|
"github.com/kube-vip/kube-vip/pkg/iptables"
|
|
"github.com/kube-vip/kube-vip/pkg/k8s"
|
|
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
|
"github.com/kube-vip/kube-vip/pkg/lease"
|
|
"github.com/kube-vip/kube-vip/pkg/manager/worker"
|
|
"github.com/kube-vip/kube-vip/pkg/networkinterface"
|
|
"github.com/kube-vip/kube-vip/pkg/nftables"
|
|
"github.com/kube-vip/kube-vip/pkg/node"
|
|
"github.com/kube-vip/kube-vip/pkg/route"
|
|
"github.com/kube-vip/kube-vip/pkg/services"
|
|
"github.com/kube-vip/kube-vip/pkg/upnp"
|
|
"github.com/kube-vip/kube-vip/pkg/utils"
|
|
"github.com/kube-vip/kube-vip/pkg/vip"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"k8s.io/client-go/kubernetes"
|
|
"k8s.io/client-go/rest"
|
|
)
|
|
|
|
// Manager degines the manager of the load-balancing services
|
|
type Manager struct {
|
|
clientSet *kubernetes.Clientset
|
|
rwClientSet *kubernetes.Clientset
|
|
configMap string
|
|
config *kubevip.Config
|
|
|
|
// Manager services
|
|
// service bool
|
|
|
|
// BGP Manager, this is a singleton that manages all BGP advertisements
|
|
bgpServer *bgp.Server
|
|
|
|
// This channel is used to catch an OS signal and trigger a shutdown
|
|
signalChan chan os.Signal
|
|
|
|
sigint sync.Once
|
|
|
|
svcProcessor *services.Processor
|
|
|
|
// This is a prometheus counter used to count the number of events received
|
|
// from the service watcher
|
|
countServiceWatchEvent *prometheus.CounterVec
|
|
|
|
// This mutex is to protect calls from various goroutines
|
|
mutex sync.Mutex
|
|
|
|
// This tracks used network interfaces and guards them with mutex for concurrent changes.
|
|
intfMgr *networkinterface.Manager
|
|
|
|
// This tracks VIPs and performs ARP/NDP advertisement.
|
|
arpMgr *arp.Manager
|
|
|
|
// This tracks node labels and performs label management
|
|
// implementation will be decided in constructor
|
|
// based on config.EnableNodeLabeling
|
|
nodeLabelManager node.LabelManager
|
|
|
|
// This variable reports if manager is being closed
|
|
closing atomic.Bool
|
|
|
|
// Will be used for leaderelection when required
|
|
electionMgr *election.Manager
|
|
|
|
// Will handle leases
|
|
leaseMgr *lease.Manager
|
|
|
|
// Will handle routes
|
|
routeMgr *route.Manager
|
|
}
|
|
|
|
// New will create a new managing object
|
|
func New(ctx context.Context, configMap string, config *kubevip.Config) (*Manager, error) {
|
|
|
|
// Instance identity should be the same as k8s node name to ensure better compatibility.
|
|
// By default k8s sets node name to `hostname -s`,
|
|
// so if node name is not provided in the config,
|
|
// we set it to hostname as a fallback.
|
|
// This mimics legacy behavior and should work on old kube-vip installations.
|
|
if config.NodeName == "" {
|
|
log.Warn("Node name is missing from the config, fall back to hostname")
|
|
hostname, err := os.Hostname()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not get hostname: %v", err)
|
|
}
|
|
config.NodeName = hostname
|
|
}
|
|
config.NodeName = normalizeNodeName(config.NodeName)
|
|
log.Info("using node name", "name", config.NodeName)
|
|
|
|
adminConfigPath := "/etc/kubernetes/admin.conf"
|
|
homeConfigPath := filepath.Join(os.Getenv("HOME"), ".kube", "config")
|
|
|
|
var clientset *kubernetes.Clientset
|
|
var clientConfig *rest.Config
|
|
var err error
|
|
|
|
switch {
|
|
case config.LeaderElectionType == "etcd":
|
|
// Do nothing, we don't construct a k8s client for etcd leader election
|
|
case config.K8sConfigFile != "" && config.K8sConfigFile != adminConfigPath &&
|
|
config.K8sConfigFile != homeConfigPath && utils.FileExists(config.K8sConfigFile):
|
|
// An explicitly configured kubeconfig (k8s_config_file env or
|
|
// --k8sConfigPath) takes precedence over the well-known host paths.
|
|
// KubernetesAddr, when set, overrides the API endpoint - static pods
|
|
// on control plane hosts use it to reach their local API server
|
|
// instead of a VIP that may not be up yet.
|
|
clientConfig, err = k8s.NewRestConfig(config.K8sConfigFile, false, config.KubernetesAddr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not create k8s REST config from file %q: %w", config.K8sConfigFile, err)
|
|
}
|
|
if clientset, err = k8s.NewClientset(clientConfig); err != nil {
|
|
return nil, fmt.Errorf("could not create k8s clientset: %w", err)
|
|
}
|
|
log.Info("Using Kubernetes configuration from explicit file", "path", config.K8sConfigFile, "address", config.KubernetesAddr)
|
|
case utils.FileExists(adminConfigPath):
|
|
if config.KubernetesAddr != "" {
|
|
log.Info("k8s address", "address", config.KubernetesAddr)
|
|
clientConfig, err = k8s.NewRestConfig(adminConfigPath, false, config.KubernetesAddr)
|
|
} else if config.EnableControlPlane {
|
|
// If this is a control plane host it will likely have started as a static pod or won't have the
|
|
// VIP up before trying to connect to the API server, we set the API endpoint to this machine to
|
|
// ensure connectivity.
|
|
if config.DetectControlPlane {
|
|
clientConfig, err = k8s.FindWorkingKubernetesAddress(adminConfigPath, false)
|
|
} else {
|
|
// This will attempt to use kubernetes as the hostname (this should be passed as a host alias) in the pod manifest
|
|
clientConfig, err = k8s.NewRestConfig(adminConfigPath, false, fmt.Sprintf("kubernetes:%v", config.Port))
|
|
}
|
|
} else {
|
|
clientConfig, err = k8s.NewRestConfig(adminConfigPath, false, "")
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not create k8s REST config from external file: %q: %w", adminConfigPath, err)
|
|
}
|
|
if clientset, err = k8s.NewClientset(clientConfig); err != nil {
|
|
return nil, fmt.Errorf("could not create k8s clientset: %w", err)
|
|
}
|
|
log.Debug("Using external Kubernetes configuration from file", "path", adminConfigPath)
|
|
case utils.FileExists(homeConfigPath):
|
|
clientConfig, err = k8s.NewRestConfig(homeConfigPath, false, "")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not create k8s REST config from external file: %q: %w", homeConfigPath, err)
|
|
}
|
|
clientset, err = k8s.NewClientset(clientConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not create k8s clientset from external file: %q: %w", homeConfigPath, err)
|
|
}
|
|
log.Debug("Using external Kubernetes configuration from file", "path", adminConfigPath)
|
|
default:
|
|
clientConfig, err = k8s.NewRestConfig("", true, "")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not create k8s REST config from incluster file: %q: %w", homeConfigPath, err)
|
|
}
|
|
clientset, err = k8s.NewClientset(clientConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not create k8s clientset from incluster config: %w", err)
|
|
}
|
|
log.Debug("Using external Kubernetes configuration from incluster config.")
|
|
}
|
|
|
|
var rwClientSet *kubernetes.Clientset
|
|
// if clientConfig is not nil, then we are not using etcd leader election
|
|
// we need to create non-timeout clientset for RetryWatcher
|
|
if clientConfig != nil {
|
|
rwConfig := *clientConfig
|
|
rwConfig.Timeout = 0
|
|
rwClientSet, err = k8s.NewClientset(&rwConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not create k8s clientset for retry watcher: %w", err)
|
|
}
|
|
}
|
|
|
|
// Flip this to something else
|
|
// if config.DetectControlPlane {
|
|
// log.Info("[k8s client] flipping to internal service account")
|
|
// _, err = clientset.CoreV1().ServiceAccounts("kube-system").Apply(context.TODO(), kubevip.GenerateSA(), v1.ApplyOptions{FieldManager: "application/apply-patch"})
|
|
// if err != nil {
|
|
// return nil, fmt.Errorf("could not create k8s clientset from incluster config: %v", err)
|
|
// }
|
|
// _, err = clientset.RbacV1().ClusterRoles().Apply(context.TODO(), kubevip.GenerateCR(), v1.ApplyOptions{FieldManager: "application/apply-patch"})
|
|
// if err != nil {
|
|
// return nil, fmt.Errorf("could not create k8s clientset from incluster config: %v", err)
|
|
// }
|
|
// _, err = clientset.RbacV1().ClusterRoleBindings().Apply(context.TODO(), kubevip.GenerateCRB(), v1.ApplyOptions{FieldManager: "application/apply-patch"})
|
|
// if err != nil {
|
|
// return nil, fmt.Errorf("could not create k8s clientset from incluster config: %v", err)
|
|
// }
|
|
// }
|
|
|
|
// listen for interrupts or the Linux SIGTERM signal and cancel
|
|
// our context, which the leader election code will observe and
|
|
// step down
|
|
signalChan := make(chan os.Signal, 1)
|
|
// Add Notification for Userland interrupt
|
|
signal.Notify(signalChan, syscall.SIGINT)
|
|
|
|
// Add Notification for SIGTERM (sent from Kubernetes)
|
|
signal.Notify(signalChan, syscall.SIGTERM)
|
|
|
|
// Add Notification for SIGUSR1 (for configuration dump)
|
|
signal.Notify(signalChan, syscall.SIGUSR1)
|
|
|
|
intfMgr := networkinterface.NewManager()
|
|
arpMgr := arp.NewManager(config)
|
|
|
|
// create the node label manager
|
|
// constructor will decide if it should be a noop or not
|
|
nodeLabelManager := node.NewManager(config, clientset)
|
|
|
|
var bgpServer *bgp.Server
|
|
// If BGP is enabled then we start a server instance that will broadcast VIPs
|
|
if config.EnableBGP {
|
|
var err error
|
|
// If Annotations have been set then we will look them up
|
|
if config.Annotations != "" {
|
|
err = annotationsWatcher(ctx, clientset, rwClientSet, config)
|
|
} else {
|
|
log.Debug("No Node annotations to parse")
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bgpServer, err = bgp.NewBGPServer(config.BGPConfig, log.Level(config.Logging))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating BGP server: %w", err)
|
|
}
|
|
}
|
|
|
|
electionMgr, err := election.NewManager(config, clientset, rwClientSet)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating election manager: %w", err)
|
|
}
|
|
|
|
leaseMgr := lease.NewManager()
|
|
routeMgr := route.NewManager()
|
|
|
|
svcProcessor := services.NewServicesProcessor(config, bgpServer, clientset, rwClientSet,
|
|
intfMgr, arpMgr, nodeLabelManager, electionMgr, leaseMgr, routeMgr)
|
|
|
|
return &Manager{
|
|
clientSet: clientset,
|
|
rwClientSet: rwClientSet,
|
|
configMap: configMap,
|
|
config: config,
|
|
countServiceWatchEvent: prometheus.NewCounterVec(prometheus.CounterOpts{
|
|
Namespace: "kube_vip",
|
|
Subsystem: "manager",
|
|
Name: "all_services_events",
|
|
Help: "Count all events fired by the service watcher categorised by event type",
|
|
}, []string{"type"}),
|
|
signalChan: signalChan,
|
|
svcProcessor: svcProcessor,
|
|
intfMgr: intfMgr,
|
|
arpMgr: arpMgr,
|
|
bgpServer: bgpServer,
|
|
nodeLabelManager: nodeLabelManager,
|
|
electionMgr: electionMgr,
|
|
leaseMgr: leaseMgr,
|
|
routeMgr: routeMgr,
|
|
}, nil
|
|
}
|
|
|
|
// Start will begin the Manager, which will start services and watch the configmap
|
|
func (sm *Manager) Start(ctx context.Context) error {
|
|
wg := sync.WaitGroup{}
|
|
defer wg.Wait()
|
|
|
|
// HealthCheck
|
|
if sm.config.HealthCheckPort != 0 {
|
|
if sm.config.HealthCheckPort < 1024 {
|
|
return fmt.Errorf("healthcheck port is using a port that is less than 1024 [%d]", sm.config.HealthCheckPort)
|
|
}
|
|
http.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
|
fmt.Fprintf(w, "OK")
|
|
})
|
|
wg.Go(func() {
|
|
server := &http.Server{
|
|
Addr: fmt.Sprintf(":%d", sm.config.HealthCheckPort),
|
|
ReadHeaderTimeout: 3 * time.Second,
|
|
}
|
|
err := server.ListenAndServe()
|
|
if err != nil {
|
|
log.Error("healthcheck", "unable to start", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// on exit, clean up the node labels
|
|
defer func() {
|
|
if err := sm.nodeLabelManager.CleanUpLabels(10 * time.Second); err != nil {
|
|
log.Error("CleanUpNodeLabels", "unable to cleanup node labels", err)
|
|
}
|
|
}()
|
|
|
|
if sm.config.EnableARP || sm.config.EnableWireguard {
|
|
if sm.config.EnableUPNP {
|
|
clients := upnp.GetConnectionClients(ctx)
|
|
if len(clients) == 0 {
|
|
log.Error("Error Enabling UPNP. No Clients found")
|
|
// Set the struct to false so nothing should use it in future
|
|
sm.config.EnableUPNP = false
|
|
} else {
|
|
for _, c := range clients {
|
|
ip, err := c.GetExternalIPAddress()
|
|
if err != nil {
|
|
log.Error("unable to find IGD2 Gateway address", "err", err)
|
|
}
|
|
log.Info("Found UPNP IGD2 Gateway address", "ip", ip)
|
|
}
|
|
}
|
|
// TODO: It would be nice to run the UPNP refresh only on the leader.
|
|
wg.Go(func() {
|
|
sm.svcProcessor.RefreshUPNPForwards(ctx)
|
|
})
|
|
}
|
|
}
|
|
|
|
return sm.startMode(ctx)
|
|
}
|
|
|
|
// Start will begin the Manager, which will start services and watch the configmap
|
|
func (sm *Manager) startMode(ctx context.Context) error {
|
|
var cpCluster *cluster.Cluster
|
|
var err error
|
|
|
|
w := worker.New(sm.arpMgr, sm.intfMgr, sm.config, &sm.closing, sm.Kill,
|
|
sm.svcProcessor, &sm.mutex, sm.clientSet, sm.bgpServer, sm.electionMgr,
|
|
sm.leaseMgr, sm.routeMgr, sm.nodeLabelManager)
|
|
|
|
// use a Go context so we can tell the leaderelection code when we
|
|
// want to step down
|
|
wg := sync.WaitGroup{}
|
|
modeCtx, cancel := context.WithCancel(ctx)
|
|
defer func() {
|
|
|
|
wg.Wait()
|
|
w.Cleanup()
|
|
cancel()
|
|
log.Info("Shutting down Kube-Vip")
|
|
}()
|
|
|
|
log.Info("starting Kube-vip Manager", "mode", w.Name())
|
|
if err := w.Configure(modeCtx, &wg); err != nil {
|
|
defer cancel()
|
|
return fmt.Errorf("failed to configure %s mode: %w", w.Name(), err)
|
|
}
|
|
|
|
if sm.config.EnableControlPlane {
|
|
err = w.InitControlPlane()
|
|
if err != nil {
|
|
defer cancel()
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Shutdown function that will wait on this signal, unless we call it ourselves
|
|
wg.Go(func() {
|
|
sm.waitForShutdown(modeCtx, cancel, cpCluster)
|
|
})
|
|
|
|
if sm.config.EnableControlPlane {
|
|
wg.Go(func() {
|
|
w.StartControlPlane(modeCtx, sm.electionMgr)
|
|
})
|
|
}
|
|
|
|
if sm.config.EnableServices {
|
|
// This will tidy any dangling kube-vip iptables rules
|
|
if sm.config.EgressClean {
|
|
tableName := nftables.EgressTableBaseNameForInstance(sm.config.InstanceName)
|
|
err := nftables.ClearTablesWithName(tableName)
|
|
if err != nil {
|
|
log.Warn("[egress]", "mode", "nftables-internal", "clearing error", err)
|
|
} else {
|
|
log.Info("[egress]", "mode", "nftables-internal", "tables", "cleared")
|
|
}
|
|
// TODO: Deprecate the iptables code v1.2.x
|
|
err = vip.ClearIPTables(sm.config.EgressWithNftables, sm.config.ServiceNamespace, iptables.ProtocolIPv4)
|
|
if err != nil {
|
|
log.Info("[egress]", "legacy-iptables", sm.config.EgressWithNftables, "mode", "IPv4", "error", err)
|
|
}
|
|
err = vip.ClearIPTables(sm.config.EgressWithNftables, sm.config.ServiceNamespace, iptables.ProtocolIPv6)
|
|
if err != nil {
|
|
log.Info("[egress]", "legacy-iptables", sm.config.EgressWithNftables, "mode", "IPv6", "error", err)
|
|
}
|
|
}
|
|
w.ConfigureServices()
|
|
|
|
for {
|
|
select {
|
|
case <-modeCtx.Done():
|
|
return nil
|
|
default:
|
|
if err = w.StartServices(modeCtx); err != nil {
|
|
if utils.IsPanicError(err) {
|
|
sm.Kill()
|
|
return fmt.Errorf("failed to reconcile services, non-recoverable error: %w", err)
|
|
} else {
|
|
log.Error("failed to reconcile services, restarting", "error", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (sm *Manager) waitForShutdown(ctx context.Context, cancel context.CancelFunc, cpCluster *cluster.Cluster) {
|
|
for {
|
|
sig := <-sm.signalChan
|
|
switch sig {
|
|
case syscall.SIGUSR1:
|
|
log.Info("Received SIGUSR1, dumping configuration")
|
|
sm.dumpConfiguration(ctx)
|
|
case syscall.SIGINT, syscall.SIGTERM:
|
|
sm.closing.Store(true)
|
|
log.Info("Received kube-vip termination, signaling shutdown")
|
|
if cpCluster != nil {
|
|
cpCluster.Stop()
|
|
}
|
|
// Cancel the context, which will in turn cancel the leadership and all goroutines
|
|
cancel()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (sm *Manager) Kill() {
|
|
sm.sigint.Do(func() {
|
|
sm.signalChan <- syscall.SIGINT
|
|
})
|
|
}
|
|
|
|
// normalizeNodeName ensures the local machine hostname conforms to
|
|
// Kubernetes RFC1123 node naming conventions (lowercase).
|
|
func normalizeNodeName(name string) string {
|
|
return strings.ToLower(name)
|
|
}
|