Repository: kubeflow/kfp-tekton (language: python)

Issue:
Update PipelineLoop controller to add new Tekton variable "iterationNumberParam"
/kind feature

**Description:**
Related #886 

Update PipelineLoop controller to add new Tekton variable `iterationNumberParam` for parameters that points to the iteration counter. Similar to the `iterateNumeric` for numeric iteration.

The current PipelineLoop controller has the iteration counter stored in the `iteration_number` variable. We want to expose that value into the PipelineLoop spec and able to refer it using a user defined key similar to the `iterateNumeric` feature

`iterateNumeric` example
https://github.com/kubeflow/kfp-tekton/blob/master/sdk/python/tests/compiler/testdata/loop_with_numeric.yaml#L110-L155

**Additional information:**
[Miscellaneous information that will assist in solving the issue.]



Discussion / hints:
/assign @ScrapCodes 

Fix patch:
diff --git a/tekton-catalog/pipeline-loops/examples/loop-example-numeric-param.yaml b/tekton-catalog/pipeline-loops/examples/loop-example-numeric-param.yaml
new file mode 100644
index 0000000000..f0f844d630
--- /dev/null
+++ b/tekton-catalog/pipeline-loops/examples/loop-example-numeric-param.yaml
@@ -0,0 +1,79 @@
+apiVersion: custom.tekton.dev/v1alpha1
+kind: PipelineLoop
+metadata:
+  name: pipelineloop
+spec:
+  # IterationNumberParam points to the current index of the loop, starting from 1.
+  iterationNumberParam: message-index
+  iterateParam: message
+  pipelineSpec:
+    params:
+    - name: message
+      type: string
+    - name: message-index
+      type: string
+    tasks:
+    - name: echo-loop-task
+      params:
+      - name: message
+        value: $(params.message)
+      - name: message-index
+        value: $(params.message-index)
+      taskSpec:
+        params:
+        - name: message
+          type: string
+        - name: message-index
+          type: string
+        steps:
+          - name: echo
+            image: ubuntu
+            imagePullPolicy: IfNotPresent
+            script: |
+              #!/usr/bin/env bash
+              echo "$(params.message-index)) $(params.message)"
+---
+apiVersion: tekton.dev/v1beta1
+kind: PipelineRun
+metadata:
+  annotations:
+    tekton.dev/example-loop-pipeline: '{"spec":{"pipelineSpec":{"params":[{"name":"message","type":"string"}],"tasks":[{"name":"echo-loop-task","params":[{"name":"message","value":"$(params.message)"}],"taskSpec":{"params":[{"name":"message","type":"string"}],"steps":[{"name":"echo","image":"ubuntu","imagePullPolicy":"IfNotPresent","script":"#!/usr/bin/env bash\necho \"$(params.message)\"\n"}]}}]},"iterateParam":"message"}}'
+  name: pr-loop-example
+  labels:
+    pipelines.kubeflow.org/cache_enabled: "true"
+spec:
+  pipelineSpec:
+    tasks:
+      - name: first-task
+        taskSpec:
+          steps:
+            - name: echo
+              image: ubuntu
+              imagePullPolicy: IfNotPresent
+              script: |
+                #!/usr/bin/env bash
+                echo "I am the first task before the loop task"
+      - name: loop-task
+        runAfter:
+          - first-task
+        params:
+          - name: message
+            value:
+              - I am the first one
+              - I am the second one
+              - I am the third one
+        taskRef:
+          apiVersion: custom.tekton.dev/v1alpha1
+          kind: PipelineLoop
+          name: pipelineloop
+      - name: last-task
+        runAfter:
+          - loop-task
+        taskSpec:
+          steps:
+            - name: echo
+              image: ubuntu
+              imagePullPolicy: IfNotPresent
+              script: |
+                #!/usr/bin/env bash
+                echo "I am the last task after the loop task"
diff --git a/tekton-catalog/pipeline-loops/pkg/apis/pipelineloop/v1alpha1/pipelineloop_types.go b/tekton-catalog/pipeline-loops/pkg/apis/pipelineloop/v1alpha1/pipelineloop_types.go
index 5d55f01d30..fcd4dbb021 100644
--- a/tekton-catalog/pipeline-loops/pkg/apis/pipelineloop/v1alpha1/pipelineloop_types.go
+++ b/tekton-catalog/pipeline-loops/pkg/apis/pipelineloop/v1alpha1/pipelineloop_types.go
@@ -55,13 +55,16 @@ type PipelineLoopSpec struct {
 	// +optional
 	IterateParamSeparator string `json:"iterateParamStringSeparator,omitempty"`
 
+	// +optional
+	IterationNumberParam string `json:"iterationNumberParam,omitempty"`
+
 	IterateNumeric string `json:"iterateNumeric"`
 
 	// Time after which the TaskRun times out.
 	// +optional
 	Timeout *metav1.Duration `json:"timeout,omitempty"`
 
-	// Parallelism represents how many pipelines can be triggered simutaniously by the loop.
+	// Parallelism represents how many pipelines can be triggered simultaneously by the loop.
 	// +optional
 	Parallelism int `json:"parallelism,omitempty"`
 
diff --git a/tekton-catalog/pipeline-loops/pkg/reconciler/pipelinelooprun/pipelinelooprun.go b/tekton-catalog/pipeline-loops/pkg/reconciler/pipelinelooprun/pipelinelooprun.go
index 9670306702..fc9aa3cd13 100644
--- a/tekton-catalog/pipeline-loops/pkg/reconciler/pipelinelooprun/pipelinelooprun.go
+++ b/tekton-catalog/pipeline-loops/pkg/reconciler/pipelinelooprun/pipelinelooprun.go
@@ -957,6 +957,12 @@ func getParameters(run *v1alpha1.Run, tls *pipelineloopv1alpha1.PipelineLoopSpec
 			Value: v1beta1.ArrayOrString{Type: v1beta1.ParamTypeString, StringVal: strconv.Itoa(iteration)},
 		})
 	}
+	if tls.IterationNumberParam != "" {
+		out = append(out, v1beta1.Param{
+			Name:  tls.IterationNumberParam,
+			Value: v1beta1.ArrayOrString{Type: v1beta1.ParamTypeString, StringVal: strconv.Itoa(iteration)},
+		})
+	}
 	return out
 }
 


Test patch:
diff --git a/tekton-catalog/pipeline-loops/pkg/reconciler/pipelinelooprun/pipelinelooprun_test.go b/tekton-catalog/pipeline-loops/pkg/reconciler/pipelinelooprun/pipelinelooprun_test.go
index 0b67727b41..ba09d35cc6 100644
--- a/tekton-catalog/pipeline-loops/pkg/reconciler/pipelinelooprun/pipelinelooprun_test.go
+++ b/tekton-catalog/pipeline-loops/pkg/reconciler/pipelinelooprun/pipelinelooprun_test.go
@@ -301,6 +301,16 @@ var aPipelineLoop = &pipelineloopv1alpha1.PipelineLoop{
 	},
 }
 
+var aPipelineLoop2 = &pipelineloopv1alpha1.PipelineLoop{
+	ObjectMeta: metav1.ObjectMeta{Name: "a-pipelineloop2", Namespace: "foo"},
+	Spec: pipelineloopv1alpha1.PipelineLoopSpec{
+		PipelineRef:           &v1beta1.PipelineRef{Name: "a-pipeline"},
+		IterateParam:          "current-item",
+		IterateParamSeparator: "separator",
+		IterationNumberParam:  "additional-parameter",
+	},
+}
+
 var wsPipelineLoop = &pipelineloopv1alpha1.PipelineLoop{
 	ObjectMeta: metav1.ObjectMeta{Name: "ws-pipelineloop", Namespace: "foo"},
 	Spec: pipelineloopv1alpha1.PipelineLoopSpec{
@@ -585,6 +595,34 @@ var runPipelineLoop = &v1alpha1.Run{
 	},
 }
 
+var runPipelineLoop2 = &v1alpha1.Run{
+	ObjectMeta: metav1.ObjectMeta{
+		Name:      "run-pipelineloop",
+		Namespace: "foo",
+		Labels: map[string]string{
+			"myTestLabel":                    "myTestLabelValue",
+			"custom.tekton.dev/pipelineLoop": "a-pipelineloop2",
+			"tekton.dev/pipeline":            "pr-loop-example",
+			"tekton.dev/pipelineRun":         "pr-loop-example",
+			"tekton.dev/pipelineTask":        "loop-task",
+		},
+		Annotations: map[string]string{
+			"myTestAnnotation": "myTestAnnotationValue",
+		},
+	},
+	Spec: v1alpha1.RunSpec{
+		Params: []v1beta1.Param{{
+			Name:  "current-item",
+			Value: v1beta1.ArrayOrString{Type: v1beta1.ParamTypeArray, ArrayVal: []string{"item1", "item2"}},
+		}},
+		Ref: &v1alpha1.TaskRef{
+			APIVersion: pipelineloopv1alpha1.SchemeGroupVersion.String(),
+			Kind:       pipelineloop.PipelineLoopControllerName,
+			Name:       "a-pipelineloop2",
+		},
+	},
+}
+
 var runNestedPipelineLoop = &v1alpha1.Run{
 	ObjectMeta: metav1.ObjectMeta{
 		Name:      "nested-pipelineloop",
@@ -1169,6 +1207,42 @@ var expectedPipelineRunIterateNumeric1 = &v1beta1.PipelineRun{
 	},
 }
 
+var expectedPipelineRunIterateNumericParam = &v1beta1.PipelineRun{
+	ObjectMeta: metav1.ObjectMeta{
+		Name:      "run-pipelineloop-00001-9l9zj",
+		Namespace: "foo",
+		OwnerReferences: []metav1.OwnerReference{{
+			APIVersion:         "tekton.dev/v1alpha1",
+			Kind:               "Run",
+			Name:               "run-pipelineloop",
+			Controller:         &trueB,
+			BlockOwnerDeletion: &trueB,
+		}},
+		Labels: map[string]string{
+			"custom.tekton.dev/originalPipelineRun":   "pr-loop-example",
+			"custom.tekton.dev/parentPipelineRun":     "pr-loop-example",
+			"custom.tekton.dev/pipelineLoop":          "a-pipelineloop2",
+			"tekton.dev/run":                          "run-pipelineloop",
+			"custom.tekton.dev/pipelineLoopIteration": "1",
+			"myTestLabel":                             "myTestLabelValue",
+		},
+		Annotations: map[string]string{
+			"myTestAnnotation": "myTestAnnotationValue",
+			"custom.tekton.dev/pipelineLoopCurrentIterationItem": `"item1"`,
+		},
+	},
+	Spec: v1beta1.PipelineRunSpec{
+		PipelineRef: &v1beta1.PipelineRef{Name: "a-pipeline"},
+		Params: []v1beta1.Param{{
+			Name:  "additional-parameter",
+			Value: v1beta1.ArrayOrString{Type: v1beta1.ParamTypeString, StringVal: "1"},
+		}, {
+			Name:  "current-item",
+			Value: v1beta1.ArrayOrString{Type: v1beta1.ParamTypeString, StringVal: "item1"},
+		}},
+	},
+}
+
 // Note: The pipelinerun for the second iteration has the same random suffix as the first due to the resetting of the seed on each test.
 var expectedPipelineRunIteration2 = &v1beta1.PipelineRun{
 	ObjectMeta: metav1.ObjectMeta{
@@ -1532,6 +1606,16 @@ func TestReconcilePipelineLoopRun(t *testing.T) {
 		expectedReason:       pipelineloopv1alpha1.PipelineLoopRunReasonRunning,
 		expectedPipelineruns: []*v1beta1.PipelineRun{expectedPipelineRunIterateNumeric1},
 		expectedEvents:       []string{"Normal Started", "Normal Running Iterations completed: 0"},
+	}, {
+		name:                 "Reconcile a new run with iterationNumberParam defined",
+		pipeline:             aPipeline,
+		pipelineloop:         aPipelineLoop2,
+		run:                  runPipelineLoop2,
+		pipelineruns:         []*v1beta1.PipelineRun{},
+		expectedStatus:       corev1.ConditionUnknown,
+		expectedReason:       pipelineloopv1alpha1.PipelineLoopRunReasonRunning,
+		expectedPipelineruns: []*v1beta1.PipelineRun{expectedPipelineRunIterateNumericParam},
+		expectedEvents:       []string{"Normal Started", "Normal Running Iterations completed: 0"},
 	}, {
 		name:                 "Reconcile a new run with a pipelineloop that contains an inline task",
 		pipelineloop:         aPipelineLoopWithInlineTask,