package set import ( "fmt" "io" "os" "reflect" "strings" "text/tabwriter" "github.com/golang/glog" "github.com/spf13/cobra" kapi "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/meta" "k8s.io/kubernetes/pkg/api/unversioned" kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/runtime" buildapi "github.com/openshift/origin/pkg/build/api" buildutil "github.com/openshift/origin/pkg/build/util" "github.com/openshift/origin/pkg/cmd/templates" cmdutil "github.com/openshift/origin/pkg/cmd/util" "github.com/openshift/origin/pkg/cmd/util/clientcmd" deployapi "github.com/openshift/origin/pkg/deploy/api" "github.com/openshift/origin/pkg/generate/app" imageapi "github.com/openshift/origin/pkg/image/api" "k8s.io/kubernetes/pkg/util/sets" ) var ( triggersLong = templates.LongDesc(` Set or remove triggers for build configs and deployment configs All build configs and deployment configs may have a set of triggers that result in a new deployment or build being created. This command enables you to alter those triggers - making them automatic or manual, adding new entries, or changing existing entries. Deployments support triggering off of image changes and on config changes. Config changes are any alterations to the pod template, while image changes will result in the container image value being updated whenever an image stream tag is updated. Build configs support triggering off of image changes, config changes, and webhooks (both GitHub-specific and generic). The config change trigger for a build config will only trigger the first build.`) triggersExample = templates.Examples(` # Print the triggers on the registry %[1]s triggers dc/registry # Set all triggers to manual %[1]s triggers dc/registry --manual # Enable all automatic triggers %[1]s triggers dc/registry --auto # Reset the GitHub webhook on a build to a new, generated secret %[1]s triggers bc/webapp --from-github %[1]s triggers bc/webapp --from-webhook # Remove all triggers %[1]s triggers bc/webapp --remove-all # Stop triggering on config change %[1]s triggers dc/registry --from-config --remove # Add an image trigger to a build config %[1]s triggers bc/webapp --from-image=namespace1/image:latest`) ) type TriggersOptions struct { Out io.Writer Err io.Writer Filenames []string Selector string All bool Builder *resource.Builder Infos []*resource.Info Encoder runtime.Encoder ShortOutput bool Mapper meta.RESTMapper OutputVersion unversioned.GroupVersion PrintTable bool PrintObject func([]*resource.Info) error Remove bool RemoveAll bool Auto bool Manual bool Reset bool ContainerNames string FromConfig bool FromGitHub *bool FromWebHook *bool FromWebHookAllowEnv *bool FromImage string // FromImageNamespace is the namespace for the FromImage FromImageNamespace string } // NewCmdTriggers implements the set triggers command func NewCmdTriggers(fullName string, f *clientcmd.Factory, out, errOut io.Writer) *cobra.Command { options := &TriggersOptions{ Out: out, Err: errOut, } cmd := &cobra.Command{ Use: "triggers RESOURCE/NAME [--from-config|--from-image|--from-github|--from-webhook] [--auto|--manual]", Short: "Update the triggers on a build or deployment config", Long: triggersLong, Example: fmt.Sprintf(triggersExample, fullName), Run: func(cmd *cobra.Command, args []string) { kcmdutil.CheckErr(options.Complete(f, cmd, args)) kcmdutil.CheckErr(options.Validate()) if err := options.Run(); err != nil { // TODO: move me to kcmdutil if err == cmdutil.ErrExit { os.Exit(1) } kcmdutil.CheckErr(err) } }, } kcmdutil.AddPrinterFlags(cmd) cmd.Flags().StringVarP(&options.Selector, "selector", "l", options.Selector, "Selector (label query) to filter on") cmd.Flags().BoolVar(&options.All, "all", options.All, "If true, select all resources in the namespace of the specified resource types") cmd.Flags().StringSliceVarP(&options.Filenames, "filename", "f", options.Filenames, "Filename, directory, or URL to file to use to edit the resource.") cmd.Flags().BoolVar(&options.Remove, "remove", options.Remove, "If true, remove the specified trigger(s).") cmd.Flags().BoolVar(&options.RemoveAll, "remove-all", options.RemoveAll, "If true, remove all triggers.") cmd.Flags().BoolVar(&options.Auto, "auto", options.Auto, "If true, enable all triggers, or just the specified trigger") cmd.Flags().BoolVar(&options.Manual, "manual", options.Manual, "If true, set all triggers to manual, or just the specified trigger") cmd.Flags().BoolVar(&options.FromConfig, "from-config", options.FromConfig, "If set, configuration changes will result in a change") 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") cmd.Flags().StringVar(&options.FromImage, "from-image", options.FromImage, "An image stream tag to trigger off of") options.FromGitHub = cmd.Flags().Bool("from-github", false, "If true, a GitHub webhook - a secret value will be generated automatically") options.FromWebHook = cmd.Flags().Bool("from-webhook", false, "If true, a generic webhook - a secret value will be generated automatically") options.FromWebHookAllowEnv = cmd.Flags().Bool("from-webhook-allow-env", false, "If true, a generic webhook which can provide environment variables - a secret value will be generated automatically") cmd.MarkFlagFilename("filename", "yaml", "yml", "json") return cmd } func (o *TriggersOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string) error { cmdNamespace, explicit, err := f.DefaultNamespace() if err != nil { return err } clientConfig, err := f.ClientConfig() if err != nil { return err } o.OutputVersion, err = kcmdutil.OutputVersion(cmd, clientConfig.GroupVersion) if err != nil { return err } if !cmd.Flags().Lookup("from-github").Changed { o.FromGitHub = nil } if !cmd.Flags().Lookup("from-webhook").Changed { o.FromWebHook = nil } if !cmd.Flags().Lookup("from-webhook-allow-env").Changed { o.FromWebHookAllowEnv = nil } if len(o.FromImage) > 0 { ref, err := imageapi.ParseDockerImageReference(o.FromImage) if err != nil { return fmt.Errorf("the value of --from-image does not appear to be a valid reference to an image: %v", err) } if len(ref.Registry) > 0 || len(ref.ID) > 0 { return fmt.Errorf("the value of --from-image must point to an image stream tag on this server") } if len(ref.Tag) == 0 { return fmt.Errorf("the value of --from-image must include the tag you wish to pull from") } o.FromImage = ref.NameString() o.FromImageNamespace = defaultNamespace(ref.Namespace, cmdNamespace) } count := o.count() o.Reset = count == 0 && (o.Auto || o.Manual) switch { case count == 0 && !o.Remove && !o.RemoveAll && !o.Auto && !o.Manual: o.PrintTable = true case !o.RemoveAll && !o.Auto && !o.Manual: o.Auto = true } mapper, typer := f.Object(false) o.Builder = resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), kapi.Codecs.UniversalDecoder()). ContinueOnError(). NamespaceParam(cmdNamespace).DefaultNamespace(). FilenameParam(explicit, false, o.Filenames...). SelectorParam(o.Selector). ResourceTypeOrNameArgs(o.All, args...). Flatten() output := kcmdutil.GetFlagString(cmd, "output") if len(output) > 0 { o.PrintObject = func(infos []*resource.Info) error { return f.PrintResourceInfos(cmd, infos, o.Out) } } o.Encoder = f.JSONEncoder() o.ShortOutput = kcmdutil.GetFlagString(cmd, "output") == "name" o.Mapper = mapper return nil } func (o *TriggersOptions) count() int { count := 0 if o.FromConfig { count++ } if o.FromGitHub != nil { count++ } if o.FromWebHook != nil { count++ } if o.FromWebHookAllowEnv != nil { count++ } if len(o.FromImage) > 0 { count++ } return count } func (o *TriggersOptions) Validate() error { count := o.count() switch { case o.Auto && o.Manual: return fmt.Errorf("you must specify at most one of --auto or --manual") case o.Remove && o.RemoveAll: return fmt.Errorf("you must specify either --remove or --remove-all") case o.RemoveAll && (count != 0 || o.Auto || o.Manual): return fmt.Errorf("--remove-all may not be used with any other flag") case o.Remove && count < 1: return fmt.Errorf("--remove requires a flag defining a trigger type to be specified") case count > 1: return fmt.Errorf("you may only set one trigger type at a time") case count == 0 && !o.Remove && !o.RemoveAll && !o.Auto && !o.Manual && !o.PrintTable: return fmt.Errorf("specify one of the --from-* flags to add a trigger, --remove to remove, or --auto|--manual to control existing triggers") } return nil } func (o *TriggersOptions) Run() error { infos := o.Infos singular := len(o.Infos) <= 1 if o.Builder != nil { loaded, err := o.Builder.Do().IntoSingular(&singular).Infos() if err != nil { return err } infos = loaded } if o.PrintTable && o.PrintObject == nil { return o.printTriggers(infos) } updateTriggerFn := func(triggers *TriggerDefinition) error { o.updateTriggers(triggers) return nil } patches := CalculatePatches(infos, o.Encoder, func(info *resource.Info) (bool, error) { return UpdateTriggersForObject(info.Object, updateTriggerFn) }) if singular && len(patches) == 0 { return fmt.Errorf("%s/%s is not a deployment config or build config", infos[0].Mapping.Resource, infos[0].Name) } if o.PrintObject != nil { return o.PrintObject(infos) } failed := false for _, patch := range patches { info := patch.Info if patch.Err != nil { failed = true fmt.Fprintf(o.Err, "error: %s/%s %v\n", info.Mapping.Resource, info.Name, patch.Err) continue } if string(patch.Patch) == "{}" || len(patch.Patch) == 0 { fmt.Fprintf(o.Err, "info: %s %q was not changed\n", info.Mapping.Resource, info.Name) continue } glog.V(4).Infof("Calculated patch %s", patch.Patch) obj, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, kapi.StrategicMergePatchType, patch.Patch) if err != nil { handlePodUpdateError(o.Err, err, "triggered") failed = true continue } info.Refresh(obj, true) kcmdutil.PrintSuccess(o.Mapper, o.ShortOutput, o.Out, info.Mapping.Resource, info.Name, false, "updated") } if failed { return cmdutil.ErrExit } return nil } // printTriggers displays a tabular output of the triggers for each object. func (o *TriggersOptions) printTriggers(infos []*resource.Info) error { w := tabwriter.NewWriter(o.Out, 0, 2, 2, ' ', 0) defer w.Flush() fmt.Fprintf(w, "NAME\tTYPE\tVALUE\tAUTO\n") for _, info := range infos { _, err := UpdateTriggersForObject(info.Object, func(triggers *TriggerDefinition) error { fmt.Fprintf(w, "%s/%s\t%s\t%s\t%t\n", info.Mapping.Resource, info.Name, "config", "", triggers.ConfigChange) for _, image := range triggers.ImageChange { var details string switch { case len(image.Names) > 0: if len(image.Namespace) > 0 { details = fmt.Sprintf("%s/%s (%s)", image.Namespace, image.From, strings.Join(image.Names, ", ")) } else { details = fmt.Sprintf("%s (%s)", image.From, strings.Join(image.Names, ", ")) } case len(image.Namespace) > 0: details = fmt.Sprintf("%s/%s", image.Namespace, image.From) default: details = image.From } fmt.Fprintf(w, "%s/%s\t%s\t%s\t%t\n", info.Mapping.Resource, info.Name, "image", details, image.Auto) } for _, s := range triggers.WebHooks { fmt.Fprintf(w, "%s/%s\t%s\t%s\t%s\n", info.Mapping.Resource, info.Name, "webhook", s, "") } for _, s := range triggers.GitHubWebHooks { fmt.Fprintf(w, "%s/%s\t%s\t%s\t%s\n", info.Mapping.Resource, info.Name, "github", s, "") } return nil }) if err != nil { fmt.Fprintf(w, "%s/%s\t%s\t%s\t%t\n", info.Mapping.Resource, info.Name, "", "", false) } } return nil } // updateTriggers updates only those fields with flags set by the user func (o *TriggersOptions) updateTriggers(triggers *TriggerDefinition) { // clear everything if o.RemoveAll { *triggers = TriggerDefinition{} return } // clear a specific field if o.Remove { if o.FromConfig { triggers.ConfigChange = false } if len(o.FromImage) > 0 { var newTriggers []ImageChangeTrigger for _, trigger := range triggers.ImageChange { if trigger.From != o.FromImage { newTriggers = append(newTriggers, trigger) } } triggers.ImageChange = newTriggers } if o.FromWebHook != nil && *o.FromWebHook { triggers.WebHooks = nil } if o.FromWebHookAllowEnv != nil && *o.FromWebHookAllowEnv { triggers.WebHooks = nil triggers.WebHooksAllowEnv = false } if o.FromGitHub != nil && *o.FromGitHub { triggers.GitHubWebHooks = nil } return } // change the automated status if o.Reset { triggers.ConfigChange = o.Auto for i := range triggers.ImageChange { triggers.ImageChange[i].Auto = o.Auto } return } // change individual elements if o.FromConfig { triggers.ConfigChange = true } if len(o.FromImage) > 0 { names := strings.Split(o.ContainerNames, ",") if len(o.ContainerNames) == 0 { names = nil } found := false for i, trigger := range triggers.ImageChange { if trigger.From == o.FromImage && trigger.Namespace == o.FromImageNamespace { found = true triggers.ImageChange[i].Auto = !o.Manual triggers.ImageChange[i].Names = names break } } if !found { triggers.ImageChange = append(triggers.ImageChange, ImageChangeTrigger{ From: o.FromImage, Namespace: o.FromImageNamespace, Auto: !o.Manual, Names: names, }) } } if o.FromWebHook != nil && *o.FromWebHook { triggers.WebHooks = []string{app.GenerateSecret(20)} } if o.FromWebHookAllowEnv != nil && *o.FromWebHookAllowEnv { triggers.WebHooks = []string{app.GenerateSecret(20)} triggers.WebHooksAllowEnv = true } if o.FromGitHub != nil && *o.FromGitHub { triggers.GitHubWebHooks = []string{app.GenerateSecret(20)} } } // ImageChangeTrigger represents the capabilities present in deployment config and build // config objects in a consistent way. type ImageChangeTrigger struct { // If this trigger is automatically applied Auto bool // An ImageStreamTag name to target From string // The target namespace, normalized if set Namespace string // A list of names this trigger targets Names []string } // TriggerDefinition is the abstract representation of triggers for builds and deploymnet configs. type TriggerDefinition struct { ConfigChange bool ImageChange []ImageChangeTrigger WebHooks []string WebHooksAllowEnv bool GitHubWebHooks []string } // defaultNamespace returns an empty string if the provided namespace matches the default namespace, or // returns the namespace. func defaultNamespace(namespace, defaultNamespace string) string { if namespace == defaultNamespace { return "" } return namespace } // NewDeploymentConfigTriggers creates a trigger definition from a deployment config. func NewDeploymentConfigTriggers(config *deployapi.DeploymentConfig) *TriggerDefinition { t := &TriggerDefinition{} for _, trigger := range config.Spec.Triggers { switch trigger.Type { case deployapi.DeploymentTriggerOnConfigChange: t.ConfigChange = true case deployapi.DeploymentTriggerOnImageChange: t.ImageChange = append(t.ImageChange, ImageChangeTrigger{ Auto: trigger.ImageChangeParams.Automatic, Names: trigger.ImageChangeParams.ContainerNames, From: trigger.ImageChangeParams.From.Name, Namespace: defaultNamespace(trigger.ImageChangeParams.From.Namespace, config.Namespace), }) } } return t } // NewBuildConfigTriggers creates a trigger definition from a build config. func NewBuildConfigTriggers(config *buildapi.BuildConfig) *TriggerDefinition { t := &TriggerDefinition{} setStrategy := false for _, trigger := range config.Spec.Triggers { switch trigger.Type { case buildapi.ConfigChangeBuildTriggerType: t.ConfigChange = true case buildapi.GenericWebHookBuildTriggerType: t.WebHooks = append(t.WebHooks, trigger.GenericWebHook.Secret) t.WebHooksAllowEnv = trigger.GenericWebHook.AllowEnv case buildapi.GitHubWebHookBuildTriggerType: t.GitHubWebHooks = append(t.GitHubWebHooks, trigger.GitHubWebHook.Secret) case buildapi.ImageChangeBuildTriggerType: if trigger.ImageChange.From == nil { if strategyTrigger := strategyTrigger(config); strategyTrigger != nil { setStrategy = true strategyTrigger.Auto = true t.ImageChange = append(t.ImageChange, *strategyTrigger) } continue } // normalize the trigger trigger.ImageChange.From.Namespace = defaultNamespace(trigger.ImageChange.From.Namespace, config.Namespace) t.ImageChange = append(t.ImageChange, ImageChangeTrigger{ Auto: true, From: trigger.ImageChange.From.Name, Namespace: trigger.ImageChange.From.Namespace, }) } } if !setStrategy { if strategyTrigger := strategyTrigger(config); strategyTrigger != nil { t.ImageChange = append(t.ImageChange, *strategyTrigger) } } return t } // Apply writes a trigger definition back to a build or deployment config. func (t *TriggerDefinition) Apply(obj runtime.Object) error { switch c := obj.(type) { case *deployapi.DeploymentConfig: if len(t.GitHubWebHooks) > 0 { return fmt.Errorf("deployment configs do not support GitHub web hooks") } if len(t.WebHooks) > 0 { return fmt.Errorf("deployment configs do not support web hooks") } existingTriggers := filterDeploymentTriggers(c.Spec.Triggers, deployapi.DeploymentTriggerOnConfigChange) var triggers []deployapi.DeploymentTriggerPolicy if t.ConfigChange { triggers = append(triggers, deployapi.DeploymentTriggerPolicy{Type: deployapi.DeploymentTriggerOnConfigChange}) } allNames := sets.NewString() for _, container := range c.Spec.Template.Spec.Containers { allNames.Insert(container.Name) } for _, trigger := range t.ImageChange { if len(trigger.Names) == 0 { return fmt.Errorf("you must specify --containers when setting --from-image") } if !allNames.HasAll(trigger.Names...) { return fmt.Errorf( "not all container names exist: %s (accepts: %s)", strings.Join(sets.NewString(trigger.Names...).Difference(allNames).List(), ", "), strings.Join(allNames.List(), ", "), ) } triggers = append(triggers, deployapi.DeploymentTriggerPolicy{ Type: deployapi.DeploymentTriggerOnImageChange, ImageChangeParams: &deployapi.DeploymentTriggerImageChangeParams{ Automatic: trigger.Auto, From: kapi.ObjectReference{ Kind: "ImageStreamTag", Name: trigger.From, }, ContainerNames: trigger.Names, }, }) } c.Spec.Triggers = mergeDeployTriggers(existingTriggers, triggers) return nil case *buildapi.BuildConfig: var triggers []buildapi.BuildTriggerPolicy if t.ConfigChange { triggers = append(triggers, buildapi.BuildTriggerPolicy{Type: buildapi.ConfigChangeBuildTriggerType}) } for _, trigger := range t.WebHooks { triggers = append(triggers, buildapi.BuildTriggerPolicy{ Type: buildapi.GenericWebHookBuildTriggerType, GenericWebHook: &buildapi.WebHookTrigger{ Secret: trigger, AllowEnv: t.WebHooksAllowEnv, }, }) } for _, trigger := range t.GitHubWebHooks { triggers = append(triggers, buildapi.BuildTriggerPolicy{ Type: buildapi.GitHubWebHookBuildTriggerType, GitHubWebHook: &buildapi.WebHookTrigger{ Secret: trigger, }, }) } // add new triggers, filter out any old triggers that match (if moving from automatic to manual), // and then merge the old triggers and the new triggers to preserve fields like lastTriggeredImageID existingTriggers := c.Spec.Triggers strategyTrigger := strategyTrigger(c) for _, trigger := range t.ImageChange { change := &buildapi.ImageChangeTrigger{ From: &kapi.ObjectReference{ Kind: "ImageStreamTag", Name: trigger.From, Namespace: trigger.Namespace, }, } // use the canonical ImageChangeTrigger with nil From strategyTrigger.Auto = trigger.Auto if reflect.DeepEqual(strategyTrigger, &trigger) { change.From = nil } // if this trigger is not automatic, then we need to remove it from the list of triggers if !trigger.Auto { existingTriggers = filterBuildImageTriggers(existingTriggers, trigger, strategyTrigger) continue } triggers = append(triggers, buildapi.BuildTriggerPolicy{ Type: buildapi.ImageChangeBuildTriggerType, ImageChange: change, }) } c.Spec.Triggers = mergeBuildTriggers(existingTriggers, triggers) return nil default: return fmt.Errorf("the object is not a deployment config or build config") } } // triggerMatchesBuildImageChange identifies whether the image change is equivalent to the trigger func triggerMatchesBuildImageChange(trigger ImageChangeTrigger, strategyTrigger *ImageChangeTrigger, imageChange *buildapi.ImageChangeTrigger) bool { if imageChange == nil { return false } if imageChange.From == nil { return strategyTrigger != nil && strategyTrigger.From == trigger.From && strategyTrigger.Namespace == trigger.Namespace } namespace := imageChange.From.Namespace if strategyTrigger != nil { namespace = defaultNamespace(namespace, strategyTrigger.Namespace) } return imageChange.From.Name == trigger.From && namespace == trigger.Namespace } // filterBuildImageTriggers return only triggers that do not match the provided ImageChangeTrigger. strategyTrigger may be provided // if set to remove a BuildTriggerPolicy without a From (which points to the strategy) func filterBuildImageTriggers(src []buildapi.BuildTriggerPolicy, trigger ImageChangeTrigger, strategyTrigger *ImageChangeTrigger) []buildapi.BuildTriggerPolicy { var dst []buildapi.BuildTriggerPolicy for i := range src { if triggerMatchesBuildImageChange(trigger, strategyTrigger, src[i].ImageChange) { continue } dst = append(dst, src[i]) } return dst } // filterDeploymentTriggers returns only triggers that do not have one of the provided types. func filterDeploymentTriggers(src []deployapi.DeploymentTriggerPolicy, types ...deployapi.DeploymentTriggerType) []deployapi.DeploymentTriggerPolicy { var dst []deployapi.DeploymentTriggerPolicy Outer: for i := range src { for _, t := range types { if t == src[i].Type { continue Outer } } dst = append(dst, src[i]) } return dst } // strategyTrigger returns a synthetic ImageChangeTrigger that represents the image stream tag the build strategy // points to, or nil if no such strategy trigger is possible (if the build doesn't point to an ImageStreamTag). func strategyTrigger(config *buildapi.BuildConfig) *ImageChangeTrigger { if from := buildutil.GetInputReference(config.Spec.Strategy); from != nil { if from.Kind == "ImageStreamTag" { // normalize the strategy object reference from.Namespace = defaultNamespace(from.Namespace, config.Namespace) return &ImageChangeTrigger{From: from.Name, Namespace: from.Namespace} } } return nil } // mergeDeployTriggers returns an array of DeploymentTriggerPolicies that have no duplicates. func mergeDeployTriggers(dst, src []deployapi.DeploymentTriggerPolicy) []deployapi.DeploymentTriggerPolicy { // never return an empty map, because the triggers on a deployment config default when the map is empty result := []deployapi.DeploymentTriggerPolicy{} for _, current := range dst { if findDeployTrigger(src, current) != -1 { result = append(result, current) } } for _, current := range src { if findDeployTrigger(result, current) == -1 { result = append(result, current) } } return result } // findDeployTrigger finds the position of a deployment trigger in the provided array, or -1 if no such // matching trigger is found. func findDeployTrigger(dst []deployapi.DeploymentTriggerPolicy, trigger deployapi.DeploymentTriggerPolicy) int { for i := range dst { if reflect.DeepEqual(dst[i], trigger) { return i } } return -1 } // mergeBuildTriggers returns an array of BuildTriggerPolicies that have no duplicates, in the same order // as they exist in their original arrays (a zip-merge). func mergeBuildTriggers(dst, src []buildapi.BuildTriggerPolicy) []buildapi.BuildTriggerPolicy { var result []buildapi.BuildTriggerPolicy for _, current := range dst { if findBuildTrigger(src, current) != -1 { result = append(result, current) } } for _, current := range src { if findBuildTrigger(result, current) == -1 { result = append(result, current) } } return result } // findBuildTrigger finds the equivalent build trigger position in the provided array, or -1 if // no such build trigger exists. Equality only cares about the value of the From field. func findBuildTrigger(dst []buildapi.BuildTriggerPolicy, trigger buildapi.BuildTriggerPolicy) int { // make a copy for semantic equality if trigger.ImageChange != nil { trigger.ImageChange = &buildapi.ImageChangeTrigger{From: trigger.ImageChange.From} } for i, copied := range dst { // make a copy for semantic equality if copied.ImageChange != nil { copied.ImageChange = &buildapi.ImageChangeTrigger{From: copied.ImageChange.From} } if reflect.DeepEqual(copied, trigger) { return i } } return -1 } // UpdateTriggersForObject extracts a trigger definition from the provided object, passes it to fn, and // then applies the trigger definition back on the object. It returns true if the object was mutated // and an optional error if the any part of the flow returns error. func UpdateTriggersForObject(obj runtime.Object, fn func(*TriggerDefinition) error) (bool, error) { // TODO: replace with a swagger schema based approach (identify pod template via schema introspection) switch t := obj.(type) { case *deployapi.DeploymentConfig: triggers := NewDeploymentConfigTriggers(t) if err := fn(triggers); err != nil { return true, err } return true, triggers.Apply(t) case *buildapi.BuildConfig: triggers := NewBuildConfigTriggers(t) if err := fn(triggers); err != nil { return true, err } return true, triggers.Apply(t) default: return false, fmt.Errorf("the object is not a deployment config or build config") } }