Skip to content

Commit

Permalink
K8s secrets reference from step (woodpecker-ci#3655)
Browse files Browse the repository at this point in the history
  • Loading branch information
zc-devs authored and 6543 committed Sep 5, 2024
1 parent b4339bc commit 955b8bf
Show file tree
Hide file tree
Showing 9 changed files with 607 additions and 29 deletions.
14 changes: 14 additions & 0 deletions pipeline/backend/kubernetes/backend_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type BackendOptions struct {
NodeSelector map[string]string `mapstructure:"nodeSelector"`
Tolerations []Toleration `mapstructure:"tolerations"`
SecurityContext *SecurityContext `mapstructure:"securityContext"`
Secrets []SecretRef `mapstructure:"secrets"`
}

// Resources defines two maps for kubernetes resource definitions.
Expand Down Expand Up @@ -65,6 +66,19 @@ type SecProfile struct {

type SecProfileType string

// SecretRef defines Kubernetes secret reference.
type SecretRef struct {
Name string `mapstructure:"name"`
Key string `mapstructure:"key"`
Target SecretTarget `mapstructure:"target"`
}

// SecretTarget defines secret mount target.
type SecretTarget struct {
Env string `mapstructure:"env"`
File string `mapstructure:"file"`
}

const (
SecProfileTypeRuntimeDefault SecProfileType = "RuntimeDefault"
SecProfileTypeLocalhost SecProfileType = "Localhost"
Expand Down
28 changes: 28 additions & 0 deletions pipeline/backend/kubernetes/backend_options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,22 @@ func Test_parseBackendOptions(t *testing.T) {
"localhostProfile": "k8s-apparmor-example-deny-write",
},
},
"secrets": []map[string]any{
{
"name": "aws",
"key": "access-key",
"target": map[string]any{
"env": "AWS_SECRET_ACCESS_KEY",
},
},
{
"name": "reg-cred",
"key": ".dockerconfigjson",
"target": map[string]any{
"file": "~/.docker/config.json",
},
},
},
},
},
})
Expand Down Expand Up @@ -73,5 +89,17 @@ func Test_parseBackendOptions(t *testing.T) {
LocalhostProfile: "k8s-apparmor-example-deny-write",
},
},
Secrets: []SecretRef{
{
Name: "aws",
Key: "access-key",
Target: SecretTarget{Env: "AWS_SECRET_ACCESS_KEY"},
},
{
Name: "reg-cred",
Key: ".dockerconfigjson",
Target: SecretTarget{File: "~/.docker/config.json"},
},
},
}, got)
}
10 changes: 7 additions & 3 deletions pipeline/backend/kubernetes/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@

package kubernetes

import (
"github.com/urfave/cli/v2"
)
import "github.com/urfave/cli/v2"

var Flags = []cli.Flag{
&cli.StringFlag{
Expand Down Expand Up @@ -84,4 +82,10 @@ var Flags = []cli.Flag{
Usage: "backend k8s pull secret names for private registries",
Value: cli.NewStringSlice("regcred"),
},
&cli.BoolFlag{
EnvVars: []string{"WOODPECKER_BACKEND_K8S_ALLOW_NATIVE_SECRETS"},
Name: "backend-k8s-allow-native-secrets",
Usage: "whether to allow existing Kubernetes secrets to be referenced from steps",
Value: false,
},
}
2 changes: 2 additions & 0 deletions pipeline/backend/kubernetes/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ type config struct {
PodNodeSelector map[string]string
ImagePullSecretNames []string
SecurityContext SecurityContextConfig
NativeSecretsAllowFromStep bool
}
type SecurityContextConfig struct {
RunAsNonRoot bool
Expand Down Expand Up @@ -97,6 +98,7 @@ func configFromCliContext(ctx context.Context) (*config, error) {
SecurityContext: SecurityContextConfig{
RunAsNonRoot: c.Bool("backend-k8s-secctx-nonroot"), // cspell:words secctx nonroot
},
NativeSecretsAllowFromStep: c.Bool("backend-k8s-allow-native-secrets"),
}
// TODO: remove in next major
if len(config.ImagePullSecretNames) == 1 && config.ImagePullSecretNames[0] == "regcred" {
Expand Down
48 changes: 23 additions & 25 deletions pipeline/backend/kubernetes/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,23 @@ const (
func mkPod(step *types.Step, config *config, podName, goos string, options BackendOptions) (*v1.Pod, error) {
var err error

nsp := newNativeSecretsProcessor(config, options.Secrets)
err = nsp.process()
if err != nil {
return nil, err
}

meta, err := podMeta(step, config, options, podName)
if err != nil {
return nil, err
}

spec, err := podSpec(step, config, options)
spec, err := podSpec(step, config, options, nsp)
if err != nil {
return nil, err
}

container, err := podContainer(step, podName, goos, options)
container, err := podContainer(step, podName, goos, options, nsp)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -146,27 +152,31 @@ func podAnnotations(config *config, options BackendOptions, podName string) map[
return annotations
}

func podSpec(step *types.Step, config *config, options BackendOptions) (v1.PodSpec, error) {
func podSpec(step *types.Step, config *config, options BackendOptions, nsp nativeSecretsProcessor) (v1.PodSpec, error) {
var err error
spec := v1.PodSpec{
RestartPolicy: v1.RestartPolicyNever,
RuntimeClassName: options.RuntimeClassName,
ServiceAccountName: options.ServiceAccountName,
ImagePullSecrets: imagePullSecretsReferences(config.ImagePullSecretNames),
HostAliases: hostAliases(step.ExtraHosts),
NodeSelector: nodeSelector(options.NodeSelector, config.PodNodeSelector, step.Environment["CI_SYSTEM_PLATFORM"]),
Tolerations: tolerations(options.Tolerations),
SecurityContext: podSecurityContext(options.SecurityContext, config.SecurityContext, step.Privileged),
}
spec.Volumes, err = volumes(step.Volumes)
spec.Volumes, err = pvcVolumes(step.Volumes)
if err != nil {
return spec, err
}

log.Trace().Msgf("using the image pull secrets: %v", config.ImagePullSecretNames)
spec.ImagePullSecrets = secretsReferences(config.ImagePullSecretNames)

spec.Volumes = append(spec.Volumes, nsp.volumes...)

return spec, nil
}

func podContainer(step *types.Step, podName, goos string, options BackendOptions) (v1.Container, error) {
func podContainer(step *types.Step, podName, goos string, options BackendOptions, nsp nativeSecretsProcessor) (v1.Container, error) {
var err error
container := v1.Container{
Name: podName,
Expand Down Expand Up @@ -201,24 +211,28 @@ func podContainer(step *types.Step, podName, goos string, options BackendOptions
return container, err
}

container.EnvFrom = append(container.EnvFrom, nsp.envFromSources...)
container.Env = append(container.Env, nsp.envVars...)
container.VolumeMounts = append(container.VolumeMounts, nsp.mounts...)

return container, nil
}

func volumes(volumes []string) ([]v1.Volume, error) {
func pvcVolumes(volumes []string) ([]v1.Volume, error) {
var vols []v1.Volume

for _, v := range volumes {
volumeName, err := volumeName(v)
if err != nil {
return nil, err
}
vols = append(vols, volume(volumeName))
vols = append(vols, pvcVolume(volumeName))
}

return vols, nil
}

func volume(name string) v1.Volume {
func pvcVolume(name string) v1.Volume {
pvcSource := v1.PersistentVolumeClaimVolumeSource{
ClaimName: name,
ReadOnly: false,
Expand Down Expand Up @@ -285,22 +299,6 @@ func hostAlias(extraHost types.HostAlias) v1.HostAlias {
}
}

func imagePullSecretsReferences(imagePullSecretNames []string) []v1.LocalObjectReference {
log.Trace().Msgf("using the image pull secrets: %v", imagePullSecretNames)

secretReferences := make([]v1.LocalObjectReference, len(imagePullSecretNames))
for i, imagePullSecretName := range imagePullSecretNames {
secretReferences[i] = imagePullSecretsReference(imagePullSecretName)
}
return secretReferences
}

func imagePullSecretsReference(imagePullSecretName string) v1.LocalObjectReference {
return v1.LocalObjectReference{
Name: imagePullSecretName,
}
}

func resourceRequirements(resources Resources) (v1.ResourceRequirements, error) {
var err error
requirements := v1.ResourceRequirements{}
Expand Down
120 changes: 119 additions & 1 deletion pipeline/backend/kubernetes/pod_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,124 @@ func TestScratchPod(t *testing.T) {
assert.NoError(t, err)

ja := jsonassert.New(t)
t.Log(string(podJSON))
ja.Assertf(string(podJSON), expected)
}

func TestSecrets(t *testing.T) {
expected := `
{
"metadata": {
"name": "wp-3kgk0qj36d2me01he8bebctabr-0",
"namespace": "woodpecker",
"creationTimestamp": null,
"labels": {
"step": "test-secrets"
}
},
"spec": {
"volumes": [
{
"name": "workspace",
"persistentVolumeClaim": {
"claimName": "workspace"
}
},
{
"name": "reg-cred",
"secret": {
"secretName": "reg-cred"
}
}
],
"containers": [
{
"name": "wp-3kgk0qj36d2me01he8bebctabr-0",
"image": "alpine",
"envFrom": [
{
"secretRef": {
"name": "ghcr-push-secret"
}
}
],
"env": [
{
"name": "CGO",
"value": "0"
},
{
"name": "AWS_ACCESS_KEY_ID",
"valueFrom": {
"secretKeyRef": {
"name": "aws-ecr",
"key": "AWS_ACCESS_KEY_ID"
}
}
},
{
"name": "AWS_SECRET_ACCESS_KEY",
"valueFrom": {
"secretKeyRef": {
"name": "aws-ecr",
"key": "access-key"
}
}
}
],
"resources": {},
"volumeMounts": [
{
"name": "workspace",
"mountPath": "/woodpecker/src"
},
{
"name": "reg-cred",
"mountPath": "~/.docker/config.json",
"subPath": ".dockerconfigjson",
"readOnly": true
}
]
}
],
"restartPolicy": "Never"
},
"status": {}
}`

pod, err := mkPod(&types.Step{
Name: "test-secrets",
Image: "alpine",
Environment: map[string]string{"CGO": "0"},
Volumes: []string{"workspace:/woodpecker/src"},
}, &config{
Namespace: "woodpecker",
NativeSecretsAllowFromStep: true,
}, "wp-3kgk0qj36d2me01he8bebctabr-0", "linux/amd64", BackendOptions{
Secrets: []SecretRef{
{
Name: "ghcr-push-secret",
},
{
Name: "aws-ecr",
Key: "AWS_ACCESS_KEY_ID",
},
{
Name: "aws-ecr",
Key: "access-key",
Target: SecretTarget{Env: "AWS_SECRET_ACCESS_KEY"},
},
{
Name: "reg-cred",
Key: ".dockerconfigjson",
Target: SecretTarget{File: "~/.docker/config.json"},
},
},
})
assert.NoError(t, err)

podJSON, err := json.Marshal(pod)
assert.NoError(t, err)

ja := jsonassert.New(t)
ja.Assertf(string(podJSON), expected)
}
Loading

0 comments on commit 955b8bf

Please sign in to comment.