diff --git a/deploy/crds/shipwright.io_buildruns.yaml b/deploy/crds/shipwright.io_buildruns.yaml index a9beb61243..73c3f55bf3 100644 --- a/deploy/crds/shipwright.io_buildruns.yaml +++ b/deploy/crds/shipwright.io_buildruns.yaml @@ -466,6 +466,87 @@ spec: should take to execute. format: duration type: string + trigger: + description: Trigger defines the scenarios where a new build should + be triggered. + properties: + secretRef: + description: SecretRef points to a local object carrying the + secret token to validate webhook request. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + when: + description: When the list of scenarios when a new build should + take place. + items: + description: TriggerWhen a given scenario where the webhook + trigger is applicable. + properties: + github: + description: GitHub describes how to trigger builds + based on GitHub (SCM) events. + properties: + branches: + description: Branches slice of branch names where + the event applies. + items: + type: string + type: array + events: + description: Events GitHub event names. + items: + description: GitHubEventName set of WhenGitHub + valid event names. + type: string + minItems: 1 + type: array + type: object + image: + description: Image slice of image names where the event + applies. + properties: + names: + description: Names fully qualified image names. + items: + type: string + type: array + type: object + name: + description: Name name or the short description of the + trigger condition. + type: string + objectRef: + description: ObjectRef describes how to match a foreign + resource, either using the name or the label selector, + plus the current resource status. + properties: + name: + description: Name target object name. + type: string + selector: + additionalProperties: + type: string + description: Selector label selector. + type: object + status: + description: Status object status. + items: + type: string + type: array + type: object + type: + description: Type the event type + type: string + required: + - name + - type + type: object + type: array + type: object volumes: description: Volumes contains volume Overrides of the BuildStrategy volumes in case those are allowed to be overridden. Must only @@ -4190,6 +4271,87 @@ spec: should take to execute. format: duration type: string + trigger: + description: Trigger defines the scenarios where a new build should + be triggered. + properties: + secretRef: + description: SecretRef points to a local object carrying the + secret token to validate webhook request. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + when: + description: When the list of scenarios when a new build should + take place. + items: + description: TriggerWhen a given scenario where the webhook + trigger is applicable. + properties: + github: + description: GitHub describes how to trigger builds + based on GitHub (SCM) events. + properties: + branches: + description: Branches slice of branch names where + the event applies. + items: + type: string + type: array + events: + description: Events GitHub event names. + items: + description: GitHubEventName set of WhenGitHub + valid event names. + type: string + minItems: 1 + type: array + type: object + image: + description: Image slice of image names where the event + applies. + properties: + names: + description: Names fully qualified image names. + items: + type: string + type: array + type: object + name: + description: Name name or the short description of the + trigger condition. + type: string + objectRef: + description: ObjectRef describes how to match a foreign + resource, either using the name or the label selector, + plus the current resource status. + properties: + name: + description: Name target object name. + type: string + selector: + additionalProperties: + type: string + description: Selector label selector. + type: object + status: + description: Status object status. + items: + type: string + type: array + type: object + type: + description: Type the event type + type: string + required: + - name + - type + type: object + type: array + type: object volumes: description: Volumes contains volume Overrides of the BuildStrategy volumes in case those are allowed to be overridden. Must only diff --git a/deploy/crds/shipwright.io_builds.yaml b/deploy/crds/shipwright.io_builds.yaml index db2f0d3799..eee61b10ca 100644 --- a/deploy/crds/shipwright.io_builds.yaml +++ b/deploy/crds/shipwright.io_builds.yaml @@ -444,6 +444,87 @@ spec: should take to execute. format: duration type: string + trigger: + description: Trigger defines the scenarios where a new build should + be triggered. + properties: + secretRef: + description: SecretRef points to a local object carrying the secret + token to validate webhook request. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + when: + description: When the list of scenarios when a new build should + take place. + items: + description: TriggerWhen a given scenario where the webhook + trigger is applicable. + properties: + github: + description: GitHub describes how to trigger builds based + on GitHub (SCM) events. + properties: + branches: + description: Branches slice of branch names where the + event applies. + items: + type: string + type: array + events: + description: Events GitHub event names. + items: + description: GitHubEventName set of WhenGitHub valid + event names. + type: string + minItems: 1 + type: array + type: object + image: + description: Image slice of image names where the event + applies. + properties: + names: + description: Names fully qualified image names. + items: + type: string + type: array + type: object + name: + description: Name name or the short description of the trigger + condition. + type: string + objectRef: + description: ObjectRef describes how to match a foreign + resource, either using the name or the label selector, + plus the current resource status. + properties: + name: + description: Name target object name. + type: string + selector: + additionalProperties: + type: string + description: Selector label selector. + type: object + status: + description: Status object status. + items: + type: string + type: array + type: object + type: + description: Type the event type + type: string + required: + - name + - type + type: object + type: array + type: object volumes: description: Volumes contains volume Overrides of the BuildStrategy volumes in case those are allowed to be overridden. Must only contain diff --git a/docs/build.md b/docs/build.md index 9639e21084..f30bf8e06b 100644 --- a/docs/build.md +++ b/docs/build.md @@ -17,6 +17,7 @@ SPDX-License-Identifier: Apache-2.0 - [Defining the Output](#defining-the-output) - [Defining Retention Parameters](#defining-retention-parameters) - [Defining Volumes](#defining-volumes) + - [Defining Triggers](#defining-triggers) - [BuildRun deletion](#BuildRun-deletion) ## Overview @@ -602,6 +603,109 @@ spec: name: test-config ``` +### Defining Triggers + +Using the triggers, you can submit `BuildRun` instances when certain events happen. The idea is to be able to trigger Shipwright builds in an event driven fashion, for that purpose you can watch certain types of events. + +**Note**: triggers rely on the [Shipwright Triggers](https://github.com/shipwright-io/triggers) project to be deployed and configured in the same Kubernetes cluster where you run Shipwright Build. If it is not set up, the triggers defined in a Build are ignored. + +The types of events under watch are defined on the `.spec.trigger` attribute, please consider the following example: + +```yaml +apiVersion: shipwright.io/v1alpha1 +kind: Build +spec: + source: + url: https://github.com/shipwright-io/sample-go + contextDir: docker-build + credentials: + name: webhook-secret + trigger: + when: [] +``` + +Certain types of events will use attributes defined on `.spec.source` to complete the information needed in order to dispatch events. + +#### GitHub + +The GitHub type is meant to react upon events coming from GitHub WebHook interface, the events are compared against the existing `Build` resources, and therefore it can identify the `Build` objects based on `.spec.source.url` combined with the attributes on `.spec.trigger.when[].github`. + +To identify a given `Build` object, the first criteria is the repository URL, and then the branch name listed on the GitHub event payload must also match. Following the criteria: + +- First, the branch name is checked against the `.spec.trigger.when[].github.branches` entries +- If the `.spec.trigger.when[].github.branches` is empty, the branch name is compared against `.spec.source.revision` +- If `spec.source.revision` is empty, the default revision name is used ("main") + +The following snippet shows a configuration machting `Push` and `PullRequest` events on the `main` branch, for example: + +```yaml +# [...] +spec: + source: + url: https://github.com/shipwright-io/sample-go + trigger: + when: + - name: push and pull-request on the main branch + type: GitHub + github: + events: + - Push + - PullRequest + branches: + - main +``` + +#### Image + +In order to watch over images, in combination with the [Image](https://github.com/shipwright-io/image) controller, you can trigger new builds when those container image names change. + +For instance, lets imagine the image named `ghcr.io/some/base-image` is used as input for the Build process and every time it changes we would like to trigger a new build. Please consider the following snippet: + +```yaml +# [...] +spec: + trigger: + when: + - name: watching for the base-image changes + type: Image + image: + names: + - ghcr.io/some/base-image:latest +``` + +#### Tekton Pipeline + +Shipwright can also be used in combination with [Tekton Pipeline](https://github.com/tektoncd/pipeline), you can configure the Build to watch for `Pipeline` resources in Kubernetes reacting when the object reaches the desired status (`.objectRef.status`), and is identified either by its name (`.objectRef.name`) or a label selector (`.objectRef.selector`). The example below uses the label selector approach: + +```yaml +# [...] +spec: + trigger: + when: + - name: watching over for the Tekton Pipeline + type: Pipeline + objectRef: + status: + - Succeeded + selector: + label: value +``` + +While the next snippet uses the object name for identification: + +```yaml +# [...] +spec: + trigger: + when: + - name: watching over for the Tekton Pipeline + type: Pipeline + objectRef: + status: + - Succeeded + name: tekton-pipeline-name +``` + ### Sources Sources represent remote artifacts, as in external entities added to the build context before the actual Build starts. Therefore, you may employ `.spec.sources` to download artifacts from external repositories. diff --git a/pkg/apis/build/v1alpha1/build_types.go b/pkg/apis/build/v1alpha1/build_types.go index 1bf4c814d5..0ecd43e686 100644 --- a/pkg/apis/build/v1alpha1/build_types.go +++ b/pkg/apis/build/v1alpha1/build_types.go @@ -60,6 +60,17 @@ const ( VolumeNotOverridable BuildReason = "VolumeNotOverridable" // UndefinedVolume indicates that volume defined by build is not found in the strategy UndefinedVolume BuildReason = "UndefinedVolume" + // TriggerNameCanNotBeBlank indicates the trigger condition does not have a name + TriggerNameCanNotBeBlank BuildReason = "TriggerNameCanNotBeBlank" + // TriggerInvalidType indicates the trigger type is invalid + TriggerInvalidType BuildReason = "TriggerInvalidType" + // TriggerInvalidGitHubWebHook indicates the trigger type GitHub is invalid + TriggerInvalidGitHubWebHook BuildReason = "TriggerInvalidGitHubWebHook" + // TriggerInvalidImage indicates the trigger type Image is invalid + TriggerInvalidImage BuildReason = "TriggerInvalidImage" + // TriggerInvalidPipeline indicates the trigger type Pipeline is invalid + TriggerInvalidPipeline BuildReason = "TriggerInvalidPipeline" + // AllValidationsSucceeded indicates a Build was successfully validated AllValidationsSucceeded = "all validations succeeded" ) @@ -109,6 +120,11 @@ type BuildSpec struct { // +optional Sources []BuildSource `json:"sources,omitempty"` + // Trigger defines the scenarios where a new build should be triggered. + // + // +optional + Trigger *Trigger `json:"trigger,omitempty"` + // Strategy references the BuildStrategy to use to build the container // image. Strategy Strategy `json:"strategy"` diff --git a/pkg/apis/build/v1alpha1/trigger.go b/pkg/apis/build/v1alpha1/trigger.go new file mode 100644 index 0000000000..c581a5bb64 --- /dev/null +++ b/pkg/apis/build/v1alpha1/trigger.go @@ -0,0 +1,19 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" +) + +// Trigger represents the webhook trigger configuration for a Build. +type Trigger struct { + // When the list of scenarios when a new build should take place. + When []TriggerWhen `json:"when,omitempty"` + + // SecretRef points to a local object carrying the secret token to validate webhook request. + // + // +optional + SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"` +} diff --git a/pkg/apis/build/v1alpha1/trigger_types.go b/pkg/apis/build/v1alpha1/trigger_types.go new file mode 100644 index 0000000000..697033ca80 --- /dev/null +++ b/pkg/apis/build/v1alpha1/trigger_types.go @@ -0,0 +1,66 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 +package v1alpha1 + +// TriggerType set of TriggerWhen valid names. +type TriggerType string + +const ( + // GitHubWebHookTrigger GitHubWebHookTrigger trigger type name. + GitHubWebHookTrigger TriggerType = "GitHub" + + // ImageTrigger Image trigger type name. + ImageTrigger TriggerType = "Image" + + // PipelineTrigger Tekton Pipeline trigger type name. + PipelineTrigger TriggerType = "Pipeline" +) + +// GitHubEventName set of WhenGitHub valid event names. +type GitHubEventName string + +const ( + // GitHubPullRequestEvent github pull-request event name. + GitHubPullRequestEvent GitHubEventName = "PullRequest" + + // GitHubPushEvent git push webhook event name. + GitHubPushEvent GitHubEventName = "Push" +) + +// WhenImage attributes to match Image events. +type WhenImage struct { + // Names fully qualified image names. + // + // +optional + Names []string `json:"names,omitempty"` +} + +// WhenGitHub attributes to match GitHub events. +type WhenGitHub struct { + // Events GitHub event names. + // + // +kubebuilder:validation:MinItems=1 + Events []GitHubEventName `json:"events,omitempty"` + + // Branches slice of branch names where the event applies. + // + // +optional + Branches []string `json:"branches,omitempty"` +} + +// WhenObjectRef attributes to reference local Kubernetes objects. +type WhenObjectRef struct { + // Name target object name. + // + // +optional + Name string `json:"name,omitempty"` + + // Status object status. + Status []string `json:"status,omitempty"` + + // Selector label selector. + // + // +optional + Selector map[string]string `json:"selector,omitempty"` +} diff --git a/pkg/apis/build/v1alpha1/trigger_when.go b/pkg/apis/build/v1alpha1/trigger_when.go new file mode 100644 index 0000000000..f127a0adc0 --- /dev/null +++ b/pkg/apis/build/v1alpha1/trigger_when.go @@ -0,0 +1,41 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 +package v1alpha1 + +// TriggerWhen a given scenario where the webhook trigger is applicable. +type TriggerWhen struct { + // Name name or the short description of the trigger condition. + Name string `json:"name"` + + // Type the event type + Type TriggerType `json:"type"` + + // GitHub describes how to trigger builds based on GitHub (SCM) events. + // + // +optional + GitHub *WhenGitHub `json:"github,omitempty"` + + // Image slice of image names where the event applies. + // + // +optional + Image *WhenImage `json:"image,omitempty"` + + // ObjectRef describes how to match a foreign resource, either using the name or the label + // selector, plus the current resource status. + // + // +optional + ObjectRef *WhenObjectRef `json:"objectRef,omitempty"` +} + +// GetBranches return a slice of branch names based on the WhenTypeName informed. +func (w *TriggerWhen) GetBranches(whenType TriggerType) []string { + switch whenType { + case GitHubWebHookTrigger: + if w.GitHub == nil { + return nil + } + return w.GitHub.Branches + } + return nil +} diff --git a/pkg/apis/build/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/build/v1alpha1/zz_generated.deepcopy.go index 448524741e..be57746009 100644 --- a/pkg/apis/build/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/build/v1alpha1/zz_generated.deepcopy.go @@ -394,6 +394,11 @@ func (in *BuildSpec) DeepCopyInto(out *BuildSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Trigger != nil { + in, out := &in.Trigger, &out.Trigger + *out = new(Trigger) + (*in).DeepCopyInto(*out) + } in.Strategy.DeepCopyInto(&out.Strategy) if in.Builder != nil { in, out := &in.Builder, &out.Builder @@ -1125,3 +1130,137 @@ func (in *Strategy) DeepCopy() *Strategy { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Trigger) DeepCopyInto(out *Trigger) { + *out = *in + if in.When != nil { + in, out := &in.When, &out.When + *out = make([]TriggerWhen, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(corev1.LocalObjectReference) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Trigger. +func (in *Trigger) DeepCopy() *Trigger { + if in == nil { + return nil + } + out := new(Trigger) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TriggerWhen) DeepCopyInto(out *TriggerWhen) { + *out = *in + if in.GitHub != nil { + in, out := &in.GitHub, &out.GitHub + *out = new(WhenGitHub) + (*in).DeepCopyInto(*out) + } + if in.Image != nil { + in, out := &in.Image, &out.Image + *out = new(WhenImage) + (*in).DeepCopyInto(*out) + } + if in.ObjectRef != nil { + in, out := &in.ObjectRef, &out.ObjectRef + *out = new(WhenObjectRef) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TriggerWhen. +func (in *TriggerWhen) DeepCopy() *TriggerWhen { + if in == nil { + return nil + } + out := new(TriggerWhen) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WhenGitHub) DeepCopyInto(out *WhenGitHub) { + *out = *in + if in.Events != nil { + in, out := &in.Events, &out.Events + *out = make([]GitHubEventName, len(*in)) + copy(*out, *in) + } + if in.Branches != nil { + in, out := &in.Branches, &out.Branches + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WhenGitHub. +func (in *WhenGitHub) DeepCopy() *WhenGitHub { + if in == nil { + return nil + } + out := new(WhenGitHub) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WhenImage) DeepCopyInto(out *WhenImage) { + *out = *in + if in.Names != nil { + in, out := &in.Names, &out.Names + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WhenImage. +func (in *WhenImage) DeepCopy() *WhenImage { + if in == nil { + return nil + } + out := new(WhenImage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WhenObjectRef) DeepCopyInto(out *WhenObjectRef) { + *out = *in + if in.Status != nil { + in, out := &in.Status, &out.Status + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Selector != nil { + in, out := &in.Selector, &out.Selector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WhenObjectRef. +func (in *WhenObjectRef) DeepCopy() *WhenObjectRef { + if in == nil { + return nil + } + out := new(WhenObjectRef) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/reconciler/build/build.go b/pkg/reconciler/build/build.go index 9d6f2d8ba4..ba9b2a9d2c 100644 --- a/pkg/reconciler/build/build.go +++ b/pkg/reconciler/build/build.go @@ -31,6 +31,7 @@ var validationTypes = [...]string{ validate.Sources, validate.BuildName, validate.Envs, + validate.Triggers, } // ReconcileBuild reconciles a Build object diff --git a/pkg/validate/trigger.go b/pkg/validate/trigger.go new file mode 100644 index 0000000000..562aafea1f --- /dev/null +++ b/pkg/validate/trigger.go @@ -0,0 +1,121 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 +package validate + +import ( + "context" + "fmt" + + build "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" + kerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/utils/pointer" +) + +// Trigger implements the interface BuildPath with the objective of applying validations against the +// `.spec.trigger` related attributes. +type Trigger struct { + build *build.Build // build instance +} + +// validate goes through the trigger "when" conditions to validate each entry. +func (t *Trigger) validate(triggerWhen []build.TriggerWhen) []error { + var allErrs []error + for _, when := range triggerWhen { + if when.Name == "" { + t.build.Status.Reason = build.BuildReasonPtr(build.TriggerNameCanNotBeBlank) + t.build.Status.Message = pointer.String("name is not set on when trigger condition") + allErrs = append(allErrs, fmt.Errorf("%s", *t.build.Status.Message)) + } + + switch when.Type { + case build.GitHubWebHookTrigger: + if when.GitHub == nil { + t.build.Status.Reason = build.BuildReasonPtr(build.TriggerInvalidGitHubWebHook) + t.build.Status.Message = pointer.String(fmt.Sprintf( + "%q is missing required attribute `.github`", when.Name, + )) + allErrs = append(allErrs, fmt.Errorf("%s", *t.build.Status.Message)) + } else { + if len(when.GitHub.Events) == 0 { + t.build.Status.Reason = build.BuildReasonPtr(build.TriggerInvalidGitHubWebHook) + t.build.Status.Message = pointer.String(fmt.Sprintf( + "%q is missing required attribute `.github.events`", when.Name, + )) + allErrs = append(allErrs, fmt.Errorf("%s", *t.build.Status.Message)) + } + } + case build.ImageTrigger: + if when.Image == nil { + t.build.Status.Reason = build.BuildReasonPtr(build.TriggerInvalidImage) + t.build.Status.Message = pointer.String(fmt.Sprintf( + "%q is missing required attribute `.image`", when.Name, + )) + allErrs = append(allErrs, fmt.Errorf("%s", *t.build.Status.Message)) + } else { + if len(when.Image.Names) == 0 { + t.build.Status.Reason = build.BuildReasonPtr(build.TriggerInvalidImage) + t.build.Status.Message = pointer.String(fmt.Sprintf( + "%q is missing required attribute `.image.names`", when.Name, + )) + allErrs = append(allErrs, fmt.Errorf("%s", *t.build.Status.Message)) + } + } + case build.PipelineTrigger: + if when.ObjectRef == nil { + t.build.Status.Reason = build.BuildReasonPtr(build.TriggerInvalidPipeline) + t.build.Status.Message = pointer.String(fmt.Sprintf( + "%q is missing required attribute `.objectRef`", when.Name, + )) + allErrs = append(allErrs, fmt.Errorf("%s", *t.build.Status.Message)) + } else { + if len(when.ObjectRef.Status) == 0 { + t.build.Status.Reason = build.BuildReasonPtr(build.TriggerInvalidPipeline) + t.build.Status.Message = pointer.String(fmt.Sprintf( + "%q is missing required attribute `.objectRef.status`", when.Name, + )) + allErrs = append(allErrs, fmt.Errorf("%s", *t.build.Status.Message)) + } + if when.ObjectRef.Name == "" && len(when.ObjectRef.Selector) == 0 { + t.build.Status.Reason = build.BuildReasonPtr(build.TriggerInvalidPipeline) + t.build.Status.Message = pointer.String(fmt.Sprintf( + "%q is missing required attributes `.objectRef.name` or `.objectRef.selector`", + when.Name, + )) + allErrs = append(allErrs, fmt.Errorf("%s", *t.build.Status.Message)) + } + if when.ObjectRef.Name != "" && len(when.ObjectRef.Selector) > 0 { + t.build.Status.Reason = build.BuildReasonPtr(build.TriggerInvalidPipeline) + t.build.Status.Message = pointer.String(fmt.Sprintf( + "%q contains `.objectRef.name` and `.objectRef.selector`, must be only one", + when.Name, + )) + allErrs = append(allErrs, fmt.Errorf("%s", *t.build.Status.Message)) + } + } + default: + t.build.Status.Reason = build.BuildReasonPtr(build.TriggerInvalidType) + t.build.Status.Message = pointer.String( + fmt.Sprintf("%q contains an invalid type %q", when.Name, when.Type)) + allErrs = append(allErrs, fmt.Errorf("%s", *t.build.Status.Message)) + } + } + return allErrs +} + +// ValidatePath validates the `.spec.trigger` path. +func (t *Trigger) ValidatePath(_ context.Context) error { + if t.build.Spec.Trigger == nil || len(t.build.Spec.Trigger.When) == 0 { + return nil + } + + if allErrs := t.validate(t.build.Spec.Trigger.When); len(allErrs) != 0 { + return fmt.Errorf("%s", kerrors.NewAggregate(allErrs).Error()) + } + return nil +} + +// NewTrigger instantiate Trigger validation helper. +func NewTrigger(b *build.Build) *Trigger { + return &Trigger{build: b} +} diff --git a/pkg/validate/trigger_test.go b/pkg/validate/trigger_test.go new file mode 100644 index 0000000000..728e8e35d8 --- /dev/null +++ b/pkg/validate/trigger_test.go @@ -0,0 +1,272 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 +package validate_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + build "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" + "github.com/shipwright-io/build/pkg/validate" +) + +var _ = Describe("ValidateBuildTriggers", func() { + Context("trigger name is not informed", func() { + b := &build.Build{ + Spec: build.BuildSpec{ + Trigger: &build.Trigger{ + When: []build.TriggerWhen{{ + Name: "", + }}, + }, + }, + } + + It("should error when name is not set", func() { + err := validate.NewTrigger(b).ValidatePath(context.TODO()) + Expect(err.Error()).To(ContainSubstring("name is not set")) + }) + }) + + Context("trigger type github", func() { + It("should error when github attribute is not set", func() { + b := &build.Build{ + Spec: build.BuildSpec{ + Trigger: &build.Trigger{ + When: []build.TriggerWhen{{ + Name: "github", + Type: build.GitHubWebHookTrigger, + }}, + }, + }, + } + + err := validate.NewTrigger(b).ValidatePath(context.TODO()) + Expect(err.Error()).To(ContainSubstring("missing required attribute `.github`")) + }) + + It("should error when github events attribute is empty", func() { + b := &build.Build{ + Spec: build.BuildSpec{ + Trigger: &build.Trigger{ + When: []build.TriggerWhen{{ + Name: "github", + Type: build.GitHubWebHookTrigger, + GitHub: &build.WhenGitHub{ + Events: []build.GitHubEventName{}, + }, + }}, + }, + }, + } + + err := validate.NewTrigger(b).ValidatePath(context.TODO()) + Expect(err.Error()).To(ContainSubstring("missing required attribute `.github.events`")) + }) + + It("should pass when github type is complete", func() { + b := &build.Build{ + Spec: build.BuildSpec{ + Trigger: &build.Trigger{ + When: []build.TriggerWhen{{ + Name: "github", + Type: build.GitHubWebHookTrigger, + GitHub: &build.WhenGitHub{ + Events: []build.GitHubEventName{ + build.GitHubPushEvent, + }, + }, + }}, + }, + }, + } + + err := validate.NewTrigger(b).ValidatePath(context.TODO()) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Context("trigger type image", func() { + It("should error when image attribute is not set", func() { + b := &build.Build{ + Spec: build.BuildSpec{ + Trigger: &build.Trigger{ + When: []build.TriggerWhen{{ + Name: "image", + Type: build.ImageTrigger, + }}, + }, + }, + } + + err := validate.NewTrigger(b).ValidatePath(context.TODO()) + Expect(err.Error()).To(ContainSubstring("missing required attribute `.image`")) + }) + + It("should error when image names attribute is empty", func() { + b := &build.Build{ + Spec: build.BuildSpec{ + Trigger: &build.Trigger{ + When: []build.TriggerWhen{{ + Name: "image", + Type: build.ImageTrigger, + Image: &build.WhenImage{ + Names: []string{}, + }, + }}, + }, + }, + } + + err := validate.NewTrigger(b).ValidatePath(context.TODO()) + Expect(err.Error()).To(ContainSubstring("missing required attribute `.image.names`")) + }) + + It("should pass when github type is complete", func() { + b := &build.Build{ + Spec: build.BuildSpec{ + Trigger: &build.Trigger{ + When: []build.TriggerWhen{{ + Name: "image", + Type: build.ImageTrigger, + Image: &build.WhenImage{ + Names: []string{ + "ghcr.io/shipwright-io/build:latest", + }, + }, + }}, + }, + }, + } + + err := validate.NewTrigger(b).ValidatePath(context.TODO()) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Context("trigger type pipeline", func() { + It("should error when objectRef attribute is not set", func() { + b := &build.Build{ + Spec: build.BuildSpec{ + Trigger: &build.Trigger{ + When: []build.TriggerWhen{{ + Name: "pipeline", + Type: build.PipelineTrigger, + }}, + }, + }, + } + + err := validate.NewTrigger(b).ValidatePath(context.TODO()) + Expect(err.Error()).To(ContainSubstring("missing required attribute `.objectRef`")) + }) + + It("should error when status attribute is empty", func() { + b := &build.Build{ + Spec: build.BuildSpec{ + Trigger: &build.Trigger{ + When: []build.TriggerWhen{{ + Name: "pipeline", + Type: build.PipelineTrigger, + ObjectRef: &build.WhenObjectRef{ + Status: []string{}, + }, + }}, + }, + }, + } + + err := validate.NewTrigger(b).ValidatePath(context.TODO()) + Expect(err.Error()).To(ContainSubstring("missing required attribute `.objectRef.status`")) + }) + + It("should error when missing required attributes", func() { + b := &build.Build{ + Spec: build.BuildSpec{ + Trigger: &build.Trigger{ + When: []build.TriggerWhen{{ + Name: "pipeline", + Type: build.PipelineTrigger, + ObjectRef: &build.WhenObjectRef{ + Status: []string{"Succeed"}, + }, + }}, + }, + }, + } + + err := validate.NewTrigger(b).ValidatePath(context.TODO()) + Expect(err.Error()).To(ContainSubstring( + "is missing required attributes `.objectRef.name` or `.objectRef.selector`", + )) + }) + + It("should error when declaring conflicting attributes", func() { + b := &build.Build{ + Spec: build.BuildSpec{ + Trigger: &build.Trigger{ + When: []build.TriggerWhen{{ + Name: "pipeline", + Type: build.PipelineTrigger, + ObjectRef: &build.WhenObjectRef{ + Status: []string{"Succeed"}, + Name: "name", + Selector: map[string]string{ + "k": "v", + }, + }, + }}, + }, + }, + } + + err := validate.NewTrigger(b).ValidatePath(context.TODO()) + Expect(err.Error()).To(ContainSubstring( + "contains `.objectRef.name` and `.objectRef.selector`, must be only one", + )) + }) + + It("should pass when objectRef type is complete", func() { + b := &build.Build{ + Spec: build.BuildSpec{ + Trigger: &build.Trigger{ + When: []build.TriggerWhen{{ + Name: "pipeline", + Type: build.PipelineTrigger, + ObjectRef: &build.WhenObjectRef{ + Status: []string{"Succeed"}, + Name: "name", + }, + }}, + }, + }, + } + + err := validate.NewTrigger(b).ValidatePath(context.TODO()) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Context("invalid trigger type", func() { + It("should error when declaring a invalid trigger type", func() { + b := &build.Build{ + Spec: build.BuildSpec{ + Trigger: &build.Trigger{ + When: []build.TriggerWhen{{ + Name: "pipeline", + Type: build.TriggerType("invalid"), + ObjectRef: &build.WhenObjectRef{ + Name: "name", + }, + }}, + }, + }, + } + + err := validate.NewTrigger(b).ValidatePath(context.TODO()) + Expect(err.Error()).To(ContainSubstring("contains an invalid type")) + }) + }) +}) diff --git a/pkg/validate/validate.go b/pkg/validate/validate.go index 566c6c0c2f..408fee1042 100644 --- a/pkg/validate/validate.go +++ b/pkg/validate/validate.go @@ -31,8 +31,13 @@ const ( // OwnerReferences for validating the ownerreferences between a Build // and BuildRun objects OwnerReferences = "ownerreferences" - namespace = "namespace" - name = "name" + // Triggers for validating the `.spec.triggers` entries + Triggers = "triggers" +) + +const ( + namespace = "namespace" + name = "name" ) // BuildPath is an interface that holds a ValidatePath() function @@ -64,6 +69,8 @@ func NewValidation( return &BuildNameRef{Build: build}, nil case Envs: return &Env{Build: build}, nil + case Triggers: + return &Trigger{build: build}, nil default: return nil, fmt.Errorf("unknown validation type") } @@ -113,6 +120,11 @@ func BuildRunFields(buildRun *build.BuildRun) (string, string) { return resources.BuildRunBuildFieldOverrideForbidden, "cannot use 'timeout' override and 'buildSpec' simultaneously" } + + if buildRun.Spec.BuildSpec.Trigger != nil { + return resources.BuildRunBuildFieldOverrideForbidden, + "cannot use 'triggers' override in the 'BuildRun', only allowed in the 'Build'" + } } return "", ""