diff --git a/config/config-defaults.yaml b/config/config-defaults.yaml index 052f39c87a9..cfc623b342a 100644 --- a/config/config-defaults.yaml +++ b/config/config-defaults.yaml @@ -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: \ No newline at end of file + # 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: {} diff --git a/config/controller.yaml b/config/controller.yaml index 3c87c1e5fdb..2e90cbd97ce 100644 --- a/config/controller.yaml +++ b/config/controller.yaml @@ -84,6 +84,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 8f2db75ad3b..27403cf7147 100644 --- a/docs/install.md +++ b/docs/install.md @@ -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 @@ -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) diff --git a/docs/workspaces.md b/docs/workspaces.md index b722a003879..ea348079609 100644 --- a/docs/workspaces.md +++ b/docs/workspaces.md @@ -98,6 +98,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 04b30a10a82..d939de82cd5 100644 --- a/pkg/apis/config/default.go +++ b/pkg/apis/config/default.go @@ -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 @@ -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 @@ -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 } diff --git a/pkg/apis/config/default_test.go b/pkg/apis/config/default_test.go index 5ad8c3929e3..9eafd840bc1 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 690abcf6dd9..5b919cc09b5 100644 --- a/pkg/reconciler/taskrun/taskrun.go +++ b/pkg/reconciler/taskrun/taskrun.go @@ -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" @@ -288,6 +290,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) @@ -399,6 +407,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 d2757a9c6e5..964bb449b23 100644 --- a/pkg/reconciler/taskrun/taskrun_test.go +++ b/pkg/reconciler/taskrun/taskrun_test.go @@ -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 diff --git a/pkg/workspace/apply.go b/pkg/workspace/apply.go index 964848c774a..ca5579d143c 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 (