Skip to content

Commit

Permalink
Sort the steps of taskrun based on the steps of task
Browse files Browse the repository at this point in the history
  • Loading branch information
Vincent Hou authored and tekton-robot committed Jun 14, 2019
1 parent f4b73c8 commit 8552f8b
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 5 deletions.
8 changes: 8 additions & 0 deletions docs/taskruns.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ A `TaskRun` runs until all `steps` have completed or until a failure occurs.
- [Providing resources](#providing-resources)
- [Overriding where resources are copied from](#overriding-where-resources-are-copied-from)
- [Service Account](#service-account)
- [Steps](#steps)
- [Cancelling a TaskRun](#cancelling-a-taskrun)
- [Examples](#examples)
- [Logs](logs.md)
Expand Down Expand Up @@ -229,6 +230,13 @@ spec:
emptyDir: {}
```

## Steps

If multiple `steps` are defined in the `Task` invoked by the `TaskRun`, we will see the
`status.steps` of the `TaskRun` displayed in the same order as they are defined in
`spec.steps` of the `Task`, when the `TaskRun` is accessed by the `get` command, e.g.
`kubectl get taskrun <name> -o yaml`. Replace \<name\> with the name of the `TaskRun`.

## Cancelling a TaskRun

In order to cancel a running task (`TaskRun`), you need to update its spec to
Expand Down
3 changes: 3 additions & 0 deletions docs/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ spec:
The `steps` field is required. You define one or more `steps` fields to define
the body of a `Task`.

If multiple `steps` are defined, they will be executed in the same order as they
are defined, if the `Task` is invoked by a [TaskRun](taskruns.md).

Each `steps` in a `Task` must specify a container image that adheres to the
[container contract](./container-contract.md). For each of the `steps` fields,
or container images that you define:
Expand Down
7 changes: 5 additions & 2 deletions pkg/reconciler/v1alpha1/taskrun/taskrun.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/tektoncd/pipeline/pkg/reconciler"
"github.com/tektoncd/pipeline/pkg/reconciler/v1alpha1/taskrun/entrypoint"
"github.com/tektoncd/pipeline/pkg/reconciler/v1alpha1/taskrun/resources"
"github.com/tektoncd/pipeline/pkg/status"
"go.uber.org/zap"
"golang.org/x/xerrors"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -331,6 +332,10 @@ func (c *Reconciler) reconcile(ctx context.Context, tr *v1alpha1.TaskRun) error

updateStatusFromPod(tr, pod, c.resourceLister, c.KubeClientSet, c.Logger)

status.SortTaskRunStepOrder(tr.Status.Steps, taskSpec.Steps)

updateTaskRunResourceResult(tr, pod, c.resourceLister, c.KubeClientSet, c.Logger)

after := tr.Status.GetCondition(apis.ConditionSucceeded)

reconciler.EmitEvent(c.Recorder, before, after, tr)
Expand Down Expand Up @@ -400,8 +405,6 @@ func updateStatusFromPod(taskRun *v1alpha1.TaskRun, pod *corev1.Pod, resourceLis
// update tr completed time
taskRun.Status.CompletionTime = &metav1.Time{Time: time.Now()}
}

updateTaskRunResourceResult(taskRun, pod, resourceLister, kubeclient, logger)
}

func (c *Reconciler) handlePodCreationError(tr *v1alpha1.TaskRun, err error) {
Expand Down
87 changes: 87 additions & 0 deletions pkg/reconciler/v1alpha1/taskrun/taskrun_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ var (

simpleStep = tb.Step("simple-step", "foo", tb.Command("/mycmd"))
simpleTask = tb.Task("test-task", "foo", tb.TaskSpec(simpleStep))
taskMultipleSteps = tb.Task("test-task-multi-steps", "foo", tb.TaskSpec(
tb.Step("z-step", "foo",
tb.Command("/mycmd"),
),
tb.Step("v-step", "foo",
tb.Command("/mycmd"),
),
tb.Step("x-step", "foo",
tb.Command("/mycmd"),
),
))
clustertask = tb.ClusterTask("test-cluster-task", tb.ClusterTaskSpec(simpleStep))

outputTask = tb.Task("test-output-task", "foo", tb.TaskSpec(
Expand Down Expand Up @@ -1124,6 +1135,82 @@ func TestReconcile_SetsStartTime(t *testing.T) {
}
}

func TestReconcile_SortTaskRunStatusSteps(t *testing.T) {
taskRun := tb.TaskRun("test-taskrun", "foo", tb.TaskRunSpec(
tb.TaskRunTaskRef(taskMultipleSteps.Name)),
tb.TaskRunStatus(
tb.PodName("the-pod"),
),
)

// The order of the container statuses has been shuffled, not aligning with the order of the
// spec steps of the Task any more. After Reconcile is called, we should see the order of status
// steps in TaksRun has been converted to the same one as in spec steps of the Task.
d := test.Data{
TaskRuns: []*v1alpha1.TaskRun{taskRun},
Tasks: []*v1alpha1.Task{taskMultipleSteps},
Pods: []*corev1.Pod{{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "the-pod",
},
Status: corev1.PodStatus{
Phase: corev1.PodSucceeded,
ContainerStatuses: []corev1.ContainerStatus{{
Name: "step-nop",
State: corev1.ContainerState{
Terminated: &corev1.ContainerStateTerminated{
ExitCode: 0,
},
},
}, {
Name: "step-x-step",
State: corev1.ContainerState{
Terminated: &corev1.ContainerStateTerminated{
ExitCode: 0,
},
},
}, {
Name: "step-v-step",
State: corev1.ContainerState{
Terminated: &corev1.ContainerStateTerminated{
ExitCode: 0,
},
},
}, {
Name: "step-z-step",
State: corev1.ContainerState{
Terminated: &corev1.ContainerStateTerminated{
ExitCode: 0,
},
},
}},
},
}},
}
testAssets := getTaskRunController(t, d)
if err := testAssets.Controller.Reconciler.Reconcile(context.Background(), getRunName(taskRun)); err != nil {
t.Errorf("expected no error reconciling valid TaskRun but got %v", err)
}
verify_TaskRunStatusStep(t, taskRun, taskMultipleSteps)
}

func verify_TaskRunStatusStep(t *testing.T, taskRun *v1alpha1.TaskRun, task *v1alpha1.Task) {
actualStepOrder := []string{}
for _, state := range(taskRun.Status.Steps) {
actualStepOrder = append(actualStepOrder, state.Name)
}
expectedStepOrder := []string{}
for _, state := range(taskMultipleSteps.Spec.Steps) {
expectedStepOrder = append(expectedStepOrder, state.Name)
}
// Add a nop in the end. This may be removed in future.
expectedStepOrder = append(expectedStepOrder, "nop")
if d := cmp.Diff(actualStepOrder, expectedStepOrder); d != "" {
t.Errorf("The status steps in TaksRun doesn't match the spec steps in Task, diff: %s", d)
}
}

func TestReconcile_DoesntChangeStartTime(t *testing.T) {
startTime := time.Date(2000, 1, 1, 1, 1, 1, 1, time.UTC)
taskRun := tb.TaskRun("test-taskrun", "foo", tb.TaskRunSpec(
Expand Down
81 changes: 81 additions & 0 deletions pkg/status/stepstatesorter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
Copyright 2019 Knative Authors LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package status

import (
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1"
corev1 "k8s.io/api/core/v1"
"sort"
)


// StepStateSorter implements a sorting mechanism to align the order of the steps in TaskRun
// with the spec steps in Task.
type StepStateSorter struct {
taskRunSteps []v1alpha1.StepState
mapForSort map[string]int
}

func (trt *StepStateSorter) Init(taskRunSteps []v1alpha1.StepState, taskSpecSteps []corev1.Container) {
trt.taskRunSteps = taskRunSteps
trt.mapForSort = trt.constructTaskStepsSorter(taskSpecSteps)
}

// constructTaskStepsSorter constructs a map matching the names of
// the steps to their indices for a task.
func (trt *StepStateSorter) constructTaskStepsSorter(taskSpecSteps []corev1.Container) map[string]int {
sorter := make(map[string]int)
for index, step := range taskSpecSteps {
sorter[step.Name] = index
}
return sorter
}

// changeIndex sorts the steps of the task run, based on the
// order of the steps in the task. Instead of changing the element with the one next to it,
// we directly swap it with the desired index.
func (trt *StepStateSorter) changeIndex(index int) {
// Check if the current index is equal to the desired index. If they are equal, do not swap; if they
// are not equal, swap index j with the desired index.
desiredIndex, exist := trt.mapForSort[trt.taskRunSteps[index].Name]
if exist && index != desiredIndex {
trt.taskRunSteps[desiredIndex], trt.taskRunSteps[index] = trt.taskRunSteps[index], trt.taskRunSteps[desiredIndex]
}
}

func (trt *StepStateSorter) Len() int {
return len(trt.taskRunSteps)
}

func (trt *StepStateSorter) Swap(i, j int) {
trt.changeIndex(j)
// The index j is unable to reach the last index.
// When i reaches the end of the array, we need to check whether the last one needs a swap.
if (i == trt.Len() - 1 ) {
trt.changeIndex(i)
}
}

func (trt *StepStateSorter) Less(i, j int) bool {
// Since the logic is complicated, we move it into the Swap function to decide whether
// and how to change the index. We set it to true here in order to iterate all the
// elements of the array in the Swap function.
return true
}

func SortTaskRunStepOrder(taskRunSteps []v1alpha1.StepState, taskSpecSteps []corev1.Container) {
trt := new(StepStateSorter)
trt.Init(taskRunSteps, taskSpecSteps)
sort.Sort(trt)
}
85 changes: 85 additions & 0 deletions pkg/status/stepstatesorter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
Copyright 2019 Knative Authors LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package status

import (
"github.com/google/go-cmp/cmp"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1"
tb "github.com/tektoncd/pipeline/test/builder"
corev1 "k8s.io/api/core/v1"
"testing"
)

func TestSortTaskRunStepOrder(t *testing.T) {
task := tb.Task("failing-task", "default", tb.TaskSpec(
tb.Step("hello", "busybox",
tb.Command("/bin/sh"), tb.Args("-c", "echo hello"),
),
tb.Step("exit", "busybox",
tb.Command("/bin/sh"), tb.Args("-c", "exit 1"),
),
tb.Step("world", "busybox",
tb.Command("/bin/sh"), tb.Args("-c", "sleep 30s"),
),
))

taskRunStatusSteps := []v1alpha1.StepState{{
ContainerState: corev1.ContainerState{
Terminated: &corev1.ContainerStateTerminated{
ExitCode: 0,
Reason: "Completed",
},
},
Name: "world",

}, {
ContainerState: corev1.ContainerState{
Terminated: &corev1.ContainerStateTerminated{
ExitCode: 1,
Reason: "Error",
},
},
Name: "exit",
}, {
ContainerState: corev1.ContainerState{
Terminated: &corev1.ContainerStateTerminated{
ExitCode: 0,
Reason: "Completed",
},
},
Name: "hello",

}, {

ContainerState: corev1.ContainerState{
Terminated: &corev1.ContainerStateTerminated{
ExitCode: 0,
Reason: "Completed",
},
},
Name: "nop",
}}

SortTaskRunStepOrder(taskRunStatusSteps, task.Spec.Steps)
actualStepOrder := []string{}
for _, state := range(taskRunStatusSteps) {
actualStepOrder = append(actualStepOrder, state.Name)
}

expectedStepOrder := []string{"hello", "exit", "world", "nop"}

if d := cmp.Diff(actualStepOrder, expectedStepOrder); d != "" {
t.Errorf("The status steps in TaksRun doesn't match the spec steps in Task, diff: %s", d)
}
}
6 changes: 3 additions & 3 deletions test/taskrun_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func TestTaskRunFailure(t *testing.T) {
Reason: "Completed",
},
},
Name: "nop",
Name: "hello",
}, {
ContainerState: corev1.ContainerState{
Terminated: &corev1.ContainerStateTerminated{
Expand All @@ -94,15 +94,15 @@ func TestTaskRunFailure(t *testing.T) {
Reason: "Completed",
},
},
Name: "hello",
Name: "world",
}, {
ContainerState: corev1.ContainerState{
Terminated: &corev1.ContainerStateTerminated{
ExitCode: 0,
Reason: "Completed",
},
},
Name: "world",
Name: "nop",
}}
ignoreFields := cmpopts.IgnoreFields(corev1.ContainerStateTerminated{}, "StartedAt", "FinishedAt", "ContainerID")
if d := cmp.Diff(taskrun.Status.Steps, expectedStepState, ignoreFields); d != "" {
Expand Down

0 comments on commit 8552f8b

Please sign in to comment.