-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add webhook validation for remote Tasks
A prior commit added validation for remote Pipelines by issuing dry-run create requests to the kubernetes API server, allowing validating admission webhooks to accept or reject remote pipelines without actually creating them. This commit adds the same logic for remote Tasks, and moves common logic into a shared package.
- Loading branch information
1 parent
7884311
commit 445734d
Showing
10 changed files
with
422 additions
and
80 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package apiserver | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
|
||
"github.com/google/uuid" | ||
v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" | ||
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" | ||
clientset "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" | ||
apierrors "k8s.io/apimachinery/pkg/api/errors" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
) | ||
|
||
var ( | ||
ErrReferencedObjectValidationFailed = errors.New("validation failed for referenced object") | ||
ErrCouldntValidateObjectRetryable = errors.New("retryable error validating referenced object") | ||
ErrCouldntValidateObjectPermanent = errors.New("permanent error validating referenced object") | ||
) | ||
|
||
// DryRunValidate validates the obj by issuing a dry-run create request for it in the given namespace. | ||
// This allows validating admission webhooks to process the object without actually creating it. | ||
// obj must be a v1/v1beta1 Task or Pipeline. | ||
func DryRunValidate(ctx context.Context, namespace string, obj runtime.Object, tekton clientset.Interface) error { | ||
dryRunObjName := uuid.NewString() // Use a randomized name for the Pipeline/Task in case there is already another Pipeline/Task of the same name | ||
|
||
switch obj := obj.(type) { | ||
case *v1.Pipeline: | ||
dryRunObj := obj.DeepCopy() | ||
dryRunObj.Name = dryRunObjName | ||
dryRunObj.Namespace = namespace // Make sure the namespace is the same as the PipelineRun | ||
if _, err := tekton.TektonV1().Pipelines(namespace).Create(ctx, dryRunObj, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}}); err != nil { | ||
return handleDryRunCreateErr(err, obj.Name) | ||
} | ||
case *v1beta1.Pipeline: | ||
dryRunObj := obj.DeepCopy() | ||
dryRunObj.Name = dryRunObjName | ||
dryRunObj.Namespace = namespace // Make sure the namespace is the same as the PipelineRun | ||
if _, err := tekton.TektonV1beta1().Pipelines(namespace).Create(ctx, dryRunObj, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}}); err != nil { | ||
return handleDryRunCreateErr(err, obj.Name) | ||
} | ||
|
||
case *v1.Task: | ||
dryRunObj := obj.DeepCopy() | ||
dryRunObj.Name = dryRunObjName | ||
dryRunObj.Namespace = namespace // Make sure the namespace is the same as the TaskRun | ||
if _, err := tekton.TektonV1().Tasks(namespace).Create(ctx, dryRunObj, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}}); err != nil { | ||
return handleDryRunCreateErr(err, obj.Name) | ||
} | ||
case *v1beta1.Task: | ||
dryRunObj := obj.DeepCopy() | ||
dryRunObj.Name = dryRunObjName | ||
dryRunObj.Namespace = namespace // Make sure the namespace is the same as the TaskRun | ||
if _, err := tekton.TektonV1beta1().Tasks(namespace).Create(ctx, dryRunObj, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}}); err != nil { | ||
return handleDryRunCreateErr(err, obj.Name) | ||
} | ||
default: | ||
return fmt.Errorf("unsupported object GVK %s", obj.GetObjectKind().GroupVersionKind()) | ||
} | ||
return nil | ||
} | ||
|
||
func handleDryRunCreateErr(err error, objectName string) error { | ||
var errType error | ||
switch { | ||
case apierrors.IsBadRequest(err): // Object rejected by validating webhook | ||
errType = ErrReferencedObjectValidationFailed | ||
case apierrors.IsInvalid(err), apierrors.IsMethodNotSupported(err): | ||
errType = ErrCouldntValidateObjectPermanent | ||
case apierrors.IsTimeout(err), apierrors.IsServerTimeout(err), apierrors.IsTooManyRequests(err): | ||
errType = ErrCouldntValidateObjectRetryable | ||
default: | ||
// Assume unknown errors are retryable | ||
// Additional errors can be added to the switch statements as needed | ||
errType = ErrCouldntValidateObjectRetryable | ||
} | ||
return fmt.Errorf("%w %s: %s", errType, objectName, err.Error()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
package apiserver_test | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
"github.com/google/go-cmp/cmp/cmpopts" | ||
v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" | ||
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" | ||
"github.com/tektoncd/pipeline/pkg/client/clientset/versioned/fake" | ||
"github.com/tektoncd/pipeline/pkg/reconciler/apiserver" | ||
apierrors "k8s.io/apimachinery/pkg/api/errors" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
"k8s.io/apimachinery/pkg/runtime/schema" | ||
"k8s.io/apimachinery/pkg/util/validation/field" | ||
ktesting "k8s.io/client-go/testing" | ||
) | ||
|
||
func TestDryRunCreate_Valid_DifferentGVKs(t *testing.T) { | ||
tcs := []struct { | ||
name string | ||
obj runtime.Object | ||
wantErr bool | ||
}{{ | ||
name: "v1 task", | ||
obj: &v1.Task{}, | ||
}, { | ||
name: "v1beta1 task", | ||
obj: &v1beta1.Task{}, | ||
}, { | ||
name: "v1 pipeline", | ||
obj: &v1.Pipeline{}, | ||
}, { | ||
name: "v1beta1 pipeline", | ||
obj: &v1beta1.Pipeline{}, | ||
}, { | ||
name: "unsupported gvk", | ||
obj: &v1beta1.ClusterTask{}, | ||
wantErr: true, | ||
}} | ||
for _, tc := range tcs { | ||
t.Run(tc.name, func(t *testing.T) { | ||
tektonclient := fake.NewSimpleClientset() | ||
err := apiserver.DryRunValidate(context.Background(), "default", tc.obj, tektonclient) | ||
if (err != nil) != tc.wantErr { | ||
t.Errorf("wantErr was %t but got err %v", tc.wantErr, err) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestDryRunCreate_Invalid_DifferentGVKs(t *testing.T) { | ||
tcs := []struct { | ||
name string | ||
obj runtime.Object | ||
wantErr error | ||
}{{ | ||
name: "v1 task", | ||
obj: &v1.Task{}, | ||
wantErr: apiserver.ErrReferencedObjectValidationFailed, | ||
}, { | ||
name: "v1beta1 task", | ||
obj: &v1beta1.Task{}, | ||
wantErr: apiserver.ErrReferencedObjectValidationFailed, | ||
}, { | ||
name: "v1 pipeline", | ||
obj: &v1.Pipeline{}, | ||
wantErr: apiserver.ErrReferencedObjectValidationFailed, | ||
}, { | ||
name: "v1beta1 pipeline", | ||
obj: &v1beta1.Pipeline{}, | ||
wantErr: apiserver.ErrReferencedObjectValidationFailed, | ||
}, { | ||
name: "unsupported gvk", | ||
obj: &v1beta1.ClusterTask{}, | ||
wantErr: cmpopts.AnyError, | ||
}} | ||
for _, tc := range tcs { | ||
t.Run(tc.name, func(t *testing.T) { | ||
tektonclient := fake.NewSimpleClientset() | ||
tektonclient.PrependReactor("create", "tasks", func(action ktesting.Action) (bool, runtime.Object, error) { | ||
return true, nil, apierrors.NewBadRequest("bad request") | ||
}) | ||
tektonclient.PrependReactor("create", "pipelines", func(action ktesting.Action) (bool, runtime.Object, error) { | ||
return true, nil, apierrors.NewBadRequest("bad request") | ||
}) | ||
err := apiserver.DryRunValidate(context.Background(), "default", tc.obj, tektonclient) | ||
if d := cmp.Diff(tc.wantErr, err, cmpopts.EquateErrors()); d != "" { | ||
t.Errorf("wrong error: %s", d) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestDryRunCreate_DifferentErrTypes(t *testing.T) { | ||
tcs := []struct { | ||
name string | ||
webhookErr error | ||
wantErr error | ||
}{{ | ||
name: "no error", | ||
wantErr: nil, | ||
}, { | ||
name: "bad request", | ||
webhookErr: apierrors.NewBadRequest("bad request"), | ||
wantErr: apiserver.ErrReferencedObjectValidationFailed, | ||
}, { | ||
name: "invalid", | ||
webhookErr: apierrors.NewInvalid(schema.GroupKind{Group: "tekton.dev/v1", Kind: "Task"}, "task", field.ErrorList{}), | ||
wantErr: apiserver.ErrCouldntValidateObjectPermanent, | ||
}, { | ||
name: "not supported", | ||
webhookErr: apierrors.NewMethodNotSupported(schema.GroupResource{}, "create"), | ||
wantErr: apiserver.ErrCouldntValidateObjectPermanent, | ||
}, { | ||
name: "timeout", | ||
webhookErr: apierrors.NewTimeoutError("timeout", 5), | ||
wantErr: apiserver.ErrCouldntValidateObjectRetryable, | ||
}, { | ||
name: "server timeout", | ||
webhookErr: apierrors.NewServerTimeout(schema.GroupResource{}, "create", 5), | ||
wantErr: apiserver.ErrCouldntValidateObjectRetryable, | ||
}, { | ||
name: "too many requests", | ||
webhookErr: apierrors.NewTooManyRequests("foo", 5), | ||
wantErr: apiserver.ErrCouldntValidateObjectRetryable, | ||
}} | ||
for _, tc := range tcs { | ||
t.Run(tc.name, func(t *testing.T) { | ||
tektonclient := fake.NewSimpleClientset() | ||
tektonclient.PrependReactor("create", "tasks", func(action ktesting.Action) (bool, runtime.Object, error) { | ||
return true, nil, tc.webhookErr | ||
}) | ||
err := apiserver.DryRunValidate(context.Background(), "default", &v1.Task{}, tektonclient) | ||
if d := cmp.Diff(tc.wantErr, err, cmpopts.EquateErrors()); d != "" { | ||
t.Errorf("wrong error: %s", d) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.