Skip to content

Commit

Permalink
[TEP-0142] Remote Resolution for StepAction
Browse files Browse the repository at this point in the history
This commit is part of #7259. It adds the remote resolution for
StepAction.

Signed-off-by: Yongxuan Zhang [email protected]
  • Loading branch information
Yongxuanzhang committed Nov 7, 2023
1 parent a791d49 commit 6fda995
Show file tree
Hide file tree
Showing 11 changed files with 429 additions and 19 deletions.
32 changes: 29 additions & 3 deletions docs/stepactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ A `StepAction` is the reusable and scriptable unit of work that is performed by

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.


## Configuring a `StepAction`

A `StepAction` definition supports the following fields:
Expand All @@ -29,7 +29,7 @@ A `StepAction` definition supports the following fields:
- [`metadata`][kubernetes-overview] - Specifies metadata that uniquely identifies the
`StepAction` resource object. For example, a `name`.
- [`spec`][kubernetes-overview] - Specifies the configuration information for this `StepAction` resource object.
- `image` - Specifies the image to use for the `Step`.
- `image` - Specifies the image to use for the `Step`.
- The container image must abide by the [container contract](./container-contract.md).
- Optional
- `command`
Expand All @@ -50,7 +50,7 @@ spec:
env:
- name: HOME
value: /home
image: ubuntu
image: ubuntu
command: ["ls"]
args: ["-lh"]
```
Expand Down Expand Up @@ -169,3 +169,29 @@ spec:
timeout: 1h
onError: continue
```

### Specifying Remote StepActions

A `ref` field may specify a `StepAction` in a remote location such as git.
Support for specific types of remote will depend on the `Resolvers` your
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:

```yaml
apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
generateName: step-action-run-
spec:
TaskSpec:
steps:
- name: action-runner
ref:
resolver: git
params:
- name: url
value: https://github.com/repo/repo.git
- name: revision
value: main
- name: pathInRepo
value: remote_step.yaml
```
18 changes: 18 additions & 0 deletions examples/v1/taskruns/alpha/stepaction-git-resolver.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# TODO(#7325): use StepAction from Catalog
apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
generateName: step-action-run-
spec:
TaskSpec:
steps:
- name: action-runner
ref:
resolver: git
params:
- name: url
value: https://github.com/chitrangpatel/repo1M.git
- name: revision
value: main
- name: pathInRepo
value: basic_step.yaml
3 changes: 3 additions & 0 deletions pkg/apis/pipeline/v1/taskrun_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ const (
// TaskRunReasonResolvingTaskRef indicates that the TaskRun is waiting for
// its taskRef to be asynchronously resolved.
TaskRunReasonResolvingTaskRef = "ResolvingTaskRef"
// TaskRunReasonResolvingStepActionRef indicates that the TaskRun is waiting for
// its StepAction's Ref to be asynchronously resolved.
TaskRunReasonResolvingStepActionRef = "ResolvingStepActionRef"
// TaskRunReasonImagePullFailed is the reason set when the step of a task fails due to image not being pulled
TaskRunReasonImagePullFailed TaskRunReason = "TaskRunImagePullFailed"
// TaskRunReasonResultLargerThanAllowedLimit is the reason set when one of the results exceeds its maximum allowed limit of 1 KB
Expand Down
8 changes: 8 additions & 0 deletions pkg/reconciler/apiserver/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/google/uuid"
v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
clientset "github.com/tektoncd/pipeline/pkg/client/clientset/versioned"
apierrors "k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -56,6 +57,13 @@ func DryRunValidate(ctx context.Context, namespace string, obj runtime.Object, t
if _, err := tekton.TektonV1beta1().Tasks(namespace).Create(ctx, dryRunObj, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}}); err != nil {
return handleDryRunCreateErr(err, obj.Name)
}
case *v1alpha1.StepAction:
dryRunObj := obj.DeepCopy()
dryRunObj.Name = dryRunObjName
dryRunObj.Namespace = namespace // Make sure the namespace is the same as the StepAction
if _, err := tekton.TektonV1alpha1().StepActions(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())
}
Expand Down
11 changes: 11 additions & 0 deletions pkg/reconciler/apiserver/apiserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"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/v1alpha1"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"github.com/tektoncd/pipeline/pkg/client/clientset/versioned/fake"
"github.com/tektoncd/pipeline/pkg/reconciler/apiserver"
Expand Down Expand Up @@ -34,6 +35,9 @@ func TestDryRunCreate_Valid_DifferentGVKs(t *testing.T) {
}, {
name: "v1beta1 pipeline",
obj: &v1beta1.Pipeline{},
}, {
name: "v1alpha1 stepaction",
obj: &v1alpha1.StepAction{},
}, {
name: "unsupported gvk",
obj: &v1beta1.ClusterTask{},
Expand Down Expand Up @@ -71,6 +75,10 @@ func TestDryRunCreate_Invalid_DifferentGVKs(t *testing.T) {
name: "v1beta1 pipeline",
obj: &v1beta1.Pipeline{},
wantErr: apiserver.ErrReferencedObjectValidationFailed,
}, {
name: "v1alpha1 stepaction",
obj: &v1alpha1.StepAction{},
wantErr: apiserver.ErrReferencedObjectValidationFailed,
}, {
name: "unsupported gvk",
obj: &v1beta1.ClusterTask{},
Expand All @@ -85,6 +93,9 @@ func TestDryRunCreate_Invalid_DifferentGVKs(t *testing.T) {
tektonclient.PrependReactor("create", "pipelines", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, nil, apierrors.NewBadRequest("bad request")
})
tektonclient.PrependReactor("create", "stepactions", 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)
Expand Down
33 changes: 31 additions & 2 deletions pkg/reconciler/taskrun/resources/taskref.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func GetTaskKind(taskrun *v1.TaskRun) v1.TaskKind {
}

// GetTaskFuncFromTaskRun is a factory function that will use the given TaskRef as context to return a valid GetTask function. It
// also requires a kubeclient, tektonclient, namespace, and service account in case it needs to find that task in
// It also requires a kubeclient, tektonclient, namespace, and service account in case it needs to find that task in
// cluster or authorize against an external repositroy. It will figure out whether it needs to look in the cluster or in
// a remote image to fetch the reference. It will also return the "kind" of the task being referenced.
// OCI bundle and remote resolution tasks will be verified by trusted resources if the feature is enabled
Expand Down Expand Up @@ -124,7 +124,21 @@ func GetTaskFunc(ctx context.Context, k8s kubernetes.Interface, tekton clientset
}

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

func resolveStepAction(ctx context.Context, resolver remote.Resolver, name, namespace string, k8s kubernetes.Interface, tekton clientset.Interface) (*v1alpha1.StepAction, *v1.RefSource, error) {
obj, refSource, err := resolver.Get(ctx, "StepAction", name)
if err != nil {
return nil, nil, err
}
switch obj := obj.(type) { //nolint:gocritic
case *v1alpha1.StepAction:
if err := apiserver.DryRunValidate(ctx, namespace, obj, tekton); err != nil {
return nil, nil, err
}
return obj, refSource, nil
}
return nil, nil, errors.New("resource is not a StepAction")
}

// readRuntimeObjectAsTask tries to convert a generic runtime.Object
// into a *v1.Task type so that its meta and spec fields
// can be read. v1beta1 object will be converted to v1 and returned.
Expand Down
135 changes: 129 additions & 6 deletions pkg/reconciler/taskrun/resources/taskref_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -561,14 +561,26 @@ func TestGetStepActionFunc_Local(t *testing.T) {
testcases := []struct {
name string
localStepActions []runtime.Object
ref *v1.Ref
taskRun *v1.TaskRun
expected runtime.Object
}{
{
name: "local-step-action",
localStepActions: []runtime.Object{simpleNamespacedStepAction},
ref: &v1.Ref{
Name: "simple",
taskRun: &v1.TaskRun{
ObjectMeta: metav1.ObjectMeta{
Name: "some-tr",
Namespace: "default",
},
Spec: v1.TaskRunSpec{
TaskSpec: &v1.TaskSpec{
Steps: []v1.Step{{
Ref: &v1.Ref{
Name: "simple",
},
}},
},
},
},
expected: simpleNamespacedStepAction,
},
Expand All @@ -577,10 +589,9 @@ func TestGetStepActionFunc_Local(t *testing.T) {
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
tektonclient := fake.NewSimpleClientset(tc.localStepActions...)
fn := resources.GetStepActionFunc(tektonclient, nil, nil, tc.taskRun, &tc.taskRun.Spec.TaskSpec.Steps[0])

fn := resources.GetStepActionFunc(tektonclient, "default")

stepAction, refSource, err := fn(ctx, tc.ref.Name)
stepAction, refSource, err := fn(ctx, tc.taskRun.Spec.TaskSpec.Steps[0].Ref.Name)
if err != nil {
t.Fatalf("failed to call stepActionfn: %s", err.Error())
}
Expand All @@ -596,6 +607,109 @@ func TestGetStepActionFunc_Local(t *testing.T) {
})
}
}

func TestGetStepActionFunc_RemoteResolution_Success(t *testing.T) {
ctx := context.Background()
stepRef := &v1.Ref{ResolverRef: v1.ResolverRef{Resolver: "git"}}

testcases := []struct {
name string
stepActionYAML string
wantStepAction *v1alpha1.StepAction
wantErr bool
}{{
name: "remote StepAction",
stepActionYAML: strings.Join([]string{
"kind: StepAction",
"apiVersion: tekton.dev/v1alpha1",
stepActionYAMLString,
}, "\n"),
wantStepAction: parse.MustParseV1alpha1StepAction(t, stepActionYAMLString),
}}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
resolved := test.NewResolvedResource([]byte(tc.stepActionYAML), nil /* annotations */, sampleRefSource.DeepCopy(), nil /* data error */)
requester := test.NewRequester(resolved, nil)
tr := &v1.TaskRun{
ObjectMeta: metav1.ObjectMeta{Namespace: "default"},
Spec: v1.TaskRunSpec{
TaskSpec: &v1.TaskSpec{
Steps: []v1.Step{{
Ref: stepRef,
}},
},
ServiceAccountName: "default",
},
}
tektonclient := fake.NewSimpleClientset()
fn := resources.GetStepActionFunc(tektonclient, nil, requester, tr, &tr.Spec.TaskSpec.Steps[0])

resolvedStepAction, resolvedRefSource, err := fn(ctx, tr.Spec.TaskSpec.Steps[0].Ref.Name)
if tc.wantErr {
if err == nil {
t.Fatalf("expected an error when calling GetStepActionFunc but got none")
}
} else {
if err != nil {
t.Fatalf("failed to call fn: %s", err.Error())
}

if d := cmp.Diff(sampleRefSource, resolvedRefSource); d != "" {
t.Errorf("refSources did not match: %s", diff.PrintWantGot(d))
}

if d := cmp.Diff(tc.wantStepAction, resolvedStepAction); d != "" {
t.Errorf("resolvedStepActions did not match: %s", diff.PrintWantGot(d))
}
}
})
}
}

func TestGetStepActionFunc_RemoteResolution_Error(t *testing.T) {
ctx := context.Background()
stepRef := &v1.Ref{ResolverRef: v1.ResolverRef{Resolver: "git"}}

testcases := []struct {
name string
resolvesTo []byte
}{{
name: "invalid data",
resolvesTo: []byte("INVALID YAML"),
}, {
name: "resolved not StepAction",
resolvesTo: []byte(strings.Join([]string{
"kind: Task",
"apiVersion: tekton.dev/v1beta1",
taskYAMLString,
}, "\n")),
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
resource := test.NewResolvedResource(tc.resolvesTo, nil, nil, nil)
requester := test.NewRequester(resource, nil)
tr := &v1.TaskRun{
ObjectMeta: metav1.ObjectMeta{Namespace: "default"},
Spec: v1.TaskRunSpec{
TaskSpec: &v1.TaskSpec{
Steps: []v1.Step{{
Ref: stepRef,
}},
},
ServiceAccountName: "default",
},
}
tektonclient := fake.NewSimpleClientset()
fn := resources.GetStepActionFunc(tektonclient, nil, requester, tr, &tr.Spec.TaskSpec.Steps[0])
if _, _, err := fn(ctx, tr.Spec.TaskSpec.Steps[0].Ref.Name); err == nil {
t.Fatalf("expected error due to invalid pipeline data but saw none")
}
})
}
}

func TestGetTaskFuncFromTaskRunSpecAlreadyFetched(t *testing.T) {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
Expand Down Expand Up @@ -1515,6 +1629,15 @@ spec:
echo "hello world!"
`

var stepActionYAMLString = `
metadata:
name: foo
namespace: default
spec:
image: myImage
command: ["ls"]
`

var remoteTaskYamlWithoutDefaults = `
metadata:
name: simple
Expand Down
Loading

0 comments on commit 6fda995

Please sign in to comment.