Skip to content

Commit

Permalink
Implement implicit parameter resolution.
Browse files Browse the repository at this point in the history
This is the implementation of
[TEP-0023](https://github.com/tektoncd/community/blob/main/teps/0023-implicit-mapping.md).

This adds information to the defaulting context to allow parameters to
be passed down the stack during evaluation. To start, this is only
implemented in the v1alpha1 API.

Additionally, Tasks are now allowed to accept more params than are
actually used. This should generally be safe, since extra params that
are provided should not affect the behavior of the Task itself since
they are not used. Because v1alpha1 relies on v1beta1 param types, this
change affects both v1alpha1 and v1beta1.
  • Loading branch information
wlynch committed Jul 29, 2021
1 parent 2e5780d commit 2edf64f
Show file tree
Hide file tree
Showing 13 changed files with 402 additions and 22 deletions.
28 changes: 28 additions & 0 deletions docs/pipelineruns.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,34 @@ case is when your CI system autogenerates `PipelineRuns` and it has `Parameters`
provide to all `PipelineRuns`. Because you can pass in extra `Parameters`, you don't have to
go through the complexity of checking each `Pipeline` and providing only the required params.

#### Implicit Parameters

**(v1alpha1 only)**

When using an inlined spec, parameters from the parent `PipelineRun` will be
available to any inlined specs without needing to be explicitly defined.

```yaml
apiVersion: tekton.dev/v1alpha1
kind: PipelineRun
metadata:
generateName: echo-
spec:
params:
- name: MESSAGE
value: "Good Morning!"
pipelineSpec:
tasks:
- name: echo-message
taskSpec:
steps:
- name: echo
image: ubuntu
script: |
#!/usr/bin/env bash
echo "$(params.MESSAGE)"
```

### Specifying custom `ServiceAccount` credentials

You can execute the `Pipeline` in your `PipelineRun` with a specific set of credentials by
Expand Down
26 changes: 26 additions & 0 deletions docs/taskruns.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,32 @@ spec:

**Note:** If a parameter does not have an implicit default value, you must explicitly set its value.

#### Implicit Parameters

**(v1alpha1 only)**

When using an inlined `taskSpec`, parameters from the parent `TaskRun` will be
available to the `Task` without needing to be explicitly defined.

```yaml
apiVersion: tekton.dev/v1alpha1
kind: TaskRun
metadata:
generateName: hello-
spec:
params:
- name: message
value: "hello world!"
taskSpec:
# There are no explicit params defined here.
# They are derived from the TaskRun params above.
steps:
- name: default
image: ubuntu
script: |
echo $(params.message)
```

### Specifying `Resources`

If a `Task` requires [`Resources`](tasks.md#specifying-resources) (that is, `inputs` and `outputs`) you must
Expand Down
18 changes: 18 additions & 0 deletions examples/v1alpha1/pipelineruns/pipelinerun-implicit-params.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
apiVersion: tekton.dev/v1alpha1
kind: PipelineRun
metadata:
generateName: echo-
spec:
pipelineSpec:
tasks:
- name: echo-message
taskSpec:
steps:
- name: echo
image: ubuntu
script: |
#!/usr/bin/env bash
echo "$(params.MESSAGE)"
params:
- name: MESSAGE
value: "Good Morning!"
15 changes: 15 additions & 0 deletions examples/v1alpha1/taskruns/implicit-params.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: tekton.dev/v1alpha1
kind: TaskRun
metadata:
generateName: hello-
spec:
params:
- name: message
value: "hello world!"
taskSpec:
# There are no explicit params defined here. They are derived from the TaskRun.
steps:
- name: default
image: ubuntu
script: |
echo $(params.message)
148 changes: 148 additions & 0 deletions pkg/apis/pipeline/v1alpha1/param_context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package v1alpha1

import (
"context"
"fmt"
)

// paramCtxKey is the unique identifier for referencing param information from
// a context.Context. See [context.Context.Value](https://pkg.go.dev/context#Context)
// for more details.
var paramCtxKey struct{}

// paramCtxVal is the data type stored in the param context.
// This maps param names -> ParamSpec.
type paramCtxVal map[string]ParamSpec

// AddContextParams adds the given Params to the param context. This only
// preserves the fields included in ParamSpec - Name and Type.
func AddContextParams(ctx context.Context, in []Param) context.Context {
if in == nil {
return ctx
}

out := paramCtxVal{}
// Copy map to ensure that contexts are unique.
v := ctx.Value(paramCtxKey)
if v != nil {
for n, cps := range v.(paramCtxVal) {
out[n] = cps
}
}
for _, p := range in {
// The user may have omitted type data. Fill this in in to normalize data.
if v := p.Value; v.Type == "" {
if len(v.ArrayVal) > 0 {
p.Value.Type = ParamTypeArray
}
if v.StringVal != "" {
p.Value.Type = ParamTypeString
}
}
out[p.Name] = ParamSpec{
Name: p.Name,
Type: p.Value.Type,
}
}
return context.WithValue(ctx, paramCtxKey, out)
}

// AddContextParamSpec adds the given ParamSpecs to the param context.
func AddContextParamSpec(ctx context.Context, in []ParamSpec) context.Context {
if in == nil {
return ctx
}

out := paramCtxVal{}
// Copy map to ensure that contexts are unique.
v := ctx.Value(paramCtxKey)
if v != nil {
for n, ps := range v.(paramCtxVal) {
out[n] = ps
}
}
for _, p := range in {
cps := ParamSpec{
Name: p.Name,
Type: p.Type,
Description: p.Description,
Default: p.Default,
}
out[p.Name] = cps
}
return context.WithValue(ctx, paramCtxKey, out)
}

// GetContextParams returns the current context parameters overlayed with a
// given set of params. Overrides should generally be the current layer you
// are trying to evaluate. Any context params not in the overrides will default
// to a generic pass-through param of the given type (i.e. $(params.name) or
// $(params.name[*])).
func GetContextParams(ctx context.Context, overlays ...Param) []Param {
pv := paramCtxVal{}
v := ctx.Value(paramCtxKey)
if v == nil && len(overlays) == 0 {
return nil
}
if v != nil {
pv = v.(paramCtxVal)
}
out := make([]Param, 0, len(pv))

// Overlays take precedence over any context params. Keep track of
// these and automatically add them to the output.
overrideSet := make(map[string]Param, len(overlays))
for _, p := range overlays {
overrideSet[p.Name] = p
out = append(out, p)
}

// Include the rest of the context params.
for _, ps := range pv {
// Don't do anything for any overlay params - these are already
// included.
if _, ok := overrideSet[ps.Name]; ok {
continue
}

// If there is no overlay, pass through the param to the next level.
// e.g. for strings $(params.name), for arrays $(params.name[*]).
p := Param{
Name: ps.Name,
}
if ps.Type == ParamTypeString {
p.Value = ArrayOrString{
Type: ParamTypeString,
StringVal: fmt.Sprintf("$(params.%s)", ps.Name),
}
} else {
p.Value = ArrayOrString{
Type: ParamTypeArray,
ArrayVal: []string{fmt.Sprintf("$(params.%s[*])", ps.Name)},
}
}
out = append(out, p)
}

return out
}

// GetContextParamSpecs returns the current context ParamSpecs.
func GetContextParamSpecs(ctx context.Context) []ParamSpec {
v := ctx.Value(paramCtxKey)
if v == nil {
return nil
}

pv := v.(paramCtxVal)
out := make([]ParamSpec, 0, len(pv))
for _, ps := range pv {
out = append(out, ParamSpec{
Name: ps.Name,
Type: ps.Type,
Description: ps.Description,
Default: ps.Default,
})
}
return out
}
12 changes: 8 additions & 4 deletions pkg/apis/pipeline/v1alpha1/pipeline_defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ func (p *Pipeline) SetDefaults(ctx context.Context) {
}

func (ps *PipelineSpec) SetDefaults(ctx context.Context) {
for _, pt := range ps.Tasks {
for i := range ps.Params {
ps.Params[i].SetDefaults(ctx)
}
ctx = AddContextParamSpec(ctx, ps.Params)
ps.Params = GetContextParamSpecs(ctx)
for i, pt := range ps.Tasks {
ctx := AddContextParams(ctx, pt.Params)
ps.Tasks[i].Params = GetContextParams(ctx, pt.Params...)
if pt.TaskRef != nil {
if pt.TaskRef.Kind == "" {
pt.TaskRef.Kind = NamespacedTaskKind
Expand All @@ -39,7 +46,4 @@ func (ps *PipelineSpec) SetDefaults(ctx context.Context) {
pt.TaskSpec.SetDefaults(ctx)
}
}
for i := range ps.Params {
ps.Params[i].SetDefaults(ctx)
}
}
2 changes: 1 addition & 1 deletion pkg/apis/pipeline/v1alpha1/pipelinerun_defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,6 @@ func (prs *PipelineRunSpec) SetDefaults(ctx context.Context) {
}

if prs.PipelineSpec != nil {
prs.PipelineSpec.SetDefaults(ctx)
prs.PipelineSpec.SetDefaults(AddContextParams(ctx, prs.Params))
}
}
87 changes: 87 additions & 0 deletions pkg/apis/pipeline/v1alpha1/pipelinerun_defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/tektoncd/pipeline/pkg/apis/config"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"github.com/tektoncd/pipeline/pkg/contexts"
"github.com/tektoncd/pipeline/test/diff"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -87,6 +88,92 @@ func TestPipelineRunSpec_SetDefaults(t *testing.T) {
},
},
},
{
desc: "implicit params",
prs: &v1alpha1.PipelineRunSpec{
Params: []v1alpha1.Param{
{
Name: "foo",
Value: v1alpha1.ArrayOrString{
StringVal: "a",
},
},
{
Name: "bar",
Value: v1alpha1.ArrayOrString{
ArrayVal: []string{"b"},
},
},
},
PipelineSpec: &v1alpha1.PipelineSpec{
Tasks: []v1alpha1.PipelineTask{{
TaskSpec: &v1alpha1.TaskSpec{},
}},
},
},
want: &v1alpha1.PipelineRunSpec{
ServiceAccountName: config.DefaultServiceAccountValue,
Timeout: &metav1.Duration{Duration: config.DefaultTimeoutMinutes * time.Minute},
Params: []v1alpha1.Param{
{
Name: "foo",
Value: v1alpha1.ArrayOrString{
StringVal: "a",
},
},
{
Name: "bar",
Value: v1alpha1.ArrayOrString{
ArrayVal: []string{"b"},
},
},
},
PipelineSpec: &v1alpha1.PipelineSpec{
Tasks: []v1alpha1.PipelineTask{{
TaskSpec: &v1alpha1.TaskSpec{
TaskSpec: v1beta1.TaskSpec{
Params: []v1beta1.ParamSpec{
{
Name: "foo",
Type: v1beta1.ParamTypeString,
},
{
Name: "bar",
Type: v1beta1.ParamTypeArray,
},
},
},
},
Params: []v1alpha1.Param{
{
Name: "foo",
Value: v1alpha1.ArrayOrString{
Type: v1alpha1.ParamTypeString,
StringVal: "$(params.foo)",
},
},
{
Name: "bar",
Value: v1alpha1.ArrayOrString{
Type: v1alpha1.ParamTypeArray,
ArrayVal: []string{"$(params.bar[*])"},
},
},
},
}},
Params: []v1alpha1.ParamSpec{
{
Name: "foo",
Type: v1alpha1.ParamTypeString,
},
{
Name: "bar",
Type: v1alpha1.ParamTypeArray,
},
},
},
},
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
Expand Down
4 changes: 4 additions & 0 deletions pkg/apis/pipeline/v1alpha1/task_defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,13 @@ func (ts *TaskSpec) SetDefaults(ctx context.Context) {
}
}
}

for i := range ts.Params {
ts.Params[i].SetDefaults(ctx)
}
ctx = AddContextParamSpec(ctx, ts.Params)
ts.Params = GetContextParamSpecs(ctx)

if ts.Inputs != nil {
ts.Inputs.SetDefaults(ctx)
}
Expand Down
Loading

0 comments on commit 2edf64f

Please sign in to comment.