From b2eea41639b967e8d501145c7d4fb784437c8bc0 Mon Sep 17 00:00:00 2001 From: xovoxy Date: Thu, 15 Aug 2024 17:31:55 +0800 Subject: [PATCH] Improve test coverage for /pkg/controllers/certificate, with a minor optimization Signed-off-by: xovoxy --- .../certificate/cert_rotation_controller.go | 16 +- .../cert_rotation_controller_test.go | 455 ++++++++++++++++++ 2 files changed, 463 insertions(+), 8 deletions(-) create mode 100644 pkg/controllers/certificate/cert_rotation_controller_test.go diff --git a/pkg/controllers/certificate/cert_rotation_controller.go b/pkg/controllers/certificate/cert_rotation_controller.go index 9fe53a085622..816de9ecb01a 100644 --- a/pkg/controllers/certificate/cert_rotation_controller.go +++ b/pkg/controllers/certificate/cert_rotation_controller.go @@ -118,7 +118,7 @@ func (c *CertRotationController) Reconcile(ctx context.Context, req controllerru return controllerruntime.Result{}, err } - if err = c.syncCertRotation(secret); err != nil { + if err = c.syncCertRotation(ctx, secret); err != nil { klog.Errorf("Failed to rotate the certificate of karmada-agent for the given member cluster: %s, err is: %v", cluster.Name, err) return controllerruntime.Result{}, err } @@ -137,7 +137,7 @@ func (c *CertRotationController) SetupWithManager(mgr controllerruntime.Manager) Complete(c) } -func (c *CertRotationController) syncCertRotation(secret *corev1.Secret) error { +func (c *CertRotationController) syncCertRotation(ctx context.Context, secret *corev1.Secret) error { karmadaKubeconfig, err := getKubeconfigFromSecret(secret) if err != nil { return err @@ -174,15 +174,15 @@ func (c *CertRotationController) syncCertRotation(secret *corev1.Secret) error { return fmt.Errorf("invalid private key for certificate request: %v", err) } - csr, err := c.createCSRInControlPlane(clusterName, privateKey, oldCert) + csr, err := c.createCSRInControlPlane(ctx, clusterName, privateKey, oldCert) if err != nil { return fmt.Errorf("failed to create csr in control plane, err is: %v", err) } var newCertData []byte klog.V(1).Infof("Waiting for the client certificate to be issued") - err = wait.PollUntilContextTimeout(context.TODO(), 1*time.Second, 5*time.Minute, false, func(context.Context) (done bool, err error) { - csr, err := c.KubeClient.CertificatesV1().CertificateSigningRequests().Get(context.TODO(), csr, metav1.GetOptions{}) + err = wait.PollUntilContextTimeout(ctx, 1*time.Second, 5*time.Minute, false, func(context.Context) (done bool, err error) { + csr, err := c.KubeClient.CertificatesV1().CertificateSigningRequests().Get(ctx, csr, metav1.GetOptions{}) if err != nil { return false, fmt.Errorf("failed to get the cluster csr %s. err: %v", clusterName, err) } @@ -210,7 +210,7 @@ func (c *CertRotationController) syncCertRotation(secret *corev1.Secret) error { secret.Data["karmada-kubeconfig"] = karmadaKubeconfigBytes // Update the karmada-kubeconfig secret in the member cluster. - if _, err := c.ClusterClient.KubeClient.CoreV1().Secrets(secret.Namespace).Update(context.TODO(), secret, metav1.UpdateOptions{}); err != nil { + if _, err := c.ClusterClient.KubeClient.CoreV1().Secrets(secret.Namespace).Update(ctx, secret, metav1.UpdateOptions{}); err != nil { return fmt.Errorf("unable to update secret, err: %w", err) } @@ -225,7 +225,7 @@ func (c *CertRotationController) syncCertRotation(secret *corev1.Secret) error { return nil } -func (c *CertRotationController) createCSRInControlPlane(clusterName string, privateKey interface{}, oldCert []*x509.Certificate) (string, error) { +func (c *CertRotationController) createCSRInControlPlane(ctx context.Context, clusterName string, privateKey interface{}, oldCert []*x509.Certificate) (string, error) { csrData, err := certutil.MakeCSR(privateKey, &oldCert[0].Subject, nil, nil) if err != nil { return "", fmt.Errorf("unable to generate certificate request: %v", err) @@ -252,7 +252,7 @@ func (c *CertRotationController) createCSRInControlPlane(clusterName string, pri }, } - _, err = c.KubeClient.CertificatesV1().CertificateSigningRequests().Create(context.TODO(), certificateSigningRequest, metav1.CreateOptions{}) + _, err = c.KubeClient.CertificatesV1().CertificateSigningRequests().Create(ctx, certificateSigningRequest, metav1.CreateOptions{}) if err != nil { return "", fmt.Errorf("unable to create certificate request in control plane: %v", err) } diff --git a/pkg/controllers/certificate/cert_rotation_controller_test.go b/pkg/controllers/certificate/cert_rotation_controller_test.go new file mode 100644 index 000000000000..21b58716deb8 --- /dev/null +++ b/pkg/controllers/certificate/cert_rotation_controller_test.go @@ -0,0 +1,455 @@ +/* +Copyright 2024 The Karmada Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package certificate + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "reflect" + "testing" + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + controllerruntime "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake" + + clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1" + "github.com/karmada-io/karmada/pkg/util" + "github.com/karmada-io/karmada/pkg/util/gclient" +) + +func makeFakeCertRotationController(threshold float64) *CertRotationController { + return &CertRotationController{ + Client: clientfake.NewClientBuilder().WithScheme(gclient.NewSchema()).Build(), + KubeClient: fake.NewSimpleClientset(), + CertRotationRemainingTimeThreshold: threshold, + ClusterClientSetFunc: util.NewClusterClientSetForAgent, + KarmadaKubeconfigNamespace: "karmada-system", + } +} + +func TestCertRotationController_Reconcile(t *testing.T) { + tests := []struct { + name string + cluster *clusterv1alpha1.Cluster + del bool + want controllerruntime.Result + wantErr bool + }{ + { + name: "cluster not found", + want: controllerruntime.Result{}, + wantErr: false, + }, + { + name: "cluster is deleted", + cluster: &clusterv1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Finalizers: []string{util.ClusterControllerFinalizer}, + }, + }, + del: true, + want: controllerruntime.Result{}, + wantErr: false, + }, + { + name: "secret not found", + cluster: &clusterv1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := makeFakeCertRotationController(0) + + if tt.cluster != nil { + if err := c.Client.Create(context.Background(), tt.cluster); err != nil { + t.Fatalf("create cluster failed, error %v", err) + } + } + + if tt.del { + if err := c.Client.Delete(context.Background(), tt.cluster); err != nil { + t.Fatalf("delete cluster failed, error %v", err) + } + } + + req := controllerruntime.Request{NamespacedName: client.ObjectKey{Namespace: "", Name: "test"}} + got, err := c.Reconcile(context.Background(), req) + if (err != nil) != tt.wantErr { + t.Errorf("CertRotationController.Reconcile() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("CertRotationController.Reconcile() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCertRotationController_syncCertRotation(t *testing.T) { + tests := []struct { + name string + secret *corev1.Secret + threshold float64 + wantErr bool + }{ + { + name: "shoud not rotate cert", + secret: &corev1.Secret{ + Data: map[string][]byte{"karmada-kubeconfig": []byte(`apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMvakNDQWVhZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJME1EZ3hOVEEyTVRZd01Gb1hEVE0wTURneE16QTJNVFl3TUZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTndZCjU3blNJNDgwMUZIYmtYVUhpUldTTmV3UUxRTTZQbTB5YXArd1JXR2J3emU3US9rbjl0L2xBUWwxdW1aa2ZRalUKVHgyZHV6cFpXQkRndnAreTVBNndaUyt2VTVhSFY4dE1QRi9ocHRVczB1VW11YmQ2OEs4ZnNuREd6bnJwKzdpQwo3R2VyVzB2NDNTdnpqT0dibDQ2Nlp5cXFPRmt5VVhPQ1pVWFJMbWkyMVNrbS9iU2RFS3FDZXBtRDFNSEUwVyttCkJOOXBQeFJOU1dCZGNkSFVqR29odUUrUVBJQXlDWEtBdlNlWDBOZDd6Q1Ayd1dFRE5aSmxRS0REUnFUUHdDS3QKMW9TaDdEeWhvQ0l6clBtNENIcVNHSEJCNnVORmNEZjdpNGhVY09SdW5JMHlVUEsya2FDUmdqTkZKYkJLL29SNApoSFl0SFJwUkN3b244Q3A4dWRFQ0F3RUFBYU5aTUZjd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZESUZTYXhZNDc1WlZaTlp3dGdwOU1yeFBrU2ZNQlVHQTFVZEVRUU8KTUF5Q0NtdDFZbVZ5Ym1WMFpYTXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBSnY3Ymw1L2plVlFZZkxWblByKwpBelVYNmxXdXNHajQ0b09ma2xtZGZTTmxLU3ZGUjVRMi9rWkVQZXZnU3NzZVdIWnVucmZTYkIwVDdWYjdkUkNQCjVRMTk4aUNnZDFwNm0wdXdOVGpSRi85MHhzYUdVOHBQTFQxeHlrMDlCVDc0WVVENnNtOHFVSWVrWFU0U3hlU2oKWjk3VU13azVoZndXUWpqTFc1UklwNW1qZjR2aU1uWXB6SDB4bDREV3Jka1AxbTFCdkZvWmhFMEVaKzlWcGNPYwprNTN4ZkxUR3A2S1UrQ0w4RU5teXFCeTJNcVBXdjRQKzVTZ0hldlY3Ym1WdktuMkx0blExTHdCcDdsdldYb1JRCmUzQm83d3hnSUU0Rnl0VUU4enRaS2ZJSDZPY3VzNWJGY283cGw5ckhnK1lBMHM0Y0JldjZ2UlQwODkyYUpHYmUKZnFRPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + server: https://192.168.0.180:56016 + name: kind-member1 +contexts: +- context: + cluster: kind-member1 + user: kind-member1 + name: member1 +current-context: member1 +kind: Config +preferences: {} +users: +- name: kind-member1 + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURJVENDQWdtZ0F3SUJBZ0lJWWR6SWRFMFphbEF3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TkRBNE1UVXdOakUyTURCYUZ3MHlOVEE0TVRVd05qRTJNREZhTURReApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sa3dGd1lEVlFRREV4QnJkV0psY201bGRHVnpMV0ZrCmJXbHVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXFWSGZiUUVxdHVhdHYxU3IKeEtXaTZrVTJpa0FDdTA4eFJJK2JCU1ZyNjZkbEpZYklVb2ozR0hoRTlXV3h0bVlTRjVjNU90eGJraVRiWVcyeAoxRjZ5RHAzS21SRE9VdXJreGFBSDFjNW5wbFFrYy9mM1lFVGpuTlpKUGtvTjhnWkpwclpud2FaU0lWNXppWW9ECm5rKzZ6NjJEM25pQ1ZCNjlCaDdVN0xNMXZlOGRIQXZPc0lJVUNKZ3hQVFVobUZVMEswTEtHQWZJamJHRS85RU4Kd1VhVkpVNGYvQi9aL0ppS20yaU1WMmJWdTVTUG5ubThNYjZXSGNuclVOY3Bpdkw4U0pCdG1aS0V3Z3ZGWkhJZgpMSVJBZmNmYmR5b0w0d2R5dFd3ZHNhcEpXd3A3NG5yRURudGFLSVBjYkFvVmNUSldacmtnaWRnVHFqcTN0WS85CkZwMmNtUUlEQVFBQm8xWXdWREFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUgKQXdJd0RBWURWUjBUQVFIL0JBSXdBREFmQmdOVkhTTUVHREFXZ0JReUJVbXNXT08rV1ZXVFdjTFlLZlRLOFQ1RQpuekFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBSGQ5YU92Q0pRcFBYdzdQSmE2bSs3ZUNYSmxUdy9lZXFhT1ptCkUvU3F5MER5U0hLalptZWR4WjFHdTlEL2Q0Qnl6Ri82VUFGTjArSjZ5bUdZa2Y3TzNqempCSmh6a2U1UzRzbHMKWUpZU290cWd1VW5YakpCTVJNZzl0YUpSY1RiSU9oVmNHdDByd2t2NDB6V3JpdEplTTFEYmV0Q3FORm9xcms0bQo1WWszWHVwT01VejRtb0dnUzVDbmdIS0xXckVMRjI5VWFsM1BXRUdRbnZoVEc0N2dtUmk2dVVTUGNreGJnVm4wCithVU5Rei9Yc0tpOXgwUEtQU2xJTzMrMFVaMTdIa3Zlb1ZHOGdXRkR5bU94ckMzcEZHUkNNWTFJM3RVdUtvMTYKVUI5NW1XSFFUNjlBZllyanh3R3J4QzdkNTRKL1Y1NERnSUF6cVhFT2FFK1pKTHVYTFE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBcVZIZmJRRXF0dWF0djFTcnhLV2k2a1UyaWtBQ3UwOHhSSStiQlNWcjY2ZGxKWWJJClVvajNHSGhFOVdXeHRtWVNGNWM1T3R4YmtpVGJZVzJ4MUY2eURwM0ttUkRPVXVya3hhQUgxYzVucGxRa2MvZjMKWUVUam5OWkpQa29OOGdaSnByWm53YVpTSVY1emlZb0Ruays2ejYyRDNuaUNWQjY5Qmg3VTdMTTF2ZThkSEF2TwpzSUlVQ0pneFBUVWhtRlUwSzBMS0dBZklqYkdFLzlFTndVYVZKVTRmL0IvWi9KaUttMmlNVjJiVnU1U1Bubm04Ck1iNldIY25yVU5jcGl2TDhTSkJ0bVpLRXdndkZaSElmTElSQWZjZmJkeW9MNHdkeXRXd2RzYXBKV3dwNzRuckUKRG50YUtJUGNiQW9WY1RKV1pya2dpZGdUcWpxM3RZLzlGcDJjbVFJREFRQUJBb0lCQUhOWC85RTgrMEV2c2VybQpIa0ZPNk1XWXNzZkpSVk1lWnB5akMyS2RGSUNyUHg4cVN5cldmU1doUUxDL1B5blhMTG4vWFpBNGJ2VUd3S0FGClh5QWlCa0Fvdzh3MEE2bSt0R3ZnVVpZWURzbmpCaFl1Tk1uVEJBOWlkdkRFOSswSTFZR0pQdk5QZnlHV096VFYKM1JNcWswTkltODJnNEgrM2xPTW5lN3RuZGRQVmhad2NUc3UxR2RsOG4zNkZIbDdmSVFDbzhWTzVTOEtwU2E1TQpnL2pwYXpVMXkrdy9Bc2R2UWdmaEcxd2RQRExDWjNUaTdtbG9sS0tML0l1aFhjWUQ4UE9qRGlaWG0rNXpSb0MzCjdOTFllcjNyV01hNVBCaUkxRjUrSHV6NS9tTVQ2RGNZQStZZ1ZOYUowVkVMaUIwNGZmTnpUSmVGcVBKOTJQVUoKUG9SZFVBRUNnWUVBemJtMWFhY1BtNERwSzZpaUhrOFV4N00wMlFhQkRQYlhyVS9vYU4wbnlhVUhFWkZ2dkZaeQo1N1J1VW9jeG8vS3hTajhQSUlaeGRDRkJiZEFIZFVmWFRheUdpQ2FmOEo2MWk4TjYxL2ZSUWpYVU9mV25EOWJsCkcyakdBNHd6NGpVT1dCcmxIQk5td01PVVBTaEVaZ0padHgySFFNZDJseHhkOXlTK056eHJ0Z0VDZ1lFQTByS2MKSnAxNVk0TFY3QlhyeitjWGtDUmsxakJWQ2I3aGJydHFnTmFVYkpBMCtkSE1veUFiSEk2QlBYdGN2YTRnLzJMVQo3Yk1ZRWFTNml2c0Y4OCt1ak9hcG1yYkJSbXhRVjVob2ZWWlozVXBVc2JSVDBSSkxBNEt5SmFwbkplZTBkSzZ4CkFndStGVHFyK01UOFlGOVBHQTd5TnF3bDdlSHMvcEhJRnlvWjFwa0NnWUFzQW9Kd3E3Q0hEN2pTQWkxTVZwYVgKY2hyb0lxQVE2ZTJSd2ZweUZIMmlnWTlWanN0Y2V4SHh4NE9pWEJHZWhSaXdUWFVxL0JmaFJBdi9OZldpZlUvdAo2ZTZOeWRXRllDNXVTODlIekZnVDFmZ2t2Z3lUTXVHb1QyMnM3SjFjMHdUU2pFNWZCemVBSFZibERtd3pkbVZDCjZ5bzREWE90T0FCU0VxWXVvdHBZQVFLQmdFYi9zT2JDUWRscWlUT1kzM0diYWJGRTBrWHEyMzBCT1czYThiU2cKbWp0TERNN3lCNGNnbG9JMDh4QUl0ZU9rL2JHUldEY1JGcGM3YnpET0RkOXVxRjhLaXpSN1NQMjJHZ3lxYXV0eApZYTdVRVY0Z2FlNFZ2L2xhM1RVY0NzNFhHaFFIbWRZYXB0NzRlbUxGM2xXTFNGZlBFWWVpMHRVcVFIWTRJYmpNCmt4QmhBb0dBRGRJMHZRck1DRUtLSG1JWW8yTldBcHd1VVB0NEhqQXZRK3k4UGRzQzZOb3UzdysxcndDM3NhOVcKV1FHL092d2pmWWxMR1hqeURwelBUTHpEMENnU1FWTFptdFlzbmt5UFQ3QlVpTllSM1EwVzRML2xrSzl5U2p3Wgp3UjBUSm1kOW1iZUpMWE5CQUEzbDhNZWVxaGg5bHYrRnhNd1NFUThBd29DNGJmVE14eU09Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==`)}, + }, + threshold: 0, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := makeFakeCertRotationController(tt.threshold) + if err := c.syncCertRotation(context.Background(), tt.secret); (err != nil) != tt.wantErr { + t.Errorf("CertRotationController.syncCertRotation() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestCertRotationController_createCSRInControlPlane(t *testing.T) { + now := time.Now() + privateKey, _ := rsa.GenerateKey(rand.Reader, 2048) + oldCert := &x509.Certificate{ + Subject: pkix.Name{CommonName: "test-cert"}, + NotBefore: now.Add(-24 * time.Hour), + NotAfter: now.Add(24 * time.Hour), + } + + tests := []struct { + name string + clusterName string + privateKey interface{} + oldCert []*x509.Certificate + wantErr bool + }{ + { + name: "valid CSR creation", + clusterName: "test-cluster", + privateKey: privateKey, + oldCert: []*x509.Certificate{oldCert}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := makeFakeCertRotationController(0) + + csrName, err := c.createCSRInControlPlane(context.Background(), tt.clusterName, tt.privateKey, tt.oldCert) + + if (err != nil) != tt.wantErr { + t.Fatalf("createCSRInControlPlane() error = %v, wantErr %v", err, tt.wantErr) + } + + if !tt.wantErr { + csr, err := c.KubeClient.CertificatesV1().CertificateSigningRequests().Get(context.Background(), csrName, metav1.GetOptions{}) + if err != nil { + t.Fatalf("expected CSR to be created, but got error: %v", err) + } + + if csr.Spec.SignerName != SignerName { + t.Errorf("expected SignerName %v, but got %v", SignerName, csr.Spec.SignerName) + } + + expectedExpirationSeconds := int32((tt.oldCert[0].NotAfter.Sub(tt.oldCert[0].NotBefore)).Seconds()) + if *csr.Spec.ExpirationSeconds != expectedExpirationSeconds { + t.Errorf("expected ExpirationSeconds %v, but got %v", expectedExpirationSeconds, *csr.Spec.ExpirationSeconds) + } + } + }) + } +} + +func Test_getKubeconfigFromSecret(t *testing.T) { + tests := []struct { + name string + secret *corev1.Secret + wantErr bool + }{ + { + name: "get kubeconfig from secret success", + secret: &corev1.Secret{ + Data: map[string][]byte{"karmada-kubeconfig": []byte(`apiVersion: v1 +clusters: +- cluster: + insecure-skip-tls-verify: true + server: https://192.168.0.180:5443 + name: karmada-apiserver +contexts: +- context: + cluster: karmada-apiserver + user: karmada-apiserver + name: karmada-apiserver +- context: + cluster: kind-karmada-host + user: kind-karmada-host + name: karmada-host +kind: Config +preferences: {} +users: +- name: karmada-apiserver + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUU4ekNDQTF1Z0F3SUJBZ0lVQmpSVFVyY3l0YnBmOVp6R1ZlZC9yWHIxVlAwd0RRWUpLb1pJaHZjTkFRRU0KQlFBd0VqRVFNQTRHQTFVRUF3d0hhMkZ5YldGa1lUQWVGdzB5TkRBNE1Ea3dNVFEyTURCYUZ3MHlPVEE0TURndwpNVFEyTURCYU1EQXhGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1SVXdFd1lEVlFRREV3eHplWE4wClpXMDZZV1J0YVc0d2dnR2lNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJqd0F3Z2dHS0FvSUJnUUM5eFJxbEw3V2MKSE1MWGI3L3BjQWN0MElzR3JQZzdrdmRHM0xYWTc3NHdEK3dHVXdSWjg2YlQ5dVVvYWllTEFmbVVKQWJzbkNscApkbjRhVVhhOUFUQW5MNStYYUkvYzUxYlVHOERlS0EySHRxUFZOc2xrNzM2KzVySk1WWFdOT1dwQzFoRU9yRjNFCnA5Y2ZoRDkzenRtZEdMQWVja3kyYW1vL1FnVWxkZElVL1Nua0xIdlhQUEtFOEs4Z2J1N012NHhETjI4SWo1cGUKQlNzTFBIR3MrdkdrOGpvbmFKaVhWQUs4U1JuS1J0TnhnK2VsZlB3akZ3S0JieHMwS0g5U1dWbkhTVFRiU1d2Nwo1d2d5MHFNTk1la1BWS0tNOVcrLytXVnUyM3g4bmRENXU5K2tPWUUvK3QveHlLbzdLanB2dmpFWmdXVkUzcERQCndVSkVTT2o4V1o0YUh4L2RLdFBVQkNUdWtVNkJ5MFNsRnM4V1VMSVFVRFdUZGFVYTEweGVsVlB6V05xazJVVVkKWm5rcithSHg5U3NxV1lVOVBDV3ltN1d2NU52eWVEZ3huT05NSDBrWmhBWFE5eVpES2RZMkx3WTEya0pUUVJ4Sgp4S2hqTTBWQ0VXSVN5VWRBdUx2WTkrRkFVQnQ3Y2daVU15ZThhS2xwYWNtR3ZzYzdMU3hYalRFQ0F3RUFBYU9DCkFTRXdnZ0VkTUE0R0ExVWREd0VCL3dRRUF3SUZvREFkQmdOVkhTVUVGakFVQmdnckJnRUZCUWNEQWdZSUt3WUIKQlFVSEF3RXdEQVlEVlIwVEFRSC9CQUl3QURBZEJnTlZIUTRFRmdRVTNYM3lZM3VHVDVYZXpKRmE0cnhtd0tSNgp3c013SHdZRFZSMGpCQmd3Rm9BVVBxV2JjYzU5WG95bnVVZUZmcHBVL2JaZi9iSXdnWjBHQTFVZEVRU0JsVENCCmtvSVdhM1ZpWlhKdVpYUmxjeTVrWldaaGRXeDBMbk4yWTRJbktpNWxkR05rTG10aGNtMWhaR0V0YzNsemRHVnQKTG5OMll5NWpiSFZ6ZEdWeUxteHZZMkZzZ2lJcUxtdGhjbTFoWkdFdGMzbHpkR1Z0TG5OMll5NWpiSFZ6ZEdWeQpMbXh2WTJGc2doUXFMbXRoY20xaFpHRXRjM2x6ZEdWdExuTjJZNElKYkc5allXeG9iM04waHdSL0FBQUJod1RBCnFBQUlNQTBHQ1NxR1NJYjNEUUVCREFVQUE0SUJnUUExMTNnL3NIb0UxempmMEZZUDRVdEphWC9XMHJGZmhJY0MKMFFILzFCWnJ5K0o2bi8waVRwOEZEeXBFdXpKVHRWbG5hb1RId2d6SzVZaU9sWEt5UlJ2V2hHQzZJR1lhZXgxUQo0eWlacUpDaUtDdkdtbzB2REQrVG5BNllSUjR1bmR3VTllbmhrbWdQcEFiR3ZMNXkveGViUTJJM1luNWNuNElSCm5JaGZiK2psSWZhT2ZjdTlpa1htbzlPcVUyM0czRkRDcUROMVZSSU5UYms2S204bVJYaE5kMitBNVUrYXA3bEkKYWZwT0VPbFZCWjh4QkJvQ0RRUTZabDFCMFVlMEdaUjk5MCs0Zk5JMDB3QzU0emh6OGdqbzdWMEpudlJQWkIzbwplK2FjQ1poOUxoYlpTZFZXNEZHYlRzNEM3VzFWRkhPM20wUjBLRUdXNzZ6ZEdwZkR4Ty9ZaWxEVTkyaDlTY1hOCkhVUUl4aHhtUWFWSEd4MGtvb3pxWkowa0VNa29vSndaRjFHdGRjSG5ORXl5NDNFOE9VQXhPVS9CNC85R0ZtaUsKd0RncFBJRTZPK2VTWWh2SzV0b000UjRhc05UTUhBMkQ3QS9GY05lVDZmK2JkN3ZlMy9aZGJZQVBHN1RsaE1rZwp2aXNUREk4YUh2U0JpMnY1RUt5SkdpL0h5d3llSTJ3PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlHNUFJQkFBS0NBWUVBdmNVYXBTKzFuQnpDMTIrLzZYQUhMZENMQnF6NE81TDNSdHkxMk8rK01BL3NCbE1FCldmT20wL2JsS0dvbml3SDVsQ1FHN0p3cGFYWitHbEYydlFFd0p5K2ZsMmlQM09kVzFCdkEzaWdOaDdhajFUYkoKWk85K3Z1YXlURlYxalRscVF0WVJEcXhkeEtmWEg0US9kODdablJpd0huSk10bXBxUDBJRkpYWFNGUDBwNUN4Nwoxenp5aFBDdklHN3V6TCtNUXpkdkNJK2FYZ1VyQ3p4eHJQcnhwUEk2SjJpWWwxUUN2RWtaeWtiVGNZUG5wWHo4Ckl4Y0NnVzhiTkNoL1VsbFp4MGswMjBscisrY0lNdEtqRFRIcEQxU2lqUFZ2di9sbGJ0dDhmSjNRK2J2ZnBEbUIKUC9yZjhjaXFPeW82Yjc0eEdZRmxSTjZRejhGQ1JFam8vRm1lR2g4ZjNTclQxQVFrN3BGT2djdEVwUmJQRmxDeQpFRkExazNXbEd0ZE1YcFZUODFqYXBObEZHR1o1Sy9taDhmVXJLbG1GUFR3bHNwdTFyK1RiOG5nNE1aempUQjlKCkdZUUYwUGNtUXluV05pOEdOZHBDVTBFY1NjU29Zek5GUWhGaUVzbEhRTGk3MlBmaFFGQWJlM0lHVkRNbnZHaXAKYVduSmhyN0hPeTBzVjQweEFnTUJBQUVDZ2dHQU5oNTZCTlhnVXc4WXVkdlN2VDRIcmhjbEx0Z3hTcndVN1E2UQpoYmVKWTZlR3hoN3l4THhwdlZWUjhQNmxIRTJKNGFlTW1mbkhEWjZQUSs0cmtLZEFGa3pNbEo5ck43SjUzcSt6Cmh5ZWhCQzBmOS9SUjV3a3QrajlNWmR5UlU1OG1JTDF2eStGNE5GY3hVSG1rcUFSRHB6dWRMbkdtVXZkeUtXd28KajVCVVEwd1hvRXpYWElDeklScUt0eE1yNHhSVmFlM3JLbVBad0NwMUpoVXN6Z29hMDlHSXFDaDZLOTMza2I2LwpMaUtZdG16SnFUWkZxYlFzTU1HQTNwVVdDbVFVYXJkRzNadWl5dVkvQUR1d0c2aExMMmlXenpGb3lrWG9Fb2lLCmRJZkdteUhad2xUUTRHWVkvVW5RUnJ4Y05paW5lTjMrd1B2TDlPQ2pkblc3Si9UcXBpVyt5QWFQWlYwZlNhWXMKM3dXRXUrSGRUM0dWRFBWZ2tvUDZUay9QYlpWcGZMZXg0M2M3Q2tKcXBwUi9mL2l2V0tFL1VsQUFXTXNmSDE1UQoyVE10dWZWWUxmUDQ0bDhCUml2ZkhZSUhiTTZTc2pzd040ZXFJMHZ6Wjh5MkRuSUF3VjZRNXZGTXpyUTV1bXlqCjRjMTh5RU5XbjdhZlJhZkN6dEtrRlRxNHpENkJBb0hCQU9BR2hmeCsxNThsVkc4YVR0SVMzNHpDeXE5TWlEZTMKditPSmlrLzJabk01V1lPbElhMVJlVlhkS0FyMi81SVcvaGhKYU5YeEhCTHNVYjQ1RUFSeldTalQrN0ZwRHR2QwphMjg0bzF5V3h3czBrUllCL3RwcEw1Nmh5ek9nZytDNTZ3cVJIM3NhYi9Ba1dTazUzNXVRNU1zdEozU0hrWjQ2CnllMnJmWllnWktOd3pQMzUzVnRuMFlGdTUrczNhRjFRbmIyVEU3enFYclhqZHFaaVl6U1plUzkreGJ0TTl5eHYKQW1BbkQ4TGwxeWlBa2dTZDNEWHpIT1YwMTZoczBkR3BpUUtCd1FEWTJ2Sk5rUzdVNU5tNS9GRXI5M2ZieE1zdgpiY21tUG1IS3djaVRQaGpFc0ZDbXFyenFoNFc4eURzSnBjZXhaa0NaVWIzdTlndUJ1K3lDVWVhSVoxRFJMcFZzCkZuc0lrNS90ajZST3lNZUw3bzJsWDVoM1huZ09JZUxwZFhBRUtQTFBnYVhxcEJhL0MwdnBBcEtqTSs3cE80c2cKcllkd24yR1RNaHNyUjRxZHQ2L09LR1UzMHg4Q1dNQmtsbVlWUmZjbjU1alhDNzFLWVNUQmpiYk8reXE1bXkyegpoZG5naXRaSzFDeUlnYThwRU9zemcrcy9zZGI1UW5IY3lZZkQ1R2tDZ2NFQTA5YlhWRkJOYlB4THYxUE5QUEVECjI0TkhUa1M4ZXBPVExJS2UrOFl3VXJ6a1hJd0dNSEplbmtjbTJsZCtqSktaYkRYZW9KQUMwQklQcWlVamRGSHUKK2FDYTNNUlBmSmQ0S0JBU3lqYjV1U3JZRjB1RHh0eTRYdlQzeGJYL2ZyM1ArSW9LanNvTkY3UUhhK2lCL2dVaApsTTc2QkVNOSs3WHp4SDdwbnRDNjJhS005WTBWT0o0UGlxbHBQUStEN05tU2ovVklNTmdlL3pnOHRsRkxKaTRLClhsTXVmMUxrV0d3UWZ3UHc3NGVQMFlqaWl1TUxuU2hySnI4NlB0WElBczZoQW9IQkFMRlZjcG9kNUpjeVFoNUcKaUxhVW0wNTd3dENaVGpLRmh3RUFzUjQveEk5cnc3aFhuVCtJN2NPd1ZoOFFrNW10alp3VGJXZXhpejFmU1F4VgpIMWxpYTROMFBicnpNMCtUTVZCYWk3bmxsTkVJbG9xcS93cDJsV1N4TDFkTXN2cWo1ZFB0S0pvVjc0ZnhHL1dECkpoa1NDZ2h1cWRwaUxZVGF3VGRXM1VMcU1SL0NxNkRDQmV1OTJQalpUUk8wcmV3dGwrenBCbUtzOWZHU09UV1MKeHZEQ0VuRlB4Zkt0dmJ0U3JlVHBWeldXSDRCTWxwbzlBYTdIcG8yVldYbDgwR3BRS1FLQndGOVYrOWpXTjZwOApoYkxxU0psWGlWZXQzUEx1UHZ0ckNOSU0xS3dldkc5VWlJN09GRGRUQWUrdHVWVFVFTk5obHBjeDMrSGtsVGVZCktMekhuKzJXWFhvbjA4UFB2Vmc5N1Y1ckJpb2l6cnhVRmZPcnBqZWpMQnAvNFo0QXlZc2k1S1lUaEhCVW5JNDgKQ1JnQ2FMVTdOS0Nvem9pZWJHaE9ydGlIbXM3STZpQ3BiSGppaEZtc1dnYlZzZEtHYU9hS0dHcVlFcFN5d2ZKYQpWczg5ellsNGdHSGRLM01HczdzODVUWkpYV3VrajZwdWd5d2hzaW11UHBLYUxCYmU2aDZIbGc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=`)}, + }, + wantErr: false, + }, + { + name: "secret data is nil", + secret: &corev1.Secret{}, + wantErr: true, + }, + { + name: "cluster name is wrong", + secret: &corev1.Secret{ + Data: map[string][]byte{"karmada-kubeconfig-wrong": []byte("")}, + }, + wantErr: true, + }, + { + name: "kubeconfig is wrong", + secret: &corev1.Secret{ + Data: map[string][]byte{"karmada-kubeconfig": []byte(`apiVersion: v1 +clusters: +- cluster: + insecure-skip-tls-verify: true + server: https://192.168.0.180:5443 + name: karmada-apiserver +contexts: +- context: + cluster: karmada-apiserver + user: karmada-apiserver + name: karmada-apiserver +- context: + cluster: kind-karmada-host + user: kind-karmada-host + name: karmada-host +kind: Config +preferences: {} +users: +- name: karmada-apiserver + user: + client-certificate-data: + client-key-data: `)}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := getKubeconfigFromSecret(tt.secret) + if (err != nil) != tt.wantErr { + t.Errorf("getKubeconfigFromSecret() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + +func TestCertRotationController_shouldRotateCert(t *testing.T) { + now := time.Now() + tests := []struct { + name string + certTime []map[string]time.Time + want bool + wantErr bool + }{ + { + name: "should rotate cert", + certTime: []map[string]time.Time{ + { + "notBefore": now.Add(-72 * time.Hour).UTC().Truncate(time.Second), + "notAfter": now.Add(12 * time.Hour).UTC().Truncate(time.Second), + }, + }, + want: true, + wantErr: false, + }, + { + name: "should not rotate cert", + certTime: []map[string]time.Time{ + { + "notBefore": now.Add(-24 * time.Hour).UTC().Truncate(time.Second), + "notAfter": now.Add(72 * time.Hour).UTC().Truncate(time.Second), + }, + }, + want: false, + wantErr: false, + }, + { + name: "cert is empty", + certTime: []map[string]time.Time{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + certData := []byte{} + for _, ct := range tt.certTime { + cert, err := newMockCert(ct["notBefore"], ct["notAfter"]) + if err != nil { + t.Fatal(err) + } + certData = append(certData, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})...) + } + + c := makeFakeCertRotationController(0.5) + got, err := c.shouldRotateCert(certData) + if (err != nil) != tt.wantErr { + t.Errorf("CertRotationController.shouldRotateCert() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("CertRotationController.shouldRotateCert() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getCertValidityPeriod(t *testing.T) { + now := time.Now() + tests := []struct { + name string + certTime []map[string]time.Time + wantBefore time.Time + wantAfter time.Time + wantErr bool + }{ + { + name: "get cert validity period success", + certTime: []map[string]time.Time{ + { + "notBefore": now.Add(-36 * time.Hour).UTC().Truncate(time.Second), + "notAfter": now.Add(72 * time.Hour).UTC().Truncate(time.Second), + }, + { + "notBefore": now.Add(-24 * time.Hour).UTC().Truncate(time.Second), + "notAfter": now.Add(36 * time.Hour).UTC().Truncate(time.Second), + }, + }, + wantBefore: now.Add(-24 * time.Hour).UTC().Truncate(time.Second), + wantAfter: now.Add(36 * time.Hour).UTC().Truncate(time.Second), + wantErr: false, + }, + { + name: "parse cert fail", + certTime: []map[string]time.Time{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + certData := []byte{} + for _, ct := range tt.certTime { + cert, err := newMockCert(ct["notBefore"], ct["notAfter"]) + if err != nil { + t.Fatal(err) + } + certData = append(certData, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})...) + } + + notBefore, notAfter, err := getCertValidityPeriod(certData) + if tt.wantErr { + if err == nil { + t.Error("expected error but got nil") + } + return + } + + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + if !tt.wantBefore.Equal(*notBefore) || !tt.wantAfter.Equal(*notAfter) { + t.Errorf("got notBefore=%s, notAfter=%s; want notBefore=%s, notAfter=%s", notBefore, notAfter, tt.wantBefore, tt.wantAfter) + } + }) + } +} + +func newMockCert(notBefore, notAfter time.Time) (*x509.Certificate, error) { + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return nil, err + } + + testSigner, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: "karmada.com", + Organization: []string{"karmada"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + certData, err := x509.CreateCertificate(rand.Reader, &template, &template, &testSigner.PublicKey, testSigner) + if err != nil { + return nil, err + } + + return x509.ParseCertificate(certData) +}