Skip to content

✨clusterctl: add upgrade plan cmd #2178

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

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
135 changes: 133 additions & 2 deletions cmd/clusterctl/cmd/upgrade.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2019 The Kubernetes Authors.
Copyright 2020 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -16,4 +16,135 @@ limitations under the License.

package cmd

// TODO: upgrade command
import (
"fmt"
"os"
"sort"
"text/tabwriter"

"github.com/spf13/cobra"
"sigs.k8s.io/cluster-api/cmd/clusterctl/pkg/client"
)

var upgradeCmd = &cobra.Command{
Use: "upgrade",
Short: "Upgrades Cluster API providers in a management cluster",
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}

type upgradePlanOptions struct {
kubeconfig string
}

var up = &upgradePlanOptions{}

var upgradePlanCmd = &cobra.Command{
Use: "plan",
Short: "Provide a list of recommended target versions for upgrading Cluster API providers in a management cluster",
Long: LongDesc(`
The upgrade plan command provides a list of recommended target versions for upgrading Cluster API providers in a management cluster.

The providers are grouped into management groups, each one defining a set of providers that should be supporting the same cluster API version
in order to guarantee the proper functioning of the management cluster.

Then, for each provider in a management group, the following upgrade options are provided:
- The latest patch release for the current Cluster API version.
- The latest patch release for the next Cluster API version, if available.`),

Example: Examples(`
# Gets the recommended target versions for upgrading Cluster API providers.
clusterctl upgrade plan`),

RunE: func(cmd *cobra.Command, args []string) error {
return runUpgradePlan()
},
}

func init() {
upgradePlanCmd.Flags().StringVarP(&up.kubeconfig, "kubeconfig", "", "", "Path to the kubeconfig file to use for accessing the management cluster. If empty, default rules for kubeconfig discovery will be used")

upgradeCmd.AddCommand(upgradePlanCmd)

RootCmd.AddCommand(upgradeCmd)
}

func runUpgradePlan() error {
c, err := client.New(cfgFile)
if err != nil {
return err
}

//TODO: switch to klog as soon as https://github.com/kubernetes-sigs/cluster-api/pull/2150 merge
Copy link
Member

Choose a reason for hiding this comment

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

Seems we can switch now?

Copy link
Member Author

Choose a reason for hiding this comment

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

Unfortunately no, because we switched to the global logger approach and this is not yet merged. Waiting for #2191

Copy link
Contributor

Choose a reason for hiding this comment

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

Since 2191 is blocked on this PR, we can create an issue to track this work or I'm assuming @fabriziopandini will be submitting a PR to refactor the upgrade cmd to use the global logger.

Copy link
Contributor

Choose a reason for hiding this comment

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

He's going to update 2191 after this goes in

fmt.Println("Checking new release availability...")
upgradePlans, err := c.PlanUpgrade(client.PlanUpgradeOptions{
Kubeconfig: up.kubeconfig,
})
if err != nil {
return err
}

// ensure upgrade plans are sorted consistently (by CoreProvider.Namespace, ClusterAPIVersion).
sortUpgradePlans(upgradePlans)

if len(upgradePlans) == 0 {
fmt.Println("There are no management groups in the cluster. Please use clusterctl init to initialize a Cluster API management cluster.")
return nil
}

for _, plan := range upgradePlans {
// ensure provider are sorted consistently (by Type, Name, Namespace).
sortUpgradeItems(plan)

upgradeAvailable := false

fmt.Println("")
fmt.Printf("Management group: %s/%s, latest release available for the %s Cluster API version:\n", plan.CoreProvider.Namespace, plan.CoreProvider.Name, plan.ClusterAPIVersion)
fmt.Println("")
w := tabwriter.NewWriter(os.Stdout, 10, 4, 3, ' ', 0)
fmt.Fprintln(w, "NAME\tNAMESPACE\tTYPE\tCURRENT VERSION\tNEXT VERSION")
for _, upgradeItem := range plan.Providers {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", upgradeItem.Provider.Name, upgradeItem.Provider.Namespace, upgradeItem.Provider.Type, upgradeItem.Provider.Version, prettifyTargetVersion(upgradeItem.NextVersion))
if upgradeItem.NextVersion != "" {
upgradeAvailable = true
}
}
w.Flush()
fmt.Println("")

if upgradeAvailable {
fmt.Println("You can now apply the upgrade by executing the following command:")
fmt.Println("")
fmt.Println(fmt.Sprintf(" upgrade apply --management-group %s/%s --cluster-api-version %s", plan.CoreProvider.Namespace, plan.CoreProvider.Name, plan.ClusterAPIVersion))
} else {
fmt.Println("You are already up to date!")
}
fmt.Println("")

}

return nil
}

func sortUpgradeItems(plan client.UpgradePlan) {
sort.Slice(plan.Providers, func(i, j int) bool {
return plan.Providers[i].Provider.Type < plan.Providers[j].Provider.Type ||
(plan.Providers[i].Provider.Type == plan.Providers[j].Provider.Type && plan.Providers[i].Provider.Name < plan.Providers[j].Provider.Name) ||
(plan.Providers[i].Provider.Type == plan.Providers[j].Provider.Type && plan.Providers[i].Provider.Name == plan.Providers[j].Provider.Name && plan.Providers[i].Provider.Namespace < plan.Providers[j].Provider.Namespace)
})
}

func sortUpgradePlans(upgradePlans []client.UpgradePlan) {
sort.Slice(upgradePlans, func(i, j int) bool {
return upgradePlans[i].CoreProvider.Namespace < upgradePlans[j].CoreProvider.Namespace ||
(upgradePlans[i].CoreProvider.Namespace == upgradePlans[j].CoreProvider.Namespace && upgradePlans[i].ClusterAPIVersion < upgradePlans[j].ClusterAPIVersion)
})
}

func prettifyTargetVersion(version string) string {
if version == "" {
return "Already up to date"
}
return version
}
4 changes: 4 additions & 0 deletions cmd/clusterctl/pkg/client/alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package client

import (
"sigs.k8s.io/cluster-api/cmd/clusterctl/pkg/client/cluster"
"sigs.k8s.io/cluster-api/cmd/clusterctl/pkg/client/config"
"sigs.k8s.io/cluster-api/cmd/clusterctl/pkg/client/repository"
)
Expand All @@ -32,3 +33,6 @@ type Components repository.Components

// Template wraps a YAML file that defines the cluster objects (Cluster, Machines etc.).
type Template repository.Template

// Template wraps a YAML file that defines the cluster objects (Cluster, Machines etc.).
type UpgradePlan cluster.UpgradePlan
19 changes: 16 additions & 3 deletions cmd/clusterctl/pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ type MoveOptions struct {
Namespace string
}

// PlanUpgradeOptions carries the options supported by upgrade plan.
type PlanUpgradeOptions struct {
// Kubeconfig file to use for accessing the management cluster. If empty, default rules for kubeconfig discovery will be used.
Kubeconfig string
}

// Client is exposes the clusterctl high-level client library.
type Client interface {
// GetProvidersConfig returns the list of providers configured for this instance of clusterctl.
Expand All @@ -136,6 +142,13 @@ type Client interface {

// Move moves all the Cluster API objects existing in a namespace (or from all the namespaces if empty) to a target management cluster.
Move(options MoveOptions) error

// PlanUpgrade returns a set of suggested Upgrade plans for the cluster, and more specifically:
// - Each management group gets separated upgrade plans.
// - For each management group, an upgrade plan is generated for each ClusterAPIVersion available, e.g.
// - Upgrade to the latest version in the the v1alpha2 series: ....
// - Upgrade to the latest version in the the v1alpha3 series: ....
PlanUpgrade(options PlanUpgradeOptions) ([]UpgradePlan, error)
}

// clusterctlClient implements Client.
Expand Down Expand Up @@ -205,16 +218,16 @@ func newClusterctlClient(path string, options ...Option) (*clusterctlClient, err

// if there is an injected ClusterFactory, use it, otherwise use a default one.
if client.clusterClientFactory == nil {
client.clusterClientFactory = defaultClusterFactory()
client.clusterClientFactory = defaultClusterFactory(client.configClient)
}

return client, nil
}

// defaultClusterFactory is a ClusterClientFactory func the uses the default client provided by the cluster low level library.
func defaultClusterFactory() func(kubeconfig string) (cluster.Client, error) {
func defaultClusterFactory(configClient config.Client) func(kubeconfig string) (cluster.Client, error) {
return func(kubeconfig string) (cluster.Client, error) {
return cluster.New(kubeconfig), nil
return cluster.New(kubeconfig, configClient), nil
}
}

Expand Down
12 changes: 10 additions & 2 deletions cmd/clusterctl/pkg/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ func (f fakeClient) Move(options MoveOptions) error {
return f.internalClient.Move(options)
}

func (f fakeClient) PlanUpgrade(options PlanUpgradeOptions) ([]UpgradePlan, error) {
return f.internalClient.PlanUpgrade(options)
}

// newFakeClient returns a clusterctl client that allows to execute tests on a set of fake config, fake repositories and fake clusters.
// you can use WithCluster and WithRepository to prepare for the test case.
func newFakeClient(configClient config.Client) *fakeClient {
Expand Down Expand Up @@ -140,13 +144,13 @@ func (f *fakeClient) WithRepository(repositoryClient repository.Client) *fakeCli
// internally uses a FakeProxy (based on the controller-runtime FakeClient).
// You can use WithObjs to pre-load a set of runtime objects in the cluster.
func newFakeCluster(kubeconfig string) *fakeClusterClient {
configClient := newFakeConfig()
fakeProxy := test.NewFakeProxy()

pollImmediateWaiter := func(interval, timeout time.Duration, condition wait.ConditionFunc) error {
return nil
}

client := cluster.New("", cluster.InjectProxy(fakeProxy), cluster.InjectPollImmediateWaiter(pollImmediateWaiter))
client := cluster.New("", configClient, cluster.InjectProxy(fakeProxy), cluster.InjectPollImmediateWaiter(pollImmediateWaiter))

return &fakeClusterClient{
kubeconfig: kubeconfig,
Expand Down Expand Up @@ -201,6 +205,10 @@ func (f *fakeClusterClient) ObjectMover() cluster.ObjectMover {
return f.internalclient.ObjectMover()
}

func (f *fakeClusterClient) ProviderUpgrader() cluster.ProviderUpgrader {
return f.internalclient.ProviderUpgrader()
}

func (f *fakeClusterClient) WithObjs(objs ...runtime.Object) *fakeClusterClient {
f.fakeProxy.WithObjs(objs...)
return f
Expand Down
17 changes: 13 additions & 4 deletions cmd/clusterctl/pkg/client/cluster/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ type Client interface {
// ObjectMover returns an ObjectMover that implements support for moving Cluster API objects (e.g. clusters, AWS clusters, machines, etc.).
// from one management cluster to another management cluster.
ObjectMover() ObjectMover

// ProviderUpgrader returns a ProviderUpgrader that supports upgrading Cluster API providers.
ProviderUpgrader() ProviderUpgrader
}

// PollImmediateWaiter tries a condition func until it returns true, an error, or the timeout is reached.
Expand All @@ -73,6 +76,7 @@ type PollImmediateWaiter func(interval, timeout time.Duration, condition wait.Co
type clusterClient struct {
kubeconfig string
proxy Proxy
configClient config.Client
repositoryClientFactory RepositoryClientFactory
pollImmediateWaiter PollImmediateWaiter
}
Expand Down Expand Up @@ -112,6 +116,10 @@ func (c *clusterClient) ObjectMover() ObjectMover {
return newObjectMover(c.proxy, log)
}

func (c *clusterClient) ProviderUpgrader() ProviderUpgrader {
return newProviderUpgrader(c.configClient, c.repositoryClientFactory, c.ProviderInventory())
}

// Option is a configuration option supplied to New
type Option func(*clusterClient)

Expand All @@ -138,13 +146,14 @@ func InjectPollImmediateWaiter(pollImmediateWaiter PollImmediateWaiter) Option {
}

// New returns a cluster.Client.
func New(kubeconfig string, options ...Option) Client {
return newClusterClient(kubeconfig, options...)
func New(kubeconfig string, configClient config.Client, options ...Option) Client {
return newClusterClient(kubeconfig, configClient, options...)
}

func newClusterClient(kubeconfig string, options ...Option) *clusterClient {
func newClusterClient(kubeconfig string, configClient config.Client, options ...Option) *clusterClient {
client := &clusterClient{
kubeconfig: kubeconfig,
configClient: configClient,
kubeconfig: kubeconfig,
}
for _, o := range options {
o(client)
Expand Down
10 changes: 5 additions & 5 deletions cmd/clusterctl/pkg/client/cluster/components_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import (

func Test_providerComponents_Delete(t *testing.T) {
labels := map[string]string{
clusterv1.ProviderLabelName: "aws",
clusterv1.ProviderLabelName: "infra",
}

crd := unstructured.Unstructured{}
Expand Down Expand Up @@ -107,7 +107,7 @@ func Test_providerComponents_Delete(t *testing.T) {
{
name: "Delete provider while preserving Namespace and CRDs",
args: args{
provider: clusterctlv1.Provider{ObjectMeta: metav1.ObjectMeta{Name: "aws", Namespace: "ns1"}},
provider: clusterctlv1.Provider{ObjectMeta: metav1.ObjectMeta{Name: "infra", Namespace: "ns1"}},
forceDeleteNamespace: false,
forceDeleteCRD: false,
},
Expand All @@ -123,7 +123,7 @@ func Test_providerComponents_Delete(t *testing.T) {
{
name: "Delete provider and provider namespace, while preserving CRDs",
args: args{
provider: clusterctlv1.Provider{ObjectMeta: metav1.ObjectMeta{Name: "aws", Namespace: "ns1"}},
provider: clusterctlv1.Provider{ObjectMeta: metav1.ObjectMeta{Name: "infra", Namespace: "ns1"}},
forceDeleteNamespace: true,
forceDeleteCRD: false,
},
Expand All @@ -140,7 +140,7 @@ func Test_providerComponents_Delete(t *testing.T) {
{
name: "Delete provider and provider CRDs, while preserving the provider namespace",
args: args{
provider: clusterctlv1.Provider{ObjectMeta: metav1.ObjectMeta{Name: "aws", Namespace: "ns1"}},
provider: clusterctlv1.Provider{ObjectMeta: metav1.ObjectMeta{Name: "infra", Namespace: "ns1"}},
forceDeleteNamespace: false,
forceDeleteCRD: true,
},
Expand All @@ -157,7 +157,7 @@ func Test_providerComponents_Delete(t *testing.T) {
{
name: "Delete provider, provider namespace and provider CRDs",
args: args{
provider: clusterctlv1.Provider{ObjectMeta: metav1.ObjectMeta{Name: "aws", Namespace: "ns1"}},
provider: clusterctlv1.Provider{ObjectMeta: metav1.ObjectMeta{Name: "infra", Namespace: "ns1"}},
forceDeleteNamespace: true,
forceDeleteCRD: true,
},
Expand Down
Loading