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

Add refresh-certs Command #623

Merged
merged 4 commits into from
Aug 27, 2024
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
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
}
Comment on lines +30 to +34
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, maybe the k8sd service isn't running. If that's the only reason, maybe indicate how to check that.


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 {
bschimke95 marked this conversation as resolved.
Show resolved Hide resolved
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
}
addyess marked this conversation as resolved.
Show resolved Hide resolved

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
Loading