Skip to content

Commit c2d2f48

Browse files
committed
[TEP-0142] Remote Resolution for StepAction
This commit is part of #7259. It adds the remote resolution for StepAction. Signed-off-by: Yongxuan Zhang [email protected]
1 parent a791d49 commit c2d2f48

File tree

11 files changed

+428
-18
lines changed

11 files changed

+428
-18
lines changed

docs/stepactions.md

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ A `StepAction` is the reusable and scriptable unit of work that is performed by
1717

1818
A `Step` is not reusable, the work it performs is reusable and referenceable. `Steps` are in-lined in the `Task` definition and either perform work directly or perform a `StepAction`. A `StepAction` cannot be run stand-alone (unlike a `TaskRun` or a `PipelineRun`). It has to be referenced by a `Step`. Another way to ehink about this is that a `Step` is not composed of `StepActions` (unlike a `Task` being composed of `Steps` and `Sidecars`). Instead, a `Step` is an actionable component, meaning that it has the ability to refer to a `StepAction`. The author of the `StepAction` must be able to compose a `Step` using a `StepAction` and provide all the necessary context (or orchestration) to it.
1919

20-
20+
2121
## Configuring a `StepAction`
2222

2323
A `StepAction` definition supports the following fields:
@@ -29,7 +29,7 @@ A `StepAction` definition supports the following fields:
2929
- [`metadata`][kubernetes-overview] - Specifies metadata that uniquely identifies the
3030
`StepAction` resource object. For example, a `name`.
3131
- [`spec`][kubernetes-overview] - Specifies the configuration information for this `StepAction` resource object.
32-
- `image` - Specifies the image to use for the `Step`.
32+
- `image` - Specifies the image to use for the `Step`.
3333
- The container image must abide by the [container contract](./container-contract.md).
3434
- Optional
3535
- `command`
@@ -50,7 +50,7 @@ spec:
5050
env:
5151
- name: HOME
5252
value: /home
53-
image: ubuntu
53+
image: ubuntu
5454
command: ["ls"]
5555
args: ["-lh"]
5656
```
@@ -169,3 +169,29 @@ spec:
169169
timeout: 1h
170170
onError: continue
171171
```
172+
173+
### Specifying Remote StepActions
174+
175+
A `ref` field may specify a StepAction in a remote location such as git.
176+
Support for specific types of remote will depend on the Resolvers your
177+
cluster's operator has installed. For more information including a tutorial, please check [resolution docs](resolution.md). The below example demonstrates referencing a StepAction in git:
178+
179+
```yaml
180+
apiVersion: tekton.dev/v1
181+
kind: TaskRun
182+
metadata:
183+
generateName: step-action-run-
184+
spec:
185+
TaskSpec:
186+
steps:
187+
- name: action-runner
188+
ref:
189+
resolver: git
190+
params:
191+
- name: url
192+
value: https://github.com/repo/repo.git
193+
- name: revision
194+
value: main
195+
- name: pathInRepo
196+
value: remote_step.yaml
197+
```
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# TODO(#7325): use StepAction from Catalog
2+
apiVersion: tekton.dev/v1
3+
kind: TaskRun
4+
metadata:
5+
generateName: step-action-run-
6+
spec:
7+
TaskSpec:
8+
steps:
9+
- name: action-runner
10+
ref:
11+
resolver: git
12+
params:
13+
- name: url
14+
value: https://github.com/chitrangpatel/repo1M.git
15+
- name: revision
16+
value: main
17+
- name: pathInRepo
18+
value: basic_step.yaml

pkg/apis/pipeline/v1/taskrun_types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@ const (
179179
// TaskRunReasonResolvingTaskRef indicates that the TaskRun is waiting for
180180
// its taskRef to be asynchronously resolved.
181181
TaskRunReasonResolvingTaskRef = "ResolvingTaskRef"
182+
// TaskRunReasonResolvingStepActionRef indicates that the TaskRun is waiting for
183+
// its StepAction's Ref to be asynchronously resolved.
184+
TaskRunReasonResolvingStepActionRef = "ResolvingRef"
182185
// TaskRunReasonImagePullFailed is the reason set when the step of a task fails due to image not being pulled
183186
TaskRunReasonImagePullFailed TaskRunReason = "TaskRunImagePullFailed"
184187
// TaskRunReasonResultLargerThanAllowedLimit is the reason set when one of the results exceeds its maximum allowed limit of 1 KB

pkg/reconciler/apiserver/apiserver.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/google/uuid"
99
v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
10+
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1"
1011
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
1112
clientset "github.com/tektoncd/pipeline/pkg/client/clientset/versioned"
1213
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -56,6 +57,13 @@ func DryRunValidate(ctx context.Context, namespace string, obj runtime.Object, t
5657
if _, err := tekton.TektonV1beta1().Tasks(namespace).Create(ctx, dryRunObj, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}}); err != nil {
5758
return handleDryRunCreateErr(err, obj.Name)
5859
}
60+
case *v1alpha1.StepAction:
61+
dryRunObj := obj.DeepCopy()
62+
dryRunObj.Name = dryRunObjName
63+
dryRunObj.Namespace = namespace // Make sure the namespace is the same as the StepAction
64+
if _, err := tekton.TektonV1alpha1().StepActions(namespace).Create(ctx, dryRunObj, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}}); err != nil {
65+
return handleDryRunCreateErr(err, obj.Name)
66+
}
5967
default:
6068
return fmt.Errorf("unsupported object GVK %s", obj.GetObjectKind().GroupVersionKind())
6169
}

pkg/reconciler/apiserver/apiserver_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/google/go-cmp/cmp"
88
"github.com/google/go-cmp/cmp/cmpopts"
99
v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
10+
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1"
1011
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
1112
"github.com/tektoncd/pipeline/pkg/client/clientset/versioned/fake"
1213
"github.com/tektoncd/pipeline/pkg/reconciler/apiserver"
@@ -34,6 +35,9 @@ func TestDryRunCreate_Valid_DifferentGVKs(t *testing.T) {
3435
}, {
3536
name: "v1beta1 pipeline",
3637
obj: &v1beta1.Pipeline{},
38+
}, {
39+
name: "v1alpha1 stepaction",
40+
obj: &v1alpha1.StepAction{},
3741
}, {
3842
name: "unsupported gvk",
3943
obj: &v1beta1.ClusterTask{},
@@ -71,6 +75,10 @@ func TestDryRunCreate_Invalid_DifferentGVKs(t *testing.T) {
7175
name: "v1beta1 pipeline",
7276
obj: &v1beta1.Pipeline{},
7377
wantErr: apiserver.ErrReferencedObjectValidationFailed,
78+
}, {
79+
name: "v1alpha1 stepaction",
80+
obj: &v1alpha1.StepAction{},
81+
wantErr: apiserver.ErrReferencedObjectValidationFailed,
7482
}, {
7583
name: "unsupported gvk",
7684
obj: &v1beta1.ClusterTask{},
@@ -85,6 +93,9 @@ func TestDryRunCreate_Invalid_DifferentGVKs(t *testing.T) {
8593
tektonclient.PrependReactor("create", "pipelines", func(action ktesting.Action) (bool, runtime.Object, error) {
8694
return true, nil, apierrors.NewBadRequest("bad request")
8795
})
96+
tektonclient.PrependReactor("create", "stepactions", func(action ktesting.Action) (bool, runtime.Object, error) {
97+
return true, nil, apierrors.NewBadRequest("bad request")
98+
})
8899
err := apiserver.DryRunValidate(context.Background(), "default", tc.obj, tektonclient)
89100
if d := cmp.Diff(tc.wantErr, err, cmpopts.EquateErrors()); d != "" {
90101
t.Errorf("wrong error: %s", d)

pkg/reconciler/taskrun/resources/taskref.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,21 @@ func GetTaskFunc(ctx context.Context, k8s kubernetes.Interface, tekton clientset
124124
}
125125

126126
// GetStepActionFunc is a factory function that will use the given Ref as context to return a valid GetStepAction function.
127-
func GetStepActionFunc(tekton clientset.Interface, namespace string) GetStepAction {
127+
// also requires a kubeclient, tektonclient, requester in case it needs to find that task in
128+
// cluster or authorize against an external repositroy. It will figure out whether it needs to look in the cluster or in
129+
// a remote image to fetch the reference.
130+
func GetStepActionFunc(tekton clientset.Interface, k8s kubernetes.Interface, requester remoteresource.Requester, tr *v1.TaskRun, step *v1.Step) GetStepAction {
131+
trName := tr.Name
132+
namespace := tr.Namespace
133+
if step.Ref != nil && step.Ref.Resolver != "" && requester != nil {
134+
// Return an inline function that implements GetStepAction by calling Resolver.Get with the specified StepAction type and
135+
// casting it to a StepAction.
136+
return func(ctx context.Context, name string) (*v1alpha1.StepAction, *v1.RefSource, error) {
137+
// TODO(#7259): support params replacements for resolver params
138+
resolver := resolution.NewResolver(requester, tr, string(step.Ref.Resolver), trName, namespace, step.Ref.Params)
139+
return resolveStepAction(ctx, resolver, name, namespace, k8s, tekton)
140+
}
141+
}
128142
local := &LocalStepActionRefResolver{
129143
Namespace: namespace,
130144
Tektonclient: tekton,
@@ -151,6 +165,21 @@ func resolveTask(ctx context.Context, resolver remote.Resolver, name, namespace
151165
return taskObj, refSource, vr, nil
152166
}
153167

168+
func resolveStepAction(ctx context.Context, resolver remote.Resolver, name, namespace string, k8s kubernetes.Interface, tekton clientset.Interface) (*v1alpha1.StepAction, *v1.RefSource, error) {
169+
obj, refSource, err := resolver.Get(ctx, "StepAction", name)
170+
if err != nil {
171+
return nil, nil, err
172+
}
173+
switch obj := obj.(type) { //nolint:gocritic
174+
case *v1alpha1.StepAction:
175+
if err := apiserver.DryRunValidate(ctx, namespace, obj, tekton); err != nil {
176+
return nil, nil, err
177+
}
178+
return obj, refSource, nil
179+
}
180+
return nil, nil, errors.New("resource is not a step action")
181+
}
182+
154183
// readRuntimeObjectAsTask tries to convert a generic runtime.Object
155184
// into a *v1.Task type so that its meta and spec fields
156185
// can be read. v1beta1 object will be converted to v1 and returned.

pkg/reconciler/taskrun/resources/taskref_test.go

Lines changed: 129 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -561,14 +561,26 @@ func TestGetStepActionFunc_Local(t *testing.T) {
561561
testcases := []struct {
562562
name string
563563
localStepActions []runtime.Object
564-
ref *v1.Ref
564+
taskRun *v1.TaskRun
565565
expected runtime.Object
566566
}{
567567
{
568568
name: "local-step-action",
569569
localStepActions: []runtime.Object{simpleNamespacedStepAction},
570-
ref: &v1.Ref{
571-
Name: "simple",
570+
taskRun: &v1.TaskRun{
571+
ObjectMeta: metav1.ObjectMeta{
572+
Name: "some-tr",
573+
Namespace: "default",
574+
},
575+
Spec: v1.TaskRunSpec{
576+
TaskSpec: &v1.TaskSpec{
577+
Steps: []v1.Step{{
578+
Ref: &v1.Ref{
579+
Name: "simple",
580+
},
581+
}},
582+
},
583+
},
572584
},
573585
expected: simpleNamespacedStepAction,
574586
},
@@ -577,10 +589,9 @@ func TestGetStepActionFunc_Local(t *testing.T) {
577589
for _, tc := range testcases {
578590
t.Run(tc.name, func(t *testing.T) {
579591
tektonclient := fake.NewSimpleClientset(tc.localStepActions...)
592+
fn := resources.GetStepActionFunc(tektonclient, nil, nil, tc.taskRun, &tc.taskRun.Spec.TaskSpec.Steps[0])
580593

581-
fn := resources.GetStepActionFunc(tektonclient, "default")
582-
583-
stepAction, refSource, err := fn(ctx, tc.ref.Name)
594+
stepAction, refSource, err := fn(ctx, tc.taskRun.Spec.TaskSpec.Steps[0].Ref.Name)
584595
if err != nil {
585596
t.Fatalf("failed to call stepActionfn: %s", err.Error())
586597
}
@@ -596,6 +607,109 @@ func TestGetStepActionFunc_Local(t *testing.T) {
596607
})
597608
}
598609
}
610+
611+
func TestGetStepActionFunc_RemoteResolution_Success(t *testing.T) {
612+
ctx := context.Background()
613+
stepRef := &v1.Ref{ResolverRef: v1.ResolverRef{Resolver: "git"}}
614+
615+
testcases := []struct {
616+
name string
617+
stepActionYAML string
618+
wantStepAction *v1alpha1.StepAction
619+
wantErr bool
620+
}{{
621+
name: "remote StepAction",
622+
stepActionYAML: strings.Join([]string{
623+
"kind: StepAction",
624+
"apiVersion: tekton.dev/v1alpha1",
625+
stepActionYAMLString,
626+
}, "\n"),
627+
wantStepAction: parse.MustParseV1alpha1StepAction(t, stepActionYAMLString),
628+
}}
629+
for _, tc := range testcases {
630+
t.Run(tc.name, func(t *testing.T) {
631+
resolved := test.NewResolvedResource([]byte(tc.stepActionYAML), nil /* annotations */, sampleRefSource.DeepCopy(), nil /* data error */)
632+
requester := test.NewRequester(resolved, nil)
633+
tr := &v1.TaskRun{
634+
ObjectMeta: metav1.ObjectMeta{Namespace: "default"},
635+
Spec: v1.TaskRunSpec{
636+
TaskSpec: &v1.TaskSpec{
637+
Steps: []v1.Step{{
638+
Ref: stepRef,
639+
}},
640+
},
641+
ServiceAccountName: "default",
642+
},
643+
}
644+
tektonclient := fake.NewSimpleClientset()
645+
fn := resources.GetStepActionFunc(tektonclient, nil, requester, tr, &tr.Spec.TaskSpec.Steps[0])
646+
647+
resolvedStepAction, resolvedRefSource, err := fn(ctx, tr.Spec.TaskSpec.Steps[0].Ref.Name)
648+
if tc.wantErr {
649+
if err == nil {
650+
t.Fatalf("expected an error when calling GetStepActionFunc but got none")
651+
}
652+
} else {
653+
if err != nil {
654+
t.Fatalf("failed to call fn: %s", err.Error())
655+
}
656+
657+
if d := cmp.Diff(sampleRefSource, resolvedRefSource); d != "" {
658+
t.Errorf("refSources did not match: %s", diff.PrintWantGot(d))
659+
}
660+
661+
if d := cmp.Diff(tc.wantStepAction, resolvedStepAction); d != "" {
662+
t.Errorf("resolvedStepActions did not match: %s", diff.PrintWantGot(d))
663+
}
664+
}
665+
})
666+
}
667+
}
668+
669+
func TestGetStepActionFunc_RemoteResolution_Error(t *testing.T) {
670+
ctx := context.Background()
671+
stepRef := &v1.Ref{ResolverRef: v1.ResolverRef{Resolver: "git"}}
672+
673+
testcases := []struct {
674+
name string
675+
resolvesTo []byte
676+
}{{
677+
name: "invalid data",
678+
resolvesTo: []byte("INVALID YAML"),
679+
}, {
680+
name: "resolved not StepAction",
681+
resolvesTo: []byte(strings.Join([]string{
682+
"kind: Task",
683+
"apiVersion: tekton.dev/v1beta1",
684+
taskYAMLString,
685+
}, "\n")),
686+
},
687+
}
688+
689+
for _, tc := range testcases {
690+
t.Run(tc.name, func(t *testing.T) {
691+
resource := test.NewResolvedResource(tc.resolvesTo, nil, nil, nil)
692+
requester := test.NewRequester(resource, nil)
693+
tr := &v1.TaskRun{
694+
ObjectMeta: metav1.ObjectMeta{Namespace: "default"},
695+
Spec: v1.TaskRunSpec{
696+
TaskSpec: &v1.TaskSpec{
697+
Steps: []v1.Step{{
698+
Ref: stepRef,
699+
}},
700+
},
701+
ServiceAccountName: "default",
702+
},
703+
}
704+
tektonclient := fake.NewSimpleClientset()
705+
fn := resources.GetStepActionFunc(tektonclient, nil, requester, tr, &tr.Spec.TaskSpec.Steps[0])
706+
if _, _, err := fn(ctx, tr.Spec.TaskSpec.Steps[0].Ref.Name); err == nil {
707+
t.Fatalf("expected error due to invalid pipeline data but saw none")
708+
}
709+
})
710+
}
711+
}
712+
599713
func TestGetTaskFuncFromTaskRunSpecAlreadyFetched(t *testing.T) {
600714
ctx := context.Background()
601715
ctx, cancel := context.WithCancel(ctx)
@@ -1515,6 +1629,15 @@ spec:
15151629
echo "hello world!"
15161630
`
15171631

1632+
var stepActionYAMLString = `
1633+
metadata:
1634+
name: foo
1635+
namespace: default
1636+
spec:
1637+
image: myImage
1638+
command: ["ls"]
1639+
`
1640+
15181641
var remoteTaskYamlWithoutDefaults = `
15191642
metadata:
15201643
name: simple

pkg/reconciler/taskrun/resources/taskspec.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ import (
2525
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1"
2626
clientset "github.com/tektoncd/pipeline/pkg/client/clientset/versioned"
2727
resolutionutil "github.com/tektoncd/pipeline/pkg/internal/resolution"
28+
remoteresource "github.com/tektoncd/pipeline/pkg/resolution/resource"
2829
"github.com/tektoncd/pipeline/pkg/trustedresources"
2930
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31+
"k8s.io/client-go/kubernetes"
3032
)
3133

3234
// ResolvedTask contains the data that is needed to execute
@@ -99,14 +101,13 @@ func GetTaskData(ctx context.Context, taskRun *v1.TaskRun, getTask GetTask) (*re
99101
}
100102

101103
// GetStepActionsData extracts the StepActions and merges them with the inlined Step specification.
102-
func GetStepActionsData(ctx context.Context, taskSpec v1.TaskSpec, tr *v1.TaskRun, tekton clientset.Interface) ([]v1.Step, error) {
104+
func GetStepActionsData(ctx context.Context, taskSpec v1.TaskSpec, taskRun *v1.TaskRun, tekton clientset.Interface, k8s kubernetes.Interface, requester remoteresource.Requester) ([]v1.Step, error) {
103105
steps := []v1.Step{}
104106
for _, step := range taskSpec.Steps {
105107
s := step.DeepCopy()
106108
if step.Ref != nil {
107-
s.Ref = nil
108-
getStepAction := GetStepActionFunc(tekton, tr.Namespace)
109-
stepAction, _, err := getStepAction(ctx, step.Ref.Name)
109+
getStepAction := GetStepActionFunc(tekton, k8s, requester, taskRun, s)
110+
stepAction, _, err := getStepAction(ctx, s.Ref.Name)
110111
if err != nil {
111112
return nil, err
112113
}
@@ -124,6 +125,7 @@ func GetStepActionsData(ctx context.Context, taskSpec v1.TaskSpec, tr *v1.TaskRu
124125
if stepActionSpec.Env != nil {
125126
s.Env = stepActionSpec.Env
126127
}
128+
s.Ref = nil
127129
steps = append(steps, *s)
128130
} else {
129131
steps = append(steps, step)

0 commit comments

Comments
 (0)