From b6880141e2c33f3031a795b579ffd89b7e89220b Mon Sep 17 00:00:00 2001 From: Julien Vincent Date: Tue, 26 May 2026 17:43:46 +0100 Subject: [PATCH] Allow disabling service workspace volumes in k8s (#6644) --- .../11-backends/20-kubernetes.md | 16 ++++++++ .../backend/kubernetes/backend_options.go | 1 + .../kubernetes/backend_options_test.go | 2 + pipeline/backend/kubernetes/pod.go | 25 +++++++++++- pipeline/backend/kubernetes/pod_test.go | 38 +++++++++++++++++++ 5 files changed, 80 insertions(+), 2 deletions(-) diff --git a/docs/docs/30-administration/10-configuration/11-backends/20-kubernetes.md b/docs/docs/30-administration/10-configuration/11-backends/20-kubernetes.md index a6f43a683a..e2796f5ba6 100644 --- a/docs/docs/30-administration/10-configuration/11-backends/20-kubernetes.md +++ b/docs/docs/30-administration/10-configuration/11-backends/20-kubernetes.md @@ -81,6 +81,22 @@ steps: To give steps access to the Kubernetes API via service account, take a look at [RBAC Authorization](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) +### Workspace volume + +`workspaceVolume` controls whether the default workspace volume is mounted into a service Pod. It only affects service +containers and does not disable explicitly configured service volumes. + +If unset, the default workspace volume is mounted. + +```yaml +services: + postgres: + image: postgres:16 + backend_options: + kubernetes: + workspaceVolume: false +``` + ### Node selector `nodeSelector` specifies the labels which are used to select the node on which the step will be executed. diff --git a/pipeline/backend/kubernetes/backend_options.go b/pipeline/backend/kubernetes/backend_options.go index 5f37f2690a..e0882e3bee 100644 --- a/pipeline/backend/kubernetes/backend_options.go +++ b/pipeline/backend/kubernetes/backend_options.go @@ -33,6 +33,7 @@ type BackendOptions struct { Affinity *kube_core_v1.Affinity `mapstructure:"affinity"` SecurityContext *SecurityContext `mapstructure:"securityContext"` Secrets []SecretRef `mapstructure:"secrets"` + WorkspaceVolume *bool `mapstructure:"workspaceVolume"` } // Resources defines two maps for kubernetes resource definitions. diff --git a/pipeline/backend/kubernetes/backend_options_test.go b/pipeline/backend/kubernetes/backend_options_test.go index f1d22cae27..7a228e85c8 100644 --- a/pipeline/backend/kubernetes/backend_options_test.go +++ b/pipeline/backend/kubernetes/backend_options_test.go @@ -48,6 +48,7 @@ func Test_parseBackendOptions(t *testing.T) { "kubernetes": map[string]any{ "nodeSelector": map[string]string{"storage": "ssd"}, "serviceAccountName": "wp-svc-acc", + "workspaceVolume": false, "labels": map[string]string{"app": "test"}, "annotations": map[string]string{"apps.kubernetes.io/pod-index": "0"}, "tolerations": []map[string]any{ @@ -107,6 +108,7 @@ func Test_parseBackendOptions(t *testing.T) { want: BackendOptions{ NodeSelector: map[string]string{"storage": "ssd"}, ServiceAccountName: "wp-svc-acc", + WorkspaceVolume: newBool(false), Labels: map[string]string{"app": "test"}, Annotations: map[string]string{"apps.kubernetes.io/pod-index": "0"}, Tolerations: []Toleration{{Key: "net-port", Value: "100Mbit", Effect: TaintEffectNoSchedule}}, diff --git a/pipeline/backend/kubernetes/pod.go b/pipeline/backend/kubernetes/pod.go index c77ccb94d2..06e1bd9d79 100644 --- a/pipeline/backend/kubernetes/pod.go +++ b/pipeline/backend/kubernetes/pod.go @@ -206,7 +206,7 @@ func podSpec(step *types.Step, config *config, options BackendOptions, nsp nativ spec.Tolerations = tolerations(config.PodTolerations) } - spec.Volumes, err = pvcVolumes(step.Volumes) + spec.Volumes, err = pvcVolumes(podVolumes(step, options)) if err != nil { return spec, err } @@ -278,7 +278,7 @@ func podContainer(step *types.Step, podName, goos string, options BackendOptions return container, err } - container.VolumeMounts, err = volumeMounts(step.Volumes) + container.VolumeMounts, err = volumeMounts(podVolumes(step, options)) if err != nil { return container, err } @@ -388,6 +388,27 @@ func pvcVolumes(volumes []string) ([]kube_core_v1.Volume, error) { return vols, nil } +func podVolumes(step *types.Step, options BackendOptions) []string { + if !isService(step) || useWorkspaceVolume(options) { + return step.Volumes + } + + volumes := make([]string, 0, len(step.Volumes)) + for _, volume := range step.Volumes { + if volumeMountPath(volume) != step.WorkspaceBase { + volumes = append(volumes, volume) + } + } + return volumes +} + +func useWorkspaceVolume(options BackendOptions) bool { + if options.WorkspaceVolume != nil { + return *options.WorkspaceVolume + } + return true +} + func pvcVolume(name string) kube_core_v1.Volume { pvcSource := kube_core_v1.PersistentVolumeClaimVolumeSource{ ClaimName: name, diff --git a/pipeline/backend/kubernetes/pod_test.go b/pipeline/backend/kubernetes/pod_test.go index 1aa17ac5c9..5b5a5c1edf 100644 --- a/pipeline/backend/kubernetes/pod_test.go +++ b/pipeline/backend/kubernetes/pod_test.go @@ -220,6 +220,44 @@ func TestTinyPod(t *testing.T) { ja.Assertf(string(podJSON), expected) } +func TestServiceWorkspaceVolume(t *testing.T) { + useWorkspaceVolume := true + disableWorkspaceVolume := false + step := &types.Step{ + Name: "postgres", + Image: "postgres:16", + UUID: "01he8bebctabr3kgk0qj36d2me-0", + Type: types.StepTypeService, + WorkingDir: "/woodpecker/src", + WorkspaceBase: "/woodpecker", + Environment: map[string]string{}, + Volumes: []string{"workspace:/woodpecker", "cache:/cache"}, + } + + pod, err := mkPod(step, &config{Namespace: "woodpecker"}, "wp-svc-postgres", "linux/amd64", BackendOptions{}, taskUUID) + assert.NoError(t, err) + assert.Len(t, pod.Spec.Volumes, 2) + assert.Equal(t, "workspace", pod.Spec.Volumes[0].Name) + assert.Equal(t, "/woodpecker", pod.Spec.Containers[0].VolumeMounts[0].MountPath) + assert.Equal(t, "cache", pod.Spec.Volumes[1].Name) + assert.Equal(t, "/cache", pod.Spec.Containers[0].VolumeMounts[1].MountPath) + + pod, err = mkPod(step, &config{Namespace: "woodpecker"}, "wp-svc-postgres", "linux/amd64", BackendOptions{WorkspaceVolume: &disableWorkspaceVolume}, taskUUID) + assert.NoError(t, err) + assert.Len(t, pod.Spec.Volumes, 1) + assert.Equal(t, "cache", pod.Spec.Volumes[0].Name) + assert.Len(t, pod.Spec.Containers[0].VolumeMounts, 1) + assert.Equal(t, "/cache", pod.Spec.Containers[0].VolumeMounts[0].MountPath) + + pod, err = mkPod(step, &config{Namespace: "woodpecker"}, "wp-svc-postgres", "linux/amd64", BackendOptions{WorkspaceVolume: &useWorkspaceVolume}, taskUUID) + assert.NoError(t, err) + assert.Len(t, pod.Spec.Volumes, 2) + assert.Equal(t, "workspace", pod.Spec.Volumes[0].Name) + assert.Equal(t, "/woodpecker", pod.Spec.Containers[0].VolumeMounts[0].MountPath) + assert.Equal(t, "cache", pod.Spec.Volumes[1].Name) + assert.Equal(t, "/cache", pod.Spec.Containers[0].VolumeMounts[1].MountPath) +} + func TestFullPod(t *testing.T) { const expected = ` {