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

readOnly flag on workspace declarations #1760

Merged
merged 1 commit into from Dec 20, 2019
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
4 changes: 4 additions & 0 deletions docs/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,10 @@ this with `mountPath`. The value at `mountPath` can be anywhere on your pod's fi
The path will be available via [variable substitution](#variable-substitution) with
`$(workspaces.myworkspace.path)`.

A task can declare that it will not write to the volume by adding `readOnly: true`
to the workspace declaration. This will in turn mark the volumeMount as `readOnly`
on the Task's underlying pod.

The actual volumes must be provided at runtime
[in the `TaskRun`](taskruns.md#workspaces).
In a future iteration ([#1438](https:/tektoncd/pipeline/issues/1438))
Expand Down
47 changes: 47 additions & 0 deletions examples/taskruns/workspace-readonly.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc-2
spec:
resources:
requests:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
---
apiVersion: tekton.dev/v1alpha1
kind: TaskRun
metadata:
generateName: workspaces-readonly-
spec:
workspaces:
- name: write-allowed
persistentVolumeClaim:
claimName: my-pvc-2
- name: write-disallowed
persistentVolumeClaim:
claimName: my-pvc-2
taskSpec:
workspaces:
- name: write-allowed
- name: write-disallowed
readOnly: true
steps:
- name: write-allowed
image: ubuntu
script: echo "hello" > $(workspaces.write-allowed.path)/foo
- name: read-allowed
image: ubuntu
script: cat $(workspaces.write-allowed.path)/foo | grep "hello"
- name: write-disallowed
image: ubuntu
script:
echo "goodbye" > $(workspaces.write-disallowed.path)/foo || touch write-failed.txt
test -f write-failed.txt
- name: read-again
# We should get "hello" when reading again because writing "goodbye" to
# the file should have been disallowed.
image: ubuntu
script:
cat $(workspaces.write-disallowed.path)/foo | grep "hello"
3 changes: 3 additions & 0 deletions pkg/apis/pipeline/v1alpha1/workspace_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ type WorkspaceDeclaration struct {
// MountPath overrides the directory that the volume will be made available at.
// +optional
MountPath string `json:"mountPath,omitempty"`
// ReadOnly dictates whether a mounted volume is writable. By default this
// field is false and so mounted volumes are writable.
ReadOnly bool `json:"readOnly,omitempty"`
}

// GetMountPath returns the mountPath for w which is the MountPath if provided or the
Expand Down
1 change: 1 addition & 0 deletions pkg/workspace/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ func Apply(ts v1alpha1.TaskSpec, wb []v1alpha1.WorkspaceBinding) (*v1alpha1.Task
Name: vv.Name,
MountPath: w.GetMountPath(),
SubPath: wb[i].SubPath,
ReadOnly: w.ReadOnly,
})

// Only add this volume if it hasn't already been added
Expand Down
40 changes: 38 additions & 2 deletions pkg/workspace/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,8 +413,44 @@ func TestApply(t *testing.T) {
MountPath: "/my/fancy/mount/path",
}},
},
},
} {
}, {
name: "readOnly true marks volume mount readOnly",
ts: v1alpha1.TaskSpec{
Workspaces: []v1alpha1.WorkspaceDeclaration{{
Name: "custom",
MountPath: "/my/fancy/mount/path",
ReadOnly: true,
}},
},
workspaces: []v1alpha1.WorkspaceBinding{{
Name: "custom",
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
ClaimName: "mypvc",
},
}},
expectedTaskSpec: v1alpha1.TaskSpec{
StepTemplate: &corev1.Container{
VolumeMounts: []corev1.VolumeMount{{
Name: "ws-twkr2",
MountPath: "/my/fancy/mount/path",
ReadOnly: true,
}},
},
Volumes: []corev1.Volume{{
Name: "ws-twkr2",
VolumeSource: corev1.VolumeSource{
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
ClaimName: "mypvc",
},
},
}},
Workspaces: []v1alpha1.WorkspaceDeclaration{{
Name: "custom",
MountPath: "/my/fancy/mount/path",
ReadOnly: true,
}},
},
}} {
t.Run(tc.name, func(t *testing.T) {
ts, err := workspace.Apply(tc.ts, tc.workspaces)
if err != nil {
Expand Down
6 changes: 6 additions & 0 deletions test/builder/step.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ func StepVolumeMount(name, mountPath string, ops ...VolumeMountOp) StepOp {
}
}

func StepScript(script string) StepOp {
return func(step *v1alpha1.Step) {
step.Script = script
}
}

// StepResources adds ResourceRequirements to the Container (step).
func StepResources(ops ...ResourceRequirementsOp) StepOp {
return func(step *v1alpha1.Step) {
Expand Down
3 changes: 2 additions & 1 deletion test/builder/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,13 @@ func Sidecar(name, image string, ops ...ContainerOp) TaskSpecOp {
}

// TaskWorkspace adds a workspace declaration.
func TaskWorkspace(name, desc, mountPath string) TaskSpecOp {
func TaskWorkspace(name, desc, mountPath string, readOnly bool) TaskSpecOp {
return func(spec *v1alpha1.TaskSpec) {
spec.Workspaces = append(spec.Workspaces, v1alpha1.WorkspaceDeclaration{
Name: name,
Description: desc,
MountPath: mountPath,
ReadOnly: readOnly,
})
}
}
Expand Down
7 changes: 6 additions & 1 deletion test/builder/task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,14 @@ func TestTask(t *testing.T) {
tb.Step("mycontainer", "myimage", tb.StepCommand("/mycmd"), tb.StepArgs(
"--my-other-arg=$(inputs.resources.workspace.url)",
)),
tb.Step("mycontainer2", "myimage2", tb.StepScript("echo foo")),
tb.TaskVolume("foo", tb.VolumeSource(corev1.VolumeSource{
HostPath: &corev1.HostPathVolumeSource{Path: "/foo/bar"},
})),
tb.TaskStepTemplate(
tb.EnvVar("FRUIT", "BANANA"),
),
tb.TaskWorkspace("bread", "kind of bread", "/bread/path"),
tb.TaskWorkspace("bread", "kind of bread", "/bread/path", false),
))
expectedTask := &v1alpha1.Task{
ObjectMeta: metav1.ObjectMeta{Name: "test-task", Namespace: "foo"},
Expand All @@ -71,6 +72,9 @@ func TestTask(t *testing.T) {
Image: "myimage",
Command: []string{"/mycmd"},
Args: []string{"--my-other-arg=$(inputs.resources.workspace.url)"},
}}, {Script: "echo foo", Container: corev1.Container{
Name: "mycontainer2",
Image: "myimage2",
}}},
Inputs: &v1alpha1.Inputs{
Resources: []v1alpha1.TaskResource{{
Expand Down Expand Up @@ -125,6 +129,7 @@ func TestTask(t *testing.T) {
Name: "bread",
Description: "kind of bread",
MountPath: "/bread/path",
ReadOnly: false,
}},
},
}
Expand Down
85 changes: 85 additions & 0 deletions test/workspace_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// +build e2e

/*
Copyright 2019 The Tekton Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package test

import (
"strings"
"testing"

tb "github.com/tektoncd/pipeline/test/builder"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
knativetest "knative.dev/pkg/test"
)

func TestWorkspaceReadOnlyDisallowsWrite(t *testing.T) {
c, namespace := setup(t)

taskName := "write-disallowed"
taskRunName := "write-disallowed-tr"

knativetest.CleanupOnInterrupt(func() { tearDown(t, c, namespace) }, t.Logf)
defer tearDown(t, c, namespace)

task := tb.Task(taskName, namespace, tb.TaskSpec(
tb.Step("attempt-write", "alpine", tb.StepScript("echo foo > /workspace/test/file")),
tb.TaskWorkspace("test", "test workspace", "/workspace/test", true),
))
if _, err := c.TaskClient.Create(task); err != nil {
t.Fatalf("Failed to create Task: %s", err)
}

taskRun := tb.TaskRun(taskRunName, namespace, tb.TaskRunSpec(
tb.TaskRunTaskRef(taskName), tb.TaskRunServiceAccountName("default"),
tb.TaskRunWorkspaceEmptyDir("test", ""),
))
if _, err := c.TaskRunClient.Create(taskRun); err != nil {
t.Fatalf("Failed to create TaskRun: %s", err)
}

t.Logf("Waiting for TaskRun in namespace %s to finish", namespace)
if err := WaitForTaskRunState(c, taskRunName, TaskRunFailed(taskRunName), "error"); err != nil {
t.Errorf("Error waiting for TaskRun to finish with error: %s", err)
}

tr, err := c.TaskRunClient.Get(taskRunName, metav1.GetOptions{})
if err != nil {
t.Errorf("Error retrieving taskrun: %s", err)
}
if tr.Status.PodName == "" {
t.Fatal("Error getting a PodName (empty)")
}
p, err := c.KubeClient.Kube.CoreV1().Pods(namespace).Get(tr.Status.PodName, metav1.GetOptions{})

if err != nil {
t.Fatalf("Error getting pod `%s` in namespace `%s`", tr.Status.PodName, namespace)
}
for _, stat := range p.Status.ContainerStatuses {
if strings.Contains(stat.Name, "step-attempt-write") {
req := c.KubeClient.Kube.CoreV1().Pods(namespace).GetLogs(p.Name, &corev1.PodLogOptions{Container: stat.Name})
logContent, err := req.Do().Raw()
if err != nil {
t.Fatalf("Error getting pod logs for pod `%s` and container `%s` in namespace `%s`", tr.Status.PodName, stat.Name, namespace)
}
if !strings.Contains(string(logContent), "Read-only file system") {
t.Fatalf("Expected read-only file system error but received %v", logContent)
}
}
}
}