Skip to content

Commit

Permalink
Add refresh-certs Command (#623)
Browse files Browse the repository at this point in the history
This commit adds the `refresh-certs` command to the snap, allowing the administrator to refresh the certificates for any node in the cluster.
  • Loading branch information
mateoflorido authored Aug 27, 2024
1 parent edb4cc1 commit 545d1fb
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 4 deletions.
1 change: 1 addition & 0 deletions docs/src/_parts/commands/k8s.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Canonical Kubernetes CLI
* [k8s get-join-token](k8s_get-join-token.md) - Create a token for a node to join the cluster
* [k8s join-cluster](k8s_join-cluster.md) - Join a cluster using the provided token
* [k8s kubectl](k8s_kubectl.md) - Integrated Kubernetes kubectl client
* [k8s refresh-certs](k8s_refresh-certs.md) - Refresh the certificates of the running node
* [k8s remove-node](k8s_remove-node.md) - Remove a node from the cluster
* [k8s set](k8s_set.md) - Set cluster configuration
* [k8s status](k8s_status.md) - Retrieve the current status of the cluster
Expand Down
21 changes: 21 additions & 0 deletions docs/src/_parts/commands/k8s_refresh-certs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## k8s refresh-certs

Refresh the certificates of the running node

```
k8s refresh-certs [flags]
```

### Options

```
--expires-in string the time until the certificates expire, e.g., 1h, 2d, 4mo, 5y. Aditionally, any valid time unit for ParseDuration is accepted.
--extra-sans stringArray extra SANs to add to the certificates.
-h, --help help for refresh-certs
--timeout duration the max time to wait for the command to execute (default 1m30s)
```

### SEE ALSO

* [k8s](k8s.md) - Canonical Kubernetes CLI

1 change: 1 addition & 0 deletions src/k8s/cmd/k8s/k8s.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ func NewRootCmd(env cmdutil.ExecutionEnvironment) *cobra.Command {
&cobra.Group{ID: "management", Title: "Management Commands:"},
newEnableCmd(env),
newDisableCmd(env),
newRefreshCertsCmd(env),
newSetCmd(env),
newGetCmd(env),
)
Expand Down
77 changes: 77 additions & 0 deletions src/k8s/cmd/k8s/k8s_refresh_certs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package k8s

import (
"context"
"time"

apiv1 "github.com/canonical/k8s-snap-api/api/v1"
cmdutil "github.com/canonical/k8s/cmd/util"
"github.com/canonical/k8s/pkg/utils"
"github.com/spf13/cobra"
)

func newRefreshCertsCmd(env cmdutil.ExecutionEnvironment) *cobra.Command {
var opts struct {
extraSANs []string
expiresIn string
timeout time.Duration
}
cmd := &cobra.Command{
Use: "refresh-certs",
Short: "Refresh the certificates of the running node",
PreRun: chainPreRunHooks(hookRequireRoot(env)),
Run: func(cmd *cobra.Command, args []string) {
ttl, err := utils.TTLToSeconds(opts.expiresIn)
if err != nil {
cmd.PrintErrf("Error: Failed to parse TTL. \n\nThe error was: %v\n", err)
}

client, err := env.Snap.K8sdClient("")
if err != nil {
cmd.PrintErrf("Error: Failed to create a k8sd client. Make sure that the k8sd service is running.\n\nThe error was: %v\n", err)
env.Exit(1)
return
}

ctx, cancel := context.WithTimeout(cmd.Context(), opts.timeout)
cobra.OnFinalize(cancel)

plan, err := client.RefreshCertificatesPlan(ctx, apiv1.RefreshCertificatesPlanRequest{})
if err != nil {
cmd.PrintErrf("Error: Failed to get the certificates refresh plan.\n\nThe error was: %v\n", err)
env.Exit(1)
return
}

if len(plan.CertificateSigningRequests) > 0 {
cmd.Println("The following CertificateSigningRequests should be approved. Run the following commands on any of the control plane nodes of the cluster:")
for _, csr := range plan.CertificateSigningRequests {
cmd.Printf("k8s kubectl certificate approve %s\n", csr)
}
}

runRequest := apiv1.RefreshCertificatesRunRequest{
Seed: plan.Seed,
ExpirationSeconds: ttl,
ExtraSANs: opts.extraSANs,
}

cmd.Println("Waiting for certificates to be created...")
runResponse, err := client.RefreshCertificatesRun(ctx, runRequest)
if err != nil {
cmd.PrintErrf("Error: Failed to refresh the certificates.\n\nThe error was: %v\n", err)
env.Exit(1)
return
}

expiryTimeUNIX := time.Unix(int64(runResponse.ExpirationSeconds), 0)
cmd.Printf("Certificates have been successfully refreshed, and will expire at %v.\n", expiryTimeUNIX)
},
}
cmd.Flags().StringVar(&opts.expiresIn, "expires-in", "", "the time until the certificates expire, e.g., 1h, 2d, 4mo, 5y. Aditionally, any valid time unit for ParseDuration is accepted.")
cmd.Flags().DurationVar(&opts.timeout, "timeout", 90*time.Second, "the max time to wait for the command to execute")
cmd.Flags().StringArrayVar(&opts.extraSANs, "extra-sans", []string{}, "extra SANs to add to the certificates.")

cmd.MarkFlagRequired("expires-in")
return cmd
}
9 changes: 9 additions & 0 deletions src/k8s/pkg/client/k8sd/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ type ConfigClient interface {
SetClusterConfig(context.Context, apiv1.SetClusterConfigRequest) error
}

// ClusterMaintenanceClient implements methods to manage the cluster.
type ClusterMaintenanceClient interface {
// RefreshCertificatesPlan generates a plan to refresh the Kubernetes certificates of the node.
RefreshCertificatesPlan(context.Context, apiv1.RefreshCertificatesPlanRequest) (apiv1.RefreshCertificatesPlanResponse, error)
// RefreshCertificatesRun refreshes the Kubernetes certificates of the node.
RefreshCertificatesRun(context.Context, apiv1.RefreshCertificatesRunRequest) (apiv1.RefreshCertificatesRunResponse, error)
}

// UserClient implements methods to enable accessing the cluster.
type UserClient interface {
// KubeConfig retrieves a kubeconfig file that can be used to access the cluster.
Expand All @@ -51,6 +59,7 @@ type Client interface {
ClusterClient
StatusClient
ConfigClient
ClusterMaintenanceClient
UserClient
ClusterAPIClient
}
15 changes: 15 additions & 0 deletions src/k8s/pkg/client/k8sd/manage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package k8sd

import (
"context"

apiv1 "github.com/canonical/k8s-snap-api/api/v1"
)

func (c *k8sd) RefreshCertificatesPlan(ctx context.Context, request apiv1.RefreshCertificatesPlanRequest) (apiv1.RefreshCertificatesPlanResponse, error) {
return query(ctx, c, "POST", apiv1.RefreshCertificatesPlanRPC, request, &apiv1.RefreshCertificatesPlanResponse{})
}

func (c *k8sd) RefreshCertificatesRun(ctx context.Context, request apiv1.RefreshCertificatesRunRequest) (apiv1.RefreshCertificatesRunResponse, error) {
return query(ctx, c, "POST", apiv1.RefreshCertificatesRunRPC, request, &apiv1.RefreshCertificatesRunResponse{})
}
16 changes: 16 additions & 0 deletions src/k8s/pkg/client/k8sd/mock/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ type Mock struct {
SetClusterConfigCalledWith apiv1.SetClusterConfigRequest
SetClusterConfigErr error

// k8sd.ClusterMaintenanceClient
RefreshCertificatesPlanCalledWith apiv1.RefreshCertificatesPlanRequest
RefreshCertificatesPlanResponse apiv1.RefreshCertificatesPlanResponse
RefreshCertificatesPlanErr error
RefreshCertificatesRunCalledWith apiv1.RefreshCertificatesRunRequest
RefreshCertificatesRunResponse apiv1.RefreshCertificatesRunResponse
RefreshCertificatesRunErr error

// k8sd.UserClient
KubeConfigCalledWith apiv1.KubeConfigRequest
KubeConfigResponse apiv1.KubeConfigResponse
Expand Down Expand Up @@ -68,6 +76,14 @@ func (m *Mock) ClusterStatus(_ context.Context, waitReady bool) (apiv1.ClusterSt
return m.ClusterStatusResponse, m.ClusterStatusErr
}

func (m *Mock) RefreshCertificatesPlan(_ context.Context, request apiv1.RefreshCertificatesPlanRequest) (apiv1.RefreshCertificatesPlanResponse, error) {
return m.RefreshCertificatesPlanResponse, m.RefreshCertificatesPlanErr
}

func (m *Mock) RefreshCertificatesRun(_ context.Context, request apiv1.RefreshCertificatesRunRequest) (apiv1.RefreshCertificatesRunResponse, error) {
return m.RefreshCertificatesRunResponse, m.RefreshCertificatesRunErr
}

func (m *Mock) GetClusterConfig(_ context.Context) (apiv1.GetClusterConfigResponse, error) {
return m.GetClusterConfigResponse, m.GetClusterConfigErr
}
Expand Down
8 changes: 4 additions & 4 deletions src/k8s/pkg/k8sd/api/certificates_refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,10 @@ func refreshCertsRunControlPlane(s state.State, r *http.Request, snap snap.Snap)
return response.InternalError(fmt.Errorf("failed to read kubelet certificate: %w", err))
}

expirationDuration := kubeletCert.NotAfter.Sub(kubeletCert.NotBefore)
expirationTimeUNIX := kubeletCert.NotAfter.Unix()

return response.SyncResponse(true, apiv1.RefreshCertificatesRunResponse{
ExpirationSeconds: int(expirationDuration.Seconds()),
ExpirationSeconds: int(expirationTimeUNIX),
})
}

Expand Down Expand Up @@ -283,9 +283,9 @@ func refreshCertsRunWorker(s state.State, r *http.Request, snap snap.Snap) respo
return response.InternalError(fmt.Errorf("failed to load kubelet certificate: %w", err))
}

expirationDuration := cert.NotAfter.Sub(cert.NotBefore)
expirationTimeUNIX := cert.NotAfter.Unix()
return response.SyncResponse(true, apiv1.RefreshCertificatesRunResponse{
ExpirationSeconds: int(expirationDuration.Seconds()),
ExpirationSeconds: int(expirationTimeUNIX),
})

}
Expand Down

0 comments on commit 545d1fb

Please sign in to comment.