From 5e66106f85d902b192b2a528d6cf381e4e5b4974 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 | 358 ++++++++++++++++++ 2 files changed, 366 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..9033eb541595 --- /dev/null +++ b/pkg/controllers/certificate/cert_rotation_controller_test.go @@ -0,0 +1,358 @@ +/* +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" + "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" +) + +// cert will expire on 2124-07-28T02:18:16Z +var testCA = `-----BEGIN CERTIFICATE----- +MIIDbTCCAlWgAwIBAgIUQkIQIcbPnAqI8ucdX54k1QhlzQ4wDQYJKoZIhvcNAQEL +BQAwNDEZMBcGA1UEAwwQa3ViZXJuZXRlcy1hZG1pbjEXMBUGA1UECgwOc3lzdGVt +Om1hc3RlcnMwIBcNMjQwODIxMDIxODE2WhgPMjEyNDA3MjgwMjE4MTZaMDQxGTAX +BgNVBAMMEGt1YmVybmV0ZXMtYWRtaW4xFzAVBgNVBAoMDnN5c3RlbTptYXN0ZXJz +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqVWggRDZWNvCgNEMoncb +ASN3RNpZfA+cK4vNC/D2R9m5ATR/ZGt06aP3zWLkqc43MmwsLcFDy/wY+hB50/It +Zqjt5EaIl1jqZnRXuEXe5phq/fZICSM2vL9tt0JX9L9c5LedSWJwSZ8gvjpwQacK +SGMy+HM5lC5Ta3bOR98sTEyFG6Z8kX9KT2HgYsveShO242TRUSJPKW+xocJjxqL+ +GFHKoZp4D+yYkZ2dahHvPiSCxe9WDXKbpZRPTxNb/EMkJ6YOuU8N2QW44u9Lx1y7 +jIAPL6vGcUJeo2UhvyShGKzPrI1tGSWnjKxKCOv8rK5NPuhIXTXDHhTCDB4/r6xt +xQIDAQABo3UwczAdBgNVHQ4EFgQU3vrp4HTLqAPRBqVbo9CV53KjlQkwHwYDVR0j +BBgwFoAU3vrp4HTLqAPRBqVbo9CV53KjlQkwDgYDVR0PAQH/BAQDAgWgMBMGA1Ud +JQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggEB +ADy6VgqstiGXTxV0VzyXtHqVkHX2GNl58HYwC8ti5uy/T7U/WhzOmjDMxoZonA+m +wzE25Dp1J7DN2y02skHoYMP8u0fsDBACWtXxFhwUja+De1CiEZCGhDUeMNS3ka2j +4z9Ow3yChanJXmR6n91hA5TGJ4uk9eFrQgKoLqZ/poRaoxj6861XKWiJS1Wvrz1g +fmbSjVIn4QFA9f611iwS/wGNHJ1dLUza9WuiQeOjculCqxqBl4+kQWmRBcmOkse2 ++KuZJMIMJfS2521AZO35EgXblA2BG1TgZZz6i3E0NMjzi+T1NIMwXawiWXDt4W/l +umubw9aqN/m5NUa3hZ6XmXQ= +-----END CERTIFICATE-----` + +func makeFakeCertRotationController(threshold float64) *CertRotationController { + client := clientfake.NewClientBuilder().WithScheme(gclient.NewSchema()).WithObjects( + &clusterv1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test", Finalizers: []string{util.ClusterControllerFinalizer}}, + Spec: clusterv1alpha1.ClusterSpec{ + APIEndpoint: "https://127.0.0.1", + SecretRef: &clusterv1alpha1.LocalSecretReference{Namespace: "ns1", Name: "secret1"}, + }, + Status: clusterv1alpha1.ClusterStatus{ + Conditions: []metav1.Condition{ + { + Type: clusterv1alpha1.ClusterConditionReady, + Status: metav1.ConditionTrue, + }, + }, + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Namespace: "ns1", Name: "secret1"}, + Data: map[string][]byte{clusterv1alpha1.SecretTokenKey: []byte("token"), clusterv1alpha1.SecretCADataKey: []byte(testCA)}, + }).Build() + return &CertRotationController{ + Client: client, + KubeClient: fake.NewSimpleClientset(), + CertRotationRemainingTimeThreshold: threshold, + ClusterClientSetFunc: util.NewClusterClientSet, + 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", + cluster: &clusterv1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test1", + }, + }, + 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: "get secret failed", + 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.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: tt.cluster.Name}} + 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 + signer bool + threshold float64 + wantErr bool + }{ + { + name: "shoud not rotate cert", + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Namespace: "ns1", Name: "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: + ## cert will expire on 2124-07-28T02:18:16Z + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURiVENDQWxXZ0F3SUJBZ0lVUWtJUUljYlBuQXFJOHVjZFg1NGsxUWhselE0d0RRWUpLb1pJaHZjTkFRRUwKQlFBd05ERVpNQmNHQTFVRUF3d1FhM1ZpWlhKdVpYUmxjeTFoWkcxcGJqRVhNQlVHQTFVRUNnd09jM2x6ZEdWdApPbTFoYzNSbGNuTXdJQmNOTWpRd09ESXhNREl4T0RFMldoZ1BNakV5TkRBM01qZ3dNakU0TVRaYU1EUXhHVEFYCkJnTlZCQU1NRUd0MVltVnlibVYwWlhNdFlXUnRhVzR4RnpBVkJnTlZCQW9NRG5ONWMzUmxiVHB0WVhOMFpYSnoKTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFxVldnZ1JEWldOdkNnTkVNb25jYgpBU04zUk5wWmZBK2NLNHZOQy9EMlI5bTVBVFIvWkd0MDZhUDN6V0xrcWM0M01td3NMY0ZEeS93WStoQjUwL0l0ClpxanQ1RWFJbDFqcVpuUlh1RVhlNXBocS9mWklDU00ydkw5dHQwSlg5TDljNUxlZFNXSndTWjhndmpwd1FhY0sKU0dNeStITTVsQzVUYTNiT1I5OHNURXlGRzZaOGtYOUtUMkhnWXN2ZVNoTzI0MlRSVVNKUEtXK3hvY0pqeHFMKwpHRkhLb1pwNEQreVlrWjJkYWhIdlBpU0N4ZTlXRFhLYnBaUlBUeE5iL0VNa0o2WU91VThOMlFXNDR1OUx4MXk3CmpJQVBMNnZHY1VKZW8yVWh2eVNoR0t6UHJJMXRHU1duakt4S0NPdjhySzVOUHVoSVhUWERIaFRDREI0L3I2eHQKeFFJREFRQUJvM1V3Y3pBZEJnTlZIUTRFRmdRVTN2cnA0SFRMcUFQUkJxVmJvOUNWNTNLamxRa3dId1lEVlIwagpCQmd3Rm9BVTN2cnA0SFRMcUFQUkJxVmJvOUNWNTNLamxRa3dEZ1lEVlIwUEFRSC9CQVFEQWdXZ01CTUdBMVVkCkpRUU1NQW9HQ0NzR0FRVUZCd01DTUF3R0ExVWRFd0VCL3dRQ01BQXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUIKQUR5NlZncXN0aUdYVHhWMFZ6eVh0SHFWa0hYMkdObDU4SFl3Qzh0aTV1eS9UN1UvV2h6T21qRE14b1pvbkErbQp3ekUyNURwMUo3RE4yeTAyc2tIb1lNUDh1MGZzREJBQ1d0WHhGaHdVamErRGUxQ2lFWkNHaERVZU1OUzNrYTJqCjR6OU93M3lDaGFuSlhtUjZuOTFoQTVUR0o0dWs5ZUZyUWdLb0xxWi9wb1Jhb3hqNjg2MVhLV2lKUzFXdnJ6MWcKZm1iU2pWSW40UUZBOWY2MTFpd1Mvd0dOSEoxZExVemE5V3VpUWVPamN1bENxeHFCbDQra1FXbVJCY21Pa3NlMgorS3VaSk1JTUpmUzI1MjFBWk8zNUVnWGJsQTJCRzFUZ1paejZpM0UwTk1qemkrVDFOSU13WGF3aVdYRHQ0Vy9sCnVtdWJ3OWFxTi9tNU5VYTNoWjZYbVhRPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t + client-key-data: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQ3BWYUNCRU5sWTI4S0EKMFF5aWR4c0JJM2RFMmxsOEQ1d3JpODBMOFBaSDJia0JOSDlrYTNUcG8vZk5ZdVNwempjeWJDd3R3VVBML0JqNgpFSG5UOGkxbXFPM2tSb2lYV09wbWRGZTRSZDdtbUdyOTlrZ0pJemE4djIyM1FsZjB2MXprdDUxSlluQkpueUMrCk9uQkJwd3BJWXpMNGN6bVVMbE5yZHM1SDN5eE1USVVicG55UmYwcFBZZUJpeTk1S0U3YmpaTkZSSWs4cGI3R2gKd21QR292NFlVY3FobW5nUDdKaVJuWjFxRWU4K0pJTEY3MVlOY3B1bGxFOVBFMXY4UXlRbnBnNjVUdzNaQmJqaQo3MHZIWEx1TWdBOHZxOFp4UWw2alpTRy9KS0VZck0rc2pXMFpKYWVNckVvSTYveXNyazArNkVoZE5jTWVGTUlNCkhqK3ZyRzNGQWdNQkFBRUNnZ0VBSTdLaGU1UUp2ZW5XUDBIUzRBMHI3RG1GMDBZVXgwcWpLYXIzTnlVOVJqaG8KQUJFSktpcGRJMFFsNFc2UHRoeDdGbTRuZ2gzVUpSU29UMDlaMzR5V2RhWDNRTUI5MnlvcmdCM1d3RW82aTNKbQpXOU9uckFWNGJLSU9oeXU5VHlOb2VlOGJnWFQzSnc0YzRQMkEzTlpTSEtDTkJrT0VSL0RjTlROK21UZzdKbnBDCnMvVmoyd2pibllQNmt6MVRTcEVjRksrb3NnYldXQ1AxVDFUeFRFN1k5VlBjbWhibzU5Lzdxc2EzaE8vUjgxRysKQ0VxU3U1emgrQmJvRFZHUStpZFV3OGtqUlhUS2MzWFBWb0R6SmR6cUtJVUYwTkc1Nm4wdGNoZkVEMUpWS09PSQp5a3REdjM1Qi9JWEkwYzk1UHpJN0crOWJvSHM1aW9BcUZKUlo2bllJQVFLQmdRRHNvTmJRc0p1U1pVcW9ZZTBLCkhERFpXS3F5NUM3Q1IvOFNXTXcxeFQzdnJVV2t2Wm5TZUJHUnpvYUEvZmRXQ3lGWHdBd1ZkS0xFSzFkdW5UWDEKUkQ4Zk9odFBDdTdiaXVvV2l4YlpMcGRPUXVzZlhRcDNHUWtoOUNIQVRPc1pML0tkMmxSd0F6dHpGNTZkVHRtdQplZFIxVENiTEVZK1A1Vzg0MXhjV2Y4OEh3UUtCZ1FDM01uaXBXY0xWNDBlS2xZMEg2alhBMDByWCs3V1U1M21RClFKNXAzbWlxSW5qRWlmV2ZQZHh4Y0hyVWRndzlpRk9pVUVvNENIQnNnam5wRU5wUjVhcUJpTzRWUFBuRXdXM2EKSmJ5eWdmRW4wREdBci9PdFpTTUF6OGN4NFVnZmZpSmZPSk8rZXRNemhDMXlMcFAwR05oM2UyRzlDZ0M3eVhDSQpGT1BvdWlzSEJRS0JnSGROT0VFTGFjUkxrWEtIdk0wR0haTFhZMmpDSnRrSkY0OFdlZzc2SFJvRUVFTFkzUDhDClRrbG5DT1ZzSmhHWmx2djQ5WjZ6cVlTaUhYakZobmpjS2I4Q3V0WUZPeHd4VTRoK0k4em44cDBnbkE2NkNCYTMKNXFUWncxS0M5VjFEa1YwSXdOMmdvNDZKY0F6N3ZrQjdhQ1NqZWtPVDNQKzl1Mis2OGdjRDlVdUJBb0dBUjkxdgp3aGRwUEJpZG52ck55VllTWWlOQkQvczVIMEd5eVdqZisrMzRwdzFBelBERnZ3TTRiL1BNNjMybmpaZm1IeDFhCkVDTVhYeW15NS8vcGRRa2dXeEpKTzJHaEpaTXZzY3p0K2lUSlluSGtpWFA4cG4rdlBJbEZ2Z1ovRVlPY25qZ0cKbFVsL2dvME9ldVZVdXdQb0h1N3l4NEtlQ1F5YnJYWnNkWVphakxVQ2dZRUEyeEhLenJ3T1FwMlNNbXlzOFR1bgpZbm54bzMwV0diQzNpYVJIZkQ0TGRvNC9iNHY2TWtkMG9wdXBoMzArQjduRkZoRjlWSVplbVdGWkMrcXNKWHdLCmNwOGMzdjBNZFJkeUtTcTJ4Ly9ZL2s1KytaeU5DSXRZdzdhRy91Wks4TlZyY3h0c2hXOXBobDFJTThXTjBUYmgKNmxwd2xWZGFha0RPY09ZNjE2clRGOXM9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=`)}, + }, + threshold: 0, + signer: false, + wantErr: false, + }, + { + name: "update secret failed", + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Namespace: "ns1", Name: "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: + ## cert will expire on 2124-07-28T02:18:16Z + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURiVENDQWxXZ0F3SUJBZ0lVUWtJUUljYlBuQXFJOHVjZFg1NGsxUWhselE0d0RRWUpLb1pJaHZjTkFRRUwKQlFBd05ERVpNQmNHQTFVRUF3d1FhM1ZpWlhKdVpYUmxjeTFoWkcxcGJqRVhNQlVHQTFVRUNnd09jM2x6ZEdWdApPbTFoYzNSbGNuTXdJQmNOTWpRd09ESXhNREl4T0RFMldoZ1BNakV5TkRBM01qZ3dNakU0TVRaYU1EUXhHVEFYCkJnTlZCQU1NRUd0MVltVnlibVYwWlhNdFlXUnRhVzR4RnpBVkJnTlZCQW9NRG5ONWMzUmxiVHB0WVhOMFpYSnoKTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFxVldnZ1JEWldOdkNnTkVNb25jYgpBU04zUk5wWmZBK2NLNHZOQy9EMlI5bTVBVFIvWkd0MDZhUDN6V0xrcWM0M01td3NMY0ZEeS93WStoQjUwL0l0ClpxanQ1RWFJbDFqcVpuUlh1RVhlNXBocS9mWklDU00ydkw5dHQwSlg5TDljNUxlZFNXSndTWjhndmpwd1FhY0sKU0dNeStITTVsQzVUYTNiT1I5OHNURXlGRzZaOGtYOUtUMkhnWXN2ZVNoTzI0MlRSVVNKUEtXK3hvY0pqeHFMKwpHRkhLb1pwNEQreVlrWjJkYWhIdlBpU0N4ZTlXRFhLYnBaUlBUeE5iL0VNa0o2WU91VThOMlFXNDR1OUx4MXk3CmpJQVBMNnZHY1VKZW8yVWh2eVNoR0t6UHJJMXRHU1duakt4S0NPdjhySzVOUHVoSVhUWERIaFRDREI0L3I2eHQKeFFJREFRQUJvM1V3Y3pBZEJnTlZIUTRFRmdRVTN2cnA0SFRMcUFQUkJxVmJvOUNWNTNLamxRa3dId1lEVlIwagpCQmd3Rm9BVTN2cnA0SFRMcUFQUkJxVmJvOUNWNTNLamxRa3dEZ1lEVlIwUEFRSC9CQVFEQWdXZ01CTUdBMVVkCkpRUU1NQW9HQ0NzR0FRVUZCd01DTUF3R0ExVWRFd0VCL3dRQ01BQXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUIKQUR5NlZncXN0aUdYVHhWMFZ6eVh0SHFWa0hYMkdObDU4SFl3Qzh0aTV1eS9UN1UvV2h6T21qRE14b1pvbkErbQp3ekUyNURwMUo3RE4yeTAyc2tIb1lNUDh1MGZzREJBQ1d0WHhGaHdVamErRGUxQ2lFWkNHaERVZU1OUzNrYTJqCjR6OU93M3lDaGFuSlhtUjZuOTFoQTVUR0o0dWs5ZUZyUWdLb0xxWi9wb1Jhb3hqNjg2MVhLV2lKUzFXdnJ6MWcKZm1iU2pWSW40UUZBOWY2MTFpd1Mvd0dOSEoxZExVemE5V3VpUWVPamN1bENxeHFCbDQra1FXbVJCY21Pa3NlMgorS3VaSk1JTUpmUzI1MjFBWk8zNUVnWGJsQTJCRzFUZ1paejZpM0UwTk1qemkrVDFOSU13WGF3aVdYRHQ0Vy9sCnVtdWJ3OWFxTi9tNU5VYTNoWjZYbVhRPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t + client-key-data: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQ3BWYUNCRU5sWTI4S0EKMFF5aWR4c0JJM2RFMmxsOEQ1d3JpODBMOFBaSDJia0JOSDlrYTNUcG8vZk5ZdVNwempjeWJDd3R3VVBML0JqNgpFSG5UOGkxbXFPM2tSb2lYV09wbWRGZTRSZDdtbUdyOTlrZ0pJemE4djIyM1FsZjB2MXprdDUxSlluQkpueUMrCk9uQkJwd3BJWXpMNGN6bVVMbE5yZHM1SDN5eE1USVVicG55UmYwcFBZZUJpeTk1S0U3YmpaTkZSSWs4cGI3R2gKd21QR292NFlVY3FobW5nUDdKaVJuWjFxRWU4K0pJTEY3MVlOY3B1bGxFOVBFMXY4UXlRbnBnNjVUdzNaQmJqaQo3MHZIWEx1TWdBOHZxOFp4UWw2alpTRy9KS0VZck0rc2pXMFpKYWVNckVvSTYveXNyazArNkVoZE5jTWVGTUlNCkhqK3ZyRzNGQWdNQkFBRUNnZ0VBSTdLaGU1UUp2ZW5XUDBIUzRBMHI3RG1GMDBZVXgwcWpLYXIzTnlVOVJqaG8KQUJFSktpcGRJMFFsNFc2UHRoeDdGbTRuZ2gzVUpSU29UMDlaMzR5V2RhWDNRTUI5MnlvcmdCM1d3RW82aTNKbQpXOU9uckFWNGJLSU9oeXU5VHlOb2VlOGJnWFQzSnc0YzRQMkEzTlpTSEtDTkJrT0VSL0RjTlROK21UZzdKbnBDCnMvVmoyd2pibllQNmt6MVRTcEVjRksrb3NnYldXQ1AxVDFUeFRFN1k5VlBjbWhibzU5Lzdxc2EzaE8vUjgxRysKQ0VxU3U1emgrQmJvRFZHUStpZFV3OGtqUlhUS2MzWFBWb0R6SmR6cUtJVUYwTkc1Nm4wdGNoZkVEMUpWS09PSQp5a3REdjM1Qi9JWEkwYzk1UHpJN0crOWJvSHM1aW9BcUZKUlo2bllJQVFLQmdRRHNvTmJRc0p1U1pVcW9ZZTBLCkhERFpXS3F5NUM3Q1IvOFNXTXcxeFQzdnJVV2t2Wm5TZUJHUnpvYUEvZmRXQ3lGWHdBd1ZkS0xFSzFkdW5UWDEKUkQ4Zk9odFBDdTdiaXVvV2l4YlpMcGRPUXVzZlhRcDNHUWtoOUNIQVRPc1pML0tkMmxSd0F6dHpGNTZkVHRtdQplZFIxVENiTEVZK1A1Vzg0MXhjV2Y4OEh3UUtCZ1FDM01uaXBXY0xWNDBlS2xZMEg2alhBMDByWCs3V1U1M21RClFKNXAzbWlxSW5qRWlmV2ZQZHh4Y0hyVWRndzlpRk9pVUVvNENIQnNnam5wRU5wUjVhcUJpTzRWUFBuRXdXM2EKSmJ5eWdmRW4wREdBci9PdFpTTUF6OGN4NFVnZmZpSmZPSk8rZXRNemhDMXlMcFAwR05oM2UyRzlDZ0M3eVhDSQpGT1BvdWlzSEJRS0JnSGROT0VFTGFjUkxrWEtIdk0wR0haTFhZMmpDSnRrSkY0OFdlZzc2SFJvRUVFTFkzUDhDClRrbG5DT1ZzSmhHWmx2djQ5WjZ6cVlTaUhYakZobmpjS2I4Q3V0WUZPeHd4VTRoK0k4em44cDBnbkE2NkNCYTMKNXFUWncxS0M5VjFEa1YwSXdOMmdvNDZKY0F6N3ZrQjdhQ1NqZWtPVDNQKzl1Mis2OGdjRDlVdUJBb0dBUjkxdgp3aGRwUEJpZG52ck55VllTWWlOQkQvczVIMEd5eVdqZisrMzRwdzFBelBERnZ3TTRiL1BNNjMybmpaZm1IeDFhCkVDTVhYeW15NS8vcGRRa2dXeEpKTzJHaEpaTXZzY3p0K2lUSlluSGtpWFA4cG4rdlBJbEZ2Z1ovRVlPY25qZ0cKbFVsL2dvME9ldVZVdXdQb0h1N3l4NEtlQ1F5YnJYWnNkWVphakxVQ2dZRUEyeEhLenJ3T1FwMlNNbXlzOFR1bgpZbm54bzMwV0diQzNpYVJIZkQ0TGRvNC9iNHY2TWtkMG9wdXBoMzArQjduRkZoRjlWSVplbVdGWkMrcXNKWHdLCmNwOGMzdjBNZFJkeUtTcTJ4Ly9ZL2s1KytaeU5DSXRZdzdhRy91Wks4TlZyY3h0c2hXOXBobDFJTThXTjBUYmgKNmxwd2xWZGFha0RPY09ZNjE2clRGOXM9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=`)}, + }, + threshold: 1, + signer: true, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := makeFakeCertRotationController(tt.threshold) + cc, err := util.NewClusterClientSet("test", c.Client, &util.ClientOption{}) + if err != nil { + t.Fatal(err) + } + c.ClusterClient = cc + + if tt.signer { + go mockCSRSigner(c.KubeClient) + } + + if err := c.syncCertRotation(context.Background(), tt.secret); (err != nil) != tt.wantErr { + t.Errorf("CertRotationController.syncCertRotation() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func mockCSRSigner(cs kubernetes.Interface) { + for { + select { + case <-time.After(10 * time.Second): + return + default: + csrL, _ := cs.CertificatesV1().CertificateSigningRequests().List(context.Background(), metav1.ListOptions{}) + for _, csr := range csrL.Items { + t := csr.DeepCopy() + t.Status.Certificate = []byte(testCA) + if _, err := cs.CertificatesV1().CertificateSigningRequests().UpdateStatus(context.Background(), t, metav1.UpdateOptions{}); err != nil { + return + } + } + } + } +} + +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) +}