diff --git a/docs/conditions.md b/docs/conditions.md new file mode 100644 index 00000000000..036e39e5b43 --- /dev/null +++ b/docs/conditions.md @@ -0,0 +1,50 @@ +# Conditions + +This document defines `Conditions` and their capabilities. + +*NOTE*: This feature is currently a WIP being tracked in [#1137](https://github.com/tektoncd/pipeline/issues/1137) + +--- + +- [Syntax](#syntax) + - [Check](#check) +- [Examples](#examples) + +## Syntax + +To define a configuration file for a `Condition` resource, you can specify the +following fields: + +- Required: + - [`apiVersion`][kubernetes-overview] - Specifies the API version, for example + `tekton.dev/v1alpha1`. + - [`kind`][kubernetes-overview] - Specify the `Condition` resource object. + - [`metadata`][kubernetes-overview] - Specifies data to uniquely identify the + `Condition` resource object, for example a `name`. + - [`spec`][kubernetes-overview] - Specifies the configuration information for + your `Condition` resource object. In order for a `Condition` to do anything, + the spec must include: + - [`check`](#check) - Specifies a container that you want to run for evaluating the condition + +[kubernetes-overview]: + https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/#required-fields + +### Check + +The `check` field is required. You define a single check to define the body of a `Condition`. The +check must specify a container image that adheres to the [container contract](./container-contract.md). The container image +runs till completion. The container must exit successfully i.e. with an exit code 0 for the +condition evaluation to be successful. All other exit codes are considered to be a condition check +failure. + +## Examples + +For complete examples, see +[the examples folder](https://github.com/tektoncd/pipeline/tree/master/examples). + +--- + +Except as otherwise noted, the content of this page is licensed under the +[Creative Commons Attribution 4.0 License](https://creativecommons.org/licenses/by/4.0/), +and code samples are licensed under the +[Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0). \ No newline at end of file diff --git a/docs/pipelines.md b/docs/pipelines.md index a47d6aabf9d..d79464ea9a3 100644 --- a/docs/pipelines.md +++ b/docs/pipelines.md @@ -45,6 +45,8 @@ following fields: - [`retries`](#retries) - Used when the task is wanted to be executed if it fails. Could a network error or a missing dependency. It does not apply to cancellations. + - [`conditions`](#conditions) - Used when a task is to be executed only if the specified + conditons are evaluated to be true. [kubernetes-overview]: https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/#required-fields @@ -285,6 +287,28 @@ In this example, the task "build-the-image" will be executed and if the first run fails a second one would triggered. But, if that fails no more would triggered: a max of two executions. + +#### conditions + +Sometimes you will need to run tasks only when some conditions are true. The `conditions` field +allows you to list a series of references to [`Conditions`](./conditions.md) that are run before the task +is run. If all of the conditions evaluate to true, the task is run. If any of the conditions are false, +the Task is not run. Its status.ConditionSucceeded is set to False with the reason set to `ConditionCheckFailed`. +However, unlike regular task failures, condition failures do not automatically fail the entire pipeline +run -- other tasks that are not dependent on the task (via `from` or `runAfter`) are still run. + +```yaml +tasks: + - name: conditional-task + taskRef: + name: build-push + conditions: + - conditionRef: my-condition +``` + +In this example, `my-condition` refers to a [Condition](#conditions) custom resource. The `build-push` +task will only be executed if the condition evaluates to true. + ## Ordering The [Pipeline Tasks](#pipeline-tasks) in a `Pipeline` can be connected and run diff --git a/examples/pipelineruns/conditional-pipelinerun.yaml b/examples/pipelineruns/conditional-pipelinerun.yaml new file mode 100644 index 00000000000..0dbf0c27b6e --- /dev/null +++ b/examples/pipelineruns/conditional-pipelinerun.yaml @@ -0,0 +1,68 @@ +apiVersion: tekton.dev/v1alpha1 +kind: Condition +metadata: + name: always-true +spec: + check: + image: alpine + command: ["/bin/sh"] + args: ['-c', 'exit 0'] +--- +apiVersion: tekton.dev/v1alpha1 +kind: PipelineResource +metadata: + name: pipeline-git +spec: + type: git + params: + - name: revision + value: master + - name: url + value: https://github.com/tektoncd/pipeline +--- +apiVersion: tekton.dev/v1alpha1 +kind: Task +metadata: + name: list-files +spec: + inputs: + resources: + - name: workspace + type: git + steps: + - name: run-ls + image: ubuntu + command: ["/bin/bash"] + args: ['-c', 'ls -al ${inputs.resources.workspace.path}'] +--- +apiVersion: tekton.dev/v1alpha1 +kind: Pipeline +metadata: + name: list-files-pipeline +spec: + resources: + - name: source-repo + type: git + tasks: + - name: list-files-1 + taskRef: + name: list-files + conditions: + - conditionRef: "always-true" + resources: + inputs: + - name: workspace + resource: source-repo +--- +apiVersion: tekton.dev/v1alpha1 +kind: PipelineRun +metadata: + name: demo-condtional-pr +spec: + pipelineRef: + name: list-files-pipeline + serviceAccount: 'default' + resources: + - name: source-repo + resourceRef: + name: pipeline-git \ No newline at end of file diff --git a/pkg/apis/pipeline/register.go b/pkg/apis/pipeline/register.go index 45ad33835e4..012b6fec54c 100644 --- a/pkg/apis/pipeline/register.go +++ b/pkg/apis/pipeline/register.go @@ -16,12 +16,25 @@ limitations under the License. package pipeline -// GroupName is the Kubernetes resource group name for Pipeline types. const ( - GroupName = "tekton.dev" - TaskLabelKey = "/task" - TaskRunLabelKey = "/taskRun" - PipelineLabelKey = "/pipeline" - PipelineRunLabelKey = "/pipelineRun" + // GroupName is the Kubernetes resource group name for Pipeline types. + GroupName = "tekton.dev" + + // TaskLabelKey is used as the label identifier for a task + TaskLabelKey = "/task" + + // TaskRunLabelKey is used as the label identifier for a TaskRun + TaskRunLabelKey = "/taskRun" + + // PipelineLabelKey is used as the label identifier for a Pipeline + PipelineLabelKey = "/pipeline" + + // PipelineRunLabelKey is used as the label identifier for a PipelineRun + PipelineRunLabelKey = "/pipelineRun" + + // PipelineRunLabelKey is used as the label identifier for a PipelineTask PipelineTaskLabelKey = "/pipelineTask" + + // ConditionCheck is used as the label identifier for a ConditionCheck + ConditionCheckKey = "/conditionCheck" ) diff --git a/pkg/apis/pipeline/v1alpha1/condition_types.go b/pkg/apis/pipeline/v1alpha1/condition_types.go index 761e6a2f490..c4eae582326 100644 --- a/pkg/apis/pipeline/v1alpha1/condition_types.go +++ b/pkg/apis/pipeline/v1alpha1/condition_types.go @@ -98,3 +98,13 @@ func NewConditionCheck(tr *TaskRun) *ConditionCheck { cc := ConditionCheck(*tr) return &cc } + +// IsDone returns true if the ConditionCheck's status indicates that it is done. +func (cc *ConditionCheck) IsDone() bool { + return !cc.Status.GetCondition(apis.ConditionSucceeded).IsUnknown() +} + +// IsSuccessful returns true if the ConditionCheck's status indicates that it is done. +func (cc *ConditionCheck) IsSuccessful() bool { + return cc.Status.GetCondition(apis.ConditionSucceeded).IsTrue() +} diff --git a/pkg/apis/pipeline/v1alpha1/condition_types_test.go b/pkg/apis/pipeline/v1alpha1/condition_types_test.go new file mode 100644 index 00000000000..568c133bfba --- /dev/null +++ b/pkg/apis/pipeline/v1alpha1/condition_types_test.go @@ -0,0 +1,55 @@ +/* + Copyright 2019 The Tekton Authors + + 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 v1alpha1_test + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + "knative.dev/pkg/apis" + + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + tb "github.com/tektoncd/pipeline/test/builder" +) + +func TestConditionCheck_IsDone(t *testing.T) { + tr := tb.TaskRun("", "", tb.TaskRunStatus(tb.StatusCondition( + apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + }, + ))) + + cc := v1alpha1.ConditionCheck(*tr) + if !cc.IsDone() { + t.Fatal("Expected conditionCheck status to be done") + } +} + +func TestConditionCheck_IsSuccessful(t *testing.T) { + tr := tb.TaskRun("", "", tb.TaskRunStatus(tb.StatusCondition( + apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }, + ))) + + cc := v1alpha1.ConditionCheck(*tr) + if !cc.IsSuccessful() { + t.Fatal("Expected conditionCheck status to be done") + } +} diff --git a/pkg/reconciler/v1alpha1/pipelinerun/controller.go b/pkg/reconciler/v1alpha1/pipelinerun/controller.go index 96a9ffa3fc6..7911688dafa 100644 --- a/pkg/reconciler/v1alpha1/pipelinerun/controller.go +++ b/pkg/reconciler/v1alpha1/pipelinerun/controller.go @@ -22,6 +22,7 @@ import ( pipelineclient "github.com/tektoncd/pipeline/pkg/client/injection/client" clustertaskinformer "github.com/tektoncd/pipeline/pkg/client/injection/informers/pipeline/v1alpha1/clustertask" + conditioninformer "github.com/tektoncd/pipeline/pkg/client/injection/informers/pipeline/v1alpha1/condition" pipelineinformer "github.com/tektoncd/pipeline/pkg/client/injection/informers/pipeline/v1alpha1/pipeline" resourceinformer "github.com/tektoncd/pipeline/pkg/client/injection/informers/pipeline/v1alpha1/pipelineresource" pipelineruninformer "github.com/tektoncd/pipeline/pkg/client/injection/informers/pipeline/v1alpha1/pipelinerun" @@ -54,6 +55,7 @@ func NewController( pipelineRunInformer := pipelineruninformer.Get(ctx) pipelineInformer := pipelineinformer.Get(ctx) resourceInformer := resourceinformer.Get(ctx) + conditionInformer := conditioninformer.Get(ctx) timeoutHandler := reconciler.NewTimeoutHandler(ctx.Done(), logger) opt := reconciler.Options{ @@ -72,6 +74,7 @@ func NewController( clusterTaskLister: clusterTaskInformer.Lister(), taskRunLister: taskRunInformer.Lister(), resourceLister: resourceInformer.Lister(), + conditionLister: conditionInformer.Lister(), timeoutHandler: timeoutHandler, } impl := controller.NewImpl(c, c.Logger, pipelineRunControllerName) diff --git a/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun.go b/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun.go index ee0dc47ddb1..e1b618f00ef 100644 --- a/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun.go +++ b/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun.go @@ -26,7 +26,7 @@ import ( apisconfig "github.com/tektoncd/pipeline/pkg/apis/config" "github.com/tektoncd/pipeline/pkg/apis/pipeline" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" - artifacts "github.com/tektoncd/pipeline/pkg/artifacts" + "github.com/tektoncd/pipeline/pkg/artifacts" listers "github.com/tektoncd/pipeline/pkg/client/listers/pipeline/v1alpha1" "github.com/tektoncd/pipeline/pkg/reconciler" "github.com/tektoncd/pipeline/pkg/reconciler/v1alpha1/pipeline/dag" @@ -62,6 +62,9 @@ const ( // ReasonCouldntGetResource indicates that the reason for the failure status is that the // associated PipelineRun's bound PipelineResources couldn't all be retrieved ReasonCouldntGetResource = "CouldntGetResource" + // ReasonCouldntGetCondition indicates that the reason for the failure status is that the + // associated Pipeline's Conditions couldn't all be retrieved + ReasonCouldntGetCondition = "CouldntGetCondition" // ReasonFailedValidation indicates that the reason for failure status is // that pipelinerun failed runtime validation ReasonFailedValidation = "PipelineValidationFailed" @@ -93,6 +96,7 @@ type Reconciler struct { taskLister listers.TaskLister clusterTaskLister listers.ClusterTaskLister resourceLister listers.PipelineResourceLister + conditionLister listers.ConditionLister tracker tracker.Interface configStore configStore timeoutHandler *reconciler.TimeoutSet @@ -278,6 +282,9 @@ func (c *Reconciler) reconcile(ctx context.Context, pr *v1alpha1.PipelineRun) er return c.clusterTaskLister.Get(name) }, c.resourceLister.PipelineResources(pr.Namespace).Get, + func(name string) (*v1alpha1.Condition, error) { + return c.conditionLister.Conditions(pr.Namespace).Get(name) + }, p.Spec.Tasks, providedResources, ) if err != nil { @@ -299,6 +306,14 @@ func (c *Reconciler) reconcile(ctx context.Context, pr *v1alpha1.PipelineRun) er Message: fmt.Sprintf("PipelineRun %s can't be Run; it tries to bind Resources that don't exist: %s", fmt.Sprintf("%s/%s", p.Namespace, pr.Name), err), }) + case *resources.ConditionNotFoundError: + pr.Status.SetCondition(&apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + Reason: ReasonCouldntGetCondition, + Message: fmt.Sprintf("PipelineRun %s can't be Run; it contains Conditions that don't exist: %s", + fmt.Sprintf("%s/%s", p.Namespace, pr.Name), err), + }) default: pr.Status.SetCondition(&apis.Condition{ Type: apis.ConditionSucceeded, @@ -353,6 +368,7 @@ func (c *Reconciler) reconcile(ctx context.Context, pr *v1alpha1.PipelineRun) er if err != nil { c.Logger.Errorf("Error getting potential next tasks for valid pipelinerun %s: %v", pr.Name, err) } + rprts := pipelineState.GetNextTasks(candidateTasks) var as artifacts.ArtifactStorageInterface @@ -362,13 +378,23 @@ func (c *Reconciler) reconcile(ctx context.Context, pr *v1alpha1.PipelineRun) er } for _, rprt := range rprts { - if rprt != nil { - c.Logger.Infof("Creating a new TaskRun object %s", rprt.TaskRunName) - rprt.TaskRun, err = c.createTaskRun(c.Logger, rprt, pr, as.StorageBasePath(pr)) + if rprt == nil { + continue + } + if rprt.ResolvedConditionChecks == nil || rprt.ResolvedConditionChecks.IsSuccess() { + rprt.TaskRun, err = c.createTaskRun(rprt, pr, as.StorageBasePath(pr)) if err != nil { c.Recorder.Eventf(pr, corev1.EventTypeWarning, "TaskRunCreationFailed", "Failed to create TaskRun %q: %v", rprt.TaskRunName, err) return xerrors.Errorf("error creating TaskRun called %s for PipelineTask %s from PipelineRun %s: %w", rprt.TaskRunName, rprt.PipelineTask.Name, pr.Name, err) } + } else if !rprt.ResolvedConditionChecks.HasStarted() { + for _, rcc := range rprt.ResolvedConditionChecks { + rcc.ConditionCheck, err = c.makeConditionCheckContainer(rprt, rcc, pr) + if err != nil { + c.Recorder.Eventf(pr, corev1.EventTypeWarning, "ConditionCheckCreationFailed", "Failed to create TaskRun %q: %v", rcc.ConditionCheckName, err) + return xerrors.Errorf("error creating ConditionCheck container called %s for PipelineTask %s from PipelineRun %s: %w", rcc.ConditionCheckName, rprt.PipelineTask.Name, pr.Name, err) + } + } } } before := pr.Status.GetCondition(apis.ConditionSucceeded) @@ -376,29 +402,64 @@ func (c *Reconciler) reconcile(ctx context.Context, pr *v1alpha1.PipelineRun) er pr.Status.SetCondition(after) reconciler.EmitEvent(c.Recorder, before, after, pr) - updateTaskRunsStatus(pr, pipelineState) + pr.Status.TaskRuns = getTaskRunsStatus(pr, pipelineState) c.Logger.Infof("PipelineRun %s status is being set to %s", pr.Name, pr.Status.GetCondition(apis.ConditionSucceeded)) return nil } -func updateTaskRunsStatus(pr *v1alpha1.PipelineRun, pipelineState []*resources.ResolvedPipelineRunTask) { - for _, rprt := range pipelineState { +func getTaskRunsStatus(pr *v1alpha1.PipelineRun, state []*resources.ResolvedPipelineRunTask) map[string]*v1alpha1.PipelineRunTaskRunStatus { + status := make(map[string]*v1alpha1.PipelineRunTaskRunStatus) + for _, rprt := range state { + if rprt.TaskRun == nil && rprt.ResolvedConditionChecks == nil { + continue + } + + var prtrs *v1alpha1.PipelineRunTaskRunStatus if rprt.TaskRun != nil { - prtrs := pr.Status.TaskRuns[rprt.TaskRun.Name] - if prtrs == nil { - prtrs = &v1alpha1.PipelineRunTaskRunStatus{ - PipelineTaskName: rprt.PipelineTask.Name, - } - pr.Status.TaskRuns[rprt.TaskRun.Name] = prtrs + prtrs = pr.Status.TaskRuns[rprt.TaskRun.Name] + } + if prtrs == nil { + prtrs = &v1alpha1.PipelineRunTaskRunStatus{ + PipelineTaskName: rprt.PipelineTask.Name, } + } + + if rprt.TaskRun != nil { prtrs.Status = &rprt.TaskRun.Status } + + if len(rprt.ResolvedConditionChecks) > 0 { + cStatus := make(map[string]*v1alpha1.PipelineRunConditionCheckStatus) + for _, c := range rprt.ResolvedConditionChecks { + cStatus[c.ConditionCheckName] = &v1alpha1.PipelineRunConditionCheckStatus{ + ConditionName: c.Condition.Name, + } + if c.ConditionCheck != nil { + cStatus[c.ConditionCheckName].Status = c.NewConditionCheckStatus() + } + } + prtrs.ConditionChecks = cStatus + if rprt.ResolvedConditionChecks.IsDone() && !rprt.ResolvedConditionChecks.IsSuccess() { + if prtrs.Status == nil { + prtrs.Status = &v1alpha1.TaskRunStatus{} + } + prtrs.Status.SetCondition(&apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + Reason: resources.ReasonConditionCheckFailed, + Message: fmt.Sprintf("ConditionChecks failed for Task %s in PipelineRun %s", rprt.TaskRunName, pr.Name), + }) + } + } + status[rprt.TaskRunName] = prtrs } + return status } func (c *Reconciler) updateTaskRunsStatusDirectly(pr *v1alpha1.PipelineRun) error { for taskRunName := range pr.Status.TaskRuns { + // TODO(dibyom): Add conditionCheck statuses here prtrs := pr.Status.TaskRuns[taskRunName] tr, err := c.taskRunLister.TaskRuns(pr.Namespace).Get(taskRunName) if err != nil { @@ -410,13 +471,11 @@ func (c *Reconciler) updateTaskRunsStatusDirectly(pr *v1alpha1.PipelineRun) erro prtrs.Status = &tr.Status } } - return nil } -func (c *Reconciler) createTaskRun(logger *zap.SugaredLogger, rprt *resources.ResolvedPipelineRunTask, pr *v1alpha1.PipelineRun, storageBasePath string) (*v1alpha1.TaskRun, error) { +func (c *Reconciler) createTaskRun(rprt *resources.ResolvedPipelineRunTask, pr *v1alpha1.PipelineRun, storageBasePath string) (*v1alpha1.TaskRun, error) { tr, _ := c.taskRunLister.TaskRuns(pr.Namespace).Get(rprt.TaskRunName) - if tr != nil { //is a retry addRetryHistory(tr) @@ -451,7 +510,7 @@ func (c *Reconciler) createTaskRun(logger *zap.SugaredLogger, rprt *resources.Re }} resources.WrapSteps(&tr.Spec, rprt.PipelineTask, rprt.ResolvedTaskResources.Inputs, rprt.ResolvedTaskResources.Outputs, storageBasePath) - + c.Logger.Infof("Creating a new TaskRun object %s", rprt.TaskRunName) return c.PipelineClientSet.TektonV1alpha1().TaskRuns(pr.Namespace).Create(tr) } @@ -558,3 +617,29 @@ func (c *Reconciler) updateLabelsAndAnnotations(pr *v1alpha1.PipelineRun) (*v1al } return newPr, nil } + +func (c *Reconciler) makeConditionCheckContainer(rprt *resources.ResolvedPipelineRunTask, rcc *resources.ResolvedConditionCheck, pr *v1alpha1.PipelineRun) (*v1alpha1.ConditionCheck, error) { + labels := getTaskrunLabels(pr, rprt.PipelineTask.Name) + labels[pipeline.GroupName+pipeline.ConditionCheckKey] = rcc.ConditionCheckName + + tr := &v1alpha1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: rcc.ConditionCheckName, + Namespace: pr.Namespace, + OwnerReferences: pr.GetOwnerReference(), + Labels: labels, + Annotations: getTaskrunAnnotations(pr), // Propagate annotations from PipelineRun to TaskRun. + }, + Spec: v1alpha1.TaskRunSpec{ + TaskSpec: rcc.ConditionToTaskSpec(), + ServiceAccount: getServiceAccount(pr, rprt.PipelineTask.Name), + Timeout: getTaskRunTimeout(pr), + NodeSelector: pr.Spec.NodeSelector, + Tolerations: pr.Spec.Tolerations, + Affinity: pr.Spec.Affinity, + }} + + cctr, err := c.PipelineClientSet.TektonV1alpha1().TaskRuns(pr.Namespace).Create(tr) + cc := v1alpha1.ConditionCheck(*cctr) + return &cc, err +} diff --git a/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun_test.go b/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun_test.go index d7801e997e7..cc3e6fae227 100644 --- a/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun_test.go +++ b/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun_test.go @@ -22,6 +22,14 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ktesting "k8s.io/client-go/testing" + "knative.dev/pkg/apis" + duckv1beta1 "knative.dev/pkg/apis/duck/v1beta1" + "knative.dev/pkg/configmap" + rtesting "knative.dev/pkg/reconciler/testing" + "github.com/tektoncd/pipeline/pkg/apis/pipeline" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" "github.com/tektoncd/pipeline/pkg/reconciler/v1alpha1/pipelinerun/resources" @@ -30,13 +38,10 @@ import ( "github.com/tektoncd/pipeline/test" tb "github.com/tektoncd/pipeline/test/builder" "github.com/tektoncd/pipeline/test/names" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - ktesting "k8s.io/client-go/testing" - "knative.dev/pkg/apis" - duckv1beta1 "knative.dev/pkg/apis/duck/v1beta1" - "knative.dev/pkg/configmap" - rtesting "knative.dev/pkg/reconciler/testing" +) + +var ( + ignoreLastTransitionTime = cmpopts.IgnoreTypes(apis.Condition{}.LastTransitionTime.Inner.Time) ) func getRunName(pr *v1alpha1.PipelineRun) string { @@ -59,6 +64,12 @@ func getPipelineRunController(t *testing.T, d test.Data) (test.TestAssets, func( }, cancel } +// conditionCheckFromTaskRun converts takes a pointer to a TaskRun and wraps it into a ConditionCheck +func conditionCheckFromTaskRun(tr *v1alpha1.TaskRun) *v1alpha1.ConditionCheck { + cc := v1alpha1.ConditionCheck(*tr) + return &cc +} + func TestReconcile(t *testing.T) { names.TestingSeed() @@ -264,6 +275,7 @@ func TestReconcile_InvalidPipelineRuns(t *testing.T) { tb.Pipeline("a-pipeline-with-array-params", "foo", tb.PipelineSpec( tb.PipelineParamSpec("some-param", v1alpha1.ParamTypeArray), tb.PipelineTask("some-task", "a-task-that-needs-array-params"))), + tb.Pipeline("a-pipeline-with-missing-conditions", "foo", tb.PipelineSpec(tb.PipelineTask("some-task", "a-task-that-exists", tb.PipelineTaskCondition("condition-does-not-exist")))), } prs := []*v1alpha1.PipelineRun{ tb.PipelineRun("invalid-pipeline", "foo", tb.PipelineRunSpec("pipeline-not-exist")), @@ -274,6 +286,7 @@ func TestReconcile_InvalidPipelineRuns(t *testing.T) { tb.PipelineRunResourceBinding("a-resource", tb.PipelineResourceBindingRef("missing-resource")))), tb.PipelineRun("pipeline-resources-not-declared", "foo", tb.PipelineRunSpec("a-pipeline-that-should-be-caught-by-admission-control")), tb.PipelineRun("pipeline-mismatching-param-type", "foo", tb.PipelineRunSpec("a-pipeline-with-array-params", tb.PipelineRunParam("some-param", "stringval"))), + tb.PipelineRun("pipeline-conditions-missing", "foo", tb.PipelineRunSpec("a-pipeline-with-missing-conditions")), } d := test.Data{ Tasks: ts, @@ -313,6 +326,10 @@ func TestReconcile_InvalidPipelineRuns(t *testing.T) { name: "invalid-pipeline-mismatching-parameter-types", pipelineRun: prs[6], reason: ReasonParameterTypeMismatch, + }, { + name: "invalid-pipeline-missing-conditions-shd-stop-reconciling", + pipelineRun: prs[7], + reason: ReasonCouldntGetCondition, }, } @@ -422,20 +439,154 @@ func TestUpdateTaskRunsState(t *testing.T) { }, }} pr.Status.InitializeConditions() - updateTaskRunsStatus(pr, state) - if d := cmp.Diff(pr.Status.TaskRuns, expectedPipelineRunStatus.TaskRuns); d != "" { + status := getTaskRunsStatus(pr, state) + if d := cmp.Diff(status, expectedPipelineRunStatus.TaskRuns); d != "" { t.Fatalf("Expected PipelineRun status to match TaskRun(s) status, but got a mismatch: %s", d) } } +func TestUpdateTaskRunStateWithConditionChecks(t *testing.T) { + taskrunName := "task-run" + successConditionCheckName := "success-condition" + failingConditionCheckName := "fail-condition" + + successCondition := v1alpha1.Condition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cond-1", + }, + } + failingCondition := v1alpha1.Condition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cond-2", + }, + } + + pipelineTask := v1alpha1.PipelineTask{ + TaskRef: v1alpha1.TaskRef{Name: "unit-test-task"}, + Conditions: []v1alpha1.PipelineTaskCondition{{ + ConditionRef: successCondition.Name, + }, { + ConditionRef: failingCondition.Name, + }}, + } + + successConditionCheck := conditionCheckFromTaskRun(tb.TaskRun(successConditionCheckName, "foo", tb.TaskRunStatus( + tb.StatusCondition(apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }), + tb.StepState(tb.StateTerminated(0)), + ))) + failingConditionCheck := conditionCheckFromTaskRun(tb.TaskRun(failingConditionCheckName, "foo", tb.TaskRunStatus( + tb.StatusCondition(apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + }), + tb.StepState(tb.StateTerminated(127)), + ))) + + successConditionCheckStatus := &v1alpha1.PipelineRunConditionCheckStatus{ + ConditionName: successCondition.Name, + Status: &v1alpha1.ConditionCheckStatus{ + Check: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ExitCode: 0}, + }, + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{{Type: apis.ConditionSucceeded, Status: corev1.ConditionTrue}}, + }, + }, + } + failingConditionCheckStatus := &v1alpha1.PipelineRunConditionCheckStatus{ + ConditionName: failingCondition.Name, + Status: &v1alpha1.ConditionCheckStatus{ + Check: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ExitCode: 127}, + }, + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{{Type: apis.ConditionSucceeded, Status: corev1.ConditionFalse}}, + }, + }, + } + + successrcc := resources.ResolvedConditionCheck{ + ConditionCheckName: successConditionCheckName, + Condition: &successCondition, + ConditionCheck: successConditionCheck, + } + failingrcc := resources.ResolvedConditionCheck{ + ConditionCheckName: failingConditionCheckName, + Condition: &failingCondition, + ConditionCheck: failingConditionCheck, + } + + failedTaskRunStatus := v1alpha1.TaskRunStatus{ + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + Reason: resources.ReasonConditionCheckFailed, + Message: fmt.Sprintf("ConditionChecks failed for Task %s in PipelineRun %s", taskrunName, "test-pipeline-run"), + }}, + }, + } + + tcs := []struct { + name string + rcc resources.TaskConditionCheckState + expectedStatus v1alpha1.PipelineRunTaskRunStatus + }{{ + name: "success-condition-checks", + rcc: resources.TaskConditionCheckState{&successrcc}, + expectedStatus: v1alpha1.PipelineRunTaskRunStatus{ + ConditionChecks: map[string]*v1alpha1.PipelineRunConditionCheckStatus{ + successrcc.ConditionCheck.Name: successConditionCheckStatus, + }, + }, + }, { + name: "failing-condition-checks", + rcc: resources.TaskConditionCheckState{&failingrcc}, + expectedStatus: v1alpha1.PipelineRunTaskRunStatus{ + Status: &failedTaskRunStatus, + ConditionChecks: map[string]*v1alpha1.PipelineRunConditionCheckStatus{ + failingrcc.ConditionCheck.Name: failingConditionCheckStatus, + }, + }, + }, { + name: "multiple-condition-checks", + rcc: resources.TaskConditionCheckState{&successrcc, &failingrcc}, + expectedStatus: v1alpha1.PipelineRunTaskRunStatus{ + Status: &failedTaskRunStatus, + ConditionChecks: map[string]*v1alpha1.PipelineRunConditionCheckStatus{ + successrcc.ConditionCheck.Name: successConditionCheckStatus, + failingrcc.ConditionCheck.Name: failingConditionCheckStatus, + }, + }, + }} + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + pr := tb.PipelineRun("test-pipeline-run", "foo", tb.PipelineRunSpec("test-pipeline")) + + state := []*resources.ResolvedPipelineRunTask{{ + PipelineTask: &pipelineTask, + TaskRunName: taskrunName, + ResolvedConditionChecks: tc.rcc, + }} + pr.Status.InitializeConditions() + status := getTaskRunsStatus(pr, state) + expected := map[string]*v1alpha1.PipelineRunTaskRunStatus{ + taskrunName: &tc.expectedStatus, + } + if d := cmp.Diff(status, expected, ignoreLastTransitionTime); d != "" { + t.Fatalf("Did not get expected status for %s : %s", tc.name, d) + } + }) + } +} + func TestReconcileOnCompletedPipelineRun(t *testing.T) { - prtrs := make(map[string]*v1alpha1.PipelineRunTaskRunStatus) taskRunName := "test-pipeline-run-completed-hello-world" - prtrs[taskRunName] = &v1alpha1.PipelineRunTaskRunStatus{ - PipelineTaskName: "hello-world-1", - Status: &v1alpha1.TaskRunStatus{}, - } prs := []*v1alpha1.PipelineRun{tb.PipelineRun("test-pipeline-run-completed", "foo", tb.PipelineRunSpec("test-pipeline", tb.PipelineRunServiceAccount("test-sa")), tb.PipelineRunStatus(tb.PipelineRunStatusCondition(apis.Condition{ @@ -444,7 +595,10 @@ func TestReconcileOnCompletedPipelineRun(t *testing.T) { Reason: resources.ReasonSucceeded, Message: "All Tasks have completed executing", }), - tb.PipelineRunTaskRunsStatus(prtrs), + tb.PipelineRunTaskRunsStatus(taskRunName, &v1alpha1.PipelineRunTaskRunStatus{ + PipelineTaskName: "hello-world-1", + Status: &v1alpha1.TaskRunStatus{}, + }), ), )} ps := []*v1alpha1.Pipeline{tb.Pipeline("test-pipeline", "foo", tb.PipelineSpec( @@ -510,7 +664,7 @@ func TestReconcileOnCompletedPipelineRun(t *testing.T) { expectedTaskRunsStatus := make(map[string]*v1alpha1.PipelineRunTaskRunStatus) expectedTaskRunsStatus[taskRunName] = &v1alpha1.PipelineRunTaskRunStatus{ - PipelineTaskName: prtrs[taskRunName].PipelineTaskName, + PipelineTaskName: "hello-world-1", Status: &v1alpha1.TaskRunStatus{ Status: duckv1beta1.Status{ Conditions: []apis.Condition{{Type: apis.ConditionSucceeded}}, @@ -1062,3 +1216,224 @@ func TestGetTaskRunTimeout(t *testing.T) { }) } } + +func TestReconcileWithConditionChecks(t *testing.T) { + names.TestingSeed() + prName := "test-pipeline-run" + conditions := []*v1alpha1.Condition{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cond-1", + Namespace: "foo", + }, + Spec: v1alpha1.ConditionSpec{ + Check: corev1.Container{ + Image: "foo", + Args: []string{"bar"}, + }, + }, + }, { + ObjectMeta: metav1.ObjectMeta{ + Name: "cond-2", + Namespace: "foo", + }, + Spec: v1alpha1.ConditionSpec{ + Check: corev1.Container{ + Image: "foo", + Args: []string{"bar"}, + }, + }, + }} + ps := []*v1alpha1.Pipeline{tb.Pipeline("test-pipeline", "foo", tb.PipelineSpec( + tb.PipelineTask("hello-world-1", "hello-world", + tb.PipelineTaskCondition("cond-1"), + tb.PipelineTaskCondition("cond-2")), + ))} + prs := []*v1alpha1.PipelineRun{tb.PipelineRun(prName, "foo", + tb.PipelineRunAnnotation("PipelineRunAnnotation", "PipelineRunValue"), + tb.PipelineRunSpec("test-pipeline", + tb.PipelineRunServiceAccount("test-sa"), + ), + )} + ts := []*v1alpha1.Task{tb.Task("hello-world", "foo")} + + d := test.Data{ + PipelineRuns: prs, + Pipelines: ps, + Tasks: ts, + Conditions: conditions, + } + + testAssets, cancel := getPipelineRunController(t, d) + defer cancel() + c := testAssets.Controller + clients := testAssets.Clients + + err := c.Reconciler.Reconcile(context.Background(), "foo/"+prName) + if err != nil { + t.Errorf("Did not expect to see error when reconciling completed PipelineRun but saw %s", err) + } + + // Check that the PipelineRun was reconciled correctly + _, err = clients.Pipeline.Tekton().PipelineRuns("foo").Get(prName, metav1.GetOptions{}) + if err != nil { + t.Fatalf("Somehow had error getting completed reconciled run out of fake client: %s", err) + } + ccNameBase := prName + "-hello-world-1-9l9zj" + expectedConditionChecks := []*v1alpha1.TaskRun{ + makeExpectedTr("cond-1", ccNameBase+"-cond-1-mz4c7"), + makeExpectedTr("cond-2", ccNameBase+"-cond-2-mssqb"), + } + + // Check that the expected TaskRun was created + condCheck0 := clients.Pipeline.Actions()[0].(ktesting.CreateAction).GetObject().(*v1alpha1.TaskRun) + condCheck1 := clients.Pipeline.Actions()[1].(ktesting.CreateAction).GetObject().(*v1alpha1.TaskRun) + if condCheck0 == nil || condCheck1 == nil { + t.Errorf("Expected two ConditionCheck TaskRuns to be created, but it wasn't.") + } + + actual := []*v1alpha1.TaskRun{condCheck0, condCheck1} + if d := cmp.Diff(actual, expectedConditionChecks); d != "" { + t.Errorf("expected to see 2 ConditionCheck TaskRuns created. Diff %s", d) + } +} + +func TestReconcileWithFailingConditionChecks(t *testing.T) { + names.TestingSeed() + conditions := []*v1alpha1.Condition{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "always-false", + Namespace: "foo", + }, + Spec: v1alpha1.ConditionSpec{ + Check: corev1.Container{ + Image: "foo", + Args: []string{"bar"}, + }, + }, + }} + pipelineRunName := "test-pipeline-run-with-conditions" + prccs := make(map[string]*v1alpha1.PipelineRunConditionCheckStatus) + + conditionCheckName := pipelineRunName + "task-2-always-false-xxxyyy" + prccs[conditionCheckName] = &v1alpha1.PipelineRunConditionCheckStatus{ + ConditionName: "always-false", + Status: &v1alpha1.ConditionCheckStatus{}, + } + ps := []*v1alpha1.Pipeline{tb.Pipeline("test-pipeline", "foo", tb.PipelineSpec( + tb.PipelineTask("task-1", "hello-world"), + tb.PipelineTask("task-2", "hello-world", tb.PipelineTaskCondition("always-false")), + tb.PipelineTask("task-3", "hello-world", tb.RunAfter("task-1")), + ))} + + prs := []*v1alpha1.PipelineRun{tb.PipelineRun("test-pipeline-run-with-conditions", "foo", + tb.PipelineRunAnnotation("PipelineRunAnnotation", "PipelineRunValue"), + tb.PipelineRunSpec("test-pipeline", + tb.PipelineRunServiceAccount("test-sa"), + ), + tb.PipelineRunStatus(tb.PipelineRunStatusCondition(apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionUnknown, + Reason: resources.ReasonRunning, + Message: "Not all Tasks in the Pipeline have finished executing", + }), tb.PipelineRunTaskRunsStatus(pipelineRunName+"task-1", &v1alpha1.PipelineRunTaskRunStatus{ + PipelineTaskName: "task-1", + Status: &v1alpha1.TaskRunStatus{}, + }), tb.PipelineRunTaskRunsStatus(pipelineRunName+"task-2", &v1alpha1.PipelineRunTaskRunStatus{ + PipelineTaskName: "task-2", + Status: &v1alpha1.TaskRunStatus{}, + ConditionChecks: prccs, + })), + )} + + ts := []*v1alpha1.Task{tb.Task("hello-world", "foo")} + trs := []*v1alpha1.TaskRun{ + tb.TaskRun(pipelineRunName+"task-1", "foo", + tb.TaskRunOwnerReference("kind", "name"), + tb.TaskRunLabel(pipeline.GroupName+pipeline.PipelineLabelKey, "test-pipeline-run-with-conditions"), + tb.TaskRunLabel(pipeline.GroupName+pipeline.PipelineRunLabelKey, "test-pipeline"), + tb.TaskRunSpec(tb.TaskRunTaskRef("hello-world")), + tb.TaskRunStatus(tb.StatusCondition(apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }), + ), + ), + tb.TaskRun(conditionCheckName, "foo", + tb.TaskRunOwnerReference("kind", "name"), + tb.TaskRunLabel(pipeline.GroupName+pipeline.PipelineLabelKey, "test-pipeline-run-with-conditions"), + tb.TaskRunLabel(pipeline.GroupName+pipeline.PipelineRunLabelKey, "test-pipeline"), + tb.TaskRunLabel(pipeline.GroupName+pipeline.ConditionCheckKey, conditionCheckName), + tb.TaskRunSpec(tb.TaskRunTaskSpec()), + tb.TaskRunStatus(tb.StatusCondition(apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + }), + ), + ), + } + d := test.Data{ + PipelineRuns: prs, + Pipelines: ps, + Tasks: ts, + Conditions: conditions, + TaskRuns: trs, + } + + testAssets, cancel := getPipelineRunController(t, d) + defer cancel() + c := testAssets.Controller + clients := testAssets.Clients + + err := c.Reconciler.Reconcile(context.Background(), "foo/test-pipeline-run-with-conditions") + if err != nil { + t.Errorf("Did not expect to see error when reconciling completed PipelineRun but saw %s", err) + } + + // Check that the PipelineRun was reconciled correctly + _, err = clients.Pipeline.Tekton().PipelineRuns("foo").Get("test-pipeline-run-with-conditions", metav1.GetOptions{}) + if err != nil { + t.Fatalf("Somehow had error getting completed reconciled run out of fake client: %s", err) + } + + // Check that the expected TaskRun was created + actual := clients.Pipeline.Actions()[0].(ktesting.CreateAction).GetObject().(*v1alpha1.TaskRun) + if actual == nil { + t.Errorf("Expected a ConditionCheck TaskRun to be created, but it wasn't.") + } + expectedTaskRun := tb.TaskRun("test-pipeline-run-with-conditions-task-3-9l9zj", "foo", + tb.TaskRunOwnerReference("PipelineRun", "test-pipeline-run-with-conditions", + tb.OwnerReferenceAPIVersion("tekton.dev/v1alpha1"), + tb.Controller, tb.BlockOwnerDeletion, + ), + tb.TaskRunLabel("tekton.dev/pipeline", "test-pipeline"), + tb.TaskRunLabel(pipeline.GroupName+pipeline.PipelineTaskLabelKey, "task-3"), + tb.TaskRunLabel("tekton.dev/pipelineRun", "test-pipeline-run-with-conditions"), + tb.TaskRunAnnotation("PipelineRunAnnotation", "PipelineRunValue"), + tb.TaskRunSpec( + tb.TaskRunTaskRef("hello-world"), + tb.TaskRunServiceAccount("test-sa"), + ), + ) + + if d := cmp.Diff(actual, expectedTaskRun); d != "" { + t.Errorf("expected to see ConditionCheck TaskRun %v created. Diff %s", expectedTaskRun, d) + } +} + +func makeExpectedTr(condName, ccName string) *v1alpha1.TaskRun { + return tb.TaskRun(ccName, "foo", + tb.TaskRunOwnerReference("PipelineRun", "test-pipeline-run", + tb.OwnerReferenceAPIVersion("tekton.dev/v1alpha1"), + tb.Controller, tb.BlockOwnerDeletion, + ), + tb.TaskRunLabel("tekton.dev/pipeline", "test-pipeline"), + tb.TaskRunLabel(pipeline.GroupName+pipeline.PipelineTaskLabelKey, "hello-world-1"), + tb.TaskRunLabel("tekton.dev/pipelineRun", "test-pipeline-run"), + tb.TaskRunLabel("tekton.dev/conditionCheck", ccName), + tb.TaskRunAnnotation("PipelineRunAnnotation", "PipelineRunValue"), + tb.TaskRunSpec( + tb.TaskRunTaskSpec(tb.Step("condition-check-"+condName, "foo", tb.Args("bar"))), + tb.TaskRunServiceAccount("test-sa"), + ), + ) +} diff --git a/pkg/reconciler/v1alpha1/pipelinerun/resources/conditionresolution.go b/pkg/reconciler/v1alpha1/pipelinerun/resources/conditionresolution.go new file mode 100644 index 00000000000..30f04992bb5 --- /dev/null +++ b/pkg/reconciler/v1alpha1/pipelinerun/resources/conditionresolution.go @@ -0,0 +1,121 @@ +/* + * + * Copyright 2019 The Tekton Authors + * + * 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 resources + +import ( + corev1 "k8s.io/api/core/v1" + + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" +) + +const ( + // unnamedCheckNamePrefix is the prefix added to the name of a condition's + // spec.Check.Image if the name is missing + unnamedCheckNamePrefix = "condition-check-" +) + +// GetCondition is a function used to retrieve PipelineConditions. +type GetCondition func(string) (*v1alpha1.Condition, error) + +// ResolvedConditionCheck contains a Condition and its associated ConditionCheck, if it +// exists. ConditionCheck can be nil to represent there being no ConditionCheck (i.e the condition +// has not been evaluated). +type ResolvedConditionCheck struct { + ConditionCheckName string + Condition *v1alpha1.Condition + ConditionCheck *v1alpha1.ConditionCheck +} + +// TaskConditionCheckState is a slice of ResolvedConditionCheck the represents the current execution +// state of Conditions for a Task in a pipeline run. +type TaskConditionCheckState []*ResolvedConditionCheck + +// HasStarted returns true if the conditionChecks for a given object have been created +func (state TaskConditionCheckState) HasStarted() bool { + hasStarted := true + for _, j := range state { + if j.ConditionCheck == nil { + hasStarted = false + } + } + return hasStarted +} + +// IsComplete returns true if the status for all conditionChecks for a task indicate that they are done +func (state TaskConditionCheckState) IsDone() bool { + if !state.HasStarted() { + return false + } + isDone := true + for _, rcc := range state { + isDone = isDone && rcc.ConditionCheck.IsDone() + } + return isDone +} + +// IsComplete returns true if the status for all conditionChecks for a task indicate they have +// completed successfully +func (state TaskConditionCheckState) IsSuccess() bool { + if !state.IsDone() { + return false + } + isSuccess := true + for _, rcc := range state { + isSuccess = isSuccess && rcc.ConditionCheck.IsSuccessful() + } + return isSuccess +} + +// ConditionToTaskSpec creates a TaskSpec from a given Condition +func (rcc *ResolvedConditionCheck) ConditionToTaskSpec() *v1alpha1.TaskSpec { + if rcc.Condition.Spec.Check.Name == "" { + rcc.Condition.Spec.Check.Name = unnamedCheckNamePrefix + rcc.Condition.Name + } + + t := &v1alpha1.TaskSpec{ + Steps: []corev1.Container{rcc.Condition.Spec.Check}, + } + + if len(rcc.Condition.Spec.Params) > 0 { + t.Inputs = &v1alpha1.Inputs{ + Params: rcc.Condition.Spec.Params, + } + } + + return t +} + +// NewConditionCheck status creates a ConditionCheckStatus from a ConditionCheck +func (rcc *ResolvedConditionCheck) NewConditionCheckStatus() *v1alpha1.ConditionCheckStatus { + var checkStep corev1.ContainerState + trs := rcc.ConditionCheck.Status + for _, s := range trs.Steps { + if s.Name == rcc.Condition.Spec.Check.Name { + checkStep = s.ContainerState + break + } + } + + return &v1alpha1.ConditionCheckStatus{ + Status: trs.Status, + PodName: trs.PodName, + StartTime: trs.StartTime, + CompletionTime: trs.CompletionTime, + Check: checkStep, + } +} diff --git a/pkg/reconciler/v1alpha1/pipelinerun/resources/conditionresolution_test.go b/pkg/reconciler/v1alpha1/pipelinerun/resources/conditionresolution_test.go new file mode 100644 index 00000000000..6d71597fe8f --- /dev/null +++ b/pkg/reconciler/v1alpha1/pipelinerun/resources/conditionresolution_test.go @@ -0,0 +1,269 @@ +/* + * + * Copyright 2019 The Tekton Authors + * + * 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 resources + +import ( + "github.com/google/go-cmp/cmp" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "knative.dev/pkg/apis" + duckv1beta1 "knative.dev/pkg/apis/duck/v1beta1" + + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + + "testing" +) + +var c = &v1alpha1.Condition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "conditionname", + }, + Spec: v1alpha1.ConditionSpec{ + Check: corev1.Container{}, + }, +} + +var notStartedState = TaskConditionCheckState{{ + ConditionCheckName: "foo", + Condition: c, +}} + +var runningState = TaskConditionCheckState{{ + ConditionCheckName: "foo", + Condition: c, + ConditionCheck: &v1alpha1.ConditionCheck{ + ObjectMeta: metav1.ObjectMeta{ + Name: "running-condition-check", + }, + }, +}} + +var successState = TaskConditionCheckState{{ + ConditionCheckName: "foo", + Condition: c, + ConditionCheck: &v1alpha1.ConditionCheck{ + ObjectMeta: metav1.ObjectMeta{ + Name: "successful-condition-check", + }, + Spec: v1alpha1.TaskRunSpec{}, + Status: v1alpha1.TaskRunStatus{ + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }}, + }, + }, + }, +}} + +var failedState = TaskConditionCheckState{{ + ConditionCheckName: "foo", + Condition: c, + ConditionCheck: &v1alpha1.ConditionCheck{ + ObjectMeta: metav1.ObjectMeta{ + Name: "failed-condition-check", + }, + Spec: v1alpha1.TaskRunSpec{}, + Status: v1alpha1.TaskRunStatus{ + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + }}, + }, + }, + }, +}} + +func TestTaskConditionCheckState_HasStarted(t *testing.T) { + tcs := []struct { + name string + state TaskConditionCheckState + want bool + }{{ + name: "no-condition-checks", + state: notStartedState, + want: false, + }, { + name: "running-condition-check", + state: runningState, + want: true, + }, { + name: "successful-condition-check", + state: successState, + want: true, + }, { + name: "failed-condition-check", + state: failedState, + want: true, + }} + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + got := tc.state.HasStarted() + if got != tc.want { + t.Errorf("Expected HasStarted to be %v but got %v for %s", tc.want, got, tc.name) + } + }) + } +} + +func TestTaskConditionCheckState_IsComplete(t *testing.T) { + tcs := []struct { + name string + state TaskConditionCheckState + want bool + }{{ + name: "no-condition-checks", + state: notStartedState, + want: false, + }, { + name: "running-condition-check", + state: runningState, + want: false, + }, { + name: "successful-condition-check", + state: successState, + want: true, + }, { + name: "failed-condition-check", + state: failedState, + want: true, + }} + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + got := tc.state.IsDone() + if got != tc.want { + t.Errorf("Expected IsComplete to be %v but got %v for %s", tc.want, got, tc.name) + } + }) + } +} + +func TestTaskConditionCheckState_IsSuccess(t *testing.T) { + tcs := []struct { + name string + state TaskConditionCheckState + want bool + }{{ + name: "no-condition-checks", + state: notStartedState, + want: false, + }, { + name: "running-condition-check", + state: runningState, + want: false, + }, { + name: "successful-condition-check", + state: successState, + want: true, + }, { + name: "failed-condition-check", + state: failedState, + want: false, + }} + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + got := tc.state.IsSuccess() + if got != tc.want { + t.Errorf("Expected IsSuccess to be %v but got %v for %s", tc.want, got, tc.name) + } + }) + } +} + +func TestResolvedConditionCheck_ConditionToTaskSpec(t *testing.T) { + tcs := []struct { + name string + cond v1alpha1.Condition + want v1alpha1.TaskSpec + }{{ + name: "user-provided-container-name", + cond: v1alpha1.Condition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + }, + Spec: v1alpha1.ConditionSpec{ + Check: corev1.Container{ + Name: "foo", + Image: "ubuntu", + }, + }, + }, + want: v1alpha1.TaskSpec{ + Steps: []corev1.Container{{ + Name: "foo", + Image: "ubuntu", + }}, + }, + }, { + name: "default-container-name", + cond: v1alpha1.Condition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + }, + Spec: v1alpha1.ConditionSpec{ + Check: corev1.Container{ + Image: "ubuntu", + }, + }, + }, + want: v1alpha1.TaskSpec{ + Steps: []corev1.Container{{ + Name: "condition-check-bar", + Image: "ubuntu", + }}, + }, + }, { + name: "with-input-params", + cond: v1alpha1.Condition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + }, + Spec: v1alpha1.ConditionSpec{ + Params: []v1alpha1.ParamSpec{{Name: "abc"}}, + Check: corev1.Container{ + Image: "ubuntu", + }, + }, + }, + want: v1alpha1.TaskSpec{ + Inputs: &v1alpha1.Inputs{ + Params: []v1alpha1.ParamSpec{{ + Name: "abc", + }}, + }, + Steps: []corev1.Container{{ + Name: "condition-check-bar", + Image: "ubuntu", + }}, + }, + }} + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + rcc := &ResolvedConditionCheck{Condition: &tc.cond} + if d := cmp.Diff(tc.want, *rcc.ConditionToTaskSpec()); d != "" { + t.Errorf("TaskSpec generated from Condition is unexpected -want, +got: %v", d) + } + }) + } +} diff --git a/pkg/reconciler/v1alpha1/pipelinerun/resources/pipelinerunresolution.go b/pkg/reconciler/v1alpha1/pipelinerun/resources/pipelinerunresolution.go index 425f6ef6c40..f84516c6adc 100644 --- a/pkg/reconciler/v1alpha1/pipelinerun/resources/pipelinerunresolution.go +++ b/pkg/reconciler/v1alpha1/pipelinerun/resources/pipelinerunresolution.go @@ -48,6 +48,10 @@ const ( // ReasonTimedOut indicates that the PipelineRun has taken longer than its configured // timeout ReasonTimedOut = "PipelineRunTimeout" + + // ReasonConditionCheckFailed indicates that the reason for the failure status is that the + // condition check associated to the pipeline task evaluated to false + ReasonConditionCheckFailed = "ConditionCheckFailed" ) // ResolvedPipelineRunTask contains a Task and its associated TaskRun, if it @@ -57,6 +61,8 @@ type ResolvedPipelineRunTask struct { TaskRun *v1alpha1.TaskRun PipelineTask *v1alpha1.PipelineTask ResolvedTaskResources *resources.ResolvedTaskResources + // ConditionChecks ~~TaskRuns but for evaling conditions + ResolvedConditionChecks TaskConditionCheckState // Could also be a TaskRun or maybe just a Pod? } // PipelineRunState is a slice of ResolvedPipelineRunTasks the represents the current execution @@ -100,7 +106,7 @@ func (state PipelineRunState) GetNextTasks(candidateTasks map[string]v1alpha1.Pi if _, ok := candidateTasks[t.PipelineTask.Name]; ok && t.TaskRun != nil { status := t.TaskRun.Status.GetCondition(apis.ConditionSucceeded) if status != nil && status.IsFalse() { - if !(t.TaskRun.IsCancelled() || status.Reason == "TaskRunCancelled") { + if !(t.TaskRun.IsCancelled() || status.Reason == v1alpha1.TaskRunSpecStatusCancelled || status.Reason == ReasonConditionCheckFailed) { if len(t.TaskRun.Status.RetriesStatus) < t.PipelineTask.Retries { tasks = append(tasks, t) } @@ -200,6 +206,15 @@ func (e *ResourceNotFoundError) Error() string { return fmt.Sprintf("Couldn't retrieve PipelineResource: %s", e.Msg) } +type ConditionNotFoundError struct { + Name string + Msg string +} + +func (e *ConditionNotFoundError) Error() string { + return fmt.Sprintf("Couldn't retrieve Condition %q: %s", e.Name, e.Msg) +} + // ResolvePipelineRun retrieves all Tasks instances which are reference by tasks, getting // instances from getTask. If it is unable to retrieve an instance of a referenced Task, it // will return an error, otherwise it returns a list of all of the Tasks retrieved. @@ -211,6 +226,7 @@ func ResolvePipelineRun( getTaskRun resources.GetTaskRun, getClusterTask resources.GetClusterTask, getResource resources.GetResource, + getCondition GetCondition, tasks []v1alpha1.PipelineTask, providedResources map[string]v1alpha1.PipelineResourceRef, ) (PipelineRunState, error) { @@ -224,7 +240,7 @@ func ResolvePipelineRun( TaskRunName: getTaskRunName(pipelineRun.Status.TaskRuns, pt.Name, pipelineRun.Name), } - // Find the Task that this task in the Pipeline this PipelineTask is using + // Find the Task that this PipelineTask is using var t v1alpha1.TaskInterface var err error if pt.TaskRef.Kind == v1alpha1.ClusterTaskKind { @@ -261,12 +277,36 @@ func ResolvePipelineRun( if taskRun != nil { rprt.TaskRun = taskRun } + + // Get all conditions that this pipelineTask will be using, if any + if len(pt.Conditions) > 0 { + rcc, err := resolveConditionChecks(&pt, pipelineRun.Status.TaskRuns, rprt.TaskRunName, getTaskRun, getCondition) + if err != nil { + return nil, err + } + rprt.ResolvedConditionChecks = rcc + } + // Add this task to the state of the PipelineRun state = append(state, &rprt) } return state, nil } +// getConditionCheckName should return a unique name for a `ConditionCheck` if one has not already been defined, and the existing one otherwise. +func getConditionCheckName(taskRunStatus map[string]*v1alpha1.PipelineRunTaskRunStatus, trName, conditionName string) string { + trStatus, ok := taskRunStatus[trName] + if ok && trStatus.ConditionChecks != nil { + for k, v := range trStatus.ConditionChecks { + // TODO(1022): Should we allow multiple conditions of the same type? + if conditionName == v.ConditionName { + return k + } + } + } + return names.SimpleNameGenerator.RestrictLengthWithRandomSuffix(fmt.Sprintf("%s-%s", trName, conditionName)) +} + // getTaskRunName should return a unique name for a `TaskRun` if one has not already been defined, and the existing one otherwise. func getTaskRunName(taskRunsStatus map[string]*v1alpha1.PipelineRunTaskRunStatus, ptName, prName string) string { for k, v := range taskRunsStatus { @@ -282,7 +322,6 @@ func getTaskRunName(taskRunsStatus map[string]*v1alpha1.PipelineRunTaskRunStatus // updated with, based on the status of the TaskRuns in state. func GetPipelineConditionStatus(prName string, state PipelineRunState, logger *zap.SugaredLogger, startTime *metav1.Time, pipelineTimeout *metav1.Duration) *apis.Condition { - allFinished := true if !startTime.IsZero() && pipelineTimeout != nil { timeout := pipelineTimeout.Duration runtime := time.Since(startTime.Time) @@ -298,10 +337,27 @@ func GetPipelineConditionStatus(prName string, state PipelineRunState, logger *z } } } + allFinished := true for _, rprt := range state { if rprt.TaskRun == nil { - logger.Infof("TaskRun %s doesn't have a Status, so PipelineRun %s isn't finished", rprt.TaskRunName, prName) - allFinished = false + + if rprt.ResolvedConditionChecks == nil { + logger.Infof("TaskRun %s doesn't have a Status, so PipelineRun %s isn't finished", rprt.TaskRunName, prName) + allFinished = false + continue + } + if !rprt.ResolvedConditionChecks.IsDone() { + logger.Infof("ConditionChecks for TaskRun %s in progress, so PipelineRun %s isn't finished", rprt.TaskRunName, prName) + allFinished = false + continue + } + if rprt.ResolvedConditionChecks.IsSuccess() { + logger.Infof("ConditionChecks for TaskRun %s successful but TaskRun doesn't have a Status, so PipelineRun %s isn't finished", rprt.TaskRunName, prName) + allFinished = false + continue + } + + logger.Info("ConditionChecks for TaskRun %s failed, so PipelineRun %s might be finished unless other TaskRuns are still running", rprt.TaskRunName, prName) continue } c := rprt.TaskRun.Status.GetCondition(apis.ConditionSucceeded) @@ -389,3 +445,33 @@ func ValidateFrom(state PipelineRunState) error { return nil } + +func resolveConditionChecks(pt *v1alpha1.PipelineTask, + taskRunStatus map[string]*v1alpha1.PipelineRunTaskRunStatus, + taskRunName string, getTaskRun resources.GetTaskRun, getCondition GetCondition) ([]*ResolvedConditionCheck, error) { + rcc := []*ResolvedConditionCheck{} + for j := range pt.Conditions { + cName := pt.Conditions[j].ConditionRef + c, err := getCondition(cName) + if err != nil { + return nil, &ConditionNotFoundError{ + Name: cName, + Msg: err.Error(), + } + } + conditionCheckName := getConditionCheckName(taskRunStatus, taskRunName, cName) + cctr, err := getTaskRun(conditionCheckName) + if err != nil { + if !errors.IsNotFound(err) { + return nil, xerrors.Errorf("error retrieving ConditionCheck %s for taskRun name %s : %w", conditionCheckName, taskRunName, err) + } + } + + rcc = append(rcc, &ResolvedConditionCheck{ + Condition: c, + ConditionCheckName: conditionCheckName, + ConditionCheck: v1alpha1.NewConditionCheck(cctr), + }) + } + return rcc, nil +} diff --git a/pkg/reconciler/v1alpha1/pipelinerun/resources/pipelinerunresolution_test.go b/pkg/reconciler/v1alpha1/pipelinerun/resources/pipelinerunresolution_test.go index 39cf107f81d..f8b6b6a87aa 100644 --- a/pkg/reconciler/v1alpha1/pipelinerun/resources/pipelinerunresolution_test.go +++ b/pkg/reconciler/v1alpha1/pipelinerun/resources/pipelinerunresolution_test.go @@ -57,6 +57,12 @@ var pts = []v1alpha1.PipelineTask{{ Name: "mytask5", TaskRef: v1alpha1.TaskRef{Name: "cancelledTask"}, Retries: 2, +}, { + Name: "mytask6", + TaskRef: v1alpha1.TaskRef{Name: "taskWithConditions"}, + Conditions: []v1alpha1.PipelineTaskCondition{{ + ConditionRef: "always-true", + }}, }} var p = &v1alpha1.Pipeline{ @@ -105,6 +111,23 @@ var trs = []v1alpha1.TaskRun{{ Spec: v1alpha1.TaskRunSpec{}, }} +var condition = v1alpha1.Condition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "always-true", + }, + Spec: v1alpha1.ConditionSpec{ + Check: corev1.Container{}, + }, +} + +var conditionChecks = []v1alpha1.TaskRun{{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "namespace", + Name: "always-true", + }, + Spec: v1alpha1.TaskRunSpec{}, +}} + func makeStarted(tr v1alpha1.TaskRun) *v1alpha1.TaskRun { newTr := newTaskRun(tr) newTr.Status.Conditions[0].Status = corev1.ConditionUnknown @@ -248,6 +271,94 @@ var allFinishedState = PipelineRunState{{ }, }} +var conditionCheckSuccessNoTaskStartedState = PipelineRunState{{ + PipelineTask: &pts[5], + TaskRunName: "pipeleinerun-conditionaltask", + TaskRun: nil, + ResolvedTaskResources: &resources.ResolvedTaskResources{ + TaskSpec: &task.Spec, + }, + ResolvedConditionChecks: TaskConditionCheckState{{ + ConditionCheckName: "myconditionCheck", + Condition: &condition, + ConditionCheck: v1alpha1.NewConditionCheck(makeSucceeded(conditionChecks[0])), + }}, +}} + +var conditionCheckStartedState = PipelineRunState{{ + PipelineTask: &pts[5], + TaskRunName: "pipeleinerun-conditionaltask", + TaskRun: nil, + ResolvedTaskResources: &resources.ResolvedTaskResources{ + TaskSpec: &task.Spec, + }, + ResolvedConditionChecks: TaskConditionCheckState{{ + ConditionCheckName: "myconditionCheck", + Condition: &condition, + ConditionCheck: v1alpha1.NewConditionCheck(makeStarted(conditionChecks[0])), + }}, +}} + +var conditionCheckFailedWithNoOtherTasksState = PipelineRunState{{ + PipelineTask: &pts[5], + TaskRunName: "pipeleinerun-conditionaltask", + TaskRun: nil, + ResolvedTaskResources: &resources.ResolvedTaskResources{ + TaskSpec: &task.Spec, + }, + ResolvedConditionChecks: TaskConditionCheckState{{ + ConditionCheckName: "myconditionCheck", + Condition: &condition, + ConditionCheck: v1alpha1.NewConditionCheck(makeFailed(conditionChecks[0])), + }}, +}} + +var conditionCheckFailedWithOthersPassedState = PipelineRunState{{ + PipelineTask: &pts[5], + TaskRunName: "pipeleinerun-conditionaltask", + TaskRun: nil, + ResolvedTaskResources: &resources.ResolvedTaskResources{ + TaskSpec: &task.Spec, + }, + ResolvedConditionChecks: TaskConditionCheckState{{ + ConditionCheckName: "myconditionCheck", + Condition: &condition, + ConditionCheck: v1alpha1.NewConditionCheck(makeFailed(conditionChecks[0])), + }}, +}, + { + PipelineTask: &pts[0], + TaskRunName: "pipelinerun-mytask1", + TaskRun: makeSucceeded(trs[0]), + ResolvedTaskResources: &resources.ResolvedTaskResources{ + TaskSpec: &task.Spec, + }, + }, +} + +var conditionCheckFailedWithOthersFailedState = PipelineRunState{{ + PipelineTask: &pts[5], + TaskRunName: "pipeleinerun-conditionaltask", + TaskRun: nil, + ResolvedTaskResources: &resources.ResolvedTaskResources{ + TaskSpec: &task.Spec, + }, + ResolvedConditionChecks: TaskConditionCheckState{{ + ConditionCheckName: "myconditionCheck", + Condition: &condition, + ConditionCheck: v1alpha1.NewConditionCheck(makeFailed(conditionChecks[0])), + }}, +}, + { + PipelineTask: &pts[0], + TaskRunName: "pipelinerun-mytask1", + TaskRun: makeFailed(trs[0]), + ResolvedTaskResources: &resources.ResolvedTaskResources{ + TaskSpec: &task.Spec, + }, + }, +} + var taskCancelled = PipelineRunState{{ PipelineTask: &pts[4], TaskRunName: "pipelinerun-mytask1", @@ -747,6 +858,50 @@ func TestGetPipelineConditionStatus(t *testing.T) { name: "one-retry-needed", state: taskRetriedState, expectedStatus: corev1.ConditionUnknown, + }, { + name: "condition-success-no-task started", + state: conditionCheckSuccessNoTaskStartedState, + expectedStatus: corev1.ConditionUnknown, + }, { + name: "condition-check-in-progress", + state: conditionCheckStartedState, + expectedStatus: corev1.ConditionUnknown, + }, { + name: "condition-failed-no-other-tasks", // 1 task pipeline with a condition that fails + state: conditionCheckFailedWithNoOtherTasksState, + expectedStatus: corev1.ConditionTrue, + }, { + name: "condition-failed-another-task-succeeded", // 1 task skipped due to condition, but others pass + state: conditionCheckFailedWithOthersPassedState, + expectedStatus: corev1.ConditionTrue, + }, { + name: "condition-failed-another-task-failed", // 1 task skipped due to condition, but others failed + state: conditionCheckFailedWithOthersFailedState, + expectedStatus: corev1.ConditionFalse, + }, { + name: "no-tasks-started", + state: noneStartedState, + expectedStatus: corev1.ConditionUnknown, + }, { + name: "one-task-started", + state: oneStartedState, + expectedStatus: corev1.ConditionUnknown, + }, { + name: "one-task-finished", + state: oneFinishedState, + expectedStatus: corev1.ConditionUnknown, + }, { + name: "one-task-failed", + state: oneFailedState, + expectedStatus: corev1.ConditionFalse, + }, { + name: "all-finished", + state: allFinishedState, + expectedStatus: corev1.ConditionTrue, + }, { + name: "one-retry-needed", + state: taskRetriedState, + expectedStatus: corev1.ConditionUnknown, }} for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { @@ -847,8 +1002,9 @@ func TestResolvePipelineRun(t *testing.T) { getTaskRun := func(name string) (*v1alpha1.TaskRun, error) { return nil, nil } getClusterTask := func(name string) (v1alpha1.TaskInterface, error) { return nil, nil } getResource := func(name string) (*v1alpha1.PipelineResource, error) { return r, nil } + getCondition := func(name string) (*v1alpha1.Condition, error) { return nil, nil } - pipelineState, err := ResolvePipelineRun(pr, getTask, getTaskRun, getClusterTask, getResource, p.Spec.Tasks, providedResources) + pipelineState, err := ResolvePipelineRun(pr, getTask, getTaskRun, getClusterTask, getResource, getCondition, p.Spec.Tasks, providedResources) if err != nil { t.Fatalf("Error getting tasks for fake pipeline %s: %s", p.ObjectMeta.Name, err) } @@ -914,12 +1070,13 @@ func TestResolvePipelineRun_PipelineTaskHasNoResources(t *testing.T) { getResource := func(name string) (*v1alpha1.PipelineResource, error) { return nil, xerrors.New("should not get called") } + getCondition := func(name string) (*v1alpha1.Condition, error) { return nil, nil } pr := v1alpha1.PipelineRun{ ObjectMeta: metav1.ObjectMeta{ Name: "pipelinerun", }, } - pipelineState, err := ResolvePipelineRun(pr, getTask, getTaskRun, getClusterTask, getResource, pts, providedResources) + pipelineState, err := ResolvePipelineRun(pr, getTask, getTaskRun, getClusterTask, getResource, getCondition, pts, providedResources) if err != nil { t.Fatalf("Did not expect error when resolving PipelineRun without Resources: %v", err) } @@ -961,12 +1118,15 @@ func TestResolvePipelineRun_TaskDoesntExist(t *testing.T) { getResource := func(name string) (*v1alpha1.PipelineResource, error) { return nil, xerrors.New("should not get called") } + getCondition := func(name string) (*v1alpha1.Condition, error) { + return nil, nil + } pr := v1alpha1.PipelineRun{ ObjectMeta: metav1.ObjectMeta{ Name: "pipelinerun", }, } - _, err := ResolvePipelineRun(pr, getTask, getTaskRun, getClusterTask, getResource, pts, providedResources) + _, err := ResolvePipelineRun(pr, getTask, getTaskRun, getClusterTask, getResource, getCondition, pts, providedResources) switch err := err.(type) { case nil: t.Fatalf("Expected error getting non-existent Tasks for Pipeline %s but got none", p.Name) @@ -1004,6 +1164,9 @@ func TestResolvePipelineRun_ResourceBindingsDontExist(t *testing.T) { getResource := func(name string) (*v1alpha1.PipelineResource, error) { return nil, xerrors.New("shouldnt be called") } + getCondition := func(name string) (*v1alpha1.Condition, error) { + return nil, nil + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1012,7 +1175,7 @@ func TestResolvePipelineRun_ResourceBindingsDontExist(t *testing.T) { Name: "pipelinerun", }, } - _, err := ResolvePipelineRun(pr, getTask, getTaskRun, getClusterTask, getResource, tt.p.Spec.Tasks, providedResources) + _, err := ResolvePipelineRun(pr, getTask, getTaskRun, getClusterTask, getResource, getCondition, tt.p.Spec.Tasks, providedResources) if err == nil { t.Fatalf("Expected error when bindings are in incorrect state for Pipeline %s but got none", p.Name) } @@ -1051,6 +1214,9 @@ func TestResolvePipelineRun_ResourcesDontExist(t *testing.T) { getResource := func(name string) (*v1alpha1.PipelineResource, error) { return nil, errors.NewNotFound(v1alpha1.Resource("pipelineresource"), name) } + getCondition := func(name string) (*v1alpha1.Condition, error) { + return nil, nil + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1059,7 +1225,7 @@ func TestResolvePipelineRun_ResourcesDontExist(t *testing.T) { Name: "pipelinerun", }, } - _, err := ResolvePipelineRun(pr, getTask, getTaskRun, getClusterTask, getResource, tt.p.Spec.Tasks, providedResources) + _, err := ResolvePipelineRun(pr, getTask, getTaskRun, getClusterTask, getResource, getCondition, tt.p.Spec.Tasks, providedResources) switch err := err.(type) { case nil: t.Fatalf("Expected error getting non-existent Resources for Pipeline %s but got none", p.Name) @@ -1291,8 +1457,8 @@ func TestResolvePipelineRun_withExistingTaskRuns(t *testing.T) { getClusterTask := func(name string) (v1alpha1.TaskInterface, error) { return nil, nil } getTaskRun := func(name string) (*v1alpha1.TaskRun, error) { return nil, nil } getResource := func(name string) (*v1alpha1.PipelineResource, error) { return r, nil } - - pipelineState, err := ResolvePipelineRun(pr, getTask, getTaskRun, getClusterTask, getResource, p.Spec.Tasks, providedResources) + getCondition := func(name string) (*v1alpha1.Condition, error) { return nil, nil } + pipelineState, err := ResolvePipelineRun(pr, getTask, getTaskRun, getClusterTask, getResource, getCondition, p.Spec.Tasks, providedResources) if err != nil { t.Fatalf("Error getting tasks for fake pipeline %s: %s", p.ObjectMeta.Name, err) } @@ -1314,3 +1480,205 @@ func TestResolvePipelineRun_withExistingTaskRuns(t *testing.T) { t.Fatalf("Expected to get current pipeline state %v, but actual differed: %s", expectedState, d) } } + +func TestResolveConditionChecks(t *testing.T) { + names.TestingSeed() + ccName := "pipelinerun-mytask1-9l9zj-always-true-mz4c7" + + cc := &v1alpha1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: ccName, + }, + Spec: v1alpha1.TaskRunSpec{}, + } + pts := []v1alpha1.PipelineTask{{ + Name: "mytask1", + TaskRef: v1alpha1.TaskRef{Name: "task"}, + Conditions: []v1alpha1.PipelineTaskCondition{{ + ConditionRef: "always-true", + }}, + }} + providedResources := map[string]v1alpha1.PipelineResourceRef{} + + getTask := func(name string) (v1alpha1.TaskInterface, error) { return task, nil } + getClusterTask := func(name string) (v1alpha1.TaskInterface, error) { return nil, xerrors.New("should not get called") } + getResource := func(name string) (*v1alpha1.PipelineResource, error) { + return nil, xerrors.New("should not get called") + } + getCondition := func(name string) (*v1alpha1.Condition, error) { return &condition, nil } + pr := v1alpha1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipelinerun", + }, + } + + tcs := []struct { + name string + getTaskRun resources.GetTaskRun + expectedConditionCheck TaskConditionCheckState + }{ + { + name: "conditionCheck exists", + getTaskRun: func(name string) (*v1alpha1.TaskRun, error) { + if name == "pipelinerun-mytask1-9l9zj-always-true-mz4c7" { + return cc, nil + } else if name == "pipelinerun-mytask1-9l9zj" { + return &trs[0], nil + } + return nil, xerrors.Errorf("getTaskRun called with unexpected name %s", name) + }, + expectedConditionCheck: TaskConditionCheckState{{ + ConditionCheckName: "pipelinerun-mytask1-9l9zj-always-true-mz4c7", + Condition: &condition, + ConditionCheck: v1alpha1.NewConditionCheck(cc), + }}, + }, + { + name: "conditionCheck doesn't exist", + getTaskRun: func(name string) (*v1alpha1.TaskRun, error) { + if name == "pipelinerun-mytask1-mssqb-always-true-78c5n" { + return nil, nil + } else if name == "pipelinerun-mytask1-mssqb" { + return &trs[0], nil + } + return nil, xerrors.Errorf("getTaskRun called with unexpected name %s", name) + }, + expectedConditionCheck: TaskConditionCheckState{{ + ConditionCheckName: "pipelinerun-mytask1-mssqb-always-true-78c5n", + Condition: &condition, + }}, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + pipelineState, err := ResolvePipelineRun(pr, getTask, tc.getTaskRun, getClusterTask, getResource, getCondition, pts, providedResources) + if err != nil { + t.Fatalf("Did not expect error when resolving PipelineRun without Conditions: %v", err) + } + + if d := cmp.Diff(pipelineState[0].ResolvedConditionChecks, tc.expectedConditionCheck, cmpopts.IgnoreUnexported(v1alpha1.TaskRunSpec{})); d != "" { + t.Fatalf("ConditionChecks did not resolve as expected for case %s : %s", tc.name, d) + } + }) + } +} + +func TestResolveConditionChecks_ConditionDoesNotExist(t *testing.T) { + names.TestingSeed() + trName := "pipelinerun-mytask1-9l9zj" + ccName := "pipelinerun-mytask1-9l9zj-does-not-exist-mz4c7" + + pts := []v1alpha1.PipelineTask{{ + Name: "mytask1", + TaskRef: v1alpha1.TaskRef{Name: "task"}, + Conditions: []v1alpha1.PipelineTaskCondition{{ + ConditionRef: "does-not-exist", + }}, + }} + providedResources := map[string]v1alpha1.PipelineResourceRef{} + + getTask := func(name string) (v1alpha1.TaskInterface, error) { return task, nil } + getTaskRun := func(name string) (*v1alpha1.TaskRun, error) { + if name == ccName { + return nil, xerrors.Errorf("should not be called") + } else if name == trName { + return &trs[0], nil + } + return nil, xerrors.Errorf("getTaskRun called with unexpected name %s", name) + } + getClusterTask := func(name string) (v1alpha1.TaskInterface, error) { return nil, xerrors.New("should not get called") } + getResource := func(name string) (*v1alpha1.PipelineResource, error) { + return nil, xerrors.New("should not get called") + } + getCondition := func(name string) (*v1alpha1.Condition, error) { + return nil, errors.NewNotFound(v1alpha1.Resource("condition"), name) + } + pr := v1alpha1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipelinerun", + }, + } + + _, err := ResolvePipelineRun(pr, getTask, getTaskRun, getClusterTask, getResource, getCondition, pts, providedResources) + + switch err := err.(type) { + case nil: + t.Fatalf("Expected error getting non-existent Conditions but got none") + case *ConditionNotFoundError: + // expected error + default: + t.Fatalf("Expected specific error type returned by func for non-existent Condition got %s", err) + } +} + +func TestResolveConditionCheck_UseExistingConditionCheckName(t *testing.T) { + names.TestingSeed() + + trName := "pipelinerun-mytask1-9l9zj" + ccName := "some-random-name" + + cc := &v1alpha1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: ccName, + }, + Spec: v1alpha1.TaskRunSpec{}, + } + + pts := []v1alpha1.PipelineTask{{ + Name: "mytask1", + TaskRef: v1alpha1.TaskRef{Name: "task"}, + Conditions: []v1alpha1.PipelineTaskCondition{{ + ConditionRef: "always-true", + }}, + }} + providedResources := map[string]v1alpha1.PipelineResourceRef{} + + getTask := func(name string) (v1alpha1.TaskInterface, error) { return task, nil } + getTaskRun := func(name string) (*v1alpha1.TaskRun, error) { + if name == ccName { + return cc, nil + } else if name == trName { + return &trs[0], nil + } + return nil, xerrors.Errorf("getTaskRun called with unexpected name %s", name) + } + getClusterTask := func(name string) (v1alpha1.TaskInterface, error) { return nil, xerrors.New("should not get called") } + getResource := func(name string) (*v1alpha1.PipelineResource, error) { + return nil, xerrors.New("should not get called") + } + getCondition := func(name string) (*v1alpha1.Condition, error) { return &condition, nil } + + ccStatus := make(map[string]*v1alpha1.PipelineRunConditionCheckStatus) + ccStatus[ccName] = &v1alpha1.PipelineRunConditionCheckStatus{ + ConditionName: "always-true", + } + trStatus := make(map[string]*v1alpha1.PipelineRunTaskRunStatus) + trStatus[trName] = &v1alpha1.PipelineRunTaskRunStatus{ + PipelineTaskName: "mytask-1", + ConditionChecks: ccStatus, + } + pr := v1alpha1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipelinerun", + }, + Status: v1alpha1.PipelineRunStatus{ + Status: duckv1beta1.Status{}, + TaskRuns: trStatus, + }, + } + + pipelineState, err := ResolvePipelineRun(pr, getTask, getTaskRun, getClusterTask, getResource, getCondition, pts, providedResources) + if err != nil { + t.Fatalf("Did not expect error when resolving PipelineRun without Conditions: %v", err) + } + expectedConditionChecks := TaskConditionCheckState{{ + ConditionCheckName: ccName, + Condition: &condition, + ConditionCheck: v1alpha1.NewConditionCheck(cc), + }} + + if d := cmp.Diff(pipelineState[0].ResolvedConditionChecks, expectedConditionChecks, cmpopts.IgnoreUnexported(v1alpha1.TaskRunSpec{})); d != "" { + t.Fatalf("ConditionChecks did not resolve as expected : %s", d) + } +} diff --git a/test/builder/pipeline.go b/test/builder/pipeline.go index 03e2af2354e..c5928a12136 100644 --- a/test/builder/pipeline.go +++ b/test/builder/pipeline.go @@ -211,6 +211,17 @@ func PipelineTaskOutputResource(name, resource string) PipelineTaskOp { } } +// PipelineTaskCondition adds a condition to the PipelineTask with the +// specified conditionRef +func PipelineTaskCondition(conditionRef string) PipelineTaskOp { + return func(pt *v1alpha1.PipelineTask) { + c := v1alpha1.PipelineTaskCondition{ + ConditionRef: conditionRef, + } + pt.Conditions = append(pt.Conditions, c) + } +} + // PipelineRun creates a PipelineRun with default values. // Any number of PipelineRun modifier can be passed to transform it. func PipelineRun(name, namespace string, ops ...PipelineRunOp) *v1alpha1.PipelineRun { @@ -384,10 +395,13 @@ func PipelineRunCompletionTime(t time.Time) PipelineRunStatusOp { } } -// PipelineRunTaskRunsStatus sets the TaskRuns of the PipelineRunStatus. -func PipelineRunTaskRunsStatus(taskRuns map[string]*v1alpha1.PipelineRunTaskRunStatus) PipelineRunStatusOp { +// PipelineRunTaskRunsStatus sets the status of TaskRun to the PipelineRunStatus. +func PipelineRunTaskRunsStatus(taskRunName string, status *v1alpha1.PipelineRunTaskRunStatus) PipelineRunStatusOp { return func(s *v1alpha1.PipelineRunStatus) { - s.TaskRuns = taskRuns + if s.TaskRuns == nil { + s.TaskRuns = make(map[string]*v1alpha1.PipelineRunTaskRunStatus) + } + s.TaskRuns[taskRunName] = status } } diff --git a/test/builder/pipeline_test.go b/test/builder/pipeline_test.go index 17813104a70..94fa1d40225 100644 --- a/test/builder/pipeline_test.go +++ b/test/builder/pipeline_test.go @@ -35,6 +35,7 @@ func TestPipeline(t *testing.T) { tb.PipelineTask("foo", "banana", tb.PipelineTaskParam("stringparam", "value"), tb.PipelineTaskParam("arrayparam", "array", "value"), + tb.PipelineTaskCondition("some-condition-ref"), ), tb.PipelineTask("bar", "chocolate", tb.PipelineTaskRefKind(v1alpha1.ClusterTaskKind), @@ -76,6 +77,7 @@ func TestPipeline(t *testing.T) { Name: "arrayparam", Value: *tb.ArrayOrString("array", "value"), }}, + Conditions: []v1alpha1.PipelineTaskCondition{{ConditionRef: "some-condition-ref"}}, }, { Name: "bar", TaskRef: v1alpha1.TaskRef{Name: "chocolate", Kind: v1alpha1.ClusterTaskKind}, @@ -117,6 +119,9 @@ func TestPipelineRun(t *testing.T) { apis.Condition{Type: apis.ConditionSucceeded}), tb.PipelineRunStartTime(startTime), tb.PipelineRunCompletionTime(completedTime), + tb.PipelineRunTaskRunsStatus("trname", &v1alpha1.PipelineRunTaskRunStatus{ + PipelineTaskName: "task-1", + }), ), tb.PipelineRunLabel("label-key", "label-value")) expectedPipelineRun := &v1alpha1.PipelineRun{ ObjectMeta: metav1.ObjectMeta{ @@ -151,6 +156,9 @@ func TestPipelineRun(t *testing.T) { }, StartTime: &metav1.Time{Time: startTime}, CompletionTime: &metav1.Time{Time: completedTime}, + TaskRuns: map[string]*v1alpha1.PipelineRunTaskRunStatus{ + "trname": {PipelineTaskName: "task-1"}, + }, }, } if d := cmp.Diff(expectedPipelineRun, pipelineRun); d != "" { diff --git a/test/clients.go b/test/clients.go index 45d420fc464..3d08f62e9f3 100644 --- a/test/clients.go +++ b/test/clients.go @@ -52,6 +52,7 @@ type clients struct { TaskRunClient v1alpha1.TaskRunInterface PipelineRunClient v1alpha1.PipelineRunInterface PipelineResourceClient v1alpha1.PipelineResourceInterface + ConditionClient v1alpha1.ConditionInterface } // newClients instantiates and returns several clientsets required for making requests to the @@ -81,5 +82,6 @@ func newClients(t *testing.T, configPath, clusterName, namespace string) *client c.TaskRunClient = cs.TektonV1alpha1().TaskRuns(namespace) c.PipelineRunClient = cs.TektonV1alpha1().PipelineRuns(namespace) c.PipelineResourceClient = cs.TektonV1alpha1().PipelineResources(namespace) + c.ConditionClient = cs.TektonV1alpha1().Conditions(namespace) return c } diff --git a/test/controller.go b/test/controller.go index fd34e79b937..b079c64679e 100644 --- a/test/controller.go +++ b/test/controller.go @@ -20,6 +20,7 @@ import ( // Link in the fakes so they get injected into injection.Fake fakepipelineclient "github.com/tektoncd/pipeline/pkg/client/injection/client/fake" fakeclustertaskinformer "github.com/tektoncd/pipeline/pkg/client/injection/informers/pipeline/v1alpha1/clustertask/fake" + fakeconditioninformer "github.com/tektoncd/pipeline/pkg/client/injection/informers/pipeline/v1alpha1/condition/fake" fakepipelineinformer "github.com/tektoncd/pipeline/pkg/client/injection/informers/pipeline/v1alpha1/pipeline/fake" fakeresourceinformer "github.com/tektoncd/pipeline/pkg/client/injection/informers/pipeline/v1alpha1/pipelineresource/fake" fakepipelineruninformer "github.com/tektoncd/pipeline/pkg/client/injection/informers/pipeline/v1alpha1/pipelinerun/fake" @@ -56,6 +57,7 @@ type Data struct { Tasks []*v1alpha1.Task ClusterTasks []*v1alpha1.ClusterTask PipelineResources []*v1alpha1.PipelineResource + Conditions []*v1alpha1.Condition Pods []*corev1.Pod Namespaces []*corev1.Namespace } @@ -74,6 +76,7 @@ type Informers struct { Task informersv1alpha1.TaskInformer ClusterTask informersv1alpha1.ClusterTaskInformer PipelineResource informersv1alpha1.PipelineResourceInformer + Condition informersv1alpha1.ConditionInformer Pod coreinformers.PodInformer } @@ -99,6 +102,7 @@ func SeedTestData(t *testing.T, ctx context.Context, d Data) (Clients, Informers Task: faketaskinformer.Get(ctx), ClusterTask: fakeclustertaskinformer.Get(ctx), PipelineResource: fakeresourceinformer.Get(ctx), + Condition: fakeconditioninformer.Get(ctx), Pod: fakepodinformer.Get(ctx), } @@ -150,6 +154,14 @@ func SeedTestData(t *testing.T, ctx context.Context, d Data) (Clients, Informers t.Fatal(err) } } + for _, cond := range d.Conditions { + if err := i.Condition.Informer().GetIndexer().Add(cond); err != nil { + t.Fatal(err) + } + if _, err := c.Pipeline.TektonV1alpha1().Conditions(cond.Namespace).Create(cond); err != nil { + t.Fatal(err) + } + } for _, p := range d.Pods { if err := i.Pod.Informer().GetIndexer().Add(p); err != nil { t.Fatal(err) diff --git a/test/e2e-common.sh b/test/e2e-common.sh index d503f385f47..1dec12d9d57 100755 --- a/test/e2e-common.sh +++ b/test/e2e-common.sh @@ -123,7 +123,7 @@ function install_pipeline_crd() { ko apply -f config/ || fail_test "Build pipeline installation failed" # Make sure thateveything is cleaned up in the current namespace. - for res in pipelineresources tasks pipelines taskruns pipelineruns; do + for res in conditions pipelineresources tasks pipelines taskruns pipelineruns; do kubectl delete --ignore-not-found=true ${res}.tekton.dev --all done diff --git a/test/pipelinerun_test.go b/test/pipelinerun_test.go index 7fb84caa7c7..affc3dd88ed 100644 --- a/test/pipelinerun_test.go +++ b/test/pipelinerun_test.go @@ -45,6 +45,7 @@ var ( saName = "service-account" taskName = "task" task1Name = "task1" + cond1Name = "cond-1" pipelineRunTimeout = 10 * time.Minute ) @@ -113,6 +114,32 @@ func TestPipelineRun(t *testing.T) { // 1 from PipelineRun and 1 from Tasks defined in pipelinerun expectedNumberOfEvents: 2, pipelineRunFunc: getHelloWorldPipelineRun, + }, { + name: "pipeline succeeds when task skipped due to failed condition", + testSetup: func(t *testing.T, c *clients, namespace string, index int) { + t.Helper() + cond := getFailingCondition(namespace) + if _, err := c.ConditionClient.Create(cond); err != nil { + t.Fatalf("Failed to create Condition `%s`: %s", cond1Name, err) + } + + task := tb.Task(getName(taskName, index), namespace, tb.TaskSpec( + tb.Step("echo-hello", "ubuntu", + tb.Command("/bin/bash"), + tb.Args("-c", "echo hello, world"), + ), + )) + if _, err := c.TaskClient.Create(task); err != nil { + t.Fatalf("Failed to create Task `%s`: %s", getName(taskName, index), err) + } + if _, err := c.PipelineClient.Create(getHelloWorldPipelineWithCondition(index, namespace)); err != nil { + t.Fatalf("Failed to create Pipeline `%s`: %s", getName(pipelineName, index), err) + } + }, + expectedTaskRuns: []string{}, + // 1 from PipelineRun; 0 from taskrun since it should not be executed due to condition failing + expectedNumberOfEvents: 1, + pipelineRunFunc: getConditionalPipelineRun, }} for i, td := range tds { @@ -506,3 +533,21 @@ func assertAnnotationsMatch(t *testing.T, expectedAnnotations, actualAnnotations } } } + +func getHelloWorldPipelineWithCondition(suffix int, namespace string) *v1alpha1.Pipeline { + return tb.Pipeline(getName(pipelineName, suffix), namespace, tb.PipelineSpec( + tb.PipelineTask(task1Name, getName(taskName, suffix), tb.PipelineTaskCondition(cond1Name)), + )) +} + +func getFailingCondition(namespace string) *v1alpha1.Condition { + return tb.Condition(cond1Name, namespace, tb.ConditionSpec(tb.ConditionSpecCheck("ubuntu", + tb.Command("/bin/bash"), tb.Args("exit 1")))) +} + +func getConditionalPipelineRun(suffix int, namespace string) *v1alpha1.PipelineRun { + return tb.PipelineRun(getName(pipelineRunName, suffix), namespace, + tb.PipelineRunLabel("hello-world-key", "hello-world-value"), + tb.PipelineRunSpec(getName(pipelineName, suffix)), + ) +}