Skip to content

Commit

Permalink
Add Tolerations to Build and BuildRun objects
Browse files Browse the repository at this point in the history
Signed-off-by: Dylan Orzel <[email protected]>
  • Loading branch information
dorzel committed Dec 6, 2024
1 parent 028e45d commit aba3865
Show file tree
Hide file tree
Showing 10 changed files with 284 additions and 14 deletions.
25 changes: 14 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ jobs:
version: v0.24.0
node_image: kindest/node:${{ matrix.kubernetes }}
cluster_name: kind
config: test/kind/config.yaml
config: test/kind/config_three_node.yaml
wait: 120s
- name: Verify kind cluster
run: |
Expand All @@ -190,18 +190,21 @@ jobs:
echo "# KinD nodes:"
kubectl get nodes
NODE_STATUS=$(kubectl get node kind-control-plane -o json | jq -r .'status.conditions[] | select(.type == "Ready") | .status')
if [ "${NODE_STATUS}" != "True" ]; then
echo "# Node is not ready:"
kubectl describe node kind-control-plane
for nodename in $(kubectl get nodes -o name); do
kubectl wait --for=condition=Ready=true ${nodename} --timeout=60s
NODE_STATUS=$(kubectl get ${nodename} -o json | jq -r .'status.conditions[] | select(.type == "Ready") | .status')
if [ "${NODE_STATUS}" != "True" ]; then
echo "# Node is not ready:"
kubectl describe ${nodename}
echo "# Pods:"
kubectl get pod -A
echo "# Events:"
kubectl get events -A
echo "# Pods:"
kubectl get pod -A
echo "# Events:"
kubectl get events -A
exit 1
fi
exit 1
fi
done
- name: Install Tekton
env:
TEKTON_VERSION: ${{ matrix.tekton }}
Expand Down
117 changes: 117 additions & 0 deletions deploy/crds/shipwright.io_buildruns.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7535,6 +7535,45 @@ spec:
Build should take to execute.
format: duration
type: string
tolerations:
description: If specified, the pod's tolerations.
items:
description: |-
The pod this Toleration is attached to tolerates any taint that matches
the triple <key,value,effect> using the matching operator <operator>.
properties:
effect:
description: |-
Effect indicates the taint effect to match. Empty means match all taint effects.
When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
type: string
key:
description: |-
Key is the taint key that the toleration applies to. Empty means match all taint keys.
If the key is empty, operator must be Exists; this combination means to match all values and all keys.
type: string
operator:
description: |-
Operator represents a key's relationship to the value.
Valid operators are Exists and Equal. Defaults to Equal.
Exists is equivalent to wildcard for value, so that a pod can
tolerate all taints of a particular category.
type: string
tolerationSeconds:
description: |-
TolerationSeconds represents the period of time the toleration (which must be
of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
it is not set, which means tolerate the taint forever (do not evict). Zero and
negative values will be treated as 0 (evict immediately) by the system.
format: int64
type: integer
value:
description: |-
Value is the taint value the toleration matches to.
If the operator is Exists, the value should be empty, otherwise just a regular string.
type: string
type: object
type: array
trigger:
description: Trigger defines the scenarios where a new build
should be triggered.
Expand Down Expand Up @@ -9753,6 +9792,45 @@ spec:
description: Timeout defines the maximum run time of this BuildRun.
format: duration
type: string
tolerations:
description: If specified, the pod's tolerations.
items:
description: |-
The pod this Toleration is attached to tolerates any taint that matches
the triple <key,value,effect> using the matching operator <operator>.
properties:
effect:
description: |-
Effect indicates the taint effect to match. Empty means match all taint effects.
When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
type: string
key:
description: |-
Key is the taint key that the toleration applies to. Empty means match all taint keys.
If the key is empty, operator must be Exists; this combination means to match all values and all keys.
type: string
operator:
description: |-
Operator represents a key's relationship to the value.
Valid operators are Exists and Equal. Defaults to Equal.
Exists is equivalent to wildcard for value, so that a pod can
tolerate all taints of a particular category.
type: string
tolerationSeconds:
description: |-
TolerationSeconds represents the period of time the toleration (which must be
of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
it is not set, which means tolerate the taint forever (do not evict). Zero and
negative values will be treated as 0 (evict immediately) by the system.
format: int64
type: integer
value:
description: |-
Value is the taint value the toleration matches to.
If the operator is Exists, the value should be empty, otherwise just a regular string.
type: string
type: object
type: array
volumes:
description: |-
Volumes contains volume Overrides of the BuildStrategy volumes in case those are allowed
Expand Down Expand Up @@ -11959,6 +12037,45 @@ spec:
should take to execute.
format: duration
type: string
tolerations:
description: If specified, the pod's tolerations.
items:
description: |-
The pod this Toleration is attached to tolerates any taint that matches
the triple <key,value,effect> using the matching operator <operator>.
properties:
effect:
description: |-
Effect indicates the taint effect to match. Empty means match all taint effects.
When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
type: string
key:
description: |-
Key is the taint key that the toleration applies to. Empty means match all taint keys.
If the key is empty, operator must be Exists; this combination means to match all values and all keys.
type: string
operator:
description: |-
Operator represents a key's relationship to the value.
Valid operators are Exists and Equal. Defaults to Equal.
Exists is equivalent to wildcard for value, so that a pod can
tolerate all taints of a particular category.
type: string
tolerationSeconds:
description: |-
TolerationSeconds represents the period of time the toleration (which must be
of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
it is not set, which means tolerate the taint forever (do not evict). Zero and
negative values will be treated as 0 (evict immediately) by the system.
format: int64
type: integer
value:
description: |-
Value is the taint value the toleration matches to.
If the operator is Exists, the value should be empty, otherwise just a regular string.
type: string
type: object
type: array
trigger:
description: Trigger defines the scenarios where a new build should
be triggered.
Expand Down
39 changes: 39 additions & 0 deletions deploy/crds/shipwright.io_builds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2912,6 +2912,45 @@ spec:
should take to execute.
format: duration
type: string
tolerations:
description: If specified, the pod's tolerations.
items:
description: |-
The pod this Toleration is attached to tolerates any taint that matches
the triple <key,value,effect> using the matching operator <operator>.
properties:
effect:
description: |-
Effect indicates the taint effect to match. Empty means match all taint effects.
When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
type: string
key:
description: |-
Key is the taint key that the toleration applies to. Empty means match all taint keys.
If the key is empty, operator must be Exists; this combination means to match all values and all keys.
type: string
operator:
description: |-
Operator represents a key's relationship to the value.
Valid operators are Exists and Equal. Defaults to Equal.
Exists is equivalent to wildcard for value, so that a pod can
tolerate all taints of a particular category.
type: string
tolerationSeconds:
description: |-
TolerationSeconds represents the period of time the toleration (which must be
of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
it is not set, which means tolerate the taint forever (do not evict). Zero and
negative values will be treated as 0 (evict immediately) by the system.
format: int64
type: integer
value:
description: |-
Value is the taint value the toleration matches to.
If the operator is Exists, the value should be empty, otherwise just a regular string.
type: string
type: object
type: array
trigger:
description: Trigger defines the scenarios where a new build should
be triggered.
Expand Down
8 changes: 8 additions & 0 deletions pkg/apis/build/v1beta1/build_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ const (
OutputTimestampNotValid BuildReason = "OutputTimestampNotValid"
// NodeSelectorNotValid indicates that the nodeSelector value is not valid
NodeSelectorNotValid BuildReason = "NodeSelectorNotValid"
// TolerationNotValid indicates that the Toleration value is not valid
TolerationNotValid BuildReason = "TolerationNotValid"

// AllValidationsSucceeded indicates a Build was successfully validated
AllValidationsSucceeded = "all validations succeeded"
Expand Down Expand Up @@ -183,6 +185,12 @@ type BuildSpec struct {
//
// +optional
NodeSelector map[string]string `json:"nodeSelector,omitempty"`

// If specified, the pod's tolerations.
// +optional
// +patchMergeKey=Key
// +patchStrategy=merge
Tolerations []corev1.Toleration `json:"tolerations,omitempty" patchStrategy:"merge" patchMergeKey:"Key"`
}

// BuildVolume is a volume that will be mounted in build pod during build step
Expand Down
6 changes: 6 additions & 0 deletions pkg/apis/build/v1beta1/buildrun_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ type BuildRunSpec struct {
//
// +optional
NodeSelector map[string]string `json:"nodeSelector,omitempty"`

// If specified, the pod's tolerations.
// +optional
// +patchMergeKey=Key
// +patchStrategy=merge
Tolerations []corev1.Toleration `json:"tolerations,omitempty" patchStrategy:"merge" patchMergeKey:"Key"`
}

// BuildRunRequestedState defines the buildrun state the user can provide to override whatever is the current state.
Expand Down
1 change: 1 addition & 0 deletions pkg/reconciler/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ var validationTypes = [...]string{
validate.Envs,
validate.Triggers,
validate.NodeSelector,
validate.Tolerations,
}

// ReconcileBuild reconciles a Build object
Expand Down
1 change: 1 addition & 0 deletions pkg/reconciler/buildrun/buildrun.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ func (r *ReconcileBuildRun) Reconcile(ctx context.Context, request reconcile.Req
validate.NewBuildName(build),
validate.NewEnv(build),
validate.NewNodeSelector(build),
validate.NewTolerations(build),
)

// an internal/technical error during validation happened
Expand Down
31 changes: 28 additions & 3 deletions pkg/reconciler/buildrun/resources/taskrun.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package resources
import (
"fmt"
"path"
"slices"
"strconv"
"strings"

Expand Down Expand Up @@ -235,12 +236,21 @@ func GenerateTaskRun(
},
}

taskRunPodTemplate := &pod.PodTemplate{}
// Merge Build and BuildRun NodeSelectors, giving preference to BuildRun NodeSelector
taskRunNodeSelector := mergeMaps(build.Spec.NodeSelector, buildRun.Spec.NodeSelector)
if len(taskRunNodeSelector) > 0 {
expectedTaskRun.Spec.PodTemplate = &pod.PodTemplate{
NodeSelector: taskRunNodeSelector,
}
taskRunPodTemplate.NodeSelector = taskRunNodeSelector
}

// Merge Build and BuildRun Tolerations, giving preference to BuildRun Tolerations values
taskRunTolerations := mergeTolerations(build.Spec.Tolerations, buildRun.Spec.Tolerations)
if len(taskRunTolerations) > 0 {
taskRunPodTemplate.Tolerations = taskRunTolerations
}

if !(taskRunPodTemplate.Equals(&pod.PodTemplate{})) {
expectedTaskRun.Spec.PodTemplate = taskRunPodTemplate
}

// assign the annotations from the build strategy, filter out those that should not be propagated
Expand Down Expand Up @@ -354,6 +364,21 @@ func effectiveTimeout(build *buildv1beta1.Build, buildRun *buildv1beta1.BuildRun
return nil
}

// mergeTolerations merges the values for Spec.Tolerations in the given Build and BuildRun objects, with values in the BuildRun object overriding values
// in the Build object (if present).
func mergeTolerations(buildTolerations []corev1.Toleration, buildRunTolerations []corev1.Toleration) []corev1.Toleration {
mergedTolerations := []corev1.Toleration{}
mergedTolerations = append(mergedTolerations, buildRunTolerations...)
for _, toleration := range buildTolerations {
if !slices.ContainsFunc(mergedTolerations, func(t corev1.Toleration) bool {
return t.Key == toleration.Key
}) {
mergedTolerations = append(mergedTolerations, toleration)
}
}
return mergedTolerations
}

// isPropagatableAnnotation filters the last-applied-configuration annotation from kubectl because this would break the meaning of this annotation on the target object;
// also, annotations using our own custom resource domains are filtered out because we have no annotations with a semantic for both TaskRun and Pod
func isPropagatableAnnotation(key string) bool {
Expand Down
66 changes: 66 additions & 0 deletions pkg/validate/tolerations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright The Shipwright Contributors
//
// SPDX-License-Identifier: Apache-2.0

package validate

import (
"context"
"fmt"
"strings"

v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/utils/ptr"

build "github.com/shipwright-io/build/pkg/apis/build/v1beta1"
)

// TolerationsRef contains all required fields
// to validate tolerations
type TolerationsRef struct {
Build *build.Build // build instance for analysis
}

func NewTolerations(build *build.Build) *TolerationsRef {
return &TolerationsRef{build}
}

// ValidatePath implements BuildPath interface and validates
// that tolerations key/operator/value are valid
func (b *TolerationsRef) ValidatePath(_ context.Context) error {
for i, toleration := range b.Build.Spec.Tolerations {
// validate Key
if errs := validation.IsQualifiedName(toleration.Key); errs != nil {
b.Build.Status.Reason = ptr.To(build.TolerationNotValid)
b.Build.Status.Message = ptr.To(strings.Join(errs, ", "))
}
// validate Operator
if !((toleration.Operator == v1.TolerationOpExists) || (toleration.Operator == v1.TolerationOpEqual)) {
b.Build.Status.Reason = ptr.To(build.TolerationNotValid)
b.Build.Status.Message = ptr.To(fmt.Sprintf("Toleration operator not valid. Must be one of: '%v', '%v'", v1.TolerationOpExists, v1.TolerationOpEqual))
}
// validate Value
if errs := validation.IsValidLabelValue(toleration.Value); errs != nil {
b.Build.Status.Reason = ptr.To(build.TolerationNotValid)
b.Build.Status.Message = ptr.To(strings.Join(errs, ", "))
}
// validate Effect, of which only "NoSchedule" is supported
if toleration.Effect != v1.TaintEffectNoSchedule {
if !(len(toleration.Effect) > 0) {
// Effect was not specified, set it to the supported default
b.Build.Spec.Tolerations[i].Effect = v1.TaintEffectNoSchedule
} else {
b.Build.Status.Reason = ptr.To(build.TolerationNotValid)
b.Build.Status.Message = ptr.To(fmt.Sprintf("Only the '%v' toleration effect is supported.", v1.TaintEffectNoSchedule))
}
}
// validate TolerationSeconds, which should not be specified
if toleration.TolerationSeconds != nil {
b.Build.Status.Reason = ptr.To(build.TolerationNotValid)
b.Build.Status.Message = ptr.To("Specifying TolerationSeconds is not supported.")
}
}

return nil
}
Loading

0 comments on commit aba3865

Please sign in to comment.