diff --git a/docs/developers/README.md b/docs/developers/README.md index 435537ac837..4d444241cac 100644 --- a/docs/developers/README.md +++ b/docs/developers/README.md @@ -229,3 +229,49 @@ The result is added to a file name with the specified result's name into the `/t task run status. Internally the results are a new argument `-results`to the entrypoint defined for the task. A user can defined more than one result for a single task. + +For this task definition, + +```yaml +apiVersion: tekton.dev/v1alpha1 +kind: Task +metadata: + name: print-date + annotations: + description: | + A simple task that prints the date to make sure your cluster / Tekton is working properly. +spec: + results: + - name: current-date-unix-timestamp + description: The current date in unix timestamp format + - name: current-date-human-readable + description: The current date in humand readable format + steps: + - name: print-date-unix-timestamp + image: bash:latest + script: | + #!/usr/bin/env bash + date +%s | tee /tekton/results/current-date-unix-timestamp + - name: print-date-humman-readable + image: bash:latest + script: | + #!/usr/bin/env bash + date | tee /tekton/results/current-date-human-readable +``` + +you end up with this task run status: + +```yaml +apiVersion: tekton.dev/v1alpha1 +kind: TaskRun +... +status: +... + taskResults: + - name: current-date-human-readable + value: | + Wed Jan 22 19:47:26 UTC 2020 + - name: current-date-unix-timestamp + value: | + 1579722445 +``` diff --git a/docs/taskruns.md b/docs/taskruns.md index 2aa3d07f06b..3e00f818022 100644 --- a/docs/taskruns.md +++ b/docs/taskruns.md @@ -21,6 +21,7 @@ A `TaskRun` runs until all `steps` have completed or until a failure occurs. - [Workspaces](#workspaces) - [Status](#status) - [Steps](#steps) + - [Task Results] (#results) - [Cancelling a TaskRun](#cancelling-a-taskrun) - [Examples](#examples) - [Sidecars](#sidecars) @@ -229,10 +230,10 @@ at runtime you need to map the `workspaces` to actual physical volumes with `workspaces`. Values in `workspaces` are [`Volumes`](https://kubernetes.io/docs/tasks/configure-pod-container/configure-volume-storage/), however currently we only support a subset of `VolumeSources`: -* [`emptyDir`](https://kubernetes.io/docs/concepts/storage/volumes/#emptydir) -* [`persistentVolumeClaim`](https://kubernetes.io/docs/concepts/storage/volumes/#persistentvolumeclaim) -* [`configMap`](https://kubernetes.io/docs/concepts/storage/volumes/#configmap) -* [`secret`](https://kubernetes.io/docs/concepts/storage/volumes/#secret) +- [`emptyDir`](https://kubernetes.io/docs/concepts/storage/volumes/#emptydir) +- [`persistentVolumeClaim`](https://kubernetes.io/docs/concepts/storage/volumes/#persistentvolumeclaim) +- [`configMap`](https://kubernetes.io/docs/concepts/storage/volumes/#configmap) +- [`secret`](https://kubernetes.io/docs/concepts/storage/volumes/#secret) _If you need support for a `VolumeSource` not listed here [please open an issue](https://github.com/tektoncd/pipeline/issues) or feel free to @@ -343,6 +344,26 @@ If multiple `steps` are defined in the `Task` invoked by the `TaskRun`, we will `spec.steps` of the `Task`, when the `TaskRun` is accessed by the `get` command, e.g. `kubectl get taskrun -o yaml`. Replace \ with the name of the `TaskRun`. +### Results + +If one or more `results` are defined in the `Task` invoked by the `TaskRun`, we will get an new entry +`Task Results` added to the status. +Here is an example: + +```yaml +Status: + # […] + Steps: + # […] + Task Results: + Name: current-date-human-readable + Value: Thu Jan 23 16:29:06 UTC 2020 + + Name: current-date-unix-timestamp + Value: 1579796946 + +``` + ## Cancelling a TaskRun In order to cancel a running task (`TaskRun`), you need to update its spec to @@ -667,7 +688,7 @@ Note: There are some known issues with the existing implementation of sidecars: - The configured "nop" image must not provide the command that the sidecar is expected to run. If it does provide the command then it will not exit. This will result in the sidecar running forever and the Task -eventually timing out. https://github.com/tektoncd/pipeline/issues/1347 +eventually timing out. is the issue where this bug is being tracked. - `kubectl get pods` will show a TaskRun's Pod as "Completed" if a sidecar diff --git a/docs/tasks.md b/docs/tasks.md index 9fcb65e2380..3f5f0c643e3 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -25,6 +25,7 @@ entire Kubernetes cluster. - [Volumes](#volumes) - [Workspaces](#workspaces) - [Step Template](#step-template) + - [Results] (#results) - [Variable Substitution](#variable-substitution) - [Examples](#examples) - [Debugging Tips](#debugging) @@ -75,6 +76,7 @@ following fields: [`PipelineResources`](resources.md) needed by your `Task` - [`outputs`](#outputs) - Specifies [`PipelineResources`](resources.md) created by your `Task` + - [`results`](#results) - Specifies the result file name where the task can write its result - [`volumes`](#volumes) - Specifies one or more volumes that you want to make available to your `Task`'s steps. - [`workspaces`](#workspaces) - Specifies paths at which you expect volumes to @@ -164,6 +166,7 @@ line will use the following default preamble: #!/bin/sh set -xe ``` + Users can override this by starting their script with a shebang to declare what tool should be used to interpret the script. That tool must then also be available within the step's container. @@ -296,7 +299,6 @@ spec: Use input [`PipelineResources`](resources.md) field to provide your `Task` with data or context that is needed by your `Task`. See the [using resources docs](./resources.md#using-resources). - ### Outputs `Task` definitions can include inputs and outputs @@ -356,6 +358,41 @@ steps: args: ['-c', 'cd /workspace/tar-scratch-space/ && tar -cvf /workspace/customworkspace/rules_docker-master.tar rules_docker-master'] ``` +### Results + +Specifies one or more result files in which you want the task's [`steps`](#steps) to write a result. All result files are written +into the `/tekton/results` folder. This folder is created automatically if the task defines one or more results. + +For example, this task: + +```yaml +apiVersion: tekton.dev/v1alpha1 +kind: Task +metadata: + name: print-date + annotations: + description: | + A simple task that prints the date +spec: + results: + - name: current-date-unix-timestamp + description: The current date in unix timestamp format + - name: current-date-human-readable + description: The current date in humand readable format + steps: + - name: print-date-unix-timestamp + image: bash:latest + script: | + #!/usr/bin/env bash + date +%s | tee /tekton/results/current-date-unix-timestamp + - name: print-date-humman-readable + image: bash:latest + script: | + #!/usr/bin/env bash + date | tee /tekton/results/current-date-human-readable +``` + +defines two results `current-date-unix-timestamp` and `current-date-human-readable`. To define a result, you specify a `name` that will correspond to the file name in the `/tekton/results` folder and a `description` where you can explain the purpose of the result. ### Volumes @@ -502,17 +539,17 @@ is configurable using a flag of the Tekton controller. If the configured "nop" image contains the command that the sidecar was running before the sidecar was stopped then the sidecar will actually keep running, causing the TaskRun's Pod to remain running, and eventually causing the TaskRun to timeout rather -then exit successfully. Issue https://github.com/tektoncd/pipeline/issues/1347 +then exit successfully. Issue has been created to track this bug. ### Variable Substitution `Tasks` support string replacement using values from: -* [Inputs and Outputs](#input-and-output-substitution) - * [Array params](#variable-substitution-with-parameters-of-type-array) -* [`workspaces`](#variable-substitution-with-workspaces) -* [`volumes`](#variable-substitution-with-volumes) +- [Inputs and Outputs](#input-and-output-substitution) +- [Array params](#variable-substitution-with-parameters-of-type-array) +- [`workspaces`](#variable-substitution-with-workspaces) +- [`volumes`](#variable-substitution-with-volumes) #### Input and Output substitution @@ -533,7 +570,8 @@ Param values from resources can also be accessed using [variable substitution](. Referenced parameters of type `array` will expand to insert the array elements in the reference string's spot. So, with the following parameter: -``` + +```yaml inputs: params: - name: array-param @@ -542,30 +580,33 @@ inputs: - "array" - "elements" ``` + then `command: ["first", "$(inputs.params.array-param)", "last"]` will become `command: ["first", "some", "array", "elements", "last"]` - Note that array parameters __*must*__ be referenced in a completely isolated string within a larger string array. Any other attempt to reference an array is invalid and will throw an error. For instance, if `build-args` is a declared parameter of type `array`, then this is an invalid step because the string isn't isolated: -``` + +```yaml - name: build-step image: gcr.io/cloud-builders/some-image args: ["build", "additionalArg $(inputs.params.build-args)"] ``` Similarly, referencing `build-args` in a non-array field is also invalid: -``` + +```yaml - name: build-step image: "$(inputs.params.build-args)" args: ["build", "args"] ``` A valid reference to the `build-args` parameter is isolated and in an eligible field (`args`, in this case): -``` + +```yaml - name: build-step image: gcr.io/cloud-builders/some-image args: ["build", "$(inputs.params.build-args)", "additonalArg"] @@ -575,14 +616,14 @@ A valid reference to the `build-args` parameter is isolated and in an eligible f Paths to a `Task's` declared [workspaces](#workspaces) can be substituted with: -``` +```yaml $(workspaces.myworkspace.path) ``` Since the name of the `Volume` is not known until runtime and is randomized, you can also substitute the volume name with: -``` +```yaml $(workspaces.myworkspace.volume) ``` diff --git a/examples/taskruns/task-result.yaml b/examples/taskruns/task-result.yaml new file mode 100644 index 00000000000..840167a625c --- /dev/null +++ b/examples/taskruns/task-result.yaml @@ -0,0 +1,24 @@ +apiVersion: tekton.dev/v1alpha1 +kind: Task +metadata: + name: print-date + annotations: + description: | + A simple task that prints the date. +spec: + results: + - name: current-date-unix-timestamp + description: The current date in unix timestamp format + - name: current-date-human-readable + description: The current date in humand readable format + steps: + - name: print-date-unix-timestamp + image: bash:latest + script: | + #!/usr/bin/env bash + date +%s | tee /tekton/results/current-date-unix-timestamp + - name: print-date-humman-readable + image: bash:latest + script: | + #!/usr/bin/env bash + date | tee /tekton/results/current-date-human-readable \ No newline at end of file diff --git a/pkg/apis/pipeline/v1alpha1/task_types.go b/pkg/apis/pipeline/v1alpha1/task_types.go index fc5566c1927..a24d42b6313 100644 --- a/pkg/apis/pipeline/v1alpha1/task_types.go +++ b/pkg/apis/pipeline/v1alpha1/task_types.go @@ -26,6 +26,10 @@ import ( const ( // TaskRunResultType default task run result value TaskRunResultType ResultType = "TaskRunResult" + // PipelineResourceResultType default pipeline result value + PipelineResourceResultType ResultType = "PipelineResourceResult" + // UnknownResultType default unknown result type value + UnknownResultType ResultType = "" ) func (t *Task) TaskSpec() TaskSpec { diff --git a/pkg/apis/pipeline/v1alpha1/taskrun_types.go b/pkg/apis/pipeline/v1alpha1/taskrun_types.go index f243d07909c..e4f48b0d9f4 100644 --- a/pkg/apis/pipeline/v1alpha1/taskrun_types.go +++ b/pkg/apis/pipeline/v1alpha1/taskrun_types.go @@ -136,11 +136,24 @@ type TaskRunStatusFields struct { // optional ResourcesResult []PipelineResourceResult `json:"resourcesResult,omitempty"` + // TaskRunResult from task runs + // optional + TaskRunResults []TaskRunResult `json:"taskResults,omitempty"` + // The list has one entry per sidecar in the manifest. Each entry is // represents the imageid of the corresponding sidecar. Sidecars []SidecarState `json:"sidecars,omitempty"` } +// TaskRunResult used to describe the results of a task +type TaskRunResult struct { + // Name the given name + Name string `json:"name"` + + // Value the given value of the result + Value string `json:"value"` +} + // GetCondition returns the Condition matching the given type. func (tr *TaskRunStatus) GetCondition(t apis.ConditionType) *apis.Condition { return taskRunCondSet.Manage(tr).GetCondition(t) diff --git a/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go index 3a040ce8901..868f09f9d0f 100644 --- a/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go @@ -1310,6 +1310,22 @@ func (in *TaskRunOutputs) DeepCopy() *TaskRunOutputs { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TaskRunResult) DeepCopyInto(out *TaskRunResult) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TaskRunResult. +func (in *TaskRunResult) DeepCopy() *TaskRunResult { + if in == nil { + return nil + } + out := new(TaskRunResult) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TaskRunSpec) DeepCopyInto(out *TaskRunSpec) { *out = *in @@ -1410,6 +1426,11 @@ func (in *TaskRunStatusFields) DeepCopyInto(out *TaskRunStatusFields) { *out = make([]PipelineResourceResult, len(*in)) copy(*out, *in) } + if in.TaskRunResults != nil { + in, out := &in.TaskRunResults, &out.TaskRunResults + *out = make([]TaskRunResult, len(*in)) + copy(*out, *in) + } if in.Sidecars != nil { in, out := &in.Sidecars, &out.Sidecars *out = make([]SidecarState, len(*in)) diff --git a/pkg/reconciler/taskrun/taskrun.go b/pkg/reconciler/taskrun/taskrun.go index b7c8c185e2c..9097f4fec89 100644 --- a/pkg/reconciler/taskrun/taskrun.go +++ b/pkg/reconciler/taskrun/taskrun.go @@ -409,13 +409,35 @@ func updateTaskRunResourceResult(taskRun *v1alpha1.TaskRun, podStatus corev1.Pod if err != nil { return fmt.Errorf("parsing message for container status %d: %v", idx, err) } - taskRun.Status.ResourcesResult = append(taskRun.Status.ResourcesResult, r...) + taskResults, pipelineResults := getResults(r) + taskRun.Status.TaskRunResults = append(taskRun.Status.TaskRunResults, taskResults...) + taskRun.Status.ResourcesResult = append(taskRun.Status.ResourcesResult, pipelineResults...) } } } return nil } +func getResults(results []v1alpha1.PipelineResourceResult) ([]v1alpha1.TaskRunResult, []v1alpha1.PipelineResourceResult) { + var taskResults []v1alpha1.TaskRunResult + var pipelineResourceResults []v1alpha1.PipelineResourceResult + for _, r := range results { + switch r.ResultType { + case v1alpha1.TaskRunResultType: + taskRunResult := v1alpha1.TaskRunResult{ + Name: r.Key, + Value: r.Value, + } + taskResults = append(taskResults, taskRunResult) + case v1alpha1.PipelineResourceResultType: + fallthrough + default: + pipelineResourceResults = append(pipelineResourceResults, r) + } + } + return taskResults, pipelineResourceResults +} + func (c *Reconciler) updateStatus(taskrun *v1alpha1.TaskRun) (*v1alpha1.TaskRun, error) { newtaskrun, err := c.taskRunLister.TaskRuns(taskrun.Namespace).Get(taskrun.Name) if err != nil { diff --git a/pkg/reconciler/taskrun/taskrun_test.go b/pkg/reconciler/taskrun/taskrun_test.go index a1ae1271b36..a0b012b6f91 100644 --- a/pkg/reconciler/taskrun/taskrun_test.go +++ b/pkg/reconciler/taskrun/taskrun_test.go @@ -1555,6 +1555,178 @@ func TestUpdateTaskRunResourceResult(t *testing.T) { } } +func TestUpdateTaskRunResult(t *testing.T) { + for _, c := range []struct { + desc string + podStatus corev1.PodStatus + taskRunStatus *v1alpha1.TaskRunStatus + wantResults []v1alpha1.TaskRunResult + want []v1alpha1.PipelineResourceResult + }{{ + desc: "test result with pipeline result", + podStatus: corev1.PodStatus{ + ContainerStatuses: []corev1.ContainerStatus{{ + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + Message: `[{"key":"resultName","value":"resultValue", "type": "TaskRunResult"}, {"name":"source-image","digest":"sha256:1234", "type": "PipelineResourceResult"}]`, + }, + }, + }}, + }, + wantResults: []v1alpha1.TaskRunResult{{ + Name: "resultName", + Value: "resultValue", + }}, + want: []v1alpha1.PipelineResourceResult{{ + Name: "source-image", + Digest: "sha256:1234", + ResultType: "PipelineResourceResult", + }}, + }} { + t.Run(c.desc, func(t *testing.T) { + names.TestingSeed() + tr := &v1alpha1.TaskRun{} + tr.Status.SetCondition(&apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }) + if err := updateTaskRunResourceResult(tr, c.podStatus); err != nil { + t.Errorf("updateTaskRunResourceResult: %s", err) + } + if d := cmp.Diff(c.wantResults, tr.Status.TaskRunResults); d != "" { + t.Errorf("updateTaskRunResourceResult TaskRunResults (-want, +got): %s", d) + } + if d := cmp.Diff(c.want, tr.Status.ResourcesResult); d != "" { + t.Errorf("updateTaskRunResourceResult ResourcesResult (-want, +got): %s", d) + } + }) + } +} +func TestUpdateTaskRunResult2(t *testing.T) { + for _, c := range []struct { + desc string + podStatus corev1.PodStatus + taskRunStatus *v1alpha1.TaskRunStatus + wantResults []v1alpha1.TaskRunResult + want []v1alpha1.PipelineResourceResult + }{{ + desc: "test result with pipeline result - no result type", + podStatus: corev1.PodStatus{ + ContainerStatuses: []corev1.ContainerStatus{{ + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + Message: `[{"key":"resultName","value":"resultValue", "type": "TaskRunResult"}, {"name":"source-image","digest":"sha256:1234"}]`, + }, + }, + }}, + }, + wantResults: []v1alpha1.TaskRunResult{{ + Name: "resultName", + Value: "resultValue", + }}, + want: []v1alpha1.PipelineResourceResult{{ + Name: "source-image", + Digest: "sha256:1234", + }}, + }} { + t.Run(c.desc, func(t *testing.T) { + names.TestingSeed() + tr := &v1alpha1.TaskRun{} + tr.Status.SetCondition(&apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }) + if err := updateTaskRunResourceResult(tr, c.podStatus); err != nil { + t.Errorf("updateTaskRunResourceResult: %s", err) + } + if d := cmp.Diff(c.wantResults, tr.Status.TaskRunResults); d != "" { + t.Errorf("updateTaskRunResourceResult (-want, +got): %s", d) + } + if d := cmp.Diff(c.want, tr.Status.ResourcesResult); d != "" { + t.Errorf("updateTaskRunResourceResult (-want, +got): %s", d) + } + }) + } +} +func TestUpdateTaskRunResultTwoResults(t *testing.T) { + for _, c := range []struct { + desc string + podStatus corev1.PodStatus + taskRunStatus *v1alpha1.TaskRunStatus + want []v1alpha1.TaskRunResult + }{{ + desc: "two test results", + podStatus: corev1.PodStatus{ + ContainerStatuses: []corev1.ContainerStatus{{ + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + Message: `[{"key":"resultNameOne","value":"resultValueOne", "type": "TaskRunResult"},{"key":"resultNameTwo","value":"resultValueTwo", "type": "TaskRunResult"}]`, + }, + }, + }}, + }, + want: []v1alpha1.TaskRunResult{{ + Name: "resultNameOne", + Value: "resultValueOne", + }, { + Name: "resultNameTwo", + Value: "resultValueTwo", + }}, + }} { + t.Run(c.desc, func(t *testing.T) { + names.TestingSeed() + tr := &v1alpha1.TaskRun{} + tr.Status.SetCondition(&apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }) + if err := updateTaskRunResourceResult(tr, c.podStatus); err != nil { + t.Errorf("updateTaskRunResourceResult: %s", err) + } + if d := cmp.Diff(c.want, tr.Status.TaskRunResults); d != "" { + t.Errorf("updateTaskRunResourceResult (-want, +got): %s", d) + } + }) + } +} +func TestUpdateTaskRunResultWhenTaskFailed(t *testing.T) { + for _, c := range []struct { + desc string + podStatus corev1.PodStatus + taskRunStatus *v1alpha1.TaskRunStatus + wantResults []v1alpha1.TaskRunResult + want []v1alpha1.PipelineResourceResult + }{{ + desc: "update task results when task fails", + podStatus: corev1.PodStatus{ + ContainerStatuses: []corev1.ContainerStatus{{ + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + Message: `[{"key":"resultName","value":"resultValue", "type": "TaskRunResult"}, {"name":"source-image","digest":"sha256:1234"}]`, + }, + }, + }}, + }, + taskRunStatus: &v1alpha1.TaskRunStatus{ + Status: duckv1beta1.Status{Conditions: []apis.Condition{{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + }}}, + }, + wantResults: nil, + want: nil, + }} { + t.Run(c.desc, func(t *testing.T) { + names.TestingSeed() + if d := cmp.Diff(c.want, c.taskRunStatus.ResourcesResult); d != "" { + t.Errorf("updateTaskRunResourceResult resources (-want, +got): %s", d) + } + if d := cmp.Diff(c.wantResults, c.taskRunStatus.TaskRunResults); d != "" { + t.Errorf("updateTaskRunResourceResult results (-want, +got): %s", d) + } + }) + } +} func TestUpdateTaskRunResourceResult_Errors(t *testing.T) { for _, c := range []struct { desc string