From 2093cb4b9d1126179ec810b442c5d522985915a4 Mon Sep 17 00:00:00 2001 From: Thomas Anderson <127358482+zc-devs@users.noreply.github.com> Date: Sun, 14 Jan 2024 21:25:57 +0300 Subject: [PATCH] Secured kubernetes backend configuration --- pipeline/backend/kubernetes/kubernetes.go | 13 +++++ .../backend/kubernetes/kubernetes_test.go | 53 +++++++++++++++++++ pipeline/backend/kubernetes/pod.go | 22 ++++---- pipeline/backend/kubernetes/service.go | 9 ++-- pipeline/backend/kubernetes/service_test.go | 2 +- pipeline/backend/kubernetes/volume.go | 17 +++--- pipeline/backend/kubernetes/volume_test.go | 21 ++++++-- 7 files changed, 111 insertions(+), 26 deletions(-) create mode 100644 pipeline/backend/kubernetes/kubernetes_test.go diff --git a/pipeline/backend/kubernetes/kubernetes.go b/pipeline/backend/kubernetes/kubernetes.go index fc152504f6..8b8b57676a 100644 --- a/pipeline/backend/kubernetes/kubernetes.go +++ b/pipeline/backend/kubernetes/kubernetes.go @@ -18,8 +18,10 @@ import ( "context" "fmt" "io" + "maps" "os" "runtime" + "slices" "time" "github.com/rs/zerolog/log" @@ -155,6 +157,17 @@ func (e *kube) Load(ctx context.Context) (*types.BackendInfo, error) { }, nil } +func (e *kube) getConfig() *config { + if e.config == nil { + return nil + } + c := *e.config + c.PodLabels = maps.Clone(e.config.PodLabels) + c.PodAnnotations = maps.Clone(e.config.PodLabels) + c.ImagePullSecretNames = slices.Clone(e.config.ImagePullSecretNames) + return &c +} + // Setup the pipeline environment. func (e *kube) SetupWorkflow(ctx context.Context, conf *types.Config, taskUUID string) error { log.Trace().Str("taskUUID", taskUUID).Msgf("Setting up Kubernetes primitives") diff --git a/pipeline/backend/kubernetes/kubernetes_test.go b/pipeline/backend/kubernetes/kubernetes_test.go new file mode 100644 index 0000000000..5a0c3b75fc --- /dev/null +++ b/pipeline/backend/kubernetes/kubernetes_test.go @@ -0,0 +1,53 @@ +// Copyright 2024 Woodpecker Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package kubernetes + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGettingConfig(t *testing.T) { + engine := kube{ + config: &config{ + Namespace: "default", + StorageClass: "hdd", + VolumeSize: "1G", + StorageRwx: false, + PodLabels: map[string]string{"l1": "v1"}, + PodAnnotations: map[string]string{"a1": "v1"}, + ImagePullSecretNames: []string{"regcred"}, + SecurityContext: SecurityContextConfig{RunAsNonRoot: false}, + }, + } + config := engine.getConfig() + config.Namespace = "wp" + config.StorageClass = "ssd" + config.StorageRwx = true + config.PodLabels = nil + config.PodAnnotations["a2"] = "v2" + config.ImagePullSecretNames = append(config.ImagePullSecretNames, "docker.io") + config.SecurityContext.RunAsNonRoot = true + + assert.Equal(t, "default", engine.config.Namespace) + assert.Equal(t, "hdd", engine.config.StorageClass) + assert.Equal(t, "1G", engine.config.VolumeSize) + assert.Equal(t, false, engine.config.StorageRwx) + assert.Equal(t, 1, len(engine.config.PodLabels)) + assert.Equal(t, 1, len(engine.config.PodAnnotations)) + assert.Equal(t, 1, len(engine.config.ImagePullSecretNames)) + assert.Equal(t, false, engine.config.SecurityContext.RunAsNonRoot) +} diff --git a/pipeline/backend/kubernetes/pod.go b/pipeline/backend/kubernetes/pod.go index dbee57abd6..92f02db4fb 100644 --- a/pipeline/backend/kubernetes/pod.go +++ b/pipeline/backend/kubernetes/pod.go @@ -74,15 +74,16 @@ func podMeta(step *types.Step, config *config, podName string) metav1.ObjectMeta Namespace: config.Namespace, } - labels := make(map[string]string, len(config.PodLabels)+1) - // copy to not alter the engine config - maps.Copy(labels, config.PodLabels) - labels[StepLabel] = step.Name - meta.Labels = labels + meta.Labels = config.PodLabels + if meta.Labels == nil { + meta.Labels = make(map[string]string, 1) + } + meta.Labels[StepLabel] = step.Name - // copy to not alter the engine config - meta.Annotations = make(map[string]string, len(config.PodAnnotations)) - maps.Copy(meta.Annotations, config.PodAnnotations) + meta.Annotations = config.PodAnnotations + if meta.Annotations == nil { + meta.Annotations = make(map[string]string) + } securityContext := step.BackendOptions.Kubernetes.SecurityContext if securityContext != nil { @@ -442,13 +443,14 @@ func startPod(ctx context.Context, engine *kube, step *types.Step) (*v1.Pod, err if err != nil { return nil, err } - pod, err := mkPod(step, engine.config, podName, engine.goos) + engineConfig := engine.getConfig() + pod, err := mkPod(step, engineConfig, podName, engine.goos) if err != nil { return nil, err } log.Trace().Msgf("creating pod: %s", pod.Name) - return engine.client.CoreV1().Pods(engine.config.Namespace).Create(ctx, pod, metav1.CreateOptions{}) + return engine.client.CoreV1().Pods(engineConfig.Namespace).Create(ctx, pod, metav1.CreateOptions{}) } func stopPod(ctx context.Context, engine *kube, step *types.Step, deleteOpts metav1.DeleteOptions) error { diff --git a/pipeline/backend/kubernetes/service.go b/pipeline/backend/kubernetes/service.go index ed440deeeb..6d3d54df02 100644 --- a/pipeline/backend/kubernetes/service.go +++ b/pipeline/backend/kubernetes/service.go @@ -32,7 +32,7 @@ const ( ServiceLabel = "service" ) -func mkService(step *types.Step, namespace string) (*v1.Service, error) { +func mkService(step *types.Step, config *config) (*v1.Service, error) { name, err := serviceName(step) if err != nil { return nil, err @@ -51,7 +51,7 @@ func mkService(step *types.Step, namespace string) (*v1.Service, error) { return &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: name, - Namespace: namespace, + Namespace: config.Namespace, }, Spec: v1.ServiceSpec{ Type: v1.ServiceTypeClusterIP, @@ -77,13 +77,14 @@ func servicePort(port types.Port) v1.ServicePort { } func startService(ctx context.Context, engine *kube, step *types.Step) (*v1.Service, error) { - svc, err := mkService(step, engine.config.Namespace) + engineConfig := engine.getConfig() + svc, err := mkService(step, engineConfig) if err != nil { return nil, err } log.Trace().Str("name", svc.Name).Interface("selector", svc.Spec.Selector).Interface("ports", svc.Spec.Ports).Msg("creating service") - return engine.client.CoreV1().Services(engine.config.Namespace).Create(ctx, svc, metav1.CreateOptions{}) + return engine.client.CoreV1().Services(engineConfig.Namespace).Create(ctx, svc, metav1.CreateOptions{}) } func stopService(ctx context.Context, engine *kube, step *types.Step, deleteOpts metav1.DeleteOptions) error { diff --git a/pipeline/backend/kubernetes/service_test.go b/pipeline/backend/kubernetes/service_test.go index a176ad1dca..0380f11019 100644 --- a/pipeline/backend/kubernetes/service_test.go +++ b/pipeline/backend/kubernetes/service_test.go @@ -82,7 +82,7 @@ func TestService(t *testing.T) { s, err := mkService(&types.Step{ Name: "bar", Ports: ports, - }, "foo") + }, &config{Namespace: "foo"}) assert.NoError(t, err) j, err := json.Marshal(s) assert.NoError(t, err) diff --git a/pipeline/backend/kubernetes/volume.go b/pipeline/backend/kubernetes/volume.go index 0f6c7b9324..ab08691796 100644 --- a/pipeline/backend/kubernetes/volume.go +++ b/pipeline/backend/kubernetes/volume.go @@ -25,15 +25,15 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func mkPersistentVolumeClaim(namespace, name, storageClass, size string, storageRwx bool) (*v1.PersistentVolumeClaim, error) { - _storageClass := &storageClass - if storageClass == "" { +func mkPersistentVolumeClaim(config *config, name string) (*v1.PersistentVolumeClaim, error) { + _storageClass := &config.StorageClass + if config.StorageClass == "" { _storageClass = nil } var accessMode v1.PersistentVolumeAccessMode - if storageRwx { + if config.StorageRwx { accessMode = v1.ReadWriteMany } else { accessMode = v1.ReadWriteOnce @@ -47,14 +47,14 @@ func mkPersistentVolumeClaim(namespace, name, storageClass, size string, storage pvc := &v1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: volumeName, - Namespace: namespace, + Namespace: config.Namespace, }, Spec: v1.PersistentVolumeClaimSpec{ AccessModes: []v1.PersistentVolumeAccessMode{accessMode}, StorageClassName: _storageClass, Resources: v1.VolumeResourceRequirements{ Requests: v1.ResourceList{ - v1.ResourceStorage: resource.MustParse(size), + v1.ResourceStorage: resource.MustParse(config.VolumeSize), }, }, }, @@ -76,13 +76,14 @@ func volumeMountPath(name string) string { } func startVolume(ctx context.Context, engine *kube, name string) (*v1.PersistentVolumeClaim, error) { - pvc, err := mkPersistentVolumeClaim(engine.config.Namespace, name, engine.config.StorageClass, engine.config.VolumeSize, engine.config.StorageRwx) + engineConfig := engine.getConfig() + pvc, err := mkPersistentVolumeClaim(engineConfig, name) if err != nil { return nil, err } log.Trace().Msgf("creating volume: %s", pvc.Name) - return engine.client.CoreV1().PersistentVolumeClaims(engine.config.Namespace).Create(ctx, pvc, metav1.CreateOptions{}) + return engine.client.CoreV1().PersistentVolumeClaims(engineConfig.Namespace).Create(ctx, pvc, metav1.CreateOptions{}) } func stopVolume(ctx context.Context, engine *kube, name string, deleteOpts metav1.DeleteOptions) error { diff --git a/pipeline/backend/kubernetes/volume_test.go b/pipeline/backend/kubernetes/volume_test.go index 8f906bde5b..07ec7c30e9 100644 --- a/pipeline/backend/kubernetes/volume_test.go +++ b/pipeline/backend/kubernetes/volume_test.go @@ -84,20 +84,35 @@ func TestPersistentVolumeClaim(t *testing.T) { "status": {} }` - pvc, err := mkPersistentVolumeClaim("someNamespace", "somename", "local-storage", "1Gi", true) + pvc, err := mkPersistentVolumeClaim(&config{ + Namespace: "someNamespace", + StorageClass: "local-storage", + VolumeSize: "1Gi", + StorageRwx: true, + }, "somename") assert.NoError(t, err) j, err := json.Marshal(pvc) assert.NoError(t, err) assert.JSONEq(t, expectedRwx, string(j)) - pvc, err = mkPersistentVolumeClaim("someNamespace", "somename", "local-storage", "1Gi", false) + pvc, err = mkPersistentVolumeClaim(&config{ + Namespace: "someNamespace", + StorageClass: "local-storage", + VolumeSize: "1Gi", + StorageRwx: false, + }, "somename") assert.NoError(t, err) j, err = json.Marshal(pvc) assert.NoError(t, err) assert.JSONEq(t, expectedRwo, string(j)) - _, err = mkPersistentVolumeClaim("someNamespace", "some0..INVALID3name", "local-storage", "1Gi", false) + _, err = mkPersistentVolumeClaim(&config{ + Namespace: "someNamespace", + StorageClass: "local-storage", + VolumeSize: "1Gi", + StorageRwx: false, + }, "some0..INVALID3name") assert.Error(t, err) }