mirror of https://github.com/kubernetes/kops.git
503 lines
14 KiB
Go
503 lines
14 KiB
Go
/*
|
|
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 model
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"k8s.io/kops/nodeup/pkg/distros"
|
|
"k8s.io/kops/pkg/apis/kops"
|
|
"k8s.io/kops/pkg/apis/kops/util"
|
|
"k8s.io/kops/pkg/apis/nodeup"
|
|
"k8s.io/kops/pkg/kubeconfig"
|
|
"k8s.io/kops/upup/pkg/fi"
|
|
"k8s.io/kops/upup/pkg/fi/nodeup/nodetasks"
|
|
"k8s.io/kops/util/pkg/vfs"
|
|
|
|
"github.com/blang/semver"
|
|
"github.com/golang/glog"
|
|
)
|
|
|
|
// NodeupModelContext is the context supplied the nodeup tasks
|
|
type NodeupModelContext struct {
|
|
Architecture Architecture
|
|
Assets *fi.AssetStore
|
|
Cluster *kops.Cluster
|
|
Distribution distros.Distribution
|
|
InstanceGroup *kops.InstanceGroup
|
|
KeyStore fi.CAStore
|
|
NodeupConfig *nodeup.Config
|
|
SecretStore fi.SecretStore
|
|
|
|
// IsMaster is true if the InstanceGroup has a role of master (populated by Init)
|
|
IsMaster bool
|
|
|
|
kubernetesVersion semver.Version
|
|
}
|
|
|
|
// Init completes initialization of the object, for example pre-parsing the kubernetes version
|
|
func (c *NodeupModelContext) Init() error {
|
|
k8sVersion, err := util.ParseKubernetesVersion(c.Cluster.Spec.KubernetesVersion)
|
|
if err != nil || k8sVersion == nil {
|
|
return fmt.Errorf("unable to parse KubernetesVersion %q", c.Cluster.Spec.KubernetesVersion)
|
|
}
|
|
c.kubernetesVersion = *k8sVersion
|
|
|
|
if c.InstanceGroup == nil {
|
|
glog.Warningf("cannot determine role, InstanceGroup not set")
|
|
} else if c.InstanceGroup.Spec.Role == kops.InstanceGroupRoleMaster {
|
|
c.IsMaster = true
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SSLHostPaths returns the TLS paths for the distribution
|
|
func (c *NodeupModelContext) SSLHostPaths() []string {
|
|
paths := []string{"/etc/ssl", "/etc/pki/tls", "/etc/pki/ca-trust"}
|
|
|
|
switch c.Distribution {
|
|
case distros.DistributionCoreOS:
|
|
// Because /usr is read-only on CoreOS, we can't have any new directories; docker will try (and fail) to create them
|
|
// TODO: Just check if the directories exist?
|
|
paths = append(paths, "/usr/share/ca-certificates")
|
|
case distros.DistributionContainerOS:
|
|
paths = append(paths, "/usr/share/ca-certificates")
|
|
default:
|
|
paths = append(paths, "/usr/share/ssl", "/usr/ssl", "/usr/lib/ssl", "/usr/local/openssl", "/var/ssl", "/etc/openssl")
|
|
}
|
|
|
|
return paths
|
|
}
|
|
|
|
// PathSrvKubernetes returns the path for the kubernetes service files
|
|
func (c *NodeupModelContext) PathSrvKubernetes() string {
|
|
switch c.Distribution {
|
|
case distros.DistributionContainerOS:
|
|
return "/etc/srv/kubernetes"
|
|
default:
|
|
return "/srv/kubernetes"
|
|
}
|
|
}
|
|
|
|
// FileAssetsDefaultPath is the default location for assets which have no path
|
|
func (c *NodeupModelContext) FileAssetsDefaultPath() string {
|
|
return filepath.Join(c.PathSrvKubernetes(), "assets")
|
|
}
|
|
|
|
// PathSrvSshproxy returns the path for the SSL proxy
|
|
func (c *NodeupModelContext) PathSrvSshproxy() string {
|
|
switch c.Distribution {
|
|
case distros.DistributionContainerOS:
|
|
return "/etc/srv/sshproxy"
|
|
default:
|
|
return "/srv/sshproxy"
|
|
}
|
|
}
|
|
|
|
// CNIBinDir returns the path for the CNI binaries
|
|
func (c *NodeupModelContext) CNIBinDir() string {
|
|
switch c.Distribution {
|
|
case distros.DistributionContainerOS:
|
|
return "/home/kubernetes/bin/"
|
|
default:
|
|
return "/opt/cni/bin/"
|
|
}
|
|
}
|
|
|
|
// KubeletBootstrapKubeconfig is the path the bootstrap config file
|
|
func (c *NodeupModelContext) KubeletBootstrapKubeconfig() string {
|
|
path := c.Cluster.Spec.Kubelet.BootstrapKubeconfig
|
|
|
|
if c.IsMaster {
|
|
if c.Cluster.Spec.MasterKubelet != nil && c.Cluster.Spec.MasterKubelet.BootstrapKubeconfig != "" {
|
|
path = c.Cluster.Spec.MasterKubelet.BootstrapKubeconfig
|
|
}
|
|
}
|
|
|
|
if path != "" {
|
|
return path
|
|
}
|
|
|
|
return "/var/lib/kubelet/bootstrap-kubeconfig"
|
|
}
|
|
|
|
// KubeletKubeConfig is the path of the kubelet kubeconfig file
|
|
func (c *NodeupModelContext) KubeletKubeConfig() string {
|
|
return "/var/lib/kubelet/kubeconfig"
|
|
}
|
|
|
|
// CNIConfDir returns the CNI directory
|
|
func (c *NodeupModelContext) CNIConfDir() string {
|
|
return "/etc/cni/net.d/"
|
|
}
|
|
|
|
// BuildPKIKubeconfig generates a kubeconfig
|
|
func (c *NodeupModelContext) BuildPKIKubeconfig(name string) (string, error) {
|
|
ca, err := c.FindCert(fi.CertificateId_CA)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
cert, err := c.FindCert(name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
key, err := c.FindPrivateKey(name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return c.BuildKubeConfig(name, ca, cert, key)
|
|
}
|
|
|
|
// BuildKubeConfig is responsible for building a kubeconfig
|
|
func (c *NodeupModelContext) BuildKubeConfig(username string, ca, certificate, privateKey []byte) (string, error) {
|
|
user := kubeconfig.KubectlUser{
|
|
ClientCertificateData: certificate,
|
|
ClientKeyData: privateKey,
|
|
}
|
|
cluster := kubeconfig.KubectlCluster{
|
|
CertificateAuthorityData: ca,
|
|
}
|
|
|
|
if c.IsMaster {
|
|
if c.IsKubernetesGTE("1.6") {
|
|
// @note: use https >= 1.6m even for local connections, so we can turn off the insecure port
|
|
cluster.Server = "https://127.0.0.1"
|
|
} else {
|
|
cluster.Server = "http://127.0.0.1:8080"
|
|
}
|
|
} else {
|
|
cluster.Server = "https://" + c.Cluster.Spec.MasterInternalName
|
|
}
|
|
|
|
config := &kubeconfig.KubectlConfig{
|
|
ApiVersion: "v1",
|
|
Kind: "Config",
|
|
Users: []*kubeconfig.KubectlUserWithName{
|
|
{
|
|
Name: username,
|
|
User: user,
|
|
},
|
|
},
|
|
Clusters: []*kubeconfig.KubectlClusterWithName{
|
|
{
|
|
Name: "local",
|
|
Cluster: cluster,
|
|
},
|
|
},
|
|
Contexts: []*kubeconfig.KubectlContextWithName{
|
|
{
|
|
Name: "service-account-context",
|
|
Context: kubeconfig.KubectlContext{
|
|
Cluster: "local",
|
|
User: username,
|
|
},
|
|
},
|
|
},
|
|
CurrentContext: "service-account-context",
|
|
}
|
|
|
|
yaml, err := kops.ToRawYaml(config)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error marshalling kubeconfig to yaml: %v", err)
|
|
}
|
|
|
|
return string(yaml), nil
|
|
}
|
|
|
|
// IsKubernetesGTE checks if the version is greater-than-or-equal
|
|
func (c *NodeupModelContext) IsKubernetesGTE(version string) bool {
|
|
if c.kubernetesVersion.Major == 0 {
|
|
glog.Fatalf("kubernetesVersion not set (%s); Init not called", c.kubernetesVersion)
|
|
}
|
|
return util.IsKubernetesGTE(version, c.kubernetesVersion)
|
|
}
|
|
|
|
// UseEtcdTLS checks if the etcd cluster has TLS enabled bool
|
|
func (c *NodeupModelContext) UseEtcdTLS() bool {
|
|
// @note: because we enforce that 'both' have to be enabled for TLS we only need to check one here.
|
|
for _, x := range c.Cluster.Spec.EtcdClusters {
|
|
if x.EnableEtcdTLS {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// UseEtcdTLSAuth checks the peer-auth is set in both cluster
|
|
// @NOTE: in retrospect i think we should have consolidated the common config in the wrapper struct; it
|
|
// feels weird we set things like version, tls etc per cluster since they both have to be the same.
|
|
func (c *NodeupModelContext) UseEtcdTLSAuth() bool {
|
|
if !c.UseEtcdTLS() {
|
|
return false
|
|
}
|
|
|
|
for _, x := range c.Cluster.Spec.EtcdClusters {
|
|
if x.EnableTLSAuth {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// UsesCNI checks if the cluster has CNI configured
|
|
func (c *NodeupModelContext) UsesCNI() bool {
|
|
networking := c.Cluster.Spec.Networking
|
|
if networking == nil || networking.Classic != nil {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// UseNodeAuthorization checks if have a node authorization policy
|
|
func (c *NodeupModelContext) UseNodeAuthorization() bool {
|
|
return c.Cluster.Spec.NodeAuthorization != nil
|
|
}
|
|
|
|
// UseNodeAuthorizer checks if node authorization is enabled
|
|
func (c *NodeupModelContext) UseNodeAuthorizer() bool {
|
|
if !c.UseNodeAuthorization() || !c.UseBootstrapTokens() {
|
|
return false
|
|
}
|
|
|
|
return c.Cluster.Spec.NodeAuthorization.NodeAuthorizer != nil
|
|
}
|
|
|
|
// UsesSecondaryIP checks if the CNI in use attaches secondary interfaces to the host.
|
|
func (c *NodeupModelContext) UsesSecondaryIP() bool {
|
|
if (c.Cluster.Spec.Networking.CNI != nil && c.Cluster.Spec.Networking.CNI.UsesSecondaryIP) || c.Cluster.Spec.Networking.AmazonVPC != nil || c.Cluster.Spec.Networking.LyftVPC != nil {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// UseBootstrapTokens checks if we are using bootstrap tokens
|
|
func (c *NodeupModelContext) UseBootstrapTokens() bool {
|
|
if c.IsMaster {
|
|
return fi.BoolValue(c.Cluster.Spec.KubeAPIServer.EnableBootstrapAuthToken)
|
|
}
|
|
|
|
return c.Cluster.Spec.Kubelet != nil && c.Cluster.Spec.Kubelet.BootstrapKubeconfig != ""
|
|
}
|
|
|
|
// UseSecureKubelet checks if the kubelet api should be protected by a client certificate. Note: the settings are
|
|
// in one of three section, master specific kubelet, cluster wide kubelet or the InstanceGroup. Though arguably is
|
|
// doesn't make much sense to unset this on a per InstanceGroup level, but hey :)
|
|
func (c *NodeupModelContext) UseSecureKubelet() bool {
|
|
cluster := &c.Cluster.Spec // just to shorten the typing
|
|
group := &c.InstanceGroup.Spec
|
|
|
|
// @check on the InstanceGroup itself
|
|
if group.Kubelet != nil && group.Kubelet.AnonymousAuth != nil && *group.Kubelet.AnonymousAuth == false {
|
|
return true
|
|
}
|
|
|
|
// @check if we have anything specific to master kubelet
|
|
if c.IsMaster {
|
|
if cluster.MasterKubelet != nil && cluster.MasterKubelet.AnonymousAuth != nil && *cluster.MasterKubelet.AnonymousAuth == false {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// @check the default settings for master and kubelet
|
|
if cluster.Kubelet != nil && cluster.Kubelet.AnonymousAuth != nil && *cluster.Kubelet.AnonymousAuth == false {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// KubectlPath returns distro based path for kubectl
|
|
func (c *NodeupModelContext) KubectlPath() string {
|
|
kubeletCommand := "/usr/local/bin"
|
|
if c.Distribution == distros.DistributionCoreOS {
|
|
kubeletCommand = "/opt/bin"
|
|
}
|
|
if c.Distribution == distros.DistributionContainerOS {
|
|
kubeletCommand = "/home/kubernetes/bin"
|
|
}
|
|
return kubeletCommand
|
|
}
|
|
|
|
// BuildCertificatePairTask creates the tasks to pull down the certificate and private key
|
|
func (c *NodeupModelContext) BuildCertificatePairTask(ctx *fi.ModelBuilderContext, key, path, filename string) error {
|
|
certificateName := filepath.Join(path, filename+".pem")
|
|
keyName := filepath.Join(path, filename+"-key.pem")
|
|
|
|
if err := c.BuildCertificateTask(ctx, key, certificateName); err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.BuildPrivateKeyTask(ctx, key, keyName)
|
|
}
|
|
|
|
// BuildCertificateTask is responsible for build a certificate request task
|
|
func (c *NodeupModelContext) BuildCertificateTask(ctx *fi.ModelBuilderContext, name, filename string) error {
|
|
cert, err := c.KeyStore.FindCert(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if cert == nil {
|
|
return fmt.Errorf("certificate %q not found", name)
|
|
}
|
|
|
|
serialized, err := cert.AsString()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
p := filename
|
|
if !filepath.IsAbs(p) {
|
|
p = filepath.Join(c.PathSrvKubernetes(), filename)
|
|
}
|
|
|
|
ctx.AddTask(&nodetasks.File{
|
|
Path: p,
|
|
Contents: fi.NewStringResource(serialized),
|
|
Type: nodetasks.FileType_File,
|
|
Mode: s("0600"),
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// BuildPrivateKeyTask is responsible for build a certificate request task
|
|
func (c *NodeupModelContext) BuildPrivateKeyTask(ctx *fi.ModelBuilderContext, name, filename string) error {
|
|
cert, err := c.KeyStore.FindPrivateKey(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if cert == nil {
|
|
return fmt.Errorf("private key %q not found", name)
|
|
}
|
|
|
|
serialized, err := cert.AsString()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
p := filename
|
|
if !filepath.IsAbs(p) {
|
|
p = filepath.Join(c.PathSrvKubernetes(), filename)
|
|
}
|
|
|
|
ctx.AddTask(&nodetasks.File{
|
|
Path: p,
|
|
Contents: fi.NewStringResource(serialized),
|
|
Type: nodetasks.FileType_File,
|
|
Mode: s("0600"),
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// NodeName returns the name of the local Node, as it will be created in k8s
|
|
func (c *NodeupModelContext) NodeName() (string, error) {
|
|
// This mirrors nodeutil.GetHostName
|
|
hostnameOverride := c.Cluster.Spec.Kubelet.HostnameOverride
|
|
|
|
if c.IsMaster && c.Cluster.Spec.MasterKubelet.HostnameOverride != "" {
|
|
hostnameOverride = c.Cluster.Spec.MasterKubelet.HostnameOverride
|
|
}
|
|
|
|
nodeName, err := EvaluateHostnameOverride(hostnameOverride)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error evaluating hostname: %v", err)
|
|
}
|
|
|
|
if nodeName == "" {
|
|
hostname, err := os.Hostname()
|
|
if err != nil {
|
|
glog.Fatalf("Couldn't determine hostname: %v", err)
|
|
}
|
|
nodeName = hostname
|
|
}
|
|
|
|
return strings.ToLower(strings.TrimSpace(nodeName)), nil
|
|
}
|
|
|
|
// EvaluateHostnameOverride returns the hostname after replacing some well-known placeholders
|
|
func EvaluateHostnameOverride(hostnameOverride string) (string, error) {
|
|
if hostnameOverride == "" || hostnameOverride == "@hostname" {
|
|
return "", nil
|
|
}
|
|
k := strings.TrimSpace(hostnameOverride)
|
|
k = strings.ToLower(k)
|
|
|
|
if k != "@aws" {
|
|
return hostnameOverride, nil
|
|
}
|
|
|
|
// We recognize @aws as meaning "the local-hostname from the aws metadata service"
|
|
vBytes, err := vfs.Context.ReadFile("metadata://aws/meta-data/local-hostname")
|
|
if err != nil {
|
|
return "", fmt.Errorf("error reading local hostname from AWS metadata: %v", err)
|
|
}
|
|
|
|
// The local-hostname gets it's hostname from the AWS DHCP Option Set, which
|
|
// may provide multiple hostnames separated by spaces. For now just choose
|
|
// the first one as the hostname.
|
|
domains := strings.Fields(string(vBytes))
|
|
if len(domains) == 0 {
|
|
glog.Warningf("Local hostname from AWS metadata service was empty")
|
|
return "", nil
|
|
}
|
|
domain := domains[0]
|
|
|
|
glog.Infof("Using hostname from AWS metadata service: %s", domain)
|
|
|
|
return domain, nil
|
|
}
|
|
|
|
// FindCert is a helper method to retrieving a certificate from the store
|
|
func (c *NodeupModelContext) FindCert(name string) ([]byte, error) {
|
|
cert, err := c.KeyStore.FindCert(name)
|
|
if err != nil {
|
|
return []byte{}, fmt.Errorf("error fetching certificate: %v from keystore: %v", name, err)
|
|
}
|
|
if cert == nil {
|
|
return []byte{}, fmt.Errorf("unable to found certificate: %s", name)
|
|
}
|
|
|
|
return cert.AsBytes()
|
|
}
|
|
|
|
// FindPrivateKey is a helper method to retrieving a private key from the store
|
|
func (c *NodeupModelContext) FindPrivateKey(name string) ([]byte, error) {
|
|
key, err := c.KeyStore.FindPrivateKey(name)
|
|
if err != nil {
|
|
return []byte{}, fmt.Errorf("error fetching private key: %v from keystore: %v", name, err)
|
|
}
|
|
if key == nil {
|
|
return []byte{}, fmt.Errorf("unable to found private key: %s", name)
|
|
}
|
|
|
|
return key.AsBytes()
|
|
}
|