github.com/SUSE/skuba@v1.4.17/pkg/skuba/actions/cluster/init/init.go (about)

     1  /*
     2   * Copyright (c) 2019 SUSE LLC.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   *
    16   */
    17  
    18  package cluster
    19  
    20  import (
    21  	"bytes"
    22  	"fmt"
    23  	"io/ioutil"
    24  	"os"
    25  	"path/filepath"
    26  	"text/template"
    27  
    28  	"github.com/pkg/errors"
    29  	v1 "k8s.io/api/core/v1"
    30  	"k8s.io/apimachinery/pkg/runtime/schema"
    31  	versionutil "k8s.io/apimachinery/pkg/util/version"
    32  	"k8s.io/klog"
    33  	kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
    34  	kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme"
    35  	kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
    36  	kubeadmconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config"
    37  
    38  	"github.com/SUSE/skuba/internal/pkg/skuba/addons"
    39  	"github.com/SUSE/skuba/internal/pkg/skuba/kubeadm"
    40  	"github.com/SUSE/skuba/internal/pkg/skuba/kubernetes"
    41  	"github.com/SUSE/skuba/internal/pkg/skuba/util"
    42  	"github.com/SUSE/skuba/pkg/skuba"
    43  )
    44  
    45  // Basic initial cluster configuration
    46  type InitConfiguration struct {
    47  	ClusterName       string
    48  	ControlPlane      string
    49  	PauseImage        string
    50  	KubernetesVersion *versionutil.Version
    51  	ImageRepository   string
    52  	EtcdImageTag      string
    53  	CoreDNSImageTag   string
    54  	CloudProvider     string
    55  	StrictCapDefaults bool
    56  	// Note: UseHyperKube can be removed when we drop the support of
    57  	// provisioning clusters of version 1.17.
    58  	UseHyperKube bool
    59  }
    60  
    61  func (initConfiguration InitConfiguration) ControlPlaneHost() string {
    62  	return util.ControlPlaneHost(initConfiguration.ControlPlane)
    63  }
    64  
    65  func (initConfiguration InitConfiguration) ControlPlaneHostAndPort() string {
    66  	return util.ControlPlaneHostAndPort(initConfiguration.ControlPlane)
    67  }
    68  
    69  func NewInitConfiguration(clusterName, cloudProvider, controlPlane, kubernetesDesiredVersion string, strictCapDefaults bool) (InitConfiguration, error) {
    70  	kubernetesVersion := kubernetes.LatestVersion()
    71  	var err error
    72  	needsHyperKube := false
    73  	if kubernetesDesiredVersion != "" {
    74  		kubernetesVersion, err = versionutil.ParseSemantic(kubernetesDesiredVersion)
    75  		if err != nil || !kubernetes.IsVersionAvailable(kubernetesVersion) {
    76  			return InitConfiguration{}, fmt.Errorf("Version %s does not exist or cannot be parsed.\n", kubernetesDesiredVersion)
    77  		}
    78  	}
    79  
    80  	// Without this, it will be impossible to greenfield an older caasp cluster:
    81  	// defaults have been changed in 1.17, so we *need* to have UseHyperKubeImage: set into the init configuration.
    82  	if kubernetesVersion.Minor() < 18 {
    83  		needsHyperKube = true
    84  	}
    85  
    86  	return InitConfiguration{
    87  		ClusterName:       clusterName,
    88  		CloudProvider:     cloudProvider,
    89  		ControlPlane:      controlPlane,
    90  		PauseImage:        kubernetes.ComponentContainerImageForClusterVersion(kubernetes.Pause, kubernetesVersion),
    91  		KubernetesVersion: kubernetesVersion,
    92  		ImageRepository:   skuba.ImageRepository,
    93  		EtcdImageTag:      kubernetes.ComponentVersionForClusterVersion(kubernetes.Etcd, kubernetesVersion),
    94  		CoreDNSImageTag:   kubernetes.ComponentVersionForClusterVersion(kubernetes.CoreDNS, kubernetesVersion),
    95  		StrictCapDefaults: strictCapDefaults,
    96  		UseHyperKube:      needsHyperKube,
    97  	}, nil
    98  }
    99  
   100  // Init creates a cluster definition scaffold in the local machine, in the current
   101  // folder, at a directory named after ClusterName provided in the InitConfiguration
   102  // parameter
   103  func Init(initConfiguration InitConfiguration) error {
   104  	if _, err := os.Stat(initConfiguration.ClusterName); err == nil {
   105  		return errors.Errorf("cluster configuration directory %q already exists", initConfiguration.ClusterName)
   106  	}
   107  
   108  	scaffoldFilesToWrite := criScaffoldFiles["criconfig"]
   109  	kubernetesVersion := initConfiguration.KubernetesVersion
   110  	if kubernetesVersion.Minor() < 18 {
   111  		scaffoldFilesToWrite = criScaffoldFiles["sysconfig"]
   112  	}
   113  
   114  	if len(initConfiguration.CloudProvider) > 0 {
   115  		if cloudScaffoldFiles, found := cloudScaffoldFiles[initConfiguration.CloudProvider]; found {
   116  			scaffoldFilesToWrite = append(scaffoldFilesToWrite, cloudScaffoldFiles...)
   117  		} else {
   118  			klog.Fatalf("unknown cloud provider integration provided: %s", initConfiguration.CloudProvider)
   119  		}
   120  	}
   121  
   122  	if err := os.MkdirAll(initConfiguration.ClusterName, 0700); err != nil {
   123  		return errors.Wrapf(err, "could not create cluster directory %q", initConfiguration.ClusterName)
   124  	}
   125  	if err := os.Chdir(initConfiguration.ClusterName); err != nil {
   126  		return errors.Wrapf(err, "could not change to cluster directory %q", initConfiguration.ClusterName)
   127  	}
   128  	for _, file := range scaffoldFilesToWrite {
   129  		filePath, _ := filepath.Split(file.Location)
   130  		if filePath != "" {
   131  			if err := os.MkdirAll(filePath, 0700); err != nil {
   132  				return errors.Wrapf(err, "could not create directory %q", filePath)
   133  			}
   134  		}
   135  		f, err := os.Create(file.Location)
   136  		if err != nil {
   137  			return errors.Wrapf(err, "could not create file %q", file.Location)
   138  		}
   139  		str, err := renderTemplate(file.Content, initConfiguration)
   140  		if err != nil {
   141  			return errors.Wrap(err, "unable to render template")
   142  		}
   143  		_, err = f.WriteString(str)
   144  		if err != nil {
   145  			return errors.Wrapf(err, "unable to write template to file %s", f.Name())
   146  		}
   147  		if err := f.Chmod(0600); err != nil {
   148  			return errors.Wrapf(err, "unable to chmod file %s", f.Name())
   149  		}
   150  		if err := f.Close(); err != nil {
   151  			return errors.Wrapf(err, "unable to close file %s", f.Name())
   152  		}
   153  	}
   154  
   155  	// Write kubeadm-init.conf and kubeadm-join.conf.d templates
   156  	if err := writeKubeadmInitConf(initConfiguration); err != nil {
   157  		return err
   158  	}
   159  	if err := os.MkdirAll(skuba.JoinConfDir(), 0700); err != nil {
   160  		return errors.Wrapf(err, "could not create directory %q", skuba.JoinConfDir())
   161  	}
   162  	if err := writeKubeadmJoinMasterConf(initConfiguration); err != nil {
   163  		return err
   164  	}
   165  	if err := writeKubeadmJoinWorkerConf(initConfiguration); err != nil {
   166  		return err
   167  	}
   168  
   169  	// Write addon configuration files
   170  	addonConfiguration := addons.AddonConfiguration{
   171  		ClusterVersion: initConfiguration.KubernetesVersion,
   172  		ControlPlane:   initConfiguration.ControlPlane,
   173  		ClusterName:    initConfiguration.ClusterName,
   174  	}
   175  	for addonName, addon := range addons.Addons {
   176  		if !addon.IsPresentForClusterVersion(initConfiguration.KubernetesVersion) {
   177  			continue
   178  		}
   179  		if err := addon.Write(addonConfiguration); err != nil {
   180  			return errors.Wrapf(err, "could not write %q addon configuration", addonName)
   181  		}
   182  	}
   183  
   184  	currentDir, err := os.Getwd()
   185  	if err != nil {
   186  		fmt.Println("[init] configuration files written, unable to get directory")
   187  		return nil
   188  	}
   189  
   190  	fmt.Printf("[init] configuration files written to %s\n", currentDir)
   191  	return nil
   192  }
   193  
   194  func renderTemplate(templateContents string, initConfiguration InitConfiguration) (string, error) {
   195  	template, err := template.New("").Parse(templateContents)
   196  	if err != nil {
   197  		return "", errors.Wrap(err, "could not parse template")
   198  	}
   199  	var rendered bytes.Buffer
   200  	if err := template.Execute(&rendered, initConfiguration); err != nil {
   201  		return "", errors.Wrap(err, "could not render configuration")
   202  	}
   203  	return rendered.String(), nil
   204  }
   205  
   206  func writeKubeadmInitConf(initConfiguration InitConfiguration) error {
   207  	initCfg := kubeadmapi.InitConfiguration{
   208  		ClusterConfiguration: kubeadmapi.ClusterConfiguration{
   209  			APIServer: kubeadmapi.APIServer{
   210  				CertSANs: []string{initConfiguration.ControlPlaneHost()},
   211  				ControlPlaneComponent: kubeadmapi.ControlPlaneComponent{
   212  					ExtraArgs: map[string]string{
   213  						"oidc-issuer-url":     fmt.Sprintf("https://%s:32000", initConfiguration.ControlPlaneHost()),
   214  						"oidc-client-id":      "oidc",
   215  						"oidc-ca-file":        "/etc/kubernetes/pki/ca.crt",
   216  						"oidc-username-claim": "email",
   217  						"oidc-groups-claim":   "groups",
   218  					},
   219  				},
   220  			},
   221  			ClusterName:          initConfiguration.ClusterName,
   222  			ControlPlaneEndpoint: initConfiguration.ControlPlaneHostAndPort(),
   223  			DNS: kubeadmapi.DNS{
   224  				Type: kubeadmapi.CoreDNS,
   225  				ImageMeta: kubeadmapi.ImageMeta{
   226  					ImageRepository: initConfiguration.ImageRepository,
   227  					ImageTag:        initConfiguration.CoreDNSImageTag,
   228  				},
   229  			},
   230  			Etcd: kubeadmapi.Etcd{
   231  				Local: &kubeadmapi.LocalEtcd{
   232  					ImageMeta: kubeadmapi.ImageMeta{
   233  						ImageRepository: initConfiguration.ImageRepository,
   234  						ImageTag:        initConfiguration.EtcdImageTag,
   235  					},
   236  				},
   237  			},
   238  			ImageRepository:   initConfiguration.ImageRepository,
   239  			KubernetesVersion: initConfiguration.KubernetesVersion.String(),
   240  			Networking: kubeadmapi.Networking{
   241  				PodSubnet:     "10.244.0.0/16",
   242  				ServiceSubnet: "10.96.0.0/12",
   243  			},
   244  			UseHyperKubeImage: initConfiguration.UseHyperKube,
   245  		},
   246  	}
   247  	if len(initConfiguration.CloudProvider) > 0 {
   248  		updateInitConfigurationWithCloudIntegration(&initCfg, initConfiguration)
   249  	}
   250  	kubeadm.UpdateClusterConfigurationWithClusterVersion(&initCfg, initConfiguration.KubernetesVersion)
   251  	initCfgContents, err := kubeadmconfigutil.MarshalInitConfigurationToBytes(&initCfg, schema.GroupVersion{
   252  		Group:   "kubeadm.k8s.io",
   253  		Version: kubeadm.GetKubeadmApisVersion(initConfiguration.KubernetesVersion),
   254  	})
   255  	if err != nil {
   256  		return err
   257  	}
   258  	if err := ioutil.WriteFile(skuba.KubeadmInitConfFile(), initCfgContents, 0600); err != nil {
   259  		return errors.Wrap(err, "error writing init configuration")
   260  	}
   261  	return nil
   262  }
   263  
   264  func writeKubeadmJoinMasterConf(initConfiguration InitConfiguration) error {
   265  	joinCfg := kubeadmapi.JoinConfiguration{
   266  		Discovery: kubeadmapi.Discovery{
   267  			BootstrapToken: &kubeadmapi.BootstrapTokenDiscovery{
   268  				APIServerEndpoint:        initConfiguration.ControlPlaneHostAndPort(),
   269  				UnsafeSkipCAVerification: true,
   270  			},
   271  		},
   272  		ControlPlane: &kubeadmapi.JoinControlPlane{},
   273  	}
   274  	if len(initConfiguration.CloudProvider) > 0 {
   275  		updateJoinConfigurationWithCloudIntegration(&joinCfg, initConfiguration)
   276  	}
   277  	joinCfgContents, err := kubeadmutil.MarshalToYamlForCodecs(&joinCfg, schema.GroupVersion{
   278  		Group:   "kubeadm.k8s.io",
   279  		Version: kubeadm.GetKubeadmApisVersion(initConfiguration.KubernetesVersion),
   280  	}, kubeadmscheme.Codecs)
   281  	if err != nil {
   282  		return err
   283  	}
   284  	if err := ioutil.WriteFile(skuba.MasterConfTemplateFile(), joinCfgContents, 0600); err != nil {
   285  		return errors.Wrap(err, "error writing control plane join configuration")
   286  	}
   287  	return nil
   288  }
   289  
   290  func writeKubeadmJoinWorkerConf(initConfiguration InitConfiguration) error {
   291  	joinCfg := kubeadmapi.JoinConfiguration{
   292  		Discovery: kubeadmapi.Discovery{
   293  			BootstrapToken: &kubeadmapi.BootstrapTokenDiscovery{
   294  				APIServerEndpoint:        initConfiguration.ControlPlaneHostAndPort(),
   295  				UnsafeSkipCAVerification: true,
   296  			},
   297  		},
   298  	}
   299  	if len(initConfiguration.CloudProvider) > 0 {
   300  		updateJoinConfigurationWithCloudIntegration(&joinCfg, initConfiguration)
   301  	}
   302  	joinCfgContents, err := kubeadmutil.MarshalToYamlForCodecs(&joinCfg, schema.GroupVersion{
   303  		Group:   "kubeadm.k8s.io",
   304  		Version: kubeadm.GetKubeadmApisVersion(initConfiguration.KubernetesVersion),
   305  	}, kubeadmscheme.Codecs)
   306  	if err != nil {
   307  		return err
   308  	}
   309  	if err := ioutil.WriteFile(skuba.WorkerConfTemplateFile(), joinCfgContents, 0600); err != nil {
   310  		return errors.Wrap(err, "error writing worker join configuration")
   311  	}
   312  	return nil
   313  }
   314  
   315  func updateInitConfigurationWithCloudIntegration(initCfg *kubeadmapi.InitConfiguration, initConfiguration InitConfiguration) {
   316  	if initCfg.APIServer.ExtraArgs == nil {
   317  		initCfg.APIServer.ExtraArgs = map[string]string{}
   318  	}
   319  	initCfg.APIServer.ExtraArgs["cloud-provider"] = initConfiguration.CloudProvider
   320  	if initCfg.ControllerManager.ExtraArgs == nil {
   321  		initCfg.ControllerManager.ExtraArgs = map[string]string{}
   322  	}
   323  	initCfg.ControllerManager.ExtraArgs["cloud-provider"] = initConfiguration.CloudProvider
   324  	if initCfg.NodeRegistration.KubeletExtraArgs == nil {
   325  		initCfg.NodeRegistration.KubeletExtraArgs = map[string]string{}
   326  	}
   327  	initCfg.NodeRegistration.KubeletExtraArgs["cloud-provider"] = initConfiguration.CloudProvider
   328  
   329  	switch initConfiguration.CloudProvider {
   330  	case "aws":
   331  		initCfg.ControllerManager.ExtraArgs["allocate-node-cidrs"] = "false"
   332  	case "openstack":
   333  		initCfg.APIServer.ExtraArgs["cloud-config"] = skuba.OpenstackConfigRuntimeFile()
   334  		initCfg.APIServer.ExtraVolumes = append(initCfg.APIServer.ExtraVolumes, kubeadmapi.HostPathMount{
   335  			Name:      "cloud-config",
   336  			HostPath:  skuba.OpenstackConfigRuntimeFile(),
   337  			MountPath: skuba.OpenstackConfigRuntimeFile(),
   338  			ReadOnly:  true,
   339  			PathType:  v1.HostPathFileOrCreate,
   340  		})
   341  		initCfg.ControllerManager.ExtraArgs["cloud-config"] = skuba.OpenstackConfigRuntimeFile()
   342  		initCfg.ControllerManager.ExtraVolumes = append(initCfg.ControllerManager.ExtraVolumes, kubeadmapi.HostPathMount{
   343  			Name:      "cloud-config",
   344  			HostPath:  skuba.OpenstackConfigRuntimeFile(),
   345  			MountPath: skuba.OpenstackConfigRuntimeFile(),
   346  			ReadOnly:  true,
   347  			PathType:  v1.HostPathFileOrCreate,
   348  		})
   349  		initCfg.NodeRegistration.KubeletExtraArgs["cloud-config"] = skuba.OpenstackConfigRuntimeFile()
   350  	case "vsphere":
   351  		initCfg.APIServer.ExtraArgs["cloud-config"] = skuba.VSphereConfigRuntimeFile()
   352  		initCfg.APIServer.ExtraVolumes = append(initCfg.APIServer.ExtraVolumes, kubeadmapi.HostPathMount{
   353  			Name:      "cloud-config",
   354  			HostPath:  skuba.VSphereConfigRuntimeFile(),
   355  			MountPath: skuba.VSphereConfigRuntimeFile(),
   356  			ReadOnly:  true,
   357  			PathType:  v1.HostPathFileOrCreate,
   358  		})
   359  		initCfg.ControllerManager.ExtraArgs["cloud-config"] = skuba.VSphereConfigRuntimeFile()
   360  		initCfg.ControllerManager.ExtraVolumes = append(initCfg.ControllerManager.ExtraVolumes, kubeadmapi.HostPathMount{
   361  			Name:      "cloud-config",
   362  			HostPath:  skuba.VSphereConfigRuntimeFile(),
   363  			MountPath: skuba.VSphereConfigRuntimeFile(),
   364  			ReadOnly:  true,
   365  			PathType:  v1.HostPathFileOrCreate,
   366  		})
   367  		initCfg.NodeRegistration.KubeletExtraArgs["cloud-config"] = skuba.VSphereConfigRuntimeFile()
   368  	}
   369  }
   370  
   371  func updateJoinConfigurationWithCloudIntegration(joinCfg *kubeadmapi.JoinConfiguration, initConfiguration InitConfiguration) {
   372  	if joinCfg.NodeRegistration.KubeletExtraArgs == nil {
   373  		joinCfg.NodeRegistration.KubeletExtraArgs = map[string]string{}
   374  	}
   375  	joinCfg.NodeRegistration.KubeletExtraArgs["cloud-provider"] = initConfiguration.CloudProvider
   376  
   377  	switch initConfiguration.CloudProvider {
   378  	case "openstack":
   379  		joinCfg.NodeRegistration.KubeletExtraArgs["cloud-config"] = skuba.OpenstackConfigRuntimeFile()
   380  	case "vsphere":
   381  		joinCfg.NodeRegistration.KubeletExtraArgs["cloud-config"] = skuba.VSphereConfigRuntimeFile()
   382  	}
   383  }