Browse code

Set triggers via the CLI

`oc set triggers` will display, remove, update, or alter triggers on
deployment configs and build configs.

Clayton Coleman authored on 2016/02/18 15:16:36
Showing 7 changed files
... ...
@@ -1412,6 +1412,74 @@ _oc_set_probe()
1412 1412
     must_have_one_noun=()
1413 1413
 }
1414 1414
 
1415
+_oc_set_triggers()
1416
+{
1417
+    last_command="oc_set_triggers"
1418
+    commands=()
1419
+
1420
+    flags=()
1421
+    two_word_flags=()
1422
+    flags_with_completion=()
1423
+    flags_completion=()
1424
+
1425
+    flags+=("--all")
1426
+    flags+=("--auto")
1427
+    flags+=("--containers=")
1428
+    two_word_flags+=("-c")
1429
+    flags+=("--filename=")
1430
+    flags_with_completion+=("--filename")
1431
+    flags_completion+=("__handle_filename_extension_flag yaml|yml|json")
1432
+    two_word_flags+=("-f")
1433
+    flags_with_completion+=("-f")
1434
+    flags_completion+=("__handle_filename_extension_flag yaml|yml|json")
1435
+    flags+=("--from-config")
1436
+    flags+=("--from-github")
1437
+    flags+=("--from-image=")
1438
+    flags+=("--from-webhook")
1439
+    flags+=("--manual")
1440
+    flags+=("--no-headers")
1441
+    flags+=("--output=")
1442
+    two_word_flags+=("-o")
1443
+    flags+=("--output-version=")
1444
+    flags+=("--remove")
1445
+    flags+=("--remove-all")
1446
+    flags+=("--selector=")
1447
+    two_word_flags+=("-l")
1448
+    flags+=("--show-all")
1449
+    flags+=("-a")
1450
+    flags+=("--show-labels")
1451
+    flags+=("--sort-by=")
1452
+    flags+=("--template=")
1453
+    two_word_flags+=("-t")
1454
+    flags+=("--api-version=")
1455
+    flags+=("--certificate-authority=")
1456
+    flags_with_completion+=("--certificate-authority")
1457
+    flags_completion+=("_filedir")
1458
+    flags+=("--client-certificate=")
1459
+    flags_with_completion+=("--client-certificate")
1460
+    flags_completion+=("_filedir")
1461
+    flags+=("--client-key=")
1462
+    flags_with_completion+=("--client-key")
1463
+    flags_completion+=("_filedir")
1464
+    flags+=("--cluster=")
1465
+    flags+=("--config=")
1466
+    flags_with_completion+=("--config")
1467
+    flags_completion+=("_filedir")
1468
+    flags+=("--context=")
1469
+    flags+=("--google-json-key=")
1470
+    flags+=("--insecure-skip-tls-verify")
1471
+    flags+=("--log-flush-frequency=")
1472
+    flags+=("--match-server-version")
1473
+    flags+=("--namespace=")
1474
+    two_word_flags+=("-n")
1475
+    flags+=("--server=")
1476
+    flags+=("--token=")
1477
+    flags+=("--user=")
1478
+
1479
+    must_have_one_flag=()
1480
+    must_have_one_noun=()
1481
+}
1482
+
1415 1483
 _oc_set()
1416 1484
 {
1417 1485
     last_command="oc_set"
... ...
@@ -1419,6 +1487,7 @@ _oc_set()
1419 1419
     commands+=("env")
1420 1420
     commands+=("volumes")
1421 1421
     commands+=("probe")
1422
+    commands+=("triggers")
1422 1423
 
1423 1424
     flags=()
1424 1425
     two_word_flags=()
... ...
@@ -4749,6 +4749,74 @@ _openshift_cli_set_probe()
4749 4749
     must_have_one_noun=()
4750 4750
 }
4751 4751
 
4752
+_openshift_cli_set_triggers()
4753
+{
4754
+    last_command="openshift_cli_set_triggers"
4755
+    commands=()
4756
+
4757
+    flags=()
4758
+    two_word_flags=()
4759
+    flags_with_completion=()
4760
+    flags_completion=()
4761
+
4762
+    flags+=("--all")
4763
+    flags+=("--auto")
4764
+    flags+=("--containers=")
4765
+    two_word_flags+=("-c")
4766
+    flags+=("--filename=")
4767
+    flags_with_completion+=("--filename")
4768
+    flags_completion+=("__handle_filename_extension_flag yaml|yml|json")
4769
+    two_word_flags+=("-f")
4770
+    flags_with_completion+=("-f")
4771
+    flags_completion+=("__handle_filename_extension_flag yaml|yml|json")
4772
+    flags+=("--from-config")
4773
+    flags+=("--from-github")
4774
+    flags+=("--from-image=")
4775
+    flags+=("--from-webhook")
4776
+    flags+=("--manual")
4777
+    flags+=("--no-headers")
4778
+    flags+=("--output=")
4779
+    two_word_flags+=("-o")
4780
+    flags+=("--output-version=")
4781
+    flags+=("--remove")
4782
+    flags+=("--remove-all")
4783
+    flags+=("--selector=")
4784
+    two_word_flags+=("-l")
4785
+    flags+=("--show-all")
4786
+    flags+=("-a")
4787
+    flags+=("--show-labels")
4788
+    flags+=("--sort-by=")
4789
+    flags+=("--template=")
4790
+    two_word_flags+=("-t")
4791
+    flags+=("--api-version=")
4792
+    flags+=("--certificate-authority=")
4793
+    flags_with_completion+=("--certificate-authority")
4794
+    flags_completion+=("_filedir")
4795
+    flags+=("--client-certificate=")
4796
+    flags_with_completion+=("--client-certificate")
4797
+    flags_completion+=("_filedir")
4798
+    flags+=("--client-key=")
4799
+    flags_with_completion+=("--client-key")
4800
+    flags_completion+=("_filedir")
4801
+    flags+=("--cluster=")
4802
+    flags+=("--config=")
4803
+    flags_with_completion+=("--config")
4804
+    flags_completion+=("_filedir")
4805
+    flags+=("--context=")
4806
+    flags+=("--google-json-key=")
4807
+    flags+=("--insecure-skip-tls-verify")
4808
+    flags+=("--log-flush-frequency=")
4809
+    flags+=("--match-server-version")
4810
+    flags+=("--namespace=")
4811
+    two_word_flags+=("-n")
4812
+    flags+=("--server=")
4813
+    flags+=("--token=")
4814
+    flags+=("--user=")
4815
+
4816
+    must_have_one_flag=()
4817
+    must_have_one_noun=()
4818
+}
4819
+
4752 4820
 _openshift_cli_set()
4753 4821
 {
4754 4822
     last_command="openshift_cli_set"
... ...
@@ -4756,6 +4824,7 @@ _openshift_cli_set()
4756 4756
     commands+=("env")
4757 4757
     commands+=("volumes")
4758 4758
     commands+=("probe")
4759
+    commands+=("triggers")
4759 4760
 
4760 4761
     flags=()
4761 4762
     two_word_flags=()
... ...
@@ -1688,6 +1688,38 @@ Update a probe on a pod template
1688 1688
 ====
1689 1689
 
1690 1690
 
1691
+== oc set triggers
1692
+Update the triggers on a build or deployment config
1693
+
1694
+====
1695
+
1696
+[options="nowrap"]
1697
+----
1698
+  # Print the triggers on the registry
1699
+  $ oc set triggers dc/registry
1700
+
1701
+  # Set all triggers to manual
1702
+  $ oc set triggers dc/registry --manual
1703
+
1704
+  # Enable all automatic triggers
1705
+  $ oc set triggers dc/registry --auto
1706
+
1707
+  # Reset the GitHub webhook on a build to a new, generated secret
1708
+  $ oc set triggers bc/webapp --from-github=
1709
+  $ oc set triggers bc/webapp --from-webhook=
1710
+
1711
+  # Remove all triggers
1712
+  $ oc set triggers bc/webapp --remove-all
1713
+
1714
+  # Stop triggering on config change
1715
+  $ oc set triggers dc/registry --from-config --remove
1716
+
1717
+  # Add an image trigger to a build config
1718
+  $ oc set triggers bc/webapp --from-image=namespace1/image:latest
1719
+----
1720
+====
1721
+
1722
+
1691 1723
 == oc set volumes
1692 1724
 Update volumes on a pod template
1693 1725
 
... ...
@@ -38,6 +38,12 @@ func NewCmdSet(fullName string, f *clientcmd.Factory, in io.Reader, out, errout
38 38
 				NewCmdProbe(name, f, out, errout),
39 39
 			},
40 40
 		},
41
+		{
42
+			Message: "Manage application flows:",
43
+			Commands: []*cobra.Command{
44
+				NewCmdTriggers(name, f, out, errout),
45
+			},
46
+		},
41 47
 	}
42 48
 	groups.Add(set)
43 49
 	templates.ActsAsRootCommand(set, []string{"options"}, groups...)
44 50
new file mode 100644
... ...
@@ -0,0 +1,770 @@
0
+package set
1
+
2
+import (
3
+	"fmt"
4
+	"io"
5
+	"os"
6
+	"reflect"
7
+	"strings"
8
+	"text/tabwriter"
9
+
10
+	"github.com/golang/glog"
11
+	"github.com/spf13/cobra"
12
+
13
+	kapi "k8s.io/kubernetes/pkg/api"
14
+	"k8s.io/kubernetes/pkg/api/meta"
15
+	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
16
+	"k8s.io/kubernetes/pkg/kubectl/resource"
17
+	"k8s.io/kubernetes/pkg/runtime"
18
+
19
+	buildapi "github.com/openshift/origin/pkg/build/api"
20
+	buildutil "github.com/openshift/origin/pkg/build/util"
21
+	cmdutil "github.com/openshift/origin/pkg/cmd/util"
22
+	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
23
+	deployapi "github.com/openshift/origin/pkg/deploy/api"
24
+	"github.com/openshift/origin/pkg/generate/app"
25
+	imageapi "github.com/openshift/origin/pkg/image/api"
26
+	"k8s.io/kubernetes/pkg/util/sets"
27
+)
28
+
29
+const (
30
+	triggersLong = `
31
+Set or remove triggers for build configs and deployment configs
32
+
33
+All build configs and deployment configs may have a set of triggers that result in a new deployment
34
+or build being created. This command enables you to alter those triggers - making them automatic or
35
+manual, adding new entries, or changing existing entries.
36
+
37
+Deployments support triggering off of image changes and on config changes. Config changes are any
38
+alterations to the pod template, while image changes will result in the container image value being
39
+updated whenever an image stream tag is updated.
40
+
41
+Build configs support triggering off of image changes, config changes, and webhooks (both GitHub-specific
42
+and generic). The config change trigger for a build config will only trigger the first build.`
43
+
44
+	triggersExample = `  # Print the triggers on the registry
45
+  $ %[1]s triggers dc/registry
46
+
47
+  # Set all triggers to manual
48
+  $ %[1]s triggers dc/registry --manual
49
+
50
+  # Enable all automatic triggers
51
+  $ %[1]s triggers dc/registry --auto
52
+
53
+  # Reset the GitHub webhook on a build to a new, generated secret
54
+  $ %[1]s triggers bc/webapp --from-github=
55
+  $ %[1]s triggers bc/webapp --from-webhook=
56
+
57
+  # Remove all triggers
58
+  $ %[1]s triggers bc/webapp --remove-all
59
+
60
+  # Stop triggering on config change
61
+  $ %[1]s triggers dc/registry --from-config --remove
62
+
63
+  # Add an image trigger to a build config
64
+  $ %[1]s triggers bc/webapp --from-image=namespace1/image:latest`
65
+)
66
+
67
+type TriggersOptions struct {
68
+	Out io.Writer
69
+	Err io.Writer
70
+
71
+	Filenames []string
72
+	Selector  string
73
+	All       bool
74
+
75
+	Builder *resource.Builder
76
+	Infos   []*resource.Info
77
+
78
+	Encoder runtime.Encoder
79
+
80
+	ShortOutput bool
81
+	Mapper      meta.RESTMapper
82
+
83
+	PrintTable  bool
84
+	PrintObject func(runtime.Object) error
85
+
86
+	Remove    bool
87
+	RemoveAll bool
88
+	Auto      bool
89
+	Manual    bool
90
+	Reset     bool
91
+
92
+	ContainerNames string
93
+	FromConfig     bool
94
+	FromGitHub     *bool
95
+	FromWebHook    *bool
96
+	FromImage      string
97
+	// FromImageNamespace is the namespace for the FromImage
98
+	FromImageNamespace string
99
+}
100
+
101
+// NewCmdTriggers implements the set triggers command
102
+func NewCmdTriggers(fullName string, f *clientcmd.Factory, out, errOut io.Writer) *cobra.Command {
103
+	options := &TriggersOptions{
104
+		Out: out,
105
+		Err: errOut,
106
+	}
107
+	cmd := &cobra.Command{
108
+		Use:     "triggers RESOURCE/NAME [--from-config|--from-image|--from-github|--from-webhook] [--auto|--manual]",
109
+		Short:   "Update the triggers on a build or deployment config",
110
+		Long:    triggersLong,
111
+		Example: fmt.Sprintf(triggersExample, fullName),
112
+		Run: func(cmd *cobra.Command, args []string) {
113
+			kcmdutil.CheckErr(options.Complete(f, cmd, args))
114
+			kcmdutil.CheckErr(options.Validate())
115
+			if err := options.Run(); err != nil {
116
+				// TODO: move met to kcmdutil
117
+				if err == cmdutil.ErrExit {
118
+					os.Exit(1)
119
+				}
120
+				kcmdutil.CheckErr(err)
121
+			}
122
+		},
123
+	}
124
+
125
+	kcmdutil.AddPrinterFlags(cmd)
126
+	cmd.Flags().StringVarP(&options.Selector, "selector", "l", options.Selector, "Selector (label query) to filter on")
127
+	cmd.Flags().BoolVar(&options.All, "all", options.All, "Select all resources in the namespace of the specified resource types")
128
+	cmd.Flags().StringSliceVarP(&options.Filenames, "filename", "f", options.Filenames, "Filename, directory, or URL to file to use to edit the resource.")
129
+
130
+	cmd.Flags().BoolVar(&options.Remove, "remove", options.Remove, "If true, remove the specified trigger(s).")
131
+	cmd.Flags().BoolVar(&options.RemoveAll, "remove-all", options.RemoveAll, "If true, remove all triggers.")
132
+	cmd.Flags().BoolVar(&options.Auto, "auto", options.Auto, "Enable all triggers, or just the specified trigger")
133
+	cmd.Flags().BoolVar(&options.Manual, "manual", options.Manual, "Set all triggers to manual, or just the specified trigger")
134
+
135
+	cmd.Flags().BoolVar(&options.FromConfig, "from-config", options.FromConfig, "If set, configuration changes will result in a change")
136
+	cmd.Flags().StringVarP(&options.ContainerNames, "containers", "c", options.ContainerNames, "Comma delimited list of container names this trigger applies to on deployments; defaults to the name of the only container")
137
+	cmd.Flags().StringVar(&options.FromImage, "from-image", options.FromImage, "An image stream tag to trigger off of")
138
+	options.FromGitHub = cmd.Flags().Bool("from-github", false, "A GitHub webhook - a secret value will be generated automatically")
139
+	options.FromWebHook = cmd.Flags().Bool("from-webhook", false, "A generic webhook - a secret value will be generated automatically")
140
+
141
+	cmd.MarkFlagFilename("filename", "yaml", "yml", "json")
142
+
143
+	return cmd
144
+}
145
+
146
+func (o *TriggersOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string) error {
147
+	cmdNamespace, explicit, err := f.DefaultNamespace()
148
+	if err != nil {
149
+		return err
150
+	}
151
+
152
+	if !cmd.Flags().Lookup("from-github").Changed {
153
+		o.FromGitHub = nil
154
+	}
155
+	if !cmd.Flags().Lookup("from-webhook").Changed {
156
+		o.FromWebHook = nil
157
+	}
158
+
159
+	if len(o.FromImage) > 0 {
160
+		ref, err := imageapi.ParseDockerImageReference(o.FromImage)
161
+		if err != nil {
162
+			return fmt.Errorf("the value of --from-image does not appear to be a valid reference to an image: %v", err)
163
+		}
164
+		if len(ref.Registry) > 0 || len(ref.ID) > 0 {
165
+			return fmt.Errorf("the value of --from-image must point to an image stream tag on this server")
166
+		}
167
+		if len(ref.Tag) == 0 {
168
+			return fmt.Errorf("the value of --from-image must include the tag you wish to pull from")
169
+		}
170
+		o.FromImage = ref.NameString()
171
+		o.FromImageNamespace = defaultNamespace(ref.Namespace, cmdNamespace)
172
+	}
173
+
174
+	count := o.count()
175
+	o.Reset = count == 0 && (o.Auto || o.Manual)
176
+	switch {
177
+	case count == 0 && !o.Remove && !o.RemoveAll && !o.Auto && !o.Manual:
178
+		o.PrintTable = true
179
+	case !o.RemoveAll && !o.Auto && !o.Manual:
180
+		o.Auto = true
181
+	}
182
+
183
+	mapper, typer := f.Object()
184
+	o.Builder = resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), kapi.Codecs.UniversalDecoder()).
185
+		ContinueOnError().
186
+		NamespaceParam(cmdNamespace).DefaultNamespace().
187
+		FilenameParam(explicit, o.Filenames...).
188
+		SelectorParam(o.Selector).
189
+		ResourceTypeOrNameArgs(o.All, args...).
190
+		Flatten()
191
+
192
+	output := kcmdutil.GetFlagString(cmd, "output")
193
+	if len(output) != 0 {
194
+		o.PrintObject = func(obj runtime.Object) error { return f.PrintObject(cmd, obj, o.Out) }
195
+	}
196
+
197
+	o.Encoder = f.JSONEncoder()
198
+	o.ShortOutput = kcmdutil.GetFlagString(cmd, "output") == "name"
199
+	o.Mapper = mapper
200
+
201
+	return nil
202
+}
203
+
204
+func (o *TriggersOptions) count() int {
205
+	count := 0
206
+	if o.FromConfig {
207
+		count++
208
+	}
209
+	if o.FromGitHub != nil {
210
+		count++
211
+	}
212
+	if o.FromWebHook != nil {
213
+		count++
214
+	}
215
+	if len(o.FromImage) > 0 {
216
+		count++
217
+	}
218
+	return count
219
+}
220
+
221
+func (o *TriggersOptions) Validate() error {
222
+	count := o.count()
223
+	switch {
224
+	case o.Auto && o.Manual:
225
+		return fmt.Errorf("you must specify at most one of --auto or --manual")
226
+	case o.Remove && o.RemoveAll:
227
+		return fmt.Errorf("you must specify either --remove or --remove-all")
228
+	case o.RemoveAll && (count != 0 || o.Auto || o.Manual):
229
+		return fmt.Errorf("--remove-all may not be used with any other flag")
230
+	case o.Remove && count < 1:
231
+		return fmt.Errorf("--remove requires a flag defining a trigger type to be specified")
232
+	case count > 1:
233
+		return fmt.Errorf("you may only set one trigger type at a time")
234
+	case count == 0 && !o.Remove && !o.RemoveAll && !o.Auto && !o.Manual && !o.PrintTable:
235
+		return fmt.Errorf("specify one of the --from-* flags to add a trigger, --remove to remove, or --auto|--manual to control existing triggers")
236
+	}
237
+	return nil
238
+}
239
+
240
+func (o *TriggersOptions) Run() error {
241
+	infos := o.Infos
242
+	singular := len(o.Infos) <= 1
243
+	if o.Builder != nil {
244
+		loaded, err := o.Builder.Do().IntoSingular(&singular).Infos()
245
+		if err != nil {
246
+			return err
247
+		}
248
+		infos = loaded
249
+	}
250
+
251
+	if o.PrintTable && o.PrintObject == nil {
252
+		return o.printTriggers(infos)
253
+	}
254
+
255
+	updateTriggerFn := func(triggers *TriggerDefinition) error {
256
+		o.updateTriggers(triggers)
257
+		return nil
258
+	}
259
+	patches := CalculatePatches(infos, o.Encoder, func(info *resource.Info) (bool, error) {
260
+		return UpdateTriggersForObject(info.Object, updateTriggerFn)
261
+	})
262
+	if singular && len(patches) == 0 {
263
+		return fmt.Errorf("%s/%s is not a deployment config or build config", infos[0].Mapping.Resource, infos[0].Name)
264
+	}
265
+	if len(patches) == 0 {
266
+		return nil
267
+	}
268
+
269
+	if o.PrintObject != nil {
270
+		var infos []*resource.Info
271
+		for _, patch := range patches {
272
+			info := patch.Info
273
+			if patch.Err != nil {
274
+				fmt.Fprintf(o.Err, "error: %s/%s %v\n", info.Mapping.Resource, info.Name, patch.Err)
275
+				continue
276
+			}
277
+			infos = append(infos, info)
278
+		}
279
+		if len(infos) == 0 {
280
+			return cmdutil.ErrExit
281
+		}
282
+		object, err := resource.AsVersionedObject(infos, !singular, "", nil)
283
+		if err != nil {
284
+			return err
285
+		}
286
+		return o.PrintObject(object)
287
+	}
288
+
289
+	failed := false
290
+	for _, patch := range patches {
291
+		info := patch.Info
292
+		if patch.Err != nil {
293
+			failed = true
294
+			fmt.Fprintf(o.Err, "error: %s/%s %v\n", info.Mapping.Resource, info.Name, patch.Err)
295
+			continue
296
+		}
297
+
298
+		if string(patch.Patch) == "{}" || len(patch.Patch) == 0 {
299
+			fmt.Fprintf(o.Err, "info: %s %q was not changed\n", info.Mapping.Resource, info.Name)
300
+			continue
301
+		}
302
+
303
+		glog.V(4).Infof("Calculated patch %s", patch.Patch)
304
+
305
+		obj, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, kapi.StrategicMergePatchType, patch.Patch)
306
+		if err != nil {
307
+			handlePodUpdateError(o.Err, err, "triggered")
308
+			failed = true
309
+			continue
310
+		}
311
+
312
+		info.Refresh(obj, true)
313
+		kcmdutil.PrintSuccess(o.Mapper, o.ShortOutput, o.Out, info.Mapping.Resource, info.Name, "updated")
314
+	}
315
+	if failed {
316
+		return cmdutil.ErrExit
317
+	}
318
+	return nil
319
+}
320
+
321
+// printTriggers displays a tabular output of the triggers for each object.
322
+func (o *TriggersOptions) printTriggers(infos []*resource.Info) error {
323
+	w := tabwriter.NewWriter(o.Out, 0, 2, 2, ' ', 0)
324
+	defer w.Flush()
325
+	fmt.Fprintf(w, "NAME\tTYPE\tVALUE\tAUTO\n")
326
+	for _, info := range infos {
327
+		_, err := UpdateTriggersForObject(info.Object, func(triggers *TriggerDefinition) error {
328
+			fmt.Fprintf(w, "%s/%s\t%s\t%s\t%t\n", info.Mapping.Resource, info.Name, "config", "", triggers.ConfigChange)
329
+			for _, image := range triggers.ImageChange {
330
+				var details string
331
+				switch {
332
+				case len(image.Names) > 0:
333
+					if len(image.Namespace) > 0 {
334
+						details = fmt.Sprintf("%s/%s (%s)", image.Namespace, image.From, strings.Join(image.Names, ", "))
335
+					} else {
336
+						details = fmt.Sprintf("%s (%s)", image.From, strings.Join(image.Names, ", "))
337
+					}
338
+				case len(image.Namespace) > 0:
339
+					details = fmt.Sprintf("%s/%s", image.Namespace, image.From)
340
+				default:
341
+					details = image.From
342
+				}
343
+				fmt.Fprintf(w, "%s/%s\t%s\t%s\t%t\n", info.Mapping.Resource, info.Name, "image", details, image.Auto)
344
+			}
345
+			for _, s := range triggers.WebHooks {
346
+				fmt.Fprintf(w, "%s/%s\t%s\t%s\t%s\n", info.Mapping.Resource, info.Name, "webhook", s, "")
347
+			}
348
+			for _, s := range triggers.GitHubWebHooks {
349
+				fmt.Fprintf(w, "%s/%s\t%s\t%s\t%s\n", info.Mapping.Resource, info.Name, "github", s, "")
350
+			}
351
+			return nil
352
+		})
353
+		if err != nil {
354
+			fmt.Fprintf(w, "%s/%s\t%s\t%s\t%t\n", info.Mapping.Resource, info.Name, "<error>", "", false)
355
+		}
356
+	}
357
+	return nil
358
+}
359
+
360
+// updateTriggers updates only those fields with flags set by the user
361
+func (o *TriggersOptions) updateTriggers(triggers *TriggerDefinition) {
362
+	// clear everything
363
+	if o.RemoveAll {
364
+		*triggers = TriggerDefinition{}
365
+		return
366
+	}
367
+
368
+	// clear a specific field
369
+	if o.Remove {
370
+		if o.FromConfig {
371
+			triggers.ConfigChange = false
372
+		}
373
+		if len(o.FromImage) > 0 {
374
+			var newTriggers []ImageChangeTrigger
375
+			for _, trigger := range triggers.ImageChange {
376
+				if trigger.From != o.FromImage {
377
+					newTriggers = append(newTriggers, trigger)
378
+				}
379
+			}
380
+			triggers.ImageChange = newTriggers
381
+		}
382
+		if o.FromWebHook != nil && *o.FromWebHook {
383
+			triggers.WebHooks = nil
384
+		}
385
+		if o.FromGitHub != nil && *o.FromGitHub {
386
+			triggers.GitHubWebHooks = nil
387
+		}
388
+		return
389
+	}
390
+
391
+	// change the automated status
392
+	if o.Reset {
393
+		triggers.ConfigChange = o.Auto
394
+		for i := range triggers.ImageChange {
395
+			triggers.ImageChange[i].Auto = o.Auto
396
+		}
397
+		return
398
+	}
399
+
400
+	// change individual elements
401
+	if o.FromConfig {
402
+		triggers.ConfigChange = true
403
+	}
404
+	if len(o.FromImage) > 0 {
405
+		names := strings.Split(o.ContainerNames, ",")
406
+		if len(o.ContainerNames) == 0 {
407
+			names = nil
408
+		}
409
+		found := false
410
+		for i, trigger := range triggers.ImageChange {
411
+			if trigger.From == o.FromImage && trigger.Namespace == o.FromImageNamespace {
412
+				found = true
413
+				triggers.ImageChange[i].Auto = !o.Manual
414
+				triggers.ImageChange[i].Names = names
415
+				break
416
+			}
417
+		}
418
+		if !found {
419
+			triggers.ImageChange = append(triggers.ImageChange, ImageChangeTrigger{
420
+				From:      o.FromImage,
421
+				Namespace: o.FromImageNamespace,
422
+				Auto:      !o.Manual,
423
+				Names:     names,
424
+			})
425
+		}
426
+	}
427
+	if o.FromWebHook != nil && *o.FromWebHook {
428
+		triggers.WebHooks = []string{app.GenerateSecret(20)}
429
+	}
430
+	if o.FromGitHub != nil && *o.FromGitHub {
431
+		triggers.GitHubWebHooks = []string{app.GenerateSecret(20)}
432
+	}
433
+}
434
+
435
+// ImageChangeTrigger represents the capabilities present in deployment config and build
436
+// config objects in a consistent way.
437
+type ImageChangeTrigger struct {
438
+	// If this trigger is automatically applied
439
+	Auto bool
440
+	// An ImageStreamTag name to target
441
+	From string
442
+	// The target namespace, normalized if set
443
+	Namespace string
444
+	// A list of names this trigger targets
445
+	Names []string
446
+}
447
+
448
+// TriggerDefinition is the abstract representation of triggers for builds and deploymnet configs.
449
+type TriggerDefinition struct {
450
+	ConfigChange   bool
451
+	ImageChange    []ImageChangeTrigger
452
+	WebHooks       []string
453
+	GitHubWebHooks []string
454
+}
455
+
456
+// defaultNamespace returns an empty string if the provided namespace matches the default namespace, or
457
+// returns the namespace.
458
+func defaultNamespace(namespace, defaultNamespace string) string {
459
+	if namespace == defaultNamespace {
460
+		return ""
461
+	}
462
+	return namespace
463
+}
464
+
465
+// NewDeploymentConfigTriggers creates a trigger definition from a deployment config.
466
+func NewDeploymentConfigTriggers(config *deployapi.DeploymentConfig) *TriggerDefinition {
467
+	t := &TriggerDefinition{}
468
+	for _, trigger := range config.Spec.Triggers {
469
+		switch trigger.Type {
470
+		case deployapi.DeploymentTriggerOnConfigChange:
471
+			t.ConfigChange = true
472
+		case deployapi.DeploymentTriggerOnImageChange:
473
+			t.ImageChange = append(t.ImageChange, ImageChangeTrigger{
474
+				Auto:      trigger.ImageChangeParams.Automatic,
475
+				Names:     trigger.ImageChangeParams.ContainerNames,
476
+				From:      trigger.ImageChangeParams.From.Name,
477
+				Namespace: defaultNamespace(trigger.ImageChangeParams.From.Namespace, config.Namespace),
478
+			})
479
+		}
480
+	}
481
+	return t
482
+}
483
+
484
+// NewBuildConfigTriggers creates a trigger definition from a build config.
485
+func NewBuildConfigTriggers(config *buildapi.BuildConfig) *TriggerDefinition {
486
+	t := &TriggerDefinition{}
487
+	setStrategy := false
488
+	for _, trigger := range config.Spec.Triggers {
489
+		switch trigger.Type {
490
+		case buildapi.ConfigChangeBuildTriggerType:
491
+			t.ConfigChange = true
492
+		case buildapi.GenericWebHookBuildTriggerType:
493
+			t.WebHooks = append(t.WebHooks, trigger.GenericWebHook.Secret)
494
+		case buildapi.GitHubWebHookBuildTriggerType:
495
+			t.GitHubWebHooks = append(t.GitHubWebHooks, trigger.GitHubWebHook.Secret)
496
+		case buildapi.ImageChangeBuildTriggerType:
497
+			if trigger.ImageChange.From == nil {
498
+				if strategyTrigger := strategyTrigger(config); strategyTrigger != nil {
499
+					setStrategy = true
500
+					strategyTrigger.Auto = true
501
+					t.ImageChange = append(t.ImageChange, *strategyTrigger)
502
+				}
503
+				continue
504
+			}
505
+			// normalize the trigger
506
+			trigger.ImageChange.From.Namespace = defaultNamespace(trigger.ImageChange.From.Namespace, config.Namespace)
507
+			t.ImageChange = append(t.ImageChange, ImageChangeTrigger{
508
+				Auto:      true,
509
+				From:      trigger.ImageChange.From.Name,
510
+				Namespace: trigger.ImageChange.From.Namespace,
511
+			})
512
+		}
513
+	}
514
+	if !setStrategy {
515
+		if strategyTrigger := strategyTrigger(config); strategyTrigger != nil {
516
+			t.ImageChange = append(t.ImageChange, *strategyTrigger)
517
+		}
518
+	}
519
+	return t
520
+}
521
+
522
+// Apply writes a trigger definition back to a build or deployment config.
523
+func (t *TriggerDefinition) Apply(obj runtime.Object) error {
524
+	switch c := obj.(type) {
525
+	case *deployapi.DeploymentConfig:
526
+		if len(t.GitHubWebHooks) > 0 {
527
+			return fmt.Errorf("deployment configs do not support GitHub web hooks")
528
+		}
529
+		if len(t.WebHooks) > 0 {
530
+			return fmt.Errorf("deployment configs do not support web hooks")
531
+		}
532
+
533
+		existingTriggers := filterDeploymentTriggers(c.Spec.Triggers, deployapi.DeploymentTriggerOnConfigChange)
534
+		var triggers []deployapi.DeploymentTriggerPolicy
535
+		if t.ConfigChange {
536
+			triggers = append(triggers, deployapi.DeploymentTriggerPolicy{Type: deployapi.DeploymentTriggerOnConfigChange})
537
+		}
538
+		allNames := sets.NewString()
539
+		for _, container := range c.Spec.Template.Spec.Containers {
540
+			allNames.Insert(container.Name)
541
+		}
542
+		for _, trigger := range t.ImageChange {
543
+			if len(trigger.Names) == 0 {
544
+				return fmt.Errorf("you must specify --containers when setting --from-image")
545
+			}
546
+			if !allNames.HasAll(trigger.Names...) {
547
+				return fmt.Errorf(
548
+					"not all container names exist: %s (accepts: %s)",
549
+					strings.Join(sets.NewString(trigger.Names...).Difference(allNames).List(), ", "),
550
+					strings.Join(allNames.List(), ", "),
551
+				)
552
+			}
553
+			triggers = append(triggers, deployapi.DeploymentTriggerPolicy{
554
+				Type: deployapi.DeploymentTriggerOnImageChange,
555
+				ImageChangeParams: &deployapi.DeploymentTriggerImageChangeParams{
556
+					Automatic: trigger.Auto,
557
+					From: kapi.ObjectReference{
558
+						Kind: "ImageStreamTag",
559
+						Name: trigger.From,
560
+					},
561
+					ContainerNames: trigger.Names,
562
+				},
563
+			})
564
+		}
565
+		c.Spec.Triggers = mergeDeployTriggers(existingTriggers, triggers)
566
+		return nil
567
+
568
+	case *buildapi.BuildConfig:
569
+		var triggers []buildapi.BuildTriggerPolicy
570
+		if t.ConfigChange {
571
+			triggers = append(triggers, buildapi.BuildTriggerPolicy{Type: buildapi.ConfigChangeBuildTriggerType})
572
+		}
573
+		for _, trigger := range t.WebHooks {
574
+			triggers = append(triggers, buildapi.BuildTriggerPolicy{
575
+				Type: buildapi.GenericWebHookBuildTriggerType,
576
+				GenericWebHook: &buildapi.WebHookTrigger{
577
+					Secret: trigger,
578
+				},
579
+			})
580
+		}
581
+		for _, trigger := range t.GitHubWebHooks {
582
+			triggers = append(triggers, buildapi.BuildTriggerPolicy{
583
+				Type: buildapi.GitHubWebHookBuildTriggerType,
584
+				GitHubWebHook: &buildapi.WebHookTrigger{
585
+					Secret: trigger,
586
+				},
587
+			})
588
+		}
589
+
590
+		// add new triggers, filter out any old triggers that match (if moving from automatic to manual),
591
+		// and then merge the old triggers and the new triggers to preserve fields like lastTriggeredImageID
592
+		existingTriggers := c.Spec.Triggers
593
+		strategyTrigger := strategyTrigger(c)
594
+		for _, trigger := range t.ImageChange {
595
+			change := &buildapi.ImageChangeTrigger{
596
+				From: &kapi.ObjectReference{
597
+					Kind:      "ImageStreamTag",
598
+					Name:      trigger.From,
599
+					Namespace: trigger.Namespace,
600
+				},
601
+			}
602
+
603
+			// use the canonical ImageChangeTrigger with nil From
604
+			strategyTrigger.Auto = trigger.Auto
605
+			if reflect.DeepEqual(strategyTrigger, &trigger) {
606
+				change.From = nil
607
+			}
608
+
609
+			// if this trigger is not automatic, then we need to remove it from the list of triggers
610
+			if !trigger.Auto {
611
+				existingTriggers = filterBuildImageTriggers(existingTriggers, trigger, strategyTrigger)
612
+				continue
613
+			}
614
+
615
+			triggers = append(triggers, buildapi.BuildTriggerPolicy{
616
+				Type:        buildapi.ImageChangeBuildTriggerType,
617
+				ImageChange: change,
618
+			})
619
+		}
620
+		c.Spec.Triggers = mergeBuildTriggers(existingTriggers, triggers)
621
+		return nil
622
+
623
+	default:
624
+		return fmt.Errorf("the object is not a deployment config or build config")
625
+	}
626
+}
627
+
628
+// triggerMatchesBuildImageChange identifies whether the image change is equivalent to the trigger
629
+func triggerMatchesBuildImageChange(trigger ImageChangeTrigger, strategyTrigger *ImageChangeTrigger, imageChange *buildapi.ImageChangeTrigger) bool {
630
+	if imageChange == nil {
631
+		return false
632
+	}
633
+	if imageChange.From == nil {
634
+		return strategyTrigger != nil && strategyTrigger.From == trigger.From && strategyTrigger.Namespace == trigger.Namespace
635
+	}
636
+	namespace := imageChange.From.Namespace
637
+	if strategyTrigger != nil {
638
+		namespace = defaultNamespace(namespace, strategyTrigger.Namespace)
639
+	}
640
+	return imageChange.From.Name == trigger.From && namespace == trigger.Namespace
641
+}
642
+
643
+// filterBuildImageTriggers return only triggers that do not match the provided ImageChangeTrigger.  strategyTrigger may be provided
644
+// if set to remove a BuildTriggerPolicy without a From (which points to the strategy)
645
+func filterBuildImageTriggers(src []buildapi.BuildTriggerPolicy, trigger ImageChangeTrigger, strategyTrigger *ImageChangeTrigger) []buildapi.BuildTriggerPolicy {
646
+	var dst []buildapi.BuildTriggerPolicy
647
+	for i := range src {
648
+		if triggerMatchesBuildImageChange(trigger, strategyTrigger, src[i].ImageChange) {
649
+			continue
650
+		}
651
+		dst = append(dst, src[i])
652
+	}
653
+	return dst
654
+}
655
+
656
+// filterDeploymentTriggers returns only triggers that do not have one of the provided types.
657
+func filterDeploymentTriggers(src []deployapi.DeploymentTriggerPolicy, types ...deployapi.DeploymentTriggerType) []deployapi.DeploymentTriggerPolicy {
658
+	var dst []deployapi.DeploymentTriggerPolicy
659
+Outer:
660
+	for i := range src {
661
+		for _, t := range types {
662
+			if t == src[i].Type {
663
+				continue Outer
664
+			}
665
+		}
666
+		dst = append(dst, src[i])
667
+	}
668
+	return dst
669
+}
670
+
671
+// strategyTrigger returns a synthetic ImageChangeTrigger that represents the image stream tag the build strategy
672
+// points to, or nil if no such strategy trigger is possible (if the build doesn't point to an ImageStreamTag).
673
+func strategyTrigger(config *buildapi.BuildConfig) *ImageChangeTrigger {
674
+	if from := buildutil.GetImageStreamForStrategy(config.Spec.Strategy); from != nil {
675
+		if from.Kind == "ImageStreamTag" {
676
+			// normalize the strategy object reference
677
+			from.Namespace = defaultNamespace(from.Namespace, config.Namespace)
678
+			return &ImageChangeTrigger{From: from.Name, Namespace: from.Namespace}
679
+		}
680
+	}
681
+	return nil
682
+}
683
+
684
+// mergeDeployTriggers returns an array of DeploymentTriggerPolicies that have no duplicates.
685
+func mergeDeployTriggers(dst, src []deployapi.DeploymentTriggerPolicy) []deployapi.DeploymentTriggerPolicy {
686
+	// never return an empty map, because the triggers on a deployment config default when the map is empty
687
+	result := []deployapi.DeploymentTriggerPolicy{}
688
+	for _, current := range dst {
689
+		if findDeployTrigger(src, current) != -1 {
690
+			result = append(result, current)
691
+		}
692
+	}
693
+	for _, current := range src {
694
+		if findDeployTrigger(result, current) == -1 {
695
+			result = append(result, current)
696
+		}
697
+	}
698
+	return result
699
+}
700
+
701
+// findDeployTrigger finds the position of a deployment trigger in the provided array, or -1 if no such
702
+// matching trigger is found.
703
+func findDeployTrigger(dst []deployapi.DeploymentTriggerPolicy, trigger deployapi.DeploymentTriggerPolicy) int {
704
+	for i := range dst {
705
+		if reflect.DeepEqual(dst[i], trigger) {
706
+			return i
707
+		}
708
+	}
709
+	return -1
710
+}
711
+
712
+// mergeBuildTriggers returns an array of BuildTriggerPolicies that have no duplicates, in the same order
713
+// as they exist in their original arrays (a zip-merge).
714
+func mergeBuildTriggers(dst, src []buildapi.BuildTriggerPolicy) []buildapi.BuildTriggerPolicy {
715
+	var result []buildapi.BuildTriggerPolicy
716
+	for _, current := range dst {
717
+		if findBuildTrigger(src, current) != -1 {
718
+			result = append(result, current)
719
+		}
720
+	}
721
+	for _, current := range src {
722
+		if findBuildTrigger(result, current) == -1 {
723
+			result = append(result, current)
724
+		}
725
+	}
726
+	return result
727
+}
728
+
729
+// findBuildTrigger finds the equivalent build trigger position in the provided array, or -1 if
730
+// no such build trigger exists.  Equality only cares about the value of the From field.
731
+func findBuildTrigger(dst []buildapi.BuildTriggerPolicy, trigger buildapi.BuildTriggerPolicy) int {
732
+	// make a copy for semantic equality
733
+	if trigger.ImageChange != nil {
734
+		trigger.ImageChange = &buildapi.ImageChangeTrigger{From: trigger.ImageChange.From}
735
+	}
736
+	for i, copied := range dst {
737
+		// make a copy for semantic equality
738
+		if copied.ImageChange != nil {
739
+			copied.ImageChange = &buildapi.ImageChangeTrigger{From: copied.ImageChange.From}
740
+		}
741
+		if reflect.DeepEqual(copied, trigger) {
742
+			return i
743
+		}
744
+	}
745
+	return -1
746
+}
747
+
748
+// UpdateTriggersForObject extracts a trigger definition from the provided object, passes it to fn, and
749
+// then applies the trigger definition back on the object. It returns true if the object was mutated
750
+// and an optional error if the any part of the flow returns error.
751
+func UpdateTriggersForObject(obj runtime.Object, fn func(*TriggerDefinition) error) (bool, error) {
752
+	// TODO: replace with a swagger schema based approach (identify pod template via schema introspection)
753
+	switch t := obj.(type) {
754
+	case *deployapi.DeploymentConfig:
755
+		triggers := NewDeploymentConfigTriggers(t)
756
+		if err := fn(triggers); err != nil {
757
+			return true, err
758
+		}
759
+		return true, triggers.Apply(t)
760
+	case *buildapi.BuildConfig:
761
+		triggers := NewBuildConfigTriggers(t)
762
+		if err := fn(triggers); err != nil {
763
+			return true, err
764
+		}
765
+		return true, triggers.Apply(t)
766
+	default:
767
+		return false, fmt.Errorf("the object is not a deployment config or build config")
768
+	}
769
+}
... ...
@@ -132,13 +132,13 @@ func (r *SourceRef) BuildSource() (*buildapi.BuildSource, []buildapi.BuildTrigge
132 132
 		{
133 133
 			Type: buildapi.GitHubWebHookBuildTriggerType,
134 134
 			GitHubWebHook: &buildapi.WebHookTrigger{
135
-				Secret: generateSecret(20),
135
+				Secret: GenerateSecret(20),
136 136
 			},
137 137
 		},
138 138
 		{
139 139
 			Type: buildapi.GenericWebHookBuildTriggerType,
140 140
 			GenericWebHook: &buildapi.WebHookTrigger{
141
-				Secret: generateSecret(20),
141
+				Secret: GenerateSecret(20),
142 142
 			},
143 143
 		},
144 144
 	}
... ...
@@ -358,8 +358,8 @@ func (r *DeploymentConfigRef) DeploymentConfig() (*deployapi.DeploymentConfig, e
358 358
 	}, nil
359 359
 }
360 360
 
361
-// generateSecret generates a random secret string
362
-func generateSecret(n int) string {
361
+// GenerateSecret generates a random secret string
362
+func GenerateSecret(n int) string {
363 363
 	n = n * 3 / 4
364 364
 	b := make([]byte, n)
365 365
 	read, _ := rand.Read(b)
366 366
new file mode 100755
... ...
@@ -0,0 +1,88 @@
0
+#!/bin/bash
1
+
2
+set -o errexit
3
+set -o nounset
4
+set -o pipefail
5
+
6
+OS_ROOT=$(dirname "${BASH_SOURCE}")/../..
7
+source "${OS_ROOT}/hack/util.sh"
8
+source "${OS_ROOT}/hack/cmd_util.sh"
9
+os::log::install_errexit
10
+
11
+# Cleanup cluster resources created by this test
12
+(
13
+  set +e
14
+  oc delete all --all
15
+  exit 0
16
+) &>/dev/null
17
+
18
+
19
+url=":${API_PORT:-8443}"
20
+project="$(oc project -q)"
21
+
22
+# This test validates builds and build related commands
23
+
24
+os::cmd::expect_success 'oc new-app centos/ruby-22-centos7~https://github.com/openshift/ruby-hello-world.git'
25
+os::cmd::expect_success 'oc get bc/ruby-hello-world'
26
+os::cmd::expect_success 'oc get dc/ruby-hello-world'
27
+
28
+## Build configs
29
+
30
+# error conditions
31
+os::cmd::expect_failure_and_text 'oc set triggers bc/ruby-hello-world --remove --remove-all' 'specify either --remove or --remove-all'
32
+os::cmd::expect_failure_and_text 'oc set triggers bc/ruby-hello-world --auto --manual' 'at most one of --auto or --manual'
33
+# print
34
+os::cmd::expect_success_and_text 'oc set triggers bc/ruby-hello-world' 'config.*true'
35
+os::cmd::expect_success_and_text 'oc set triggers bc/ruby-hello-world' 'image.*ruby-22-centos7:latest.*true'
36
+os::cmd::expect_success_and_text 'oc set triggers bc/ruby-hello-world' 'webhook'
37
+os::cmd::expect_success_and_text 'oc set triggers bc/ruby-hello-world' 'github'
38
+# remove all
39
+os::cmd::expect_success_and_text 'oc set triggers bc/ruby-hello-world --remove-all' 'updated'
40
+os::cmd::expect_success_and_not_text 'oc set triggers bc/ruby-hello-world' 'webhook|github'
41
+os::cmd::expect_success_and_text 'oc set triggers bc/ruby-hello-world' 'config.*false'
42
+os::cmd::expect_success_and_text 'oc set triggers bc/ruby-hello-world' 'image.*ruby-22-centos7:latest.*false'
43
+# set github hook
44
+os::cmd::expect_success_and_text 'oc set triggers bc/ruby-hello-world --from-github' 'updated'
45
+os::cmd::expect_success_and_text 'oc set triggers bc/ruby-hello-world' 'github'
46
+os::cmd::expect_success_and_text 'oc set triggers bc/ruby-hello-world --remove --from-github' 'updated'
47
+os::cmd::expect_success_and_not_text 'oc set triggers bc/ruby-hello-world' 'github'
48
+# set webhook
49
+os::cmd::expect_success_and_text 'oc set triggers bc/ruby-hello-world --from-webhook' 'updated'
50
+os::cmd::expect_success_and_text 'oc set triggers bc/ruby-hello-world' 'webhook'
51
+os::cmd::expect_success_and_text 'oc set triggers bc/ruby-hello-world --remove --from-webhook' 'updated'
52
+os::cmd::expect_success_and_not_text 'oc set triggers bc/ruby-hello-world' 'webhook'
53
+# set from-image
54
+os::cmd::expect_success_and_text 'oc set triggers bc/ruby-hello-world --from-image=ruby-22-centos7:other' 'updated'
55
+os::cmd::expect_success_and_text 'oc set triggers bc/ruby-hello-world' 'image.*ruby-22-centos7:other.*true'
56
+# manual and remove both clear build configs
57
+os::cmd::expect_success_and_text 'oc set triggers bc/ruby-hello-world --from-image=ruby-22-centos7:other --manual' 'updated'
58
+os::cmd::expect_success_and_not_text 'oc set triggers bc/ruby-hello-world' 'image.*ruby-22-centos7:other.*false'
59
+os::cmd::expect_success_and_text 'oc set triggers bc/ruby-hello-world --from-image=ruby-22-centos7:other' 'updated'
60
+os::cmd::expect_success_and_text 'oc set triggers bc/ruby-hello-world --from-image=ruby-22-centos7:other --remove' 'updated'
61
+os::cmd::expect_success_and_not_text 'oc set triggers bc/ruby-hello-world' 'image.*ruby-22-centos7:other'
62
+# test --all
63
+os::cmd::expect_success_and_text 'oc set triggers bc --all' 'buildconfigs/ruby-hello-world.*image.*ruby-22-centos7:latest.*false'
64
+os::cmd::expect_success_and_text 'oc set triggers bc --all --auto' 'updated'
65
+os::cmd::expect_success_and_text 'oc set triggers bc --all' 'buildconfigs/ruby-hello-world.*image.*ruby-22-centos7:latest.*true'
66
+
67
+## Deployment configs
68
+
69
+# error conditions
70
+os::cmd::expect_failure_and_text 'oc set triggers dc/ruby-hello-world --from-github' 'deployment configs do not support GitHub web hooks'
71
+os::cmd::expect_failure_and_text 'oc set triggers dc/ruby-hello-world --from-webhook' 'deployment configs do not support web hooks'
72
+os::cmd::expect_failure_and_text 'oc set triggers dc/ruby-hello-world --from-image=test:latest' 'you must specify --containers when setting --from-image'
73
+os::cmd::expect_failure_and_text 'oc set triggers dc/ruby-hello-world --from-image=test:latest --containers=other' 'not all container names exist: other \(accepts: ruby-hello-world\)'
74
+# print
75
+os::cmd::expect_success_and_text 'oc set triggers dc/ruby-hello-world' 'config.*true'
76
+os::cmd::expect_success_and_text 'oc set triggers dc/ruby-hello-world' 'image.*ruby-hello-world:latest \(ruby-hello-world\).*true'
77
+os::cmd::expect_success_and_not_text 'oc set triggers dc/ruby-hello-world' 'webhook'
78
+os::cmd::expect_success_and_not_text 'oc set triggers dc/ruby-hello-world' 'github'
79
+# remove all
80
+os::cmd::expect_success_and_text 'oc set triggers dc/ruby-hello-world --remove-all' 'updated'
81
+os::cmd::expect_success_and_not_text 'oc set triggers dc/ruby-hello-world' 'webhook|github|image'
82
+os::cmd::expect_success_and_text 'oc set triggers dc/ruby-hello-world' 'config.*false'
83
+# auto
84
+os::cmd::expect_success_and_text 'oc set triggers dc/ruby-hello-world --auto' 'updated'
85
+os::cmd::expect_success_and_text 'oc set triggers dc/ruby-hello-world' 'config.*true'
86
+os::cmd::expect_success_and_text 'oc set triggers dc/ruby-hello-world --from-image=ruby-hello-world:latest -c ruby-hello-world' 'updated'
87
+os::cmd::expect_success_and_text 'oc set triggers dc/ruby-hello-world' 'image.*ruby-hello-world:latest \(ruby-hello-world\).*true'