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 8ba332322..cb1f10491 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 @@ -283,6 +283,32 @@ backend_options: In order to enable this configuration you need to set the appropriate environment variables to `true` on the woodpecker agent: [WOODPECKER_BACKEND_K8S_POD_ANNOTATIONS_ALLOW_FROM_STEP](#backend_k8s_pod_annotations_allow_from_step) and/or [WOODPECKER_BACKEND_K8S_POD_LABELS_ALLOW_FROM_STEP](#backend_k8s_pod_labels_allow_from_step). +### Sidecars + +Sidecars allow you to run additional containers alongside your main step container. This is particularly useful for services like Docker-in-Docker (DinD), databases, or other dependencies that need to run during your pipeline step. + +#### Docker-in-Docker (DinD) Example + +Here's how to configure a sidecar for Docker-in-Docker functionality: + +```yaml +steps: + - name: build-with-docker + image: docker:cli + commands: + - docker build -t my-app . + - docker run --rm my-app + backend_options: + kubernetes: + sidecars: + - name: docker-in-docker + image: docker:dind + privileged: true + volumeMounts: + - name: docker-socket + mountPath: /var/run +``` + ## Tips and tricks ### CRI-O diff --git a/pipeline/backend/kubernetes/backend_options.go b/pipeline/backend/kubernetes/backend_options.go index 3b228fea3..ca1f33683 100644 --- a/pipeline/backend/kubernetes/backend_options.go +++ b/pipeline/backend/kubernetes/backend_options.go @@ -18,6 +18,7 @@ type BackendOptions struct { Tolerations []Toleration `mapstructure:"tolerations"` SecurityContext *SecurityContext `mapstructure:"securityContext"` Secrets []SecretRef `mapstructure:"secrets"` + Sidecars []Sidecar `mapstructure:"sidecars"` } // Resources defines two maps for kubernetes resource definitions. @@ -81,6 +82,21 @@ type SecretTarget struct { File string `mapstructure:"file"` } +type Sidecar struct { + Name string `json:"name"` + Image string `json:"image,omitempty"` + Pull bool `json:"pull,omitempty"` + Privileged bool `json:"privileged,omitempty"` + Environment map[string]string `json:"environment,omitempty"` + Commands []string `json:"commands,omitempty"` + VolumeMounts []VolumeMount `json:"volume_mounts,omitempty"` +} + +type VolumeMount struct { + Name string `json:"name"` + MountPath string `json:"mount_path"` +} + const ( SecProfileTypeRuntimeDefault SecProfileType = "RuntimeDefault" SecProfileTypeLocalhost SecProfileType = "Localhost" diff --git a/pipeline/backend/kubernetes/pod.go b/pipeline/backend/kubernetes/pod.go index c99419ebd..96bc44e77 100644 --- a/pipeline/backend/kubernetes/pod.go +++ b/pipeline/backend/kubernetes/pod.go @@ -66,6 +66,11 @@ func mkPod(step *types.Step, config *config, podName, goos string, options Backe } spec.Containers = append(spec.Containers, container) + for _, sidecarSpec := range options.Sidecars { + sidecarContainer := sidecarContainer(sidecarSpec, options) + spec.Containers = append(spec.Containers, sidecarContainer) + } + pod := &v1.Pod{ ObjectMeta: meta, Spec: spec, @@ -219,6 +224,7 @@ func podSpec(step *types.Step, config *config, options BackendOptions, nsp nativ } spec.Volumes = append(spec.Volumes, nsp.volumes...) + spec.Volumes = append(spec.Volumes, sidecarPodVolumes(options)...) return spec, nil } @@ -272,10 +278,33 @@ func podContainer(step *types.Step, podName, goos string, options BackendOptions container.EnvFrom = append(container.EnvFrom, nsp.envFromSources...) container.Env = append(container.Env, nsp.envVars...) container.VolumeMounts = append(container.VolumeMounts, nsp.mounts...) + container.VolumeMounts = append(container.VolumeMounts, sidecarVolumeMounts(flatSidecarVolumeMounts(options))...) return container, nil } +func sidecarContainer(sidecars Sidecar, options BackendOptions) v1.Container { + container := v1.Container{ + Name: sidecars.Name, + Image: sidecars.Image, + Command: sidecars.Commands, + Env: mapToEnvVars(sidecars.Environment), + SecurityContext: containerSecurityContext(options.SecurityContext, sidecars.Privileged), + } + + if sidecars.Pull { + container.ImagePullPolicy = v1.PullAlways + } + + if len(sidecars.Commands) > 0 { + container.Command = sidecars.Commands + } + + container.VolumeMounts = sidecarVolumeMounts(sidecars.VolumeMounts) + + return container +} + func mapToEnvVarsFromStepSecrets(secs []string, stepSecretName string) []v1.EnvVar { var ev []v1.EnvVar for _, key := range secs { @@ -335,6 +364,22 @@ func pvcVolume(name string) v1.Volume { } } +func sidecarPodVolumes(options BackendOptions) []v1.Volume { + var vols []v1.Volume + allContainerVolumes := flatSidecarVolumeMounts(options) + + for _, v := range allContainerVolumes { + vols = append(vols, v1.Volume{ + Name: v.Name, + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{}, + }, + }) + } + + return vols +} + func volumeMounts(volumes []string) ([]v1.VolumeMount, error) { var mounts []v1.VolumeMount @@ -357,6 +402,24 @@ func volumeMount(name, path string) v1.VolumeMount { } } +func sidecarVolumeMounts(sidecarVolumeMounts []VolumeMount) []v1.VolumeMount { + var mounts []v1.VolumeMount + + for _, v := range sidecarVolumeMounts { + mounts = append(mounts, volumeMount(v.Name, v.MountPath)) + } + + return mounts +} + +func flatSidecarVolumeMounts(options BackendOptions) []VolumeMount { + var allContainerVolumes []VolumeMount + for _, sidecar := range options.Sidecars { + allContainerVolumes = append(allContainerVolumes, sidecar.VolumeMounts...) + } + return allContainerVolumes +} + func containerPorts(ports []types.Port) []v1.ContainerPort { containerPorts := make([]v1.ContainerPort, len(ports)) for i, port := range ports { diff --git a/pipeline/backend/kubernetes/pod_test.go b/pipeline/backend/kubernetes/pod_test.go index 4a9d00db7..26a33da33 100644 --- a/pipeline/backend/kubernetes/pod_test.go +++ b/pipeline/backend/kubernetes/pod_test.go @@ -856,3 +856,90 @@ func TestStepSecret(t *testing.T) { ja := jsonassert.New(t) ja.Assertf(string(secretJSON), expected) } + +func TestSidecarPod(t *testing.T) { + const expected = ` + { + "metadata": { + "name": "wp-01he8bebctabr3kgk0qj36d2me-0", + "namespace": "woodpecker", + "labels": { + "step": "curl-google", + "woodpecker-ci.org/step": "curl-google", + "woodpecker-ci.org/task-uuid": "11301" + } + }, + "spec": { + "containers": [ + { + "name": "wp-01he8bebctabr3kgk0qj36d2me-0", + "image": "quay.io/curl/curl", + "command": [ + "/usr/bin/curl", + "-v", + "google.com" + ], + "resources": {}, + "volumeMounts": [ + { + "name": "dockersock", + "mountPath": "/var/run" + } + ] + }, + { + "name": "docker-in-docker", + "image": "docker:dind", + "resources": {}, + "volumeMounts": [ + { + "name": "dockersock", + "mountPath": "/var/run" + } + ], + "securityContext": { + "privileged": true + } + } + ], + "volumes": [ + { + "name": "dockersock", + "emptyDir": {} + } + ], + "restartPolicy": "Never" + }, + "status": {} + }` + + sidecarContainer := &Sidecar{ + Name: "docker-in-docker", + Image: "docker:dind", + Privileged: true, + VolumeMounts: []VolumeMount{ + { + Name: "dockersock", + MountPath: "/var/run", + }, + }, + } + + pod, err := mkPod(&types.Step{ + Name: "curl-google", + Image: "quay.io/curl/curl", + UUID: "01he8bebctabr3kgk0qj36d2me-0", + Entrypoint: []string{"/usr/bin/curl", "-v", "google.com"}, + }, &config{ + Namespace: "woodpecker", + }, "wp-01he8bebctabr3kgk0qj36d2me-0", "linux/amd64", BackendOptions{ + Sidecars: []Sidecar{*sidecarContainer}, + }, taskUUID) + assert.NoError(t, err) + + podJSON, err := json.Marshal(pod) + assert.NoError(t, err) + + ja := jsonassert.New(t) + ja.Assertf(string(podJSON), expected) +}