Skip to content

Commit

Permalink
Referencing StepActions in Steps
Browse files Browse the repository at this point in the history
This PR allows the Step to reference a StepAction CRD deployed on the cluster. This capability is gated behind a feature flag `enable-step-actions`.  Remote resolution of StepActions will be implemented in a follow-up PR.

This is the second item on the implementation Issue #7259
  • Loading branch information
chitrangpatel committed Oct 27, 2023
1 parent 43ac007 commit 1f59118
Show file tree
Hide file tree
Showing 10 changed files with 715 additions and 10 deletions.
84 changes: 84 additions & 0 deletions docs/stepactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,87 @@ spec:
timeout: 1h
onError: continue
```

## Referencing a StepAction

`StepActions` can be referenced from the `Step` using the `ref` field, as follows:

```yaml
apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
name: step-action-run
spec:
TaskSpec:
steps:
- name: action-runner
ref:
name: step-action
```

Upon resolution and execution of the `TaskRun`, the `Status` will look something like:

```yaml
status:
completionTime: "2023-10-24T20:28:42Z"
conditions:
- lastTransitionTime: "2023-10-24T20:28:42Z"
message: All Steps have completed executing
reason: Succeeded
status: "True"
type: Succeeded
podName: step-action-run-pod
provenance:
featureFlags:
EnableStepActions: true
...
startTime: "2023-10-24T20:28:32Z"
steps:
- container: step-action-runner
imageID: docker.io/library/alpine@sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978
name: action-runner
terminated:
containerID: containerd://46a836588967202c05b594696077b147a0eb0621976534765478925bb7ce57f6
exitCode: 0
finishedAt: "2023-10-24T20:28:42Z"
reason: Completed
startedAt: "2023-10-24T20:28:42Z"
taskSpec:
steps:
- computeResources: {}
image: alpine
name: action-runner
script: |
echo "I am a Step Action!!!"
```

If a `Step` is referencing a `StepAction`, it cannot contain the fields supported by `StepActions`. This includes:
- `image`
- `command`
- `args`
- `script`
- `env`

Using any of the above fields and referencing a `StepAction` in the same `Step` is not allowed and will cause an validation error.

```yaml
# This is not allowed and will result in a validation error.
# Because the image is expected to be provided by the StepAction
# and not inlined.
apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
name: step-action-run
spec:
TaskSpec:
steps:
- name: action-runner
ref:
name: step-action
image: ubuntu
```
Executing the above `TaskRun` will result in an error that looks like:

```
Error from server (BadRequest): error when creating "STDIN": admission webhook "validation.webhook.pipeline.tekton.dev" denied the request: validation failed: image cannot be used with Ref: spec.taskSpec.steps[0].image
```
19 changes: 19 additions & 0 deletions examples/v1/taskruns/alpha/stepaction.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
apiVersion: tekton.dev/v1alpha1
kind: StepAction
metadata:
name: step-action
spec:
image: alpine
script: |
echo "I am a Step Action!!!"
---
apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
name: step-action-run
spec:
TaskSpec:
steps:
- name: action-runner
ref:
name: step-action
29 changes: 29 additions & 0 deletions pkg/reconciler/taskrun/resources/taskref.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@ 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 {
local := &LocalRefResolver{
Namespace: namespace,
Tektonclient: tekton,
}
return local.GetStepAction
}

// resolveTask accepts an impl of remote.Resolver and attempts to
// fetch a task with given name and verify the v1beta1 task if trusted resources is enabled.
// An error is returned if the remoteresource doesn't work
Expand Down Expand Up @@ -222,6 +231,26 @@ func (l *LocalTaskRefResolver) GetTask(ctx context.Context, name string) (*v1.Ta
return task, nil, nil, nil
}

// LocalRefResolver uses the current cluster to resolve a stepaction reference.
type LocalRefResolver struct {
Namespace string
Tektonclient clientset.Interface
}

// GetStepAction will resolve a StepAction from the local cluster using a versioned Tekton client.
// It will return an error if it can't find an appropriate StepAction for any reason.
func (l *LocalRefResolver) GetStepAction(ctx context.Context, name string) (*v1alpha1.StepAction, *v1.RefSource, error) {
// If we are going to resolve this reference locally, we need a namespace scope.
if l.Namespace == "" {
return nil, nil, fmt.Errorf("must specify namespace to resolve reference to step action %s", name)
}
stepAction, err := l.Tektonclient.TektonV1alpha1().StepActions(l.Namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return nil, nil, err
}
return stepAction, nil, nil
}

// IsGetTaskErrTransient returns true if an error returned by GetTask is retryable.
func IsGetTaskErrTransient(err error) bool {
return strings.Contains(err.Error(), errEtcdLeaderChange)
Expand Down
152 changes: 152 additions & 0 deletions pkg/reconciler/taskrun/resources/taskref_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ import (
)

var (
simpleNamespacedStepAction = &v1alpha1.StepAction{
ObjectMeta: metav1.ObjectMeta{
Name: "simple",
Namespace: "default",
},
TypeMeta: metav1.TypeMeta{
APIVersion: "tekton.dev/v1alpha1",
Kind: "StepAction",
},
Spec: v1alpha1.StepActionSpec{
Image: "something",
},
}
simpleNamespacedTask = &v1.Task{
ObjectMeta: metav1.ObjectMeta{
Name: "simple",
Expand Down Expand Up @@ -296,6 +309,104 @@ func TestLocalTaskRef(t *testing.T) {
}
}

func TestLocalRef(t *testing.T) {
testcases := []struct {
name string
namespace string
stepactions []runtime.Object
ref *v1.Ref
expected runtime.Object
wantErr error
}{
{
name: "local-step-action",
namespace: "default",
stepactions: []runtime.Object{
&v1alpha1.StepAction{
ObjectMeta: metav1.ObjectMeta{
Name: "simple",
Namespace: "default",
},
},
&v1alpha1.StepAction{
ObjectMeta: metav1.ObjectMeta{
Name: "dummy",
Namespace: "default",
},
},
},
ref: &v1.Ref{
Name: "simple",
},
expected: &v1alpha1.StepAction{
ObjectMeta: metav1.ObjectMeta{
Name: "simple",
Namespace: "default",
},
},
wantErr: nil,
}, {
name: "step-action-not-found",
namespace: "default",
stepactions: []runtime.Object{},
ref: &v1.Ref{
Name: "simple",
},
expected: nil,
wantErr: errors.New(`stepactions.tekton.dev "simple" not found`),
}, {
name: "local-step-action-missing-namespace",
namespace: "",
stepactions: []runtime.Object{
&v1alpha1.StepAction{
ObjectMeta: metav1.ObjectMeta{
Name: "simple",
Namespace: "default",
},
},
},
ref: &v1.Ref{
Name: "simple",
},
wantErr: fmt.Errorf("must specify namespace to resolve reference to step action simple"),
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
tektonclient := fake.NewSimpleClientset(tc.stepactions...)

lc := &resources.LocalRefResolver{
Namespace: tc.namespace,
Tektonclient: tektonclient,
}

task, refSource, err := lc.GetStepAction(ctx, tc.ref.Name)
if tc.wantErr != nil {
if err == nil {
t.Fatal("Expected error but found nil instead")
}
if tc.wantErr.Error() != err.Error() {
t.Fatalf("Received different error ( %#v )", err)
}
} else if tc.wantErr == nil && err != nil {
t.Fatalf("Received unexpected error ( %#v )", err)
}

if d := cmp.Diff(tc.expected, task); tc.expected != nil && d != "" {
t.Error(diff.PrintWantGot(d))
}

// local cluster step actions have empty source for now. This may be changed in future.
if refSource != nil {
t.Errorf("expected refsource is nil, but got %v", refSource)
}
})
}
}
func TestGetTaskFunc_Local(t *testing.T) {
ctx := context.Background()

Expand Down Expand Up @@ -405,6 +516,47 @@ func TestGetTaskFunc_Local(t *testing.T) {
}
}

func TestGetStepActionFunc_Local(t *testing.T) {
ctx := context.Background()

testcases := []struct {
name string
localStepActions []runtime.Object
ref *v1.Ref
expected runtime.Object
}{
{
name: "local-step-action",
localStepActions: []runtime.Object{simpleNamespacedStepAction},
ref: &v1.Ref{
Name: "simple",
},
expected: simpleNamespacedStepAction,
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
tektonclient := fake.NewSimpleClientset(tc.localStepActions...)

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

stepAction, refSource, err := fn(ctx, tc.ref.Name)
if err != nil {
t.Fatalf("failed to call stepActionfn: %s", err.Error())
}

if diff := cmp.Diff(stepAction, tc.expected); tc.expected != nil && diff != "" {
t.Error(diff)
}

// local cluster task has empty RefSource for now. This may be changed in future.
if refSource != nil {
t.Errorf("expected refSource is nil, but got %v", refSource)
}
})
}
}
func TestGetTaskFuncFromTaskRunSpecAlreadyFetched(t *testing.T) {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
Expand Down
Loading

0 comments on commit 1f59118

Please sign in to comment.