Skip to content

Commit

Permalink
Add Default TaskRun Workspace Bindings to config-default
Browse files Browse the repository at this point in the history
Currently, users have to completely specify `Workspaces` they don't care
about or whose configuration should be entirely in the hands of admins.

This PR enables users to specify default `Workspaces` for `TaskRuns`,
for example they can use `emptyDir` by default.

The `WorkspaceBinding` can be set in the `config-defaults` ConfigMap
in `default-task-run-workspace-binding`.

Fixes tektoncd#2398 and partially fixes tektoncd#2595.
  • Loading branch information
Scott authored and jerop committed Jul 10, 2020
1 parent 568efa4 commit 8ceca9b
Show file tree
Hide file tree
Showing 11 changed files with 260 additions and 18 deletions.
8 changes: 7 additions & 1 deletion config/config-defaults.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,10 @@ data:
# Note that right now it is still not possible to set a PipelineRun or
# TaskRun specific sink, so the default is the only option available.
# If no sink is specified, no CloudEvent is generated
# default-cloud-events-sink:
# default-cloud-events-sink:
# default-task-run-workspace-binding contains the default workspace
# configuration provided for any Workspaces that a Task declares
# but that a TaskRun does not explicitly provide.
# default-task-run-workspace-binding: |
# emptyDir: {}
2 changes: 2 additions & 0 deletions config/controller.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ spec:
# If you are changing these names, you will also need to update
# the controller's Role in 200-role.yaml to include the new
# values in the "configmaps" "get" rule.
- name: CONFIG_DEFAULTS_NAME
value: config-defaults
- name: CONFIG_LOGGING_NAME
value: config-logging
- name: CONFIG_OBSERVABILITY_NAME
Expand Down
3 changes: 3 additions & 0 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ The example below customizes the following:
- the default `app.kubernetes.io/managed-by` label is applied to all Pods created to execute `TaskRuns`.
- the default Pod template to include a node selector to select the node where the Pod will be scheduled by default.
For more information, see [`PodTemplate` in `TaskRuns`](./taskruns.md#specifying-a-pod-template) or [`PodTemplate` in `PipelineRuns`](./pipelineruns.md#specifying-a-pod-template).
- the default `Workspace` configuration can be set for any `Workspaces` that a Task declares but that a TaskRun does not explicitly provide
```yaml
apiVersion: v1
Expand All @@ -278,6 +279,8 @@ data:
nodeSelector:
kops.k8s.io/instancegroup: build-instance-group
default-managed-by-label-value: "my-tekton-installation"
default-task-run-workspace-binding:
emptyDir: {}
```
**Note:** The `_example` key in the provided [config-defaults.yaml](./../config/config-defaults.yaml)
Expand Down
2 changes: 2 additions & 0 deletions docs/workspaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ Note the following:
start with the name of a directory. For example, a `mountPath` of `"/foobar"` is absolute and exposes
the `Workspace` at `/foobar` inside the `Task's` `Steps`, but a `mountPath` of `"foobar"` is relative and
exposes the `Workspace` at `/workspace/foobar`.
- A default `Workspace` configuration can be set for any `Workspaces` that a Task declares but that a TaskRun
does not explicitly provide. It can be set in the `config-defaults` ConfigMap in `default-task-run-workspace-binding`.

Below is an example `Task` definition that includes a `Workspace` called `messages` to which the `Task` writes a message:

Expand Down
45 changes: 45 additions & 0 deletions examples/v1beta1/taskruns/no-ci/default-workspaces.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: test-task
spec:
workspaces:
- name: source
steps:
- name: write-file
image: ubuntu
script: |
echo "Hello, world!" > /workspace/source/hello.txt || exit 0
- name: read-file
image: ubuntu
script: |
grep "Hello, world" /workspace/source/hello.txt
---
# Uses default workspace specified in config-defaults
apiVersion: tekton.dev/v1beta1
kind: TaskRun
metadata:
name: test-taskrun
spec:
taskRef:
name: test-task
---
apiVersion: v1
kind: ConfigMap
metadata:
name: my-configmap
data:
hello.txt: "Hello, world!"
---
# Uses provided workspace (not default)
apiVersion: tekton.dev/v1beta1
kind: TaskRun
metadata:
name: test-taskrun-configmap
spec:
workspaces:
- name: source
configMap:
name: my-configmap
taskRef:
name: test-task
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/tektoncd/pipeline
go 1.13

require (
cloud.google.com/go/storage v1.6.0
contrib.go.opencensus.io/exporter/stackdriver v0.13.1 // indirect
github.com/GoogleCloudPlatform/cloud-builders/gcs-fetcher v0.0.0-20191203181535-308b93ad1f39
github.com/aws/aws-sdk-go v1.30.16 // indirect
Expand All @@ -26,6 +27,7 @@ require (
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/text v0.3.3 // indirect
gomodules.xyz/jsonpatch/v2 v2.1.0
google.golang.org/api v0.20.0
google.golang.org/protobuf v1.22.0 // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
k8s.io/api v0.17.6
Expand Down
40 changes: 23 additions & 17 deletions pkg/apis/config/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,29 +28,31 @@ import (
)

const (
DefaultTimeoutMinutes = 60
NoTimeoutDuration = 0 * time.Minute
defaultTimeoutMinutesKey = "default-timeout-minutes"
defaultServiceAccountKey = "default-service-account"
defaultManagedByLabelValueKey = "default-managed-by-label-value"
DefaultManagedByLabelValue = "tekton-pipelines"
defaultPodTemplateKey = "default-pod-template"
defaultCloudEventsSinkKey = "default-cloud-events-sink"
DefaultCloudEventSinkValue = ""
DefaultTimeoutMinutes = 60
NoTimeoutDuration = 0 * time.Minute
defaultTimeoutMinutesKey = "default-timeout-minutes"
defaultServiceAccountKey = "default-service-account"
defaultManagedByLabelValueKey = "default-managed-by-label-value"
DefaultManagedByLabelValue = "tekton-pipelines"
defaultPodTemplateKey = "default-pod-template"
defaultCloudEventsSinkKey = "default-cloud-events-sink"
DefaultCloudEventSinkValue = ""
defaultTaskRunWorkspaceBinding = "default-task-run-workspace-binding"
)

// Defaults holds the default configurations
// +k8s:deepcopy-gen=true
type Defaults struct {
DefaultTimeoutMinutes int
DefaultServiceAccount string
DefaultManagedByLabelValue string
DefaultPodTemplate *pod.Template
DefaultCloudEventsSink string
DefaultTimeoutMinutes int
DefaultServiceAccount string
DefaultManagedByLabelValue string
DefaultPodTemplate *pod.Template
DefaultCloudEventsSink string
DefaultTaskRunWorkspaceBinding string
}

// GetBucketConfigName returns the name of the configmap containing all
// customizations for the storage bucket.
// GetDefaultsConfigName returns the name of the configmap containing all
// defined defaults.
func GetDefaultsConfigName() string {
if e := os.Getenv("CONFIG_DEFAULTS_NAME"); e != "" {
return e
Expand All @@ -72,7 +74,8 @@ func (cfg *Defaults) Equals(other *Defaults) bool {
other.DefaultServiceAccount == cfg.DefaultServiceAccount &&
other.DefaultManagedByLabelValue == cfg.DefaultManagedByLabelValue &&
other.DefaultPodTemplate.Equals(cfg.DefaultPodTemplate) &&
other.DefaultCloudEventsSink == cfg.DefaultCloudEventsSink
other.DefaultCloudEventsSink == cfg.DefaultCloudEventsSink &&
other.DefaultTaskRunWorkspaceBinding == cfg.DefaultTaskRunWorkspaceBinding
}

// NewDefaultsFromMap returns a Config given a map corresponding to a ConfigMap
Expand Down Expand Up @@ -111,6 +114,9 @@ func NewDefaultsFromMap(cfgMap map[string]string) (*Defaults, error) {
tc.DefaultCloudEventsSink = defaultCloudEventsSink
}

if bindingYAML, ok := cfgMap[defaultTaskRunWorkspaceBinding]; ok {
tc.DefaultTaskRunWorkspaceBinding = bindingYAML
}
return &tc, nil
}

Expand Down
20 changes: 20 additions & 0 deletions pkg/apis/config/default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,26 @@ func TestEquals(t *testing.T) {
},
expected: true,
},
{
name: "different default workspace",
left: &config.Defaults{
DefaultTaskRunWorkspaceBinding: "emptyDir: {}",
},
right: &config.Defaults{
DefaultTaskRunWorkspaceBinding: "source",
},
expected: false,
},
{
name: "same default workspace",
left: &config.Defaults{
DefaultTaskRunWorkspaceBinding: "emptyDir: {}",
},
right: &config.Defaults{
DefaultTaskRunWorkspaceBinding: "emptyDir: {}",
},
expected: true,
},
}

for _, tc := range testCases {
Expand Down
41 changes: 41 additions & 0 deletions pkg/reconciler/taskrun/taskrun.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import (
"strings"
"time"

"github.com/ghodss/yaml"
"github.com/hashicorp/go-multierror"
"github.com/tektoncd/pipeline/pkg/apis/config"
"github.com/tektoncd/pipeline/pkg/apis/pipeline"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"github.com/tektoncd/pipeline/pkg/apis/resource"
Expand Down Expand Up @@ -293,6 +295,12 @@ func (c *Reconciler) prepare(ctx context.Context, tr *v1beta1.TaskRun) (*v1beta1
return nil, nil, controller.NewPermanentError(err)
}

if err := c.updateTaskRunWithDefaultWorkspaces(ctx, tr, taskSpec); err != nil {
logger.Errorf("Failed to update taskrun %s with default workspace: %v", tr.Name, err)
tr.Status.MarkResourceFailed(podconvert.ReasonFailedResolution, err)
return nil, nil, controller.NewPermanentError(err)
}

if err := workspace.ValidateBindings(taskSpec.Workspaces, tr.Spec.Workspaces); err != nil {
logger.Errorf("TaskRun %q workspaces are invalid: %v", tr.Name, err)
tr.Status.MarkResourceFailed(podconvert.ReasonFailedValidation, err)
Expand Down Expand Up @@ -398,6 +406,39 @@ func (c *Reconciler) reconcile(ctx context.Context, tr *v1beta1.TaskRun,
return nil
}

func (c *Reconciler) updateTaskRunWithDefaultWorkspaces(ctx context.Context, tr *v1beta1.TaskRun, taskSpec *v1beta1.TaskSpec) error {
configMap := config.FromContextOrDefaults(ctx)
defaults := configMap.Defaults
if defaults.DefaultTaskRunWorkspaceBinding != "" {
var defaultWS v1beta1.WorkspaceBinding
if err := yaml.Unmarshal([]byte(defaults.DefaultTaskRunWorkspaceBinding), &defaultWS); err != nil {
return fmt.Errorf("failed to unmarshal %v", defaults.DefaultTaskRunWorkspaceBinding)
}
workspaceBindings := map[string]v1beta1.WorkspaceBinding{}
for _, tsWorkspace := range taskSpec.Workspaces {
workspaceBindings[tsWorkspace.Name] = v1beta1.WorkspaceBinding{
Name: tsWorkspace.Name,
SubPath: defaultWS.SubPath,
VolumeClaimTemplate: defaultWS.VolumeClaimTemplate,
PersistentVolumeClaim: defaultWS.PersistentVolumeClaim,
EmptyDir: defaultWS.EmptyDir,
ConfigMap: defaultWS.ConfigMap,
Secret: defaultWS.Secret,
}
}

for _, trWorkspace := range tr.Spec.Workspaces {
workspaceBindings[trWorkspace.Name] = trWorkspace
}

tr.Spec.Workspaces = []v1beta1.WorkspaceBinding{}
for _, wsBinding := range workspaceBindings {
tr.Spec.Workspaces = append(tr.Spec.Workspaces, wsBinding)
}
}
return nil
}

func (c *Reconciler) updateLabelsAndAnnotations(tr *v1beta1.TaskRun) (*v1beta1.TaskRun, error) {
newTr, err := c.taskRunLister.TaskRuns(tr.Namespace).Get(tr.Name)
if err != nil {
Expand Down
100 changes: 100 additions & 0 deletions pkg/reconciler/taskrun/taskrun_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2727,6 +2727,106 @@ func TestReconcileWorkspaceMissing(t *testing.T) {
}
}

// TestReconcileValidDefaultWorkspace tests a reconcile of a TaskRun that does
// not include a Workspace that the Task is expecting and it uses the default Workspace instead.
func TestReconcileValidDefaultWorkspace(t *testing.T) {
taskWithWorkspace := tb.Task("test-task-with-workspace", tb.TaskNamespace("foo"),
tb.TaskSpec(
tb.TaskWorkspace("ws1", "a test task workspace", "", true),
tb.Step("foo", tb.StepName("simple-step"), tb.StepCommand("/mycmd")),
))
taskRun := tb.TaskRun("test-taskrun-default-workspace", tb.TaskRunNamespace("foo"), tb.TaskRunSpec(
tb.TaskRunTaskRef(taskWithWorkspace.Name, tb.TaskRefAPIVersion("a1")),
))
d := test.Data{
Tasks: []*v1beta1.Task{taskWithWorkspace},
TaskRuns: []*v1beta1.TaskRun{taskRun},
ClusterTasks: nil,
PipelineResources: nil,
}

d.ConfigMaps = append(d.ConfigMaps, &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: config.GetDefaultsConfigName(), Namespace: system.GetNamespace()},
Data: map[string]string{
"default-task-run-workspace-binding": "emptyDir: {}",
},
})
names.TestingSeed()
testAssets, cancel := getTaskRunController(t, d)
defer cancel()
clients := testAssets.Clients

t.Logf("Creating SA %s in %s", "default", "foo")
if _, err := clients.Kube.CoreV1().ServiceAccounts("foo").Create(&corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: "foo",
},
}); err != nil {
t.Fatal(err)
}

if err := testAssets.Controller.Reconciler.Reconcile(context.Background(), getRunName(taskRun)); err != nil {
t.Errorf("Expected no error reconciling valid TaskRun but got %v", err)
}

tr, err := clients.Pipeline.TektonV1beta1().TaskRuns(taskRun.Namespace).Get(taskRun.Name, metav1.GetOptions{})
if err != nil {
t.Fatalf("Expected TaskRun %s to exist but instead got error when getting it: %v", taskRun.Name, err)
}

for _, c := range tr.Status.Conditions {
if c.Type == apis.ConditionSucceeded && c.Status == corev1.ConditionFalse && c.Reason == podconvert.ReasonFailedValidation {
t.Errorf("Expected TaskRun to pass Validation by using the default workspace but it did not. Final conditions were:\n%#v", tr.Status.Conditions)
}
}
}

// TestReconcileInvalidDefaultWorkspace tests a reconcile of a TaskRun that does
// not include a Workspace that the Task is expecting, and gets an error updating
// the TaskRun with an invalid default workspace.
func TestReconcileInvalidDefaultWorkspace(t *testing.T) {
taskWithWorkspace := tb.Task("test-task-with-workspace", tb.TaskNamespace("foo"),
tb.TaskSpec(
tb.TaskWorkspace("ws1", "a test task workspace", "", true),
tb.Step("foo", tb.StepName("simple-step"), tb.StepCommand("/mycmd")),
))
taskRun := tb.TaskRun("test-taskrun-default-workspace", tb.TaskRunNamespace("foo"), tb.TaskRunSpec(
tb.TaskRunTaskRef(taskWithWorkspace.Name, tb.TaskRefAPIVersion("a1")),
))
d := test.Data{
Tasks: []*v1beta1.Task{taskWithWorkspace},
TaskRuns: []*v1beta1.TaskRun{taskRun},
ClusterTasks: nil,
PipelineResources: nil,
}

d.ConfigMaps = append(d.ConfigMaps, &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: config.GetDefaultsConfigName(), Namespace: system.GetNamespace()},
Data: map[string]string{
"default-task-run-workspace-binding": "emptyDir == {}",
},
})
names.TestingSeed()
testAssets, cancel := getTaskRunController(t, d)
defer cancel()
clients := testAssets.Clients

t.Logf("Creating SA %s in %s", "default", "foo")
if _, err := clients.Kube.CoreV1().ServiceAccounts("foo").Create(&corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: "foo",
},
}); err != nil {
t.Fatal(err)
}

if err := testAssets.Controller.Reconciler.Reconcile(context.Background(), getRunName(taskRun)); err == nil {
t.Errorf("Expected error reconciling invalid TaskRun due to invalid workspace but got %v", err)
}
}

func TestReconcileTaskResourceResolutionAndValidation(t *testing.T) {
for _, tt := range []struct {
desc string
Expand Down
15 changes: 15 additions & 0 deletions pkg/workspace/apply.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
/*
Copyright 2020 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 workspace

import (
Expand Down

0 comments on commit 8ceca9b

Please sign in to comment.