From 3bb9075cd27c695891550154eeeb207e52569a76 Mon Sep 17 00:00:00 2001 From: Scott Date: Fri, 29 May 2020 10:07:23 -0400 Subject: [PATCH] Add Default TaskRun Workspace Bindings to config-default 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. Partially fixes #2398 --- config/config-defaults.yaml | 8 +++- config/controller.yaml | 2 + docs/install.md | 3 ++ docs/workspaces.md | 2 + .../taskruns/no-ci/default-workspaces.yaml | 45 +++++++++++++++++++ pkg/apis/config/default.go | 35 +++++++++------ pkg/apis/config/default_test.go | 20 +++++++++ pkg/reconciler/taskrun/taskrun.go | 36 +++++++++++++++ pkg/reconciler/taskrun/taskrun_test.go | 45 +++++++++++++++++++ pkg/workspace/apply.go | 15 +++++++ 10 files changed, 196 insertions(+), 15 deletions(-) create mode 100644 examples/v1beta1/taskruns/no-ci/default-workspaces.yaml diff --git a/config/config-defaults.yaml b/config/config-defaults.yaml index 1c23c72f20a..3cf0dabd699 100644 --- a/config/config-defaults.yaml +++ b/config/config-defaults.yaml @@ -54,4 +54,10 @@ data: # default-pod-template contains the default pod template to use # TaskRun and PipelineRun, if none is specified. If a pod template # is specified, the default pod template is ignored. - # default-pod-template: \ No newline at end of file + # default-pod-template: + + # 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: {} diff --git a/config/controller.yaml b/config/controller.yaml index 0ea463145ac..45dccbd153d 100644 --- a/config/controller.yaml +++ b/config/controller.yaml @@ -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 diff --git a/docs/install.md b/docs/install.md index 57d10df40fc..0506a04aabf 100644 --- a/docs/install.md +++ b/docs/install.md @@ -246,6 +246,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 @@ -259,6 +260,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) diff --git a/docs/workspaces.md b/docs/workspaces.md index 8699b3752f9..40bb1682038 100644 --- a/docs/workspaces.md +++ b/docs/workspaces.md @@ -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: diff --git a/examples/v1beta1/taskruns/no-ci/default-workspaces.yaml b/examples/v1beta1/taskruns/no-ci/default-workspaces.yaml new file mode 100644 index 00000000000..343ec4ddb62 --- /dev/null +++ b/examples/v1beta1/taskruns/no-ci/default-workspaces.yaml @@ -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 diff --git a/pkg/apis/config/default.go b/pkg/apis/config/default.go index 4f89ca4bd45..783f7a37fac 100644 --- a/pkg/apis/config/default.go +++ b/pkg/apis/config/default.go @@ -28,26 +28,28 @@ 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" + 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" + 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 + DefaultTimeoutMinutes int + DefaultServiceAccount string + DefaultManagedByLabelValue string + DefaultPodTemplate *pod.Template + 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 @@ -68,7 +70,8 @@ func (cfg *Defaults) Equals(other *Defaults) bool { return other.DefaultTimeoutMinutes == cfg.DefaultTimeoutMinutes && other.DefaultServiceAccount == cfg.DefaultServiceAccount && other.DefaultManagedByLabelValue == cfg.DefaultManagedByLabelValue && - other.DefaultPodTemplate.Equals(cfg.DefaultPodTemplate) + other.DefaultPodTemplate.Equals(cfg.DefaultPodTemplate) && + other.DefaultTaskRunWorkspaceBinding == cfg.DefaultTaskRunWorkspaceBinding } // NewDefaultsFromMap returns a Config given a map corresponding to a ConfigMap @@ -102,6 +105,10 @@ func NewDefaultsFromMap(cfgMap map[string]string) (*Defaults, error) { tc.DefaultPodTemplate = &podTemplate } + if bindingYAML, ok := cfgMap[defaultTaskRunWorkspaceBinding]; ok { + tc.DefaultTaskRunWorkspaceBinding = bindingYAML + } + return &tc, nil } diff --git a/pkg/apis/config/default_test.go b/pkg/apis/config/default_test.go index 5ad8c3929e3..fd936f28b8f 100644 --- a/pkg/apis/config/default_test.go +++ b/pkg/apis/config/default_test.go @@ -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 { diff --git a/pkg/reconciler/taskrun/taskrun.go b/pkg/reconciler/taskrun/taskrun.go index 24bea0d0ed4..782a9a411bb 100644 --- a/pkg/reconciler/taskrun/taskrun.go +++ b/pkg/reconciler/taskrun/taskrun.go @@ -25,7 +25,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" @@ -289,6 +291,7 @@ func (c *Reconciler) prepare(ctx context.Context, tr *v1beta1.TaskRun) (*v1beta1 return nil, nil, err } + c.updateTaskRunWithDefaultWorkspaces(ctx, tr, taskSpec) 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) @@ -393,6 +396,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 { diff --git a/pkg/reconciler/taskrun/taskrun_test.go b/pkg/reconciler/taskrun/taskrun_test.go index 224c250c6e1..d86043db0c8 100644 --- a/pkg/reconciler/taskrun/taskrun_test.go +++ b/pkg/reconciler/taskrun/taskrun_test.go @@ -2525,6 +2525,51 @@ func TestReconcileWorkspaceMissing(t *testing.T) { } } +// TestReconcileDefaultWorkspace tests a reconcile of a TaskRun that does +// not include a Workspace that the Task is expecting. +func TestReconcileDefaultWorkspace(t *testing.T) { + taskWithWorkspace := tb.Task("test-task-with-workspace", + tb.TaskSpec( + tb.TaskWorkspace("ws1", "a test task workspace", "", true), + ), tb.TaskNamespace("foo")) + taskRun := tb.TaskRun("test-taskrun-missing-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 + + 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) + } + } +} + func TestReconcileTaskResourceResolutionAndValidation(t *testing.T) { for _, tt := range []struct { desc string diff --git a/pkg/workspace/apply.go b/pkg/workspace/apply.go index 844470c503f..4d00a7a19ec 100644 --- a/pkg/workspace/apply.go +++ b/pkg/workspace/apply.go @@ -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 (