From 7df83d2c9ec2dec572e5bbd4cee27b4d62899d9a Mon Sep 17 00:00:00 2001 From: Dan Mace Date: Wed, 28 Jan 2015 17:31:56 -0500 Subject: [PATCH] Implement deployment rollback CLI support Add a new 'rollback' osc command which exposes deployment rollback functionality for end-users. --- pkg/client/deploymentconfigs.go | 12 ++ pkg/client/fake_deploymentconfigs.go | 5 + pkg/cmd/cli/cli.go | 2 + pkg/cmd/cli/cmd/rollback.go | 108 +++++++++++++++++ pkg/cmd/cli/describe/deployments.go | 160 +++++++++++++++++++++++++ pkg/cmd/cli/describe/describer.go | 53 +------- pkg/cmd/cli/describe/describer_test.go | 24 +++- pkg/deploy/api/test/ok.go | 13 ++ pkg/deploy/rollback/rest.go | 26 ++-- pkg/deploy/rollback/rest_test.go | 3 +- 10 files changed, 345 insertions(+), 61 deletions(-) create mode 100644 pkg/cmd/cli/cmd/rollback.go create mode 100644 pkg/cmd/cli/describe/deployments.go diff --git a/pkg/client/deploymentconfigs.go b/pkg/client/deploymentconfigs.go index 91e610a5d54c..19a7b9288ded 100644 --- a/pkg/client/deploymentconfigs.go +++ b/pkg/client/deploymentconfigs.go @@ -21,6 +21,7 @@ type DeploymentConfigInterface interface { Delete(name string) error Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) Generate(name string) (*deployapi.DeploymentConfig, error) + Rollback(config *deployapi.DeploymentConfigRollback) (*deployapi.DeploymentConfig, error) } // deploymentConfigs implements DeploymentConfigsNamespacer interface @@ -94,3 +95,14 @@ func (c *deploymentConfigs) Generate(name string) (result *deployapi.DeploymentC err = c.r.Get().Namespace(c.ns).Resource("generateDeploymentConfigs").Name(name).Do().Into(result) return } + +func (c *deploymentConfigs) Rollback(config *deployapi.DeploymentConfigRollback) (result *deployapi.DeploymentConfig, err error) { + result = &deployapi.DeploymentConfig{} + err = c.r.Post(). + Namespace(c.ns). + Resource("deploymentConfigRollbacks"). + Body(config). + Do(). + Into(result) + return +} diff --git a/pkg/client/fake_deploymentconfigs.go b/pkg/client/fake_deploymentconfigs.go index 90f7bf44bf43..69c4f3d43d6f 100644 --- a/pkg/client/fake_deploymentconfigs.go +++ b/pkg/client/fake_deploymentconfigs.go @@ -48,3 +48,8 @@ func (c *FakeDeploymentConfigs) Generate(name string) (*deployapi.DeploymentConf c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "generate-deploymentconfig"}) return nil, nil } + +func (c *FakeDeploymentConfigs) Rollback(config *deployapi.DeploymentConfigRollback) (result *deployapi.DeploymentConfig, err error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "rollback"}) + return nil, nil +} diff --git a/pkg/cmd/cli/cli.go b/pkg/cmd/cli/cli.go index 0da498804511..391f54f18f04 100644 --- a/pkg/cmd/cli/cli.go +++ b/pkg/cmd/cli/cli.go @@ -71,6 +71,8 @@ func NewCommandCLI(name string) *cobra.Command { cmds.AddCommand(cmd.NewCmdStartBuild(f, out)) cmds.AddCommand(cmd.NewCmdCancelBuild(f, out)) + cmds.AddCommand(cmd.NewCmdRollback(name, "rollback", f, out)) + return cmds } diff --git a/pkg/cmd/cli/cmd/rollback.go b/pkg/cmd/cli/cmd/rollback.go new file mode 100644 index 000000000000..101da569ad7c --- /dev/null +++ b/pkg/cmd/cli/cmd/rollback.go @@ -0,0 +1,108 @@ +package cmd + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + + kubectl "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" + kcmd "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd" + + describe "github.com/openshift/origin/pkg/cmd/cli/describe" + deployapi "github.com/openshift/origin/pkg/deploy/api" +) + +const rollbackLongDesc = ` +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. + +If you would like to review the outcome of the rollback, pass '--dry-run' to print +a human-readable representation of the updated deployment configuration instead of +executing the rollback. This is useful if you're not quite sure what the outcome +will be. + +Examples: + Perform a rollback: + + $ %[1]s %[2]s deployment-1 + + See what the rollback will look like, but don't perform the rollback: + + $ %[1]s %[2]s deployment-1 --dry-run + + Perform the rollback manually by piping the JSON of the new config back to %[1]s: + + $ %[1]s %[2]s deployment-1 --output=json | %[1]s update deploymentConfigs deployment -f - +` + +func NewCmdRollback(parentName string, name string, f *Factory, out io.Writer) *cobra.Command { + rollback := &deployapi.DeploymentConfigRollback{ + Spec: deployapi.DeploymentConfigRollbackSpec{ + IncludeTemplate: true, + }, + } + + cmd := &cobra.Command{ + Use: fmt.Sprintf("%s ", name), + Short: "Revert part of an application back to a previous deployment.", + Long: fmt.Sprintf(rollbackLongDesc, parentName, name), + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 || len(args[0]) == 0 { + usageError(cmd, "A deployment name is required.") + } + + rollback.Spec.From.Name = args[0] + + outputFormat := kcmd.GetFlagString(cmd, "output") + outputTemplate := kcmd.GetFlagString(cmd, "template") + dryRun := kcmd.GetFlagBool(cmd, "dry-run") + + osClient, _, err := f.Clients(cmd) + checkErr(err) + + namespace := getOriginNamespace(cmd) + + // Generate the rollback config + newConfig, err := osClient.DeploymentConfigs(namespace).Rollback(rollback) + checkErr(err) + + // If dry-run is specified, describe the rollback and exit + if dryRun { + describer := describe.NewDeploymentConfigDescriberForConfig(newConfig) + description, descErr := describer.Describe(newConfig.Namespace, newConfig.Name) + checkErr(descErr) + out.Write([]byte(description)) + return + } + + // If an output format is specified, display the rollback config JSON and exit + // WITHOUT performing a rollback. + if len(outputFormat) > 0 { + printer, _, perr := kubectl.GetPrinter(outputFormat, outputTemplate) + checkErr(perr) + printer.PrintObj(newConfig, out) + return + } + + // Apply the rollback config + _, updateErr := osClient.DeploymentConfigs(namespace).Update(newConfig) + checkErr(updateErr) + }, + } + + 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.") + + return cmd +} diff --git a/pkg/cmd/cli/describe/deployments.go b/pkg/cmd/cli/describe/deployments.go new file mode 100644 index 000000000000..700a0b28af9f --- /dev/null +++ b/pkg/cmd/cli/describe/deployments.go @@ -0,0 +1,160 @@ +package describe + +import ( + "fmt" + "io" + "strconv" + "strings" + "text/tabwriter" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + + "github.com/openshift/origin/pkg/client" + deployapi "github.com/openshift/origin/pkg/deploy/api" +) + +// DeploymentConfigDescriber generates information about a DeploymentConfig +type DeploymentConfigDescriber struct { + client deploymentDescriberClient +} + +type deploymentDescriberClient interface { + GetDeploymentConfig(namespace, name string) (*deployapi.DeploymentConfig, error) +} + +type genericDeploymentDescriberClient struct { + getDeploymentConfig func(namespace, name string) (*deployapi.DeploymentConfig, error) +} + +func (c *genericDeploymentDescriberClient) GetDeploymentConfig(namespace, name string) (*deployapi.DeploymentConfig, error) { + return c.getDeploymentConfig(namespace, name) +} + +func NewDeploymentConfigDescriberForConfig(config *deployapi.DeploymentConfig) *DeploymentConfigDescriber { + return &DeploymentConfigDescriber{ + client: &genericDeploymentDescriberClient{ + getDeploymentConfig: func(namespace, name string) (*deployapi.DeploymentConfig, error) { + return config, nil + }, + }, + } +} + +func NewDeploymentConfigDescriber(client client.Interface) *DeploymentConfigDescriber { + return &DeploymentConfigDescriber{ + client: &genericDeploymentDescriberClient{ + getDeploymentConfig: func(namespace, name string) (*deployapi.DeploymentConfig, error) { + return client.DeploymentConfigs(namespace).Get(name) + }, + }, + } +} + +func (d *DeploymentConfigDescriber) Describe(namespace, name string) (string, error) { + deploymentConfig, err := d.client.GetDeploymentConfig(namespace, name) + if err != nil { + return "", err + } + + return tabbedString(func(out *tabwriter.Writer) error { + formatMeta(out, deploymentConfig.ObjectMeta) + + if deploymentConfig.LatestVersion == 0 { + formatString(out, "Latest Version", "Not deployed") + } else { + formatString(out, "Latest Version", strconv.Itoa(deploymentConfig.LatestVersion)) + } + + printStrategy(deploymentConfig.Template.Strategy, out) + printTriggers(deploymentConfig.Triggers, out) + printReplicationController(deploymentConfig.Template.ControllerTemplate, out) + + return nil + }) +} + +func printStrategy(strategy deployapi.DeploymentStrategy, w io.Writer) { + fmt.Fprintf(w, "Strategy:\t%s\n", strategy.Type) + switch strategy.Type { + case deployapi.DeploymentStrategyTypeRecreate: + case deployapi.DeploymentStrategyTypeCustom: + fmt.Fprintf(w, "\t- Image:\t%s\n", strategy.CustomParams.Image) + + if len(strategy.CustomParams.Environment) > 0 { + fmt.Fprintf(w, "\t- Environment:\t%s\n", formatLabels(convertEnv(strategy.CustomParams.Environment))) + } + + if len(strategy.CustomParams.Command) > 0 { + fmt.Fprintf(w, "\t- Command:\t%v\n", strings.Join(strategy.CustomParams.Command, " ")) + } + } +} + +func printTriggers(triggers []deployapi.DeploymentTriggerPolicy, w io.Writer) { + if len(triggers) == 0 { + fmt.Fprint(w, "No triggers.") + return + } + + fmt.Fprint(w, "Triggers:\n") + for _, t := range triggers { + fmt.Fprintf(w, "\t- %s\n", t.Type) + switch t.Type { + case deployapi.DeploymentTriggerOnConfigChange: + fmt.Fprintf(w, "\t\t\n") + case deployapi.DeploymentTriggerOnImageChange: + fmt.Fprintf(w, "\t\tAutomatic:\t%v\n\t\tRepository:\t%s\n\t\tTag:\t%s\n", + t.ImageChangeParams.Automatic, + t.ImageChangeParams.RepositoryName, + t.ImageChangeParams.Tag, + ) + default: + fmt.Fprint(w, "unknown\n") + } + } +} + +func printReplicationController(spec kapi.ReplicationControllerSpec, w io.Writer) error { + fmt.Fprint(w, "Template:\n") + + fmt.Fprintf(w, "\tSelector:\t%s\n\tReplicas:\t%d\n", + formatLabels(spec.Selector), + spec.Replicas) + + fmt.Fprintf(w, "\tContainers:\n\t\tNAME\tIMAGE\tENV\n") + for _, container := range spec.Template.Spec.Containers { + fmt.Fprintf(w, "\t\t%s\t%s\t%s\n", + container.Name, + container.Image, + formatLabels(convertEnv(container.Env))) + } + return nil +} + +// DeploymentDescriber generates information about a deployment +// DEPRECATED. +type DeploymentDescriber struct { + client.Interface +} + +func (d *DeploymentDescriber) Describe(namespace, name string) (string, error) { + c := d.Deployments(namespace) + deployment, err := c.Get(name) + if err != nil { + return "", err + } + + return tabbedString(func(out *tabwriter.Writer) error { + formatMeta(out, deployment.ObjectMeta) + formatString(out, "Status", bold(deployment.Status)) + formatString(out, "Strategy", deployment.Strategy.Type) + causes := []string{} + if deployment.Details != nil { + for _, c := range deployment.Details.Causes { + causes = append(causes, string(c.Type)) + } + } + formatString(out, "Causes", strings.Join(causes, ",")) + return nil + }) +} diff --git a/pkg/cmd/cli/describe/describer.go b/pkg/cmd/cli/describe/describer.go index d04538fef606..1e1c8265a014 100644 --- a/pkg/cmd/cli/describe/describer.go +++ b/pkg/cmd/cli/describe/describer.go @@ -20,7 +20,7 @@ func DescriberFor(kind string, c *client.Client, host string) (kctl.Describer, b case "Deployment": return &DeploymentDescriber{c}, true case "DeploymentConfig": - return &DeploymentConfigDescriber{c}, true + return NewDeploymentConfigDescriber(c), true case "Image": return &ImageDescriber{c}, true case "ImageRepository": @@ -157,57 +157,6 @@ func (d *BuildConfigDescriber) Describe(namespace, name string) (string, error) }) } -// DeploymentDescriber generates information about a deployment -type DeploymentDescriber struct { - client.Interface -} - -func (d *DeploymentDescriber) Describe(namespace, name string) (string, error) { - c := d.Deployments(namespace) - deployment, err := c.Get(name) - if err != nil { - return "", err - } - - return tabbedString(func(out *tabwriter.Writer) error { - formatMeta(out, deployment.ObjectMeta) - formatString(out, "Status", bold(deployment.Status)) - formatString(out, "Strategy", deployment.Strategy.Type) - causes := []string{} - if deployment.Details != nil { - for _, c := range deployment.Details.Causes { - causes = append(causes, string(c.Type)) - } - } - formatString(out, "Causes", strings.Join(causes, ",")) - return nil - }) -} - -// DeploymentConfigDescriber generates information about a DeploymentConfig -type DeploymentConfigDescriber struct { - client.Interface -} - -func (d *DeploymentConfigDescriber) Describe(namespace, name string) (string, error) { - c := d.DeploymentConfigs(namespace) - deploymentConfig, err := c.Get(name) - if err != nil { - return "", err - } - - return tabbedString(func(out *tabwriter.Writer) error { - formatMeta(out, deploymentConfig.ObjectMeta) - formatString(out, "Latest Version", deploymentConfig.LatestVersion) - triggers := []string{} - for _, t := range deploymentConfig.Triggers { - triggers = append(triggers, string(t.Type)) - } - formatString(out, "Triggers", strings.Join(triggers, ",")) - return nil - }) -} - // ImageDescriber generates information about a Image type ImageDescriber struct { client.Interface diff --git a/pkg/cmd/cli/describe/describer_test.go b/pkg/cmd/cli/describe/describer_test.go index eb7b1486a28a..df381fcadd81 100644 --- a/pkg/cmd/cli/describe/describer_test.go +++ b/pkg/cmd/cli/describe/describer_test.go @@ -6,6 +6,8 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" "github.com/openshift/origin/pkg/client" + + deployapitest "github.com/openshift/origin/pkg/deploy/api/test" ) type describeClient struct { @@ -37,7 +39,6 @@ func TestDescribers(t *testing.T) { &BuildDescriber{c}, &BuildConfigDescriber{c, ""}, &DeploymentDescriber{c}, - &DeploymentConfigDescriber{c}, &ImageDescriber{c}, &ImageRepositoryDescriber{c}, &RouteDescriber{c}, @@ -54,3 +55,24 @@ func TestDescribers(t *testing.T) { } } } + +func TestDeploymentConfigDescriber(t *testing.T) { + config := deployapitest.OkDeploymentConfig(0) + d := NewDeploymentConfigDescriberForConfig(config) + + describe := func() { + if output, err := d.Describe("test", "deployment"); err != nil { + t.Fatalf("unexpected error: %v", err) + } else { + t.Logf("describer output:\n%s\n", output) + } + } + + describe() + + config.Triggers = append(config.Triggers, deployapitest.OkConfigChangeTrigger()) + describe() + + config.Template.Strategy = deployapitest.OkCustomStrategy() + describe() +} diff --git a/pkg/deploy/api/test/ok.go b/pkg/deploy/api/test/ok.go index a33a3eb675e4..361ad7055927 100644 --- a/pkg/deploy/api/test/ok.go +++ b/pkg/deploy/api/test/ok.go @@ -25,6 +25,13 @@ func OkCustomStrategy() deployapi.DeploymentStrategy { func OkCustomParams() *deployapi.CustomDeploymentStrategyParams { return &deployapi.CustomDeploymentStrategyParams{ Image: "openshift/origin-deployer", + Environment: []kapi.EnvVar{ + { + Name: "ENV1", + Value: "VAL1", + }, + }, + Command: []string{"/bin/echo", "hello", "world"}, } } @@ -56,6 +63,12 @@ func OkPodTemplate() *kapi.PodTemplateSpec { Image: "registry:8080/repo1:ref1", CPU: resource.Quantity{Amount: inf.NewDec(0, 3), Format: "DecimalSI"}, Memory: resource.Quantity{Amount: inf.NewDec(0, 0), Format: "DecimalSI"}, + Env: []kapi.EnvVar{ + { + Name: "ENV1", + Value: "VAL1", + }, + }, }, { Name: "container2", diff --git a/pkg/deploy/rollback/rest.go b/pkg/deploy/rollback/rest.go index eac02d0a24de..8c49baa805eb 100644 --- a/pkg/deploy/rollback/rest.go +++ b/pkg/deploy/rollback/rest.go @@ -63,7 +63,7 @@ func (s *REST) Create(ctx kapi.Context, obj runtime.Object) (<-chan apiserver.RE } if errs := validation.ValidateDeploymentConfigRollback(rollback); len(errs) > 0 { - return nil, kerrors.NewInvalid("deploymentConfigRollback", "", errs) + return nil, kerrors.NewInvalid("DeploymentConfigRollback", "", errs) } // Roll back "from" the current deployment "to" a target deployment @@ -71,23 +71,35 @@ func (s *REST) Create(ctx kapi.Context, obj runtime.Object) (<-chan apiserver.RE // Find the target ("to") deployment and decode the DeploymentConfig targetDeployment, err := s.generator.GetDeployment(ctx, rollback.Spec.From.Name) if err != nil { - // TODO: correct error type? - return nil, kerrors.NewBadRequest(fmt.Sprintf("Couldn't get specified deployment: %v", err)) + if kerrors.IsNotFound(err) { + return nil, newInvalidDeploymentError(rollback, "Deployment not found") + } + return nil, newInvalidDeploymentError(rollback, fmt.Sprintf("%v", err)) } + to, err := deployutil.DecodeDeploymentConfig(targetDeployment, s.codec) if err != nil { - // TODO: correct error type? - return nil, kerrors.NewBadRequest(fmt.Sprintf("deploymentConfig on target deployment is invalid: %v", err)) + return nil, newInvalidDeploymentError(rollback, + fmt.Sprintf("Couldn't decode deploymentConfig from deployment: %v", err)) } // Find the current ("from") version of the target deploymentConfig from, err := s.generator.GetDeploymentConfig(ctx, to.Name) if err != nil { - // TODO: correct error type? - return nil, kerrors.NewBadRequest(fmt.Sprintf("Couldn't find current deploymentConfig %s/%s: %v", targetDeployment.Namespace, to.Name, err)) + if kerrors.IsNotFound(err) { + return nil, newInvalidDeploymentError(rollback, + fmt.Sprintf("Couldn't find a current deploymentConfig %s/%s", targetDeployment.Namespace, to.Name)) + } + return nil, newInvalidDeploymentError(rollback, + fmt.Sprintf("Error finding current deploymentConfig %s/%s: %v", targetDeployment.Namespace, to.Name, err)) } return apiserver.MakeAsync(func() (runtime.Object, error) { return s.generator.GenerateRollback(from, to, &rollback.Spec) }), nil } + +func newInvalidDeploymentError(rollback *deployapi.DeploymentConfigRollback, reason string) error { + err := kerrors.NewFieldInvalid("spec.from.name", rollback.Spec.From.Name, reason) + return kerrors.NewInvalid("DeploymentConfigRollback", "", kerrors.ValidationErrorList{err}) +} diff --git a/pkg/deploy/rollback/rest_test.go b/pkg/deploy/rollback/rest_test.go index df95536a2785..46e102383cde 100644 --- a/pkg/deploy/rollback/rest_test.go +++ b/pkg/deploy/rollback/rest_test.go @@ -2,6 +2,7 @@ package rollback import ( "errors" + "fmt" "testing" "time" @@ -88,7 +89,7 @@ func TestCreateGeneratorError(t *testing.T) { rest := REST{ generator: Client{ GRFn: func(from, to *deployapi.DeploymentConfig, spec *deployapi.DeploymentConfigRollbackSpec) (*deployapi.DeploymentConfig, error) { - return nil, errors.New("something terrible happened") + return nil, kerrors.NewInternalError(fmt.Errorf("something terrible happened")) }, RCFn: func(ctx kapi.Context, name string) (*kapi.ReplicationController, error) { deployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec)