diff --git a/bootstrap/controllers/certificates_controller.go b/bootstrap/controllers/certificates_controller.go index 39ca2b98..6e313a60 100644 --- a/bootstrap/controllers/certificates_controller.go +++ b/bootstrap/controllers/certificates_controller.go @@ -149,7 +149,9 @@ func (r *CertificatesReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrl.Result{}, err } } else { - log.Info("worker nodes are not supported yet") + if err := r.refreshWorkerCertificates(ctx, scope); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, nil } } @@ -188,7 +190,7 @@ func (r *CertificatesReconciler) refreshControlPlaneCertificates(ctx context.Con extraSANs := controlPlaneConfig.ExtraSANs extraSANs = append(extraSANs, controlPlaneEndpoint) - expirySecondsUnix, err := scope.Workload.RefreshCertificates(ctx, scope.Machine, *nodeToken, seconds, extraSANs) + expirySecondsUnix, err := scope.Workload.RefreshControlPlaneCertificates(ctx, scope.Machine, *nodeToken, seconds, extraSANs) if err != nil { r.recorder.Eventf( scope.Machine, @@ -245,3 +247,64 @@ func (r *CertificatesReconciler) updateExpiryDateAnnotation(ctx context.Context, return nil } + +func (r *CertificatesReconciler) refreshWorkerCertificates(ctx context.Context, scope *CertificatesScope) error { + nodeToken, err := token.LookupNodeToken(ctx, r.Client, util.ObjectKey(scope.Cluster), scope.Machine.Name) + if err != nil { + return fmt.Errorf("failed to lookup node token: %w", err) + } + + mAnnotations := scope.Machine.GetAnnotations() + + refreshAnnotation, ok := mAnnotations[bootstrapv1.CertificatesRefreshAnnotation] + if !ok { + return nil + } + + r.recorder.Eventf( + scope.Machine, + corev1.EventTypeNormal, + bootstrapv1.CertificatesRefreshInProgressEvent, + "Certificates refresh in progress. TTL: %s", refreshAnnotation, + ) + + seconds, err := utiltime.TTLToSeconds(refreshAnnotation) + if err != nil { + return fmt.Errorf("failed to parse expires-in annotation value: %w", err) + } + + expirySecondsUnix, err := scope.Workload.RefreshWorkerCertificates(ctx, scope.Machine, *nodeToken, seconds) + if err != nil { + r.recorder.Eventf( + scope.Machine, + corev1.EventTypeWarning, + bootstrapv1.CertificatesRefreshFailedEvent, + "Failed to refresh certificates: %v", err, + ) + return fmt.Errorf("failed to refresh certificates: %w", err) + } + + expiryTime := time.Unix(int64(expirySecondsUnix), 0) + + delete(mAnnotations, bootstrapv1.CertificatesRefreshAnnotation) + mAnnotations[bootstrapv1.MachineCertificatesExpiryDateAnnotation] = expiryTime.Format(time.RFC3339) + scope.Machine.SetAnnotations(mAnnotations) + if err := scope.Patcher.Patch(ctx, scope.Machine); err != nil { + return fmt.Errorf("failed to patch machine annotations: %w", err) + } + + r.recorder.Eventf( + scope.Machine, + corev1.EventTypeNormal, + bootstrapv1.CertificatesRefreshDoneEvent, + "Certificates refreshed, will expire at %s", expiryTime, + ) + + scope.Log.Info("Certificates refreshed", + "cluster", scope.Cluster.Name, + "machine", scope.Machine.Name, + "expiry", expiryTime.Format(time.RFC3339), + ) + + return nil +} diff --git a/go.mod b/go.mod index 52fd366d..e09f131e 100644 --- a/go.mod +++ b/go.mod @@ -119,7 +119,7 @@ require ( golang.org/x/mod v0.19.0 golang.org/x/net v0.23.0 // indirect golang.org/x/oauth2 v0.18.0 // indirect - golang.org/x/sync v0.6.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/go.sum b/go.sum index bbb4cc19..9b2643a8 100644 --- a/go.sum +++ b/go.sum @@ -361,6 +361,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/pkg/ck8s/workload_cluster.go b/pkg/ck8s/workload_cluster.go index 769144f8..535436e9 100644 --- a/pkg/ck8s/workload_cluster.go +++ b/pkg/ck8s/workload_cluster.go @@ -10,6 +10,7 @@ import ( apiv1 "github.com/canonical/k8s-snap-api/api/v1" "github.com/pkg/errors" + "golang.org/x/sync/errgroup" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -242,7 +243,32 @@ func (w *Workload) GetCertificatesExpiryDate(ctx context.Context, machine *clust return response.ExpiryDate, nil } -func (w *Workload) RefreshCertificates(ctx context.Context, machine *clusterv1.Machine, nodeToken string, expirationSeconds int, extraSANs []string) (int, error) { +type ApproveWorkerCSRRequest struct { + Seed int `json:"seed"` +} + +type ApproveWorkerCSRResponse struct{} + +func (w *Workload) ApproveCertificates(ctx context.Context, machine *clusterv1.Machine, capiToken string, seed int) error { + request := ApproveWorkerCSRRequest{} + response := &ApproveWorkerCSRResponse{} + k8sdProxy, err := w.GetK8sdProxyForControlPlane(ctx, k8sdProxyOptions{}) + if err != nil { + return fmt.Errorf("failed to create k8sd proxy: %w", err) + } + + header := map[string][]string{ + "capi-auth-token": {w.authToken}, + } + + if err := w.doK8sdRequest(ctx, k8sdProxy, http.MethodPost, "1.0/x/capi/refresh-certs/approve", header, request, response); err != nil { + return fmt.Errorf("failed to approve certificates: %w", err) + } + + return nil +} + +func (w *Workload) refreshCertificatesPlan(ctx context.Context, machine *clusterv1.Machine, nodeToken string) (int, error) { planRequest := apiv1.ClusterAPICertificatesPlanRequest{} planResponse := &apiv1.ClusterAPICertificatesPlanResponse{} @@ -259,17 +285,76 @@ func (w *Workload) RefreshCertificates(ctx context.Context, machine *clusterv1.M return 0, fmt.Errorf("failed to refresh certificates: %w", err) } + return planResponse.Seed, nil +} + +func (w *Workload) refreshCertificatesRun(ctx context.Context, machine *clusterv1.Machine, nodeToken string, request *apiv1.ClusterAPICertificatesRunRequest) (int, error) { + runResponse := &apiv1.ClusterAPICertificatesRunResponse{} + header := map[string][]string{ + "node-token": {nodeToken}, + } + + k8sdProxy, err := w.GetK8sdProxyForMachine(ctx, machine) + if err != nil { + return 0, fmt.Errorf("failed to create k8sd proxy: %w", err) + } + + if err := w.doK8sdRequest(ctx, k8sdProxy, http.MethodPost, "1.0/x/capi/refresh-certs/run", header, request, runResponse); err != nil { + return 0, fmt.Errorf("failed to run refresh certificates: %w", err) + } + + return runResponse.ExpirationSeconds, nil +} + +func (w *Workload) RefreshWorkerCertificates(ctx context.Context, machine *clusterv1.Machine, nodeToken string, expirationSeconds int) (int, error) { + seed, err := w.refreshCertificatesPlan(ctx, machine, nodeToken) + if err != nil { + return 0, fmt.Errorf("failed to get refresh certificates plan: %w", err) + } + + request := apiv1.ClusterAPICertificatesRunRequest{ + Seed: seed, + ExpirationSeconds: expirationSeconds, + } + + var seconds int + + eg, ctx := errgroup.WithContext(ctx) + eg.Go(func() error { + seconds, err = w.refreshCertificatesRun(ctx, machine, nodeToken, &request) + return err + }) + + eg.Go(func() error { + return w.ApproveCertificates(ctx, machine, nodeToken, seed) + }) + + if err := eg.Wait(); err != nil { + return 0, fmt.Errorf("failed to refresh worker certificates: %w", err) + } + + return seconds, nil + +} + +func (w *Workload) RefreshControlPlaneCertificates(ctx context.Context, machine *clusterv1.Machine, nodeToken string, expirationSeconds int, extraSANs []string) (int, error) { + seed, err := w.refreshCertificatesPlan(ctx, machine, nodeToken) + if err != nil { + return 0, fmt.Errorf("failed to get refresh certificates plan: %w", err) + } + runRequest := apiv1.ClusterAPICertificatesRunRequest{ ExpirationSeconds: expirationSeconds, - Seed: planResponse.Seed, + Seed: seed, ExtraSANs: extraSANs, } - runResponse := &apiv1.ClusterAPICertificatesRunResponse{} - if err := w.doK8sdRequest(ctx, k8sdProxy, http.MethodPost, "1.0/x/capi/refresh-certs/run", header, runRequest, runResponse); err != nil { + + seconds, err := w.refreshCertificatesRun(ctx, machine, nodeToken, &runRequest) + if err != nil { return 0, fmt.Errorf("failed to run refresh certificates: %w", err) } - return runResponse.ExpirationSeconds, nil + return seconds, nil } func (w *Workload) RefreshMachine(ctx context.Context, machine *clusterv1.Machine, nodeToken string, upgradeOption string) (string, error) {