mirror of
https://git.vectorsigma.ru/public/k3s.git
synced 2026-07-30 05:59:50 +00:00
Splits server startup into prepare/start phases. Server's agent is now started after server is prepared, but before it is started. This allows us to properly bootstrap the executor before starting server components, and use the executor to provide a shared channel to wait on apiserver readiness. This allows us to replace four separate callers of WaitForAPIServerReady with reads from a common ready channel. Signed-off-by: Brad Davidson <brad.davidson@rancher.com>
484 lines
16 KiB
Go
484 lines
16 KiB
Go
package control
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"math/rand"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/k3s-io/k3s/pkg/authenticator"
|
|
"github.com/k3s-io/k3s/pkg/cluster"
|
|
"github.com/k3s-io/k3s/pkg/daemons/config"
|
|
"github.com/k3s-io/k3s/pkg/daemons/control/deps"
|
|
"github.com/k3s-io/k3s/pkg/daemons/executor"
|
|
"github.com/k3s-io/k3s/pkg/util"
|
|
"github.com/k3s-io/k3s/pkg/version"
|
|
pkgerrors "github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
authorizationv1 "k8s.io/api/authorization/v1"
|
|
v1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
k8sruntime "k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/watch"
|
|
typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
|
"k8s.io/client-go/tools/cache"
|
|
toolswatch "k8s.io/client-go/tools/watch"
|
|
cloudproviderapi "k8s.io/cloud-provider/api"
|
|
logsapi "k8s.io/component-base/logs/api/v1"
|
|
"k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes"
|
|
"k8s.io/kubernetes/pkg/registry/core/node"
|
|
|
|
// for client metric registration
|
|
_ "k8s.io/component-base/metrics/prometheus/restclient"
|
|
)
|
|
|
|
// Prepare loads bootstrap data from the datastore and sets up the initial
|
|
// tunnel server request handler and stub authenticator.
|
|
func Prepare(ctx context.Context, cfg *config.Control) error {
|
|
rand.Seed(time.Now().UTC().UnixNano())
|
|
|
|
logsapi.ReapplyHandling = logsapi.ReapplyHandlingIgnoreUnchanged
|
|
if err := prepare(ctx, cfg); err != nil {
|
|
return pkgerrors.WithMessage(err, "preparing server")
|
|
}
|
|
|
|
tunnel, err := setupTunnel(ctx, cfg)
|
|
if err != nil {
|
|
return pkgerrors.WithMessage(err, "setup tunnel server")
|
|
}
|
|
cfg.Runtime.Tunnel = tunnel
|
|
|
|
node.DisableProxyHostnameCheck = true
|
|
|
|
authArgs := []string{
|
|
"--basic-auth-file=" + cfg.Runtime.PasswdFile,
|
|
"--client-ca-file=" + cfg.Runtime.ClientCA,
|
|
}
|
|
auth, err := authenticator.FromArgs(authArgs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cfg.Runtime.Authenticator = auth
|
|
|
|
return nil
|
|
}
|
|
|
|
// Server starts the apiserver and whatever other control-plane components are
|
|
// not disabled on this node.
|
|
func Server(ctx context.Context, cfg *config.Control) error {
|
|
if ready, err := cfg.Cluster.Start(ctx); err != nil {
|
|
return pkgerrors.WithMessage(err, "failed to start cluster")
|
|
} else {
|
|
cfg.Runtime.ETCDReady = ready
|
|
}
|
|
|
|
if !cfg.DisableAPIServer {
|
|
go waitForAPIServerHandlers(ctx, cfg.Runtime)
|
|
|
|
if err := apiServer(ctx, cfg); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if !cfg.DisableScheduler {
|
|
if err := scheduler(ctx, cfg); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !cfg.DisableControllerManager {
|
|
if err := controllerManager(ctx, cfg); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if !cfg.DisableCCM || !cfg.DisableServiceLB {
|
|
if err := cloudControllerManager(ctx, cfg); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func controllerManager(ctx context.Context, cfg *config.Control) error {
|
|
runtime := cfg.Runtime
|
|
argsMap := map[string]string{
|
|
"controllers": "*,tokencleaner",
|
|
"kubeconfig": runtime.KubeConfigController,
|
|
"authorization-kubeconfig": runtime.KubeConfigController,
|
|
"authentication-kubeconfig": runtime.KubeConfigController,
|
|
"service-account-private-key-file": runtime.ServiceCurrentKey,
|
|
"allocate-node-cidrs": "true",
|
|
"service-cluster-ip-range": util.JoinIPNets(cfg.ServiceIPRanges),
|
|
"cluster-cidr": util.JoinIPNets(cfg.ClusterIPRanges),
|
|
"root-ca-file": runtime.ServerCA,
|
|
"profiling": "false",
|
|
"bind-address": cfg.Loopback(false),
|
|
"secure-port": "10257",
|
|
"use-service-account-credentials": "true",
|
|
"cluster-signing-kube-apiserver-client-cert-file": runtime.SigningClientCA,
|
|
"cluster-signing-kube-apiserver-client-key-file": runtime.ClientCAKey,
|
|
"cluster-signing-kubelet-client-cert-file": runtime.SigningClientCA,
|
|
"cluster-signing-kubelet-client-key-file": runtime.ClientCAKey,
|
|
"cluster-signing-kubelet-serving-cert-file": runtime.SigningServerCA,
|
|
"cluster-signing-kubelet-serving-key-file": runtime.ServerCAKey,
|
|
"cluster-signing-legacy-unknown-cert-file": runtime.SigningServerCA,
|
|
"cluster-signing-legacy-unknown-key-file": runtime.ServerCAKey,
|
|
}
|
|
if cfg.NoLeaderElect {
|
|
argsMap["leader-elect"] = "false"
|
|
}
|
|
if !cfg.DisableCCM {
|
|
argsMap["configure-cloud-routes"] = "false"
|
|
argsMap["controllers"] = argsMap["controllers"] + ",-service,-route,-cloud-node-lifecycle"
|
|
}
|
|
|
|
if cfg.VLevel != 0 {
|
|
argsMap["v"] = strconv.Itoa(cfg.VLevel)
|
|
}
|
|
if cfg.VModule != "" {
|
|
argsMap["vmodule"] = cfg.VModule
|
|
}
|
|
|
|
args := config.GetArgs(argsMap, cfg.ExtraControllerArgs)
|
|
logrus.Infof("Running kube-controller-manager %s", config.ArgString(args))
|
|
|
|
return executor.ControllerManager(ctx, args)
|
|
}
|
|
|
|
func scheduler(ctx context.Context, cfg *config.Control) error {
|
|
runtime := cfg.Runtime
|
|
argsMap := map[string]string{
|
|
"kubeconfig": runtime.KubeConfigScheduler,
|
|
"authorization-kubeconfig": runtime.KubeConfigScheduler,
|
|
"authentication-kubeconfig": runtime.KubeConfigScheduler,
|
|
"bind-address": cfg.Loopback(false),
|
|
"secure-port": "10259",
|
|
"profiling": "false",
|
|
}
|
|
if cfg.NoLeaderElect {
|
|
argsMap["leader-elect"] = "false"
|
|
}
|
|
|
|
if cfg.VLevel != 0 {
|
|
argsMap["v"] = strconv.Itoa(cfg.VLevel)
|
|
}
|
|
if cfg.VModule != "" {
|
|
argsMap["vmodule"] = cfg.VModule
|
|
}
|
|
|
|
args := config.GetArgs(argsMap, cfg.ExtraSchedulerAPIArgs)
|
|
|
|
nodeReady := make(chan struct{})
|
|
|
|
go func() {
|
|
defer close(nodeReady)
|
|
|
|
<-executor.APIServerReadyChan()
|
|
|
|
// If we're running the embedded cloud controller, wait for it to untaint at least one
|
|
// node (usually, the local node) before starting the scheduler to ensure that it
|
|
// finds a node that is ready to run pods during its initial scheduling loop.
|
|
if !cfg.DisableCCM {
|
|
logrus.Infof("Waiting for untainted node")
|
|
if err := waitForUntaintedNode(ctx, runtime.KubeConfigScheduler); err != nil {
|
|
logrus.Fatalf("failed to wait for untained node: %v", err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
logrus.Infof("Running kube-scheduler %s", config.ArgString(args))
|
|
return executor.Scheduler(ctx, nodeReady, args)
|
|
}
|
|
|
|
func apiServer(ctx context.Context, cfg *config.Control) error {
|
|
runtime := cfg.Runtime
|
|
argsMap := map[string]string{}
|
|
|
|
setupStorageBackend(argsMap, cfg)
|
|
|
|
certDir := filepath.Join(cfg.DataDir, "tls", "temporary-certs")
|
|
os.MkdirAll(certDir, 0700)
|
|
|
|
argsMap["cert-dir"] = certDir
|
|
argsMap["allow-privileged"] = "true"
|
|
argsMap["enable-bootstrap-token-auth"] = "true"
|
|
argsMap["authorization-mode"] = strings.Join([]string{modes.ModeNode, modes.ModeRBAC}, ",")
|
|
argsMap["service-account-signing-key-file"] = runtime.ServiceCurrentKey
|
|
argsMap["service-cluster-ip-range"] = util.JoinIPNets(cfg.ServiceIPRanges)
|
|
argsMap["service-node-port-range"] = cfg.ServiceNodePortRange.String()
|
|
argsMap["advertise-port"] = strconv.Itoa(cfg.AdvertisePort)
|
|
if cfg.AdvertiseIP != "" {
|
|
argsMap["advertise-address"] = cfg.AdvertiseIP
|
|
}
|
|
argsMap["secure-port"] = strconv.Itoa(cfg.APIServerPort)
|
|
if cfg.APIServerBindAddress == "" {
|
|
argsMap["bind-address"] = cfg.Loopback(false)
|
|
} else {
|
|
argsMap["bind-address"] = cfg.APIServerBindAddress
|
|
}
|
|
if cfg.EgressSelectorMode != config.EgressSelectorModeDisabled {
|
|
argsMap["enable-aggregator-routing"] = "true"
|
|
argsMap["egress-selector-config-file"] = runtime.EgressSelectorConfig
|
|
}
|
|
argsMap["tls-cert-file"] = runtime.ServingKubeAPICert
|
|
argsMap["tls-private-key-file"] = runtime.ServingKubeAPIKey
|
|
argsMap["service-account-key-file"] = runtime.ServiceKey
|
|
argsMap["service-account-issuer"] = "https://kubernetes.default.svc." + cfg.ClusterDomain
|
|
argsMap["api-audiences"] = "https://kubernetes.default.svc." + cfg.ClusterDomain + "," + version.Program
|
|
argsMap["kubelet-certificate-authority"] = runtime.ServerCA
|
|
argsMap["kubelet-client-certificate"] = runtime.ClientKubeAPICert
|
|
argsMap["kubelet-client-key"] = runtime.ClientKubeAPIKey
|
|
if cfg.FlannelExternalIP {
|
|
argsMap["kubelet-preferred-address-types"] = "ExternalIP,InternalIP,Hostname"
|
|
} else {
|
|
argsMap["kubelet-preferred-address-types"] = "InternalIP,ExternalIP,Hostname"
|
|
}
|
|
argsMap["requestheader-client-ca-file"] = runtime.RequestHeaderCA
|
|
argsMap["requestheader-allowed-names"] = deps.RequestHeaderCN
|
|
argsMap["proxy-client-cert-file"] = runtime.ClientAuthProxyCert
|
|
argsMap["proxy-client-key-file"] = runtime.ClientAuthProxyKey
|
|
argsMap["requestheader-extra-headers-prefix"] = "X-Remote-Extra-"
|
|
argsMap["requestheader-group-headers"] = "X-Remote-Group"
|
|
argsMap["requestheader-username-headers"] = "X-Remote-User"
|
|
argsMap["client-ca-file"] = runtime.ClientCA
|
|
argsMap["enable-admission-plugins"] = "NodeRestriction"
|
|
argsMap["anonymous-auth"] = "false"
|
|
argsMap["profiling"] = "false"
|
|
if cfg.EncryptSecrets {
|
|
argsMap["encryption-provider-config"] = runtime.EncryptionConfig
|
|
argsMap["encryption-provider-config-automatic-reload"] = "true"
|
|
}
|
|
if cfg.VLevel != 0 {
|
|
argsMap["v"] = strconv.Itoa(cfg.VLevel)
|
|
}
|
|
if cfg.VModule != "" {
|
|
argsMap["vmodule"] = cfg.VModule
|
|
}
|
|
|
|
args := config.GetArgs(argsMap, cfg.ExtraAPIArgs)
|
|
|
|
logrus.Infof("Running kube-apiserver %s", config.ArgString(args))
|
|
|
|
return executor.APIServer(ctx, runtime.ETCDReady, args)
|
|
}
|
|
|
|
func defaults(config *config.Control) {
|
|
if config.AdvertisePort == 0 {
|
|
config.AdvertisePort = config.HTTPSPort
|
|
}
|
|
|
|
if config.APIServerPort == 0 {
|
|
if config.HTTPSPort != 0 {
|
|
config.APIServerPort = config.HTTPSPort + 1
|
|
} else {
|
|
config.APIServerPort = 6444
|
|
}
|
|
}
|
|
|
|
if config.DataDir == "" {
|
|
config.DataDir = "./management-state"
|
|
}
|
|
}
|
|
|
|
// prepare sets up the server data-dir, calls deps.GenServerDeps to
|
|
// set paths, extracts the cluster bootstrap data to the
|
|
// configured paths, and starts the supervisor listener.
|
|
func prepare(ctx context.Context, config *config.Control) error {
|
|
defaults(config)
|
|
|
|
if err := os.MkdirAll(config.DataDir, 0700); err != nil {
|
|
return err
|
|
}
|
|
|
|
if dataDir, err := filepath.Abs(config.DataDir); err != nil {
|
|
return err
|
|
} else {
|
|
config.DataDir = dataDir
|
|
}
|
|
|
|
os.MkdirAll(filepath.Join(config.DataDir, "etc"), 0700)
|
|
os.MkdirAll(filepath.Join(config.DataDir, "tls"), 0700)
|
|
os.MkdirAll(filepath.Join(config.DataDir, "cred"), 0700)
|
|
|
|
deps.CreateRuntimeCertFiles(config)
|
|
|
|
config.Cluster = cluster.New(config)
|
|
if err := config.Cluster.Bootstrap(ctx, config.ClusterReset); err != nil {
|
|
return pkgerrors.WithMessage(err, "failed to bootstrap cluster data")
|
|
}
|
|
|
|
if err := deps.GenServerDeps(config); err != nil {
|
|
return pkgerrors.WithMessage(err, "failed to generate server dependencies")
|
|
}
|
|
|
|
if err := config.Cluster.ListenAndServe(ctx); err != nil {
|
|
return pkgerrors.WithMessage(err, "failed to start supervisor listener")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func setupStorageBackend(argsMap map[string]string, cfg *config.Control) {
|
|
argsMap["storage-backend"] = "etcd3"
|
|
// specify the endpoints
|
|
if len(cfg.Datastore.Endpoint) > 0 {
|
|
argsMap["etcd-servers"] = cfg.Datastore.Endpoint
|
|
}
|
|
// storage backend tls configuration
|
|
if len(cfg.Datastore.BackendTLSConfig.CAFile) > 0 {
|
|
argsMap["etcd-cafile"] = cfg.Datastore.BackendTLSConfig.CAFile
|
|
}
|
|
if len(cfg.Datastore.BackendTLSConfig.CertFile) > 0 {
|
|
argsMap["etcd-certfile"] = cfg.Datastore.BackendTLSConfig.CertFile
|
|
}
|
|
if len(cfg.Datastore.BackendTLSConfig.KeyFile) > 0 {
|
|
argsMap["etcd-keyfile"] = cfg.Datastore.BackendTLSConfig.KeyFile
|
|
}
|
|
}
|
|
|
|
func cloudControllerManager(ctx context.Context, cfg *config.Control) error {
|
|
runtime := cfg.Runtime
|
|
argsMap := map[string]string{
|
|
"profiling": "false",
|
|
"allocate-node-cidrs": "true",
|
|
"leader-elect-resource-name": version.Program + "-cloud-controller-manager",
|
|
"cloud-provider": version.Program,
|
|
"cloud-config": runtime.CloudControllerConfig,
|
|
"cluster-cidr": util.JoinIPNets(cfg.ClusterIPRanges),
|
|
"configure-cloud-routes": "false",
|
|
"controllers": "*,-route",
|
|
"kubeconfig": runtime.KubeConfigCloudController,
|
|
"authorization-kubeconfig": runtime.KubeConfigCloudController,
|
|
"authentication-kubeconfig": runtime.KubeConfigCloudController,
|
|
"node-status-update-frequency": "1m0s",
|
|
"bind-address": cfg.Loopback(false),
|
|
}
|
|
if cfg.NoLeaderElect {
|
|
argsMap["leader-elect"] = "false"
|
|
}
|
|
if cfg.DisableCCM {
|
|
argsMap["controllers"] = argsMap["controllers"] + ",-cloud-node,-cloud-node-lifecycle"
|
|
argsMap["secure-port"] = "0"
|
|
}
|
|
if cfg.DisableServiceLB {
|
|
argsMap["controllers"] = argsMap["controllers"] + ",-service"
|
|
}
|
|
if cfg.VLevel != 0 {
|
|
argsMap["v"] = strconv.Itoa(cfg.VLevel)
|
|
}
|
|
if cfg.VModule != "" {
|
|
argsMap["vmodule"] = cfg.VModule
|
|
}
|
|
|
|
args := config.GetArgs(argsMap, cfg.ExtraCloudControllerArgs)
|
|
|
|
logrus.Infof("Running cloud-controller-manager %s", config.ArgString(args))
|
|
|
|
ccmRBACReady := make(chan struct{})
|
|
|
|
go func() {
|
|
defer close(ccmRBACReady)
|
|
|
|
<-executor.APIServerReadyChan()
|
|
|
|
logrus.Infof("Waiting for cloud-controller-manager privileges to become available")
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case err := <-promise(func() error { return checkForCloudControllerPrivileges(ctx, cfg.Runtime, 5*time.Second) }):
|
|
if err != nil {
|
|
logrus.Infof("Waiting for cloud-controller-manager privileges to become available: %v", err)
|
|
continue
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
return executor.CloudControllerManager(ctx, ccmRBACReady, args)
|
|
}
|
|
|
|
// checkForCloudControllerPrivileges makes a SubjectAccessReview request to the apiserver
|
|
// to validate that the embedded cloud controller manager has the required privileges,
|
|
// and does not return until the requested access is granted.
|
|
// If the CCM RBAC changes, the ResourceAttributes checked for by this function should
|
|
// be modified to check for the most recently added privilege.
|
|
func checkForCloudControllerPrivileges(ctx context.Context, runtime *config.ControlRuntime, timeout time.Duration) error {
|
|
return util.WaitForRBACReady(ctx, runtime.KubeConfigSupervisor, timeout, authorizationv1.ResourceAttributes{
|
|
Namespace: metav1.NamespaceSystem,
|
|
Verb: "watch",
|
|
Resource: "endpointslices",
|
|
Group: "discovery.k8s.io",
|
|
}, version.Program+"-cloud-controller-manager")
|
|
}
|
|
|
|
func waitForAPIServerHandlers(ctx context.Context, runtime *config.ControlRuntime) {
|
|
auth, handler, err := executor.APIServerHandlers(ctx)
|
|
if err != nil {
|
|
logrus.Fatalf("Failed to get request handlers from apiserver: %v", err)
|
|
}
|
|
runtime.Authenticator = authenticator.Combine(runtime.Authenticator, auth)
|
|
runtime.APIServer = handler
|
|
}
|
|
|
|
func promise(f func() error) <-chan error {
|
|
c := make(chan error, 1)
|
|
go func() {
|
|
c <- f()
|
|
close(c)
|
|
}()
|
|
return c
|
|
}
|
|
|
|
// waitForUntaintedNode watches nodes, waiting to find one not tainted as
|
|
// uninitialized by the external cloud provider.
|
|
func waitForUntaintedNode(ctx context.Context, kubeConfig string) error {
|
|
restConfig, err := util.GetRESTConfig(kubeConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
coreClient, err := typedcorev1.NewForConfig(restConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nodes := coreClient.Nodes()
|
|
|
|
lw := &cache.ListWatch{
|
|
ListFunc: func(options metav1.ListOptions) (object k8sruntime.Object, e error) {
|
|
return nodes.List(ctx, options)
|
|
},
|
|
WatchFunc: func(options metav1.ListOptions) (i watch.Interface, e error) {
|
|
return nodes.Watch(ctx, options)
|
|
},
|
|
}
|
|
|
|
condition := func(ev watch.Event) (bool, error) {
|
|
if node, ok := ev.Object.(*v1.Node); ok {
|
|
return getCloudTaint(node.Spec.Taints) == nil, nil
|
|
}
|
|
return false, errors.New("event object not of type v1.Node")
|
|
}
|
|
|
|
if _, err := toolswatch.UntilWithSync(ctx, lw, &v1.Node{}, nil, condition); err != nil {
|
|
return pkgerrors.WithMessage(err, "failed to wait for untainted node")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getCloudTaint returns the external cloud provider taint, if present.
|
|
// Cribbed from k8s.io/cloud-provider/controllers/node/node_controller.go
|
|
func getCloudTaint(taints []v1.Taint) *v1.Taint {
|
|
for _, taint := range taints {
|
|
if taint.Key == cloudproviderapi.TaintExternalCloudProvider {
|
|
return &taint
|
|
}
|
|
}
|
|
return nil
|
|
}
|