Skip to content

Commit 3f3d21c

Browse files
committed
Add subPath to WorkspacePipelineTaskBinding
Use case: Use two instances of a task that writes to the same workspace volume - but they write to different path of the volume. A typical use case is two git-clone tasks that clone two git repositories, but the files should be located in two different directories on the workspace volume. This commit solves this by adding the `subPath` field to the WorkspacePipelineTaskBinding.
1 parent 35d5121 commit 3f3d21c

File tree

9 files changed

+236
-13
lines changed

9 files changed

+236
-13
lines changed

docs/workspaces.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,10 @@ spec:
222222
- use-ws-from-pipeline # important: use-ws-from-pipeline writes to the workspace first
223223
```
224224

225+
Include a `subPath` in the workspace binding to mount different parts of the same volume for different Tasks. See [a full example of this kind of Pipeline](../examples/v1beta1/pipelineruns/pipelinerun-using-different-subpaths-of-workspace.yaml) which writes data to two adjacent directories on the same Volume.
226+
227+
The `subPath` specified in a `Pipeline` will be appended to any `subPath` specified as part of the `PipelineRun` workspace declaration. So a `PipelineRun` declaring a Workspace with `subPath` of `/foo` for a `Pipeline` who binds it to a `Task` with `subPath` of `/bar` will end up mounting the `Volume`'s `/foo/bar` directory.
228+
225229
#### Specifying `Workspace` order in a `Pipeline`
226230

227231
Sharing a `Workspace` between `Tasks` requires you to define the order in which those `Tasks`
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
apiVersion: tekton.dev/v1beta1
2+
kind: Task
3+
metadata:
4+
name: writer
5+
spec:
6+
steps:
7+
- name: write
8+
image: ubuntu
9+
script: echo bar > $(workspaces.task-ws.path)/foo
10+
workspaces:
11+
- name: task-ws
12+
---
13+
apiVersion: tekton.dev/v1beta1
14+
kind: Task
15+
metadata:
16+
name: read-both
17+
spec:
18+
params:
19+
- name: directory1
20+
type: string
21+
- name: directory2
22+
type: string
23+
workspaces:
24+
- name: local-ws
25+
steps:
26+
- name: read-1
27+
image: ubuntu
28+
script: cat $(workspaces.local-ws.path)/$(params.directory1)/foo | grep bar
29+
- name: read-2
30+
image: ubuntu
31+
script: cat $(workspaces.local-ws.path)/$(params.directory2)/foo | grep bar
32+
---
33+
apiVersion: tekton.dev/v1beta1
34+
kind: Pipeline
35+
metadata:
36+
name: pipeline-using-different-subpaths
37+
spec:
38+
workspaces:
39+
- name: ws
40+
tasks:
41+
- name: writer-1
42+
taskRef:
43+
name: writer
44+
workspaces:
45+
- name: task-ws
46+
workspace: ws
47+
subPath: dir-1
48+
- name: writer-2
49+
taskRef:
50+
name: writer
51+
workspaces:
52+
- name: task-ws
53+
workspace: ws
54+
subPath: dir-2
55+
- name: read-all
56+
runAfter:
57+
- writer-1
58+
- writer-2
59+
params:
60+
- name: directory1
61+
value: dir-1
62+
- name: directory2
63+
value: dir-2
64+
taskRef:
65+
name: read-both
66+
workspaces:
67+
- name: local-ws
68+
workspace: ws
69+
---
70+
apiVersion: tekton.dev/v1beta1
71+
kind: PipelineRun
72+
metadata:
73+
generateName: pr-
74+
spec:
75+
pipelineRef:
76+
name: pipeline-using-different-subpaths
77+
workspaces:
78+
- name: ws
79+
volumeClaimTemplate:
80+
spec:
81+
accessModes:
82+
- ReadWriteOnce
83+
resources:
84+
requests:
85+
storage: 1Gi

pkg/apis/pipeline/v1alpha1/pipeline_validation_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ func TestPipeline_Validate(t *testing.T) {
359359
p: tb.Pipeline("name", tb.PipelineSpec(
360360
tb.PipelineWorkspaceDeclaration("foo"),
361361
tb.PipelineTask("taskname", "taskref",
362-
tb.PipelineTaskWorkspaceBinding("taskWorkspaceName", "pipelineWorkspaceName")),
362+
tb.PipelineTaskWorkspaceBinding("taskWorkspaceName", "pipelineWorkspaceName", "")),
363363
)),
364364
failureExpected: true,
365365
}, {

pkg/apis/pipeline/v1beta1/workspace_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,8 @@ type WorkspacePipelineTaskBinding struct {
9595
Name string `json:"name"`
9696
// Workspace is the name of the workspace declared by the pipeline
9797
Workspace string `json:"workspace"`
98+
// SubPath is optionally a directory on the volume which should be used
99+
// for this binding (i.e. the volume will be mounted at this sub directory).
100+
// +optional
101+
SubPath string `json:"subPath,omitempty"`
98102
}

pkg/reconciler/pipelinerun/pipelinerun.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package pipelinerun
1919
import (
2020
"context"
2121
"fmt"
22+
"path/filepath"
2223
"reflect"
2324
"strings"
2425
"time"
@@ -733,9 +734,9 @@ func (c *Reconciler) createTaskRun(rprt *resources.ResolvedPipelineRunTask, pr *
733734
pipelineRunWorkspaces[binding.Name] = binding
734735
}
735736
for _, ws := range rprt.PipelineTask.Workspaces {
736-
taskWorkspaceName, pipelineWorkspaceName := ws.Name, ws.Workspace
737+
taskWorkspaceName, pipelineTaskSubPath, pipelineWorkspaceName := ws.Name, ws.SubPath, ws.Workspace
737738
if b, hasBinding := pipelineRunWorkspaces[pipelineWorkspaceName]; hasBinding {
738-
tr.Spec.Workspaces = append(tr.Spec.Workspaces, taskWorkspaceByWorkspaceVolumeSource(b, taskWorkspaceName, pr.GetOwnerReference()))
739+
tr.Spec.Workspaces = append(tr.Spec.Workspaces, taskWorkspaceByWorkspaceVolumeSource(b, taskWorkspaceName, pipelineTaskSubPath, pr.GetOwnerReference()))
739740
} else {
740741
return nil, fmt.Errorf("expected workspace %q to be provided by pipelinerun for pipeline task %q", pipelineWorkspaceName, rprt.PipelineTask.Name)
741742
}
@@ -748,16 +749,17 @@ func (c *Reconciler) createTaskRun(rprt *resources.ResolvedPipelineRunTask, pr *
748749

749750
// taskWorkspaceByWorkspaceVolumeSource is returning the WorkspaceBinding with the TaskRun specified name.
750751
// If the volume source is a volumeClaimTemplate, the template is applied and passed to TaskRun as a persistentVolumeClaim
751-
func taskWorkspaceByWorkspaceVolumeSource(wb v1alpha1.WorkspaceBinding, taskWorkspaceName string, owner metav1.OwnerReference) v1alpha1.WorkspaceBinding {
752+
func taskWorkspaceByWorkspaceVolumeSource(wb v1alpha1.WorkspaceBinding, taskWorkspaceName string, pipelineTaskSubPath string, owner metav1.OwnerReference) v1alpha1.WorkspaceBinding {
752753
if wb.VolumeClaimTemplate == nil {
753754
binding := *wb.DeepCopy()
754755
binding.Name = taskWorkspaceName
756+
binding.SubPath = combinedSubPath(wb.SubPath, pipelineTaskSubPath)
755757
return binding
756758
}
757759

758760
// apply template
759761
binding := v1alpha1.WorkspaceBinding{
760-
SubPath: wb.SubPath,
762+
SubPath: combinedSubPath(wb.SubPath, pipelineTaskSubPath),
761763
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
762764
ClaimName: volumeclaim.GetPersistentVolumeClaimName(wb.VolumeClaimTemplate, wb, owner),
763765
},
@@ -766,6 +768,18 @@ func taskWorkspaceByWorkspaceVolumeSource(wb v1alpha1.WorkspaceBinding, taskWork
766768
return binding
767769
}
768770

771+
// combinedSubPath returns the combined value of the optional subPath from workspaceBinding and the optional
772+
// subPath from pipelineTask. If both is set, they are joined with a slash.
773+
func combinedSubPath(workspaceSubPath string, pipelineTaskSubPath string) string {
774+
if workspaceSubPath == "" {
775+
return pipelineTaskSubPath
776+
} else if pipelineTaskSubPath == "" {
777+
return workspaceSubPath
778+
} else {
779+
return filepath.Join(workspaceSubPath, pipelineTaskSubPath)
780+
}
781+
}
782+
769783
func addRetryHistory(tr *v1alpha1.TaskRun) {
770784
newStatus := *tr.Status.DeepCopy()
771785
newStatus.RetriesStatus = nil

pkg/reconciler/pipelinerun/pipelinerun_test.go

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1718,13 +1718,13 @@ func TestReconcileWithVolumeClaimTemplateWorkspace(t *testing.T) {
17181718
claimName := "myclaim"
17191719
pipelineRunName := "test-pipeline-run"
17201720
ps := []*v1alpha1.Pipeline{tb.Pipeline("test-pipeline", tb.PipelineNamespace("foo"), tb.PipelineSpec(
1721-
tb.PipelineTask("hello-world-1", "hello-world", tb.PipelineTaskWorkspaceBinding("taskWorkspaceName", workspaceName)),
1721+
tb.PipelineTask("hello-world-1", "hello-world", tb.PipelineTaskWorkspaceBinding("taskWorkspaceName", workspaceName, "")),
17221722
tb.PipelineTask("hello-world-2", "hello-world"),
17231723
tb.PipelineWorkspaceDeclaration(workspaceName),
17241724
))}
17251725

17261726
prs := []*v1alpha1.PipelineRun{tb.PipelineRun(pipelineRunName, tb.PipelineRunNamespace("foo"),
1727-
tb.PipelineRunSpec("test-pipeline", tb.PipelineRunWorkspaceBindingVolumeClaimTemplate(workspaceName, claimName))),
1727+
tb.PipelineRunSpec("test-pipeline", tb.PipelineRunWorkspaceBindingVolumeClaimTemplate(workspaceName, claimName, ""))),
17281728
}
17291729
ts := []*v1alpha1.Task{tb.Task("hello-world", tb.TaskNamespace("foo"))}
17301730

@@ -1792,6 +1792,120 @@ func TestReconcileWithVolumeClaimTemplateWorkspace(t *testing.T) {
17921792
}
17931793
}
17941794

1795+
// TestReconcileWithVolumeClaimTemplateWorkspaceUsingSubPaths tests that given a pipeline with volumeClaimTemplate workspace and
1796+
// multiple instances of the same task, but using different subPaths in the volume - is seen as taskRuns with expected subPaths.
1797+
func TestReconcileWithVolumeClaimTemplateWorkspaceUsingSubPaths(t *testing.T) {
1798+
workspaceName := "ws1"
1799+
workspaceNameWithSubPath := "ws2"
1800+
subPath1 := "customdirectory"
1801+
subPath2 := "otherdirecory"
1802+
pipelineRunWsSubPath := "mypath"
1803+
ps := []*v1alpha1.Pipeline{tb.Pipeline("test-pipeline", tb.PipelineNamespace("foo"), tb.PipelineSpec(
1804+
tb.PipelineTask("hello-world-1", "hello-world", tb.PipelineTaskWorkspaceBinding("taskWorkspaceName", workspaceName, subPath1)),
1805+
tb.PipelineTask("hello-world-2", "hello-world", tb.PipelineTaskWorkspaceBinding("taskWorkspaceName", workspaceName, subPath2)),
1806+
tb.PipelineTask("hello-world-3", "hello-world", tb.PipelineTaskWorkspaceBinding("taskWorkspaceName", workspaceName, "")),
1807+
tb.PipelineTask("hello-world-4", "hello-world", tb.PipelineTaskWorkspaceBinding("taskWorkspaceName", workspaceNameWithSubPath, "")),
1808+
tb.PipelineTask("hello-world-5", "hello-world", tb.PipelineTaskWorkspaceBinding("taskWorkspaceName", workspaceNameWithSubPath, subPath1)),
1809+
tb.PipelineWorkspaceDeclaration(workspaceName, workspaceNameWithSubPath),
1810+
))}
1811+
1812+
prs := []*v1alpha1.PipelineRun{tb.PipelineRun("test-pipeline-run", tb.PipelineRunNamespace("foo"),
1813+
tb.PipelineRunSpec("test-pipeline",
1814+
tb.PipelineRunWorkspaceBindingVolumeClaimTemplate(workspaceName, "myclaim", ""),
1815+
tb.PipelineRunWorkspaceBindingVolumeClaimTemplate(workspaceNameWithSubPath, "myclaim", pipelineRunWsSubPath))),
1816+
}
1817+
ts := []*v1alpha1.Task{tb.Task("hello-world", tb.TaskNamespace("foo"))}
1818+
1819+
d := test.Data{
1820+
PipelineRuns: prs,
1821+
Pipelines: ps,
1822+
Tasks: ts,
1823+
}
1824+
1825+
testAssets, cancel := getPipelineRunController(t, d)
1826+
defer cancel()
1827+
c := testAssets.Controller
1828+
clients := testAssets.Clients
1829+
1830+
err := c.Reconciler.Reconcile(context.Background(), "foo/test-pipeline-run")
1831+
if err != nil {
1832+
t.Errorf("Did not expect to see error when reconciling PipelineRun but saw %s", err)
1833+
}
1834+
1835+
// Check that the PipelineRun was reconciled correctly
1836+
reconciledRun, err := clients.Pipeline.TektonV1alpha1().PipelineRuns("foo").Get("test-pipeline-run", metav1.GetOptions{})
1837+
if err != nil {
1838+
t.Fatalf("Somehow had error getting reconciled run out of fake client: %s", err)
1839+
}
1840+
1841+
taskRuns, err := clients.Pipeline.TektonV1alpha1().TaskRuns("foo").List(metav1.ListOptions{})
1842+
if err != nil {
1843+
t.Fatalf("unexpected error when listing TaskRuns: %v", err)
1844+
}
1845+
1846+
if len(taskRuns.Items) != 5 {
1847+
t.Fatalf("unexpected number of taskRuns found, expected 2, but found %d", len(taskRuns.Items))
1848+
}
1849+
1850+
hasSeenWorkspaceWithPipelineTaskSubPath1 := false
1851+
hasSeenWorkspaceWithPipelineTaskSubPath2 := false
1852+
hasSeenWorkspaceWithEmptyPipelineTaskSubPath := false
1853+
hasSeenWorkspaceWithRunSubPathAndEmptyPipelineTaskSubPath := false
1854+
hasSeenWorkspaceWithRunSubPathAndPipelineTaskSubPath1 := false
1855+
for _, tr := range taskRuns.Items {
1856+
for _, ws := range tr.Spec.Workspaces {
1857+
1858+
if ws.PersistentVolumeClaim == nil {
1859+
t.Fatalf("found taskRun workspace that is not PersistentVolumeClaim workspace. Did only expect PersistentVolumeClaims workspaces")
1860+
}
1861+
1862+
if ws.SubPath == subPath1 {
1863+
hasSeenWorkspaceWithPipelineTaskSubPath1 = true
1864+
}
1865+
1866+
if ws.SubPath == subPath2 {
1867+
hasSeenWorkspaceWithPipelineTaskSubPath2 = true
1868+
}
1869+
1870+
if ws.SubPath == "" {
1871+
hasSeenWorkspaceWithEmptyPipelineTaskSubPath = true
1872+
}
1873+
1874+
if ws.SubPath == pipelineRunWsSubPath {
1875+
hasSeenWorkspaceWithRunSubPathAndEmptyPipelineTaskSubPath = true
1876+
}
1877+
1878+
if ws.SubPath == fmt.Sprintf("%s/%s", pipelineRunWsSubPath, subPath1) {
1879+
hasSeenWorkspaceWithRunSubPathAndPipelineTaskSubPath1 = true
1880+
}
1881+
}
1882+
}
1883+
1884+
if !hasSeenWorkspaceWithPipelineTaskSubPath1 {
1885+
t.Fatalf("did not see a taskRun with a workspace using pipelineTask subPath1")
1886+
}
1887+
1888+
if !hasSeenWorkspaceWithPipelineTaskSubPath2 {
1889+
t.Fatalf("did not see a taskRun with a workspace using pipelineTask subPath2")
1890+
}
1891+
1892+
if !hasSeenWorkspaceWithEmptyPipelineTaskSubPath {
1893+
t.Fatalf("did not see a taskRun with a workspace using empty pipelineTask subPath")
1894+
}
1895+
1896+
if !hasSeenWorkspaceWithRunSubPathAndEmptyPipelineTaskSubPath {
1897+
t.Fatalf("did not see a taskRun with workspace using empty pipelineTask subPath and a subPath from pipelineRun")
1898+
}
1899+
1900+
if !hasSeenWorkspaceWithRunSubPathAndPipelineTaskSubPath1 {
1901+
t.Fatalf("did not see a taskRun with workspace using pipelineTaks subPath1 and a subPath from pipelineRun")
1902+
}
1903+
1904+
if !reconciledRun.Status.GetCondition(apis.ConditionSucceeded).IsUnknown() {
1905+
t.Errorf("Expected PipelineRun to be running, but condition status is %s", reconciledRun.Status.GetCondition(apis.ConditionSucceeded))
1906+
}
1907+
}
1908+
17951909
func TestReconcileWithTaskResults(t *testing.T) {
17961910
names.TestingSeed()
17971911
ps := []*v1alpha1.Pipeline{tb.Pipeline("test-pipeline", tb.PipelineNamespace("foo"), tb.PipelineSpec(

test/builder/pipeline.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -302,11 +302,12 @@ func PipelineTaskConditionResource(name, resource string, from ...string) Pipeli
302302
}
303303
}
304304

305-
func PipelineTaskWorkspaceBinding(name, workspace string) PipelineTaskOp {
305+
func PipelineTaskWorkspaceBinding(name, workspace, subPath string) PipelineTaskOp {
306306
return func(pt *v1alpha1.PipelineTask) {
307307
pt.Workspaces = append(pt.Workspaces, v1alpha1.WorkspacePipelineTaskBinding{
308308
Name: name,
309309
Workspace: workspace,
310+
SubPath: subPath,
310311
})
311312
}
312313
}
@@ -619,10 +620,11 @@ func PipelineRunWorkspaceBindingEmptyDir(name string) PipelineRunSpecOp {
619620
}
620621

621622
// PipelineRunWorkspaceBindingVolumeClaimTemplate adds an VolumeClaimTemplate Workspace to the workspaces of a pipelineRun spec.
622-
func PipelineRunWorkspaceBindingVolumeClaimTemplate(name string, claimName string) PipelineRunSpecOp {
623+
func PipelineRunWorkspaceBindingVolumeClaimTemplate(name string, claimName string, subPath string) PipelineRunSpecOp {
623624
return func(spec *v1alpha1.PipelineRunSpec) {
624625
spec.Workspaces = append(spec.Workspaces, v1alpha1.WorkspaceBinding{
625-
Name: name,
626+
Name: name,
627+
SubPath: subPath,
626628
VolumeClaimTemplate: &corev1.PersistentVolumeClaim{
627629
ObjectMeta: metav1.ObjectMeta{
628630
Name: claimName,

test/builder/pipeline_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func TestPipeline(t *testing.T) {
4646
tb.PipelineTaskConditionParam("param-name", "param-value"),
4747
tb.PipelineTaskConditionResource("some-resource", "my-only-git-resource", "bar", "never-gonna"),
4848
),
49-
tb.PipelineTaskWorkspaceBinding("task-workspace1", "workspace1"),
49+
tb.PipelineTaskWorkspaceBinding("task-workspace1", "workspace1", ""),
5050
),
5151
tb.PipelineTask("bar", "chocolate",
5252
tb.PipelineTaskRefKind(v1alpha1.ClusterTaskKind),

test/v1alpha1/workspace_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ func TestWorkspacePipelineRunDuplicateWorkspaceEntriesInvalid(t *testing.T) {
105105

106106
pipeline := tb.Pipeline(pipelineName, tb.PipelineSpec(
107107
tb.PipelineWorkspaceDeclaration("foo"),
108-
tb.PipelineTask("task1", taskName, tb.PipelineTaskWorkspaceBinding("test", "foo")),
108+
tb.PipelineTask("task1", taskName, tb.PipelineTaskWorkspaceBinding("test", "foo", "")),
109109
))
110110
if _, err := c.PipelineClient.Create(pipeline); err != nil {
111111
t.Fatalf("Failed to create Pipeline: %s", err)
@@ -146,7 +146,7 @@ func TestWorkspacePipelineRunMissingWorkspaceInvalid(t *testing.T) {
146146

147147
pipeline := tb.Pipeline(pipelineName, tb.PipelineSpec(
148148
tb.PipelineWorkspaceDeclaration("foo"),
149-
tb.PipelineTask("task1", taskName, tb.PipelineTaskWorkspaceBinding("test", "foo")),
149+
tb.PipelineTask("task1", taskName, tb.PipelineTaskWorkspaceBinding("test", "foo", "")),
150150
))
151151
if _, err := c.PipelineClient.Create(pipeline); err != nil {
152152
t.Fatalf("Failed to create Pipeline: %s", err)

0 commit comments

Comments
 (0)