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 }