From 9db9349e69608eb969a28d9480a9691f6bb63301 Mon Sep 17 00:00:00 2001 From: kamal2730 Date: Wed, 27 May 2026 11:29:26 +0530 Subject: [PATCH] Fix logs and describe for Pipelines-in-Pipelines Resolve child PipelineRun TaskRuns recursively in tkn pipelinerun logs and describe. TaskRuns from child PipelineRuns are now displayed with a ">" prefix (e.g., "call-child > greet") to indicate nesting depth. This works for any level of nesting and supports both available and follow/live modes. Implementation Details: - Logs: recursive getOrderedTasks/getChildOrderedTasks and recursive GetTaskRunsWithStatus tracking. - Describe: replaced direct ChildReferences iteration with GetTaskRunsWithStatus for automatic PinP resolution. --- pkg/cmd/pipelinerun/describe_v1_test.go | 128 ++++ pkg/cmd/pipelinerun/logs_test.go | 593 ++++++++++++++++++ .../TestPipelineRunDescribe_PinP.golden | 18 + pkg/log/pipeline_reader.go | 46 +- pkg/pipelinerun/description.go | 23 +- pkg/pipelinerun/tracker.go | 37 +- pkg/pipelinerun/tracker_test.go | 353 +++++++++++ 7 files changed, 1171 insertions(+), 27 deletions(-) create mode 100644 pkg/cmd/pipelinerun/testdata/TestPipelineRunDescribe_PinP.golden diff --git a/pkg/cmd/pipelinerun/describe_v1_test.go b/pkg/cmd/pipelinerun/describe_v1_test.go index 2105c78f0a..569ad6b36c 100644 --- a/pkg/cmd/pipelinerun/describe_v1_test.go +++ b/pkg/cmd/pipelinerun/describe_v1_test.go @@ -2743,3 +2743,131 @@ func TestPipelineRunDescribe_with_annotations(t *testing.T) { } golden.Assert(t, actual, fmt.Sprintf("%s.golden", t.Name())) } + +func TestPipelineRunDescribe_PinP(t *testing.T) { + clock := test.FakeClock() + greetTR := &v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pinp-run-call-child-greet", + Namespace: "ns", + }, + Status: v1.TaskRunStatus{ + TaskRunStatusFields: v1.TaskRunStatusFields{ + StartTime: &metav1.Time{Time: clock.Now().Add(2 * time.Minute)}, + CompletionTime: &metav1.Time{Time: clock.Now().Add(5 * time.Minute)}, + }, + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionTrue, + Type: apis.ConditionSucceeded, + }, + }, + }, + }, + } + childPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pinp-run-call-child", + Namespace: "ns", + }, + Spec: v1.PipelineRunSpec{ + PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{ + {Name: "greet", TaskRef: &v1.TaskRef{Name: "greet"}}, + }, + }, + }, + Status: v1.PipelineRunStatus{ + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{ + { + Name: "pinp-run-call-child-greet", + PipelineTaskName: "greet", + TypeMeta: runtime.TypeMeta{ + Kind: "TaskRun", + }, + }, + }, + }, + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionTrue, + Type: apis.ConditionSucceeded, + }, + }, + }, + }, + } + parentPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pinp-pipeline-run", + Namespace: "ns", + CreationTimestamp: metav1.Time{Time: clock.Now()}, + Labels: map[string]string{"tekton.dev/pipeline": "pipeline"}, + }, + Spec: v1.PipelineRunSpec{ + PipelineRef: &v1.PipelineRef{ + Name: "pipeline", + }, + Timeouts: &v1.TimeoutFields{ + Pipeline: &metav1.Duration{Duration: 1 * time.Hour}, + }, + }, + Status: v1.PipelineRunStatus{ + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + StartTime: &metav1.Time{Time: clock.Now()}, + CompletionTime: &metav1.Time{Time: clock.Now().Add(5 * time.Minute)}, + ChildReferences: []v1.ChildStatusReference{ + { + Name: "pinp-run-call-child", + PipelineTaskName: "call-child", + TypeMeta: runtime.TypeMeta{ + Kind: "PipelineRun", + }, + }, + }, + }, + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionTrue, + Type: apis.ConditionSucceeded, + }, + }, + }, + }, + } + namespaces := []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ns", + }, + }, + } + version := "v1" + tdc := testDynamic.Options{} + dynamic, err := tdc.Client( + cb.UnstructuredPR(parentPR, version), + cb.UnstructuredPR(childPR, version), + cb.UnstructuredTR(greetTR, version), + ) + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Namespaces: namespaces, + PipelineRuns: []*v1.PipelineRun{parentPR, childPR}, + TaskRuns: []*v1.TaskRun{greetTR}, + }) + cs.Pipeline.Resources = cb.APIResourceList(version, []string{"pipelinerun", "taskrun"}) + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dynamic, Clock: clock} + pipelinerun := Command(p) + clock.Advance(10 * time.Minute) + actual, err := test.ExecuteCommand(pipelinerun, "desc", "pinp-pipeline-run", "-n", "ns") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + golden.Assert(t, actual, fmt.Sprintf("%s.golden", t.Name())) +} diff --git a/pkg/cmd/pipelinerun/logs_test.go b/pkg/cmd/pipelinerun/logs_test.go index 1528fb3f7f..58f5c46313 100644 --- a/pkg/cmd/pipelinerun/logs_test.go +++ b/pkg/cmd/pipelinerun/logs_test.go @@ -4613,3 +4613,596 @@ func fetchLogs(lo *options.LogOptions) (string, error) { err := Run(lo) return out.String(), err } + +func TestPipelineRunLogs_PinP_Available(t *testing.T) { + ns := "namespace" + pipelineName := "parent-pipeline" + prName := "parent-run" + childPRName := "parent-run-call-child" + childTRName := "parent-run-call-child-greet" + childTRPod := "parent-run-call-child-greet-pod" + childTR := &v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{Name: childTRName, Namespace: ns}, + Spec: v1.TaskRunSpec{TaskRef: &v1.TaskRef{Name: "greet"}}, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded, + }}}, + TaskRunStatusFields: v1.TaskRunStatusFields{ + StartTime: &metav1.Time{Time: time.Now()}, + PodName: childTRPod, + Steps: []v1.StepState{{Name: "echo", + ContainerState: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{ExitCode: 0}}}}, + }, + }, + } + childPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: childPRName, Namespace: ns}, + Spec: v1.PipelineRunSpec{ + PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{{Name: "greet", TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{ + Steps: []v1.Step{{Name: "echo", Image: "busybox", Script: "echo hello"}}, + }, + }}}, + }, + }, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded, + }}}, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{{ + Name: childTRName, PipelineTaskName: "greet", + TypeMeta: runtime.TypeMeta{Kind: "TaskRun"}, + }}, + }, + }, + } + parentPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: prName, Namespace: ns, + Labels: map[string]string{"tekton.dev/pipeline": pipelineName}}, + Spec: v1.PipelineRunSpec{PipelineRef: &v1.PipelineRef{Name: pipelineName}}, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded, + }}}, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{{ + Name: childPRName, PipelineTaskName: "call-child", + TypeMeta: runtime.TypeMeta{Kind: "PipelineRun"}, + }}, + }, + }, + } + parentPipeline := &v1.Pipeline{ + ObjectMeta: metav1.ObjectMeta{Name: pipelineName, Namespace: ns}, + Spec: v1.PipelineSpec{ + Tasks: []v1.PipelineTask{{Name: "call-child", TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{Steps: []v1.Step{{Name: "placeholder", Image: "busybox"}}}, + }}}, + }, + } + nsList := []*corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: ns}}} + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + PipelineRuns: []*v1.PipelineRun{parentPR, childPR}, + Pipelines: []*v1.Pipeline{parentPipeline}, + TaskRuns: []*v1.TaskRun{childTR}, + Pods: []*corev1.Pod{{ObjectMeta: metav1.ObjectMeta{Name: childTRPod, Namespace: ns}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "echo", Image: "busybox"}}}, + }}, + Namespaces: nsList, + }) + cs.Pipeline.Resources = cb.APIResourceList("v1", []string{"task", "taskrun", "pipeline", "pipelinerun"}) + tdc := testDynamic.Options{} + dc, err := tdc.Client( + cb.UnstructuredPR(parentPR, "v1"), + cb.UnstructuredPR(childPR, "v1"), + cb.UnstructuredP(parentPipeline, "v1"), + cb.UnstructuredTR(childTR, "v1"), + ) + if err != nil { + t.Fatal(err) + } + fakeLogStream := fake.Logs( + fake.Task(childTRPod, fake.Step("echo", "Hello from child\n")), + ) + prlo := logOpts(prName, ns, cs, dc, fake.Streamer(fakeLogStream), false, false, true) + output, err := fetchLogs(prlo) + if err != nil { + t.Fatal(err) + } + test.AssertOutput(t, "[call-child > greet : echo] Hello from child\n\n", output) +} +func TestPipelineRunLogs_PinP_WithDirectTasks(t *testing.T) { + ns := "namespace" + pipelineName := "mix-pipeline" + prName := "mix-run" + // Build: task "build" (direct TaskRun) + task "deploy" (PinP child with task "greet") + // Expected: [build : step1] ... [deploy > greet : echo] ... + buildTRName := "mix-run-build" + buildTRPod := "mix-run-build-pod" + childPRName := "mix-run-deploy" + childTRName := "mix-run-deploy-greet" + childTRPod := "mix-run-deploy-greet-pod" + buildTR := &v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{Name: buildTRName, Namespace: ns}, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded}}}, + TaskRunStatusFields: v1.TaskRunStatusFields{ + StartTime: &metav1.Time{Time: time.Now()}, PodName: buildTRPod, + Steps: []v1.StepState{{Name: "step1", + ContainerState: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{ExitCode: 0}}}}, + }, + }, + } + childTR := &v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{Name: childTRName, Namespace: ns}, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded}}}, + TaskRunStatusFields: v1.TaskRunStatusFields{ + StartTime: &metav1.Time{Time: time.Now()}, PodName: childTRPod, + Steps: []v1.StepState{{Name: "echo", + ContainerState: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{ExitCode: 0}}}}, + }, + }, + } + childPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: childPRName, Namespace: ns}, + Spec: v1.PipelineRunSpec{PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{{Name: "greet", TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{Steps: []v1.Step{{Name: "echo", Image: "busybox"}}}}, + }}, + }}, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded}}}, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{{ + Name: childTRName, PipelineTaskName: "greet", + TypeMeta: runtime.TypeMeta{Kind: "TaskRun"}}}, + }, + }, + } + parentPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: prName, Namespace: ns, + Labels: map[string]string{"tekton.dev/pipeline": pipelineName}}, + Spec: v1.PipelineRunSpec{PipelineRef: &v1.PipelineRef{Name: pipelineName}}, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded}}}, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{ + {Name: buildTRName, PipelineTaskName: "build", + TypeMeta: runtime.TypeMeta{Kind: "TaskRun"}}, + {Name: childPRName, PipelineTaskName: "deploy", + TypeMeta: runtime.TypeMeta{Kind: "PipelineRun"}}, + }, + }, + }, + } + parentPipeline := &v1.Pipeline{ + ObjectMeta: metav1.ObjectMeta{Name: pipelineName, Namespace: ns}, + Spec: v1.PipelineSpec{ + Tasks: []v1.PipelineTask{ + {Name: "build", TaskRef: &v1.TaskRef{Name: "build-task"}}, + {Name: "deploy", TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{Steps: []v1.Step{{Name: "p", Image: "busybox"}}}}}, + }, + }, + } + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + PipelineRuns: []*v1.PipelineRun{parentPR, childPR}, + Pipelines: []*v1.Pipeline{parentPipeline}, + TaskRuns: []*v1.TaskRun{buildTR, childTR}, + Pods: []*corev1.Pod{ + {ObjectMeta: metav1.ObjectMeta{Name: buildTRPod, Namespace: ns}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "step1", Image: "alpine"}}}}, + {ObjectMeta: metav1.ObjectMeta{Name: childTRPod, Namespace: ns}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "echo", Image: "busybox"}}}}, + }, + Namespaces: []*corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: ns}}}, + }) + cs.Pipeline.Resources = cb.APIResourceList("v1", []string{"task", "taskrun", "pipeline", "pipelinerun"}) + tdc := testDynamic.Options{} + dc, err := tdc.Client( + cb.UnstructuredPR(parentPR, "v1"), + cb.UnstructuredPR(childPR, "v1"), + cb.UnstructuredP(parentPipeline, "v1"), + cb.UnstructuredTR(buildTR, "v1"), + cb.UnstructuredTR(childTR, "v1"), + ) + if err != nil { + t.Fatal(err) + } + fakeLogStream := fake.Logs( + fake.Task(buildTRPod, fake.Step("step1", "building done\n")), + fake.Task(childTRPod, fake.Step("echo", "deploy done\n")), + ) + prlo := logOpts(prName, ns, cs, dc, fake.Streamer(fakeLogStream), false, false, true) + output, err := fetchLogs(prlo) + if err != nil { + t.Fatal(err) + } + expected := "[build : step1] building done\n\n[deploy > greet : echo] deploy done" + test.AssertOutput(t, expected, strings.TrimSpace(output)) +} +func TestPipelineRunLogs_PinP_DeepNesting(t *testing.T) { + ns := "namespace" + pipelineName := "deep-parent-pipeline" + prName := "deep-parent-run" + grandchildTRName := "deep-parent-run-call-greet" + grandchildTRPod := "deep-parent-run-call-greet-pod" + grandchildTR := &v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{Name: grandchildTRName, Namespace: ns}, + Spec: v1.TaskRunSpec{TaskRef: &v1.TaskRef{Name: "greet"}}, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded, + }}}, + TaskRunStatusFields: v1.TaskRunStatusFields{ + StartTime: &metav1.Time{Time: time.Now()}, + PodName: grandchildTRPod, + Steps: []v1.StepState{{Name: "echo", + ContainerState: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{ExitCode: 0}}}}, + }, + }, + } + grandchildPRName := "deep-parent-run-call-grandchild" + grandchildPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: grandchildPRName, Namespace: ns}, + Spec: v1.PipelineRunSpec{ + PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{{Name: "greet", TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{ + Steps: []v1.Step{{Name: "echo", Image: "busybox", Script: "echo hello"}}, + }, + }}}, + }, + }, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded, + }}}, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{{ + Name: grandchildTRName, PipelineTaskName: "greet", + TypeMeta: runtime.TypeMeta{Kind: "TaskRun"}, + }}, + }, + }, + } + childPRName := "deep-parent-run-call-child" + childPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: childPRName, Namespace: ns}, + Spec: v1.PipelineRunSpec{ + PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{{Name: "call-grandchild", TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{Steps: []v1.Step{{Name: "placeholder", Image: "busybox"}}}, + }}}, + }, + }, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded, + }}}, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{{ + Name: grandchildPRName, PipelineTaskName: "call-grandchild", + TypeMeta: runtime.TypeMeta{Kind: "PipelineRun"}, + }}, + }, + }, + } + parentPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: prName, Namespace: ns, + Labels: map[string]string{"tekton.dev/pipeline": pipelineName}}, + Spec: v1.PipelineRunSpec{PipelineRef: &v1.PipelineRef{Name: pipelineName}}, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded, + }}}, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{{ + Name: childPRName, PipelineTaskName: "call-child", + TypeMeta: runtime.TypeMeta{Kind: "PipelineRun"}, + }}, + }, + }, + } + parentPipeline := &v1.Pipeline{ + ObjectMeta: metav1.ObjectMeta{Name: pipelineName, Namespace: ns}, + Spec: v1.PipelineSpec{ + Tasks: []v1.PipelineTask{{Name: "call-child", TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{Steps: []v1.Step{{Name: "placeholder", Image: "busybox"}}}, + }}}, + }, + } + nsList := []*corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: ns}}} + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + PipelineRuns: []*v1.PipelineRun{parentPR, childPR, grandchildPR}, + Pipelines: []*v1.Pipeline{parentPipeline}, + TaskRuns: []*v1.TaskRun{grandchildTR}, + Pods: []*corev1.Pod{{ObjectMeta: metav1.ObjectMeta{Name: grandchildTRPod, Namespace: ns}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "echo", Image: "busybox"}}}, + }}, + Namespaces: nsList, + }) + cs.Pipeline.Resources = cb.APIResourceList("v1", []string{"task", "taskrun", "pipeline", "pipelinerun"}) + tdc := testDynamic.Options{} + dc, err := tdc.Client( + cb.UnstructuredPR(parentPR, "v1"), + cb.UnstructuredPR(childPR, "v1"), + cb.UnstructuredPR(grandchildPR, "v1"), + cb.UnstructuredP(parentPipeline, "v1"), + cb.UnstructuredTR(grandchildTR, "v1"), + ) + if err != nil { + t.Fatal(err) + } + fakeLogStream := fake.Logs( + fake.Task(grandchildTRPod, fake.Step("echo", "Hello from grandchild\n")), + ) + prlo := logOpts(prName, ns, cs, dc, fake.Streamer(fakeLogStream), false, false, true) + output, err := fetchLogs(prlo) + if err != nil { + t.Fatal(err) + } + test.AssertOutput(t, "[call-child > call-grandchild > greet : echo] Hello from grandchild\n\n", output) +} +func TestPipelineRunLogs_PinP_Finally(t *testing.T) { + ns := "namespace" + pipelineName := "finally-pipeline" + prName := "finally-run" + childPRName := "finally-run-deploy" + greetTRName := "finally-run-deploy-greet" + greetTRPod := "finally-run-deploy-greet-pod" + cleanupTRName := "finally-run-deploy-cleanup" + cleanupTRPod := "finally-run-deploy-cleanup-pod" + greetTR := &v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{Name: greetTRName, Namespace: ns}, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded}}}, + TaskRunStatusFields: v1.TaskRunStatusFields{ + StartTime: &metav1.Time{Time: time.Now()}, PodName: greetTRPod, + Steps: []v1.StepState{{Name: "echo", + ContainerState: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{ExitCode: 0}}}}, + }, + }, + } + cleanupTR := &v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{Name: cleanupTRName, Namespace: ns}, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded}}}, + TaskRunStatusFields: v1.TaskRunStatusFields{ + StartTime: &metav1.Time{Time: time.Now()}, PodName: cleanupTRPod, + Steps: []v1.StepState{{Name: "echo", + ContainerState: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{ExitCode: 0}}}}, + }, + }, + } + childPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: childPRName, Namespace: ns}, + Spec: v1.PipelineRunSpec{PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{{Name: "greet", TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{Steps: []v1.Step{{Name: "echo", Image: "busybox"}}}}}, + }, + Finally: []v1.PipelineTask{{Name: "cleanup", TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{Steps: []v1.Step{{Name: "echo", Image: "busybox"}}}}}, + }, + }}, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded}}}, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{ + {Name: greetTRName, PipelineTaskName: "greet", + TypeMeta: runtime.TypeMeta{Kind: "TaskRun"}}, + {Name: cleanupTRName, PipelineTaskName: "cleanup", + TypeMeta: runtime.TypeMeta{Kind: "TaskRun"}}, + }, + }, + }, + } + parentPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: prName, Namespace: ns, + Labels: map[string]string{"tekton.dev/pipeline": pipelineName}}, + Spec: v1.PipelineRunSpec{PipelineRef: &v1.PipelineRef{Name: pipelineName}}, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded}}}, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{{ + Name: childPRName, PipelineTaskName: "deploy", + TypeMeta: runtime.TypeMeta{Kind: "PipelineRun"}}, + }, + }, + }, + } + parentPipeline := &v1.Pipeline{ + ObjectMeta: metav1.ObjectMeta{Name: pipelineName, Namespace: ns}, + Spec: v1.PipelineSpec{ + Tasks: []v1.PipelineTask{{Name: "deploy", TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{Steps: []v1.Step{{Name: "p", Image: "busybox"}}}}}, + }, + }, + } + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + PipelineRuns: []*v1.PipelineRun{parentPR, childPR}, + Pipelines: []*v1.Pipeline{parentPipeline}, + TaskRuns: []*v1.TaskRun{greetTR, cleanupTR}, + Pods: []*corev1.Pod{ + {ObjectMeta: metav1.ObjectMeta{Name: greetTRPod, Namespace: ns}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "echo", Image: "busybox"}}}}, + {ObjectMeta: metav1.ObjectMeta{Name: cleanupTRPod, Namespace: ns}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "echo", Image: "busybox"}}}}, + }, + Namespaces: []*corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: ns}}}, + }) + cs.Pipeline.Resources = cb.APIResourceList("v1", []string{"task", "taskrun", "pipeline", "pipelinerun"}) + tdc := testDynamic.Options{} + dc, err := tdc.Client( + cb.UnstructuredPR(parentPR, "v1"), + cb.UnstructuredPR(childPR, "v1"), + cb.UnstructuredP(parentPipeline, "v1"), + cb.UnstructuredTR(greetTR, "v1"), + cb.UnstructuredTR(cleanupTR, "v1"), + ) + if err != nil { + t.Fatal(err) + } + fakeLogStream := fake.Logs( + fake.Task(greetTRPod, fake.Step("echo", "Hello from child\n")), + fake.Task(cleanupTRPod, fake.Step("echo", "Cleaning up\n")), + ) + prlo := logOpts(prName, ns, cs, dc, fake.Streamer(fakeLogStream), false, false, true) + output, err := fetchLogs(prlo) + if err != nil { + t.Fatal(err) + } + expectedLogs := []string{ + "[deploy > greet : echo] Hello from child\n", + "[deploy > cleanup : echo] Cleaning up\n", + } + test.AssertOutput(t, strings.Join(expectedLogs, "\n")+"\n", output) +} +func TestPipelineRunLogs_PinP_MultipleChildren(t *testing.T) { + ns := "namespace" + pipelineName := "multi-pipeline" + prName := "multi-run" + alphaPRName := "multi-run-alpha" + betaPRName := "multi-run-beta" + greetTRName := "multi-run-alpha-greet" + greetTRPod := "multi-run-alpha-greet-pod" + buildTRName := "multi-run-beta-build" + buildTRPod := "multi-run-beta-build-pod" + greetTR := &v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{Name: greetTRName, Namespace: ns}, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded}}}, + TaskRunStatusFields: v1.TaskRunStatusFields{ + StartTime: &metav1.Time{Time: time.Now()}, PodName: greetTRPod, + Steps: []v1.StepState{{Name: "echo", + ContainerState: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{ExitCode: 0}}}}, + }, + }, + } + buildTR := &v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{Name: buildTRName, Namespace: ns}, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded}}}, + TaskRunStatusFields: v1.TaskRunStatusFields{ + StartTime: &metav1.Time{Time: time.Now()}, PodName: buildTRPod, + Steps: []v1.StepState{{Name: "step1", + ContainerState: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{ExitCode: 0}}}}, + }, + }, + } + alphaPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: alphaPRName, Namespace: ns}, + Spec: v1.PipelineRunSpec{PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{{Name: "greet", TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{Steps: []v1.Step{{Name: "echo", Image: "busybox"}}}}}, + }, + }}, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded}}}, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{{ + Name: greetTRName, PipelineTaskName: "greet", + TypeMeta: runtime.TypeMeta{Kind: "TaskRun"}}}, + }, + }, + } + betaPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: betaPRName, Namespace: ns}, + Spec: v1.PipelineRunSpec{PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{{Name: "build", TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{Steps: []v1.Step{{Name: "step1", Image: "alpine"}}}}}, + }, + }}, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded}}}, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{{ + Name: buildTRName, PipelineTaskName: "build", + TypeMeta: runtime.TypeMeta{Kind: "TaskRun"}}}, + }, + }, + } + parentPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: prName, Namespace: ns, + Labels: map[string]string{"tekton.dev/pipeline": pipelineName}}, + Spec: v1.PipelineRunSpec{PipelineRef: &v1.PipelineRef{Name: pipelineName}}, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded}}}, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{ + {Name: alphaPRName, PipelineTaskName: "call-alpha", + TypeMeta: runtime.TypeMeta{Kind: "PipelineRun"}}, + {Name: betaPRName, PipelineTaskName: "call-beta", + TypeMeta: runtime.TypeMeta{Kind: "PipelineRun"}}, + }, + }, + }, + } + parentPipeline := &v1.Pipeline{ + ObjectMeta: metav1.ObjectMeta{Name: pipelineName, Namespace: ns}, + Spec: v1.PipelineSpec{ + Tasks: []v1.PipelineTask{ + {Name: "call-alpha", TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{Steps: []v1.Step{{Name: "p", Image: "busybox"}}}}}, + {Name: "call-beta", TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{Steps: []v1.Step{{Name: "p", Image: "busybox"}}}}}, + }, + }, + } + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + PipelineRuns: []*v1.PipelineRun{parentPR, alphaPR, betaPR}, + Pipelines: []*v1.Pipeline{parentPipeline}, + TaskRuns: []*v1.TaskRun{greetTR, buildTR}, + Pods: []*corev1.Pod{ + {ObjectMeta: metav1.ObjectMeta{Name: greetTRPod, Namespace: ns}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "echo", Image: "busybox"}}}}, + {ObjectMeta: metav1.ObjectMeta{Name: buildTRPod, Namespace: ns}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "step1", Image: "alpine"}}}}, + }, + Namespaces: []*corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: ns}}}, + }) + cs.Pipeline.Resources = cb.APIResourceList("v1", []string{"task", "taskrun", "pipeline", "pipelinerun"}) + tdc := testDynamic.Options{} + dc, err := tdc.Client( + cb.UnstructuredPR(parentPR, "v1"), + cb.UnstructuredPR(alphaPR, "v1"), + cb.UnstructuredPR(betaPR, "v1"), + cb.UnstructuredP(parentPipeline, "v1"), + cb.UnstructuredTR(greetTR, "v1"), + cb.UnstructuredTR(buildTR, "v1"), + ) + if err != nil { + t.Fatal(err) + } + fakeLogStream := fake.Logs( + fake.Task(greetTRPod, fake.Step("echo", "Hello from alpha\n")), + fake.Task(buildTRPod, fake.Step("step1", "Hello from beta\n")), + ) + prlo := logOpts(prName, ns, cs, dc, fake.Streamer(fakeLogStream), false, false, true) + output, err := fetchLogs(prlo) + if err != nil { + t.Fatal(err) + } + expectedLogs := []string{ + "[call-alpha > greet : echo] Hello from alpha\n", + "[call-beta > build : step1] Hello from beta\n", + } + test.AssertOutput(t, strings.Join(expectedLogs, "\n")+"\n", output) +} diff --git a/pkg/cmd/pipelinerun/testdata/TestPipelineRunDescribe_PinP.golden b/pkg/cmd/pipelinerun/testdata/TestPipelineRunDescribe_PinP.golden new file mode 100644 index 0000000000..e1178f41c9 --- /dev/null +++ b/pkg/cmd/pipelinerun/testdata/TestPipelineRunDescribe_PinP.golden @@ -0,0 +1,18 @@ +Name: pinp-pipeline-run +Namespace: ns +Pipeline Ref: pipeline +Labels: + tekton.dev/pipeline=pipeline + +Status + +STARTED DURATION STATUS +10 minutes ago 5m0s Succeeded + +Timeouts + Pipeline: 1h0m0s + +Taskruns + + NAME TASK NAME STARTED DURATION STATUS + pinp-run-call-child-greet call-child > greet 8 minutes ago 3m0s Succeeded diff --git a/pkg/log/pipeline_reader.go b/pkg/log/pipeline_reader.go index 19cb59312b..12d1618852 100644 --- a/pkg/log/pipeline_reader.go +++ b/pkg/log/pipeline_reader.go @@ -221,6 +221,7 @@ func (r *Reader) getOrderedTasks(pr *v1.PipelineRun) ([]taskrunpkg.Run, error) { if pr.Spec.PipelineRef.Resolver != "" { if pr.Status.PipelineSpec != nil { tasks = append(tasks, pr.Status.PipelineSpec.Tasks...) + tasks = append(tasks, pr.Status.PipelineSpec.Finally...) } else { return nil, fmt.Errorf("pipelinerun %s does not have the PipelineRunSpec", pr.Name) } @@ -238,14 +239,51 @@ func (r *Reader) getOrderedTasks(pr *v1.PipelineRun) ([]taskrunpkg.Run, error) { default: return nil, fmt.Errorf("pipelinerun %s did not provide PipelineRef or PipelineSpec", pr.Name) } - trsMap, err := pipelinerunpkg.GetTaskRunsWithStatus(pr, r.clients, r.ns) if err != nil { return nil, err } - - // Sort taskruns, to display the taskrun logs as per pipeline tasks order - return taskrunpkg.SortTasksBySpecOrder(tasks, trsMap), nil + // Build child PipelineRun lookup + childPRs := map[string]*v1.PipelineRun{} + for _, cr := range pr.Status.ChildReferences { + if cr.Kind == "PipelineRun" { + childPR, err := pipelinerunpkg.GetPipelineRun(pipelineRunGroupResource, r.clients, cr.Name, r.ns) + if err != nil { + return nil, err + } + childPRs[cr.PipelineTaskName] = childPR + } + } + // Build PipelineTaskName -> TaskRun name lookup for direct TaskRun children + trNames := map[string]string{} + for name, t := range trsMap { + trNames[t.PipelineTaskName] = name + } + var ordered []taskrunpkg.Run + for _, pt := range tasks { + if _, ok := trNames[pt.Name]; ok { + // Direct TaskRun child — use existing sort logic + ordered = append(ordered, taskrunpkg.SortTasksBySpecOrder([]v1.PipelineTask{pt}, trsMap)...) + } else if childPR, ok := childPRs[pt.Name]; ok { + // PipelineRun child — recurse and prefix + childTasks, err := r.getChildOrderedTasks(childPR, pt.Name) + if err != nil { + return nil, err + } + ordered = append(ordered, childTasks...) + } + } + return ordered, nil +} +func (r *Reader) getChildOrderedTasks(childPR *v1.PipelineRun, parentTaskName string) ([]taskrunpkg.Run, error) { + childOrdered, err := r.getOrderedTasks(childPR) + if err != nil { + return nil, err + } + for i := range childOrdered { + childOrdered[i].Task = parentTaskName + " > " + childOrdered[i].Task + } + return childOrdered, nil } func empty(status v1.PipelineRunStatus) bool { diff --git a/pkg/pipelinerun/description.go b/pkg/pipelinerun/description.go index 08ed306e15..25b84078eb 100644 --- a/pkg/pipelinerun/description.go +++ b/pkg/pipelinerun/description.go @@ -175,20 +175,17 @@ func PrintPipelineRunDescription(out io.Writer, c *cli.Clients, ns string, prNam return fmt.Errorf("failed to find pipelinerun %q", prName) } + trStatuses, err := GetTaskRunsWithStatus(pr, c, ns) + if err != nil { + return err + } var taskRunList TaskRunWithStatusList - for _, child := range pr.Status.ChildReferences { - if child.Kind == "TaskRun" { - var tr *v1.TaskRun - err = actions.GetV1(taskrunGroupResource, c, child.Name, ns, metav1.GetOptions{}, &tr) - if err != nil { - return fmt.Errorf("failed to find get taskruns of the pipelineruns") - } - taskRunList = append(taskRunList, TaskRunWithStatus{ - tr.Name, - child.PipelineTaskName, - &tr.Status, - }) - } + for trName, trs := range trStatuses { + taskRunList = append(taskRunList, TaskRunWithStatus{ + TaskRunName: trName, + PipelineTaskName: trs.PipelineTaskName, + Status: trs.Status, + }) } if len(taskRunList) != 0 { diff --git a/pkg/pipelinerun/tracker.go b/pkg/pipelinerun/tracker.go index cac39174c6..29871412a7 100644 --- a/pkg/pipelinerun/tracker.go +++ b/pkg/pipelinerun/tracker.go @@ -213,32 +213,49 @@ func (t *Tracker) loggingInProgress(tr string) bool { } func GetTaskRunsWithStatus(pr *v1.PipelineRun, c *cli.Clients, ns string) (map[string]*v1.PipelineRunTaskRunStatus, error) { - // If the PipelineRun is nil, just return + return getTaskRunsWithStatusRecursive(pr, c, ns, "") +} + +func getTaskRunsWithStatusRecursive(pr *v1.PipelineRun, c *cli.Clients, ns string, prefix string) (map[string]*v1.PipelineRunTaskRunStatus, error) { if pr == nil { return nil, nil } - - // If there are no child references return the existing TaskRuns and Runs maps if len(pr.Status.ChildReferences) == 0 { return map[string]*v1.PipelineRunTaskRunStatus{}, nil } - trStatuses := make(map[string]*v1.PipelineRunTaskRunStatus) for _, cr := range pr.Status.ChildReferences { - //TODO: Needs to handle Run, CustomRun later - if cr.Kind == "TaskRun" { + switch cr.Kind { + case "TaskRun": tr, err := taskrunpkg.GetTaskRun(taskrunGroupResource, c, cr.Name, ns) if err != nil { return nil, err } - + taskName := cr.PipelineTaskName + if prefix != "" { + taskName = prefix + " > " + taskName + } trStatuses[cr.Name] = &v1.PipelineRunTaskRunStatus{ - PipelineTaskName: cr.PipelineTaskName, + PipelineTaskName: taskName, Status: &tr.Status, } - + case "PipelineRun": + childPR, err := GetPipelineRun(pipelineRunGroupResource, c, cr.Name, ns) + if err != nil { + return nil, err + } + childPrefix := cr.PipelineTaskName + if prefix != "" { + childPrefix = prefix + " > " + childPrefix + } + childTRs, err := getTaskRunsWithStatusRecursive(childPR, c, ns, childPrefix) + if err != nil { + return nil, err + } + for k, v := range childTRs { + trStatuses[k] = v + } } } - return trStatuses, nil } diff --git a/pkg/pipelinerun/tracker_test.go b/pkg/pipelinerun/tracker_test.go index 49d5822cd8..5e4de6f60a 100644 --- a/pkg/pipelinerun/tracker_test.go +++ b/pkg/pipelinerun/tracker_test.go @@ -37,6 +37,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" k8stest "k8s.io/client-go/testing" + "knative.dev/pkg/apis" duckv1 "knative.dev/pkg/apis/duck/v1" ) @@ -266,3 +267,355 @@ func TestTracker_watchErrorHandler(t *testing.T) { }) } } + +func TestGetTaskRunsWithStatus_DirectTaskRuns(t *testing.T) { + ns := "namespace" + trName := "tr-1" + taskName := "task-1" + tr := &v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{Name: trName, Namespace: ns}, + Status: v1.TaskRunStatus{ + TaskRunStatusFields: v1.TaskRunStatusFields{ + PodName: "pod-1", + StartTime: &metav1.Time{Time: time.Now()}, + }, + Status: duckv1.Status{ + Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, + Type: apis.ConditionSucceeded, + }}, + }, + }, + } + pr := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "pr", Namespace: ns}, + Status: v1.PipelineRunStatus{ + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{{ + Name: trName, + PipelineTaskName: taskName, + TypeMeta: runtime.TypeMeta{Kind: "TaskRun"}, + }}, + }, + }, + } + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + PipelineRuns: []*v1.PipelineRun{pr}, + TaskRuns: []*v1.TaskRun{tr}, + Namespaces: []*corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: ns}}}, + }) + cs.Pipeline.Resources = cb.APIResourceList("v1", []string{"taskrun", "pipelinerun"}) + tdc := testDynamic.Options{} + dc, err := tdc.Client( + cb.UnstructuredPR(pr, "v1"), + cb.UnstructuredTR(tr, "v1"), + ) + if err != nil { + t.Fatal(err) + } + clients := &cli.Clients{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dc} + if err := actions.InitializeAPIGroupRes(clients.Tekton.Discovery()); err != nil { + t.Fatal(err) + } + result, err := GetTaskRunsWithStatus(pr, clients, ns) + if err != nil { + t.Fatal(err) + } + if len(result) != 1 { + t.Fatalf("expected 1 TaskRun, got %d", len(result)) + } + trs, ok := result[trName] + if !ok { + t.Fatalf("expected TaskRun %q in result", trName) + } + if trs.PipelineTaskName != taskName { + t.Errorf("expected PipelineTaskName %q, got %q", taskName, trs.PipelineTaskName) + } +} +func TestGetTaskRunsWithStatus_ChildPipelineRun(t *testing.T) { + ns := "namespace" + childTRName := "parent-run-call-child-greet" + childTaskName := "greet" + parentTaskName := "call-child" + childTR := &v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{Name: childTRName, Namespace: ns}, + Status: v1.TaskRunStatus{ + TaskRunStatusFields: v1.TaskRunStatusFields{ + PodName: "pod-greet", + StartTime: &metav1.Time{Time: time.Now()}, + }, + Status: duckv1.Status{ + Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, + Type: apis.ConditionSucceeded, + }}, + }, + }, + } + childPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "parent-run-call-child", Namespace: ns}, + Status: v1.PipelineRunStatus{ + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{{ + Name: childTRName, + PipelineTaskName: childTaskName, + TypeMeta: runtime.TypeMeta{Kind: "TaskRun"}, + }}, + }, + }, + } + parentPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "parent-run", Namespace: ns}, + Status: v1.PipelineRunStatus{ + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{{ + Name: "parent-run-call-child", + PipelineTaskName: parentTaskName, + TypeMeta: runtime.TypeMeta{Kind: "PipelineRun"}, + }}, + }, + }, + } + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + PipelineRuns: []*v1.PipelineRun{parentPR, childPR}, + TaskRuns: []*v1.TaskRun{childTR}, + Namespaces: []*corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: ns}}}, + }) + cs.Pipeline.Resources = cb.APIResourceList("v1", []string{"taskrun", "pipelinerun"}) + tdc := testDynamic.Options{} + dc, err := tdc.Client( + cb.UnstructuredPR(parentPR, "v1"), + cb.UnstructuredPR(childPR, "v1"), + cb.UnstructuredTR(childTR, "v1"), + ) + if err != nil { + t.Fatal(err) + } + clients := &cli.Clients{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dc} + if err := actions.InitializeAPIGroupRes(clients.Tekton.Discovery()); err != nil { + t.Fatal(err) + } + result, err := GetTaskRunsWithStatus(parentPR, clients, ns) + if err != nil { + t.Fatal(err) + } + if len(result) != 1 { + t.Fatalf("expected 1 TaskRun, got %d", len(result)) + } + trs, ok := result[childTRName] + if !ok { + t.Fatalf("expected TaskRun %q in result", childTRName) + } + expectedTaskName := parentTaskName + " > " + childTaskName + if trs.PipelineTaskName != expectedTaskName { + t.Errorf("expected PipelineTaskName %q, got %q", expectedTaskName, trs.PipelineTaskName) + } +} +func TestGetTaskRunsWithStatus_DeepNesting(t *testing.T) { + ns := "namespace" + grandchildPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "a-b-c", Namespace: ns}, + Status: v1.PipelineRunStatus{ + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{{ + Name: "tr-c", + PipelineTaskName: "c-task", + TypeMeta: runtime.TypeMeta{Kind: "TaskRun"}, + }}, + }, + }, + } + trC := &v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{Name: "tr-c", Namespace: ns}, + Status: v1.TaskRunStatus{ + TaskRunStatusFields: v1.TaskRunStatusFields{PodName: "pod-c", + StartTime: &metav1.Time{Time: time.Now()}}, + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded}}}, + }, + } + childPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "a-b", Namespace: ns}, + Status: v1.PipelineRunStatus{ + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{{ + Name: "a-b-c", + PipelineTaskName: "b-task", + TypeMeta: runtime.TypeMeta{Kind: "PipelineRun"}, + }}, + }, + }, + } + parentPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "a", Namespace: ns}, + Status: v1.PipelineRunStatus{ + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{{ + Name: "a-b", + PipelineTaskName: "a-task", + TypeMeta: runtime.TypeMeta{Kind: "PipelineRun"}, + }}, + }, + }, + } + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + PipelineRuns: []*v1.PipelineRun{parentPR, childPR, grandchildPR}, + TaskRuns: []*v1.TaskRun{trC}, + Namespaces: []*corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: ns}}}, + }) + cs.Pipeline.Resources = cb.APIResourceList("v1", []string{"taskrun", "pipelinerun"}) + tdc := testDynamic.Options{} + dc, err := tdc.Client( + cb.UnstructuredPR(parentPR, "v1"), + cb.UnstructuredPR(childPR, "v1"), + cb.UnstructuredPR(grandchildPR, "v1"), + cb.UnstructuredTR(trC, "v1"), + ) + if err != nil { + t.Fatal(err) + } + clients := &cli.Clients{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dc} + if err := actions.InitializeAPIGroupRes(clients.Tekton.Discovery()); err != nil { + t.Fatal(err) + } + result, err := GetTaskRunsWithStatus(parentPR, clients, ns) + if err != nil { + t.Fatal(err) + } + if len(result) != 1 { + t.Fatalf("expected 1 TaskRun, got %d", len(result)) + } + trs := result["tr-c"] + expected := "a-task > b-task > c-task" + if trs.PipelineTaskName != expected { + t.Errorf("expected %q, got %q", expected, trs.PipelineTaskName) + } +} +func TestGetTaskRunsWithStatus_Mixed(t *testing.T) { + ns := "namespace" + trDirect := &v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{Name: "tr-build", Namespace: ns}, + Status: v1.TaskRunStatus{ + TaskRunStatusFields: v1.TaskRunStatusFields{PodName: "pod-build", + StartTime: &metav1.Time{Time: time.Now()}}, + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded}}}, + }, + } + trChild := &v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{Name: "tr-child-greet", Namespace: ns}, + Status: v1.TaskRunStatus{ + TaskRunStatusFields: v1.TaskRunStatusFields{PodName: "pod-greet", + StartTime: &metav1.Time{Time: time.Now()}}, + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded}}}, + }, + } + childPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "pr-child", Namespace: ns}, + Status: v1.PipelineRunStatus{ + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{{ + Name: "tr-child-greet", PipelineTaskName: "greet", + TypeMeta: runtime.TypeMeta{Kind: "TaskRun"}, + }}, + }, + }, + } + parentPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "pr-parent", Namespace: ns}, + Status: v1.PipelineRunStatus{ + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{ + { + Name: "tr-build", PipelineTaskName: "build", + TypeMeta: runtime.TypeMeta{Kind: "TaskRun"}, + }, + { + Name: "pr-child", PipelineTaskName: "deploy", + TypeMeta: runtime.TypeMeta{Kind: "PipelineRun"}, + }, + }, + }, + }, + } + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + PipelineRuns: []*v1.PipelineRun{parentPR, childPR}, + TaskRuns: []*v1.TaskRun{trDirect, trChild}, + Namespaces: []*corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: ns}}}, + }) + cs.Pipeline.Resources = cb.APIResourceList("v1", []string{"taskrun", "pipelinerun"}) + tdc := testDynamic.Options{} + dc, err := tdc.Client( + cb.UnstructuredPR(parentPR, "v1"), + cb.UnstructuredPR(childPR, "v1"), + cb.UnstructuredTR(trDirect, "v1"), + cb.UnstructuredTR(trChild, "v1"), + ) + if err != nil { + t.Fatal(err) + } + clients := &cli.Clients{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dc} + if err := actions.InitializeAPIGroupRes(clients.Tekton.Discovery()); err != nil { + t.Fatal(err) + } + result, err := GetTaskRunsWithStatus(parentPR, clients, ns) + if err != nil { + t.Fatal(err) + } + if len(result) != 2 { + t.Fatalf("expected 2 TaskRuns, got %d", len(result)) + } + // Direct TaskRun — no prefix + trs, ok := result["tr-build"] + if !ok { + t.Fatal("expected tr-build in result") + } + if trs.PipelineTaskName != "build" { + t.Errorf("expected 'build', got %q", trs.PipelineTaskName) + } + // Child PipelineRun TaskRun — prefixed + trs, ok = result["tr-child-greet"] + if !ok { + t.Fatal("expected tr-child-greet in result") + } + if trs.PipelineTaskName != "deploy > greet" { + t.Errorf("expected 'deploy > greet', got %q", trs.PipelineTaskName) + } +} +func TestGetTaskRunsWithStatus_Empty(t *testing.T) { + ns := "namespace" + pr := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "pr", Namespace: ns}, + } + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + PipelineRuns: []*v1.PipelineRun{pr}, + Namespaces: []*corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: ns}}}, + }) + cs.Pipeline.Resources = cb.APIResourceList("v1", []string{"pipelinerun"}) + tdc := testDynamic.Options{} + dc, err := tdc.Client(cb.UnstructuredPR(pr, "v1")) + if err != nil { + t.Fatal(err) + } + clients := &cli.Clients{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dc} + if err := actions.InitializeAPIGroupRes(clients.Tekton.Discovery()); err != nil { + t.Fatal(err) + } + result, err := GetTaskRunsWithStatus(pr, clients, ns) + if err != nil { + t.Fatal(err) + } + if len(result) != 0 { + t.Errorf("expected empty map, got %d entries", len(result)) + } +} +func TestGetTaskRunsWithStatus_NilPR(t *testing.T) { + result, err := GetTaskRunsWithStatus(nil, nil, "ns") + if err != nil { + t.Fatal(err) + } + if result != nil { + t.Errorf("expected nil, got %v", result) + } +}