Secured kubernetes backend configuration (#3204)

Follow up of #3165
This commit is contained in:
Thomas Anderson 2024-01-15 05:59:08 +03:00 committed by GitHub
parent 59d824ebf8
commit 10f2e209d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 111 additions and 26 deletions

View file

@ -18,8 +18,10 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"maps"
"os" "os"
"runtime" "runtime"
"slices"
"time" "time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -155,6 +157,17 @@ func (e *kube) Load(ctx context.Context) (*types.BackendInfo, error) {
}, nil }, 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. // Setup the pipeline environment.
func (e *kube) SetupWorkflow(ctx context.Context, conf *types.Config, taskUUID string) error { func (e *kube) SetupWorkflow(ctx context.Context, conf *types.Config, taskUUID string) error {
log.Trace().Str("taskUUID", taskUUID).Msgf("Setting up Kubernetes primitives") log.Trace().Str("taskUUID", taskUUID).Msgf("Setting up Kubernetes primitives")

View file

@ -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)
}

View file

@ -74,15 +74,16 @@ func podMeta(step *types.Step, config *config, podName string) metav1.ObjectMeta
Namespace: config.Namespace, Namespace: config.Namespace,
} }
labels := make(map[string]string, len(config.PodLabels)+1) meta.Labels = config.PodLabels
// copy to not alter the engine config if meta.Labels == nil {
maps.Copy(labels, config.PodLabels) meta.Labels = make(map[string]string, 1)
labels[StepLabel] = step.Name }
meta.Labels = labels meta.Labels[StepLabel] = step.Name
// copy to not alter the engine config meta.Annotations = config.PodAnnotations
meta.Annotations = make(map[string]string, len(config.PodAnnotations)) if meta.Annotations == nil {
maps.Copy(meta.Annotations, config.PodAnnotations) meta.Annotations = make(map[string]string)
}
securityContext := step.BackendOptions.Kubernetes.SecurityContext securityContext := step.BackendOptions.Kubernetes.SecurityContext
if securityContext != nil { if securityContext != nil {
@ -442,13 +443,14 @@ func startPod(ctx context.Context, engine *kube, step *types.Step) (*v1.Pod, err
if err != nil { if err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
log.Trace().Msgf("creating pod: %s", pod.Name) 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 { func stopPod(ctx context.Context, engine *kube, step *types.Step, deleteOpts metav1.DeleteOptions) error {

View file

@ -32,7 +32,7 @@ const (
ServiceLabel = "service" 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) name, err := serviceName(step)
if err != nil { if err != nil {
return nil, err return nil, err
@ -51,7 +51,7 @@ func mkService(step *types.Step, namespace string) (*v1.Service, error) {
return &v1.Service{ return &v1.Service{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: name, Name: name,
Namespace: namespace, Namespace: config.Namespace,
}, },
Spec: v1.ServiceSpec{ Spec: v1.ServiceSpec{
Type: v1.ServiceTypeClusterIP, 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) { 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 { if err != nil {
return nil, err return nil, err
} }
log.Trace().Str("name", svc.Name).Interface("selector", svc.Spec.Selector).Interface("ports", svc.Spec.Ports).Msg("creating service") 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 { func stopService(ctx context.Context, engine *kube, step *types.Step, deleteOpts metav1.DeleteOptions) error {

View file

@ -82,7 +82,7 @@ func TestService(t *testing.T) {
s, err := mkService(&types.Step{ s, err := mkService(&types.Step{
Name: "bar", Name: "bar",
Ports: ports, Ports: ports,
}, "foo") }, &config{Namespace: "foo"})
assert.NoError(t, err) assert.NoError(t, err)
j, err := json.Marshal(s) j, err := json.Marshal(s)
assert.NoError(t, err) assert.NoError(t, err)

View file

@ -25,15 +25,15 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
func mkPersistentVolumeClaim(namespace, name, storageClass, size string, storageRwx bool) (*v1.PersistentVolumeClaim, error) { func mkPersistentVolumeClaim(config *config, name string) (*v1.PersistentVolumeClaim, error) {
_storageClass := &storageClass _storageClass := &config.StorageClass
if storageClass == "" { if config.StorageClass == "" {
_storageClass = nil _storageClass = nil
} }
var accessMode v1.PersistentVolumeAccessMode var accessMode v1.PersistentVolumeAccessMode
if storageRwx { if config.StorageRwx {
accessMode = v1.ReadWriteMany accessMode = v1.ReadWriteMany
} else { } else {
accessMode = v1.ReadWriteOnce accessMode = v1.ReadWriteOnce
@ -47,14 +47,14 @@ func mkPersistentVolumeClaim(namespace, name, storageClass, size string, storage
pvc := &v1.PersistentVolumeClaim{ pvc := &v1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: volumeName, Name: volumeName,
Namespace: namespace, Namespace: config.Namespace,
}, },
Spec: v1.PersistentVolumeClaimSpec{ Spec: v1.PersistentVolumeClaimSpec{
AccessModes: []v1.PersistentVolumeAccessMode{accessMode}, AccessModes: []v1.PersistentVolumeAccessMode{accessMode},
StorageClassName: _storageClass, StorageClassName: _storageClass,
Resources: v1.VolumeResourceRequirements{ Resources: v1.VolumeResourceRequirements{
Requests: v1.ResourceList{ 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) { 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 { if err != nil {
return nil, err return nil, err
} }
log.Trace().Msgf("creating volume: %s", pvc.Name) 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 { func stopVolume(ctx context.Context, engine *kube, name string, deleteOpts metav1.DeleteOptions) error {

View file

@ -84,20 +84,35 @@ func TestPersistentVolumeClaim(t *testing.T) {
"status": {} "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) assert.NoError(t, err)
j, err := json.Marshal(pvc) j, err := json.Marshal(pvc)
assert.NoError(t, err) assert.NoError(t, err)
assert.JSONEq(t, expectedRwx, string(j)) 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) assert.NoError(t, err)
j, err = json.Marshal(pvc) j, err = json.Marshal(pvc)
assert.NoError(t, err) assert.NoError(t, err)
assert.JSONEq(t, expectedRwo, string(j)) 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) assert.Error(t, err)
} }