mirror of
https://github.com/wardviaene/kubernetes-course.git
synced 2026-02-14 17:49:56 +00:00
27
mutatingwebhook/Dockerfile
Normal file
27
mutatingwebhook/Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
||||
#
|
||||
# Build go project
|
||||
#
|
||||
FROM golang:1.15-alpine as go-builder
|
||||
|
||||
WORKDIR /go/src/github.com/wardviaene/kubernetes-course/mutatingwebhook
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o mutatingwebhook *.go
|
||||
|
||||
#
|
||||
# Runtime container
|
||||
#
|
||||
FROM alpine:latest
|
||||
|
||||
RUN mkdir -p /app && \
|
||||
addgroup -S app && adduser -S app -G app && \
|
||||
chown app:app /app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=go-builder /go/src/github.com/wardviaene/kubernetes-course/mutatingwebhook .
|
||||
|
||||
USER app
|
||||
|
||||
CMD ["./mutatingwebhook"]
|
||||
23
mutatingwebhook/README.md
Normal file
23
mutatingwebhook/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Mutating Webhook example
|
||||
|
||||
## Setup the webhook
|
||||
|
||||
```
|
||||
kubectl apply -f webhook.yaml
|
||||
```
|
||||
|
||||
## Set up the CA Certificate
|
||||
Once the webhook runs (give it a few seconds to initialize), the CA certificate can be downloaded by executing a curl command within the container. To retrieve the base64 encoded version of this ca.pem, use the following command:
|
||||
```
|
||||
kubectl exec -it -n mutatingwebhook $(kubectl get pods --no-headers -o custom-columns=":metadata.name" -n mutatingwebhook) -- wget -q -O- localhost:8080/ca.pem?base64
|
||||
```
|
||||
|
||||
The output of this command should replace the base64 string in caBundle in webhook.yaml:
|
||||
```
|
||||
caBundle: "cGxhY2Vob2xkZXIK" # <= replace this string within quotes
|
||||
```
|
||||
|
||||
Then reapply the webhook using:
|
||||
```
|
||||
kubectl apply -f webhook.yaml
|
||||
```
|
||||
3
mutatingwebhook/go.mod
Normal file
3
mutatingwebhook/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/wardviaene/kubernetes-course/mutatingwebhook
|
||||
|
||||
go 1.15
|
||||
167
mutatingwebhook/main.go
Normal file
167
mutatingwebhook/main.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
// setup certs
|
||||
fmt.Printf("Initializing certificates...\n")
|
||||
serverTLSConf, clientTLSConf, caPEM, err := certsetup()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
s := Server{
|
||||
ServerTLSConf: serverTLSConf,
|
||||
ClientTLSConf: clientTLSConf,
|
||||
CaPEM: caPEM,
|
||||
}
|
||||
|
||||
go func() {
|
||||
handler := http.NewServeMux()
|
||||
|
||||
handler.HandleFunc("/ca.pem", s.getCA)
|
||||
fmt.Printf("Starting localhost http server on :8080 with ca.pem endpoint\n")
|
||||
err = http.ListenAndServe("localhost:8080", handler)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// start TLS server
|
||||
fmt.Printf("Starting TLS server on :8443\n")
|
||||
handler := http.NewServeMux()
|
||||
handler.HandleFunc("/webhook", s.postWebhook)
|
||||
|
||||
https := &http.Server{
|
||||
Addr: ":8443",
|
||||
TLSConfig: serverTLSConf,
|
||||
Handler: handler,
|
||||
}
|
||||
|
||||
log.Fatal(https.ListenAndServeTLS("", ""))
|
||||
|
||||
}
|
||||
|
||||
// certsetup adapted from https://gist.github.com/shaneutt/5e1995295cff6721c89a71d13a71c251
|
||||
func certsetup() (serverTLSConf *tls.Config, clientTLSConf *tls.Config, caPEMBytes []byte, err error) {
|
||||
// set up our CA certificate
|
||||
ca := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(2019),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Company, INC."},
|
||||
Country: []string{"US"},
|
||||
Province: []string{""},
|
||||
Locality: []string{"San Francisco"},
|
||||
StreetAddress: []string{"Golden Gate Bridge"},
|
||||
PostalCode: []string{"94016"},
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(10, 0, 0),
|
||||
IsCA: true,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
// create our private and public key
|
||||
caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
return nil, nil, []byte{}, err
|
||||
}
|
||||
|
||||
// create the CA
|
||||
caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
|
||||
if err != nil {
|
||||
return nil, nil, []byte{}, err
|
||||
}
|
||||
|
||||
// pem encode
|
||||
caPEM := new(bytes.Buffer)
|
||||
pem.Encode(caPEM, &pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: caBytes,
|
||||
})
|
||||
|
||||
caPrivKeyPEM := new(bytes.Buffer)
|
||||
pem.Encode(caPrivKeyPEM, &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey),
|
||||
})
|
||||
|
||||
// set up our server certificate
|
||||
cert := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(2019),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Company, INC."},
|
||||
Country: []string{"US"},
|
||||
Province: []string{""},
|
||||
Locality: []string{"San Francisco"},
|
||||
StreetAddress: []string{"Golden Gate Bridge"},
|
||||
PostalCode: []string{"94016"},
|
||||
},
|
||||
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(10, 0, 0),
|
||||
SubjectKeyId: []byte{1, 2, 3, 4, 6},
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
DNSNames: []string{"mutatingwebhook.mutatingwebhook.svc"},
|
||||
}
|
||||
|
||||
certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
return nil, nil, []byte{}, err
|
||||
}
|
||||
|
||||
certBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey)
|
||||
if err != nil {
|
||||
return nil, nil, []byte{}, err
|
||||
}
|
||||
|
||||
certPEM := new(bytes.Buffer)
|
||||
pem.Encode(certPEM, &pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: certBytes,
|
||||
})
|
||||
|
||||
certPrivKeyPEM := new(bytes.Buffer)
|
||||
pem.Encode(certPrivKeyPEM, &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
|
||||
})
|
||||
|
||||
serverCert, err := tls.X509KeyPair(certPEM.Bytes(), certPrivKeyPEM.Bytes())
|
||||
if err != nil {
|
||||
return nil, nil, []byte{}, err
|
||||
}
|
||||
|
||||
serverTLSConf = &tls.Config{
|
||||
Certificates: []tls.Certificate{serverCert},
|
||||
}
|
||||
|
||||
certpool := x509.NewCertPool()
|
||||
certpool.AppendCertsFromPEM(caPEM.Bytes())
|
||||
clientTLSConf = &tls.Config{
|
||||
RootCAs: certpool,
|
||||
}
|
||||
|
||||
caPEMBytes = caPEM.Bytes()
|
||||
|
||||
return
|
||||
}
|
||||
13
mutatingwebhook/pod.yaml
Normal file
13
mutatingwebhook/pod.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
namespace: testmutatingwebhook
|
||||
name: ubuntu
|
||||
labels:
|
||||
app: ubuntu
|
||||
spec:
|
||||
containers:
|
||||
- name: ubuntu
|
||||
image: ubuntu:latest
|
||||
command: ["/bin/sleep", "1d"]
|
||||
imagePullPolicy: IfNotPresent
|
||||
71
mutatingwebhook/server.go
Normal file
71
mutatingwebhook/server.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//Server contains the functions handling server requests
|
||||
type Server struct {
|
||||
ServerTLSConf *tls.Config
|
||||
ClientTLSConf *tls.Config
|
||||
CaPEM []byte
|
||||
}
|
||||
|
||||
func (s Server) getCA(w http.ResponseWriter, req *http.Request) {
|
||||
if len(s.CaPEM) == 0 {
|
||||
fmt.Fprintf(w, "No certificate found\n")
|
||||
return
|
||||
}
|
||||
|
||||
// if base64 parameter is set, return in base64 format
|
||||
req.ParseForm()
|
||||
if _, hasParam := req.Form["base64"]; hasParam {
|
||||
fmt.Fprintf(w, string(base64.StdEncoding.EncodeToString(s.CaPEM)))
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, string(s.CaPEM))
|
||||
}
|
||||
|
||||
func (s Server) postWebhook(w http.ResponseWriter, r *http.Request) {
|
||||
var request AdmissionReviewRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&request)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("JSON body in invalid format: %s\n", err.Error()), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if request.APIVersion != "admission.k8s.io/v1" || request.Kind != "AdmissionReview" {
|
||||
http.Error(w, fmt.Sprintf("wrong APIVersion or kind: %s - %s", request.APIVersion, request.Kind), http.StatusBadRequest)
|
||||
return
|
||||
|
||||
}
|
||||
fmt.Printf("debug: %+v\n", request.Request)
|
||||
response := AdmissionReviewResponse{
|
||||
APIVersion: "admission.k8s.io/v1",
|
||||
Kind: "AdmissionReview",
|
||||
Response: Response{
|
||||
UID: request.Request.UID,
|
||||
Allowed: true,
|
||||
},
|
||||
}
|
||||
|
||||
// add label if we're creating a pod
|
||||
if request.Request.Kind.Group == "" && request.Request.Kind.Version == "v1" && request.Request.Kind.Kind == "Pod" && request.Request.Operation == "CREATE" {
|
||||
patch := `[{"op": "add", "path": "/metadata/labels/myExtraLabel", "value": "webhook-was-here"}]`
|
||||
patchEnc := base64.StdEncoding.EncodeToString([]byte(patch))
|
||||
response.Response.PatchType = "JSONPatch"
|
||||
response.Response.Patch = patchEnc
|
||||
}
|
||||
|
||||
out, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("JSON output marshal error: %s\n", err.Error()), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Got request, response: %s\n", string(out))
|
||||
fmt.Fprintln(w, string(out))
|
||||
}
|
||||
79
mutatingwebhook/types.go
Normal file
79
mutatingwebhook/types.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package main
|
||||
|
||||
//AdmissionReviewRespons for replies to incoming webhooks
|
||||
type AdmissionReviewResponse struct {
|
||||
APIVersion string `json:"apiVersion"`
|
||||
Kind string `json:"kind"`
|
||||
Response Response `json:"response"`
|
||||
}
|
||||
type Response struct {
|
||||
UID string `json:"uid"`
|
||||
Allowed bool `json:"allowed"`
|
||||
Patch string `json:"patch,omitempty"`
|
||||
PatchType string `json:"patchType,omitempty"`
|
||||
}
|
||||
|
||||
//AdmissionReviewRequest for incoming webhooks
|
||||
type AdmissionReviewRequest struct {
|
||||
APIVersion string `json:"apiVersion"`
|
||||
Kind string `json:"kind"`
|
||||
Request Request `json:"request"`
|
||||
}
|
||||
type Kind struct {
|
||||
Group string `json:"group"`
|
||||
Version string `json:"version"`
|
||||
Kind string `json:"kind"`
|
||||
}
|
||||
type Resource struct {
|
||||
Group string `json:"group"`
|
||||
Version string `json:"version"`
|
||||
Resource string `json:"resource"`
|
||||
}
|
||||
type RequestKind struct {
|
||||
Group string `json:"group"`
|
||||
Version string `json:"version"`
|
||||
Kind string `json:"kind"`
|
||||
}
|
||||
type RequestResource struct {
|
||||
Group string `json:"group"`
|
||||
Version string `json:"version"`
|
||||
Resource string `json:"resource"`
|
||||
}
|
||||
type Extra struct {
|
||||
SomeKey []string `json:"some-key"`
|
||||
}
|
||||
type UserInfo struct {
|
||||
Username string `json:"username"`
|
||||
UID string `json:"uid"`
|
||||
Groups []string `json:"groups"`
|
||||
Extra Extra `json:"extra"`
|
||||
}
|
||||
type Object struct {
|
||||
APIVersion string `json:"apiVersion"`
|
||||
Kind string `json:"kind"`
|
||||
}
|
||||
type OldObject struct {
|
||||
APIVersion string `json:"apiVersion"`
|
||||
Kind string `json:"kind"`
|
||||
}
|
||||
type Options struct {
|
||||
APIVersion string `json:"apiVersion"`
|
||||
Kind string `json:"kind"`
|
||||
}
|
||||
type Request struct {
|
||||
UID string `json:"uid"`
|
||||
Kind Kind `json:"kind"`
|
||||
Resource Resource `json:"resource"`
|
||||
SubResource string `json:"subResource"`
|
||||
RequestKind RequestKind `json:"requestKind"`
|
||||
RequestResource RequestResource `json:"requestResource"`
|
||||
RequestSubResource string `json:"requestSubResource"`
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Operation string `json:"operation"`
|
||||
UserInfo UserInfo `json:"userInfo"`
|
||||
Object Object `json:"object"`
|
||||
OldObject OldObject `json:"oldObject"`
|
||||
Options Options `json:"options"`
|
||||
DryRun bool `json:"dryRun"`
|
||||
}
|
||||
72
mutatingwebhook/webhook.yaml
Normal file
72
mutatingwebhook/webhook.yaml
Normal file
@@ -0,0 +1,72 @@
|
||||
apiVersion: admissionregistration.k8s.io/v1
|
||||
kind: MutatingWebhookConfiguration
|
||||
metadata:
|
||||
name: "mymutatingwebhook.example.com"
|
||||
webhooks:
|
||||
- name: "mymutatingwebhook.example.com"
|
||||
namespaceSelector:
|
||||
matchLabels:
|
||||
webhook: enabled
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
apiVersions: ["v1"]
|
||||
operations: ["CREATE"]
|
||||
resources: ["pods"]
|
||||
clientConfig:
|
||||
service:
|
||||
namespace: "mutatingwebhook"
|
||||
name: "mutatingwebhook"
|
||||
path: "/webhook"
|
||||
caBundle: "cGxhY2Vob2xkZXIK"
|
||||
admissionReviewVersions: ["v1", "v1beta1"]
|
||||
sideEffects: None
|
||||
timeoutSeconds: 5
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: mutatingwebhook
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: testmutatingwebhook
|
||||
labels:
|
||||
webhook: enabled
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: mutatingwebhook
|
||||
namespace: mutatingwebhook
|
||||
labels:
|
||||
app: mutatingwebhook
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: mutatingwebhook
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mutatingwebhook
|
||||
spec:
|
||||
containers:
|
||||
- name: mutatingwebhook
|
||||
imagePullPolicy: Always
|
||||
image: wardviaene/mutatingwebhook-example
|
||||
ports:
|
||||
- containerPort: 8443
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mutatingwebhook
|
||||
namespace: mutatingwebhook
|
||||
spec:
|
||||
selector:
|
||||
app: mutatingwebhook
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 443
|
||||
targetPort: 8443
|
||||
Reference in New Issue
Block a user