Use UUID as podName and cleanup arguments for Kubernetes backend (#3135)

to much args are just horrible to maintain. And we already have it nice
structured stored as step.
This commit is contained in:
6543 2024-01-11 16:32:37 +01:00 committed by GitHub
parent 7756c60a33
commit d1fe86b7be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 201 additions and 108 deletions

View file

@ -32,27 +32,18 @@ import (
const ( const (
StepLabel = "step" StepLabel = "step"
podPrefix = "wp-"
) )
func mkPod(namespace, name, image, workDir, goos, serviceAccountName string, func mkPod(step *types.Step, config *config, podName, goos string) (*v1.Pod, error) {
pool, privileged bool, meta := podMeta(step, config, podName)
commands, vols, pullSecretNames []string,
labels, annotations, env, nodeSelector map[string]string,
extraHosts []types.HostAlias, tolerations []types.Toleration, resources types.Resources,
securityContext *types.SecurityContext, securityContextConfig SecurityContextConfig,
) (*v1.Pod, error) {
var err error
meta := podMeta(name, namespace, labels, annotations) spec, err := podSpec(step, config)
spec, err := podSpec(serviceAccountName, vols, pullSecretNames, env, nodeSelector, extraHosts, tolerations,
securityContext, securityContextConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
container, err := podContainer(name, image, workDir, goos, pool, privileged, commands, vols, env, container, err := podContainer(step, podName, goos)
resources, securityContext)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -74,41 +65,37 @@ func stepToPodName(step *types.Step) (name string, err error) {
} }
func podName(step *types.Step) (string, error) { func podName(step *types.Step) (string, error) {
return dnsName(step.Name) return dnsName(podPrefix + step.UUID)
} }
func podMeta(name, namespace string, labels, annotations map[string]string) metav1.ObjectMeta { func podMeta(step *types.Step, config *config, podName string) metav1.ObjectMeta {
meta := metav1.ObjectMeta{ meta := metav1.ObjectMeta{
Name: name, Name: podName,
Namespace: namespace, Namespace: config.Namespace,
Annotations: annotations, Annotations: config.PodAnnotations,
} }
if labels == nil { labels := make(map[string]string, len(config.PodLabels)+1)
labels = make(map[string]string, 1) // copy to not alter the engine config
} maps.Copy(labels, config.PodLabels)
labels[StepLabel] = name labels[StepLabel] = step.Name
meta.Labels = labels meta.Labels = labels
return meta return meta
} }
func podSpec(serviceAccountName string, vols, pullSecretNames []string, env, backendNodeSelector map[string]string, func podSpec(step *types.Step, config *config) (v1.PodSpec, error) {
extraHosts []types.HostAlias, backendTolerations []types.Toleration,
securityContext *types.SecurityContext, securityContextConfig SecurityContextConfig,
) (v1.PodSpec, error) {
var err error var err error
spec := v1.PodSpec{ spec := v1.PodSpec{
RestartPolicy: v1.RestartPolicyNever, RestartPolicy: v1.RestartPolicyNever,
ServiceAccountName: serviceAccountName, ServiceAccountName: step.BackendOptions.Kubernetes.ServiceAccountName,
ImagePullSecrets: imagePullSecretsReferences(pullSecretNames), ImagePullSecrets: imagePullSecretsReferences(config.ImagePullSecretNames),
HostAliases: hostAliases(step.ExtraHosts),
NodeSelector: nodeSelector(step.BackendOptions.Kubernetes.NodeSelector, step.Environment["CI_SYSTEM_PLATFORM"]),
Tolerations: tolerations(step.BackendOptions.Kubernetes.Tolerations),
SecurityContext: podSecurityContext(step.BackendOptions.Kubernetes.SecurityContext, config.SecurityContext),
} }
spec.Volumes, err = volumes(step.Volumes)
spec.HostAliases = hostAliases(extraHosts)
spec.NodeSelector = nodeSelector(backendNodeSelector, env["CI_SYSTEM_PLATFORM"])
spec.Tolerations = tolerations(backendTolerations)
spec.SecurityContext = podSecurityContext(securityContext, securityContextConfig)
spec.Volumes, err = volumes(vols)
if err != nil { if err != nil {
return spec, err return spec, err
} }
@ -116,36 +103,34 @@ func podSpec(serviceAccountName string, vols, pullSecretNames []string, env, bac
return spec, nil return spec, nil
} }
func podContainer(name, image, workDir, goos string, pull, privileged bool, commands, volumes []string, env map[string]string, resources types.Resources, func podContainer(step *types.Step, podName, goos string) (v1.Container, error) {
securityContext *types.SecurityContext,
) (v1.Container, error) {
var err error var err error
container := v1.Container{ container := v1.Container{
Name: name, Name: podName,
Image: image, Image: step.Image,
WorkingDir: workDir, WorkingDir: step.WorkingDir,
} }
if pull { if step.Pull {
container.ImagePullPolicy = v1.PullAlways container.ImagePullPolicy = v1.PullAlways
} }
if len(commands) != 0 { if len(step.Commands) != 0 {
scriptEnv, command, args := common.GenerateContainerConf(commands, goos) scriptEnv, command, args := common.GenerateContainerConf(step.Commands, goos)
container.Command = command container.Command = command
container.Args = args container.Args = args
maps.Copy(env, scriptEnv) maps.Copy(step.Environment, scriptEnv)
} }
container.Env = mapToEnvVars(env) container.Env = mapToEnvVars(step.Environment)
container.SecurityContext = containerSecurityContext(securityContext, privileged) container.SecurityContext = containerSecurityContext(step.BackendOptions.Kubernetes.SecurityContext, step.Privileged)
container.Resources, err = resourceRequirements(resources) container.Resources, err = resourceRequirements(step.BackendOptions.Kubernetes.Resources)
if err != nil { if err != nil {
return container, err return container, err
} }
container.VolumeMounts, err = volumeMounts(volumes) container.VolumeMounts, err = volumeMounts(step.Volumes)
if err != nil { if err != nil {
return container, err return container, err
} }
@ -378,12 +363,7 @@ 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)
pod, err := mkPod(engine.config.Namespace, podName, step.Image, step.WorkingDir, engine.goos, step.BackendOptions.Kubernetes.ServiceAccountName,
step.Pull, step.Privileged,
step.Commands, step.Volumes, engine.config.ImagePullSecretNames,
engine.config.PodLabels, engine.config.PodAnnotations, step.Environment, step.BackendOptions.Kubernetes.NodeSelector,
step.ExtraHosts, step.BackendOptions.Kubernetes.Tolerations, step.BackendOptions.Kubernetes.Resources, step.BackendOptions.Kubernetes.SecurityContext, engine.config.SecurityContext)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -24,16 +24,33 @@ import (
) )
func TestPodName(t *testing.T) { func TestPodName(t *testing.T) {
name, err := podName(&types.Step{Name: "wp_01he8bebctabr3kgk0qj36d2me_0"}) name, err := podName(&types.Step{UUID: "01he8bebctabr3kgk0qj36d2me-0"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "wp-01he8bebctabr3kgk0qj36d2me-0", name) assert.Equal(t, "wp-01he8bebctabr3kgk0qj36d2me-0", name)
name, err = podName(&types.Step{Name: "wp\\01he8bebctabr3kgk0qj36d2me-0"}) _, err = podName(&types.Step{UUID: "01he8bebctabr3kgk0qj36d2me\\0a"})
assert.NoError(t, err)
assert.Equal(t, "wp\\01he8bebctabr3kgk0qj36d2me-0", name)
_, err = podName(&types.Step{Name: "wp-01he8bebctabr3kgk0qj36d2me-0-services-0.woodpecker-runtime.svc.cluster.local"})
assert.ErrorIs(t, err, ErrDNSPatternInvalid) assert.ErrorIs(t, err, ErrDNSPatternInvalid)
_, err = podName(&types.Step{UUID: "01he8bebctabr3kgk0qj36d2me-0-services-0..woodpecker-runtime.svc.cluster.local"})
assert.ErrorIs(t, err, ErrDNSPatternInvalid)
}
func TestStepToPodName(t *testing.T) {
name, err := stepToPodName(&types.Step{UUID: "01he8bebctabr3kg", Name: "clone", Type: types.StepTypeClone})
assert.NoError(t, err)
assert.EqualValues(t, "wp-01he8bebctabr3kg", name)
name, err = stepToPodName(&types.Step{UUID: "01he8bebctabr3kg", Name: "clone", Type: types.StepTypeCache})
assert.NoError(t, err)
assert.EqualValues(t, "wp-01he8bebctabr3kg", name)
name, err = stepToPodName(&types.Step{UUID: "01he8bebctabr3kg", Name: "clone", Type: types.StepTypePlugin})
assert.NoError(t, err)
assert.EqualValues(t, "wp-01he8bebctabr3kg", name)
name, err = stepToPodName(&types.Step{UUID: "01he8bebctabr3kg", Name: "clone", Type: types.StepTypeCommands})
assert.NoError(t, err)
assert.EqualValues(t, "wp-01he8bebctabr3kg", name)
name, err = stepToPodName(&types.Step{UUID: "01he8bebctabr3kg", Name: "clone", Type: types.StepTypeService})
assert.NoError(t, err)
assert.EqualValues(t, "clone", name)
} }
func TestTinyPod(t *testing.T) { func TestTinyPod(t *testing.T) {
@ -44,7 +61,7 @@ func TestTinyPod(t *testing.T) {
"namespace": "woodpecker", "namespace": "woodpecker",
"creationTimestamp": null, "creationTimestamp": null,
"labels": { "labels": {
"step": "wp-01he8bebctabr3kgk0qj36d2me-0" "step": "build-via-gradle"
} }
}, },
"spec": { "spec": {
@ -101,13 +118,18 @@ func TestTinyPod(t *testing.T) {
"status": {} "status": {}
}` }`
pod, err := mkPod("woodpecker", "wp-01he8bebctabr3kgk0qj36d2me-0", "gradle:8.4.0-jdk21", "/woodpecker/src", "linux/amd64", "", pod, err := mkPod(&types.Step{
false, false, Name: "build-via-gradle",
[]string{"gradle build"}, []string{"workspace:/woodpecker/src"}, nil, Image: "gradle:8.4.0-jdk21",
nil, nil, map[string]string{"CI": "woodpecker"}, nil, WorkingDir: "/woodpecker/src",
nil, nil, Pull: false,
types.Resources{Requests: nil, Limits: nil}, nil, SecurityContextConfig{}, Privileged: false,
) Commands: []string{"gradle build"},
Volumes: []string{"workspace:/woodpecker/src"},
Environment: map[string]string{"CI": "woodpecker"},
}, &config{
Namespace: "woodpecker",
}, "wp-01he8bebctabr3kgk0qj36d2me-0", "linux/amd64")
assert.NoError(t, err) assert.NoError(t, err)
json, err := json.Marshal(pod) json, err := json.Marshal(pod)
@ -126,7 +148,7 @@ func TestFullPod(t *testing.T) {
"creationTimestamp": null, "creationTimestamp": null,
"labels": { "labels": {
"app": "test", "app": "test",
"step": "wp-01he8bebctabr3kgk0qj36d2me-0" "step": "go-test"
}, },
"annotations": { "annotations": {
"apparmor.security": "runtime/default" "apparmor.security": "runtime/default"
@ -242,15 +264,41 @@ func TestFullPod(t *testing.T) {
{Name: "cloudflare", IP: "1.1.1.1"}, {Name: "cloudflare", IP: "1.1.1.1"},
{Name: "cf.v6", IP: "2606:4700:4700::64"}, {Name: "cf.v6", IP: "2606:4700:4700::64"},
} }
pod, err := mkPod("woodpecker", "wp-01he8bebctabr3kgk0qj36d2me-0", "meltwater/drone-cache", "/woodpecker/src", "linux/amd64", "wp-svc-acc", pod, err := mkPod(&types.Step{
true, true, Name: "go-test",
[]string{"go get", "go test"}, []string{"woodpecker-cache:/woodpecker/src/cache"}, []string{"regcred", "another-pull-secret"}, Image: "meltwater/drone-cache",
map[string]string{"app": "test"}, map[string]string{"apparmor.security": "runtime/default"}, map[string]string{"CGO": "0"}, map[string]string{"storage": "ssd"}, WorkingDir: "/woodpecker/src",
hostAliases, []types.Toleration{{Key: "net-port", Value: "100Mbit", Effect: types.TaintEffectNoSchedule}}, Pull: true,
types.Resources{Requests: map[string]string{"memory": "128Mi", "cpu": "1000m"}, Limits: map[string]string{"memory": "256Mi", "cpu": "2"}}, Privileged: true,
&types.SecurityContext{Privileged: newBool(true), RunAsNonRoot: newBool(true), RunAsUser: newInt64(101), RunAsGroup: newInt64(101), FSGroup: newInt64(101)}, Commands: []string{"go get", "go test"},
SecurityContextConfig{RunAsNonRoot: false}, Volumes: []string{"woodpecker-cache:/woodpecker/src/cache"},
) Environment: map[string]string{"CGO": "0"},
ExtraHosts: hostAliases,
BackendOptions: types.BackendOptions{
Kubernetes: types.KubernetesBackendOptions{
NodeSelector: map[string]string{"storage": "ssd"},
ServiceAccountName: "wp-svc-acc",
Tolerations: []types.Toleration{{Key: "net-port", Value: "100Mbit", Effect: types.TaintEffectNoSchedule}},
Resources: types.Resources{
Requests: map[string]string{"memory": "128Mi", "cpu": "1000m"},
Limits: map[string]string{"memory": "256Mi", "cpu": "2"},
},
SecurityContext: &types.SecurityContext{
Privileged: newBool(true),
RunAsNonRoot: newBool(true),
RunAsUser: newInt64(101),
RunAsGroup: newInt64(101),
FSGroup: newInt64(101),
},
},
},
}, &config{
Namespace: "woodpecker",
ImagePullSecretNames: []string{"regcred", "another-pull-secret"},
PodLabels: map[string]string{"app": "test"},
PodAnnotations: map[string]string{"apparmor.security": "runtime/default"},
SecurityContext: SecurityContextConfig{RunAsNonRoot: false},
}, "wp-01he8bebctabr3kgk0qj36d2me-0", "linux/amd64")
assert.NoError(t, err) assert.NoError(t, err)
json, err := json.Marshal(pod) json, err := json.Marshal(pod)

View file

@ -26,11 +26,22 @@ import (
"k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/intstr"
) )
func mkService(namespace, name string, ports []uint16, selector map[string]string) *v1.Service { const (
log.Trace().Str("name", name).Interface("selector", selector).Interface("ports", ports).Msg("Creating service") ServiceLabel = "service"
)
func mkService(step *types.Step, namespace string) (*v1.Service, error) {
name, err := serviceName(step)
if err != nil {
return nil, err
}
selector := map[string]string{
ServiceLabel: name,
}
var svcPorts []v1.ServicePort var svcPorts []v1.ServicePort
for _, port := range ports { for _, port := range step.Ports {
svcPorts = append(svcPorts, v1.ServicePort{ svcPorts = append(svcPorts, v1.ServicePort{
Name: fmt.Sprintf("port-%d", port), Name: fmt.Sprintf("port-%d", port),
Port: int32(port), Port: int32(port),
@ -48,7 +59,7 @@ func mkService(namespace, name string, ports []uint16, selector map[string]strin
Selector: selector, Selector: selector,
Ports: svcPorts, Ports: svcPorts,
}, },
} }, nil
} }
func serviceName(step *types.Step) (string, error) { func serviceName(step *types.Step) (string, error) {
@ -56,21 +67,12 @@ func serviceName(step *types.Step) (string, error) {
} }
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) {
name, err := serviceName(step) svc, err := mkService(step, engine.config.Namespace)
if err != nil {
return nil, err
}
podName, err := podName(step)
if err != nil { if err != nil {
return nil, err return nil, err
} }
selector := map[string]string{ log.Trace().Str("name", svc.Name).Interface("selector", svc.Spec.Selector).Interface("ports", svc.Spec.Ports).Msg("creating service")
StepLabel: podName,
}
svc := mkService(engine.config.Namespace, name, step.Ports, selector)
return engine.client.CoreV1().Services(engine.config.Namespace).Create(ctx, svc, metav1.CreateOptions{}) return engine.client.CoreV1().Services(engine.config.Namespace).Create(ctx, svc, metav1.CreateOptions{})
} }

View file

@ -23,16 +23,17 @@ import (
) )
func TestServiceName(t *testing.T) { func TestServiceName(t *testing.T) {
name, err := serviceName(&types.Step{Name: "wp_01he8bebctabr3kgk0qj36d2me_0_services_0"}) name, err := serviceName(&types.Step{Name: "database"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "wp-01he8bebctabr3kgk0qj36d2me-0-services-0", name) assert.Equal(t, "database", name)
name, err = serviceName(&types.Step{Name: "wp-01he8bebctabr3kgk0qj36d2me-0\\services-0"}) name, err = serviceName(&types.Step{Name: "wp-01he8bebctabr3kgk0qj36d2me-0-services-0.woodpecker-runtime.svc.cluster.local"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "wp-01he8bebctabr3kgk0qj36d2me-0\\services-0", name) assert.Equal(t, "wp-01he8bebctabr3kgk0qj36d2me-0-services-0.woodpecker-runtime.svc.cluster.local", name)
_, err = serviceName(&types.Step{Name: "wp-01he8bebctabr3kgk0qj36d2me-0-services-0.woodpecker-runtime.svc.cluster.local"}) name, err = serviceName(&types.Step{Name: "awesome_service"})
assert.ErrorIs(t, err, ErrDNSPatternInvalid) assert.NoError(t, err)
assert.Equal(t, "awesome-service", name)
} }
func TestService(t *testing.T) { func TestService(t *testing.T) {
@ -62,7 +63,7 @@ func TestService(t *testing.T) {
} }
], ],
"selector": { "selector": {
"step": "baz" "service": "bar"
}, },
"type": "ClusterIP" "type": "ClusterIP"
}, },
@ -71,7 +72,11 @@ func TestService(t *testing.T) {
} }
}` }`
s := mkService("foo", "bar", []uint16{1, 2, 3}, map[string]string{"step": "baz"}) s, err := mkService(&types.Step{
Name: "bar",
Ports: []uint16{1, 2, 3},
}, "foo")
assert.NoError(t, err)
j, err := json.Marshal(s) j, err := json.Marshal(s)
assert.NoError(t, err) assert.NoError(t, err)
assert.JSONEq(t, expected, string(j)) assert.JSONEq(t, expected, string(j))

View file

@ -27,12 +27,15 @@ import (
) )
var ( var (
dnsPattern = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`) dnsPattern = regexp.MustCompile(`^[a-z0-9]` + // must start with
`([-a-z0-9]*[a-z0-9])?` + // inside can als contain -
`(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`, // allow the same pattern as before with dots in between but only one dot
)
ErrDNSPatternInvalid = errors.New("name is not a valid kubernetes DNS name") ErrDNSPatternInvalid = errors.New("name is not a valid kubernetes DNS name")
) )
func dnsName(i string) (string, error) { func dnsName(i string) (string, error) {
res := strings.ReplaceAll(i, "_", "-") res := strings.ToLower(strings.ReplaceAll(i, "_", "-"))
if found := dnsPattern.FindStringIndex(res); found == nil { if found := dnsPattern.FindStringIndex(res); found == nil {
return "", ErrDNSPatternInvalid return "", ErrDNSPatternInvalid

View file

@ -0,0 +1,56 @@
// 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 TestDNSName(t *testing.T) {
name, err := dnsName("wp_01he8bebctabr3kgk0qj36d2me_0_services_0")
assert.NoError(t, err)
assert.Equal(t, "wp-01he8bebctabr3kgk0qj36d2me-0-services-0", name)
name, err = dnsName("a.0-AA")
assert.NoError(t, err)
assert.Equal(t, "a.0-aa", name)
name, err = dnsName("wp-01he8bebctabr3kgk0qj36d2me-0-services-0.woodpecker-runtime.svc.cluster.local")
assert.NoError(t, err)
assert.Equal(t, "wp-01he8bebctabr3kgk0qj36d2me-0-services-0.woodpecker-runtime.svc.cluster.local", name)
_, err = dnsName(".0-a")
assert.ErrorIs(t, err, ErrDNSPatternInvalid)
_, err = dnsName("ABC..DEF")
assert.ErrorIs(t, err, ErrDNSPatternInvalid)
_, err = dnsName("0.-a")
assert.ErrorIs(t, err, ErrDNSPatternInvalid)
_, err = dnsName("test-")
assert.ErrorIs(t, err, ErrDNSPatternInvalid)
_, err = dnsName("-test")
assert.ErrorIs(t, err, ErrDNSPatternInvalid)
_, err = dnsName("0-a.")
assert.ErrorIs(t, err, ErrDNSPatternInvalid)
_, err = dnsName("abc\\def")
assert.ErrorIs(t, err, ErrDNSPatternInvalid)
}

View file

@ -26,9 +26,8 @@ func TestPvcName(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "woodpecker-cache", name) assert.Equal(t, "woodpecker-cache", name)
name, err = volumeName("woodpecker\\cache") _, err = volumeName("woodpecker\\cache")
assert.NoError(t, err) assert.ErrorIs(t, err, ErrDNSPatternInvalid)
assert.Equal(t, "woodpecker\\cache", name)
_, err = volumeName("-woodpecker.cache:/woodpecker/src/cache") _, err = volumeName("-woodpecker.cache:/woodpecker/src/cache")
assert.ErrorIs(t, err, ErrDNSPatternInvalid) assert.ErrorIs(t, err, ErrDNSPatternInvalid)
@ -99,6 +98,6 @@ func TestPersistentVolumeClaim(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.JSONEq(t, expectedRwo, string(j)) assert.JSONEq(t, expectedRwo, string(j))
_, err = mkPersistentVolumeClaim("someNamespace", "some0INVALID3name", "local-storage", "1Gi", false) _, err = mkPersistentVolumeClaim("someNamespace", "some0..INVALID3name", "local-storage", "1Gi", false)
assert.Error(t, err) assert.Error(t, err)
} }