From f0cd7c931e5b7104168c33c2b4af3b303a7d83fc Mon Sep 17 00:00:00 2001 From: Hiromu Asahina Date: Tue, 27 Dec 2022 04:52:48 +0900 Subject: [PATCH] Add rollout history --- api/v1beta1/common_types.go | 3 + cmd/clusterctl/client/alpha/rollout.go | 1 + cmd/clusterctl/client/alpha/rollout_viewer.go | 109 ++++++ .../client/alpha/rollout_viewer_test.go | 339 ++++++++++++++++++ cmd/clusterctl/client/client.go | 2 + cmd/clusterctl/client/client_test.go | 4 + cmd/clusterctl/client/rollout.go | 34 ++ cmd/clusterctl/client/rollout_test.go | 83 +++++ cmd/clusterctl/cmd/rollout.go | 6 +- cmd/clusterctl/cmd/rollout/history.go | 74 ++++ 10 files changed, 654 insertions(+), 1 deletion(-) create mode 100644 cmd/clusterctl/client/alpha/rollout_viewer.go create mode 100644 cmd/clusterctl/client/alpha/rollout_viewer_test.go create mode 100644 cmd/clusterctl/cmd/rollout/history.go diff --git a/api/v1beta1/common_types.go b/api/v1beta1/common_types.go index 017f7f675791..782e79989d39 100644 --- a/api/v1beta1/common_types.go +++ b/api/v1beta1/common_types.go @@ -190,6 +190,9 @@ const ( // VariableDefinitionFromInline indicates a patch or variable was defined in the `.spec` of a ClusterClass // rather than from an external patch extension. VariableDefinitionFromInline = "inline" + // ChangeCauseAnnotation is the annotation set on MachineSets by users to identify the cause of revision changes. + // This annotation will be shown when users run `clusterctl alpha rollout history`. + ChangeCauseAnnotation = "cluster.x-k8s.io/change-cause" ) // MachineSetPreflightCheck defines a valid MachineSet preflight check. diff --git a/cmd/clusterctl/client/alpha/rollout.go b/cmd/clusterctl/client/alpha/rollout.go index 8736ae79df0d..1ba7b890128c 100644 --- a/cmd/clusterctl/client/alpha/rollout.go +++ b/cmd/clusterctl/client/alpha/rollout.go @@ -46,6 +46,7 @@ type Rollout interface { ObjectPauser(context.Context, cluster.Proxy, corev1.ObjectReference) error ObjectResumer(context.Context, cluster.Proxy, corev1.ObjectReference) error ObjectRollbacker(context.Context, cluster.Proxy, corev1.ObjectReference, int64) error + ObjectViewer(context.Context, cluster.Proxy, corev1.ObjectReference, int64) error } var _ Rollout = &rollout{} diff --git a/cmd/clusterctl/client/alpha/rollout_viewer.go b/cmd/clusterctl/client/alpha/rollout_viewer.go new file mode 100644 index 000000000000..be512174e2f6 --- /dev/null +++ b/cmd/clusterctl/client/alpha/rollout_viewer.go @@ -0,0 +1,109 @@ +/* +Copyright 2022 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" + "fmt" + "os" + "sort" + "text/tabwriter" + + "github.com/pkg/errors" + "gopkg.in/yaml.v2" + corev1 "k8s.io/api/core/v1" + + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" + logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log" + "sigs.k8s.io/cluster-api/internal/controllers/machinedeployment/mdutil" +) + +// ObjectViewer will issue a view on the specified cluster-api resource. +func (r *rollout) ObjectViewer(ctx context.Context, proxy cluster.Proxy, ref corev1.ObjectReference, revision int64) error { + switch ref.Kind { + case MachineDeployment: + deployment, err := getMachineDeployment(ctx, proxy, ref.Name, ref.Namespace) + if err != nil || deployment == nil { + return errors.Wrapf(err, "failed to get %v/%v", ref.Kind, ref.Name) + } + if err := viewMachineDeployment(ctx, proxy, deployment, revision); err != nil { + return err + } + default: + return errors.Errorf("invalid resource type %q, valid values are %v", ref.Kind, validResourceTypes) + } + return nil +} + +func viewMachineDeployment(ctx context.Context, proxy cluster.Proxy, d *clusterv1.MachineDeployment, revision int64) error { + log := logf.Log + msList, err := getMachineSetsForDeployment(ctx, proxy, d) + if err != nil { + return err + } + + if revision < 0 { + return errors.Errorf("revision number cannot be negative: %v", revision) + } + + // Print details of a specific revision + if revision > 0 { + ms, err := findMachineDeploymentRevision(revision, msList) + if err != nil { + return errors.Errorf("unable to find the spcified revision") + } + output, err := yaml.Marshal(ms.Spec.Template) + if err != nil { + return err + } + fmt.Fprint(os.Stdout, string(output)) + return nil + } + + // Print an overview of all revisions + // Create a revisionToChangeCause map + historyInfo := make(map[int64]string) + for _, ms := range msList { + v, err := mdutil.Revision(ms) + if err != nil { + log.V(7).Error(err, fmt.Sprintf("unable to get revision from machineset %s for machinedeployment %s in namespace %s", ms.Name, d.Name, d.Namespace)) + continue + } + historyInfo[v] = ms.Annotations[clusterv1.ChangeCauseAnnotation] + } + + // Sort the revisions + revisions := make([]int64, 0, len(historyInfo)) + for r := range historyInfo { + revisions = append(revisions, r) + } + sort.Slice(revisions, func(i, j int) bool { return revisions[i] < revisions[j] }) + + // Output the revisionToChangeCause map + writer := new(tabwriter.Writer) + writer.Init(os.Stdout, 0, 8, 2, ' ', 0) + fmt.Fprintf(writer, "REVISION\tCHANGE-CAUSE\n") + for _, r := range revisions { + changeCause := historyInfo[r] + if changeCause == "" { + changeCause = "" + } + fmt.Fprintf(writer, "%d\t%s\n", r, changeCause) + } + return writer.Flush() +} diff --git a/cmd/clusterctl/client/alpha/rollout_viewer_test.go b/cmd/clusterctl/client/alpha/rollout_viewer_test.go new file mode 100644 index 000000000000..871df2f71f1a --- /dev/null +++ b/cmd/clusterctl/client/alpha/rollout_viewer_test.go @@ -0,0 +1,339 @@ +/* +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 ( + "bytes" + "context" + "io" + "os" + "testing" + + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" + + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test" +) + +func captureStdout(fnc func()) string { + r, w, _ := os.Pipe() + + stdout := os.Stdout + defer func() { + os.Stdout = stdout + }() + os.Stdout = w + + fnc() + _ = w.Close() + + var buf bytes.Buffer + _, _ = io.Copy(&buf, r) + return buf.String() +} + +func Test_ObjectViewer(t *testing.T) { + namespace := "default" + clusterName := "test" + labels := map[string]string{ + clusterv1.ClusterNameLabel: clusterName, + clusterv1.MachineDeploymentNameLabel: "test-md-0", + } + deployment := &clusterv1.MachineDeployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "MachineDeployment", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "test-md-0", + }, + Spec: clusterv1.MachineDeploymentSpec{ + ClusterName: clusterName, + Selector: metav1.LabelSelector{ + MatchLabels: labels, + }, + }, + } + + type fields struct { + objs []client.Object + ref corev1.ObjectReference + revision int64 + } + tests := []struct { + name string + fields fields + expectedOutput string + wantErr bool + }{ + + { + name: "should print an overview of all revisions", + fields: fields{ + objs: []client.Object{ + deployment, + &clusterv1.MachineSet{ + TypeMeta: metav1.TypeMeta{ + Kind: "MachineSet", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ms-rev-1", + Namespace: namespace, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(deployment, clusterv1.GroupVersion.WithKind("MachineDeployment")), + }, + Labels: labels, + Annotations: map[string]string{ + clusterv1.RevisionAnnotation: "1", + clusterv1.ChangeCauseAnnotation: "update to the latest version", + }, + }, + }, + &clusterv1.MachineSet{ + TypeMeta: metav1.TypeMeta{ + Kind: "MachineSet", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ms-rev-3", + Namespace: namespace, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(deployment, clusterv1.GroupVersion.WithKind("MachineDeployment")), + }, + Labels: labels, + Annotations: map[string]string{ + clusterv1.RevisionAnnotation: "3", + }, + }, + }, + &clusterv1.MachineSet{ + TypeMeta: metav1.TypeMeta{ + Kind: "MachineSet", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ms-rev-2", + Namespace: namespace, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(deployment, clusterv1.GroupVersion.WithKind("MachineDeployment")), + }, + Labels: labels, + Annotations: map[string]string{ + clusterv1.RevisionAnnotation: "2", + }, + }, + }, + }, + ref: corev1.ObjectReference{ + Kind: MachineDeployment, + Name: deployment.Name, + Namespace: namespace, + }, + }, + expectedOutput: `REVISION CHANGE-CAUSE +1 update to the latest version +2 +3 +`, + }, + { + name: "should print an overview of all revisions even if there is no machineSets", + fields: fields{ + objs: []client.Object{ + deployment, + }, + ref: corev1.ObjectReference{ + Kind: MachineDeployment, + Name: deployment.Name, + Namespace: namespace, + }, + }, + expectedOutput: `REVISION CHANGE-CAUSE +`, + }, + { + name: "should print the details of revision=999", + fields: fields{ + objs: []client.Object{ + deployment, + &clusterv1.MachineSet{ + TypeMeta: metav1.TypeMeta{ + Kind: "MachineSet", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ms-rev-2", + Namespace: namespace, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(deployment, clusterv1.GroupVersion.WithKind("MachineDeployment")), + }, + Labels: labels, + Annotations: map[string]string{ + clusterv1.RevisionAnnotation: "2", + }, + }, + }, + &clusterv1.MachineSet{ + TypeMeta: metav1.TypeMeta{ + Kind: "MachineSet", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "ms-rev-999", + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(deployment, clusterv1.GroupVersion.WithKind("MachineDeployment")), + }, + Labels: labels, + Annotations: map[string]string{ + clusterv1.RevisionAnnotation: "999", + }, + }, + Spec: clusterv1.MachineSetSpec{ + ClusterName: clusterName, + Selector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + clusterv1.ClusterNameLabel: clusterName, + }, + }, + Template: clusterv1.MachineTemplateSpec{ + ObjectMeta: clusterv1.ObjectMeta{ + Labels: map[string]string{ + clusterv1.ClusterNameLabel: clusterName, + }, + Annotations: map[string]string{"foo": "bar"}, + }, + Spec: clusterv1.MachineSpec{ + ClusterName: clusterName, + Bootstrap: clusterv1.Bootstrap{ + ConfigRef: &corev1.ObjectReference{ + APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", + Kind: "KubeadmConfigTemplate", + Name: "md-template", + Namespace: namespace, + }, + DataSecretName: pointer.String("secret-name"), + }, + InfrastructureRef: corev1.ObjectReference{ + APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1", + Kind: "InfrastructureMachineTemplate", + Name: "md-template", + Namespace: namespace, + }, + Version: pointer.String("v1.25.1"), + ProviderID: pointer.String("test://id-1"), + FailureDomain: pointer.String("one"), + NodeDrainTimeout: &metav1.Duration{Duration: 0}, + NodeVolumeDetachTimeout: &metav1.Duration{Duration: 0}, + NodeDeletionTimeout: &metav1.Duration{Duration: 0}, + }, + }, + }, + }, + }, + ref: corev1.ObjectReference{ + Kind: MachineDeployment, + Name: deployment.Name, + Namespace: namespace, + }, + revision: int64(999), + }, + expectedOutput: `objectmeta: + labels: + cluster.x-k8s.io/cluster-name: test + annotations: + foo: bar +spec: + clustername: test + bootstrap: + configref: + kind: KubeadmConfigTemplate + namespace: default + name: md-template + uid: "" + apiversion: bootstrap.cluster.x-k8s.io/v1beta1 + resourceversion: "" + fieldpath: "" + datasecretname: secret-name + infrastructureref: + kind: InfrastructureMachineTemplate + namespace: default + name: md-template + uid: "" + apiversion: infrastructure.cluster.x-k8s.io/v1beta1 + resourceversion: "" + fieldpath: "" + version: v1.25.1 + providerid: test://id-1 + failuredomain: one + nodedraintimeout: + duration: 0s + nodevolumedetachtimeout: + duration: 0s + nodedeletiontimeout: + duration: 0s +`, + }, + { + name: "should print an error for non-existent revision", + fields: fields{ + objs: []client.Object{ + deployment, + }, + ref: corev1.ObjectReference{ + Kind: MachineDeployment, + Name: deployment.Name, + Namespace: namespace, + }, + revision: int64(999), + }, + wantErr: true, + }, + { + name: "should print an error for an invalid revision", + fields: fields{ + objs: []client.Object{ + deployment, + }, + ref: corev1.ObjectReference{ + Kind: MachineDeployment, + Name: deployment.Name, + Namespace: namespace, + }, + revision: int64(-1), + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + r := newRolloutClient() + proxy := test.NewFakeProxy().WithObjs(tt.fields.objs...) + output := captureStdout(func() { + err := r.ObjectViewer(context.Background(), proxy, tt.fields.ref, tt.fields.revision) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).ToNot(HaveOccurred()) + } + }) + g.Expect(output).To(Equal(tt.expectedOutput)) + }) + } +} diff --git a/cmd/clusterctl/client/client.go b/cmd/clusterctl/client/client.go index dd76da457b33..321d8510f5ad 100644 --- a/cmd/clusterctl/client/client.go +++ b/cmd/clusterctl/client/client.go @@ -86,6 +86,8 @@ type AlphaClient interface { RolloutResume(ctx context.Context, options RolloutResumeOptions) error // RolloutUndo provides rollout rollback of cluster-api resources RolloutUndo(ctx context.Context, options RolloutUndoOptions) error + // RolloutHistory provides rollout history of cluster-api resources + RolloutHistory(ctx context.Context, options RolloutHistoryOptions) error // TopologyPlan dry runs the topology reconciler TopologyPlan(ctx context.Context, options TopologyPlanOptions) (*TopologyPlanOutput, error) } diff --git a/cmd/clusterctl/client/client_test.go b/cmd/clusterctl/client/client_test.go index 90c93f92110a..b7c2aa030e25 100644 --- a/cmd/clusterctl/client/client_test.go +++ b/cmd/clusterctl/client/client_test.go @@ -147,6 +147,10 @@ func (f fakeClient) RolloutUndo(ctx context.Context, options RolloutUndoOptions) return f.internalClient.RolloutUndo(ctx, options) } +func (f fakeClient) RolloutHistory(ctx context.Context, options RolloutHistoryOptions) error { + return f.internalClient.RolloutHistory(ctx, options) +} + func (f fakeClient) TopologyPlan(ctx context.Context, options TopologyPlanOptions) (*cluster.TopologyPlanOutput, error) { return f.internalClient.TopologyPlan(ctx, options) } diff --git a/cmd/clusterctl/client/rollout.go b/cmd/clusterctl/client/rollout.go index d30dc3665294..cb1d2c997f1a 100644 --- a/cmd/clusterctl/client/rollout.go +++ b/cmd/clusterctl/client/rollout.go @@ -86,6 +86,23 @@ type RolloutUndoOptions struct { ToRevision int64 } +// RolloutHistoryOptions carries the options supported by RolloutHistory. +type RolloutHistoryOptions 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 for the rollout command + Resources []string + + // Namespace where the resource(s) live. If unspecified, the namespace name will be inferred + // from the current configuration. + Namespace string + + // Revision number to view details + Revision int64 +} + func (c *clusterctlClient) RolloutRestart(ctx context.Context, options RolloutRestartOptions) error { clusterClient, err := c.clusterClientFactory(ClusterClientFactoryInput{Kubeconfig: options.Kubeconfig}) if err != nil { @@ -154,6 +171,23 @@ func (c *clusterctlClient) RolloutUndo(ctx context.Context, options RolloutUndoO return nil } +func (c *clusterctlClient) RolloutHistory(ctx context.Context, options RolloutHistoryOptions) error { + clusterClient, err := c.clusterClientFactory(ClusterClientFactoryInput{Kubeconfig: options.Kubeconfig}) + if err != nil { + return err + } + objRefs, err := getObjectRefs(clusterClient, options.Namespace, options.Resources) + if err != nil { + return err + } + for _, ref := range objRefs { + if err := c.alphaClient.Rollout().ObjectViewer(ctx, clusterClient.Proxy(), ref, options.Revision); err != nil { + return err + } + } + return nil +} + func getObjectRefs(clusterClient cluster.Client, namespace string, resources []string) ([]corev1.ObjectReference, error) { // If the option specifying the Namespace is empty, try to detect it. if namespace == "" { diff --git a/cmd/clusterctl/client/rollout_test.go b/cmd/clusterctl/client/rollout_test.go index 01f36f23e538..9898f42206fa 100644 --- a/cmd/clusterctl/client/rollout_test.go +++ b/cmd/clusterctl/client/rollout_test.go @@ -354,3 +354,86 @@ func Test_clusterctlClient_RolloutResume(t *testing.T) { }) } } + +func Test_clusterctlClient_RolloutHistory(t *testing.T) { + type fields struct { + client *fakeClient + } + type args struct { + options RolloutHistoryOptions + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "return an error if machinedeployment is not found", + fields: fields{ + client: fakeClientForRollout(), + }, + args: args{ + options: RolloutHistoryOptions{ + Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, + Resources: []string{"machinedeployment/foo"}, + Namespace: "default", + }, + }, + wantErr: true, + }, + { + name: "return error if one of the machinedeployments is not found", + fields: fields{ + client: fakeClientForRollout(), + }, + args: args{ + options: RolloutHistoryOptions{ + Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, + Resources: []string{"machinedeployment/md-1", "machinedeployment/md-does-not-exist"}, + Namespace: "default", + }, + }, + wantErr: true, + }, + { + name: "return error if unknown resource specified", + fields: fields{ + client: fakeClientForRollout(), + }, + args: args{ + options: RolloutHistoryOptions{ + Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, + Resources: []string{"foo/bar"}, + Namespace: "default", + }, + }, + wantErr: true, + }, + { + name: "return error if no resource specified", + fields: fields{ + client: fakeClientForRollout(), + }, + args: args{ + options: RolloutHistoryOptions{ + Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, + Namespace: "default", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + err := tt.fields.client.RolloutHistory(ctx, 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 fab191a2d978..7a3ac8d93bbf 100644 --- a/cmd/clusterctl/cmd/rollout.go +++ b/cmd/clusterctl/cmd/rollout.go @@ -45,7 +45,10 @@ var ( clusterctl alpha rollout resume kubeadmcontrolplane/my-kcp # Rollback a machinedeployment - clusterctl alpha rollout undo machinedeployment/my-md-0 --to-revision=3`) + clusterctl alpha rollout undo machinedeployment/my-md-0 --to-revision=3 + + # View rollback history of a machinedeployment + clusterctl alpha rollout history machinedeployment/my-md-0 --revision=3`) rolloutCmd = &cobra.Command{ Use: "rollout SUBCOMMAND", @@ -61,4 +64,5 @@ func init() { rolloutCmd.AddCommand(rollout.NewCmdRolloutPause(cfgFile)) rolloutCmd.AddCommand(rollout.NewCmdRolloutResume(cfgFile)) rolloutCmd.AddCommand(rollout.NewCmdRolloutUndo(cfgFile)) + rolloutCmd.AddCommand(rollout.NewCmdRolloutHistory(cfgFile)) } diff --git a/cmd/clusterctl/cmd/rollout/history.go b/cmd/clusterctl/cmd/rollout/history.go new file mode 100644 index 000000000000..e9cc2f37c87e --- /dev/null +++ b/cmd/clusterctl/cmd/rollout/history.go @@ -0,0 +1,74 @@ +/* +Copyright 2022 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 ( + "context" + + "github.com/spf13/cobra" + "k8s.io/kubectl/pkg/util/templates" + + "sigs.k8s.io/cluster-api/cmd/clusterctl/client" +) + +var ( + historyLong = templates.LongDesc(` + View previous rollout revisions and configurations.`) + + historyExample = templates.Examples(` + # View the rollout history of a machine deployment + clusterctl alpha rollout history machinedeployment/my-md-0 + + # View the details of machine deployment revision 3 + clusterctl alpha rollout history machinedeployment/my-md-0 --revision=3`) +) + +// NewCmdRolloutHistory returns a Command instance for 'rollout history' sub command. +func NewCmdRolloutHistory(cfgFile string) *cobra.Command { + historyOpt := client.RolloutHistoryOptions{} + cmd := &cobra.Command{ + Use: "history RESOURCE", + DisableFlagsInUseLine: true, + Short: "History a cluster-api resource", + Long: historyLong, + Example: historyExample, + RunE: func(cmd *cobra.Command, args []string) error { + return runHistory(cfgFile, args, historyOpt) + }, + } + cmd.Flags().StringVar(&historyOpt.Kubeconfig.Path, "kubeconfig", "", + "Path to the kubeconfig file to use for accessing the management cluster. If unspecified, default discovery rules apply.") + cmd.Flags().StringVar(&historyOpt.Kubeconfig.Context, "kubeconfig-context", "", + "Context to be used within the kubeconfig file. If empty, current context will be used.") + cmd.Flags().StringVarP(&historyOpt.Namespace, "namespace", "n", "", "Namespace where the resource(s) reside. If unspecified, the defult namespace will be used.") + cmd.Flags().Int64Var(&historyOpt.Revision, "revision", historyOpt.Revision, "See the details, including podTemplate of the revision specified.") + + return cmd +} + +func runHistory(cfgFile string, args []string, historyOpt client.RolloutHistoryOptions) error { + historyOpt.Resources = args + + ctx := context.Background() + + c, err := client.New(ctx, cfgFile) + if err != nil { + return err + } + + return c.RolloutHistory(ctx, historyOpt) +}