initial import

This commit is contained in:
2025-10-15 11:57:36 +02:00
commit 30e0debf95
13 changed files with 808 additions and 0 deletions

60
README.md Normal file
View File

@@ -0,0 +1,60 @@
# ImagePullSecrets Webhook
This is a Kubernetes Mutating Admission Webhook that automatically adds imagePullSecrets to pods based on the registry they're using.
## Features
- Automatically injects imagePullSecrets into pods
- Configurable registry-to-secret mappings
- Ignores public registries by default
- Supports both regular and init containers
- Prevents duplicate secret injection
- Uses cert-manager for certificate management
## Configuration
The webhook can be configured using environment variables:
- `DEFAULT_SECRET`: Default secret to use for private registries
- `IGNORED_REGISTRIES`: Comma-separated list of registries that don't need secrets
- `REGISTRY_MAPPINGS`: Specific registry-to-secret mappings (format: "registry1:secret1,registry2:secret2")
## Prerequisites
- cert-manager installed in the cluster
- A ClusterIssuer named "selfsigned-cluster-issuer" (or update the Certificate resource to use a different issuer)
## Building
To build the Docker image:
```bash
./build.sh
```
## How It Works
1. The webhook examines each container's image to determine which registry it's using
2. Based on the registry, it determines which imagePullSecret to inject
3. Using JSON Patch, it adds the appropriate imagePullSecrets to the pod specification
4. Only adds secrets for private registries, leaving public images unaffected
## Certificate Management
This webhook uses cert-manager to automatically generate and manage TLS certificates:
1. A Certificate resource requests a certificate from the selfsigned-cluster-issuer
2. cert-manager creates a Secret containing the TLS certificate and key
3. The webhook server mounts this secret and uses the certificates
4. The MutatingWebhookConfiguration uses the cert-manager annotation to automatically inject the CA bundle
## Deployment
The webhook is deployed using standard Kubernetes manifests:
- Namespace
- Certificate (for cert-manager)
- Deployment
- Service
- MutatingWebhookConfiguration
- ImagePullSecret (to be injected into pods)

7
build.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
# Build the Docker image
docker build --platform linux/amd64 -t harbor.cl1.parano.ch/library/imagepullsecrets-webhook:latest src/
# Push the Docker image to local registry
docker push harbor.cl1.parano.ch/library/imagepullsecrets-webhook:latest

27
deploy/certs.yaml Normal file
View File

@@ -0,0 +1,27 @@
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: imagepullsecrets-webhook-cert
namespace: imagepullsecrets-system
spec:
secretName: imagepullsecrets-webhook-certs
duration: 2160h # 90d
renewBefore: 360h # 15d
subject:
organizations:
- imagepullsecrets-webhook
isCA: false
privateKey:
algorithm: RSA
encoding: PKCS1
size: 2048
usages:
- digital signature
- key encipherment
dnsNames:
- imagepullsecrets-webhook.imagepullsecrets-system.svc
- imagepullsecrets-webhook.imagepullsecrets-system.svc.cluster.local
issuerRef:
name: selfsigned-cluster-issuer
kind: ClusterIssuer
group: cert-manager.io

45
deploy/deployment.yaml Normal file
View File

@@ -0,0 +1,45 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: imagepullsecrets-webhook
namespace: imagepullsecrets-system
labels:
app: imagepullsecrets-webhook
spec:
replicas: 2
selector:
matchLabels:
app: imagepullsecrets-webhook
template:
metadata:
labels:
app: imagepullsecrets-webhook
spec:
serviceAccountName: imagepullsecrets-webhook
containers:
- name: webhook-server
image: imagepullsecrets-webhook
imagePullPolicy: Always
envFrom:
- configMapRef:
name: imagepullsecrets-webhook-config
env:
- name: TLS_CERT_FILE
value: "/etc/webhook/certs/tls.crt"
- name: TLS_PRIVATE_KEY_FILE
value: "/etc/webhook/certs/tls.key"
ports:
- containerPort: 8443
volumeMounts:
- name: webhook-certs
mountPath: /etc/webhook/certs
readOnly: true
volumes:
- name: webhook-certs
secret:
secretName: imagepullsecrets-webhook-certs
securityContext:
runAsNonRoot: true
runAsUser: 1000
imagePullSecrets:
- name: regcred-harbor

13
deploy/kustomization.yaml Normal file
View File

@@ -0,0 +1,13 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- certs.yaml
- rbac.yaml
- deployment.yaml
- service.yaml
- mutatingwebhook.yaml
images:
- name: imagepullsecrets-webhook
newName: "harbor.cl1.parano.ch/library/imagepullsecrets-webhook@sha256"
newTag: "f3c2a78782ebc195305f4766376985d7563a6a107c57b6c62dbf80a7e10c39b7"

View File

@@ -0,0 +1,22 @@
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: imagepullsecrets-mutator
annotations:
cert-manager.io/inject-ca-from: imagepullsecrets-system/imagepullsecrets-webhook-cert
webhooks:
- name: imagepullsecrets.imagepullsecrets-system.svc
clientConfig:
service:
name: imagepullsecrets-webhook
namespace: imagepullsecrets-system
path: "/mutate"
rules:
- operations: ["CREATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
failurePolicy: Ignore
sideEffects: None
admissionReviewVersions: ["v1", "v1beta1"]
timeoutSeconds: 5

4
deploy/namespace.yaml Normal file
View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: imagepullsecrets-system

30
deploy/rbac.yaml Normal file
View File

@@ -0,0 +1,30 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: imagepullsecrets-webhook
namespace: imagepullsecrets-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: imagepullsecrets-webhook
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "create"]
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: imagepullsecrets-webhook
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: imagepullsecrets-webhook
subjects:
- kind: ServiceAccount
name: imagepullsecrets-webhook
namespace: imagepullsecrets-system

13
deploy/service.yaml Normal file
View File

@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: imagepullsecrets-webhook
namespace: imagepullsecrets-system
labels:
app: imagepullsecrets-webhook
spec:
ports:
- port: 443
targetPort: 8443
selector:
app: imagepullsecrets-webhook

14
src/Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY *.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o webhook .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/webhook .
CMD ["./webhook"]

47
src/go.mod Normal file
View File

@@ -0,0 +1,47 @@
module github.com/yourorg/imagepullsecrets-webhook
go 1.21
require (
k8s.io/api v0.28.0
k8s.io/apimachinery v0.28.0
k8s.io/client-go v0.28.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
golang.org/x/net v0.13.0 // indirect
golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/term v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.100.1 // indirect
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

154
src/go.sum Normal file
View File

@@ -0,0 +1,154 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE=
github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE=
github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLywzIhbKM=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.28.0 h1:3j3VPWmN9tTDI68NETBWlDiA9qOiGJ7sdKeufehBYsM=
k8s.io/api v0.28.0/go.mod h1:0l8NZJzB0i/etuWnIXcwfIv+xnDOhL3lLW919AWYDuY=
k8s.io/apimachinery v0.28.0 h1:ScHS2AG16UlYWk63r46oU3D5y54T53cVI5mMJwwqFNA=
k8s.io/apimachinery v0.28.0/go.mod h1:X0xh/chESs2hP9koe+SdIAcXWcQ+RM5hy0ZynB+yEvw=
k8s.io/client-go v0.28.0 h1:ebcPRDZsCjpj62+cMk1eGNX1QkMdRmQ6lmz5BLoFWeM=
k8s.io/client-go v0.28.0/go.mod h1:0Asy9Xt3U98RypWJmU1ZrRAGKhP6NqDPmptlAzK2kMc=
k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ=
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM=
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

372
src/main.go Normal file
View File

@@ -0,0 +1,372 @@
package main
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
v1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
var (
scheme = runtime.NewScheme()
codecs = serializer.NewCodecFactory(scheme)
clientset *kubernetes.Clientset
)
// Config holds the configuration for our webhook
type Config struct {
// Registry to secret mapping
RegistrySecrets map[string]string `json:"registrySecrets"`
// Default secret to use if no specific mapping exists
DefaultSecret string `json:"defaultSecret"`
// Registries to ignore (won't get secrets injected)
IgnoredRegistries []string `json:"ignoredRegistries"`
}
var config Config
func init() {
// Add core v1 types to the scheme
_ = corev1.AddToScheme(scheme)
_ = v1.AddToScheme(scheme)
// Load configuration from environment or config file
loadConfig()
}
func loadConfig() {
// Load from environment variables
config.DefaultSecret = os.Getenv("DEFAULT_SECRET")
// Parse registry to secret mappings
config.RegistrySecrets = make(map[string]string)
if mappings := os.Getenv("REGISTRY_MAPPINGS"); mappings != "" {
// Format: registry1:secret1,registry2:secret2
pairs := strings.Split(mappings, ",")
for _, pair := range pairs {
parts := strings.Split(pair, ":")
if len(parts) == 2 {
config.RegistrySecrets[parts[0]] = parts[1]
}
}
}
// Parse ignored registries
if ignored := os.Getenv("IGNORED_REGISTRIES"); ignored != "" {
config.IgnoredRegistries = strings.Split(ignored, ",")
} else {
// Default ignored registries
config.IgnoredRegistries = []string{
"k8s.gcr.io", "gcr.io", "quay.io", "docker.io",
}
}
}
// shouldIgnoreRegistry checks if a registry should be ignored
func shouldIgnoreRegistry(registry string) bool {
for _, ignored := range config.IgnoredRegistries {
if registry == ignored {
return true
}
}
return false
}
// getSecretForRegistry determines which secret to use for a given registry
func getSecretForRegistry(registry string) string {
// Check if there's a specific mapping for this registry
if secret, exists := config.RegistrySecrets[registry]; exists {
return secret
}
// Use default secret if available
if config.DefaultSecret != "" {
return config.DefaultSecret
}
// No secret to inject
return ""
}
// replicateSecret copies a secret from the source namespace to the target namespace
func replicateSecret(secretName, sourceNamespace, targetNamespace string) error {
// Check if the secret already exists in the target namespace
_, err := clientset.CoreV1().Secrets(targetNamespace).Get(context.TODO(), secretName, metav1.GetOptions{})
if err == nil {
// Secret already exists, no need to replicate
return nil
}
// Get the secret from the source namespace
sourceSecret, err := clientset.CoreV1().Secrets(sourceNamespace).Get(context.TODO(), secretName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get secret %s from namespace %s: %v", secretName, sourceNamespace, err)
}
// Create a new secret for the target namespace
targetSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: targetNamespace,
Labels: sourceSecret.Labels,
Annotations: map[string]string{
"replicated-from": sourceNamespace,
},
},
Data: sourceSecret.Data,
StringData: sourceSecret.StringData,
Type: sourceSecret.Type,
}
// Create the secret in the target namespace
_, err = clientset.CoreV1().Secrets(targetNamespace).Create(context.TODO(), targetSecret, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("failed to create secret %s in namespace %s: %v", secretName, targetNamespace, err)
}
fmt.Printf("Replicated secret %s from namespace %s to namespace %s\n", secretName, sourceNamespace, targetNamespace)
return nil
}
// extractRegistry extracts the registry from an image name
func extractRegistry(image string) string {
// Handle cases like:
// nginx -> docker.io (implicit)
// docker.io/nginx -> docker.io
// gcr.io/myproject/myimage -> gcr.io
// myregistry.example.com:5000/myimage -> myregistry.example.com
// Split by first slash to separate registry from image path
parts := strings.SplitN(image, "/", 2)
// If there's no slash or the first part doesn't look like a registry,
// it's probably from docker.io
if len(parts) == 1 || !strings.Contains(parts[0], ".") && !strings.Contains(parts[0], ":") {
return "docker.io"
}
// Return the registry part
return parts[0]
}
// mutatePod modifies the pod to add imagePullSecrets
func mutatePod(pod *corev1.Pod) ([]byte, error) {
var patches []map[string]interface{}
// Track secrets we want to add to avoid duplicates
secretsToAdd := make(map[string]bool)
// Check all containers in the pod
containers := pod.Spec.Containers
initContainers := pod.Spec.InitContainers
// Collect all secrets we want to add
// Process regular containers
for _, container := range containers {
registry := extractRegistry(container.Image)
// Skip if registry should be ignored
if shouldIgnoreRegistry(registry) {
continue
}
// Determine which secret to use
secretName := getSecretForRegistry(registry)
if secretName == "" {
continue
}
// Mark this secret to be added
secretsToAdd[secretName] = true
}
// Process init containers
for _, container := range initContainers {
registry := extractRegistry(container.Image)
// Skip if registry should be ignored
if shouldIgnoreRegistry(registry) {
continue
}
// Determine which secret to use
secretName := getSecretForRegistry(registry)
if secretName == "" {
continue
}
// Mark this secret to be added
secretsToAdd[secretName] = true
}
// If we have secrets to add, replicate them to the target namespace and create the patch
if len(secretsToAdd) > 0 {
// Replicate secrets to the target namespace
sourceNamespace := os.Getenv("SOURCE_NAMESPACE")
if sourceNamespace == "" {
sourceNamespace = "imagepullsecrets-system"
}
targetNamespace := pod.Namespace
for secretName := range secretsToAdd {
if err := replicateSecret(secretName, sourceNamespace, targetNamespace); err != nil {
fmt.Printf("Warning: failed to replicate secret %s: %v\n", secretName, err)
// Continue anyway, as the pod creation might still succeed if the secret exists
}
}
// Create a slice of LocalObjectReference from our secrets
var imagePullSecrets []corev1.LocalObjectReference
// Add existing imagePullSecrets first
for _, secret := range pod.Spec.ImagePullSecrets {
imagePullSecrets = append(imagePullSecrets, secret)
}
// Add new secrets
for secretName := range secretsToAdd {
imagePullSecrets = append(imagePullSecrets, corev1.LocalObjectReference{Name: secretName})
}
// Create the patch to replace the entire imagePullSecrets array
patches = append(patches, map[string]interface{}{
"op": "replace",
"path": "/spec/imagePullSecrets",
"value": imagePullSecrets,
})
}
// Convert patches to JSON
if len(patches) > 0 {
return json.Marshal(patches)
}
// No changes needed
return nil, nil
}
// handleMutate handles the admission review request
func handleMutate(w http.ResponseWriter, r *http.Request) {
var body []byte
if r.Body != nil {
if data, err := ioutil.ReadAll(r.Body); err == nil {
body = data
}
}
if len(body) == 0 {
http.Error(w, "empty body", http.StatusBadRequest)
return
}
// Verify the content type is correct
contentType := r.Header.Get("Content-Type")
if contentType != "application/json" {
http.Error(w, "invalid Content-Type, want `application/json`", http.StatusUnsupportedMediaType)
return
}
// Parse the AdmissionReview request
var admissionReviewReq v1.AdmissionReview
if _, _, err := codecs.UniversalDeserializer().Decode(body, nil, &admissionReviewReq); err != nil {
http.Error(w, fmt.Sprintf("could not deserialize request: %v", err), http.StatusBadRequest)
return
}
var admissionReviewResp v1.AdmissionReview
admissionReviewResp.SetGroupVersionKind(admissionReviewReq.GroupVersionKind())
admissionReviewResp.Response = &v1.AdmissionResponse{
UID: admissionReviewReq.Request.UID,
}
// Get the pod object from the request
var pod corev1.Pod
if err := json.Unmarshal(admissionReviewReq.Request.Object.Raw, &pod); err != nil {
admissionReviewResp.Response.Allowed = false
admissionReviewResp.Response.Result = &metav1.Status{
Message: err.Error(),
}
} else {
// Mutate the pod
patchBytes, err := mutatePod(&pod)
if err != nil {
admissionReviewResp.Response.Allowed = false
admissionReviewResp.Response.Result = &metav1.Status{
Message: err.Error(),
}
} else {
admissionReviewResp.Response.Allowed = true
if patchBytes != nil {
admissionReviewResp.Response.Patch = patchBytes
patchType := v1.PatchTypeJSONPatch
admissionReviewResp.Response.PatchType = &patchType
}
}
}
// Send the response
respBytes, err := json.Marshal(admissionReviewResp)
if err != nil {
http.Error(w, fmt.Sprintf("could not marshal response: %v", err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(respBytes)
}
func main() {
// Initialize Kubernetes client
config, err := rest.InClusterConfig()
if err != nil {
fmt.Printf("Failed to create in-cluster config: %v\n", err)
os.Exit(1)
}
clientset, err = kubernetes.NewForConfig(config)
if err != nil {
fmt.Printf("Failed to create Kubernetes client: %v\n", err)
os.Exit(1)
}
// Set up the HTTP server
http.HandleFunc("/mutate", handleMutate)
// Get certificate and key paths from environment
certFile := os.Getenv("TLS_CERT_FILE")
keyFile := os.Getenv("TLS_PRIVATE_KEY_FILE")
if certFile == "" || keyFile == "" {
fmt.Println("TLS_CERT_FILE and TLS_PRIVATE_KEY_FILE environment variables must be set")
os.Exit(1)
}
// Configure TLS
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
}
server := &http.Server{
Addr: ":8443",
TLSConfig: tlsConfig,
}
fmt.Println("Starting webhook server on :8443...")
if err := server.ListenAndServeTLS(certFile, keyFile); err != nil {
fmt.Printf("Failed to listen and serve webhook server: %v\n", err)
os.Exit(1)
}
}