Add support for headless Kubernetes services (#5764)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Henrik Huitti <henrik.huitti@henhu.fi>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Robert Kaussow <mail@thegeeklab.de>
This commit is contained in:
Martin Schmidt
2025-12-02 14:42:00 +01:00
committed by GitHub
parent 761cc67f11
commit 9f828c96b0
8 changed files with 316 additions and 140 deletions

View File

@@ -304,6 +304,36 @@ It configures the address of the Kubernetes API server to connect to.
If running the agent within Kubernetes, this will already be set and you don't have to add it manually.
### Headless services
For each workflow run a [headless services](https://kubernetes.io/docs/concepts/services-networking/service/#headless-services) is created,
and all steps asigned the subdomain that matches the headless service, so any step can reach other steps via DNS by using the step name as hostname.
Using the headless services, the step pod is connected to directly, so any port on the other step pods can be reached.
This is useful for some use-cases, like test-containers in a docker-in-docker setup, where the step needs to connect to many ports on the docker host service.
```yaml
steps:
- name: test
image: docker:cli # use 'docker:<major-version>-cli' or similar in production
environment:
DOCKER_HOST: 'tcp://docker:2376'
DOCKER_CERT_PATH: '/woodpecker/dind-certs/client'
DOCKER_TLS_VERIFY: '1'
commands:
- docker run hello-world
- name: docker
image: docker:dind # use 'docker:<major-version>-dind' or similar in production
detached: true
privileged: true
environment:
DOCKER_TLS_CERTDIR: /woodpecker/dind-certs
```
If ports are defined on a service, then woodpecker will create a normal service for the pod, which use hosts override using the services cluster IP.
## Environment variables
These env vars can be set in the `env:` sections of the agent.

View File

@@ -41,7 +41,6 @@ import (
"k8s.io/client-go/tools/cache"
"go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types"
pipelineErrors "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors/types"
)
const (
@@ -221,38 +220,23 @@ func (e *kube) SetupWorkflow(ctx context.Context, conf *types.Config, taskUUID s
namespace := e.config.GetNamespace(conf.Stages[0].Steps[0].OrgID)
if e.config.EnableNamespacePerOrg {
log.Trace().Str("taskUUID", taskUUID).Msgf("Ensure organization namespace: %s", namespace)
err := mkNamespace(ctx, e.client.CoreV1().Namespaces(), namespace)
if err != nil {
return err
}
}
log.Trace().Str("taskUUID", taskUUID).Msgf("Creating workflow volume")
_, err := startVolume(ctx, e, conf.Volume, namespace)
if err != nil {
return err
}
var extraHosts []types.HostAlias
for _, stage := range conf.Stages {
for _, step := range stage.Steps {
if isService(step) {
svc, err := startService(ctx, e, step)
if err != nil {
return &pipelineErrors.ErrInvalidWorkflowSetup{
Err: err,
Step: step,
}
}
hostAlias := types.HostAlias{Name: step.Networks[0].Aliases[0], IP: svc.Spec.ClusterIP}
extraHosts = append(extraHosts, hostAlias)
}
}
}
log.Trace().Msgf("adding extra hosts: %v", extraHosts)
for _, stage := range conf.Stages {
for _, step := range stage.Steps {
step.ExtraHosts = extraHosts
}
log.Trace().Str("taskUUID", taskUUID).Msgf("Creating workflow headless service")
_, err = startHeadlessService(ctx, e, namespace, taskUUID)
if err != nil {
return err
}
return nil
@@ -471,23 +455,16 @@ func (e *kube) DestroyStep(ctx context.Context, step *types.Step, taskUUID strin
func (e *kube) DestroyWorkflow(ctx context.Context, conf *types.Config, taskUUID string) error {
log.Trace().Str("taskUUID", taskUUID).Msg("deleting Kubernetes primitives")
for _, stage := range conf.Stages {
for _, step := range stage.Steps {
err := stopPod(ctx, e, step, defaultDeleteOptions)
if err != nil {
return err
}
namespace := e.config.GetNamespace(conf.Stages[0].Steps[0].OrgID)
if isService(step) {
err := stopService(ctx, e, step, defaultDeleteOptions)
if err != nil {
return err
}
}
}
log.Trace().Str("taskUUID", taskUUID).Msgf("deleting workflow headless service")
err := stopHeadlessService(ctx, e, namespace, taskUUID)
if err != nil {
return err
}
err := stopVolume(ctx, e, conf.Volume, e.config.GetNamespace(conf.Stages[0].Steps[0].OrgID), defaultDeleteOptions)
log.Trace().Str("taskUUID", taskUUID).Msgf("deleting workflow volume")
err = stopVolume(ctx, e, conf.Volume, e.config.GetNamespace(conf.Stages[0].Steps[0].OrgID), defaultDeleteOptions)
if err != nil {
return err
}

View File

@@ -15,9 +15,14 @@
package kubernetes
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
"go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types"
)
func TestGettingConfig(t *testing.T) {
@@ -51,3 +56,77 @@ func TestGettingConfig(t *testing.T) {
assert.Len(t, engine.config.ImagePullSecretNames, 1)
assert.False(t, engine.config.SecurityContext.RunAsNonRoot)
}
func TestSetupWorkflow(t *testing.T) {
namespace := "foo"
volumeName := "volume-name"
volumePath := volumeName + ":/woodpecker"
networkName := "test-network"
taskUUID := "11301"
engine := kube{
config: &config{
Namespace: namespace,
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},
},
client: fake.NewClientset(),
}
serviceWithPorts := types.Step{
OrgID: 42,
Name: "service",
UUID: "123",
Type: types.StepTypeService,
Volumes: []string{volumePath},
Networks: []types.Conn{{Name: networkName, Aliases: []string{"alias"}}},
Ports: []types.Port{
{Number: 8080, Protocol: "tcp"},
},
}
conf := &types.Config{
Volume: volumePath,
Network: networkName,
Stages: []*types.Stage{
{
Steps: []*types.Step{
&serviceWithPorts,
{
OrgID: 42,
UUID: "234",
Name: "service2",
Type: types.StepTypeService,
Volumes: []string{volumePath},
Networks: []types.Conn{{Name: networkName, Aliases: []string{"alias"}}},
},
},
},
{
Steps: []*types.Step{
{
OrgID: 42,
UUID: "456",
Name: "step-1",
Volumes: []string{volumePath},
Networks: []types.Conn{{Name: networkName, Aliases: []string{"alias"}}},
},
},
},
},
}
err := engine.SetupWorkflow(context.Background(), conf, taskUUID)
assert.NoError(t, err, "SetupWorkflow should not error with minimal config and fake client")
_, err = engine.client.CoreV1().PersistentVolumeClaims(namespace).Get(context.Background(), "volume-name", meta_v1.GetOptions{})
assert.NoError(t, err, "persistent volume should be created during workflow setup")
_, err = engine.client.CoreV1().Services(namespace).Get(context.Background(), "wp-hsvc-"+taskUUID, meta_v1.GetOptions{})
assert.NoError(t, err, "headless service should be created during workflow setup")
}

View File

@@ -55,7 +55,7 @@ func mkPod(step *types.Step, config *config, podName, goos string, options Backe
return nil, err
}
spec, err := podSpec(step, config, options, nsp)
spec, err := podSpec(step, config, options, nsp, taskUUID)
if err != nil {
return nil, err
}
@@ -172,14 +172,21 @@ func podAnnotations(config *config, options BackendOptions) map[string]string {
return annotations
}
func podSpec(step *types.Step, config *config, options BackendOptions, nsp nativeSecretsProcessor) (v1.PodSpec, error) {
var err error
func podSpec(step *types.Step, config *config, options BackendOptions, nsp nativeSecretsProcessor, taskUUID string) (v1.PodSpec, error) {
subdomain, err := subdomain(taskUUID)
if err != nil {
return v1.PodSpec{}, err
}
spec := v1.PodSpec{
RestartPolicy: v1.RestartPolicyNever,
RuntimeClassName: options.RuntimeClassName,
ServiceAccountName: options.ServiceAccountName,
PriorityClassName: config.PriorityClassName,
HostAliases: hostAliases(step.ExtraHosts),
Hostname: step.Name,
Subdomain: subdomain,
DNSConfig: dnsConfig(config.GetNamespace(step.OrgID), subdomain),
NodeSelector: nodeSelector(options.NodeSelector, config.PodNodeSelector, step.Environment["CI_SYSTEM_PLATFORM"]),
Tolerations: tolerations(options.Tolerations),
SecurityContext: podSecurityContext(options.SecurityContext, config.SecurityContext, step.Privileged),
@@ -603,6 +610,12 @@ func mapToEnvVars(m map[string]string) []v1.EnvVar {
return ev
}
func dnsConfig(namespace, subdomain string) *v1.PodDNSConfig {
return &v1.PodDNSConfig{
Searches: []string{fmt.Sprintf("%s.%s.svc.cluster.local", subdomain, namespace)},
}
}
func startPod(ctx context.Context, engine *kube, step *types.Step, options BackendOptions, taskUUID string) (*v1.Pod, error) {
podName, err := stepToPodName(step)
if err != nil {

View File

@@ -52,11 +52,11 @@ func TestStepToPodName(t *testing.T) {
name, err = stepToPodName(&types.Step{UUID: "01he8bebctabr3kg", Name: "prepare-env", Type: types.StepTypeCommands})
assert.NoError(t, err)
assert.EqualValues(t, "wp-01he8bebctabr3kg", name)
name, err = stepToPodName(&types.Step{UUID: "01he8bebctabr3kg", Name: "postgres", Type: types.StepTypeService})
name, err = stepToPodName(&types.Step{UUID: "01he8bebctabr3kg", Name: "postgres", Type: types.StepTypeService, Ports: []types.Port{{Number: 5432}}})
assert.NoError(t, err)
assert.EqualValues(t, "wp-svc-01he8bebctabr3kg-postgres", name)
// Detached service
name, err = stepToPodName(&types.Step{UUID: "01he8bebctabr3kg", Name: "postgres", Detached: true})
name, err = stepToPodName(&types.Step{UUID: "01he8bebctabr3kg", Name: "postgres", Detached: true, Ports: []types.Port{{Number: 5432}}})
assert.NoError(t, err)
assert.EqualValues(t, "wp-svc-01he8bebctabr3kg-postgres", name)
// Detached long running container
@@ -73,6 +73,7 @@ func TestPodMeta(t *testing.T) {
Image: "postgres:16",
WorkingDir: "/woodpecker/src",
Environment: map[string]string{"CI": "woodpecker"},
Ports: []types.Port{{Number: 5432}},
}, &config{
Namespace: "woodpecker",
}, BackendOptions{}, "wp-01he8bebctabr3kg-0", taskUUID)
@@ -88,6 +89,7 @@ func TestPodMeta(t *testing.T) {
Image: "postgres:16",
WorkingDir: "/woodpecker/src",
Environment: map[string]string{"CI": "woodpecker"},
Ports: []types.Port{{Number: 5432}},
}, &config{
Namespace: "woodpecker",
}, BackendOptions{}, "wp-01he8bebctabr3kg-0", taskUUID)
@@ -172,7 +174,12 @@ func TestTinyPod(t *testing.T) {
]
}
],
"restartPolicy": "Never"
"restartPolicy": "Never",
"dnsConfig": {
"searches": ["wp-hsvc-11301.woodpecker.svc.cluster.local"]
},
"subdomain": "wp-hsvc-11301",
"hostname": "build-via-gradle"
},
"status": {}
}`
@@ -337,7 +344,11 @@ func TestFullPod(t *testing.T) {
"cf.v6"
]
}
]
],
"dnsConfig": {
"searches": ["wp-hsvc-11301.woodpecker.svc.cluster.local"]},
"subdomain": "wp-hsvc-11301",
"hostname": "go-test"
},
"status": {}
}`
@@ -433,7 +444,7 @@ func TestPodPrivilege(t *testing.T) {
SecurityContext: SecurityContextConfig{RunAsNonRoot: globalRunAsRoot},
}, "wp-01he8bebctabr3kgk0qj36d2me-0", "linux/amd64", BackendOptions{
SecurityContext: &secCtx,
}, "")
}, "11301")
}
// securty context is requesting user and group 101 (non-root)
@@ -529,7 +540,12 @@ func TestScratchPod(t *testing.T) {
"resources": {}
}
],
"restartPolicy": "Never"
"restartPolicy": "Never",
"dnsConfig": {
"searches": ["wp-hsvc-11301.woodpecker.svc.cluster.local"]
},
"subdomain": "wp-hsvc-11301",
"hostname": "curl-google"
},
"status": {}
}`
@@ -628,7 +644,12 @@ func TestSecrets(t *testing.T) {
]
}
],
"restartPolicy": "Never"
"restartPolicy": "Never",
"dnsConfig": {
"searches": ["wp-hsvc-11301.woodpecker.svc.cluster.local"]
},
"subdomain": "wp-hsvc-11301",
"hostname": "test-secrets"
},
"status": {}
}`
@@ -704,7 +725,12 @@ func TestPodTolerations(t *testing.T) {
"value": "qux",
"effect": "NoExecute"
}
]
],
"dnsConfig": {
"searches": ["wp-hsvc-11301.woodpecker.svc.cluster.local"]
},
"subdomain": "wp-hsvc-11301",
"hostname": "toleration-test"
},
"status": {}
}`
@@ -752,7 +778,12 @@ func TestPodTolerationsAllowFromStep(t *testing.T) {
"resources": {}
}
],
"restartPolicy": "Never"
"restartPolicy": "Never",
"dnsConfig": {
"searches": ["wp-hsvc-11301.woodpecker.svc.cluster.local"]
},
"subdomain": "wp-hsvc-11301",
"hostname": "toleration-test"
},
"status": {}
}`
@@ -782,7 +813,12 @@ func TestPodTolerationsAllowFromStep(t *testing.T) {
"value": "value",
"effect": "NoSchedule"
}
]
],
"dnsConfig": {
"searches": ["wp-hsvc-11301.woodpecker.svc.cluster.local"]
},
"subdomain": "wp-hsvc-11301",
"hostname": "toleration-test"
},
"status": {}
}`

View File

@@ -16,93 +16,79 @@ package kubernetes
import (
"context"
"fmt"
"strings"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
int_str "k8s.io/apimachinery/pkg/util/intstr"
"go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types"
)
const (
ServiceLabel = "service"
servicePrefix = "wp-svc-"
ServiceLabel = "service"
HeadlessServicePrefix = "wp-hsvc-"
ServicePrefix = "wp-svc-"
)
func mkService(step *types.Step, config *config) (*v1.Service, error) {
name, err := serviceName(step)
func mkHeadlessService(namespace, taskUUID string) (*v1.Service, error) {
selector := map[string]string{
TaskUUIDLabel: taskUUID,
}
name, err := subdomain(taskUUID)
if err != nil {
return nil, err
}
selector := map[string]string{
ServiceLabel: name,
}
if len(step.Ports) == 0 {
return nil, fmt.Errorf("kubernetes backend requires explicitly exposed ports for service steps, add 'ports' configuration to step '%s'", step.Name)
}
var svcPorts []v1.ServicePort
for _, port := range step.Ports {
svcPorts = append(svcPorts, servicePort(port))
}
log.Trace().Str("name", name).Interface("selector", selector).Interface("ports", svcPorts).Msg("creating service")
log.Trace().Str("name", name).Interface("selector", selector).Msg("creating headless service")
return &v1.Service{
ObjectMeta: meta_v1.ObjectMeta{
Name: name,
Namespace: config.GetNamespace(step.OrgID),
Namespace: namespace,
},
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeClusterIP,
Selector: selector,
Ports: svcPorts,
Type: v1.ServiceTypeClusterIP,
ClusterIP: "None",
Selector: selector,
},
}, nil
}
func serviceName(step *types.Step) (string, error) {
return dnsName(servicePrefix + step.UUID + "-" + step.Name)
return dnsName(ServicePrefix + step.UUID + "-" + step.Name)
}
func servicePort(port types.Port) v1.ServicePort {
portNumber := int32(port.Number)
portProtocol := strings.ToUpper(port.Protocol)
return v1.ServicePort{
Name: fmt.Sprintf("port-%d", portNumber),
Port: portNumber,
Protocol: v1.Protocol(portProtocol),
TargetPort: int_str.IntOrString{IntVal: portNumber},
}
func isService(step *types.Step) bool {
return step.Type == types.StepTypeService || (step.Detached && dnsPattern.FindStringIndex(step.Name) != nil)
}
func startService(ctx context.Context, engine *kube, step *types.Step) (*v1.Service, error) {
engineConfig := engine.getConfig()
svc, err := mkService(step, engineConfig)
func subdomain(taskUUID string) (string, error) {
return dnsName(HeadlessServicePrefix + taskUUID)
}
func startHeadlessService(ctx context.Context, engine *kube, namespace, taskUUID string) (*v1.Service, error) {
svc, err := mkHeadlessService(namespace, taskUUID)
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(engineConfig.GetNamespace(step.OrgID)).Create(ctx, svc, meta_v1.CreateOptions{})
log.Trace().Str("name", svc.Name).Interface("selector", svc.Spec.Selector).Msg("creating headless service")
return engine.client.CoreV1().Services(namespace).Create(ctx, svc, meta_v1.CreateOptions{})
}
func stopService(ctx context.Context, engine *kube, step *types.Step, deleteOpts meta_v1.DeleteOptions) error {
svcName, err := serviceName(step)
func stopHeadlessService(ctx context.Context, engine *kube, namespace, taskUUID string) error {
name, err := subdomain(taskUUID)
if err != nil {
return err
}
log.Trace().Str("name", svcName).Msg("deleting service")
err = engine.client.CoreV1().Services(engine.config.GetNamespace(step.OrgID)).Delete(ctx, svcName, deleteOpts)
log.Trace().Str("name", name).Msg("deleting headless service")
err = engine.client.CoreV1().Services(namespace).Delete(ctx, name, defaultDeleteOptions)
if errors.IsNotFound(err) {
// Don't abort on 404 errors from k8s, they most likely mean that the pod hasn't been created yet, usually because pipeline was canceled before running all steps.
log.Trace().Err(err).Msgf("unable to delete service %s", svcName)
log.Trace().Err(err).Msgf("unable to delete headless service %s", name)
return nil
}
return err

View File

@@ -15,10 +15,14 @@
package kubernetes
import (
"context"
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
"go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types"
)
@@ -37,54 +41,111 @@ func TestServiceName(t *testing.T) {
assert.Equal(t, "wp-svc-01he8bebctabr3kgk0qj36d2me-awesome-service", name)
}
func TestService(t *testing.T) {
func TestHeadlessService(t *testing.T) {
expected := `
{
"metadata": {
"name": "wp-svc-01he8bebctabr3kgk0qj36d2me-0-bar",
"namespace": "foo"
"name": "wp-hsvc-11301",
"namespace": "foo"
},
"spec": {
"ports": [
{
"name": "port-1",
"port": 1,
"targetPort": 1
},
{
"name": "port-2",
"protocol": "TCP",
"port": 2,
"targetPort": 2
},
{
"name": "port-3",
"protocol": "UDP",
"port": 3,
"targetPort": 3
}
],
"selector": {
"service": "wp-svc-01he8bebctabr3kgk0qj36d2me-0-bar"
},
"type": "ClusterIP"
"selector": {
"woodpecker-ci.org/task-uuid": "11301"
},
"clusterIP": "None",
"type": "ClusterIP"
},
"status": {
"loadBalancer": {}
"loadBalancer": {}
}
}`
ports := []types.Port{
{Number: 1},
{Number: 2, Protocol: "tcp"},
{Number: 3, Protocol: "udp"},
}
s, err := mkService(&types.Step{
Name: "bar",
UUID: "01he8bebctabr3kgk0qj36d2me-0",
Ports: ports,
}, &config{Namespace: "foo"})
assert.NoError(t, err)
s, err := mkHeadlessService("foo", "11301")
assert.NoError(t, err, "expected no error when creating headless service")
j, err := json.Marshal(s)
assert.NoError(t, err)
assert.JSONEq(t, expected, string(j))
assert.NoError(t, err, "expected no error when marshaling headless service to JSON")
assert.JSONEq(t, expected, string(j), "expected headless service JSON to match")
}
func TestInvalidHeadlessService(t *testing.T) {
_, err := mkHeadlessService("foo", "invalid_task_uuid!")
assert.Error(t, err, "expected error due to invalid task UUID")
}
func TestStartHeadlessService(t *testing.T) {
t.Run("successfully creates headless service", func(t *testing.T) {
engine := &kube{
client: fake.NewClientset(),
config: &config{Namespace: "test-namespace"},
}
svc, err := startHeadlessService(context.Background(), engine, "foo", "11301")
assert.NoError(t, err, "expected no error when starting headless service")
assert.NotNil(t, svc, "expected headless service to be created")
assert.Equal(t, "wp-hsvc-11301", svc.Name, "expected headless service name to match")
assert.Equal(t, "foo", svc.Namespace, "expected headless service namespace to match")
assert.Equal(t, v1.ServiceTypeClusterIP, svc.Spec.Type, "expected headless service type to be ClusterIP")
assert.Equal(t, "None", svc.Spec.ClusterIP, "expected headless service ClusterIP to be 'None'")
assert.Equal(t, map[string]string{TaskUUIDLabel: "11301"}, svc.Spec.Selector)
createdSvc, err := engine.client.CoreV1().Services("foo").Get(context.Background(), "wp-hsvc-11301", meta_v1.GetOptions{})
assert.NoError(t, err, "expected no error when getting the created service")
assert.Equal(t, svc.Name, createdSvc.Name, "expected created service name to match")
})
t.Run("error on invalid task UUID resulting in invalid domain-name", func(t *testing.T) {
engine := &kube{
client: fake.NewClientset(),
config: &config{Namespace: "test-namespace"},
}
_, err := startHeadlessService(context.Background(), engine, "test-namespace", "invalid_task_uuid!")
assert.Error(t, err, "expected error due to invalid task UUID")
})
}
func TestStopHeadlessService(t *testing.T) {
t.Run("successfully deletes headless service", func(t *testing.T) {
engine := &kube{
client: fake.NewClientset(),
config: &config{Namespace: "test-namespace"},
}
// arrage
_, err := startHeadlessService(context.Background(), engine, "foo", "11301")
assert.NoError(t, err, "expected no error when starting headless service")
_, err = engine.client.CoreV1().Services("foo").Get(context.Background(), "wp-hsvc-11301", meta_v1.GetOptions{})
assert.NoError(t, err, "expected no error when getting the created service")
// act
err = stopHeadlessService(context.Background(), engine, "foo", "11301")
assert.NoError(t, err, "expected no error when deleting headless service")
// assert
_, err = engine.client.CoreV1().Services("foo").Get(context.Background(), "wp-hsvc-11301", meta_v1.GetOptions{})
assert.Error(t, err, "expected error when getting a deleted service")
assert.True(t, err != nil, "expected error to be non-nil")
})
t.Run("handles non-existent service gracefully", func(t *testing.T) {
engine := &kube{
client: fake.NewClientset(),
config: &config{Namespace: "test-namespace"},
}
err := stopHeadlessService(context.Background(), engine, "foo", "nonexistent")
assert.NoError(t, err, "expected no error when deleting a non-existent service")
})
t.Run("error on invalid task UUID resulting in invalid domain-name", func(t *testing.T) {
engine := &kube{
client: fake.NewClientset(),
config: &config{Namespace: "test-namespace"},
}
err := stopHeadlessService(context.Background(), engine, "test-namespace", "invalid_task_uuid!")
assert.Error(t, err, "expected error due to invalid task UUID")
})
}

View File

@@ -24,8 +24,6 @@ import (
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
client_cmd "k8s.io/client-go/tools/clientcmd"
"go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types"
)
var (
@@ -105,10 +103,6 @@ func getClientInsideOfCluster() (kubernetes.Interface, error) {
return kubernetes.NewForConfig(config)
}
func isService(step *types.Step) bool {
return step.Type == types.StepTypeService || (step.Detached && dnsPattern.FindStringIndex(step.Name) != nil)
}
func newBool(val bool) *bool {
ptr := new(bool)
*ptr = val