From c2b9aa064871c715135094b8c6fe3ae5b4ba0c91 Mon Sep 17 00:00:00 2001 From: Scott Date: Thu, 14 Jan 2021 11:42:16 -0500 Subject: [PATCH] Add Step and Sidecar Workspaces feature Access to Workspaces can now be limited to specific Steps and Sidecars in a Task. This allows Task authors to isolate sensitive data to specific images, reducing exposure of assets like credentials. As a side-effect of this change Workspaces are now mounted to Sidecars by default, just as they are to Steps. --- DEVELOPMENT.md | 3 +- docs/install.md | 1 + docs/workspaces.md | 74 +++++ .../alpha/isolated-workspaces.yaml | 53 +++ .../alpha/authenticating-git-commands.yaml | 201 +++++++++++ .../taskruns/alpha/workspace-in-sidecar.yaml | 39 +++ .../taskruns/alpha/workspace-isolation.yaml | 55 +++ hack/verify-codegen.sh | 2 +- .../pipeline/v1beta1/openapi_generated.go | 63 +++- pkg/apis/pipeline/v1beta1/swagger.json | 36 ++ pkg/apis/pipeline/v1beta1/task_types.go | 24 ++ pkg/apis/pipeline/v1beta1/task_validation.go | 42 +++ .../pipeline/v1beta1/task_validation_test.go | 203 ++++++++++++ pkg/apis/pipeline/v1beta1/workspace_types.go | 10 + .../pipeline/v1beta1/zz_generated.deepcopy.go | 26 ++ pkg/reconciler/taskrun/resources/apply.go | 45 ++- .../taskrun/resources/apply_test.go | 77 ++++- pkg/reconciler/taskrun/taskrun.go | 4 +- pkg/workspace/apply.go | 82 ++++- pkg/workspace/apply_test.go | 313 +++++++++++++++++- 20 files changed, 1337 insertions(+), 16 deletions(-) create mode 100644 examples/v1beta1/pipelineruns/alpha/isolated-workspaces.yaml create mode 100644 examples/v1beta1/taskruns/alpha/authenticating-git-commands.yaml create mode 100644 examples/v1beta1/taskruns/alpha/workspace-in-sidecar.yaml create mode 100644 examples/v1beta1/taskruns/alpha/workspace-isolation.yaml diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 8ddaabbc176..090a86531bb 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -258,13 +258,14 @@ While iterating on the project, you may need to: 1. Verify it's working by [looking at the logs](#accessing-logs) 1. Update your (external) dependencies with: `./hack/update-deps.sh`. 1. Update your type definitions with: `./hack/update-codegen.sh`. +1. Update your OpenAPI specs with: `./hack/update-openapigen.sh`. 1. [Add new CRD types](#adding-new-types) 1. [Add and run tests](./test/README.md#tests) To make changes to these CRDs, you will probably interact with: - The CRD type definitions in - [./pkg/apis/pipeline/alpha1](./pkg/apis/pipeline/v1alpha1) + [./pkg/apis/pipeline/v1beta1](./pkg/apis/pipeline/v1beta1) - The reconcilers in [./pkg/reconciler](./pkg/reconciler) - The clients are in [./pkg/client](./pkg/client) (these are generated by `./hack/update-codegen.sh`) diff --git a/docs/install.md b/docs/install.md index fe2dda9900c..fc401fbc023 100644 --- a/docs/install.md +++ b/docs/install.md @@ -384,6 +384,7 @@ Features currently in "alpha" are: - [Tekton Bundles](./taskruns.md#tekton-bundles) - [Custom Tasks](./runs.md) +- [Isolated Step & Sidecar Workspaces](./workspaces.md#isolated-workspaces) ## Configuring High Availability diff --git a/docs/workspaces.md b/docs/workspaces.md index 6e91e6c6512..9cb7ee09fb1 100644 --- a/docs/workspaces.md +++ b/docs/workspaces.md @@ -9,8 +9,12 @@ weight: 5 - [Overview](#overview) - [`Workspaces` in `Tasks` and `TaskRuns`](#workspaces-in-tasks-and-taskruns) - [`Workspaces` in `Pipelines` and `PipelineRuns`](#workspaces-in-pipelines-and-pipelineruns) + - [Optional `Workspaces`](#optional-workspaces) + - [Isolated `Workspaces`](#isolated-workspaces) - [Configuring `Workspaces`](#configuring-workspaces) - [Using `Workspaces` in `Tasks`](#using-workspaces-in-tasks) + - [Isolating `Workspaces` to Specific `Steps` or `Sidecars`](#isolating-workspaces-to-specific-steps-or-sidecars) + - [Setting a default `TaskRun` `Workspace Binding`](#setting-a-default-taskrun-workspace-binding) - [Using `Workspace` variables in `Tasks`](#using-workspace-variables-in-tasks) - [Mapping `Workspaces` in `Tasks` to `TaskRuns`](#mapping-workspaces-in-tasks-to-taskruns) - [Examples of `TaskRun` definition using `Workspaces`](#examples-of-taskrun-definition-using-workspaces) @@ -62,6 +66,9 @@ information in the `TaskRun` changes. configuration involved to add the required `volumeMount`. This allows for a long-running process in a `Sidecar` to share data with the executing `Steps` of a `Task`. +**Note**: If the `enable-api-fields` feature-flag is set to `"alpha"` then workspaces +will automatically be available to `Sidecars` too! + ### `Workspaces` in `Pipelines` and `PipelineRuns` A `Pipeline` can use `Workspaces` to show how storage will be shared through @@ -88,6 +95,22 @@ many uses: parameters used. - An optional build cache may be provided to speed up compile times. +See the section [Using `Workspaces` in `Tasks`](#using-workspaces-in-tasks) for more info on +the `optional` field. + +### Isolated `Workspaces` + +This is an alpha feature. The `enable-api-fields` feature flag [must be set to `"alpha"`](./install.md) +for Isolated Workspaces to function. + +Certain kinds of data are more sensitive than others. To reduce exposure of sensitive data Task +authors can isolate `Workspaces` to only those `Steps` and `Sidecars` that require access to +them. The primary use-case for this is credentials but it can apply to any data that should have +its access strictly limited to only specific container images. + +See the section [Isolating `Workspaces` to Specific `Steps` or `Sidecars`](#isolating-workspaces-to-specific-steps-or-sidecars) +for more info on this feature. + ## Configuring `Workspaces` This section describes how to configure one or more `Workspaces` in a `TaskRun`. @@ -166,6 +189,57 @@ spec: **Note:** `Sidecars` _must_ explicitly opt-in to receiving the `Workspace` volume. Injected `Sidecars` from non-Tekton sources will not receive access to `Workspaces`. +#### Isolating `Workspaces` to Specific `Steps` or `Sidecars` + +This is an alpha feature. The `enable-api-fields` feature flag [must be set to `"alpha"`](./install.md) +for Isolated Workspaces to function. + +To limit access to a `Workspace` from a subset of a `Task's` `Steps` or `Sidecars` requires +adding a `workspaces` declaration to those sections. In the following example a `Task` has several +`Steps` but only the one that performs a `git clone` will be able to access the SSH credentials +passed into it: + +```yaml +spec: + workspaces: + - name: ssh-credentials + description: An .ssh directory with keys, known_host and config files used to clone the repo. + steps: + - name: clone-repo + workspaces: + - name: ssh-credentials # This Step receives the sensitive workspace; the others do not. + image: git + script: # git clone ... + - name: build-source + image: third-party-source-builder:latest # This image doesn't get access to ssh-credentials. + - name: lint-source + image: third-party-source-linter:latest # This image doesn't get access to ssh-credentials. +``` + +It can potentially be useful to mount `Workspaces` to different locations on a per-`Step` or +per-`Sidecar` basis and this is also supported: + +```yaml +kind: Task +spec: + workspaces: + - name: ws + mountPath: /workspaces/ws + steps: + - name: edit-files-1 + workspaces: + - name: ws + mountPath: /foo # overrides mountPath + - name: edit-files-2 + workspaces: + - name: ws # no mountPath specified so will use /workspaces/ws + sidecars: + - name: watch-files-on-workspace + workspaces: + - name: ws + mountPath: /files # overrides mountPath +``` + #### Setting a default `TaskRun` `Workspace Binding` An organization may want to specify default `Workspace` configuration for `TaskRuns`. This allows users to diff --git a/examples/v1beta1/pipelineruns/alpha/isolated-workspaces.yaml b/examples/v1beta1/pipelineruns/alpha/isolated-workspaces.yaml new file mode 100644 index 00000000000..74dbdb53646 --- /dev/null +++ b/examples/v1beta1/pipelineruns/alpha/isolated-workspaces.yaml @@ -0,0 +1,53 @@ +# In this example a PipelineRun accepts a workspace +# to use as ssh credentials. The PipelineTask +# is configured so that only one of its Steps has access +# to those creds. +kind: Secret +apiVersion: v1 +metadata: + name: test-ssh-credentials +stringData: + id_rsa: | + -----BEGIN OPENSSH PRIVATE KEY----- + abcdefghijklmnopqrstuvwxy1234567890 + -----END OPENSSH PRIVATE KEY----- +--- +kind: PipelineRun +apiVersion: tekton.dev/v1beta1 +metadata: + generateName: isolated-workspaces- +spec: + timeout: 60s + workspaces: + - name: ssh-credentials + secret: + secretName: test-ssh-credentials + + pipelineSpec: + workspaces: + - name: ssh-credentials + + tasks: + - name: test-isolation + workspaces: + - name: creds + workspace: ssh-credentials + taskSpec: + workspaces: + - name: creds + steps: + - image: alpine:3.12.0 + script: | + if [ ! -d /creds ] ; then + echo "unable to access creds" + exit 1 + fi + workspaces: + - name: creds + mountpath: /creds + - image: alpine:3.12.0 + script: | + if [ -d /workspace/creds ] ; then + echo "this step should not be able to see creds" + exit 255 + fi diff --git a/examples/v1beta1/taskruns/alpha/authenticating-git-commands.yaml b/examples/v1beta1/taskruns/alpha/authenticating-git-commands.yaml new file mode 100644 index 00000000000..86385ea5c7e --- /dev/null +++ b/examples/v1beta1/taskruns/alpha/authenticating-git-commands.yaml @@ -0,0 +1,201 @@ +# This example demonstrates usage of creds-init credentials to issue +# git commands without a Git PipelineResource or git-clone catalog task. +# +# In order to exercise creds-init a sidecar is used to run a +# git server fronted by SSH. The sidecar does the following things: +# - Generates a host key pair, providing the public key to Steps for their known_hosts file +# - Accepts a public key generated from creds-init credentials and uses that for an authorized_keys file +# - Creates a bare git repo for the test git commands to run against +# - Starts sshd and tails its log, waiting for the git commands to come in over SSH +# +# Two separate Steps then perform authenticated git actions against the sidecar +# git server using the credentials mounted by Tekton's credential helper +# (aka "creds-init"): + +# The first step makes a git clone of the bare repository and populates it +# with a file. +# +# The second step makes a git clone of the populated repository and checks +# the contents of the repo match expectations. This step runs as a non-root +# user in order to exercise creds-init credentials when a securityContext +# is set. +# +# Notice that in each Step there is different code for handling creds-init +# credentials when the disable-home-env-overwrite flag is "false" and when +# it's "true". +apiVersion: v1 +kind: Secret +type: kubernetes.io/ssh-auth +metadata: + name: ssh-key-for-git + annotations: + tekton.dev/git-0: localhost +data: + # This key was generated for this test and isn't used for anything else. + ssh-privatekey: LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUJsd0FBQUFkemMyZ3RjbgpOaEFBQUFBd0VBQVFBQUFZRUF5T1g3ZG5OWlFBZVk4cHNMOXlaUnp3NXNDVG1yWGh6Zld1YTZuZ2VDQ0VRRTY4YjVUSThTCkNlbEhlNG9oTUtBdXZ0ZTE4YXJMK2EvVldpeFN6a2tBMmFIZVhkdUJ1bStkS2R2TlVVSUhNc1dOUythcENQYmE4R3ZGaHYKdG81Tkx0bWpxT2M0WjJkK1RPS3AvakMrS3pvUDFHQWdRL25QMitMTldabzlvTTc4TzQ3Z1dSem9FNlFKeGJqbFlPMHRMbwp4YXUxdTNrbUtsNSthbUxsNHpGN25wdmV1dGlSWDhmY2hGam5Ka2dqK3BVeFJvTGF4SDdDN0NTcDExWUkyMEhKRVFXeEk3CllaekNTYml5KzZ1a2l0Tk1MZ29qMnpSTGl2ZTVvZm9nenpYbkdWUUpZdUIzOFhQM0ZIQWMvOXhzUXdzd3dQS2hkQ3g4T0QKbjErYXpLOHp5SGhXK0dxckJhS1R4cDlrcVRpKzZSMWk4ZjVxOEt6NXpGVTZmd05qQXZ3STFBZ3IwS2FzU1JxWTVMcGxnTgpZcW1DY01JODZKUnRGWHRWWVQrT05tdWFhYUQ1QUErbnpkNW81R0haZTlFSlNqUThZMHZwbjhmNjNjeEw2RTdzVmxpMnpzCnNhN1RST2JMK3YyVnFuSlpEY2pIZXMzS1M5Mld0V3RJbXdXOG81VkRBQUFGaU04K0NUL1BQZ2svQUFBQUIzTnphQzF5YzIKRUFBQUdCQU1qbCszWnpXVUFIbVBLYkMvY21VYzhPYkFrNXExNGMzMXJtdXA0SGdnaEVCT3ZHK1V5UEVnbnBSM3VLSVRDZwpMcjdYdGZHcXkvbXYxVm9zVXM1SkFObWgzbDNiZ2Jwdm5TbmJ6VkZDQnpMRmpVdm1xUWoyMnZCcnhZYjdhT1RTN1pvNmpuCk9HZG5ma3ppcWY0d3ZpczZEOVJnSUVQNXo5dml6Vm1hUGFETy9EdU80RmtjNkJPa0NjVzQ1V0R0TFM2TVdydGJ0NUppcGUKZm1waTVlTXhlNTZiM3JyWWtWL0gzSVJZNXlaSUkvcVZNVWFDMnNSK3d1d2txZGRXQ050QnlSRUZzU08yR2N3a200c3Z1cgpwSXJUVEM0S0k5czBTNHIzdWFINklNODE1eGxVQ1dMZ2QvRno5eFJ3SFAvY2JFTUxNTUR5b1hRc2ZEZzU5Zm1zeXZNOGg0ClZ2aHFxd1dpazhhZlpLazR2dWtkWXZIK2F2Q3MrY3hWT244RFl3TDhDTlFJSzlDbXJFa2FtT1M2WllEV0twZ25EQ1BPaVUKYlJWN1ZXRS9qalpybW1tZytRQVBwODNlYU9SaDJYdlJDVW8wUEdOTDZaL0grdDNNUytoTzdGWll0czdMR3UwMFRteS9yOQpsYXB5V1EzSXgzck55a3ZkbHJWclNKc0Z2S09WUXdBQUFBTUJBQUVBQUFHQUNQSGtmbU9vWjZkdThlNWhYQUhDeHJ0WHFCCmwvUGROL1JtYmJqRW05U216czR5cWEwd1BUdzhrMU81VHM0V05nY1hMZFVRTlB6YkE4aWFWTGtvL0JqKzhiSFlhMmdmeVMKUE5qaWpXbXBOR09EWlF2Q0h2b095WUdpNjkycHovWnNTZCt0bEFzNm54LzY1ZjcwZHdVREJub0FjZnFLY28wQnVMRlNBKworamY5RnhISGYzQkFEUS9TdDVFQjlZelo1Q2F2cTRQcjZvS2w3R3RpbnRIbTZIbUlwTUlubWVEMnV3cjl2ZGZ1RGJhVDdYClVOSm10elVGck1uOUhlOWd1WkoyTXd3a015S09ScnRhVFA3VjFZK3FOM3ZncStmRkNtU0VkekxBU3BWTHMyL1hQTCtwTnAKTTVZUVRRMFJSZWdKTEdtTHZ0ZmpkK1RRMFQ0bjBucnBJVGRXRTRsL05sTG9taTVhUndzQXFtY2hZSGxhN0g3YlNyS2lKawpyWTg1RTliZm8wSXJqUDNQNzFYNmxjcTB0VDhDTklUQUNleWJQT3kwcDVDc3JwZTdhZEJBOXF4MTZjR2tkZ0NPWk9GMnRpCktoWWJHeTc4ei9YNEh2OEptVmhaSHF3RlFQQzVleWljbE1PTFJXNDJOcUJhNEVFc3RHT3l4MHZwa0lVS3VhRlJuQkFBQUEKd1FESytXYzU1WHVpWjgySXM5NnN2bWIrR0Y5c2pBRWVaWHpHSWpDL1NHVEhIWTZSQmc1TnlQOUdZNmtoWnBjd0cyTU52dQpZUjhuN0psRWlVanU2cjY2Smh0WGtvdTR4WlU1dDkvMlJvdHRmeWpKODJmYm8yTHZmNERUOVNvSURxZnk5VmlMSjhSWUNkCkt6NnpYSHFTZ1RBRU1vaUhjbFpIZzRqTitrOW1ma2tPMDBQbEJJaE1YU0ZMLzUrZUhGdStQTmxaU1g5NUlMRjJvZ0Y1RG0KWTFuaTRUOGJjdzY2dmFzamthcjFZekptM1VidEVnSzQ2VVllNGJac2NXbWt4dngwMEFBQURCQVA4UysyTmtheWkvb3NzVApTQXpJMi9QU2tJMDVEY1lTYnNOQjZ4a3pobzdKaDlHeUNvbW5xZ1IxR2ZBOTBqV3AxVks0TG43TmtYYWJaVmJPc0xoT21DCkdBbVRZTHRjaTB0bkhhYk5HTEZ3ZmdiUitqRzZNQ2p4cEh5anM5MDlKSHhtYmswbElpczdPN1N3VThERGcrSEVxc0EvNUoKQ1VMTWU3em9mNERhUnZXdFhTRks2ZW5LNnpGaHBINjVQY29TN0o0NjJhNzdUMDVGQXhMemNaRkc5VWZ5WUdMa1ZmdHRTTApNVDhudW9LaW5XTGNLSlVQeis1MjJlM3lIcis4c3pVUUFBQU1FQXlhQ28xSnRBcjRpczY0YTBuZTFUV0o0dXcyT3FDdUlDCm9acG1QN2UyRnh3bVRCSWMrbzZkSEVNVHo2c2ZZSkFxU2l4ZzYydXYzWlRTc25STWljaDZ0b1k0SVI4cWFMa1prLzU5cmEKQWFONFlvTkdpQTZxY0Jzc3NLMmZuM2YxRFJhckxPbWZHTnpTMU41S1RvSFVlUkVGWDExdHVNM1pqOGxTelFBOWZSakk1OQpFWmFnOWJaOXRJOEg5dmEvTGRMK3U3dTZZWkVRSEJCS1MxMW1tOVVXd1pDMkdUV3ZnNzRlTnRmemtZeDQxdlhIeTZBbW9ECmxuOHo2N3lvWEZzbEpUQUFBQURuTmpiM1IwUUcxbFkyZ3ViR0Z1QVFJREJBPT0KLS0tLS1FTkQgT1BFTlNTSCBQUklWQVRFIEtFWS0tLS0tCg== + # Note: we intentionally omit a known_hosts entry here. You should include + # one in your own Secrets as a security measure, otherwise the Git PipelineResource + # and git-clone Tasks will blindly accept any public key returned by a repository. + # + # We're able to omit known_hosts here because the file is generated by the + # git server sidecar. The benefit of omitting it here is that it exercises + # a codepath in Tekton that used to fail. In prior versions Tekton would + # run ssh-keyscan if known_hosts was omitted, which would fail for this example + # because the git server sidecar is not up and running at the time the scan + # would have happened. +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ssh-key-service-account +secrets: +- name: ssh-key-for-git +--- +apiVersion: tekton.dev/v1beta1 +kind: TaskRun +metadata: + name: authenticating-git-commands +spec: + serviceAccountName: ssh-key-service-account + workspaces: + - name: messages + emptyDir: {} + taskSpec: + workspaces: + # Both the Steps and Sidecar will receive this workspace. + - name: messages + mountPath: /messages + sidecars: + - name: server + image: alpine/git:v2.26.2 + securityContext: + runAsUser: 0 + script: | + #!/usr/bin/env ash + + # Generate a private host key and give the Steps access to its public + # key for their known_hosts file. + ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key + chmod 0600 /etc/ssh/ssh_host_rsa_key* + HOST_PUBLIC_KEY=$(cat /etc/ssh/ssh_host_rsa_key.pub | awk '{ print $2 }') + echo "localhost ssh-rsa $HOST_PUBLIC_KEY" > /messages/known_hosts + + # Wait for a Step to supply the server a public key generated from creds-init + # credentials. + while [ ! -f /messages/authorized_keys ] ; do + sleep 1 + done + + # Allow Steps to SSH login as root to this server. + mkdir /root/.ssh + cp /messages/authorized_keys /root/.ssh/ + + # "Unlock" the root account, allowing SSH login to succeed. + sed -i s/root:!/"root:*"/g /etc/shadow + + # Create the git repo we're going to test against. + cd /root/ + mkdir repo + cd repo + git init . --bare + + # Start the sshd server. + /usr/sbin/sshd -E /var/log/sshd + touch /messages/sshd-ready + tail -f /var/log/sshd + steps: + - name: setup + # This Step is only necessary as part of the test, it's not something you'll + # ever need in a real-world scenario involving an external git repo. + image: alpine/git:v2.26.2 + securityContext: + runAsUser: 0 + script: | + #!/usr/bin/env ash + + # Generate authorized_keys file from the creds-init private key and give + # it to the sidecar server so that Steps can successfully SSH login + # using creds-init credentials. + ssh-keygen -y -f $(credentials.path)/.ssh/id_ssh-key-for-git > /messages/authorized_keys + + # Wait for sshd to start on the git server. + while [ ! -f /messages/sshd-ready ] ; do + sleep 1 + done + - name: git-clone-and-push + image: alpine/git:v2.26.2 + securityContext: + runAsUser: 0 + workingDir: /root + script: | + #!/usr/bin/env ash + set -xe + + if [ -d /tekton/home/.ssh ] ; then + # When disable-home-env-overwrite is "false", creds-init credentials + # will be copied to /tekton/home/.ssh by the entrypoint. But we need + # them in /root/.ssh. + + # Overwrite the creds-init known_hosts file with that of our test + # git server. You wouldn't need to do this in any kind of real-world + # scenario involving an external git repo. + cp /messages/known_hosts $(credentials.path)/.ssh/ + + # Symlink /tekton/creds/.ssh to /root/.ssh because this script issues + # vanilla git commands of its own. Git PipelineResources and the git-clone + # catalog task handle this for you. + ln -s $(credentials.path)/.ssh /root/.ssh + else + # When disable-home-env-overwrite is "true", creds-init credentials + # will be copied to /root/.ssh by the entrypoint. We just need to + # overwrite the known_hosts file with that of our test git server. + cp /messages/known_hosts /root/.ssh/known_hosts + fi + + git clone root@localhost:/root/repo ./repo + cd repo + git config user.email "example@example.com" + git config user.name "Example" + echo "Hello, world!" > README + git add README + git commit -m "Test commit!" + git push origin master + - name: git-clone-and-check + image: gcr.io/tekton-releases/dogfooding/alpine-git-nonroot:latest + # Because this Step runs with a non-root security context, the creds-init + # credentials will fail to copy into /tekton/home. This happens because + # our previous step _already_ wrote to /tekton/home and ran as a root + # user. So there will be warning messages reporting "unsuccessful cred + # copy". These can be safely ignored and instead this Step will copy + # the credentials out of /tekton/creds to nonroot's HOME directory. + securityContext: + runAsUser: 1000 + workingDir: /home/nonroot + script: | + #!/usr/bin/env ash + set -xe + + if [ -d /tekton/home/.ssh ] ; then + # When disable-home-env-overwrite is "false", creds-init credentials + # will be copied to /tekton/home/.ssh by the entrypoint. But we need + # them in /home/nonroot/.ssh. + + # Overwrite the creds-init known_hosts file with that of our test + # git server. You wouldn't need to do this in any kind of real-world + # scenario involving an external git repo. + cp /messages/known_hosts $(credentials.path)/.ssh/ + + # Symlink /tekton/creds/.ssh to /home/nonroot/.ssh because this script issues + # vanilla git commands of its own and we're running as a non-root user. + # Git PipelineResources and the git-clone catalog task handle this for you. + ln -s $(credentials.path)/.ssh /home/nonroot/.ssh + else + # When disable-home-env-overwrite is "true", creds-init credentials + # will be copied to /home/nonroot/.ssh by the entrypoint. We just need to + # overwrite the known_hosts file with that of our test git server. + cp /messages/known_hosts /home/nonroot/ssh/known_hosts + fi + + git clone root@localhost:/root/repo ./repo + cd repo + cat README | grep "Hello, world!" diff --git a/examples/v1beta1/taskruns/alpha/workspace-in-sidecar.yaml b/examples/v1beta1/taskruns/alpha/workspace-in-sidecar.yaml new file mode 100644 index 00000000000..2d586c97cb4 --- /dev/null +++ b/examples/v1beta1/taskruns/alpha/workspace-in-sidecar.yaml @@ -0,0 +1,39 @@ +# This example TaskRun demonstrates sharing files between a Step +# and Sidecar via a Workspace. +# +# The Step writes a file to the Workspace and then waits for the Sidecar +# to respond. The Sidecar sees the Step's file and writes its response. +# The Step sees the response and exits. +kind: TaskRun +apiVersion: tekton.dev/v1beta1 +metadata: + generateName: workspace-in-sidecar- +spec: + timeout: 60s + workspaces: + - name: signals + emptyDir: {} + taskSpec: + workspaces: + - name: signals + steps: + - image: alpine:3.12.0 + script: | + #!/usr/bin/env ash + echo "foo" > "$(workspaces.signals.path)"/bar + echo "Wrote bar file" + while [ ! -f "$(workspaces.signals.path)"/ready ] ; do + echo "Waiting for $(workspaces.signals.path)/ready" + sleep 1 + done + echo "Saw ready file" + sidecars: + - image: alpine:3.12.0 + script: | + #!/usr/bin/env ash + while [ ! -f "$(workspaces.signals.path)"/bar ] ; do + echo "Waiting for $(workspaces.signals.path)/bar" + sleep 1 + done + touch "$(workspaces.signals.path)"/ready + echo "Wrote ready file" diff --git a/examples/v1beta1/taskruns/alpha/workspace-isolation.yaml b/examples/v1beta1/taskruns/alpha/workspace-isolation.yaml new file mode 100644 index 00000000000..8c53d7e821d --- /dev/null +++ b/examples/v1beta1/taskruns/alpha/workspace-isolation.yaml @@ -0,0 +1,55 @@ +# This example TaskRun demonstrates how to isolate a Workspace to specific Steps +# and Sidecars. +# +# The Step writes a file to the Workspace and then waits for the Sidecar +# to respond. The Sidecar sees the Step's file and writes its response. +# The Step sees the response and exits. +kind: TaskRun +apiVersion: tekton.dev/v1beta1 +metadata: + generateName: workspace-isolation- +spec: + timeout: 2m0s + workspaces: + - name: signals + emptyDir: {} + taskSpec: + workspaces: + - name: signals + steps: + - name: await-sidecar-signal + image: alpine:3.12.0 + workspaces: + - name: signals + script: | + #!/usr/bin/env ash + echo "hello world" > "$(workspaces.signals.path)"/start + echo "Signalled start" + # We protect from looping indefinitely by the TaskRun's timeout. + while [ ! -f "$(workspaces.signals.path)"/ready ] ; do + echo "Waiting for $(workspaces.signals.path)/ready" + sleep 1 + done + echo "Saw ready file" + - name: check-signals-access + image: alpine:3.12.0 + script: | + #!/usr/bin/env ash + if [ -f "$(workspaces.signals.path)"/start ] ; then + echo "This step should not have access to the signals workspace." + exit 255 + fi + sidecars: + - name: await-step-signal + image: alpine:3.12.0 + workspaces: + - name: signals + script: | + #!/usr/bin/env ash + # We protect from looping indefinitely by the TaskRun's timeout. + while [ ! -f "$(workspaces.signals.path)"/start ] ; do + echo "Waiting for $(workspaces.signals.path)/start" + sleep 1 + done + touch "$(workspaces.signals.path)"/ready + echo "Wrote ready file" diff --git a/hack/verify-codegen.sh b/hack/verify-codegen.sh index 2eeff57e645..6ca7413299a 100755 --- a/hack/verify-codegen.sh +++ b/hack/verify-codegen.sh @@ -52,6 +52,6 @@ if [[ $ret -eq 0 ]] then echo "${REPO_ROOT_DIR} up to date." else - echo "${REPO_ROOT_DIR} is out of date. Please run hack/update-codegen.sh" + echo "${REPO_ROOT_DIR} is out of date. Please run hack/update-codegen.sh and hack/update-openapigen.sh" exit 1 fi diff --git a/pkg/apis/pipeline/v1beta1/openapi_generated.go b/pkg/apis/pipeline/v1beta1/openapi_generated.go index 3e5d9f22cf7..134d8e3b6c2 100644 --- a/pkg/apis/pipeline/v1beta1/openapi_generated.go +++ b/pkg/apis/pipeline/v1beta1/openapi_generated.go @@ -99,6 +99,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.WorkspaceBinding": schema_pkg_apis_pipeline_v1beta1_WorkspaceBinding(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.WorkspaceDeclaration": schema_pkg_apis_pipeline_v1beta1_WorkspaceDeclaration(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.WorkspacePipelineTaskBinding": schema_pkg_apis_pipeline_v1beta1_WorkspacePipelineTaskBinding(ref), + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.WorkspaceUsage": schema_pkg_apis_pipeline_v1beta1_WorkspaceUsage(ref), "github.com/tektoncd/pipeline/pkg/apis/resource/v1alpha1.PipelineResource": schema_pkg_apis_resource_v1alpha1_PipelineResource(ref), "github.com/tektoncd/pipeline/pkg/apis/resource/v1alpha1.PipelineResourceList": schema_pkg_apis_resource_v1alpha1_PipelineResourceList(ref), "github.com/tektoncd/pipeline/pkg/apis/resource/v1alpha1.PipelineResourceSpec": schema_pkg_apis_resource_v1alpha1_PipelineResourceSpec(ref), @@ -2686,12 +2687,26 @@ func schema_pkg_apis_pipeline_v1beta1_Sidecar(ref common.ReferenceCallback) comm Format: "", }, }, + "Workspaces": { + SchemaProps: spec.SchemaProps{ + Description: "This is an alpha field. You must set the \"enable-api-fields\" feature flag to \"alpha\" for this field to be supported.\n\nWorkspaces is a list of workspaces from the Task that this Sidecar wants exclusive access to. Adding a workspace to this list means that any other Step or Sidecar that does not also request this Workspace will not have access to it.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.WorkspaceUsage"), + }, + }, + }, + }, + }, }, Required: []string{"name"}, }, }, Dependencies: []string{ - "k8s.io/api/core/v1.ContainerPort", "k8s.io/api/core/v1.EnvFromSource", "k8s.io/api/core/v1.EnvVar", "k8s.io/api/core/v1.Lifecycle", "k8s.io/api/core/v1.Probe", "k8s.io/api/core/v1.ResourceRequirements", "k8s.io/api/core/v1.SecurityContext", "k8s.io/api/core/v1.VolumeDevice", "k8s.io/api/core/v1.VolumeMount"}, + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.WorkspaceUsage", "k8s.io/api/core/v1.ContainerPort", "k8s.io/api/core/v1.EnvFromSource", "k8s.io/api/core/v1.EnvVar", "k8s.io/api/core/v1.Lifecycle", "k8s.io/api/core/v1.Probe", "k8s.io/api/core/v1.ResourceRequirements", "k8s.io/api/core/v1.SecurityContext", "k8s.io/api/core/v1.VolumeDevice", "k8s.io/api/core/v1.VolumeMount"}, } } @@ -3034,12 +3049,26 @@ func schema_pkg_apis_pipeline_v1beta1_Step(ref common.ReferenceCallback) common. Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Duration"), }, }, + "workspaces": { + SchemaProps: spec.SchemaProps{ + Description: "This is an alpha field. You must set the \"enable-api-fields\" feature flag to \"alpha\" for this field to be supported.\n\nWorkspaces is a list of workspaces from the Task that this Step wants exclusive access to. Adding a workspace to this list means that any other Step or Sidecar that does not also request this Workspace will not have access to it.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.WorkspaceUsage"), + }, + }, + }, + }, + }, }, Required: []string{"name"}, }, }, Dependencies: []string{ - "k8s.io/api/core/v1.ContainerPort", "k8s.io/api/core/v1.EnvFromSource", "k8s.io/api/core/v1.EnvVar", "k8s.io/api/core/v1.Lifecycle", "k8s.io/api/core/v1.Probe", "k8s.io/api/core/v1.ResourceRequirements", "k8s.io/api/core/v1.SecurityContext", "k8s.io/api/core/v1.VolumeDevice", "k8s.io/api/core/v1.VolumeMount", "k8s.io/apimachinery/pkg/apis/meta/v1.Duration"}, + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.WorkspaceUsage", "k8s.io/api/core/v1.ContainerPort", "k8s.io/api/core/v1.EnvFromSource", "k8s.io/api/core/v1.EnvVar", "k8s.io/api/core/v1.Lifecycle", "k8s.io/api/core/v1.Probe", "k8s.io/api/core/v1.ResourceRequirements", "k8s.io/api/core/v1.SecurityContext", "k8s.io/api/core/v1.VolumeDevice", "k8s.io/api/core/v1.VolumeMount", "k8s.io/apimachinery/pkg/apis/meta/v1.Duration"}, } } @@ -4356,6 +4385,36 @@ func schema_pkg_apis_pipeline_v1beta1_WorkspacePipelineTaskBinding(ref common.Re } } +func schema_pkg_apis_pipeline_v1beta1_WorkspaceUsage(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "WorkspaceUsage is used by a Step or Sidecar to declare that it wants isolated access to a Workspace defined in a Task.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "Name is the name of the workspace this Step or Sidecar wants access to.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "mountPath": { + SchemaProps: spec.SchemaProps{ + Description: "MountPath is the path that the workspace should be mounted to inside the Step or Sidecar, overriding any MountPath specified in the Task's WorkspaceDeclaration.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name", "mountPath"}, + }, + }, + } +} + func schema_pkg_apis_resource_v1alpha1_PipelineResource(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/pkg/apis/pipeline/v1beta1/swagger.json b/pkg/apis/pipeline/v1beta1/swagger.json index 2798797c9df..ec20a201170 100644 --- a/pkg/apis/pipeline/v1beta1/swagger.json +++ b/pkg/apis/pipeline/v1beta1/swagger.json @@ -1496,6 +1496,14 @@ "name" ], "properties": { + "Workspaces": { + "description": "This is an alpha field. You must set the \"enable-api-fields\" feature flag to \"alpha\" for this field to be supported.\n\nWorkspaces is a list of workspaces from the Task that this Sidecar wants exclusive access to. Adding a workspace to this list means that any other Step or Sidecar that does not also request this Workspace will not have access to it.", + "type": "array", + "items": { + "default": {}, + "$ref": "#/definitions/v1beta1.WorkspaceUsage" + } + }, "args": { "description": "Arguments to the entrypoint. The docker image's CMD is used if this is not provided. Variable references $(VAR_NAME) are expanded using the container's environment. If a variable cannot be resolved, the reference in the input string will be unchanged. The $(VAR_NAME) syntax can be escaped with a double $$, ie: $$(VAR_NAME). Escaped references will never be expanded, regardless of whether the variable exists or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell", "type": "array", @@ -1827,6 +1835,14 @@ "workingDir": { "description": "Container's working directory. If not specified, the container runtime's default will be used, which might be configured in the container image. Cannot be updated.", "type": "string" + }, + "workspaces": { + "description": "This is an alpha field. You must set the \"enable-api-fields\" feature flag to \"alpha\" for this field to be supported.\n\nWorkspaces is a list of workspaces from the Task that this Step wants exclusive access to. Adding a workspace to this list means that any other Step or Sidecar that does not also request this Workspace will not have access to it.", + "type": "array", + "items": { + "default": {}, + "$ref": "#/definitions/v1beta1.WorkspaceUsage" + } } } }, @@ -2578,6 +2594,26 @@ "default": "" } } + }, + "v1beta1.WorkspaceUsage": { + "description": "WorkspaceUsage is used by a Step or Sidecar to declare that it wants isolated access to a Workspace defined in a Task.", + "type": "object", + "required": [ + "name", + "mountPath" + ], + "properties": { + "mountPath": { + "description": "MountPath is the path that the workspace should be mounted to inside the Step or Sidecar, overriding any MountPath specified in the Task's WorkspaceDeclaration.", + "type": "string", + "default": "" + }, + "name": { + "description": "Name is the name of the workspace this Step or Sidecar wants access to.", + "type": "string", + "default": "" + } + } } } } diff --git a/pkg/apis/pipeline/v1beta1/task_types.go b/pkg/apis/pipeline/v1beta1/task_types.go index 85a9c299f2e..53e5d4f0c40 100644 --- a/pkg/apis/pipeline/v1beta1/task_types.go +++ b/pkg/apis/pipeline/v1beta1/task_types.go @@ -124,10 +124,23 @@ type Step struct { // Script is the contents of an executable file to execute. // // If Script is not empty, the Step cannot have an Command and the Args will be passed to the Script. + // +optional Script string `json:"script,omitempty"` + // Timeout is the time after which the step times out. Defaults to never. // Refer to Go's ParseDuration documentation for expected format: https://golang.org/pkg/time/#ParseDuration + // +optional Timeout *metav1.Duration `json:"timeout,omitempty"` + + // This is an alpha field. You must set the "enable-api-fields" feature flag to "alpha" + // for this field to be supported. + // + // Workspaces is a list of workspaces from the Task that this Step wants + // exclusive access to. Adding a workspace to this list means that any + // other Step or Sidecar that does not also request this Workspace will + // not have access to it. + // +optional + Workspaces []WorkspaceUsage `json:"workspaces,omitempty"` } // Sidecar has nearly the same data structure as Step, consisting of a Container and an optional Script, but does not have the ability to timeout. @@ -137,7 +150,18 @@ type Sidecar struct { // Script is the contents of an executable file to execute. // // If Script is not empty, the Step cannot have an Command or Args. + // +optional Script string `json:"script,omitempty"` + + // This is an alpha field. You must set the "enable-api-fields" feature flag to "alpha" + // for this field to be supported. + // + // Workspaces is a list of workspaces from the Task that this Sidecar wants + // exclusive access to. Adding a workspace to this list means that any + // other Step or Sidecar that does not also request this Workspace will + // not have access to it. + // +optional + Workspaces []WorkspaceUsage } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/pipeline/v1beta1/task_validation.go b/pkg/apis/pipeline/v1beta1/task_validation.go index c783903f336..16846842a92 100644 --- a/pkg/apis/pipeline/v1beta1/task_validation.go +++ b/pkg/apis/pipeline/v1beta1/task_validation.go @@ -23,6 +23,7 @@ import ( "strings" "time" + "github.com/tektoncd/pipeline/pkg/apis/config" "github.com/tektoncd/pipeline/pkg/apis/validate" "github.com/tektoncd/pipeline/pkg/substitution" corev1 "k8s.io/api/core/v1" @@ -44,6 +45,7 @@ func (ts *TaskSpec) Validate(ctx context.Context) (errs *apis.FieldError) { } errs = errs.Also(ValidateVolumes(ts.Volumes).ViaField("volumes")) errs = errs.Also(validateDeclaredWorkspaces(ts.Workspaces, ts.Steps, ts.StepTemplate).ViaField("workspaces")) + errs = errs.Also(validateWorkspaceUsages(ctx, ts)) mergedSteps, err := MergeStepsWithStepTemplate(ts.StepTemplate, ts.Steps) if err != nil { errs = errs.Also(&apis.FieldError{ @@ -110,6 +112,46 @@ func validateDeclaredWorkspaces(workspaces []WorkspaceDeclaration, steps []Step, return errs } +// validateWorkspaceUsages checks that all WorkspaceUsage objects in Steps +// refer to workspaces that are defined in the Task. +// +// This is an alpha feature and will fail validation if it's used by a step +// or sidecar when the enable-api-fields feature gate is anything but "alpha". +func validateWorkspaceUsages(ctx context.Context, ts *TaskSpec) (errs *apis.FieldError) { + workspaces := ts.Workspaces + steps := ts.Steps + sidecars := ts.Sidecars + + wsNames := sets.NewString() + for _, w := range workspaces { + wsNames.Insert(w.Name) + } + + for stepIdx, step := range steps { + if len(step.Workspaces) != 0 { + errs = errs.Also(ValidateEnabledAPIFields(ctx, "step workspaces", config.AlphaAPIFields).ViaIndex(stepIdx).ViaField("steps")) + } + for workspaceIdx, w := range step.Workspaces { + if !wsNames.Has(w.Name) { + errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("undefined workspace %q", w.Name), "name").ViaIndex(workspaceIdx).ViaField("workspaces").ViaIndex(stepIdx).ViaField("steps")) + } + } + } + + for sidecarIdx, sidecar := range sidecars { + if len(sidecar.Workspaces) != 0 { + errs = errs.Also(ValidateEnabledAPIFields(ctx, "sidecar workspaces", config.AlphaAPIFields).ViaIndex(sidecarIdx).ViaField("sidecars")) + } + for workspaceIdx, w := range sidecar.Workspaces { + if !wsNames.Has(w.Name) { + errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("undefined workspace %q", w.Name), "name").ViaIndex(workspaceIdx).ViaField("workspaces").ViaIndex(sidecarIdx).ViaField("sidecars")) + } + } + } + + return errs +} + func ValidateVolumes(volumes []corev1.Volume) (errs *apis.FieldError) { // Task must not have duplicate volume names. vols := sets.NewString() diff --git a/pkg/apis/pipeline/v1beta1/task_validation_test.go b/pkg/apis/pipeline/v1beta1/task_validation_test.go index d3eb72aaeb0..e0c4e7ff1f5 100644 --- a/pkg/apis/pipeline/v1beta1/task_validation_test.go +++ b/pkg/apis/pipeline/v1beta1/task_validation_test.go @@ -18,11 +18,13 @@ package v1beta1_test import ( "context" + "fmt" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/tektoncd/pipeline/pkg/apis/config" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "github.com/tektoncd/pipeline/test/diff" corev1 "k8s.io/api/core/v1" @@ -967,3 +969,204 @@ func TestTaskSpecValidateError(t *testing.T) { }) } } + +func TestStepAndSidecarWorkspaces(t *testing.T) { + type fields struct { + Steps []v1beta1.Step + Sidecars []v1beta1.Sidecar + Workspaces []v1beta1.WorkspaceDeclaration + } + tests := []struct { + name string + fields fields + }{{ + name: "valid step workspace usage", + fields: fields{ + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + Image: "my-image", + Args: []string{"arg"}, + }, + Workspaces: []v1beta1.WorkspaceUsage{{ + Name: "foo-workspace", + MountPath: "/a/custom/mountpath", + }}, + }}, + Workspaces: []v1beta1.WorkspaceDeclaration{{ + Name: "foo-workspace", + Description: "my great workspace", + MountPath: "some/path", + }}, + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := &v1beta1.TaskSpec{ + Steps: tt.fields.Steps, + Sidecars: tt.fields.Sidecars, + Workspaces: tt.fields.Workspaces, + } + featureFlags, _ := config.NewFeatureFlagsFromMap(map[string]string{ + "enable-api-fields": "alpha", + }) + cfg := &config.Config{ + FeatureFlags: featureFlags, + } + ctx := config.ToContext(context.Background(), cfg) + ts.SetDefaults(ctx) + if err := ts.Validate(ctx); err != nil { + t.Errorf("TaskSpec.Validate() = %v", err) + } + }) + } +} + +func TestStepAndSidecarWorkspacesErrors(t *testing.T) { + type fields struct { + Steps []v1beta1.Step + Sidecars []v1beta1.Sidecar + } + tests := []struct { + name string + fields fields + expectedError apis.FieldError + }{{ + name: "step workspace that refers to non-existent workspace declaration fails", + fields: fields{ + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + Image: "foo", + }, + Workspaces: []v1beta1.WorkspaceUsage{{ + Name: "foo", + }}, + }}, + }, + expectedError: apis.FieldError{ + Message: `undefined workspace "foo"`, + Paths: []string{"steps[0].workspaces[0].name"}, + }, + }, { + name: "sidecar workspace that refers to non-existent workspace declaration fails", + fields: fields{ + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + Image: "foo", + }, + }}, + Sidecars: []v1beta1.Sidecar{{ + Container: corev1.Container{ + Image: "foo", + }, + Workspaces: []v1beta1.WorkspaceUsage{{ + Name: "foo", + }}, + }}, + }, + expectedError: apis.FieldError{ + Message: `undefined workspace "foo"`, + Paths: []string{"sidecars[0].workspaces[0].name"}, + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := &v1beta1.TaskSpec{ + Steps: tt.fields.Steps, + Sidecars: tt.fields.Sidecars, + } + + featureFlags, _ := config.NewFeatureFlagsFromMap(map[string]string{ + "enable-api-fields": "alpha", + }) + cfg := &config.Config{ + FeatureFlags: featureFlags, + } + + ctx := config.ToContext(context.Background(), cfg) + ts.SetDefaults(ctx) + err := ts.Validate(ctx) + if err == nil { + t.Fatalf("Expected an error, got nothing for %v", ts) + } + + if d := cmp.Diff(tt.expectedError.Error(), err.Error(), cmpopts.IgnoreUnexported(apis.FieldError{})); d != "" { + t.Errorf("TaskSpec.Validate() errors diff %s", diff.PrintWantGot(d)) + } + }) + } +} + +// TestIncompatibleAPIVersions exercises validation of fields that +// require a specific feature gate version in order to work. +func TestIncompatibleAPIVersions(t *testing.T) { + tests := []struct { + name string + requiredVersion string + spec v1beta1.TaskSpec + }{{ + name: "step workspace requires alpha", + requiredVersion: "alpha", + spec: v1beta1.TaskSpec{ + Workspaces: []v1beta1.WorkspaceDeclaration{{ + Name: "foo", + }}, + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + Image: "foo", + }, + Workspaces: []v1beta1.WorkspaceUsage{{ + Name: "foo", + }}, + }}, + }, + }, { + name: "sidecar workspace requires alpha", + requiredVersion: "alpha", + spec: v1beta1.TaskSpec{ + Workspaces: []v1beta1.WorkspaceDeclaration{{ + Name: "foo", + }}, + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + Image: "foo", + }, + }}, + Sidecars: []v1beta1.Sidecar{{ + Container: corev1.Container{ + Image: "foo", + }, + Workspaces: []v1beta1.WorkspaceUsage{{ + Name: "foo", + }}, + }}, + }, + }} + versions := []string{"alpha", "stable"} + for _, tt := range tests { + for _, version := range versions { + testName := fmt.Sprintf("(using %s) %s", version, tt.name) + t.Run(testName, func(t *testing.T) { + ts := tt.spec + featureFlags, _ := config.NewFeatureFlagsFromMap(map[string]string{ + "enable-api-fields": version, + }) + cfg := &config.Config{ + FeatureFlags: featureFlags, + } + + ctx := config.ToContext(context.Background(), cfg) + + ts.SetDefaults(ctx) + err := ts.Validate(ctx) + + if tt.requiredVersion != version && err == nil { + t.Fatalf("no error received even though version required is %q while feature gate is %q", tt.requiredVersion, version) + } + + if tt.requiredVersion == version && err != nil { + t.Fatalf("error received despite required version and feature gate matching %q: %v", version, err) + } + }) + } + } +} diff --git a/pkg/apis/pipeline/v1beta1/workspace_types.go b/pkg/apis/pipeline/v1beta1/workspace_types.go index 814e4f841e4..8f45bfb36c1 100644 --- a/pkg/apis/pipeline/v1beta1/workspace_types.go +++ b/pkg/apis/pipeline/v1beta1/workspace_types.go @@ -111,3 +111,13 @@ type WorkspacePipelineTaskBinding struct { // +optional SubPath string `json:"subPath,omitempty"` } + +// WorkspaceUsage is used by a Step or Sidecar to declare that it wants isolated access +// to a Workspace defined in a Task. +type WorkspaceUsage struct { + // Name is the name of the workspace this Step or Sidecar wants access to. + Name string `json:"name"` + // MountPath is the path that the workspace should be mounted to inside the Step or Sidecar, + // overriding any MountPath specified in the Task's WorkspaceDeclaration. + MountPath string `json:"mountPath"` +} diff --git a/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go index 2905500acbe..e8ae972e0bd 100644 --- a/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go @@ -1188,6 +1188,11 @@ func (in *ResultRef) DeepCopy() *ResultRef { func (in *Sidecar) DeepCopyInto(out *Sidecar) { *out = *in in.Container.DeepCopyInto(&out.Container) + if in.Workspaces != nil { + in, out := &in.Workspaces, &out.Workspaces + *out = make([]WorkspaceUsage, len(*in)) + copy(*out, *in) + } return } @@ -1250,6 +1255,11 @@ func (in *Step) DeepCopyInto(out *Step) { *out = new(metav1.Duration) **out = **in } + if in.Workspaces != nil { + in, out := &in.Workspaces, &out.Workspaces + *out = make([]WorkspaceUsage, len(*in)) + copy(*out, *in) + } return } @@ -1923,3 +1933,19 @@ func (in *WorkspacePipelineTaskBinding) DeepCopy() *WorkspacePipelineTaskBinding in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkspaceUsage) DeepCopyInto(out *WorkspaceUsage) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkspaceUsage. +func (in *WorkspaceUsage) DeepCopy() *WorkspaceUsage { + if in == nil { + return nil + } + out := new(WorkspaceUsage) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/reconciler/taskrun/resources/apply.go b/pkg/reconciler/taskrun/resources/apply.go index 52da59addb5..36a71104763 100644 --- a/pkg/reconciler/taskrun/resources/apply.go +++ b/pkg/reconciler/taskrun/resources/apply.go @@ -17,12 +17,14 @@ limitations under the License. package resources import ( + "context" "fmt" "path/filepath" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/sets" + "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/substitution" @@ -112,7 +114,7 @@ func ApplyContexts(spec *v1beta1.TaskSpec, rtr *ResolvedTaskResources, tr *v1bet // ApplyWorkspaces applies the substitution from paths that the workspaces in declarations mounted to, the // volumes that bindings are realized with in the task spec and the PersistentVolumeClaim names for the // workspaces. -func ApplyWorkspaces(spec *v1beta1.TaskSpec, declarations []v1beta1.WorkspaceDeclaration, bindings []v1beta1.WorkspaceBinding, vols map[string]corev1.Volume) *v1beta1.TaskSpec { +func ApplyWorkspaces(ctx context.Context, spec *v1beta1.TaskSpec, declarations []v1beta1.WorkspaceDeclaration, bindings []v1beta1.WorkspaceBinding, vols map[string]corev1.Volume) *v1beta1.TaskSpec { stringReplacements := map[string]string{} bindNames := sets.NewString() @@ -127,7 +129,12 @@ func ApplyWorkspaces(spec *v1beta1.TaskSpec, declarations []v1beta1.WorkspaceDec stringReplacements[prefix+"path"] = "" } else { stringReplacements[prefix+"bound"] = "true" - stringReplacements[prefix+"path"] = declaration.GetMountPath() + alphaAPIEnabled := config.FromContextOrDefaults(ctx).FeatureFlags.EnableAPIFields == config.AlphaAPIFields + if alphaAPIEnabled { + spec = applyWorkspaceMountPath(prefix+"path", spec, declaration) + } else { + stringReplacements[prefix+"path"] = declaration.GetMountPath() + } } } @@ -144,6 +151,40 @@ func ApplyWorkspaces(spec *v1beta1.TaskSpec, declarations []v1beta1.WorkspaceDec return ApplyReplacements(spec, stringReplacements, map[string][]string{}) } +// applyWorkspaceMountPath accepts a workspace path variable of the form $(workspaces.foo.path) and replaces +// it in the fields of the TaskSpec. A new updated TaskSpec is returned. Steps or Sidecars in the TaskSpec +// that override the mountPath will receive that mountPath in place of the variable's value. Other Steps and +// Sidecars will see either the workspace's declared mountPath or the default of /workspaces/. +func applyWorkspaceMountPath(variable string, spec *v1beta1.TaskSpec, declaration v1beta1.WorkspaceDeclaration) *v1beta1.TaskSpec { + stringReplacements := map[string]string{variable: ""} + emptyArrayReplacements := map[string][]string{} + defaultMountPath := declaration.GetMountPath() + // Replace instances of the workspace path variable that are overridden per-Step + for i := range spec.Steps { + step := &spec.Steps[i] + for _, usage := range step.Workspaces { + if usage.Name == declaration.Name && usage.MountPath != "" { + stringReplacements[variable] = usage.MountPath + v1beta1.ApplyStepReplacements(step, stringReplacements, emptyArrayReplacements) + } + } + } + // Replace instances of the workspace path variable that are overridden per-Sidecar + for i := range spec.Sidecars { + sidecar := &spec.Sidecars[i] + for _, usage := range sidecar.Workspaces { + if usage.Name == declaration.Name && usage.MountPath != "" { + stringReplacements[variable] = usage.MountPath + v1beta1.ApplySidecarReplacements(sidecar, stringReplacements, emptyArrayReplacements) + } + } + } + // Replace any remaining instances of the workspace path variable, which should fall + // back to the mount path specified in the declaration. + stringReplacements[variable] = defaultMountPath + return ApplyReplacements(spec, stringReplacements, emptyArrayReplacements) +} + // ApplyTaskResults applies the substitution from values in results which are referenced in spec as subitems // of the replacementStr. func ApplyTaskResults(spec *v1beta1.TaskSpec) *v1beta1.TaskSpec { diff --git a/pkg/reconciler/taskrun/resources/apply_test.go b/pkg/reconciler/taskrun/resources/apply_test.go index 3e526c301e4..74bcf8b435d 100644 --- a/pkg/reconciler/taskrun/resources/apply_test.go +++ b/pkg/reconciler/taskrun/resources/apply_test.go @@ -17,9 +17,11 @@ limitations under the License. package resources_test import ( + "context" "testing" "github.com/google/go-cmp/cmp" + "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" @@ -788,7 +790,80 @@ func TestApplyWorkspaces(t *testing.T) { }} { t.Run(tc.name, func(t *testing.T) { vols := workspace.CreateVolumes(tc.binds) - got := resources.ApplyWorkspaces(tc.spec, tc.decls, tc.binds, vols) + got := resources.ApplyWorkspaces(context.Background(), tc.spec, tc.decls, tc.binds, vols) + if d := cmp.Diff(tc.want, got); d != "" { + t.Errorf("TestApplyWorkspaces() got diff %s", diff.PrintWantGot(d)) + } + }) + } +} + +func TestApplyWorkspaces_IsolatedWorkspaces(t *testing.T) { + for _, tc := range []struct { + name string + spec *v1beta1.TaskSpec + decls []v1beta1.WorkspaceDeclaration + binds []v1beta1.WorkspaceBinding + want *v1beta1.TaskSpec + }{{ + name: "step-workspace-with-custom-mountpath", + spec: &v1beta1.TaskSpec{Steps: []v1beta1.Step{{ + Script: `echo "$(workspaces.ws.path)"`, + Workspaces: []v1beta1.WorkspaceUsage{{ + Name: "ws", + MountPath: "/foo", + }}, + }, { + Script: `echo "$(workspaces.ws.path)"`, + }}, Sidecars: []v1beta1.Sidecar{{ + Script: `echo "$(workspaces.ws.path)"`, + }}}, + decls: []v1beta1.WorkspaceDeclaration{{ + Name: "ws", + }}, + want: &v1beta1.TaskSpec{Steps: []v1beta1.Step{{ + Script: `echo "/foo"`, + Workspaces: []v1beta1.WorkspaceUsage{{ + Name: "ws", + MountPath: "/foo", + }}, + }, { + Script: `echo "/workspace/ws"`, + }}, Sidecars: []v1beta1.Sidecar{{ + Script: `echo "/workspace/ws"`, + }}}, + }, { + name: "sidecar-workspace-with-custom-mountpath", + spec: &v1beta1.TaskSpec{Steps: []v1beta1.Step{{ + Script: `echo "$(workspaces.ws.path)"`, + }}, Sidecars: []v1beta1.Sidecar{{ + Script: `echo "$(workspaces.ws.path)"`, + Workspaces: []v1beta1.WorkspaceUsage{{ + Name: "ws", + MountPath: "/bar", + }}, + }}}, + decls: []v1beta1.WorkspaceDeclaration{{ + Name: "ws", + }}, + want: &v1beta1.TaskSpec{Steps: []v1beta1.Step{{ + Script: `echo "/workspace/ws"`, + }}, Sidecars: []v1beta1.Sidecar{{ + Script: `echo "/bar"`, + Workspaces: []v1beta1.WorkspaceUsage{{ + Name: "ws", + MountPath: "/bar", + }}, + }}}, + }} { + t.Run(tc.name, func(t *testing.T) { + ctx := config.ToContext(context.Background(), &config.Config{ + FeatureFlags: &config.FeatureFlags{ + EnableAPIFields: "alpha", + }, + }) + vols := workspace.CreateVolumes(tc.binds) + got := resources.ApplyWorkspaces(ctx, tc.spec, tc.decls, tc.binds, vols) if d := cmp.Diff(tc.want, got); d != "" { t.Errorf("TestApplyWorkspaces() got diff %s", diff.PrintWantGot(d)) } diff --git a/pkg/reconciler/taskrun/taskrun.go b/pkg/reconciler/taskrun/taskrun.go index 84d731a9d1a..42fef297560 100644 --- a/pkg/reconciler/taskrun/taskrun.go +++ b/pkg/reconciler/taskrun/taskrun.go @@ -662,12 +662,12 @@ func (c *Reconciler) createPod(ctx context.Context, tr *v1beta1.TaskRun, rtr *re workspaceVolumes := workspace.CreateVolumes(tr.Spec.Workspaces) // Apply workspace resource substitution - ts = resources.ApplyWorkspaces(ts, ts.Workspaces, tr.Spec.Workspaces, workspaceVolumes) + ts = resources.ApplyWorkspaces(ctx, ts, ts.Workspaces, tr.Spec.Workspaces, workspaceVolumes) // Apply task result substitution ts = resources.ApplyTaskResults(ts) - ts, err = workspace.Apply(*ts, tr.Spec.Workspaces, workspaceVolumes) + 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 diff --git a/pkg/workspace/apply.go b/pkg/workspace/apply.go index 002bf26e819..6af116a4943 100644 --- a/pkg/workspace/apply.go +++ b/pkg/workspace/apply.go @@ -16,8 +16,10 @@ limitations under the License. package workspace import ( + "context" "fmt" + "github.com/tektoncd/pipeline/pkg/apis/config" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "github.com/tektoncd/pipeline/pkg/names" corev1 "k8s.io/api/core/v1" @@ -82,10 +84,10 @@ func getDeclaredWorkspace(name string, w []v1beta1.WorkspaceDeclaration) (*v1bet return nil, fmt.Errorf("even though validation should have caught it, bound workspace %s did not exist in declared workspaces", name) } -// Apply will update the StepTemplate and Volumes declaration in ts so that the workspaces +// Apply will update the StepTemplate, Sidecars and Volumes declaration in ts so that the workspaces // specified through wb combined with the declared workspaces in ts will be available for -// all containers in the resulting pod. -func Apply(ts v1beta1.TaskSpec, wb []v1beta1.WorkspaceBinding, v map[string]corev1.Volume) (*v1beta1.TaskSpec, error) { +// all Step and Sidecar containers in the resulting pod. +func Apply(ctx context.Context, ts v1beta1.TaskSpec, wb []v1beta1.WorkspaceBinding, v map[string]corev1.Volume) (*v1beta1.TaskSpec, error) { // If there are no bound workspaces, we don't need to do anything if len(wb) == 0 { return &ts, nil @@ -98,6 +100,23 @@ func Apply(ts v1beta1.TaskSpec, wb []v1beta1.WorkspaceBinding, v map[string]core ts.StepTemplate = &corev1.Container{} } + isolatedWorkspaces := sets.NewString() + + alphaAPIEnabled := config.FromContextOrDefaults(ctx).FeatureFlags.EnableAPIFields == config.AlphaAPIFields + + if alphaAPIEnabled { + for _, step := range ts.Steps { + for _, workspaceUsage := range step.Workspaces { + isolatedWorkspaces.Insert(workspaceUsage.Name) + } + } + for _, sidecar := range ts.Sidecars { + for _, workspaceUsage := range sidecar.Workspaces { + isolatedWorkspaces.Insert(workspaceUsage.Name) + } + } + } + for i := range wb { w, err := getDeclaredWorkspace(wb[i].Name, ts.Workspaces) if err != nil { @@ -106,12 +125,23 @@ func Apply(ts v1beta1.TaskSpec, wb []v1beta1.WorkspaceBinding, v map[string]core // Get the volume we should be using for this binding vv := v[wb[i].Name] - ts.StepTemplate.VolumeMounts = append(ts.StepTemplate.VolumeMounts, corev1.VolumeMount{ + volumeMount := corev1.VolumeMount{ Name: vv.Name, MountPath: w.GetMountPath(), SubPath: wb[i].SubPath, ReadOnly: w.ReadOnly, - }) + } + + if alphaAPIEnabled { + if isolatedWorkspaces.Has(w.Name) { + mountAsIsolatedWorkspace(ts, w.Name, volumeMount) + } else { + mountAsSharedWorkspace(ts, volumeMount) + } + } else { + // Prior to the alpha feature gate only Steps may receive workspaces. + ts.StepTemplate.VolumeMounts = append(ts.StepTemplate.VolumeMounts, volumeMount) + } // Only add this volume if it hasn't already been added if !addedVolumes.Has(vv.Name) { @@ -121,3 +151,45 @@ func Apply(ts v1beta1.TaskSpec, wb []v1beta1.WorkspaceBinding, v map[string]core } return &ts, nil } + +// mountAsSharedWorkspace takes a volumeMount and adds it to all the steps and sidecars in +// a TaskSpec. +func mountAsSharedWorkspace(ts v1beta1.TaskSpec, volumeMount corev1.VolumeMount) { + ts.StepTemplate.VolumeMounts = append(ts.StepTemplate.VolumeMounts, volumeMount) + + for i := range ts.Sidecars { + sidecar := &ts.Sidecars[i] + sidecar.VolumeMounts = append(sidecar.VolumeMounts, volumeMount) + } +} + +// mountAsIsolatedWorkspace takes a volumeMount and adds it only to the steps and sidecars +// that have requested access to it. +func mountAsIsolatedWorkspace(ts v1beta1.TaskSpec, workspaceName string, volumeMount corev1.VolumeMount) { + for i := range ts.Steps { + step := &ts.Steps[i] + for _, workspaceUsage := range step.Workspaces { + if workspaceUsage.Name == workspaceName { + vm := volumeMount + if workspaceUsage.MountPath != "" { + vm.MountPath = workspaceUsage.MountPath + } + step.VolumeMounts = append(step.VolumeMounts, vm) + break + } + } + } + for i := range ts.Sidecars { + sidecar := &ts.Sidecars[i] + for _, workspaceUsage := range sidecar.Workspaces { + if workspaceUsage.Name == workspaceName { + vm := volumeMount + if workspaceUsage.MountPath != "" { + vm.MountPath = workspaceUsage.MountPath + } + sidecar.VolumeMounts = append(sidecar.VolumeMounts, vm) + break + } + } + } +} diff --git a/pkg/workspace/apply_test.go b/pkg/workspace/apply_test.go index 19291753551..66f518057e5 100644 --- a/pkg/workspace/apply_test.go +++ b/pkg/workspace/apply_test.go @@ -1,9 +1,11 @@ package workspace_test import ( + "context" "testing" "github.com/google/go-cmp/cmp" + "github.com/tektoncd/pipeline/pkg/apis/config" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "github.com/tektoncd/pipeline/pkg/workspace" "github.com/tektoncd/pipeline/test/diff" @@ -220,8 +222,8 @@ func TestApply(t *testing.T) { Name: "ws-9l9zj", MountPath: "/workspace/custom", SubPath: "/foo/bar/baz", - }, }}, + }, Volumes: []corev1.Volume{{ Name: "ws-9l9zj", VolumeSource: corev1.VolumeSource{ @@ -512,7 +514,7 @@ func TestApply(t *testing.T) { }} { t.Run(tc.name, func(t *testing.T) { vols := workspace.CreateVolumes(tc.workspaces) - ts, err := workspace.Apply(tc.ts, tc.workspaces, vols) + ts, err := workspace.Apply(context.Background(), tc.ts, tc.workspaces, vols) if err != nil { t.Fatalf("Did not expect error but got %v", err) } @@ -522,3 +524,310 @@ func TestApply(t *testing.T) { }) } } + +func TestApply_IsolatedWorkspaces(t *testing.T) { + names.TestingSeed() + for _, tc := range []struct { + name string + ts v1beta1.TaskSpec + workspaces []v1beta1.WorkspaceBinding + expectedTaskSpec v1beta1.TaskSpec + }{{ + name: "workspace isolated to step does not appear in step template or sidecars", + ts: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Workspaces: []v1beta1.WorkspaceUsage{{ + Name: "source", + }}, + }}, + Sidecars: []v1beta1.Sidecar{{Container: corev1.Container{Name: "foo"}}}, + Workspaces: []v1beta1.WorkspaceDeclaration{{ + Name: "source", + }}, + }, + workspaces: []v1beta1.WorkspaceBinding{{ + Name: "source", + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "testpvc", + }, + }}, + expectedTaskSpec: v1beta1.TaskSpec{ + StepTemplate: &corev1.Container{}, + Volumes: []corev1.Volume{{ + Name: "ws-9l9zj", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "testpvc", + }, + }, + }}, + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + VolumeMounts: []corev1.VolumeMount{{ + Name: "ws-9l9zj", + MountPath: "/workspace/source", + }}, + }, + Workspaces: []v1beta1.WorkspaceUsage{{ + Name: "source", + }}, + }}, + Sidecars: []v1beta1.Sidecar{{Container: corev1.Container{Name: "foo"}}}, + Workspaces: []v1beta1.WorkspaceDeclaration{{ + Name: "source", + }}, + }, + }, { + name: "workspace isolated to sidecar does not appear in steps", + ts: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Container: corev1.Container{Name: "step1"}, + }}, + Sidecars: []v1beta1.Sidecar{{ + Workspaces: []v1beta1.WorkspaceUsage{{ + Name: "source", + }}, + }}, + Workspaces: []v1beta1.WorkspaceDeclaration{{ + Name: "source", + }}, + }, + workspaces: []v1beta1.WorkspaceBinding{{ + Name: "source", + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "testpvc", + }, + }}, + expectedTaskSpec: v1beta1.TaskSpec{ + StepTemplate: &corev1.Container{}, + Volumes: []corev1.Volume{{ + Name: "ws-mz4c7", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "testpvc", + }, + }, + }}, + Steps: []v1beta1.Step{{ + Container: corev1.Container{Name: "step1"}, + }}, + Sidecars: []v1beta1.Sidecar{{ + Container: corev1.Container{ + VolumeMounts: []corev1.VolumeMount{{ + Name: "ws-mz4c7", + MountPath: "/workspace/source", + }}, + }, + Workspaces: []v1beta1.WorkspaceUsage{{ + Name: "source", + }}, + }}, + Workspaces: []v1beta1.WorkspaceDeclaration{{ + Name: "source", + }}, + }, + }, { + name: "workspace isolated to one step and one sidecar does not appear in step template", + ts: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Workspaces: []v1beta1.WorkspaceUsage{{ + Name: "source", + }}, + }, { + Container: corev1.Container{Name: "step2"}, + }}, + Sidecars: []v1beta1.Sidecar{{ + Workspaces: []v1beta1.WorkspaceUsage{{ + Name: "source", + }}, + }}, + Workspaces: []v1beta1.WorkspaceDeclaration{{ + Name: "source", + }}, + }, + workspaces: []v1beta1.WorkspaceBinding{{ + Name: "source", + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "testpvc", + }, + }}, + expectedTaskSpec: v1beta1.TaskSpec{ + StepTemplate: &corev1.Container{}, + Volumes: []corev1.Volume{{ + Name: "ws-mssqb", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "testpvc", + }, + }, + }}, + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + VolumeMounts: []corev1.VolumeMount{{ + Name: "ws-mssqb", + MountPath: "/workspace/source", + }}, + }, + Workspaces: []v1beta1.WorkspaceUsage{{ + Name: "source", + }}, + }, { + Container: corev1.Container{Name: "step2"}, + }}, + Sidecars: []v1beta1.Sidecar{{ + Container: corev1.Container{ + VolumeMounts: []corev1.VolumeMount{{ + Name: "ws-mssqb", + MountPath: "/workspace/source", + }}, + }, + Workspaces: []v1beta1.WorkspaceUsage{{ + Name: "source", + }}, + }}, + Workspaces: []v1beta1.WorkspaceDeclaration{{ + Name: "source", + }}, + }, + }, { + name: "workspaces are mounted to sidecars by default", + ts: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{}}, + Sidecars: []v1beta1.Sidecar{{}}, + Workspaces: []v1beta1.WorkspaceDeclaration{{ + Name: "source", + }}, + }, + workspaces: []v1beta1.WorkspaceBinding{{ + Name: "source", + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "testpvc", + }, + }}, + expectedTaskSpec: v1beta1.TaskSpec{ + StepTemplate: &corev1.Container{ + VolumeMounts: []corev1.VolumeMount{{ + Name: "ws-78c5n", MountPath: "/workspace/source", + }}, + }, + Volumes: []corev1.Volume{{ + Name: "ws-78c5n", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "testpvc", + }, + }, + }}, + Steps: []v1beta1.Step{{}}, + Sidecars: []v1beta1.Sidecar{{ + Container: corev1.Container{ + VolumeMounts: []corev1.VolumeMount{{ + Name: "ws-78c5n", + MountPath: "/workspace/source", + }}, + }, + }}, + Workspaces: []v1beta1.WorkspaceDeclaration{{ + Name: "source", + }}, + }, + }, { + name: "isolated workspaces custom mountpaths appear in volumemounts", + ts: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Workspaces: []v1beta1.WorkspaceUsage{{ + Name: "source", + MountPath: "/foo", + }}, + }}, + Sidecars: []v1beta1.Sidecar{{ + Workspaces: []v1beta1.WorkspaceUsage{{ + Name: "source", + MountPath: "/bar", + }}, + }}, + Workspaces: []v1beta1.WorkspaceDeclaration{{ + Name: "source", + }}, + }, + workspaces: []v1beta1.WorkspaceBinding{{ + Name: "source", + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "testpvc", + }, + }}, + expectedTaskSpec: v1beta1.TaskSpec{ + StepTemplate: &corev1.Container{}, + Volumes: []corev1.Volume{{ + Name: "ws-6nl7g", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "testpvc", + }, + }, + }}, + Steps: []v1beta1.Step{{ + Workspaces: []v1beta1.WorkspaceUsage{{ + Name: "source", + MountPath: "/foo", + }}, + Container: corev1.Container{ + VolumeMounts: []corev1.VolumeMount{{ + Name: "ws-6nl7g", + MountPath: "/foo", + }}, + }, + }}, + Sidecars: []v1beta1.Sidecar{{ + Workspaces: []v1beta1.WorkspaceUsage{{ + Name: "source", + MountPath: "/bar", + }}, + Container: corev1.Container{ + VolumeMounts: []corev1.VolumeMount{{ + Name: "ws-6nl7g", + MountPath: "/bar", + }}, + }, + }}, + Workspaces: []v1beta1.WorkspaceDeclaration{{ + Name: "source", + }}, + }, + }} { + 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 TestApplyWithMissingWorkspaceDeclaration(t *testing.T) { + names.TestingSeed() + ts := v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{}}, + Sidecars: []v1beta1.Sidecar{{}}, + Workspaces: []v1beta1.WorkspaceDeclaration{}, // Intentionally missing workspace declaration + } + bindings := []v1beta1.WorkspaceBinding{{ + Name: "source", + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "testpvc", + }, + }} + vols := workspace.CreateVolumes(bindings) + if _, err := workspace.Apply(context.Background(), ts, bindings, vols); err == nil { + t.Errorf("Expected error because workspace doesnt exist.") + } +}