github.com/sealerio/sealer@v0.11.1-0.20240507115618-f4f89c5853ae/pkg/clustercert/kubeconfig.go (about) 1 // Copyright 2016 The Kubernetes Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package clustercert 16 17 import ( 18 "bytes" 19 "crypto" 20 "crypto/x509" 21 "fmt" 22 "os" 23 "path/filepath" 24 25 "github.com/sealerio/sealer/pkg/clustercert/cert" 26 27 "github.com/pkg/errors" 28 "github.com/sirupsen/logrus" 29 "k8s.io/client-go/tools/clientcmd" 30 clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 31 "k8s.io/client-go/util/keyutil" 32 ) 33 34 // clientCertAuth struct holds info required to build a client certificate to provide authentication info in a kubeconfig object 35 type clientCertAuth struct { 36 CAKey crypto.Signer 37 Organizations []string 38 } 39 40 // tokenAuth struct holds info required to use a token to provide authentication info in a kubeconfig object 41 type tokenAuth struct { 42 Token string 43 } 44 45 // kubeConfigSpec struct holds info required to build a KubeConfig object 46 type kubeConfigSpec struct { 47 CACert *x509.Certificate 48 APIServer string 49 ClientName string 50 TokenAuth *tokenAuth 51 ClientCertAuth *clientCertAuth 52 } 53 54 // CreateJoinControlPlaneKubeConfigFiles will create and write to disk the kubeconfig files required by kubeadm 55 // join --control-plane workflow, plus the admin kubeconfig file used by the administrator and kubeadm itself; the 56 // kubelet.conf file must not be created because it will be created and signed by the kubelet TLS bootstrap process. 57 // If any kubeconfig files already exists, it used only if evaluated equal; otherwise an error is returned. 58 func CreateJoinControlPlaneKubeConfigFiles(outDir string, caCertPath, caCertName, nodeName, controlPlaneEndpoint, clusterName string) error { 59 return createKubeConfigFiles( 60 outDir, 61 caCertPath, caCertName, 62 nodeName, 63 controlPlaneEndpoint, 64 clusterName, 65 "admin.conf", 66 "controller-manager.conf", 67 "scheduler.conf", 68 "kubelet.conf", 69 ) 70 } 71 72 // createKubeConfigFiles creates all the requested kubeconfig files. 73 // If kubeconfig files already exists, they are used only if evaluated equal; otherwise an error is returned. 74 func createKubeConfigFiles(outDir string, certPath, certName, nodeName, controlPlaneEndpoint, clusterName string, kubeConfigFileNames ...string) error { 75 // gets the KubeConfigSpecs, actualized for the current InitConfiguration 76 specs, err := getKubeConfigSpecs(certPath, certName, nodeName, controlPlaneEndpoint) 77 if err != nil { 78 return err 79 } 80 81 for _, kubeConfigFileName := range kubeConfigFileNames { 82 // retrieves the KubeConfigSpec for given kubeConfigFileName 83 spec, exists := specs[kubeConfigFileName] 84 if !exists { 85 return errors.Errorf("couldn't retrieve KubeConfigSpec for %s", kubeConfigFileName) 86 } 87 88 // builds the KubeConfig object 89 config, err := buildKubeConfigFromSpec(spec, clusterName) 90 if err != nil { 91 return err 92 } 93 94 // writes the kubeconfig to disk if it not exists 95 if err = createKubeConfigFileIfNotExists(outDir, kubeConfigFileName, config); err != nil { 96 return err 97 } 98 } 99 100 return nil 101 } 102 103 // getKubeConfigSpecs returns all KubeConfigSpecs actualized to the context of the current InitConfiguration 104 // NB. these methods holds the information about how kubeadm creates kubeconfig files. 105 func getKubeConfigSpecs(certPath, certName, nodeName, controlPlaneEndpoint string) (map[string]*kubeConfigSpec, error) { 106 caCert, caKey, err := cert.NewCertificateFileManger(certPath, certName).Read() 107 if err != nil { 108 return nil, errors.Wrap(err, "couldn't create a kubeconfig; the CA cert file couldn't be loaded") 109 } 110 111 if len(nodeName) == 0 { 112 return nil, errors.New("nodeName can not be empty") 113 } 114 115 if len(controlPlaneEndpoint) == 0 { 116 return nil, errors.New("controlPlaneEndpoint can not be empty") 117 } 118 119 var kubeConfigSpec = map[string]*kubeConfigSpec{ 120 "admin.conf": { 121 CACert: caCert, 122 APIServer: controlPlaneEndpoint, 123 ClientName: "kubernetes-admin", 124 ClientCertAuth: &clientCertAuth{ 125 CAKey: caKey, 126 Organizations: []string{"system:masters"}, 127 }, 128 }, 129 "kubelet.conf": { 130 CACert: caCert, 131 APIServer: controlPlaneEndpoint, 132 ClientName: fmt.Sprintf("%s%s", "system:node:", nodeName), 133 ClientCertAuth: &clientCertAuth{ 134 CAKey: caKey, 135 Organizations: []string{"system:nodes"}, 136 }, 137 }, 138 "controller-manager.conf": { 139 CACert: caCert, 140 APIServer: controlPlaneEndpoint, 141 ClientName: "system:kube-controller-manager", 142 ClientCertAuth: &clientCertAuth{ 143 CAKey: caKey, 144 }, 145 }, 146 "scheduler.conf": { 147 CACert: caCert, 148 APIServer: controlPlaneEndpoint, 149 ClientName: "system:kube-scheduler", 150 ClientCertAuth: &clientCertAuth{ 151 CAKey: caKey, 152 }, 153 }, 154 } 155 156 return kubeConfigSpec, nil 157 } 158 159 // buildKubeConfigFromSpec creates a kubeconfig object for the given kubeConfigSpec 160 func buildKubeConfigFromSpec(spec *kubeConfigSpec, clustername string) (*clientcmdapi.Config, error) { 161 // If this kubeconfig should use token 162 if spec.TokenAuth != nil { 163 // create a kubeconfig with a token 164 return CreateWithToken( 165 spec.APIServer, 166 clustername, 167 spec.ClientName, 168 cert.EncodeCertPEM(spec.CACert), 169 spec.TokenAuth.Token, 170 ), nil 171 } 172 173 // otherwise, create a client certs 174 clientCertConfig := cert.CertificateDescriptor{ 175 CommonName: spec.ClientName, 176 Organization: spec.ClientCertAuth.Organizations, 177 Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, 178 Year: 100, 179 } 180 181 g, err := cert.NewCommonCertificateGenerator(clientCertConfig, spec.CACert, spec.ClientCertAuth.CAKey) 182 if err != nil { 183 return nil, err 184 } 185 186 clientCert, clientKey, err := g.Generate() 187 188 if err != nil { 189 return nil, errors.Wrapf(err, "failure while creating %s client certificate", spec.ClientName) 190 } 191 192 encodedClientKey, err := keyutil.MarshalPrivateKeyToPEM(clientKey) 193 if err != nil { 194 return nil, errors.Wrapf(err, "failed to marshal private key to PEM") 195 } 196 // create a kubeconfig with the client certs 197 return CreateWithCerts( 198 spec.APIServer, 199 clustername, 200 spec.ClientName, 201 cert.EncodeCertPEM(spec.CACert), 202 encodedClientKey, 203 cert.EncodeCertPEM(clientCert), 204 ), nil 205 } 206 207 // validateKubeConfig check if the kubeconfig file exist and has the expected CA and server URL 208 func validateKubeConfig(outDir, filename string, config *clientcmdapi.Config) error { 209 kubeConfigFilePath := filepath.Join(outDir, filename) 210 211 if _, err := os.Stat(kubeConfigFilePath); err != nil { 212 return err 213 } 214 215 // The kubeconfig already exists, let's check if it has got the same CA and server URL 216 currentConfig, err := clientcmd.LoadFromFile(kubeConfigFilePath) 217 if err != nil { 218 return errors.Wrapf(err, "failed to load kubeconfig file %s that already exists on disk", kubeConfigFilePath) 219 } 220 221 expectedCtx, exists := config.Contexts[config.CurrentContext] 222 if !exists { 223 return errors.Errorf("failed to find expected context %s", config.CurrentContext) 224 } 225 expectedCluster := expectedCtx.Cluster 226 currentCtx, exists := currentConfig.Contexts[currentConfig.CurrentContext] 227 if !exists { 228 return errors.Errorf("failed to find CurrentContext in Contexts of the kubeconfig file %s", kubeConfigFilePath) 229 } 230 currentCluster := currentCtx.Cluster 231 if currentConfig.Clusters[currentCluster] == nil { 232 return errors.Errorf("failed to find the given CurrentContext Cluster in Clusters of the kubeconfig file %s", kubeConfigFilePath) 233 } 234 235 // Make sure the compared CAs are whitespace-trimmed. The function clientcmd.LoadFromFile() just decodes 236 // the base64 CA and places it raw in the v1.Config object. In case the user has extra whitespace 237 // in the CA they used to create a kubeconfig this comparison to a generated v1.Config will otherwise fail. 238 caCurrent := bytes.TrimSpace(currentConfig.Clusters[currentCluster].CertificateAuthorityData) 239 caExpected := bytes.TrimSpace(config.Clusters[expectedCluster].CertificateAuthorityData) 240 241 // If the current CA cert on disk doesn't match the expected CA cert, error out because we have a file, but it's stale 242 if !bytes.Equal(caCurrent, caExpected) { 243 return errors.Errorf("a kubeconfig file %q exists already but has got the wrong CA cert", kubeConfigFilePath) 244 } 245 // If the current API Server location on disk doesn't match the expected API server, error out because we have a file, but it's stale 246 if currentConfig.Clusters[currentCluster].Server != config.Clusters[expectedCluster].Server { 247 return errors.Errorf("a kubeconfig file %q exists already but has got the wrong API Server URL", kubeConfigFilePath) 248 } 249 250 return nil 251 } 252 253 // createKubeConfigFileIfNotExists saves the KubeConfig object into a file if there isn't any file at the given path. 254 // If there already is a kubeconfig file at the given path; kubeadm tries to load it and check if the values in the 255 // existing and the expected config equals. If they do; kubeadm will just skip writing the file as it's up-to-date, 256 // but if a file exists but has old content or isn't a kubeconfig file, this function returns an error. 257 func createKubeConfigFileIfNotExists(outDir, filename string, config *clientcmdapi.Config) error { 258 kubeConfigFilePath := filepath.Join(outDir, filename) 259 260 err := validateKubeConfig(outDir, filename, config) 261 if err != nil { 262 // Check if the file exist, and if it doesn't, just write it to disk 263 if !os.IsNotExist(err) { 264 return err 265 } 266 logrus.Infof("[kubeconfig] Writing %q kubeconfig file", filename) 267 err = WriteToDisk(kubeConfigFilePath, config) 268 if err != nil { 269 return errors.Wrapf(err, "failed to save kubeconfig file %q on disk", kubeConfigFilePath) 270 } 271 return nil 272 } 273 // kubeadm doesn't validate the existing kubeconfig file more than this (kubeadm trusts the client certs to be valid) 274 // Basically, if we find a kubeconfig file with the same path; the same CA cert and the same server URL; 275 // kubeadm thinks those files are equal and doesn't bother writing a new file 276 logrus.Infof("[kubeconfig] Using existing kubeconfig file: %q\n", kubeConfigFilePath) 277 278 return nil 279 } 280 281 // cmd/kubeadm/app/util/kubeconfig/kubeconfig.go 282 // CreateBasic creates a basic, general KubeConfig object that then can be extended 283 func CreateBasic(serverURL, clusterName, userName string, caCert []byte) *clientcmdapi.Config { 284 // Use the cluster and the username as the context name 285 contextName := fmt.Sprintf("%s@%s", userName, clusterName) 286 287 return &clientcmdapi.Config{ 288 Clusters: map[string]*clientcmdapi.Cluster{ 289 clusterName: { 290 Server: serverURL, 291 CertificateAuthorityData: caCert, 292 }, 293 }, 294 Contexts: map[string]*clientcmdapi.Context{ 295 contextName: { 296 Cluster: clusterName, 297 AuthInfo: userName, 298 }, 299 }, 300 AuthInfos: map[string]*clientcmdapi.AuthInfo{}, 301 CurrentContext: contextName, 302 } 303 } 304 305 // cmd/kubeadm/app/util/kubeconfig/kubeconfig.go 306 // CreateWithToken creates a KubeConfig object with access to the API server with a token 307 func CreateWithToken(serverURL, clusterName, userName string, caCert []byte, token string) *clientcmdapi.Config { 308 config := CreateBasic(serverURL, clusterName, userName, caCert) 309 config.AuthInfos[userName] = &clientcmdapi.AuthInfo{ 310 Token: token, 311 } 312 return config 313 } 314 315 // cmd/kubeadm/app/util/kubeconfig/kubeconfig.go 316 // CreateWithCerts creates a KubeConfig object with access to the API server with client certificates 317 func CreateWithCerts(serverURL, clusterName, userName string, caCert []byte, clientKey []byte, clientCert []byte) *clientcmdapi.Config { 318 config := CreateBasic(serverURL, clusterName, userName, caCert) 319 config.AuthInfos[userName] = &clientcmdapi.AuthInfo{ 320 ClientKeyData: clientKey, 321 ClientCertificateData: clientCert, 322 } 323 return config 324 } 325 326 // WriteToDisk writes a KubeConfig object down to disk with mode 0600 327 func WriteToDisk(filename string, kubeconfig *clientcmdapi.Config) error { 328 err := clientcmd.WriteToFile(*kubeconfig, filename) 329 if err != nil { 330 return err 331 } 332 333 return nil 334 }