Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ KCP regenerates kubeconfigs before client certs expire #3140

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions controlplane/kubeadm/controllers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal/hash"
capierrors "sigs.k8s.io/cluster-api/errors"
"sigs.k8s.io/cluster-api/util"
"sigs.k8s.io/cluster-api/util/certs"
"sigs.k8s.io/cluster-api/util/kubeconfig"
"sigs.k8s.io/cluster-api/util/patch"
"sigs.k8s.io/cluster-api/util/secret"
Expand All @@ -46,26 +47,42 @@ func (r *KubeadmControlPlaneReconciler) reconcileKubeconfig(ctx context.Context,
return nil
}

_, err := secret.GetFromNamespacedName(ctx, r.Client, clusterName, secret.Kubeconfig)
controllerOwnerRef := *metav1.NewControllerRef(kcp, controlplanev1.GroupVersion.WithKind("KubeadmControlPlane"))
configSecret, err := secret.GetFromNamespacedName(ctx, r.Client, clusterName, secret.Kubeconfig)
switch {
case apierrors.IsNotFound(err):
createErr := kubeconfig.CreateSecretWithOwner(
ctx,
r.Client,
clusterName,
endpoint.String(),
*metav1.NewControllerRef(kcp, controlplanev1.GroupVersion.WithKind("KubeadmControlPlane")),
controllerOwnerRef,
)
if createErr != nil {
if createErr == kubeconfig.ErrDependentCertificateNotFound {
return errors.Wrapf(&capierrors.RequeueAfterError{RequeueAfter: dependentCertRequeueAfter},
"could not find secret %q for Cluster %q in namespace %q, requeuing",
secret.ClusterCA, clusterName.Name, clusterName.Namespace)
}
return createErr
if errors.Is(createErr, kubeconfig.ErrDependentCertificateNotFound) {
return errors.Wrapf(&capierrors.RequeueAfterError{RequeueAfter: dependentCertRequeueAfter},
"could not find secret %q, requeuing", secret.ClusterCA)
}
// always return if we have just created in order to skip rotation checks
return createErr
case err != nil:
return errors.Wrapf(err, "failed to retrieve kubeconfig Secret for Cluster %q in namespace %q", clusterName.Name, clusterName.Namespace)
return errors.Wrap(err, "failed to retrieve kubeconfig Secret")
}

// only do rotation on owned secrets
if !util.IsControlledBy(configSecret, kcp) {
return nil
}

needsRotation, err := kubeconfig.NeedsClientCertRotation(configSecret, certs.ClientCertificateRenewalDuration)
if err != nil {
return err
}

if needsRotation {
r.Log.Info("rotating kubeconfig secret")
vincepri marked this conversation as resolved.
Show resolved Hide resolved
if err := kubeconfig.RegenerateSecret(ctx, r.Client, configSecret); err != nil {
return errors.Wrap(err, "failed to regenerate kubeconfig")
}
}

return nil
Expand Down
10 changes: 10 additions & 0 deletions docs/book/src/developer/architecture/controllers/control-plane.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,13 @@ spec:
```

[scale]: https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#scale-subresource

## Kubeconfig management

Control Plane providers are expected to create and maintain a Kubeconfig
secret for operators to gain initial access to the cluster. If a provider uses
client certificates for authentication in these Kubeconfigs, the client
certificate should be kept with a reasonably short expiration period and
periodically regenerated to keep a valid set of credentials available. As an
example, the Kubeadm Control Plane provider uses a year of validity and
refreshes the certificate after 6 months.
7 changes: 7 additions & 0 deletions docs/book/src/tasks/kubeadm-control-plane.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,10 @@ Caveats:
* `kubeadmConfigSpec.clusterConfiguration.scheduler.extraArgs`
* Anything underneath `kubeadmConfigSpec.clusterConfiguration.etcd`
* etc.

### Kubeconfig management

KCP will generate and manage the admin Kubeconfig for clusters. The client
certificate for the admin user is created with a valid lifespan of a year, and
will be automatically regenerated when the cluster is reconciled and has less
than 6 months of validity remaining.
4 changes: 4 additions & 0 deletions util/certs/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,8 @@ const (

// DefaultCertDuration is the default lifespan used when creating certificates.
DefaultCertDuration = time.Hour * 24 * 365

// When client certificates have less than ClientCertificateRenewalDuration
// left before expiry, they will be regenerated.
ClientCertificateRenewalDuration = DefaultCertDuration / 2
)
133 changes: 99 additions & 34 deletions util/kubeconfig/kubeconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"crypto/rsa"
"crypto/x509"
"fmt"
"time"

"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
Expand All @@ -45,11 +46,7 @@ func FromSecret(ctx context.Context, c client.Reader, cluster client.ObjectKey)
if err != nil {
return nil, err
}
data, ok := out.Data[secret.KubeconfigDataName]
if !ok {
return nil, errors.Errorf("missing key %q in secret data", secret.KubeconfigDataName)
}
return data, nil
return toKubeconfigBytes(out)
}

// New creates a new Kubeconfig using the cluster name and specified endpoint.
Expand Down Expand Up @@ -109,37 +106,10 @@ func CreateSecret(ctx context.Context, c client.Client, cluster *clusterv1.Clust

// CreateSecretWithOwner creates the Kubeconfig secret for the given cluster name, namespace, endpoint, and owner reference.
func CreateSecretWithOwner(ctx context.Context, c client.Client, clusterName client.ObjectKey, endpoint string, owner metav1.OwnerReference) error {
clusterCA, err := secret.GetFromNamespacedName(ctx, c, clusterName, secret.ClusterCA)
if err != nil {
if apierrors.IsNotFound(err) {
return ErrDependentCertificateNotFound
}
return err
}

cert, err := certs.DecodeCertPEM(clusterCA.Data[secret.TLSCrtDataName])
if err != nil {
return errors.Wrap(err, "failed to decode CA Cert")
} else if cert == nil {
return errors.New("certificate not found in config")
}

key, err := certs.DecodePrivateKeyPEM(clusterCA.Data[secret.TLSKeyDataName])
if err != nil {
return errors.Wrap(err, "failed to decode private key")
} else if key == nil {
return errors.New("CA private key not found")
}

server := fmt.Sprintf("https://%s", endpoint)
cfg, err := New(clusterName.Name, server, cert, key)
out, err := generateKubeconfig(ctx, c, clusterName, server)
if err != nil {
return errors.Wrap(err, "failed to generate a kubeconfig")
}

out, err := clientcmd.Write(*cfg)
if err != nil {
return errors.Wrap(err, "failed to serialize config to yaml")
return err
}

return c.Create(ctx, GenerateSecretWithOwner(clusterName, out, owner))
Expand Down Expand Up @@ -174,3 +144,98 @@ func GenerateSecretWithOwner(clusterName client.ObjectKey, data []byte, owner me
},
}
}

// NeedsClientCertRotation returns whether any of the Kubeconfig secret's client certificates will expire before the given threshold.
func NeedsClientCertRotation(configSecret *corev1.Secret, threshold time.Duration) (bool, error) {
benmoss marked this conversation as resolved.
Show resolved Hide resolved
now := time.Now()

data, err := toKubeconfigBytes(configSecret)
if err != nil {
return false, err
}

config, err := clientcmd.Load(data)
if err != nil {
return false, errors.Wrap(err, "failed to convert kubeconfig Secret into a clientcmdapi.Config")
}

for _, authInfo := range config.AuthInfos {
cert, err := certs.DecodeCertPEM(authInfo.ClientCertificateData)
if err != nil {
return false, errors.Wrap(err, "failed to decode kubeconfig client certificate")
}
if cert.NotAfter.Sub(now) < threshold {
return true, nil
}
}

return false, nil
}

// RegenerateSecret creates and stores a new Kubeconfig in the given secret.
func RegenerateSecret(ctx context.Context, c client.Client, configSecret *corev1.Secret) error {
clusterName, _, err := secret.ParseSecretName(configSecret.Name)
if err != nil {
return errors.Wrap(err, "failed to parse secret name")
}
data, err := toKubeconfigBytes(configSecret)
if err != nil {
return err
}

config, err := clientcmd.Load(data)
if err != nil {
return errors.Wrap(err, "failed to convert kubeconfig Secret into a clientcmdapi.Config")
}
endpoint := config.Clusters[clusterName].Server
key := client.ObjectKey{Name: clusterName, Namespace: configSecret.Namespace}
out, err := generateKubeconfig(ctx, c, key, endpoint)
if err != nil {
return err
}
configSecret.Data[secret.KubeconfigDataName] = out
return c.Update(ctx, configSecret)
}

func generateKubeconfig(ctx context.Context, c client.Client, clusterName client.ObjectKey, endpoint string) ([]byte, error) {
clusterCA, err := secret.GetFromNamespacedName(ctx, c, clusterName, secret.ClusterCA)
if err != nil {
if apierrors.IsNotFound(err) {
return nil, ErrDependentCertificateNotFound
}
return nil, err
}

cert, err := certs.DecodeCertPEM(clusterCA.Data[secret.TLSCrtDataName])
if err != nil {
return nil, errors.Wrap(err, "failed to decode CA Cert")
} else if cert == nil {
return nil, errors.New("certificate not found in config")
}

key, err := certs.DecodePrivateKeyPEM(clusterCA.Data[secret.TLSKeyDataName])
if err != nil {
return nil, errors.Wrap(err, "failed to decode private key")
} else if key == nil {
return nil, errors.New("CA private key not found")
}

cfg, err := New(clusterName.Name, endpoint, cert, key)
if err != nil {
return nil, errors.Wrap(err, "failed to generate a kubeconfig")
}

out, err := clientcmd.Write(*cfg)
if err != nil {
return nil, errors.Wrap(err, "failed to serialize config to yaml")
}
return out, nil
}

func toKubeconfigBytes(out *corev1.Secret) ([]byte, error) {
data, ok := out.Data[secret.KubeconfigDataName]
if !ok {
return nil, errors.Errorf("missing key %q in secret data", secret.KubeconfigDataName)
}
return data, nil
}
84 changes: 78 additions & 6 deletions util/kubeconfig/kubeconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client/fake"

clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
"sigs.k8s.io/cluster-api/util"
"sigs.k8s.io/cluster-api/util/certs"
"sigs.k8s.io/cluster-api/util/secret"
)
Expand All @@ -47,17 +48,17 @@ clusters:
- cluster:
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRFNU1ERXhNREU0TURBME1Gb1hEVEk1TURFd056RTRNREEwTUZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBT1EvCmVndmViNk1qMHkzM3hSbGFjczd6OXE4QTNDajcrdnRrZ0pUSjRUSVB4TldRTEd0Q0dmL0xadzlHMW9zNmRKUHgKZFhDNmkwaHA5czJuT0Y2VjQvREUrYUNTTU45VDYzckdWb2s0TkcwSlJPYmlRWEtNY1VQakpiYm9PTXF2R2lLaAoyMGlFY0h5K3B4WkZOb3FzdnlaRGM5L2dRSHJVR1FPNXp6TDNHZGhFL0k1Nkczek9JaWhhbFRHSTNaakRRS05CCmVFV3FONTVDcHZzT3I1b0ZnTmZZTXk2YzZ4WXlUTlhWSUkwNFN0Z2xBbUk4bzZWaTNUVEJhZ1BWaldIVnRha1EKU2w3VGZtVUlIdndKZUo3b2hxbXArVThvaGVTQUIraHZSbDIrVHE5NURTemtKcmRjNmphcyswd2FWaEJydEh1agpWMU15NlNvV2VVUlkrdW5VVFgwQ0F3RUFBYU1qTUNFd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFIT2thSXNsd0pCOE5PaENUZkF4UWlnaUc1bEMKQlo0LytGeHZ3Y1pnWGhlL0IyUWo1UURMNWlRUU1VL2NqQ0tyYUxkTFFqM1o1aHA1dzY0K2NWRUg3Vm9PSTFCaQowMm13YTc4eWo4aDNzQ2lLQXJiU21kKzNld1QrdlNpWFMzWk9EYWRHVVRRa1BnUHB0THlaMlRGakF0SW43WjcyCmpnYlVnT2FXaklKbnlwRVJ5UmlSKzBvWlk4SUlmWWFsTHUwVXlXcmkwaVhNRmZqQUQ1UVNURy8zRGN5djhEN1UKZHBxU2l5ekJkZVRjSExyenpEbktBeXhQWWgvcWpKZ0tRdEdIakhjY0FCSE1URlFtRy9Ea1pNTnZWL2FZSnMrKwp0aVJCbHExSFhlQ0d4aExFcGdQcGxVb3IrWmVYTGF2WUo0Z2dMVmIweGl2QTF2RUtyaUUwak1Wd2lQaz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
server: https://test-cluster-api:6443
name: test-cluster-api
name: test1
contexts:
- context:
cluster: test-cluster-api
user: kubernetes-admin
name: kubernetes-admin@test-cluster-api
current-context: kubernetes-admin@test-cluster-api
cluster: test1
user: test1-admin
name: test1-admin@test1
current-context: test1-admin@test1
kind: Config
preferences: {}
users:
- name: kubernetes-admin
- name: test1-admin
user:
client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM4akNDQWRxZ0F3SUJBZ0lJUTJFS3c0cU0wbFV3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB4T1RBeE1UQXhPREF3TkRCYUZ3MHlNREF4TVRBeE9EQXdOREphTURReApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sa3dGd1lEVlFRREV4QnJkV0psY201bGRHVnpMV0ZrCmJXbHVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXUxeWNHcVdLMlZBUGpmWlYKeUdHWmhvQWdxZ1oreVFqU0pYQlVwNkR4VkZyYStENHJDVkNpRDNhWTVmTWNXaVpYVy9uanJsNFRudWJydndhWgpTN0hhMUxELzFZdmhFUFhLcnlBMzVNMStsN0JkUjA3T3NlRnFqNXNJQk9xWDNoNEJmckQ0SFQ5VGxSS082TXgxClMycSt5NzVaYjI5eXN0UTk3SGk4ZXVBS0sxN0JuSmJ5Zk80NlMvOFVxc2tlb0JXT3VnRkJHMlQrTFd6RXluK1oKVjdUUHZxdDE0MG1lQU40TStZUy91dFp2VmE0WFFmKy80czB4TjVwMGw4M0RrYnVWbnErcjR5dzBiSHM4cHdWdwo0Z3RuTVhrbjFhcGUwOGN4YUtBMGpodnZ4cFgyVnhHbEsxUTR0aDk1S2JNQWlpMlVINFcyNE1GSnlxOHloOUljCk54UldZd0lEQVFBQm95Y3dKVEFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUgKQXdJd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFCbDUwSmNVblhGRHB4TWtkL2JOdlRHMkxVTnVWTVNnQmlCNQpmbFlpWTJibUQ3NVMreEE0eEwrZEYxanlJZHRDTGVtZURoS2MwRWtyVUpyYk5XSTZnVDB0OFAwWVNuYmxiY09ICmxBRHdmMjhoVW5TYTdIR1NOaWNBdDdKOGpnd296ZzFDVnNWbE9YM2cvcWdmSkdYeld0QzJMRFVvdjR3MFVNMVgKQ2pLNFdtMk8xTDFybEpzaHE1VysxalZzTEllYnZIcjlYb0cxRlcyY0ovcHJTK0dFS2dtNWc4YjZ1MWdJQXVFNAozOHNVQTltU3ZBYlgwR1RWdXI3Um9taWhSR2QwTktZK0k1S3A2bWtIRnpLVEVRbks2YXcrQWMvVFJObmFwUEh6Ci9IMXA0eGkyWlFHYi84MURhQjVTZDRwTURzK09FK1BNNitudkN4amN2eFdFbEdTdjE2Yz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBdTF5Y0dxV0syVkFQamZaVnlHR1pob0FncWdaK3lRalNKWEJVcDZEeFZGcmErRDRyCkNWQ2lEM2FZNWZNY1dpWlhXL25qcmw0VG51YnJ2d2FaUzdIYTFMRC8xWXZoRVBYS3J5QTM1TTErbDdCZFIwN08Kc2VGcWo1c0lCT3FYM2g0QmZyRDRIVDlUbFJLTzZNeDFTMnEreTc1WmIyOXlzdFE5N0hpOGV1QUtLMTdCbkpieQpmTzQ2Uy84VXFza2VvQldPdWdGQkcyVCtMV3pFeW4rWlY3VFB2cXQxNDBtZUFONE0rWVMvdXRadlZhNFhRZisvCjRzMHhONXAwbDgzRGtidVZucStyNHl3MGJIczhwd1Z3NGd0bk1Ya24xYXBlMDhjeGFLQTBqaHZ2eHBYMlZ4R2wKSzFRNHRoOTVLYk1BaWkyVUg0VzI0TUZKeXE4eWg5SWNOeFJXWXdJREFRQUJBb0lCQVFDc0JLamw1aHNHemZTWgorQkptT1FXRmNWbU1BUTZpY0ZEUVFzUFdhM05tYVV3bElwN01uSlZOOFNzTDVCcWh3aFh1d2cwQjZDbkhlR2YxCktJL1I2V2JxWTk5ZkpsL3EvRitzVGI1RGVVL0M0UStqQ24zRzN4akE1Q3VHcUFQcTBFMjdEYXVlM3FkVWRJZDAKd1ZMbmZRZlRjOTRVNjVPNUVCZ1NaZjlXS1IvdEZDNHpGSlVselhHTlYxT2hOTWVyeXovbllmSVRZZGppUWNiRwplcDJucHk1cHZ5dEFPY1RiV0xXUEw4T2RKTDMvTER3b0h2aHlSa3huZXhWRTc0K3ZGd2lYbkRkNEp6ODVJVzBvCkFyeGEyRlJzOGZyWXFreHNSQ1VGYmRXNUpMTzhRVFBWYnl3S1c3Z0Z4S0c1U1c4Y004cmJLTHEzT01JOXBXVkoKTzNscVQxc1JBb0dCQU50QUxzR2VodzZsZWN3Zi9VU0RFN0FWNFQrdDJsWit6a3J1d3dodloyWXJYZHpXbGVqTAprNGpZWjhkQUNUUVdDRkJlaWtmYi9XdzZtMFF3ZUZudzExdVd1WndWRVJPS3BnRDFTa0krcVRtdGd0V2J2Y2lBClg4U0t4SU5qTGNzTzRLZUoxdEdkaVVDVEg3MW8zV0pBOXYzR3NaTlkrdW1WTVhnaGQ2d2YrTnB0QW9HQkFOckUKR3djOWNLVGVtZWZWSTcraFVtS2YvNm9SQ2NYdWxIK3gwSEZwNVBIQzl3NEhTMVp0Zk9NS3F6QzlJMWt6a200RwpjYW11WHovRy9iQXg4WGdaa3lURnRxTk5hdjE3Y0UzV25GRlMxejRHeGRQNDMvSkdLVWJrUzhkM1dZc0pkZnRYCkt5Vm45anl3Yjc0VG5hSnFIVlBSWFJRSkNFR3E2VlR4RVVGNlIzSVBBb0dBSmFTYlluckpUV1p6eHV3bkc4QTEKZlNJRWpsNVhBa3E3T0hwTjJnRG1pOUFlU1hBK1JMM1BFc3UwNWF6RTU4QndwUHZXV2dnWE5xSEpUcWZUd2Yxcgp2RG5nbkQreHN0MDNLeXJ5R1BXUk1HbnQ4S2JRcXIvL3NVcngrbXpveTlnK0VnWEVjRERRQTlvK3ROSndVQkkvClZjcnJhaFQ0MzJuU0dJSUdmZkx2VXZFQ2dZQmtNRGVvb3l5NWRQRExTY09yZVhnL2pzTUo0ZSsxNUVQQ0QyOUUKNFpobVdFSEkvUEkxek1MTFFCR1NxcXhMcCtEQjN0V2pQaWFGRU44U0dHMWI4V3FBQnNSVUdacU1LRUlRZzk3bgpKNmRIMHRZNjg5bXNIUkcrVThPWXdFSVQrT3M5aG5oT0UwU2tHckd5UFUyT0drY0FJZndjdHQ0L0pNVGpqOXUxClB3a0ZaUUtCZ1FDTWppdkpGL3crQXlUZUo0K1piWWpvZ091QkJFRE9oeXdiRnN5NC9ubVduTm4rQXRYQklDaGkKR2J6LzFuWkZTOGc2Nlh2cGFTWEQ5blVpS1BOUW5ORzUralJmclluTlI4WERCL3ZiNk9TMVFHbXBvWWxJZ2Q3UgpjTVpSRm1sbTUvbkJMWkdoVVpjOXZVN1pRVis4RXdLK2lHaGNrTFduVGhHNURZTkRWaksxcFE9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
Expand Down Expand Up @@ -349,3 +350,74 @@ func TestCreateSecret(t *testing.T) {
g.Expect(restClient.CAData).To(Equal(certs.EncodeCertPEM(caCert)))
g.Expect(restClient.Host).To(Equal("https://localhost:8443"))
}

func TestNeedsClientCertRotation(t *testing.T) {
g := NewWithT(t)
caKey, err := certs.NewPrivateKey()
g.Expect(err).NotTo(HaveOccurred())

caCert, err := getTestCACert(caKey)
g.Expect(err).NotTo(HaveOccurred())

config, err := New("foo", "https://127:0.0.1:4003", caCert, caKey)
g.Expect(err).NotTo(HaveOccurred())

out, err := clientcmd.Write(*config)
g.Expect(err).NotTo(HaveOccurred())

owner := metav1.OwnerReference{
Name: "test1",
Kind: "Cluster",
APIVersion: clusterv1.GroupVersion.String(),
}

kubeconfigSecret := GenerateSecretWithOwner(
client.ObjectKey{
Name: "test1",
Namespace: "test",
},
out,
owner,
)

g.Expect(NeedsClientCertRotation(kubeconfigSecret, certs.DefaultCertDuration)).To(BeTrue())
g.Expect(NeedsClientCertRotation(kubeconfigSecret, certs.DefaultCertDuration-time.Hour)).To(BeFalse())
}

func TestRegenerateClientCerts(t *testing.T) {
g := NewWithT(t)
caKey, err := certs.NewPrivateKey()
g.Expect(err).NotTo(HaveOccurred())

caCert, err := getTestCACert(caKey)
g.Expect(err).NotTo(HaveOccurred())

caSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test1-ca",
Namespace: "test",
},
Data: map[string][]byte{
secret.TLSKeyDataName: certs.EncodePrivateKeyPEM(caKey),
secret.TLSCrtDataName: certs.EncodeCertPEM(caCert),
},
}

c := fake.NewFakeClientWithScheme(setupScheme(), validSecret, caSecret)

oldConfig, err := clientcmd.Load(validSecret.Data[secret.KubeconfigDataName])
g.Expect(err).NotTo(HaveOccurred())
oldCert, err := certs.DecodeCertPEM(oldConfig.AuthInfos["test1-admin"].ClientCertificateData)
g.Expect(err).NotTo(HaveOccurred())

g.Expect(RegenerateSecret(context.Background(), c, validSecret)).To(Succeed())

newSecret := &corev1.Secret{}
g.Expect(c.Get(context.Background(), util.ObjectKey(validSecret), newSecret)).To(Succeed())
newConfig, err := clientcmd.Load(newSecret.Data[secret.KubeconfigDataName])
g.Expect(err).NotTo(HaveOccurred())
newCert, err := certs.DecodeCertPEM(newConfig.AuthInfos["test1-admin"].ClientCertificateData)
g.Expect(err).NotTo(HaveOccurred())

g.Expect(newCert.NotAfter).To(BeTemporally(">", oldCert.NotAfter))
}