Skip to content

Commit

Permalink
[TEP 108] Mapping workspaces
Browse files Browse the repository at this point in the history
According to TEP-0108:
auto-mapping Workspaces from Pipelines to PipelineTasks
when the names of the Workspaces declared in the Pipeline
and PipelineTask are the same to reduce verbosity and improve usability of Pipelines.
  • Loading branch information
Aleromerog authored and tekton-robot committed May 26, 2022
1 parent 30b2bcb commit c040920
Show file tree
Hide file tree
Showing 9 changed files with 349 additions and 26 deletions.
33 changes: 31 additions & 2 deletions docs/pipelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ weight: 400
-->
# Pipelines

- [Overview](#pipelines)
- [Configuring a `Pipeline`](#configuring-a-pipeline)
- [Pipelines](#pipelines)
- [Overview](#overview)
- [Configuring a `Pipeline`](#configuring-a-pipeline)
- [Specifying `Resources`](#specifying-resources)
- [Specifying `Workspaces`](#specifying-workspaces)
- [Specifying `Parameters`](#specifying-parameters)
Expand Down Expand Up @@ -177,10 +178,38 @@ spec:
workspace: pipeline-ws1
```

For simplicity you can also map the name of the `Workspace` in `PipelineTask` to match with
the `Workspace` from the `Pipeline`.
For example:

```yaml
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: pipeline
spec:
workspaces:
- name: source
tasks:
- name: gen-code
taskRef:
name: gen-code # gen-code expects a Workspace named "source"
workspaces:
- name: source # <- mapping workspace name
- name: commit
taskRef:
name: commit # commit expects a Workspace named "source"
workspaces:
- name: source # <- mapping workspace name
runAfter:
- gen-code
```

For more information, see:
- [Using `Workspaces` in `Pipelines`](workspaces.md#using-workspaces-in-pipelines)
- The [`Workspaces` in a `PipelineRun`](../examples/v1beta1/pipelineruns/workspaces.yaml) code example
- The [variables available in a `PipelineRun`](variables.md#variables-available-in-a-pipeline), including `workspaces.<name>.bound`.
- [Mapping `Workspaces`](https://github.com/tektoncd/community/blob/main/teps/0108-mapping-workspaces.md)

## Specifying `Parameters`

Expand Down
131 changes: 131 additions & 0 deletions examples/v1beta1/pipelineruns/mapping-workspaces.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# In this contrived example 3 different kinds of workspace volume are used to thread
# data through a pipeline's tasks.
# 1. A ConfigMap is used as source of recipe data.
# 2. A Secret is used to store a password.
# 3. A PVC is used to share data from one task to the next.
#
# The end result is a pipeline that first checks if the password is correct and, if so,
# copies data out of a recipe store onto a shared volume. The recipe data is then read
# by a subsequent task and printed to screen.
apiVersion: v1
kind: ConfigMap
metadata:
name: sensitive-recipe-storage
data:
brownies: |
1. Heat oven to 325 degrees F
2. Melt 1/2 cup butter w/ 1/2 cup cocoa, stirring smooth.
3. Remove from heat, allow to cool for a few minutes.
4. Transfer to bowl.
5. Whisk in 2 eggs, one at a time.
6. Stir in vanilla.
7. Separately combine 1 cup sugar, 1/4 cup flour, 1 cup chopped
walnuts and pinch of salt
8. Combine mixtures.
9. Bake in greased pan for 30 minutes. Watch carefully for
appropriate level of gooeyness.
---
apiVersion: v1
kind: Secret
metadata:
name: secret-password
type: Opaque
data:
password: aHVudGVyMg==
---
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: fetch-secure-data
spec:
workspaces:
- name: password-vault
- name: recipe-store
- name: shared-data
steps:
- name: fetch-and-write
image: ubuntu
script: |
if [ "hunter2" = "$(cat $(workspaces.password-vault.path)/password)" ]; then
cp $(workspaces.recipe-store.path)/recipe.txt $(workspaces.shared-data.path)
else
echo "wrong password!"
exit 1
fi
---
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: print-data
spec:
workspaces:
- name: shared-data
readOnly: true
params:
- name: filename
steps:
- name: print-secrets
image: ubuntu
script: cat $(workspaces.shared-data.path)/$(params.filename)
---
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: fetch-and-print-recipe
spec:
workspaces:
- name: password-vault
- name: recipe-store
- name: shared-data
tasks:
- name: fetch-the-recipe
taskRef:
name: fetch-secure-data
workspaces:
- name: password-vault
- name: recipe-store
- name: shared-data
- name: print-the-recipe
taskRef:
name: print-data
# Note: this is currently required to ensure order of write / read on PVC is correct.
runAfter:
- fetch-the-recipe
params:
- name: filename
value: recipe.txt
workspaces:
- name: shared-data
---
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
generateName: recipe-time-
spec:
pipelineRef:
name: fetch-and-print-recipe
workspaces:
- name: password-vault
secret:
secretName: secret-password
- name: recipe-store
configMap:
name: sensitive-recipe-storage
items:
- key: brownies
path: recipe.txt
- name: shared-data
persistentVolumeClaim:
claimName: shared-task-storage
3 changes: 1 addition & 2 deletions pkg/apis/pipeline/v1beta1/openapi_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion pkg/apis/pipeline/v1beta1/pipeline_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,14 @@ func validateExecutionStatusVariablesExpressions(expressions []string, ptNames s

func (pt *PipelineTask) validateWorkspaces(workspaceNames sets.String) (errs *apis.FieldError) {
for i, ws := range pt.Workspaces {
if !workspaceNames.Has(ws.Workspace) {
if ws.Workspace == "" {
if !workspaceNames.Has(ws.Name) {
errs = errs.Also(apis.ErrInvalidValue(
fmt.Sprintf("pipeline task %q expects workspace with name %q but none exists in pipeline spec", pt.Name, ws.Name),
"",
).ViaFieldIndex("workspaces", i))
}
} else if !workspaceNames.Has(ws.Workspace) {
errs = errs.Also(apis.ErrInvalidValue(
fmt.Sprintf("pipeline task %q expects workspace with name %q but none exists in pipeline spec", pt.Name, ws.Workspace),
"",
Expand Down
62 changes: 49 additions & 13 deletions pkg/apis/pipeline/v1beta1/pipeline_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1701,21 +1701,41 @@ func TestValidatePipelineWorkspacesDeclarations_Success(t *testing.T) {
}

func TestValidatePipelineWorkspacesUsage_Success(t *testing.T) {
desc := "unused pipeline spec workspaces do not cause an error"
workspaces := []PipelineWorkspaceDeclaration{{
Name: "foo",
tests := []struct {
name string
workspaces []PipelineWorkspaceDeclaration
tasks []PipelineTask
}{{
name: "unused pipeline spec workspaces do not cause an error",
workspaces: []PipelineWorkspaceDeclaration{{
Name: "foo",
}, {
Name: "bar",
}},
tasks: []PipelineTask{{
Name: "foo", TaskRef: &TaskRef{Name: "foo"},
}},
}, {
Name: "bar",
}}
tasks := []PipelineTask{{
Name: "foo", TaskRef: &TaskRef{Name: "foo"},
name: "valid mapping pipeline-task workspace name with pipeline workspace name",
workspaces: []PipelineWorkspaceDeclaration{{
Name: "pipelineWorkspaceName",
}},
tasks: []PipelineTask{{
Name: "foo", TaskRef: &TaskRef{Name: "foo"},
Workspaces: []WorkspacePipelineTaskBinding{{
Name: "pipelineWorkspaceName",
Workspace: "",
}},
}},
}}
t.Run(desc, func(t *testing.T) {
err := validatePipelineWorkspacesUsage(workspaces, tasks)
if err != nil {
t.Errorf("Pipeline.validatePipelineWorkspacesUsage() returned error for valid pipeline workspaces: %v", err)
}
})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errs := validatePipelineWorkspacesUsage(tt.workspaces, tt.tasks).ViaField("tasks")
if errs != nil {
t.Errorf("Pipeline.validatePipelineWorkspacesUsage() returned error for valid pipeline workspaces: %v", errs)
}
})
}
}

func TestValidatePipelineWorkspacesDeclarations_Failure(t *testing.T) {
Expand Down Expand Up @@ -1786,6 +1806,22 @@ func TestValidatePipelineWorkspacesUsage_Failure(t *testing.T) {
Message: `invalid value: pipeline task "foo" expects workspace with name "pipelineWorkspaceName" but none exists in pipeline spec`,
Paths: []string{"tasks[0].workspaces[0]"},
},
}, {
name: "invalid mapping workspace with different name",
workspaces: []PipelineWorkspaceDeclaration{{
Name: "pipelineWorkspaceName",
}},
tasks: []PipelineTask{{
Name: "foo", TaskRef: &TaskRef{Name: "foo"},
Workspaces: []WorkspacePipelineTaskBinding{{
Name: "taskWorkspaceName",
Workspace: "",
}},
}},
expectedError: apis.FieldError{
Message: `invalid value: pipeline task "foo" expects workspace with name "taskWorkspaceName" but none exists in pipeline spec`,
Paths: []string{"tasks[0].workspaces[0]"},
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
6 changes: 2 additions & 4 deletions pkg/apis/pipeline/v1beta1/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -3095,8 +3095,7 @@
"description": "WorkspacePipelineTaskBinding describes how a workspace passed into the pipeline should be mapped to a task's declared workspace.",
"type": "object",
"required": [
"name",
"workspace"
"name"
],
"properties": {
"name": {
Expand All @@ -3110,8 +3109,7 @@
},
"workspace": {
"description": "Workspace is the name of the workspace declared by the pipeline",
"type": "string",
"default": ""
"type": "string"
}
}
},
Expand Down
3 changes: 2 additions & 1 deletion pkg/apis/pipeline/v1beta1/workspace_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ type WorkspacePipelineTaskBinding struct {
// Name is the name of the workspace as declared by the task
Name string `json:"name"`
// Workspace is the name of the workspace declared by the pipeline
Workspace string `json:"workspace"`
// +optional
Workspace string `json:"workspace,omitempty"`
// SubPath is optionally a directory on the volume which should be used
// for this binding (i.e. the volume will be mounted at this sub directory).
// +optional
Expand Down
14 changes: 11 additions & 3 deletions pkg/reconciler/pipelinerun/pipelinerun.go
Original file line number Diff line number Diff line change
Expand Up @@ -888,9 +888,16 @@ func getTaskrunWorkspaces(pr *v1beta1.PipelineRun, rprt *resources.ResolvedPipel
}
for _, ws := range rprt.PipelineTask.Workspaces {
taskWorkspaceName, pipelineTaskSubPath, pipelineWorkspaceName := ws.Name, ws.SubPath, ws.Workspace
if b, hasBinding := pipelineRunWorkspaces[pipelineWorkspaceName]; hasBinding {

pipelineWorkspace := pipelineWorkspaceName

if pipelineWorkspaceName == "" {
pipelineWorkspace = taskWorkspaceName
}

if b, hasBinding := pipelineRunWorkspaces[pipelineWorkspace]; hasBinding {
if b.PersistentVolumeClaim != nil || b.VolumeClaimTemplate != nil {
pipelinePVCWorkspaceName = pipelineWorkspaceName
pipelinePVCWorkspaceName = pipelineWorkspace
}
workspaces = append(workspaces, taskWorkspaceByWorkspaceVolumeSource(b, taskWorkspaceName, pipelineTaskSubPath, *kmeta.NewControllerRef(pr)))
} else {
Expand All @@ -904,9 +911,10 @@ func getTaskrunWorkspaces(pr *v1beta1.PipelineRun, rprt *resources.ResolvedPipel
}
}
if !workspaceIsOptional {
return nil, "", fmt.Errorf("expected workspace %q to be provided by pipelinerun for pipeline task %q", pipelineWorkspaceName, rprt.PipelineTask.Name)
return nil, "", fmt.Errorf("expected workspace %q to be provided by pipelinerun for pipeline task %q", pipelineWorkspace, rprt.PipelineTask.Name)
}
}

}
return workspaces, pipelinePVCWorkspaceName, nil
}
Expand Down
Loading

0 comments on commit c040920

Please sign in to comment.