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

Issue:
Pipelineloop controller cannot define workspaces.
/kind feature

**Description:**
The pipelineRuns generated by the pipelineloop controller doesn't support workspaces. We need to add a new spec for workspaces at https://github.com/kubeflow/kfp-tekton/blob/master/tekton-catalog/pipeline-loops/pkg/apis/pipelineloop/v1alpha1/pipelineloop_types.go#L41 similar to timeout and retries.


**Additional information:**
[Miscellaneous information that will assist in solving the issue.]



Discussion / hints:
@ScrapCodes can you prioritize this issue first? We can come back to #775 later on. Thanks
/assign @ScrapCodes 

Fix patch:
diff --git a/README.md b/README.md
index a711acb972..1a02add85e 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ for this project.
 
 ![kfp-tekton](images/kfp-tekton.png)
 
-Kubeflow Pipelines is a platform for building and deploying portable, scalable machine learning (ML) workflows. More architectural details about the Kubeflow Pipelines can be found on the [Kubeflow website](https://www.kubeflow.org/docs/components/pipelines/overview/pipelines-overview/).
+Kubeflow Pipelines is a platform for building and deploying portable, scalable machine learning (ML) workflows. More architectural details about the Kubeflow Pipelines can be found on the [Kubeflow website](https://www.kubeflow.org/docs/components/pipelines/overview/).
 
 The Tekton Pipelines project provides Kubernetes-style resources for declaring
 CI/CD-style pipelines. Tekton introduces several [Custom Resource Definitions](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/)(CRDs) including Task, Pipeline, TaskRun, and PipelineRun. A PipelineRun represents a single running instance of a Pipeline and is responsible for creating a Pod for each of its Tasks and as many containers within each Pod as it has Steps. Please look for more details in the [Tekton repo](https://github.com/tektoncd/pipeline).
diff --git a/tekton-catalog/pipeline-loops/examples/loop-example-workspaces.yaml b/tekton-catalog/pipeline-loops/examples/loop-example-workspaces.yaml
new file mode 100644
index 0000000000..81bae24645
--- /dev/null
+++ b/tekton-catalog/pipeline-loops/examples/loop-example-workspaces.yaml
@@ -0,0 +1,132 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: recipe-storage
+data:
+  recipe: |
+    1. dry roast split green gram 1 cup
+    2. turn it into a fine powder somehow
+    3. While it is still warm add jaggery powder 1 cup
+    4. In a seperate pan, heat clarified butter to molten consistency
+    5. Mix all the items, and add only as much ghee that mixture can be bound into sphere ball
+    6. Tasty and healthy (great source of protein/energy) sunnandulu laddus are ready
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+  name: shared-task-storage
+spec:
+  resources:
+    requests:
+      storage: 16Mi
+  volumeMode: Filesystem
+  accessModes:
+    - ReadWriteOnce
+---
+apiVersion: tekton.dev/v1beta1
+kind: Task
+metadata:
+  name: print-data
+spec:
+  workspaces:
+    - name: storage
+      readOnly: true
+  params:
+    - name: filename
+  steps:
+    - name: print-recipe
+      image: ubuntu
+      script: cat $(workspaces.storage.path)/$(params.filename)
+---
+apiVersion: tekton.dev/v1beta1
+kind: Task
+metadata:
+  name: fetch-store-data
+spec:
+  workspaces:
+    - name: recipe-ws
+    - name: storage
+  params:
+    - name: src-filename
+    - name: dst-filename
+  steps:
+    - name: fetch-store-recipe-data
+      image: ubuntu
+      script: |
+        cp $(workspaces.recipe-ws.path)/$(params.src-filename) $(workspaces.storage.path)/$(params.dst-filename)
+---
+apiVersion: tekton.dev/v1beta1
+kind: Pipeline
+metadata:
+  name: fetch-and-print-recipe
+spec:
+  params:
+    - name: dst-filename
+      type: string
+  workspaces:
+    - name: shared-data
+    - name: recipe-store
+  tasks:
+    - name: fetch-the-recipe
+      taskRef:
+        name: fetch-store-data
+      params:
+        - name: src-filename
+          value: recipe.txt
+        - name: dst-filename
+          value: $(params.dst-filename)
+      workspaces:
+        - name: recipe-ws
+          workspace: recipe-store
+        - name: storage
+          workspace: shared-data
+    - name: print-the-recipe
+      runAfter:
+        - fetch-the-recipe
+      taskRef:
+        name: print-data
+      params:
+        - name: filename
+          value: $(params.dst-filename)
+      workspaces:
+        - name: storage
+          workspace: shared-data
+---
+apiVersion: custom.tekton.dev/v1alpha1
+kind: PipelineLoop
+metadata:
+  name: pipelineloop-ws-example
+spec:
+  pipelineRef:
+    name: fetch-and-print-recipe
+  workspaces:
+    - name: recipe-store
+      configMap:
+        name: recipe-storage
+        items:
+          - key: recipe
+            path: recipe.txt
+    - name: shared-data
+      persistentVolumeClaim:
+        claimName: shared-task-storage
+  iterateParam: dst-filename
+---
+apiVersion: tekton.dev/v1beta1
+kind: PipelineRun
+metadata:
+  name: pr-ws-loop-example
+spec:
+  pipelineSpec:
+    tasks:
+      - name: produce-recipies
+        retries: 3
+        params:
+          - name: dst-filename
+            value:
+              - first-recipe.txt
+              - second-recipe.txt
+              - third-recipe.txt
+        taskRef:
+          apiVersion: custom.tekton.dev/v1alpha1
+          kind: PipelineLoop
+          name: pipelineloop-ws-example
diff --git a/tekton-catalog/pipeline-loops/go.mod b/tekton-catalog/pipeline-loops/go.mod
index 142653a094..060840aa8b 100644
--- a/tekton-catalog/pipeline-loops/go.mod
+++ b/tekton-catalog/pipeline-loops/go.mod
@@ -8,7 +8,7 @@ require (
 	github.com/go-openapi/validate v0.19.5 // indirect
 	github.com/google/go-cmp v0.5.6
 	github.com/hashicorp/go-multierror v1.1.1
-	github.com/tektoncd/pipeline v0.29.1-0.20211111015141-52590be06673
+	github.com/tektoncd/pipeline v0.30.0
 	go.uber.org/zap v1.19.1
 	gomodules.xyz/jsonpatch/v2 v2.2.0
 	gopkg.in/evanphx/json-patch.v4 v4.9.0 // indirect
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 fd955a1ea1..c8a3bd57f1 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
@@ -64,6 +64,10 @@ type PipelineLoopSpec struct {
 	// Retries represents how many times a task should be retried in case of task failure.
 	// +optional
 	Retries int `json:"retries,omitempty"`
+
+	// Workspace to a volume mapping to be consumed by a PipelineRun.
+	// +optional
+	Workspaces []v1beta1.WorkspaceBinding `json:"workspaces,omitempty"`
 }
 
 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
diff --git a/tekton-catalog/pipeline-loops/pkg/reconciler/pipelinelooprun/controller.go b/tekton-catalog/pipeline-loops/pkg/reconciler/pipelinelooprun/controller.go
index 07fae50447..9a4043ec66 100644
--- a/tekton-catalog/pipeline-loops/pkg/reconciler/pipelinelooprun/controller.go
+++ b/tekton-catalog/pipeline-loops/pkg/reconciler/pipelinelooprun/controller.go
@@ -46,11 +46,11 @@ func NewController(namespace string) func(context.Context, configmap.Watcher) *c
 		pipelineRunInformer := pipelineruninformer.Get(ctx)
 
 		c := &Reconciler{
-			pipelineClientSet: pipelineclientset,
+			pipelineClientSet:     pipelineclientset,
 			pipelineloopClientSet: pipelineloopclientset,
-			runLister:         runInformer.Lister(),
+			runLister:             runInformer.Lister(),
 			pipelineLoopLister:    pipelineLoopInformer.Lister(),
-			pipelineRunLister: pipelineRunInformer.Lister(),
+			pipelineRunLister:     pipelineRunInformer.Lister(),
 		}
 
 		impl := runreconciler.NewImpl(ctx, c, func(impl *controller.Impl) controller.Options {
diff --git a/tekton-catalog/pipeline-loops/pkg/reconciler/pipelinelooprun/pipelinelooprun.go b/tekton-catalog/pipeline-loops/pkg/reconciler/pipelinelooprun/pipelinelooprun.go
index 2cf94cc97f..abd7bf1150 100644
--- a/tekton-catalog/pipeline-loops/pkg/reconciler/pipelinelooprun/pipelinelooprun.go
+++ b/tekton-catalog/pipeline-loops/pkg/reconciler/pipelinelooprun/pipelinelooprun.go
@@ -466,6 +466,7 @@ func (c *Reconciler) createPipelineRun(ctx context.Context, logger *zap.SugaredL
 			Timeout:            tls.Timeout,
 			ServiceAccountName: "",  // TODO: Implement service account name
 			PodTemplate:        nil, // TODO: Implement pod template
+			Workspaces:         tls.Workspaces,
 		}}
 
 	if tls.PipelineRef != nil {


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 2d9ce70793..6cae8d08a2 100644
--- a/tekton-catalog/pipeline-loops/pkg/reconciler/pipelinelooprun/pipelinelooprun_test.go
+++ b/tekton-catalog/pipeline-loops/pkg/reconciler/pipelinelooprun/pipelinelooprun_test.go
@@ -279,6 +279,21 @@ var aPipelineLoop = &pipelineloopv1alpha1.PipelineLoop{
 	},
 }
 
+var wsPipelineLoop = &pipelineloopv1alpha1.PipelineLoop{
+	ObjectMeta: metav1.ObjectMeta{Name: "ws-pipelineloop", Namespace: "foo"},
+	Spec: pipelineloopv1alpha1.PipelineLoopSpec{
+		PipelineRef:  &v1beta1.PipelineRef{Name: "a-pipeline"},
+		IterateParam: "current-item",
+		Workspaces: []v1beta1.WorkspaceBinding{{
+			Name: "test",
+			ConfigMap: &corev1.ConfigMapVolumeSource{
+				LocalObjectReference: corev1.LocalObjectReference{Name: "test"},
+				Items:                []corev1.KeyToPath{},
+			},
+		}},
+	},
+}
+
 var nPipeline = &v1beta1.Pipeline{
 	ObjectMeta: metav1.ObjectMeta{Name: "n-pipeline", Namespace: "foo"},
 	Spec: v1beta1.PipelineSpec{
@@ -419,6 +434,37 @@ var aPipelineLoopWithInlineTask = &pipelineloopv1alpha1.PipelineLoop{
 	},
 }
 
+var runWsPipelineLoop = &v1alpha1.Run{
+	ObjectMeta: metav1.ObjectMeta{
+		Name:      "run-ws-pipelineloop",
+		Namespace: "foo",
+		Labels: map[string]string{
+			"myTestLabel":                    "myTestLabelValue",
+			"custom.tekton.dev/pipelineLoop": "ws-pipelineloop",
+			"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"}},
+		}, {
+			Name:  "additional-parameter",
+			Value: v1beta1.ArrayOrString{Type: v1beta1.ParamTypeString, StringVal: "stuff"},
+		}},
+		Ref: &v1alpha1.TaskRef{
+			APIVersion: pipelineloopv1alpha1.SchemeGroupVersion.String(),
+			Kind:       pipelineloop.PipelineLoopControllerName,
+			Name:       "ws-pipelineloop",
+		},
+	},
+}
+
 var runPipelineLoop = &v1alpha1.Run{
 	ObjectMeta: metav1.ObjectMeta{
 		Name:      "run-pipelineloop",
@@ -997,6 +1043,51 @@ var expectedPipelineRunIteration2 = &v1beta1.PipelineRun{
 	},
 }
 
+var expectedPipelineRunWithWorkSpace = &v1beta1.PipelineRun{
+	ObjectMeta: metav1.ObjectMeta{
+		Name:      "run-ws-pipelineloop-00001-9l9zj",
+		Namespace: "foo",
+		OwnerReferences: []metav1.OwnerReference{{
+			APIVersion:         "tekton.dev/v1alpha1",
+			Kind:               "Run",
+			Name:               "run-ws-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":          "ws-pipelineloop",
+			"tekton.dev/run":                          "run-ws-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: "stuff"},
+		}, {
+			Name:  "current-item",
+			Value: v1beta1.ArrayOrString{Type: v1beta1.ParamTypeString, StringVal: "item1"},
+		}},
+		Workspaces: []v1beta1.WorkspaceBinding{{
+			Name: "test",
+			ConfigMap: &corev1.ConfigMapVolumeSource{
+				LocalObjectReference: corev1.LocalObjectReference{Name: "test"},
+				Items:                []corev1.KeyToPath{},
+			},
+		}},
+	},
+}
+
 var expectedPipelineRunWithInlineTaskIteration1 = &v1beta1.PipelineRun{
 	ObjectMeta: metav1.ObjectMeta{
 		Name:      "run-pipelineloop-with-inline-task-00001-9l9zj",
@@ -1213,6 +1304,16 @@ func TestReconcilePipelineLoopRun(t *testing.T) {
 		expectedReason:       pipelineloopv1alpha1.PipelineLoopRunReasonRunning,
 		expectedPipelineruns: []*v1beta1.PipelineRun{expectedPipelineRunWithInlineTaskIteration1},
 		expectedEvents:       []string{"Normal Started", "Normal Running Iterations completed: 0"},
+	}, {
+		name:                 "Reconcile a new run with a pipelineloop that contains a workspace",
+		pipeline:             aPipeline,
+		pipelineloop:         wsPipelineLoop,
+		run:                  runWsPipelineLoop,
+		pipelineruns:         []*v1beta1.PipelineRun{},
+		expectedStatus:       corev1.ConditionUnknown,
+		expectedReason:       pipelineloopv1alpha1.PipelineLoopRunReasonRunning,
+		expectedPipelineruns: []*v1beta1.PipelineRun{expectedPipelineRunWithWorkSpace},
+		expectedEvents:       []string{"Normal Started", "Normal Running Iterations completed: 0"},
 	}, {
 		name:                 "Reconcile a run after all PipelineRuns have succeeded",
 		pipeline:             aPipeline,