Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TEP-0061, Allow custom task to be embedded. #3901

Merged
merged 4 commits into from
May 25, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/pipelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,31 @@ If the `taskRef` specifies a name, the custom task controller should look up the
If the `taskRef` does not specify a name, the custom task controller might support
some default behavior for executing unnamed tasks.

### Specifying a Custom Task Spec in-line (or embedded)

```yaml
spec:
tasks:
- name: run-custom-task
taskSpec:
apiVersion: example.dev/v1alpha1
kind: Example
spec:
field1: value1
field2: value2
```

If the custom task controller supports the in-line or embedded task spec, this will create a `Run` of a custom task of
type `Example` in the `example.dev` API group with the version `v1alpha1`.

If the `taskSpec` is not supported, the custom task controller should produce proper validation errors.

Please take a look at the
[developer guide for custom controllers supporting `taskSpec`.](runs.md#developer-guide-for-custom-controllers-supporting-spec)

ScrapCodes marked this conversation as resolved.
Show resolved Hide resolved
`taskSpec` support for `pipelineRun` was designed and discussed in
[TEP-0061](https://github.com/tektoncd/community/blob/main/teps/0061-allow-custom-task-to-be-embedded-in-pipeline.md)

### Specifying parameters

If a custom task supports [`parameters`](tasks.md#parameters), you can use the
Expand Down
56 changes: 55 additions & 1 deletion docs/runs.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ A `Run` definition supports the following fields:
- [`metadata`][kubernetes-overview] - Specifies the metadata that uniquely identifies the
`Run`, such as a `name`.
- [`spec`][kubernetes-overview] - Specifies the configuration for the `Run`.
- [`ref`](#specifying-the-target-custom-task) - Specifies the type and
- [`ref`](#1-specifying-the-target-custom-task-with-ref) - Specifies the type and
(optionally) name of the custom task type to execute.
- [`spec`](#2-specifying-the-target-custom-task-by-embedding-its-spec) - Embed the custom task resource spec
directly in a `Run`.
- Optional:
- [`params`](#specifying-parameters) - Specifies the desired execution
parameters for the custom task.
Expand All @@ -64,6 +66,19 @@ A `Run` definition supports the following fields:

### Specifying the target Custom Task

A custom task resource's `Spec` may be directly embedded in the `Run` or it may
be referred to by a `Ref`. But, not both at the same time.
This conversation was marked as resolved.
Show resolved Hide resolved

1. [Specifying the target Custom Task with ref](#1-specifying-the-target-custom-task-with-ref)
Referring a custom task (i.e. `Ref` ) promotes reuse of custom task definitions.

2. [Specifying the target Custom Task by embedding its spec](#2-specifying-the-target-custom-task-by-embedding-its-spec)
Embedding a custom task (i.e. `Spec` ) helps in avoiding name collisions with other users within the same namespace.
Additionally, in a pipeline with multiple embedded custom tasks, the details of entire pipeline can be fetched in a
single API request.

### 1. Specifying the target Custom Task with ref

To specify the custom task type you want to execute in your `Run`, use the
`ref` field as shown below:

Expand Down Expand Up @@ -99,6 +114,45 @@ In either case, if the named resource cannot be found, or if unnamed tasks are
not supported, the custom task controller should update the `Run`'s status to
indicate the error.

### 2. Specifying the target Custom Task by embedding its spec

To specify the custom task spec, it can be embedded directly into a
`Run`'s spec as shown below:

```yaml
apiVersion: tekton.dev/v1alpha1
kind: Run
metadata:
name: embedded-run
spec:
spec:
apiVersion: example.dev/v1alpha1
kind: Example
spec:
field1: value1
field2: value2
```

This initiates the execution of a `Run` of a custom task of type `Example`, in
the `example.dev` API group, with the version `v1alpha1`.

#### Developer guide for custom controllers supporting `spec`.

1. A custom controller may or may not support a `Spec`. In cases where it is
not supported the custom controller should respond with proper validation error.

2. Validation of the fields of the custom task is delegated to the custom task controller. It is recommended to
implement validations as asynchronous
(i.e. at reconcile time), rather than part of the webhook. Using a webhook for validation is problematic because, it
is not possible to filter custom task resource objects before validation step, as a result each custom task resource
has to undergo validation by all the installed custom task controllers.

3. A custom task may have an empty spec, but cannot have an empty
`ApiVersion` and `Kind`. Custom task controllers should handle
an empty spec, either with a default behaviour, in a case no default
behaviour is supported then, appropriate validation error should be
updated to the `Run`'s status.

### Specifying `Parameters`

If a custom task supports [`parameters`](tasks.md#parameters), you can use the
Expand Down
4 changes: 2 additions & 2 deletions internal/builder/v1beta1/pipeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,10 @@ func TestPipeline(t *testing.T) {
Labels: map[string]string{"label": "labelvalue"},
Annotations: map[string]string{"annotation": "annotationvalue"},
},

TaskSpec: getTaskSpec(),
},
},
},
}},
Workspaces: []v1beta1.PipelineWorkspaceDeclaration{{
Name: "workspace1",
}},
Expand Down
19 changes: 18 additions & 1 deletion pkg/apis/pipeline/v1alpha1/run_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ import (
"fmt"

"github.com/tektoncd/pipeline/pkg/apis/pipeline"
v1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
runv1alpha1 "github.com/tektoncd/pipeline/pkg/apis/run/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"knative.dev/pkg/apis"
duckv1 "knative.dev/pkg/apis/duck/v1"
Expand All @@ -36,11 +37,27 @@ var (
}
)

// EmbeddedRunSpec allows custom task definitions to be embedded
type EmbeddedRunSpec struct {
runtime.TypeMeta `json:",inline"`

// +optional
Metadata v1beta1.PipelineTaskMetadata `json:"metadata,omitempty"`

// Spec is a specification of a custom task
// +optional
Spec runtime.RawExtension `json:"spec,omitempty"`
}

// RunSpec defines the desired state of Run
type RunSpec struct {
// +optional
Ref *TaskRef `json:"ref,omitempty"`

// Spec is a specification of a custom task
// +optional
Spec *EmbeddedRunSpec `json:"spec,omitempty"`

// +optional
Params []v1beta1.Param `json:"params,omitempty"`

Expand Down
24 changes: 17 additions & 7 deletions pkg/apis/pipeline/v1alpha1/run_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,30 @@ func (r *Run) Validate(ctx context.Context) *apis.FieldError {

// Validate Run spec
func (rs *RunSpec) Validate(ctx context.Context) *apis.FieldError {
// this covers the case rs.Ref == nil && rs.Spec == nil
if equality.Semantic.DeepEqual(rs, &RunSpec{}) {
return apis.ErrMissingField("spec")
}

if rs.Ref == nil {
return apis.ErrMissingField("spec.ref")
if rs.Ref != nil && rs.Spec != nil {
Copy link
Member

Choose a reason for hiding this comment

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

need an additional check here, if both are nil

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @pritidesai Thanks for observing !

The check above this covers the case if both are nil. Infact, previously when I was testing with the additional explicit check of both nil. The code was just unreachable. So equality.Semantic.DeepEqual(rs, &RunSpec{}) take care of both nil case as well. For the same reason, it is not possible to write a separate test.
Matter of factly, There is a comment at line 39, just above the check explaining the same.

Copy link
Member

Choose a reason for hiding this comment

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

thanks @ScrapCodes

right but the DeepEqual used this way checks if the rs is empty, for example:

if equality.Semantic.DeepEqual(ps, &PipelineSpec{}) {
errs = errs.Also(apis.ErrGeneric("expected at least one, got none", "description", "params", "resources", "tasks", "workspaces"))
}

This check will not catch rs.Spec and rs.Ref both being nil if rs had any other field initialized. Since the run specifications are generated by the controller, this is not blocking this PR but since run specifications are being consumed by the custom controller, it will be great to have such validation here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, now I have understood ! Will open a PR with the fix soon.

return apis.ErrMultipleOneOf("spec.ref", "spec.spec")
}
if rs.Ref.APIVersion == "" {
return apis.ErrMissingField("spec.ref.apiVersion")
if rs.Ref != nil {
if rs.Ref.APIVersion == "" {
return apis.ErrMissingField("spec.ref.apiVersion")
}
if rs.Ref.Kind == "" {
return apis.ErrMissingField("spec.ref.kind")
}
}
if rs.Ref.Kind == "" {
return apis.ErrMissingField("spec.ref.kind")
if rs.Spec != nil {
if rs.Spec.APIVersion == "" {
return apis.ErrMissingField("spec.spec.apiVersion")
}
if rs.Spec.Kind == "" {
return apis.ErrMissingField("spec.spec.kind")
}
}

if err := validateParameters("spec.params", rs.Params); err != nil {
return err
}
Expand Down
76 changes: 72 additions & 4 deletions pkg/apis/pipeline/v1alpha1/run_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/tektoncd/pipeline/test/diff"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"knative.dev/pkg/apis"
)

Expand All @@ -43,18 +44,39 @@ func TestRun_Invalid(t *testing.T) {
},
want: apis.ErrMissingField("spec"),
}, {
name: "missing ref",
name: "Empty spec",
Copy link
Member

Choose a reason for hiding this comment

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

=> empty ref and spec

Copy link
Member

Choose a reason for hiding this comment

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

the test name does not match with ref and spec both being nil, please add one more check in the validation routine and update this test.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reason is, both nil and Empty spec are same in terms of what happens underneath.
Actual check says,
if equality.Semantic.DeepEqual(rs, &RunSpec{}) { return apis.ErrMissingField("spec") i.e. empty spec and so is the name. Please see the reply to your comment above as well, and let me know, if this is still concerning.

run: &v1alpha1.Run{
ObjectMeta: metav1.ObjectMeta{
Name: "temp",
},
Spec: v1alpha1.RunSpec{
Ref: nil,
Ref: nil,
Spec: nil,
ScrapCodes marked this conversation as resolved.
Show resolved Hide resolved
},
},
want: apis.ErrMissingField("spec"),
}, {
name: "missing apiVersion",
name: "Both Ref and Spec",
run: &v1alpha1.Run{
ObjectMeta: metav1.ObjectMeta{
Name: "temp",
},
Spec: v1alpha1.RunSpec{
Ref: &v1alpha1.TaskRef{
APIVersion: "apiVersion",
Kind: "kind",
},
Spec: &v1alpha1.EmbeddedRunSpec{
TypeMeta: runtime.TypeMeta{
APIVersion: "apiVersion",
Kind: "kind",
},
},
},
},
want: apis.ErrMultipleOneOf("spec.ref", "spec.spec"),
}, {
name: "missing apiVersion in Ref",
run: &v1alpha1.Run{
ObjectMeta: metav1.ObjectMeta{
Name: "temp",
Expand All @@ -67,7 +89,7 @@ func TestRun_Invalid(t *testing.T) {
},
want: apis.ErrMissingField("spec.ref.apiVersion"),
}, {
name: "missing kind",
name: "missing kind in Ref",
run: &v1alpha1.Run{
ObjectMeta: metav1.ObjectMeta{
Name: "temp",
Expand All @@ -80,6 +102,37 @@ func TestRun_Invalid(t *testing.T) {
},
},
want: apis.ErrMissingField("spec.ref.kind"),
}, {
name: "missing apiVersion in Spec",
run: &v1alpha1.Run{
ObjectMeta: metav1.ObjectMeta{
Name: "temp",
},
Spec: v1alpha1.RunSpec{
Spec: &v1alpha1.EmbeddedRunSpec{
TypeMeta: runtime.TypeMeta{
APIVersion: "",
},
},
},
},
want: apis.ErrMissingField("spec.spec.apiVersion"),
}, {
name: "missing kind in Spec",
run: &v1alpha1.Run{
ObjectMeta: metav1.ObjectMeta{
Name: "temp",
},
Spec: v1alpha1.RunSpec{
Spec: &v1alpha1.EmbeddedRunSpec{
TypeMeta: runtime.TypeMeta{
APIVersion: "apiVersion",
Kind: "",
},
},
},
},
want: apis.ErrMissingField("spec.spec.kind"),
}, {
name: "non-unique params",
run: &v1alpha1.Run{
Expand Down Expand Up @@ -142,6 +195,21 @@ func TestRun_Valid(t *testing.T) {
},
},
},
}, {
name: "Spec with valid ApiVersion and Kind",
run: &v1alpha1.Run{
ObjectMeta: metav1.ObjectMeta{
Name: "temp",
},
Spec: v1alpha1.RunSpec{
Spec: &v1alpha1.EmbeddedRunSpec{
TypeMeta: runtime.TypeMeta{
APIVersion: "apiVersion",
Kind: "kind",
},
},
},
},
}, {
name: "unique params",
run: &v1alpha1.Run{
Expand Down
24 changes: 24 additions & 0 deletions pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 20 additions & 1 deletion pkg/apis/pipeline/v1beta1/openapi_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.