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"
"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")

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,
}
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 {

View file

@ -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 {

View file

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

View file

@ -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 {

View file

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