From 14c47aa3d2e64263cfe4589b1d6a2b962b5e3402 Mon Sep 17 00:00:00 2001 From: Dan Mace Date: Wed, 8 Jul 2015 17:04:22 -0400 Subject: [PATCH] Simplify rollback arguments Make the rollback command speak in terms of deployment configs and versions, instead of deployments which aren't formal API resources. This keeps the command consistent with other deployment related APIs and commands. Rollback now accepts a deployment config and version for the rollback target; if no version is supplied, the last completed deployment will be inferred. --- docs/generated/oc_by_example_content.adoc | 13 +- hack/test-cmd.sh | 15 +- pkg/cmd/cli/cmd/rollback.go | 374 +++++++++++++++------- pkg/cmd/cli/cmd/rollback_test.go | 110 +++++++ rel-eng/completions/bash/oc | 1 + rel-eng/completions/bash/openshift | 1 + 6 files changed, 396 insertions(+), 118 deletions(-) create mode 100644 pkg/cmd/cli/cmd/rollback_test.go diff --git a/docs/generated/oc_by_example_content.adoc b/docs/generated/oc_by_example_content.adoc index 6ffca93b7ff0..bd5acf126b16 100644 --- a/docs/generated/oc_by_example_content.adoc +++ b/docs/generated/oc_by_example_content.adoc @@ -613,14 +613,17 @@ Revert part of an application back to a previous deployment [options="nowrap"] ---- - // Perform a rollback - $ openshift cli rollback deployment-1 + // Perform a rollback to the last successfully completed deployment for a deploymentconfig + $ openshift cli rollback frontend - // See what the rollback will look like, but don't perform the rollback - $ openshift cli rollback deployment-1 --dry-run + // See what a rollback to version 3 will look like, but don't perform the rollback + $ openshift cli rollback frontend --to-version=3 --dry-run + + // Perform a rollback to a specific deployment + $ openshift cli rollback frontend-2 // Perform the rollback manually by piping the JSON of the new config back to openshift cli - $ openshift cli rollback deployment-1 --output=json | openshift cli update deploymentConfigs deployment -f - + $ openshift cli rollback frontend --output=json | openshift cli update deploymentConfigs deployment -f - ---- ==== diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index ab1ef2fc1441..38bfd40a6e2a 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -569,16 +569,29 @@ echo "scale: ok" oc process -f examples/sample-app/application-template-dockerbuild.json -l app=dockerbuild | oc create -f - wait_for_command 'oc get rc/database-1' "${TIME_MIN}" + +oc rollback database --to-version=1 -o=yaml +oc rollback dc/database --to-version=1 -o=yaml +oc rollback dc/database --to-version=1 --dry-run +oc rollback database-1 -o=yaml +oc rollback rc/database-1 -o=yaml +# should fail because there's no previous deployment +[ ! "$(oc rollback database -o yaml)" ] +echo "rollback: ok" + oc get dc/database oc stop dc/database [ ! "$(oc get dc/database)" ] [ ! "$(oc get rc/database-1)" ] echo "stop: ok" + oc label bc ruby-sample-build acustom=label [ "$(oc describe bc/ruby-sample-build | grep 'acustom=label')" ] -oc delete all -l app=dockerbuild echo "label: ok" +oc delete all -l app=dockerbuild +echo "delete: ok" + oc process -f examples/sample-app/application-template-dockerbuild.json -l build=docker | oc create -f - oc get buildConfigs oc get bc diff --git a/pkg/cmd/cli/cmd/rollback.go b/pkg/cmd/cli/cmd/rollback.go index f6993a134a35..c14c90338bea 100644 --- a/pkg/cmd/cli/cmd/rollback.go +++ b/pkg/cmd/cli/cmd/rollback.go @@ -3,12 +3,16 @@ package cmd import ( "fmt" "io" + "sort" "strings" kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + kerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" kubectl "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" cmdutil "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/util" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/spf13/cobra" latest "github.com/openshift/origin/pkg/api/latest" @@ -16,17 +20,18 @@ import ( describe "github.com/openshift/origin/pkg/cmd/cli/describe" "github.com/openshift/origin/pkg/cmd/util/clientcmd" deployapi "github.com/openshift/origin/pkg/deploy/api" + deployutil "github.com/openshift/origin/pkg/deploy/util" ) const ( rollbackLong = `Revert part of an application back to a previous deployment. -When you run this command your deployment configuration will be updated to match -the provided deployment. By default only the pod and container configuration -will be changed and scaling or trigger settings will be left as-is. Note that -environment variables and volumes are included in rollbacks, so if you've -recently updated security credentials in your environment your previous -deployment may not have the correct values. +When you run this command your deployment configuration will be updated to +match a previous deployment. By default only the pod and container +configuration will be changed and scaling or trigger settings will be left as- +is. Note that environment variables and volumes are included in rollbacks, so +if you've recently updated security credentials in your environment your +previous deployment may not have the correct values. Any image triggers present in the rolled back configuration will be disabled with a warning. This is to help prevent your rolled back deployment from being @@ -38,151 +43,296 @@ a human-readable representation of the updated deployment configuration instead executing the rollback. This is useful if you're not quite sure what the outcome will be.` - rollbackExample = ` // Perform a rollback - $ %[1]s rollback deployment-1 + rollbackExample = ` // Perform a rollback to the last successfully completed deployment for a deploymentconfig + $ %[1]s rollback frontend - // See what the rollback will look like, but don't perform the rollback - $ %[1]s rollback deployment-1 --dry-run + // See what a rollback to version 3 will look like, but don't perform the rollback + $ %[1]s rollback frontend --to-version=3 --dry-run + + // Perform a rollback to a specific deployment + $ %[1]s rollback frontend-2 // Perform the rollback manually by piping the JSON of the new config back to %[1]s - $ %[1]s rollback deployment-1 --output=json | %[1]s update deploymentConfigs deployment -f -` + $ %[1]s rollback frontend --output=json | %[1]s update deploymentConfigs deployment -f -` ) // NewCmdRollback creates a CLI rollback command. func NewCmdRollback(fullName string, f *clientcmd.Factory, out io.Writer) *cobra.Command { - rollback := &deployapi.DeploymentConfigRollback{ - Spec: deployapi.DeploymentConfigRollbackSpec{ - IncludeTemplate: true, - }, - } - + opts := &RollbackOptions{} cmd := &cobra.Command{ - Use: "rollback DEPLOYMENT", + Use: "rollback (DEPLOYMENTCONFIG | DEPLOYMENT)", Short: "Revert part of an application back to a previous deployment", Long: rollbackLong, Example: fmt.Sprintf(rollbackExample, fullName), Run: func(cmd *cobra.Command, args []string) { - // Validate arguments - if len(args) == 0 || len(args[0]) == 0 { - cmdutil.CheckErr(cmdutil.UsageError(cmd, "A deployment name is required.")) + if err := opts.Complete(f, args, out); err != nil { + cmdutil.CheckErr(cmdutil.UsageError(cmd, err.Error())) } - // Extract arguments - format := cmdutil.GetFlagString(cmd, "output") - template := cmdutil.GetFlagString(cmd, "template") - dryRun := cmdutil.GetFlagBool(cmd, "dry-run") - - // Get globally provided stuff - namespace, err := f.DefaultNamespace() - cmdutil.CheckErr(err) - oClient, kClient, err := f.Clients() - cmdutil.CheckErr(err) - - // Set up the rollback config - rollback.Spec.From.Name = args[0] - - // Make a helper and generate a rolled back config - helper := newHelper(oClient, kClient) - config, err := helper.Generate(namespace, rollback) - cmdutil.CheckErr(err) - - // If this is a dry run, print and exit - if dryRun { - err := helper.Describe(config, out) - cmdutil.CheckErr(err) - return + if err := opts.Validate(); err != nil { + cmdutil.CheckErr(cmdutil.UsageError(cmd, err.Error())) } - // If an output format is specified, print and exit - if len(format) > 0 { - err := helper.Print(config, format, template, out) + if err := opts.Run(); err != nil { cmdutil.CheckErr(err) - return - } - - // Perform the rollback - rolledback, err := helper.Update(config) - cmdutil.CheckErr(err) - - // Notify the user of any disabled image triggers - fmt.Fprintf(out, "#%d rolled back to %s\n", rolledback.LatestVersion, rollback.Spec.From.Name) - for _, trigger := range rolledback.Triggers { - disabled := []string{} - if trigger.Type == deployapi.DeploymentTriggerOnImageChange && !trigger.ImageChangeParams.Automatic { - disabled = append(disabled, trigger.ImageChangeParams.From.Name) - } - if len(disabled) > 0 { - reenable := fmt.Sprintf("%s deploy %s --enable-triggers", fullName, rolledback.Name) - fmt.Fprintf(cmd.Out(), "Warning: the following images triggers were disabled: %s\n You can re-enable them with: %s\n", strings.Join(disabled, ","), reenable) - } } }, } - cmd.Flags().BoolVar(&rollback.Spec.IncludeTriggers, "change-triggers", false, "Include the previous deployment's triggers in the rollback") - cmd.Flags().BoolVar(&rollback.Spec.IncludeStrategy, "change-strategy", false, "Include the previous deployment's strategy in the rollback") - cmd.Flags().BoolVar(&rollback.Spec.IncludeReplicationMeta, "change-scaling-settings", false, "Include the previous deployment's replicationController replica count and selector in the rollback") - cmd.Flags().BoolP("dry-run", "d", false, "Instead of performing the rollback, describe what the rollback will look like in human-readable form") - cmd.Flags().StringP("output", "o", "", "Instead of performing the rollback, print the updated deployment configuration in the specified format (json|yaml|template|templatefile)") - cmd.Flags().StringP("template", "t", "", "Template string or path to template file to use when -o=template or -o=templatefile.") + cmd.Flags().BoolVar(&opts.IncludeTriggers, "change-triggers", false, "Include the previous deployment's triggers in the rollback") + cmd.Flags().BoolVar(&opts.IncludeStrategy, "change-strategy", false, "Include the previous deployment's strategy in the rollback") + cmd.Flags().BoolVar(&opts.IncludeScalingSettings, "change-scaling-settings", false, "Include the previous deployment's replicationController replica count and selector in the rollback") + cmd.Flags().BoolVarP(&opts.DryRun, "dry-run", "d", false, "Instead of performing the rollback, describe what the rollback will look like in human-readable form") + cmd.Flags().StringVarP(&opts.Format, "output", "o", "", "Instead of performing the rollback, print the updated deployment configuration in the specified format (json|yaml|template|templatefile)") + cmd.Flags().StringVarP(&opts.Template, "template", "t", "", "Template string or path to template file to use when -o=template or -o=templatefile.") + cmd.Flags().IntVar(&opts.DesiredVersion, "to-version", 0, "A config version to rollback to. Specifying version 0 is the same as omitting a version (the version will be auto-detected). This option is ignored when specifying a deployment.") return cmd } -// newHelper makes a hew helper using real clients. -func newHelper(oClient client.Interface, kClient kclient.Interface) *helper { - return &helper{ - generateRollback: func(namespace string, config *deployapi.DeploymentConfigRollback) (*deployapi.DeploymentConfig, error) { - return oClient.DeploymentConfigs(namespace).Rollback(config) - }, - describe: func(config *deployapi.DeploymentConfig) (string, error) { - describer := describe.NewDeploymentConfigDescriberForConfig(oClient, kClient, config) - return describer.Describe(config.Namespace, config.Name) - }, - updateConfig: func(namespace string, config *deployapi.DeploymentConfig) (*deployapi.DeploymentConfig, error) { - return oClient.DeploymentConfigs(namespace).Update(config) - }, - } -} +// RollbackOptions contains all the necessary state to perform a rollback. +type RollbackOptions struct { + Namespace string + TargetName string + DesiredVersion int + Format string + Template string + DryRun bool + IncludeTriggers bool + IncludeStrategy bool + IncludeScalingSettings bool -// helper knows how to perform various rollback related tasks. -type helper struct { - // generateRollback generates a rolled back config from the input config - generateRollback func(namespace string, config *deployapi.DeploymentConfigRollback) (*deployapi.DeploymentConfig, error) - // describe returns the describer output for config - describe func(config *deployapi.DeploymentConfig) (string, error) - // updateConfig persists config - updateConfig func(namespace string, config *deployapi.DeploymentConfig) (*deployapi.DeploymentConfig, error) + // out is a place to write user-facing output. + out io.Writer + // oc is an openshift client. + oc client.Interface + // kc is a kube client. + kc kclient.Interface + // getBuilder returns a new builder each time it is called. A + // resource.Builder is stateful and isn't safe to reuse (e.g. across + // resource types). + getBuilder func() *resource.Builder } -// Generate generates a rolled back DeploymentConfig. -func (r *helper) Generate(namespace string, config *deployapi.DeploymentConfigRollback) (*deployapi.DeploymentConfig, error) { - return r.generateRollback(namespace, config) -} +// Complete turns a partially defined RollbackActions into a solvent structure +// which can be validated and used for a rollback. +func (o *RollbackOptions) Complete(f *clientcmd.Factory, args []string, out io.Writer) error { + // Extract basic flags. + if len(args) == 1 { + o.TargetName = args[0] + } + namespace, err := f.DefaultNamespace() + if err != nil { + return err + } + o.Namespace = namespace -// Describe describes a DeploymentConfig. -func (r *helper) Describe(config *deployapi.DeploymentConfig, out io.Writer) error { - description, err := r.describe(config) + // Set up client based support. + mapper, typer := f.Object() + o.getBuilder = func() *resource.Builder { + return resource.NewBuilder(mapper, typer, f.ClientMapperForCommand()) + } + + oClient, kClient, err := f.Clients() if err != nil { return err } - out.Write([]byte(description)) + o.oc = oClient + o.kc = kClient + + o.out = out + return nil +} + +// Validate ensures that a RollbackOptions is valid and can be used to execute +// a rollback. +func (o *RollbackOptions) Validate() error { + if len(o.TargetName) == 0 { + return fmt.Errorf("a deployment or deploymentconfig name is required") + } + if o.DesiredVersion < 0 { + return fmt.Errorf("the to version must be >= 0") + } + if o.out == nil { + return fmt.Errorf("out must not be nil") + } + if o.oc == nil { + return fmt.Errorf("oc must not be nil") + } + if o.kc == nil { + return fmt.Errorf("kc must not be nil") + } + if o.getBuilder == nil { + return fmt.Errorf("getBuilder must not be nil") + } else { + b := o.getBuilder() + if b == nil { + return fmt.Errorf("getBuilder must return a resource.Builder") + } + } return nil } -// Print prints a deployment config in the specified format with the given -// template. -func (r *helper) Print(config *deployapi.DeploymentConfig, format, template string, out io.Writer) error { - printer, _, err := kubectl.GetPrinter(format, template) +// Run performs a rollback. +func (o *RollbackOptions) Run() error { + // Get the resource referenced in the command args. + obj, err := o.findResource(o.TargetName) if err != nil { return err } - versionedPrinter := kubectl.NewVersionedPrinter(printer, kapi.Scheme, latest.Version) - versionedPrinter.PrintObj(config, out) + + // Interpret the resource to resolve a target for rollback. + var target *kapi.ReplicationController + switch r := obj.(type) { + case *kapi.ReplicationController: + // A specific deployment was used. + target = r + case *deployapi.DeploymentConfig: + // A deploymentconfig was used. Find the target deployment by the + // specified version, or by a lookup of the last completed deployment if + // no version was supplied. + deployment, err := o.findTargetDeployment(r, o.DesiredVersion) + if err != nil { + return err + } + target = deployment + } + if target == nil { + return fmt.Errorf("%s is not a valid deployment or deploymentconfig", o.TargetName) + } + + // Set up the rollback and generate a new rolled back config. + rollback := &deployapi.DeploymentConfigRollback{ + Spec: deployapi.DeploymentConfigRollbackSpec{ + From: kapi.ObjectReference{ + Name: target.Name, + }, + IncludeTemplate: true, + IncludeTriggers: o.IncludeTriggers, + IncludeStrategy: o.IncludeStrategy, + IncludeReplicationMeta: o.IncludeScalingSettings, + }, + } + newConfig, err := o.oc.DeploymentConfigs(o.Namespace).Rollback(rollback) + if err != nil { + return err + } + + // If this is a dry run, print and exit. + if o.DryRun { + describer := describe.NewDeploymentConfigDescriberForConfig(o.oc, o.kc, newConfig) + description, err := describer.Describe(newConfig.Namespace, newConfig.Name) + if err != nil { + return err + } + o.out.Write([]byte(description)) + return nil + } + + // If an output format is specified, print and exit. + if len(o.Format) > 0 { + printer, _, err := kubectl.GetPrinter(o.Format, o.Template) + if err != nil { + return err + } + versionedPrinter := kubectl.NewVersionedPrinter(printer, kapi.Scheme, latest.Version) + versionedPrinter.PrintObj(newConfig, o.out) + return nil + } + + // Perform a real rollback. + rolledback, err := o.oc.DeploymentConfigs(newConfig.Namespace).Update(newConfig) + if err != nil { + return err + } + + // Print warnings about any image triggers disabled during the rollback. + fmt.Fprintf(o.out, "#%d rolled back to %s\n", rolledback.LatestVersion, rollback.Spec.From.Name) + for _, trigger := range rolledback.Triggers { + disabled := []string{} + if trigger.Type == deployapi.DeploymentTriggerOnImageChange && !trigger.ImageChangeParams.Automatic { + disabled = append(disabled, trigger.ImageChangeParams.From.Name) + } + if len(disabled) > 0 { + reenable := fmt.Sprintf("oc deploy %s --enable-triggers", rolledback.Name) + fmt.Fprintf(o.out, "Warning: the following images triggers were disabled: %s\n You can re-enable them with: %s\n", strings.Join(disabled, ","), reenable) + } + } + return nil } -// Update persists the given DeploymentConfig. -func (r *helper) Update(config *deployapi.DeploymentConfig) (*deployapi.DeploymentConfig, error) { - return r.updateConfig(config.Namespace, config) +// findResource tries to find a deployment or deploymentconfig named +// targetName using a resource.Builder. For compatibility, if the resource +// name is unprefixed, treat it as an rc first and a dc second. +func (o *RollbackOptions) findResource(targetName string) (runtime.Object, error) { + candidates := []string{} + if strings.Index(targetName, "/") == -1 { + candidates = append(candidates, "rc/"+targetName) + candidates = append(candidates, "dc/"+targetName) + } else { + candidates = append(candidates, targetName) + } + var obj runtime.Object + for _, name := range candidates { + r := o.getBuilder(). + NamespaceParam(o.Namespace). + ResourceTypeOrNameArgs(false, name). + SingleResourceType(). + Do() + if r.Err() != nil { + return nil, r.Err() + } + resultObj, err := r.Object() + if err != nil { + // If the resource wasn't found, try another candidate. + if kerrors.IsNotFound(err) { + continue + } + return nil, err + } + obj = resultObj + break + } + if obj == nil { + return nil, fmt.Errorf("%s is not a valid deployment or deploymentconfig", targetName) + } + return obj, nil +} + +// findTargetDeployment finds the deployment which is the rollback target by +// searching for deployments associated with config. If desiredVersion is >0, +// the deployment matching desiredVersion will be returned. If desiredVersion +// is <=0, the last completed deployment which is older than the config's +// version will be returned. +func (o *RollbackOptions) findTargetDeployment(config *deployapi.DeploymentConfig, desiredVersion int) (*kapi.ReplicationController, error) { + // Find deployments for the config sorted by version descending. + deployments, err := o.kc.ReplicationControllers(config.Namespace).List(deployutil.ConfigSelector(config.Name)) + if err != nil { + return nil, err + } + sort.Sort(deployutil.DeploymentsByLatestVersionDesc(deployments.Items)) + + // Find the target deployment for rollback. If a version was specified, + // use the version for a search. Otherwise, use the last completed + // deployment. + var target *kapi.ReplicationController + for _, deployment := range deployments.Items { + version := deployutil.DeploymentVersionFor(&deployment) + if desiredVersion > 0 { + if version == desiredVersion { + target = &deployment + break + } + } else { + if version < config.LatestVersion && deployutil.DeploymentStatusFor(&deployment) == deployapi.DeploymentStatusComplete { + target = &deployment + break + } + } + } + if target == nil { + return nil, fmt.Errorf("couldn't find deployment for rollback") + } + return target, nil } diff --git a/pkg/cmd/cli/cmd/rollback_test.go b/pkg/cmd/cli/cmd/rollback_test.go new file mode 100644 index 000000000000..b91d329cf4b6 --- /dev/null +++ b/pkg/cmd/cli/cmd/rollback_test.go @@ -0,0 +1,110 @@ +package cmd + +import ( + "testing" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + ktc "github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient" + + deployapi "github.com/openshift/origin/pkg/deploy/api" + deploytest "github.com/openshift/origin/pkg/deploy/api/test" + deployutil "github.com/openshift/origin/pkg/deploy/util" +) + +func TestRollbackOptions_findTargetDeployment(t *testing.T) { + type existingDeployment struct { + version int + status deployapi.DeploymentStatus + } + tests := []struct { + name string + configVersion int + desiredVersion int + existing []existingDeployment + expectedVersion int + errorExpected bool + }{ + { + name: "desired found", + configVersion: 3, + existing: []existingDeployment{ + {1, deployapi.DeploymentStatusComplete}, + {2, deployapi.DeploymentStatusComplete}, + {3, deployapi.DeploymentStatusComplete}, + }, + desiredVersion: 1, + expectedVersion: 1, + errorExpected: false, + }, + { + name: "desired not found", + configVersion: 3, + existing: []existingDeployment{ + {2, deployapi.DeploymentStatusComplete}, + {3, deployapi.DeploymentStatusComplete}, + }, + desiredVersion: 1, + errorExpected: true, + }, + { + name: "desired not supplied, target found", + configVersion: 3, + existing: []existingDeployment{ + {1, deployapi.DeploymentStatusComplete}, + {2, deployapi.DeploymentStatusFailed}, + {3, deployapi.DeploymentStatusComplete}, + }, + desiredVersion: 0, + expectedVersion: 1, + errorExpected: false, + }, + { + name: "desired not supplied, target not found", + configVersion: 3, + existing: []existingDeployment{ + {1, deployapi.DeploymentStatusFailed}, + {2, deployapi.DeploymentStatusFailed}, + {3, deployapi.DeploymentStatusComplete}, + }, + desiredVersion: 0, + errorExpected: true, + }, + } + + for _, test := range tests { + t.Logf("evaluating test: %s", test.name) + + existingControllers := &kapi.ReplicationControllerList{} + for _, existing := range test.existing { + config := deploytest.OkDeploymentConfig(existing.version) + deployment, _ := deployutil.MakeDeployment(config, kapi.Codec) + deployment.Annotations[deployapi.DeploymentStatusAnnotation] = string(existing.status) + existingControllers.Items = append(existingControllers.Items, *deployment) + } + + fakekc := ktc.NewSimpleFake(existingControllers) + opts := &RollbackOptions{ + kc: fakekc, + } + + config := deploytest.OkDeploymentConfig(test.configVersion) + target, err := opts.findTargetDeployment(config, test.desiredVersion) + if err != nil { + if !test.errorExpected { + t.Fatalf("unexpected error: %s", err) + } + continue + } else { + if test.errorExpected && err == nil { + t.Fatalf("expected an error") + } + } + + if target == nil { + t.Fatalf("expected a target deployment") + } + if e, a := test.expectedVersion, deployutil.DeploymentVersionFor(target); e != a { + t.Errorf("expected target version %d, got %d", e, a) + } + } +} diff --git a/rel-eng/completions/bash/oc b/rel-eng/completions/bash/oc index 7df85e8dbbf4..0f7845359851 100644 --- a/rel-eng/completions/bash/oc +++ b/rel-eng/completions/bash/oc @@ -445,6 +445,7 @@ _oc_rollback() two_word_flags+=("-o") flags+=("--template=") two_word_flags+=("-t") + flags+=("--to-version=") must_have_one_flag=() must_have_one_noun=() diff --git a/rel-eng/completions/bash/openshift b/rel-eng/completions/bash/openshift index 65159125dbe4..914591afc565 100644 --- a/rel-eng/completions/bash/openshift +++ b/rel-eng/completions/bash/openshift @@ -1820,6 +1820,7 @@ _openshift_cli_rollback() two_word_flags+=("-o") flags+=("--template=") two_word_flags+=("-t") + flags+=("--to-version=") must_have_one_flag=() must_have_one_noun=()