diff --git a/cmd/controller/main.go b/cmd/controller/main.go index eb5b60f4c75..f789c148f49 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -114,6 +114,7 @@ func main() { taskRunInformer := pipelineInformerFactory.Tekton().V1alpha1().TaskRuns() resourceInformer := pipelineInformerFactory.Tekton().V1alpha1().PipelineResources() podInformer := kubeInformerFactory.Core().V1().Pods() + conditionInformer := pipelineInformerFactory.Tekton().V1alpha1().Conditions() pipelineInformer := pipelineInformerFactory.Tekton().V1alpha1().Pipelines() pipelineRunInformer := pipelineInformerFactory.Tekton().V1alpha1().PipelineRuns() @@ -140,6 +141,7 @@ func main() { clusterTaskInformer, taskRunInformer, resourceInformer, + conditionInformer, timeoutHandler, ) // Build all of our controllers, with the clients constructed above. diff --git a/examples/pipelineruns/conditional-pipelinerun.yaml b/examples/pipelineruns/conditional-pipelinerun.yaml new file mode 100644 index 00000000000..bc6f14d25dd --- /dev/null +++ b/examples/pipelineruns/conditional-pipelinerun.yaml @@ -0,0 +1,79 @@ +apiVersion: tekton.dev/v1alpha1 +kind: Condition +metadata: + name: always-true +spec: + check: + image: ubuntu + command: ["/bin/bash"] + args: ['-c', 'exit 0'] +--- +apiVersion: tekton.dev/v1alpha1 +kind: Condition +metadata: + name: always-false +spec: + params: + check: + image: ubuntu + command: ["/bin/bash"] + args: ['-c', 'exit 1'] +--- +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 + 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 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/reconciler/v1alpha1/pipelinerun/pipelinerun.go b/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun.go index b02e7dbdc2a..86c1f45768a 100644 --- a/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun.go +++ b/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun.go @@ -28,7 +28,7 @@ import ( "github.com/knative/pkg/tracker" "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" informers "github.com/tektoncd/pipeline/pkg/client/informers/externalversions/pipeline/v1alpha1" listers "github.com/tektoncd/pipeline/pkg/client/listers/pipeline/v1alpha1" "github.com/tektoncd/pipeline/pkg/reconciler" @@ -58,6 +58,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 +92,7 @@ type Reconciler struct { taskLister listers.TaskLister clusterTaskLister listers.ClusterTaskLister resourceLister listers.PipelineResourceLister + conditionLister listers.ConditionLister tracker tracker.Interface configStore configStore timeoutHandler *reconciler.TimeoutSet @@ -106,6 +110,7 @@ func NewController( clusterTaskInformer informers.ClusterTaskInformer, taskRunInformer informers.TaskRunInformer, resourceInformer informers.PipelineResourceInformer, + conditionInformer informers.ConditionInformer, timeoutHandler *reconciler.TimeoutSet, ) *controller.Impl { @@ -117,6 +122,7 @@ func NewController( clusterTaskLister: clusterTaskInformer.Lister(), taskRunLister: taskRunInformer.Lister(), resourceLister: resourceInformer.Lister(), + conditionLister: conditionInformer.Lister(), timeoutHandler: timeoutHandler, } @@ -302,6 +308,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 { @@ -323,6 +332,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, @@ -377,6 +394,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 @@ -387,11 +405,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) + } + } } } } @@ -408,21 +436,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 := v1alpha1.ConditionCheckStatus(c.ConditionCheck.Status) + 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: Add conditionCheck statuses here prtrs := pr.Status.TaskRuns[taskRunName] tr, err := c.taskRunLister.TaskRuns(pr.Namespace).Get(taskRunName) if err != nil { @@ -434,53 +495,10 @@ 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: 0 * time.Second} - - if pr.Spec.Timeout != nil { - pTimeoutTime := pr.Status.StartTime.Add(pr.Spec.Timeout.Duration) - 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 a timeout of 0. - taskRunTimeout = &metav1.Duration{Duration: time.Until(pTimeoutTime)} - if taskRunTimeout.Duration < 0 { - taskRunTimeout = &metav1.Duration{Duration: 0 * time.Second} - } - } else { - taskRunTimeout = pr.Spec.Timeout - } - } else { - taskRunTimeout = nil - } - - // 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 { @@ -498,8 +516,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{ @@ -509,19 +527,38 @@ 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), + Timeout: getTaskRunTimeout(pr), NodeSelector: pr.Spec.NodeSelector, Tolerations: pr.Spec.Tolerations, Affinity: pr.Spec.Affinity, - }, - } + }} resources.WrapSteps(&tr.Spec, rprt.PipelineTask, rprt.ResolvedTaskResources.Inputs, rprt.ResolvedTaskResources.Outputs, storageBasePath) 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 @@ -565,3 +602,61 @@ 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: 0 * time.Second} + + if pr.Spec.Timeout != nil { + pTimeoutTime := pr.Status.StartTime.Add(pr.Spec.Timeout.Duration) + 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 a timeout of 0. + taskRunTimeout = &metav1.Duration{Duration: time.Until(pTimeoutTime)} + if taskRunTimeout.Duration < 0 { + taskRunTimeout = &metav1.Duration{Duration: 0 * time.Second} + } + } else { + taskRunTimeout = pr.Spec.Timeout + } + } else { + taskRunTimeout = nil + } + 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 233e8ed9c80..41be8aebc91 100644 --- a/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun_test.go +++ b/pkg/reconciler/v1alpha1/pipelinerun/pipelinerun_test.go @@ -83,6 +83,7 @@ func getPipelineRunController(t *testing.T, d test.Data, recorder record.EventRe i.ClusterTask, i.TaskRun, i.PipelineResource, + i.Condition, th, ), Logs: logs, @@ -91,6 +92,12 @@ func getPipelineRunController(t *testing.T, d test.Data, recorder record.EventRe } } +// 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() @@ -294,6 +301,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")), @@ -303,6 +311,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, @@ -338,6 +347,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, }, } @@ -469,6 +482,172 @@ func TestUpdateTaskRunsState(t *testing.T) { } +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.TaskCondition{ + ConditionRef: "always-true", + } + + pipelineTask := v1alpha1.PipelineTask{ + Name: "unit-test-1", + TaskRef: v1alpha1.TaskRef{Name: "unit-test-task"}, + Conditions: []v1alpha1.TaskCondition{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{ + Steps: []v1alpha1.StepState{{ + ContainerState: 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.TaskCondition{ + ConditionRef: "always-true", + } + + pipelineTask := v1alpha1.PipelineTask{ + Name: "unit-test-1", + TaskRef: v1alpha1.TaskRef{Name: "unit-test-task"}, + Conditions: []v1alpha1.TaskCondition{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{ + Steps: []v1alpha1.StepState{{ + ContainerState: 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 := []*resources.ResolvedPipelineRunTask{{ + PipelineTask: &pipelineTask, + TaskRunName: taskrunName, + 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) + 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 TestReconcileOnCompletedPipelineRun(t *testing.T) { prtrs := make(map[string]*v1alpha1.PipelineRunTaskRunStatus) taskRunName := "test-pipeline-run-completed-hello-world" @@ -1134,3 +1313,79 @@ func TestReconcilePropagateAnnotations(t *testing.T) { t.Errorf("expected to see TaskRun %v created. Diff %s", expectedTaskRun, d) } } + +func TestReconcileWithConditionChecks(t *testing.T) { + names.TestingSeed() + conditions := []*v1alpha1.Condition{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "always-true", + 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("always-true")), + ))} + prs := []*v1alpha1.PipelineRun{tb.PipelineRun("test-pipeline-run-with-conditions", "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, + } + + // create fake recorder for testing + fr := record.NewFakeRecorder(2) + + testAssets := getPipelineRunController(t, d, fr) + 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-hello-world-1-9l9zj-alw-mz4c7", "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, "hello-world-1"), + tb.TaskRunLabel("tekton.dev/pipelineRun", "test-pipeline-run-with-conditions"), + tb.TaskRunLabel("tekton.dev/pipelineConditionCheck", "test-pipeline-run-with-conditions-hello-world-1-9l9zj-alw-mz4c7"), + tb.TaskRunAnnotation("PipelineRunAnnotation", "PipelineRunValue"), + tb.TaskRunSpec( + tb.TaskRunTaskSpec(tb.Step("", "foo", tb.Args("bar"))), + tb.TaskRunServiceAccount("test-sa"), + ), + ) + + if d := cmp.Diff(actual, expectedTaskRun); d != "" { + t.Errorf("expected to see ConditionCheck TaskRun %v created. Diff %s", expectedTaskRun, d) + } +} diff --git a/pkg/reconciler/v1alpha1/pipelinerun/resources/conditionresolution.go b/pkg/reconciler/v1alpha1/pipelinerun/resources/conditionresolution.go new file mode 100644 index 00000000000..e6526667a77 --- /dev/null +++ b/pkg/reconciler/v1alpha1/pipelinerun/resources/conditionresolution.go @@ -0,0 +1,82 @@ +/* + * + * 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" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + corev1 "k8s.io/api/core/v1" +) + +// 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 { + 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 +} 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..5e3622932e8 --- /dev/null +++ b/pkg/reconciler/v1alpha1/pipelinerun/resources/conditionresolution_test.go @@ -0,0 +1,245 @@ +/* + * + * 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) { + container := corev1.Container{ + Image: "ubuntu", + Command: []string{"/bin/bash"}, + Args: []string{"-c", "ls"}, + } + + rcc := &ResolvedConditionCheck{ + ConditionCheckName: "somename", + Condition: &v1alpha1.Condition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + }, + Spec: v1alpha1.ConditionSpec{ + Params: []v1alpha1.ParamSpec{{Name: "abc"}}, + Check: container, + }, + }, + } + + want := &v1alpha1.TaskSpec{ + Inputs: &v1alpha1.Inputs{ + Params: []v1alpha1.ParamSpec{{ + Name: "abc", + }}, + }, + Steps: []corev1.Container{container}, + } + + if d := cmp.Diff(rcc.ConditionToTaskSpec(), want); d != "" { + t.Errorf("TaskSpec generated from Condition is unexpected: %v", d) + } + +} diff --git a/pkg/reconciler/v1alpha1/pipelinerun/resources/pipelinerunresolution.go b/pkg/reconciler/v1alpha1/pipelinerun/resources/pipelinerunresolution.go index 74dc29271bb..f6dc7ca0131 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 == "TaskRunCancelled" || 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 7e8f31138aa..b5cd37d3df8 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.TaskCondition{{ + 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", @@ -798,6 +909,31 @@ func TestGetPipelineConditionStatus(t *testing.T) { 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, + }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { @@ -898,8 +1034,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) } @@ -965,12 +1102,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) } @@ -1012,12 +1150,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) @@ -1058,6 +1199,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) { @@ -1066,7 +1210,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) } @@ -1108,6 +1252,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) { @@ -1116,7 +1263,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) @@ -1348,8 +1495,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) } @@ -1371,3 +1518,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.TaskCondition{{ + 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.TaskCondition{{ + 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.TaskCondition{{ + 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 1610c11ed6a..7d1aee24574 100644 --- a/test/builder/pipeline.go +++ b/test/builder/pipeline.go @@ -226,6 +226,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.TaskCondition{ + 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 { diff --git a/test/controller.go b/test/controller.go index a57de94a48b..7c992272a4a 100644 --- a/test/controller.go +++ b/test/controller.go @@ -47,6 +47,7 @@ type Data struct { Tasks []*v1alpha1.Task ClusterTasks []*v1alpha1.ClusterTask PipelineResources []*v1alpha1.PipelineResource + Conditions []*v1alpha1.Condition Pods []*corev1.Pod Namespaces []*corev1.Namespace } @@ -65,6 +66,7 @@ type Informers struct { Task informersv1alpha1.TaskInformer ClusterTask informersv1alpha1.ClusterTaskInformer PipelineResource informersv1alpha1.PipelineResourceInformer + Condition informersv1alpha1.ConditionInformer Pod coreinformers.PodInformer } @@ -120,6 +122,7 @@ func SeedTestData(t *testing.T, d Data) (Clients, Informers) { Task: sharedInformer.Tekton().V1alpha1().Tasks(), ClusterTask: sharedInformer.Tekton().V1alpha1().ClusterTasks(), PipelineResource: sharedInformer.Tekton().V1alpha1().PipelineResources(), + Condition: sharedInformer.Tekton().V1alpha1().Conditions(), Pod: kubeInformer.Core().V1().Pods(), } @@ -153,6 +156,11 @@ func SeedTestData(t *testing.T, d Data) (Clients, Informers) { t.Fatal(err) } } + for _, r := range d.Conditions { + if err := i.Condition.Informer().GetIndexer().Add(r); err != nil { + t.Fatal(err) + } + } for _, p := range d.Pods { if err := i.Pod.Informer().GetIndexer().Add(p); err != nil { t.Fatal(err)