From 6e9afcebf09abfabbd21205fb93326b57481ebbd Mon Sep 17 00:00:00 2001 From: Stanislav Jakuschevskij Date: Mon, 2 Jun 2025 19:41:36 +0200 Subject: [PATCH 1/7] feature: implement e2e PinP using PipelineSpec [TEP-0056] Child `PipelineRuns` (PinP) are created by the `PipelineRun` reconciler, equal to the `TaskRun/CustomRun` implementations. An event handler for child `PipelineRuns` is registered in controller entrypoint. This will trigger the reconciliation loop when child `PipelineRuns` change their state. Extend `ResolvedPipelineTask` data structure to store child `PipelineRuns` and the resolved pipeline in `ResolvedPipeline` data structure equal to `ResolvedTask`. Add `GetPipelineRun` function alias for convenience. Add condition to `func ResolvePipelineTask` which checks if the given `PipelineTask` is a `Pipeline` and resolves it by getting the actual spec. In our "happy path" case it's right on the `PipelineTask` because we use `PipelineSpec` and don't need to fetch it from anywhere. A few downstream methods equal to resolving a `TaskRun` were added. Extend `resolvePipelineState` with getter for child `PipelineRuns` using lister and extend `runNextSchedulableTask` with a condition check for a `PipelineTask` which is a `Pipeline` and implement the creation of a new `PipelineRun` from the resolved pipeline state and pipeline facts. To get the status of the child `PipelineRun` success and failure checks were added to the `PipelineRun` types and extended in the resolution methods. Test were added to cover the new public methods. Setting the `ChildReferences` was extended for child `PipelineRuns`. Extract getTask/-Run/Run functions in `pipelinerunresolution_test.go`. The function declarations were duplicated up to 10 times. Now they are extracted and generalized to a large extent. Rename label/annotation factory. Issues #8760, #7166. Signed-off-by: Stanislav Jakuschevskij --- pkg/apis/pipeline/v1/pipelinerun_types.go | 10 + .../pipeline/v1/pipelinerun_types_test.go | 80 ++++++++ pkg/pod/pod_test.go | 1 - pkg/reconciler/pipelinerun/controller.go | 7 + pkg/reconciler/pipelinerun/pipelinerun.go | 187 +++++++++++++++--- .../pipelinerun_updatestatus_test.go | 4 +- .../resources/pipelinerunresolution.go | 141 ++++++++++++- .../resources/pipelinerunresolution_test.go | 96 ++++----- .../pipelinerun/resources/pipelinerunstate.go | 69 +++++-- .../pipelinerun/resources/pipelinespec.go | 14 ++ 10 files changed, 502 insertions(+), 107 deletions(-) create mode 100644 pkg/reconciler/pipelinerun/resources/pipelinespec.go diff --git a/pkg/apis/pipeline/v1/pipelinerun_types.go b/pkg/apis/pipeline/v1/pipelinerun_types.go index 52175cab2d4..fd25e0bd249 100644 --- a/pkg/apis/pipeline/v1/pipelinerun_types.go +++ b/pkg/apis/pipeline/v1/pipelinerun_types.go @@ -84,6 +84,16 @@ func (pr *PipelineRun) HasStarted() bool { return pr.Status.StartTime != nil && !pr.Status.StartTime.IsZero() } +// IsSuccessful returns true if the PipelineRun's status indicates that it has succeeded. +func (pr *PipelineRun) IsSuccessful() bool { + return pr != nil && pr.Status.GetCondition(apis.ConditionSucceeded).IsTrue() +} + +// IsFailure returns true if the PipelineRun's status indicates that it has failed. +func (pr *PipelineRun) IsFailure() bool { + return pr != nil && pr.Status.GetCondition(apis.ConditionSucceeded).IsFalse() +} + // IsCancelled returns true if the PipelineRun's spec status is set to Cancelled state func (pr *PipelineRun) IsCancelled() bool { return pr.Spec.Status == PipelineRunSpecStatusCancelled diff --git a/pkg/apis/pipeline/v1/pipelinerun_types_test.go b/pkg/apis/pipeline/v1/pipelinerun_types_test.go index 34bb36e249c..9f453cee962 100644 --- a/pkg/apis/pipeline/v1/pipelinerun_types_test.go +++ b/pkg/apis/pipeline/v1/pipelinerun_types_test.go @@ -781,3 +781,83 @@ func TestPipelineRunMarkFailedCondition(t *testing.T) { }) } } + +func TestPipelineRunIsSuccessful(t *testing.T) { + tcs := []struct { + name string + pipelineRun *v1.PipelineRun + want bool + }{{ + name: "nil pipelinerun", + want: false, + }, { + name: "still running", + pipelineRun: &v1.PipelineRun{Status: v1.PipelineRunStatus{Status: duckv1.Status{Conditions: []apis.Condition{{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionUnknown, + }}}}}, + want: false, + }, { + name: "succeeded", + pipelineRun: &v1.PipelineRun{Status: v1.PipelineRunStatus{Status: duckv1.Status{Conditions: []apis.Condition{{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }}}}}, + want: true, + }, { + name: "failed", + pipelineRun: &v1.PipelineRun{Status: v1.PipelineRunStatus{Status: duckv1.Status{Conditions: []apis.Condition{{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + }}}}}, + want: false, + }} + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + got := tc.pipelineRun.IsSuccessful() + if tc.want != got { + t.Errorf("wanted IsSuccessful to be %t but was %t", tc.want, got) + } + }) + } +} + +func TestPipelineRunIsFailure(t *testing.T) { + tcs := []struct { + name string + pipelineRun *v1.PipelineRun + want bool + }{{ + name: "nil pipelinerun", + want: false, + }, { + name: "still running", + pipelineRun: &v1.PipelineRun{Status: v1.PipelineRunStatus{Status: duckv1.Status{Conditions: []apis.Condition{{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionUnknown, + }}}}}, + want: false, + }, { + name: "succeeded", + pipelineRun: &v1.PipelineRun{Status: v1.PipelineRunStatus{Status: duckv1.Status{Conditions: []apis.Condition{{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }}}}}, + want: false, + }, { + name: "failed", + pipelineRun: &v1.PipelineRun{Status: v1.PipelineRunStatus{Status: duckv1.Status{Conditions: []apis.Condition{{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + }}}}}, + want: true, + }} + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + got := tc.pipelineRun.IsFailure() + if tc.want != got { + t.Errorf("wanted IsFailure to be %t but was %t", tc.want, got) + } + }) + } +} diff --git a/pkg/pod/pod_test.go b/pkg/pod/pod_test.go index 4bc31f1baf7..aa60790153e 100644 --- a/pkg/pod/pod_test.go +++ b/pkg/pod/pod_test.go @@ -2622,7 +2622,6 @@ _EOF_ } else { trAnnotations = c.trAnnotation trAnnotations[ReleaseAnnotation] = fakeVersion - } testTaskRunName := taskRunName if c.trName != "" { diff --git a/pkg/reconciler/pipelinerun/controller.go b/pkg/reconciler/pipelinerun/controller.go index d47ef8d7760..4cae73c1452 100644 --- a/pkg/reconciler/pipelinerun/controller.go +++ b/pkg/reconciler/pipelinerun/controller.go @@ -100,6 +100,13 @@ func NewController(opts *pipeline.Options, clock clock.PassiveClock) func(contex logging.FromContext(ctx).Panicf("Couldn't register PipelineRun informer event handler: %w", err) } + if _, err := pipelineRunInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{ + FilterFunc: controller.FilterController(&v1.PipelineRun{}), + Handler: controller.HandleAll(impl.EnqueueControllerOf), + }); err != nil { + logging.FromContext(ctx).Panicf("Couldn't register PipelineRun informer event handler: %w", err) + } + if _, err := taskRunInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{ FilterFunc: controller.FilterController(&v1.PipelineRun{}), Handler: controller.HandleAll(impl.EnqueueControllerOf), diff --git a/pkg/reconciler/pipelinerun/pipelinerun.go b/pkg/reconciler/pipelinerun/pipelinerun.go index ed70b0aa8e9..37958c28bad 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun.go +++ b/pkg/reconciler/pipelinerun/pipelinerun.go @@ -145,8 +145,9 @@ var ( // "ControllerName" const in describing the type of run, we import these // constants (for consistency) but rename them (for ergonomic semantics). const ( - taskRun = pipeline.TaskRunControllerName - customRun = pipeline.CustomRunControllerName + taskRun = pipeline.TaskRunControllerName + customRun = pipeline.CustomRunControllerName + pipelineRun = pipeline.PipelineRunControllerName ) // Reconciler implements controller.Reconciler for Configuration resources. @@ -359,6 +360,10 @@ func (c *Reconciler) resolvePipelineState( return nil, fmt.Errorf("failed to list VerificationPolicies from namespace %s with error %w", pr.Namespace, err) } + getChildPipelineRunFunc := func(name string) (*v1.PipelineRun, error) { + return c.pipelineRunLister.PipelineRuns(pr.Namespace).Get(name) + } + getTaskFunc := tresources.GetTaskFunc( ctx, c.KubeClientSet, @@ -386,6 +391,7 @@ func (c *Reconciler) resolvePipelineState( resolvedTask, err := resources.ResolvePipelineTask(ctx, *pr, + getChildPipelineRunFunc, getTaskFunc, getTaskRunFunc, getCustomRunFunc, @@ -679,7 +685,8 @@ func (c *Reconciler) reconcile(ctx context.Context, pr *v1.PipelineRun, getPipel } for i, rpt := range pipelineRunFacts.State { - if !rpt.IsCustomTask() { + // Task? + if !rpt.IsCustomTask() && rpt.PipelineTask.PipelineSpec == nil { err := taskrun.ValidateResolvedTask(ctx, rpt.PipelineTask.Params, rpt.PipelineTask.Matrix, rpt.ResolvedTask) if err != nil { logger.Errorf("Failed to validate pipelinerun %s with error %w", pr.Name, err) @@ -953,14 +960,22 @@ func (c *Reconciler) runNextSchedulableTask(ctx context.Context, pr *v1.Pipeline } } - if rpt.IsCustomTask() { + switch { + case rpt.PipelineTask.PipelineSpec != nil: + rpt.ChildPipelineRuns, err = c.createChildPipelineRuns(ctx, rpt, pr, pipelineRunFacts) + if err != nil { + recorder.Eventf(pr, corev1.EventTypeWarning, "ChildPipelineRunsCreationFailed", "Failed to create child (PIP) PipelineRuns %q: %v", rpt.ChildPipelineRunNames, err) + err = fmt.Errorf("error creating child PipelineRuns called %s for PipelineTask %s from PipelineRun %s: %w", rpt.ChildPipelineRunNames, rpt.PipelineTask.Name, pr.Name, err) + return err + } + case rpt.IsCustomTask(): rpt.CustomRuns, err = c.createCustomRuns(ctx, rpt, pr, pipelineRunFacts) if err != nil { recorder.Eventf(pr, corev1.EventTypeWarning, "RunsCreationFailed", "Failed to create CustomRuns %q: %v", rpt.CustomRunNames, err) err = fmt.Errorf("error creating CustomRuns called %s for PipelineTask %s from PipelineRun %s: %w", rpt.CustomRunNames, rpt.PipelineTask.Name, pr.Name, err) return err } - } else { + default: rpt.TaskRuns, err = c.createTaskRuns(ctx, rpt, pr, pipelineRunFacts) if err != nil { recorder.Eventf(pr, corev1.EventTypeWarning, "TaskRunsCreationFailed", "Failed to create TaskRuns %q: %v", rpt.TaskRunNames, err) @@ -983,6 +998,67 @@ func (c *Reconciler) setFinallyStartedTimeIfNeeded(pr *v1.PipelineRun, facts *re } } +func (c *Reconciler) createChildPipelineRuns( + ctx context.Context, + rpt *resources.ResolvedPipelineTask, + pr *v1.PipelineRun, + facts *resources.PipelineRunFacts, +) ([]*v1.PipelineRun, error) { + ctx, span := c.tracerProvider.Tracer(TracerName).Start(ctx, "createChildPipelineRuns") + defer span.End() + + var childPipelineRuns []*v1.PipelineRun + for _, childPipelineRunName := range rpt.ChildPipelineRunNames { + var params v1.Params + childPipelineRun, err := c.createChildPipelineRun(ctx, childPipelineRunName, params, rpt, pr, facts) + if err != nil { + err := c.handleRunCreationError(pr, err) + return nil, err + } + childPipelineRuns = append(childPipelineRuns, childPipelineRun) + } + + return childPipelineRuns, nil +} + +func (c *Reconciler) createChildPipelineRun( + ctx context.Context, + childPipelineRunName string, + params v1.Params, + rpt *resources.ResolvedPipelineTask, + pr *v1.PipelineRun, + facts *resources.PipelineRunFacts, +) (*v1.PipelineRun, error) { + ctx, span := c.tracerProvider.Tracer(TracerName).Start(ctx, "createChildPipelineRun") + defer span.End() + + logger := logging.FromContext(ctx) + rpt.PipelineTask = resources.ApplyPipelineTaskContexts(rpt.PipelineTask, pr.Status, facts) + + newChildPipelineRun := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: childPipelineRunName, + Namespace: pr.Namespace, + OwnerReferences: []metav1.OwnerReference{*kmeta.NewControllerRef(pr)}, + Labels: createChildResourceLabels(pr, rpt.PipelineTask.Name, true), + Annotations: createChildResourceAnnotations(pr), + }, + Spec: v1.PipelineRunSpec{ + PipelineSpec: rpt.PipelineTask.PipelineSpec, + }, + } + + logger.Infof( + "Creating a new child (PIP) PipelineRun object %s for pipeline task %s", + childPipelineRunName, + rpt.PipelineTask.Name, + ) + + return c.PipelineClientSet.TektonV1(). + PipelineRuns(pr.Namespace). + Create(ctx, newChildPipelineRun, metav1.CreateOptions{}) +} + func (c *Reconciler) createTaskRuns(ctx context.Context, rpt *resources.ResolvedPipelineTask, pr *v1.PipelineRun, facts *resources.PipelineRunFacts) ([]*v1.TaskRun, error) { ctx, span := c.tracerProvider.Tracer(TracerName).Start(ctx, "createTaskRuns") defer span.End() @@ -1017,7 +1093,7 @@ func (c *Reconciler) createTaskRuns(ctx context.Context, rpt *resources.Resolved } taskRun, err := c.createTaskRun(ctx, taskRunName, params, rpt, pr, facts) if err != nil { - err := c.handleRunCreationError(ctx, pr, err) + err := c.handleRunCreationError(pr, err) return nil, err } taskRuns = append(taskRuns, taskRun) @@ -1092,12 +1168,13 @@ func (c *Reconciler) createTaskRun(ctx context.Context, taskRunName string, para } // handleRunCreationError marks the PipelineRun as failed and returns a permanent error if the run creation error is not retryable -func (c *Reconciler) handleRunCreationError(ctx context.Context, pr *v1.PipelineRun, err error) error { +func (c *Reconciler) handleRunCreationError(pr *v1.PipelineRun, err error) error { if controller.IsPermanentError(err) { pr.Status.MarkFailed(v1.PipelineRunReasonCreateRunFailed.String(), err.Error()) return err } - // This is not a complete list of permanent errors. Any permanent error with TaskRun/CustomRun creation can be added here. + // This is not a complete list of permanent errors. Any permanent error with child (PinP) + // PipelinRun/TaskRun/CustomRun creation can be added here. if apierrors.IsInvalid(err) || apierrors.IsBadRequest(err) { pr.Status.MarkFailed(v1.PipelineRunReasonCreateRunFailed.String(), err.Error()) return controller.NewPermanentError(err) @@ -1121,7 +1198,7 @@ func (c *Reconciler) createCustomRuns(ctx context.Context, rpt *resources.Resolv } customRun, err := c.createCustomRun(ctx, customRunName, params, rpt, pr, facts) if err != nil { - err := c.handleRunCreationError(ctx, pr, err) + err := c.handleRunCreationError(pr, err) return nil, err } customRuns = append(customRuns, customRun) @@ -1150,8 +1227,8 @@ func (c *Reconciler) createCustomRun(ctx context.Context, runName string, params Name: runName, Namespace: pr.Namespace, OwnerReferences: []metav1.OwnerReference{*kmeta.NewControllerRef(pr)}, - Labels: getTaskrunLabels(pr, rpt.PipelineTask.Name, true), - Annotations: getTaskrunAnnotations(pr), + Labels: createChildResourceLabels(pr, rpt.PipelineTask.Name, true), + Annotations: createChildResourceAnnotations(pr), } // TaskRef, Params and Workspaces are converted to v1beta1 since CustomRuns @@ -1349,8 +1426,8 @@ func combinedSubPath(workspaceSubPath string, pipelineTaskSubPath string) string return filepath.Join(workspaceSubPath, pipelineTaskSubPath) } -func getTaskrunAnnotations(pr *v1.PipelineRun) map[string]string { - // Propagate annotations from PipelineRun to TaskRun. +func createChildResourceAnnotations(pr *v1.PipelineRun) map[string]string { + // propagate annotations from PipelineRun to child (PinP) PipelineRun/TaskRun/CustomRun annotations := make(map[string]string, len(pr.ObjectMeta.Annotations)+1) for key, val := range pr.ObjectMeta.Annotations { annotations[key] = val @@ -1396,10 +1473,10 @@ func propagatePipelineNameLabelToPipelineRun(pr *v1.PipelineRun) error { return nil } -func getTaskrunLabels(pr *v1.PipelineRun, pipelineTaskName string, includePipelineLabels bool) map[string]string { - // Propagate labels from PipelineRun to TaskRun. +func createChildResourceLabels(pr *v1.PipelineRun, pipelineTaskName string, includePipelineRunLabels bool) map[string]string { + // propagate labels from PipelineRun to child (PinP) PipelineRun/TaskRun/CustomRun labels := make(map[string]string, len(pr.ObjectMeta.Labels)+1) - if includePipelineLabels { + if includePipelineRunLabels { for key, val := range pr.ObjectMeta.Labels { labels[key] = val } @@ -1436,7 +1513,7 @@ func combineTaskRunAndTaskSpecLabels(pr *v1.PipelineRun, pipelineTask *v1.Pipeli addMetadataByPrecedence(labels, taskRunSpec.Metadata.Labels) } - addMetadataByPrecedence(labels, getTaskrunLabels(pr, pipelineTask.Name, true)) + addMetadataByPrecedence(labels, createChildResourceLabels(pr, pipelineTask.Name, true)) if pipelineTask.TaskSpec != nil { addMetadataByPrecedence(labels, pipelineTask.TaskSpecMetadata().Labels) @@ -1453,7 +1530,7 @@ func combineTaskRunAndTaskSpecAnnotations(pr *v1.PipelineRun, pipelineTask *v1.P addMetadataByPrecedence(annotations, taskRunSpec.Metadata.Annotations) } - addMetadataByPrecedence(annotations, getTaskrunAnnotations(pr)) + addMetadataByPrecedence(annotations, createChildResourceAnnotations(pr)) if pipelineTask.TaskSpec != nil { addMetadataByPrecedence(annotations, pipelineTask.TaskSpecMetadata().Annotations) @@ -1533,10 +1610,16 @@ func (c *Reconciler) updatePipelineRunStatusFromInformer(ctx context.Context, pr defer span.End() logger := logging.FromContext(ctx) - // Get the pipelineRun label that is set on each TaskRun. Do not include the propagated labels from the - // Pipeline and PipelineRun. The user could change them during the lifetime of the PipelineRun so the + // Get the parent PipelineRun label that is set on each child (PIP) PipelineRun/TaskRun/CustomRun. Do not include the propagated labels from the + // Pipeline and PipelineRun. The user could change them during the lifetime of the PipelineRun so the // current labels may not be set on the previously created TaskRuns. - pipelineRunLabels := getTaskrunLabels(pr, "", false) + pipelineRunLabels := createChildResourceLabels(pr, "", false) + childPipelineRuns, err := c.pipelineRunLister.PipelineRuns(pr.Namespace).List(k8slabels.SelectorFromSet(pipelineRunLabels)) + if err != nil { + logger.Errorf("Could not list PipelineRuns %#v", err) + return err + } + taskRuns, err := c.taskRunLister.TaskRuns(pr.Namespace).List(k8slabels.SelectorFromSet(pipelineRunLabels)) if err != nil { logger.Errorf("Could not list TaskRuns %#v", err) @@ -1548,11 +1631,12 @@ func (c *Reconciler) updatePipelineRunStatusFromInformer(ctx context.Context, pr logger.Errorf("Could not list CustomRuns %#v", err) return err } - return updatePipelineRunStatusFromChildObjects(ctx, logger, pr, taskRuns, customRuns) + + return updatePipelineRunStatusFromChildObjects(ctx, logger, pr, childPipelineRuns, taskRuns, customRuns) } -func updatePipelineRunStatusFromChildObjects(ctx context.Context, logger *zap.SugaredLogger, pr *v1.PipelineRun, taskRuns []*v1.TaskRun, customRuns []*v1beta1.CustomRun) error { - updatePipelineRunStatusFromChildRefs(logger, pr, taskRuns, customRuns) +func updatePipelineRunStatusFromChildObjects(ctx context.Context, logger *zap.SugaredLogger, pr *v1.PipelineRun, childPipelineRuns []*v1.PipelineRun, taskRuns []*v1.TaskRun, customRuns []*v1beta1.CustomRun) error { + updatePipelineRunStatusFromChildRefs(logger, pr, childPipelineRuns, taskRuns, customRuns) return validateChildObjectsInPipelineRunStatus(ctx, pr.Status) } @@ -1562,7 +1646,7 @@ func validateChildObjectsInPipelineRunStatus(ctx context.Context, prs v1.Pipelin for _, cr := range prs.ChildReferences { switch cr.Kind { - case taskRun, customRun: + case taskRun, customRun, pipelineRun: continue default: err = errors.Join(err, fmt.Errorf("child with name %s has unknown kind %s", cr.Name, cr.Kind)) @@ -1572,7 +1656,23 @@ func validateChildObjectsInPipelineRunStatus(ctx context.Context, prs v1.Pipelin return err } -// filterTaskRunsForPipelineRunStatus returns TaskRuns owned by the PipelineRun. +// filterChildPipelineRunsForParentPipelineRunStatus returns child (PIP) PipelineRuns owned by the parent PipelineRun. +func filterChildPipelineRunsForParentPipelineRunStatus(logger *zap.SugaredLogger, pr *v1.PipelineRun, childPipelineRuns []*v1.PipelineRun) []*v1.PipelineRun { + var owned []*v1.PipelineRun + + for _, child := range childPipelineRuns { + // Only process child (PIP) PipelineRuns that are owned by this parent PipelineRun. + // This skips PipelineRuns that are indirectly created by the PipelineRun (e.g. by custom tasks). + if len(child.OwnerReferences) == 0 || child.OwnerReferences[0].UID != pr.ObjectMeta.UID { + logger.Debugf("Found a child (PIP) PipelineRun %s that is not owned by this parent PipelineRun", child.Name) + continue + } + owned = append(owned, child) + } + + return owned +} + func filterTaskRunsForPipelineRunStatus(logger *zap.SugaredLogger, pr *v1.PipelineRun, trs []*v1.TaskRun) []*v1.TaskRun { var ownedTaskRuns []*v1.TaskRun @@ -1618,22 +1718,46 @@ func filterCustomRunsForPipelineRunStatus(logger *zap.SugaredLogger, pr *v1.Pipe return names, taskLabels, gvks, statuses } -func updatePipelineRunStatusFromChildRefs(logger *zap.SugaredLogger, pr *v1.PipelineRun, trs []*v1.TaskRun, customRuns []*v1beta1.CustomRun) { - // If no TaskRun or CustomRun was found, nothing to be done. We never remove child references from the status. +func updatePipelineRunStatusFromChildRefs(logger *zap.SugaredLogger, pr *v1.PipelineRun, childPipelineRuns []*v1.PipelineRun, trs []*v1.TaskRun, customRuns []*v1beta1.CustomRun) { + // If no child (PIP) PipelineRun, TaskRun or CustomRun was found, nothing to be done. We never remove child references from the status. // We do still return an empty map of TaskRun/Run names keyed by PipelineTask name for later functions. - if len(trs) == 0 && len(customRuns) == 0 { + if len(childPipelineRuns) == 0 && len(trs) == 0 && len(customRuns) == 0 { return } - // Map PipelineTask names to TaskRun child references that were already in the status + // Map PipelineTask names to child (PIP) PipelineRun, TaskRun or CustomRun child references that were already in the status childRefByName := make(map[string]*v1.ChildStatusReference) for i := range pr.Status.ChildReferences { childRefByName[pr.Status.ChildReferences[i].Name] = &pr.Status.ChildReferences[i] } - taskRuns := filterTaskRunsForPipelineRunStatus(logger, pr, trs) + filteredChildPipelineRuns := filterChildPipelineRunsForParentPipelineRunStatus(logger, pr, childPipelineRuns) + + // Loop over all the child (PIP) PipelineRuns associated to the parent PipelineRun + for _, fcpr := range filteredChildPipelineRuns { + labels := fcpr.GetLabels() + pipelineTaskName := labels[pipeline.PipelineTaskLabelKey] + + // this child pipeline run is already in the status + if _, ok := childRefByName[fcpr.Name]; ok { + continue + } + + logger.Infof("Found a child (PIP) PipelineRun %s that was missing from the parent PipelineRun status", fcpr.Name) + // Since this was recovered now, add it to the map, or it might be overwritten + childRefByName[fcpr.Name] = &v1.ChildStatusReference{ + TypeMeta: runtime.TypeMeta{ + APIVersion: v1.SchemeGroupVersion.String(), + Kind: pipelineRun, + }, + Name: fcpr.Name, + PipelineTaskName: pipelineTaskName, + } + } + + taskRuns := filterTaskRunsForPipelineRunStatus(logger, pr, trs) // Loop over all the TaskRuns associated to Tasks for _, tr := range taskRuns { lbls := tr.GetLabels() @@ -1658,7 +1782,6 @@ func updatePipelineRunStatusFromChildRefs(logger *zap.SugaredLogger, pr *v1.Pipe // Get the names, their task label values, and their group/version/kind info for all CustomRuns or Runs associated with the PipelineRun names, taskLabels, gvks, _ := filterCustomRunsForPipelineRunStatus(logger, pr, customRuns) - // Loop over that data and populate the child references for idx := range names { name := names[idx] diff --git a/pkg/reconciler/pipelinerun/pipelinerun_updatestatus_test.go b/pkg/reconciler/pipelinerun/pipelinerun_updatestatus_test.go index 0c301c919d2..c397a81a2aa 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun_updatestatus_test.go +++ b/pkg/reconciler/pipelinerun/pipelinerun_updatestatus_test.go @@ -440,7 +440,7 @@ pipelineTaskName: task Status: tc.prStatus, } - updatePipelineRunStatusFromChildRefs(logger, pr, tc.trs, tc.customRuns) + updatePipelineRunStatusFromChildRefs(logger, pr, []*v1.PipelineRun{}, tc.trs, tc.customRuns) actualPrStatus := pr.Status @@ -573,7 +573,7 @@ metadata: Status: tc.prStatus(), } - if err := updatePipelineRunStatusFromChildObjects(ctx, logger, pr, tc.trs, tc.runs); err != nil { + if err := updatePipelineRunStatusFromChildObjects(ctx, logger, pr, []*v1.PipelineRun{}, tc.trs, tc.runs); err != nil { t.Fatalf("received an unexpected error: %v", err) } diff --git a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go index 5d3a9cd5649..542aba5cfdb 100644 --- a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go +++ b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go @@ -61,8 +61,12 @@ func (e *TaskNotFoundError) Error() string { return fmt.Sprintf("Couldn't retrieve Task %q: %s", e.Name, e.Msg) } -// ResolvedPipelineTask contains a PipelineTask and its associated TaskRun(s) or CustomRuns, if they exist. +// ResolvedPipelineTask contains a PipelineTask and its associated child PipelineRun(s) (Pipelines-in-Pipelines), TaskRun(s) or CustomRuns, if they exist. type ResolvedPipelineTask struct { + ChildPipelineRunNames []string + ChildPipelineRuns []*v1.PipelineRun + ResolvedPipeline ResolvedPipeline + TaskRunNames []string TaskRuns []*v1.TaskRun ResolvedTask *resources.ResolvedTask @@ -146,6 +150,20 @@ func (t ResolvedPipelineTask) IsCustomTask() bool { // If the PipelineTask has a Matrix, getReason returns the failure reason for any failure // otherwise, it returns an empty string func (t ResolvedPipelineTask) getReason() string { + if t.PipelineTask.PipelineSpec != nil { + if len(t.ChildPipelineRuns) == 0 { + return "" + } + for _, childPipelineRun := range t.ChildPipelineRuns { + if !childPipelineRun.IsSuccessful() && len(childPipelineRun.Status.Conditions) >= 1 { + return childPipelineRun.Status.Conditions[0].Reason + } + } + if len(t.ChildPipelineRuns) >= 1 && len(t.ChildPipelineRuns[0].Status.Conditions) >= 1 { + return t.ChildPipelineRuns[0].Status.Conditions[0].Reason + } + } + if t.IsCustomTask() { if len(t.CustomRuns) == 0 { return "" @@ -178,6 +196,20 @@ func (t ResolvedPipelineTask) getReason() string { // isSuccessful returns true only if the run has completed successfully // If the PipelineTask has a Matrix, isSuccessful returns true if all runs have completed successfully func (t ResolvedPipelineTask) isSuccessful() bool { + if t.PipelineTask.PipelineSpec != nil { + if len(t.ChildPipelineRuns) == 0 { + return false + } + + for _, childPipelineRun := range t.ChildPipelineRuns { + if !childPipelineRun.IsSuccessful() { + return false + } + } + + return true + } + if t.IsCustomTask() { if len(t.CustomRuns) == 0 { return false @@ -310,8 +342,12 @@ func (t ResolvedPipelineTask) isScheduled() bool { return len(t.TaskRuns) > 0 } -// haveAnyRunsFailed returns true when any of the taskRuns/customRuns have succeeded condition with status set to false +// haveAnyRunsFailed returns true when any of the child (PinP) PipelineRuns/TaskRuns/CustomRuns have succeeded condition with status set to false func (t ResolvedPipelineTask) haveAnyRunsFailed() bool { + if t.PipelineTask.PipelineSpec != nil { + return t.haveAnyChildPipelineRunsFailed() + } + if t.IsCustomTask() { return t.haveAnyCustomRunsFailed() } @@ -319,6 +355,16 @@ func (t ResolvedPipelineTask) haveAnyRunsFailed() bool { return t.haveAnyTaskRunsFailed() } +// haveAnyChildPipelineRunsFailed returns true when any of the child (PinP) PipelineRuns have succeeded condition with status set to false +func (t ResolvedPipelineTask) haveAnyChildPipelineRunsFailed() bool { + for _, childPipelineRun := range t.ChildPipelineRuns { + if childPipelineRun.IsFailure() { + return true + } + } + return false +} + // haveAnyTaskRunsFailed returns true when any of the TaskRuns have succeeded condition with status set to false func (t ResolvedPipelineTask) haveAnyTaskRunsFailed() bool { for _, taskRun := range t.TaskRuns { @@ -594,9 +640,14 @@ func ValidateTaskRunSpecs(p *v1.PipelineSpec, pr *v1.PipelineRun) error { // // If the Pipeline Task is a Custom Task, it retrieves any CustomRuns and updates the ResolvedPipelineTask with this information. // It also sets the ResolvedPipelineTask's RunName(s) with the names of CustomRuns that should be or already have been created. +// +// If the Pipeline Task is a Pipeline, it retrieves any child (PinP) PipelineRuns, plus the Pipeline spec and updates the +// ResolvedPipelineTask with this information. It also sets the ResolvedPipelineTask's ChildPipelineRunName(s) with the names +// of child (PinP) PipelineRuns that should be or already have been created. func ResolvePipelineTask( ctx context.Context, pipelineRun v1.PipelineRun, + getChildPipelineRun GetPipelineRun, getTask resources.GetTask, getTaskRun resources.GetTaskRun, getRun GetRun, @@ -621,7 +672,24 @@ func ResolvePipelineTask( if rpt.PipelineTask.IsMatrixed() { numCombinations = rpt.PipelineTask.Matrix.CountCombinations() } - if rpt.IsCustomTask() { + + switch { + case rpt.PipelineTask.PipelineSpec != nil: + rpt.ChildPipelineRunNames = GetNamesOfChildPipelineRuns( + pipelineRun.Status.ChildReferences, + pipelineTask.Name, + pipelineRun.Name, + numCombinations, + ) + + // happy path: no pipelineRef, no local/remote resolution, no getPipeline + for _, childPipelineRunName := range rpt.ChildPipelineRunNames { + if err := rpt.setChildPipelineRunsAndResolvedPipeline(ctx, childPipelineRunName, getChildPipelineRun, pipelineTask); err != nil { + return nil, err + } + } + + case rpt.IsCustomTask(): rpt.CustomRunNames = getNamesOfCustomRuns(pipelineRun.Status.ChildReferences, pipelineTask.Name, pipelineRun.Name, numCombinations) for _, runName := range rpt.CustomRunNames { run, err := getRun(runName) @@ -632,7 +700,8 @@ func ResolvePipelineTask( rpt.CustomRuns = append(rpt.CustomRuns, run) } } - } else { + + default: rpt.TaskRunNames = GetNamesOfTaskRuns(pipelineRun.Status.ChildReferences, pipelineTask.Name, pipelineRun.Name, numCombinations) for _, taskRunName := range rpt.TaskRunNames { if err := rpt.setTaskRunsAndResolvedTask(ctx, taskRunName, getTask, getTaskRun, pipelineTask); err != nil { @@ -644,6 +713,36 @@ func ResolvePipelineTask( return &rpt, nil } +func (t *ResolvedPipelineTask) setChildPipelineRunsAndResolvedPipeline( + ctx context.Context, + childPipelineRunName string, + getChildPipelineRun GetPipelineRun, + pipelineTask v1.PipelineTask, +) error { + childPipelineRun, err := getChildPipelineRun(childPipelineRunName) + if err != nil { + if !kerrors.IsNotFound(err) { + return fmt.Errorf("error retrieving child PipelineRun %s: %w", childPipelineRunName, err) + } + } + if childPipelineRun != nil { + t.ChildPipelineRuns = append(t.ChildPipelineRuns, childPipelineRun) + } + + rp := ResolvedPipeline{} + switch { + case pipelineTask.PipelineSpec != nil: + rp.PipelineSpec = pipelineTask.PipelineSpec + case pipelineTask.PipelineRef != nil: + return fmt.Errorf("PipelineRef for PipelineTask %q is not yet implemented", pipelineTask.Name) + default: + return fmt.Errorf("PipelineSpec in PipelineTask %q missing", pipelineTask.Name) + } + + t.ResolvedPipeline = rp + return nil +} + // setTaskRunsAndResolvedTask fetches the named TaskRun using the input function getTaskRun, // and the resolved Task spec of the Pipeline Task using the input function getTask. // It updates the ResolvedPipelineTask with the ResolvedTask and a pointer to the fetched TaskRun. @@ -720,7 +819,7 @@ func resolveTask( // If the alpha feature is enabled, and the user has configured pipelineSpec or pipelineRef, it will enter here. // Currently, the controller is not yet adapted, and to avoid a panic, an error message is provided here. // TODO: Adjust the logic here once the feature is supported in the future. - return nil, fmt.Errorf("Currently, Task %q does not support PipelineRef or PipelineSpec, please use TaskRef or TaskSpec instead", pipelineTask.Name) + return nil, fmt.Errorf("Currently, Task %q does not support PipelineRef, please use PipelineSpec, TaskRef or TaskSpec instead", pipelineTask.Name) } rt.TaskSpec.SetDefaults(ctx) return rt, nil @@ -744,6 +843,15 @@ func GetNamesOfTaskRuns(childRefs []v1.ChildStatusReference, ptName, prName stri return getNewRunNames(ptName, prName, numberOfTaskRuns) } +// GetNamesOfChildPipelineRuns should return unique names for child (PinP) `PipelineRuns` if one has not already been +// defined, and the existing one otherwise. +func GetNamesOfChildPipelineRuns(childRefs []v1.ChildStatusReference, ptName, prName string, numberOfPipelineRuns int) []string { + if pipelineRunNames := getChildPipelineRunNamesFromChildRefs(childRefs, ptName); pipelineRunNames != nil { + return pipelineRunNames + } + return getNewRunNames(ptName, prName, numberOfPipelineRuns) +} + // getTaskRunNamesFromChildRefs returns the names of TaskRuns defined in childRefs that are associated with the named Pipeline Task. func getTaskRunNamesFromChildRefs(childRefs []v1.ChildStatusReference, ptName string) []string { var taskRunNames []string @@ -757,25 +865,27 @@ func getTaskRunNamesFromChildRefs(childRefs []v1.ChildStatusReference, ptName st func getNewRunNames(ptName, prName string, numberOfRuns int) []string { var runNames []string - // If it is a singular TaskRun/CustomRun, we only append the ptName + // If it is a singular PipelineRun/TaskRun/CustomRun, we only append the ptName if numberOfRuns == 1 { runName := kmeta.ChildName(prName, "-"+ptName) return append(runNames, runName) } - // For a matrix we append i to the end of the fanned out TaskRuns/CustomRun "matrixed-pr-taskrun-0" + + // For a matrix we append i to the end of the fanned out PipelineRun/TaskRun/CustomRun "matrixed-pr-taskrun-0" for i := range numberOfRuns { runName := kmeta.ChildName(prName, fmt.Sprintf("-%s-%d", ptName, i)) - // check if the taskRun name ends with a matrix instance count + // check if the PipelineRun/TaskRun/CustomRun name ends with a matrix instance count if !strings.HasSuffix(runName, fmt.Sprintf("-%d", i)) { runName = kmeta.ChildName(prName, "-"+ptName) // kmeta.ChildName limits the size of a name to max of 63 characters based on k8s guidelines - // truncate the name such that "-" can be appended to the TaskRun/CustomRun name + // truncate the name such that "-" can be appended to the PipelineRun/TaskRun/CustomRun name longest := 63 - len(fmt.Sprintf("-%d", numberOfRuns)) runName = runName[0:longest] runName = fmt.Sprintf("%s-%d", runName, i) } runNames = append(runNames, runName) } + return runNames } @@ -815,6 +925,19 @@ func getRunNamesFromChildRefs(childRefs []v1.ChildStatusReference, ptName string return runNames } +// ... +func getChildPipelineRunNamesFromChildRefs(childRefs []v1.ChildStatusReference, ptName string) []string { + var childPipelineRunNames []string + for _, cr := range childRefs { + if cr.PipelineTaskName == ptName { + if cr.Kind == pipeline.PipelineRunControllerName { + childPipelineRunNames = append(childPipelineRunNames, cr.Name) + } + } + } + return childPipelineRunNames +} + func (t *ResolvedPipelineTask) hasResultReferences() bool { var matrixParams v1.Params if t.PipelineTask.IsMatrixed() { diff --git a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go index 82c33d30f34..0983062cc6a 100644 --- a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go +++ b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go @@ -59,6 +59,24 @@ func nopGetTaskRun(string) (*v1.TaskRun, error) { return nil, errors.New("GetTaskRun should not be called") } +func nopGetPipelineRun(string) (*v1.PipelineRun, error) { + return nil, errors.New("GetPipelineRun should not be called") +} + +func getTaskFn(vr *trustedresources.VerificationResult, err error) resources.GetTask { + return func(_ context.Context, name string) (*v1.Task, *v1.RefSource, *trustedresources.VerificationResult, error) { + return task, nil, vr, err + } +} + +func getTaskRunFn(tr *v1.TaskRun) resources.GetTaskRun { + return func(name string) (*v1.TaskRun, error) { return tr, nil } +} + +func getRunFn(cr *v1beta1.CustomRun) GetRun { + return func(name string) (*v1beta1.CustomRun, error) { return cr, nil } +} + var pts = []v1.PipelineTask{{ Name: "mytask1", TaskRef: &v1.TaskRef{Name: "task"}, @@ -2382,7 +2400,7 @@ func TestResolvePipelineRun_CustomTask(t *testing.T) { cfg := config.NewStore(logtesting.TestLogger(t)) ctx = cfg.ToContext(ctx) for _, task := range pts { - ps, err := ResolvePipelineTask(ctx, pr, nopGetTask, nopGetTaskRun, getRun, task, nil) + ps, err := ResolvePipelineTask(ctx, pr, nopGetPipelineRun, nopGetTask, nopGetTaskRun, getRun, task, nil) if err != nil { t.Fatalf("ResolvePipelineTask: %v", err) } @@ -2422,10 +2440,8 @@ func TestResolvePipelineRun_PipelineTaskHasNoResources(t *testing.T) { TaskRef: &v1.TaskRef{Name: "task"}, }} - getTask := func(ctx context.Context, name string) (*v1.Task, *v1.RefSource, *trustedresources.VerificationResult, error) { - return task, nil, nil, nil - } - getTaskRun := func(name string) (*v1.TaskRun, error) { return &trs[0], nil } + getTask := getTaskFn(nil, nil) + getTaskRun := getTaskRunFn(&trs[0]) pr := v1.PipelineRun{ ObjectMeta: metav1.ObjectMeta{ Name: "pipelinerun", @@ -2433,7 +2449,7 @@ func TestResolvePipelineRun_PipelineTaskHasNoResources(t *testing.T) { } pipelineState := PipelineRunState{} for _, task := range pts { - ps, err := ResolvePipelineTask(t.Context(), pr, getTask, getTaskRun, nopGetCustomRun, task, nil) + ps, err := ResolvePipelineTask(t.Context(), pr, nopGetPipelineRun, getTask, getTaskRun, nopGetCustomRun, task, nil) if err != nil { t.Errorf("Error getting tasks for fake pipeline %s: %s", p.ObjectMeta.Name, err) } @@ -2460,17 +2476,15 @@ func TestResolvePipelineRun_PipelineTaskHasPipelineRef(t *testing.T) { PipelineRef: &v1.PipelineRef{Name: "pipeline"}, } - getTask := func(ctx context.Context, name string) (*v1.Task, *v1.RefSource, *trustedresources.VerificationResult, error) { - return task, nil, nil, nil - } - getTaskRun := func(name string) (*v1.TaskRun, error) { return &trs[0], nil } + getTask := getTaskFn(nil, nil) + getTaskRun := getTaskRunFn(&trs[0]) pr := v1.PipelineRun{ ObjectMeta: metav1.ObjectMeta{ Name: "pipelinerun", }, } - _, err := ResolvePipelineTask(t.Context(), pr, getTask, getTaskRun, nopGetCustomRun, pt, nil) + _, err := ResolvePipelineTask(t.Context(), pr, nopGetPipelineRun, getTask, getTaskRun, nopGetCustomRun, pt, nil) if err == nil { t.Errorf("Error getting tasks for fake pipeline %s, expected an error but got nil.", p.ObjectMeta.Name) } @@ -2522,7 +2536,7 @@ func TestResolvePipelineRun_TaskDoesntExist(t *testing.T) { }, } for _, pt := range pts { - _, err := ResolvePipelineTask(t.Context(), pr, getTask, getTaskRun, nopGetCustomRun, pt, nil) + _, err := ResolvePipelineTask(t.Context(), pr, nopGetPipelineRun, getTask, getTaskRun, nopGetCustomRun, pt, nil) var tnf *TaskNotFoundError switch { case err == nil: @@ -2556,17 +2570,15 @@ func TestResolvePipelineRun_VerificationFailed(t *testing.T) { }, }} verificationResult := &trustedresources.VerificationResult{VerificationResultType: trustedresources.VerificationError, Err: trustedresources.ErrResourceVerificationFailed} - getTask := func(ctx context.Context, name string) (*v1.Task, *v1.RefSource, *trustedresources.VerificationResult, error) { - return task, nil, verificationResult, nil - } - getTaskRun := func(name string) (*v1.TaskRun, error) { return nil, nil } //nolint:nilnil + getTask := getTaskFn(verificationResult, nil) + getTaskRun := getTaskRunFn(nil) pr := v1.PipelineRun{ ObjectMeta: metav1.ObjectMeta{ Name: "pipelinerun", }, } for _, pt := range pts { - rt, _ := ResolvePipelineTask(t.Context(), pr, getTask, getTaskRun, nopGetCustomRun, pt, nil) + rt, _ := ResolvePipelineTask(t.Context(), pr, nopGetPipelineRun, getTask, getTaskRun, nopGetCustomRun, pt, nil) if d := cmp.Diff(verificationResult, rt.ResolvedTask.VerificationResult, cmpopts.EquateErrors()); d != "" { t.Error(diff.PrintWantGot(d)) } @@ -2579,16 +2591,14 @@ func TestResolvePipelineRun_TransientError(t *testing.T) { TaskRef: &v1.TaskRef{Name: "task"}, } - getTask := func(ctx context.Context, name string) (*v1.Task, *v1.RefSource, *trustedresources.VerificationResult, error) { - return task, nil, nil, apierrors.NewTimeoutError("some dang ol' timeout", 5) - } - getTaskRun := func(name string) (*v1.TaskRun, error) { return nil, nil } //nolint:nilnil + getTask := getTaskFn(nil, apierrors.NewTimeoutError("some dang ol' timeout", 5)) + getTaskRun := getTaskRunFn(nil) pr := v1.PipelineRun{ ObjectMeta: metav1.ObjectMeta{ Name: "pipelinerun", }, } - _, err := ResolvePipelineTask(t.Context(), pr, getTask, getTaskRun, nopGetCustomRun, pt, nil) + _, err := ResolvePipelineTask(t.Context(), pr, nopGetPipelineRun, getTask, getTaskRun, nopGetCustomRun, pt, nil) if !resolutioncommon.IsErrTransient(err) { t.Error("Transient error while getting Task did not result in a transient error") } @@ -2811,15 +2821,13 @@ func TestResolvePipeline_WhenExpressions(t *testing.T) { When: []v1.WhenExpression{ptwe1}, } - getTask := func(_ context.Context, name string) (*v1.Task, *v1.RefSource, *trustedresources.VerificationResult, error) { - return task, nil, nil, nil - } pr := v1.PipelineRun{ ObjectMeta: metav1.ObjectMeta{ Name: "pipelinerun", }, } + getTask := getTaskFn(nil, nil) getTaskRun := func(name string) (*v1.TaskRun, error) { switch name { case "pipelinerun-mytask1-always-true-0": @@ -2831,7 +2839,7 @@ func TestResolvePipeline_WhenExpressions(t *testing.T) { } t.Run("When Expressions exist", func(t *testing.T) { - _, err := ResolvePipelineTask(t.Context(), pr, getTask, getTaskRun, nopGetCustomRun, pt, nil) + _, err := ResolvePipelineTask(t.Context(), pr, nopGetPipelineRun, getTask, getTaskRun, nopGetCustomRun, pt, nil) if err != nil { t.Fatalf("Did not expect error when resolving PipelineRun: %v", err) } @@ -2844,11 +2852,9 @@ func TestIsCustomTask(t *testing.T) { Name: "pipelinerun", }, } - getTask := func(ctx context.Context, name string) (*v1.Task, *v1.RefSource, *trustedresources.VerificationResult, error) { - return task, nil, nil, nil - } - getTaskRun := func(name string) (*v1.TaskRun, error) { return nil, nil } //nolint:nilnil - getRun := func(name string) (*v1beta1.CustomRun, error) { return nil, nil } //nolint:nilnil + getTask := getTaskFn(nil, nil) + getTaskRun := getTaskRunFn(nil) + getRun := getRunFn(nil) for _, tc := range []struct { name string @@ -2933,7 +2939,7 @@ func TestIsCustomTask(t *testing.T) { ctx := t.Context() cfg := config.NewStore(logtesting.TestLogger(t)) ctx = cfg.ToContext(ctx) - rpt, err := ResolvePipelineTask(ctx, pr, getTask, getTaskRun, getRun, tc.pt, nil) + rpt, err := ResolvePipelineTask(ctx, pr, nopGetPipelineRun, getTask, getTaskRun, getRun, tc.pt, nil) if err != nil { t.Fatalf("Did not expect error when resolving PipelineRun: %v", err) } @@ -3614,11 +3620,9 @@ func TestIsMatrixed(t *testing.T) { Name: "pipelinerun", }, } - getTask := func(ctx context.Context, name string) (*v1.Task, *v1.RefSource, *trustedresources.VerificationResult, error) { - return task, nil, nil, nil - } - getTaskRun := func(name string) (*v1.TaskRun, error) { return &trs[0], nil } - getRun := func(name string) (*v1beta1.CustomRun, error) { return &customRuns[0], nil } + getTask := getTaskFn(nil, nil) + getTaskRun := getTaskRunFn(&trs[0]) + getRun := getRunFn(&customRuns[0]) for _, tc := range []struct { name string @@ -3681,7 +3685,7 @@ func TestIsMatrixed(t *testing.T) { }, }) ctx = cfg.ToContext(ctx) - rpt, err := ResolvePipelineTask(ctx, pr, getTask, getTaskRun, getRun, tc.pt, nil) + rpt, err := ResolvePipelineTask(ctx, pr, nopGetPipelineRun, getTask, getTaskRun, getRun, tc.pt, nil) if err != nil { t.Fatalf("Did not expect error when resolving PipelineRun: %v", err) } @@ -3790,11 +3794,9 @@ func TestResolvePipelineRunTask_WithMatrix(t *testing.T) { }, }} - getTask := func(ctx context.Context, name string) (*v1.Task, *v1.RefSource, *trustedresources.VerificationResult, error) { - return task, nil, nil, nil - } + getTask := getTaskFn(nil, nil) getTaskRun := func(name string) (*v1.TaskRun, error) { return taskRunsMap[name], nil } - getRun := func(name string) (*v1beta1.CustomRun, error) { return &customRuns[0], nil } + getRun := getRunFn(&customRuns[0]) for _, tc := range []struct { name string @@ -3840,7 +3842,7 @@ func TestResolvePipelineRunTask_WithMatrix(t *testing.T) { }, }) ctx = cfg.ToContext(ctx) - rpt, err := ResolvePipelineTask(ctx, pr, getTask, getTaskRun, getRun, tc.pt, tc.pst) + rpt, err := ResolvePipelineTask(ctx, pr, nopGetPipelineRun, getTask, getTaskRun, getRun, tc.pt, tc.pst) if err != nil { t.Fatalf("Did not expect error when resolving PipelineRun: %v", err) } @@ -3942,10 +3944,8 @@ func TestResolvePipelineRunTask_WithMatrixedCustomTask(t *testing.T) { }, }} - getTask := func(ctx context.Context, name string) (*v1.Task, *v1.RefSource, *trustedresources.VerificationResult, error) { - return task, nil, nil, nil - } - getTaskRun := func(name string) (*v1.TaskRun, error) { return &trs[0], nil } + getTask := getTaskFn(nil, nil) + getTaskRun := getTaskRunFn(&trs[0]) getRun := func(name string) (*v1beta1.CustomRun, error) { return runsMap[name], nil } for _, tc := range []struct { @@ -4008,7 +4008,7 @@ func TestResolvePipelineRunTask_WithMatrixedCustomTask(t *testing.T) { if tc.getRun == nil { tc.getRun = getRun } - rpt, err := ResolvePipelineTask(ctx, pr, getTask, getTaskRun, tc.getRun, tc.pt, tc.pst) + rpt, err := ResolvePipelineTask(ctx, pr, nopGetPipelineRun, getTask, getTaskRun, tc.getRun, tc.pt, tc.pst) if err != nil { t.Fatalf("Did not expect error when resolving PipelineRun: %v", err) } diff --git a/pkg/reconciler/pipelinerun/resources/pipelinerunstate.go b/pkg/reconciler/pipelinerun/resources/pipelinerunstate.go index 6d107c5bc2f..3ced899ff10 100644 --- a/pkg/reconciler/pipelinerun/resources/pipelinerunstate.go +++ b/pkg/reconciler/pipelinerun/resources/pipelinerunstate.go @@ -123,10 +123,10 @@ func (state PipelineRunState) ToMap() map[string]*ResolvedPipelineTask { return m } -// IsBeforeFirstTaskRun returns true if the PipelineRun has not yet started its first TaskRun +// IsBeforeFirstTaskRun returns true if the PipelineRun has not yet started its first child PipelineRun/TaskRun/CustomRun func (state PipelineRunState) IsBeforeFirstTaskRun() bool { for _, t := range state { - if len(t.CustomRuns) > 0 || len(t.TaskRuns) > 0 { + if len(t.ChildPipelineRuns) > 0 || len(t.CustomRuns) > 0 || len(t.TaskRuns) > 0 { return false } } @@ -144,6 +144,12 @@ func (state PipelineRunState) IsBeforeFirstTaskRun() bool { func (state PipelineRunState) AdjustStartTime(unadjustedStartTime *metav1.Time) *metav1.Time { adjustedStartTime := unadjustedStartTime for _, rpt := range state { + for _, childPipelineRun := range rpt.ChildPipelineRuns { + if childPipelineRun.CreationTimestamp.Time.Before(adjustedStartTime.Time) { + adjustedStartTime = &childPipelineRun.CreationTimestamp + } + } + for _, customRun := range rpt.CustomRuns { creationTime := customRun.GetObjectMeta().GetCreationTimestamp() if creationTime.Time.Before(adjustedStartTime.Time) { @@ -166,6 +172,9 @@ func (state PipelineRunState) AdjustStartTime(unadjustedStartTime *metav1.Time) func (state PipelineRunState) GetTaskRunsResults() map[string][]v1.TaskRunResult { results := make(map[string][]v1.TaskRunResult) for _, rpt := range state { + if rpt.PipelineTask.PipelineSpec != nil { + continue + } if rpt.IsCustomTask() { continue } @@ -248,7 +257,7 @@ func (state PipelineRunState) GetRunsResults() map[string][]v1beta1.CustomRunRes } // GetChildReferences returns a slice of references, including version, kind, name, and pipeline task name, for all -// TaskRuns and Runs in the state. +// child (PIP) PipelineRuns, TaskRuns and Runs in the state. func (facts *PipelineRunFacts) GetChildReferences() []v1.ChildStatusReference { var childRefs []v1.ChildStatusReference @@ -262,6 +271,12 @@ func (facts *PipelineRunFacts) GetChildReferences() []v1.ChildStatusReference { } switch { + case len(rpt.ChildPipelineRuns) != 0: + for _, childPipelineRun := range rpt.ChildPipelineRuns { + if childPipelineRun != nil { + childRefs = append(childRefs, rpt.getChildRefForChildPipelineRun(childPipelineRun)) + } + } case len(rpt.TaskRuns) != 0: for _, taskRun := range rpt.TaskRuns { if taskRun != nil { @@ -277,8 +292,16 @@ func (facts *PipelineRunFacts) GetChildReferences() []v1.ChildStatusReference { return childRefs } -func (t *ResolvedPipelineTask) getDisplayName(customRun *v1beta1.CustomRun, taskRun *v1.TaskRun, c v1.ChildStatusReference) v1.ChildStatusReference { +func (t *ResolvedPipelineTask) getDisplayName(pipelineRun *v1.PipelineRun, customRun *v1beta1.CustomRun, taskRun *v1.TaskRun, c v1.ChildStatusReference) v1.ChildStatusReference { replacements := make(map[string]string) + if pipelineRun != nil { + for _, p := range pipelineRun.Spec.Params { + if p.Value.Type == v1.ParamTypeString { + replacements[fmt.Sprintf("%s.%s", v1.ParamsPrefix, p.Name)] = p.Value.StringVal + } + } + } + if taskRun != nil { for _, p := range taskRun.Spec.Params { if p.Value.Type == v1.ParamTypeString { @@ -323,6 +346,19 @@ func (t *ResolvedPipelineTask) getDisplayName(customRun *v1beta1.CustomRun, task return c } +func (t *ResolvedPipelineTask) getChildRefForChildPipelineRun(pipelineRun *v1.PipelineRun) v1.ChildStatusReference { + c := v1.ChildStatusReference{ + TypeMeta: runtime.TypeMeta{ + APIVersion: v1.SchemeGroupVersion.String(), + Kind: pipeline.PipelineRunControllerName, + }, + Name: pipelineRun.Name, + PipelineTaskName: t.PipelineTask.Name, + WhenExpressions: t.PipelineTask.When, + } + return t.getDisplayName(pipelineRun, nil, nil, c) +} + func (t *ResolvedPipelineTask) getChildRefForRun(customRun *v1beta1.CustomRun) v1.ChildStatusReference { c := v1.ChildStatusReference{ TypeMeta: runtime.TypeMeta{ @@ -333,7 +369,7 @@ func (t *ResolvedPipelineTask) getChildRefForRun(customRun *v1beta1.CustomRun) v PipelineTaskName: t.PipelineTask.Name, WhenExpressions: t.PipelineTask.When, } - return t.getDisplayName(customRun, nil, c) + return t.getDisplayName(nil, customRun, nil, c) } func (t *ResolvedPipelineTask) getChildRefForTaskRun(taskRun *v1.TaskRun) v1.ChildStatusReference { @@ -346,17 +382,17 @@ func (t *ResolvedPipelineTask) getChildRefForTaskRun(taskRun *v1.TaskRun) v1.Chi PipelineTaskName: t.PipelineTask.Name, WhenExpressions: t.PipelineTask.When, } - return t.getDisplayName(nil, taskRun, c) + return t.getDisplayName(nil, nil, taskRun, c) } -// getNextTasks returns a list of tasks which should be executed next i.e. +// getNextTasks returns a list of pipeline tasks which should be executed next i.e. // a list of tasks from candidateTasks which aren't yet indicated in state to be running and // a list of cancelled/failed tasks from candidateTasks which haven't exhausted their retries func (state PipelineRunState) getNextTasks(candidateTasks sets.String) []*ResolvedPipelineTask { tasks := []*ResolvedPipelineTask{} for _, t := range state { if _, ok := candidateTasks[t.PipelineTask.Name]; ok { - if len(t.TaskRuns) == 0 && len(t.CustomRuns) == 0 { + if len(t.TaskRuns) == 0 && len(t.CustomRuns) == 0 && len(t.ChildPipelineRuns) == 0 { tasks = append(tasks, t) } } @@ -484,7 +520,7 @@ func (facts *PipelineRunFacts) IsFinalTaskStarted() bool { } // GetPipelineConditionStatus will return the Condition that the PipelineRun prName should be -// updated with, based on the status of the TaskRuns in state. +// updated with, based on the status of the child (PinP) PipelineRuns/TaskRuns/CustomRuns in state. func (facts *PipelineRunFacts) GetPipelineConditionStatus(ctx context.Context, pr *v1.PipelineRun, logger *zap.SugaredLogger, c clock.PassiveClock) *apis.Condition { // We have 4 different states here: // 1. Timed out -> Failed @@ -560,7 +596,7 @@ func (facts *PipelineRunFacts) GetPipelineConditionStatus(ctx context.Context, p reason = v1.PipelineRunReasonCancelled.String() status = corev1.ConditionFalse } - logger.Infof("All TaskRuns have finished for PipelineRun %s so it has finished", pr.Name) + logger.Infof("All child (PinP) PipelineRuns/TaskRuns/CustomRuns have finished for PipelineRun %s so it has finished", pr.Name) return &apis.Condition{ Type: apis.ConditionSucceeded, Status: status, @@ -622,8 +658,9 @@ func (facts *PipelineRunFacts) GetSkippedTasks() []v1.SkippedTask { return skipped } -// GetPipelineTaskStatus returns the status of a PipelineTask depending on its taskRun -// the checks are implemented such that the finally tasks are requesting status of the dag tasks +// GetPipelineTaskStatus returns the status of a PipelineTask depending on its child (PinP) +// PipelineRun/TaskRun/CustomRun the checks are implemented such that the finally tasks +// are requesting status of the dag tasks func (facts *PipelineRunFacts) GetPipelineTaskStatus() map[string]string { // construct a map of tasks..status and its state tStatus := make(map[string]string) @@ -649,13 +686,15 @@ func (facts *PipelineRunFacts) GetPipelineTaskStatus() map[string]string { // initialize aggregate status of all dag tasks to None aggregateStatus := PipelineTaskStateNone if facts.checkDAGTasksDone() { - // all dag tasks are done, change the aggregate status to succeeded + // all dag pipeline tasks are done, change the aggregate status to succeeded // will reset it to failed/skipped if needed aggregateStatus = v1.PipelineRunReasonSuccessful.String() for _, t := range facts.State { if facts.isDAGTask(t.PipelineTask.Name) { - // if any of the dag task failed, change the aggregate status to failed and return - if !t.IsCustomTask() && t.haveAnyTaskRunsFailed() || t.IsCustomTask() && t.haveAnyCustomRunsFailed() { + // if any of the dag pipeline tasks failed, change the aggregate status to failed and return + if !t.IsCustomTask() && t.haveAnyTaskRunsFailed() || + t.IsCustomTask() && t.haveAnyCustomRunsFailed() || + t.PipelineTask.PipelineSpec != nil && t.haveAnyChildPipelineRunsFailed() { aggregateStatus = v1.PipelineRunReasonFailed.String() break } diff --git a/pkg/reconciler/pipelinerun/resources/pipelinespec.go b/pkg/reconciler/pipelinerun/resources/pipelinespec.go new file mode 100644 index 00000000000..c05525fbfea --- /dev/null +++ b/pkg/reconciler/pipelinerun/resources/pipelinespec.go @@ -0,0 +1,14 @@ +package resources + +import v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + +// ResolvedPipeline contains the data that is needed to execute +// a child (PinP) PipelineRun. +type ResolvedPipeline struct { + PipelineName string + Kind string + PipelineSpec *v1.PipelineSpec +} + +// GetPipelineRun is a function used to retrieve child (PinP) PipelineRuns +type GetPipelineRun func(name string) (*v1.PipelineRun, error) From b13ccfbc111750ff945dd100cc08b96c71f06abd Mon Sep 17 00:00:00 2001 From: Stanislav Jakuschevskij Date: Sun, 6 Jul 2025 15:33:11 +0200 Subject: [PATCH 2/7] refactor(SPR2): remove skipped e2e testcases Remove unused code and helper functions from PipelineRun e2e tests. Fix typos premption => preemption. Issues #8760, #7166. Signed-off-by: Stanislav Jakuschevskij --- test/pipelinerun_test.go | 214 +----------------- .../{premption_test.go => preemption_test.go} | 8 +- 2 files changed, 13 insertions(+), 209 deletions(-) rename test/{premption_test.go => preemption_test.go} (96%) diff --git a/test/pipelinerun_test.go b/test/pipelinerun_test.go index 653058d7354..da2a53516db 100644 --- a/test/pipelinerun_test.go +++ b/test/pipelinerun_test.go @@ -21,7 +21,6 @@ package test import ( "context" - "encoding/base64" "fmt" "strings" "testing" @@ -31,7 +30,6 @@ import ( v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" "github.com/tektoncd/pipeline/test/parse" corev1 "k8s.io/api/core/v1" - k8sres "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "knative.dev/pkg/apis" @@ -201,97 +199,6 @@ func TestPipelineRun(t *testing.T) { expectedTaskRuns: []string{"create-file-kritis", "create-fan-out-1", "create-fan-out-2", "check-fan-in"}, // 1 from PipelineRun and 4 from Tasks defined in pipelinerun expectedNumberOfEvents: 5, - }, { - name: "service account propagation and pipeline param", - testSetup: func(ctx context.Context, t *testing.T, c *clients, namespace string, index int) *v1.Pipeline { - t.Helper() - t.Skip("build-crd-testing project got removed, the secret-sauce doesn't exist anymore, skipping") - if _, err := c.KubeClient.CoreV1().Secrets(namespace).Create(ctx, getPipelineRunSecret(index, namespace), metav1.CreateOptions{}); err != nil { - t.Fatalf("Failed to create secret `%s`: %s", getName(secretName, index), err) - } - - if _, err := c.KubeClient.CoreV1().ServiceAccounts(namespace).Create(ctx, getPipelineRunServiceAccount(index, namespace), metav1.CreateOptions{}); err != nil { - t.Fatalf("Failed to create SA `%s`: %s", getName(saName, index), err) - } - - task := parse.MustParseV1Task(t, fmt.Sprintf(` -metadata: - name: %s - namespace: %s -spec: - params: - - name: the.path - type: string - - name: the.dest - type: string - steps: - - name: config-docker - image: gcr.io/tekton-releases/dogfooding/skopeo:latest - command: ['skopeo'] - args: ['copy', '$(params["the.path"])', '$(params["the.dest"])'] -`, helpers.ObjectNameForTest(t), namespace)) - if _, err := c.V1TaskClient.Create(ctx, task, metav1.CreateOptions{}); err != nil { - t.Fatalf("Failed to create Task `%s`: %s", task.Name, err) - } - p := getHelloWorldPipelineWithSingularTask(t, namespace, task.Name) - if _, err := c.V1PipelineClient.Create(ctx, p, metav1.CreateOptions{}); err != nil { - t.Fatalf("Failed to create Pipeline `%s`: %s", p.Name, err) - } - - return p - }, - expectedTaskRuns: []string{task1Name}, - // 1 from PipelineRun and 1 from Tasks defined in pipelinerun - expectedNumberOfEvents: 2, - pipelineRunFunc: getHelloWorldPipelineRun, - }, { - name: "pipelinerun succeeds with LimitRange minimum in namespace", - testSetup: func(ctx context.Context, t *testing.T, c *clients, namespace string, index int) *v1.Pipeline { - t.Helper() - t.Skip("build-crd-testing project got removed, the secret-sauce doesn't exist anymore, skipping") - if _, err := c.KubeClient.CoreV1().LimitRanges(namespace).Create(ctx, getLimitRange("prlimitrange", namespace, "100m", "99Mi", "100m"), metav1.CreateOptions{}); err != nil { - t.Fatalf("Failed to create LimitRange `%s`: %s", "prlimitrange", err) - } - - if _, err := c.KubeClient.CoreV1().Secrets(namespace).Create(ctx, getPipelineRunSecret(index, namespace), metav1.CreateOptions{}); err != nil { - t.Fatalf("Failed to create secret `%s`: %s", getName(secretName, index), err) - } - - if _, err := c.KubeClient.CoreV1().ServiceAccounts(namespace).Create(ctx, getPipelineRunServiceAccount(index, namespace), metav1.CreateOptions{}); err != nil { - t.Fatalf("Failed to create SA `%s`: %s", getName(saName, index), err) - } - - task := parse.MustParseV1Task(t, fmt.Sprintf(` -metadata: - name: %s - namespace: %s -spec: - params: - - name: the.path - type: string - - name: the.dest - type: string - steps: - - name: config-docker - image: gcr.io/tekton-releases/dogfooding/skopeo:latest - command: ['skopeo'] - args: ['copy', '$(params["the.path"])', '$(params["the.dest"])'] -`, helpers.ObjectNameForTest(t), namespace)) - if _, err := c.V1TaskClient.Create(ctx, task, metav1.CreateOptions{}); err != nil { - t.Fatalf("Failed to create Task `%s`: %s", fmt.Sprint("task", index), err) - } - - p := getHelloWorldPipelineWithSingularTask(t, namespace, task.Name) - if _, err := c.V1PipelineClient.Create(ctx, p, metav1.CreateOptions{}); err != nil { - t.Fatalf("Failed to create Pipeline `%s`: %s", p.Name, err) - } - - return p - }, - expectedTaskRuns: []string{task1Name}, - // 1 from PipelineRun and 1 from Tasks defined in pipelinerun - expectedNumberOfEvents: 2, - pipelineRunFunc: getHelloWorldPipelineRun, }} for i, td := range tds { @@ -364,7 +271,7 @@ spec: collectedEvents += ", " } } - t.Fatalf("Expected %d number of successful events from pipelinerun and taskrun but got %d; list of receieved events : %#v", td.expectedNumberOfEvents, len(events), collectedEvents) + t.Fatalf("Expected %d number of successful events from pipelinerun and taskrun but got %d; list of received events : %#v", td.expectedNumberOfEvents, len(events), collectedEvents) } t.Logf("Successfully finished test %q", td.name) @@ -392,30 +299,6 @@ spec: `, namespace, task1Name, taskName)) } -func getHelloWorldPipelineWithSingularTask(t *testing.T, namespace string, taskName string) *v1.Pipeline { - t.Helper() - return parse.MustParseV1Pipeline(t, fmt.Sprintf(` -metadata: - name: %s - namespace: %s -spec: - params: - - name: the.path - type: string - - name: dest - type: string - tasks: - - name: %s - params: - - name: the.path - value: $(params["the.path"]) - - name: dest - value: $(params.dest) - taskRef: - name: %s -`, helpers.ObjectNameForTest(t), namespace, task1Name, taskName)) -} - // TestPipelineRunRefDeleted tests that a running PipelineRun doesn't fail when the Pipeline // it references is deleted. func TestPipelineRunRefDeleted(t *testing.T) { @@ -690,47 +573,6 @@ spec: `, helpers.ObjectNameForTest(t), namespace, pipelineName)) } -func getPipelineRunServiceAccount(suffix int, namespace string) *corev1.ServiceAccount { - return &corev1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: getName(saName, suffix), - }, - Secrets: []corev1.ObjectReference{{ - Name: getName(secretName, suffix), - }}, - } -} - -func getPipelineRunSecret(suffix int, namespace string) *corev1.Secret { - // Generated by: - // cat /tmp/key.json | base64 -w 0 - // This service account is JUST a storage reader on gcr.io/build-crd-testing - encoedDockercred := "ewogICJ0eXBlIjogInNlcnZpY2VfYWNjb3VudCIsCiAgInByb2plY3RfaWQiOiAiYnVpbGQtY3JkLXRlc3RpbmciLAogICJwcml2YXRlX2tleV9pZCI6ICIwNTAyYTQxYTgxMmZiNjRjZTU2YTY4ZWM1ODMyYWIwYmExMWMxMWU2IiwKICAicHJpdmF0ZV9rZXkiOiAiLS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tXG5NSUlFdlFJQkFEQU5CZ2txaGtpRzl3MEJBUUVGQUFTQ0JLY3dnZ1NqQWdFQUFvSUJBUUM5WDRFWU9BUmJ4UU04XG5EMnhYY2FaVGsrZ1k4ZWp1OTh0THFDUXFUckdNVzlSZVQyeE9ZNUF5Z2FsUFArcDd5WEVja3dCRC9IaE0wZ2xJXG43TVRMZGVlS1dyK3JBMUx3SFp5V0ZXN0gwT25mN3duWUhFSExXVW1jM0JDT1JFRHRIUlo3WnJQQmYxSFRBQS8zXG5Nblc1bFpIU045b2p6U1NGdzZBVnU2ajZheGJCSUlKNzU0THJnS2VBWXVyd2ZJUTJSTFR1MjAxazJJcUxZYmhiXG4zbVNWRzVSK3RiS3oxQ3ZNNTNuSENiN0NmdVZlV3NyQThrazd4SHJyTFFLTW1JOXYyc2dSdWd5TUF6d3ovNnpOXG5oNS9pTXh4Z2VxNVc4eGtWeDNKMm5ZOEpKZEhhZi9UNkFHc09ORW80M3B4ZWlRVmpuUmYvS24xMFRDYzJFc0lZXG5TNDlVc1o3QkFnTUJBQUVDZ2dFQUF1cGxkdWtDUVF1RDVVL2dhbUh0N0dnVzNBTVYxOGVxbkhuQ2EyamxhaCtTXG5BZVVHbmhnSmpOdkUrcE1GbFN2NXVmMnAySzRlZC9veEQ2K0NwOVpYRFJqZ3ZmdEl5cWpsemJ3dkZjZ3p3TnVEXG55Z1VrdXA3SGVjRHNEOFR0ZUFvYlQvVnB3cTZ6S01yQndDdk5rdnk2YlZsb0VqNXgzYlhzYXhlOTVETy95cHU2XG53MFc5N3p4d3dESlk2S1FjSVdNamhyR3h2d1g3bmlVQ2VNNGxlV0JEeUd0dzF6ZUpuNGhFYzZOM2FqUWFjWEtjXG4rNFFseGNpYW1ZcVFXYlBudHhXUWhoUXpjSFdMaTJsOWNGYlpENyt1SkxGNGlONnk4bVZOVTNLM0sxYlJZclNEXG5SVXAzYVVWQlhtRmcrWi8ycHVWTCttVTNqM0xMV1l5Qk9rZXZ1T21kZ1FLQmdRRGUzR0lRa3lXSVMxNFRkTU9TXG5CaUtCQ0R5OGg5NmVoTDBIa0RieU9rU3RQS2RGOXB1RXhaeGh5N29qSENJTTVGVnJwUk4yNXA0c0V6d0ZhYyt2XG5KSUZnRXZxN21YZm1YaVhJTmllUG9FUWFDbm54RHhXZ21yMEhVS0VtUzlvTWRnTGNHVStrQ1ZHTnN6N0FPdW0wXG5LcVkzczIyUTlsUTY3Rk95cWl1OFdGUTdRUUtCZ1FEWmlGaFRFWmtQRWNxWmpud0pwVEI1NlpXUDlLVHNsWlA3XG53VTRiemk2eSttZXlmM01KKzRMMlN5SGMzY3BTTWJqdE5PWkN0NDdiOTA4RlVtTFhVR05oY3d1WmpFUXhGZXkwXG5tNDFjUzVlNFA0OWI5bjZ5TEJqQnJCb3FzMldCYWwyZWdkaE5KU3NDV29pWlA4L1pUOGVnWHZoN2I5MWp6b0syXG5xMlBVbUE0RGdRS0JnQVdMMklqdkVJME95eDJTMTFjbi9lM1dKYVRQZ05QVEc5MDNVcGErcW56aE9JeCtNYXFoXG5QRjRXc3VBeTBBb2dHSndnTkpiTjhIdktVc0VUdkE1d3l5TjM5WE43dzBjaGFyRkwzN29zVStXT0F6RGpuamNzXG5BcTVPN0dQR21YdWI2RUJRQlBKaEpQMXd5NHYvSzFmSGcvRjQ3cTRmNDBMQUpPa2FZUkpENUh6QkFvR0JBTlVoXG5uSUJQSnFxNElNdlE2Y0M5ZzhCKzF4WURlYTkvWWsxdytTbVBHdndyRVh5M0dLeDRLN2xLcGJQejdtNFgzM3N4XG5zRVUvK1kyVlFtd1JhMXhRbS81M3JLN1YybDVKZi9ENDAwalJtNlpmU0FPdmdEVHJ0Wm5VR0pNcno5RTd1Tnc3XG5sZ1VIM0pyaXZ5Ri9meE1JOHFzelFid1hQMCt4bnlxQXhFQWdkdUtCQW9HQUlNK1BTTllXQ1pYeERwU0hJMThkXG5qS2tvQWJ3Mk1veXdRSWxrZXVBbjFkWEZhZDF6c1hRR2RUcm1YeXY3TlBQKzhHWEJrbkJMaTNjdnhUaWxKSVN5XG51Y05yQ01pcU5BU24vZHE3Y1dERlVBQmdqWDE2SkgyRE5GWi9sL1VWRjNOREFKalhDczFYN3lJSnlYQjZveC96XG5hU2xxbElNVjM1REJEN3F4Unl1S3Nnaz1cbi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS1cbiIsCiAgImNsaWVudF9lbWFpbCI6ICJwdWxsLXNlY3JldC10ZXN0aW5nQGJ1aWxkLWNyZC10ZXN0aW5nLmlhbS5nc2VydmljZWFjY291bnQuY29tIiwKICAiY2xpZW50X2lkIjogIjEwNzkzNTg2MjAzMzAyNTI1MTM1MiIsCiAgImF1dGhfdXJpIjogImh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi9hdXRoIiwKICAidG9rZW5fdXJpIjogImh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi90b2tlbiIsCiAgImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6ICJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9vYXV0aDIvdjEvY2VydHMiLAogICJjbGllbnRfeDUwOV9jZXJ0X3VybCI6ICJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9yb2JvdC92MS9tZXRhZGF0YS94NTA5L3B1bGwtc2VjcmV0LXRlc3RpbmclNDBidWlsZC1jcmQtdGVzdGluZy5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIKfQo=" - - decoded, err := base64.StdEncoding.DecodeString(encoedDockercred) - if err != nil { - return nil - } - return &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: getName(secretName, suffix), - Annotations: map[string]string{ - "tekton.dev/docker-0": "https://us.gcr.io", - "tekton.dev/docker-1": "https://eu.gcr.io", - "tekton.dev/docker-2": "https://asia.gcr.io", - "tekton.dev/docker-3": "https://gcr.io", - }, - }, - Type: "kubernetes.io/basic-auth", - Data: map[string][]byte{ - "username": []byte("_json_key"), - "password": decoded, - }, - } -} - func getUpdatedStatusSpecPipelineRun(t *testing.T, _ int, namespace string, pipelineName string) *v1.PipelineRun { t.Helper() return parse.MustParseV1PipelineRun(t, fmt.Sprintf(` @@ -744,38 +586,18 @@ spec: pipelineRef: name: %s `, namespace, pipelineName)) - // `, helpers.ObjectNameForTest(t), namespace, pipelineName)) -} - -func getHelloWorldPipelineRun(t *testing.T, suffix int, namespace string, pipelineName string) *v1.PipelineRun { - t.Helper() - return parse.MustParseV1PipelineRun(t, fmt.Sprintf(` -metadata: - labels: - hello-world-key: hello-world-value - name: %s - namespace: %s -spec: - params: - - name: the.path - value: docker://gcr.io/build-crd-testing/secret-sauce - - name: dest - value: dir:///tmp/ - pipelineRef: - name: %s - taskRunTemplate: - serviceAccountName: %s%d -`, helpers.ObjectNameForTest(t), namespace, pipelineName, saName, suffix)) -} - -func getName(namespace string, suffix int) string { - return fmt.Sprintf("%s%d", namespace, suffix) } // collectMatchingEvents collects list of events under 5 seconds that match // 1. matchKinds which is a map of Kind of Object with name of objects // 2. reason which is the expected reason of event -func collectMatchingEvents(ctx context.Context, kubeClient kubernetes.Interface, namespace string, kinds map[string][]string, reason string) ([]*corev1.Event, error) { +func collectMatchingEvents( + ctx context.Context, + kubeClient kubernetes.Interface, + namespace string, + kinds map[string][]string, + reason string, +) ([]*corev1.Event, error) { var events []*corev1.Event watchEvents, err := kubeClient.CoreV1().Events(namespace).Watch(ctx, metav1.ListOptions{}) @@ -848,7 +670,7 @@ func checkLabelPropagation(ctx context.Context, t *testing.T, c *clients, namesp } assertLabelsMatch(t, labels, tr.ObjectMeta.Labels) - // PodName is "" iff a retry happened and pod is deleted + // PodName is "" if a retry happened and pod is deleted // This label is added to every Pod by the TaskRun controller if tr.Status.PodName != "" { // Check label propagation to Pods. @@ -932,24 +754,6 @@ func assertAnnotationsMatch(t *testing.T, expectedAnnotations, actualAnnotations } } -func getLimitRange(name, namespace, resourceCPU, resourceMemory, resourceEphemeralStorage string) *corev1.LimitRange { - return &corev1.LimitRange{ - ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, - Spec: corev1.LimitRangeSpec{ - Limits: []corev1.LimitRangeItem{ - { - Type: corev1.LimitTypeContainer, - Min: corev1.ResourceList{ - corev1.ResourceCPU: k8sres.MustParse(resourceCPU), - corev1.ResourceMemory: k8sres.MustParse(resourceMemory), - corev1.ResourceEphemeralStorage: k8sres.MustParse(resourceEphemeralStorage), - }, - }, - }, - }, - } -} - func TestPipelineRunTaskFailed(t *testing.T) { ctx := t.Context() ctx, cancel := context.WithCancel(ctx) diff --git a/test/premption_test.go b/test/preemption_test.go similarity index 96% rename from test/premption_test.go rename to test/preemption_test.go index 246549ccf35..e2539348c0b 100644 --- a/test/premption_test.go +++ b/test/preemption_test.go @@ -33,9 +33,9 @@ import ( "knative.dev/pkg/test/helpers" ) -// TestTaskRunPremption tests that Taskrun can run again -// after its pod has been prempted before completion. -func TestTaskRunPremption(t *testing.T) { +// TestTaskRunPreemption tests that Taskrun can run again +// after its pod has been preempted before completion. +func TestTaskRunPreemption(t *testing.T) { ctx := t.Context() ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -85,7 +85,7 @@ spec: type: string default: Good Morning! tasks: - - name: test-pod-premption + - name: test-pod-preemption workspaces: - name: task-ws workspace: ws From f0b84f40af29db41e28d39b3852017d0b11b50ad Mon Sep 17 00:00:00 2001 From: Stanislav Jakuschevskij Date: Wed, 4 Jun 2025 10:09:08 +0200 Subject: [PATCH 3/7] test: add PinP yaml for manual and e2e tests Issues #8760, #7166. Signed-off-by: Stanislav Jakuschevskij --- ...pipelinerun-with-pinp-in-pipelinespec.yaml | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 examples/v1/pipelineruns/alpha/pipelinerun-with-pinp-in-pipelinespec.yaml diff --git a/examples/v1/pipelineruns/alpha/pipelinerun-with-pinp-in-pipelinespec.yaml b/examples/v1/pipelineruns/alpha/pipelinerun-with-pinp-in-pipelinespec.yaml new file mode 100644 index 00000000000..237966629b3 --- /dev/null +++ b/examples/v1/pipelineruns/alpha/pipelinerun-with-pinp-in-pipelinespec.yaml @@ -0,0 +1,23 @@ +apiVersion: tekton.dev/v1 +kind: Pipeline +metadata: + name: pip-parent +spec: + tasks: + - name: pip-child + pipelineSpec: + tasks: + - name: pip-child-task + taskSpec: + steps: + - name: hello-pip + image: mirror.gcr.io/alpine + script: echo "Hello from pip!" +--- +apiVersion: tekton.dev/v1 +kind: PipelineRun +metadata: + generateName: pip-parent-run- +spec: + pipelineRef: + name: pip-parent \ No newline at end of file From 297df3b4d5c911eb498312e7d83f6a477c533b44 Mon Sep 17 00:00:00 2001 From: Stanislav Jakuschevskij Date: Thu, 12 Jun 2025 15:44:49 +0200 Subject: [PATCH 4/7] feature(first PR): add IsChildPipeline with tests Switch to IsChildPipeline method. Fix typos and change wording in docs. Add child pipeline check to `isFailure`. Issues #8760, #7166. --- pkg/reconciler/pipelinerun/pipelinerun.go | 4 +- .../resources/pipelinerunresolution.go | 25 +++++++++-- .../resources/pipelinerunresolution_test.go | 45 +++++++++++++++++++ .../pipelinerun/resources/pipelinerunstate.go | 13 +++--- 4 files changed, 76 insertions(+), 11 deletions(-) diff --git a/pkg/reconciler/pipelinerun/pipelinerun.go b/pkg/reconciler/pipelinerun/pipelinerun.go index 37958c28bad..ac865c1a487 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun.go +++ b/pkg/reconciler/pipelinerun/pipelinerun.go @@ -686,7 +686,7 @@ func (c *Reconciler) reconcile(ctx context.Context, pr *v1.PipelineRun, getPipel for i, rpt := range pipelineRunFacts.State { // Task? - if !rpt.IsCustomTask() && rpt.PipelineTask.PipelineSpec == nil { + if !rpt.IsCustomTask() && !rpt.IsChildPipeline() { err := taskrun.ValidateResolvedTask(ctx, rpt.PipelineTask.Params, rpt.PipelineTask.Matrix, rpt.ResolvedTask) if err != nil { logger.Errorf("Failed to validate pipelinerun %s with error %w", pr.Name, err) @@ -961,7 +961,7 @@ func (c *Reconciler) runNextSchedulableTask(ctx context.Context, pr *v1.Pipeline } switch { - case rpt.PipelineTask.PipelineSpec != nil: + case rpt.IsChildPipeline(): rpt.ChildPipelineRuns, err = c.createChildPipelineRuns(ctx, rpt, pr, pipelineRunFacts) if err != nil { recorder.Eventf(pr, corev1.EventTypeWarning, "ChildPipelineRunsCreationFailed", "Failed to create child (PIP) PipelineRuns %q: %v", rpt.ChildPipelineRunNames, err) diff --git a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go index 542aba5cfdb..14068ecc403 100644 --- a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go +++ b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go @@ -146,11 +146,16 @@ func (t ResolvedPipelineTask) IsCustomTask() bool { return t.CustomTask } +// IsChildPipeline returns true if the PipelineTask references a child (PinP) Pipeline. +func (t ResolvedPipelineTask) IsChildPipeline() bool { + return t.PipelineTask.PipelineSpec != nil +} + // getReason returns the latest reason if the run has completed successfully // If the PipelineTask has a Matrix, getReason returns the failure reason for any failure // otherwise, it returns an empty string func (t ResolvedPipelineTask) getReason() string { - if t.PipelineTask.PipelineSpec != nil { + if t.IsChildPipeline() { if len(t.ChildPipelineRuns) == 0 { return "" } @@ -196,7 +201,7 @@ func (t ResolvedPipelineTask) getReason() string { // isSuccessful returns true only if the run has completed successfully // If the PipelineTask has a Matrix, isSuccessful returns true if all runs have completed successfully func (t ResolvedPipelineTask) isSuccessful() bool { - if t.PipelineTask.PipelineSpec != nil { + if t.IsChildPipeline() { if len(t.ChildPipelineRuns) == 0 { return false } @@ -237,6 +242,17 @@ func (t ResolvedPipelineTask) isSuccessful() bool { // If the PipelineTask has a Matrix, isFailure returns true if any run has failed and all other runs are done. func (t ResolvedPipelineTask) isFailure() bool { var isDone bool + if t.IsChildPipeline() { + if len(t.ChildPipelineRuns) == 0 { + return false + } + isDone = true + for _, childPipelineRun := range t.ChildPipelineRuns { + isDone = isDone && childPipelineRun.IsDone() + } + return t.haveAnyChildPipelineRunsFailed() && isDone + } + if t.IsCustomTask() { if len(t.CustomRuns) == 0 { return false @@ -247,6 +263,7 @@ func (t ResolvedPipelineTask) isFailure() bool { } return t.haveAnyRunsFailed() && isDone } + if len(t.TaskRuns) == 0 { return false } @@ -344,7 +361,7 @@ func (t ResolvedPipelineTask) isScheduled() bool { // haveAnyRunsFailed returns true when any of the child (PinP) PipelineRuns/TaskRuns/CustomRuns have succeeded condition with status set to false func (t ResolvedPipelineTask) haveAnyRunsFailed() bool { - if t.PipelineTask.PipelineSpec != nil { + if t.IsChildPipeline() { return t.haveAnyChildPipelineRunsFailed() } @@ -674,7 +691,7 @@ func ResolvePipelineTask( } switch { - case rpt.PipelineTask.PipelineSpec != nil: + case rpt.IsChildPipeline(): rpt.ChildPipelineRunNames = GetNamesOfChildPipelineRuns( pipelineRun.Status.ChildReferences, pipelineTask.Name, diff --git a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go index 0983062cc6a..9b1e99804ac 100644 --- a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go +++ b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go @@ -2951,6 +2951,51 @@ func TestIsCustomTask(t *testing.T) { } } +func TestIsChildPipeline(t *testing.T) { + pr := v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipelinerun", + }, + } + getTask := getTaskFn(nil, nil) + getTaskRun := getTaskRunFn(nil) + getChildPipelineRun := func(name string) (*v1.PipelineRun, error) { return nil, nil } //nolint:nilnil + + for _, tc := range []struct { + name string + pt v1.PipelineTask + want bool + }{{ + name: "parent pipeline with child pipeline using PipelineSpec", + pt: v1.PipelineTask{ + PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{{Name: "pip-child-task"}}, + }, + }, + want: true, + }, { + name: "pipeline with task using TaskSpec", + pt: v1.PipelineTask{ + TaskSpec: &v1.EmbeddedTask{}, + }, + want: false, + }} { + t.Run(tc.name, func(t *testing.T) { + ctx := t.Context() + cfg := config.NewStore(logtesting.TestLogger(t)) + ctx = cfg.ToContext(ctx) + rpt, err := ResolvePipelineTask(ctx, pr, getChildPipelineRun, getTask, getTaskRun, nopGetCustomRun, tc.pt, nil) + if err != nil { + t.Fatalf("Did not expect error when resolving PipelineRun: %v", err) + } + got := rpt.IsChildPipeline() + if d := cmp.Diff(tc.want, got); d != "" { + t.Errorf("IsChildPipeline: %s", diff.PrintWantGot(d)) + } + }) + } +} + func TestResolvedPipelineRunTask_IsFinallySkipped(t *testing.T) { tr := &v1.TaskRun{ ObjectMeta: metav1.ObjectMeta{ diff --git a/pkg/reconciler/pipelinerun/resources/pipelinerunstate.go b/pkg/reconciler/pipelinerun/resources/pipelinerunstate.go index 3ced899ff10..510112b4386 100644 --- a/pkg/reconciler/pipelinerun/resources/pipelinerunstate.go +++ b/pkg/reconciler/pipelinerun/resources/pipelinerunstate.go @@ -172,7 +172,7 @@ func (state PipelineRunState) AdjustStartTime(unadjustedStartTime *metav1.Time) func (state PipelineRunState) GetTaskRunsResults() map[string][]v1.TaskRunResult { results := make(map[string][]v1.TaskRunResult) for _, rpt := range state { - if rpt.PipelineTask.PipelineSpec != nil { + if rpt.IsChildPipeline() { continue } if rpt.IsCustomTask() { @@ -198,6 +198,9 @@ func (state PipelineRunState) GetTaskRunsResults() map[string][]v1.TaskRunResult func (state PipelineRunState) GetTaskRunsArtifacts() map[string]*v1.Artifacts { results := make(map[string]*v1.Artifacts) for _, rpt := range state { + if rpt.IsChildPipeline() { + continue + } if rpt.IsCustomTask() { continue } @@ -257,7 +260,7 @@ func (state PipelineRunState) GetRunsResults() map[string][]v1beta1.CustomRunRes } // GetChildReferences returns a slice of references, including version, kind, name, and pipeline task name, for all -// child (PIP) PipelineRuns, TaskRuns and Runs in the state. +// child (PinP) PipelineRuns, TaskRuns and Runs in the state. func (facts *PipelineRunFacts) GetChildReferences() []v1.ChildStatusReference { var childRefs []v1.ChildStatusReference @@ -659,8 +662,8 @@ func (facts *PipelineRunFacts) GetSkippedTasks() []v1.SkippedTask { } // GetPipelineTaskStatus returns the status of a PipelineTask depending on its child (PinP) -// PipelineRun/TaskRun/CustomRun the checks are implemented such that the finally tasks -// are requesting status of the dag tasks +// PipelineRun/TaskRun/CustomRun. The checks are implemented such that the finally tasks +// are requesting status of the dag tasks. func (facts *PipelineRunFacts) GetPipelineTaskStatus() map[string]string { // construct a map of tasks..status and its state tStatus := make(map[string]string) @@ -694,7 +697,7 @@ func (facts *PipelineRunFacts) GetPipelineTaskStatus() map[string]string { // if any of the dag pipeline tasks failed, change the aggregate status to failed and return if !t.IsCustomTask() && t.haveAnyTaskRunsFailed() || t.IsCustomTask() && t.haveAnyCustomRunsFailed() || - t.PipelineTask.PipelineSpec != nil && t.haveAnyChildPipelineRunsFailed() { + t.IsChildPipeline() && t.haveAnyChildPipelineRunsFailed() { aggregateStatus = v1.PipelineRunReasonFailed.String() break } From 65c3511943454f042dbbad19cfc6309d10ecffb9 Mon Sep 17 00:00:00 2001 From: Stanislav Jakuschevskij Date: Thu, 12 Jun 2025 15:47:05 +0200 Subject: [PATCH 5/7] test(second PR): add state tests and mock data Add mock PipelineRun factory and PipelineRun condition transition functions. Now that it also includes child PipelineRuns in its check additional tests with mock data were added. Add `getNextTasks` tests. Now that `DAGExecutionQueue` downstream takes child (PinP) pipelines into account, additional tests were added. Now that `GetPipelineConditionStatus` downstream takes child (PinP) pipelines into account, additional test cases with necessary mock data were added. Now that `AdjustStartTime` takes child (PinP) pipelines into account, additional tests cases were added. Now that `GetPipelineTaskStatus` takes child (PinP) pipelines into account, additional test cases were added. `GetTaskRunsResults` now also skips over child (PinP) pipelines when getting the TaskRun results. One test case was added to test skipping the child `PipelineRun`. Now that `GetChildReferences` takes child (PinP) into account, additional tests were added. Issues #8760, #7166. Signed-off-by: Stanislav Jakuschevskij --- .../resources/pipelinerunresolution_test.go | 183 ++++++ .../resources/pipelinerunstate_test.go | 572 +++++++++++++++--- 2 files changed, 663 insertions(+), 92 deletions(-) diff --git a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go index 9b1e99804ac..3230ad146b8 100644 --- a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go +++ b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go @@ -198,6 +198,38 @@ var pts = []v1.PipelineTask{{ Value: v1.ParamValue{ArrayVal: []string{"safari", "chrome"}}, }}, }, +}, { + Name: "mytask22", + PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{{ + Name: "pip-child-task", + TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{ + Steps: []v1.Step{{ + Name: "hello-pip", + Image: "mirror.gcr.io/alpine", + Script: "echo \"Hello from mytask22-pip-child-task!\"", + }}, + }, + }, + }}, + }, +}, { + Name: "mytask23", + PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{{ + Name: "pip-child-task", + TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{ + Steps: []v1.Step{{ + Name: "goodbye-pip", + Image: "mirror.gcr.io/alpine", + Script: "echo \"Goodbye from mytask23-pip-child-task!\"", + }}, + }, + }, + }}, + }, }} var p = &v1.Pipeline{ @@ -255,6 +287,20 @@ var customRuns = []v1beta1.CustomRun{{ Spec: v1beta1.CustomRunSpec{}, }} +var prs = []v1.PipelineRun{{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "namespace", + Name: "pipelinerun-mytask22", + }, + Spec: v1.PipelineRunSpec{}, +}, { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "namespace", + Name: "pipelinerun-mytask23", + }, + Spec: v1.PipelineRunSpec{}, +}} + var matrixedPipelineTask = &v1.PipelineTask{ Name: "task", TaskSpec: &v1.EmbeddedTask{ @@ -287,6 +333,12 @@ func makeScheduled(tr v1.TaskRun) *v1.TaskRun { return newTr } +func makePipelineRunScheduled(pr v1.PipelineRun) *v1.PipelineRun { + newPr := newPipelineRun(pr) + newPr.Status = v1.PipelineRunStatus{ /* explicitly empty */ } + return newPr +} + func makeStarted(tr v1.TaskRun) *v1.TaskRun { newTr := newTaskRun(tr) newTr.Status.Conditions[0].Status = corev1.ConditionUnknown @@ -299,6 +351,12 @@ func makeCustomRunStarted(run v1beta1.CustomRun) *v1beta1.CustomRun { return newRun } +func makePipelineRunStarted(pr v1.PipelineRun) *v1.PipelineRun { + newPr := newPipelineRun(pr) + newPr.Status.Conditions[0].Status = corev1.ConditionUnknown + return newPr +} + func makeSucceeded(tr v1.TaskRun) *v1.TaskRun { newTr := newTaskRun(tr) newTr.Status.Conditions[0].Status = corev1.ConditionTrue @@ -313,6 +371,13 @@ func makeCustomRunSucceeded(run v1beta1.CustomRun) *v1beta1.CustomRun { return newRun } +func makePipelineRunSucceeded(pr v1.PipelineRun) *v1.PipelineRun { + newPr := newPipelineRun(pr) + newPr.Status.Conditions[0].Status = corev1.ConditionTrue + newPr.Status.Conditions[0].Reason = "Succeeded" + return newPr +} + func makeFailed(tr v1.TaskRun) *v1.TaskRun { newTr := newTaskRun(tr) newTr.Status.Conditions[0].Status = corev1.ConditionFalse @@ -334,6 +399,13 @@ func makeCustomRunFailed(run v1beta1.CustomRun) *v1beta1.CustomRun { return newRun } +func makePipelineRunFailed(pr v1.PipelineRun) *v1.PipelineRun { + newPr := newPipelineRun(pr) + newPr.Status.Conditions[0].Status = corev1.ConditionFalse + newPr.Status.Conditions[0].Reason = "Failed" + return newPr +} + func withCancelled(tr *v1.TaskRun) *v1.TaskRun { tr.Status.Conditions[0].Reason = v1.TaskRunSpecStatusCancelled return tr @@ -431,6 +503,21 @@ func newCustomRun(run v1beta1.CustomRun) *v1beta1.CustomRun { } } +func newPipelineRun(pr v1.PipelineRun) *v1.PipelineRun { + return &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: pr.Namespace, + Name: pr.Name, + }, + Spec: pr.Spec, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{ + Conditions: []apis.Condition{{Type: apis.ConditionSucceeded}}, + }, + }, + } +} + var noneStartedState = PipelineRunState{{ PipelineTask: &pts[0], TaskRunNames: []string{"pipelinerun-mytask1"}, @@ -731,6 +818,102 @@ var taskCancelledMatrix = PipelineRunState{{ }, }} +var noneStartedChildPipelineRunState = PipelineRunState{{ + PipelineTask: &pts[21], + ChildPipelineRunNames: []string{"pipelinerun-mytask22"}, + ChildPipelineRuns: nil, + ResolvedPipeline: ResolvedPipeline{ + PipelineSpec: pts[21].PipelineSpec, + }, +}, { + PipelineTask: &pts[22], + ChildPipelineRunNames: []string{"pipelinerun-mytask23"}, + ChildPipelineRuns: nil, + ResolvedPipeline: ResolvedPipeline{ + PipelineSpec: pts[22].PipelineSpec, + }, +}} + +var oneChildPipelineRunStartedState = PipelineRunState{{ + PipelineTask: &pts[21], + ChildPipelineRunNames: []string{"pipelinerun-mytask22"}, + ChildPipelineRuns: []*v1.PipelineRun{makePipelineRunStarted(prs[0])}, + ResolvedPipeline: ResolvedPipeline{ + PipelineSpec: pts[21].PipelineSpec, + }, +}, { + PipelineTask: &pts[22], + ChildPipelineRunNames: []string{"pipelinerun-mytask23"}, + ChildPipelineRuns: nil, + ResolvedPipeline: ResolvedPipeline{ + PipelineSpec: pts[22].PipelineSpec, + }, +}} + +var oneChildPipelineRunFinishedState = PipelineRunState{{ + PipelineTask: &pts[21], + ChildPipelineRunNames: []string{"pipelinerun-mytask22"}, + ChildPipelineRuns: []*v1.PipelineRun{makePipelineRunSucceeded(prs[0])}, + ResolvedPipeline: ResolvedPipeline{ + PipelineSpec: pts[21].PipelineSpec, + }, +}, { + PipelineTask: &pts[22], + ChildPipelineRunNames: []string{"pipelinerun-mytask23"}, + ChildPipelineRuns: nil, + ResolvedPipeline: ResolvedPipeline{ + PipelineSpec: pts[22].PipelineSpec, + }, +}} + +var oneChildPipelineRunFailedState = PipelineRunState{{ + PipelineTask: &pts[21], + ChildPipelineRunNames: []string{"pipelinerun-mytask22"}, + ChildPipelineRuns: []*v1.PipelineRun{makePipelineRunFailed(prs[0])}, + ResolvedPipeline: ResolvedPipeline{ + PipelineSpec: pts[21].PipelineSpec, + }, +}, { + PipelineTask: &pts[22], + ChildPipelineRunNames: []string{"pipelinerun-mytask23"}, + ChildPipelineRuns: nil, + ResolvedPipeline: ResolvedPipeline{ + PipelineSpec: pts[22].PipelineSpec, + }, +}} + +var allChildPipelineRunsFinishedState = PipelineRunState{{ + PipelineTask: &pts[21], + ChildPipelineRunNames: []string{"pipelinerun-mytask22"}, + ChildPipelineRuns: []*v1.PipelineRun{makePipelineRunSucceeded(prs[0])}, + ResolvedPipeline: ResolvedPipeline{ + PipelineSpec: pts[21].PipelineSpec, + }, +}, { + PipelineTask: &pts[22], + ChildPipelineRunNames: []string{"pipelinerun-mytask23"}, + ChildPipelineRuns: []*v1.PipelineRun{makePipelineRunSucceeded(prs[1])}, + ResolvedPipeline: ResolvedPipeline{ + PipelineSpec: pts[22].PipelineSpec, + }, +}} + +var finalChildPipelineRunsScheduledState = PipelineRunState{{ + PipelineTask: &pts[21], + ChildPipelineRunNames: []string{"pipelinerun-mytask22"}, + ChildPipelineRuns: []*v1.PipelineRun{makePipelineRunSucceeded(prs[0])}, + ResolvedPipeline: ResolvedPipeline{ + PipelineSpec: pts[21].PipelineSpec, + }, +}, { + PipelineTask: &pts[22], + ChildPipelineRunNames: []string{"pipelinerun-mytask23"}, + ChildPipelineRuns: []*v1.PipelineRun{makePipelineRunScheduled(prs[1])}, + ResolvedPipeline: ResolvedPipeline{ + PipelineSpec: pts[22].PipelineSpec, + }, +}} + func dagFromState(state PipelineRunState) (*dag.Graph, error) { pts := []v1.PipelineTask{} for _, rpt := range state { diff --git a/pkg/reconciler/pipelinerun/resources/pipelinerunstate_test.go b/pkg/reconciler/pipelinerun/resources/pipelinerunstate_test.go index 491658b62d9..8944ae86636 100644 --- a/pkg/reconciler/pipelinerun/resources/pipelinerunstate_test.go +++ b/pkg/reconciler/pipelinerun/resources/pipelinerunstate_test.go @@ -281,6 +281,24 @@ func TestIsBeforeFirstTaskRun_WithSucceededMatrixedTask(t *testing.T) { } } +func TestIsBeforeFirstTaskRun_WithNotStartedChildPipeline(t *testing.T) { + if !noneStartedChildPipelineRunState.IsBeforeFirstTaskRun() { + t.Fatalf("Expected state to be before first child pipelinerun") + } +} + +func TestIsBeforeFirstTaskRun_WithStartedChildPipeline(t *testing.T) { + if oneChildPipelineRunStartedState.IsBeforeFirstTaskRun() { + t.Fatalf("Expected state to be after first child pipelinerun") + } +} + +func TestIsBeforeFirstTaskRun_WithFinalScheduledChildPipeline(t *testing.T) { + if finalChildPipelineRunsScheduledState.IsBeforeFirstTaskRun() { + t.Fatalf("Expected state to be after first child pipelinerun") + } +} + func TestGetNextTasks(t *testing.T) { tcs := []struct { name string @@ -427,6 +445,21 @@ func TestGetNextTasks(t *testing.T) { state: oneCustomRunFailedState, candidates: sets.NewString("mytask13", "mytask14"), expectedNext: []*ResolvedPipelineTask{oneCustomRunFailedState[1]}, + }, { + name: "no-child-pipelineruns-started-both-candidates", + state: noneStartedChildPipelineRunState, + candidates: sets.NewString("mytask22", "mytask23"), + expectedNext: []*ResolvedPipelineTask{noneStartedChildPipelineRunState[0], noneStartedChildPipelineRunState[1]}, + }, { + name: "one-child-pipelinerun-started-both-candidates", + state: oneChildPipelineRunStartedState, + candidates: sets.NewString("mytask22", "mytask23"), + expectedNext: []*ResolvedPipelineTask{oneChildPipelineRunStartedState[1]}, + }, { + name: "one-child-pipelinerun-failed-both-candidates", + state: oneChildPipelineRunFailedState, + candidates: sets.NewString("mytask22", "mytask23"), + expectedNext: []*ResolvedPipelineTask{oneChildPipelineRunFailedState[1]}, }, { name: "no-tasks-started-no-candidates-matrix", state: noneStartedStateMatrix, @@ -895,6 +928,16 @@ func TestDAGExecutionQueue(t *testing.T) { CustomRunNames: []string{"createdrun"}, CustomTask: true, } + createdChildPipeline := ResolvedPipelineTask{ + PipelineTask: &v1.PipelineTask{ + Name: "createdchildpipeline", + PipelineSpec: &v1.PipelineSpec{Tasks: []v1.PipelineTask{{Name: "childpipeline"}}}, + }, + ChildPipelineRunNames: []string{"createdchildpipeline"}, + ResolvedPipeline: ResolvedPipeline{ + PipelineSpec: p.Spec.Tasks[21].PipelineSpec, + }, + } runningTask := ResolvedPipelineTask{ PipelineTask: &v1.PipelineTask{ Name: "runningtask", @@ -915,6 +958,17 @@ func TestDAGExecutionQueue(t *testing.T) { CustomRuns: []*v1beta1.CustomRun{newCustomRun(customRuns[0])}, CustomTask: true, } + runningChildPipeline := ResolvedPipelineTask{ + PipelineTask: &v1.PipelineTask{ + Name: "runningchildpipeline", + PipelineSpec: &v1.PipelineSpec{Tasks: []v1.PipelineTask{{Name: "childpipeline"}}}, + }, + ChildPipelineRunNames: []string{"runningchildpipeline"}, + ChildPipelineRuns: []*v1.PipelineRun{newPipelineRun(prs[0])}, + ResolvedPipeline: ResolvedPipeline{ + PipelineSpec: p.Spec.Tasks[21].PipelineSpec, + }, + } successfulTask := ResolvedPipelineTask{ PipelineTask: &v1.PipelineTask{ Name: "successfultask", @@ -935,6 +989,17 @@ func TestDAGExecutionQueue(t *testing.T) { CustomRuns: []*v1beta1.CustomRun{makeCustomRunSucceeded(customRuns[0])}, CustomTask: true, } + successfulChildPipeline := ResolvedPipelineTask{ + PipelineTask: &v1.PipelineTask{ + Name: "successfulchildpipeline", + PipelineSpec: &v1.PipelineSpec{Tasks: []v1.PipelineTask{{Name: "childpipeline"}}}, + }, + ChildPipelineRunNames: []string{"successfulchildpipeline"}, + ChildPipelineRuns: []*v1.PipelineRun{makePipelineRunSucceeded(prs[0])}, + ResolvedPipeline: ResolvedPipeline{ + PipelineSpec: p.Spec.Tasks[21].PipelineSpec, + }, + } failedTask := ResolvedPipelineTask{ PipelineTask: &v1.PipelineTask{ Name: "failedtask", @@ -955,6 +1020,17 @@ func TestDAGExecutionQueue(t *testing.T) { CustomRuns: []*v1beta1.CustomRun{makeCustomRunFailed(customRuns[0])}, CustomTask: true, } + failedChildPipeline := ResolvedPipelineTask{ + PipelineTask: &v1.PipelineTask{ + Name: "failedchildpipeline", + PipelineSpec: &v1.PipelineSpec{Tasks: []v1.PipelineTask{{Name: "childpipeline"}}}, + }, + ChildPipelineRunNames: []string{"failedchildpipeline"}, + ChildPipelineRuns: []*v1.PipelineRun{makePipelineRunFailed(prs[0])}, + ResolvedPipeline: ResolvedPipeline{ + PipelineSpec: p.Spec.Tasks[21].PipelineSpec, + }, + } tcs := []struct { name string state PipelineRunState @@ -964,38 +1040,48 @@ func TestDAGExecutionQueue(t *testing.T) { name: "cancelled", specStatus: v1.PipelineRunSpecStatusCancelled, state: PipelineRunState{ - &createdTask, &createdRun, - &runningTask, &runningRun, &successfulTask, &successfulRun, + &createdTask, &createdRun, &createdChildPipeline, + &runningTask, &runningRun, &runningChildPipeline, + &successfulTask, &successfulRun, &successfulChildPipeline, }, }, { name: "gracefully cancelled", specStatus: v1.PipelineRunSpecStatusCancelledRunFinally, state: PipelineRunState{ - &createdTask, &createdRun, - &runningTask, &runningRun, &successfulTask, &successfulRun, + &createdTask, &createdRun, &createdChildPipeline, + &runningTask, &runningRun, &runningChildPipeline, + &successfulTask, &successfulRun, &successfulChildPipeline, }, }, { name: "gracefully stopped", specStatus: v1.PipelineRunSpecStatusStoppedRunFinally, state: PipelineRunState{ - &createdTask, &createdRun, &runningTask, &runningRun, &successfulTask, &successfulRun, + &createdTask, &createdRun, &createdChildPipeline, + &runningTask, &runningRun, &runningChildPipeline, + &successfulTask, &successfulRun, &successfulChildPipeline, }, }, { name: "running", state: PipelineRunState{ - &createdTask, &createdRun, &runningTask, &runningRun, - &successfulTask, &successfulRun, + &createdTask, &createdRun, &createdChildPipeline, + &runningTask, &runningRun, &runningChildPipeline, + &successfulTask, &successfulRun, &successfulChildPipeline, }, - want: PipelineRunState{&createdTask, &createdRun}, + want: PipelineRunState{&createdTask, &createdRun, &createdChildPipeline}, }, { name: "stopped", state: PipelineRunState{ - &createdTask, &createdRun, &runningTask, &runningRun, - &successfulTask, &successfulRun, &failedTask, &failedCustomRun, + &createdTask, &createdRun, &createdChildPipeline, + &runningTask, &runningRun, &runningChildPipeline, + &successfulTask, &successfulRun, &successfulChildPipeline, + &failedTask, &failedCustomRun, &failedChildPipeline, }, }, { - name: "all tasks finished", - state: PipelineRunState{&successfulTask, &successfulRun, &failedTask, &failedCustomRun}, + name: "all tasks finished", + state: PipelineRunState{ + &successfulTask, &successfulRun, &successfulChildPipeline, + &failedTask, &failedCustomRun, &failedChildPipeline, + }, }} for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { @@ -1213,6 +1299,105 @@ func TestDAGExecutionQueueSequentialRuns(t *testing.T) { } } +// TestDAGExecutionQueueSequentialChildPipelines tests the DAGExecutionQueue function for sequential child +// PipelineRuns in different states for a running or stopping PipelineRun. +func TestDAGExecutionQueueSequentialChildPipelines(t *testing.T) { + tcs := []struct { + name string + firstChildPipelineRun *v1.PipelineRun + secondChildPipelineRun *v1.PipelineRun + specStatus v1.PipelineRunSpecStatus + wantFirst bool + wantSecond bool + }{{ + name: "not started", + wantFirst: true, + }, { + name: "first child pipeline running", + firstChildPipelineRun: newPipelineRun(prs[0]), + }, { + name: "first child pipeline succeeded", + firstChildPipelineRun: makePipelineRunSucceeded(prs[0]), + wantSecond: true, + }, { + name: "first child pipeline failed", + firstChildPipelineRun: makePipelineRunFailed(prs[0]), + }, { + name: "first child pipeline succeeded, second child pipeline running", + firstChildPipelineRun: makePipelineRunSucceeded(prs[0]), + secondChildPipelineRun: newPipelineRun(prs[1]), + }, { + name: "first child pipeline succeeded, second child pipeline succeeded", + firstChildPipelineRun: makePipelineRunSucceeded(prs[0]), + secondChildPipelineRun: makePipelineRunSucceeded(prs[1]), + }, { + name: "first child pipeline succeeded, second child pipeline failed", + firstChildPipelineRun: makePipelineRunSucceeded(prs[0]), + secondChildPipelineRun: makePipelineRunFailed(prs[1]), + }} + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + firstTask := ResolvedPipelineTask{ + PipelineTask: &v1.PipelineTask{ + Name: "pip-child-1", + PipelineSpec: &v1.PipelineSpec{Tasks: []v1.PipelineTask{{Name: "pip-child-1-task"}}}, + }, + ChildPipelineRunNames: []string{"pip-child-1"}, + ResolvedPipeline: ResolvedPipeline{ + PipelineSpec: p.Spec.Tasks[21].PipelineSpec, + }, + } + + secondTask := ResolvedPipelineTask{ + PipelineTask: &v1.PipelineTask{ + Name: "pip-child-2", + PipelineSpec: &v1.PipelineSpec{Tasks: []v1.PipelineTask{{Name: "pip-child-2-task"}}}, + RunAfter: []string{"pip-child-1"}, + }, + ChildPipelineRunNames: []string{"pip-child-2"}, + ResolvedPipeline: ResolvedPipeline{ + PipelineSpec: p.Spec.Tasks[22].PipelineSpec, + }, + } + + if tc.firstChildPipelineRun != nil { + firstTask.ChildPipelineRuns = append(firstTask.ChildPipelineRuns, tc.firstChildPipelineRun) + } + if tc.secondChildPipelineRun != nil { + secondTask.ChildPipelineRuns = append(secondTask.ChildPipelineRuns, tc.secondChildPipelineRun) + } + state := PipelineRunState{&firstTask, &secondTask} + d, err := dagFromState(state) + if err != nil { + t.Fatalf("Unexpected error while building DAG for state %v: %v", state, err) + } + facts := PipelineRunFacts{ + State: state, + SpecStatus: tc.specStatus, + TasksGraph: d, + FinalTasksGraph: &dag.Graph{}, + TimeoutsState: PipelineRunTimeoutsState{ + Clock: testClock, + }, + } + queue, err := facts.DAGExecutionQueue() + if err != nil { + t.Errorf("unexpected error getting DAG execution queue but got error %s", err) + } + var expectedQueue PipelineRunState + if tc.wantFirst && &firstTask != nil { + expectedQueue = append(expectedQueue, &firstTask) + } + if tc.wantSecond && &secondTask != nil { + expectedQueue = append(expectedQueue, &secondTask) + } + if d := cmp.Diff(expectedQueue, queue, cmpopts.EquateEmpty()); d != "" { + t.Errorf("Didn't get expected execution queue: %s", diff.PrintWantGot(d)) + } + }) + } +} + func TestPipelineRunState_CompletedOrSkippedDAGTasks(t *testing.T) { largePipelineState := buildPipelineStateWithLargeDependencyGraph(t) tcs := []struct { @@ -1618,7 +1803,7 @@ func TestPipelineRunState_IsFinalTaskStarted(t *testing.T) { func TestGetPipelineConditionStatus(t *testing.T) { var taskRetriedState = PipelineRunState{{ PipelineTask: &pts[3], // 1 retry needed - TaskRunNames: []string{"pipelinerun-mytask1"}, + TaskRunNames: []string{"pipelinerun-mytask4"}, TaskRuns: []*v1.TaskRun{withCancelled(makeRetried(trs[0]))}, ResolvedTask: &resources.ResolvedTask{ TaskSpec: &task.Spec, @@ -1627,19 +1812,19 @@ func TestGetPipelineConditionStatus(t *testing.T) { var taskCancelledFailed = PipelineRunState{{ PipelineTask: &pts[4], - TaskRunNames: []string{"pipelinerun-mytask1"}, + TaskRunNames: []string{"pipelinerun-mytask5"}, TaskRuns: []*v1.TaskRun{withCancelled(makeFailed(trs[0]))}, }} var taskCancelledFailedTimedOut = PipelineRunState{{ PipelineTask: &pts[4], - TaskRunNames: []string{"pipelinerun-mytask1"}, + TaskRunNames: []string{"pipelinerun-mytask5"}, TaskRuns: []*v1.TaskRun{withCancelledForTimeout(makeFailed(trs[0]))}, }} var cancelledTask = PipelineRunState{{ - PipelineTask: &pts[3], // 1 retry needed - TaskRunNames: []string{"pipelinerun-mytask1"}, + PipelineTask: &pts[3], + TaskRunNames: []string{"pipelinerun-mytask4"}, TaskRuns: []*v1.TaskRun{{ Status: v1.TaskRunStatus{ Status: duckv1.Status{Conditions: []apis.Condition{{ @@ -1673,7 +1858,7 @@ func TestGetPipelineConditionStatus(t *testing.T) { var timedOutRun = PipelineRunState{{ PipelineTask: &pts[12], CustomTask: true, - CustomRunNames: []string{"pipelinerun-mytask14"}, + CustomRunNames: []string{"pipelinerun-mytask13"}, CustomRuns: []*v1beta1.CustomRun{ { Spec: v1beta1.CustomRunSpec{ @@ -1692,15 +1877,12 @@ func TestGetPipelineConditionStatus(t *testing.T) { var notRunningRun = PipelineRunState{{ PipelineTask: &pts[12], CustomTask: true, - CustomRunNames: []string{"pipelinerun-mytask14"}, + CustomRunNames: []string{"pipelinerun-mytask13"}, }} - // 6 Tasks, 4 that run in parallel in the beginning - // Of the 4, 1 passed, 1 cancelled, 2 failed - // 1 runAfter the passed one, currently running - // 1 runAfter the failed one, which is marked as incomplete - var taskMultipleFailuresSkipRunning = PipelineRunState{{ - TaskRunNames: []string{"task0taskrun"}, + // 3 Tasks, 1 successful, 1 running, 1 failed + var tasksWithOneFailureSkipRunning = PipelineRunState{{ + TaskRunNames: []string{"successfulTaskRun"}, PipelineTask: &pts[5], TaskRuns: []*v1.TaskRun{makeSucceeded(trs[0])}, }, { @@ -1713,11 +1895,11 @@ func TestGetPipelineConditionStatus(t *testing.T) { TaskRuns: []*v1.TaskRun{makeFailed(trs[0])}, }} - var taskMultipleFailuresOneCancel = taskMultipleFailuresSkipRunning - taskMultipleFailuresOneCancel = append(taskMultipleFailuresOneCancel, cancelledTask[0]) + var tasksWithOneFailureOneCancel = tasksWithOneFailureSkipRunning + tasksWithOneFailureOneCancel = append(tasksWithOneFailureOneCancel, cancelledTask[0]) - var taskNotRunningWithSuccesfulParentsOneFailed = PipelineRunState{{ - TaskRunNames: []string{"task0taskrun"}, + var taskNotRunningWithSuccessfulParentsOneFailed = PipelineRunState{{ + TaskRunNames: []string{"successfulTaskRun"}, PipelineTask: &pts[5], TaskRuns: []*v1.TaskRun{makeSucceeded(trs[0])}, }, { @@ -1828,8 +2010,8 @@ func TestGetPipelineConditionStatus(t *testing.T) { expectedStatus: corev1.ConditionFalse, expectedFailed: 1, }, { - name: "task with multiple failures", - state: taskMultipleFailuresSkipRunning, + name: "tasks with one failed task", + state: tasksWithOneFailureSkipRunning, expectedReason: v1.PipelineRunReasonStopping.String(), expectedStatus: corev1.ConditionUnknown, expectedSucceeded: 1, @@ -1838,8 +2020,8 @@ func TestGetPipelineConditionStatus(t *testing.T) { expectedCancelled: 0, expectedSkipped: 0, }, { - name: "task with multiple failures; one cancelled", - state: taskMultipleFailuresOneCancel, + name: "tasks with one failed task; one cancelled", + state: tasksWithOneFailureOneCancel, expectedReason: v1.PipelineRunReasonStopping.String(), expectedStatus: corev1.ConditionUnknown, expectedSucceeded: 1, @@ -1849,7 +2031,7 @@ func TestGetPipelineConditionStatus(t *testing.T) { expectedSkipped: 0, }, { name: "task not started with passed parent; one failed", - state: taskNotRunningWithSuccesfulParentsOneFailed, + state: taskNotRunningWithSuccessfulParentsOneFailed, expectedReason: v1.PipelineRunReasonFailed.String(), expectedStatus: corev1.ConditionFalse, expectedSucceeded: 1, @@ -1883,6 +2065,38 @@ func TestGetPipelineConditionStatus(t *testing.T) { expectedStatus: corev1.ConditionFalse, expectedReason: v1.PipelineRunReasonFailed.String(), expectedSkipped: 1, + }, { + name: "no-child-pipelines-started", + state: noneStartedChildPipelineRunState, + expectedStatus: corev1.ConditionUnknown, + expectedReason: v1.PipelineRunReasonRunning.String(), + expectedIncomplete: 2, + }, { + name: "one-child-pipeline-started", + state: oneChildPipelineRunStartedState, + expectedStatus: corev1.ConditionUnknown, + expectedReason: v1.PipelineRunReasonRunning.String(), + expectedIncomplete: 2, + }, { + name: "one-child-pipeline-finished", + state: oneChildPipelineRunFinishedState, + expectedStatus: corev1.ConditionUnknown, + expectedReason: v1.PipelineRunReasonRunning.String(), + expectedSucceeded: 1, + expectedIncomplete: 1, + }, { + name: "one-child-pipeline-failed", + state: oneChildPipelineRunFailedState, + expectedStatus: corev1.ConditionFalse, + expectedReason: v1.PipelineRunReasonFailed.String(), + expectedFailed: 1, + expectedSkipped: 1, + }, { + name: "all-child-pipelines-finished", + state: allChildPipelineRunsFinishedState, + expectedStatus: corev1.ConditionTrue, + expectedReason: v1.PipelineRunReasonSuccessful.String(), + expectedSucceeded: 2, }} for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { @@ -2261,7 +2475,7 @@ func TestAdjustStartTime(t *testing.T) { // We expect this to adjust to the earlier time. want: baseline.Time.Add(-1 * time.Second), }, { - name: "multiple taskruns, some earlier", + name: "multiple taskruns each in a separate state, some earlier", prs: PipelineRunState{{ TaskRuns: []*v1.TaskRun{{ ObjectMeta: metav1.ObjectMeta{ @@ -2289,7 +2503,7 @@ func TestAdjustStartTime(t *testing.T) { // We expect this to adjust to the earlier time. want: baseline.Time.Add(-2 * time.Second), }, { - name: "multiple taskruns, some earlier", + name: "multiple taskruns in one state, some earlier", prs: PipelineRunState{{ TaskRuns: []*v1.TaskRun{{ ObjectMeta: metav1.ObjectMeta{ @@ -2359,6 +2573,53 @@ func TestAdjustStartTime(t *testing.T) { }}, // We expect this to adjust to the earlier time. want: baseline.Time.Add(-1 * time.Second), + }, { + name: "child PipelineRun starts earlier", + prs: PipelineRunState{{ + ChildPipelineRuns: []*v1.PipelineRun{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "blah", + CreationTimestamp: metav1.Time{Time: baseline.Time.Add(-1 * time.Second)}, + }, + }}, + }}, + // We expect this to adjust to the earlier time. + want: baseline.Time.Add(-1 * time.Second), + }, { + name: "child PipelineRun starts later", + prs: PipelineRunState{{ + ChildPipelineRuns: []*v1.PipelineRun{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "blah", + CreationTimestamp: metav1.Time{Time: baseline.Time.Add(1 * time.Second)}, + }, + }}, + }}, + // Stay where you are, you are before the Run. + want: baseline.Time, + }, { + name: "multiple child PipelineRuns, some earlier", + prs: PipelineRunState{{ + ChildPipelineRuns: []*v1.PipelineRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "blah1", + CreationTimestamp: metav1.Time{Time: baseline.Time.Add(-1 * time.Second)}, + }, + }, { + ObjectMeta: metav1.ObjectMeta{ + Name: "blah2", + CreationTimestamp: metav1.Time{Time: baseline.Time.Add(-2 * time.Second)}, + }, + }, { + ObjectMeta: metav1.ObjectMeta{ + Name: "blah3", + CreationTimestamp: metav1.Time{Time: baseline.Time.Add(2 * time.Second)}, + }, + }}, + }}, + // We expect this to adjust to the earlier time. + want: baseline.Time.Add(-2 * time.Second), }} for _, test := range tests { @@ -2519,6 +2780,61 @@ func TestPipelineRunFacts_GetPipelineTaskStatus(t *testing.T) { PipelineTaskStatusPrefix + pts[10].Name + PipelineTaskReasonSuffix: "", v1.PipelineTasksAggregateStatus: v1.PipelineRunReasonFailed.String(), }, + }, { + name: "no-child-pipelines-started", + state: noneStartedChildPipelineRunState, + dagTasks: []v1.PipelineTask{pts[21], pts[22]}, + expectedStatus: map[string]string{ + PipelineTaskStatusPrefix + pts[21].Name + PipelineTaskStatusSuffix: PipelineTaskStateNone, + PipelineTaskStatusPrefix + pts[21].Name + PipelineTaskReasonSuffix: "", + PipelineTaskStatusPrefix + pts[22].Name + PipelineTaskStatusSuffix: PipelineTaskStateNone, + PipelineTaskStatusPrefix + pts[22].Name + PipelineTaskReasonSuffix: "", + v1.PipelineTasksAggregateStatus: PipelineTaskStateNone, + }, + }, { + name: "one-child-pipeline-started", + state: oneChildPipelineRunStartedState, + dagTasks: []v1.PipelineTask{pts[21], pts[22]}, + expectedStatus: map[string]string{ + PipelineTaskStatusPrefix + pts[21].Name + PipelineTaskStatusSuffix: PipelineTaskStateNone, + PipelineTaskStatusPrefix + pts[21].Name + PipelineTaskReasonSuffix: "", + PipelineTaskStatusPrefix + pts[22].Name + PipelineTaskStatusSuffix: PipelineTaskStateNone, + PipelineTaskStatusPrefix + pts[22].Name + PipelineTaskReasonSuffix: "", + v1.PipelineTasksAggregateStatus: PipelineTaskStateNone, + }, + }, { + name: "one-child-pipeline-finished", + state: oneChildPipelineRunFinishedState, + dagTasks: []v1.PipelineTask{pts[21], pts[22]}, + expectedStatus: map[string]string{ + PipelineTaskStatusPrefix + pts[21].Name + PipelineTaskStatusSuffix: v1.PipelineRunReasonSuccessful.String(), + PipelineTaskStatusPrefix + pts[21].Name + PipelineTaskReasonSuffix: "Succeeded", + PipelineTaskStatusPrefix + pts[22].Name + PipelineTaskStatusSuffix: PipelineTaskStateNone, + PipelineTaskStatusPrefix + pts[22].Name + PipelineTaskReasonSuffix: "", + v1.PipelineTasksAggregateStatus: PipelineTaskStateNone, + }, + }, { + name: "one-child-pipeline-failed", + state: oneChildPipelineRunFailedState, + dagTasks: []v1.PipelineTask{pts[21], pts[22]}, + expectedStatus: map[string]string{ + PipelineTaskStatusPrefix + pts[21].Name + PipelineTaskStatusSuffix: v1.PipelineRunReasonFailed.String(), + PipelineTaskStatusPrefix + pts[21].Name + PipelineTaskReasonSuffix: "Failed", + PipelineTaskStatusPrefix + pts[22].Name + PipelineTaskStatusSuffix: PipelineTaskStateNone, + PipelineTaskStatusPrefix + pts[22].Name + PipelineTaskReasonSuffix: "", + v1.PipelineTasksAggregateStatus: v1.PipelineRunReasonFailed.String(), + }, + }, { + name: "all-child-pipelines-finished", + state: allChildPipelineRunsFinishedState, + dagTasks: []v1.PipelineTask{pts[21], pts[22]}, + expectedStatus: map[string]string{ + PipelineTaskStatusPrefix + pts[21].Name + PipelineTaskStatusSuffix: v1.PipelineRunReasonSuccessful.String(), + PipelineTaskStatusPrefix + pts[21].Name + PipelineTaskReasonSuffix: "Succeeded", + PipelineTaskStatusPrefix + pts[22].Name + PipelineTaskStatusSuffix: v1.PipelineRunReasonSuccessful.String(), + PipelineTaskStatusPrefix + pts[22].Name + PipelineTaskReasonSuffix: "Succeeded", + v1.PipelineTasksAggregateStatus: v1.PipelineRunReasonSuccessful.String(), + }, }} for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { @@ -3146,6 +3462,22 @@ func TestPipelineRunState_GetResultsFuncs(t *testing.T) { }, }, }, + }, { + ChildPipelineRunNames: []string{"successful-child-pipeline-without-results"}, + PipelineTask: &v1.PipelineTask{ + Name: "successful-child-pipeline-without-results-1", + PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{{Name: "pip-child"}}, + }}, + ChildPipelineRuns: []*v1.PipelineRun{{ + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{Conditions: []apis.Condition{{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }}}, + PipelineRunStatusFields: v1.PipelineRunStatusFields{}, + }, + }}, }} expectedTaskResults := map[string][]v1.TaskRunResult{ @@ -3490,6 +3822,92 @@ func TestPipelineRunState_GetTaskRunsArtifacts(t *testing.T) { } func TestPipelineRunState_GetChildReferences(t *testing.T) { + resolvedTask := &ResolvedPipelineTask{ + TaskRunNames: []string{"single-task-run"}, + PipelineTask: &v1.PipelineTask{ + Name: "single-task-1", + DisplayName: "Single Task 1", + TaskRef: &v1.TaskRef{ + Name: "single-task", + Kind: "Task", + APIVersion: "v1", + }}, + TaskRuns: []*v1.TaskRun{{ + TypeMeta: metav1.TypeMeta{APIVersion: "tekton.dev/v1"}, + ObjectMeta: metav1.ObjectMeta{Name: "single-task-run"}, + }}, + } + childRefForTask := v1.ChildStatusReference{ + TypeMeta: runtime.TypeMeta{ + APIVersion: "tekton.dev/v1", + Kind: "TaskRun", + }, + Name: "single-task-run", + PipelineTaskName: "single-task-1", + DisplayName: "Single Task 1", + } + + resolvedCustomTask := &ResolvedPipelineTask{ + CustomRunNames: []string{"single-custom-task-run"}, + CustomTask: true, + PipelineTask: &v1.PipelineTask{ + Name: "single-custom-task-1", + DisplayName: "Single Custom Task 1", + TaskRef: &v1.TaskRef{ + APIVersion: "example.dev/v0", + Kind: "Example", + Name: "single-custom-task", + }}, + CustomRuns: []*v1beta1.CustomRun{{ + TypeMeta: metav1.TypeMeta{APIVersion: "tekton.dev/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{Name: "single-custom-task-run"}, + }}, + } + childRefForCustomTask := v1.ChildStatusReference{ + TypeMeta: runtime.TypeMeta{ + APIVersion: "tekton.dev/v1beta1", + Kind: "CustomRun", + }, + Name: "single-custom-task-run", + PipelineTaskName: "single-custom-task-1", + DisplayName: "Single Custom Task 1", + } + + resolvedChildPipeline := &ResolvedPipelineTask{ + ChildPipelineRunNames: []string{"single-child-pipeline-run"}, + PipelineTask: &v1.PipelineTask{ + Name: "single-child-pipeline-1", + DisplayName: "Human readable name for single-child-pipeline-1 with $(params.foobar)", + PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{{ + Name: "single-child-pipeline-1-task", + }}}, + Params: v1.Params{{ + Name: "foobar", + Value: v1.ParamValue{Type: "string", StringVal: "foo"}, + }}, + }, + ChildPipelineRuns: []*v1.PipelineRun{{ + TypeMeta: metav1.TypeMeta{APIVersion: "tekton.dev/v1"}, + ObjectMeta: metav1.ObjectMeta{Name: "single-child-pipeline-run"}, + Spec: v1.PipelineRunSpec{ + Params: v1.Params{{ + Name: "foobar", + Value: v1.ParamValue{Type: "string", StringVal: "foo"}, + }}, + }, + }}, + } + childRefForChildPipeline := v1.ChildStatusReference{ + TypeMeta: runtime.TypeMeta{ + APIVersion: "tekton.dev/v1", + Kind: "PipelineRun", + }, + Name: "single-child-pipeline-run", + PipelineTaskName: "single-child-pipeline-1", + DisplayName: "Human readable name for single-child-pipeline-1 with foo", + } + testCases := []struct { name string state PipelineRunState @@ -3569,8 +3987,7 @@ func TestPipelineRunState_GetChildReferences(t *testing.T) { Values: []string{"foo", "bar"}, }}, }}, - }, - { + }, { name: "single-custom-task", state: PipelineRunState{{ CustomRunNames: []string{"single-custom-task-run"}, @@ -3609,61 +4026,32 @@ func TestPipelineRunState_GetChildReferences(t *testing.T) { Values: []string{"foo", "bar"}, }}, }}, - }, - { + }, { + name: "single-child-pipeline", + state: PipelineRunState{resolvedChildPipeline}, + childRefs: []v1.ChildStatusReference{childRefForChildPipeline}, + }, { name: "task-and-custom-task", - state: PipelineRunState{{ - TaskRunNames: []string{"single-task-run"}, - PipelineTask: &v1.PipelineTask{ - Name: "single-task-1", - DisplayName: "Single Task 1", - TaskRef: &v1.TaskRef{ - Name: "single-task", - Kind: "Task", - APIVersion: "v1", - }, - }, - TaskRuns: []*v1.TaskRun{{ - TypeMeta: metav1.TypeMeta{APIVersion: "tekton.dev/v1"}, - ObjectMeta: metav1.ObjectMeta{Name: "single-task-run"}, - }}, - }, { - CustomRunNames: []string{"single-custom-task-run"}, - CustomTask: true, - PipelineTask: &v1.PipelineTask{ - Name: "single-custom-task-1", - DisplayName: "Single Custom Task 1", - TaskRef: &v1.TaskRef{ - APIVersion: "example.dev/v0", - Kind: "Example", - Name: "single-custom-task", - }, - }, - CustomRuns: []*v1beta1.CustomRun{ - { - TypeMeta: metav1.TypeMeta{APIVersion: "tekton.dev/v1beta1"}, - ObjectMeta: metav1.ObjectMeta{Name: "single-custom-task-run"}, - }}, - }}, - childRefs: []v1.ChildStatusReference{{ - TypeMeta: runtime.TypeMeta{ - APIVersion: "tekton.dev/v1", - Kind: "TaskRun", - }, - Name: "single-task-run", - PipelineTaskName: "single-task-1", - DisplayName: "Single Task 1", - }, { - TypeMeta: runtime.TypeMeta{ - APIVersion: "tekton.dev/v1beta1", - Kind: "CustomRun", - }, - Name: "single-custom-task-run", - PipelineTaskName: "single-custom-task-1", - DisplayName: "Single Custom Task 1", - }}, - }, - { + state: PipelineRunState{ + resolvedTask, + resolvedCustomTask, + }, + childRefs: []v1.ChildStatusReference{ + childRefForTask, + childRefForCustomTask, + }}, { + name: "task-and-custom-task-and-child-pipeline", + state: PipelineRunState{ + resolvedTask, + resolvedCustomTask, + resolvedChildPipeline, + }, + childRefs: []v1.ChildStatusReference{ + childRefForTask, + childRefForCustomTask, + childRefForChildPipeline, + }, + }, { name: "unresolved-matrixed-task", state: PipelineRunState{{ TaskRunNames: []string{"task-run-0", "task-run-1", "task-run-2", "task-run-3"}, From fe8ac03514c87980cba8359c9951ddb5264e982b Mon Sep 17 00:00:00 2001 From: Stanislav Jakuschevskij Date: Tue, 17 Jun 2025 11:52:06 +0200 Subject: [PATCH 6/7] test(first PR): add `PipelineRun` resolution tests Now that `getReason` takes child (PinP) into account, additional tests were added. This commit does not contain tests for a matrixed task. Those will be added once the matrix feature for child (PinP) PipelineRuns is implemented. Now that `isFailure` takes child (PinP) into account, additional tests were added. Now that `isSuccessful` takes child (PinP) into account, additional tests were added. Test that the pipeline state contains the resolved child (PinP) pipeline , the correct child `PipelineRun` name and the child `PipelineRun` if there is any. Issues #8760, #7166. Signed-off-by: Stanislav Jakuschevskij --- .../resources/pipelinerunresolution_test.go | 195 +++++++++++++++++- 1 file changed, 194 insertions(+), 1 deletion(-) diff --git a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go index 3230ad146b8..a10a51f1f3b 100644 --- a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go +++ b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution_test.go @@ -1470,6 +1470,12 @@ func TestIsFailure(t *testing.T) { CustomTask: true, }, want: false, + }, { + name: "child pipelinerun not started", + rpt: ResolvedPipelineTask{ + PipelineTask: &pts[21], + }, + want: false, }, { name: "taskrun running", rpt: ResolvedPipelineTask{ @@ -1485,6 +1491,13 @@ func TestIsFailure(t *testing.T) { CustomRuns: []*v1beta1.CustomRun{makeCustomRunStarted(customRuns[0])}, }, want: false, + }, { + name: "child pipelinerun running", + rpt: ResolvedPipelineTask{ + PipelineTask: &pts[21], + ChildPipelineRuns: []*v1.PipelineRun{makePipelineRunStarted(prs[0])}, + }, + want: false, }, { name: "taskrun succeeded", rpt: ResolvedPipelineTask{ @@ -1500,6 +1513,13 @@ func TestIsFailure(t *testing.T) { CustomRuns: []*v1beta1.CustomRun{makeCustomRunSucceeded(customRuns[0])}, }, want: false, + }, { + name: "child pipelinerun succeeded", + rpt: ResolvedPipelineTask{ + PipelineTask: &pts[21], + ChildPipelineRuns: []*v1.PipelineRun{makePipelineRunSucceeded(prs[0])}, + }, + want: false, }, { name: "taskrun failed", rpt: ResolvedPipelineTask{ @@ -1515,6 +1535,13 @@ func TestIsFailure(t *testing.T) { CustomRuns: []*v1beta1.CustomRun{makeCustomRunFailed(customRuns[0])}, }, want: true, + }, { + name: "child pipelinerun failed", + rpt: ResolvedPipelineTask{ + PipelineTask: &pts[21], + ChildPipelineRuns: []*v1.PipelineRun{makePipelineRunFailed(prs[0])}, + }, + want: true, }, { name: "run failed: retries remaining", rpt: ResolvedPipelineTask{ @@ -2611,6 +2638,53 @@ func TestResolvePipelineRun_CustomTask(t *testing.T) { } } +func TestResolvePipelineRun_ChildPipeline(t *testing.T) { + cfg := config.NewStore(logtesting.TestLogger(t)) + ctx := cfg.ToContext(t.Context()) + + parentPr := v1.PipelineRun{ObjectMeta: metav1.ObjectMeta{Name: "pipelinerun"}} + pinpPts := []v1.PipelineTask{pts[21], pts[22]} + getChildPipelineRun := func(name string) (*v1.PipelineRun, error) { + if name == "pipelinerun-mytask23" { + return &prs[1], nil + } + return nil, kerrors.NewNotFound(v1.Resource("pipelinerun"), name) + } + expectedState := PipelineRunState{{ + PipelineTask: &pinpPts[0], + ChildPipelineRunNames: []string{"pipelinerun-mytask22"}, + ChildPipelineRuns: nil, + ResolvedPipeline: ResolvedPipeline{PipelineSpec: pinpPts[0].PipelineSpec}, + }, { + PipelineTask: &pinpPts[1], + ChildPipelineRunNames: []string{"pipelinerun-mytask23"}, + ChildPipelineRuns: []*v1.PipelineRun{&prs[1]}, + ResolvedPipeline: ResolvedPipeline{PipelineSpec: pinpPts[1].PipelineSpec}, + }} + actualState := PipelineRunState{} + + for _, pipelineTask := range pinpPts { + ps, err := ResolvePipelineTask( + ctx, + parentPr, + getChildPipelineRun, + nopGetTask, + nopGetTaskRun, + nopGetCustomRun, + pipelineTask, + nil, + ) + if err != nil { + t.Fatalf("ResolvePipelineTask: %v", err) + } + actualState = append(actualState, ps) + } + + if d := cmp.Diff(expectedState, actualState); d != "" { + t.Errorf("Unexpected pipeline state: %s", diff.PrintWantGot(d)) + } +} + func TestResolvePipelineRun_PipelineTaskHasNoResources(t *testing.T) { pts := []v1.PipelineTask{{ Name: "mytask1", @@ -2671,7 +2745,7 @@ func TestResolvePipelineRun_PipelineTaskHasPipelineRef(t *testing.T) { if err == nil { t.Errorf("Error getting tasks for fake pipeline %s, expected an error but got nil.", p.ObjectMeta.Name) } - if !strings.Contains(err.Error(), "does not support PipelineRef or PipelineSpec") { + if !strings.Contains(err.Error(), "does not support PipelineRef, please use PipelineSpec, TaskRef or TaskSpec instead") { t.Errorf("Error getting tasks for fake pipeline %s: expected contains keyword but got %s", p.ObjectMeta.Name, err) } } @@ -3842,6 +3916,62 @@ func TestGetRunName(t *testing.T) { } } +func TestGetNamesOfChildPipelineRuns(t *testing.T) { + prName := "mypipelinerun" + childRefs := []v1.ChildStatusReference{{ + TypeMeta: runtime.TypeMeta{Kind: "PipelineRun"}, + Name: "mypipelinerun-mychildpipelinetask", + PipelineTaskName: "mychildpipelinetask", + }} + + for _, tc := range []struct { + name string + ptName string + prName string + wantCprNames []string + }{{ + name: "existing child pipelinerun", + ptName: "mychildpipelinetask", + wantCprNames: []string{"mypipelinerun-mychildpipelinetask"}, + }, { + name: "new child pipelinerun", + ptName: "mynewchildpipelinetask", + wantCprNames: []string{"mypipelinerun-mynewchildpipelinetask"}, + }, { + name: "new pipelinetask with long names", + ptName: "longtask-0123456789-0123456789-0123456789-0123456789-0123456789", + wantCprNames: []string{ + "mypipelineruna56c4ee0aab148ee219d40b21dfe935a-longtask-01234567", + }, + }, { + name: "new child pipelinetask, pipelinerun with long name", + ptName: "mynewchildpipelinetask", + prName: "pipeline-run-0123456789-0123456789-0123456789-0123456789", + wantCprNames: []string{ + "pipeline1276ed292277c9bebded38d907a517fe-mynewchildpipelinetask", + }, + }, { + name: "new child pipelinerun, pipelinetask and pipelinerun with long name", + ptName: "mychildpipelinetask-0123456789-0123456789-0123456789-0123456789-0123456789", + prName: "pipeline-run-0123456789-0123456789-0123456789-0123456789", + wantCprNames: []string{ + "pipeline-run-0123456789-012345628c128b6df49e753c1f628b073961e99", + }, + }} { + t.Run(tc.name, func(t *testing.T) { + testPrName := prName + if tc.prName != "" { + testPrName = tc.prName + } + namesOfChildPipelineRunsFromChildRefs := GetNamesOfChildPipelineRuns(childRefs, tc.ptName, testPrName, 1) + sort.Strings(namesOfChildPipelineRunsFromChildRefs) + if d := cmp.Diff(tc.wantCprNames, namesOfChildPipelineRunsFromChildRefs); d != "" { + t.Errorf("GetNamesOfChildPipelineRuns: %s", diff.PrintWantGot(d)) + } + }) + } +} + func TestIsMatrixed(t *testing.T) { pr := v1.PipelineRun{ ObjectMeta: metav1.ObjectMeta{ @@ -4265,6 +4395,12 @@ func TestIsSuccessful(t *testing.T) { CustomTask: true, }, want: false, + }, { + name: "child pipelinerun not started", + rpt: ResolvedPipelineTask{ + PipelineTask: &pts[21], + }, + want: false, }, { name: "taskrun running", rpt: ResolvedPipelineTask{ @@ -4280,6 +4416,13 @@ func TestIsSuccessful(t *testing.T) { CustomRuns: []*v1beta1.CustomRun{makeCustomRunStarted(customRuns[0])}, }, want: false, + }, { + name: "child pipelinerun running", + rpt: ResolvedPipelineTask{ + PipelineTask: &pts[21], + ChildPipelineRuns: []*v1.PipelineRun{makePipelineRunStarted(prs[0])}, + }, + want: false, }, { name: "taskrun succeeded", rpt: ResolvedPipelineTask{ @@ -4295,6 +4438,13 @@ func TestIsSuccessful(t *testing.T) { CustomRuns: []*v1beta1.CustomRun{makeCustomRunSucceeded(customRuns[0])}, }, want: true, + }, { + name: "child pipelinerun succeeded", + rpt: ResolvedPipelineTask{ + PipelineTask: &pts[21], + ChildPipelineRuns: []*v1.PipelineRun{makePipelineRunSucceeded(prs[0])}, + }, + want: true, }, { name: "taskrun failed", rpt: ResolvedPipelineTask{ @@ -4310,6 +4460,13 @@ func TestIsSuccessful(t *testing.T) { CustomRuns: []*v1beta1.CustomRun{makeCustomRunFailed(customRuns[0])}, }, want: false, + }, { + name: "child pipelinerun failed", + rpt: ResolvedPipelineTask{ + PipelineTask: &pts[21], + ChildPipelineRuns: []*v1.PipelineRun{makePipelineRunFailed(prs[0])}, + }, + want: false, }, { name: "taskrun failed: retries remaining", rpt: ResolvedPipelineTask{ @@ -5082,6 +5239,13 @@ func TestGetReason(t *testing.T) { }}, }, }, + { + name: "child pipelinerun created but the conditions were not initialized", + rpt: ResolvedPipelineTask{ + PipelineTask: &pts[21], + ChildPipelineRuns: []*v1.PipelineRun{&prs[0]}, + }, + }, { name: "taskrun not started", rpt: ResolvedPipelineTask{ @@ -5095,6 +5259,12 @@ func TestGetReason(t *testing.T) { CustomTask: true, }, }, + { + name: "child pipelinerun not started", + rpt: ResolvedPipelineTask{ + PipelineTask: &pts[21], + }, + }, { name: "taskrun running", rpt: ResolvedPipelineTask{ @@ -5110,6 +5280,13 @@ func TestGetReason(t *testing.T) { CustomRuns: []*v1beta1.CustomRun{makeCustomRunStarted(customRuns[0])}, }, }, + { + name: "child pipelinerun running", + rpt: ResolvedPipelineTask{ + PipelineTask: &pts[21], + ChildPipelineRuns: []*v1.PipelineRun{makePipelineRunStarted(prs[0])}, + }, + }, { name: "taskrun succeeded", rpt: ResolvedPipelineTask{ @@ -5127,6 +5304,14 @@ func TestGetReason(t *testing.T) { }, want: "Succeeded", }, + { + name: "child pipelinerun succeeded", + rpt: ResolvedPipelineTask{ + PipelineTask: &pts[21], + ChildPipelineRuns: []*v1.PipelineRun{makePipelineRunSucceeded(prs[0])}, + }, + want: "Succeeded", + }, { name: "taskrun failed", rpt: ResolvedPipelineTask{ @@ -5144,6 +5329,14 @@ func TestGetReason(t *testing.T) { }, want: "Failed", }, + { + name: "child pipelinerun failed", + rpt: ResolvedPipelineTask{ + PipelineTask: &pts[21], + ChildPipelineRuns: []*v1.PipelineRun{makePipelineRunFailed(prs[0])}, + }, + want: "Failed", + }, { name: "taskrun failed: retried", rpt: ResolvedPipelineTask{ From 85ce1b382c194418117d05bfd88e0b8f0791ca8e Mon Sep 17 00:00:00 2001 From: Stanislav Jakuschevskij Date: Sat, 21 Jun 2025 17:17:40 +0200 Subject: [PATCH 7/7] test(third PR): add reconcile child pipeline tests The test setup is a parent pipeline with one or more embedded child/grandchild pipelines using the `PipelineSpec` (alpha) field. It follows the given-when-then test flow arrangement. The pipeline manifests yaml definitions use variables for every field which is validated. Multiple helper functions were created equal to reconciliation unit tests for TaskRuns/CustomRuns: - `testing.OnePipelineInPipeline` - `testing.TwoPipelinesInPipelineMixedTasks` - `testing.NestedPipelinesInPipeline` - `parse.MustParseChildPipelineRunWithObjectMeta` - `reconcileOncePinP` - `validatePinP` - `getChildPipelineRunsForPipelineRun` - `getChildPipelineRunByName` - `validateChildPipelineRunCount` - `getChildPipelineRunByName` - `testing.VerifyChildPipelineRunStatusesCount` - `testing.VerifyChildPipelineRunStatusesNames` - `testing.childPipelineRunWithObjectMeta` Test data factory functions were put in the `testing` package in the `factory.go` file. The test validates: - the status and condition of the parent PipelineRuns which should trigger the creation of the child PipelineRuns, - the actual created child/grandchild PipelineRuns if they have the correct metadata i.e. name, owner reference, etc. and the embedded pipelines from the `PipelineSpec` fields in the pipeline tasks of the parent pipeline. Similar checks are performed in `TestReconcile` for `TaskRun` and in `TestReconcile_V1Beta1CustomTask` for `CustomTasks`. Add annotations/label propagation tests: - `TestReconcile_PropagateLabelsAndAnnotationsToChild` - `TestReconcile_ChildPipelineRunHasDefaultLabels` Aadd child pipeline creation error tests. New table driven test `TestReconcile_ChildPipelineRunCreationError` and test framework additions: - `type clientErrors` - `reconcileWithError` `reconcileWithError` can be reused in every test with fake client error returns i.e. where `Pipeline.PrependReactor` is used. Add e2e child pipeline tests. The test setup is a parent pipeline with one or more embedded child/grandchild pipelines using the `PipelineSpec` (alpha) field equal to the reconciliation unit tests. The e2e tests also follow the Given-When-Then test structure. The pipeline manifests yaml definitions use variables for every field which is validated. Multiple helper functions were created equal to PipelineRun e2e tests: - `setupPinP` - `tearDownOptDump` - `createKindsMap` - `assertPinP` - `createResourcesAndWaitForPipelineRun` - `checkLabelPropagationToChildPipelineRun` - `checkAnnotationPropagationToChildPipelineRun` - `assertEvents` The tests validate: - parent PipelineRun creation, - child PipelineRun creation, - successful finish of all resources, - correct label and annotation propagation, - amount of events published. Similar checks are performed in `TestPipelineRun|TestPipelineRunStatusSpec|...` for `TaskRun` and in `TestCustomTask` for `CustomTask`. Issues #8760, #7166 Signed-off-by: Stanislav Jakuschevskij --- .../pipelinerun/pipelinerun_pinp_test.go | 460 ++++++++++++++++++ pkg/reconciler/testing/factory.go | 333 +++++++++++++ pkg/reconciler/testing/status.go | 10 + test/parse/yaml.go | 8 + test/pipelinerun_pinp_test.go | 343 +++++++++++++ test/util.go | 36 ++ 6 files changed, 1190 insertions(+) create mode 100644 pkg/reconciler/pipelinerun/pipelinerun_pinp_test.go create mode 100644 pkg/reconciler/testing/factory.go create mode 100644 test/pipelinerun_pinp_test.go diff --git a/pkg/reconciler/pipelinerun/pipelinerun_pinp_test.go b/pkg/reconciler/pipelinerun/pipelinerun_pinp_test.go new file mode 100644 index 00000000000..adf528bc75b --- /dev/null +++ b/pkg/reconciler/pipelinerun/pipelinerun_pinp_test.go @@ -0,0 +1,460 @@ +package pipelinerun + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/tektoncd/pipeline/pkg/apis/pipeline" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + th "github.com/tektoncd/pipeline/pkg/reconciler/testing" + "github.com/tektoncd/pipeline/test" + "github.com/tektoncd/pipeline/test/diff" + "github.com/tektoncd/pipeline/test/names" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + ktesting "k8s.io/client-go/testing" +) + +// TestReconcile_ChildPipelineRunPipelineSpec verifies the reconciliation logic for PipelineRuns that create child +// PipelineRuns from PipelineSpecs. It tests scenarios with one or more child PipelineRuns (with mixed TaskSpec and +// TaskRef), ensuring that: +// - The parent PipelineRun is correctly marked as running after reconciliation. +// - The correct number of child PipelineRuns are created and referenced in the parent status. +// - The actual child PipelineRuns match the expected specifications. +// - The expected events are emitted during reconciliation. +func TestReconcile_ChildPipelineRunPipelineSpec(t *testing.T) { + names.TestingSeed() + // GIVEN + namespace := "foo" + parentPipelineRunName := "parent-pipeline-run" + parentPipeline1, + parentPipelineRun1, + expectedChildPipelineRun1 := th.OnePipelineInPipeline(t, namespace, parentPipelineRunName) + _, parentPipeline2, + parentPipelineRun2, + expectedChildPipelineRun1And2 := th.TwoPipelinesInPipelineMixedTasks(t, namespace, parentPipelineRunName) + expectedEvents := []string{ + "Normal Started", + "Normal Running Tasks Completed: 0", + } + testCases := []struct { + name string + parentPipeline *v1.Pipeline + parentPipelineRun *v1.PipelineRun + expectedChildPipelineRuns []*v1.PipelineRun + }{ + { + name: "one child PipelineRun from PipelineSpec", + parentPipeline: parentPipeline1, + parentPipelineRun: parentPipelineRun1, + expectedChildPipelineRuns: []*v1.PipelineRun{expectedChildPipelineRun1}, + }, + { + name: "two child PipelineRuns from PipelineSpecs, one with TaskSpec and one with TaskRef", + parentPipeline: parentPipeline2, + parentPipelineRun: parentPipelineRun2, + expectedChildPipelineRuns: expectedChildPipelineRun1And2, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testData := test.Data{ + PipelineRuns: []*v1.PipelineRun{tc.parentPipelineRun}, + Pipelines: []*v1.Pipeline{tc.parentPipeline}, + ConfigMaps: []*corev1.ConfigMap{withEnabledAlphaAPIFields(newFeatureFlagsConfigMap())}, + } + + // WHEN + reconciledRun, childPipelineRuns := reconcileOncePinP( + t, + testData, + namespace, + tc.parentPipelineRun.Name, + expectedEvents, + ) + + // THEN + validatePinP( + t, + reconciledRun.Status, + reconciledRun.Name, + childPipelineRuns, + tc.expectedChildPipelineRuns, + ) + }) + } +} + +func reconcileOncePinP( + t *testing.T, + testData test.Data, + namespace, + parentPipelineRunName string, + expectedEvents []string, +) (*v1.PipelineRun, map[string]*v1.PipelineRun) { + t.Helper() + + prt := newPipelineRunTest(t, testData) + defer prt.Cancel() + + // reconcile once given parent PipelineRun + reconciledRun, clients := prt.reconcileRun( + namespace, + parentPipelineRunName, + expectedEvents, + false, + ) + + // fetch created child PipelineRun(s) + childPipelineRuns := getChildPipelineRunsForPipelineRun( + prt.TestAssets.Ctx, + t, + clients, + namespace, + parentPipelineRunName, + ) + + return reconciledRun, childPipelineRuns +} + +func getChildPipelineRunsForPipelineRun( + ctx context.Context, + t *testing.T, + clients test.Clients, + namespace, parentPipelineRunName string, +) map[string]*v1.PipelineRun { + t.Helper() + + opt := metav1.ListOptions{ + LabelSelector: pipeline.PipelineRunLabelKey + "=" + parentPipelineRunName, + } + + pipelineRunList, err := clients. + Pipeline. + TektonV1(). + PipelineRuns(namespace). + List(ctx, opt) + if err != nil { + t.Fatalf("failed to list child PipelineRuns: %v", err) + } + + result := make(map[string]*v1.PipelineRun) + for _, pipelineRun := range pipelineRunList.Items { + result[pipelineRun.Name] = &pipelineRun + } + + return result +} + +func validatePinP( + t *testing.T, + reconciledRunStatus v1.PipelineRunStatus, + reconciledRunName string, + childPipelineRuns map[string]*v1.PipelineRun, + expectedChildPipelineRuns []*v1.PipelineRun, +) { + t.Helper() + + // validate parent PipelineRun is in progress; the status should reflect that + th.CheckPipelineRunConditionStatusAndReason( + t, + reconciledRunStatus, + corev1.ConditionUnknown, + v1.PipelineRunReasonRunning.String(), + ) + + // validate there is the correct number of child references with the correct names of the child PipelineRuns + th.VerifyChildPipelineRunStatusesCount(t, reconciledRunStatus, len(expectedChildPipelineRuns)) + var expectedNames []string + for _, cpr := range expectedChildPipelineRuns { + expectedNames = append(expectedNames, cpr.Name) + } + th.VerifyChildPipelineRunStatusesNames(t, reconciledRunStatus, expectedNames...) + + validateChildPipelineRunCount(t, childPipelineRuns, len(expectedChildPipelineRuns)) + + // validate the actual child PipelineRuns are as expected + for _, expectedChild := range expectedChildPipelineRuns { + actualChild := getChildPipelineRunByName(t, childPipelineRuns, expectedChild.Name) + if d := cmp.Diff(expectedChild, actualChild, ignoreTypeMeta, ignoreResourceVersion); d != "" { + t.Errorf("expected to see child PipelineRun %v created. Diff %s", expectedChild, diff.PrintWantGot(d)) + } + + // validate correct owner reference + if len(actualChild.OwnerReferences) != 1 || actualChild.OwnerReferences[0].Name != reconciledRunName { + t.Errorf("Child PipelineRun should be owned by parent %s", reconciledRunName) + } + } +} + +func validateChildPipelineRunCount(t *testing.T, pipelineRuns map[string]*v1.PipelineRun, expectedCount int) { + t.Helper() + + actualCount := len(pipelineRuns) + if actualCount != expectedCount { + t.Fatalf("Expected %d child PipelineRuns, got %d", expectedCount, actualCount) + } +} + +func getChildPipelineRunByName(t *testing.T, pipelineRuns map[string]*v1.PipelineRun, expectedName string) *v1.PipelineRun { + t.Helper() + + pr, exist := pipelineRuns[expectedName] + if !exist { + t.Fatalf("Expected pipelinerun %s does not exist", expectedName) + } + + return pr +} + +// TestReconcile_NestedChildPipelineRuns verifies the reconciliation logic for multi-level nested PipelineRuns. +// It tests a parent pipeline that creates a child pipeline, which itself creates a grandchild pipeline. +// This test requires multiple reconciliation cycles: +// - First reconciliation: Parent creates child pipeline +// - Second reconciliation: Child creates grandchild pipeline +func TestReconcile_NestedChildPipelineRuns(t *testing.T) { + names.TestingSeed() + // GIVEN + namespace := "foo" + parentPipelineRunName := "parent-pipeline-run" + parentPipeline, + parentPipelineRun, + expectedChildPipelineRun, + expectedGrandchildPipelineRun := th.NestedPipelinesInPipeline(t, namespace, parentPipelineRunName) + expectedEvents := []string{ + "Normal Started", + "Normal Running Tasks Completed: 0", + } + testData := test.Data{ + PipelineRuns: []*v1.PipelineRun{parentPipelineRun}, + Pipelines: []*v1.Pipeline{parentPipeline}, + ConfigMaps: []*corev1.ConfigMap{withEnabledAlphaAPIFields(newFeatureFlagsConfigMap())}, + } + + // WHEN + // first reconcile parent PipelineRun once which creates the child + reconciledRunParent, childPipelineRuns := reconcileOncePinP( + t, + testData, + namespace, + parentPipelineRun.Name, + expectedEvents, + ) + + // THEN + validatePinP( + t, + reconciledRunParent.Status, + reconciledRunParent.Name, + childPipelineRuns, + []*v1.PipelineRun{expectedChildPipelineRun}, + ) + + // GIVEN + // use the child from previous reconcile + childPipelineRun := getChildPipelineRunByName(t, childPipelineRuns, expectedChildPipelineRun.Name) + childTestData := test.Data{ + PipelineRuns: []*v1.PipelineRun{childPipelineRun}, + ConfigMaps: []*corev1.ConfigMap{withEnabledAlphaAPIFields(newFeatureFlagsConfigMap())}, + } + + // WHEN + // second reconcile child PipelineRun which creates the grandchild + reconciledRunChild, grandchildPipelineRuns := reconcileOncePinP( + t, + childTestData, + namespace, + childPipelineRun.Name, + expectedEvents, + ) + + // THEN + validatePinP( + t, + reconciledRunChild.Status, + reconciledRunChild.Name, + grandchildPipelineRuns, + []*v1.PipelineRun{expectedGrandchildPipelineRun}, + ) +} + +func TestReconcile_PropagateLabelsAndAnnotationsToChildPipelineRun(t *testing.T) { + names.TestingSeed() + // GIVEN + namespace := "foo" + parentPipeline, + parentPipelineRun, + expectedChildPipelineRun := th.OnePipelineInPipeline(t, namespace, "parent-pipeline-run") + expectedChildPipelineRun = th.WithAnnotationAndLabel(expectedChildPipelineRun, false) + testData := test.Data{ + PipelineRuns: []*v1.PipelineRun{th.WithAnnotationAndLabel(parentPipelineRun, true)}, + Pipelines: []*v1.Pipeline{parentPipeline}, + ConfigMaps: []*corev1.ConfigMap{withEnabledAlphaAPIFields(newFeatureFlagsConfigMap())}, + } + + // WHEN + reconciledRun, childPipelineRuns := reconcileOncePinP( + t, + testData, + namespace, + parentPipelineRun.Name, + []string{}, + ) + + // THEN + validatePinP( + t, + reconciledRun.Status, + reconciledRun.Name, + childPipelineRuns, + []*v1.PipelineRun{expectedChildPipelineRun}, + ) +} + +func TestReconcile_ChildPipelineRunHasDefaultLabels(t *testing.T) { + names.TestingSeed() + // GIVEN + namespace := "foo" + parentPipeline, + parentPipelineRun, + expectedChildPipelineRun := th.OnePipelineInPipeline(t, namespace, "parent-pipeline-run") + expectedLabels := map[string]string{ + pipeline.PipelineRunLabelKey: parentPipelineRun.Name, + pipeline.PipelineLabelKey: parentPipelineRun.Spec.PipelineRef.Name, + pipeline.PipelineRunUIDLabelKey: string(parentPipelineRun.UID), + pipeline.PipelineTaskLabelKey: parentPipeline.Spec.Tasks[0].Name, + pipeline.MemberOfLabelKey: v1.PipelineTasks, + } + testData := test.Data{ + PipelineRuns: []*v1.PipelineRun{parentPipelineRun}, + Pipelines: []*v1.Pipeline{parentPipeline}, + ConfigMaps: []*corev1.ConfigMap{withEnabledAlphaAPIFields(newFeatureFlagsConfigMap())}, + } + + // WHEN + _, childPipelineRuns := reconcileOncePinP( + t, + testData, + namespace, + parentPipelineRun.Name, + []string{}, + ) + + // THEN + validateChildPipelineRunCount(t, childPipelineRuns, 1) + + actualLabels := childPipelineRuns[expectedChildPipelineRun.Name].Labels + for k, v := range expectedLabels { + if actualLabels[k] != v { + t.Errorf("Expected label %q=%q on child PipelineRun, got %q", k, v, actualLabels[k]) + } + } +} + +func TestReconcile_ChildPipelineRunCreationError(t *testing.T) { + names.TestingSeed() + // GIVEN + namespace := "foo" + parentPipeline, + parentPipelineRun, + expectedChildPipelineRun := th.OnePipelineInPipeline(t, namespace, "parent-pipeline-run") + testData := test.Data{ + PipelineRuns: []*v1.PipelineRun{parentPipelineRun}, + Pipelines: []*v1.Pipeline{parentPipeline}, + ConfigMaps: []*corev1.ConfigMap{withEnabledAlphaAPIFields(newFeatureFlagsConfigMap())}, + } + testCases := []struct { + name string + creationErr clientError + }{ + { + name: "invalid", + creationErr: clientError{ + verb: "create", + resource: "pipelineruns", + actualError: apierrors.NewInvalid( + schema.GroupKind{}, + expectedChildPipelineRun.Name, + field.ErrorList{}), + }, + }, + { + name: "bad request", + creationErr: clientError{ + verb: "create", + resource: "pipelineruns", + actualError: apierrors.NewBadRequest("bad request"), + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // WHEN + reconciledRun := reconcileWithError( + t, + testData, + namespace, + parentPipelineRun.Name, + tc.creationErr, + ) + + // THEN + th.CheckPipelineRunConditionStatusAndReason( + t, + reconciledRun.Status, + corev1.ConditionFalse, + "CreateRunFailed", + ) + + if reconciledRun.Status.CompletionTime == nil { + t.Errorf("Expected a CompletionTime on invalid PipelineRun but was nil") + } + }) + } +} + +type clientError struct { + verb, + resource string + actualError error +} + +func reconcileWithError( + t *testing.T, + testData test.Data, + namespace, + pipelineRunName string, + clientErr clientError, +) *v1.PipelineRun { + t.Helper() + + prt := newPipelineRunTest(t, testData) + defer prt.Cancel() + + // simulate error when creating child resource + prt.TestAssets.Clients. + Pipeline. + PrependReactor( + clientErr.verb, + clientErr.resource, + func(_ ktesting.Action) (bool, runtime.Object, error) { + return true, nil, clientErr.actualError + }, + ) + + reconciledRun, _ := prt.reconcileRun( + namespace, + pipelineRunName, + []string{}, + true, + ) + + return reconciledRun +} diff --git a/pkg/reconciler/testing/factory.go b/pkg/reconciler/testing/factory.go new file mode 100644 index 00000000000..7825bf17b8f --- /dev/null +++ b/pkg/reconciler/testing/factory.go @@ -0,0 +1,333 @@ +package testing + +import ( + "fmt" + "testing" + + "github.com/tektoncd/pipeline/pkg/apis/pipeline" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + "github.com/tektoncd/pipeline/test/parse" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var ( + trueb = true +) + +// TwoPipelinesInPipelineMixedTasks creates a parent Pipeline with two embedded child Pipelines: +// one using an embedded taskSpec and the other using a taskRef. It also creates a PipelineRun +// for the parent Pipeline, the expected child PipelineRuns for each child Pipeline and the +// referenced task. +func TwoPipelinesInPipelineMixedTasks(t *testing.T, namespace, parentPipelineRunName string) (*v1.Task, *v1.Pipeline, *v1.PipelineRun, []*v1.PipelineRun) { + t.Helper() + uid := "bar" + taskName := "ref-task" + parentPipelineName := "parent-pipeline-mixed" + childPipelineName1 := "child-pipeline-taskspec" + childPipelineName2 := "child-pipeline-taskref" + childPipelineTaskName1 := "child-taskspec" + childPipelineTaskName2 := "child-taskref" + + task := parse.MustParseV1Task(t, fmt.Sprintf(` +metadata: + name: %s + namespace: %s +spec: + steps: + - name: mystep + image: mirror.gcr.io/busybox + script: 'echo "Hello from referenced task in child PipelineRun 2!"' +`, taskName, namespace)) + + parentPipeline := parse.MustParseV1Pipeline(t, fmt.Sprintf(` +metadata: + name: %s + namespace: %s +spec: + tasks: + - name: %s + pipelineSpec: + tasks: + - name: %s + taskSpec: + steps: + - name: mystep + image: mirror.gcr.io/busybox + script: 'echo "Hello from child PipelineRun 1!"' + - name: %s + pipelineSpec: + tasks: + - name: %s + taskRef: + name: %s +`, parentPipelineName, namespace, childPipelineName1, childPipelineTaskName1, childPipelineName2, childPipelineTaskName2, taskName)) + + parentPipelineRun := parse.MustParseV1PipelineRun(t, fmt.Sprintf(` +metadata: + name: %s + namespace: %s + uid: %s +spec: + pipelineRef: + name: %s +`, parentPipelineRunName, namespace, uid, parentPipelineName)) + + expectedName1 := parentPipelineRunName + "-" + childPipelineName1 + expectedChildPipelineRun1 := parse.MustParseChildPipelineRunWithObjectMeta( + t, + childPipelineRunWithObjectMeta( + expectedName1, + namespace, + parentPipelineRunName, + parentPipelineName, + childPipelineName1, + uid, + ), + fmt.Sprintf(` +spec: + pipelineSpec: + tasks: + - name: %s + taskSpec: + steps: + - name: mystep + image: mirror.gcr.io/busybox + script: 'echo "Hello from child PipelineRun 1!"' +`, childPipelineTaskName1), + ) + + expectedName2 := parentPipelineRunName + "-" + childPipelineName2 + expectedChildPipelineRun2 := parse.MustParseChildPipelineRunWithObjectMeta( + t, + childPipelineRunWithObjectMeta( + expectedName2, + namespace, + parentPipelineRunName, + parentPipelineName, + childPipelineName2, + uid, + ), + fmt.Sprintf(` +spec: + pipelineSpec: + tasks: + - name: %s + taskRef: + name: %s +`, childPipelineTaskName2, taskName), + ) + + return task, parentPipeline, parentPipelineRun, []*v1.PipelineRun{expectedChildPipelineRun1, expectedChildPipelineRun2} +} + +// OnePipelineInPipeline creates a single Pipeline with one child pipeline using +// PipelineSpec with TaskSpec. It also creates the according PipelineRun for it +// and the expected child PipelineRun against which the test will validate. +func OnePipelineInPipeline(t *testing.T, namespace, parentPipelineRunName string) (*v1.Pipeline, *v1.PipelineRun, *v1.PipelineRun) { + t.Helper() + uid := "bar" + parentPipelineName := "parent-pipeline" + childPipelineName := "child-pipeline" + childPipelineTaskName := "child-pipeline-task" + + parentPipeline := parse.MustParseV1Pipeline(t, fmt.Sprintf(` +metadata: + name: %s + namespace: %s +spec: + tasks: + - name: %s + pipelineSpec: + tasks: + - name: %s + taskSpec: + steps: + - name: mystep + image: mirror.gcr.io/busybox + script: 'echo "Hello from child PipelineRun!"' +`, parentPipelineName, namespace, childPipelineName, childPipelineTaskName)) + + parentPipelineRun := parse.MustParseV1PipelineRun(t, fmt.Sprintf(` +metadata: + name: %s + namespace: %s + uid: %s +spec: + pipelineRef: + name: %s +`, parentPipelineRunName, namespace, uid, parentPipelineName)) + + expectedName := parentPipelineRunName + "-" + childPipelineName + expectedChildPipelineRun := parse.MustParseChildPipelineRunWithObjectMeta( + t, + childPipelineRunWithObjectMeta( + expectedName, + namespace, + parentPipelineRunName, + parentPipelineName, + childPipelineName, + uid, + ), + fmt.Sprintf(` +spec: + pipelineSpec: + tasks: + - name: %s + taskSpec: + steps: + - name: mystep + image: mirror.gcr.io/busybox + script: 'echo "Hello from child PipelineRun!"' +`, childPipelineTaskName), + ) + + return parentPipeline, parentPipelineRun, expectedChildPipelineRun +} + +func WithAnnotationAndLabel(pr *v1.PipelineRun, withUnused bool) *v1.PipelineRun { + if pr.Annotations == nil { + pr.Annotations = map[string]string{} + } + pr.Annotations["tekton.test/annotation"] = "test-annotation-value" + + if pr.Labels == nil { + pr.Labels = map[string]string{} + } + pr.Labels["tekton.test/label"] = "test-label-value" + + if withUnused { + pr.Labels["tekton.dev/pipeline"] = "will-not-be-used" + } + + return pr +} + +func childPipelineRunWithObjectMeta( + childPipelineRunName, + ns, + parentPipelineRunName, + parentPipelineName, + pipelineTaskName, + uid string, +) metav1.ObjectMeta { + om := metav1.ObjectMeta{ + Name: childPipelineRunName, + Namespace: ns, + OwnerReferences: []metav1.OwnerReference{{ + Kind: pipeline.PipelineRunControllerName, + Name: parentPipelineRunName, + APIVersion: "tekton.dev/v1", + Controller: &trueb, + BlockOwnerDeletion: &trueb, + UID: types.UID(uid), + }}, + Labels: map[string]string{ + pipeline.PipelineLabelKey: parentPipelineName, + pipeline.PipelineRunLabelKey: parentPipelineRunName, + pipeline.PipelineTaskLabelKey: pipelineTaskName, + pipeline.PipelineRunUIDLabelKey: uid, + pipeline.MemberOfLabelKey: v1.PipelineTasks, + }, + Annotations: map[string]string{}, + } + + return om +} + +// NestedPipelinesInPipeline creates a three-level nested pipeline structure: +// Parent Pipeline -> Child Pipeline -> Grandchild Pipeline +// Returns the parent pipeline, parent pipelinerun, expected child pipelinerun, and expected grandchild pipelinerun +func NestedPipelinesInPipeline(t *testing.T, namespace, parentPipelineRunName string) (*v1.Pipeline, *v1.PipelineRun, *v1.PipelineRun, *v1.PipelineRun) { + t.Helper() + uid := "nested" + parentPipelineName := "parent-pipeline" + childPipelineName := "child-ppl" + grandchildPipelineName := "grandchild-ppl" + grandchildPipelineTaskName := "grandchild-task" + + parentPipeline := parse.MustParseV1Pipeline(t, fmt.Sprintf(` +metadata: + name: %s + namespace: %s +spec: + tasks: + - name: %s + pipelineSpec: + tasks: + - name: %s + pipelineSpec: + tasks: + - name: %s + taskSpec: + steps: + - name: mystep + image: mirror.gcr.io/busybox + script: 'echo "Hello from grandchild Pipeline!"' +`, parentPipelineName, namespace, childPipelineName, grandchildPipelineName, grandchildPipelineTaskName)) + + parentPipelineRun := parse.MustParseV1PipelineRun(t, fmt.Sprintf(` +metadata: + name: %s + namespace: %s + uid: %s +spec: + pipelineRef: + name: %s +`, parentPipelineRunName, namespace, uid, parentPipelineName)) + + // expected child pipeline run created by parent + expectedChildName := parentPipelineRunName + "-" + childPipelineName + expectedChildPipelineRun := parse.MustParseChildPipelineRunWithObjectMeta( + t, + childPipelineRunWithObjectMeta( + expectedChildName, + namespace, + parentPipelineRunName, + parentPipelineName, + childPipelineName, + uid, + ), + fmt.Sprintf(` +spec: + pipelineSpec: + tasks: + - name: %s + pipelineSpec: + tasks: + - name: %s + taskSpec: + steps: + - name: mystep + image: mirror.gcr.io/busybox + script: 'echo "Hello from grandchild Pipeline!"' +`, grandchildPipelineName, grandchildPipelineTaskName), + ) + + // expected grandchild pipeline run created by child + expectedGrandchildName := expectedChildName + "-" + grandchildPipelineName + expectedGrandchildPipelineRun := parse.MustParseChildPipelineRunWithObjectMeta( + t, + childPipelineRunWithObjectMeta( + expectedGrandchildName, + namespace, + expectedChildName, + expectedChildName, + grandchildPipelineName, + "", // keep empty, UID is not set on actual child PipelineRun by fake client + ), + fmt.Sprintf(` +spec: + pipelineSpec: + tasks: + - name: %s + taskSpec: + steps: + - name: mystep + image: mirror.gcr.io/busybox + script: 'echo "Hello from grandchild Pipeline!"' +`, grandchildPipelineTaskName), + ) + + return parentPipeline, parentPipelineRun, expectedChildPipelineRun, expectedGrandchildPipelineRun +} diff --git a/pkg/reconciler/testing/status.go b/pkg/reconciler/testing/status.go index ea38b884d92..c2966de71ef 100644 --- a/pkg/reconciler/testing/status.go +++ b/pkg/reconciler/testing/status.go @@ -111,3 +111,13 @@ func VerifyCustomRunOrRunStatusesNames(t *testing.T, prStatus v1.PipelineRunStat t.Helper() verifyNames(t, prStatus, expectedNames, customRun) } + +func VerifyChildPipelineRunStatusesCount(t *testing.T, prStatus v1.PipelineRunStatus, expectedCount int) { + t.Helper() + verifyCount(t, prStatus, expectedCount, pipelineRun) +} + +func VerifyChildPipelineRunStatusesNames(t *testing.T, prStatus v1.PipelineRunStatus, expectedNames ...string) { + t.Helper() + verifyNames(t, prStatus, expectedNames, pipelineRun) +} diff --git a/test/parse/yaml.go b/test/parse/yaml.go index a9983bd94be..79ddc5d7907 100644 --- a/test/parse/yaml.go +++ b/test/parse/yaml.go @@ -210,3 +210,11 @@ func MustParseCustomRunWithObjectMeta(t *testing.T, objectMeta metav1.ObjectMeta r.ObjectMeta = objectMeta return r } + +// MustParseChildPipelineRunWithObjectMeta parses YAML to *v1.PipelineRun and adds objectMeta to it +func MustParseChildPipelineRunWithObjectMeta(t *testing.T, objectMeta metav1.ObjectMeta, asYAML string) *v1.PipelineRun { + t.Helper() + pr := MustParseV1PipelineRun(t, asYAML) + pr.ObjectMeta = objectMeta + return pr +} diff --git a/test/pipelinerun_pinp_test.go b/test/pipelinerun_pinp_test.go new file mode 100644 index 00000000000..329eab10827 --- /dev/null +++ b/test/pipelinerun_pinp_test.go @@ -0,0 +1,343 @@ +//go:build e2e +// +build e2e + +/* +Copyright 2025 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 test + +import ( + "context" + "fmt" + "testing" + + "github.com/tektoncd/pipeline/pkg/apis/pipeline" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + th "github.com/tektoncd/pipeline/pkg/reconciler/testing" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestPipelineRun_OneChildPipelineRunFromPipelineSpec(t *testing.T) { + ctx, cancel, c, namespace := setupPinP(t) + defer cancel() + defer tearDownOptDump(ctx, t, c, namespace, true) + + // GIVEN + t.Logf("Setting up test resources for one child PipelineRun from PipelineSpec in namespace %q", namespace) + parentPipeline, + parentPipelineRun, + expectedChildPipelineRun := th.OnePipelineInPipeline(t, namespace, "parent-pipeline-run") + parentPipelineRun = th.WithAnnotationAndLabel(parentPipelineRun, false) + expectedKinds := createKindsMap(parentPipelineRun, []*v1.PipelineRun{expectedChildPipelineRun}) + expectedEventsAmount := 3 + + // WHEN + createResourcesAndWaitForPipelineRun(ctx, t, c, namespace, parentPipeline, parentPipelineRun, nil) + + // THEN + assertPinP(ctx, t, c, namespace, expectedChildPipelineRun) + assertEvents(ctx, t, expectedEventsAmount, expectedKinds, c, namespace) +} + +func createKindsMap(parentPipelineRun *v1.PipelineRun, childPipelineRuns []*v1.PipelineRun) map[string][]string { + prNames := []string{parentPipelineRun.Name} + for _, cpr := range childPipelineRuns { + prNames = append(prNames, cpr.Name) + } + + var trNames []string + for _, cpr := range childPipelineRuns { + // collect names of TaskRuns; ignore nested PipelineRuns + if cpr.Spec.PipelineSpec.Tasks[0].PipelineSpec == nil { + trNames = append(trNames, cpr.Name+"-"+cpr.Spec.PipelineSpec.Tasks[0].Name) + } + } + + return map[string][]string{ + "PipelineRun": prNames, + "TaskRun": trNames, + } +} + +func assertPinP( + ctx context.Context, + t *testing.T, + c *clients, + namespace string, + childPipelineRun *v1.PipelineRun, +) { + t.Helper() + + t.Logf("Making sure the expected child PipelineRun %q was created", childPipelineRun.Name) + actualCpr, err := c.V1PipelineRunClient.Get(ctx, childPipelineRun.Name, metav1.GetOptions{}) + if err != nil { + t.Fatalf("Error listing child PipelineRuns for PipelineRun %s: %s", childPipelineRun.Name, err) + } + th.CheckPipelineRunConditionStatusAndReason( + t, + actualCpr.Status, + corev1.ConditionTrue, + v1.PipelineRunReasonSuccessful.String(), + ) + t.Logf("Checking that labels were propagated correctly for child PipelineRun %q", actualCpr.Name) + directParentPrName := actualCpr.OwnerReferences[0].Name + checkLabelPropagationToChildPipelineRun(ctx, t, c, namespace, directParentPrName, actualCpr) + t.Logf("Checking that annotations were propagated correctly for child PipelineRun %q", actualCpr.Name) + checkAnnotationPropagationToChildPipelineRun(ctx, t, c, namespace, directParentPrName, actualCpr) +} + +func TestPipelineRun_TwoChildPipelineRunsMixedTasks(t *testing.T) { + ctx, cancel, c, namespace := setupPinP(t) + defer cancel() + defer tearDownOptDump(ctx, t, c, namespace, true) + + // GIVEN + t.Logf("Setting up test resources for two child PipelineRuns (mixed tasks) in namespace %q", namespace) + task, + parentPipeline, + parentPipelineRun, + expectedChildPipelineRuns := th.TwoPipelinesInPipelineMixedTasks(t, namespace, "parent-pipeline-mixed") + parentPipelineRun = th.WithAnnotationAndLabel(parentPipelineRun, false) + expectedKinds := createKindsMap(parentPipelineRun, expectedChildPipelineRuns) + expectedEventsAmount := 5 + + // WHEN + createResourcesAndWaitForPipelineRun(ctx, t, c, namespace, parentPipeline, parentPipelineRun, task) + + // THEN + assertPinP(ctx, t, c, namespace, expectedChildPipelineRuns[0]) + assertPinP(ctx, t, c, namespace, expectedChildPipelineRuns[1]) + assertEvents(ctx, t, expectedEventsAmount, expectedKinds, c, namespace) +} + +func TestPipelineRun_TwoLevelDeepNestedChildPipelineRuns(t *testing.T) { + ctx, cancel, c, namespace := setupPinP(t) + defer cancel() + defer tearDownOptDump(ctx, t, c, namespace, true) + + // GIVEN + t.Logf("Setting up test resources for two level deep nested child PipelineRuns in namespace %q", namespace) + parentPipeline, + parentPipelineRun, + expectedChildPipelineRun, + expectedGrandchildPipelineRun := th.NestedPipelinesInPipeline(t, namespace, "parent-pipeline-nested") + parentPipelineRun = th.WithAnnotationAndLabel(parentPipelineRun, false) + expectedKinds := createKindsMap( + parentPipelineRun, + []*v1.PipelineRun{ + expectedChildPipelineRun, + expectedGrandchildPipelineRun, + }) + expectedEventsAmount := 4 + + // WHEN + createResourcesAndWaitForPipelineRun(ctx, t, c, namespace, parentPipeline, parentPipelineRun, nil) + + // THEN + assertPinP(ctx, t, c, namespace, expectedChildPipelineRun) + assertPinP(ctx, t, c, namespace, expectedGrandchildPipelineRun) + assertEvents(ctx, t, expectedEventsAmount, expectedKinds, c, namespace) +} + +func createResourcesAndWaitForPipelineRun( + ctx context.Context, + t *testing.T, + c *clients, + namespace string, + pipeline *v1.Pipeline, + pipelineRun *v1.PipelineRun, + task *v1.Task, +) { + t.Helper() + + if _, err := c.V1PipelineClient.Create(ctx, pipeline, metav1.CreateOptions{}); err != nil { + t.Fatalf("Failed to create Pipeline `%s`: %s", pipeline.Name, err) + } + + if task != nil { + if _, err := c.V1TaskClient.Create(ctx, task, metav1.CreateOptions{}); err != nil { + t.Fatalf("Failed to create Task `%s`: %s", task.Name, err) + } + } + + if _, err := c.V1PipelineRunClient.Create(ctx, pipelineRun, metav1.CreateOptions{}); err != nil { + t.Fatalf("Failed to create PipelineRun `%s`: %s", pipelineRun.Name, err) + } + + t.Logf("Waiting for PipelineRun %q in namespace %q to complete", pipelineRun.Name, namespace) + if err := WaitForPipelineRunState( + ctx, + c, + pipelineRun.Name, + timeout, + PipelineRunSucceed(pipelineRun.Name), + "PipelineRunSuccess", + v1Version, + ); err != nil { + t.Fatalf("Error waiting for PipelineRun %s to finish: %s", pipelineRun.Name, err) + } +} + +func assertEvents( + ctx context.Context, + t *testing.T, + expectedEventsAmount int, + matchKinds map[string][]string, + c *clients, + namespace string, +) { + t.Helper() + + t.Logf( + "Making sure %d events were created from parent PipelineRun, child PipelineRun and TaskRun with kinds %v", + expectedEventsAmount, + matchKinds, + ) + + events, err := collectMatchingEvents( + ctx, + c.KubeClient, + namespace, + matchKinds, + "Succeeded", + ) + if err != nil { + t.Fatalf("Failed to collect matching events: %q", err) + } + if len(events) != expectedEventsAmount { + collectedEvents := "" + for i, event := range events { + collectedEvents += fmt.Sprintf("%#v", event) + if i < (len(events) - 1) { + collectedEvents += ", " + } + } + t.Fatalf( + "Expected %d number of successful events from parent PipelineRun, child PipelineRun and "+ + "TaskRun but got %d; list of received events: %#v", + expectedEventsAmount, + len(events), + collectedEvents, + ) + } +} + +// checkLabelPropagationToChildPipelineRun checks that labels are correctly propagating from +// Pipelines and PipelineRuns to child/grandchild PipelineRuns. +func checkLabelPropagationToChildPipelineRun( + ctx context.Context, + t *testing.T, + c *clients, + namespace string, + parentPrName string, + childPr *v1.PipelineRun, +) { + t.Helper() + + labels := make(map[string]string) + + parentPr, err := c.V1PipelineRunClient.Get(ctx, parentPrName, metav1.GetOptions{}) + if err != nil { + t.Fatalf("Couldn't get expected PipelineRun for %s: %s", childPr.Name, err) + } + + // Does the parent PipelineRun have an owner? If not its the initial PipelineRun + // and we have to check for labels propagated from the Pipeline. + if parentPr.OwnerReferences == nil { + p, err := c.V1PipelineClient.Get(ctx, parentPr.Spec.PipelineRef.Name, metav1.GetOptions{}) + if err != nil { + t.Fatalf("Couldn't get expected Pipeline for %s: %s", parentPr.Name, err) + } + // Extract every label the Pipeline has. + for key, val := range p.ObjectMeta.Labels { + labels[key] = val + } + // This label is added to every PipelineRun by the PipelineRun controller. + labels[pipeline.PipelineLabelKey] = p.Name + // Check label propagation from Pipeline to parent PipelineRun. + assertLabelsMatch(t, labels, parentPr.ObjectMeta.Labels) + t.Logf("Labels propagated from Pipeline to PipelineRun: %#v", labels) + } + + // Check label propagation from parent PipelineRun to child PipelineRun. + for key, val := range parentPr.ObjectMeta.Labels { + // Skip overwritten labels. + if key == pipeline.MemberOfLabelKey || + key == pipeline.PipelineLabelKey || + key == pipeline.PipelineRunLabelKey || + key == pipeline.PipelineRunUIDLabelKey || + key == pipeline.PipelineTaskLabelKey { + continue + } + + labels[key] = val + } + + // Child always references the parent PipelineRun its labels. + labels[pipeline.PipelineRunLabelKey] = parentPr.Name + // The parent PipelineRun references an existing Pipeline via PipelineRef + // the child PipelineRun does not reference any existing Pipeline but it uses the + // PipelineSpec embedded field, that is why its label "tekton.dev/pipeline:" is + // set to its own name. Refer to "propagatePipelineNameLabelToPipelineRun" for + // more implementation details. + labels[pipeline.PipelineLabelKey] = childPr.Name + assertLabelsMatch(t, labels, childPr.ObjectMeta.Labels) + t.Logf("Labels propagated from parent PipelineRun to child PipelineRun: %#v", labels) +} + +// checkAnnotationPropagationToChildPipelineRun checks that annotations are correctly propagating from +// Pipelines and PipelineRuns to child PipelineRuns. +func checkAnnotationPropagationToChildPipelineRun( + ctx context.Context, + t *testing.T, + c *clients, + namespace string, + parentPrName string, + childPr *v1.PipelineRun, +) { + t.Helper() + + annotations := make(map[string]string) + parentPr, err := c.V1PipelineRunClient.Get(ctx, parentPrName, metav1.GetOptions{}) + if err != nil { + t.Fatalf("Couldn't get expected PipelineRun for %s: %s", childPr.Name, err) + } + + // Does the parent PipelineRun have an owner? If not its the initial PipelineRun + // and we have to check for annotations propagated from the Pipeline. + if parentPr.OwnerReferences == nil { + p, err := c.V1PipelineClient.Get(ctx, parentPr.Spec.PipelineRef.Name, metav1.GetOptions{}) + if err != nil { + t.Fatalf("Couldn't get expected Pipeline for %s: %s", parentPr.Name, err) + } + for key, val := range p.ObjectMeta.Annotations { + annotations[key] = val + } + + assertAnnotationsMatch(t, annotations, parentPr.ObjectMeta.Annotations) + } + + // Check annotation propagation to child PipelineRuns. + for key, val := range parentPr.ObjectMeta.Annotations { + annotations[key] = val + } + assertAnnotationsMatch(t, annotations, childPr.ObjectMeta.Annotations) + + if len(annotations) > 0 { + t.Logf("Propagated annotations: %#v", annotations) + } +} diff --git a/test/util.go b/test/util.go index bb8a3600e28..7eee2a06f5f 100644 --- a/test/util.go +++ b/test/util.go @@ -85,6 +85,31 @@ func setup(ctx context.Context, t *testing.T, fn ...func(context.Context, *testi return c, namespace } +func setupPinP(t *testing.T) (context.Context, context.CancelFunc, *clients, string) { + t.Helper() + + t.Parallel() + ctx := t.Context() + ctx, cancel := context.WithCancel(ctx) + c, namespace := setup(ctx, t) + + knativetest.CleanupOnInterrupt(func() { tearDown(ctx, t, c, namespace) }, t.Logf) + + t.Log("Activating alpha feature flags") + configMapData := map[string]string{"enable-api-fields": "alpha"} + if err := updateConfigMap( + ctx, + c.KubeClient, + system.Namespace(), + config.GetFeatureFlagsConfigName(), + configMapData, + ); err != nil { + t.Fatal(err) + } + + return ctx, cancel, c, namespace +} + func header(t *testing.T, text string) { t.Helper() left := "### " @@ -131,6 +156,17 @@ func tearDown(ctx context.Context, t *testing.T, cs *clients, namespace string) } } +// tearDownNoDump prevents dumping Task/Pipeline/PipelineRun/TaskRun yamls to the terminal +// when a test fails. Useful for investigating issues from terminal logs without the +// need to scroll over all the deployed resource yamls. +func tearDownOptDump(ctx context.Context, t *testing.T, c *clients, namespace string, dump bool) { + t.Helper() + if !dump { + c.KubeClient = nil + } + tearDown(ctx, t, c, namespace) +} + func initializeLogsAndMetrics(t *testing.T) { t.Helper() initMetrics.Do(func() {