Gate Kubernetes nodeSelector backend step config behind agent config (#6809)

Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: Lauris B <lauris@nix.lv>
This commit is contained in:
kt
2026-07-06 03:06:31 +02:00
committed by GitHub
co-authored by 6543 Lauris B
parent c0aec32051
commit 4fada4b5d5
6 changed files with 90 additions and 18 deletions
@@ -607,6 +607,15 @@ Additional node selector to apply to worker pods. Must be a YAML object, e.g. `{
---
### BACKEND_K8S_POD_NODE_SELECTOR_ALLOW_FROM_STEP
- Name: `WOODPECKER_BACKEND_K8S_POD_NODE_SELECTOR_ALLOW_FROM_STEP`
- Default: `false`
Determines if the Pod `nodeSelector` can be defined from a step's backend options. Disabled by default, as it would otherwise let any user with push access pin pipeline pods onto chosen nodes.
---
### BACKEND_K8S_SECCTX_NONROOT <!-- cspell:ignore SECCTX NONROOT -->
- Name: `WOODPECKER_BACKEND_K8S_SECCTX_NONROOT`
+6
View File
@@ -91,6 +91,12 @@ var Flags = []cli.Flag{
Usage: "whether to allow using tolerations from step's backend options",
Value: true,
},
&cli.BoolFlag{
Sources: cli.EnvVars("WOODPECKER_BACKEND_K8S_POD_NODE_SELECTOR_ALLOW_FROM_STEP"),
Name: "backend-k8s-pod-node-selector-allow-from-step",
Usage: "whether to allow using node selector from step's backend options",
Value: false,
},
&cli.StringFlag{
Sources: cli.EnvVars("WOODPECKER_BACKEND_K8S_POD_AFFINITY"),
Name: "backend-k8s-pod-affinity",
+16 -14
View File
@@ -69,6 +69,7 @@ type config struct {
PodAnnotations map[string]string
PodAnnotationsAllowFromStep bool
PodNodeSelector map[string]string
PodNodeSelectorAllowFromStep bool
PodTolerationsAllowFromStep bool
PodTolerations []Toleration
PodAffinity *kube_core_v1.Affinity
@@ -107,20 +108,21 @@ func configFromCliContext(ctx context.Context) (*config, error) {
if ctx != nil {
if c, ok := ctx.Value(types.CliCommand).(*cli.Command); ok {
config := config{
Namespace: c.String("backend-k8s-namespace"),
EnableNamespacePerOrg: c.Bool("backend-k8s-namespace-per-org"),
StorageClass: c.String("backend-k8s-storage-class"),
VolumeSize: c.String("backend-k8s-volume-size"),
StorageRwx: c.Bool("backend-k8s-storage-rwx"),
PriorityClassName: c.String("backend-k8s-priority-class"),
PodLabels: make(map[string]string), // just init empty map to prevent nil panic
PodLabelsAllowFromStep: c.Bool("backend-k8s-pod-labels-allow-from-step"),
PodAnnotations: make(map[string]string), // just init empty map to prevent nil panic
PodAnnotationsAllowFromStep: c.Bool("backend-k8s-pod-annotations-allow-from-step"),
PodTolerationsAllowFromStep: c.Bool("backend-k8s-pod-tolerations-allow-from-step"),
PodNodeSelector: make(map[string]string), // just init empty map to prevent nil panic
PodAffinityAllowFromStep: c.Bool("backend-k8s-pod-affinity-allow-from-step"),
ImagePullSecretNames: c.StringSlice("backend-k8s-pod-image-pull-secret-names"),
Namespace: c.String("backend-k8s-namespace"),
EnableNamespacePerOrg: c.Bool("backend-k8s-namespace-per-org"),
StorageClass: c.String("backend-k8s-storage-class"),
VolumeSize: c.String("backend-k8s-volume-size"),
StorageRwx: c.Bool("backend-k8s-storage-rwx"),
PriorityClassName: c.String("backend-k8s-priority-class"),
PodLabels: make(map[string]string), // just init empty map to prevent nil panic
PodLabelsAllowFromStep: c.Bool("backend-k8s-pod-labels-allow-from-step"),
PodAnnotations: make(map[string]string), // just init empty map to prevent nil panic
PodAnnotationsAllowFromStep: c.Bool("backend-k8s-pod-annotations-allow-from-step"),
PodTolerationsAllowFromStep: c.Bool("backend-k8s-pod-tolerations-allow-from-step"),
PodNodeSelectorAllowFromStep: c.Bool("backend-k8s-pod-node-selector-allow-from-step"),
PodNodeSelector: make(map[string]string), // just init empty map to prevent nil panic
PodAffinityAllowFromStep: c.Bool("backend-k8s-pod-affinity-allow-from-step"),
ImagePullSecretNames: c.StringSlice("backend-k8s-pod-image-pull-secret-names"),
SecurityContext: SecurityContextConfig{
RunAsNonRoot: c.Bool("backend-k8s-secctx-nonroot"), // cspell:words secctx nonroot
FSGroup: newInt64(defaultFSGroup),
@@ -0,0 +1,50 @@
// Copyright 2026 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"
"go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types"
)
func TestNodeSelectorAllowFromStep(t *testing.T) {
step := &types.Step{
Name: "ns-test",
Image: "alpine",
UUID: "01he8bebctabr3kgk0qj36d2me-0",
}
// When disabled (default), a step-provided node selector must be ignored.
pod, err := mkPod(step, &config{
Namespace: "woodpecker",
}, "wp-01he8bebctabr3kgk0qj36d2me-0", "linux/amd64", BackendOptions{
NodeSelector: map[string]string{"attacker-target": "sensitive-node"},
}, taskUUID)
assert.NoError(t, err)
assert.NotContains(t, pod.Spec.NodeSelector, "attacker-target")
// When explicitly enabled by the admin, the step value is honored.
pod, err = mkPod(step, &config{
Namespace: "woodpecker",
PodNodeSelectorAllowFromStep: true,
}, "wp-01he8bebctabr3kgk0qj36d2me-0", "linux/amd64", BackendOptions{
NodeSelector: map[string]string{"attacker-target": "sensitive-node"},
}, taskUUID)
assert.NoError(t, err)
assert.Equal(t, "sensitive-node", pod.Spec.NodeSelector["attacker-target"])
}
+8 -4
View File
@@ -193,7 +193,7 @@ func podSpec(step *types.Step, config *config, options BackendOptions, nsp nativ
Hostname: getHostnameOrEmpty(step.Name),
Subdomain: subdomain,
DNSConfig: dnsConfig(config.GetNamespace(step.OrgID), subdomain),
NodeSelector: nodeSelector(options.NodeSelector, config.PodNodeSelector, step.Environment["CI_SYSTEM_PLATFORM"]),
NodeSelector: nodeSelector(options.NodeSelector, config.PodNodeSelector, config.PodNodeSelectorAllowFromStep, step.Environment["CI_SYSTEM_PLATFORM"]),
Tolerations: tolerations(options.Tolerations),
Affinity: affinity(options.Affinity, config.PodAffinity, config.PodAffinityAllowFromStep),
SecurityContext: podSecurityContext(options.SecurityContext, config.SecurityContext, step.Privileged, options.HostUsers),
@@ -512,7 +512,7 @@ func resourceList(resources map[string]string) (kube_core_v1.ResourceList, error
return requestResources, nil
}
func nodeSelector(backendNodeSelector, configNodeSelector map[string]string, platform string) map[string]string {
func nodeSelector(backendNodeSelector, configNodeSelector map[string]string, allowFromStep bool, platform string) map[string]string {
nodeSelector := make(map[string]string)
if platform != "" {
@@ -527,8 +527,12 @@ func nodeSelector(backendNodeSelector, configNodeSelector map[string]string, pla
}
if len(backendNodeSelector) > 0 {
log.Trace().Msgf("appending labels to the node selector from the backend options: %v", backendNodeSelector)
maps.Copy(nodeSelector, backendNodeSelector)
if allowFromStep {
log.Trace().Msgf("appending labels to the node selector from the backend options: %v", backendNodeSelector)
maps.Copy(nodeSelector, backendNodeSelector)
} else {
log.Debug().Msg("Step node selector is disallowed by instance configuration, ignoring it")
}
}
return nodeSelector
+1
View File
@@ -466,6 +466,7 @@ func TestFullPod(t *testing.T) {
PodAnnotationsAllowFromStep: true,
PodTolerationsAllowFromStep: true,
PodNodeSelector: map[string]string{"topology.kubernetes.io/region": "eu-central-1"},
PodNodeSelectorAllowFromStep: true,
SecurityContext: SecurityContextConfig{RunAsNonRoot: false},
ServiceAccountNameAllowFromStep: true,
},