Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TEP-0108: Mapping Workspaces #4887

Merged
merged 1 commit into from
May 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
})
}
Aleromerog marked this conversation as resolved.
Show resolved Hide resolved
}

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"`
Aleromerog marked this conversation as resolved.
Show resolved Hide resolved
// 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
Aleromerog marked this conversation as resolved.
Show resolved Hide resolved
jerop marked this conversation as resolved.
Show resolved Hide resolved

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