Repository: tektoncd/pipeline (language: go)

Issue:
Support for variable interpolation in workspace.configmap.items * (in PipelineRun and TaskRun)
This is follow up issue of https://github.com/tektoncd/pipeline/issues/6384 

Currently, we do support workspace provided as

```
    - name: custom4
      configMap:
        name: $(params.name)-configmap
        items:
          - key: message
            path: my-message.txt 
```

but we do not support workspace provided as

```
    - name: custom4
      configMap:
        name: $(params.name)-configmap
        items:
          - key: message
            path: $(params.name)-message.txt 
 ```

This is to add the support for param replacement at items level in workspace


Discussion / hints:
/assign l-qing

Fix patch:
diff --git a/docs/variables.md b/docs/variables.md
index 8f606d6425b..df3dadd818f 100644
--- a/docs/variables.md
+++ b/docs/variables.md
@@ -120,11 +120,20 @@ For instructions on using variable substitutions see the relevant section of [th
 | `Task`        | `spec.sidecars[].args`                                          |
 | `Task`        | `spec.sidecars[].script`                                        |
 | `Task`        | `spec.workspaces[].mountPath`                                   |
+| `TaskRun`     | `spec.workspaces[].subPath`                                     |
 | `TaskRun`     | `spec.workspaces[].persistentVolumeClaim.claimName`             |
 | `TaskRun`     | `spec.workspaces[].configMap.name`                              |
+| `TaskRun`     | `spec.workspaces[].configMap.items[].key`                       |
+| `TaskRun`     | `spec.workspaces[].configMap.items[].path`                      |
 | `TaskRun`     | `spec.workspaces[].secret.secretName`                           |
+| `TaskRun`     | `spec.workspaces[].secret.items[].key`                          |
+| `TaskRun`     | `spec.workspaces[].secret.items[].path`                         |
 | `TaskRun`     | `spec.workspaces[].projected.sources[].secret.name`             |
+| `TaskRun`     | `spec.workspaces[].projected.sources[].secret.items[].key`      |
+| `TaskRun`     | `spec.workspaces[].projected.sources[].secret.items[].path`     |
 | `TaskRun`     | `spec.workspaces[].projected.sources[].configMap.name`          |
+| `TaskRun`     | `spec.workspaces[].projected.sources[].configMap.items[].key`   |
+| `TaskRun`     | `spec.workspaces[].projected.sources[].configMap.items[].path`  |
 | `TaskRun`     | `spec.workspaces[].csi.driver`                                  |
 | `TaskRun`     | `spec.workspaces[].csi.nodePublishSecretRef.name`               |
 | `Pipeline`    | `spec.tasks[].params[].value`                                   |
@@ -134,10 +143,19 @@ For instructions on using variable substitutions see the relevant section of [th
 | `Pipeline`    | `spec.tasks[].when[].values`                                    |
 | `Pipeline`    | `spec.tasks[].workspaces[].subPath`                             |
 | `Pipeline`    | `spec.tasks[].displayName`                                      |
+| `PipelineRun` | `spec.workspaces[].subPath`                                     |
 | `PipelineRun` | `spec.workspaces[].persistentVolumeClaim.claimName`             |
 | `PipelineRun` | `spec.workspaces[].configMap.name`                              |
+| `PipelineRun` | `spec.workspaces[].configMap.items[].key`                       |
+| `PipelineRun` | `spec.workspaces[].configMap.items[].path`                      |
 | `PipelineRun` | `spec.workspaces[].secret.secretName`                           |
+| `PipelineRun` | `spec.workspaces[].secret.items[].key`                          |
+| `PipelineRun` | `spec.workspaces[].secret.items[].path`                         |
 | `PipelineRun` | `spec.workspaces[].projected.sources[].secret.name`             |
+| `PipelineRun` | `spec.workspaces[].projected.sources[].secret.items[].key`      |
+| `PipelineRun` | `spec.workspaces[].projected.sources[].secret.items[].path`     |
 | `PipelineRun` | `spec.workspaces[].projected.sources[].configMap.name`          |
+| `PipelineRun` | `spec.workspaces[].projected.sources[].configMap.items[].key`   |
+| `PipelineRun` | `spec.workspaces[].projected.sources[].configMap.items[].path`  |
 | `PipelineRun` | `spec.workspaces[].csi.driver`                                  |
 | `PipelineRun` | `spec.workspaces[].csi.nodePublishSecretRef.name`               |
diff --git a/pkg/reconciler/pipelinerun/resources/apply.go b/pkg/reconciler/pipelinerun/resources/apply.go
index d0dae45cf79..e48d8e18d0a 100644
--- a/pkg/reconciler/pipelinerun/resources/apply.go
+++ b/pkg/reconciler/pipelinerun/resources/apply.go
@@ -26,6 +26,7 @@ import (
 	"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
 	"github.com/tektoncd/pipeline/pkg/reconciler/taskrun/resources"
 	"github.com/tektoncd/pipeline/pkg/substitution"
+	"github.com/tektoncd/pipeline/pkg/workspace"
 )
 
 const (
@@ -396,34 +397,7 @@ func ApplyResultsToWorkspaceBindings(trResults map[string][]v1.TaskRunResult, pr
 		}
 	}
 
-	for i, binding := range pr.Spec.Workspaces {
-		if pr.Spec.Workspaces[i].PersistentVolumeClaim != nil {
-			pr.Spec.Workspaces[i].PersistentVolumeClaim.ClaimName = substitution.ApplyReplacements(binding.PersistentVolumeClaim.ClaimName, stringReplacements)
-		}
-		pr.Spec.Workspaces[i].SubPath = substitution.ApplyReplacements(binding.SubPath, stringReplacements)
-		if pr.Spec.Workspaces[i].ConfigMap != nil {
-			pr.Spec.Workspaces[i].ConfigMap.Name = substitution.ApplyReplacements(binding.ConfigMap.Name, stringReplacements)
-		}
-		if pr.Spec.Workspaces[i].CSI != nil {
-			pr.Spec.Workspaces[i].CSI.Driver = substitution.ApplyReplacements(binding.CSI.Driver, stringReplacements)
-			if pr.Spec.Workspaces[i].CSI.NodePublishSecretRef != nil {
-				pr.Spec.Workspaces[i].CSI.NodePublishSecretRef.Name = substitution.ApplyReplacements(binding.CSI.NodePublishSecretRef.Name, stringReplacements)
-			}
-		}
-		if pr.Spec.Workspaces[i].Secret != nil {
-			pr.Spec.Workspaces[i].Secret.SecretName = substitution.ApplyReplacements(binding.Secret.SecretName, stringReplacements)
-		}
-		if pr.Spec.Workspaces[i].Projected != nil {
-			for j, source := range binding.Projected.Sources {
-				if pr.Spec.Workspaces[i].Projected.Sources[j].ConfigMap != nil {
-					pr.Spec.Workspaces[i].Projected.Sources[j].ConfigMap.Name = substitution.ApplyReplacements(source.ConfigMap.Name, stringReplacements)
-				}
-				if pr.Spec.Workspaces[i].Projected.Sources[j].Secret != nil {
-					pr.Spec.Workspaces[i].Projected.Sources[j].Secret.Name = substitution.ApplyReplacements(source.Secret.Name, stringReplacements)
-				}
-			}
-		}
-	}
+	pr.Spec.Workspaces = workspace.ReplaceWorkspaceBindingsVars(pr.Spec.Workspaces, stringReplacements)
 }
 
 // PropagateResults propagate the result of the completed task to the unfinished task that is not explicitly specify in the params
@@ -605,32 +579,5 @@ func runResultValue(taskName string, resultName string, runResults map[string][]
 // placeholders in various binding types with values from provided parameters.
 func ApplyParametersToWorkspaceBindings(ctx context.Context, pr *v1.PipelineRun) {
 	parameters, _, _ := paramsFromPipelineRun(ctx, pr)
-	for i, binding := range pr.Spec.Workspaces {
-		if pr.Spec.Workspaces[i].PersistentVolumeClaim != nil {
-			pr.Spec.Workspaces[i].PersistentVolumeClaim.ClaimName = substitution.ApplyReplacements(binding.PersistentVolumeClaim.ClaimName, parameters)
-		}
-		pr.Spec.Workspaces[i].SubPath = substitution.ApplyReplacements(binding.SubPath, parameters)
-		if pr.Spec.Workspaces[i].ConfigMap != nil {
-			pr.Spec.Workspaces[i].ConfigMap.Name = substitution.ApplyReplacements(binding.ConfigMap.Name, parameters)
-		}
-		if pr.Spec.Workspaces[i].Secret != nil {
-			pr.Spec.Workspaces[i].Secret.SecretName = substitution.ApplyReplacements(binding.Secret.SecretName, parameters)
-		}
-		if pr.Spec.Workspaces[i].CSI != nil {
-			pr.Spec.Workspaces[i].CSI.Driver = substitution.ApplyReplacements(binding.CSI.Driver, parameters)
-			if pr.Spec.Workspaces[i].CSI.NodePublishSecretRef != nil {
-				pr.Spec.Workspaces[i].CSI.NodePublishSecretRef.Name = substitution.ApplyReplacements(binding.CSI.NodePublishSecretRef.Name, parameters)
-			}
-		}
-		if pr.Spec.Workspaces[i].Projected != nil {
-			for j, source := range binding.Projected.Sources {
-				if pr.Spec.Workspaces[i].Projected.Sources[j].ConfigMap != nil {
-					pr.Spec.Workspaces[i].Projected.Sources[j].ConfigMap.Name = substitution.ApplyReplacements(source.ConfigMap.Name, parameters)
-				}
-				if pr.Spec.Workspaces[i].Projected.Sources[j].Secret != nil {
-					pr.Spec.Workspaces[i].Projected.Sources[j].Secret.Name = substitution.ApplyReplacements(source.Secret.Name, parameters)
-				}
-			}
-		}
-	}
+	pr.Spec.Workspaces = workspace.ReplaceWorkspaceBindingsVars(pr.Spec.Workspaces, parameters)
 }
diff --git a/pkg/reconciler/taskrun/resources/apply.go b/pkg/reconciler/taskrun/resources/apply.go
index 2283246b95e..b0d6d43b1d8 100644
--- a/pkg/reconciler/taskrun/resources/apply.go
+++ b/pkg/reconciler/taskrun/resources/apply.go
@@ -30,6 +30,7 @@ import (
 	"github.com/tektoncd/pipeline/pkg/internal/resultref"
 	"github.com/tektoncd/pipeline/pkg/pod"
 	"github.com/tektoncd/pipeline/pkg/substitution"
+	"github.com/tektoncd/pipeline/pkg/workspace"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/util/sets"
 )
@@ -312,34 +313,7 @@ func ApplyWorkspaces(ctx context.Context, spec *v1.TaskSpec, declarations []v1.W
 func ApplyParametersToWorkspaceBindings(ts *v1.TaskSpec, tr *v1.TaskRun) *v1.TaskRun {
 	tsCopy := ts.DeepCopy()
 	parameters, _, _ := getTaskParameters(tsCopy, tr, tsCopy.Params...)
-	for i, binding := range tr.Spec.Workspaces {
-		if tr.Spec.Workspaces[i].PersistentVolumeClaim != nil {
-			tr.Spec.Workspaces[i].PersistentVolumeClaim.ClaimName = substitution.ApplyReplacements(binding.PersistentVolumeClaim.ClaimName, parameters)
-		}
-		tr.Spec.Workspaces[i].SubPath = substitution.ApplyReplacements(binding.SubPath, parameters)
-		if tr.Spec.Workspaces[i].ConfigMap != nil {
-			tr.Spec.Workspaces[i].ConfigMap.Name = substitution.ApplyReplacements(binding.ConfigMap.Name, parameters)
-		}
-		if tr.Spec.Workspaces[i].Secret != nil {
-			tr.Spec.Workspaces[i].Secret.SecretName = substitution.ApplyReplacements(binding.Secret.SecretName, parameters)
-		}
-		if tr.Spec.Workspaces[i].CSI != nil {
-			tr.Spec.Workspaces[i].CSI.Driver = substitution.ApplyReplacements(binding.CSI.Driver, parameters)
-			if tr.Spec.Workspaces[i].CSI.NodePublishSecretRef != nil {
-				tr.Spec.Workspaces[i].CSI.NodePublishSecretRef.Name = substitution.ApplyReplacements(binding.CSI.NodePublishSecretRef.Name, parameters)
-			}
-		}
-		if binding.Projected != nil {
-			for j, source := range binding.Projected.Sources {
-				if source.ConfigMap != nil {
-					tr.Spec.Workspaces[i].Projected.Sources[j].ConfigMap.Name = substitution.ApplyReplacements(source.ConfigMap.Name, parameters)
-				}
-				if source.Secret != nil {
-					tr.Spec.Workspaces[i].Projected.Sources[j].Secret.Name = substitution.ApplyReplacements(source.Secret.Name, parameters)
-				}
-			}
-		}
-	}
+	tr.Spec.Workspaces = workspace.ReplaceWorkspaceBindingsVars(tr.Spec.Workspaces, parameters)
 	return tr
 }
 
diff --git a/pkg/workspace/apply.go b/pkg/workspace/apply.go
index 776888debee..993e0f1c9da 100644
--- a/pkg/workspace/apply.go
+++ b/pkg/workspace/apply.go
@@ -275,3 +275,88 @@ func FindWorkspacesUsedByTask(ts v1.TaskSpec) (sets.String, error) {
 	}
 	return workspacesUsedInSteps, nil
 }
+
+// ReplaceWorkspaceBindingsVars returns a new slice of WorkspaceBinding with references to parameters replaced,
+// based on the mapping provided in replacements.
+func ReplaceWorkspaceBindingsVars(wbs []v1.WorkspaceBinding, replacements map[string]string) []v1.WorkspaceBinding {
+	for i := range wbs {
+		replaceWorkspaceBindingVars(&wbs[i], replacements)
+	}
+	return wbs
+}
+
+// replaceWorkspaceBindingVars returns a new WorkspaceBinding with references to parameters replaced,
+// based on the mapping provided in replacements.
+func replaceWorkspaceBindingVars(wb *v1.WorkspaceBinding, replacements map[string]string) *v1.WorkspaceBinding {
+	wb.SubPath = substitution.ApplyReplacements(wb.SubPath, replacements)
+	if wb.PersistentVolumeClaim != nil {
+		wb.PersistentVolumeClaim = applyPersistentVolumeClaimVolumeSource(wb.PersistentVolumeClaim, replacements)
+	}
+	if wb.ConfigMap != nil {
+		wb.ConfigMap = applyConfigMapVolumeSource(wb.ConfigMap, replacements)
+	}
+	if wb.Secret != nil {
+		wb.Secret = applySecretVolumeSource(wb.Secret, replacements)
+	}
+	if wb.Projected != nil {
+		for j, source := range wb.Projected.Sources {
+			if source.ConfigMap != nil {
+				wb.Projected.Sources[j].ConfigMap = applyConfigMapProjection(wb.Projected.Sources[j].ConfigMap, replacements)
+			}
+			if source.Secret != nil {
+				wb.Projected.Sources[j].Secret = applySecretProjection(wb.Projected.Sources[j].Secret, replacements)
+			}
+		}
+	}
+	if wb.CSI != nil {
+		wb.CSI = applyCSIVolumeSource(wb.CSI, replacements)
+	}
+	return wb
+}
+
+func applyPersistentVolumeClaimVolumeSource(pvc *corev1.PersistentVolumeClaimVolumeSource,
+	replacements map[string]string) *corev1.PersistentVolumeClaimVolumeSource {
+	pvc.ClaimName = substitution.ApplyReplacements(pvc.ClaimName, replacements)
+	return pvc
+}
+
+func applyConfigMapVolumeSource(cm *corev1.ConfigMapVolumeSource, replacements map[string]string) *corev1.ConfigMapVolumeSource {
+	cm.Name = substitution.ApplyReplacements(cm.Name, replacements)
+	cm.Items = applyKeyToPathItems(cm.Items, replacements)
+	return cm
+}
+
+func applySecretVolumeSource(s *corev1.SecretVolumeSource, replacements map[string]string) *corev1.SecretVolumeSource {
+	s.SecretName = substitution.ApplyReplacements(s.SecretName, replacements)
+	s.Items = applyKeyToPathItems(s.Items, replacements)
+	return s
+}
+
+func applyConfigMapProjection(cm *corev1.ConfigMapProjection, replacements map[string]string) *corev1.ConfigMapProjection {
+	cm.Name = substitution.ApplyReplacements(cm.Name, replacements)
+	cm.Items = applyKeyToPathItems(cm.Items, replacements)
+	return cm
+}
+
+func applySecretProjection(s *corev1.SecretProjection, replacements map[string]string) *corev1.SecretProjection {
+	s.Name = substitution.ApplyReplacements(s.Name, replacements)
+	s.Items = applyKeyToPathItems(s.Items, replacements)
+	return s
+}
+
+func applyCSIVolumeSource(csi *corev1.CSIVolumeSource, replacements map[string]string) *corev1.CSIVolumeSource {
+	csi.Driver = substitution.ApplyReplacements(csi.Driver, replacements)
+	if csi.NodePublishSecretRef != nil {
+		csi.NodePublishSecretRef.Name = substitution.ApplyReplacements(csi.NodePublishSecretRef.Name, replacements)
+	}
+	return csi
+}
+
+func applyKeyToPathItems(items []corev1.KeyToPath, replacements map[string]string) []corev1.KeyToPath {
+	for i := range items {
+		item := &items[i]
+		item.Key = substitution.ApplyReplacements(item.Key, replacements)
+		item.Path = substitution.ApplyReplacements(item.Path, replacements)
+	}
+	return items
+}


Test patch:
diff --git a/pkg/reconciler/pipelinerun/pipelinerun_test.go b/pkg/reconciler/pipelinerun/pipelinerun_test.go
index 837b7982607..32d0d57618f 100644
--- a/pkg/reconciler/pipelinerun/pipelinerun_test.go
+++ b/pkg/reconciler/pipelinerun/pipelinerun_test.go
@@ -4891,10 +4891,10 @@ spec:
       steps:
         - name: s1
           image: alpine
-          script: | 
+          script: |
             echo $(params.version) + $(params.tag)
   - name: b-task
-    params: 
+    params:
     - name: ref-p1
       value: $(params.version)
     - name: ref-p2
@@ -4902,7 +4902,7 @@ spec:
     taskRef:
       name: ref-task
   - name: c-task-matrixed
-    matrix: 
+    matrix:
       params:
       - name: ref-p1
         value: [v1, v2]
@@ -4981,7 +4981,7 @@ spec:
       steps:
         - name: s1
           image: alpine
-          script: | 
+          script: |
             echo $(params.version)
 `)}
 	prs := []*v1.PipelineRun{parse.MustParseV1PipelineRun(t, `
@@ -5870,8 +5870,8 @@ spec:
 `),
 	}
 	trs := []*v1.TaskRun{mustParseTaskRunWithObjectMeta(t,
-		taskRunObjectMeta("test-pipeline-run-different-service-accs-a-task-xxyyy", "foo",
-			"test-pipeline-run-different-service-accs", "test-pipeline", "a-task", true),
+		taskRunObjectMeta("test-pipeline-run-variable-substitution-a-task-xxyyy", "foo",
+			"test-pipeline-run-variable-substitution", "test-pipeline", "a-task", true),
 		`
 spec:
   serviceAccountName: test-sa
@@ -5899,7 +5899,7 @@ status:
 			name: "pcv success",
 			prs: []*v1.PipelineRun{parse.MustParseV1PipelineRun(t, `
 metadata:
-  name: test-pipeline-run-different-service-accs
+  name: test-pipeline-run-variable-substitution
   namespace: foo
 spec:
   pipelineRef:
@@ -5913,8 +5913,8 @@ spec:
 `)},
 			disableAA: true,
 			expectedTr: mustParseTaskRunWithObjectMeta(t,
-				taskRunObjectMeta("test-pipeline-run-different-service-accs-b-task", "foo",
-					"test-pipeline-run-different-service-accs", "test-pipeline", "b-task", false),
+				taskRunObjectMeta("test-pipeline-run-variable-substitution-b-task", "foo",
+					"test-pipeline-run-variable-substitution", "test-pipeline", "b-task", false),
 				`spec:
   serviceAccountName: test-sa-0
   taskRef:
@@ -5930,7 +5930,7 @@ spec:
 			name: "subPath success",
 			prs: []*v1.PipelineRun{parse.MustParseV1PipelineRun(t, `
 metadata:
-  name: test-pipeline-run-different-service-accs
+  name: test-pipeline-run-variable-substitution
   namespace: foo
 spec:
   pipelineRef:
@@ -5942,8 +5942,8 @@ spec:
       subPath: $(tasks.a-task.results.aResult)
 `)},
 			expectedTr: mustParseTaskRunWithObjectMeta(t,
-				taskRunObjectMeta("test-pipeline-run-different-service-accs-b-task", "foo",
-					"test-pipeline-run-different-service-accs", "test-pipeline", "b-task", false),
+				taskRunObjectMeta("test-pipeline-run-variable-substitution-b-task", "foo",
+					"test-pipeline-run-variable-substitution", "test-pipeline", "b-task", false),
 				`spec:
   serviceAccountName: test-sa-0
   taskRef:
@@ -5958,7 +5958,7 @@ spec:
 			name: "secret.secretName success",
 			prs: []*v1.PipelineRun{parse.MustParseV1PipelineRun(t, `
 metadata:
-  name: test-pipeline-run-different-service-accs
+  name: test-pipeline-run-variable-substitution
   namespace: foo
 spec:
   pipelineRef:
@@ -5967,12 +5967,12 @@ spec:
     serviceAccountName: test-sa-0
   workspaces:
     - name: ws-1
-      secret: 
+      secret:
         secretName: $(tasks.a-task.results.aResult)
 `)},
 			expectedTr: mustParseTaskRunWithObjectMeta(t,
-				taskRunObjectMeta("test-pipeline-run-different-service-accs-b-task", "foo",
-					"test-pipeline-run-different-service-accs", "test-pipeline", "b-task", false),
+				taskRunObjectMeta("test-pipeline-run-variable-substitution-b-task", "foo",
+					"test-pipeline-run-variable-substitution", "test-pipeline", "b-task", false),
 				`spec:
   serviceAccountName: test-sa-0
   taskRef:
@@ -5988,7 +5988,7 @@ spec:
 			name: "projected.sources.configMap.name success",
 			prs: []*v1.PipelineRun{parse.MustParseV1PipelineRun(t, `
 metadata:
-  name: test-pipeline-run-different-service-accs
+  name: test-pipeline-run-variable-substitution
   namespace: foo
 spec:
   pipelineRef:
@@ -5997,14 +5997,14 @@ spec:
     serviceAccountName: test-sa-0
   workspaces:
     - name: ws-1
-      projected: 
+      projected:
         sources:
          - configMap:
              name: $(tasks.a-task.results.aResult)
 `)},
 			expectedTr: mustParseTaskRunWithObjectMeta(t,
-				taskRunObjectMeta("test-pipeline-run-different-service-accs-b-task", "foo",
-					"test-pipeline-run-different-service-accs", "test-pipeline", "b-task", false),
+				taskRunObjectMeta("test-pipeline-run-variable-substitution-b-task", "foo",
+					"test-pipeline-run-variable-substitution", "test-pipeline", "b-task", false),
 				`spec:
   serviceAccountName: test-sa-0
   taskRef:
@@ -6012,17 +6012,17 @@ spec:
     kind: Task
   workspaces:
     - name: s1
-      projected: 
+      projected:
         sources:
          - configMap:
-             name: aResultValue 
+             name: aResultValue
 `),
 		},
 		{
 			name: "projected.sources.secret.name success",
 			prs: []*v1.PipelineRun{parse.MustParseV1PipelineRun(t, `
 metadata:
-  name: test-pipeline-run-different-service-accs
+  name: test-pipeline-run-variable-substitution
   namespace: foo
 spec:
   pipelineRef:
@@ -6031,14 +6031,51 @@ spec:
     serviceAccountName: test-sa-0
   workspaces:
     - name: ws-1
-      projected: 
+      projected:
         sources:
          - secret:
              name: $(tasks.a-task.results.aResult)
 `)},
 			expectedTr: mustParseTaskRunWithObjectMeta(t,
-				taskRunObjectMeta("test-pipeline-run-different-service-accs-b-task", "foo",
-					"test-pipeline-run-different-service-accs", "test-pipeline", "b-task", false),
+				taskRunObjectMeta("test-pipeline-run-variable-substitution-b-task", "foo",
+					"test-pipeline-run-variable-substitution", "test-pipeline", "b-task", false),
+				`spec:
+  serviceAccountName: test-sa-0
+  taskRef:
+    name: b-task
+    kind: Task
+  workspaces:
+    - name: s1
+      projected:
+        sources:
+         - secret:
+             name: aResultValue
+`),
+		},
+		{
+			name: "projected.sources.secret.items success",
+			prs: []*v1.PipelineRun{parse.MustParseV1PipelineRun(t, `
+metadata:
+  name: test-pipeline-run-variable-substitution
+  namespace: foo
+spec:
+  pipelineRef:
+    name: test-pipeline
+  taskRunTemplate:
+    serviceAccountName: test-sa-0
+  workspaces:
+    - name: ws-1
+      projected:
+        sources:
+         - secret:
+             name: name
+             items:
+               - key: $(tasks.a-task.results.aResult)
+                 path: $(tasks.a-task.results.aResult)
+`)},
+			expectedTr: mustParseTaskRunWithObjectMeta(t,
+				taskRunObjectMeta("test-pipeline-run-variable-substitution-b-task", "foo",
+					"test-pipeline-run-variable-substitution", "test-pipeline", "b-task", false),
 				`spec:
   serviceAccountName: test-sa-0
   taskRef:
@@ -6046,17 +6083,20 @@ spec:
     kind: Task
   workspaces:
     - name: s1
-      projected: 
+      projected:
         sources:
          - secret:
-             name: aResultValue 
+             name: name
+             items:
+               - key: aResultValue
+                 path: aResultValue
 `),
 		},
 		{
 			name: "csi.driver success",
 			prs: []*v1.PipelineRun{parse.MustParseV1PipelineRun(t, `
 metadata:
-  name: test-pipeline-run-different-service-accs
+  name: test-pipeline-run-variable-substitution
   namespace: foo
 spec:
   pipelineRef:
@@ -6069,8 +6109,8 @@ spec:
         driver: $(tasks.a-task.results.aResult)
 `)},
 			expectedTr: mustParseTaskRunWithObjectMeta(t,
-				taskRunObjectMeta("test-pipeline-run-different-service-accs-b-task", "foo",
-					"test-pipeline-run-different-service-accs", "test-pipeline", "b-task", false),
+				taskRunObjectMeta("test-pipeline-run-variable-substitution-b-task", "foo",
+					"test-pipeline-run-variable-substitution", "test-pipeline", "b-task", false),
 				`spec:
   serviceAccountName: test-sa-0
   taskRef:
@@ -6079,14 +6119,14 @@ spec:
   workspaces:
     - name: s1
       csi:
-        driver: aResultValue 
+        driver: aResultValue
 `),
 		},
 		{
 			name: "nodePublishSecretRef.name success",
 			prs: []*v1.PipelineRun{parse.MustParseV1PipelineRun(t, `
 metadata:
-  name: test-pipeline-run-different-service-accs
+  name: test-pipeline-run-variable-substitution
   namespace: foo
 spec:
   pipelineRef:
@@ -6100,8 +6140,8 @@ spec:
           name: $(tasks.a-task.results.aResult)
 `)},
 			expectedTr: mustParseTaskRunWithObjectMeta(t,
-				taskRunObjectMeta("test-pipeline-run-different-service-accs-b-task", "foo",
-					"test-pipeline-run-different-service-accs", "test-pipeline", "b-task", false),
+				taskRunObjectMeta("test-pipeline-run-variable-substitution-b-task", "foo",
+					"test-pipeline-run-variable-substitution", "test-pipeline", "b-task", false),
 				`spec:
   serviceAccountName: test-sa-0
   taskRef:
@@ -6110,7 +6150,7 @@ spec:
   workspaces:
     - name: s1
       csi:
-        nodePublishSecretRef: 
+        nodePublishSecretRef:
           name: aResultValue
 `),
 		},
@@ -6132,10 +6172,10 @@ spec:
 			prt := newPipelineRunTest(t, d)
 			defer prt.Cancel()
 
-			_, clients := prt.reconcileRun("foo", "test-pipeline-run-different-service-accs", []string{}, false)
+			_, clients := prt.reconcileRun("foo", "test-pipeline-run-variable-substitution", []string{}, false)
 			// Check that the expected TaskRun was created
 			actual, err := clients.Pipeline.TektonV1().TaskRuns("foo").List(prt.TestAssets.Ctx, metav1.ListOptions{
-				LabelSelector: "tekton.dev/pipelineTask=b-task,tekton.dev/pipelineRun=test-pipeline-run-different-service-accs",
+				LabelSelector: "tekton.dev/pipelineTask=b-task,tekton.dev/pipelineRun=test-pipeline-run-variable-substitution",
 				Limit:         1,
 			})
 
diff --git a/pkg/workspace/apply_test.go b/pkg/workspace/apply_test.go
index 7c0528d872c..8e64881ecca 100644
--- a/pkg/workspace/apply_test.go
+++ b/pkg/workspace/apply_test.go
@@ -1238,3 +1238,210 @@ func TestFindWorkspacesUsedByTask(t *testing.T) {
 		})
 	}
 }
+
+func TestReplaceWorkspaceBindingsVars(t *testing.T) {
+	testCases := []struct {
+		name              string
+		workspaceBindings []v1.WorkspaceBinding
+		replacements      map[string]string
+		expected          []v1.WorkspaceBinding
+	}{
+		{
+			name:         "No replacements",
+			replacements: nil,
+			workspaceBindings: []v1.WorkspaceBinding{
+				{SubPath: "$(params.to-replace)"},
+			},
+			expected: []v1.WorkspaceBinding{
+				{SubPath: "$(params.to-replace)"},
+			},
+		},
+		{
+			name: "Replace SubPath",
+			replacements: map[string]string{
+				"params.to-replace": "replaced",
+			},
+			workspaceBindings: []v1.WorkspaceBinding{
+				{SubPath: "$(params.to-replace)"},
+			},
+			expected: []v1.WorkspaceBinding{
+				{SubPath: "replaced"},
+			},
+		},
+		{
+			name: "Replace PersistentVolumeClaim",
+			replacements: map[string]string{
+				"params.to-replace": "replaced",
+			},
+			workspaceBindings: []v1.WorkspaceBinding{
+				{
+					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+						ClaimName: "$(params.to-replace)",
+					},
+				},
+			},
+			expected: []v1.WorkspaceBinding{
+				{
+					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+						ClaimName: "replaced",
+					},
+				},
+			},
+		},
+		{
+			name: "Replace ConfigMap",
+			replacements: map[string]string{
+				"params.to-replace": "replaced",
+			},
+			workspaceBindings: []v1.WorkspaceBinding{
+				{
+					ConfigMap: &corev1.ConfigMapVolumeSource{
+						LocalObjectReference: corev1.LocalObjectReference{
+							Name: "$(params.to-replace)",
+						},
+						Items: []corev1.KeyToPath{{
+							Key:  "$(params.to-replace)",
+							Path: "$(params.to-replace)",
+						}},
+					},
+				},
+			},
+			expected: []v1.WorkspaceBinding{
+				{
+					ConfigMap: &corev1.ConfigMapVolumeSource{
+						LocalObjectReference: corev1.LocalObjectReference{
+							Name: "replaced",
+						},
+						Items: []corev1.KeyToPath{{
+							Key:  "replaced",
+							Path: "replaced",
+						}},
+					},
+				},
+			},
+		},
+		{
+			name: "Replace Secret",
+			replacements: map[string]string{
+				"params.to-replace": "replaced",
+			},
+			workspaceBindings: []v1.WorkspaceBinding{
+				{
+					Secret: &corev1.SecretVolumeSource{
+						SecretName: "$(params.to-replace)",
+						Items: []corev1.KeyToPath{{
+							Key:  "$(params.to-replace)",
+							Path: "$(params.to-replace)",
+						}},
+					},
+				},
+			},
+			expected: []v1.WorkspaceBinding{
+				{
+					Secret: &corev1.SecretVolumeSource{
+						SecretName: "replaced",
+						Items: []corev1.KeyToPath{{
+							Key:  "replaced",
+							Path: "replaced",
+						}},
+					},
+				},
+			},
+		},
+		{
+			name: "Replace Projected",
+			replacements: map[string]string{
+				"params.to-replace": "replaced",
+			},
+			workspaceBindings: []v1.WorkspaceBinding{
+				{
+					Projected: &corev1.ProjectedVolumeSource{
+						Sources: []corev1.VolumeProjection{{
+							ConfigMap: &corev1.ConfigMapProjection{
+								LocalObjectReference: corev1.LocalObjectReference{
+									Name: "$(params.to-replace)",
+								},
+								Items: []corev1.KeyToPath{{
+									Key:  "$(params.to-replace)",
+									Path: "$(params.to-replace)",
+								}},
+							},
+						}, {
+							Secret: &corev1.SecretProjection{
+								LocalObjectReference: corev1.LocalObjectReference{
+									Name: "$(params.to-replace)",
+								},
+								Items: []corev1.KeyToPath{{
+									Key:  "$(params.to-replace)",
+									Path: "$(params.to-replace)",
+								}},
+							},
+						}},
+					},
+				},
+			},
+			expected: []v1.WorkspaceBinding{
+				{
+					Projected: &corev1.ProjectedVolumeSource{
+						Sources: []corev1.VolumeProjection{{
+							ConfigMap: &corev1.ConfigMapProjection{
+								LocalObjectReference: corev1.LocalObjectReference{
+									Name: "replaced",
+								},
+								Items: []corev1.KeyToPath{{
+									Key:  "replaced",
+									Path: "replaced",
+								}},
+							},
+						}, {
+							Secret: &corev1.SecretProjection{
+								LocalObjectReference: corev1.LocalObjectReference{
+									Name: "replaced",
+								},
+								Items: []corev1.KeyToPath{{
+									Key:  "replaced",
+									Path: "replaced",
+								}},
+							},
+						}},
+					},
+				},
+			},
+		},
+		{
+			name: "Replace CSI",
+			replacements: map[string]string{
+				"params.to-replace": "replaced",
+			},
+			workspaceBindings: []v1.WorkspaceBinding{
+				{
+					CSI: &corev1.CSIVolumeSource{
+						Driver: "$(params.to-replace)",
+						NodePublishSecretRef: &corev1.LocalObjectReference{
+							Name: "$(params.to-replace)",
+						},
+					},
+				},
+			},
+			expected: []v1.WorkspaceBinding{
+				{
+					CSI: &corev1.CSIVolumeSource{
+						Driver: "replaced",
+						NodePublishSecretRef: &corev1.LocalObjectReference{
+							Name: "replaced",
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			result := workspace.ReplaceWorkspaceBindingsVars(tc.workspaceBindings, tc.replacements)
+			if d := cmp.Diff(tc.expected, result); d != "" {
+				t.Errorf("Test case %q, diff: %s", tc.name, diff.PrintWantGot(d))
+			}
+		})
+	}
+}