mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-06-05 14:55:50 -04:00
This is the last step in the process, simply linking ReleaseOnCancel to ControllerManagerReleaseLeaderElectionLockOnExit feature gate. The original logic when the leader election release on exit feature gate is disabled is to not catch signals and exit immediately when the LE lock is lost. This is being put back into place so that the new behavior can be tested without affecting the former approach at all.
878 lines
35 KiB
Go
878 lines
35 KiB
Go
/*
|
|
Copyright 2014 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 implements a server that runs a set of active
|
|
// components. This includes replication controllers, service endpoints and
|
|
// nodes.
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"net/http"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/blang/semver/v4"
|
|
"github.com/spf13/cobra"
|
|
|
|
coordinationv1 "k8s.io/api/coordination/v1"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
"k8s.io/apimachinery/pkg/util/uuid"
|
|
"k8s.io/apimachinery/pkg/util/wait"
|
|
"k8s.io/apiserver/pkg/server"
|
|
"k8s.io/apiserver/pkg/server/flagz"
|
|
"k8s.io/apiserver/pkg/server/healthz"
|
|
"k8s.io/apiserver/pkg/server/mux"
|
|
"k8s.io/apiserver/pkg/server/statusz"
|
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
|
cacheddiscovery "k8s.io/client-go/discovery/cached/memory"
|
|
"k8s.io/client-go/informers"
|
|
"k8s.io/client-go/kubernetes"
|
|
v1core "k8s.io/client-go/kubernetes/typed/core/v1"
|
|
"k8s.io/client-go/metadata"
|
|
"k8s.io/client-go/metadata/metadatainformer"
|
|
restclient "k8s.io/client-go/rest"
|
|
"k8s.io/client-go/restmapper"
|
|
"k8s.io/client-go/tools/cache"
|
|
"k8s.io/client-go/tools/leaderelection"
|
|
"k8s.io/client-go/tools/leaderelection/resourcelock"
|
|
certutil "k8s.io/client-go/util/cert"
|
|
cliflag "k8s.io/component-base/cli/flag"
|
|
"k8s.io/component-base/cli/globalflag"
|
|
basecompatibility "k8s.io/component-base/compatibility"
|
|
"k8s.io/component-base/configz"
|
|
"k8s.io/component-base/featuregate"
|
|
"k8s.io/component-base/logs"
|
|
logsapi "k8s.io/component-base/logs/api/v1"
|
|
metricsfeatures "k8s.io/component-base/metrics/features"
|
|
controllersmetrics "k8s.io/component-base/metrics/prometheus/controllers"
|
|
"k8s.io/component-base/metrics/prometheus/slis"
|
|
"k8s.io/component-base/term"
|
|
utilversion "k8s.io/component-base/version"
|
|
"k8s.io/component-base/version/verflag"
|
|
zpagesfeatures "k8s.io/component-base/zpages/features"
|
|
genericcontrollermanager "k8s.io/controller-manager/app"
|
|
"k8s.io/controller-manager/controller"
|
|
"k8s.io/controller-manager/pkg/clientbuilder"
|
|
cmfeatures "k8s.io/controller-manager/pkg/features"
|
|
controllerhealthz "k8s.io/controller-manager/pkg/healthz"
|
|
"k8s.io/controller-manager/pkg/informerfactory"
|
|
"k8s.io/controller-manager/pkg/leadermigration"
|
|
"k8s.io/klog/v2"
|
|
configv1alpha1 "k8s.io/kube-controller-manager/config/v1alpha1"
|
|
"k8s.io/kubernetes/cmd/kube-controller-manager/app/config"
|
|
"k8s.io/kubernetes/cmd/kube-controller-manager/app/options"
|
|
"k8s.io/kubernetes/cmd/kube-controller-manager/names"
|
|
kubectrlmgrconfig "k8s.io/kubernetes/pkg/controller/apis/config"
|
|
configv1alpha1conversion "k8s.io/kubernetes/pkg/controller/apis/config/v1alpha1"
|
|
garbagecollector "k8s.io/kubernetes/pkg/controller/garbagecollector"
|
|
kubefeatures "k8s.io/kubernetes/pkg/features"
|
|
)
|
|
|
|
func init() {
|
|
utilruntime.Must(logsapi.AddFeatureGates(utilfeature.DefaultMutableFeatureGate))
|
|
utilruntime.Must(metricsfeatures.AddFeatureGates(utilfeature.DefaultMutableFeatureGate))
|
|
}
|
|
|
|
const (
|
|
// ControllerStartJitter is the Jitter used when starting controller managers
|
|
ControllerStartJitter = 1.0
|
|
// ConfigzName is the name used for register kube-controller manager /configz, same with GroupName.
|
|
ConfigzName = "kubecontrollermanager.config.k8s.io"
|
|
// kubeControllerManager defines variable used internally when referring to cloud-controller-manager component
|
|
kubeControllerManager = "kube-controller-manager"
|
|
)
|
|
|
|
// NewControllerManagerCommand creates a *cobra.Command object with default parameters
|
|
func NewControllerManagerCommand() *cobra.Command {
|
|
s, err := options.NewKubeControllerManagerOptions()
|
|
if err != nil {
|
|
klog.Background().Error(err, "Unable to initialize command options")
|
|
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
|
|
}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: kubeControllerManager,
|
|
Long: `The Kubernetes controller manager is a daemon that embeds
|
|
the core control loops shipped with Kubernetes. In applications of robotics and
|
|
automation, a control loop is a non-terminating loop that regulates the state of
|
|
the system. In Kubernetes, a controller is a control loop that watches the shared
|
|
state of the cluster through the apiserver and makes changes attempting to move the
|
|
current state towards the desired state. Examples of controllers that ship with
|
|
Kubernetes today are the replication controller, endpoints controller, namespace
|
|
controller, and serviceaccounts controller.`,
|
|
PersistentPreRunE: func(*cobra.Command, []string) error {
|
|
// silence client-go warnings.
|
|
// kube-controller-manager generically watches APIs (including deprecated ones),
|
|
// and CI ensures it works properly against matching kube-apiserver versions.
|
|
restclient.SetDefaultWarningHandler(restclient.NoWarnings{})
|
|
// makes sure feature gates are set before RunE.
|
|
return s.ComponentGlobalsRegistry.Set()
|
|
},
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
verflag.PrintAndExitIfRequested()
|
|
|
|
// Activate logging as soon as possible, after that
|
|
// show flags with the final logging configuration.
|
|
if err := logsapi.ValidateAndApply(s.Logs, utilfeature.DefaultFeatureGate); err != nil {
|
|
return err
|
|
}
|
|
cliflag.PrintFlags(cmd.Flags())
|
|
|
|
// We use context.Background() here still because using server.SetupSignalContext() would cause
|
|
// components like the event broadcaster to terminate on signal immediately, which is not what we want.
|
|
// Termination for that case is being handled explicitly in Run() later on.
|
|
ctx := context.Background()
|
|
c, err := s.Config(ctx, KnownControllers(), ControllersDisabledByDefault(), ControllerAliases())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// add feature enablement metrics
|
|
fg := s.ComponentGlobalsRegistry.FeatureGateFor(basecompatibility.DefaultKubeComponent)
|
|
fg.(featuregate.MutableFeatureGate).AddMetrics()
|
|
// add component version metrics
|
|
s.ComponentGlobalsRegistry.AddMetrics()
|
|
|
|
if utilfeature.DefaultFeatureGate.Enabled(cmfeatures.ControllerManagerReleaseLeaderElectionLockOnExit) {
|
|
ctx = server.SetupSignalContext()
|
|
}
|
|
return Run(ctx, c.Complete())
|
|
},
|
|
Args: func(cmd *cobra.Command, args []string) error {
|
|
for _, arg := range args {
|
|
if len(arg) > 0 {
|
|
return fmt.Errorf("%q does not take any arguments, got %q", cmd.CommandPath(), args)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
fs := cmd.Flags()
|
|
namedFlagSets := s.Flags(KnownControllers(), ControllersDisabledByDefault(), ControllerAliases())
|
|
s.ParsedFlags = &namedFlagSets
|
|
verflag.AddFlags(namedFlagSets.FlagSet("global"))
|
|
globalflag.AddGlobalFlags(namedFlagSets.FlagSet("global"), cmd.Name(), logs.SkipLoggingConfigurationFlags())
|
|
for _, f := range namedFlagSets.FlagSets {
|
|
fs.AddFlagSet(f)
|
|
}
|
|
|
|
cols, _, _ := term.TerminalSize(cmd.OutOrStdout())
|
|
cliflag.SetUsageAndHelpFunc(cmd, namedFlagSets, cols)
|
|
|
|
return cmd
|
|
}
|
|
|
|
// ResyncPeriod returns a function which generates a duration each time it is
|
|
// invoked; this is because that multiple controllers don't get into lock-step.
|
|
func ResyncPeriod(c *config.CompletedConfig) func() time.Duration {
|
|
return func() time.Duration {
|
|
factor := rand.Float64() + 1
|
|
return time.Duration(float64(c.ComponentConfig.Generic.MinResyncPeriod.Nanoseconds()) * factor)
|
|
}
|
|
}
|
|
|
|
// Run runs the KubeControllerManagerOptions.
|
|
func Run(ctx context.Context, c *config.CompletedConfig) error {
|
|
logger := klog.FromContext(ctx)
|
|
stopCh := ctx.Done()
|
|
|
|
// To help debugging, immediately log version
|
|
logger.Info("Starting", "version", utilversion.Get())
|
|
|
|
logger.Info("Golang settings", "GOGC", os.Getenv("GOGC"), "GOMAXPROCS", os.Getenv("GOMAXPROCS"), "GOTRACEBACK", os.Getenv("GOTRACEBACK"))
|
|
|
|
// Start events processing pipeline.
|
|
c.EventBroadcaster.StartStructuredLogging(0)
|
|
c.EventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: c.Client.CoreV1().Events("")})
|
|
defer c.EventBroadcaster.Shutdown()
|
|
|
|
externalConfig := &configv1alpha1.KubeControllerManagerConfiguration{}
|
|
if err := configv1alpha1conversion.Convert_config_KubeControllerManagerConfiguration_To_v1alpha1_KubeControllerManagerConfiguration(&c.ComponentConfig, externalConfig, nil); err != nil {
|
|
return fmt.Errorf("unable to convert configz: %w", err)
|
|
}
|
|
externalConfig.SetGroupVersionKind(configv1alpha1.SchemeGroupVersion.WithKind("KubeControllerManagerConfiguration"))
|
|
|
|
if cfgz, err := configz.New(ConfigzName); err != nil {
|
|
return fmt.Errorf("unable to register configz: %w", err)
|
|
} else if err := cfgz.Set(externalConfig); err != nil {
|
|
return fmt.Errorf("unable to set configz: %w", err)
|
|
}
|
|
|
|
// Setup any healthz checks we will want to use.
|
|
var checks []healthz.HealthChecker
|
|
var electionChecker *leaderelection.HealthzAdaptor
|
|
if c.ComponentConfig.Generic.LeaderElection.LeaderElect {
|
|
electionChecker = leaderelection.NewLeaderHealthzAdaptor(time.Second * 20)
|
|
checks = append(checks, electionChecker)
|
|
}
|
|
healthzHandler := controllerhealthz.NewMutableHealthzHandler(checks...)
|
|
|
|
// Start the controller manager HTTP server
|
|
// unsecuredMux is the handler for these controller *after* authn/authz filters have been applied
|
|
var unsecuredMux *mux.PathRecorderMux
|
|
if c.SecureServing != nil {
|
|
unsecuredMux = genericcontrollermanager.NewBaseHandler(&c.ComponentConfig.Generic.Debugging, healthzHandler)
|
|
slis.SLIMetricsWithReset{}.Install(unsecuredMux)
|
|
if utilfeature.DefaultFeatureGate.Enabled(zpagesfeatures.ComponentFlagz) {
|
|
if c.Flagz != nil {
|
|
flagz.Install(unsecuredMux, kubeControllerManager, c.Flagz)
|
|
}
|
|
}
|
|
|
|
if utilfeature.DefaultFeatureGate.Enabled(zpagesfeatures.ComponentStatusz) {
|
|
statusz.Install(
|
|
unsecuredMux,
|
|
kubeControllerManager,
|
|
statusz.NewRegistry(
|
|
c.ComponentGlobalsRegistry.EffectiveVersionFor(basecompatibility.DefaultKubeComponent),
|
|
statusz.WithListedPaths(unsecuredMux.ListedPaths()),
|
|
),
|
|
)
|
|
}
|
|
|
|
handler := genericcontrollermanager.BuildHandlerChain(unsecuredMux, &c.Authorization, &c.Authentication)
|
|
// TODO: handle stoppedCh and listenerStoppedCh returned by c.SecureServing.Serve
|
|
if _, _, err := c.SecureServing.Serve(handler, 0, stopCh); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
clientBuilder, rootClientBuilder := createClientBuilders(c)
|
|
|
|
saTokenControllerDescriptor := newServiceAccountTokenControllerDescriptor(rootClientBuilder)
|
|
|
|
run := func(ctx context.Context, controllerDescriptors map[string]*ControllerDescriptor) error {
|
|
controllerContext, err := CreateControllerContext(ctx, c, rootClientBuilder, clientBuilder)
|
|
if err != nil {
|
|
logger.Error(err, "Error building controller context")
|
|
return err
|
|
}
|
|
|
|
// Prepare all controllers in advance.
|
|
controllers, err := BuildControllers(ctx, controllerContext, controllerDescriptors, unsecuredMux, healthzHandler)
|
|
if err != nil {
|
|
logger.Error(err, "Error building controllers")
|
|
return err
|
|
}
|
|
|
|
// Start the informers.
|
|
stopCh := ctx.Done()
|
|
controllerContext.InformerFactory.Start(stopCh)
|
|
defer controllerContext.InformerFactory.Shutdown()
|
|
controllerContext.ObjectOrMetadataInformerFactory.Start(stopCh)
|
|
close(controllerContext.InformersStarted)
|
|
|
|
// Actually start the controllers.
|
|
if len(controllers) > 0 {
|
|
if !RunControllers(ctx, controllerContext, controllers, ControllerStartJitter, c.ControllerShutdownTimeout) {
|
|
return errors.New("controller shutdown timeout reached")
|
|
}
|
|
} else {
|
|
<-ctx.Done()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// No leader election, run directly
|
|
if !c.ComponentConfig.Generic.LeaderElection.LeaderElect {
|
|
controllerDescriptors := NewControllerDescriptors()
|
|
controllerDescriptors[names.ServiceAccountTokenController] = saTokenControllerDescriptor
|
|
return run(ctx, controllerDescriptors)
|
|
}
|
|
|
|
id, err := os.Hostname()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// add a uniquifier so that two processes on the same host don't accidentally both become active
|
|
id = id + "_" + string(uuid.NewUUID())
|
|
|
|
// leaderMigrator will be non-nil if and only if Leader Migration is enabled.
|
|
var leaderMigrator *leadermigration.LeaderMigrator = nil
|
|
|
|
// If leader migration is enabled, create the LeaderMigrator and prepare for migration
|
|
if leadermigration.Enabled(&c.ComponentConfig.Generic) {
|
|
logger.Info("starting leader migration")
|
|
|
|
leaderMigrator = leadermigration.NewLeaderMigrator(&c.ComponentConfig.Generic.LeaderMigration,
|
|
kubeControllerManager)
|
|
|
|
// startSATokenControllerInit is the original constructor.
|
|
saTokenControllerInit := saTokenControllerDescriptor.GetControllerConstructor()
|
|
|
|
// Wrap saTokenControllerDescriptor to signal readiness for migration after starting the controller.
|
|
saTokenControllerDescriptor.constructor = func(ctx context.Context, controllerContext ControllerContext, controllerName string) (Controller, error) {
|
|
ctrl, err := saTokenControllerInit(ctx, controllerContext, controllerName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// This wrapping is not exactly flawless as RunControllers uses type casting,
|
|
// which is now not possible for the wrapped controller.
|
|
// This fortunately doesn't matter for this particular controller.
|
|
return newControllerLoop(func(ctx context.Context) {
|
|
close(leaderMigrator.MigrationReady)
|
|
ctrl.Run(ctx)
|
|
}, controllerName), nil
|
|
}
|
|
}
|
|
|
|
if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.CoordinatedLeaderElection) {
|
|
binaryVersion, err := semver.ParseTolerant(c.ComponentGlobalsRegistry.EffectiveVersionFor(basecompatibility.DefaultKubeComponent).BinaryVersion().String())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
emulationVersion, err := semver.ParseTolerant(c.ComponentGlobalsRegistry.EffectiveVersionFor(basecompatibility.DefaultKubeComponent).EmulationVersion().String())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Start lease candidate controller for coordinated leader election
|
|
leaseCandidate, waitForSync, err := leaderelection.NewCandidate(
|
|
c.Client,
|
|
"kube-system",
|
|
id,
|
|
kubeControllerManager,
|
|
binaryVersion.FinalizeVersion(),
|
|
emulationVersion.FinalizeVersion(),
|
|
coordinationv1.OldestEmulationVersion,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
healthzHandler.AddHealthChecker(healthz.NewInformerSyncHealthz(waitForSync))
|
|
|
|
go leaseCandidate.Run(ctx)
|
|
}
|
|
|
|
// Start the main lock.
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
// startedLeading must be used to wrap any OnStartedLeading leader election callback.
|
|
var (
|
|
errs []error
|
|
errsLock sync.Mutex
|
|
)
|
|
startedLeading := func(next func(context.Context) error) func(context.Context) {
|
|
return func(ctx context.Context) {
|
|
// It's more efficient to cancel the context at the end of OnStartedLeading to signal termination,
|
|
// because OnStoppedLeading is only called once the LE lock is released.
|
|
defer cancel()
|
|
if err := next(ctx); err != nil && !errors.Is(err, context.Canceled) {
|
|
errsLock.Lock()
|
|
errs = append(errs, err)
|
|
errsLock.Unlock()
|
|
}
|
|
}
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Go(func() {
|
|
leaderElectAndRun(ctx, c, id, electionChecker,
|
|
c.ComponentConfig.Generic.LeaderElection.ResourceLock,
|
|
c.ComponentConfig.Generic.LeaderElection.ResourceName,
|
|
leaderelection.LeaderCallbacks{
|
|
OnStartedLeading: startedLeading(func(ctx context.Context) error {
|
|
controllerDescriptors := NewControllerDescriptors()
|
|
if leaderMigrator != nil {
|
|
// If leader migration is enabled, we should start only non-migrated controllers
|
|
// for the main lock.
|
|
controllerDescriptors = filteredControllerDescriptors(controllerDescriptors, leaderMigrator.FilterFunc, leadermigration.ControllerNonMigrated)
|
|
logger.Info("leader migration: starting main controllers.")
|
|
}
|
|
controllerDescriptors[names.ServiceAccountTokenController] = saTokenControllerDescriptor
|
|
return run(ctx, controllerDescriptors)
|
|
}),
|
|
OnStoppedLeading: func() {
|
|
logger.Error(nil, "leaderelection lost/stopped")
|
|
if !utilfeature.DefaultFeatureGate.Enabled(cmfeatures.ControllerManagerReleaseLeaderElectionLockOnExit) {
|
|
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
|
|
}
|
|
},
|
|
})
|
|
})
|
|
|
|
// If Leader Migration is enabled, proceed to attempt the migration lock.
|
|
if leaderMigrator != nil {
|
|
// Wait for Service Account Token Controller to start before acquiring the migration lock.
|
|
// At this point, the main lock must have already been acquired, or the KCM process already exited.
|
|
// We wait for the main lock before acquiring the migration lock to prevent the situation
|
|
// where KCM instance A holds the main lock while KCM instance B holds the migration lock.
|
|
select {
|
|
case <-leaderMigrator.MigrationReady:
|
|
// Start the migration lock.
|
|
wg.Go(func() {
|
|
leaderElectAndRun(ctx, c, id, electionChecker,
|
|
c.ComponentConfig.Generic.LeaderMigration.ResourceLock,
|
|
c.ComponentConfig.Generic.LeaderMigration.LeaderName,
|
|
leaderelection.LeaderCallbacks{
|
|
OnStartedLeading: startedLeading(func(ctx context.Context) error {
|
|
logger.Info("leader migration: starting migrated controllers.")
|
|
controllerDescriptors := NewControllerDescriptors()
|
|
controllerDescriptors = filteredControllerDescriptors(controllerDescriptors, leaderMigrator.FilterFunc, leadermigration.ControllerMigrated)
|
|
// DO NOT start saTokenController under migration lock
|
|
delete(controllerDescriptors, names.ServiceAccountTokenController)
|
|
return run(ctx, controllerDescriptors)
|
|
}),
|
|
OnStoppedLeading: func() {
|
|
logger.Error(nil, "migration leaderelection lost/stopped")
|
|
if !utilfeature.DefaultFeatureGate.Enabled(cmfeatures.ControllerManagerReleaseLeaderElectionLockOnExit) {
|
|
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
|
|
}
|
|
},
|
|
})
|
|
})
|
|
|
|
case <-ctx.Done():
|
|
}
|
|
}
|
|
|
|
// Block until all leader elections are stopped.
|
|
wg.Wait()
|
|
// There is no need to hold errsLock since by this time all goroutines have terminated.
|
|
return utilerrors.NewAggregate(errs)
|
|
}
|
|
|
|
// ControllerContext defines the context object for controller
|
|
type ControllerContext struct {
|
|
// ClientBuilder will provide a client for this controller to use
|
|
ClientBuilder clientbuilder.ControllerClientBuilder
|
|
|
|
// InformerFactory gives access to informers for the controller.
|
|
InformerFactory informers.SharedInformerFactory
|
|
|
|
// ObjectOrMetadataInformerFactory gives access to informers for typed resources
|
|
// and dynamic resources by their metadata. All generic controllers currently use
|
|
// object metadata - if a future controller needs access to the full object this
|
|
// would become GenericInformerFactory and take a dynamic client.
|
|
ObjectOrMetadataInformerFactory informerfactory.InformerFactory
|
|
|
|
// ComponentConfig provides access to init options for a given controller
|
|
ComponentConfig kubectrlmgrconfig.KubeControllerManagerConfiguration
|
|
|
|
// DeferredDiscoveryRESTMapper is a RESTMapper that will defer
|
|
// initialization of the RESTMapper until the first mapping is
|
|
// requested.
|
|
RESTMapper *restmapper.DeferredDiscoveryRESTMapper
|
|
|
|
// InformersStarted is closed after all of the controllers have been initialized and are running. After this point it is safe,
|
|
// for an individual controller to start the shared informers. Before it is closed, they should not.
|
|
InformersStarted chan struct{}
|
|
|
|
// ResyncPeriod generates a duration each time it is invoked; this is so that
|
|
// multiple controllers don't get into lock-step and all hammer the apiserver
|
|
// with list requests simultaneously.
|
|
ResyncPeriod func() time.Duration
|
|
|
|
// ControllerManagerMetrics provides a proxy to set controller manager specific metrics.
|
|
ControllerManagerMetrics *controllersmetrics.ControllerManagerMetrics
|
|
|
|
// GraphBuilder gives an access to dependencyGraphBuilder which keeps tracks of resources in the cluster
|
|
GraphBuilder *garbagecollector.GraphBuilder
|
|
}
|
|
|
|
// IsControllerEnabled checks if the context's controllers enabled or not
|
|
func (c ControllerContext) IsControllerEnabled(controllerDescriptor *ControllerDescriptor) bool {
|
|
controllersDisabledByDefault := sets.NewString()
|
|
if controllerDescriptor.IsDisabledByDefault() {
|
|
controllersDisabledByDefault.Insert(controllerDescriptor.Name())
|
|
}
|
|
return genericcontrollermanager.IsControllerEnabled(controllerDescriptor.Name(), controllersDisabledByDefault, c.ComponentConfig.Generic.Controllers)
|
|
}
|
|
|
|
// NewClientConfig is a shortcut for ClientBuilder.Config. It wraps the error with an additional message.
|
|
func (c ControllerContext) NewClientConfig(name string) (*restclient.Config, error) {
|
|
config, err := c.ClientBuilder.Config(name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create Kubernetes client config for %q: %w", name, err)
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
// NewClient is a shortcut for ClientBuilder.Client. It wraps the error with an additional message.
|
|
func (c ControllerContext) NewClient(name string) (kubernetes.Interface, error) {
|
|
client, err := c.ClientBuilder.Client(name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create Kubernetes client for %q: %w", name, err)
|
|
}
|
|
return client, nil
|
|
}
|
|
|
|
// CreateControllerContext creates a context struct containing references to resources needed by the
|
|
// controllers such as the cloud provider and clientBuilder. rootClientBuilder is only used for
|
|
// the shared-informers client and token controller.
|
|
func CreateControllerContext(ctx context.Context, s *config.CompletedConfig, rootClientBuilder, clientBuilder clientbuilder.ControllerClientBuilder) (ControllerContext, error) {
|
|
// Informer transform to trim ManagedFields for memory efficiency.
|
|
trim := func(obj interface{}) (interface{}, error) {
|
|
if accessor, err := meta.Accessor(obj); err == nil {
|
|
if accessor.GetManagedFields() != nil {
|
|
accessor.SetManagedFields(nil)
|
|
}
|
|
}
|
|
return obj, nil
|
|
}
|
|
|
|
versionedClient, err := rootClientBuilder.Client("shared-informers")
|
|
if err != nil {
|
|
return ControllerContext{}, fmt.Errorf("failed to create Kubernetes client for %q: %w", "shared-informers", err)
|
|
}
|
|
|
|
informerName, err := cache.NewInformerName("kube-controller-manager")
|
|
if err != nil {
|
|
return ControllerContext{}, fmt.Errorf("failed to create informer name: %w", err)
|
|
}
|
|
|
|
sharedInformers := informers.NewSharedInformerFactoryWithOptions(versionedClient, ResyncPeriod(s)(), informers.WithTransform(trim), informers.WithInformerName(informerName))
|
|
|
|
metadataConfig, err := rootClientBuilder.Config("metadata-informers")
|
|
if err != nil {
|
|
return ControllerContext{}, fmt.Errorf("failed to create metadata client config: %w", err)
|
|
}
|
|
|
|
metadataClient, err := metadata.NewForConfig(metadataConfig)
|
|
if err != nil {
|
|
return ControllerContext{}, fmt.Errorf("failed to create metadata client: %w", err)
|
|
}
|
|
|
|
metadataInformers := metadatainformer.NewSharedInformerFactoryWithOptions(metadataClient, ResyncPeriod(s)(), metadatainformer.WithTransform(trim))
|
|
|
|
// If apiserver is not running we should wait for some time and fail only then. This is particularly
|
|
// important when we start apiserver and controller manager at the same time.
|
|
if err := genericcontrollermanager.WaitForAPIServer(versionedClient, 10*time.Second); err != nil {
|
|
return ControllerContext{}, fmt.Errorf("failed to wait for apiserver being healthy: %w", err)
|
|
}
|
|
|
|
// Use a discovery client capable of being refreshed.
|
|
discoveryClient, err := rootClientBuilder.DiscoveryClient("controller-discovery")
|
|
if err != nil {
|
|
return ControllerContext{}, fmt.Errorf("failed to create discovery client: %w", err)
|
|
}
|
|
|
|
cachedClient := cacheddiscovery.NewMemCacheClient(discoveryClient)
|
|
restMapper := restmapper.NewDeferredDiscoveryRESTMapper(cachedClient)
|
|
go wait.Until(func() {
|
|
restMapper.Reset()
|
|
}, 30*time.Second, ctx.Done())
|
|
|
|
controllerContext := ControllerContext{
|
|
ClientBuilder: clientBuilder,
|
|
InformerFactory: sharedInformers,
|
|
ObjectOrMetadataInformerFactory: informerfactory.NewInformerFactory(sharedInformers, metadataInformers),
|
|
ComponentConfig: s.ComponentConfig,
|
|
RESTMapper: restMapper,
|
|
InformersStarted: make(chan struct{}),
|
|
ResyncPeriod: ResyncPeriod(s),
|
|
ControllerManagerMetrics: controllersmetrics.NewControllerManagerMetrics(kubeControllerManager),
|
|
}
|
|
|
|
if controllerContext.ComponentConfig.GarbageCollectorController.EnableGarbageCollector &&
|
|
controllerContext.IsControllerEnabled(NewControllerDescriptors()[names.GarbageCollectorController]) {
|
|
ignoredResources := make(map[schema.GroupResource]struct{})
|
|
for _, r := range controllerContext.ComponentConfig.GarbageCollectorController.GCIgnoredResources {
|
|
ignoredResources[schema.GroupResource{Group: r.Group, Resource: r.Resource}] = struct{}{}
|
|
}
|
|
|
|
controllerContext.GraphBuilder = garbagecollector.NewDependencyGraphBuilder(
|
|
ctx,
|
|
metadataClient,
|
|
controllerContext.RESTMapper,
|
|
ignoredResources,
|
|
controllerContext.ObjectOrMetadataInformerFactory,
|
|
controllerContext.InformersStarted,
|
|
)
|
|
}
|
|
|
|
controllersmetrics.Register()
|
|
return controllerContext, nil
|
|
}
|
|
|
|
// HealthCheckAdder is an interface to represent a healthz handler.
|
|
// The extra level of indirection is useful for testing.
|
|
type HealthCheckAdder interface {
|
|
AddHealthChecker(checks ...healthz.HealthChecker)
|
|
}
|
|
|
|
// BuildControllers builds all controllers in the given descriptor map. Disabled controllers are obviously skipped.
|
|
//
|
|
// A health check is registered for each controller using the controller name. The default check always passes.
|
|
// If the controller implements controller.HealthCheckable, though, the given check is used.
|
|
// The controller can also implement controller.Debuggable, in which case the debug handler is registered with the given mux.
|
|
func BuildControllers(ctx context.Context, controllerCtx ControllerContext, controllerDescriptors map[string]*ControllerDescriptor,
|
|
unsecuredMux *mux.PathRecorderMux, healthzHandler HealthCheckAdder) ([]Controller, error) {
|
|
logger := klog.FromContext(ctx)
|
|
var (
|
|
controllers []Controller
|
|
checks []healthz.HealthChecker
|
|
)
|
|
buildController := func(controllerDesc *ControllerDescriptor) error {
|
|
controllerName := controllerDesc.Name()
|
|
ctrl, err := controllerDesc.BuildController(ctx, controllerCtx)
|
|
if err != nil {
|
|
logger.Error(err, "Error initializing a controller", "controller", controllerName)
|
|
return err
|
|
}
|
|
if ctrl == nil {
|
|
logger.Info("Warning: skipping controller", "controller", controllerName)
|
|
return nil
|
|
}
|
|
|
|
check := controllerhealthz.NamedPingChecker(controllerName)
|
|
// check if the controller supports and requests a debugHandler,
|
|
// and it needs the unsecuredMux to mount the handler onto.
|
|
if debuggable, ok := ctrl.(controller.Debuggable); ok && unsecuredMux != nil {
|
|
if debugHandler := debuggable.DebuggingHandler(); debugHandler != nil {
|
|
basePath := "/debug/controllers/" + controllerName
|
|
unsecuredMux.UnlistedHandle(basePath, http.StripPrefix(basePath, debugHandler))
|
|
unsecuredMux.UnlistedHandlePrefix(basePath+"/", http.StripPrefix(basePath, debugHandler))
|
|
}
|
|
}
|
|
if healthCheckable, ok := ctrl.(controller.HealthCheckable); ok {
|
|
if realCheck := healthCheckable.HealthChecker(); realCheck != nil {
|
|
check = controllerhealthz.NamedHealthChecker(controllerName, realCheck)
|
|
}
|
|
}
|
|
|
|
controllers = append(controllers, ctrl)
|
|
checks = append(checks, check)
|
|
return nil
|
|
}
|
|
|
|
// Always start the SA token controller first using a full-power client, since it needs to mint tokens for the rest
|
|
// If this fails, just return here and fail since other controllers won't be able to get credentials.
|
|
if serviceAccountTokenControllerDescriptor, ok := controllerDescriptors[names.ServiceAccountTokenController]; ok {
|
|
if err := buildController(serviceAccountTokenControllerDescriptor); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Each controller is passed a context where the logger has the name of
|
|
// the controller set through WithName. That name then becomes the prefix of
|
|
// all log messages emitted by that controller.
|
|
//
|
|
// In StartController, an explicit "controller" key is used instead, for two reasons:
|
|
// - while contextual logging is alpha, klog.LoggerWithName is still a no-op,
|
|
// so we cannot rely on it yet to add the name
|
|
// - it allows distinguishing between log entries emitted by the controller
|
|
// and those emitted for it - this is a bit debatable and could be revised.
|
|
for _, controllerDesc := range controllerDescriptors {
|
|
if controllerDesc.RequiresSpecialHandling() {
|
|
continue
|
|
}
|
|
|
|
if !controllerCtx.IsControllerEnabled(controllerDesc) {
|
|
logger.Info("Warning: controller is disabled", "controller", controllerDesc.Name())
|
|
continue
|
|
}
|
|
|
|
if err := buildController(controllerDesc); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Register the checks.
|
|
if len(checks) > 0 {
|
|
healthzHandler.AddHealthChecker(checks...)
|
|
}
|
|
return controllers, nil
|
|
}
|
|
|
|
// RunControllers runs all controllers concurrently and blocks until the context is cancelled and all controllers are terminated.
|
|
//
|
|
// Once the context is cancelled, RunControllers waits for shutdownTimeout for all controllers to terminate.
|
|
// When the timeout is reached, the function unblocks and returns false.
|
|
// Zero shutdown timeout means that there is no timeout.
|
|
func RunControllers(ctx context.Context, controllerCtx ControllerContext, controllers []Controller,
|
|
controllerStartJitterMaxFactor float64, shutdownTimeout time.Duration) bool {
|
|
logger := klog.FromContext(ctx)
|
|
|
|
// We gather running controllers names for logging purposes.
|
|
// When the context is cancelled, the controllers still running are logged periodically.
|
|
runningControllers := sets.New[string]()
|
|
var runningControllersLock sync.Mutex
|
|
|
|
loggingCtx, cancelLoggingCtx := context.WithCancel(context.Background())
|
|
defer cancelLoggingCtx()
|
|
go func() {
|
|
// Only start logging when terminating.
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-loggingCtx.Done():
|
|
return
|
|
}
|
|
|
|
// Regularly print the controllers that still haven't returned.
|
|
logPeriod := shutdownTimeout / 3
|
|
if logPeriod == 0 {
|
|
logPeriod = 5 * time.Second
|
|
}
|
|
ticker := time.NewTicker(logPeriod)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
runningControllersLock.Lock()
|
|
running := sets.List(runningControllers)
|
|
runningControllersLock.Unlock()
|
|
|
|
logger.Info("Still waiting for some controllers to terminate...", "runningControllers", running)
|
|
|
|
case <-loggingCtx.Done():
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
terminatedCh := make(chan struct{})
|
|
go func() {
|
|
defer close(terminatedCh)
|
|
var wg sync.WaitGroup
|
|
wg.Add(len(controllers))
|
|
for _, controller := range controllers {
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
// It would be better to unblock and return on context cancelled here,
|
|
// but that makes tests more flaky regarding timing.
|
|
time.Sleep(wait.Jitter(controllerCtx.ComponentConfig.Generic.ControllerStartInterval.Duration, controllerStartJitterMaxFactor))
|
|
|
|
logger.V(1).Info("Controller starting...", "controller", controller.Name())
|
|
|
|
runningControllersLock.Lock()
|
|
runningControllers.Insert(controller.Name())
|
|
runningControllersLock.Unlock()
|
|
|
|
defer func() {
|
|
logger.V(1).Info("Controller terminated", "controller", controller.Name())
|
|
|
|
runningControllersLock.Lock()
|
|
runningControllers.Delete(controller.Name())
|
|
runningControllersLock.Unlock()
|
|
}()
|
|
controller.Run(ctx)
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
logger.Info("All controllers terminated")
|
|
}()
|
|
|
|
// Wait for a signal to terminate.
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-terminatedCh:
|
|
return true
|
|
}
|
|
|
|
// Wait for the shutdown timeout.
|
|
var shutdownCh <-chan time.Time
|
|
if shutdownTimeout > 0 {
|
|
shutdownCh = time.After(shutdownTimeout)
|
|
}
|
|
select {
|
|
case <-terminatedCh:
|
|
return true
|
|
case <-shutdownCh:
|
|
runningControllersLock.Lock()
|
|
running := sets.List(runningControllers)
|
|
runningControllersLock.Unlock()
|
|
logger.Info("Controller shutdown timeout reached", "timeout", shutdownTimeout, "runningControllers", running)
|
|
return false
|
|
}
|
|
}
|
|
|
|
func readCA(file string) ([]byte, error) {
|
|
rootCA, err := os.ReadFile(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := certutil.ParseCertsPEM(rootCA); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return rootCA, err
|
|
}
|
|
|
|
// createClientBuilders creates clientBuilder and rootClientBuilder from the given configuration
|
|
func createClientBuilders(c *config.CompletedConfig) (clientBuilder clientbuilder.ControllerClientBuilder, rootClientBuilder clientbuilder.ControllerClientBuilder) {
|
|
|
|
rootClientBuilder = clientbuilder.SimpleControllerClientBuilder{
|
|
ClientConfig: c.Kubeconfig,
|
|
}
|
|
if c.ComponentConfig.KubeCloudShared.UseServiceAccountCredentials {
|
|
|
|
clientBuilder = clientbuilder.NewDynamicClientBuilder(
|
|
restclient.AnonymousClientConfig(c.Kubeconfig),
|
|
c.Client.CoreV1(),
|
|
metav1.NamespaceSystem)
|
|
} else {
|
|
clientBuilder = rootClientBuilder
|
|
}
|
|
return
|
|
}
|
|
|
|
// leaderElectAndRun runs the leader election, and runs the callbacks once the leader lease is acquired.
|
|
// TODO: extract this function into staging/controller-manager
|
|
func leaderElectAndRun(ctx context.Context, c *config.CompletedConfig, lockIdentity string, electionChecker *leaderelection.HealthzAdaptor, resourceLock string, leaseName string, callbacks leaderelection.LeaderCallbacks) {
|
|
logger := klog.FromContext(ctx)
|
|
rl, err := resourcelock.NewFromKubeconfig(resourceLock,
|
|
c.ComponentConfig.Generic.LeaderElection.ResourceNamespace,
|
|
leaseName,
|
|
resourcelock.ResourceLockConfig{
|
|
Identity: lockIdentity,
|
|
EventRecorder: c.EventRecorder,
|
|
},
|
|
c.Kubeconfig,
|
|
c.ComponentConfig.Generic.LeaderElection.RenewDeadline.Duration)
|
|
if err != nil {
|
|
logger.Error(err, "Error creating lock")
|
|
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
|
|
}
|
|
|
|
leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
|
|
Lock: rl,
|
|
LeaseDuration: c.ComponentConfig.Generic.LeaderElection.LeaseDuration.Duration,
|
|
RenewDeadline: c.ComponentConfig.Generic.LeaderElection.RenewDeadline.Duration,
|
|
RetryPeriod: c.ComponentConfig.Generic.LeaderElection.RetryPeriod.Duration,
|
|
Callbacks: callbacks,
|
|
WatchDog: electionChecker,
|
|
ReleaseOnCancel: utilfeature.DefaultFeatureGate.Enabled(cmfeatures.ControllerManagerReleaseLeaderElectionLockOnExit),
|
|
Name: leaseName,
|
|
Coordinated: utilfeature.DefaultFeatureGate.Enabled(kubefeatures.CoordinatedLeaderElection),
|
|
})
|
|
}
|
|
|
|
// filteredControllerDescriptors returns all controllerDescriptors after filtering through filterFunc.
|
|
func filteredControllerDescriptors(controllerDescriptors map[string]*ControllerDescriptor, filterFunc leadermigration.FilterFunc, expected leadermigration.FilterResult) map[string]*ControllerDescriptor {
|
|
resultControllers := make(map[string]*ControllerDescriptor)
|
|
for name, controllerDesc := range controllerDescriptors {
|
|
if filterFunc(name) == expected {
|
|
resultControllers[name] = controllerDesc
|
|
}
|
|
}
|
|
return resultControllers
|
|
}
|