Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions pkg/controller/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
Copyright 2025 The Tekton Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Package controller provides helper methods for external controllers for
// Custom Task types.
package controller

import (
"errors"
"net/http"
"strings"

apierrors "k8s.io/apimachinery/pkg/api/errors"
)

// IsWebhookTimeout checks if the error is due to a mutating admission webhook timeout.
// This function is used to determine if an error should trigger exponential backoff retry logic.
func IsWebhookTimeout(err error) bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var statusErr *apierrors.StatusError
if errors.As(err, &statusErr) {
return statusErr.ErrStatus.Code == http.StatusInternalServerError &&
strings.Contains(statusErr.ErrStatus.Message, "timeout")
}
return false
}
164 changes: 164 additions & 0 deletions pkg/controller/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
Copyright 2025 The Tekton Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controller

import (
"errors"
"net/http"
"testing"

apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestIsWebhookTimeout(t *testing.T) {
tests := []struct {
name string
err error
expected bool
}{
{
name: "nil error",
err: nil,
expected: false,
},
{
name: "non-status error",
err: errors.New("some other error"),
expected: false,
},
{
name: "webhook timeout error - exact match",
err: &apierrors.StatusError{
ErrStatus: metav1.Status{
Code: http.StatusInternalServerError,
Message: "timeout",
},
},
expected: true,
},
{
name: "webhook timeout error - contains timeout in message",
err: &apierrors.StatusError{
ErrStatus: metav1.Status{
Code: http.StatusInternalServerError,
Message: "admission webhook timeout occurred",
},
},
expected: true,
},
{
name: "webhook timeout error - case insensitive",
err: &apierrors.StatusError{
ErrStatus: metav1.Status{
Code: http.StatusInternalServerError,
Message: "TIMEOUT",
},
},
expected: false, // Case-sensitive matching, so "TIMEOUT" should not match "timeout"
},
{
name: "non-webhook error - different status code",
err: &apierrors.StatusError{
ErrStatus: metav1.Status{
Code: http.StatusBadRequest,
Message: "timeout",
},
},
expected: false,
},
{
name: "non-webhook error - different message",
err: &apierrors.StatusError{
ErrStatus: metav1.Status{
Code: http.StatusInternalServerError,
Message: "server error",
},
},
expected: false,
},
{
name: "non-webhook error - forbidden",
err: &apierrors.StatusError{
ErrStatus: metav1.Status{
Code: http.StatusForbidden,
Message: "forbidden",
},
},
expected: false,
},
{
name: "non-webhook error - conflict",
err: &apierrors.StatusError{
ErrStatus: metav1.Status{
Code: http.StatusConflict,
Message: "conflict",
},
},
expected: false,
},
{
name: "non-webhook error - not found",
err: &apierrors.StatusError{
ErrStatus: metav1.Status{
Code: http.StatusNotFound,
Message: "not found",
},
},
expected: false,
},
{
name: "webhook timeout error - with additional context",
err: &apierrors.StatusError{
ErrStatus: metav1.Status{
Code: http.StatusInternalServerError,
Message: "failed calling webhook: timeout",
},
},
expected: true,
},
{
name: "webhook timeout error - with error details",
err: &apierrors.StatusError{
ErrStatus: metav1.Status{
Code: http.StatusInternalServerError,
Message: "admission webhook \"example.com\" failed: timeout",
},
},
expected: true,
},
{
name: "non-webhook error - timeout in different context",
err: &apierrors.StatusError{
ErrStatus: metav1.Status{
Code: http.StatusInternalServerError,
Message: "operation completed successfully, no timeout",
},
},
expected: true, // Contains "timeout" substring, so it should match
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsWebhookTimeout(tt.err)
if result != tt.expected {
t.Errorf("IsWebhookTimeout() = %v, want %v", result, tt.expected)
}
})
}
}
16 changes: 3 additions & 13 deletions pkg/reconciler/pipelinerun/pipelinerun.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"path/filepath"
"reflect"
"regexp"
Expand All @@ -40,6 +39,7 @@ import (
listers "github.com/tektoncd/pipeline/pkg/client/listers/pipeline/v1"
alpha1listers "github.com/tektoncd/pipeline/pkg/client/listers/pipeline/v1alpha1"
beta1listers "github.com/tektoncd/pipeline/pkg/client/listers/pipeline/v1beta1"
ctrl "github.com/tektoncd/pipeline/pkg/controller"
"github.com/tektoncd/pipeline/pkg/internal/affinityassistant"
resolutionutil "github.com/tektoncd/pipeline/pkg/internal/resolution"
"github.com/tektoncd/pipeline/pkg/pipelinerunmetrics"
Expand Down Expand Up @@ -1108,7 +1108,7 @@ func (c *Reconciler) createTaskRun(ctx context.Context, taskRunName string, para
result = nil
result, err = c.PipelineClientSet.TektonV1().TaskRuns(pr.Namespace).Create(ctx, tr, metav1.CreateOptions{})
if err != nil {
if isWebhookTimeout(err) {
if ctrl.IsWebhookTimeout(err) {
return false, nil // retry
}
return false, err // do not retry
Expand All @@ -1121,16 +1121,6 @@ func (c *Reconciler) createTaskRun(ctx context.Context, taskRunName string, para
return result, nil
}

// isWebhookTimeout checks if the error is due to a mutating admission webhook timeout
func isWebhookTimeout(err error) bool {
var statusErr *apierrors.StatusError
if errors.As(err, &statusErr) {
return statusErr.ErrStatus.Code == http.StatusInternalServerError &&
strings.Contains(statusErr.ErrStatus.Message, "timeout")
}
return false
}

// handleRunCreationError marks the PipelineRun as failed and returns a permanent error if the run creation error is not retryable
func (c *Reconciler) handleRunCreationError(ctx context.Context, pr *v1.PipelineRun, err error) error {
if controller.IsPermanentError(err) {
Expand Down Expand Up @@ -1273,7 +1263,7 @@ func (c *Reconciler) createCustomRun(ctx context.Context, runName string, params
result = nil
result, err = c.PipelineClientSet.TektonV1beta1().CustomRuns(pr.Namespace).Create(ctx, r, metav1.CreateOptions{})
if err != nil {
if isWebhookTimeout(err) {
if ctrl.IsWebhookTimeout(err) {
return false, nil // retry
}
return false, err // do not retry
Expand Down
33 changes: 31 additions & 2 deletions pkg/reconciler/taskrun/taskrun.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
taskrunreconciler "github.com/tektoncd/pipeline/pkg/client/injection/reconciler/pipeline/v1/taskrun"
listers "github.com/tektoncd/pipeline/pkg/client/listers/pipeline/v1"
alphalisters "github.com/tektoncd/pipeline/pkg/client/listers/pipeline/v1alpha1"
ctrl "github.com/tektoncd/pipeline/pkg/controller"
"github.com/tektoncd/pipeline/pkg/internal/affinityassistant"
"github.com/tektoncd/pipeline/pkg/internal/computeresources"
"github.com/tektoncd/pipeline/pkg/internal/defaultresourcerequirements"
Expand All @@ -61,6 +62,7 @@ import (
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"
corev1Listers "k8s.io/client-go/listers/core/v1"
"k8s.io/utils/clock"
Expand Down Expand Up @@ -906,7 +908,31 @@ func (c *Reconciler) createPod(ctx context.Context, ts *v1.TaskSpec, tr *v1.Task
// Stash the podname in case there's create conflict so that we can try
// to fetch it.
podName := pod.Name
pod, err = c.KubeClientSet.CoreV1().Pods(tr.Namespace).Create(ctx, pod, metav1.CreateOptions{})

cfg := config.FromContextOrDefaults(ctx)
if !cfg.FeatureFlags.EnableWaitExponentialBackoff {
pod, err = c.KubeClientSet.CoreV1().Pods(tr.Namespace).Create(ctx, pod, metav1.CreateOptions{})
} else {
backoff := wait.Backoff{
Duration: cfg.WaitExponentialBackoff.Duration, // Initial delay before retry
Factor: cfg.WaitExponentialBackoff.Factor, // Multiplier for exponential growth
Steps: cfg.WaitExponentialBackoff.Steps, // Maximum number of retry attempts
Cap: cfg.WaitExponentialBackoff.Cap, // Maximum time spent before giving up
}
var result *corev1.Pod
err = wait.ExponentialBackoff(backoff, func() (bool, error) {
result = nil
result, err = c.KubeClientSet.CoreV1().Pods(tr.Namespace).Create(ctx, pod, metav1.CreateOptions{})
if err != nil {
if ctrl.IsWebhookTimeout(err) {
return false, nil // retry
}
return false, err // do not retry
}
pod = result
return true, nil
})
}

if err == nil && willOverwritePodSetAffinity(tr) {
if recorder := controller.GetEventRecorder(ctx); recorder != nil {
Expand All @@ -921,7 +947,10 @@ func (c *Reconciler) createPod(ctx context.Context, ts *v1.TaskSpec, tr *v1.Task
return p, nil
}
}
return pod, err
if err != nil {
return nil, err
}
return pod, nil
}

// applyParamsContextsResultsAndWorkspaces applies paramater, context, results and workspace substitutions to the TaskSpec.
Expand Down
Loading
Loading