Browse code

Disable image triggers on rollback

Disable all image triggers on rollback to prevent undesirable
side-effects. Provide a new deploy option `--enable-triggers` to
allow users to re-enable image triggers automatically.

Dan Mace authored on 2015/06/12 05:48:47
Showing 5 changed files
... ...
@@ -34,6 +34,7 @@ type DeployOptions struct {
34 34
 	deployLatest         bool
35 35
 	retryDeploy          bool
36 36
 	cancelDeploy         bool
37
+	enableTriggers       bool
37 38
 }
38 39
 
39 40
 const (
... ...
@@ -86,6 +87,7 @@ func NewCmdDeploy(fullName string, f *clientcmd.Factory, out io.Writer) *cobra.C
86 86
 	cmd.Flags().BoolVar(&options.deployLatest, "latest", false, "Start a new deployment now.")
87 87
 	cmd.Flags().BoolVar(&options.retryDeploy, "retry", false, "Retry the latest failed deployment.")
88 88
 	cmd.Flags().BoolVar(&options.cancelDeploy, "cancel", false, "Cancel the in-progress deployment.")
89
+	cmd.Flags().BoolVar(&options.enableTriggers, "enable-triggers", false, "Enables all image triggers for the deployment config.")
89 90
 
90 91
 	return cmd
91 92
 }
... ...
@@ -181,6 +183,13 @@ func (o DeployOptions) RunDeploy() error {
181 181
 	case o.cancelDeploy:
182 182
 		c := &cancelDeploymentCommand{client: commandClient}
183 183
 		err = c.cancel(config, o.out)
184
+	case o.enableTriggers:
185
+		t := &triggerEnabler{
186
+			updateConfig: func(namespace string, config *deployapi.DeploymentConfig) (*deployapi.DeploymentConfig, error) {
187
+				return o.osClient.DeploymentConfigs(namespace).Update(config)
188
+			},
189
+		}
190
+		t.enableTriggers(config, o.out)
184 191
 	default:
185 192
 		describer := describe.NewLatestDeploymentsDescriber(o.osClient, o.kubeClient, -1)
186 193
 		desc, err := describer.Describe(config.Namespace, config.Name)
... ...
@@ -337,6 +346,33 @@ func (c *cancelDeploymentCommand) cancel(config *deployapi.DeploymentConfig, out
337 337
 	}
338 338
 }
339 339
 
340
+// triggerEnabler can enable image triggers for a config.
341
+type triggerEnabler struct {
342
+	// updateConfig persists config.
343
+	updateConfig func(namespace string, config *deployapi.DeploymentConfig) (*deployapi.DeploymentConfig, error)
344
+}
345
+
346
+// enableTriggers enables all image triggers and then persists config.
347
+func (t *triggerEnabler) enableTriggers(config *deployapi.DeploymentConfig, out io.Writer) error {
348
+	enabled := []string{}
349
+	for _, trigger := range config.Triggers {
350
+		if trigger.Type == deployapi.DeploymentTriggerOnImageChange {
351
+			trigger.ImageChangeParams.Automatic = true
352
+			enabled = append(enabled, trigger.ImageChangeParams.From.Name)
353
+		}
354
+	}
355
+	if len(enabled) == 0 {
356
+		fmt.Fprintln(out, "no image triggers found to enable")
357
+		return nil
358
+	}
359
+	_, err := t.updateConfig(config.Namespace, config)
360
+	if err != nil {
361
+		return err
362
+	}
363
+	fmt.Fprintf(out, "enabled image triggers: %s\n", strings.Join(enabled, ","))
364
+	return nil
365
+}
366
+
340 367
 // deployCommandClientImpl is a pluggable deployCommandClient.
341 368
 type deployCommandClientImpl struct {
342 369
 	GetDeploymentFn            func(namespace, name string) (*kapi.ReplicationController, error)
... ...
@@ -332,3 +332,44 @@ func TestCmdDeploy_cancelOk(t *testing.T) {
332 332
 		}
333 333
 	}
334 334
 }
335
+
336
+func TestDeploy_triggerEnable(t *testing.T) {
337
+	var updated *deployapi.DeploymentConfig
338
+	triggerEnabler := &triggerEnabler{
339
+		updateConfig: func(namespace string, config *deployapi.DeploymentConfig) (*deployapi.DeploymentConfig, error) {
340
+			updated = config
341
+			return config, nil
342
+		},
343
+	}
344
+
345
+	mktrigger := func() deployapi.DeploymentTriggerPolicy {
346
+		t := deploytest.OkImageChangeTrigger()
347
+		t.ImageChangeParams.Automatic = false
348
+		return t
349
+	}
350
+	count := 3
351
+
352
+	config := deploytest.OkDeploymentConfig(1)
353
+	config.Triggers = []deployapi.DeploymentTriggerPolicy{}
354
+	for i := 0; i < count; i++ {
355
+		config.Triggers = append(config.Triggers, mktrigger())
356
+	}
357
+
358
+	err := triggerEnabler.enableTriggers(config, ioutil.Discard)
359
+	if err != nil {
360
+		t.Fatalf("unexpected error: %v", err)
361
+	}
362
+
363
+	if updated == nil {
364
+		t.Fatalf("expected an updated config")
365
+	}
366
+
367
+	if e, a := count, len(config.Triggers); e != a {
368
+		t.Fatalf("expected %d triggers, got %d", e, a)
369
+	}
370
+	for _, trigger := range config.Triggers {
371
+		if !trigger.ImageChangeParams.Automatic {
372
+			t.Errorf("expected trigger to be enabled: %#v", trigger.ImageChangeParams)
373
+		}
374
+	}
375
+}
... ...
@@ -3,13 +3,16 @@ package cmd
3 3
 import (
4 4
 	"fmt"
5 5
 	"io"
6
+	"strings"
6 7
 
7 8
 	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
9
+	kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
8 10
 	kubectl "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl"
9 11
 	cmdutil "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/util"
10 12
 	"github.com/spf13/cobra"
11 13
 
12 14
 	latest "github.com/openshift/origin/pkg/api/latest"
15
+	"github.com/openshift/origin/pkg/client"
13 16
 	describe "github.com/openshift/origin/pkg/cmd/cli/describe"
14 17
 	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
15 18
 	deployapi "github.com/openshift/origin/pkg/deploy/api"
... ...
@@ -25,6 +28,11 @@ environment variables and volumes are included in rollbacks, so if you've
25 25
 recently updated security credentials in your environment your previous
26 26
 deployment may not have the correct values.
27 27
 
28
+Any image triggers present in the rolled back configuration will be disabled
29
+with a warning. This is to help prevent your rolled back deployment from being
30
+replaced by a triggered deployment soon after your rollback. To re-enable the
31
+triggers, use the 'deploy' command.
32
+
28 33
 If you would like to review the outcome of the rollback, pass '--dry-run' to print
29 34
 a human-readable representation of the updated deployment configuration instead of
30 35
 executing the rollback. This is useful if you're not quite sure what the outcome
... ...
@@ -40,7 +48,7 @@ will be.`
40 40
   $ %[1]s rollback deployment-1 --output=json | %[1]s update deploymentConfigs deployment -f -`
41 41
 )
42 42
 
43
-// NewCmdRollback implements the OpenShift cli rollback command
43
+// NewCmdRollback creates a CLI rollback command.
44 44
 func NewCmdRollback(fullName string, f *clientcmd.Factory, out io.Writer) *cobra.Command {
45 45
 	rollback := &deployapi.DeploymentConfigRollback{
46 46
 		Spec: deployapi.DeploymentConfigRollbackSpec{
... ...
@@ -54,8 +62,60 @@ func NewCmdRollback(fullName string, f *clientcmd.Factory, out io.Writer) *cobra
54 54
 		Long:    rollbackLong,
55 55
 		Example: fmt.Sprintf(rollbackExample, fullName),
56 56
 		Run: func(cmd *cobra.Command, args []string) {
57
-			err := RunRollback(f, out, cmd, args, rollback)
57
+			// Validate arguments
58
+			if len(args) == 0 || len(args[0]) == 0 {
59
+				cmdutil.CheckErr(cmdutil.UsageError(cmd, "A deployment name is required."))
60
+			}
61
+
62
+			// Extract arguments
63
+			format := cmdutil.GetFlagString(cmd, "output")
64
+			template := cmdutil.GetFlagString(cmd, "template")
65
+			dryRun := cmdutil.GetFlagBool(cmd, "dry-run")
66
+
67
+			// Get globally provided stuff
68
+			namespace, err := f.DefaultNamespace()
69
+			cmdutil.CheckErr(err)
70
+			oClient, kClient, err := f.Clients()
71
+			cmdutil.CheckErr(err)
72
+
73
+			// Set up the rollback config
74
+			rollback.Spec.From.Name = args[0]
75
+
76
+			// Make a helper and generate a rolled back config
77
+			helper := newHelper(oClient, kClient)
78
+			config, err := helper.Generate(namespace, rollback)
79
+			cmdutil.CheckErr(err)
80
+
81
+			// If this is a dry run, print and exit
82
+			if dryRun {
83
+				err := helper.Describe(config, out)
84
+				cmdutil.CheckErr(err)
85
+				return
86
+			}
87
+
88
+			// If an output format is specified, print and exit
89
+			if len(format) > 0 {
90
+				err := helper.Print(config, format, template, out)
91
+				cmdutil.CheckErr(err)
92
+				return
93
+			}
94
+
95
+			// Perform the rollback
96
+			rolledback, err := helper.Update(config)
58 97
 			cmdutil.CheckErr(err)
98
+
99
+			// Notify the user of any disabled image triggers
100
+			fmt.Fprintf(out, "#%d rolled back to %s\n", rolledback.LatestVersion, rollback.Spec.From.Name)
101
+			for _, trigger := range rolledback.Triggers {
102
+				disabled := []string{}
103
+				if trigger.Type == deployapi.DeploymentTriggerOnImageChange && !trigger.ImageChangeParams.Automatic {
104
+					disabled = append(disabled, trigger.ImageChangeParams.From.Name)
105
+				}
106
+				if len(disabled) > 0 {
107
+					reenable := fmt.Sprintf("%s deploy %s --enable-triggers", fullName, rolledback.Name)
108
+					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)
109
+				}
110
+			}
59 111
 		},
60 112
 	}
61 113
 
... ...
@@ -69,58 +129,60 @@ func NewCmdRollback(fullName string, f *clientcmd.Factory, out io.Writer) *cobra
69 69
 	return cmd
70 70
 }
71 71
 
72
-// RunRollback contains all the necessary functionality for OpenShift cli rollback command
73
-func RunRollback(f *clientcmd.Factory, out io.Writer, cmd *cobra.Command, args []string, rollback *deployapi.DeploymentConfigRollback) error {
74
-	if len(args) == 0 || len(args[0]) == 0 {
75
-		return cmdutil.UsageError(cmd, "A deployment name is required.")
72
+// newHelper makes a hew helper using real clients.
73
+func newHelper(oClient client.Interface, kClient kclient.Interface) *helper {
74
+	return &helper{
75
+		generateRollback: func(namespace string, config *deployapi.DeploymentConfigRollback) (*deployapi.DeploymentConfig, error) {
76
+			return oClient.DeploymentConfigs(namespace).Rollback(config)
77
+		},
78
+		describe: func(config *deployapi.DeploymentConfig) (string, error) {
79
+			describer := describe.NewDeploymentConfigDescriberForConfig(oClient, kClient, config)
80
+			return describer.Describe(config.Namespace, config.Name)
81
+		},
82
+		updateConfig: func(namespace string, config *deployapi.DeploymentConfig) (*deployapi.DeploymentConfig, error) {
83
+			return oClient.DeploymentConfigs(namespace).Update(config)
84
+		},
76 85
 	}
86
+}
77 87
 
78
-	rollback.Spec.From.Name = args[0]
79
-
80
-	outputFormat := cmdutil.GetFlagString(cmd, "output")
81
-	outputTemplate := cmdutil.GetFlagString(cmd, "template")
82
-	dryRun := cmdutil.GetFlagBool(cmd, "dry-run")
88
+// helper knows how to perform various rollback related tasks.
89
+type helper struct {
90
+	// generateRollback generates a rolled back config from the input config
91
+	generateRollback func(namespace string, config *deployapi.DeploymentConfigRollback) (*deployapi.DeploymentConfig, error)
92
+	// describe returns the describer output for config
93
+	describe func(config *deployapi.DeploymentConfig) (string, error)
94
+	// updateConfig persists config
95
+	updateConfig func(namespace string, config *deployapi.DeploymentConfig) (*deployapi.DeploymentConfig, error)
96
+}
83 97
 
84
-	osClient, kClient, err := f.Clients()
85
-	if err != nil {
86
-		return err
87
-	}
98
+// Generate generates a rolled back DeploymentConfig.
99
+func (r *helper) Generate(namespace string, config *deployapi.DeploymentConfigRollback) (*deployapi.DeploymentConfig, error) {
100
+	return r.generateRollback(namespace, config)
101
+}
88 102
 
89
-	namespace, err := f.DefaultNamespace()
103
+// Describe describes a DeploymentConfig.
104
+func (r *helper) Describe(config *deployapi.DeploymentConfig, out io.Writer) error {
105
+	description, err := r.describe(config)
90 106
 	if err != nil {
91 107
 		return err
92 108
 	}
109
+	out.Write([]byte(description))
110
+	return nil
111
+}
93 112
 
94
-	// Generate the rollback config
95
-	newConfig, err := osClient.DeploymentConfigs(namespace).Rollback(rollback)
113
+// Print prints a deployment config in the specified format with the given
114
+// template.
115
+func (r *helper) Print(config *deployapi.DeploymentConfig, format, template string, out io.Writer) error {
116
+	printer, _, err := kubectl.GetPrinter(format, template)
96 117
 	if err != nil {
97 118
 		return err
98 119
 	}
120
+	versionedPrinter := kubectl.NewVersionedPrinter(printer, kapi.Scheme, latest.Version)
121
+	versionedPrinter.PrintObj(config, out)
122
+	return nil
123
+}
99 124
 
100
-	// If dry-run is specified, describe the rollback and exit
101
-	if dryRun {
102
-		describer := describe.NewDeploymentConfigDescriberForConfig(osClient, kClient, newConfig)
103
-		description, err := describer.Describe(newConfig.Namespace, newConfig.Name)
104
-		if err != nil {
105
-			return err
106
-		}
107
-		out.Write([]byte(description))
108
-		return nil
109
-	}
110
-
111
-	// If an output format is specified, display the rollback config JSON and exit
112
-	// WITHOUT performing a rollback.
113
-	if len(outputFormat) > 0 {
114
-		printer, _, err := kubectl.GetPrinter(outputFormat, outputTemplate)
115
-		if err != nil {
116
-			return err
117
-		}
118
-		versionedPrinter := kubectl.NewVersionedPrinter(printer, kapi.Scheme, latest.Version)
119
-		versionedPrinter.PrintObj(newConfig, out)
120
-		return nil
121
-	}
122
-
123
-	// Apply the rollback config
124
-	_, err = osClient.DeploymentConfigs(namespace).Update(newConfig)
125
-	return err
125
+// Update persists the given DeploymentConfig.
126
+func (r *helper) Update(config *deployapi.DeploymentConfig) (*deployapi.DeploymentConfig, error) {
127
+	return r.updateConfig(config.Namespace, config)
126 128
 }
... ...
@@ -12,9 +12,14 @@ import (
12 12
 // in a configurable way.
13 13
 type RollbackGenerator struct{}
14 14
 
15
-// GenerateRollback creates a new DeploymentConfig by merging to onto from based on the options provided
16
-// by spec. The LatestVersion of the result is unconditionally incremented, as rollback candidates are
17
-// should be possible to be deployed manually regardless of other system behavior such as triggering.
15
+// GenerateRollback creates a new DeploymentConfig by merging to onto from
16
+// based on the options provided by spec. The LatestVersion of the result is
17
+// unconditionally incremented, as rollback candidates are should be possible
18
+// to be deployed manually regardless of other system behavior such as
19
+// triggering.
20
+//
21
+// Any image change triggers on the new config are disabled to prevent
22
+// triggered deployments from immediately replacing the rollback.
18 23
 func (g *RollbackGenerator) GenerateRollback(from, to *deployapi.DeploymentConfig, spec *deployapi.DeploymentConfigRollbackSpec) (*deployapi.DeploymentConfig, error) {
19 24
 	rollback := &deployapi.DeploymentConfig{}
20 25
 
... ...
@@ -49,6 +54,13 @@ func (g *RollbackGenerator) GenerateRollback(from, to *deployapi.DeploymentConfi
49 49
 		}
50 50
 	}
51 51
 
52
+	// Disable any image change triggers.
53
+	for _, trigger := range rollback.Triggers {
54
+		if trigger.Type == deployapi.DeploymentTriggerOnImageChange {
55
+			trigger.ImageChangeParams.Automatic = false
56
+		}
57
+	}
58
+
52 59
 	// TODO: add a new cause?
53 60
 	rollback.LatestVersion++
54 61
 
... ...
@@ -15,6 +15,7 @@ func TestGeneration(t *testing.T) {
15 15
 		Type: deployapi.DeploymentStrategyTypeCustom,
16 16
 	}
17 17
 	from.Triggers = append(from.Triggers, deployapi.DeploymentTriggerPolicy{Type: deployapi.DeploymentTriggerOnConfigChange})
18
+	from.Triggers = append(from.Triggers, deploytest.OkImageChangeTrigger())
18 19
 	from.Template.ControllerTemplate.Template.Spec.Containers[0].Name = "changed"
19 20
 	from.Template.ControllerTemplate.Replicas = 5
20 21
 	from.Template.ControllerTemplate.Selector = map[string]string{
... ...
@@ -64,6 +65,12 @@ func TestGeneration(t *testing.T) {
64 64
 			if hasReplicationMetaDiff(from, rollback) && !spec.IncludeReplicationMeta {
65 65
 				t.Fatalf("unexpected replication meta diff: from=%v, rollback=%v", from, rollback)
66 66
 			}
67
+
68
+			for i, trigger := range rollback.Triggers {
69
+				if trigger.Type == deployapi.DeploymentTriggerOnImageChange && trigger.ImageChangeParams.Automatic {
70
+					t.Errorf("image change trigger %d should be disabled", i)
71
+				}
72
+			}
67 73
 		}
68 74
 	}
69 75
 }