k8s.io/kubernetes@v1.29.3/pkg/kubelet/certificate/bootstrap/bootstrap_test.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     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  package bootstrap
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"path/filepath"
    25  	"reflect"
    26  	"testing"
    27  
    28  	utiltesting "k8s.io/client-go/util/testing"
    29  
    30  	"github.com/google/go-cmp/cmp"
    31  	certificatesv1 "k8s.io/api/certificates/v1"
    32  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	"k8s.io/apimachinery/pkg/runtime"
    35  	"k8s.io/apimachinery/pkg/watch"
    36  	"k8s.io/client-go/kubernetes/fake"
    37  	certificatesclient "k8s.io/client-go/kubernetes/typed/certificates/v1beta1"
    38  	restclient "k8s.io/client-go/rest"
    39  	clienttesting "k8s.io/client-go/testing"
    40  	"k8s.io/client-go/util/certificate"
    41  	"k8s.io/client-go/util/keyutil"
    42  )
    43  
    44  func copyFile(src, dst string) (err error) {
    45  	in, err := os.Open(src)
    46  	if err != nil {
    47  		return err
    48  	}
    49  	defer in.Close()
    50  	out, err := os.Create(dst)
    51  	if err != nil {
    52  		return err
    53  	}
    54  	defer func() {
    55  		cerr := out.Close()
    56  		if err == nil {
    57  			err = cerr
    58  		}
    59  	}()
    60  	_, err = io.Copy(out, in)
    61  	return err
    62  }
    63  
    64  func TestLoadClientConfig(t *testing.T) {
    65  	//Create a temporary folder under tmp to store the required certificate files and configuration files.
    66  	fileDir := t.TempDir()
    67  	//Copy the required certificate file to the temporary directory.
    68  	copyFile("./testdata/mycertinvalid.crt", fileDir+"/mycertinvalid.crt")
    69  	copyFile("./testdata/mycertvalid.crt", fileDir+"/mycertvalid.crt")
    70  	copyFile("./testdata/mycertinvalid.key", fileDir+"/mycertinvalid.key")
    71  	copyFile("./testdata/mycertvalid.key", fileDir+"/mycertvalid.key")
    72  	testDataValid := []byte(`
    73  apiVersion: v1
    74  kind: Config
    75  clusters:
    76  - cluster:
    77      certificate-authority: ca-a.crt
    78      server: https://cluster-a.com
    79    name: cluster-a
    80  - cluster:
    81      server: https://cluster-b.com
    82    name: cluster-b
    83  contexts:
    84  - context:
    85      cluster: cluster-a
    86      namespace: ns-a
    87      user: user-a
    88    name: context-a
    89  - context:
    90      cluster: cluster-b
    91      namespace: ns-b
    92      user: user-b
    93    name: context-b
    94  current-context: context-b
    95  users:
    96  - name: user-a
    97    user:
    98      client-certificate: mycertvalid.crt
    99      client-key: mycertvalid.key
   100  - name: user-b
   101    user:
   102      client-certificate: mycertvalid.crt
   103      client-key: mycertvalid.key
   104  
   105  `)
   106  	filevalid, err := os.CreateTemp(fileDir, "kubeconfigvalid")
   107  	if err != nil {
   108  		t.Fatal(err)
   109  	}
   110  	// os.CreateTemp also opens the file, and removing it without closing it will result in a failure.
   111  	defer filevalid.Close()
   112  	os.WriteFile(filevalid.Name(), testDataValid, os.FileMode(0755))
   113  
   114  	testDataInvalid := []byte(`
   115  apiVersion: v1
   116  kind: Config
   117  clusters:
   118  - cluster:
   119      certificate-authority: ca-a.crt
   120      server: https://cluster-a.com
   121    name: cluster-a
   122  - cluster:
   123      server: https://cluster-b.com
   124    name: cluster-b
   125  contexts:
   126  - context:
   127      cluster: cluster-a
   128      namespace: ns-a
   129      user: user-a
   130    name: context-a
   131  - context:
   132      cluster: cluster-b
   133      namespace: ns-b
   134      user: user-b
   135    name: context-b
   136  current-context: context-b
   137  users:
   138  - name: user-a
   139    user:
   140      client-certificate: mycertinvalid.crt
   141      client-key: mycertinvalid.key
   142  - name: user-b
   143    user:
   144      client-certificate: mycertinvalid.crt
   145      client-key: mycertinvalid.key
   146  
   147  `)
   148  	fileinvalid, err := os.CreateTemp(fileDir, "kubeconfiginvalid")
   149  	if err != nil {
   150  		t.Fatal(err)
   151  	}
   152  	defer fileinvalid.Close()
   153  	os.WriteFile(fileinvalid.Name(), testDataInvalid, os.FileMode(0755))
   154  
   155  	testDatabootstrap := []byte(`
   156  apiVersion: v1
   157  kind: Config
   158  clusters:
   159  - cluster:
   160      certificate-authority: ca-a.crt
   161      server: https://cluster-a.com
   162    name: cluster-a
   163  - cluster:
   164      server: https://cluster-b.com
   165    name: cluster-b
   166  contexts:
   167  - context:
   168      cluster: cluster-a
   169      namespace: ns-a
   170      user: user-a
   171    name: context-a
   172  - context:
   173      cluster: cluster-b
   174      namespace: ns-b
   175      user: user-b
   176    name: context-b
   177  current-context: context-b
   178  users:
   179  - name: user-a
   180    user:
   181     token: mytoken-b
   182  - name: user-b
   183    user:
   184     token: mytoken-b
   185  `)
   186  	fileboot, err := os.CreateTemp(fileDir, "kubeconfig")
   187  	if err != nil {
   188  		t.Fatal(err)
   189  	}
   190  	defer fileboot.Close()
   191  	os.WriteFile(fileboot.Name(), testDatabootstrap, os.FileMode(0755))
   192  
   193  	dir, err := os.MkdirTemp(fileDir, "k8s-test-certstore-current")
   194  	if err != nil {
   195  		t.Fatalf("Unable to create the test directory %q: %v", dir, err)
   196  	}
   197  
   198  	store, err := certificate.NewFileStore("kubelet-client", dir, dir, "", "")
   199  	if err != nil {
   200  		t.Errorf("unable to build bootstrap cert store")
   201  	}
   202  
   203  	tests := []struct {
   204  		name                 string
   205  		kubeconfigPath       string
   206  		bootstrapPath        string
   207  		certDir              string
   208  		expectedCertConfig   *restclient.Config
   209  		expectedClientConfig *restclient.Config
   210  	}{
   211  		{
   212  			name:           "bootstrapPath is empty",
   213  			kubeconfigPath: filevalid.Name(),
   214  			bootstrapPath:  "",
   215  			certDir:        dir,
   216  			expectedCertConfig: &restclient.Config{
   217  				Host: "https://cluster-b.com",
   218  				TLSClientConfig: restclient.TLSClientConfig{
   219  					CertFile: filepath.Join(fileDir, "mycertvalid.crt"),
   220  					KeyFile:  filepath.Join(fileDir, "mycertvalid.key"),
   221  				},
   222  				BearerToken: "",
   223  			},
   224  			expectedClientConfig: &restclient.Config{
   225  				Host: "https://cluster-b.com",
   226  				TLSClientConfig: restclient.TLSClientConfig{
   227  					CertFile: filepath.Join(fileDir, "mycertvalid.crt"),
   228  					KeyFile:  filepath.Join(fileDir, "mycertvalid.key"),
   229  				},
   230  				BearerToken: "",
   231  			},
   232  		},
   233  		{
   234  			name:           "bootstrap path is set and the contents of kubeconfigPath are valid",
   235  			kubeconfigPath: filevalid.Name(),
   236  			bootstrapPath:  fileboot.Name(),
   237  			certDir:        dir,
   238  			expectedCertConfig: &restclient.Config{
   239  				Host: "https://cluster-b.com",
   240  				TLSClientConfig: restclient.TLSClientConfig{
   241  					CertFile: filepath.Join(fileDir, "mycertvalid.crt"),
   242  					KeyFile:  filepath.Join(fileDir, "mycertvalid.key"),
   243  				},
   244  				BearerToken: "",
   245  			},
   246  			expectedClientConfig: &restclient.Config{
   247  				Host: "https://cluster-b.com",
   248  				TLSClientConfig: restclient.TLSClientConfig{
   249  					CertFile: filepath.Join(fileDir, "mycertvalid.crt"),
   250  					KeyFile:  filepath.Join(fileDir, "mycertvalid.key"),
   251  				},
   252  				BearerToken: "",
   253  			},
   254  		},
   255  		{
   256  			name:           "bootstrap path is set and the contents of kubeconfigPath are not valid",
   257  			kubeconfigPath: fileinvalid.Name(),
   258  			bootstrapPath:  fileboot.Name(),
   259  			certDir:        dir,
   260  			expectedCertConfig: &restclient.Config{
   261  				Host:            "https://cluster-b.com",
   262  				TLSClientConfig: restclient.TLSClientConfig{},
   263  				BearerToken:     "mytoken-b",
   264  			},
   265  			expectedClientConfig: &restclient.Config{
   266  				Host: "https://cluster-b.com",
   267  				TLSClientConfig: restclient.TLSClientConfig{
   268  					CertFile: store.CurrentPath(),
   269  					KeyFile:  store.CurrentPath(),
   270  				},
   271  				BearerToken: "",
   272  			},
   273  		},
   274  	}
   275  
   276  	for _, test := range tests {
   277  		t.Run(test.name, func(t *testing.T) {
   278  			certConfig, clientConfig, err := LoadClientConfig(test.kubeconfigPath, test.bootstrapPath, test.certDir)
   279  			if err != nil {
   280  				t.Fatal(err)
   281  			}
   282  			if !reflect.DeepEqual(certConfig, test.expectedCertConfig) {
   283  				t.Errorf("Unexpected certConfig: %s", cmp.Diff(certConfig, test.expectedCertConfig))
   284  			}
   285  			if !reflect.DeepEqual(clientConfig, test.expectedClientConfig) {
   286  				t.Errorf("Unexpected clientConfig: %s", cmp.Diff(clientConfig, test.expectedClientConfig))
   287  			}
   288  		})
   289  	}
   290  }
   291  
   292  func TestLoadRESTClientConfig(t *testing.T) {
   293  	testData := []byte(`
   294  apiVersion: v1
   295  kind: Config
   296  clusters:
   297  - cluster:
   298      certificate-authority: ca-a.crt
   299      server: https://cluster-a.com
   300    name: cluster-a
   301  - cluster:
   302      certificate-authority-data: VGVzdA==
   303      server: https://cluster-b.com
   304    name: cluster-b
   305  contexts:
   306  - context:
   307      cluster: cluster-a
   308      namespace: ns-a
   309      user: user-a
   310    name: context-a
   311  - context:
   312      cluster: cluster-b
   313      namespace: ns-b
   314      user: user-b
   315    name: context-b
   316  current-context: context-b
   317  users:
   318  - name: user-a
   319    user:
   320      token: mytoken-a
   321  - name: user-b
   322    user:
   323      token: mytoken-b
   324  `)
   325  	f, err := os.CreateTemp("", "kubeconfig")
   326  	if err != nil {
   327  		t.Fatal(err)
   328  	}
   329  	defer utiltesting.CloseAndRemove(t, f)
   330  	f.Write(testData)
   331  
   332  	config, err := loadRESTClientConfig(f.Name())
   333  	if err != nil {
   334  		t.Fatal(err)
   335  	}
   336  
   337  	expectedConfig := &restclient.Config{
   338  		Host: "https://cluster-b.com",
   339  		TLSClientConfig: restclient.TLSClientConfig{
   340  			CAData: []byte(`Test`),
   341  		},
   342  		BearerToken: "mytoken-b",
   343  	}
   344  
   345  	if !reflect.DeepEqual(config, expectedConfig) {
   346  		t.Errorf("Unexpected config: %s", cmp.Diff(config, expectedConfig))
   347  	}
   348  }
   349  
   350  func TestRequestNodeCertificateNoKeyData(t *testing.T) {
   351  	certData, err := requestNodeCertificate(context.TODO(), newClientset(fakeClient{}), []byte{}, "fake-node-name")
   352  	if err == nil {
   353  		t.Errorf("Got no error, wanted error an error because there was an empty private key passed in.")
   354  	}
   355  	if certData != nil {
   356  		t.Errorf("Got cert data, wanted nothing as there should have been an error.")
   357  	}
   358  }
   359  
   360  func TestRequestNodeCertificateErrorCreatingCSR(t *testing.T) {
   361  	client := newClientset(fakeClient{
   362  		failureType: createError,
   363  	})
   364  	privateKeyData, err := keyutil.MakeEllipticPrivateKeyPEM()
   365  	if err != nil {
   366  		t.Fatalf("Unable to generate a new private key: %v", err)
   367  	}
   368  
   369  	certData, err := requestNodeCertificate(context.TODO(), client, privateKeyData, "fake-node-name")
   370  	if err == nil {
   371  		t.Errorf("Got no error, wanted error an error because client.Create failed.")
   372  	}
   373  	if certData != nil {
   374  		t.Errorf("Got cert data, wanted nothing as there should have been an error.")
   375  	}
   376  }
   377  
   378  func TestRequestNodeCertificate(t *testing.T) {
   379  	privateKeyData, err := keyutil.MakeEllipticPrivateKeyPEM()
   380  	if err != nil {
   381  		t.Fatalf("Unable to generate a new private key: %v", err)
   382  	}
   383  
   384  	certData, err := requestNodeCertificate(context.TODO(), newClientset(fakeClient{}), privateKeyData, "fake-node-name")
   385  	if err != nil {
   386  		t.Errorf("Got %v, wanted no error.", err)
   387  	}
   388  	if certData == nil {
   389  		t.Errorf("Got nothing, expected a CSR.")
   390  	}
   391  }
   392  
   393  type failureType int
   394  
   395  const (
   396  	noError failureType = iota //nolint:deadcode,varcheck
   397  	createError
   398  	certificateSigningRequestDenied
   399  )
   400  
   401  type fakeClient struct {
   402  	certificatesclient.CertificateSigningRequestInterface
   403  	failureType failureType
   404  }
   405  
   406  func newClientset(opts fakeClient) *fake.Clientset {
   407  	f := fake.NewSimpleClientset()
   408  	switch opts.failureType {
   409  	case createError:
   410  		f.PrependReactor("create", "certificatesigningrequests", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
   411  			switch action.GetResource().Version {
   412  			case "v1":
   413  				return true, nil, fmt.Errorf("create error")
   414  			default:
   415  				return true, nil, apierrors.NewNotFound(certificatesv1.Resource("certificatesigningrequests"), "")
   416  			}
   417  		})
   418  	default:
   419  		f.PrependReactor("create", "certificatesigningrequests", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
   420  			switch action.GetResource().Version {
   421  			case "v1":
   422  				return true, &certificatesv1.CertificateSigningRequest{ObjectMeta: metav1.ObjectMeta{Name: "fake-certificate-signing-request-name", UID: "fake-uid"}}, nil
   423  			default:
   424  				return true, nil, apierrors.NewNotFound(certificatesv1.Resource("certificatesigningrequests"), "")
   425  			}
   426  		})
   427  		f.PrependReactor("list", "certificatesigningrequests", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
   428  			switch action.GetResource().Version {
   429  			case "v1":
   430  				return true, &certificatesv1.CertificateSigningRequestList{Items: []certificatesv1.CertificateSigningRequest{{ObjectMeta: metav1.ObjectMeta{Name: "fake-certificate-signing-request-name", UID: "fake-uid"}}}}, nil
   431  			default:
   432  				return true, nil, apierrors.NewNotFound(certificatesv1.Resource("certificatesigningrequests"), "")
   433  			}
   434  		})
   435  		f.PrependWatchReactor("certificatesigningrequests", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
   436  			switch action.GetResource().Version {
   437  			case "v1":
   438  				w := watch.NewFakeWithChanSize(1, false)
   439  				w.Add(opts.generateCSR())
   440  				w.Stop()
   441  				return true, w, nil
   442  
   443  			default:
   444  				return true, nil, apierrors.NewNotFound(certificatesv1.Resource("certificatesigningrequests"), "")
   445  			}
   446  		})
   447  	}
   448  	return f
   449  }
   450  
   451  func (c fakeClient) generateCSR() runtime.Object {
   452  	var condition certificatesv1.CertificateSigningRequestCondition
   453  	var certificateData []byte
   454  	if c.failureType == certificateSigningRequestDenied {
   455  		condition = certificatesv1.CertificateSigningRequestCondition{
   456  			Type: certificatesv1.CertificateDenied,
   457  		}
   458  	} else {
   459  		condition = certificatesv1.CertificateSigningRequestCondition{
   460  			Type: certificatesv1.CertificateApproved,
   461  		}
   462  		certificateData = []byte(`issued certificate`)
   463  	}
   464  
   465  	csr := certificatesv1.CertificateSigningRequest{
   466  		ObjectMeta: metav1.ObjectMeta{
   467  			UID: "fake-uid",
   468  		},
   469  		Status: certificatesv1.CertificateSigningRequestStatus{
   470  			Conditions: []certificatesv1.CertificateSigningRequestCondition{
   471  				condition,
   472  			},
   473  			Certificate: certificateData,
   474  		},
   475  	}
   476  	return &csr
   477  }