Skip to content

Commit 69bbfec

Browse files
committed
Add Tolerations to Build and BuildRun objects
Signed-off-by: Dylan Orzel <dorzel@redhat.com>
1 parent 0060a5c commit 69bbfec

File tree

10 files changed

+274
-2
lines changed

10 files changed

+274
-2
lines changed

deploy/crds/shipwright.io_buildruns.yaml

+117
Original file line numberDiff line numberDiff line change
@@ -7535,6 +7535,45 @@ spec:
75357535
Build should take to execute.
75367536
format: duration
75377537
type: string
7538+
tolerations:
7539+
description: If specified, the pod's tolerations.
7540+
items:
7541+
description: |-
7542+
The pod this Toleration is attached to tolerates any taint that matches
7543+
the triple <key,value,effect> using the matching operator <operator>.
7544+
properties:
7545+
effect:
7546+
description: |-
7547+
Effect indicates the taint effect to match. Empty means match all taint effects.
7548+
When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
7549+
type: string
7550+
key:
7551+
description: |-
7552+
Key is the taint key that the toleration applies to. Empty means match all taint keys.
7553+
If the key is empty, operator must be Exists; this combination means to match all values and all keys.
7554+
type: string
7555+
operator:
7556+
description: |-
7557+
Operator represents a key's relationship to the value.
7558+
Valid operators are Exists and Equal. Defaults to Equal.
7559+
Exists is equivalent to wildcard for value, so that a pod can
7560+
tolerate all taints of a particular category.
7561+
type: string
7562+
tolerationSeconds:
7563+
description: |-
7564+
TolerationSeconds represents the period of time the toleration (which must be
7565+
of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
7566+
it is not set, which means tolerate the taint forever (do not evict). Zero and
7567+
negative values will be treated as 0 (evict immediately) by the system.
7568+
format: int64
7569+
type: integer
7570+
value:
7571+
description: |-
7572+
Value is the taint value the toleration matches to.
7573+
If the operator is Exists, the value should be empty, otherwise just a regular string.
7574+
type: string
7575+
type: object
7576+
type: array
75387577
trigger:
75397578
description: Trigger defines the scenarios where a new build
75407579
should be triggered.
@@ -9753,6 +9792,45 @@ spec:
97539792
description: Timeout defines the maximum run time of this BuildRun.
97549793
format: duration
97559794
type: string
9795+
tolerations:
9796+
description: If specified, the pod's tolerations.
9797+
items:
9798+
description: |-
9799+
The pod this Toleration is attached to tolerates any taint that matches
9800+
the triple <key,value,effect> using the matching operator <operator>.
9801+
properties:
9802+
effect:
9803+
description: |-
9804+
Effect indicates the taint effect to match. Empty means match all taint effects.
9805+
When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
9806+
type: string
9807+
key:
9808+
description: |-
9809+
Key is the taint key that the toleration applies to. Empty means match all taint keys.
9810+
If the key is empty, operator must be Exists; this combination means to match all values and all keys.
9811+
type: string
9812+
operator:
9813+
description: |-
9814+
Operator represents a key's relationship to the value.
9815+
Valid operators are Exists and Equal. Defaults to Equal.
9816+
Exists is equivalent to wildcard for value, so that a pod can
9817+
tolerate all taints of a particular category.
9818+
type: string
9819+
tolerationSeconds:
9820+
description: |-
9821+
TolerationSeconds represents the period of time the toleration (which must be
9822+
of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
9823+
it is not set, which means tolerate the taint forever (do not evict). Zero and
9824+
negative values will be treated as 0 (evict immediately) by the system.
9825+
format: int64
9826+
type: integer
9827+
value:
9828+
description: |-
9829+
Value is the taint value the toleration matches to.
9830+
If the operator is Exists, the value should be empty, otherwise just a regular string.
9831+
type: string
9832+
type: object
9833+
type: array
97569834
volumes:
97579835
description: |-
97589836
Volumes contains volume Overrides of the BuildStrategy volumes in case those are allowed
@@ -11959,6 +12037,45 @@ spec:
1195912037
should take to execute.
1196012038
format: duration
1196112039
type: string
12040+
tolerations:
12041+
description: If specified, the pod's tolerations.
12042+
items:
12043+
description: |-
12044+
The pod this Toleration is attached to tolerates any taint that matches
12045+
the triple <key,value,effect> using the matching operator <operator>.
12046+
properties:
12047+
effect:
12048+
description: |-
12049+
Effect indicates the taint effect to match. Empty means match all taint effects.
12050+
When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
12051+
type: string
12052+
key:
12053+
description: |-
12054+
Key is the taint key that the toleration applies to. Empty means match all taint keys.
12055+
If the key is empty, operator must be Exists; this combination means to match all values and all keys.
12056+
type: string
12057+
operator:
12058+
description: |-
12059+
Operator represents a key's relationship to the value.
12060+
Valid operators are Exists and Equal. Defaults to Equal.
12061+
Exists is equivalent to wildcard for value, so that a pod can
12062+
tolerate all taints of a particular category.
12063+
type: string
12064+
tolerationSeconds:
12065+
description: |-
12066+
TolerationSeconds represents the period of time the toleration (which must be
12067+
of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
12068+
it is not set, which means tolerate the taint forever (do not evict). Zero and
12069+
negative values will be treated as 0 (evict immediately) by the system.
12070+
format: int64
12071+
type: integer
12072+
value:
12073+
description: |-
12074+
Value is the taint value the toleration matches to.
12075+
If the operator is Exists, the value should be empty, otherwise just a regular string.
12076+
type: string
12077+
type: object
12078+
type: array
1196212079
trigger:
1196312080
description: Trigger defines the scenarios where a new build should
1196412081
be triggered.

deploy/crds/shipwright.io_builds.yaml

+39
Original file line numberDiff line numberDiff line change
@@ -2912,6 +2912,45 @@ spec:
29122912
should take to execute.
29132913
format: duration
29142914
type: string
2915+
tolerations:
2916+
description: If specified, the pod's tolerations.
2917+
items:
2918+
description: |-
2919+
The pod this Toleration is attached to tolerates any taint that matches
2920+
the triple <key,value,effect> using the matching operator <operator>.
2921+
properties:
2922+
effect:
2923+
description: |-
2924+
Effect indicates the taint effect to match. Empty means match all taint effects.
2925+
When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
2926+
type: string
2927+
key:
2928+
description: |-
2929+
Key is the taint key that the toleration applies to. Empty means match all taint keys.
2930+
If the key is empty, operator must be Exists; this combination means to match all values and all keys.
2931+
type: string
2932+
operator:
2933+
description: |-
2934+
Operator represents a key's relationship to the value.
2935+
Valid operators are Exists and Equal. Defaults to Equal.
2936+
Exists is equivalent to wildcard for value, so that a pod can
2937+
tolerate all taints of a particular category.
2938+
type: string
2939+
tolerationSeconds:
2940+
description: |-
2941+
TolerationSeconds represents the period of time the toleration (which must be
2942+
of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
2943+
it is not set, which means tolerate the taint forever (do not evict). Zero and
2944+
negative values will be treated as 0 (evict immediately) by the system.
2945+
format: int64
2946+
type: integer
2947+
value:
2948+
description: |-
2949+
Value is the taint value the toleration matches to.
2950+
If the operator is Exists, the value should be empty, otherwise just a regular string.
2951+
type: string
2952+
type: object
2953+
type: array
29152954
trigger:
29162955
description: Trigger defines the scenarios where a new build should
29172956
be triggered.

pkg/apis/build/v1beta1/build_types.go

+8
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ const (
7878
OutputTimestampNotValid BuildReason = "OutputTimestampNotValid"
7979
// NodeSelectorNotValid indicates that the nodeSelector value is not valid
8080
NodeSelectorNotValid BuildReason = "NodeSelectorNotValid"
81+
// TolerationNotValid indicates that the Toleration value is not valid
82+
TolerationNotValid BuildReason = "TolerationNotValid"
8183

8284
// AllValidationsSucceeded indicates a Build was successfully validated
8385
AllValidationsSucceeded = "all validations succeeded"
@@ -183,6 +185,12 @@ type BuildSpec struct {
183185
//
184186
// +optional
185187
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
188+
189+
// If specified, the pod's tolerations.
190+
// +optional
191+
// +patchMergeKey=Key
192+
// +patchStrategy=merge
193+
Tolerations []corev1.Toleration `json:"tolerations,omitempty" patchStrategy:"merge" patchMergeKey:"Key"`
186194
}
187195

188196
// BuildVolume is a volume that will be mounted in build pod during build step

pkg/apis/build/v1beta1/buildrun_types.go

+6
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,12 @@ type BuildRunSpec struct {
115115
//
116116
// +optional
117117
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
118+
119+
// If specified, the pod's tolerations.
120+
// +optional
121+
// +patchMergeKey=Key
122+
// +patchStrategy=merge
123+
Tolerations []corev1.Toleration `json:"tolerations,omitempty" patchStrategy:"merge" patchMergeKey:"Key"`
118124
}
119125

120126
// BuildRunRequestedState defines the buildrun state the user can provide to override whatever is the current state.

pkg/reconciler/build/build.go

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ var validationTypes = [...]string{
3434
validate.Envs,
3535
validate.Triggers,
3636
validate.NodeSelector,
37+
validate.Tolerations,
3738
}
3839

3940
// ReconcileBuild reconciles a Build object

pkg/reconciler/buildrun/buildrun.go

+1
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ func (r *ReconcileBuildRun) Reconcile(ctx context.Context, request reconcile.Req
161161
validate.NewBuildName(build),
162162
validate.NewEnv(build),
163163
validate.NewNodeSelector(build),
164+
validate.NewTolerations(build),
164165
)
165166

166167
// an internal/technical error during validation happened

pkg/reconciler/buildrun/resources/build.go

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ func GetBuildObject(ctx context.Context, client client.Client, buildRun *buildv1
4040
build.Namespace = buildRun.Namespace
4141
build.Status = buildv1beta1.BuildStatus{}
4242
buildRun.Spec.Build.Spec.DeepCopyInto(&build.Spec)
43+
// In this case, fields set only on the BuildRun object do not get validated as they are not copied to the transient Build resource.
44+
// explicitly setting them here is required for validation to happen.
45+
build.Spec.NodeSelector = buildRun.Spec.NodeSelector
46+
build.Spec.Tolerations = buildRun.Spec.Tolerations
4347
return nil
4448
}
4549

pkg/reconciler/buildrun/resources/taskrun.go

+33-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package resources
77
import (
88
"fmt"
99
"path"
10+
"slices"
1011
"strconv"
1112
"strings"
1213

@@ -235,12 +236,27 @@ func GenerateTaskRun(
235236
},
236237
}
237238

239+
taskRunPodTemplate := &pod.PodTemplate{}
238240
// Merge Build and BuildRun NodeSelectors, giving preference to BuildRun NodeSelector
239241
taskRunNodeSelector := mergeMaps(build.Spec.NodeSelector, buildRun.Spec.NodeSelector)
240242
if len(taskRunNodeSelector) > 0 {
241-
expectedTaskRun.Spec.PodTemplate = &pod.PodTemplate{
242-
NodeSelector: taskRunNodeSelector,
243+
taskRunPodTemplate.NodeSelector = taskRunNodeSelector
244+
}
245+
246+
// Merge Build and BuildRun Tolerations, giving preference to BuildRun Tolerations values
247+
taskRunTolerations := mergeTolerations(build.Spec.Tolerations, buildRun.Spec.Tolerations)
248+
if len(taskRunTolerations) > 0 {
249+
for i, toleration := range taskRunTolerations {
250+
if toleration.Effect == "" {
251+
// set unspecified effects to TainEffectNoSchedule, as that is the only supported effect
252+
taskRunTolerations[i].Effect = corev1.TaintEffectNoSchedule
253+
}
243254
}
255+
taskRunPodTemplate.Tolerations = taskRunTolerations
256+
}
257+
258+
if !(taskRunPodTemplate.Equals(&pod.PodTemplate{})) {
259+
expectedTaskRun.Spec.PodTemplate = taskRunPodTemplate
244260
}
245261

246262
// assign the annotations from the build strategy, filter out those that should not be propagated
@@ -354,6 +370,21 @@ func effectiveTimeout(build *buildv1beta1.Build, buildRun *buildv1beta1.BuildRun
354370
return nil
355371
}
356372

373+
// mergeTolerations merges the values for Spec.Tolerations in the given Build and BuildRun objects, with values in the BuildRun object overriding values
374+
// in the Build object (if present).
375+
func mergeTolerations(buildTolerations []corev1.Toleration, buildRunTolerations []corev1.Toleration) []corev1.Toleration {
376+
mergedTolerations := []corev1.Toleration{}
377+
mergedTolerations = append(mergedTolerations, buildRunTolerations...)
378+
for _, toleration := range buildTolerations {
379+
if !slices.ContainsFunc(mergedTolerations, func(t corev1.Toleration) bool {
380+
return t.Key == toleration.Key
381+
}) {
382+
mergedTolerations = append(mergedTolerations, toleration)
383+
}
384+
}
385+
return mergedTolerations
386+
}
387+
357388
// isPropagatableAnnotation filters the last-applied-configuration annotation from kubectl because this would break the meaning of this annotation on the target object;
358389
// also, annotations using our own custom resource domains are filtered out because we have no annotations with a semantic for both TaskRun and Pod
359390
func isPropagatableAnnotation(key string) bool {

pkg/validate/tolerations.go

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright The Shipwright Contributors
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package validate
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"strings"
11+
12+
v1 "k8s.io/api/core/v1"
13+
"k8s.io/apimachinery/pkg/util/validation"
14+
"k8s.io/utils/ptr"
15+
16+
build "github.com/shipwright-io/build/pkg/apis/build/v1beta1"
17+
)
18+
19+
// TolerationsRef contains all required fields
20+
// to validate tolerations
21+
type TolerationsRef struct {
22+
Build *build.Build // build instance for analysis
23+
}
24+
25+
func NewTolerations(build *build.Build) *TolerationsRef {
26+
return &TolerationsRef{build}
27+
}
28+
29+
// ValidatePath implements BuildPath interface and validates
30+
// that tolerations key/operator/value are valid
31+
func (b *TolerationsRef) ValidatePath(_ context.Context) error {
32+
for _, toleration := range b.Build.Spec.Tolerations {
33+
// validate Key
34+
if errs := validation.IsQualifiedName(toleration.Key); errs != nil {
35+
b.Build.Status.Reason = ptr.To(build.TolerationNotValid)
36+
b.Build.Status.Message = ptr.To(strings.Join(errs, ", "))
37+
}
38+
// validate Operator
39+
if !((toleration.Operator == v1.TolerationOpExists) || (toleration.Operator == v1.TolerationOpEqual)) {
40+
b.Build.Status.Reason = ptr.To(build.TolerationNotValid)
41+
b.Build.Status.Message = ptr.To(fmt.Sprintf("Toleration operator not valid. Must be one of: '%v', '%v'", v1.TolerationOpExists, v1.TolerationOpEqual))
42+
}
43+
// validate Value
44+
if errs := validation.IsValidLabelValue(toleration.Value); errs != nil {
45+
b.Build.Status.Reason = ptr.To(build.TolerationNotValid)
46+
b.Build.Status.Message = ptr.To(strings.Join(errs, ", "))
47+
}
48+
// validate Taint Effect, of which only "NoSchedule" is supported
49+
if !((toleration.Effect) == "" || (toleration.Effect == v1.TaintEffectNoSchedule)) {
50+
b.Build.Status.Reason = ptr.To(build.TolerationNotValid)
51+
b.Build.Status.Message = ptr.To(fmt.Sprintf("Only the '%v' toleration effect is supported.", v1.TaintEffectNoSchedule))
52+
}
53+
// validate TolerationSeconds, which should not be specified
54+
if toleration.TolerationSeconds != nil {
55+
b.Build.Status.Reason = ptr.To(build.TolerationNotValid)
56+
b.Build.Status.Message = ptr.To("Specifying TolerationSeconds is not supported.")
57+
}
58+
}
59+
60+
return nil
61+
}

pkg/validate/validate.go

+4
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ const (
3737
Triggers = "triggers"
3838
// NodeSelector for validating `spec.nodeSelector` entry
3939
NodeSelector = "nodeselector"
40+
// Tolerations for validating `spec.tolerations` entry
41+
Tolerations = "tolerations"
4042
)
4143

4244
const (
@@ -79,6 +81,8 @@ func NewValidation(
7981
return &Trigger{build: build}, nil
8082
case NodeSelector:
8183
return &NodeSelectorRef{Build: build}, nil
84+
case Tolerations:
85+
return &TolerationsRef{Build: build}, nil
8286
default:
8387
return nil, fmt.Errorf("unknown validation type")
8488
}

0 commit comments

Comments
 (0)