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=()