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 }