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  }