Skip to content

Commit 0e3ae9c

Browse files
Add kubeconfig cert rotation tests
Signed-off-by: Furkat Gofurov <[email protected]>
1 parent 7c0bc0b commit 0e3ae9c

File tree

2 files changed

+135
-3
lines changed

2 files changed

+135
-3
lines changed

controlplane/internal/controllers/rke2controlplane_controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ func (r *RKE2ControlPlaneReconciler) reconcileNormal(
468468
conditions.MarkFalse(
469469
rcp, controlplanev1.CertificatesAvailableCondition,
470470
controlplanev1.CertificatesGenerationFailedReason,
471-
clusterv1.ConditionSeverityWarning, err.Error())
471+
clusterv1.ConditionSeverityWarning, "%s", err.Error())
472472

473473
return ctrl.Result{}, err
474474
}

controlplane/internal/controllers/rke2controlplane_controller_test.go

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
package controllers
22

33
import (
4+
"crypto/rand"
5+
"crypto/rsa"
6+
"crypto/x509"
7+
"crypto/x509/pkix"
8+
"encoding/base64"
9+
"encoding/pem"
10+
"fmt"
11+
"math/big"
412
"time"
513

614
. "github.com/onsi/ginkgo/v2"
715
. "github.com/onsi/gomega"
816
bootstrapv1 "github.com/rancher/cluster-api-provider-rke2/bootstrap/api/v1beta1"
917
controlplanev1 "github.com/rancher/cluster-api-provider-rke2/controlplane/api/v1beta1"
10-
11-
// "github.com/rancher/cluster-api-provider-rke2/pkg/kubeconfig"
1218
"github.com/rancher/cluster-api-provider-rke2/pkg/rke2"
1319
corev1 "k8s.io/api/core/v1"
1420
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1521
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
22+
"sigs.k8s.io/cluster-api/util/certs"
1623
"sigs.k8s.io/cluster-api/util/collections"
1724
"sigs.k8s.io/cluster-api/util/conditions"
1825
"sigs.k8s.io/cluster-api/util/kubeconfig"
@@ -251,4 +258,129 @@ var _ = Describe("Reconcile control plane conditions", func() {
251258
Expect(conditions.GetMessage(rcp, controlplanev1.ControlPlaneComponentsHealthyCondition)).To(Equal(
252259
"Control plane node missing-machine does not have a corresponding machine"))
253260
})
261+
262+
It("should rotate kubeconfig secret if needed", func() {
263+
r := &RKE2ControlPlaneReconciler{
264+
Client: testEnv.GetClient(),
265+
Scheme: testEnv.GetScheme(),
266+
managementCluster: &rke2.Management{Client: testEnv.GetClient(), SecretCachingClient: testEnv.GetClient()},
267+
managementClusterUncached: &rke2.Management{Client: testEnv.GetClient()},
268+
}
269+
clusterName := client.ObjectKey{Namespace: ns.Name, Name: "test"}
270+
endpoint := clusterv1.APIEndpoint{Host: "1.2.3.4", Port: 6443}
271+
272+
// Create kubeconfig secret with short expiry
273+
shortExpiryDate := time.Now().Add(24 * time.Hour) // 1 day from now
274+
secret, err := createKubeconfigSecret(ns.Name, shortExpiryDate)
275+
Expect(err).ToNot(HaveOccurred())
276+
Expect(testEnv.Create(ctx, secret)).To(Succeed())
277+
278+
// Check that rotation is needed
279+
needsRotation, err := kubeconfig.NeedsClientCertRotation(secret, certs.ClientCertificateRenewalDuration)
280+
Expect(err).ToNot(HaveOccurred())
281+
Expect(needsRotation).To(BeTrue())
282+
283+
// Rotate kubeconfig secret
284+
_, err = r.reconcileKubeconfig(ctx, clusterName, endpoint, rcp)
285+
Expect(err).ToNot(HaveOccurred())
286+
287+
Expect(testEnv.Get(ctx, client.ObjectKey{Namespace: ns.Name, Name: secret.Name}, secret)).To(Succeed())
288+
})
289+
290+
It("should not rotate kubeconfig secret if not needed", func() {
291+
r := &RKE2ControlPlaneReconciler{
292+
Client: testEnv.GetClient(),
293+
Scheme: testEnv.GetScheme(),
294+
managementCluster: &rke2.Management{Client: testEnv.GetClient(), SecretCachingClient: testEnv.GetClient()},
295+
managementClusterUncached: &rke2.Management{Client: testEnv.GetClient()},
296+
}
297+
clusterName := client.ObjectKey{Namespace: ns.Name, Name: "test"}
298+
endpoint := clusterv1.APIEndpoint{Host: "1.2.3.4", Port: 6443}
299+
300+
// Create kubeconfig secret with long expiry
301+
longExpiryDate := time.Now().Add(365 * 24 * time.Hour) // 1 year from now
302+
secret, err := createKubeconfigSecret(ns.Name, longExpiryDate)
303+
Expect(err).ToNot(HaveOccurred())
304+
Expect(testEnv.Create(ctx, secret)).To(Succeed())
305+
306+
// Check that no rotation is needed
307+
needsRotation, err := kubeconfig.NeedsClientCertRotation(secret, certs.ClientCertificateRenewalDuration)
308+
Expect(err).ToNot(HaveOccurred())
309+
Expect(needsRotation).To(BeFalse())
310+
311+
// Ensure no rotation occurs
312+
_, err = r.reconcileKubeconfig(ctx, clusterName, endpoint, rcp)
313+
Expect(err).ToNot(HaveOccurred())
314+
315+
Expect(testEnv.Get(ctx, client.ObjectKey{Namespace: ns.Name, Name: secret.Name}, secret)).To(Succeed())
316+
})
254317
})
318+
319+
// generateCertAndKey generates a self-signed certificate and private key.
320+
func generateCertAndKey(expiryDate time.Time) ([]byte, []byte, error) {
321+
priv, err := rsa.GenerateKey(rand.Reader, 2048)
322+
if err != nil {
323+
return nil, nil, err
324+
}
325+
326+
template := x509.Certificate{
327+
SerialNumber: big.NewInt(1),
328+
Subject: pkix.Name{
329+
Organization: []string{"Test Org"},
330+
},
331+
NotBefore: time.Now(),
332+
NotAfter: expiryDate,
333+
334+
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
335+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
336+
BasicConstraintsValid: true,
337+
}
338+
339+
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
340+
if err != nil {
341+
return nil, nil, err
342+
}
343+
344+
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
345+
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
346+
347+
return certPEM, keyPEM, nil
348+
}
349+
350+
// createKubeconfigSecret creates a Kubernetes secret with a kubeconfig containing a client certificate and key.
351+
func createKubeconfigSecret(namespace string, expiryDate time.Time) (*corev1.Secret, error) {
352+
certPEM, keyPEM, err := generateCertAndKey(expiryDate)
353+
if err != nil {
354+
return nil, err
355+
}
356+
357+
secret := &corev1.Secret{
358+
ObjectMeta: metav1.ObjectMeta{
359+
Name: "test-kubeconfig-secret",
360+
Namespace: namespace,
361+
},
362+
Data: map[string][]byte{
363+
"value": []byte(fmt.Sprintf(`
364+
apiVersion: v1
365+
kind: Config
366+
clusters:
367+
- cluster:
368+
server: https://1.2.3.4:6443
369+
name: test-cluster
370+
contexts:
371+
- context:
372+
cluster: test-cluster
373+
user: test-user
374+
name: test-context
375+
current-context: test-context
376+
users:
377+
- name: test-user
378+
user:
379+
client-certificate-data: %s
380+
client-key-data: %s
381+
`, base64.StdEncoding.EncodeToString(certPEM), base64.StdEncoding.EncodeToString(keyPEM))),
382+
},
383+
}
384+
385+
return secret, nil
386+
}

0 commit comments

Comments
 (0)