Skip to content

Commit

Permalink
Add "volume" PipelineResource
Browse files Browse the repository at this point in the history
This will allow copying content either into or out of a `TaskRun`,
either to an existing volume or a newly created volume. The immediate
use case is for copying a pipeline's workspace to be made available as
the input for another pipeline's workspace without needing to deal
with uploading everything to a bucket. The volume, whether already
existing or created, will not be deleted at the end of the
`PipelineRun`, unlike the artifact storage PVC.

The Volume resource is a sub-type of the general Storage resource.

Since this type will require the creation of a PVC to function (to be
configurable later), this commit adds a Setup interface that
PipelineResources can implement if they need to do setup that involves
instantiating objects in Kube. This could be a place to later add
features like caching, and also is the sort of design we'd expect once
PipelineResources are extensible (PipelineResources will be free to do
whatever setup they need).

The behavior of this volume resource is:
1. For inputs, copy data _from_ the PVC to the workspace path
2. For outputs, copy data _to_ the PVC from the workspace path

If a user does want to control where the data is copied from, they can:
1. Add a step that copies from the location they want to copy from on
   disk to /workspace/whatever
2. Use the "targetPath" argument in the PipelineResource to control the
   location the data is copied to (still relative to targetPath
   https://github.com/tektoncd/pipeline/blob/master/docs/resources.md#controlling-where-resources-are-mounted)
3. Use `path` https://github.com/tektoncd/pipeline/blob/master/docs/resources.md#overriding-where-resources-are-copied-from
   (tbd if we want to keep this feature post PVC)

The underlying PVC will need to be created by the Task reonciler, if
only a TaskRun is being used, or by the PipelineRun reconciler if a
Pipeline is being used. The PipelineRun reconciler cannot delegate this
to the TaskRun reconciler b/c when two different reconcilers create PVCs
and Tekton is running on a regional GKE cluster, they can get created in
different zones, resulting in a pod that tries to use both being
unschedulable.

fixes #1062

Co-authored-by: Dan Lorenc <lorenc.d@gmail.com>
Co-authored-by: Christie Wilson <bobcatfish@gmail.com>
  • Loading branch information
3 people committed Oct 1, 2019
1 parent c30f1d2 commit f414a5d
Show file tree
Hide file tree
Showing 30 changed files with 1,425 additions and 69 deletions.
168 changes: 168 additions & 0 deletions examples/pipelineruns/volume-output-pipelinerun.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
apiVersion: tekton.dev/v1alpha1
kind: PipelineResource
metadata:
name: volume-resource-1
spec:
type: storage
params:
- name: type
value: volume
---
apiVersion: tekton.dev/v1alpha1
kind: PipelineResource
metadata:
name: volume-resource-2
spec:
type: storage
params:
- name: type
value: volume
- name: path
value: special-folder
---
# Task writes "some stuff" to a predefined path
apiVersion: tekton.dev/v1alpha1
kind: Task
metadata:
name: create-files
spec:
outputs:
# This Task uses two volume outputs to ensure that multiple volume
# outputs can be used
resources:
- name: volume1
type: storage
- name: volume2
type: storage
steps:
- name: write-new-stuff-1
image: ubuntu
command: ['bash']
args: ['-c', 'echo some stuff1 > /workspace/output/volume1/stuff1']
- name: write-new-stuff-2
image: ubuntu
command: ['bash']
args: ['-c', 'echo some stuff2 > /workspace/output/volume2/stuff2']
---
# Reads a file from a predefined path and writes as well
apiVersion: tekton.dev/v1alpha1
kind: Task
metadata:
name: files-exist-and-add-new
spec:
inputs:
resources:
- name: volume1
type: storage
targetPath: newpath
- name: volume2
type: storage
outputs:
# This Task uses the same volume as an input and an output to ensure this works
resources:
- name: volume1
type: storage
steps:
- name: read1
image: ubuntu
command: ["/bin/bash"]
args:
- '-c'
- '[[ stuff1 == $(cat $(inputs.resources.volume1.path)/stuff1) ]]"'
- name: read2
image: ubuntu
command: ["/bin/bash"]
args:
- '-c'
# TODO: should fail
- '[[ stuff == $(cat $(inputs.resources.volume2.path)/stuff1) ]]"'
- name: write-new-stuff-3
image: ubuntu
command: ['bash']
args: ['-c', 'echo some stuff3 > /workspace/output/volume1/stuff3']
---
# Reads a file from a predefined path and writes as well
apiVersion: tekton.dev/v1alpha1
kind: Task
metadata:
name: files-exist
spec:
inputs:
resources:
- name: volume1
type: storage
steps:
- name: read1
image: ubuntu
command: ["/bin/bash"]
args:
- '-c'
- '[[ stuff == $(cat $(inputs.resources.volume1.path)/stuff1) ]]"'
- name: read3
image: ubuntu
command: ["/bin/bash"]
args:
- '-c'
- '[[ stuff3 == $(cat $(inputs.resources.volume1.path)/stuff3) ]]"'
---
# First task writees files to two volumes. The next task ensures these files exist
# then writes a third file to the first volume. The last Task ensures both expected
# files exist on this volume.
apiVersion: tekton.dev/v1alpha1
kind: Pipeline
metadata:
name: volume-output-pipeline
spec:
resources:
- name: volume1
type: storage
- name: volume2
type: storage
tasks:
- name: first-create-files
taskRef:
name: create-files
resources:
outputs:
- name: volume1
resource: volume1
- name: volume2
resource: volume2
- name: then-check-and-write
taskRef:
name: files-exist-and-add-new
resources:
inputs:
- name: volume1
resource: volume1
from: [first-create-files]
- name: volume2
resource: volume2
from: [first-create-files]
outputs:
- name: volume1
resource: volume1
- name: then-check
taskRef:
name: files-exist
resources:
inputs:
- name: volume1
resource: volume1
from: [first-create-files]
---
apiVersion: tekton.dev/v1alpha1
kind: PipelineRun
metadata:
name: volume-output-pipeline-run
spec:
pipelineRef:
name: volume-output-pipeline
serviceAccount: 'default'
resources:
- name: volume1
resourceRef:
name: volume-resource-1
- name: volume2
resourceRef:
name: volume-resource-2
17 changes: 10 additions & 7 deletions pkg/apis/pipeline/v1alpha1/artifact_pvc.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (p *ArtifactPVC) GetCopyFromStorageToSteps(name, sourcePath, destinationPat
}}}
}

// GetCopyToStorageFromSteps returns a container used to upload artifacts for temporary storage
// GetCopyToStorageFromSteps returns a container used to upload artifacts for temporary storageCreateDirStep
func (p *ArtifactPVC) GetCopyToStorageFromSteps(name, sourcePath, destinationPath string) []Step {
return []Step{{Container: corev1.Container{
Name: names.SimpleNameGenerator.RestrictLengthWithRandomSuffix(fmt.Sprintf("source-mkdir-%s", name)),
Expand Down Expand Up @@ -86,13 +86,16 @@ func GetPvcMount(name string) corev1.VolumeMount {
}
}

// CreateDirStep returns a container step to create a dir
func CreateDirStep(name, destinationPath string) Step {
// CreateDirStep returns a container step to create a dir at destinationPath. The name
// of the step will include name. Optionally will mount included volumeMounts if the
// dir is to be created on the volume.
func CreateDirStep(name, destinationPath string, volumeMounts []corev1.VolumeMount) Step {
return Step{Container: corev1.Container{
Name: names.SimpleNameGenerator.RestrictLengthWithRandomSuffix(fmt.Sprintf("create-dir-%s", strings.ToLower(name))),
Image: *BashNoopImage,
Command: []string{"/ko-app/bash"},
Args: []string{"-args", strings.Join([]string{"mkdir", "-p", destinationPath}, " ")},
Name: names.SimpleNameGenerator.RestrictLengthWithRandomSuffix(fmt.Sprintf("create-dir-%s", strings.ToLower(name))),
Image: *BashNoopImage,
Command: []string{"/ko-app/bash"},
Args: []string{"-args", strings.Join([]string{"mkdir", "-p", destinationPath}, " ")},
VolumeMounts: volumeMounts,
}}
}

Expand Down
26 changes: 22 additions & 4 deletions pkg/apis/pipeline/v1alpha1/artifact_pvc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func TestPVCGetPvcMount(t *testing.T) {
}
}

func TestPVCGetMakeStep(t *testing.T) {
func TestCreateDirStepWithoutVolume(t *testing.T) {
names.TestingSeed()

want := v1alpha1.Step{Container: corev1.Container{
Expand All @@ -95,9 +95,27 @@ func TestPVCGetMakeStep(t *testing.T) {
Command: []string{"/ko-app/bash"},
Args: []string{"-args", "mkdir -p /workspace/destination"},
}}
got := v1alpha1.CreateDirStep("workspace", "/workspace/destination")
if d := cmp.Diff(got, want); d != "" {
t.Errorf("Diff:\n%s", d)
got := v1alpha1.CreateDirStep("workspace", "/workspace/destination", nil)
if d := cmp.Diff(want, got); d != "" {
t.Errorf("Did not get expected step for creating directory (-want, +got): %s", d)
}
}

func TestCreateDirStepWithVolume(t *testing.T) {
names.TestingSeed()

want := v1alpha1.Step{Container: corev1.Container{
Name: "create-dir-workspace-9l9zj",
Image: "override-with-bash-noop:latest",
Command: []string{"/ko-app/bash"},
Args: []string{"-args", "mkdir -p /special-workspace/destination"},
VolumeMounts: []corev1.VolumeMount{{Name: "foo", MountPath: "/special-workspace"}},
}}
got := v1alpha1.CreateDirStep("workspace", "/special-workspace/destination", []corev1.VolumeMount{{
Name: "foo", MountPath: "/special-workspace",
}})
if d := cmp.Diff(want, got); d != "" {
t.Errorf("Did not get expected step for creating directory (-want, +got): %s", d)
}
}

Expand Down
5 changes: 4 additions & 1 deletion pkg/apis/pipeline/v1alpha1/build_gcs_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ type BuildGCSResource struct {
ArtifactType GCSArtifactType
}

// GetSetup returns a PipelineResourceSetupInterface that does nothing because no setup is needed.
func (s BuildGCSResource) GetSetup() PipelineResourceSetupInterface { return &NoSetup{} }

// NewBuildGCSResource creates a new BuildGCS resource to pass to a Task
func NewBuildGCSResource(r *PipelineResource) (*BuildGCSResource, error) {
if r.Spec.Type != PipelineResourceTypeStorage {
Expand Down Expand Up @@ -135,7 +138,7 @@ func (s *BuildGCSResource) GetInputTaskModifier(ts *TaskSpec, sourcePath string)
}

steps := []Step{
CreateDirStep(s.Name, sourcePath),
CreateDirStep(s.Name, sourcePath, nil),
{Container: corev1.Container{
Name: names.SimpleNameGenerator.RestrictLengthWithRandomSuffix(fmt.Sprintf("storage-fetch-%s", s.Name)),
Command: []string{"/ko-app/gcs-fetcher"},
Expand Down
2 changes: 1 addition & 1 deletion pkg/apis/pipeline/v1alpha1/build_gcs_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func Test_Invalid_BuildGCSResource(t *testing.T) {
)),
}} {
t.Run(tc.name, func(t *testing.T) {
_, err := v1alpha1.NewStorageResource(tc.pipelineResource)
_, err := v1alpha1.NewBuildGCSResource(tc.pipelineResource)
if err == nil {
t.Error("Expected error creating BuildGCS resource")
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/apis/pipeline/v1alpha1/cloud_event_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ func NewCloudEventResource(r *PipelineResource) (*CloudEventResource, error) {
}, nil
}

// GetSetup returns a PipelineResourceSetupInterface that can create the backing PVC if needed.
func (s CloudEventResource) GetSetup() PipelineResourceSetupInterface { return SetupPVC{} }

// GetName returns the name of the resource
func (s CloudEventResource) GetName() string {
return s.Name
Expand Down
3 changes: 3 additions & 0 deletions pkg/apis/pipeline/v1alpha1/cluster_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ type ClusterResource struct {
Secrets []SecretParam `json:"secrets"`
}

// GetSetup returns a PipelineResourceSetupInterface that does nothing because no setup is needed.
func (s ClusterResource) GetSetup() PipelineResourceSetupInterface { return &NoSetup{} }

// NewClusterResource create a new k8s cluster resource to pass to a pipeline task
func NewClusterResource(r *PipelineResource) (*ClusterResource, error) {
if r.Spec.Type != PipelineResourceTypeCluster {
Expand Down
5 changes: 4 additions & 1 deletion pkg/apis/pipeline/v1alpha1/gcs_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ type GCSResource struct {
Secrets []SecretParam `json:"secrets"`
}

// GetSetup returns a PipelineResourceSetupInterface that does nothing because no setup is needed.
func (s GCSResource) GetSetup() PipelineResourceSetupInterface { return &NoSetup{} }

// NewGCSResource creates a new GCS resource to pass to a Task
func NewGCSResource(r *PipelineResource) (*GCSResource, error) {
if r.Spec.Type != PipelineResourceTypeStorage {
Expand Down Expand Up @@ -142,7 +145,7 @@ func (s *GCSResource) GetInputTaskModifier(ts *TaskSpec, path string) (TaskModif

envVars, secretVolumeMount := getSecretEnvVarsAndVolumeMounts(s.Name, gcsSecretVolumeMountPath, s.Secrets)
steps := []Step{
CreateDirStep(s.Name, path),
CreateDirStep(s.Name, path, nil),
{Container: corev1.Container{
Name: names.SimpleNameGenerator.RestrictLengthWithRandomSuffix(fmt.Sprintf("fetch-%s", s.Name)),
Image: *gsutilImage,
Expand Down
3 changes: 3 additions & 0 deletions pkg/apis/pipeline/v1alpha1/git_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ type GitResource struct {
Revision string `json:"revision"`
}

// GetSetup returns a PipelineResourceSetupInterface that does nothing because no setup is needed.
func (s GitResource) GetSetup() PipelineResourceSetupInterface { return &NoSetup{} }

// NewGitResource creates a new git resource to pass to a Task
func NewGitResource(r *PipelineResource) (*GitResource, error) {
if r.Spec.Type != PipelineResourceTypeGit {
Expand Down
3 changes: 3 additions & 0 deletions pkg/apis/pipeline/v1alpha1/image_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ type ImageResource struct {
OutputImageDir string
}

// GetSetup returns a PipelineResourceSetupInterface that does nothing because no setup is needed.
func (s ImageResource) GetSetup() PipelineResourceSetupInterface { return &NoSetup{} }

// GetName returns the name of the resource
func (s ImageResource) GetName() string {
return s.Name
Expand Down
11 changes: 7 additions & 4 deletions pkg/apis/pipeline/v1alpha1/pipelineresource_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,24 +77,25 @@ func (rs *PipelineResourceSpec) Validate(ctx context.Context) *apis.FieldError {
}
}
if rs.Type == PipelineResourceTypeStorage {
foundTypeParam := false
var location string
t := ""
for _, param := range rs.Params {
switch {
case strings.EqualFold(param.Name, "type"):
if !AllowedStorageType(param.Value) {
return apis.ErrInvalidValue(param.Value, "spec.params.type")
}
foundTypeParam = true
t = param.Value
case strings.EqualFold(param.Name, "Location"):
location = param.Value
}
}

if !foundTypeParam {
if t == "" {
return apis.ErrMissingField("spec.params.type")
}
if location == "" {
// TODO: volume resource does not need location
if location == "" && t != string(PipelineResourceTypeVolume) {
return apis.ErrMissingField("spec.params.location")
}
}
Expand All @@ -114,6 +115,8 @@ func AllowedStorageType(gotType string) bool {
return true
case string(PipelineResourceTypeBuildGCS):
return true
case string(PipelineResourceTypeVolume):
return true
}
return false
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ func TestResourceValidation_Invalid(t *testing.T) {
},
want: apis.ErrInvalidValue("spec.type", "not-supported"),
},
// TODO: add volume use cases here
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
3 changes: 3 additions & 0 deletions pkg/apis/pipeline/v1alpha1/pull_request_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ type PullRequestResource struct {
Secrets []SecretParam `json:"secrets"`
}

// GetSetup returns a PipelineResourceSetupInterface that does nothing because no setup is needed.
func (s PullRequestResource) GetSetup() PipelineResourceSetupInterface { return &NoSetup{} }

// NewPullRequestResource create a new git resource to pass to a Task
func NewPullRequestResource(r *PipelineResource) (*PullRequestResource, error) {
if r.Spec.Type != PipelineResourceTypePullRequest {
Expand Down
Loading

0 comments on commit f414a5d

Please sign in to comment.