diff --git a/docs/install.md b/docs/install.md index 009603025a4..eb37e379763 100644 --- a/docs/install.md +++ b/docs/install.md @@ -427,6 +427,7 @@ Features currently in "alpha" are: | [Isolated `Step` & `Sidecar` `Workspaces`](./workspaces.md#isolated-workspaces) | [TEP-0029](https://github.com/tektoncd/community/blob/main/teps/0029-step-workspaces.md) | [v0.24.0](https://github.com/tektoncd/pipeline/releases/tag/v0.24.0) | | | [Hermetic Execution Mode](./hermetic.md) | [TEP-0025](https://github.com/tektoncd/community/blob/main/teps/0025-hermekton.md) | [v0.25.0](https://github.com/tektoncd/pipeline/releases/tag/v0.25.0) | | | [Propagated `Parameters`](./taskruns.md#propagated-parameters) | [TEP-0107](https://github.com/tektoncd/community/blob/main/teps/0107-propagating-parameters.md) | [v0.36.0](https://github.com/tektoncd/pipeline/releases/tag/v0.36.0) | | +| [Propagated `Workspaces`](./pipelineruns.md#propagated-workspaces) | [TEP-0111](https://github.com/tektoncd/community/blob/main/teps/0111-propagating-workspaces.md) | | | | [Windows Scripts](./tasks.md#windows-scripts) | [TEP-0057](https://github.com/tektoncd/community/blob/main/teps/0057-windows-support.md) | [v0.28.0](https://github.com/tektoncd/pipeline/releases/tag/v0.28.0) | | | [Remote Tasks](./taskruns.md#remote-tasks) and [Remote Pipelines](./pipelineruns.md#remote-pipelines) | [TEP-0060](https://github.com/tektoncd/community/blob/main/teps/0060-remote-resolutiond.md) | | | | [Debug](./debug.md) | [TEP-0042](https://github.com/tektoncd/community/blob/main/teps/0042-taskrun-breakpoint-on-failure.md) | [v0.26.0](https://github.com/tektoncd/pipeline/releases/tag/v0.26.0) | | diff --git a/docs/taskruns.md b/docs/taskruns.md index b1807213e74..f0459f6eeeb 100644 --- a/docs/taskruns.md +++ b/docs/taskruns.md @@ -422,6 +422,57 @@ For more information, see the following topics: - For a list of supported `Volume` types, see [Specifying `VolumeSources` in `Workspaces`](workspaces.md#specifying-volumesources-in-workspaces). - For an end-to-end example, see [`Workspaces` in a `TaskRun`](../examples/v1beta1/taskruns/workspace.yaml). +#### Propagated Workspaces + +**([alpha only](https://github.com/tektoncd/pipeline/blob/main/docs/install.md#alpha-features))** + +When using an embedded spec, workspaces from the parent `TaskRun` will be +propagated to any inlined specs without needing to be explicitly defined. This +allows authors to simplify specs by automatically propagating top-level +workspaces down to other inlined resources. +**Workspace substutions will only be made for `commands`, `args` and `script` fields of `steps`, `stepTemplates`, and `sidecars`.** + +```yaml +apiVersion: tekton.dev/v1beta1 +kind: TaskRun +metadata: + generateName: propagating-workspaces- +spec: + taskSpec: + steps: + - name: simple-step + image: ubuntu + command: + - echo + args: + - $(workspaces.tr-workspace.path) + workspaces: + - emptyDir: {} + name: tr-workspace +``` + +Upon execution, the workspaces will be interpolated during resolution through to the taskspec. + +```yaml +apiVersion: tekton.dev/v1beta1 +kind: TaskRun +metadata: + name: propagating-workspaces-ndxnc + ... +spec: + ... +status: + ... + taskSpec: + steps: + ... + workspaces: + - name: tr-workspace + +``` + +Workspaces can be propagated to embedded task specs, not referenced Tasks. For more information, see [Referenced TaskRuns within Embedded PipelineRuns](pipelineruns.md#referenced-taskruns-within-embedded-pipelineruns). + ### Specifying `Sidecars` A `Sidecar` is a container that runs alongside the containers specified diff --git a/examples/v1beta1/taskruns/alpha/propagating_workspaces.yaml b/examples/v1beta1/taskruns/alpha/propagating_workspaces.yaml new file mode 100644 index 00000000000..804367a6663 --- /dev/null +++ b/examples/v1beta1/taskruns/alpha/propagating_workspaces.yaml @@ -0,0 +1,16 @@ +apiVersion: tekton.dev/v1beta1 +kind: TaskRun +metadata: + generateName: propagating-workspaces- +spec: + taskSpec: + steps: + - name: simple-step + image: ubuntu + command: + - echo + args: + - $(workspaces.tr-workspace.path) + workspaces: + - emptyDir: {} + name: tr-workspace diff --git a/pkg/apis/pipeline/v1beta1/pipelinerun_validation.go b/pkg/apis/pipeline/v1beta1/pipelinerun_validation.go index 13779aa9b4a..13b734e1013 100644 --- a/pkg/apis/pipeline/v1beta1/pipelinerun_validation.go +++ b/pkg/apis/pipeline/v1beta1/pipelinerun_validation.go @@ -143,7 +143,6 @@ func (ps *PipelineRunSpec) validatePipelineRunParameters(ctx context.Context) (e } } } - return errs } diff --git a/pkg/reconciler/pipelinerun/pipelinerun.go b/pkg/reconciler/pipelinerun/pipelinerun.go index c8f0fb5c9bb..e4d471f4f4c 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun.go +++ b/pkg/reconciler/pipelinerun/pipelinerun.go @@ -1020,6 +1020,7 @@ func getTaskrunWorkspaces(pr *v1beta1.PipelineRun, rpt *resources.ResolvedPipeli for _, binding := range pr.Spec.Workspaces { pipelineRunWorkspaces[binding.Name] = binding } + for _, ws := range rpt.PipelineTask.Workspaces { taskWorkspaceName, pipelineTaskSubPath, pipelineWorkspaceName := ws.Name, ws.SubPath, ws.Workspace diff --git a/pkg/reconciler/taskrun/taskrun.go b/pkg/reconciler/taskrun/taskrun.go index 0548542b4ca..916a2c30c51 100644 --- a/pkg/reconciler/taskrun/taskrun.go +++ b/pkg/reconciler/taskrun/taskrun.go @@ -388,7 +388,21 @@ func (c *Reconciler) prepare(ctx context.Context, tr *v1beta1.TaskRun) (*v1beta1 return nil, nil, controller.NewPermanentError(err) } - if err := workspace.ValidateBindings(ctx, taskSpec.Workspaces, tr.Spec.Workspaces); err != nil { + var workspaceDeclarations []v1beta1.WorkspaceDeclaration + // Propagating workspaces allows users to skip declarations + // In order to validate the workspace bindings we create declarations based on + // the workspaces provided in the task run spec. This logic is hidden behind the + // alpha feature gate since propagating workspaces is behind that feature gate. + if config.FromContextOrDefaults(ctx).FeatureFlags.EnableAPIFields == config.AlphaAPIFields { + for _, ws := range tr.Spec.Workspaces { + wspaceDeclaration := v1beta1.WorkspaceDeclaration{Name: ws.Name} + workspaceDeclarations = append(workspaceDeclarations, wspaceDeclaration) + } + workspaceDeclarations = append(workspaceDeclarations, taskSpec.Workspaces...) + } else { + workspaceDeclarations = taskSpec.Workspaces + } + if err := workspace.ValidateBindings(ctx, workspaceDeclarations, tr.Spec.Workspaces); err != nil { logger.Errorf("TaskRun %q workspaces are invalid: %v", tr.Name, err) tr.Status.MarkResourceFailed(podconvert.ReasonFailedValidation, err) return nil, nil, controller.NewPermanentError(err) @@ -427,13 +441,20 @@ func (c *Reconciler) reconcile(ctx context.Context, tr *v1beta1.TaskRun, rtr *re defer c.durationAndCountMetrics(ctx, tr) logger := logging.FromContext(ctx) recorder := controller.GetEventRecorder(ctx) + var err error - ts := updateTaskSpecParamsContextsResults(ctx, tr, rtr) + // Get the randomized volume names assigned to workspace bindings + workspaceVolumes := workspace.CreateVolumes(tr.Spec.Workspaces) + + ts, err := applyParamsContextsResultsAndWorkspaces(ctx, tr, rtr, workspaceVolumes) + if err != nil { + logger.Errorf("Error updating task spec parameters, contexts, results and workspaces: %s", err) + return err + } tr.Status.TaskSpec = ts // Get the TaskRun's Pod if it should have one. Otherwise, create the Pod. var pod *corev1.Pod - var err error if tr.Status.PodName != "" { pod, err = c.podLister.Pods(tr.Namespace).Get(tr.Status.PodName) @@ -479,7 +500,7 @@ func (c *Reconciler) reconcile(ctx context.Context, tr *v1beta1.TaskRun, rtr *re // This is used by createPod below. Changes to the Spec are not updated. tr.Spec.Workspaces = taskRunWorkspaces } - pod, err = c.createPod(ctx, ts, tr, rtr) + pod, err = c.createPod(ctx, ts, tr, rtr, workspaceVolumes) if err != nil { newErr := c.handlePodCreationError(tr, err) logger.Errorf("Failed to create task run pod for taskrun %q: %v", tr.Name, newErr) @@ -658,7 +679,7 @@ func (c *Reconciler) failTaskRun(ctx context.Context, tr *v1beta1.TaskRun, reaso // createPod creates a Pod based on the Task's configuration, with pvcName as a volumeMount // TODO(dibyom): Refactor resource setup/substitution logic to its own function in the resources package -func (c *Reconciler) createPod(ctx context.Context, ts *v1beta1.TaskSpec, tr *v1beta1.TaskRun, rtr *resources.ResolvedTaskResources) (*corev1.Pod, error) { +func (c *Reconciler) createPod(ctx context.Context, ts *v1beta1.TaskSpec, tr *v1beta1.TaskRun, rtr *resources.ResolvedTaskResources, workspaceVolumes map[string]corev1.Volume) (*corev1.Pod, error) { logger := logging.FromContext(ctx) inputResources, err := resourceImplBinding(rtr.Inputs, c.Images) if err != nil { @@ -694,12 +715,6 @@ func (c *Reconciler) createPod(ctx context.Context, ts *v1beta1.TaskSpec, tr *v1 ts = resources.ApplyResources(ts, inputResources, "inputs") ts = resources.ApplyResources(ts, outputResources, "outputs") - // Get the randomized volume names assigned to workspace bindings - workspaceVolumes := workspace.CreateVolumes(tr.Spec.Workspaces) - - // Apply workspace resource substitution - ts = resources.ApplyWorkspaces(ctx, ts, ts.Workspaces, tr.Spec.Workspaces, workspaceVolumes) - // By this time, params and workspaces should be propagated down so we can // validate that all parameter variables and workspaces used in the TaskSpec are declared by the Task. ctx = config.SkipValidationDueToPropagatedParametersAndWorkspaces(ctx, false) @@ -709,6 +724,7 @@ func (c *Reconciler) createPod(ctx context.Context, ts *v1beta1.TaskSpec, tr *v1 } ts, err = workspace.Apply(ctx, *ts, tr.Spec.Workspaces, workspaceVolumes) + if err != nil { logger.Errorf("Failed to create a pod for taskrun: %s due to workspace error %v", tr.Name, err) return nil, err @@ -750,7 +766,8 @@ func (c *Reconciler) createPod(ctx context.Context, ts *v1beta1.TaskSpec, tr *v1 return pod, err } -func updateTaskSpecParamsContextsResults(ctx context.Context, tr *v1beta1.TaskRun, rtr *resources.ResolvedTaskResources) *v1beta1.TaskSpec { +// applyParamsContextsResultsAndWorkspaces applies paramater, context, results and workspace substitutions to the TaskSpec. +func applyParamsContextsResultsAndWorkspaces(ctx context.Context, tr *v1beta1.TaskRun, rtr *resources.ResolvedTaskResources, workspaceVolumes map[string]corev1.Volume) (*v1beta1.TaskSpec, error) { ts := rtr.TaskSpec.DeepCopy() var defaults []v1beta1.ParamSpec if len(ts.Params) > 0 { @@ -768,7 +785,30 @@ func updateTaskSpecParamsContextsResults(ctx context.Context, tr *v1beta1.TaskRu // Apply step exitCode path substitution ts = resources.ApplyStepExitCodePath(ts) - return ts + // Apply workspace resource substitution + if config.FromContextOrDefaults(ctx).FeatureFlags.EnableAPIFields == config.AlphaAPIFields { + // propagate workspaces from taskrun to task. + twn := []string{} + for _, tw := range ts.Workspaces { + twn = append(twn, tw.Name) + } + + for _, trw := range tr.Spec.Workspaces { + skip := false + for _, tw := range twn { + if tw == trw.Name { + skip = true + break + } + } + if !skip { + ts.Workspaces = append(ts.Workspaces, v1beta1.WorkspaceDeclaration{Name: trw.Name}) + } + } + } + ts = resources.ApplyWorkspaces(ctx, ts, ts.Workspaces, tr.Spec.Workspaces, workspaceVolumes) + + return ts, nil } func isExceededResourceQuotaError(err error) bool { diff --git a/pkg/reconciler/taskrun/taskrun_test.go b/pkg/reconciler/taskrun/taskrun_test.go index bb7a771fa88..d74a79abd4b 100644 --- a/pkg/reconciler/taskrun/taskrun_test.go +++ b/pkg/reconciler/taskrun/taskrun_test.go @@ -44,6 +44,7 @@ import ( ttesting "github.com/tektoncd/pipeline/pkg/reconciler/testing" "github.com/tektoncd/pipeline/pkg/reconciler/volumeclaim" resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + "github.com/tektoncd/pipeline/pkg/workspace" "github.com/tektoncd/pipeline/test" "github.com/tektoncd/pipeline/test/diff" eventstest "github.com/tektoncd/pipeline/test/events" @@ -2467,6 +2468,50 @@ status: } } +func TestPropagatedWorkspaces(t *testing.T) { + taskRun := parse.MustParseTaskRun(t, ` +metadata: + name: test-taskrun-propagating-workspaces + namespace: foo +spec: + taskSpec: + steps: + - args: + - replacedArgs - $(workspaces.tr-workspace.path) + command: + - echo + image: foo + name: simple-step + workspaces: + - emptyDir: {} + name: tr-workspace +`) + d := test.Data{ + TaskRuns: []*v1beta1.TaskRun{taskRun}, + } + d.ConfigMaps = []*corev1.ConfigMap{{ + ObjectMeta: metav1.ObjectMeta{Namespace: system.Namespace(), Name: config.GetFeatureFlagsConfigName()}, + Data: map[string]string{ + "enable-api-fields": config.AlphaAPIFields, + }, + }} + testAssets, cancel := getTaskRunController(t, d) + defer cancel() + createServiceAccount(t, testAssets, "default", taskRun.Namespace) + c := testAssets.Controller + if err := c.Reconciler.Reconcile(testAssets.Ctx, getRunName(taskRun)); err == nil { + t.Fatalf("Could not reconcile the taskrun: %v", err) + } + getTaskRun, _ := testAssets.Clients.Pipeline.TektonV1beta1().TaskRuns(taskRun.Namespace).Get(testAssets.Ctx, taskRun.Name, metav1.GetOptions{}) + + want := []v1beta1.WorkspaceDeclaration{{ + Name: "tr-workspace", + }} + if c := cmp.Diff(want, getTaskRun.Status.TaskSpec.Workspaces); c != "" { + t.Errorf("TestPropagatedWorkspaces errored with: %s", diff.PrintWantGot(c)) + } +} + func TestExpandMountPath(t *testing.T) { expectedMountPath := "/temppath/replaced" expectedReplacedArgs := fmt.Sprintf("replacedArgs - %s", expectedMountPath) @@ -2545,8 +2590,14 @@ spec: Kind: "Task", TaskSpec: &v1beta1.TaskSpec{Steps: simpleTask.Spec.Steps, Workspaces: simpleTask.Spec.Workspaces}, } - taskSpec := updateTaskSpecParamsContextsResults(context.Background(), taskRun, rtr) - pod, err := r.createPod(testAssets.Ctx, taskSpec, taskRun, rtr) + ctx := config.EnableAlphaAPIFields(context.Background()) + workspaceVolumes := workspace.CreateVolumes(taskRun.Spec.Workspaces) + taskSpec, err := applyParamsContextsResultsAndWorkspaces(ctx, taskRun, rtr, workspaceVolumes) + if err != nil { + t.Fatalf("update task spec threw error %v", err) + } + + pod, err := r.createPod(testAssets.Ctx, taskSpec, taskRun, rtr, workspaceVolumes) if err != nil { t.Fatalf("create pod threw error %v", err) @@ -2649,8 +2700,13 @@ spec: TaskSpec: &v1beta1.TaskSpec{Steps: simpleTask.Spec.Steps, Workspaces: simpleTask.Spec.Workspaces}, } - taskSpec := updateTaskSpecParamsContextsResults(context.Background(), taskRun, rtr) - _, err := r.createPod(testAssets.Ctx, taskSpec, taskRun, rtr) + workspaceVolumes := workspace.CreateVolumes(taskRun.Spec.Workspaces) + ctx := config.EnableAlphaAPIFields(context.Background()) + taskSpec, err := applyParamsContextsResultsAndWorkspaces(ctx, taskRun, rtr, workspaceVolumes) + if err != nil { + t.Errorf("update task spec threw an error: %v", err) + } + _, err = r.createPod(testAssets.Ctx, taskSpec, taskRun, rtr, workspaceVolumes) if err == nil || err.Error() != expectedError { t.Errorf("Expected to fail validation for duplicate Workspace mount paths, error was %v", err) diff --git a/pkg/workspace/apply.go b/pkg/workspace/apply.go index 822be5d50af..897d8a161db 100644 --- a/pkg/workspace/apply.go +++ b/pkg/workspace/apply.go @@ -125,6 +125,19 @@ func Apply(ctx context.Context, ts v1beta1.TaskSpec, wb []v1beta1.WorkspaceBindi } for i := range wb { + if alphaAPIEnabled { + // Propagate missing Workspaces + addWorkspace := true + for _, ws := range ts.Workspaces { + if ws.Name == wb[i].Name { + addWorkspace = false + break + } + } + if addWorkspace { + ts.Workspaces = append(ts.Workspaces, v1beta1.WorkspaceDeclaration{Name: wb[i].Name}) + } + } w, err := getDeclaredWorkspace(wb[i].Name, ts.Workspaces) if err != nil { return nil, err diff --git a/pkg/workspace/apply_test.go b/pkg/workspace/apply_test.go index c93bd1f4567..8af82f90033 100644 --- a/pkg/workspace/apply_test.go +++ b/pkg/workspace/apply_test.go @@ -11,6 +11,7 @@ import ( "github.com/tektoncd/pipeline/test/diff" "github.com/tektoncd/pipeline/test/names" corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" ) func TestCreateVolumes(t *testing.T) { @@ -695,6 +696,67 @@ func TestApply(t *testing.T) { } } +func TestApply_PropagatedWorkspacesFromWorkspaceBindingToDeclarations(t *testing.T) { + names.TestingSeed() + for _, tc := range []struct { + name string + ts v1beta1.TaskSpec + workspaces []v1beta1.WorkspaceBinding + expectedTaskSpec v1beta1.TaskSpec + }{{ + name: "propagate workspaces", + ts: v1beta1.TaskSpec{ + Workspaces: []v1beta1.WorkspaceDeclaration{{ + Name: "workspace1", + }}, + }, + workspaces: []v1beta1.WorkspaceBinding{{ + Name: "workspace2", + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "mypvc", + }, + }}, + expectedTaskSpec: v1beta1.TaskSpec{ + Volumes: []corev1.Volume{{ + Name: "ws-9l9zj", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "mypvc", + }, + }, + }}, + Workspaces: []v1beta1.WorkspaceDeclaration{{ + Name: "workspace1", + MountPath: "", + ReadOnly: false, + }, { + Name: "workspace2", + MountPath: "", + ReadOnly: false, + }}, + StepTemplate: &v1beta1.StepTemplate{ + VolumeMounts: []v1.VolumeMount{{Name: "ws-9l9zj", MountPath: "/workspace/workspace2"}}, + }, + }, + }} { + t.Run(tc.name, func(t *testing.T) { + ctx := config.ToContext(context.Background(), &config.Config{ + FeatureFlags: &config.FeatureFlags{ + EnableAPIFields: "alpha", + }, + }) + vols := workspace.CreateVolumes(tc.workspaces) + ts, err := workspace.Apply(ctx, tc.ts, tc.workspaces, vols) + if err != nil { + t.Fatalf("Did not expect error but got %v", err) + } + if d := cmp.Diff(tc.expectedTaskSpec, *ts); d != "" { + t.Errorf("Didn't get expected TaskSpec modifications %s", diff.PrintWantGot(d)) + } + }) + } +} + func TestApply_IsolatedWorkspaces(t *testing.T) { names.TestingSeed() for _, tc := range []struct { diff --git a/pkg/workspace/validate_test.go b/pkg/workspace/validate_test.go index 0859e6950ae..ac5f08f6fac 100644 --- a/pkg/workspace/validate_test.go +++ b/pkg/workspace/validate_test.go @@ -142,6 +142,16 @@ func TestValidateBindingsInvalid(t *testing.T) { Name: "beth", PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{}, }}, + }, { + name: "Mismatch between declarations and bindings", + declarations: []v1beta1.WorkspaceDeclaration{{ + Name: "Notbeth", + Optional: true, + }}, + bindings: []v1beta1.WorkspaceBinding{{ + Name: "beth", + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }}, }} { t.Run(tc.name, func(t *testing.T) { if err := ValidateBindings(context.Background(), tc.declarations, tc.bindings); err == nil {