/* Copyright 2016 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 controlplane import ( "bytes" "fmt" "os" "path/filepath" "strings" "github.com/ghodss/yaml" "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/sets" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" "k8s.io/kubernetes/cmd/kubeadm/app/images" authzmodes "k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" kubetypes "k8s.io/kubernetes/pkg/kubelet/types" "k8s.io/kubernetes/pkg/util/version" ) // Static pod definitions in golang form are included below so that `kubeadm init` can get going. const ( DefaultCloudConfigPath = "/etc/kubernetes/cloud-config" defaultv17AdmissionControl = "Initializers,NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,DefaultTolerationSeconds,NodeRestriction,ResourceQuota" ) // WriteStaticPodManifests builds manifest objects based on user provided configuration and then dumps it to disk // where kubelet will pick and schedule them. func WriteStaticPodManifests(cfg *kubeadmapi.MasterConfiguration, k8sVersion *version.Version, manifestsDir string) error { // Get the required hostpath mounts mounts := getHostPathVolumesForTheControlPlane(cfg) // Prepare static pod specs staticPodSpecs := map[string]v1.Pod{ kubeadmconstants.KubeAPIServer: componentPod(v1.Container{ Name: kubeadmconstants.KubeAPIServer, Image: images.GetCoreImage(kubeadmconstants.KubeAPIServer, cfg.ImageRepository, cfg.KubernetesVersion, cfg.UnifiedControlPlaneImage), Command: getAPIServerCommand(cfg, k8sVersion), VolumeMounts: mounts.GetVolumeMounts(kubeadmconstants.KubeAPIServer), LivenessProbe: componentProbe(int(cfg.API.BindPort), "/healthz", v1.URISchemeHTTPS), Resources: componentResources("250m"), Env: getProxyEnvVars(), }, mounts.GetVolumes(kubeadmconstants.KubeAPIServer)), kubeadmconstants.KubeControllerManager: componentPod(v1.Container{ Name: kubeadmconstants.KubeControllerManager, Image: images.GetCoreImage(kubeadmconstants.KubeControllerManager, cfg.ImageRepository, cfg.KubernetesVersion, cfg.UnifiedControlPlaneImage), Command: getControllerManagerCommand(cfg, k8sVersion), VolumeMounts: mounts.GetVolumeMounts(kubeadmconstants.KubeControllerManager), LivenessProbe: componentProbe(10252, "/healthz", v1.URISchemeHTTP), Resources: componentResources("200m"), Env: getProxyEnvVars(), }, mounts.GetVolumes(kubeadmconstants.KubeControllerManager)), kubeadmconstants.KubeScheduler: componentPod(v1.Container{ Name: kubeadmconstants.KubeScheduler, Image: images.GetCoreImage(kubeadmconstants.KubeScheduler, cfg.ImageRepository, cfg.KubernetesVersion, cfg.UnifiedControlPlaneImage), Command: getSchedulerCommand(cfg), VolumeMounts: mounts.GetVolumeMounts(kubeadmconstants.KubeScheduler), LivenessProbe: componentProbe(10251, "/healthz", v1.URISchemeHTTP), Resources: componentResources("100m"), Env: getProxyEnvVars(), }, mounts.GetVolumes(kubeadmconstants.KubeScheduler)), } // Add etcd static pod spec only if external etcd is not configured if len(cfg.Etcd.Endpoints) == 0 { etcdPod := componentPod(v1.Container{ Name: kubeadmconstants.Etcd, Command: getEtcdCommand(cfg), Image: images.GetCoreImage(kubeadmconstants.Etcd, cfg.ImageRepository, "", cfg.Etcd.Image), // Mount the etcd datadir path read-write so etcd can store data in a more persistent manner VolumeMounts: []v1.VolumeMount{newVolumeMount(etcdVolumeName, cfg.Etcd.DataDir, false)}, LivenessProbe: componentProbe(2379, "/health", v1.URISchemeHTTP), }, []v1.Volume{newVolume(etcdVolumeName, cfg.Etcd.DataDir)}) staticPodSpecs[kubeadmconstants.Etcd] = etcdPod } if err := os.MkdirAll(manifestsDir, 0700); err != nil { return fmt.Errorf("failed to create directory %q [%v]", manifestsDir, err) } for name, spec := range staticPodSpecs { filename := kubeadmconstants.GetStaticPodFilepath(name, manifestsDir) serialized, err := yaml.Marshal(spec) if err != nil { return fmt.Errorf("failed to marshal manifest for %q to YAML [%v]", name, err) } if err := cmdutil.DumpReaderToFile(bytes.NewReader(serialized), filename); err != nil { return fmt.Errorf("failed to create static pod manifest file for %q (%q) [%v]", name, filename, err) } } return nil } // componentResources returns the v1.ResourceRequirements object needed for allocating a specified amount of the CPU func componentResources(cpu string) v1.ResourceRequirements { return v1.ResourceRequirements{ Requests: v1.ResourceList{ v1.ResourceName(v1.ResourceCPU): resource.MustParse(cpu), }, } } // componentProbe is a helper function building a ready v1.Probe object from some simple parameters func componentProbe(port int, path string, scheme v1.URIScheme) *v1.Probe { return &v1.Probe{ Handler: v1.Handler{ HTTPGet: &v1.HTTPGetAction{ // Host has to be set to "127.0.0.1" here due to that our static Pods are on the host's network Host: "127.0.0.1", Path: path, Port: intstr.FromInt(port), Scheme: scheme, }, }, InitialDelaySeconds: 15, TimeoutSeconds: 15, FailureThreshold: 8, } } // componentPod returns a Pod object from the container and volume specifications func componentPod(container v1.Container, volumes []v1.Volume) v1.Pod { return v1.Pod{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", Kind: "Pod", }, ObjectMeta: metav1.ObjectMeta{ Name: container.Name, Namespace: metav1.NamespaceSystem, Annotations: map[string]string{kubetypes.CriticalPodAnnotationKey: ""}, // The component and tier labels are useful for quickly identifying the control plane Pods when doing a .List() // against Pods in the kube-system namespace. Can for example be used together with the WaitForPodsWithLabel function Labels: map[string]string{"component": container.Name, "tier": "control-plane"}, }, Spec: v1.PodSpec{ Containers: []v1.Container{container}, HostNetwork: true, Volumes: volumes, }, } } // getAPIServerCommand builds the right API server command from the given config object and version func getAPIServerCommand(cfg *kubeadmapi.MasterConfiguration, k8sVersion *version.Version) []string { defaultArguments := map[string]string{ "advertise-address": cfg.API.AdvertiseAddress, "insecure-port": "0", "admission-control": defaultv17AdmissionControl, "service-cluster-ip-range": cfg.Networking.ServiceSubnet, "service-account-key-file": filepath.Join(cfg.CertificatesDir, kubeadmconstants.ServiceAccountPublicKeyName), "client-ca-file": filepath.Join(cfg.CertificatesDir, kubeadmconstants.CACertName), "tls-cert-file": filepath.Join(cfg.CertificatesDir, kubeadmconstants.APIServerCertName), "tls-private-key-file": filepath.Join(cfg.CertificatesDir, kubeadmconstants.APIServerKeyName), "kubelet-client-certificate": filepath.Join(cfg.CertificatesDir, kubeadmconstants.APIServerKubeletClientCertName), "kubelet-client-key": filepath.Join(cfg.CertificatesDir, kubeadmconstants.APIServerKubeletClientKeyName), "secure-port": fmt.Sprintf("%d", cfg.API.BindPort), "allow-privileged": "true", "experimental-bootstrap-token-auth": "true", "kubelet-preferred-address-types": "InternalIP,ExternalIP,Hostname", // add options to configure the front proxy. Without the generated client cert, this will never be useable // so add it unconditionally with recommended values "requestheader-username-headers": "X-Remote-User", "requestheader-group-headers": "X-Remote-Group", "requestheader-extra-headers-prefix": "X-Remote-Extra-", "requestheader-client-ca-file": filepath.Join(cfg.CertificatesDir, kubeadmconstants.FrontProxyCACertName), "requestheader-allowed-names": "front-proxy-client", "proxy-client-cert-file": filepath.Join(cfg.CertificatesDir, kubeadmconstants.FrontProxyClientCertName), "proxy-client-key-file": filepath.Join(cfg.CertificatesDir, kubeadmconstants.FrontProxyClientKeyName), } command := []string{"kube-apiserver"} command = append(command, getExtraParameters(cfg.APIServerExtraArgs, defaultArguments)...) command = append(command, getAuthzParameters(cfg.AuthorizationModes)...) // Check if the user decided to use an external etcd cluster if len(cfg.Etcd.Endpoints) > 0 { command = append(command, fmt.Sprintf("--etcd-servers=%s", strings.Join(cfg.Etcd.Endpoints, ","))) } else { command = append(command, "--etcd-servers=http://127.0.0.1:2379") } // Is etcd secured? if cfg.Etcd.CAFile != "" { command = append(command, fmt.Sprintf("--etcd-cafile=%s", cfg.Etcd.CAFile)) } if cfg.Etcd.CertFile != "" && cfg.Etcd.KeyFile != "" { etcdClientFileArg := fmt.Sprintf("--etcd-certfile=%s", cfg.Etcd.CertFile) etcdKeyFileArg := fmt.Sprintf("--etcd-keyfile=%s", cfg.Etcd.KeyFile) command = append(command, etcdClientFileArg, etcdKeyFileArg) } if cfg.CloudProvider != "" { command = append(command, "--cloud-provider="+cfg.CloudProvider) // Only append the --cloud-config option if there's a such file if _, err := os.Stat(DefaultCloudConfigPath); err == nil { command = append(command, "--cloud-config="+DefaultCloudConfigPath) } } return command } // getEtcdCommand builds the right etcd command from the given config object func getEtcdCommand(cfg *kubeadmapi.MasterConfiguration) []string { defaultArguments := map[string]string{ "listen-client-urls": "http://127.0.0.1:2379", "advertise-client-urls": "http://127.0.0.1:2379", "data-dir": cfg.Etcd.DataDir, } command := []string{"etcd"} command = append(command, getExtraParameters(cfg.Etcd.ExtraArgs, defaultArguments)...) return command } // getControllerManagerCommand builds the right controller manager command from the given config object and version func getControllerManagerCommand(cfg *kubeadmapi.MasterConfiguration, k8sVersion *version.Version) []string { defaultArguments := map[string]string{ "address": "127.0.0.1", "leader-elect": "true", "kubeconfig": filepath.Join(kubeadmconstants.KubernetesDir, kubeadmconstants.ControllerManagerKubeConfigFileName), "root-ca-file": filepath.Join(cfg.CertificatesDir, kubeadmconstants.CACertName), "service-account-private-key-file": filepath.Join(cfg.CertificatesDir, kubeadmconstants.ServiceAccountPrivateKeyName), "cluster-signing-cert-file": filepath.Join(cfg.CertificatesDir, kubeadmconstants.CACertName), "cluster-signing-key-file": filepath.Join(cfg.CertificatesDir, kubeadmconstants.CAKeyName), "use-service-account-credentials": "true", "controllers": "*,bootstrapsigner,tokencleaner", } command := []string{"kube-controller-manager"} command = append(command, getExtraParameters(cfg.ControllerManagerExtraArgs, defaultArguments)...) if cfg.CloudProvider != "" { command = append(command, "--cloud-provider="+cfg.CloudProvider) // Only append the --cloud-config option if there's a such file if _, err := os.Stat(DefaultCloudConfigPath); err == nil { command = append(command, "--cloud-config="+DefaultCloudConfigPath) } } // Let the controller-manager allocate Node CIDRs for the Pod network. // Each node will get a subspace of the address CIDR provided with --pod-network-cidr. if cfg.Networking.PodSubnet != "" { command = append(command, "--allocate-node-cidrs=true", "--cluster-cidr="+cfg.Networking.PodSubnet) } return command } // getSchedulerCommand builds the right scheduler command from the given config object and version func getSchedulerCommand(cfg *kubeadmapi.MasterConfiguration) []string { defaultArguments := map[string]string{ "address": "127.0.0.1", "leader-elect": "true", "kubeconfig": filepath.Join(kubeadmconstants.KubernetesDir, kubeadmconstants.SchedulerKubeConfigFileName), } command := []string{"kube-scheduler"} command = append(command, getExtraParameters(cfg.SchedulerExtraArgs, defaultArguments)...) return command } // getProxyEnvVars builds a list of environment variables to use in the control plane containers in order to use the right proxy func getProxyEnvVars() []v1.EnvVar { envs := []v1.EnvVar{} for _, env := range os.Environ() { pos := strings.Index(env, "=") if pos == -1 { // malformed environment variable, skip it. continue } name := env[:pos] value := env[pos+1:] if strings.HasSuffix(strings.ToLower(name), "_proxy") && value != "" { envVar := v1.EnvVar{Name: name, Value: value} envs = append(envs, envVar) } } return envs } // getAuthzParameters gets the authorization-related parameters to the api server // At this point, we can assume the list of authorization modes is valid (due to that it has been validated in the API machinery code already) // If the list is empty; it's defaulted (mostly for unit testing) func getAuthzParameters(modes []string) []string { command := []string{} strset := sets.NewString(modes...) if len(modes) == 0 { return []string{fmt.Sprintf("--authorization-mode=%s", kubeadmapiext.DefaultAuthorizationModes)} } if strset.Has(authzmodes.ModeABAC) { command = append(command, "--authorization-policy-file="+kubeadmconstants.AuthorizationPolicyPath) } if strset.Has(authzmodes.ModeWebhook) { command = append(command, "--authorization-webhook-config-file="+kubeadmconstants.AuthorizationWebhookConfigPath) } command = append(command, "--authorization-mode="+strings.Join(modes, ",")) return command } // getExtraParameters builds a list of flag arguments two string-string maps, one with default, base commands and one with overrides func getExtraParameters(overrides map[string]string, defaults map[string]string) []string { var command []string for k, v := range overrides { if len(v) > 0 { command = append(command, fmt.Sprintf("--%s=%s", k, v)) } } for k, v := range defaults { if _, overrideExists := overrides[k]; !overrideExists { command = append(command, fmt.Sprintf("--%s=%s", k, v)) } } return command }