From b769eab5ab39fce0e2a21ada40dc67f32d63e902 Mon Sep 17 00:00:00 2001 From: Arvinderpal Wander Date: Sun, 20 Dec 2020 16:07:39 -0800 Subject: [PATCH] Adds command and client for the `clusterctl alpha rollout pause/resume` for MachineDeployments. --- cmd/clusterctl/client/alpha/rollout.go | 6 + cmd/clusterctl/client/alpha/rollout_pauser.go | 53 ++++++ .../client/alpha/rollout_pauser_test.go | 115 +++++++++++++ .../client/alpha/rollout_restarter.go | 2 - .../client/alpha/rollout_resumer.go | 54 ++++++ .../client/alpha/rollout_resumer_test.go | 118 +++++++++++++ cmd/clusterctl/client/client.go | 6 +- cmd/clusterctl/client/client_test.go | 10 +- cmd/clusterctl/client/rollout.go | 69 ++++++-- cmd/clusterctl/client/rollout_test.go | 162 ++++++++++++------ cmd/clusterctl/cmd/rollout.go | 10 +- cmd/clusterctl/cmd/rollout/pause.go | 85 +++++++++ cmd/clusterctl/cmd/rollout/restart.go | 18 +- cmd/clusterctl/cmd/rollout/resume.go | 84 +++++++++ 14 files changed, 711 insertions(+), 81 deletions(-) create mode 100644 cmd/clusterctl/client/alpha/rollout_pauser.go create mode 100644 cmd/clusterctl/client/alpha/rollout_pauser_test.go create mode 100644 cmd/clusterctl/client/alpha/rollout_resumer.go create mode 100644 cmd/clusterctl/client/alpha/rollout_resumer_test.go create mode 100644 cmd/clusterctl/cmd/rollout/pause.go create mode 100644 cmd/clusterctl/cmd/rollout/resume.go diff --git a/cmd/clusterctl/client/alpha/rollout.go b/cmd/clusterctl/client/alpha/rollout.go index 4bedd26f2ca0..4f0640f10ef3 100644 --- a/cmd/clusterctl/client/alpha/rollout.go +++ b/cmd/clusterctl/client/alpha/rollout.go @@ -21,9 +21,15 @@ import ( "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/util" ) +const machineDeployment = "machinedeployment" + +var validResourceTypes = []string{machineDeployment} + // Rollout defines the behavior of a rollout implementation. type Rollout interface { ObjectRestarter(cluster.Proxy, util.ResourceTuple, string) error + ObjectPauser(cluster.Proxy, util.ResourceTuple, string) error + ObjectResumer(cluster.Proxy, util.ResourceTuple, string) error } var _ Rollout = &rollout{} diff --git a/cmd/clusterctl/client/alpha/rollout_pauser.go b/cmd/clusterctl/client/alpha/rollout_pauser.go new file mode 100644 index 000000000000..41e78a03f4e1 --- /dev/null +++ b/cmd/clusterctl/client/alpha/rollout_pauser.go @@ -0,0 +1,53 @@ +/* +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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package alpha + +import ( + "fmt" + + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" + "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/util" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// ObjectPauser will issue a pause on the specified cluster-api resource. +func (r *rollout) ObjectPauser(proxy cluster.Proxy, tuple util.ResourceTuple, namespace string) error { + switch tuple.Resource { + case machineDeployment: + deployment, err := getMachineDeployment(proxy, tuple.Name, namespace) + if err != nil || deployment == nil { + return errors.Wrapf(err, "failed to fetch %v/%v", tuple.Resource, tuple.Name) + } + if deployment.Spec.Paused { + return errors.Errorf("MachineDeploymet is already paused: %v/%v\n", tuple.Resource, tuple.Name) + } + if err := pauseMachineDeployment(proxy, tuple.Name, namespace); err != nil { + return err + } + default: + return errors.Errorf("Invalid resource type %q, valid values are %v", tuple.Resource, validResourceTypes) + } + return nil +} + +// pauseMachineDeployment sets Paused to true in the MachineDeployment's spec. +func pauseMachineDeployment(proxy cluster.Proxy, name, namespace string) error { + patch := client.RawPatch(types.MergePatchType, []byte(fmt.Sprintf("{\"spec\":{\"paused\":%t}}", true))) + return patchMachineDeployemt(proxy, name, namespace, patch) +} diff --git a/cmd/clusterctl/client/alpha/rollout_pauser_test.go b/cmd/clusterctl/client/alpha/rollout_pauser_test.go new file mode 100644 index 000000000000..6687d4929a14 --- /dev/null +++ b/cmd/clusterctl/client/alpha/rollout_pauser_test.go @@ -0,0 +1,115 @@ +/* +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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package alpha + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4" + "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test" + "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/util" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func Test_ObjectPauser(t *testing.T) { + type fields struct { + objs []client.Object + tuple util.ResourceTuple + namespace string + } + tests := []struct { + name string + fields fields + wantErr bool + wantPaused bool + }{ + { + name: "machinedeployment should be paused", + fields: fields{ + objs: []client.Object{ + &clusterv1.MachineDeployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "MachineDeployment", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "md-1", + }, + }, + }, + tuple: util.ResourceTuple{ + Resource: "machinedeployment", + Name: "md-1", + }, + namespace: "default", + }, + wantErr: false, + wantPaused: true, + }, + { + name: "re-pausing an already paused machinedeployment should return error", + fields: fields{ + objs: []client.Object{ + &clusterv1.MachineDeployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "MachineDeployment", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "md-1", + }, + Spec: clusterv1.MachineDeploymentSpec{ + Paused: true, + }, + }, + }, + tuple: util.ResourceTuple{ + Resource: "machinedeployment", + Name: "md-1", + }, + namespace: "default", + }, + wantErr: true, + wantPaused: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + r := newRolloutClient() + proxy := test.NewFakeProxy().WithObjs(tt.fields.objs...) + err := r.ObjectPauser(proxy, tt.fields.tuple, tt.fields.namespace) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + return + } + g.Expect(err).ToNot(HaveOccurred()) + for _, obj := range tt.fields.objs { + cl, err := proxy.NewClient() + g.Expect(err).ToNot(HaveOccurred()) + key := client.ObjectKeyFromObject(obj) + md := &clusterv1.MachineDeployment{} + err = cl.Get(context.TODO(), key, md) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(md.Spec.Paused).To(Equal(tt.wantPaused)) + } + }) + } +} diff --git a/cmd/clusterctl/client/alpha/rollout_restarter.go b/cmd/clusterctl/client/alpha/rollout_restarter.go index 572085cf0def..72216405e704 100644 --- a/cmd/clusterctl/client/alpha/rollout_restarter.go +++ b/cmd/clusterctl/client/alpha/rollout_restarter.go @@ -29,8 +29,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -var validResourceTypes = []string{"machinedeployment"} - // ObjectRestarter will issue a restart on the specified cluster-api resource. func (r *rollout) ObjectRestarter(proxy cluster.Proxy, tuple util.ResourceTuple, namespace string) error { switch tuple.Resource { diff --git a/cmd/clusterctl/client/alpha/rollout_resumer.go b/cmd/clusterctl/client/alpha/rollout_resumer.go new file mode 100644 index 000000000000..dc8f5d5bf6d4 --- /dev/null +++ b/cmd/clusterctl/client/alpha/rollout_resumer.go @@ -0,0 +1,54 @@ +/* +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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package alpha + +import ( + "fmt" + + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" + "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/util" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// ObjectResumer will issue a resume on the specified cluster-api resource. +func (r *rollout) ObjectResumer(proxy cluster.Proxy, tuple util.ResourceTuple, namespace string) error { + switch tuple.Resource { + case "machinedeployment": + deployment, err := getMachineDeployment(proxy, tuple.Name, namespace) + if err != nil || deployment == nil { + return errors.Wrapf(err, "failed to fetch %v/%v", tuple.Resource, tuple.Name) + } + if !deployment.Spec.Paused { + return errors.Errorf("MachineDeployment is not currently paused: %v/%v\n", tuple.Resource, tuple.Name) + } + if err := resumeMachineDeployment(proxy, tuple.Name, namespace); err != nil { + return err + } + default: + return errors.Errorf("Invalid resource type %q, valid values are %v", tuple.Resource, validResourceTypes) + } + return nil +} + +// resumeMachineDeployment sets Paused to true in the MachineDeployment's spec. +func resumeMachineDeployment(proxy cluster.Proxy, name, namespace string) error { + patch := client.RawPatch(types.MergePatchType, []byte(fmt.Sprintf("{\"spec\":{\"paused\":%t}}", false))) + + return patchMachineDeployemt(proxy, name, namespace, patch) +} diff --git a/cmd/clusterctl/client/alpha/rollout_resumer_test.go b/cmd/clusterctl/client/alpha/rollout_resumer_test.go new file mode 100644 index 000000000000..c6a03aac0c47 --- /dev/null +++ b/cmd/clusterctl/client/alpha/rollout_resumer_test.go @@ -0,0 +1,118 @@ +/* +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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package alpha + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4" + "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test" + "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/util" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func Test_ObjectResumer(t *testing.T) { + type fields struct { + objs []client.Object + tuple util.ResourceTuple + namespace string + } + tests := []struct { + name string + fields fields + wantErr bool + wantPaused bool + }{ + { + name: "paused machinedeployment should be unpaused", + fields: fields{ + objs: []client.Object{ + &clusterv1.MachineDeployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "MachineDeployment", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "md-1", + }, + Spec: clusterv1.MachineDeploymentSpec{ + Paused: true, + }, + }, + }, + tuple: util.ResourceTuple{ + Resource: "machinedeployment", + Name: "md-1", + }, + namespace: "default", + }, + wantErr: false, + wantPaused: false, + }, + { + name: "unpausing an already unpaused machinedeployment should return error", + fields: fields{ + objs: []client.Object{ + &clusterv1.MachineDeployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "MachineDeployment", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "md-1", + }, + Spec: clusterv1.MachineDeploymentSpec{ + Paused: false, + }, + }, + }, + tuple: util.ResourceTuple{ + Resource: "machinedeployment", + Name: "md-1", + }, + namespace: "default", + }, + wantErr: true, + wantPaused: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + r := newRolloutClient() + proxy := test.NewFakeProxy().WithObjs(tt.fields.objs...) + err := r.ObjectResumer(proxy, tt.fields.tuple, tt.fields.namespace) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + return + } + g.Expect(err).ToNot(HaveOccurred()) + for _, obj := range tt.fields.objs { + cl, err := proxy.NewClient() + g.Expect(err).ToNot(HaveOccurred()) + key := client.ObjectKeyFromObject(obj) + md := &clusterv1.MachineDeployment{} + err = cl.Get(context.TODO(), key, md) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(md.Spec.Paused).To(Equal(tt.wantPaused)) + } + }) + } +} diff --git a/cmd/clusterctl/client/client.go b/cmd/clusterctl/client/client.go index 13ca7f8847c1..8e7d5def5c63 100644 --- a/cmd/clusterctl/client/client.go +++ b/cmd/clusterctl/client/client.go @@ -78,7 +78,11 @@ type Client interface { // AlphaClient exposes the alpha features in clusterctl high-level client library. type AlphaClient interface { // RolloutRestart provides rollout restart of cluster-api resources - RolloutRestart(options RolloutRestartOptions) error + RolloutRestart(options RolloutOptions) error + // RolloutPause provides rollout pause of cluster-api resources + RolloutPause(options RolloutOptions) error + // RolloutResume provides rollout resume of paused cluster-api resources + RolloutResume(options RolloutOptions) error } // YamlPrinter exposes methods that prints the processed template and diff --git a/cmd/clusterctl/client/client_test.go b/cmd/clusterctl/client/client_test.go index f00ec8e21ecd..0ab8ae024102 100644 --- a/cmd/clusterctl/client/client_test.go +++ b/cmd/clusterctl/client/client_test.go @@ -119,7 +119,7 @@ func (f fakeClient) ProcessYAML(options ProcessYAMLOptions) (YamlPrinter, error) return f.internalClient.ProcessYAML(options) } -func (f fakeClient) RolloutRestart(options RolloutRestartOptions) error { +func (f fakeClient) RolloutRestart(options RolloutOptions) error { return f.internalClient.RolloutRestart(options) } @@ -127,6 +127,14 @@ func (f fakeClient) DescribeCluster(options DescribeClusterOptions) (*tree.Objec return f.internalClient.DescribeCluster(options) } +func (f fakeClient) RolloutPause(options RolloutOptions) error { + return f.internalClient.RolloutPause(options) +} + +func (f fakeClient) RolloutResume(options RolloutOptions) error { + return f.internalClient.RolloutResume(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 { diff --git a/cmd/clusterctl/client/rollout.go b/cmd/clusterctl/client/rollout.go index ea48ba911e32..7bd5c66e4432 100644 --- a/cmd/clusterctl/client/rollout.go +++ b/cmd/clusterctl/client/rollout.go @@ -20,16 +20,17 @@ import ( "fmt" "strings" + "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/util" ) -// RolloutRestartOptions carries the options supported by rollout restart. -type RolloutRestartOptions struct { +// RolloutOptions carries the base set of options supported by rollout command. +type RolloutOptions struct { // Kubeconfig defines the kubeconfig to use for accessing the management cluster. If empty, // default rules for kubeconfig discovery will be used. Kubeconfig Kubeconfig - // Resources to be rollout restarted. + // Resources for the rollout command Resources []string // Namespace where the resource(s) live. If unspecified, the namespace name will be inferred @@ -37,36 +38,76 @@ type RolloutRestartOptions struct { Namespace string } -func (c *clusterctlClient) RolloutRestart(options RolloutRestartOptions) error { +func (c *clusterctlClient) RolloutRestart(options RolloutOptions) error { clusterClient, err := c.clusterClientFactory(ClusterClientFactoryInput{Kubeconfig: options.Kubeconfig}) if err != nil { return err } + tuples, err := getResourceTuples(clusterClient, options) + if err != nil { + return err + } + for _, t := range tuples { + if err := c.alphaClient.Rollout().ObjectRestarter(clusterClient.Proxy(), t, options.Namespace); err != nil { + return err + } + } + return nil +} + +func (c *clusterctlClient) RolloutPause(options RolloutOptions) error { + clusterClient, err := c.clusterClientFactory(ClusterClientFactoryInput{Kubeconfig: options.Kubeconfig}) + if err != nil { + return err + } + tuples, err := getResourceTuples(clusterClient, options) + if err != nil { + return err + } + for _, t := range tuples { + if err := c.alphaClient.Rollout().ObjectPauser(clusterClient.Proxy(), t, options.Namespace); err != nil { + return err + } + } + return nil +} +func (c *clusterctlClient) RolloutResume(options RolloutOptions) error { + clusterClient, err := c.clusterClientFactory(ClusterClientFactoryInput{Kubeconfig: options.Kubeconfig}) + if err != nil { + return err + } + tuples, err := getResourceTuples(clusterClient, options) + if err != nil { + return err + } + for _, t := range tuples { + if err := c.alphaClient.Rollout().ObjectResumer(clusterClient.Proxy(), t, options.Namespace); err != nil { + return err + } + } + return nil +} + +func getResourceTuples(clusterClient cluster.Client, options RolloutOptions) ([]util.ResourceTuple, error) { // If the option specifying the Namespace is empty, try to detect it. if options.Namespace == "" { currentNamespace, err := clusterClient.Proxy().CurrentNamespace() if err != nil { - return err + return []util.ResourceTuple{}, err } options.Namespace = currentNamespace } if len(options.Resources) == 0 { - return fmt.Errorf("required resource not specified") + return []util.ResourceTuple{}, fmt.Errorf("required resource not specified") } normalized := normalizeResources(options.Resources) tuples, err := util.ResourceTypeAndNameArgs(normalized...) if err != nil { - return err + return []util.ResourceTuple{}, err } - - for _, t := range tuples { - if err := c.alphaClient.Rollout().ObjectRestarter(clusterClient.Proxy(), t, options.Namespace); err != nil { - return err - } - } - return nil + return tuples, nil } func normalizeResources(input []string) []string { diff --git a/cmd/clusterctl/client/rollout_test.go b/cmd/clusterctl/client/rollout_test.go index 7f7b5269939f..f5ee312cbcb0 100644 --- a/cmd/clusterctl/client/rollout_test.go +++ b/cmd/clusterctl/client/rollout_test.go @@ -27,103 +27,78 @@ import ( "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" ) -func Test_clusterctlClient_RolloutRestart(t *testing.T) { - type fields struct { - client *fakeClient - } - type args struct { - options RolloutRestartOptions - } - tests := []struct { - name string - fields fields - args args - wantErr bool - }{ - { - name: "do not return error if machinedeployment found", - fields: fields{ - client: fakeClientForRollout(), - }, - args: args{ - options: RolloutRestartOptions{ - Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, - Resources: []string{"machinedeployment/md-1"}, - Namespace: "default", - }, - }, - wantErr: false, - }, +type rolloutTest struct { + name string + fields fields + args args + wantErr bool +} +type fields struct { + client *fakeClient +} +type args struct { + options RolloutOptions +} + +// genericTestCases are test cases that can be passed to any of the rollout subcommands. +func genericTestCases() []rolloutTest { + return []rolloutTest{ { - name: "do not return error if all machinedeployments found", + name: "return an error is machinedeployment not found", fields: fields{ client: fakeClientForRollout(), }, args: args{ - options: RolloutRestartOptions{ + options: RolloutOptions{ Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, - Resources: []string{"machinedeployment/md-1", "machinedeployment/md-2"}, + Resources: []string{"machinedeployment/foo"}, Namespace: "default", }, }, - wantErr: false, + wantErr: true, }, { - name: "return an error is machinedeployment not found", + name: "return error if one of the machinedeployments is not found", fields: fields{ client: fakeClientForRollout(), }, args: args{ - options: RolloutRestartOptions{ + options: RolloutOptions{ Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, - Resources: []string{"machinedeployment/foo"}, + Resources: []string{"machinedeployment/md-1", "machinedeployment/md-does-not-exist"}, Namespace: "default", }, }, wantErr: true, }, { - name: "return error if one of the machinedeployments is not found", + name: "return error if unknown resource specified", fields: fields{ client: fakeClientForRollout(), }, args: args{ - options: RolloutRestartOptions{ + options: RolloutOptions{ Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, - Resources: []string{"machinedeployment/md-1", "machinedeployment/md-does-not-exist"}, + Resources: []string{"foo/bar"}, Namespace: "default", }, }, wantErr: true, }, { - name: "return error if unknown resource specified", + name: "return error if no resource specified", fields: fields{ client: fakeClientForRollout(), }, args: args{ - options: RolloutRestartOptions{ + options: RolloutOptions{ Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, - Resources: []string{"foo/bar"}, Namespace: "default", }, }, wantErr: true, }, } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - - err := tt.fields.client.RolloutRestart(tt.args.options) - if tt.wantErr { - g.Expect(err).To(HaveOccurred()) - return - } - g.Expect(err).NotTo(HaveOccurred()) - }) - } } func fakeClientForRollout() *fakeClient { @@ -164,3 +139,84 @@ func fakeClientForRollout() *fakeClient { return client } + +func Test_clusterctlClient_RolloutRestart(t *testing.T) { + tests := genericTestCases() + additionalTests := []rolloutTest{ + { + name: "do not return error if machinedeployment found", + fields: fields{ + client: fakeClientForRollout(), + }, + args: args{ + options: RolloutOptions{ + Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, + Resources: []string{"machinedeployment/md-1"}, + Namespace: "default", + }, + }, + wantErr: false, + }, + { + name: "do not return error if all machinedeployments found", + fields: fields{ + client: fakeClientForRollout(), + }, + args: args{ + options: RolloutOptions{ + Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, + Resources: []string{"machinedeployment/md-1", "machinedeployment/md-2"}, + Namespace: "default", + }, + }, + wantErr: false, + }, + } + + tests = append(tests, additionalTests...) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + err := tt.fields.client.RolloutRestart(tt.args.options) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + return + } + g.Expect(err).NotTo(HaveOccurred()) + }) + } +} + +func Test_clusterctlClient_RolloutPause(t *testing.T) { + tests := genericTestCases() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + err := tt.fields.client.RolloutPause(tt.args.options) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + return + } + g.Expect(err).NotTo(HaveOccurred()) + }) + } +} + +func Test_clusterctlClient_RolloutResume(t *testing.T) { + tests := genericTestCases() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + err := tt.fields.client.RolloutResume(tt.args.options) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + return + } + g.Expect(err).NotTo(HaveOccurred()) + }) + } +} diff --git a/cmd/clusterctl/cmd/rollout.go b/cmd/clusterctl/cmd/rollout.go index a6a3d79d86a5..678f0907fa2d 100644 --- a/cmd/clusterctl/cmd/rollout.go +++ b/cmd/clusterctl/cmd/rollout.go @@ -31,7 +31,13 @@ var ( rolloutExample = Examples(` # Force an immediate rollout of machinedeployment - clusterctl alpha rollout restart machinedeployment/my-md-0`) + clusterctl alpha rollout restart machinedeployment/my-md-0 + + # Mark the machinedeployment as paused + clusterctl alpha rollout pause machinedeployment/my-md-0 + + # Resume an already paused deployment + clusterctl alpha rollout resume machinedeployment/my-md-0`) rolloutCmd = &cobra.Command{ Use: "rollout SUBCOMMAND", @@ -44,4 +50,6 @@ var ( func init() { // subcommands rolloutCmd.AddCommand(rollout.NewCmdRolloutRestart(cfgFile)) + rolloutCmd.AddCommand(rollout.NewCmdRolloutPause(cfgFile)) + rolloutCmd.AddCommand(rollout.NewCmdRolloutResume(cfgFile)) } diff --git a/cmd/clusterctl/cmd/rollout/pause.go b/cmd/clusterctl/cmd/rollout/pause.go new file mode 100644 index 000000000000..33517114b48c --- /dev/null +++ b/cmd/clusterctl/cmd/rollout/pause.go @@ -0,0 +1,85 @@ +/* +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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rollout + +import ( + "github.com/spf13/cobra" + "k8s.io/kubectl/pkg/util/templates" + "sigs.k8s.io/cluster-api/cmd/clusterctl/client" +) + +// pauseOptions is the start of the data required to perform the operation. +type pauseOptions struct { + kubeconfig string + kubeconfigContext string + resources []string + namespace string +} + +var pauseOpt = &pauseOptions{} + +var ( + pauseLong = templates.LongDesc(` + Mark the provided cluster-api resource as paused. + + Paused resources will not be reconciled by a controller. Use "clusterctl alpha rollout resume" to resume a paused resource. Currently only MachineDeployments support being paused.`) + + pauseExample = templates.Examples(` + # Mark the machinedeployment as paused. + clusterctl alpha rollout pause machinedeployment/my-md-0 +`) +) + +// NewCmdRolloutPause returns a Command instance for 'rollout pause' sub command +func NewCmdRolloutPause(cfgFile string) *cobra.Command { + + cmd := &cobra.Command{ + Use: "pause RESOURCE", + DisableFlagsInUseLine: true, + Short: "Pause a cluster-api resource", + Long: pauseLong, + Example: pauseExample, + RunE: func(cmd *cobra.Command, args []string) error { + return runPause(cfgFile, args) + }, + } + cmd.Flags().StringVar(&pauseOpt.kubeconfig, "kubeconfig", "", + "Path to the kubeconfig file to use for accessing the management cluster. If unspecified, default discovery rules apply.") + cmd.Flags().StringVar(&pauseOpt.kubeconfigContext, "kubeconfig-context", "", + "Context to be used within the kubeconfig file. If empty, current context will be used.") + cmd.Flags().StringVar(&pauseOpt.namespace, "namespace", "", "Namespace where the resource(s) reside. If unspecified, the defult namespace will be used.") + + return cmd +} + +func runPause(cfgFile string, args []string) error { + pauseOpt.resources = args + + c, err := client.New(cfgFile) + if err != nil { + return err + } + + if err := c.RolloutPause(client.RolloutOptions{ + Kubeconfig: client.Kubeconfig{Path: pauseOpt.kubeconfig, Context: pauseOpt.kubeconfigContext}, + Namespace: pauseOpt.namespace, + Resources: pauseOpt.resources, + }); err != nil { + return err + } + return nil +} diff --git a/cmd/clusterctl/cmd/rollout/restart.go b/cmd/clusterctl/cmd/rollout/restart.go index e55aa795997a..a089b214b07c 100644 --- a/cmd/clusterctl/cmd/rollout/restart.go +++ b/cmd/clusterctl/cmd/rollout/restart.go @@ -30,7 +30,7 @@ type restartOptions struct { namespace string } -var ro = &restartOptions{} +var restartOpt = &restartOptions{} var ( restartLong = templates.LongDesc(` @@ -56,27 +56,27 @@ func NewCmdRolloutRestart(cfgFile string) *cobra.Command { return runRestart(cfgFile, cmd, args) }, } - cmd.Flags().StringVar(&ro.kubeconfig, "kubeconfig", "", + cmd.Flags().StringVar(&restartOpt.kubeconfig, "kubeconfig", "", "Path to the kubeconfig file to use for accessing the management cluster. If unspecified, default discovery rules apply.") - cmd.Flags().StringVar(&ro.kubeconfigContext, "kubeconfig-context", "", + cmd.Flags().StringVar(&restartOpt.kubeconfigContext, "kubeconfig-context", "", "Context to be used within the kubeconfig file. If empty, current context will be used.") - cmd.Flags().StringVar(&ro.namespace, "namespace", "", "Namespace where the resource(s) reside. If unspecified, the defult namespace will be used.") + cmd.Flags().StringVar(&restartOpt.namespace, "namespace", "", "Namespace where the resource(s) reside. If unspecified, the defult namespace will be used.") return cmd } func runRestart(cfgFile string, cmd *cobra.Command, args []string) error { - ro.resources = args + restartOpt.resources = args c, err := client.New(cfgFile) if err != nil { return err } - if err := c.RolloutRestart(client.RolloutRestartOptions{ - Kubeconfig: client.Kubeconfig{Path: ro.kubeconfig, Context: ro.kubeconfigContext}, - Namespace: ro.namespace, - Resources: ro.resources, + if err := c.RolloutRestart(client.RolloutOptions{ + Kubeconfig: client.Kubeconfig{Path: restartOpt.kubeconfig, Context: restartOpt.kubeconfigContext}, + Namespace: restartOpt.namespace, + Resources: restartOpt.resources, }); err != nil { return err } diff --git a/cmd/clusterctl/cmd/rollout/resume.go b/cmd/clusterctl/cmd/rollout/resume.go new file mode 100644 index 000000000000..2728fdfa0c28 --- /dev/null +++ b/cmd/clusterctl/cmd/rollout/resume.go @@ -0,0 +1,84 @@ +/* +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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rollout + +import ( + "github.com/spf13/cobra" + "k8s.io/kubectl/pkg/util/templates" + "sigs.k8s.io/cluster-api/cmd/clusterctl/client" +) + +// resumeOptions is the start of the data required to perform the operation. +type resumeOptions struct { + kubeconfig string + kubeconfigContext string + resources []string + namespace string +} + +var resumeOpt = &resumeOptions{} + +var ( + resumeLong = templates.LongDesc(` + Resume a paused cluster-api resource + + Paused resources will not be reconciled by a controller. By resuming a resource, we allow it to be reconciled again. Currently only MachineDeployments support being resumed.`) + + resumeExample = templates.Examples(` + # Resume an already paused machinedeployment + clusterctl alpha rollout resume machinedeployment/my-md-0`) +) + +// NewCmdRolloutResume returns a Command instance for 'rollout resume' sub command +func NewCmdRolloutResume(cfgFile string) *cobra.Command { + + cmd := &cobra.Command{ + Use: "resume RESOURCE", + DisableFlagsInUseLine: true, + Short: "Resume a cluster-api resource", + Long: resumeLong, + Example: resumeExample, + RunE: func(cmd *cobra.Command, args []string) error { + return runResume(cfgFile, args) + }, + } + cmd.Flags().StringVar(&resumeOpt.kubeconfig, "kubeconfig", "", + "Path to the kubeconfig file to use for accessing the management cluster. If unspecified, default discovery rules apply.") + cmd.Flags().StringVar(&resumeOpt.kubeconfigContext, "kubeconfig-context", "", + "Context to be used within the kubeconfig file. If empty, current context will be used.") + cmd.Flags().StringVar(&resumeOpt.namespace, "namespace", "", "Namespace where the resource(s) reside. If unspecified, the defult namespace will be used.") + + return cmd +} + +func runResume(cfgFile string, args []string) error { + resumeOpt.resources = args + + c, err := client.New(cfgFile) + if err != nil { + return err + } + + if err := c.RolloutResume(client.RolloutOptions{ + Kubeconfig: client.Kubeconfig{Path: resumeOpt.kubeconfig, Context: resumeOpt.kubeconfigContext}, + Namespace: resumeOpt.namespace, + Resources: resumeOpt.resources, + }); err != nil { + return err + } + return nil +}