From 30bc7a6b4c66b7bbe2c17b82b1b0649333459c1b Mon Sep 17 00:00:00 2001 From: Dibyo Mukherjee Date: Wed, 24 Jul 2019 10:55:49 -0400 Subject: [PATCH] Adds an initial implementation for conditionals In this implementation, condition evaluations aka ConditionChecks are backed by TaskRuns. All conditionChecks associated with a `PipelineTask` have to succeed before the task is executed. If a ConditionCheck fails, the PipelineTask's associated TaskRun is marked failed i.e. its `Status.ConditionSucceeded` is False. However, the PipelineRun itself is not marked as failed. Signed-off-by: Dibyo Mukherjee --- .../pipelineruns/conditional-pipelinerun.yaml | 77 +++ pkg/apis/pipeline/register.go | 13 +- pkg/apis/pipeline/v1alpha1/condition_types.go | 1 + .../v1alpha1/pipelinerun/controller.go | 3 + .../v1alpha1/pipelinerun/pipelinerun.go | 243 +++++--- .../v1alpha1/pipelinerun/pipelinerun_test.go | 540 +++++++++++++++++- .../resources/conditionresolution.go | 110 ++++ .../resources/conditionresolution_test.go | 288 ++++++++++ .../resources/pipelinerunresolution.go | 96 +++- .../resources/pipelinerunresolution_test.go | 382 ++++++++++++- test/builder/pipeline.go | 20 +- test/builder/pipeline_test.go | 2 + test/controller.go | 12 + 13 files changed, 1686 insertions(+), 101 deletions(-) create mode 100644 examples/pipelineruns/conditional-pipelinerun.yaml create mode 100644 pkg/reconciler/v1alpha1/pipelinerun/resources/conditionresolution.go create mode 100644 pkg/reconciler/v1alpha1/pipelinerun/resources/conditionresolution_test.go diff --git a/examples/pipelineruns/conditional-pipelinerun.yaml b/examples/pipelineruns/conditional-pipelinerun.yaml new file mode 100644 index 00000000000..bfb8d3f2074 --- /dev/null +++ b/examples/pipelineruns/conditional-pipelinerun.yaml @@ -0,0 +1,77 @@ +apiVersion: tekton.dev/v1alpha1 +kind: Condition +metadata: + name: always-true +spec: + check: + image: alpine + command: ["/bin/sh"] + args: ['-c', 'cat /workspace/prmetadata.json'] +--- +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-0 + taskRef: + name: list-files + resources: + inputs: + - name: workspace + resource: source-repo + - name: list-files-1 + taskRef: + name: list-files + runAfter: + - list-files-0 + 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..f4ba4eefe9c 100644 --- a/pkg/apis/pipeline/register.go +++ b/pkg/apis/pipeline/register.go @@ -18,10 +18,11 @@ package pipeline // GroupName is the Kubernetes resource group name for Pipeline types. const ( - GroupName = "tekton.dev" - TaskLabelKey = "/task" - TaskRunLabelKey = "/taskRun" - PipelineLabelKey = "/pipeline" - PipelineRunLabelKey = "/pipelineRun" - PipelineTaskLabelKey = "/pipelineTask" + GroupName = "tekton.dev" + TaskLabelKey = "/task" + TaskRunLabelKey = "/taskRun" + PipelineLabelKey = "/pipeline" + PipelineRunLabelKey = "/pipelineRun" + PipelineTaskLabelKey = "/pipelineTask" + PipelineRunConditionCheckKey = "/pipelineConditionCheck" ) diff --git a/pkg/apis/pipeline/v1alpha1/condition_types.go b/pkg/apis/pipeline/v1alpha1/condition_types.go index 3dbce3ae106..0e1acabd347 100644 --- a/pkg/apis/pipeline/v1alpha1/condition_types.go +++ b/pkg/apis/pipeline/v1alpha1/condition_types.go @@ -27,6 +27,7 @@ import ( // Check that Task may be validated and defaulted. var _ apis.Validatable = (*Condition)(nil) +//var _ apis.Defaultable = (*Condition)(nil) // +genclient // +genclient:noStatus diff --git a/pkg/reconciler/v1alpha1/pipelinerun/controller.go b/pkg/reconciler/v1alpha1/pipelinerun/controller.go index 3b0353f5f63..ed7a5c8e6b5 100644 --- a/pkg/reconciler/v1alpha1/pipelinerun/controller.go +++ b/pkg/reconciler/v1alpha1/pipelinerun/controller.go @@ -27,6 +27,7 @@ import ( "github.com/knative/pkg/tracker" 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 6b64f77e1fe..c3dcb787112 100644 --- a/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun.go +++ b/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun.go @@ -26,23 +26,24 @@ import ( "github.com/knative/pkg/configmap" "github.com/knative/pkg/controller" "github.com/knative/pkg/tracker" + "go.uber.org/zap" + "golang.org/x/xerrors" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/cache" + "github.com/tektoncd/pipeline/pkg/apis/config" 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" "github.com/tektoncd/pipeline/pkg/reconciler/v1alpha1/pipelinerun/resources" "github.com/tektoncd/pipeline/pkg/reconciler/v1alpha1/taskrun" - "go.uber.org/zap" - "golang.org/x/xerrors" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/equality" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/tools/cache" ) const ( @@ -58,6 +59,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" @@ -89,6 +93,7 @@ type Reconciler struct { taskLister listers.TaskLister clusterTaskLister listers.ClusterTaskLister resourceLister listers.PipelineResourceLister + conditionLister listers.ConditionLister tracker tracker.Interface configStore configStore timeoutHandler *reconciler.TimeoutSet @@ -259,6 +264,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 { @@ -280,6 +288,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, @@ -334,6 +350,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 @@ -344,11 +361,21 @@ 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 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) + if rprt.ResolvedConditionChecks == nil || rprt.ResolvedConditionChecks.IsSuccess() { + c.Logger.Infof("Creating a new TaskRun object %s", rprt.TaskRunName) + rprt.TaskRun, err = c.createTaskRun(c.Logger, 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(c.Logger, 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) + } + } } } } @@ -365,21 +392,54 @@ func (c *Reconciler) reconcile(ctx context.Context, pr *v1alpha1.PipelineRun) er func updateTaskRunsStatus(pr *v1alpha1.PipelineRun, pipelineState []*resources.ResolvedPipelineRunTask) { for _, rprt := range pipelineState { + 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 { + ccStatus := c.NewConditionCheckStatus() + cStatus[c.ConditionCheckName].Status = &ccStatus + } + } + prtrs.ConditionChecks = cStatus + if rprt.ResolvedConditionChecks.IsComplete() && !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), + }) + } + } + pr.Status.TaskRuns[rprt.TaskRunName] = prtrs } } 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 { @@ -391,61 +451,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) { - var taskRunTimeout = &metav1.Duration{Duration: apisconfig.NoTimeoutDuration} - - var timeout time.Duration - if pr.Spec.Timeout == nil { - timeout = config.DefaultTimeoutMinutes - } else { - timeout = pr.Spec.Timeout.Duration - } - // If the value of the timeout is 0 for any resource, there is no timeout. - // It is impossible for pr.Spec.Timeout to be nil, since SetDefault always assigns it with a value. - if timeout != apisconfig.NoTimeoutDuration { - pTimeoutTime := pr.Status.StartTime.Add(timeout) - if time.Now().After(pTimeoutTime) { - // Just in case something goes awry and we're creating the TaskRun after it should have already timed out, - // set the timeout to 1 second. - taskRunTimeout = &metav1.Duration{Duration: time.Until(pTimeoutTime)} - if taskRunTimeout.Duration < 0 { - taskRunTimeout = &metav1.Duration{Duration: 1 * time.Second} - } - } else { - taskRunTimeout = &metav1.Duration{Duration: timeout} - } - } - - // If service account is configured for a given PipelineTask, override PipelineRun's seviceAccount - serviceAccount := pr.Spec.ServiceAccount - for _, sa := range pr.Spec.ServiceAccounts { - if sa.TaskName == rprt.PipelineTask.Name { - serviceAccount = sa.ServiceAccount - } - } - - // Propagate labels from PipelineRun to TaskRun. - labels := make(map[string]string, len(pr.ObjectMeta.Labels)+1) - for key, val := range pr.ObjectMeta.Labels { - labels[key] = val - } - labels[pipeline.GroupName+pipeline.PipelineRunLabelKey] = pr.Name - if rprt.PipelineTask.Name != "" { - labels[pipeline.GroupName+pipeline.PipelineTaskLabelKey] = rprt.PipelineTask.Name - } - - // Propagate annotations from PipelineRun to TaskRun. - annotations := make(map[string]string, len(pr.ObjectMeta.Annotations)+1) - for key, val := range pr.ObjectMeta.Annotations { - annotations[key] = val - } - tr, _ := c.taskRunLister.TaskRuns(pr.Namespace).Get(rprt.TaskRunName) - if tr != nil { //is a retry addRetryHistory(tr) @@ -463,8 +473,8 @@ func (c *Reconciler) createTaskRun(logger *zap.SugaredLogger, rprt *resources.Re Name: rprt.TaskRunName, Namespace: pr.Namespace, OwnerReferences: pr.GetOwnerReference(), - Labels: labels, - Annotations: annotations, + Labels: getTaskrunLabels(pr, rprt.PipelineTask.Name), // Propagate labels from PipelineRun to TaskRun. + Annotations: getTaskrunAnnotations(pr), // Propagate annotations from PipelineRun to TaskRun. }, Spec: v1alpha1.TaskRunSpec{ TaskRef: &v1alpha1.TaskRef{ @@ -474,8 +484,11 @@ func (c *Reconciler) createTaskRun(logger *zap.SugaredLogger, rprt *resources.Re Inputs: v1alpha1.TaskRunInputs{ Params: rprt.PipelineTask.Params, }, - ServiceAccount: serviceAccount, - Timeout: taskRunTimeout, + ServiceAccount: getServiceAccount(pr, rprt.PipelineTask.Name), + NodeSelector: pr.Spec.NodeSelector, + Tolerations: pr.Spec.Tolerations, + Affinity: pr.Spec.Affinity, + Timeout: getTaskRunTimeout(pr), PodTemplate: podTemplate, }} @@ -484,6 +497,26 @@ func (c *Reconciler) createTaskRun(logger *zap.SugaredLogger, rprt *resources.Re return c.PipelineClientSet.TektonV1alpha1().TaskRuns(pr.Namespace).Create(tr) } +func getTaskrunAnnotations(pr *v1alpha1.PipelineRun) map[string]string { + annotations := make(map[string]string, len(pr.ObjectMeta.Annotations)+1) + for key, val := range pr.ObjectMeta.Annotations { + annotations[key] = val + } + return annotations +} + +func getTaskrunLabels(pr *v1alpha1.PipelineRun, pipelineTaskName string) map[string]string { + labels := make(map[string]string, len(pr.ObjectMeta.Labels)+1) + for key, val := range pr.ObjectMeta.Labels { + labels[key] = val + } + labels[pipeline.GroupName+pipeline.PipelineRunLabelKey] = pr.Name + if pipelineTaskName != "" { + labels[pipeline.GroupName+pipeline.PipelineTaskLabelKey] = pipelineTaskName + } + return labels +} + func addRetryHistory(tr *v1alpha1.TaskRun) { newStatus := *tr.Status.DeepCopy() newStatus.RetriesStatus = nil @@ -527,3 +560,67 @@ func (c *Reconciler) updateLabelsAndAnnotations(pr *v1alpha1.PipelineRun) (*v1al } return newPr, nil } + +func (c *Reconciler) makeConditionCheckContainer(logger *zap.SugaredLogger, rprt *resources.ResolvedPipelineRunTask, rcc *resources.ResolvedConditionCheck, pr *v1alpha1.PipelineRun) (*v1alpha1.ConditionCheck, error) { + labels := getTaskrunLabels(pr, rprt.PipelineTask.Name) + labels[pipeline.GroupName+pipeline.PipelineRunConditionCheckKey] = 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 +} + +func getTaskRunTimeout(pr *v1alpha1.PipelineRun) *metav1.Duration { + var taskRunTimeout = &metav1.Duration{Duration: apisconfig.NoTimeoutDuration} + + var timeout time.Duration + if pr.Spec.Timeout == nil { + timeout = config.DefaultTimeoutMinutes + } else { + timeout = pr.Spec.Timeout.Duration + } + // If the value of the timeout is 0 for any resource, there is no timeout. + // It is impossible for pr.Spec.Timeout to be nil, since SetDefault always assigns it with a value. + if timeout != apisconfig.NoTimeoutDuration { + pTimeoutTime := pr.Status.StartTime.Add(timeout) + if time.Now().After(pTimeoutTime) { + // Just in case something goes awry and we're creating the TaskRun after it should have already timed out, + // set the timeout to 1 second. + taskRunTimeout = &metav1.Duration{Duration: time.Until(pTimeoutTime)} + if taskRunTimeout.Duration < 0 { + taskRunTimeout = &metav1.Duration{Duration: 1 * time.Second} + } + } else { + taskRunTimeout = &metav1.Duration{Duration: timeout} + } + } + return taskRunTimeout +} + +func getServiceAccount(pr *v1alpha1.PipelineRun, pipelineTaskName string) string { + // If service account is configured for a given PipelineTask, override PipelineRun's seviceAccount + serviceAccount := pr.Spec.ServiceAccount + for _, sa := range pr.Spec.ServiceAccounts { + if sa.TaskName == pipelineTaskName { + serviceAccount = sa.ServiceAccount + } + } + return serviceAccount +} diff --git a/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun_test.go b/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun_test.go index 22abd63a2df..f0011402c15 100644 --- a/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun_test.go +++ b/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun_test.go @@ -39,6 +39,10 @@ import ( ktesting "k8s.io/client-go/testing" ) +var ( + ignoreLastTransitionTime = cmpopts.IgnoreTypes(apis.Condition{}.LastTransitionTime.Inner.Time) +) + func getRunName(pr *v1alpha1.PipelineRun) string { return strings.Join([]string{pr.Namespace, pr.Name}, "/") } @@ -59,6 +63,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() @@ -259,6 +269,7 @@ func TestReconcile_InvalidPipelineRuns(t *testing.T) { tb.Pipeline("a-pipeline-that-should-be-caught-by-admission-control", "foo", tb.PipelineSpec( tb.PipelineTask("some-task", "a-task-that-exists", tb.PipelineTaskInputResource("needed-resource", "a-resource")))), + 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")), @@ -268,6 +279,7 @@ func TestReconcile_InvalidPipelineRuns(t *testing.T) { tb.PipelineRun("pipeline-resources-dont-exist", "foo", tb.PipelineRunSpec("a-fine-pipeline", 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-conditions-missing", "foo", tb.PipelineRunSpec("a-pipeline-with-missing-conditions")), } d := test.Data{ Tasks: ts, @@ -303,6 +315,10 @@ func TestReconcile_InvalidPipelineRuns(t *testing.T) { name: "invalid-pipeline-missing-declared-resource-shd-stop-reconciling", pipelineRun: prs[5], reason: ReasonFailedValidation, + }, { + name: "invalid-pipeline-missing-conditions-shd-stop-reconciling", + pipelineRun: prs[6], + reason: ReasonCouldntGetCondition, }, } @@ -419,13 +435,298 @@ func TestUpdateTaskRunsState(t *testing.T) { } +// TODO: Switch to single test func with diff inputs +// name, state, expected status +func TestUpdateTaskRunState_WithPassingConditionChecks(t *testing.T) { + pr := tb.PipelineRun("test-pipeline-run", "foo", tb.PipelineRunSpec("test-pipeline")) + + cond := v1alpha1.Condition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "always-true", + }, + Spec: v1alpha1.ConditionSpec{ + Check: corev1.Container{}, + }, + } + + taskCondition := v1alpha1.PipelineTaskCondition{ + ConditionRef: "always-true", + } + + pipelineTask := v1alpha1.PipelineTask{ + Name: "unit-test-1", + TaskRef: v1alpha1.TaskRef{Name: "unit-test-task"}, + Conditions: []v1alpha1.PipelineTaskCondition{taskCondition}, + } + + conditioncheck := conditionCheckFromTaskRun(tb.TaskRun("test-pipeline-run-success-unit-test-1-always-true", "foo", tb.TaskRunSpec( + tb.TaskRunTaskSpec(tb.TaskContainerTemplate()), + ), tb.TaskRunStatus( + tb.Condition(apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }), + tb.StepState(tb.StateTerminated(0)), + ))) + + expectedConditionCheckStatus := make(map[string]*v1alpha1.PipelineRunConditionCheckStatus) + expectedConditionCheckStatus[conditioncheck.Name] = &v1alpha1.PipelineRunConditionCheckStatus{ + ConditionName: cond.Name, + Status: &v1alpha1.ConditionCheckStatus{ + Check: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ExitCode: 0}, + }, + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{{Type: apis.ConditionSucceeded, Status: corev1.ConditionTrue}}, + }, + }, + } + expectedTaskRunsStatus := make(map[string]*v1alpha1.PipelineRunTaskRunStatus) + expectedTaskRunsStatus["test-pipeline-run-success-unit-test-1"] = &v1alpha1.PipelineRunTaskRunStatus{ + PipelineTaskName: "unit-test-1", + ConditionChecks: expectedConditionCheckStatus, + } + expectedPipelineRunStatus := v1alpha1.PipelineRunStatus{ + TaskRuns: expectedTaskRunsStatus, + } + + state := []*resources.ResolvedPipelineRunTask{{ + PipelineTask: &pipelineTask, + TaskRunName: "test-pipeline-run-success-unit-test-1", + TaskRun: nil, + ResolvedTaskResources: &taskrunresources.ResolvedTaskResources{ + TaskSpec: &v1alpha1.TaskSpec{}, + }, + ResolvedConditionChecks: resources.TaskConditionCheckState{{ + ConditionCheckName: "test-pipeline-run-success-unit-test-1-always-true", + Condition: &cond, + ConditionCheck: conditioncheck, + }}, + }} + pr.Status.InitializeConditions() + updateTaskRunsStatus(pr, state) + if d := cmp.Diff(pr.Status.TaskRuns, expectedPipelineRunStatus.TaskRuns); d != "" { + t.Fatalf("Expected PipelineRun status to match ConditionCheck(s) status, but got a mismatch: %s", d) + } +} + +func TestUpdateTaskRunState_WithFailingConditionChecks(t *testing.T) { + pr := tb.PipelineRun("test-pipeline-run", "foo", tb.PipelineRunSpec("test-pipeline")) + + cond := v1alpha1.Condition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "always-true", + }, + Spec: v1alpha1.ConditionSpec{ + Check: corev1.Container{}, + }, + } + + taskCondition := v1alpha1.PipelineTaskCondition{ + ConditionRef: "always-true", + } + + pipelineTask := v1alpha1.PipelineTask{ + Name: "unit-test-1", + TaskRef: v1alpha1.TaskRef{Name: "unit-test-task"}, + Conditions: []v1alpha1.PipelineTaskCondition{taskCondition}, + } + + taskrunName := "test-pipeline-run-success-unit-test-1" + conditioncheck := conditionCheckFromTaskRun(tb.TaskRun("test-pipeline-run-success-unit-test-1-always-true", "foo", tb.TaskRunSpec( + tb.TaskRunTaskSpec(tb.TaskContainerTemplate()), + ), tb.TaskRunStatus( + tb.Condition(apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + }), + tb.StepState(tb.StateTerminated(127)), + ))) + + expectedConditionCheckStatus := make(map[string]*v1alpha1.PipelineRunConditionCheckStatus) + expectedConditionCheckStatus[conditioncheck.Name] = &v1alpha1.PipelineRunConditionCheckStatus{ + ConditionName: cond.Name, + Status: &v1alpha1.ConditionCheckStatus{ + Check: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ExitCode: 127}, + }, + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{{Type: apis.ConditionSucceeded, Status: corev1.ConditionFalse}}, + }, + }, + } + + expectedTaskRunsStatus := make(map[string]*v1alpha1.PipelineRunTaskRunStatus) + expectedTaskRunsStatus["test-pipeline-run-success-unit-test-1"] = &v1alpha1.PipelineRunTaskRunStatus{ + PipelineTaskName: "unit-test-1", + ConditionChecks: expectedConditionCheckStatus, + Status: &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, pr.Name), + }}, + }, + }, + } + expectedPipelineRunStatus := v1alpha1.PipelineRunStatus{ + TaskRuns: expectedTaskRunsStatus, + } + + state := makePRState(taskrunName, pipelineTask, resources.TaskConditionCheckState{{ + ConditionCheckName: "test-pipeline-run-success-unit-test-1-always-true", + Condition: &cond, + ConditionCheck: conditioncheck, + }}) + pr.Status.InitializeConditions() + updateTaskRunsStatus(pr, state) + ignoreLastTransitionTime := cmpopts.IgnoreTypes(apis.Condition{}.LastTransitionTime.Inner.Time) + if d := cmp.Diff(pr.Status.TaskRuns, expectedPipelineRunStatus.TaskRuns, ignoreLastTransitionTime); d != "" { + t.Fatalf("Expected PipelineRun status to match ConditionCheck(s) status, but got a mismatch: %s", d) + } +} + +func makePRState(trName string, pt v1alpha1.PipelineTask, tccs resources.TaskConditionCheckState) resources.PipelineRunState { + pipelineTask := v1alpha1.PipelineTask{ + Name: "unit-test-1", + TaskRef: v1alpha1.TaskRef{Name: "unit-test-task"}, + } + var ptc []v1alpha1.PipelineTaskCondition + for _, rcc := range tccs { + ptc = append(ptc, v1alpha1.PipelineTaskCondition{ + ConditionRef:rcc.Condition.Name, + }) + } + pipelineTask.Conditions = ptc + state := []*resources.ResolvedPipelineRunTask{{ + PipelineTask: &pt, + TaskRunName: trName, + ResolvedConditionChecks: tccs, + }} + return state +} + +func TestUpdateTaskRunState_MultipleConditionChecks(t *testing.T) { + taskrunName := "test-pipeline-run-success-unit-test-1" + successConditionCheckName := "test-pipeline-run-success-unit-test-1-cond-1" + failingConditionCheckName := "test-pipeline-run-success-unit-test-1-cond-2" + + successCondition := v1alpha1.Condition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cond-1", + }, + } + failingCondition := v1alpha1.Condition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cond-2", + }, + } + + pipelineTask := v1alpha1.PipelineTask{ + Name: "unit-test-1", + TaskRef: v1alpha1.TaskRef{Name: "unit-test-task"}, + Conditions: []v1alpha1.PipelineTaskCondition{{ + ConditionRef: successCondition.Name, + }, { + ConditionRef: failingCondition.Name, + }}, + } + + successConditionCheck := conditionCheckFromTaskRun(tb.TaskRun(successConditionCheckName, "foo", tb.TaskRunSpec( + tb.TaskRunTaskSpec(tb.TaskContainerTemplate()), + ), tb.TaskRunStatus( + tb.Condition(apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }), + tb.StepState(tb.StateTerminated(0)), + ))) + failingConditionCheck := conditionCheckFromTaskRun(tb.TaskRun(failingConditionCheckName, "foo", tb.TaskRunSpec( + tb.TaskRunTaskSpec(tb.TaskContainerTemplate()), + ), tb.TaskRunStatus( + tb.Condition(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, + } + + expectedConditionCheckStatus := make(map[string]*v1alpha1.PipelineRunConditionCheckStatus) + expectedConditionCheckStatus[successConditionCheckName] = successConditionCheckStatus + expectedConditionCheckStatus[failingConditionCheckName] = failingConditionCheckStatus + + expectedTaskRunsStatus := make(map[string]*v1alpha1.PipelineRunTaskRunStatus) + + //sucessfulTaskRunStatus := v1alpha1.TaskRunStatus{} + 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"), + }}, + }, + } + + expectedTaskRunsStatus[taskrunName] = &v1alpha1.PipelineRunTaskRunStatus{ + PipelineTaskName: "unit-test-1", + ConditionChecks: expectedConditionCheckStatus, + Status: &failedTaskRunStatus, + } + expectedPipelineRunStatus := v1alpha1.PipelineRunStatus{ + TaskRuns: expectedTaskRunsStatus, + } + + + + pr := tb.PipelineRun("test-pipeline-run", "foo", tb.PipelineRunSpec("test-pipeline")) + state := makePRState(taskrunName, pipelineTask, resources.TaskConditionCheckState{&successrcc, &failingrcc}) + pr.Status.InitializeConditions() + updateTaskRunsStatus(pr, state) + if d := cmp.Diff(pr.Status.TaskRuns, expectedPipelineRunStatus.TaskRuns, ignoreLastTransitionTime); d != "" { + t.Fatalf("Expected PipelineRun status to match ConditionCheck(s) status, but got a mismatch: %s", 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{ @@ -434,7 +735,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( @@ -500,7 +804,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}}, @@ -1005,3 +1309,225 @@ func TestReconcilePropagateAnnotations(t *testing.T) { t.Errorf("expected to see TaskRun %v created. Diff %s", expectedTaskRun, d) } } + +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.Condition(apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }), + ), + ), + // TODO: add a test builder for conditions/checks + 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.PipelineRunConditionCheckKey, conditionCheckName), + tb.TaskRunSpec(tb.TaskRunTaskSpec()), + tb.TaskRunStatus(tb.Condition(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/pipelineConditionCheck", ccName), + tb.TaskRunAnnotation("PipelineRunAnnotation", "PipelineRunValue"), + tb.TaskRunSpec( + tb.TaskRunTaskSpec(tb.Step("condition-check-" + condName, "foo", tb.Args("bar"))), + tb.TaskRunServiceAccount("test-sa"), + ), + ) +} \ No newline at end of file diff --git a/pkg/reconciler/v1alpha1/pipelinerun/resources/conditionresolution.go b/pkg/reconciler/v1alpha1/pipelinerun/resources/conditionresolution.go new file mode 100644 index 00000000000..9567c1cb06d --- /dev/null +++ b/pkg/reconciler/v1alpha1/pipelinerun/resources/conditionresolution.go @@ -0,0 +1,110 @@ +/* + * + * 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/knative/pkg/apis" + corev1 "k8s.io/api/core/v1" + + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" +) + +const ( + unnamedCheckNamePrefix = "condition-check-" +) +// GetCondition is a function used to retrieve PipelineConditions. +type GetCondition func(string) (*v1alpha1.Condition, error) + +type ResolvedConditionCheck struct { + ConditionCheckName string + Condition *v1alpha1.Condition + ConditionCheck *v1alpha1.ConditionCheck +} + +type TaskConditionCheckState []*ResolvedConditionCheck + +func (state TaskConditionCheckState) HasStarted() bool { + hasStarted := true + for _, j := range state { + if j.ConditionCheck == nil { + hasStarted = false + } + } + return hasStarted +} + +func (state TaskConditionCheckState) IsComplete() bool { + if !state.HasStarted() { + return false + } + isDone := true + for _, rcc := range state { + isDone = isDone && !rcc.ConditionCheck.Status.GetCondition(apis.ConditionSucceeded).IsUnknown() + } + return isDone +} + +func (state TaskConditionCheckState) IsSuccess() bool { + if !state.IsComplete() { + return false + } + isSuccess := true + for _, rcc := range state { + isSuccess = isSuccess && rcc.ConditionCheck.Status.GetCondition(apis.ConditionSucceeded).IsTrue() + } + return isSuccess +} + +// Convert a Condition to a TaskSpec +func (rcc *ResolvedConditionCheck) ConditionToTaskSpec() *v1alpha1.TaskSpec { + // TODO(dibyom): Should be in SetDefaults? + 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 +} + +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, + } +} \ No newline at end of file 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..a859cb16e2f --- /dev/null +++ b/pkg/reconciler/v1alpha1/pipelinerun/resources/conditionresolution_test.go @@ -0,0 +1,288 @@ +/* + * + * 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" + "github.com/knative/pkg/apis" + duckv1beta1 "github.com/knative/pkg/apis/duck/v1beta1" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "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 d := cmp.Diff(got, tc.want); d != "" { + 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.IsComplete() + if d := cmp.Diff(got, tc.want); d != "" { + 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 d := cmp.Diff(got, tc.want); d != "" { + 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 74dc29271bb..f08c8ace48b 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: We should allow multiple conditions of the same Name 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.IsComplete() { + 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 36f8a3a1720..5377ddf00db 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 8d667d335e2..735c71f28ed 100644 --- a/test/builder/pipeline.go +++ b/test/builder/pipeline.go @@ -227,6 +227,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 { @@ -399,10 +410,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 c24d1a9a0a0..925627891a8 100644 --- a/test/builder/pipeline_test.go +++ b/test/builder/pipeline_test.go @@ -34,6 +34,7 @@ func TestPipeline(t *testing.T) { tb.PipelineParam("first-param", tb.PipelineParamDefault("default-value"), tb.PipelineParamDescription("default description")), tb.PipelineTask("foo", "banana", tb.PipelineTaskParam("name", "value"), + tb.PipelineTaskCondition("some-condition-ref"), ), tb.PipelineTask("bar", "chocolate", tb.PipelineTaskRefKind(v1alpha1.ClusterTaskKind), @@ -68,6 +69,7 @@ func TestPipeline(t *testing.T) { Name: "foo", TaskRef: v1alpha1.TaskRef{Name: "banana"}, Params: []v1alpha1.Param{{Name: "name", Value: "value"}}, + Conditions: []v1alpha1.PipelineTaskCondition{{ConditionRef: "some-condition-ref"}}, }, { Name: "bar", TaskRef: v1alpha1.TaskRef{Name: "chocolate", Kind: v1alpha1.ClusterTaskKind}, diff --git a/test/controller.go b/test/controller.go index e8486942bd5..a50a74d8717 100644 --- a/test/controller.go +++ b/test/controller.go @@ -25,6 +25,7 @@ import ( 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" + fakeconditioninformer "github.com/tektoncd/pipeline/pkg/client/injection/informers/pipeline/v1alpha1/condition/fake" faketaskinformer "github.com/tektoncd/pipeline/pkg/client/injection/informers/pipeline/v1alpha1/task/fake" faketaskruninformer "github.com/tektoncd/pipeline/pkg/client/injection/informers/pipeline/v1alpha1/taskrun/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)