Repository: tektoncd/pipeline (language: go)

Issue:
A panic occurs when validating optional workspace during the execution of pipelinerun with CustomRun
# Expected Behavior

PipelineRun can execute normally, and the controller does not panic.

# Actual Behavior

The controller has panicked.

# Steps to Reproduce the Problem

1. Install Tekton Pipelines
```shell
kubectl apply -f https://storage.googleapis.com/tekton-releases/pipeline/previous/v0.68.0/release.yaml
```
2. (Optional) Install Pipelines-in-Pipelines component
```shell
# Ref: https://github.com/tektoncd/experimental/tree/main/pipelines-in-pipelines
kubectl apply --filename https://storage.googleapis.com/tekton-releases-nightly/pipelines-in-pipelines/latest/release.yaml
```
3. run the pipelinerun
```yaml
cat <<'EOF' | kubectl apply -f -
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
  name: build-app
  namespace: default
spec:
  workspaces:
    - name: source
      optional: true
  params:
    - name: step
      type: string
  tasks:
    - name: step-1
      workspaces:
        - name: source
          workspace: source
      params:
        - name: step
          value: $(params.step)
      taskSpec:
        results:
          - name: outputValue
            description: the sum of the first and second operand
        workspaces:
          - name: source
            optional: true
        params:
          - name: step
            type: string
        steps:
          - name: echo
            image: ubuntu
            imagePullPolicy: IfNotPresent
            workingDir: $(workspaces.source.path)
            script: |
              $(params.step)

              echo -n "`date`: $(context.taskRun.name)" | tee $(results.outputValue.path)
---
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
  name: build-apps
spec:
  workspaces:
    - name: source
      volumeClaimTemplate:
        spec:
          accessModes:
            - ReadWriteOnce
          resources:
            requests:
              storage: 1Gi
  pipelineSpec:
    workspaces:
      - name: source
        description: Workspace for shared code source
        optional: true      # <- This configuration will trigger a panic.
    tasks:
      - name: build-app-1
        taskRef:
          apiVersion: tekton.dev/v1beta1
          kind: Pipeline
          name: build-app
        params:
          - name: step
            value: |
              #!/usr/bin/env bash
              ls -artl
        workspaces:
          - name: source
            workspace: source
EOF
```

# Additional Info

- Kubernetes version:

  **Output of `kubectl version`:**

```
Client Version: v1.32.1
Kustomize Version: v5.5.0
Server Version: v1.28.8
WARNING: version difference between client (1.32) and server (1.28) exceeds the supported minor version skew of +/-1
```

- Tekton Pipeline version:

  **Output of `tkn version` or `kubectl get pods -n tekton-pipelines -l app=tekton-pipelines-controller -o=jsonpath='{.items[0].metadata.labels.version}'`**

```
Client version: 0.39.1
Pipeline version: v0.68.0
```


<!-- Any other additional information -->



Discussion / hints:

Fix patch:
diff --git a/pkg/reconciler/pipelinerun/resources/validate_dependencies.go b/pkg/reconciler/pipelinerun/resources/validate_dependencies.go
index 81261da82bd..a5d65dc351e 100644
--- a/pkg/reconciler/pipelinerun/resources/validate_dependencies.go
+++ b/pkg/reconciler/pipelinerun/resources/validate_dependencies.go
@@ -101,12 +101,10 @@ func ValidateOptionalWorkspaces(pipelineWorkspaces []v1.PipelineWorkspaceDeclara
 
 	for _, rpt := range state {
 		for _, pws := range rpt.PipelineTask.Workspaces {
-			if optionalWorkspaces.Has(pws.Workspace) {
+			if rpt.ResolvedTask != nil && rpt.ResolvedTask.TaskSpec != nil && optionalWorkspaces.Has(pws.Workspace) {
 				for _, tws := range rpt.ResolvedTask.TaskSpec.Workspaces {
-					if tws.Name == pws.Name {
-						if !tws.Optional {
-							return fmt.Errorf("pipeline workspace %q is marked optional but pipeline task %q requires it be provided", pws.Workspace, rpt.PipelineTask.Name)
-						}
+					if tws.Name == pws.Name && !tws.Optional {
+						return fmt.Errorf("pipeline workspace %q is marked optional but pipeline task %q requires it be provided", pws.Workspace, rpt.PipelineTask.Name)
 					}
 				}
 			}


Test patch:
diff --git a/pkg/reconciler/pipelinerun/resources/validate_dependencies_test.go b/pkg/reconciler/pipelinerun/resources/validate_dependencies_test.go
index 5b57a634822..50b57b7a3a9 100644
--- a/pkg/reconciler/pipelinerun/resources/validate_dependencies_test.go
+++ b/pkg/reconciler/pipelinerun/resources/validate_dependencies_test.go
@@ -422,6 +422,23 @@ func TestValidateOptionalWorkspaces_ValidStates(t *testing.T) {
 				},
 			},
 		}},
+	}, {
+		desc: "pipeline with optional workspace combined with customrun",
+		workspaces: []v1.PipelineWorkspaceDeclaration{{
+			Name:     "ws1",
+			Optional: true,
+		}},
+		state: prresources.PipelineRunState{{
+			PipelineTask: &v1.PipelineTask{
+				Name: "pt1",
+				Workspaces: []v1.WorkspacePipelineTaskBinding{{
+					Name:      "foo",
+					Workspace: "ws1",
+				}},
+			},
+			ResolvedTask: nil,
+			CustomTask:   true,
+		}},
 	}} {
 		t.Run(tc.desc, func(t *testing.T) {
 			if err := prresources.ValidateOptionalWorkspaces(tc.workspaces, tc.state); err != nil {