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 authored and tekton-robot committed Oct 30, 2023
1 parent aa793d3 commit 7069889
Show file tree
Hide file tree
Showing 10 changed files with 640 additions and 10 deletions.
34 changes: 34 additions & 0 deletions docs/stepactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,40 @@ spec:
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
```

If a `Step` is referencing a `StepAction`, it cannot contain the fields supported by `StepActions`. This includes:
- `image`
- `command`
Expand Down
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 := &LocalStepActionRefResolver{
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
}

// LocalStepActionRefResolver uses the current cluster to resolve a StepAction reference.
type LocalStepActionRefResolver 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 *LocalStepActionRefResolver) 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
175 changes: 175 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 @@ -312,6 +325,127 @@ func TestLocalTaskRef(t *testing.T) {
}
}

func TestStepActionRef(t *testing.T) {
testcases := []struct {
name string
namespace string
stepactions []runtime.Object
ref *v1.Ref
expected runtime.Object
}{{
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",
},
},
}}

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.LocalStepActionRefResolver{
Namespace: tc.namespace,
Tektonclient: tektonclient,
}

task, refSource, err := lc.GetStepAction(ctx, tc.ref.Name)
if 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 TestStepActionRef_Error(t *testing.T) {
testcases := []struct {
name string
namespace string
stepactions []runtime.Object
ref *v1.Ref
wantErr error
}{
{
name: "step-action-not-found",
namespace: "default",
stepactions: []runtime.Object{},
ref: &v1.Ref{
Name: "simple",
},
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.LocalStepActionRefResolver{
Namespace: tc.namespace,
Tektonclient: tektonclient,
}

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

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

Expand Down Expand Up @@ -421,6 +555,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
46 changes: 45 additions & 1 deletion pkg/reconciler/taskrun/resources/taskspec.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"fmt"

v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1"
resolutionutil "github.com/tektoncd/pipeline/pkg/internal/resolution"
"github.com/tektoncd/pipeline/pkg/trustedresources"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -37,6 +38,9 @@ type ResolvedTask struct {
VerificationResult *trustedresources.VerificationResult
}

// GetStepAction is a function used to retrieve StepActions.
type GetStepAction func(context.Context, string) (*v1alpha1.StepAction, *v1.RefSource, error)

// GetTask is a function used to retrieve Tasks.
// VerificationResult is the result from trusted resources if the feature is enabled.
type GetTask func(context.Context, string) (*v1.Task, *v1.RefSource, *trustedresources.VerificationResult, error)
Expand All @@ -47,7 +51,7 @@ type GetTaskRun func(string) (*v1.TaskRun, error)
// GetTaskData will retrieve the Task metadata and Spec associated with the
// provided TaskRun. This can come from a reference Task or from the TaskRun's
// metadata and embedded TaskSpec.
func GetTaskData(ctx context.Context, taskRun *v1.TaskRun, getTask GetTask) (*resolutionutil.ResolvedObjectMeta, *v1.TaskSpec, error) {
func GetTaskData(ctx context.Context, taskRun *v1.TaskRun, getTask GetTask, getStepAction GetStepAction) (*resolutionutil.ResolvedObjectMeta, *v1.TaskSpec, error) {
taskMeta := metav1.ObjectMeta{}
taskSpec := v1.TaskSpec{}
var refSource *v1.RefSource
Expand Down Expand Up @@ -85,10 +89,50 @@ func GetTaskData(ctx context.Context, taskRun *v1.TaskRun, getTask GetTask) (*re
return nil, nil, fmt.Errorf("taskRun %s not providing TaskRef or TaskSpec", taskRun.Name)
}

steps, err := extractStepActions(ctx, taskSpec, getStepAction)
if err != nil {
return nil, nil, err
} else {
taskSpec.Steps = steps
}

taskSpec.SetDefaults(ctx)
return &resolutionutil.ResolvedObjectMeta{
ObjectMeta: &taskMeta,
RefSource: refSource,
VerificationResult: verificationResult,
}, &taskSpec, nil
}

// extractStepActions extracts the StepActions and merges them with the inlined Step specification.
func extractStepActions(ctx context.Context, taskSpec v1.TaskSpec, getStepAction GetStepAction) ([]v1.Step, error) {
steps := []v1.Step{}
for _, step := range taskSpec.Steps {
s := step.DeepCopy()
if step.Ref != nil {
s.Ref = nil
stepAction, _, err := getStepAction(ctx, step.Ref.Name)
if err != nil {
return nil, err
}
stepActionSpec := stepAction.StepActionSpec()
s.Image = stepActionSpec.Image
if len(stepActionSpec.Command) > 0 {
s.Command = stepActionSpec.Command
}
if len(stepActionSpec.Args) > 0 {
s.Args = stepActionSpec.Args
}
if stepActionSpec.Script != "" {
s.Script = stepActionSpec.Script
}
if stepActionSpec.Env != nil {
s.Env = stepActionSpec.Env
}
steps = append(steps, *s)
} else {
steps = append(steps, step)
}
}
return steps, nil
}
Loading

0 comments on commit 7069889

Please sign in to comment.