Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -296,6 +309,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 @@ -405,6 +539,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