diff --git a/pkg/cmd/pipelinerun/describe_v1_test.go b/pkg/cmd/pipelinerun/describe_v1_test.go index 2105c78f0a..194bb40c75 100644 --- a/pkg/cmd/pipelinerun/describe_v1_test.go +++ b/pkg/cmd/pipelinerun/describe_v1_test.go @@ -2743,3 +2743,246 @@ 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())) +} + +func TestPipelineRunDescribe_PinP_NilStartTime(t *testing.T) { + clock := test.FakeClock() + greetTR := &v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pinp-run-call-child-greet", + Namespace: "ns", + }, + // No StartTime set - simulates a child TaskRun still starting + } + 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..58ffd26657 100644 --- a/pkg/cmd/pipelinerun/logs_test.go +++ b/pkg/cmd/pipelinerun/logs_test.go @@ -24,6 +24,7 @@ import ( "github.com/tektoncd/cli/pkg/options" "github.com/tektoncd/cli/pkg/pods/fake" "github.com/tektoncd/cli/pkg/pods/stream" + taskrunpkg "github.com/tektoncd/cli/pkg/taskrun" "github.com/tektoncd/cli/pkg/test" cb "github.com/tektoncd/cli/pkg/test/builder" testDynamic "github.com/tektoncd/cli/pkg/test/dynamic" @@ -4607,9 +4608,871 @@ func logOpts(name string, ns string, cs pipelinetest.Clients, dc dynamic.Interfa return &logOptions } +type pinPFixture struct { + t *testing.T + ns string + prName string + apiVersion string + + prs []*v1.PipelineRun + pipelines []*v1.Pipeline + taskRuns []*v1.TaskRun + pods []*corev1.Pod + + logEntries []fake.Log + expected string + beforeFetch func(*options.LogOptions) +} + +func newPinPFixture(t *testing.T, prName string) *pinPFixture { + return &pinPFixture{ + t: t, + ns: "namespace", + prName: prName, + apiVersion: "v1", + } +} + +func (f *pinPFixture) withPipelineRuns(prs ...*v1.PipelineRun) *pinPFixture { + f.prs = append(f.prs, prs...) + return f +} + +func (f *pinPFixture) withPipelines(ps ...*v1.Pipeline) *pinPFixture { + f.pipelines = append(f.pipelines, ps...) + return f +} + +func (f *pinPFixture) withTaskRuns(trs ...*v1.TaskRun) *pinPFixture { + f.taskRuns = append(f.taskRuns, trs...) + return f +} + +func (f *pinPFixture) withPods(pods ...*corev1.Pod) *pinPFixture { + f.pods = append(f.pods, pods...) + return f +} + +func (f *pinPFixture) withLogs(entries ...fake.Log) *pinPFixture { + f.logEntries = append(f.logEntries, entries...) + return f +} + +func (f *pinPFixture) expect(expected string) *pinPFixture { + f.expected = expected + return f +} + +func (f *pinPFixture) withBeforeFetch(fn func(*options.LogOptions)) *pinPFixture { + f.beforeFetch = fn + return f +} + +func (f *pinPFixture) run() { + t := f.t + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + PipelineRuns: f.prs, + Pipelines: f.pipelines, + TaskRuns: f.taskRuns, + Pods: f.pods, + Namespaces: []*corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: f.ns}}}, + }) + cs.Pipeline.Resources = cb.APIResourceList(f.apiVersion, []string{"task", "taskrun", "pipeline", "pipelinerun"}) + + var uns []runtime.Object + for _, pr := range f.prs { + uns = append(uns, cb.UnstructuredPR(pr, f.apiVersion)) + } + for _, p := range f.pipelines { + uns = append(uns, cb.UnstructuredP(p, f.apiVersion)) + } + for _, tr := range f.taskRuns { + uns = append(uns, cb.UnstructuredTR(tr, f.apiVersion)) + } + + tdc := testDynamic.Options{} + dc, err := tdc.Client(uns...) + if err != nil { + t.Fatal(err) + } + + prlo := logOpts(f.prName, f.ns, cs, dc, fake.Streamer(fake.Logs(f.logEntries...)), false, false, true) + + if f.beforeFetch != nil { + f.beforeFetch(prlo) + } + + output, err := fetchLogs(prlo) + if err != nil { + t.Fatal(err) + } + test.AssertOutput(t, f.expected, output) +} + +func succeededTR(name, ns, pod, step string) *v1.TaskRun { + return &v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: ns}, + Spec: v1.TaskRunSpec{TaskRef: &v1.TaskRef{Name: step}}, + 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: pod, + Steps: []v1.StepState{{Name: step, + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ExitCode: 0}, + }}}, + }, + }, + } +} + +func TestLogs_Resolver_WithFinally(t *testing.T) { + var ( + pipelineName = "pipeline" + prName = "pipeline-run" + ns = "namespace" + taskName = "task" + finallyName = "finally-task" + trName = "taskrun" + trFinallyName = "taskrun-finally" + taskPodName = "taskPod" + finallyPodName = "finallyPod" + tr1StartTime = test.FakeClock().Now().Add(20 * time.Second) + tr1Step1Name = "step-1" + finallyStepName = "finally-step" + ) + + nsList := []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: ns, + }, + }, + } + + prs := []*v1.PipelineRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: prName, + Namespace: ns, + Labels: map[string]string{"tekton.dev/pipeline": prName}, + }, + Spec: v1.PipelineRunSpec{ + PipelineRef: &v1.PipelineRef{ + ResolverRef: v1.ResolverRef{ + Resolver: "cluster", + Params: v1.Params{ + { + Name: "kind", + Value: v1.ParamValue{Type: v1.ParamTypeString, StringVal: "pipeline"}, + }, + { + Name: "name", + Value: v1.ParamValue{Type: v1.ParamTypeString, StringVal: pipelineName}, + }, + { + Name: "namespace", + Value: v1.ParamValue{Type: v1.ParamTypeString, StringVal: ns}, + }, + }, + }, + }, + }, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionTrue, + Message: "Success", + }, + }, + }, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{ + { + Name: trName, + PipelineTaskName: taskName, + TypeMeta: runtime.TypeMeta{ + APIVersion: "tekton.dev/v1beta1", + Kind: "TaskRun", + }, + }, + { + Name: trFinallyName, + PipelineTaskName: finallyName, + TypeMeta: runtime.TypeMeta{ + APIVersion: "tekton.dev/v1beta1", + Kind: "TaskRun", + }, + }, + }, + PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{ + { + Name: taskName, + TaskRef: &v1.TaskRef{ + Kind: "task", + Name: taskName, + }, + }, + }, + Finally: []v1.PipelineTask{ + { + Name: finallyName, + TaskRef: &v1.TaskRef{ + Kind: "task", + Name: finallyName, + }, + }, + }, + }, + }, + }, + }, + } + + ps := []*v1.Pipeline{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: pipelineName, + Namespace: ns, + }, + Spec: v1.PipelineSpec{ + Tasks: []v1.PipelineTask{ + { + Name: taskName, + TaskRef: &v1.TaskRef{ + Name: taskName, + }, + }, + }, + Finally: []v1.PipelineTask{ + { + Name: finallyName, + TaskRef: &v1.TaskRef{ + Name: finallyName, + }, + }, + }, + }, + }, + } + + p := []*corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: taskPodName, + Namespace: ns, + Labels: map[string]string{"tekton.dev/task": pipelineName}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: tr1Step1Name, + Image: tr1Step1Name + ":latest", + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: finallyPodName, + Namespace: ns, + Labels: map[string]string{"tekton.dev/task": pipelineName}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: finallyStepName, + Image: finallyStepName + ":latest", + }, + }, + }, + }, + } + + trs := []*v1.TaskRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns, + Name: trName, + }, + Spec: v1.TaskRunSpec{ + TaskRef: &v1.TaskRef{ + Name: taskName, + }, + }, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionTrue, + Type: apis.ConditionSucceeded, + }, + }, + }, + TaskRunStatusFields: v1.TaskRunStatusFields{ + StartTime: &metav1.Time{Time: tr1StartTime}, + PodName: taskPodName, + Steps: []v1.StepState{ + { + Name: tr1Step1Name, + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + }, + }, + }, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns, + Name: trFinallyName, + }, + Spec: v1.TaskRunSpec{ + TaskRef: &v1.TaskRef{ + Name: finallyName, + }, + }, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionTrue, + Type: apis.ConditionSucceeded, + }, + }, + }, + TaskRunStatusFields: v1.TaskRunStatusFields{ + StartTime: &metav1.Time{Time: tr1StartTime.Add(30 * time.Second)}, + PodName: finallyPodName, + Steps: []v1.StepState{ + { + Name: finallyStepName, + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + }, + }, + }, + }, + }, + }, + }, + } + + cs, _ := test.SeedTestData(t, pipelinetest.Data{PipelineRuns: prs, Pipelines: ps, Pods: p, TaskRuns: trs, Namespaces: nsList}) + cs.Pipeline.Resources = cb.APIResourceList(version, []string{"task", "taskrun", "pipelinerun"}) + tdc := testDynamic.Options{} + dc, err := tdc.Client( + cb.UnstructuredP(ps[0], version), + cb.UnstructuredPR(prs[0], version), + cb.UnstructuredTR(trs[0], version), + cb.UnstructuredTR(trs[1], version), + ) + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + + fakeLogStream := fake.Logs( + fake.Task(taskPodName, + fake.Step(tr1Step1Name, "task completed\n"), + ), + fake.Task(finallyPodName, + fake.Step(finallyStepName, "finally completed\n"), + ), + ) + prlo := logOpts(prName, ns, cs, dc, fake.Streamer(fakeLogStream), false, false, false) + + output, err := fetchLogs(prlo) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expectedLogs := []string{ + "task completed\n", + "finally completed\n", + } + test.AssertOutput(t, strings.Join(expectedLogs, "\n")+"\n", output) +} + func fetchLogs(lo *options.LogOptions) (string, error) { out := new(bytes.Buffer) lo.Stream = &cli.Stream{Out: out, Err: out} err := Run(lo) return out.String(), err } + +func TestPipelineRunLogs_PinP_Available(t *testing.T) { + childTR := succeededTR("parent-run-call-child-greet", "namespace", "parent-run-call-child-greet-pod", "echo") + childPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "parent-run-call-child", Namespace: "namespace"}, + 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: "parent-run-call-child-greet", PipelineTaskName: "greet", + TypeMeta: runtime.TypeMeta{Kind: "TaskRun"}}}, + }, + }, + } + parentPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "parent-run", Namespace: "namespace", + Labels: map[string]string{"tekton.dev/pipeline": "parent-pipeline"}}, + Spec: v1.PipelineRunSpec{PipelineRef: &v1.PipelineRef{Name: "parent-pipeline"}}, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded}}}, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{{ + Name: "parent-run-call-child", PipelineTaskName: "call-child", + TypeMeta: runtime.TypeMeta{Kind: "PipelineRun"}}}, + }, + }, + } + parentPipeline := &v1.Pipeline{ + ObjectMeta: metav1.ObjectMeta{Name: "parent-pipeline", Namespace: "namespace"}, + Spec: v1.PipelineSpec{Tasks: []v1.PipelineTask{{Name: "call-child", TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{Steps: []v1.Step{{Name: "placeholder", Image: "busybox"}}}}}}}, + } + newPinPFixture(t, "parent-run"). + withPipelineRuns(parentPR, childPR). + withPipelines(parentPipeline). + withTaskRuns(childTR). + withPods(&corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "parent-run-call-child-greet-pod", Namespace: "namespace"}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "echo", Image: "busybox"}}}, + }). + withLogs(fake.Task("parent-run-call-child-greet-pod", fake.Step("echo", "Hello from child\n"))). + expect("[call-child" + taskrunpkg.ChildTaskSeparator + "greet : echo] Hello from child\n\n"). + run() +} + +func TestPipelineRunLogs_PinP_WithDirectTasks(t *testing.T) { + buildTR := succeededTR("mix-run-build", "namespace", "mix-run-build-pod", "step1") + childTR := succeededTR("mix-run-deploy-greet", "namespace", "mix-run-deploy-greet-pod", "echo") + childPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "mix-run-deploy", Namespace: "namespace"}, + 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: "mix-run-deploy-greet", PipelineTaskName: "greet", + TypeMeta: runtime.TypeMeta{Kind: "TaskRun"}}}, + }, + }, + } + parentPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "mix-run", Namespace: "namespace", + Labels: map[string]string{"tekton.dev/pipeline": "mix-pipeline"}}, + Spec: v1.PipelineRunSpec{PipelineRef: &v1.PipelineRef{Name: "mix-pipeline"}}, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded}}}, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{ + {Name: "mix-run-build", PipelineTaskName: "build", TypeMeta: runtime.TypeMeta{Kind: "TaskRun"}}, + {Name: "mix-run-deploy", PipelineTaskName: "deploy", TypeMeta: runtime.TypeMeta{Kind: "PipelineRun"}}, + }, + }, + }, + } + parentPipeline := &v1.Pipeline{ + ObjectMeta: metav1.ObjectMeta{Name: "mix-pipeline", Namespace: "namespace"}, + 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"}}}}}, + }, + }, + } + newPinPFixture(t, "mix-run"). + withPipelineRuns(parentPR, childPR). + withPipelines(parentPipeline). + withTaskRuns(buildTR, childTR). + withPods( + &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "mix-run-build-pod", Namespace: "namespace"}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "step1", Image: "alpine"}}}}, + &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "mix-run-deploy-greet-pod", Namespace: "namespace"}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "echo", Image: "busybox"}}}}, + ). + withLogs( + fake.Task("mix-run-build-pod", fake.Step("step1", "building done\n")), + fake.Task("mix-run-deploy-greet-pod", fake.Step("echo", "deploy done\n")), + ). + expect("[build : step1] building done\n\n[deploy" + taskrunpkg.ChildTaskSeparator + "greet : echo] deploy done\n\n"). + run() +} +func TestPipelineRunLogs_PinP_DeepNesting(t *testing.T) { + grandchildTR := succeededTR("deep-parent-run-call-greet", "namespace", "deep-parent-run-call-greet-pod", "echo") + grandchildPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "deep-parent-run-call-grandchild", Namespace: "namespace"}, + 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: "deep-parent-run-call-greet", PipelineTaskName: "greet", + TypeMeta: runtime.TypeMeta{Kind: "TaskRun"}}}, + }, + }, + } + childPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "deep-parent-run-call-child", Namespace: "namespace"}, + 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: "deep-parent-run-call-grandchild", PipelineTaskName: "call-grandchild", + TypeMeta: runtime.TypeMeta{Kind: "PipelineRun"}}}, + }, + }, + } + parentPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "deep-parent-run", Namespace: "namespace", + Labels: map[string]string{"tekton.dev/pipeline": "deep-parent-pipeline"}}, + Spec: v1.PipelineRunSpec{PipelineRef: &v1.PipelineRef{Name: "deep-parent-pipeline"}}, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded}}}, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{{ + Name: "deep-parent-run-call-child", PipelineTaskName: "call-child", + TypeMeta: runtime.TypeMeta{Kind: "PipelineRun"}}}, + }, + }, + } + parentPipeline := &v1.Pipeline{ + ObjectMeta: metav1.ObjectMeta{Name: "deep-parent-pipeline", Namespace: "namespace"}, + Spec: v1.PipelineSpec{Tasks: []v1.PipelineTask{{Name: "call-child", TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{Steps: []v1.Step{{Name: "placeholder", Image: "busybox"}}}}}}}, + } + newPinPFixture(t, "deep-parent-run"). + withPipelineRuns(parentPR, childPR, grandchildPR). + withPipelines(parentPipeline). + withTaskRuns(grandchildTR). + withPods(&corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "deep-parent-run-call-greet-pod", Namespace: "namespace"}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "echo", Image: "busybox"}}}, + }). + withLogs(fake.Task("deep-parent-run-call-greet-pod", fake.Step("echo", "Hello from grandchild\n"))). + expect("[call-child" + taskrunpkg.ChildTaskSeparator + "call-grandchild" + taskrunpkg.ChildTaskSeparator + "greet : echo] Hello from grandchild\n\n"). + run() +} + +func TestPipelineRunLogs_PinP_PendingChild(t *testing.T) { + startTime := test.FakeClock().Now().Add(20 * time.Second) + trCompleted := succeededTR("taskrun-completed", "namespace", "pod-completed", "build") + trCompleted.Status.TaskRunStatusFields.StartTime = &metav1.Time{Time: startTime} + childTR := succeededTR("child-taskrun", "namespace", "childPod", "deploy") + childTR.Status.TaskRunStatusFields.StartTime = &metav1.Time{Time: startTime.Add(10 * time.Second)} + + parentPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "pipeline-run", Namespace: "namespace", + Labels: map[string]string{"tekton.dev/pipeline": "pipeline-run"}}, + Spec: v1.PipelineRunSpec{PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{ + {Name: "task-completed", TaskRef: &v1.TaskRef{Name: "task-completed"}}, + {Name: "task-pending", TaskRef: &v1.TaskRef{Name: "task-pending"}}, + {Name: "call-child", TaskRef: &v1.TaskRef{Name: "call-child"}}, + }, + }}, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{Status: corev1.ConditionTrue, Message: "Success"}}}, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{ + {Name: "taskrun-completed", PipelineTaskName: "task-completed", + TypeMeta: runtime.TypeMeta{APIVersion: "tekton.dev/v1", Kind: "TaskRun"}}, + {Name: "child-pr", PipelineTaskName: "call-child", + TypeMeta: runtime.TypeMeta{APIVersion: "tekton.dev/v1", Kind: "PipelineRun"}}, + }, + }, + }, + } + childPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "child-pr", Namespace: "namespace"}, + Spec: v1.PipelineRunSpec{PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{{Name: "greet", TaskRef: &v1.TaskRef{Name: "greet"}}}, + }}, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{Status: corev1.ConditionTrue, Message: "Success"}}}, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{{ + Name: "child-taskrun", PipelineTaskName: "greet", + TypeMeta: runtime.TypeMeta{APIVersion: "tekton.dev/v1", Kind: "TaskRun"}}}, + }, + }, + } + newPinPFixture(t, "pipeline-run"). + withPipelineRuns(parentPR, childPR). + withTaskRuns(trCompleted, childTR). + withPods( + &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod-completed", Namespace: "namespace", + Labels: map[string]string{"tekton.dev/task": "task-completed"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "build", Image: "build:latest"}}}}, + &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "childPod", Namespace: "namespace", + Labels: map[string]string{"tekton.dev/task": "greet"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "deploy", Image: "deploy:latest"}}}}, + ). + withLogs( + fake.Task("pod-completed", fake.Step("build", "completed log\n")), + fake.Task("childPod", fake.Step("deploy", "child log\n")), + ). + expect("[task-completed : build] completed log\n\n[call-child" + taskrunpkg.ChildTaskSeparator + "greet : deploy] child log\n\n"). + run() +} + +func TestPipelineRunLogs_PinP_Finally(t *testing.T) { + greetTR := succeededTR("finally-run-deploy-greet", "namespace", "finally-run-deploy-greet-pod", "echo") + cleanupTR := succeededTR("finally-run-deploy-cleanup", "namespace", "finally-run-deploy-cleanup-pod", "echo") + childPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "finally-run-deploy", Namespace: "namespace"}, + 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: "finally-run-deploy-greet", PipelineTaskName: "greet", TypeMeta: runtime.TypeMeta{Kind: "TaskRun"}}, + {Name: "finally-run-deploy-cleanup", PipelineTaskName: "cleanup", TypeMeta: runtime.TypeMeta{Kind: "TaskRun"}}, + }, + }, + }, + } + parentPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "finally-run", Namespace: "namespace", + Labels: map[string]string{"tekton.dev/pipeline": "finally-pipeline"}}, + Spec: v1.PipelineRunSpec{PipelineRef: &v1.PipelineRef{Name: "finally-pipeline"}}, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded}}}, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{{ + Name: "finally-run-deploy", PipelineTaskName: "deploy", + TypeMeta: runtime.TypeMeta{Kind: "PipelineRun"}}}, + }, + }, + } + parentPipeline := &v1.Pipeline{ + ObjectMeta: metav1.ObjectMeta{Name: "finally-pipeline", Namespace: "namespace"}, + Spec: v1.PipelineSpec{Tasks: []v1.PipelineTask{{Name: "deploy", TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{Steps: []v1.Step{{Name: "p", Image: "busybox"}}}}}}}, + } + newPinPFixture(t, "finally-run"). + withPipelineRuns(parentPR, childPR). + withPipelines(parentPipeline). + withTaskRuns(greetTR, cleanupTR). + withPods( + &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "finally-run-deploy-greet-pod", Namespace: "namespace"}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "echo", Image: "busybox"}}}}, + &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "finally-run-deploy-cleanup-pod", Namespace: "namespace"}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "echo", Image: "busybox"}}}}, + ). + withLogs( + fake.Task("finally-run-deploy-greet-pod", fake.Step("echo", "Hello from child\n")), + fake.Task("finally-run-deploy-cleanup-pod", fake.Step("echo", "Cleaning up\n")), + ). + expect("[deploy" + taskrunpkg.ChildTaskSeparator + "greet : echo] Hello from child\n\n[deploy" + taskrunpkg.ChildTaskSeparator + "cleanup : echo] Cleaning up\n\n"). + run() +} + +func TestPipelineRunLogs_PinP_MultipleChildren(t *testing.T) { + greetTR := succeededTR("multi-run-alpha-greet", "namespace", "multi-run-alpha-greet-pod", "echo") + buildTR := succeededTR("multi-run-beta-build", "namespace", "multi-run-beta-build-pod", "step1") + alphaPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "multi-run-alpha", Namespace: "namespace"}, + 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: "multi-run-alpha-greet", PipelineTaskName: "greet", + TypeMeta: runtime.TypeMeta{Kind: "TaskRun"}}}, + }, + }, + } + betaPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "multi-run-beta", Namespace: "namespace"}, + 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: "multi-run-beta-build", PipelineTaskName: "build", + TypeMeta: runtime.TypeMeta{Kind: "TaskRun"}}}, + }, + }, + } + parentPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "multi-run", Namespace: "namespace", + Labels: map[string]string{"tekton.dev/pipeline": "multi-pipeline"}}, + Spec: v1.PipelineRunSpec{PipelineRef: &v1.PipelineRef{Name: "multi-pipeline"}}, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded}}}, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{ + {Name: "multi-run-alpha", PipelineTaskName: "call-alpha", TypeMeta: runtime.TypeMeta{Kind: "PipelineRun"}}, + {Name: "multi-run-beta", PipelineTaskName: "call-beta", TypeMeta: runtime.TypeMeta{Kind: "PipelineRun"}}, + }, + }, + }, + } + parentPipeline := &v1.Pipeline{ + ObjectMeta: metav1.ObjectMeta{Name: "multi-pipeline", Namespace: "namespace"}, + 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"}}}}}, + }, + }, + } + newPinPFixture(t, "multi-run"). + withPipelineRuns(parentPR, alphaPR, betaPR). + withPipelines(parentPipeline). + withTaskRuns(greetTR, buildTR). + withPods( + &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "multi-run-alpha-greet-pod", Namespace: "namespace"}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "echo", Image: "busybox"}}}}, + &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "multi-run-beta-build-pod", Namespace: "namespace"}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "step1", Image: "alpine"}}}}, + ). + withLogs( + fake.Task("multi-run-alpha-greet-pod", fake.Step("echo", "Hello from alpha\n")), + fake.Task("multi-run-beta-build-pod", fake.Step("step1", "Hello from beta\n")), + ). + expect("[call-alpha" + taskrunpkg.ChildTaskSeparator + "greet : echo] Hello from alpha\n\n[call-beta" + taskrunpkg.ChildTaskSeparator + "build : step1] Hello from beta\n\n"). + run() +} + +func TestPipelineRunLogs_PinP_DisplayName(t *testing.T) { + childTR := succeededTR("parent-run-call-child-greet", "namespace", "parent-run-call-child-greet-pod", "echo") + childPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "parent-run-call-child", Namespace: "namespace"}, + Spec: v1.PipelineRunSpec{PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{{Name: "greet", DisplayName: "my-display", 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: "parent-run-call-child-greet", PipelineTaskName: "greet", + TypeMeta: runtime.TypeMeta{Kind: "TaskRun"}}}, + }, + }, + } + parentPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "parent-run", Namespace: "namespace", + Labels: map[string]string{"tekton.dev/pipeline": "parent-pipeline"}}, + Spec: v1.PipelineRunSpec{PipelineRef: &v1.PipelineRef{Name: "parent-pipeline"}}, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{ + Status: corev1.ConditionTrue, Type: apis.ConditionSucceeded}}}, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{{ + Name: "parent-run-call-child", PipelineTaskName: "call-child", + TypeMeta: runtime.TypeMeta{Kind: "PipelineRun"}}}, + }, + }, + } + parentPipeline := &v1.Pipeline{ + ObjectMeta: metav1.ObjectMeta{Name: "parent-pipeline", Namespace: "namespace"}, + Spec: v1.PipelineSpec{Tasks: []v1.PipelineTask{{Name: "call-child", TaskSpec: &v1.EmbeddedTask{ + TaskSpec: v1.TaskSpec{Steps: []v1.Step{{Name: "placeholder", Image: "busybox"}}}}}}}, + } + newPinPFixture(t, "parent-run"). + withPipelineRuns(parentPR, childPR). + withPipelines(parentPipeline). + withTaskRuns(childTR). + withPods(&corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "parent-run-call-child-greet-pod", Namespace: "namespace"}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "echo", Image: "busybox"}}}, + }). + withLogs(fake.Task("parent-run-call-child-greet-pod", fake.Step("echo", "Hello from child\n"))). + withBeforeFetch(func(lo *options.LogOptions) { lo.Long = true }). + expect("[call-child" + taskrunpkg.ChildTaskSeparator + "my-display : echo] Hello from child\n\n"). + run() +} + +func TestPipelineRunLogs_PinP_ChildPR_NotFound(t *testing.T) { + directTR := succeededTR("build-taskrun", "namespace", "build-pod", "step1") + parentPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "pipeline-run", Namespace: "namespace", + Labels: map[string]string{"tekton.dev/pipeline": "pipeline-run"}}, + Spec: v1.PipelineRunSpec{PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{ + {Name: "build", TaskRef: &v1.TaskRef{Name: "build"}}, + {Name: "call-child", TaskRef: &v1.TaskRef{Name: "call-child"}}, + }, + }}, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{Conditions: duckv1.Conditions{{Status: corev1.ConditionTrue, Message: "Success"}}}, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{ + {Name: "build-taskrun", PipelineTaskName: "build", + TypeMeta: runtime.TypeMeta{APIVersion: "tekton.dev/v1", Kind: "TaskRun"}}, + {Name: "missing-child-pr", PipelineTaskName: "call-child", + TypeMeta: runtime.TypeMeta{APIVersion: "tekton.dev/v1", Kind: "PipelineRun"}}, + }, + }, + }, + } + newPinPFixture(t, "pipeline-run"). + withPipelineRuns(parentPR). + withTaskRuns(directTR). + withPods(&corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "build-pod", Namespace: "namespace", + Labels: map[string]string{"tekton.dev/task": "build"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "step1", Image: "step1:latest"}}}, + }). + withLogs(fake.Task("build-pod", fake.Step("step1", "build output\n"))). + expect("[build : step1] build output\n\n"). + run() +} 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/cmd/pipelinerun/testdata/TestPipelineRunDescribe_PinP_NilStartTime.golden b/pkg/cmd/pipelinerun/testdata/TestPipelineRunDescribe_PinP_NilStartTime.golden new file mode 100644 index 0000000000..13e00a0d5a --- /dev/null +++ b/pkg/cmd/pipelinerun/testdata/TestPipelineRunDescribe_PinP_NilStartTime.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 --- --- --- diff --git a/pkg/log/pipeline_reader.go b/pkg/log/pipeline_reader.go index 19cb59312b..4449252b65 100644 --- a/pkg/log/pipeline_reader.go +++ b/pkg/log/pipeline_reader.go @@ -25,6 +25,7 @@ import ( taskrunpkg "github.com/tektoncd/cli/pkg/taskrun" v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" 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/fields" "k8s.io/apimachinery/pkg/runtime" @@ -221,6 +222,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 +240,58 @@ 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 } + // Build PipelineTaskName -> child PipelineRun name lookup + childPRNames := map[string]string{} + for _, cr := range pr.Status.ChildReferences { + if cr.Kind == "PipelineRun" { + childPRNames[cr.PipelineTaskName] = cr.Name + } + } + // 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 childPRName, ok := childPRNames[pt.Name]; ok { + childPR, err := pipelinerunpkg.GetPipelineRun(pipelineRunGroupResource, r.clients, childPRName, r.ns) + if err != nil { + if apierrors.IsNotFound(err) { + continue + } + return nil, err + } + childTasks, err := r.getChildOrderedTasks(childPR, pt.Name) + if err != nil { + return nil, err + } + ordered = append(ordered, childTasks...) + } + } + return ordered, nil +} - // Sort taskruns, to display the taskrun logs as per pipeline tasks order - return taskrunpkg.SortTasksBySpecOrder(tasks, trsMap), 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 + taskrunpkg.ChildTaskSeparator + childOrdered[i].Task + if childOrdered[i].DisplayName != "" { + childOrdered[i].DisplayName = parentTaskName + taskrunpkg.ChildTaskSeparator + childOrdered[i].DisplayName + } + } + 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..8936366be2 100644 --- a/pkg/pipelinerun/tracker.go +++ b/pkg/pipelinerun/tracker.go @@ -17,9 +17,12 @@ package pipelinerun import ( "context" "errors" + "strings" "sync" "time" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "github.com/tektoncd/cli/pkg/actions" "github.com/tektoncd/cli/pkg/cli" taskrunpkg "github.com/tektoncd/cli/pkg/taskrun" @@ -178,7 +181,37 @@ func (t *Tracker) findNewTaskruns(pr *v1.PipelineRun, allowed []string, trStatus ret := []taskrunpkg.Run{} for tr, trs := range trStatuses { retries := 0 - if pr.Status.PipelineSpec != nil { + if strings.Contains(trs.PipelineTaskName, taskrunpkg.ChildTaskSeparator) { + segments := strings.Split(trs.PipelineTaskName, taskrunpkg.ChildTaskSeparator) + currentPR := pr + for i := 0; i < len(segments)-1; i++ { + for _, cr := range currentPR.Status.ChildReferences { + if cr.Kind == "PipelineRun" && cr.PipelineTaskName == segments[i] { + childPR, err := GetPipelineRun(pipelineRunGroupResource, t.Client, cr.Name, t.Ns) + if err == nil { + currentPR = childPR + } + break + } + } + } + leafTaskName := segments[len(segments)-1] + if currentPR != pr && currentPR.Status.PipelineSpec != nil { + for _, pt := range currentPR.Status.PipelineSpec.Tasks { + if pt.Name == leafTaskName { + retries = pt.Retries + break + } + } + if retries == 0 { + for _, pt := range currentPR.Status.PipelineSpec.Finally { + if pt.Name == leafTaskName { + retries = pt.Retries + } + } + } + } + } else if pr.Status.PipelineSpec != nil { for _, pipelineTask := range pr.Status.PipelineSpec.Tasks { if trs.PipelineTaskName == pipelineTask.Name { retries = pipelineTask.Retries @@ -213,32 +246,52 @@ 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 + taskrunpkg.ChildTaskSeparator + 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 { + if apierrors.IsNotFound(err) { + continue + } + return nil, err + } + childPrefix := cr.PipelineTaskName + if prefix != "" { + childPrefix = prefix + taskrunpkg.ChildTaskSeparator + 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..20c9e0074a 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,771 @@ 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) + } +} + +func TestFindNewTaskruns_PinP_RetryCount(t *testing.T) { + ns := "namespace" + childTaskName := "greet" + childTRName := "child-taskrun" + childPRName := "child-pr" + parentPRName := "parent-pr" + parentTaskName := "call-child" + + trs := []*v1.TaskRun{ + { + ObjectMeta: metav1.ObjectMeta{Name: childTRName, Namespace: ns}, + Spec: v1.TaskRunSpec{TaskRef: &v1.TaskRef{Name: childTaskName}}, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{{Status: corev1.ConditionTrue}}, + }, + TaskRunStatusFields: v1.TaskRunStatusFields{ + StartTime: &metav1.Time{Time: time.Now()}, + PodName: childTRName + "-pod", + }, + }, + }, + } + + childPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: childPRName, Namespace: ns}, + Spec: v1.PipelineRunSpec{ + PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{ + {Name: childTaskName, TaskRef: &v1.TaskRef{Name: childTaskName}, Retries: 2}, + }, + }, + }, + Status: v1.PipelineRunStatus{ + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{ + {Name: childTaskName, TaskRef: &v1.TaskRef{Name: childTaskName}, Retries: 2}, + }, + }, + ChildReferences: []v1.ChildStatusReference{ + { + Name: childTRName, + PipelineTaskName: childTaskName, + TypeMeta: runtime.TypeMeta{APIVersion: "tekton.dev/v1", Kind: "TaskRun"}, + }, + }, + }, + }, + } + + parentPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: parentPRName, Namespace: ns}, + Spec: v1.PipelineRunSpec{ + PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{ + {Name: parentTaskName, TaskRef: &v1.TaskRef{Name: parentTaskName}}, + }, + }, + }, + Status: v1.PipelineRunStatus{ + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{ + { + Name: childPRName, + PipelineTaskName: parentTaskName, + TypeMeta: runtime.TypeMeta{APIVersion: "tekton.dev/v1", Kind: "PipelineRun"}, + }, + }, + }, + }, + } + + tdc := testDynamic.Options{} + dynamic, err := tdc.Client( + cb.UnstructuredPR(parentPR, "v1"), + cb.UnstructuredPR(childPR, "v1"), + cb.UnstructuredTR(trs[0], "v1"), + ) + if err != nil { + t.Fatal(err) + } + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Namespaces: []*corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: ns}}}, + PipelineRuns: []*v1.PipelineRun{parentPR, childPR}, + TaskRuns: trs, + }) + cs.Pipeline.Resources = cb.APIResourceList("v1", []string{"pipelinerun", "taskrun"}) + + tc := &cli.Clients{ + Tekton: cs.Pipeline, + Kube: cs.Kube, + Dynamic: dynamic, + } + if err := actions.InitializeAPIGroupRes(tc.Tekton.Discovery()); err != nil { + t.Fatal(err) + } + + tracker := NewTracker(parentPRName, ns, tc) + trStatuses, err := GetTaskRunsWithStatus(parentPR, tc, ns) + if err != nil { + t.Fatal(err) + } + + runs := tracker.findNewTaskruns(parentPR, nil, trStatuses) + if len(runs) != 1 { + t.Fatalf("expected 1 run, got %d: %v", len(runs), runs) + } + + expectedTask := parentTaskName + trh.ChildTaskSeparator + childTaskName + if runs[0].Task != expectedTask { + t.Errorf("expected task %q, got %q", expectedTask, runs[0].Task) + } + if runs[0].Retries != 2 { + t.Errorf("expected retries 2, got %d", runs[0].Retries) + } +} + +func TestFindNewTaskruns_PinP_DeepNestingWithRetry(t *testing.T) { + ns := "namespace" + leafTRName := "leaf-taskrun" + leafTRPod := "leaf-taskrun-pod" + middlePRName := "middle-pr" + leafPRName := "leaf-pr" + + trs := []*v1.TaskRun{ + { + ObjectMeta: metav1.ObjectMeta{Name: leafTRName, Namespace: ns}, + Spec: v1.TaskRunSpec{TaskRef: &v1.TaskRef{Name: "leaf"}}, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{{Status: corev1.ConditionTrue}}, + }, + TaskRunStatusFields: v1.TaskRunStatusFields{ + StartTime: &metav1.Time{Time: time.Now()}, + PodName: leafTRPod, + }, + }, + }, + } + + leafPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: leafPRName, Namespace: ns}, + Status: v1.PipelineRunStatus{ + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{ + {Name: "leaf", TaskRef: &v1.TaskRef{Name: "leaf"}, Retries: 3}, + }, + }, + ChildReferences: []v1.ChildStatusReference{ + { + Name: leafTRName, + PipelineTaskName: "leaf", + TypeMeta: runtime.TypeMeta{APIVersion: "tekton.dev/v1", Kind: "TaskRun"}, + }, + }, + }, + }, + } + + middlePR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: middlePRName, Namespace: ns}, + Status: v1.PipelineRunStatus{ + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{ + {Name: "middle", TaskRef: &v1.TaskRef{Name: "middle"}}, + }, + }, + ChildReferences: []v1.ChildStatusReference{ + { + Name: leafPRName, + PipelineTaskName: "middle", + TypeMeta: runtime.TypeMeta{APIVersion: "tekton.dev/v1", Kind: "PipelineRun"}, + }, + }, + }, + }, + } + + parentPR := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "parent-pr", Namespace: ns}, + Status: v1.PipelineRunStatus{ + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{ + { + Name: middlePRName, + PipelineTaskName: "parent", + TypeMeta: runtime.TypeMeta{APIVersion: "tekton.dev/v1", Kind: "PipelineRun"}, + }, + }, + }, + }, + } + + tdc := testDynamic.Options{} + dynamic, err := tdc.Client( + cb.UnstructuredPR(parentPR, "v1"), + cb.UnstructuredPR(middlePR, "v1"), + cb.UnstructuredPR(leafPR, "v1"), + cb.UnstructuredTR(trs[0], "v1"), + ) + if err != nil { + t.Fatal(err) + } + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Namespaces: []*corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: ns}}}, + PipelineRuns: []*v1.PipelineRun{parentPR, middlePR, leafPR}, + TaskRuns: trs, + }) + cs.Pipeline.Resources = cb.APIResourceList("v1", []string{"pipelinerun", "taskrun"}) + + tc := &cli.Clients{ + Tekton: cs.Pipeline, + Kube: cs.Kube, + Dynamic: dynamic, + } + if err := actions.InitializeAPIGroupRes(tc.Tekton.Discovery()); err != nil { + t.Fatal(err) + } + + tracker := NewTracker("parent-pr", ns, tc) + trStatuses, err := GetTaskRunsWithStatus(parentPR, tc, ns) + if err != nil { + t.Fatal(err) + } + + runs := tracker.findNewTaskruns(parentPR, nil, trStatuses) + if len(runs) != 1 { + t.Fatalf("expected 1 run, got %d: %v", len(runs), runs) + } + + expectedTask := "parent" + trh.ChildTaskSeparator + "middle" + trh.ChildTaskSeparator + "leaf" + if runs[0].Task != expectedTask { + t.Errorf("expected task %q, got %q", expectedTask, runs[0].Task) + } + if runs[0].Retries != 3 { + t.Errorf("expected retries 3, got %d", runs[0].Retries) + } +} + +func TestGetTaskRunsWithStatus_ChildPR_NotFound(t *testing.T) { + ns := "namespace" + directTaskName := "build" + directTRName := "build-taskrun" + childPRTaskName := "call-child" + childPRName := "missing-child" + + trs := []*v1.TaskRun{ + { + ObjectMeta: metav1.ObjectMeta{Name: directTRName, Namespace: ns}, + Spec: v1.TaskRunSpec{TaskRef: &v1.TaskRef{Name: directTaskName}}, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{{Status: corev1.ConditionTrue}}, + }, + TaskRunStatusFields: v1.TaskRunStatusFields{ + StartTime: &metav1.Time{Time: time.Now()}, + PodName: directTRName + "-pod", + }, + }, + }, + } + + pr := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "pr", Namespace: ns}, + Status: v1.PipelineRunStatus{ + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + ChildReferences: []v1.ChildStatusReference{ + { + Name: directTRName, + PipelineTaskName: directTaskName, + TypeMeta: runtime.TypeMeta{APIVersion: "tekton.dev/v1", Kind: "TaskRun"}, + }, + { + Name: childPRName, + PipelineTaskName: childPRTaskName, + TypeMeta: runtime.TypeMeta{APIVersion: "tekton.dev/v1", Kind: "PipelineRun"}, + }, + }, + }, + }, + } + + tdc := testDynamic.Options{} + dynamic, err := tdc.Client( + cb.UnstructuredPR(pr, "v1"), + cb.UnstructuredTR(trs[0], "v1"), + ) + if err != nil { + t.Fatal(err) + } + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Namespaces: []*corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: ns}}}, + PipelineRuns: []*v1.PipelineRun{pr}, + TaskRuns: trs, + }) + cs.Pipeline.Resources = cb.APIResourceList("v1", []string{"pipelinerun", "taskrun"}) + + tc := &cli.Clients{ + Tekton: cs.Pipeline, + Kube: cs.Kube, + Dynamic: dynamic, + } + if err := actions.InitializeAPIGroupRes(tc.Tekton.Discovery()); err != nil { + t.Fatal(err) + } + + result, err := GetTaskRunsWithStatus(pr, tc, ns) + if err != nil { + t.Fatalf("expected no error despite missing child PR, got: %v", err) + } + if len(result) != 1 { + t.Fatalf("expected 1 taskrun (direct only), got %d", len(result)) + } + if result[directTRName].PipelineTaskName != directTaskName { + t.Errorf("expected PipelineTaskName %q, got %q", directTaskName, result[directTRName].PipelineTaskName) + } +} + +func TestFindNewTaskruns_DirectTaskRun_RetryCount(t *testing.T) { + ns := "namespace" + taskName := "build" + trName := "build-taskrun" + + trs := []*v1.TaskRun{ + { + ObjectMeta: metav1.ObjectMeta{Name: trName, Namespace: ns}, + Spec: v1.TaskRunSpec{TaskRef: &v1.TaskRef{Name: taskName}}, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{{Status: corev1.ConditionTrue}}, + }, + TaskRunStatusFields: v1.TaskRunStatusFields{ + StartTime: &metav1.Time{Time: time.Now()}, + PodName: trName + "-pod", + }, + }, + }, + } + + pr := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "my-pipeline-run", Namespace: ns}, + Spec: v1.PipelineRunSpec{ + PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{ + {Name: taskName, TaskRef: &v1.TaskRef{Name: taskName}, Retries: 3}, + }, + }, + }, + Status: v1.PipelineRunStatus{ + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{ + {Name: taskName, TaskRef: &v1.TaskRef{Name: taskName}, Retries: 3}, + }, + }, + ChildReferences: []v1.ChildStatusReference{ + { + Name: trName, + PipelineTaskName: taskName, + TypeMeta: runtime.TypeMeta{APIVersion: "tekton.dev/v1", Kind: "TaskRun"}, + }, + }, + }, + }, + } + + tdc := testDynamic.Options{} + dynamic, err := tdc.Client( + cb.UnstructuredPR(pr, "v1"), + cb.UnstructuredTR(trs[0], "v1"), + ) + if err != nil { + t.Fatal(err) + } + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Namespaces: []*corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: ns}}}, + PipelineRuns: []*v1.PipelineRun{pr}, + TaskRuns: trs, + }) + cs.Pipeline.Resources = cb.APIResourceList("v1", []string{"pipelinerun", "taskrun"}) + + tc := &cli.Clients{ + Tekton: cs.Pipeline, + Kube: cs.Kube, + Dynamic: dynamic, + } + if err := actions.InitializeAPIGroupRes(tc.Tekton.Discovery()); err != nil { + t.Fatal(err) + } + + tracker := NewTracker("my-pipeline-run", ns, tc) + trStatuses, err := GetTaskRunsWithStatus(pr, tc, ns) + if err != nil { + t.Fatal(err) + } + + runs := tracker.findNewTaskruns(pr, nil, trStatuses) + if len(runs) != 1 { + t.Fatalf("expected 1 run, got %d: %v", len(runs), runs) + } + + if runs[0].Task != taskName { + t.Errorf("expected task %q, got %q", taskName, runs[0].Task) + } + if runs[0].Retries != 3 { + t.Errorf("expected retries 3, got %d", runs[0].Retries) + } +} diff --git a/pkg/taskrun/taskrun.go b/pkg/taskrun/taskrun.go index 64d314c5cf..198f80274a 100644 --- a/pkg/taskrun/taskrun.go +++ b/pkg/taskrun/taskrun.go @@ -16,11 +16,16 @@ package taskrun import ( "sort" + "strings" v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +// ChildTaskSeparator separates parent and child pipeline task names in +// Pipelines-in-Pipelines describe/logs output (e.g. "call-child > greet"). +const ChildTaskSeparator = " > " + type Run struct { Name string DisplayName string @@ -63,19 +68,34 @@ func Filter(trs []Run, ts []string) []Run { if len(ts) == 0 { return trs } - filter := map[string]bool{} for _, t := range ts { filter[t] = true } - filtered := []Run{} for _, tr := range trs { if filter[tr.Task] { filtered = append(filtered, tr) + continue + } + for _, t := range ts { + if strings.Contains(t, ChildTaskSeparator) { + continue + } + segments := strings.Split(tr.Task, ChildTaskSeparator) + if len(segments) <= 1 { + continue + } + if strings.HasPrefix(tr.Task, t+ChildTaskSeparator) { + filtered = append(filtered, tr) + break + } + if segments[len(segments)-1] == t { + filtered = append(filtered, tr) + break + } } } - return filtered } diff --git a/pkg/taskrun/taskrun_test.go b/pkg/taskrun/taskrun_test.go new file mode 100644 index 0000000000..3bcc22e57e --- /dev/null +++ b/pkg/taskrun/taskrun_test.go @@ -0,0 +1,60 @@ +// Copyright © 2026 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 taskrun + +import ( + "testing" +) + +func TestFilter(t *testing.T) { + tests := []struct { + name string + trs []Run + filter []string + want int + }{ + {name: "empty filter returns all", trs: []Run{{Task: "build"}, {Task: "test"}}, filter: nil, want: 2}, + {name: "exact match", trs: []Run{{Task: "build"}, {Task: "test"}}, filter: []string{"build"}, want: 1}, + {name: "no match", trs: []Run{{Task: "build"}}, filter: []string{"nonexistent"}, want: 0}, + {name: "PinP exact", trs: []Run{{Task: "call-child" + ChildTaskSeparator + "greet"}}, filter: []string{"call-child" + ChildTaskSeparator + "greet"}, want: 1}, + {name: "PinP parent prefix", trs: []Run{{Task: "call-child" + ChildTaskSeparator + "greet"}}, filter: []string{"call-child"}, want: 1}, + {name: "PinP child suffix", trs: []Run{{Task: "call-child" + ChildTaskSeparator + "greet"}}, filter: []string{"greet"}, want: 1}, + {name: "PinP deep nesting", trs: []Run{{Task: "a" + ChildTaskSeparator + "b" + ChildTaskSeparator + "c"}}, filter: []string{"c"}, want: 1}, + {name: "PinP mixed direct and child", trs: []Run{{Task: "build"}, {Task: "deploy" + ChildTaskSeparator + "greet"}, {Task: "test"}}, filter: []string{"build", "greet"}, want: 2}, + {name: "PinP multiple filters", trs: []Run{{Task: "a" + ChildTaskSeparator + "x"}, {Task: "b" + ChildTaskSeparator + "y"}, {Task: "c" + ChildTaskSeparator + "z"}}, filter: []string{"x", "y"}, want: 2}, + {name: "partial prefix does not match", trs: []Run{{Task: "call-child" + ChildTaskSeparator + "greet"}}, filter: []string{"child"}, want: 0}, + {name: "filter with separator exact only", trs: []Run{{Task: "a" + ChildTaskSeparator + "b" + ChildTaskSeparator + "c"}}, filter: []string{"a" + ChildTaskSeparator + "b"}, want: 0}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Filter(tt.trs, tt.filter) + if len(result) != tt.want { + t.Errorf("expected %d results, got %d", tt.want, len(result)) + } + }) + } +} + +func TestIsFiltered(t *testing.T) { + if IsFiltered(Run{Task: "build"}, []string{"build"}) { + t.Error("expected IsFiltered to be false for matching task") + } + if !IsFiltered(Run{Task: "build"}, []string{"test"}) { + t.Error("expected IsFiltered to be true for non-matching task") + } + if IsFiltered(Run{Task: "call-child" + ChildTaskSeparator + "greet"}, []string{"greet"}) { + t.Error("expected IsFiltered to be false for suffix-matching task") + } +}