├── .github
├── CODEOWNERS
├── dependabot.yml
└── workflows
│ └── ci.yaml
├── .gitignore
├── .golangci.yml
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── Readme.md
├── docker
└── dev
│ └── Dockerfile
├── examples
├── Dockerfile
├── Makefile
├── Readme.md
├── create-certs.sh
├── dynamicwebhook
│ └── main.go
├── ingress-host-validator
│ ├── deploy
│ │ ├── test-ingress.yaml
│ │ ├── webhook-certs.yaml
│ │ ├── webhook-registration.yaml
│ │ ├── webhook-registration.yaml.tpl
│ │ └── webhook.yaml
│ └── main.go
├── multiwebhook
│ ├── cmd
│ │ └── multiwebhook
│ │ │ ├── flags.go
│ │ │ └── main.go
│ └── pkg
│ │ └── webhook
│ │ ├── mutating
│ │ ├── mutator.go
│ │ └── pod.go
│ │ └── validating
│ │ ├── deployment.go
│ │ └── validator.go
├── mutatortesting
│ ├── mutator.go
│ └── mutator_test.go
├── pod-annotate-metrics
│ └── main.go
├── pod-annotate-simple
│ └── main.go
├── pod-annotate-tracing
│ └── main.go
└── pod-annotate
│ ├── deploy
│ ├── test-deployment.yaml
│ ├── webhook-certs.yaml
│ ├── webhook-registration.yaml
│ ├── webhook-registration.yaml.tpl
│ └── webhook.yaml
│ └── main.go
├── go.mod
├── go.sum
├── hack
└── scripts
│ ├── check.sh
│ ├── integration-test.sh
│ ├── mockgen.sh
│ ├── run-integration-local.sh
│ ├── run-integration.sh
│ └── unit-test.sh
├── logo
├── kubewebhook_logo.svg
├── kubewebhook_logo@0,5x.png
└── kubewebhook_logo@1x.png
├── pkg
├── http
│ ├── example_test.go
│ ├── handler.go
│ └── handler_test.go
├── log
│ ├── ctx.go
│ ├── log.go
│ └── logrus
│ │ └── logrus.go
├── metrics
│ └── prometheus
│ │ ├── prometheus.go
│ │ └── prometheus_test.go
├── model
│ ├── response.go
│ ├── review.go
│ ├── review_test.go
│ └── webhook.go
├── tracing
│ ├── otel
│ │ ├── otel.go
│ │ └── otel_test.go
│ └── tracing.go
└── webhook
│ ├── internal
│ └── helpers
│ │ ├── helpers.go
│ │ └── helpers_test.go
│ ├── metrics.go
│ ├── mutating
│ ├── example_test.go
│ ├── mutatingmock
│ │ └── mutator.go
│ ├── mutator.go
│ ├── mutator_test.go
│ ├── webhook.go
│ └── webhook_test.go
│ ├── tracing.go
│ ├── validating
│ ├── example_test.go
│ ├── validatingmock
│ │ └── validator.go
│ ├── validator.go
│ ├── validator_test.go
│ ├── webhook.go
│ └── webhook_test.go
│ ├── webhook.go
│ └── webhookmock
│ └── webhook.go
└── test
├── Makefile
├── integration
├── certs
│ ├── cert.pem
│ └── key.pem
├── crd
│ ├── apis
│ │ └── building
│ │ │ ├── register.go
│ │ │ └── v1
│ │ │ ├── doc.go
│ │ │ ├── register.go
│ │ │ ├── types.go
│ │ │ └── zz_generated.deepcopy.go
│ ├── client
│ │ ├── clientset
│ │ │ └── versioned
│ │ │ │ ├── clientset.go
│ │ │ │ ├── fake
│ │ │ │ ├── clientset_generated.go
│ │ │ │ ├── doc.go
│ │ │ │ └── register.go
│ │ │ │ ├── scheme
│ │ │ │ ├── doc.go
│ │ │ │ └── register.go
│ │ │ │ └── typed
│ │ │ │ └── building
│ │ │ │ └── v1
│ │ │ │ ├── building_client.go
│ │ │ │ ├── doc.go
│ │ │ │ ├── fake
│ │ │ │ ├── doc.go
│ │ │ │ ├── fake_building_client.go
│ │ │ │ └── fake_house.go
│ │ │ │ ├── generated_expansion.go
│ │ │ │ └── house.go
│ │ ├── informers
│ │ │ └── externalversions
│ │ │ │ ├── building
│ │ │ │ ├── interface.go
│ │ │ │ └── v1
│ │ │ │ │ ├── house.go
│ │ │ │ │ └── interface.go
│ │ │ │ ├── factory.go
│ │ │ │ ├── generic.go
│ │ │ │ └── internalinterfaces
│ │ │ │ └── factory_interfaces.go
│ │ └── listers
│ │ │ └── building
│ │ │ └── v1
│ │ │ ├── expansion_generated.go
│ │ │ └── house.go
│ └── manifests
│ │ └── building.kubewebhook.slok.dev_houses.yaml
├── create-certs.sh
├── gen-crd.sh
├── helper
│ ├── cli
│ │ └── cli.go
│ └── config
│ │ └── config.go
└── webhook
│ ├── doc.go
│ ├── helper_test.go
│ ├── mutating_test.go
│ └── validating_test.go
└── manual
├── certs
├── ca-bundle.b64
├── cert.pem
└── key.pem
├── create-certs.sh
├── ns.yaml
├── test.yaml
├── test2.yaml
└── webhook-registration.yaml
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @slok
2 |
3 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "gomod"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | ignore:
8 | # Ignore Kubernetes dependencies to have full control on them.
9 | - dependency-name: "k8s.io/*"
10 | - package-ecosystem: "github-actions"
11 | directory: "/"
12 | schedule:
13 | interval: "daily"
14 | - package-ecosystem: docker
15 | directory: "/docker/dev"
16 | schedule:
17 | interval: "daily"
18 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | # Limit because of parallel ngrok max tunnels on integration tests.
6 | push:
7 | branches:
8 | - master
9 |
10 | jobs:
11 | check:
12 | name: Check
13 | runs-on: ubuntu-latest
14 | # Execute the checks inside the container instead the VM.
15 | container: golangci/golangci-lint:v1.60.3-alpine
16 | steps:
17 | - uses: actions/checkout@v4
18 | - run: |
19 | # We need this go flag because it started to error after golangci-lint is using Go 1.21.
20 | # TODO(slok): Remove it on next (>1.54.1) golangci-lint upgrade to check if this problem has gone.
21 | export GOFLAGS="-buildvcs=false"
22 | ./hack/scripts/check.sh
23 |
24 | unit-test:
25 | name: Unit test
26 | runs-on: ubuntu-latest
27 | steps:
28 | - uses: actions/checkout@v4
29 | - uses: actions/setup-go@v5
30 | with:
31 | go-version-file: go.mod
32 | - run: make ci-unit-test
33 |
34 | integration-test:
35 | name: Integration test
36 | runs-on: ubuntu-latest
37 | needs: [check, unit-test]
38 | strategy:
39 | max-parallel: 1 # Due to ngrok account limits.
40 | matrix:
41 | kubernetes: [1.28.13, 1.29.8, 1.30.4, 1.31.0]
42 | steps:
43 | - uses: actions/checkout@v4
44 | - uses: actions/setup-go@v5
45 | with:
46 | go-version-file: go.mod
47 | - name: Execute tests
48 | env:
49 | KIND_VERSION: v0.24.0
50 | NGROK_SSH_PRIVATE_KEY_B64: ${{secrets.NGROK_SSH_PRIVATE_KEY_B64}}
51 | run: |
52 | # Prepare access.
53 | mkdir -p ~/.ssh/
54 | echo -e "Host tunnel.us.ngrok.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config
55 | echo -e ${NGROK_SSH_PRIVATE_KEY_B64} | base64 -d > ~/.ssh/id_ed25519
56 | chmod 400 ~/.ssh/id_ed25519
57 |
58 | # Download dependencies.
59 | curl -Lo kind https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/kind-linux-amd64 && chmod +x kind && sudo mv kind /usr/local/bin/
60 | curl -Lo kubectl https://storage.googleapis.com/kubernetes-release/release/v${{ matrix.kubernetes }}/bin/linux/amd64/kubectl && chmod +x kubectl && sudo mv kubectl /usr/local/bin/
61 | curl -Lo /tmp/ngrok.zip https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip && unzip -o /tmp/ngrok.zip -d /tmp && sudo mv /tmp/ngrok /usr/local/bin/ && rm -rf /tmp/ngrok.zip
62 |
63 | # Execute tests.
64 | KUBERNETES_VERSION=${{ matrix.kubernetes }} NGROK=true make ci-integration-test
65 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.dll
4 | *.so
5 | *.dylib
6 |
7 | # Test binary, build with `go test -c`
8 | *.test
9 |
10 | # Output of the go coverage tool, specifically when used with LiteIDE
11 | *.out
12 |
13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
14 | .glide/
15 |
16 | # binary
17 | bin/
18 |
19 | # vendor
20 | vendor/
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | ---
2 | run:
3 | timeout: 3m
4 | build-tags:
5 | - integration
6 |
7 | linters:
8 | enable:
9 | - misspell
10 | - goimports
11 | - revive
12 | - gofmt
13 | #- depguard
14 | - godot
15 |
16 | linters-settings:
17 | revive:
18 | rules:
19 | # Spammy linter and complex to fix on lots of parameters. Makes more harm that it solves.
20 | - name: unused-parameter
21 | disabled: true
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 |
2 | # Name of this service/application
3 | SERVICE_NAME := kubewebhook
4 | SHELL := $(shell which bash)
5 | GID := $(shell id -g)
6 | UID := $(shell id -u)
7 | COMMIT=$(shell git rev-parse --short HEAD)
8 |
9 | # cmds
10 | UNIT_TEST_CMD := ./hack/scripts/unit-test.sh
11 | INTEGRATION_TEST_CMD := ./hack/scripts/run-integration.sh
12 | MOCKS_CMD := ./hack/scripts/mockgen.sh
13 | DOCKER_RUN_CMD := docker run -v ${PWD}:/src --rm -it $(SERVICE_NAME)
14 | DEPS_CMD := go mod tidy
15 | CHECK_CMD := ./hack/scripts/check.sh
16 |
17 | help: ## Show this help.
18 | @echo "Help"
19 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[93m %s\n", $$1, $$2}'
20 |
21 | .PHONY: default
22 | default: help
23 |
24 | .PHONY: build
25 | build: ## Build the development docker images.
26 | docker build -t $(SERVICE_NAME) --build-arg uid=$(UID) --build-arg gid=$(GID) -f ./docker/dev/Dockerfile .
27 |
28 | build-binary: ## Build production stuff.
29 | $(DOCKER_RUN_CMD) /bin/sh -c '$(BUILD_BINARY_CMD)'
30 |
31 | .PHONY: build-image
32 | build-image: ## Build docker image.
33 | $(BUILD_IMAGE_CMD)
34 |
35 | .PHONY: unit-test
36 | unit-test: build ## Execute unit tests.
37 | $(DOCKER_RUN_CMD) /bin/sh -c '$(UNIT_TEST_CMD)'
38 |
39 | .PHONY: integration-test
40 | integration-test: build ## Execute integration tests.
41 | $(INTEGRATION_TEST_CMD)
42 |
43 | .PHONY: test ## Alias for unit-test
44 | test: unit-test
45 |
46 | .PHONY: check
47 | check: build ## Runs checks.
48 | @$(DOCKER_RUN_CMD) /bin/sh -c '$(CHECK_CMD)'
49 |
50 | .PHONY: ci-unit-test
51 | ci-unit-test: ## Same as unit-test but for CI.
52 | $(UNIT_TEST_CMD)
53 |
54 | .PHONY: ci-integration-test
55 | ci-integration-test: ## Same as integration-tests but for CI.
56 | $(INTEGRATION_TEST_CMD)
57 |
58 | .PHONY: ci ## Execute all the tests for CI.
59 | ci: ci-unit-test ci-integration-test
60 |
61 | .PHONY: mocks
62 | mocks: build ## Generate mocks.
63 | $(DOCKER_RUN_CMD) /bin/sh -c '$(MOCKS_CMD)'
64 |
65 | .PHONY: godoc
66 | godoc: ## Run library docs.
67 | godoc -http=":6060"
68 |
69 | .PHONY: deps
70 | deps: ## Setup dependencies.
71 | $(DEPS_CMD)
72 |
73 | .PHONY: create-integration-test-certs
74 | integration-create-certs: ## Creates certificates for the integration test.
75 | ./test/integration/create-certs.sh
76 |
77 | .PHONY: generate-integration-test-crd
78 | integration-gen-crd: ## Generates CRDs for the integration test.
79 | ./test/integration/gen-crd.sh
80 |
--------------------------------------------------------------------------------
/docker/dev/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.23
2 |
3 | ARG GOLANGCI_LINT_VERSION="1.60.3"
4 | ARG MOCKERY_VERSION="2.45.0"
5 |
6 | ARG ostype=Linux
7 |
8 | RUN apt-get update && apt-get install -y \
9 | git \
10 | bash \
11 | zip
12 |
13 |
14 | RUN wget https://github.com/golangci/golangci-lint/releases/download/v${GOLANGCI_LINT_VERSION}/golangci-lint-${GOLANGCI_LINT_VERSION}-linux-amd64.tar.gz && \
15 | tar zxvf golangci-lint-${GOLANGCI_LINT_VERSION}-linux-amd64.tar.gz --strip 1 -C /usr/local/bin/ && \
16 | rm golangci-lint-${GOLANGCI_LINT_VERSION}-linux-amd64.tar.gz && \
17 | \
18 | wget https://github.com/vektra/mockery/releases/download/v${MOCKERY_VERSION}/mockery_${MOCKERY_VERSION}_Linux_x86_64.tar.gz && \
19 | tar zxvf mockery_${MOCKERY_VERSION}_Linux_x86_64.tar.gz -C /tmp && \
20 | mv /tmp/mockery /usr/local/bin/ && \
21 | rm mockery_${MOCKERY_VERSION}_Linux_x86_64.tar.gz
22 |
23 | # Create user.
24 | ARG uid=1000
25 | ARG gid=1000
26 |
27 | RUN bash -c 'if [ ${ostype} == Linux ]; then addgroup -gid $gid app; else addgroup app; fi && \
28 | adduser --disabled-password -uid $uid --ingroup app --gecos "" app && \
29 | chown app:app -R /go'
30 |
31 | # Fill go mod cache.
32 | RUN mkdir /tmp/cache
33 | COPY go.mod /tmp/cache
34 | COPY go.sum /tmp/cache
35 | RUN chown app:app -R /tmp/cache
36 | USER app
37 | RUN cd /tmp/cache && \
38 | go mod download
39 |
40 | WORKDIR /src
41 |
--------------------------------------------------------------------------------
/examples/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.22-alpine AS build-stage
2 |
3 | ARG example
4 |
5 | WORKDIR /go/src/github.com/slok/kubewebhook
6 | COPY . .
7 |
8 | RUN CGO_ENABLED=0 go build -o /bin/example --ldflags "-w -extldflags '-static'" github.com/slok/kubewebhook/examples/${example}
9 |
10 | # Final image.
11 | FROM alpine:latest
12 | RUN apk --no-cache add \
13 | ca-certificates
14 | COPY --from=build-stage /bin/example /usr/local/bin/example
15 | ENTRYPOINT ["/usr/local/bin/example"]
16 |
--------------------------------------------------------------------------------
/examples/Makefile:
--------------------------------------------------------------------------------
1 |
2 |
3 | POD_ANNOTATE_IMAGE="quay.io/slok/kubewebhook-pod-annotate-example"
4 | POD_ANNOTATE_EXAMPLE="pod-annotate"
5 | INGRESS_HOST_VALIDATOR_IMAGE="quay.io/slok/kubewebhook-ingress-host-validator-example"
6 | INGRESS_HOST_VALIDATOR_EXAMPLE="ingress-host-validator"
7 |
8 | # Build the development docker image
9 | .PHONY: build-examples
10 | build-examples:
11 | docker build -t $(POD_ANNOTATE_IMAGE) --build-arg example=$(POD_ANNOTATE_EXAMPLE) -f ./Dockerfile ../
12 | docker build -t $(INGRESS_HOST_VALIDATOR_IMAGE) --build-arg example=$(INGRESS_HOST_VALIDATOR_EXAMPLE) -f ./Dockerfile ../
13 |
14 | .PHONY: push-examples
15 | push-examples:
16 | docker push $(POD_ANNOTATE_IMAGE)
17 | docker push $(INGRESS_HOST_VALIDATOR_IMAGE)
18 |
19 | .PHONY: create-certs
20 | create-certs:
21 | ./create-certs.sh default ${POD_ANNOTATE_EXAMPLE}
22 | ./create-certs.sh default ${INGRESS_HOST_VALIDATOR_EXAMPLE}
--------------------------------------------------------------------------------
/examples/Readme.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | ## pod-annotate
4 |
5 | This example is a simple mutating webhook that adds annotations.
6 |
7 | ### steps
8 |
9 | #### Set up the mutating webhook
10 |
11 | - Deploy the mutating webhook certificates: `kubectl apply -f ./pod-annotate/deploy/webhook-certs.yaml`.
12 | - Deploy the mutating webhook: `kubectl apply -f ./pod-annotate/deploy/webhook.yaml`.
13 | - Register the mutating webhook for the apiserver: `kubectl apply -f ./pod-annotate/deploy/webhook-registration.yaml`.
14 |
15 | #### Check
16 |
17 | - Deploy a test with: `kubectl apply -f ./pod-annotate/deploy/test-deployment.yaml`.
18 | - Check all the annotations of the created pods, for example with `kubect get pods -o yaml`
19 |
20 | ## ingress-host-validator
21 |
22 | This example validates that all the ingress rules host match a regex, if it doesn't match it will not admit that ingress,
23 |
24 | ### steps
25 |
26 | #### Set up the mutating webhook
27 |
28 | - Deploy the validating webhook certificates: `kubectl apply -f ./ingress-host-validator/deploy/webhook-certs.yaml`.
29 | - Deploy the validating webhook: `kubectl apply -f ./ingress-host-validator/deploy/webhook.yaml`.
30 | - Register the validating webhook for the apiserver: `kubectl apply -f ./ingress-host-validator/deploy/webhook-registration.yaml`.
31 |
32 | #### Check
33 |
34 | - Deploy a test with: `kubectl apply -f ./test-ingress.yaml` and watch the denied and accepted ingresses.
35 |
--------------------------------------------------------------------------------
/examples/create-certs.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | WEBHOOK_NS=${1:-"default"}
4 | EXAMPLE_NAME=${2:-"pod-annotate"}
5 | WEBHOOK_SVC="${EXAMPLE_NAME}-webhook"
6 |
7 | # Create certs for our webhook
8 | openssl genrsa -out webhookCA.key 2048
9 | openssl req -new -key ./webhookCA.key -subj "/CN=${WEBHOOK_SVC}.${WEBHOOK_NS}.svc" -out ./webhookCA.csr
10 | openssl x509 -req -days 365 -in webhookCA.csr -signkey webhookCA.key -out webhook.crt
11 |
12 | # Create certs secrets for k8s
13 | kubectl create secret generic \
14 | ${WEBHOOK_SVC}-certs \
15 | --from-file=key.pem=./webhookCA.key \
16 | --from-file=cert.pem=./webhook.crt \
17 | --dry-run -o yaml > ./${EXAMPLE_NAME}/deploy/webhook-certs.yaml
18 |
19 | # Set the CABundle on the webhook registration
20 | CA_BUNDLE=$(cat ./webhook.crt | base64 -w0)
21 | sed "s/CA_BUNDLE/${CA_BUNDLE}/" ./${EXAMPLE_NAME}/deploy/webhook-registration.yaml.tpl > ./${EXAMPLE_NAME}/deploy/webhook-registration.yaml
22 |
23 | # Clean
24 | rm ./webhookCA* && rm ./webhook.crt
--------------------------------------------------------------------------------
/examples/dynamicwebhook/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "fmt"
7 | "net/http"
8 | "os"
9 |
10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11 |
12 | "github.com/sirupsen/logrus"
13 | kwhhttp "github.com/slok/kubewebhook/v2/pkg/http"
14 | kwhlogrus "github.com/slok/kubewebhook/v2/pkg/log/logrus"
15 | kwhmodel "github.com/slok/kubewebhook/v2/pkg/model"
16 | kwhmutating "github.com/slok/kubewebhook/v2/pkg/webhook/mutating"
17 | mutatingwh "github.com/slok/kubewebhook/v2/pkg/webhook/mutating"
18 | )
19 |
20 | type config struct {
21 | certFile string
22 | keyFile string
23 | }
24 |
25 | func initFlags() *config {
26 | cfg := &config{}
27 |
28 | fl := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
29 | fl.StringVar(&cfg.certFile, "tls-cert-file", "", "TLS certificate file")
30 | fl.StringVar(&cfg.keyFile, "tls-key-file", "", "TLS key file")
31 |
32 | _ = fl.Parse(os.Args[1:])
33 | return cfg
34 | }
35 |
36 | func main() {
37 | logrusLogEntry := logrus.NewEntry(logrus.New())
38 | logrusLogEntry.Logger.SetLevel(logrus.DebugLevel)
39 | logger := kwhlogrus.NewLogrus(logrusLogEntry)
40 |
41 | cfg := initFlags()
42 |
43 | // Create our mutator.
44 | mt := mutatingwh.MutatorFunc(func(_ context.Context, ar *kwhmodel.AdmissionReview, obj metav1.Object) (*kwhmutating.MutatorResult, error) {
45 | labels := obj.GetLabels()
46 | if labels == nil {
47 | labels = map[string]string{}
48 | }
49 | labels[fmt.Sprintf("kubewebhook-%s", ar.Version)] = "mutated"
50 | obj.SetLabels(labels)
51 |
52 | return &kwhmutating.MutatorResult{MutatedObject: obj}, nil
53 | })
54 |
55 | // We don't use any type, it works for any type.
56 | mcfg := mutatingwh.WebhookConfig{
57 | ID: "labeler",
58 | Mutator: mt,
59 | Logger: logger,
60 | }
61 | wh, err := mutatingwh.NewWebhook(mcfg)
62 | if err != nil {
63 | fmt.Fprintf(os.Stderr, "error creating webhook: %s", err)
64 | os.Exit(1)
65 | }
66 |
67 | // Get the handler for our webhook.
68 | whHandler, err := kwhhttp.HandlerFor(kwhhttp.HandlerConfig{Webhook: wh, Logger: logger})
69 | if err != nil {
70 | fmt.Fprintf(os.Stderr, "error creating webhook handler: %s", err)
71 | os.Exit(1)
72 | }
73 | logger.Infof("Listening on :8080")
74 | err = http.ListenAndServeTLS(":8080", cfg.certFile, cfg.keyFile, whHandler)
75 | if err != nil {
76 | fmt.Fprintf(os.Stderr, "error serving webhook: %s", err)
77 | os.Exit(1)
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/examples/ingress-host-validator/deploy/test-ingress.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: extensions/v1beta1
2 | kind: Ingress
3 | metadata:
4 | name: valid-ingress
5 | spec:
6 | rules:
7 | - host: this.is.a.valid.test.kubewebhook.github.io
8 | ---
9 | apiVersion: extensions/v1beta1
10 | kind: Ingress
11 | metadata:
12 | name: not-valid-ingress
13 | spec:
14 | rules:
15 | - host: this.is.not.a.valid.test
16 | - host: this.also.is.not.a.valid.test
17 |
18 | ---
19 | apiVersion: extensions/v1beta1
20 | kind: Ingress
21 | metadata:
22 | name: not-valid-ingress2
23 | spec:
24 | rules:
25 | - host: this.is.a.valid.test.kubewebhook.github.io
26 | - host: this.is.not.a.valid.test
--------------------------------------------------------------------------------
/examples/ingress-host-validator/deploy/webhook-certs.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | data:
3 | cert.pem: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM1akNDQWM0Q0NRQ2o1aWt3NlZlcHdUQU5CZ2txaGtpRzl3MEJBUXNGQURBMU1UTXdNUVlEVlFRRERDcHAKYm1keVpYTnpMV2h2YzNRdGRtRnNhV1JoZEc5eUxYZGxZbWh2YjJzdVpHVm1ZWFZzZEM1emRtTXdIaGNOTVRndwpOekE0TVRVME56VXdXaGNOTVRrd056QTRNVFUwTnpVd1dqQTFNVE13TVFZRFZRUUREQ3BwYm1keVpYTnpMV2h2CmMzUXRkbUZzYVdSaGRHOXlMWGRsWW1odmIyc3VaR1ZtWVhWc2RDNXpkbU13Z2dFaU1BMEdDU3FHU0liM0RRRUIKQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUMwNGMvYXk3QWltWklkZHNFbmxmcWdKUHpRTW1pOEt3YVgyZGkzb2l5Zwo1TnBNajd3enhrdFluejJkekNTTEUxaWdmQ1RzTFJoaXlSZEQrTFN0bmUrLzY0M0FQUlNRU0ZqcUtKeTh4aElTClRiVUpTVklra0NsLzU1T28wUHJ0cWtWc2VraytzTlNmazExeWFzNjF2djBFRmw5ajh1RHAzWm1CU0k0cHN5dVMKdHJUQXZzN1VpQzVXa3BiR2FXREJzbndJZ0pvdG44YWprbGlPNVdnTzFuQXc5ay8yWUhidmYzaUgyS255OEJzTwpyY2lJcjd1UzA2KzIxRXVLb1VCdWJ4SVYxbXYvbkUwWW56UHNJb0NNb3A4cTFqZWdBQXJGRjYreElrZFA2SlhqCll5emJlWkJKZ2V0UG84bmY2bEhxT1hzemhBZWZYTHVOanF2NFBLcHR6ekMvQWdNQkFBRXdEUVlKS29aSWh2Y04KQVFFTEJRQURnZ0VCQUhlQUhldERzLzVxaVI0S1RYMzJwWC9aeHMzbEx0eDh6eWRmR24rV2lEZU12cld6NXhETApNUE9YTzUrSU5NNlYvRzVrL2RCbFRRSGlWaGpzS3hrSGRCWDhwMHZUN1Q1dTl1SE5LZC9oT2hQb0IyV1VWVzVyCk85TTJPc1F2RzNyMWtFNG0xeXZFY2x0YlZ1OFVTaVl2Ym8rMWh6dUJucmpraTBCTGlkdTNKc3NjazhnS3NsZEYKYjc1QTdWLzBHVWlrVjYvVVRaUXpSTWcxeE9NRVh0TStWeW0yMkkyelZ1c05ZK2VNSlF5N1g0MFRYWHA4aEZFagp1bVJVNlR5QUhucmplUllIVENWTW1tdTlJV3JOekJPK3R2ZVdFVmZZZUVoWmZzT0VMcVFtV1c1elg1WXZJWFI5CmppZ01SQ2xUZ1oxbzZ3Q0pPY2xYaUxLMHRSSjlwSUtQTGswPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
4 | key.pem: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBdE9IUDJzdXdJcG1TSFhiQko1WDZvQ1Q4MERKb3ZDc0dsOW5ZdDZJc29PVGFUSSs4Ck04WkxXSjg5bmN3a2l4TllvSHdrN0MwWVlza1hRL2kwclozdnYrdU53RDBVa0VoWTZpaWN2TVlTRWsyMUNVbFMKSkpBcGYrZVRxTkQ2N2FwRmJIcEpQckRVbjVOZGNtck90Yjc5QkJaZlkvTGc2ZDJaZ1VpT0tiTXJrcmEwd0w3TwoxSWd1VnBLV3htbGd3Yko4Q0lDYUxaL0dvNUpZanVWb0R0WndNUFpQOW1CMjczOTRoOWlwOHZBYkRxM0lpSys3Cmt0T3Z0dFJMaXFGQWJtOFNGZFpyLzV4TkdKOHo3Q0tBaktLZkt0WTNvQUFLeFJldnNTSkhUK2lWNDJNczIzbVEKU1lIclQ2UEozK3BSNmpsN000UUhuMXk3alk2citEeXFiYzh3dndJREFRQUJBb0lCQVFDaVM5YUhONm5EY2Myagp1REVaUnIzSXdVZEJ1MmswSk5yV2x6V1hsdUM2UUgwS294RTMxMTAxbURQZUNSUms0U2Z0WDFaMXZ3U1pabHNFCnR2dk9wOTQ5Y3FvT2FmK3djZW10dUdEaUZFcVV0N3FQS1lXVm52Qk5ma2lEK1hhY2x1R1JzcXRUbjdBM3VpN24KZ2FRVmVOZG11bmcrb3VkaGoxNnpuSmxLR3lRbWoxdHpQQUNsd3E3OHA4ZmUvcWFrTlQxWllPRWJzMHJLdVQwRwpCbXE4RWdJMjNhNC8yREJCVmhCclhxUHhQYkZxajlITXhiUHNyeVRlVDNBRUI2clIrTEpXLzR4WFNhUjZHM0xVCkhKV1BWSzVnVnlKbXkzWktXSXdyUDhNL3o0QmQ0WlJIVVNwUjBnOG4yOWJ5WFdPSWlldHh0MGVML1d6R3BPeDYKVkhBNE9NVXBBb0dCQU9sZHFEUnlGbUJ6ZlZiS0xoOFFhblVMRm9JUVIyemc1WENOMFE4SjNLckdDS1JPbUNOdQp6UXZyQmZjaHRVc0N1Tko2MUlKcXNrNEduclNiMmw0TnJaZU9PM29rREhoaVRYYVJJUU44MzhkeDVOaUowWGZKCmlTUUtqOEx1TlRBVlFWWVAvQlpsdERLNGx1c3liT0cwdDV5NlMxSSs5aEx6KzZ3emFiYnBJdjY3QW9HQkFNWnQKQXlrbnB6YmR5QXZ6OTFvY3JLdG0rQWVscUF0RU5KNmYzMGVGUU5QOE1Eb0Vma0FvWW1xV0dMcFUvWmN6b2tLWApQUGhGTFVGdVhZazY2OXdBYWh4Z29nNlMvanVhdHNXbW5zdXcyNUZNd0ZhLy9kWU1xSHhqVzlBZkpreWhxVVdnCmtqc2U4Y3RmR1U4R0V1NE9kNkpNL3MwYlB0ZWlrTWM4dnhhTmNNL05Bb0dCQUtQRzBRZU5rZHE2OHBuNFpvbnkKa1dQM0ZpTE5uZkx4bXRSMUV1VkUvSkwxVEpkVGVUNVV3cDY4b0lFaUxRT3BzMkEwUi9RSFFYKzA4YWk3UUhPRgprZkN0SjF5KzgrbUF3L0NVUmVFV3dFNU94bGFaMG1JSkZMQXdvVmdpcVQxSGpZVUN6dDVCSGtnQ2VCZzBXV25GCjFKNnZ0N0RPOHcrMkgwaHBCS1lUMURXRkFvR0FVbFQvUDc5VlVaMjZtTU1VK2szMWszVTVLeXNnUy9SMEJQRi8KR3RUK3JqYnc1OUhmZE0yTThIN3hLYmVFTlkzV0lOMVNEZzNlRmswdUFIZnpUNmpZWG9mMkZpZ0YxME45M1FzaQp6by9HdTBabWRGaGJnY3BreXFBTGc1SzVPSWpIakwxd2o1bHFhS0ZBQmhzV0ZiTHBEdTMxdDdNN1l3dndHTXRxCjJuZTd2RzBDZ1lBdnFlbFFsbnlJc0QvcjRXRkdTZC9zUjhncTM0MGpDaC93b0QwZjNmVERlOS9BY2tBWTQzYm4KMEo2aFNFQm9ic2FwWXNFeGV1WkgzWTE1VTBhcHpYaVBYN3pJS0FaaVdPb0JpN3ptMXg0VnJsT2ZjME9LbEpnegozQXJRQng1NStISXJVTG1XU00vRDRUekRvNmxUWVppRTVaQ1VNTjgzdkdleE9ObzNqUUlEMnc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
5 | kind: Secret
6 | metadata:
7 | creationTimestamp: null
8 | name: ingress-host-validator-webhook-certs
9 |
--------------------------------------------------------------------------------
/examples/ingress-host-validator/deploy/webhook-registration.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: admissionregistration.k8s.io/v1beta1
2 | kind: ValidatingWebhookConfiguration
3 | metadata:
4 | name: ingress-host-validator-webhook
5 | labels:
6 | app: ingress-host-validator-webhook
7 | kind: validating
8 | webhooks:
9 | - name: ingress-host-validator-webhook.slok.dev
10 | clientConfig:
11 | service:
12 | name: ingress-host-validator-webhook
13 | namespace: default
14 | path: "/validating"
15 | caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM1akNDQWM0Q0NRQ2o1aWt3NlZlcHdUQU5CZ2txaGtpRzl3MEJBUXNGQURBMU1UTXdNUVlEVlFRRERDcHAKYm1keVpYTnpMV2h2YzNRdGRtRnNhV1JoZEc5eUxYZGxZbWh2YjJzdVpHVm1ZWFZzZEM1emRtTXdIaGNOTVRndwpOekE0TVRVME56VXdXaGNOTVRrd056QTRNVFUwTnpVd1dqQTFNVE13TVFZRFZRUUREQ3BwYm1keVpYTnpMV2h2CmMzUXRkbUZzYVdSaGRHOXlMWGRsWW1odmIyc3VaR1ZtWVhWc2RDNXpkbU13Z2dFaU1BMEdDU3FHU0liM0RRRUIKQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUMwNGMvYXk3QWltWklkZHNFbmxmcWdKUHpRTW1pOEt3YVgyZGkzb2l5Zwo1TnBNajd3enhrdFluejJkekNTTEUxaWdmQ1RzTFJoaXlSZEQrTFN0bmUrLzY0M0FQUlNRU0ZqcUtKeTh4aElTClRiVUpTVklra0NsLzU1T28wUHJ0cWtWc2VraytzTlNmazExeWFzNjF2djBFRmw5ajh1RHAzWm1CU0k0cHN5dVMKdHJUQXZzN1VpQzVXa3BiR2FXREJzbndJZ0pvdG44YWprbGlPNVdnTzFuQXc5ay8yWUhidmYzaUgyS255OEJzTwpyY2lJcjd1UzA2KzIxRXVLb1VCdWJ4SVYxbXYvbkUwWW56UHNJb0NNb3A4cTFqZWdBQXJGRjYreElrZFA2SlhqCll5emJlWkJKZ2V0UG84bmY2bEhxT1hzemhBZWZYTHVOanF2NFBLcHR6ekMvQWdNQkFBRXdEUVlKS29aSWh2Y04KQVFFTEJRQURnZ0VCQUhlQUhldERzLzVxaVI0S1RYMzJwWC9aeHMzbEx0eDh6eWRmR24rV2lEZU12cld6NXhETApNUE9YTzUrSU5NNlYvRzVrL2RCbFRRSGlWaGpzS3hrSGRCWDhwMHZUN1Q1dTl1SE5LZC9oT2hQb0IyV1VWVzVyCk85TTJPc1F2RzNyMWtFNG0xeXZFY2x0YlZ1OFVTaVl2Ym8rMWh6dUJucmpraTBCTGlkdTNKc3NjazhnS3NsZEYKYjc1QTdWLzBHVWlrVjYvVVRaUXpSTWcxeE9NRVh0TStWeW0yMkkyelZ1c05ZK2VNSlF5N1g0MFRYWHA4aEZFagp1bVJVNlR5QUhucmplUllIVENWTW1tdTlJV3JOekJPK3R2ZVdFVmZZZUVoWmZzT0VMcVFtV1c1elg1WXZJWFI5CmppZ01SQ2xUZ1oxbzZ3Q0pPY2xYaUxLMHRSSjlwSUtQTGswPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
16 | rules:
17 | - operations: [ "CREATE", "UPDATE" ]
18 | apiGroups: ["extensions"]
19 | apiVersions: ["v1beta1"]
20 | resources: ["ingresses"]
21 |
22 |
--------------------------------------------------------------------------------
/examples/ingress-host-validator/deploy/webhook-registration.yaml.tpl:
--------------------------------------------------------------------------------
1 | apiVersion: admissionregistration.k8s.io/v1beta1
2 | kind: ValidatingWebhookConfiguration
3 | metadata:
4 | name: ingress-host-validator-webhook
5 | labels:
6 | app: ingress-host-validator-webhook
7 | kind: validating
8 | webhooks:
9 | - name: ingress-host-validator-webhook.slok.dev
10 | clientConfig:
11 | service:
12 | name: ingress-host-validator-webhook
13 | namespace: default
14 | path: "/validating"
15 | caBundle: CA_BUNDLE
16 | rules:
17 | - operations: [ "CREATE", "UPDATE" ]
18 | apiGroups: ["extensions"]
19 | apiVersions: ["v1beta1"]
20 | resources: ["ingresses"]
21 |
--------------------------------------------------------------------------------
/examples/ingress-host-validator/deploy/webhook.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: extensions/v1beta1
2 | kind: Deployment
3 | metadata:
4 | name: ingress-host-validator-webhook
5 | labels:
6 | app: ingress-host-validator-webhook
7 | spec:
8 | replicas: 1
9 | template:
10 | metadata:
11 | labels:
12 | app: ingress-host-validator-webhook
13 | spec:
14 | containers:
15 | - name: ingress-host-validator-webhook
16 | image: quay.io/slok/kubewebhook-ingress-host-validator-example:latest
17 | imagePullPolicy: Always
18 | args:
19 | - -tls-cert-file=/etc/webhook/certs/cert.pem
20 | - -tls-key-file=/etc/webhook/certs/key.pem
21 | - -ingress-host-regex=^.*kubewebhook\.github\.io$
22 | volumeMounts:
23 | - name: webhook-certs
24 | mountPath: /etc/webhook/certs
25 | readOnly: true
26 | volumes:
27 | - name: webhook-certs
28 | secret:
29 | secretName: ingress-host-validator-webhook-certs
30 | ---
31 | apiVersion: v1
32 | kind: Service
33 | metadata:
34 | name: ingress-host-validator-webhook
35 | labels:
36 | app: ingress-host-validator-webhook
37 | spec:
38 | ports:
39 | - port: 443
40 | targetPort: 8080
41 | selector:
42 | app: ingress-host-validator-webhook
43 |
--------------------------------------------------------------------------------
/examples/ingress-host-validator/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "fmt"
7 | "net/http"
8 | "os"
9 | "regexp"
10 |
11 | "github.com/sirupsen/logrus"
12 | kwhhttp "github.com/slok/kubewebhook/v2/pkg/http"
13 | kwhlog "github.com/slok/kubewebhook/v2/pkg/log"
14 | kwhlogrus "github.com/slok/kubewebhook/v2/pkg/log/logrus"
15 | kwhmodel "github.com/slok/kubewebhook/v2/pkg/model"
16 | kwhvalidating "github.com/slok/kubewebhook/v2/pkg/webhook/validating"
17 | extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
18 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19 | )
20 |
21 | type ingressHostValidator struct {
22 | hostRegex *regexp.Regexp
23 | logger kwhlog.Logger
24 | }
25 |
26 | func (v *ingressHostValidator) Validate(_ context.Context, _ *kwhmodel.AdmissionReview, obj metav1.Object) (*kwhvalidating.ValidatorResult, error) {
27 | ingress, ok := obj.(*extensionsv1beta1.Ingress)
28 |
29 | if !ok {
30 | return nil, fmt.Errorf("not an ingress")
31 | }
32 |
33 | for _, r := range ingress.Spec.Rules {
34 | if !v.hostRegex.MatchString(r.Host) {
35 | v.logger.Infof("ingress %s denied, host %s is not valid for regex %s", ingress.Name, r.Host, v.hostRegex)
36 | return &kwhvalidating.ValidatorResult{
37 | Valid: false,
38 | Message: fmt.Sprintf("%s ingress host doesn't match %s regex", r.Host, v.hostRegex),
39 | }, nil
40 | }
41 | }
42 |
43 | v.logger.Infof("ingress %s is valid", ingress.Name)
44 | return &kwhvalidating.ValidatorResult{
45 | Valid: true,
46 | Message: "all hosts in the ingress are valid",
47 | }, nil
48 | }
49 |
50 | type config struct {
51 | certFile string
52 | keyFile string
53 | hostRegex string
54 | addr string
55 | }
56 |
57 | func initFlags() *config {
58 | cfg := &config{}
59 |
60 | fl := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
61 | fl.StringVar(&cfg.certFile, "tls-cert-file", "", "TLS certificate file")
62 | fl.StringVar(&cfg.keyFile, "tls-key-file", "", "TLS key file")
63 | fl.StringVar(&cfg.addr, "listen-addr", ":8080", "The address to start the server")
64 | fl.StringVar(&cfg.hostRegex, "ingress-host-regex", "", "The ingress host regex that matches valid ingresses")
65 |
66 | _ = fl.Parse(os.Args[1:])
67 | return cfg
68 | }
69 |
70 | func main() {
71 | logrusLogEntry := logrus.NewEntry(logrus.New())
72 | logrusLogEntry.Logger.SetLevel(logrus.DebugLevel)
73 | logger := kwhlogrus.NewLogrus(logrusLogEntry)
74 |
75 | cfg := initFlags()
76 |
77 | // Create our validator
78 | rgx, err := regexp.Compile(cfg.hostRegex)
79 | if err != nil {
80 | fmt.Fprintf(os.Stderr, "invalid regex: %s", err)
81 | os.Exit(1)
82 | return
83 | }
84 | vl := &ingressHostValidator{
85 | hostRegex: rgx,
86 | logger: logger,
87 | }
88 |
89 | vcfg := kwhvalidating.WebhookConfig{
90 | ID: "ingressHostValidator",
91 | Obj: &extensionsv1beta1.Ingress{},
92 | Validator: vl,
93 | Logger: logger,
94 | }
95 | wh, err := kwhvalidating.NewWebhook(vcfg)
96 | if err != nil {
97 | fmt.Fprintf(os.Stderr, "error creating webhook: %s", err)
98 | os.Exit(1)
99 | }
100 |
101 | // Serve the webhook.
102 | logger.Infof("Listening on %s", cfg.addr)
103 | err = http.ListenAndServeTLS(cfg.addr, cfg.certFile, cfg.keyFile, kwhhttp.MustHandlerFor(kwhhttp.HandlerConfig{
104 | Webhook: wh,
105 | Logger: logger,
106 | }))
107 | if err != nil {
108 | fmt.Fprintf(os.Stderr, "error serving webhook: %s", err)
109 | os.Exit(1)
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/examples/multiwebhook/cmd/multiwebhook/flags.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "os"
6 | )
7 |
8 | // Defaults.
9 | const (
10 | lAddressDef = ":8080"
11 | lMetricsAddress = ":8081"
12 | debugDef = false
13 | )
14 |
15 | // Flags are the flags of the program.
16 | type Flags struct {
17 | ListenAddress string
18 | MetricsListenAddress string
19 | Debug bool
20 | CertFile string
21 | KeyFile string
22 | }
23 |
24 | // NewFlags returns the flags of the commandline.
25 | func NewFlags() *Flags {
26 | flags := &Flags{}
27 | fl := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
28 | fl.StringVar(&flags.ListenAddress, "listen-address", lAddressDef, "webhook server listen address")
29 | fl.StringVar(&flags.MetricsListenAddress, "metrics-listen-address", lMetricsAddress, "metrics server listen address")
30 | fl.BoolVar(&flags.Debug, "debug", debugDef, "enable debug mode")
31 | fl.StringVar(&flags.CertFile, "tls-cert-file", "certs/cert.pem", "TLS certificate file")
32 | fl.StringVar(&flags.KeyFile, "tls-key-file", "certs/key.pem", "TLS key file")
33 |
34 | fl.Parse(os.Args[1:])
35 |
36 | return flags
37 | }
38 |
--------------------------------------------------------------------------------
/examples/multiwebhook/cmd/multiwebhook/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "os"
7 | "os/signal"
8 | "syscall"
9 | "time"
10 |
11 | "github.com/prometheus/client_golang/prometheus"
12 | "github.com/prometheus/client_golang/prometheus/promhttp"
13 | "github.com/sirupsen/logrus"
14 | kwhhttp "github.com/slok/kubewebhook/v2/pkg/http"
15 | kwhlog "github.com/slok/kubewebhook/v2/pkg/log"
16 | kwhlogrus "github.com/slok/kubewebhook/v2/pkg/log/logrus"
17 | kwhprometheus "github.com/slok/kubewebhook/v2/pkg/metrics/prometheus"
18 | kwhwebhook "github.com/slok/kubewebhook/v2/pkg/webhook"
19 |
20 | "github.com/slok/kubewebhook/v2/examples/multiwebhook/pkg/webhook/mutating"
21 | "github.com/slok/kubewebhook/v2/examples/multiwebhook/pkg/webhook/validating"
22 | )
23 |
24 | const (
25 | gracePeriod = 3 * time.Second
26 | minReps = 1
27 | maxReps = 12
28 | )
29 |
30 | var (
31 | defLabels = map[string]string{
32 | "webhook": "multiwebhook",
33 | "test": "kubewebhook",
34 | }
35 | )
36 |
37 | // Main is the main program.
38 | type Main struct {
39 | flags *Flags
40 | logger kwhlog.Logger
41 | stopC chan struct{}
42 | }
43 |
44 | // Run will run the main program.
45 | func (m *Main) Run() error {
46 |
47 | logrusLogEntry := logrus.NewEntry(logrus.New())
48 | logrusLogEntry.Logger.SetLevel(logrus.DebugLevel)
49 | m.logger = kwhlogrus.NewLogrus(logrusLogEntry)
50 |
51 | // Create services.
52 | promReg := prometheus.NewRegistry()
53 | metricsRec, err := kwhprometheus.NewRecorder(kwhprometheus.RecorderConfig{Registry: promReg})
54 | if err != nil {
55 | return fmt.Errorf("could not create prometheus recorder: %w", err)
56 | }
57 |
58 | // Create webhooks
59 | mpw, err := mutating.NewPodWebhook(defLabels, m.logger)
60 | if err != nil {
61 | return err
62 | }
63 | mpw = kwhwebhook.NewMeasuredWebhook(metricsRec, mpw)
64 | mpwh, err := kwhhttp.HandlerFor(kwhhttp.HandlerConfig{Webhook: mpw, Logger: m.logger})
65 | if err != nil {
66 | return err
67 | }
68 |
69 | vdw, err := validating.NewDeploymentWebhook(minReps, maxReps, m.logger)
70 | if err != nil {
71 | return err
72 | }
73 | vdw = kwhwebhook.NewMeasuredWebhook(metricsRec, vdw)
74 | vdwh, err := kwhhttp.HandlerFor(kwhhttp.HandlerConfig{Webhook: vdw, Logger: m.logger})
75 | if err != nil {
76 | return err
77 | }
78 |
79 | // Create the servers and set them listenig.
80 | errC := make(chan error)
81 |
82 | // Serve webhooks.
83 | // TODO: Move to it's own service.
84 | go func() {
85 |
86 | m.logger.Infof("webhooks listening on %s...", m.flags.ListenAddress)
87 | mux := http.NewServeMux()
88 | mux.Handle("/webhooks/mutating/pod", mpwh)
89 | mux.Handle("/webhooks/validating/deployment", vdwh)
90 | errC <- http.ListenAndServeTLS(
91 | m.flags.ListenAddress,
92 | m.flags.CertFile,
93 | m.flags.KeyFile,
94 | mux,
95 | )
96 | }()
97 |
98 | // Serve metrics.
99 | metricsHandler := promhttp.HandlerFor(promReg, promhttp.HandlerOpts{})
100 | go func() {
101 | m.logger.Infof("metrics listening on %s...", m.flags.MetricsListenAddress)
102 | errC <- http.ListenAndServe(m.flags.MetricsListenAddress, metricsHandler)
103 | }()
104 |
105 | // Run everything
106 | defer m.stop()
107 |
108 | sigC := m.createSignalChan()
109 | select {
110 | case err := <-errC:
111 | if err != nil {
112 | m.logger.Errorf("error received: %s", err)
113 | return err
114 | }
115 | m.logger.Infof("app finished successfuly")
116 | case s := <-sigC:
117 | m.logger.Infof("signal %s received", s)
118 | return nil
119 | }
120 |
121 | return nil
122 | }
123 |
124 | func (m *Main) stop() {
125 | m.logger.Infof("stopping everything, waiting %s...", gracePeriod)
126 |
127 | close(m.stopC)
128 |
129 | // Stop everything and let them time to stop.
130 | time.Sleep(gracePeriod)
131 | }
132 |
133 | func (m *Main) createSignalChan() chan os.Signal {
134 | c := make(chan os.Signal, 1)
135 | signal.Notify(c, syscall.SIGTERM, syscall.SIGINT)
136 | return c
137 | }
138 |
139 | func main() {
140 | m := Main{
141 | flags: NewFlags(),
142 | stopC: make(chan struct{}),
143 | }
144 |
145 | err := m.Run()
146 | if err != nil {
147 | fmt.Fprintf(os.Stderr, "%s", err)
148 | os.Exit(1)
149 | }
150 | os.Exit(0)
151 |
152 | }
153 |
--------------------------------------------------------------------------------
/examples/multiwebhook/pkg/webhook/mutating/mutator.go:
--------------------------------------------------------------------------------
1 | package mutating
2 |
3 | import (
4 | "context"
5 | "math/rand"
6 | "time"
7 |
8 | kwhlog "github.com/slok/kubewebhook/v2/pkg/log"
9 | kwhmodel "github.com/slok/kubewebhook/v2/pkg/model"
10 | kwhmutating "github.com/slok/kubewebhook/v2/pkg/webhook/mutating"
11 | corev1 "k8s.io/api/core/v1"
12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13 | )
14 |
15 | // podLabelMutator will add labels to a pod. Satisfies mutatingMutator interface.
16 | type podLabelMutator struct {
17 | labels map[string]string
18 | logger kwhlog.Logger
19 | }
20 |
21 | func (m *podLabelMutator) Mutate(_ context.Context, _ *kwhmodel.AdmissionReview, obj metav1.Object) (*kwhmutating.MutatorResult, error) {
22 | pod, ok := obj.(*corev1.Pod)
23 | if !ok {
24 | // If not a pod just continue the mutation chain(if there is one) and don't do nothing.
25 | return &kwhmutating.MutatorResult{}, nil
26 | }
27 |
28 | // Mutate our object with the required annotations.
29 | if pod.Labels == nil {
30 | pod.Labels = make(map[string]string)
31 | }
32 |
33 | for k, v := range m.labels {
34 | pod.Labels[k] = v
35 | }
36 |
37 | return &kwhmutating.MutatorResult{MutatedObject: obj}, nil
38 | }
39 |
40 | type lantencyMutator struct {
41 | maxLatencyMS int
42 | }
43 |
44 | func (m *lantencyMutator) Mutate(_ context.Context, _ *kwhmodel.AdmissionReview, _ metav1.Object) (*kwhmutating.MutatorResult, error) {
45 | rand := rand.New(rand.NewSource(time.Now().UnixNano()))
46 | ms := time.Duration(rand.Intn(m.maxLatencyMS)) * time.Millisecond
47 | time.Sleep(ms)
48 | return &kwhmutating.MutatorResult{}, nil
49 | }
50 |
--------------------------------------------------------------------------------
/examples/multiwebhook/pkg/webhook/mutating/pod.go:
--------------------------------------------------------------------------------
1 | package mutating
2 |
3 | import (
4 | kwhlog "github.com/slok/kubewebhook/v2/pkg/log"
5 | kwhwebhook "github.com/slok/kubewebhook/v2/pkg/webhook"
6 | kwhmutating "github.com/slok/kubewebhook/v2/pkg/webhook/mutating"
7 | corev1 "k8s.io/api/core/v1"
8 | )
9 |
10 | // NewPodWebhook returns a new pod mutating webhook.
11 | func NewPodWebhook(labels map[string]string, logger kwhlog.Logger) (kwhwebhook.Webhook, error) {
12 | // Create mutators.
13 | mutators := []kwhmutating.Mutator{
14 | &podLabelMutator{labels: labels, logger: logger},
15 | &lantencyMutator{maxLatencyMS: 20},
16 | &lantencyMutator{maxLatencyMS: 120},
17 | &lantencyMutator{maxLatencyMS: 300},
18 | &lantencyMutator{maxLatencyMS: 10},
19 | &lantencyMutator{maxLatencyMS: 175},
20 | &lantencyMutator{maxLatencyMS: 80},
21 | &lantencyMutator{maxLatencyMS: 10},
22 | }
23 |
24 | return kwhmutating.NewWebhook(kwhmutating.WebhookConfig{
25 | ID: "multiwebhook-podMutator",
26 | Obj: &corev1.Pod{},
27 | Mutator: kwhmutating.NewChain(logger, mutators...),
28 | Logger: logger,
29 | })
30 | }
31 |
--------------------------------------------------------------------------------
/examples/multiwebhook/pkg/webhook/validating/deployment.go:
--------------------------------------------------------------------------------
1 | package validating
2 |
3 | import (
4 | kwhlog "github.com/slok/kubewebhook/v2/pkg/log"
5 | kwhwebhook "github.com/slok/kubewebhook/v2/pkg/webhook"
6 | kwhvalidating "github.com/slok/kubewebhook/v2/pkg/webhook/validating"
7 | extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
8 | )
9 |
10 | // NewDeploymentWebhook returns a new deployment validationg webhook.
11 | func NewDeploymentWebhook(minReplicas, maxReplicas int, logger kwhlog.Logger) (kwhwebhook.Webhook, error) {
12 |
13 | // Create validators.
14 | repVal := &deploymentReplicasValidator{
15 | maxReplicas: maxReplicas,
16 | minReplicas: minReplicas,
17 | logger: logger,
18 | }
19 |
20 | vals := []kwhvalidating.Validator{
21 | &lantencyValidator{maxLatencyMS: 20},
22 | &lantencyValidator{maxLatencyMS: 120},
23 | &lantencyValidator{maxLatencyMS: 300},
24 | &lantencyValidator{maxLatencyMS: 10},
25 | &lantencyValidator{maxLatencyMS: 175},
26 | &lantencyValidator{maxLatencyMS: 80},
27 | &lantencyValidator{maxLatencyMS: 10},
28 | repVal,
29 | }
30 |
31 | return kwhvalidating.NewWebhook(
32 | kwhvalidating.WebhookConfig{
33 | ID: "multiwebhook-deploymentValidator",
34 | Obj: &extensionsv1beta1.Deployment{},
35 | Validator: kwhvalidating.NewChain(logger, vals...),
36 | Logger: logger,
37 | })
38 | }
39 |
--------------------------------------------------------------------------------
/examples/multiwebhook/pkg/webhook/validating/validator.go:
--------------------------------------------------------------------------------
1 | package validating
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "math/rand"
7 | "time"
8 |
9 | kwhlog "github.com/slok/kubewebhook/v2/pkg/log"
10 | kwhmodel "github.com/slok/kubewebhook/v2/pkg/model"
11 | kwhvalidating "github.com/slok/kubewebhook/v2/pkg/webhook/validating"
12 | extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14 | )
15 |
16 | // deploymentReplicasValidator will validate the replicas are between max and min (inclusive).
17 | type deploymentReplicasValidator struct {
18 | maxReplicas int
19 | minReplicas int
20 | logger kwhlog.Logger
21 | }
22 |
23 | func (d *deploymentReplicasValidator) Validate(_ context.Context, _ *kwhmodel.AdmissionReview, obj metav1.Object) (*kwhvalidating.ValidatorResult, error) {
24 | depl, ok := obj.(*extensionsv1beta1.Deployment)
25 | if !ok {
26 | // If not a deployment just continue the validation chain(if there is one) and don't do nothing.
27 | return &kwhvalidating.ValidatorResult{Valid: true}, nil
28 | }
29 |
30 | reps := int(*depl.Spec.Replicas)
31 | if reps > d.maxReplicas {
32 | return &kwhvalidating.ValidatorResult{
33 | Valid: false,
34 | Message: fmt.Sprintf("%d is not a valid replica number, deployment max replicas are %d", reps, d.maxReplicas),
35 | }, nil
36 | }
37 |
38 | if reps < d.minReplicas {
39 | return &kwhvalidating.ValidatorResult{
40 | Valid: false,
41 | Message: fmt.Sprintf("%d is not a valid replica number, deployment min replicas are %d", reps, d.minReplicas),
42 | }, nil
43 | }
44 |
45 | return &kwhvalidating.ValidatorResult{Valid: true}, nil
46 | }
47 |
48 | type lantencyValidator struct {
49 | maxLatencyMS int
50 | }
51 |
52 | func (m *lantencyValidator) Validate(_ context.Context, _ *kwhmodel.AdmissionReview, obj metav1.Object) (*kwhvalidating.ValidatorResult, error) {
53 | rand := rand.New(rand.NewSource(time.Now().UnixNano()))
54 | ms := time.Duration(rand.Intn(m.maxLatencyMS)) * time.Millisecond
55 | time.Sleep(ms)
56 | return &kwhvalidating.ValidatorResult{Valid: true}, nil
57 | }
58 |
--------------------------------------------------------------------------------
/examples/mutatortesting/mutator.go:
--------------------------------------------------------------------------------
1 | package mutatortesting
2 |
3 | import (
4 | "context"
5 |
6 | corev1 "k8s.io/api/core/v1"
7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8 |
9 | kwhmodel "github.com/slok/kubewebhook/v2/pkg/model"
10 | kwhmutating "github.com/slok/kubewebhook/v2/pkg/webhook/mutating"
11 | )
12 |
13 | // PodLabeler is a mutator that will set labels on the received pods.
14 | type PodLabeler struct {
15 | labels map[string]string
16 | }
17 |
18 | // NewPodLabeler returns a new PodLabeler initialized.
19 | func NewPodLabeler(labels map[string]string) kwhmutating.Mutator {
20 | if labels == nil {
21 | labels = make(map[string]string)
22 | }
23 | return &PodLabeler{
24 | labels: labels,
25 | }
26 | }
27 |
28 | // Mutate will set the required labels on the pods. Satisfies mutating.Mutator interface.
29 | func (p *PodLabeler) Mutate(ctx context.Context, ar *kwhmodel.AdmissionReview, obj metav1.Object) (*kwhmutating.MutatorResult, error) {
30 | pod := obj.(*corev1.Pod)
31 |
32 | if pod.Labels == nil {
33 | pod.Labels = make(map[string]string)
34 | }
35 |
36 | for k, v := range p.labels {
37 | pod.Labels[k] = v
38 | }
39 | return &kwhmutating.MutatorResult{
40 | MutatedObject: pod,
41 | }, nil
42 | }
43 |
--------------------------------------------------------------------------------
/examples/mutatortesting/mutator_test.go:
--------------------------------------------------------------------------------
1 | package mutatortesting_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | corev1 "k8s.io/api/core/v1"
9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10 |
11 | "github.com/slok/kubewebhook/v2/examples/mutatortesting"
12 | )
13 |
14 | func TestPodTaggerMutate(t *testing.T) {
15 | tests := []struct {
16 | name string
17 | pod *corev1.Pod
18 | labels map[string]string
19 | expPod *corev1.Pod
20 | expErr bool
21 | }{
22 | {
23 | name: "Mutating a pod without labels should set the labels correctly.",
24 | pod: &corev1.Pod{
25 | ObjectMeta: metav1.ObjectMeta{
26 | Name: "test",
27 | },
28 | },
29 | labels: map[string]string{"bruce": "wayne", "peter": "parker"},
30 | expPod: &corev1.Pod{
31 | ObjectMeta: metav1.ObjectMeta{
32 | Name: "test",
33 | Labels: map[string]string{"bruce": "wayne", "peter": "parker"},
34 | },
35 | },
36 | },
37 | {
38 | name: "Mutating a pod with labels should aggregate and replace the labels with the existing ones.",
39 | pod: &corev1.Pod{
40 | ObjectMeta: metav1.ObjectMeta{
41 | Name: "test",
42 | Labels: map[string]string{"bruce": "banner", "tony": "stark"},
43 | },
44 | },
45 | labels: map[string]string{"bruce": "wayne", "peter": "parker"},
46 | expPod: &corev1.Pod{
47 | ObjectMeta: metav1.ObjectMeta{
48 | Name: "test",
49 | Labels: map[string]string{"bruce": "wayne", "peter": "parker", "tony": "stark"},
50 | },
51 | },
52 | },
53 | }
54 |
55 | for _, test := range tests {
56 | t.Run(test.name, func(t *testing.T) {
57 | assert := assert.New(t)
58 |
59 | pl := mutatortesting.NewPodLabeler(test.labels)
60 | gotPod := test.pod
61 | _, err := pl.Mutate(context.TODO(), nil, gotPod)
62 |
63 | if test.expErr {
64 | assert.Error(err)
65 | } else if assert.NoError(err) {
66 | // Check the expected pod.
67 | assert.Equal(test.expPod, gotPod)
68 | }
69 | })
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/examples/pod-annotate-metrics/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "fmt"
7 | "net/http"
8 | "os"
9 |
10 | "github.com/prometheus/client_golang/prometheus"
11 | "github.com/prometheus/client_golang/prometheus/promhttp"
12 | "github.com/sirupsen/logrus"
13 | kwhhttp "github.com/slok/kubewebhook/v2/pkg/http"
14 | kwhlogrus "github.com/slok/kubewebhook/v2/pkg/log/logrus"
15 | kwhprometheus "github.com/slok/kubewebhook/v2/pkg/metrics/prometheus"
16 | kwhmodel "github.com/slok/kubewebhook/v2/pkg/model"
17 | kwhwebhook "github.com/slok/kubewebhook/v2/pkg/webhook"
18 | kwhmutating "github.com/slok/kubewebhook/v2/pkg/webhook/mutating"
19 | corev1 "k8s.io/api/core/v1"
20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21 | )
22 |
23 | type config struct {
24 | certFile string
25 | keyFile string
26 | }
27 |
28 | func initFlags() *config {
29 | cfg := &config{}
30 |
31 | fl := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
32 | fl.StringVar(&cfg.certFile, "tls-cert-file", "", "TLS certificate file")
33 | fl.StringVar(&cfg.keyFile, "tls-key-file", "", "TLS key file")
34 |
35 | fl.Parse(os.Args[1:])
36 | return cfg
37 | }
38 |
39 | func run() error {
40 | logrusLogEntry := logrus.NewEntry(logrus.New())
41 | logrusLogEntry.Logger.SetLevel(logrus.DebugLevel)
42 | logger := kwhlogrus.NewLogrus(logrusLogEntry)
43 |
44 | cfg := initFlags()
45 |
46 | // Create mutator.
47 | mt := kwhmutating.MutatorFunc(func(_ context.Context, _ *kwhmodel.AdmissionReview, obj metav1.Object) (*kwhmutating.MutatorResult, error) {
48 | pod, ok := obj.(*corev1.Pod)
49 | if !ok {
50 | return &kwhmutating.MutatorResult{}, nil
51 | }
52 |
53 | // Mutate our object with the required annotations.
54 | if pod.Annotations == nil {
55 | pod.Annotations = make(map[string]string)
56 | }
57 | pod.Annotations["mutated"] = "true"
58 | pod.Annotations["mutator"] = "pod-annotate"
59 |
60 | return &kwhmutating.MutatorResult{
61 | MutatedObject: pod,
62 | Warnings: []string{"pod mutated"},
63 | }, nil
64 | })
65 |
66 | // Prepare metrics
67 | reg := prometheus.NewRegistry()
68 | metricsRec, err := kwhprometheus.NewRecorder(kwhprometheus.RecorderConfig{Registry: reg})
69 | if err != nil {
70 | return fmt.Errorf("could not create Prometheus metrics recorder: %w", err)
71 | }
72 |
73 | // Create webhook.
74 | mcfg := kwhmutating.WebhookConfig{
75 | ID: "pod-annotate",
76 | Mutator: mt,
77 | Logger: logger,
78 | }
79 | wh, err := kwhmutating.NewWebhook(mcfg)
80 | if err != nil {
81 | return fmt.Errorf("error creating webhook: %w", err)
82 | }
83 |
84 | // Get HTTP handler from webhook.
85 | whHandler, err := kwhhttp.HandlerFor(kwhhttp.HandlerConfig{
86 | Webhook: kwhwebhook.NewMeasuredWebhook(metricsRec, wh),
87 | Logger: logger,
88 | })
89 | if err != nil {
90 | return fmt.Errorf("error creating webhook handler: %w", err)
91 | }
92 |
93 | errCh := make(chan error)
94 | // Serve webhook.
95 | go func() {
96 | logger.Infof("Listening on :8080")
97 | err = http.ListenAndServeTLS(":8080", cfg.certFile, cfg.keyFile, whHandler)
98 | if err != nil {
99 | errCh <- fmt.Errorf("error serving webhook: %w", err)
100 | }
101 | errCh <- nil
102 | }()
103 |
104 | // Serve metrics.
105 | go func() {
106 | logger.Infof("Listening metrics on :8081")
107 | err = http.ListenAndServe(":8081", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
108 | if err != nil {
109 | errCh <- fmt.Errorf("error serving webhook metrics: %w", err)
110 | }
111 | errCh <- nil
112 | }()
113 |
114 | err = <-errCh
115 | if err != nil {
116 | return err
117 | }
118 |
119 | return nil
120 | }
121 |
122 | func main() {
123 | err := run()
124 | if err != nil {
125 | fmt.Fprintf(os.Stderr, "error running app: %s", err)
126 | os.Exit(1)
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/examples/pod-annotate-simple/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "fmt"
7 | "net/http"
8 | "os"
9 |
10 | corev1 "k8s.io/api/core/v1"
11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12 |
13 | "github.com/sirupsen/logrus"
14 | kwhhttp "github.com/slok/kubewebhook/v2/pkg/http"
15 | kwhlogrus "github.com/slok/kubewebhook/v2/pkg/log/logrus"
16 | kwhmodel "github.com/slok/kubewebhook/v2/pkg/model"
17 | kwhmutating "github.com/slok/kubewebhook/v2/pkg/webhook/mutating"
18 | )
19 |
20 | type config struct {
21 | certFile string
22 | keyFile string
23 | }
24 |
25 | func initFlags() *config {
26 | cfg := &config{}
27 |
28 | fl := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
29 | fl.StringVar(&cfg.certFile, "tls-cert-file", "", "TLS certificate file")
30 | fl.StringVar(&cfg.keyFile, "tls-key-file", "", "TLS key file")
31 |
32 | fl.Parse(os.Args[1:])
33 | return cfg
34 | }
35 |
36 | func run() error {
37 | logrusLogEntry := logrus.NewEntry(logrus.New())
38 | logrusLogEntry.Logger.SetLevel(logrus.DebugLevel)
39 | logger := kwhlogrus.NewLogrus(logrusLogEntry)
40 |
41 | cfg := initFlags()
42 |
43 | // Create mutator.
44 | mt := kwhmutating.MutatorFunc(func(_ context.Context, _ *kwhmodel.AdmissionReview, obj metav1.Object) (*kwhmutating.MutatorResult, error) {
45 | pod, ok := obj.(*corev1.Pod)
46 | if !ok {
47 | return &kwhmutating.MutatorResult{}, nil
48 | }
49 |
50 | // Mutate our object with the required annotations.
51 | if pod.Annotations == nil {
52 | pod.Annotations = make(map[string]string)
53 | }
54 | pod.Annotations["mutated"] = "true"
55 | pod.Annotations["mutator"] = "pod-annotate"
56 |
57 | return &kwhmutating.MutatorResult{MutatedObject: pod}, nil
58 | })
59 |
60 | // Create webhook.
61 | mcfg := kwhmutating.WebhookConfig{
62 | ID: "pod-annotate",
63 | Mutator: mt,
64 | Logger: logger,
65 | }
66 | wh, err := kwhmutating.NewWebhook(mcfg)
67 | if err != nil {
68 | return fmt.Errorf("error creating webhook: %w", err)
69 | }
70 |
71 | // Get HTTP handler from webhook.
72 | whHandler, err := kwhhttp.HandlerFor(kwhhttp.HandlerConfig{Webhook: wh, Logger: logger})
73 | if err != nil {
74 | return fmt.Errorf("error creating webhook handler: %w", err)
75 | }
76 |
77 | // Serve.
78 | logger.Infof("Listening on :8080")
79 | err = http.ListenAndServeTLS(":8080", cfg.certFile, cfg.keyFile, whHandler)
80 | if err != nil {
81 | return fmt.Errorf("error serving webhook: %w", err)
82 | }
83 |
84 | return nil
85 | }
86 |
87 | func main() {
88 | err := run()
89 | if err != nil {
90 | fmt.Fprintf(os.Stderr, "error running app: %s", err)
91 | os.Exit(1)
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/examples/pod-annotate-tracing/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "fmt"
7 | "net/http"
8 | "os"
9 |
10 | "github.com/sirupsen/logrus"
11 | kwhhttp "github.com/slok/kubewebhook/v2/pkg/http"
12 | kwhlogrus "github.com/slok/kubewebhook/v2/pkg/log/logrus"
13 | kwhmodel "github.com/slok/kubewebhook/v2/pkg/model"
14 | kwhtracing "github.com/slok/kubewebhook/v2/pkg/tracing"
15 | kwhotel "github.com/slok/kubewebhook/v2/pkg/tracing/otel"
16 | kwhwebhook "github.com/slok/kubewebhook/v2/pkg/webhook"
17 | kwhmutating "github.com/slok/kubewebhook/v2/pkg/webhook/mutating"
18 | "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
19 | "go.opentelemetry.io/otel/propagation"
20 | "go.opentelemetry.io/otel/sdk/resource"
21 | otelsdktrace "go.opentelemetry.io/otel/sdk/trace"
22 | semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
23 | corev1 "k8s.io/api/core/v1"
24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25 | )
26 |
27 | type config struct {
28 | certFile string
29 | keyFile string
30 | }
31 |
32 | func initFlags() *config {
33 | cfg := &config{}
34 |
35 | fl := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
36 | fl.StringVar(&cfg.certFile, "tls-cert-file", "", "TLS certificate file")
37 | fl.StringVar(&cfg.keyFile, "tls-key-file", "", "TLS key file")
38 |
39 | _ = fl.Parse(os.Args[1:])
40 | return cfg
41 | }
42 |
43 | func run() error {
44 | logrusLogEntry := logrus.NewEntry(logrus.New())
45 | logrusLogEntry.Logger.SetLevel(logrus.DebugLevel)
46 | logger := kwhlogrus.NewLogrus(logrusLogEntry)
47 |
48 | cfg := initFlags()
49 |
50 | // Create mutator.
51 | mt := kwhmutating.MutatorFunc(func(_ context.Context, _ *kwhmodel.AdmissionReview, obj metav1.Object) (*kwhmutating.MutatorResult, error) {
52 | pod, ok := obj.(*corev1.Pod)
53 | if !ok {
54 | return &kwhmutating.MutatorResult{}, nil
55 | }
56 |
57 | // Mutate our object with the required annotations.
58 | if pod.Annotations == nil {
59 | pod.Annotations = make(map[string]string)
60 | }
61 | pod.Annotations["mutated"] = "true"
62 | pod.Annotations["mutator"] = "pod-annotate"
63 |
64 | return &kwhmutating.MutatorResult{
65 | MutatedObject: pod,
66 | Warnings: []string{"pod mutated"},
67 | }, nil
68 | })
69 |
70 | // Prepare Tracer.
71 | tracer, stop, err := newTracer("pod-annotate-tracing")
72 | if err != nil {
73 | return fmt.Errorf("could not create tracer: %w", err)
74 | }
75 | defer stop()
76 |
77 | // Create webhook.
78 | mcfg := kwhmutating.WebhookConfig{
79 | ID: "pod-annotate",
80 | Mutator: mt,
81 | Logger: logger,
82 | }
83 | wh, err := kwhmutating.NewWebhook(mcfg)
84 | if err != nil {
85 | return fmt.Errorf("error creating webhook: %w", err)
86 | }
87 |
88 | // Get HTTP handler from webhook.
89 | whHandler, err := kwhhttp.HandlerFor(kwhhttp.HandlerConfig{
90 | Webhook: kwhwebhook.NewTracedWebhook(tracer, wh),
91 | Tracer: tracer,
92 | Logger: logger,
93 | })
94 | if err != nil {
95 | return fmt.Errorf("error creating webhook handler: %w", err)
96 | }
97 |
98 | logger.Infof("Listening on :8080")
99 | return http.ListenAndServeTLS(":8080", cfg.certFile, cfg.keyFile, whHandler)
100 | }
101 |
102 | func newTracer(name string) (tracer kwhtracing.Tracer, stop func(), err error) {
103 | exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
104 | if err != nil {
105 | return nil, nil, err
106 | }
107 |
108 | tp := otelsdktrace.NewTracerProvider(
109 | otelsdktrace.WithBatcher(exporter),
110 | otelsdktrace.WithSampler(otelsdktrace.AlwaysSample()),
111 | otelsdktrace.WithResource(resource.NewWithAttributes(
112 | semconv.SchemaURL,
113 | semconv.ServiceNameKey.String(name),
114 | )),
115 | )
116 |
117 | propagator := propagation.NewCompositeTextMapPropagator(propagation.Baggage{}, propagation.TraceContext{})
118 | tracer = kwhotel.NewTracer(tp, propagator)
119 | stop = func() { _ = tp.Shutdown(context.Background()) }
120 | return tracer, stop, nil
121 | }
122 |
123 | func main() {
124 | err := run()
125 | if err != nil {
126 | fmt.Fprintf(os.Stderr, "error running app: %s", err)
127 | os.Exit(1)
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/examples/pod-annotate/deploy/test-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: nginx-test
5 | spec:
6 | replicas: 10
7 | selector:
8 | matchLabels:
9 | app: nginx
10 | template:
11 | metadata:
12 | labels:
13 | app: nginx
14 | spec:
15 | containers:
16 | - name: nginx
17 | image: nginx
--------------------------------------------------------------------------------
/examples/pod-annotate/deploy/webhook-certs.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | data:
3 | cert.pem: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lKQU5UY285SnEyUkwwTUEwR0NTcUdTSWIzRFFFQkJRVUFNQ3N4S1RBbkJnTlYKQkFNTUlIQnZaQzFoYm01dmRHRjBaUzEzWldKb2IyOXJMbVJsWm1GMWJIUXVjM1pqTUI0WERUSXdNREl5TWpJegpNemt6T1ZvWERUTXdNREl4T1RJek16a3pPVm93S3pFcE1DY0dBMVVFQXd3Z2NHOWtMV0Z1Ym05MFlYUmxMWGRsClltaHZiMnN1WkdWbVlYVnNkQzV6ZG1Nd2dnRWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUIKQVFDeFFCSzhrSmxVNFI5a1YzMXVnVGo4Mkwzd3k0SEJmR0hlc3YrdnNFK21PMWt3MGM4RG9ScEs1L3J2UkJDbwpGU3o4SVk2Q0poLzhUektvdTlONStKZVBzbU9PNDg1QUxoT2dqZGxwaDlyMVZyVEp0UE8zOVJtVExvc2VsWjVvClRsM2pZMWcwUURmUUFnNWY5QW4rMXRvSEJ3SmZVNnM3bjkveFo3bEdEVVBkVmFPY2ZWQ0l2bnRxaUoxZ0grOTIKVlJ5Q2I1VmQxTW5lT2hIZDY3WXRGYmp5WWpuT1lsNUxvd0dnVWZ4WnhPMGM2aHJQYmhyZFBiSnN6Znk3ajBlZgpWQmJOQ2t2MmFXZG92aG44SE5ROEVXNmIySnBheVUrOTZwc0hmeVJrYmZTL0E4U3Jxc1NSSWRkcmdBNEdiaFl5CjhVVUNXeTdGQWJVU2c2bFRyMEZOV0JHQkFnTUJBQUdqVURCT01CMEdBMVVkRGdRV0JCUTFpR0JodUhVd0RWbGcKSDh2TWxiU1RjeVpvMlRBZkJnTlZIU01FR0RBV2dCUTFpR0JodUhVd0RWbGdIOHZNbGJTVGN5Wm8yVEFNQmdOVgpIUk1FQlRBREFRSC9NQTBHQ1NxR1NJYjNEUUVCQlFVQUE0SUJBUUNYemE3SGN2QkY1Sk9OYTJEcHB4K2JhdGFECmpRaWptUzh3ZW8wNlhGTy83bG9hNGNFNkxVYTVTUGNKSXFWVVNjSGZibTZjQ0EwYWdSSDE5Y01oQUtYNlhVQkUKcjhnbFhYTFpIeHgrQ1F3TnJ5UGd6YzVNYjUwUUd2Tkw1c1VhL0c4dHpqSzE5cHgxTlNSeTAyMWptNGoxWlZTYwo0U0sxRW5VNjY1TUJUY0NqL0ttcWhOR0E3alVsc1M0a0prbXhBeFAxdUR1SzBaU2ttTUtLL3MzSGtEemI1VWZhCklXQXZHS2Q5VVBBRDRYOHEvUmRnUkxvZ3ZEMThJYXcrSkxwS2NTcStmM3dyQlcyWDRaNTlLbWJjYk13MU9NYzEKSnlvWUZ2SUdhVnU4M2FRZ1BpV0RxTkhxRW5xNC9XRHZrM1JMUDZFSGp4bGVmaGhIZzRpQWgya3NHbmtpCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
4 | key.pem: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBc1VBU3ZKQ1pWT0VmWkZkOWJvRTQvTmk5OE11QndYeGgzckwvcjdCUHBqdFpNTkhQCkE2RWFTdWY2NzBRUXFCVXMvQ0dPZ2lZZi9FOHlxTHZUZWZpWGo3SmpqdVBPUUM0VG9JM1phWWZhOVZhMHliVHoKdC9VWmt5NkxIcFdlYUU1ZDQyTllORUEzMEFJT1gvUUovdGJhQndjQ1gxT3JPNS9mOFdlNVJnMUQzVldqbkgxUQppTDU3YW9pZFlCL3ZkbFVjZ20rVlhkVEozam9SM2V1MkxSVzQ4bUk1em1KZVM2TUJvRkg4V2NUdEhPb2F6MjRhCjNUMnliTTM4dTQ5SG4xUVd6UXBMOW1sbmFMNFovQnpVUEJGdW05aWFXc2xQdmVxYkIzOGtaRzMwdndQRXE2ckUKa1NIWGE0QU9CbTRXTXZGRkFsc3V4UUcxRW9PcFU2OUJUVmdSZ1FJREFRQUJBb0lCQUgzKzU5SzJqdWd4SnRseQovNnlmbXR6UlRTTnY1Z3FkMmd3dC9XYnIwNUo4dVlma2ZGMCtGYXlOZm1pNlg0UzdtTUNaTWUzK0g5cUFpYWc3CjY3WFdLaFp4WGlmaWMyaFgySWZXaldkZ3RScVV5ZXBnQUtjUlNWN0FSUkEybHVYYVh3OFdQVXJYSTFWdlFMeWcKZ3NKdUE0bmZSNlp2bVZiRzdLOXpaZFlQODkvT281M0xzVnkxVGtBWFlIVzZ5V0cxQ1BtY3RBOTdNUFF0d0pTKwpBN2dmcDUzbEFYSWMvOVRhUElpU3FJQkl2dXd6YlNCZ1loZ01MTU5ka0Zib3ZXUWtQMDEySndZQzdHdHlwWWdWCk8zT0R4bitxQ1NQbW9sRExxb3B4YXFuSHByWVoreC9BbkhMcUNVdjZSZ0tNN0wwdk53Und0MFlhK01BeHVHa3UKU1dlUEJnRUNnWUVBMkorQ3UvaTNvV1MvSlhxSW5FcFhYTkxwNEM0KzFZbGpYenhNQmlGTG9iOUR5L1JnSHdVUgpHR3lka3RFWE9hWjZqSGo3SXpUeVY4WmlQcVhOR0VKTTQrVThqR1Rlc3orNThTQjJqZFFBdUZxUVFwejF0UTkrCmQrYTVWNjFaVGg0YUVBZU1aNUtzSlZQMGlONHY3ODJDSklscHZOY2NXZzd5dm9QK21iWG9DOUVDZ1lFQTBYaGMKZEdjRWozUzBWS3FjRVEvTWFMUXZHdUVZbzZIVmJnRWdsNFBBM0M3K2VQcVBXb0xBejNyY2QyamRpU1R3bVNvUwppN3FpeWh0RTc5dW9QdmNIS0c3bXFUTzlJdTVCYlFSQlE3dFNiMEtMSml6UEpCTm5EV05yT0xOVXljOXVmRGU3Cmc2WmhPdmQzWC9Cb0FXUVBmMzlmdnh4TDBJYzA4aTVRVk5MUkJyRUNnWUVBaHlTTWRTU251eUtWTlphS1g2YnAKZGRtSFd2cSs3STAzMTVSUWdZcUlHckt1WXpGa1BqWDFBbDNRdUdXRnJjdTByS3BWVXhPWEZUZUkxeml0Q1ROagpzTkcwd29temZmU0YxbTdBUjU4NWk4bkVNaXFtQjMxUkV4QjRGTURxOUJkSGZ6U1dYWTlkb2pRTVhNN3c3UlF5CjJ3UjNXUDZDaTVURDBDT2MxTnh0bGVFQ2dZQWV4RWtBSitsNWtMQzBCdU1wZG1LVnRuRjh4emN4UWFIeHFHUzcKSEhVRllqbXFWMU1hL2oySHZBb0oxL05DSTVUYlNseXkvVlRQenJXUGJYb0cxWTNObUl4MHFjN01CS2JEZG02SApua243WVpEQ3FLNDhKRVZzcC8rbHNtRnZ5dkgxZU5Jb0FoWWg3UnN4a2tRVWdEZnVpQ3p1Q3gvdm53eGR6Z09xCmtkUjE0UUtCZ0VaaXpOUEs2N1BGZzZrRE9Id0pZYUFyN3h6WVFWOERoaStOLzkzcHRnQURwM1A5clRPWlpMS2kKS2ZXdkx3S25PdjBpWmlCajExWkxML0NJcWFicjM0bDlWaU5BdzhBcE5BajgyN1hOVGdNdWI0TW5kZUw0cHpMRQp4ZVoxaWtURlZIYWtFWk1JMXVzUWZtUXJaTGZoSHNkNTV3ajZKS2d5Z1JRRVM3RDFwa0pqCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==
5 | kind: Secret
6 | metadata:
7 | creationTimestamp: null
8 | name: pod-annotate-webhook-certs
9 |
--------------------------------------------------------------------------------
/examples/pod-annotate/deploy/webhook-registration.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: admissionregistration.k8s.io/v1
2 | kind: MutatingWebhookConfiguration
3 | metadata:
4 | name: pod-annotate-webhook
5 | labels:
6 | app: pod-annotate-webhook
7 | kind: mutator
8 | webhooks:
9 | - name: pod-annotate-webhook.slok.dev
10 | clientConfig:
11 | service:
12 | name: pod-annotate-webhook
13 | namespace: default
14 | path: "/mutate"
15 | caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lKQU5UY285SnEyUkwwTUEwR0NTcUdTSWIzRFFFQkJRVUFNQ3N4S1RBbkJnTlYKQkFNTUlIQnZaQzFoYm01dmRHRjBaUzEzWldKb2IyOXJMbVJsWm1GMWJIUXVjM1pqTUI0WERUSXdNREl5TWpJegpNemt6T1ZvWERUTXdNREl4T1RJek16a3pPVm93S3pFcE1DY0dBMVVFQXd3Z2NHOWtMV0Z1Ym05MFlYUmxMWGRsClltaHZiMnN1WkdWbVlYVnNkQzV6ZG1Nd2dnRWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUIKQVFDeFFCSzhrSmxVNFI5a1YzMXVnVGo4Mkwzd3k0SEJmR0hlc3YrdnNFK21PMWt3MGM4RG9ScEs1L3J2UkJDbwpGU3o4SVk2Q0poLzhUektvdTlONStKZVBzbU9PNDg1QUxoT2dqZGxwaDlyMVZyVEp0UE8zOVJtVExvc2VsWjVvClRsM2pZMWcwUURmUUFnNWY5QW4rMXRvSEJ3SmZVNnM3bjkveFo3bEdEVVBkVmFPY2ZWQ0l2bnRxaUoxZ0grOTIKVlJ5Q2I1VmQxTW5lT2hIZDY3WXRGYmp5WWpuT1lsNUxvd0dnVWZ4WnhPMGM2aHJQYmhyZFBiSnN6Znk3ajBlZgpWQmJOQ2t2MmFXZG92aG44SE5ROEVXNmIySnBheVUrOTZwc0hmeVJrYmZTL0E4U3Jxc1NSSWRkcmdBNEdiaFl5CjhVVUNXeTdGQWJVU2c2bFRyMEZOV0JHQkFnTUJBQUdqVURCT01CMEdBMVVkRGdRV0JCUTFpR0JodUhVd0RWbGcKSDh2TWxiU1RjeVpvMlRBZkJnTlZIU01FR0RBV2dCUTFpR0JodUhVd0RWbGdIOHZNbGJTVGN5Wm8yVEFNQmdOVgpIUk1FQlRBREFRSC9NQTBHQ1NxR1NJYjNEUUVCQlFVQUE0SUJBUUNYemE3SGN2QkY1Sk9OYTJEcHB4K2JhdGFECmpRaWptUzh3ZW8wNlhGTy83bG9hNGNFNkxVYTVTUGNKSXFWVVNjSGZibTZjQ0EwYWdSSDE5Y01oQUtYNlhVQkUKcjhnbFhYTFpIeHgrQ1F3TnJ5UGd6YzVNYjUwUUd2Tkw1c1VhL0c4dHpqSzE5cHgxTlNSeTAyMWptNGoxWlZTYwo0U0sxRW5VNjY1TUJUY0NqL0ttcWhOR0E3alVsc1M0a0prbXhBeFAxdUR1SzBaU2ttTUtLL3MzSGtEemI1VWZhCklXQXZHS2Q5VVBBRDRYOHEvUmRnUkxvZ3ZEMThJYXcrSkxwS2NTcStmM3dyQlcyWDRaNTlLbWJjYk13MU9NYzEKSnlvWUZ2SUdhVnU4M2FRZ1BpV0RxTkhxRW5xNC9XRHZrM1JMUDZFSGp4bGVmaGhIZzRpQWgya3NHbmtpCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
16 | rules:
17 | - operations: [ "CREATE" ]
18 | apiGroups: [""]
19 | apiVersions: ["v1"]
20 | resources: ["pods"]
21 |
--------------------------------------------------------------------------------
/examples/pod-annotate/deploy/webhook-registration.yaml.tpl:
--------------------------------------------------------------------------------
1 | apiVersion: admissionregistration.k8s.io/v1beta1
2 | kind: MutatingWebhookConfiguration
3 | metadata:
4 | name: pod-annotate-webhook
5 | labels:
6 | app: pod-annotate-webhook
7 | kind: mutator
8 | webhooks:
9 | - name: pod-annotate-webhook.slok.dev
10 | clientConfig:
11 | service:
12 | name: pod-annotate-webhook
13 | namespace: default
14 | path: "/mutate"
15 | caBundle: CA_BUNDLE
16 | rules:
17 | - operations: [ "CREATE" ]
18 | apiGroups: [""]
19 | apiVersions: ["v1"]
20 | resources: ["pods"]
21 |
--------------------------------------------------------------------------------
/examples/pod-annotate/deploy/webhook.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: pod-annotate-webhook
5 | labels:
6 | app: pod-annotate-webhook
7 | spec:
8 | replicas: 1
9 | selector:
10 | matchLabels:
11 | app: pod-annotate-webhook
12 | template:
13 | metadata:
14 | labels:
15 | app: pod-annotate-webhook
16 | spec:
17 | containers:
18 | - name: pod-annotate-webhook
19 | image: quay.io/slok/kubewebhook-pod-annotate-example:latest
20 | imagePullPolicy: Always
21 | args:
22 | - -tls-cert-file=/etc/webhook/certs/cert.pem
23 | - -tls-key-file=/etc/webhook/certs/key.pem
24 | volumeMounts:
25 | - name: webhook-certs
26 | mountPath: /etc/webhook/certs
27 | readOnly: true
28 | volumes:
29 | - name: webhook-certs
30 | secret:
31 | secretName: pod-annotate-webhook-certs
32 | ---
33 | apiVersion: v1
34 | kind: Service
35 | metadata:
36 | name: pod-annotate-webhook
37 | labels:
38 | app: pod-annotate-webhook
39 | spec:
40 | ports:
41 | - port: 443
42 | targetPort: 8080
43 | selector:
44 | app: pod-annotate-webhook
45 |
--------------------------------------------------------------------------------
/examples/pod-annotate/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "fmt"
7 | "net/http"
8 | "os"
9 |
10 | corev1 "k8s.io/api/core/v1"
11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12 |
13 | "github.com/sirupsen/logrus"
14 | kwhhttp "github.com/slok/kubewebhook/v2/pkg/http"
15 | kwhlogrus "github.com/slok/kubewebhook/v2/pkg/log/logrus"
16 | kwhmodel "github.com/slok/kubewebhook/v2/pkg/model"
17 | kwhmutating "github.com/slok/kubewebhook/v2/pkg/webhook/mutating"
18 | )
19 |
20 | func annotatePodMutator(_ context.Context, _ *kwhmodel.AdmissionReview, obj metav1.Object) (*kwhmutating.MutatorResult, error) {
21 | pod, ok := obj.(*corev1.Pod)
22 | if !ok {
23 | // If not a pod just continue the mutation chain(if there is one) and don't do nothing.
24 | return &kwhmutating.MutatorResult{}, nil
25 | }
26 |
27 | // Mutate our object with the required annotations.
28 | if pod.Annotations == nil {
29 | pod.Annotations = make(map[string]string)
30 | }
31 | pod.Annotations["mutated"] = "true"
32 | pod.Annotations["mutator"] = "pod-annotate"
33 |
34 | return &kwhmutating.MutatorResult{
35 | MutatedObject: pod,
36 | }, nil
37 | }
38 |
39 | type config struct {
40 | certFile string
41 | keyFile string
42 | }
43 |
44 | func initFlags() *config {
45 | cfg := &config{}
46 |
47 | fl := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
48 | fl.StringVar(&cfg.certFile, "tls-cert-file", "", "TLS certificate file")
49 | fl.StringVar(&cfg.keyFile, "tls-key-file", "", "TLS key file")
50 |
51 | _ = fl.Parse(os.Args[1:])
52 | return cfg
53 | }
54 |
55 | func main() {
56 | logrusLogEntry := logrus.NewEntry(logrus.New())
57 | logrusLogEntry.Logger.SetLevel(logrus.DebugLevel)
58 | logger := kwhlogrus.NewLogrus(logrusLogEntry)
59 |
60 | cfg := initFlags()
61 |
62 | // Create our mutator
63 | mt := kwhmutating.MutatorFunc(annotatePodMutator)
64 |
65 | mcfg := kwhmutating.WebhookConfig{
66 | ID: "podAnnotate",
67 | Obj: &corev1.Pod{},
68 | Mutator: mt,
69 | Logger: logger,
70 | }
71 | wh, err := kwhmutating.NewWebhook(mcfg)
72 | if err != nil {
73 | fmt.Fprintf(os.Stderr, "error creating webhook: %s", err)
74 | os.Exit(1)
75 | }
76 |
77 | // Get the handler for our webhook.
78 | whHandler, err := kwhhttp.HandlerFor(kwhhttp.HandlerConfig{Webhook: wh, Logger: logger})
79 | if err != nil {
80 | fmt.Fprintf(os.Stderr, "error creating webhook handler: %s", err)
81 | os.Exit(1)
82 | }
83 | logger.Infof("Listening on :8080")
84 | err = http.ListenAndServeTLS(":8080", cfg.certFile, cfg.keyFile, whHandler)
85 | if err != nil {
86 | fmt.Fprintf(os.Stderr, "error serving webhook: %s", err)
87 | os.Exit(1)
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/slok/kubewebhook/v2
2 |
3 | go 1.23.0
4 |
5 | require (
6 | github.com/prometheus/client_golang v1.20.5
7 | github.com/sirupsen/logrus v1.9.3
8 | github.com/stretchr/testify v1.10.0
9 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0
10 | go.opentelemetry.io/otel v1.29.0
11 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.29.0
12 | go.opentelemetry.io/otel/sdk v1.29.0
13 | go.opentelemetry.io/otel/trace v1.29.0
14 | gomodules.xyz/jsonpatch/v2 v2.4.0
15 | k8s.io/api v0.31.0
16 | k8s.io/apimachinery v0.31.0
17 | k8s.io/client-go v0.31.0
18 | )
19 |
20 | require (
21 | github.com/beorn7/perks v1.0.1 // indirect
22 | github.com/cespare/xxhash/v2 v2.3.0 // indirect
23 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
24 | github.com/emicklei/go-restful/v3 v3.12.1 // indirect
25 | github.com/evanphx/json-patch v4.12.0+incompatible // indirect
26 | github.com/felixge/httpsnoop v1.0.4 // indirect
27 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect
28 | github.com/go-logr/logr v1.4.2 // indirect
29 | github.com/go-logr/stdr v1.2.2 // indirect
30 | github.com/go-openapi/jsonpointer v0.21.0 // indirect
31 | github.com/go-openapi/jsonreference v0.21.0 // indirect
32 | github.com/go-openapi/swag v0.23.0 // indirect
33 | github.com/gogo/protobuf v1.3.2 // indirect
34 | github.com/golang/protobuf v1.5.4 // indirect
35 | github.com/google/gnostic-models v0.6.8 // indirect
36 | github.com/google/go-cmp v0.6.0 // indirect
37 | github.com/google/gofuzz v1.2.0 // indirect
38 | github.com/google/uuid v1.6.0 // indirect
39 | github.com/imdario/mergo v0.3.16 // indirect
40 | github.com/josharian/intern v1.0.0 // indirect
41 | github.com/json-iterator/go v1.1.12 // indirect
42 | github.com/klauspost/compress v1.17.9 // indirect
43 | github.com/mailru/easyjson v0.7.7 // indirect
44 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
45 | github.com/modern-go/reflect2 v1.0.2 // indirect
46 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
47 | github.com/pkg/errors v0.9.1 // indirect
48 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
49 | github.com/prometheus/client_model v0.6.1 // indirect
50 | github.com/prometheus/common v0.57.0 // indirect
51 | github.com/prometheus/procfs v0.15.1 // indirect
52 | github.com/spf13/pflag v1.0.5 // indirect
53 | github.com/stretchr/objx v0.5.2 // indirect
54 | github.com/x448/float16 v0.8.4 // indirect
55 | go.opentelemetry.io/otel/metric v1.29.0 // indirect
56 | golang.org/x/net v0.33.0 // indirect
57 | golang.org/x/oauth2 v0.22.0 // indirect
58 | golang.org/x/sys v0.28.0 // indirect
59 | golang.org/x/term v0.27.0 // indirect
60 | golang.org/x/text v0.21.0 // indirect
61 | golang.org/x/time v0.6.0 // indirect
62 | google.golang.org/protobuf v1.34.2 // indirect
63 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
64 | gopkg.in/inf.v0 v0.9.1 // indirect
65 | gopkg.in/yaml.v2 v2.4.0 // indirect
66 | gopkg.in/yaml.v3 v3.0.1 // indirect
67 | k8s.io/klog/v2 v2.130.1 // indirect
68 | k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2 // indirect
69 | k8s.io/utils v0.0.0-20240821151609-f90d01438635 // indirect
70 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
71 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
72 | sigs.k8s.io/yaml v1.4.0 // indirect
73 | )
74 |
--------------------------------------------------------------------------------
/hack/scripts/check.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | set -o errexit
4 | set -o nounset
5 |
6 | golangci-lint run --timeout 2m
--------------------------------------------------------------------------------
/hack/scripts/integration-test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | set -o errexit
4 | set -o nounset
5 |
6 | go test `go list ./... | grep test/integration` -tags='integration'
--------------------------------------------------------------------------------
/hack/scripts/mockgen.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | set -o errexit
4 | set -o nounset
5 |
6 | go generate ./...
--------------------------------------------------------------------------------
/hack/scripts/run-integration-local.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # vim: ai:ts=8:sw=8:noet
3 | set -eufCo pipefail
4 | export SHELLOPTS
5 | IFS=$'\t\n'
6 |
7 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
8 | CRD_MANIFESTS_PATH="${CURRENT_DIR}/../../test/integration/crd/manifests"
9 | TEST_WEBHOOK_URL="${TEST_WEBHOOK_URL:-}"
10 |
11 | echo "[*] Make sure you have a running Kubernetes cluster (e.g 'kind create cluster')."
12 | echo "[*] The script will use default KUBECONFIG env var and fallback to default kube config path."
13 |
14 | # Check if we are already using a tunnel address
15 |
16 | if [ -z "${TEST_WEBHOOK_URL}" ]; then
17 | echo "[*] Create a TCP tunnel, for example using ngrok like 'ssh -R 0:localhost:8080 tunnel.us.ngrok.com tcp 22'"
18 | echo "Please enter the tunnel address (e.g '0.tcp.ngrok.io:18776'):"
19 | read TEST_WEBHOOK_URL
20 | echo "[*] Remember you can use 'TEST_WEBHOOK_URL' env var to skip this step."
21 | else
22 | echo "[*] Using '${TEST_WEBHOOK_URL}' from 'TEST_WEBHOOK_URL' env var"
23 | fi
24 |
25 | # Sanitize and export correct env var for tests.
26 | TEST_WEBHOOK_URL="$(echo -n ${TEST_WEBHOOK_URL} | sed s/tcp:\\/\\/// )"
27 | TEST_WEBHOOK_URL="$(echo -n ${TEST_WEBHOOK_URL} | sed s/http:\\/\\/// )"
28 | TEST_WEBHOOK_URL="$(echo -n ${TEST_WEBHOOK_URL} | sed s/https:\\/\\/// )"
29 | TEST_WEBHOOK_URL="$(echo -n ${TEST_WEBHOOK_URL} | xargs)" # Trim spaces.
30 | export TEST_WEBHOOK_URL="https://${TEST_WEBHOOK_URL}"
31 |
32 | echo "[*] Ensuring integration test CRDs are present on the cluster..."
33 | kubectl apply -f "${CRD_MANIFESTS_PATH}"
34 |
35 | echo "[*] Running tests pointing to '${TEST_WEBHOOK_URL}' webhooks..."
36 | "${CURRENT_DIR}"/integration-test.sh
--------------------------------------------------------------------------------
/hack/scripts/run-integration.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 |
4 | set -euo pipefail
5 |
6 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
7 | TUNNEL_INFO_PATH="/tmp/$(openssl rand -hex 12)-ngrok-tcp-tunnel"
8 | LOCAL_PORT=8080
9 | KUBERNETES_VERSION=v${KUBERNETES_VERSION:-1.17.0}
10 | PREVIOUS_KUBECTL_CONTEXT=$(kubectl config current-context) || PREVIOUS_KUBECTL_CONTEXT=""
11 | # If NGROK used, we need ngrok key in b64 set in `NGROK_SSH_PRIVATE_KEY_B64`:
12 | # - echo -e "Host tunnel.us.ngrok.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config
13 | # - echo -e ${NGROK_SSH_PRIVATE_KEY_B64} | base64 -d > ~/.ssh/id_ed25519
14 | # - chmod 400 ~/.ssh/id_ed25519
15 | # If serveo used (Unlimited tunnels):
16 | # - echo -e "Host serveo.net\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config
17 | NGROK=${NGROK:-false}
18 |
19 | function cleanup {
20 | echo "=> Removing kind cluster"
21 | kind delete cluster
22 | if [ ! -z ${PREVIOUS_KUBECTL_CONTEXT} ]; then
23 | echo "=> Setting previous kubectl context"
24 | kubectl config use-context ${PREVIOUS_KUBECTL_CONTEXT}
25 | fi
26 |
27 | echo "=> Removing SSH tunnel"
28 | kill ${TCP_SSH_TUNNEL_PID}
29 | }
30 | trap cleanup EXIT
31 |
32 | # Start Kubernetes cluster.
33 | echo "Start Kind Kubernetes ${KUBERNETES_VERSION} cluster..."
34 | kind create cluster --image kindest/node:${KUBERNETES_VERSION}
35 | export KUBECONFIG="${HOME}/.kube/config"
36 | chmod a+rw ${KUBECONFIG}
37 | kubectl config use-context kind-kind
38 |
39 | # Sleep a bit so the cluster can start correctly.
40 | echo "Sleeping 30s to give the cluster time to set the runtime..."
41 | sleep 30
42 |
43 | # Create tunnel.
44 | echo "Start creating SSH tunnel..."
45 | if [ "${NGROK}" = true ]; then
46 | echo "Start NGROK tunnel..."
47 | nohup ssh -R 0:localhost:${LOCAL_PORT} tunnel.us.ngrok.com tcp 22 > ${TUNNEL_INFO_PATH} &
48 | sleep 5
49 | TCP_SSH_TUNNEL_PID=$!
50 | TCP_SSH_TUNNEL_ADDRESS=$(cat ${TUNNEL_INFO_PATH} | grep Forwarding |sed 's/.*tcp:\/\///')
51 | else
52 | echo "Start serveo tunnel..."
53 | # Force pseudo terminal.
54 | nohup ssh -tt -R 0:localhost:${LOCAL_PORT} serveo.net > ${TUNNEL_INFO_PATH} &
55 | sleep 5
56 | TCP_SSH_TUNNEL_PID=$!
57 | # Filter port from got string (using sed) and sanitize to remove all non digits (using tr).
58 | TCP_SSH_TUNNEL_PORT=$(cat ${TUNNEL_INFO_PATH} | grep Forwarding | sed 's/.*net://' | tr -dc '[:digit:]')
59 | TCP_SSH_TUNNEL_ADDRESS="serveo.net:${TCP_SSH_TUNNEL_PORT}"
60 | fi
61 |
62 | if [[ -z ${TCP_SSH_TUNNEL_ADDRESS} ]]; then
63 | echo "No TCP address with SSH tunnel, something went wrong, exiting..."
64 | exit 1
65 | fi
66 |
67 | echo ""
68 | echo "Created tunnel on ${TCP_SSH_TUNNEL_ADDRESS}..."
69 |
70 | # Sleep a bit so the cluster can start correctly.
71 | echo "Sleeping 5s to give the ${TCP_SSH_TUNNEL_ADDRESS} SSH tunnel time to connect..."
72 | sleep 5
73 |
74 | # Register CRDs.
75 | echo "Registering CRDs..."
76 | kubectl apply -f ${CURRENT_DIR}/../../test/integration/crd/manifests
77 |
78 | # Run tests.
79 | echo "Run tests..."
80 | export TEST_WEBHOOK_URL="https://${TCP_SSH_TUNNEL_ADDRESS}"
81 | export TEST_LISTEN_PORT=${LOCAL_PORT}
82 | ${CURRENT_DIR}/integration-test.sh
83 |
--------------------------------------------------------------------------------
/hack/scripts/unit-test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | set -o errexit
4 | set -o nounset
5 |
6 | go test `go list ./...`
--------------------------------------------------------------------------------
/logo/kubewebhook_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/logo/kubewebhook_logo@0,5x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slok/kubewebhook/71858ec3d9e23d22ba3da0ef29e05b17a8ef3d96/logo/kubewebhook_logo@0,5x.png
--------------------------------------------------------------------------------
/logo/kubewebhook_logo@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slok/kubewebhook/71858ec3d9e23d22ba3da0ef29e05b17a8ef3d96/logo/kubewebhook_logo@1x.png
--------------------------------------------------------------------------------
/pkg/http/example_test.go:
--------------------------------------------------------------------------------
1 | package http_test
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net/http"
7 |
8 | corev1 "k8s.io/api/core/v1"
9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10 |
11 | whhttp "github.com/slok/kubewebhook/v2/pkg/http"
12 | "github.com/slok/kubewebhook/v2/pkg/model"
13 | "github.com/slok/kubewebhook/v2/pkg/webhook/mutating"
14 | "github.com/slok/kubewebhook/v2/pkg/webhook/validating"
15 | )
16 |
17 | // ServeWebhook shows how to serve a validating webhook that denies all pods.
18 | func ExampleHandlerFor_serveWebhook() {
19 | // Create (in)validator.
20 | v := validating.ValidatorFunc(func(_ context.Context, _ *model.AdmissionReview, obj metav1.Object) (*validating.ValidatorResult, error) {
21 | pod, ok := obj.(*corev1.Pod)
22 | if !ok {
23 | return &validating.ValidatorResult{Valid: true}, nil
24 | }
25 |
26 | return &validating.ValidatorResult{
27 | Valid: false,
28 | Message: fmt.Sprintf("%s/%s denied because all pods will be denied", pod.Namespace, pod.Name),
29 | }, nil
30 | })
31 |
32 | // Create webhook (don't check error).
33 | cfg := validating.WebhookConfig{
34 | ID: "serveWebhook",
35 | Obj: &corev1.Pod{},
36 | Validator: v,
37 | }
38 | wh, _ := validating.NewWebhook(cfg)
39 |
40 | // Get webhook handler and serve (webhooks need to be server with TLS).
41 | whHandler, _ := whhttp.HandlerFor(whhttp.HandlerConfig{Webhook: wh})
42 | _ = http.ListenAndServeTLS(":8080", "file.cert", "file.key", whHandler)
43 | }
44 |
45 | // ServeMultipleWebhooks shows how to serve multiple webhooks in the same server.
46 | func ExampleHandlerFor_serveMultipleWebhooks() {
47 | // Create (in)validator.
48 | v := validating.ValidatorFunc(func(_ context.Context, _ *model.AdmissionReview, obj metav1.Object) (*validating.ValidatorResult, error) {
49 | // Assume always is a pod (you should check type assertion is ok to not panic).
50 | pod, ok := obj.(*corev1.Pod)
51 | if !ok {
52 | return &validating.ValidatorResult{Valid: true}, nil
53 | }
54 |
55 | return &validating.ValidatorResult{
56 | Valid: false,
57 | Message: fmt.Sprintf("%s/%s denied because all pods will be denied", pod.Namespace, pod.Name),
58 | }, nil
59 | })
60 |
61 | // Create a stub mutator.
62 | m := mutating.MutatorFunc(func(_ context.Context, _ *model.AdmissionReview, obj metav1.Object) (*mutating.MutatorResult, error) {
63 | return &mutating.MutatorResult{}, nil
64 | })
65 |
66 | // Create webhooks (don't check error).
67 | vcfg := validating.WebhookConfig{
68 | ID: "validatingServeWebhook",
69 | Obj: &corev1.Pod{},
70 | Validator: v,
71 | }
72 | vwh, _ := validating.NewWebhook(vcfg)
73 | vwhHandler, _ := whhttp.HandlerFor(whhttp.HandlerConfig{Webhook: vwh})
74 |
75 | mcfg := mutating.WebhookConfig{
76 | ID: "muratingServeWebhook",
77 | Obj: &corev1.Pod{},
78 | Mutator: m,
79 | }
80 | mwh, _ := mutating.NewWebhook(mcfg)
81 | mwhHandler, _ := whhttp.HandlerFor(whhttp.HandlerConfig{Webhook: mwh})
82 |
83 | // Create a muxer and handle different webhooks in different paths of the server.
84 | mux := http.NewServeMux()
85 | mux.Handle("/validate-pod", vwhHandler)
86 | mux.Handle("/mutate-pod", mwhHandler)
87 | _ = http.ListenAndServeTLS(":8080", "file.cert", "file.key", mux)
88 | }
89 |
--------------------------------------------------------------------------------
/pkg/log/ctx.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "context"
5 | )
6 |
7 | type contextKey string
8 |
9 | // contextLogValuesKey used as unique key to store log values in the context.
10 | const contextLogValuesKey = contextKey("kubewebhook-log-values")
11 |
12 | // CtxWithValues returns a copy of parent in which the key values passed have been
13 | // stored ready to be used using log.Logger.
14 | func CtxWithValues(parent context.Context, kv Kv) context.Context {
15 | // Maybe we have values already set.
16 | oldValues, ok := parent.Value(contextLogValuesKey).(Kv)
17 | if !ok {
18 | oldValues = Kv{}
19 | }
20 |
21 | // Copy old and received values into the new kv.
22 | newValues := Kv{}
23 | for k, v := range oldValues {
24 | newValues[k] = v
25 | }
26 | for k, v := range kv {
27 | newValues[k] = v
28 | }
29 |
30 | return context.WithValue(parent, contextLogValuesKey, newValues)
31 | }
32 |
33 | // ValuesFromCtx gets the log Key values from a context.
34 | func ValuesFromCtx(ctx context.Context) Kv {
35 | values, ok := ctx.Value(contextLogValuesKey).(Kv)
36 | if !ok {
37 | return Kv{}
38 | }
39 |
40 | return values
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/log/log.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import "context"
4 |
5 | // Kv is a helper type for structured logging fields usage.
6 | type Kv = map[string]interface{}
7 |
8 | // Logger is the interface that the loggers used by the library will use.
9 | type Logger interface {
10 | Infof(format string, args ...interface{})
11 | Warningf(format string, args ...interface{})
12 | Errorf(format string, args ...interface{})
13 | Debugf(format string, args ...interface{})
14 | WithValues(values map[string]interface{}) Logger
15 | WithCtxValues(ctx context.Context) Logger
16 | SetValuesOnCtx(parent context.Context, values map[string]interface{}) context.Context
17 | }
18 |
19 | // Noop logger doesn't log anything.
20 | const Noop = noop(0)
21 |
22 | type noop int
23 |
24 | func (n noop) Infof(format string, args ...interface{}) {}
25 | func (n noop) Warningf(format string, args ...interface{}) {}
26 | func (n noop) Errorf(format string, args ...interface{}) {}
27 | func (n noop) Debugf(format string, args ...interface{}) {}
28 | func (n noop) WithValues(map[string]interface{}) Logger { return n }
29 | func (n noop) WithCtxValues(context.Context) Logger { return n }
30 | func (n noop) SetValuesOnCtx(parent context.Context, values Kv) context.Context { return parent }
31 |
--------------------------------------------------------------------------------
/pkg/log/logrus/logrus.go:
--------------------------------------------------------------------------------
1 | package logrus
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/sirupsen/logrus"
7 |
8 | "github.com/slok/kubewebhook/v2/pkg/log"
9 | )
10 |
11 | type logger struct {
12 | *logrus.Entry
13 | }
14 |
15 | // NewLogrus returns a new log.Logger for a logrus implementation.
16 | func NewLogrus(l *logrus.Entry) log.Logger {
17 | return logger{Entry: l}
18 | }
19 |
20 | func (l logger) WithValues(kv log.Kv) log.Logger {
21 | newLogger := l.Entry.WithFields(kv)
22 | return NewLogrus(newLogger)
23 | }
24 |
25 | func (l logger) WithCtxValues(ctx context.Context) log.Logger {
26 | return l.WithValues(log.ValuesFromCtx(ctx))
27 | }
28 |
29 | func (l logger) SetValuesOnCtx(parent context.Context, values log.Kv) context.Context {
30 | return log.CtxWithValues(parent, values)
31 | }
32 |
--------------------------------------------------------------------------------
/pkg/metrics/prometheus/prometheus.go:
--------------------------------------------------------------------------------
1 | package prometheus
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "strconv"
7 |
8 | "github.com/prometheus/client_golang/prometheus"
9 | "github.com/slok/kubewebhook/v2/pkg/webhook"
10 | )
11 |
12 | const (
13 | prefix = "kubewebhook"
14 | )
15 |
16 | // RecorderConfig is the configuration of the recorder.
17 | type RecorderConfig struct {
18 | Registry prometheus.Registerer
19 | ReviewOpBuckets []float64
20 | }
21 |
22 | func (c *RecorderConfig) defaults() error {
23 | if c.Registry == nil {
24 | c.Registry = prometheus.DefaultRegisterer
25 | }
26 |
27 | if c.ReviewOpBuckets == nil {
28 | c.ReviewOpBuckets = prometheus.DefBuckets
29 | }
30 |
31 | return nil
32 | }
33 |
34 | // Recorder knows how to measure the metrics of the library using Prometheus
35 | // as the backend for the measurements.
36 | type Recorder struct {
37 | webhookValReviewDuration *prometheus.HistogramVec
38 | webhookMutReviewDuration *prometheus.HistogramVec
39 | webhookReviewWarnings *prometheus.CounterVec
40 | }
41 |
42 | // NewRecorder returns a new Prometheus metrics recorder.
43 | func NewRecorder(config RecorderConfig) (*Recorder, error) {
44 | err := config.defaults()
45 | if err != nil {
46 | return nil, fmt.Errorf("invalid configuration: %w", err)
47 | }
48 |
49 | r := &Recorder{
50 | webhookValReviewDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
51 | Namespace: prefix,
52 | Subsystem: "validating_webhook",
53 | Name: "review_duration_seconds",
54 | Help: "The duration of the admission review handled by a validating webhook.",
55 | Buckets: config.ReviewOpBuckets,
56 | }, []string{"webhook_id", "webhook_version", "resource_namespace", "resource_kind", "operation", "dry_run", "success", "allowed"}),
57 |
58 | webhookMutReviewDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
59 | Namespace: prefix,
60 | Subsystem: "mutating_webhook",
61 | Name: "review_duration_seconds",
62 | Help: "The duration of the admission review handled by a mutating webhook.",
63 | Buckets: config.ReviewOpBuckets,
64 | }, []string{"webhook_id", "webhook_version", "resource_namespace", "resource_kind", "operation", "dry_run", "success", "mutated"}),
65 |
66 | webhookReviewWarnings: prometheus.NewCounterVec(prometheus.CounterOpts{
67 | Namespace: prefix,
68 | Subsystem: "webhook",
69 | Name: "review_warnings_total",
70 | Help: "The total number warnings the webhooks are returning on the review process.",
71 | }, []string{"webhook_id", "webhook_version", "resource_namespace", "resource_kind", "operation", "dry_run", "success"}),
72 | }
73 |
74 | // Register our metrics on the received recorder.
75 | config.Registry.MustRegister(
76 | r.webhookValReviewDuration,
77 | r.webhookMutReviewDuration,
78 | r.webhookReviewWarnings,
79 | )
80 |
81 | return r, nil
82 | }
83 |
84 | var _ webhook.MetricsRecorder = Recorder{}
85 |
86 | // MeasureValidatingWebhookReviewOp measures a validating webhook review operation on Prometheus.
87 | func (r Recorder) MeasureValidatingWebhookReviewOp(_ context.Context, data webhook.MeasureValidatingOpData) {
88 | // Measure Operation.
89 | r.webhookValReviewDuration.With(prometheus.Labels{
90 | "webhook_id": data.WebhookID,
91 | "webhook_version": data.AdmissionReviewVersion,
92 | "resource_namespace": data.ResourceNamespace,
93 | "resource_kind": data.ResourceKind,
94 | "operation": data.Operation,
95 | "dry_run": strconv.FormatBool(data.DryRun),
96 | "success": strconv.FormatBool(data.Success),
97 | "allowed": strconv.FormatBool(data.Allowed),
98 | }).Observe(data.Duration.Seconds())
99 |
100 | // Measure warnings.
101 | r.webhookReviewWarnings.With(prometheus.Labels{
102 | "webhook_id": data.WebhookID,
103 | "webhook_version": data.AdmissionReviewVersion,
104 | "resource_namespace": data.ResourceNamespace,
105 | "resource_kind": data.ResourceKind,
106 | "operation": data.Operation,
107 | "dry_run": strconv.FormatBool(data.DryRun),
108 | "success": strconv.FormatBool(data.Success),
109 | }).Add(float64(data.WarningsNumber))
110 | }
111 |
112 | // MeasureMutatingWebhookReviewOp measures a mutating webhook review operation on Prometheus.
113 | func (r Recorder) MeasureMutatingWebhookReviewOp(_ context.Context, data webhook.MeasureMutatingOpData) {
114 | // Measure operation.
115 | r.webhookMutReviewDuration.With(prometheus.Labels{
116 | "webhook_id": data.WebhookID,
117 | "webhook_version": data.AdmissionReviewVersion,
118 | "resource_namespace": data.ResourceNamespace,
119 | "resource_kind": data.ResourceKind,
120 | "operation": data.Operation,
121 | "dry_run": strconv.FormatBool(data.DryRun),
122 | "success": strconv.FormatBool(data.Success),
123 | "mutated": strconv.FormatBool(data.Mutated),
124 | }).Observe(data.Duration.Seconds())
125 |
126 | // Measure warnings.
127 | r.webhookReviewWarnings.With(prometheus.Labels{
128 | "webhook_id": data.WebhookID,
129 | "webhook_version": data.AdmissionReviewVersion,
130 | "resource_namespace": data.ResourceNamespace,
131 | "resource_kind": data.ResourceKind,
132 | "operation": data.Operation,
133 | "dry_run": strconv.FormatBool(data.DryRun),
134 | "success": strconv.FormatBool(data.Success),
135 | }).Add(float64(data.WarningsNumber))
136 | }
137 |
--------------------------------------------------------------------------------
/pkg/model/response.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | // AdmissionResponse is the interface type that all the different
4 | // types of webhooks must satisfy.
5 | type AdmissionResponse interface {
6 | isWebhookResponse() bool // Sealed interface, we only want to abstract all webhook responses.
7 | }
8 |
9 | // ValidatingAdmissionResponse is the response for validating webhooks.
10 | type ValidatingAdmissionResponse struct {
11 | admissionResponse
12 |
13 | ID string
14 | Allowed bool
15 | Message string
16 | Warnings []string
17 | }
18 |
19 | // MutatingAdmissionResponse is the response for mutating webhooks.
20 | type MutatingAdmissionResponse struct {
21 | admissionResponse
22 |
23 | ID string
24 | JSONPatchPatch []byte
25 | Warnings []string
26 | }
27 |
28 | // Helper type to satisfy the AdmissionResponse sealed interface.
29 | type admissionResponse struct{}
30 |
31 | func (admissionResponse) isWebhookResponse() bool { return true }
32 |
--------------------------------------------------------------------------------
/pkg/model/review.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | admissionv1 "k8s.io/api/admission/v1"
5 | admissionv1beta1 "k8s.io/api/admission/v1beta1"
6 | authenticationv1 "k8s.io/api/authentication/v1"
7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8 | "k8s.io/apimachinery/pkg/runtime"
9 | )
10 |
11 | // AdmissionReviewVersion reprensents the version of the admission review.
12 | type AdmissionReviewVersion string
13 |
14 | const (
15 | // AdmissionReviewVersionV1beta1 is the version of the v1beta1 webhooks admission review.
16 | AdmissionReviewVersionV1beta1 AdmissionReviewVersion = "v1beta1"
17 |
18 | // AdmissionReviewVersionV1 is the version of the v1 webhooks admission review.
19 | AdmissionReviewVersionV1 AdmissionReviewVersion = "v1"
20 | )
21 |
22 | // AdmissionReviewOp represents an admission review operation.
23 | type AdmissionReviewOp string
24 |
25 | const (
26 | // OperationUnknown is an unknown operation.
27 | OperationUnknown AdmissionReviewOp = "unknown"
28 | // OperationCreate is a create operation.
29 | OperationCreate AdmissionReviewOp = "create"
30 | // OperationUpdate is a update operation.
31 | OperationUpdate AdmissionReviewOp = "update"
32 | // OperationDelete is a delete operation.
33 | OperationDelete AdmissionReviewOp = "delete"
34 | // OperationConnect is a connect operation.
35 | OperationConnect AdmissionReviewOp = "connect"
36 | )
37 |
38 | // AdmissionReview represents a request admission review.
39 | type AdmissionReview struct {
40 | OriginalAdmissionReview runtime.Object
41 |
42 | ID string
43 | Name string
44 | Namespace string
45 | Operation AdmissionReviewOp
46 | Version AdmissionReviewVersion
47 | RequestGVR *metav1.GroupVersionResource
48 | RequestGVK *metav1.GroupVersionKind
49 | OldObjectRaw []byte
50 | NewObjectRaw []byte
51 | DryRun bool
52 | UserInfo authenticationv1.UserInfo
53 | }
54 |
55 | // NewAdmissionReviewV1Beta1 returns a new AdmissionReview from a admission/v1beta/admissionReview.
56 | func NewAdmissionReviewV1Beta1(ar *admissionv1beta1.AdmissionReview) AdmissionReview {
57 | // Default false.
58 | dryRun := false
59 | if ar.Request.DryRun != nil {
60 | dryRun = *ar.Request.DryRun
61 | }
62 |
63 | return AdmissionReview{
64 | OriginalAdmissionReview: ar,
65 | ID: string(ar.Request.UID),
66 | Name: ar.Request.Name,
67 | Version: AdmissionReviewVersionV1beta1,
68 | Namespace: ar.Request.Namespace,
69 | Operation: v1Beta1OperationToModel(ar.Request.Operation),
70 | OldObjectRaw: ar.Request.OldObject.Raw,
71 | NewObjectRaw: ar.Request.Object.Raw,
72 | RequestGVR: v1Beta1ResourceToModel(ar),
73 | RequestGVK: v1Beta1KindToModel(ar),
74 | UserInfo: ar.Request.UserInfo,
75 | DryRun: dryRun,
76 | }
77 | }
78 |
79 | func v1Beta1ResourceToModel(ar *admissionv1beta1.AdmissionReview) *metav1.GroupVersionResource {
80 | if ar.Request.RequestResource != nil {
81 | return ar.Request.RequestResource
82 | }
83 |
84 | return &metav1.GroupVersionResource{
85 | Group: ar.Request.Resource.Group,
86 | Version: ar.Request.Resource.Version,
87 | Resource: ar.Request.Resource.Resource,
88 | }
89 | }
90 |
91 | func v1Beta1KindToModel(ar *admissionv1beta1.AdmissionReview) *metav1.GroupVersionKind {
92 | if ar.Request.RequestKind != nil {
93 | return ar.Request.RequestKind
94 | }
95 |
96 | return &metav1.GroupVersionKind{
97 | Group: ar.Request.Kind.Group,
98 | Version: ar.Request.Kind.Version,
99 | Kind: ar.Request.Kind.Kind,
100 | }
101 | }
102 |
103 | func v1Beta1OperationToModel(op admissionv1beta1.Operation) AdmissionReviewOp {
104 | switch op {
105 | case admissionv1beta1.Create:
106 | return OperationCreate
107 | case admissionv1beta1.Update:
108 | return OperationUpdate
109 | case admissionv1beta1.Delete:
110 | return OperationDelete
111 | case admissionv1beta1.Connect:
112 | return OperationConnect
113 | }
114 |
115 | return OperationUnknown
116 | }
117 |
118 | // NewAdmissionReviewV1 returns a new AdmissionReview from a admission/v1/admissionReview.
119 | func NewAdmissionReviewV1(ar *admissionv1.AdmissionReview) AdmissionReview {
120 | // Default false.
121 | dryRun := false
122 | if ar.Request.DryRun != nil {
123 | dryRun = *ar.Request.DryRun
124 | }
125 |
126 | return AdmissionReview{
127 | OriginalAdmissionReview: ar,
128 | ID: string(ar.Request.UID),
129 | Name: ar.Request.Name,
130 | Namespace: ar.Request.Namespace,
131 | Version: AdmissionReviewVersionV1,
132 | Operation: v1OperationToModel(ar.Request.Operation),
133 | OldObjectRaw: ar.Request.OldObject.Raw,
134 | NewObjectRaw: ar.Request.Object.Raw,
135 | RequestGVR: v1ResourceToModel(ar),
136 | RequestGVK: v1KindToModel(ar),
137 | UserInfo: ar.Request.UserInfo,
138 | DryRun: dryRun,
139 | }
140 | }
141 |
142 | func v1ResourceToModel(ar *admissionv1.AdmissionReview) *metav1.GroupVersionResource {
143 | if ar.Request.RequestResource != nil {
144 | return ar.Request.RequestResource
145 | }
146 |
147 | return &metav1.GroupVersionResource{
148 | Group: ar.Request.Resource.Group,
149 | Version: ar.Request.Resource.Version,
150 | Resource: ar.Request.Resource.Resource,
151 | }
152 | }
153 |
154 | func v1KindToModel(ar *admissionv1.AdmissionReview) *metav1.GroupVersionKind {
155 | if ar.Request.RequestKind != nil {
156 | return ar.Request.RequestKind
157 | }
158 |
159 | return &metav1.GroupVersionKind{
160 | Group: ar.Request.Kind.Group,
161 | Version: ar.Request.Kind.Version,
162 | Kind: ar.Request.Kind.Kind,
163 | }
164 | }
165 |
166 | func v1OperationToModel(op admissionv1.Operation) AdmissionReviewOp {
167 | switch op {
168 | case admissionv1.Create:
169 | return OperationCreate
170 | case admissionv1.Update:
171 | return OperationUpdate
172 | case admissionv1.Delete:
173 | return OperationDelete
174 | case admissionv1.Connect:
175 | return OperationConnect
176 | }
177 |
178 | return OperationUnknown
179 | }
180 |
--------------------------------------------------------------------------------
/pkg/model/webhook.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | // WebhookKind is the webhook kind.
4 | type WebhookKind string
5 |
6 | const (
7 | // WebhookKindMutating is the kind of the webhooks that mutate.
8 | WebhookKindMutating = "mutating"
9 | // WebhookKindValidating is the kind of the webhooks that validate.
10 | WebhookKindValidating = "validating"
11 | )
12 |
--------------------------------------------------------------------------------
/pkg/tracing/otel/otel.go:
--------------------------------------------------------------------------------
1 | package otel
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | "net/http"
9 |
10 | "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
11 | "go.opentelemetry.io/otel/attribute"
12 | "go.opentelemetry.io/otel/codes"
13 | "go.opentelemetry.io/otel/propagation"
14 | oteltrace "go.opentelemetry.io/otel/trace"
15 |
16 | "github.com/slok/kubewebhook/v2/pkg/tracing"
17 | )
18 |
19 | const tracerName = "github.com/slok/kubewebhook"
20 |
21 | var errNoSpanOnContext = errors.New("no span on context")
22 |
23 | type tracer struct {
24 | otelTracerProvider oteltrace.TracerProvider
25 | otelTracer oteltrace.Tracer
26 | otelPropagator propagation.TextMapPropagator
27 | values map[string]interface{}
28 | }
29 |
30 | // NewTracer returns a new Open telemetry tracer.
31 | func NewTracer(otelTracerProvider oteltrace.TracerProvider, otelPropagator propagation.TextMapPropagator) tracing.Tracer {
32 | return tracer{
33 | otelTracerProvider: otelTracerProvider,
34 | otelTracer: otelTracerProvider.Tracer(tracerName),
35 | otelPropagator: otelPropagator,
36 | }
37 | }
38 |
39 | func (t tracer) WithValues(values map[string]interface{}) tracing.Tracer {
40 | return tracer{
41 | otelTracerProvider: t.otelTracerProvider,
42 | otelTracer: t.otelTracer,
43 | otelPropagator: t.otelPropagator,
44 | values: values,
45 | }
46 | }
47 |
48 | func (t tracer) TraceID(ctx context.Context) string {
49 | span, err := t.spanFromContext(ctx)
50 | if err != nil {
51 | return ""
52 | }
53 |
54 | return span.SpanContext().TraceID().String()
55 | }
56 |
57 | func (t tracer) spanFromContext(ctx context.Context) (oteltrace.Span, error) {
58 | otelSpan := oteltrace.SpanFromContext(ctx)
59 |
60 | // Is there any span on the context?.
61 | // Check if noop: https://github.com/open-telemetry/opentelemetry-go/blob/39fe8092ed0156b6cbb8225589a81b86124fa491/trace/noop.go#L57
62 | if !otelSpan.IsRecording() {
63 | return nil, errNoSpanOnContext
64 | }
65 |
66 | return otelSpan, nil
67 | }
68 |
69 | func (t tracer) NewTrace(ctx context.Context, name string) context.Context {
70 | ctx, _ = t.newOtelSpan(ctx, name)
71 | return ctx
72 | }
73 |
74 | func (t tracer) EndTrace(ctx context.Context, e error) {
75 | span, err := t.spanFromContext(ctx)
76 | if err != nil {
77 | return
78 | }
79 |
80 | if e != nil {
81 | span.RecordError(e)
82 | span.SetStatus(codes.Error, e.Error())
83 | } else {
84 | span.SetStatus(codes.Ok, "")
85 | }
86 |
87 | span.End()
88 | }
89 |
90 | func (t tracer) AddTraceValues(ctx context.Context, values map[string]interface{}) {
91 | span, err := t.spanFromContext(ctx)
92 | if err != nil {
93 | return
94 | }
95 |
96 | span.SetAttributes(
97 | t.mapValuesToOtelAttributes(values)...,
98 | )
99 | }
100 |
101 | func (t tracer) TraceHTTPHandler(name string, h http.Handler) http.Handler {
102 | return otelhttp.NewHandler(h, name,
103 | otelhttp.WithSpanOptions(oteltrace.WithAttributes(
104 | t.mapValuesToOtelAttributes(t.values)...,
105 | )),
106 | otelhttp.WithTracerProvider(t.otelTracerProvider),
107 | otelhttp.WithPropagators(t.otelPropagator))
108 | }
109 |
110 | func (t tracer) TraceHTTPClient(name string, c *http.Client) *http.Client {
111 | opts := []otelhttp.Option{
112 | otelhttp.WithSpanOptions(oteltrace.WithAttributes(
113 | t.mapValuesToOtelAttributes(t.values)...,
114 | )),
115 | otelhttp.WithTracerProvider(t.otelTracerProvider),
116 | otelhttp.WithPropagators(t.otelPropagator),
117 | }
118 |
119 | // Set custom formatter.
120 | if name != "" {
121 | opts = append(opts, otelhttp.WithSpanNameFormatter(func(_ string, r *http.Request) string {
122 | return fmt.Sprintf("%s: HTTP %s", name, r.Method)
123 | }))
124 | }
125 |
126 | return &http.Client{
127 | CheckRedirect: c.CheckRedirect,
128 | Jar: c.Jar,
129 | Timeout: c.Timeout,
130 | Transport: otelhttp.NewTransport(c.Transport, opts...),
131 | }
132 | }
133 |
134 | func (t tracer) TraceFunc(ctx context.Context, name string, f func(ctx context.Context) (values map[string]interface{}, err error)) {
135 | ctx, span := t.newOtelSpan(ctx, name)
136 |
137 | values, err := f(ctx)
138 |
139 | span.SetAttributes(
140 | t.mapValuesToOtelAttributes(values)...,
141 | )
142 |
143 | if err != nil {
144 | span.RecordError(err)
145 | span.SetStatus(codes.Error, err.Error())
146 | } else {
147 | span.SetStatus(codes.Ok, "")
148 | }
149 |
150 | span.End()
151 | }
152 |
153 | func (t tracer) AddTraceEvent(ctx context.Context, event string, values map[string]interface{}) {
154 | span, err := t.spanFromContext(ctx)
155 | if err != nil {
156 | return
157 | }
158 |
159 | attrs := t.mapValuesToOtelAttributes(values)
160 | span.AddEvent(event, oteltrace.WithAttributes(attrs...))
161 | }
162 |
163 | func (t tracer) newOtelSpan(ctx context.Context, name string) (context.Context, oteltrace.Span) {
164 | return t.otelTracer.Start(ctx, name, oteltrace.WithAttributes(
165 | t.mapValuesToOtelAttributes(t.values)...,
166 | ))
167 | }
168 |
169 | func (t tracer) mapValuesToOtelAttributes(values map[string]interface{}) []attribute.KeyValue {
170 | kvs := make([]attribute.KeyValue, 0, len(values))
171 | for k, v := range values {
172 | kvs = append(kvs, any(k, v))
173 | }
174 |
175 | return kvs
176 | }
177 |
178 | func any(k string, v interface{}) attribute.KeyValue {
179 | if v == nil {
180 | return attribute.String(k, "")
181 | }
182 |
183 | switch typed := v.(type) {
184 | case bool:
185 | return attribute.Bool(k, typed)
186 | case []bool:
187 | return attribute.BoolSlice(k, typed)
188 | case int:
189 | return attribute.Int(k, typed)
190 | case []int:
191 | return attribute.IntSlice(k, typed)
192 | case int8:
193 | return attribute.Int(k, int(typed))
194 | case []int8:
195 | ls := make([]int, 0, len(typed))
196 | for _, i := range typed {
197 | ls = append(ls, int(i))
198 | }
199 | return attribute.IntSlice(k, ls)
200 | case int16:
201 | return attribute.Int(k, int(typed))
202 | case []int16:
203 | ls := make([]int, 0, len(typed))
204 | for _, i := range typed {
205 | ls = append(ls, int(i))
206 | }
207 | return attribute.IntSlice(k, ls)
208 | case int32:
209 | return attribute.Int64(k, int64(typed))
210 | case []int32:
211 | ls := make([]int64, 0, len(typed))
212 | for _, i := range typed {
213 | ls = append(ls, int64(i))
214 | }
215 | return attribute.Int64Slice(k, ls)
216 | case int64:
217 | return attribute.Int64(k, typed)
218 | case []int64:
219 | return attribute.Int64Slice(k, typed)
220 | case float64:
221 | return attribute.Float64(k, typed)
222 | case []float64:
223 | return attribute.Float64Slice(k, typed)
224 | case string:
225 | return attribute.String(k, typed)
226 | case []string:
227 | return attribute.StringSlice(k, typed)
228 | }
229 |
230 | if stringer, ok := v.(fmt.Stringer); ok {
231 | return attribute.String(k, stringer.String())
232 | }
233 | if b, err := json.Marshal(v); b != nil && err == nil {
234 | return attribute.String(k, string(b))
235 | }
236 | return attribute.String(k, fmt.Sprintf("%v", v))
237 | }
238 |
--------------------------------------------------------------------------------
/pkg/tracing/tracing.go:
--------------------------------------------------------------------------------
1 | package tracing
2 |
3 | import (
4 | "context"
5 | "net/http"
6 | )
7 |
8 | // Tracer is the interface a tracer for the application should implement.
9 | type Tracer interface {
10 | // WithValues returns a new tracer that will add the values to all the
11 | // traces created by the returned tracer.
12 | WithValues(values map[string]interface{}) Tracer
13 | // NewTrace returns a context with a trace created in it.
14 | NewTrace(ctx context.Context, name string) context.Context
15 | // EndTrace ends the trace that is currently on the context.
16 | // If there are no traces on the context it will be a noop.
17 | EndTrace(ctx context.Context, err error)
18 | // TraceHTTPHandler returns a new http.Handler wrapped with the required
19 | // things to trace the original HTTP handler execution.
20 | TraceHTTPHandler(name string, h http.Handler) http.Handler
21 | // TraceHTTPClient returns a new http.Client based on the received one
22 | // this new Client will trace all the HTTP requests executed with the
23 | // client.
24 | //
25 | // Note: To trace correctly from parent trace/spans, the requests used by the client
26 | // should have the context set on the request.
27 | TraceHTTPClient(name string, c *http.Client) *http.Client
28 | // TraceFunc is a helper that executes a function and trace its execution, the execution
29 | // can return values and errors that will be used for the trace information.
30 | TraceFunc(ctx context.Context, name string, f func(ctx context.Context) (values map[string]interface{}, err error))
31 | // TraceID returns the current trace ID. This is useful to measure/record somewhere and point
32 | // the current trace (e.g with a logger).
33 | TraceID(ctx context.Context) string
34 | // AddTraceValues adds values to the current context trace.
35 | // If there are not traces on the context is a noop.
36 | AddTraceValues(ctx context.Context, values map[string]interface{})
37 | // AddTraceEvent adds an event on the current context trace.
38 | // If there are not traces on the context is a noop.
39 | AddTraceEvent(ctx context.Context, event string, values map[string]interface{})
40 | }
41 |
42 | // Noop tracer doesn't trace anything.
43 | const Noop = noop(0)
44 |
45 | type noop int
46 |
47 | func (n noop) WithValues(values map[string]interface{}) Tracer { return n }
48 | func (n noop) TraceID(ctx context.Context) string { return "" }
49 | func (n noop) NewTrace(ctx context.Context, name string) context.Context { return ctx }
50 | func (n noop) EndTrace(ctx context.Context, err error) {}
51 | func (n noop) TraceHTTPHandler(name string, h http.Handler) http.Handler { return h }
52 | func (n noop) TraceHTTPClient(name string, c *http.Client) *http.Client { return c }
53 | func (n noop) TraceFunc(ctx context.Context, name string, f func(ctx context.Context) (values map[string]interface{}, err error)) {
54 | _, _ = f(ctx)
55 | }
56 | func (n noop) AddTraceValues(ctx context.Context, values map[string]interface{}) {}
57 | func (n noop) AddTraceEvent(ctx context.Context, event string, values map[string]interface{}) {}
58 |
--------------------------------------------------------------------------------
/pkg/webhook/internal/helpers/helpers.go:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 |
7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
9 | "k8s.io/apimachinery/pkg/runtime"
10 | "k8s.io/apimachinery/pkg/runtime/serializer"
11 | clientsetscheme "k8s.io/client-go/kubernetes/scheme"
12 | )
13 |
14 | // K8sObject represents a full Kubernetes object.
15 | // If a Kubernetes Object is not known or doesn't satisfy both interfaces,
16 | // it should use `Unstructured` (e.g: pod/exec like `corev1.PodExecOptions`).
17 | type K8sObject interface {
18 | metav1.Object
19 | runtime.Object
20 | }
21 |
22 | // newK8sObj returns a new object of a Kubernetes type based on the type.
23 | func newK8sObj(t reflect.Type) metav1.Object {
24 | // Create a new object of the webhook resource type
25 | // convert to ptr and typeassert to Kubernetes Object.
26 | var obj interface{}
27 | newObj := reflect.New(t)
28 | obj = newObj.Interface()
29 | return obj.(metav1.Object)
30 | }
31 |
32 | // getK8sObjType returns the type (not the pointer type) of a kubernetes object.
33 | func getK8sObjType(obj metav1.Object) reflect.Type {
34 | // Object is an interface, is safe to assume that is a pointer.
35 | // Get the indirect type of the object.
36 | return reflect.Indirect(reflect.ValueOf(obj)).Type()
37 | }
38 |
39 | // ObjectCreator knows how to create objects from Raw JSON or YAML data into specific Kubernetes
40 | // types that are compatible with runtime.Object and metav1.Object, if not it will fallback to
41 | // `unstructured.Unstructured`.
42 | type ObjectCreator interface {
43 | NewObject(raw []byte) (K8sObject, error)
44 | }
45 |
46 | type staticObjectCreator struct {
47 | objType reflect.Type
48 | deserializer runtime.Decoder
49 | }
50 |
51 | // NewStaticObjectCreator doesn't need to infer the type, it will create a new schema and create a new
52 | // object with the same type from the received object type.
53 | func NewStaticObjectCreator(obj metav1.Object) ObjectCreator {
54 | codecs := serializer.NewCodecFactory(runtime.NewScheme())
55 | return staticObjectCreator{
56 | objType: getK8sObjType(obj),
57 | deserializer: codecs.UniversalDeserializer(),
58 | }
59 | }
60 |
61 | func (s staticObjectCreator) NewObject(raw []byte) (K8sObject, error) {
62 | obj, ok := newK8sObj(s.objType).(K8sObject)
63 | if !ok {
64 | return nil, fmt.Errorf("could not type assert metav1.Object and runtime.Object")
65 | }
66 |
67 | _, _, err := s.deserializer.Decode(raw, nil, obj)
68 | if err != nil {
69 | return nil, fmt.Errorf("error deseralizing request raw object: %s", err)
70 | }
71 |
72 | return obj, nil
73 | }
74 |
75 | type dynamicObjectCreator struct {
76 | universalDecoder runtime.Decoder
77 | unstructuredDecoder runtime.Decoder
78 | }
79 |
80 | // NewDynamicObjectCreator returns a object creator that knows how to return objects from raw
81 | // JSON data without the need of knowing the type.
82 | //
83 | // To be able to infer the types the types need to be registered on the global client Scheme.
84 | // Normally when a user tries casting the metav1.Object to a specific type, the object is already
85 | // registered. In case the type is not registered and the object can't be created it will fallback
86 | // to an `Unstructured` type.
87 | //
88 | // Some types like pod/exec (`corev1.PodExecOptions`) implement `runtime.Object` however they don't
89 | // implement `metav1.Object`. In that case we also fallback to `Unstructured`.
90 | //
91 | // Useful to make dynamic webhooks that expect multiple or unknown types.
92 | func NewDynamicObjectCreator() ObjectCreator {
93 | return dynamicObjectCreator{
94 | universalDecoder: clientsetscheme.Codecs.UniversalDeserializer(),
95 | unstructuredDecoder: unstructured.UnstructuredJSONScheme,
96 | }
97 | }
98 |
99 | func (d dynamicObjectCreator) NewObject(raw []byte) (K8sObject, error) {
100 | runtimeObj, _, err := d.universalDecoder.Decode(raw, nil, nil)
101 | if err == nil {
102 | // TODO(slok): Some types like pod/exec (`corev1.PodExecOptions`) implement `runtime.Object` however
103 | // they don't implement `metav1.Object`. Think if our Mutator and Validator APIs should give the
104 | // user a runtime.Object instead of a metav1.Object. In the meantime if we have this kind of
105 | // objects we will fallback to Unstructured.
106 | obj, ok := runtimeObj.(K8sObject)
107 | if ok {
108 | return obj, nil
109 | }
110 | }
111 |
112 | // Fallback to unstructured.
113 | runtimeObj, _, err = d.unstructuredDecoder.Decode(raw, nil, nil)
114 | obj, ok := runtimeObj.(K8sObject)
115 | if !ok {
116 | return nil, fmt.Errorf("could not type assert metav1.Object and runtime.Object")
117 | }
118 |
119 | return obj, err
120 | }
121 |
--------------------------------------------------------------------------------
/pkg/webhook/internal/helpers/helpers_test.go:
--------------------------------------------------------------------------------
1 | package helpers_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | appsv1 "k8s.io/api/apps/v1"
8 | v1 "k8s.io/api/core/v1"
9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
11 |
12 | "github.com/slok/kubewebhook/v2/pkg/webhook/internal/helpers"
13 | )
14 |
15 | type msi = map[string]interface{}
16 |
17 | // Deployment.
18 | var (
19 | rawYAMLDeployment = `
20 | apiVersion: apps/v1
21 | kind: Deployment
22 | metadata:
23 | name: nginx-test
24 | namespace: "test-ns"
25 | spec:
26 | replicas: 3
27 | selector:
28 | matchLabels:
29 | app: nginx
30 | template:
31 | metadata:
32 | labels:
33 | app: nginx
34 | spec:
35 | containers:
36 | - name: nginx
37 | image: nginx
38 | `
39 |
40 | rawJSONDeployment = `
41 | {
42 | "apiVersion": "apps/v1",
43 | "kind": "Deployment",
44 | "metadata": {
45 | "name": "nginx-test",
46 | "namespace": "test-ns"
47 | },
48 | "spec": {
49 | "replicas": 3,
50 | "selector": {
51 | "matchLabels": {
52 | "app": "nginx"
53 | }
54 | },
55 | "template": {
56 | "metadata": {
57 | "labels": {
58 | "app": "nginx"
59 | }
60 | },
61 | "spec": {
62 | "containers": [
63 | {
64 | "name": "nginx",
65 | "image": "nginx"
66 | }
67 | ]
68 | }
69 | }
70 | }
71 | }
72 | `
73 |
74 | k8sObjDeployment = &appsv1.Deployment{
75 | TypeMeta: metav1.TypeMeta{
76 | Kind: "Deployment",
77 | APIVersion: "apps/v1",
78 | },
79 | ObjectMeta: metav1.ObjectMeta{
80 | Name: "nginx-test",
81 | Namespace: "test-ns",
82 | },
83 | Spec: appsv1.DeploymentSpec{
84 | Replicas: &([]int32{3}[0]),
85 | Selector: &metav1.LabelSelector{MatchLabels: map[string]string{
86 | "app": "nginx",
87 | }},
88 | Template: v1.PodTemplateSpec{
89 | ObjectMeta: metav1.ObjectMeta{
90 | Labels: map[string]string{
91 | "app": "nginx",
92 | },
93 | },
94 | Spec: v1.PodSpec{
95 | Containers: []v1.Container{
96 | {Name: "nginx", Image: "nginx"},
97 | },
98 | },
99 | },
100 | },
101 | }
102 |
103 | unstructuredObjDeployment = &unstructured.Unstructured{
104 | Object: msi{
105 | "apiVersion": "apps/v1",
106 | "kind": "Deployment",
107 | "metadata": msi{
108 | "name": "nginx-test",
109 | "namespace": "test-ns",
110 | },
111 |
112 | "spec": msi{
113 | "replicas": int64(3),
114 | "selector": msi{
115 | "matchLabels": msi{
116 | "app": "nginx",
117 | },
118 | },
119 | "template": msi{
120 | "metadata": msi{
121 | "labels": msi{
122 | "app": "nginx",
123 | },
124 | },
125 | "spec": msi{
126 | "containers": []interface{}{
127 | msi{
128 | "name": "nginx",
129 | "image": "nginx",
130 | },
131 | },
132 | },
133 | },
134 | },
135 | },
136 | }
137 | )
138 |
139 | // PodExecOptions (special K8s runtime.Object types that don't satisfy metav1.Object).
140 | var (
141 | rawJSONPodExecOptions = `
142 | {
143 | "kind":"PodExecOptions",
144 | "apiVersion":"v1",
145 | "stdin":true,
146 | "stdout":true,
147 | "tty":true,
148 | "container":"nginx",
149 | "command":[
150 | "/bin/sh"
151 | ]
152 | }`
153 | UnstructuredObjPodExecOptions = &unstructured.Unstructured{
154 | Object: msi{
155 | "apiVersion": "v1",
156 | "kind": "PodExecOptions",
157 | "stdin": true,
158 | "stdout": true,
159 | "tty": true,
160 | "container": "nginx",
161 | "command": []interface{}{
162 | "/bin/sh",
163 | },
164 | },
165 | }
166 | )
167 |
168 | func TestObjectCreator(t *testing.T) {
169 | tests := map[string]struct {
170 | objectCreator func() helpers.ObjectCreator
171 | raw string
172 | expObj helpers.K8sObject
173 | expErr bool
174 | }{
175 | "Static with invalid objects should fail.": {
176 | objectCreator: func() helpers.ObjectCreator {
177 | return helpers.NewStaticObjectCreator(&appsv1.Deployment{})
178 | },
179 | raw: "{",
180 | expErr: true,
181 | },
182 |
183 | "Static object creation with JSON raw data should return the object on the specific type.": {
184 | objectCreator: func() helpers.ObjectCreator {
185 | return helpers.NewStaticObjectCreator(&appsv1.Deployment{})
186 | },
187 | raw: rawJSONDeployment,
188 | expObj: k8sObjDeployment,
189 | },
190 |
191 | "Static object creation with YAML raw data should return the object on the specific type.": {
192 | objectCreator: func() helpers.ObjectCreator {
193 | return helpers.NewStaticObjectCreator(&appsv1.Deployment{})
194 | },
195 | raw: rawYAMLDeployment,
196 | expObj: k8sObjDeployment,
197 | },
198 |
199 | "Static with unstructured object creation should return the object on unstructured type.": {
200 | objectCreator: func() helpers.ObjectCreator {
201 | return helpers.NewStaticObjectCreator(&unstructured.Unstructured{})
202 | },
203 | raw: rawYAMLDeployment,
204 | expObj: unstructuredObjDeployment,
205 | },
206 |
207 | "Dynamic with invalid objects should fail.": {
208 | objectCreator: func() helpers.ObjectCreator {
209 | return helpers.NewDynamicObjectCreator()
210 | },
211 | raw: "{",
212 | expErr: true,
213 | },
214 |
215 | "Dynamic object creation with JSON should return the object on an inferred type.": {
216 | objectCreator: func() helpers.ObjectCreator {
217 | return helpers.NewDynamicObjectCreator()
218 | },
219 | raw: rawJSONDeployment,
220 | expObj: k8sObjDeployment,
221 | },
222 |
223 | "Dynamic object creation with YAML should return the object on an inferred type.": {
224 | objectCreator: func() helpers.ObjectCreator {
225 | return helpers.NewDynamicObjectCreator()
226 | },
227 | raw: rawYAMLDeployment,
228 | expObj: k8sObjDeployment,
229 | },
230 |
231 | "Static with unstructured using only runtime.Object compatible creation should return the object unstructured type.": {
232 | objectCreator: func() helpers.ObjectCreator {
233 | return helpers.NewStaticObjectCreator(&unstructured.Unstructured{})
234 | },
235 | raw: rawJSONPodExecOptions,
236 | expObj: UnstructuredObjPodExecOptions,
237 | },
238 |
239 | "Dynamic only runtime.Object creation should return the object on an inferred unstructured type.": {
240 | objectCreator: func() helpers.ObjectCreator {
241 | return helpers.NewDynamicObjectCreator()
242 | },
243 | raw: rawJSONPodExecOptions,
244 | expObj: UnstructuredObjPodExecOptions,
245 | },
246 | }
247 |
248 | for name, test := range tests {
249 | t.Run(name, func(t *testing.T) {
250 | assert := assert.New(t)
251 |
252 | oc := test.objectCreator()
253 | gotObj, err := oc.NewObject([]byte(test.raw))
254 |
255 | if test.expErr {
256 | assert.Error(err)
257 | } else if assert.NoError(err) {
258 | assert.Equal(test.expObj, gotObj)
259 | }
260 | })
261 | }
262 | }
263 |
--------------------------------------------------------------------------------
/pkg/webhook/metrics.go:
--------------------------------------------------------------------------------
1 | package webhook
2 |
3 | import (
4 | "context"
5 | "strings"
6 | "time"
7 |
8 | "github.com/slok/kubewebhook/v2/pkg/model"
9 | )
10 |
11 | // MeasureOpCommonData is the measuring data used to measure a webhook operation.
12 | type MeasureOpCommonData struct {
13 | WebhookID string
14 | WebhookType string
15 | AdmissionReviewVersion string
16 | Duration time.Duration
17 | Success bool
18 | ResourceName string
19 | ResourceNamespace string
20 | Operation string
21 | ResourceKind string
22 | DryRun bool
23 | WarningsNumber int
24 | }
25 |
26 | // MeasureValidatingOpData is the data to measure webhook validating operation data.
27 | type MeasureValidatingOpData struct {
28 | MeasureOpCommonData
29 | Allowed bool
30 | }
31 |
32 | // MeasureMutatingOpData is the data to measure webhook mutating operation data.
33 | type MeasureMutatingOpData struct {
34 | MeasureOpCommonData
35 | Mutated bool
36 | }
37 |
38 | // MetricsRecorder knows how to record webhook recorder metrics.
39 | type MetricsRecorder interface {
40 | MeasureValidatingWebhookReviewOp(ctx context.Context, data MeasureValidatingOpData)
41 | MeasureMutatingWebhookReviewOp(ctx context.Context, data MeasureMutatingOpData)
42 | }
43 |
44 | type noopMetricsRecorder int
45 |
46 | // NoopMetricsRecorder is a no-op metrics recorder.
47 | const NoopMetricsRecorder = noopMetricsRecorder(0)
48 |
49 | var _ MetricsRecorder = NoopMetricsRecorder
50 |
51 | func (noopMetricsRecorder) MeasureValidatingWebhookReviewOp(ctx context.Context, data MeasureValidatingOpData) {
52 | }
53 | func (noopMetricsRecorder) MeasureMutatingWebhookReviewOp(ctx context.Context, data MeasureMutatingOpData) {
54 | }
55 |
56 | type measuredWebhook struct {
57 | webhookID string
58 | webhookKind model.WebhookKind
59 | rec MetricsRecorder
60 | next Webhook
61 | }
62 |
63 | // NewMeasuredWebhook returns a wrapped webhook that will measure the webhook operations.
64 | func NewMeasuredWebhook(rec MetricsRecorder, next Webhook) Webhook {
65 | return measuredWebhook{
66 | webhookID: next.ID(),
67 | webhookKind: next.Kind(),
68 | rec: rec,
69 | next: next,
70 | }
71 | }
72 |
73 | func (m measuredWebhook) ID() string { return m.next.ID() }
74 | func (m measuredWebhook) Kind() model.WebhookKind { return m.next.Kind() }
75 | func (m measuredWebhook) Review(ctx context.Context, ar model.AdmissionReview) (resp model.AdmissionResponse, err error) {
76 | defer func(t0 time.Time) {
77 | cData := MeasureOpCommonData{
78 | WebhookID: m.webhookID,
79 | AdmissionReviewVersion: string(ar.Version),
80 | Duration: time.Since(t0),
81 | Success: err == nil,
82 | ResourceName: ar.Name,
83 | ResourceNamespace: ar.Namespace,
84 | Operation: string(ar.Operation),
85 | ResourceKind: getResourceKind(ar),
86 | DryRun: ar.DryRun,
87 | }
88 |
89 | switch r := resp.(type) {
90 | case *model.ValidatingAdmissionResponse:
91 | cData.WebhookType = model.WebhookKindValidating
92 | cData.WarningsNumber = len(r.Warnings)
93 | m.rec.MeasureValidatingWebhookReviewOp(ctx, MeasureValidatingOpData{
94 | MeasureOpCommonData: cData,
95 | Allowed: r.Allowed,
96 | })
97 |
98 | case *model.MutatingAdmissionResponse:
99 | cData.WebhookType = model.WebhookKindMutating
100 | cData.WarningsNumber = len(r.Warnings)
101 | m.rec.MeasureMutatingWebhookReviewOp(ctx, MeasureMutatingOpData{
102 | MeasureOpCommonData: cData,
103 | Mutated: hasMutated(r),
104 | })
105 |
106 | default:
107 | // Unknown type, not measuring.
108 | // TODO(slok): Notify user ignore metrics.
109 | }
110 |
111 | }(time.Now())
112 |
113 | return m.next.Review(ctx, ar)
114 | }
115 |
116 | func getResourceKind(ar model.AdmissionReview) string {
117 | gvk := ar.RequestGVK
118 | return strings.Trim(strings.Join([]string{gvk.Group, gvk.Version, gvk.Kind}, "/"), "/")
119 | }
120 |
121 | func hasMutated(r *model.MutatingAdmissionResponse) bool {
122 | return len(r.JSONPatchPatch) > 0 && string(r.JSONPatchPatch) != "[]"
123 | }
124 |
--------------------------------------------------------------------------------
/pkg/webhook/mutating/example_test.go:
--------------------------------------------------------------------------------
1 | package mutating_test
2 |
3 | import (
4 | "context"
5 |
6 | corev1 "k8s.io/api/core/v1"
7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8 |
9 | "github.com/slok/kubewebhook/v2/pkg/log"
10 | "github.com/slok/kubewebhook/v2/pkg/model"
11 | "github.com/slok/kubewebhook/v2/pkg/webhook/mutating"
12 | )
13 |
14 | // PodAnnotateMutatingWebhook shows how you would create a pod mutating webhook that adds
15 | // annotations to every pod received.
16 | func ExampleMutator_podAnnotateMutatingWebhook() {
17 | // Annotations to add.
18 | annotations := map[string]string{
19 | "mutated": "true",
20 | "example": "ExamplePodAnnotateMutatingWebhook",
21 | "framework": "kubewebhook",
22 | }
23 | // Create our mutator that will add annotations to every pod.
24 | pam := mutating.MutatorFunc(func(_ context.Context, _ *model.AdmissionReview, obj metav1.Object) (*mutating.MutatorResult, error) {
25 | pod, ok := obj.(*corev1.Pod)
26 | if !ok {
27 | return &mutating.MutatorResult{}, nil
28 | }
29 |
30 | // Mutate our object with the required annotations.
31 | if pod.Annotations == nil {
32 | pod.Annotations = make(map[string]string)
33 | }
34 |
35 | for k, v := range annotations {
36 | pod.Annotations[k] = v
37 | }
38 |
39 | return &mutating.MutatorResult{MutatedObject: pod}, nil
40 | })
41 |
42 | // Create webhook.
43 | _, _ = mutating.NewWebhook(mutating.WebhookConfig{
44 | ID: "podAnnotateMutatingWebhook",
45 | Obj: &corev1.Pod{},
46 | Mutator: pam,
47 | })
48 | }
49 |
50 | // chainMutatingWebhook shows how you would create a mutator chain.
51 | func ExampleMutator_chainMutatingWebhook() {
52 | fakeMut := mutating.MutatorFunc(func(_ context.Context, _ *model.AdmissionReview, obj metav1.Object) (*mutating.MutatorResult, error) {
53 | return &mutating.MutatorResult{}, nil
54 | })
55 |
56 | fakeMut2 := mutating.MutatorFunc(func(_ context.Context, _ *model.AdmissionReview, obj metav1.Object) (*mutating.MutatorResult, error) {
57 | return &mutating.MutatorResult{}, nil
58 | })
59 |
60 | fakeMut3 := mutating.MutatorFunc(func(_ context.Context, _ *model.AdmissionReview, obj metav1.Object) (*mutating.MutatorResult, error) {
61 | return &mutating.MutatorResult{}, nil
62 | })
63 |
64 | // Create webhook using a mutator chain.
65 | _, _ = mutating.NewWebhook(mutating.WebhookConfig{
66 | ID: "podWebhook",
67 | Obj: &corev1.Pod{},
68 | Mutator: mutating.NewChain(log.Noop, fakeMut, fakeMut2, fakeMut3),
69 | })
70 | }
71 |
--------------------------------------------------------------------------------
/pkg/webhook/mutating/mutatingmock/mutator.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v2.12.2. DO NOT EDIT.
2 |
3 | package mutatingmock
4 |
5 | import (
6 | context "context"
7 |
8 | model "github.com/slok/kubewebhook/v2/pkg/model"
9 | mock "github.com/stretchr/testify/mock"
10 |
11 | mutating "github.com/slok/kubewebhook/v2/pkg/webhook/mutating"
12 |
13 | testing "testing"
14 |
15 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16 | )
17 |
18 | // Mutator is an autogenerated mock type for the Mutator type
19 | type Mutator struct {
20 | mock.Mock
21 | }
22 |
23 | // Mutate provides a mock function with given fields: ctx, ar, obj
24 | func (_m *Mutator) Mutate(ctx context.Context, ar *model.AdmissionReview, obj v1.Object) (*mutating.MutatorResult, error) {
25 | ret := _m.Called(ctx, ar, obj)
26 |
27 | var r0 *mutating.MutatorResult
28 | if rf, ok := ret.Get(0).(func(context.Context, *model.AdmissionReview, v1.Object) *mutating.MutatorResult); ok {
29 | r0 = rf(ctx, ar, obj)
30 | } else {
31 | if ret.Get(0) != nil {
32 | r0 = ret.Get(0).(*mutating.MutatorResult)
33 | }
34 | }
35 |
36 | var r1 error
37 | if rf, ok := ret.Get(1).(func(context.Context, *model.AdmissionReview, v1.Object) error); ok {
38 | r1 = rf(ctx, ar, obj)
39 | } else {
40 | r1 = ret.Error(1)
41 | }
42 |
43 | return r0, r1
44 | }
45 |
46 | // NewMutator creates a new instance of Mutator. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
47 | func NewMutator(t testing.TB) *Mutator {
48 | mock := &Mutator{}
49 | mock.Mock.Test(t)
50 |
51 | t.Cleanup(func() { mock.AssertExpectations(t) })
52 |
53 | return mock
54 | }
55 |
--------------------------------------------------------------------------------
/pkg/webhook/mutating/mutator.go:
--------------------------------------------------------------------------------
1 | package mutating
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8 |
9 | "github.com/slok/kubewebhook/v2/pkg/log"
10 | "github.com/slok/kubewebhook/v2/pkg/model"
11 | )
12 |
13 | // MutatorResult is the result of a mutator.
14 | type MutatorResult struct {
15 | // StopChain will stop the chain of validators in case there is a chain set.
16 | StopChain bool
17 | // MutatedObject is the object that has been mutated. If is nil, it will be used the one
18 | // received by the Mutator.
19 | MutatedObject metav1.Object
20 | // Warnings are special messages that can be set to warn the user (e.g deprecation messages, almost invalid resources...).
21 | Warnings []string
22 | }
23 |
24 | // Mutator knows how to mutate the received kubernetes object.
25 | type Mutator interface {
26 | // Mutate receives a Kubernetes resource object to be mutated, it must
27 | // return an error or a mutation result. What the mutator returns
28 | // as result.MutatedObject is the object that will be used as the mutation.
29 | // It must be of the same type of the received one (if is a Pod, it must return a Pod)
30 | // if no object is returned, it will be used the received one as the mutated one.
31 | // Also receives the webhook admission review in case it wants more context and
32 | // information of the review.
33 | // Mutators can be grouped in chains, that's why we have a `StopChain` boolean
34 | // in the result, to stop executing the validators chain.
35 | Mutate(ctx context.Context, ar *model.AdmissionReview, obj metav1.Object) (result *MutatorResult, err error)
36 | }
37 |
38 | //go:generate mockery --case underscore --output mutatingmock --outpkg mutatingmock --name Mutator
39 |
40 | // MutatorFunc is a helper type to create mutators from functions.
41 | type MutatorFunc func(context.Context, *model.AdmissionReview, metav1.Object) (*MutatorResult, error)
42 |
43 | // Mutate satisfies Mutator interface.
44 | func (f MutatorFunc) Mutate(ctx context.Context, ar *model.AdmissionReview, obj metav1.Object) (*MutatorResult, error) {
45 | return f(ctx, ar, obj)
46 | }
47 |
48 | // Chain is a chain of mutators that will execute secuentially all the
49 | // mutators that have been added to it. It satisfies Mutator interface.
50 | type Chain struct {
51 | mutators []Mutator
52 | logger log.Logger
53 | }
54 |
55 | // NewChain returns a new chain.
56 | func NewChain(logger log.Logger, mutators ...Mutator) *Chain {
57 | return &Chain{
58 | mutators: mutators,
59 | logger: logger,
60 | }
61 | }
62 |
63 | // Mutate will execute all the mutation chain.
64 | func (c *Chain) Mutate(ctx context.Context, ar *model.AdmissionReview, obj metav1.Object) (*MutatorResult, error) {
65 | var warnings []string
66 | for _, mt := range c.mutators {
67 | select {
68 | case <-ctx.Done():
69 | return nil, fmt.Errorf("mutator chain not finished correctly, context done")
70 | default:
71 | res, err := mt.Mutate(ctx, ar, obj)
72 | if err != nil {
73 | return nil, err
74 | }
75 |
76 | if res == nil {
77 | return nil, fmt.Errorf("validator result can't be `nil`")
78 | }
79 |
80 | // Don't lose the data through the chain, set warnings and pass around the mutated object.
81 | warnings = append(warnings, res.Warnings...)
82 | if res.MutatedObject != nil {
83 | obj = res.MutatedObject
84 | }
85 |
86 | if res.StopChain {
87 | res.Warnings = warnings
88 | return res, nil
89 | }
90 | }
91 | }
92 |
93 | return &MutatorResult{
94 | MutatedObject: obj,
95 | Warnings: warnings,
96 | }, nil
97 | }
98 |
--------------------------------------------------------------------------------
/pkg/webhook/mutating/mutator_test.go:
--------------------------------------------------------------------------------
1 | package mutating_test
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | "github.com/stretchr/testify/mock"
10 | corev1 "k8s.io/api/core/v1"
11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12 |
13 | "github.com/slok/kubewebhook/v2/pkg/log"
14 | "github.com/slok/kubewebhook/v2/pkg/webhook/mutating"
15 | "github.com/slok/kubewebhook/v2/pkg/webhook/mutating/mutatingmock"
16 | )
17 |
18 | func TestMutatorChain(t *testing.T) {
19 | tests := map[string]struct {
20 | name string
21 | initalObj metav1.Object
22 | mutatorMocks func() []mutating.Mutator
23 | expResult *mutating.MutatorResult
24 | expErr bool
25 | }{
26 | "Should call all the mutators.": {
27 | mutatorMocks: func() []mutating.Mutator {
28 | m1, m2, m3, m4, m5 := &mutatingmock.Mutator{}, &mutatingmock.Mutator{}, &mutatingmock.Mutator{}, &mutatingmock.Mutator{}, &mutatingmock.Mutator{}
29 | m1.On("Mutate", mock.Anything, mock.Anything, mock.Anything).Return(&mutating.MutatorResult{}, nil)
30 | m2.On("Mutate", mock.Anything, mock.Anything, mock.Anything).Return(&mutating.MutatorResult{}, nil)
31 | m3.On("Mutate", mock.Anything, mock.Anything, mock.Anything).Return(&mutating.MutatorResult{}, nil)
32 | m4.On("Mutate", mock.Anything, mock.Anything, mock.Anything).Return(&mutating.MutatorResult{}, nil)
33 | m5.On("Mutate", mock.Anything, mock.Anything, mock.Anything).Return(&mutating.MutatorResult{}, nil)
34 | return []mutating.Mutator{m1, m2, m3, m4, m5}
35 | },
36 | expResult: &mutating.MutatorResult{},
37 | },
38 |
39 | "Should call all the mutators and pass the previous object mutator, at last return the object of the latest mutator.": {
40 | initalObj: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "p0"}},
41 | mutatorMocks: func() []mutating.Mutator {
42 | obj0 := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "p0"}}
43 | obj1 := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "p1"}}
44 | obj2 := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "p2"}}
45 | obj3 := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "p3"}}
46 | obj4 := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "p4"}}
47 |
48 | m1, m2, m3, m4, m5 := &mutatingmock.Mutator{}, &mutatingmock.Mutator{}, &mutatingmock.Mutator{}, &mutatingmock.Mutator{}, &mutatingmock.Mutator{}
49 | m1.On("Mutate", mock.Anything, mock.Anything, obj0).Return(&mutating.MutatorResult{MutatedObject: obj1}, nil)
50 | m2.On("Mutate", mock.Anything, mock.Anything, obj1).Return(&mutating.MutatorResult{MutatedObject: obj2}, nil)
51 | m3.On("Mutate", mock.Anything, mock.Anything, obj2).Return(&mutating.MutatorResult{}, nil) // No mutation, should keep the previous one.
52 | m4.On("Mutate", mock.Anything, mock.Anything, obj2).Return(&mutating.MutatorResult{MutatedObject: obj3}, nil)
53 | m5.On("Mutate", mock.Anything, mock.Anything, obj3).Return(&mutating.MutatorResult{MutatedObject: obj4}, nil)
54 | return []mutating.Mutator{m1, m2, m3, m4, m5}
55 | },
56 | expResult: &mutating.MutatorResult{
57 | MutatedObject: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "p4"}},
58 | },
59 | },
60 |
61 | "In case the last mutator doesn't return any object, the original one should be returned.": {
62 | initalObj: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "p0"}},
63 | mutatorMocks: func() []mutating.Mutator {
64 | obj0 := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "p0"}}
65 | m1 := &mutatingmock.Mutator{}
66 | m1.On("Mutate", mock.Anything, mock.Anything, obj0).Return(&mutating.MutatorResult{}, nil)
67 | return []mutating.Mutator{m1}
68 | },
69 | expResult: &mutating.MutatorResult{
70 | MutatedObject: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "p0"}},
71 | },
72 | },
73 |
74 | "Should stop in the middle of the chain if any of the mutators stops the chain..": {
75 | mutatorMocks: func() []mutating.Mutator {
76 | m1, m2, m3, m4, m5 := &mutatingmock.Mutator{}, &mutatingmock.Mutator{}, &mutatingmock.Mutator{}, &mutatingmock.Mutator{}, &mutatingmock.Mutator{}
77 | m1.On("Mutate", mock.Anything, mock.Anything, mock.Anything).Return(&mutating.MutatorResult{}, nil)
78 | m2.On("Mutate", mock.Anything, mock.Anything, mock.Anything).Return(&mutating.MutatorResult{}, nil)
79 | m3.On("Mutate", mock.Anything, mock.Anything, mock.Anything).Return(&mutating.MutatorResult{StopChain: true}, nil)
80 | return []mutating.Mutator{m1, m2, m3, m4, m5}
81 | },
82 | expResult: &mutating.MutatorResult{StopChain: true},
83 | },
84 |
85 | "In case of error the chain should be stopped.": {
86 | mutatorMocks: func() []mutating.Mutator {
87 | m1, m2, m3, m4, m5 := &mutatingmock.Mutator{}, &mutatingmock.Mutator{}, &mutatingmock.Mutator{}, &mutatingmock.Mutator{}, &mutatingmock.Mutator{}
88 | m1.On("Mutate", mock.Anything, mock.Anything, mock.Anything).Return(&mutating.MutatorResult{}, nil)
89 | m2.On("Mutate", mock.Anything, mock.Anything, mock.Anything).Return(&mutating.MutatorResult{}, nil)
90 | m3.On("Mutate", mock.Anything, mock.Anything, mock.Anything).Return(&mutating.MutatorResult{}, fmt.Errorf("wanted error"))
91 | return []mutating.Mutator{m1, m2, m3, m4, m5}
92 | },
93 | expErr: true,
94 | },
95 | }
96 |
97 | for _, test := range tests {
98 | t.Run(test.name, func(t *testing.T) {
99 | assert := assert.New(t)
100 |
101 | // Mocks.
102 | mutators := test.mutatorMocks()
103 |
104 | // Execute.
105 | chain := mutating.NewChain(log.Noop, mutators...)
106 | res, err := chain.Mutate(context.TODO(), nil, test.initalObj)
107 |
108 | // Check result.
109 | if test.expErr {
110 | assert.Error(err)
111 | } else if assert.NoError(err) {
112 | assert.Equal(test.expResult, res)
113 | }
114 |
115 | // Check calls where ok.
116 | for _, m := range mutators {
117 | mm := m.(*mutatingmock.Mutator)
118 | mm.AssertExpectations(t)
119 | }
120 | })
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/pkg/webhook/mutating/webhook.go:
--------------------------------------------------------------------------------
1 | package mutating
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 |
8 | "gomodules.xyz/jsonpatch/v2"
9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10 |
11 | "github.com/slok/kubewebhook/v2/pkg/log"
12 | "github.com/slok/kubewebhook/v2/pkg/model"
13 | "github.com/slok/kubewebhook/v2/pkg/webhook"
14 | "github.com/slok/kubewebhook/v2/pkg/webhook/internal/helpers"
15 | )
16 |
17 | // WebhookConfig is the Mutating webhook configuration.
18 | type WebhookConfig struct {
19 | // ID is the id of the webhook.
20 | ID string
21 | // Object is the object of the webhook, to use multiple types on the same webhook or
22 | // type inference, don't set this field (will be `nil`).
23 | Obj metav1.Object
24 | // Mutator is the webhook mutator.
25 | Mutator Mutator
26 | // Logger is the app logger.
27 | Logger log.Logger
28 | }
29 |
30 | func (c *WebhookConfig) defaults() error {
31 | if c.ID == "" {
32 | return fmt.Errorf("id is required")
33 | }
34 |
35 | if c.Mutator == nil {
36 | return fmt.Errorf("mutator is required")
37 | }
38 |
39 | if c.Logger == nil {
40 | c.Logger = log.Noop
41 | }
42 | c.Logger = c.Logger.WithValues(log.Kv{"webhook-id": c.ID, "webhook-type": "mutating"})
43 |
44 | return nil
45 | }
46 |
47 | type mutatingWebhook struct {
48 | id string
49 | objectCreator helpers.ObjectCreator
50 | mutator Mutator
51 | cfg WebhookConfig
52 | logger log.Logger
53 | }
54 |
55 | // NewWebhook is a mutating webhook and will return a webhook ready for a type of resource.
56 | // It will mutate the received resources.
57 | // This webhook will always allow the admission of the resource, only will deny in case of error.
58 | func NewWebhook(cfg WebhookConfig) (webhook.Webhook, error) {
59 | if err := cfg.defaults(); err != nil {
60 | return nil, fmt.Errorf("invalid configuration: %w", err)
61 | }
62 |
63 | // If we don't have the type of the object create a dynamic object creator that will
64 | // infer the type.
65 | var oc helpers.ObjectCreator
66 | if cfg.Obj != nil {
67 | oc = helpers.NewStaticObjectCreator(cfg.Obj)
68 | } else {
69 | oc = helpers.NewDynamicObjectCreator()
70 | }
71 |
72 | return &mutatingWebhook{
73 | objectCreator: oc,
74 | id: cfg.ID,
75 | mutator: cfg.Mutator,
76 | cfg: cfg,
77 | logger: cfg.Logger,
78 | }, nil
79 | }
80 |
81 | func (w mutatingWebhook) ID() string { return w.id }
82 |
83 | func (w mutatingWebhook) Kind() model.WebhookKind { return model.WebhookKindMutating }
84 |
85 | func (w mutatingWebhook) Review(ctx context.Context, ar model.AdmissionReview) (model.AdmissionResponse, error) {
86 | // Delete operations don't have body because should be gone on the deletion, instead they have the body
87 | // of the object we want to delete as an old object.
88 | raw := ar.NewObjectRaw
89 | if ar.Operation == model.OperationDelete {
90 | raw = ar.OldObjectRaw
91 | }
92 |
93 | // Create a new object from the raw type.
94 | runtimeObj, err := w.objectCreator.NewObject(raw)
95 | if err != nil {
96 | return nil, fmt.Errorf("could not create object from raw: %w", err)
97 | }
98 |
99 | mutatingObj, ok := runtimeObj.(metav1.Object)
100 | if !ok {
101 | return nil, fmt.Errorf("impossible to type assert the deep copy to metav1.Object")
102 | }
103 |
104 | res, err := w.mutatingAdmissionReview(ctx, ar, raw, mutatingObj)
105 | if err != nil {
106 | return nil, err
107 | }
108 |
109 | w.logger.WithCtxValues(ctx).Debugf("Webhook mutating review finished with: '%s' JSON Patch", string(res.JSONPatchPatch))
110 |
111 | return res, nil
112 | }
113 |
114 | func (w mutatingWebhook) mutatingAdmissionReview(ctx context.Context, ar model.AdmissionReview, rawObj []byte, objForMutation metav1.Object) (*model.MutatingAdmissionResponse, error) {
115 | // Mutate the object.
116 | res, err := w.mutator.Mutate(ctx, &ar, objForMutation)
117 | if err != nil {
118 | return nil, fmt.Errorf("could not mutate object: %w", err)
119 | }
120 |
121 | if res == nil {
122 | return nil, fmt.Errorf("result is required, mutator result is nil")
123 | }
124 |
125 | // If the user returned a mutated object, it will not be used the one we provided to the mutator.
126 | // if nil then, we use the one we provided.
127 | mutatedObj := objForMutation
128 | if res.MutatedObject != nil {
129 | mutatedObj = res.MutatedObject
130 | }
131 | mutatedJSON, err := json.Marshal(mutatedObj)
132 | if err != nil {
133 | return nil, fmt.Errorf("could not marshal into JSON mutated object: %w", err)
134 | }
135 |
136 | patch, err := jsonpatch.CreatePatch(rawObj, mutatedJSON)
137 | if err != nil {
138 | return nil, fmt.Errorf("could not create JSON patch: %w", err)
139 | }
140 |
141 | marshalledPatch, err := json.Marshal(patch)
142 | if err != nil {
143 | return nil, fmt.Errorf("could not mashal into JSON, the JSON patch: %w", err)
144 | }
145 |
146 | // Forge response.
147 | return &model.MutatingAdmissionResponse{
148 | ID: ar.ID,
149 | JSONPatchPatch: marshalledPatch,
150 | Warnings: res.Warnings,
151 | }, nil
152 | }
153 |
--------------------------------------------------------------------------------
/pkg/webhook/tracing.go:
--------------------------------------------------------------------------------
1 | package webhook
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/slok/kubewebhook/v2/pkg/model"
8 | "github.com/slok/kubewebhook/v2/pkg/tracing"
9 | )
10 |
11 | type tracedWebhook struct {
12 | webhookID string
13 | webhookKind model.WebhookKind
14 | tracer tracing.Tracer
15 | next Webhook
16 | }
17 |
18 | // NewTracedWebhook returns a wrapped webhook that will trace the webhook operations.
19 | func NewTracedWebhook(tracer tracing.Tracer, next Webhook) Webhook {
20 | return tracedWebhook{
21 | webhookID: next.ID(),
22 | webhookKind: next.Kind(),
23 | tracer: tracer,
24 | next: next,
25 | }
26 | }
27 |
28 | func (t tracedWebhook) ID() string { return t.next.ID() }
29 | func (t tracedWebhook) Kind() model.WebhookKind { return t.next.Kind() }
30 | func (t tracedWebhook) Review(ctx context.Context, ar model.AdmissionReview) (resp model.AdmissionResponse, err error) {
31 | ctx = t.tracer.NewTrace(ctx, fmt.Sprintf("webhook.Review/%s", t.webhookID))
32 | t.tracer.AddTraceValues(ctx, map[string]interface{}{
33 | "webhook_id": t.webhookID,
34 | "admission_review_version": ar.Version,
35 | "admission_review_user_uid": ar.UserInfo.UID,
36 | "admission_review_user_username": ar.UserInfo.Username,
37 | "admission_review_user_groups": ar.UserInfo.Groups,
38 | "admission_review_id": ar.ID,
39 | "resource_name": ar.Name,
40 | "resource_namespace": ar.Namespace,
41 | "operation": ar.Operation,
42 | "resource_kind": getResourceKind(ar),
43 | "dry_run": ar.DryRun,
44 | })
45 |
46 | defer func() {
47 | switch r := resp.(type) {
48 | case *model.ValidatingAdmissionResponse:
49 | t.tracer.AddTraceValues(ctx, map[string]interface{}{
50 | "webhook_type": model.WebhookKindMutating,
51 | "warnings": r.Warnings,
52 | "has_warnings": len(r.Warnings) > 0,
53 | "allowed": r.Allowed,
54 | })
55 |
56 | case *model.MutatingAdmissionResponse:
57 | t.tracer.AddTraceValues(ctx, map[string]interface{}{
58 | "webhook_type": model.WebhookKindValidating,
59 | "warnings": r.Warnings,
60 | "has_warnings": len(r.Warnings) > 0,
61 | "mutated": hasMutated(r),
62 | })
63 |
64 | default:
65 | // Unknown type, not traced.
66 | // TODO(slok): Notify user ignored traces.
67 | }
68 |
69 | t.tracer.EndTrace(ctx, err)
70 | }()
71 |
72 | return t.next.Review(ctx, ar)
73 | }
74 |
--------------------------------------------------------------------------------
/pkg/webhook/validating/example_test.go:
--------------------------------------------------------------------------------
1 | package validating_test
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "regexp"
7 |
8 | corev1 "k8s.io/api/core/v1"
9 | extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11 |
12 | "github.com/slok/kubewebhook/v2/pkg/log"
13 | "github.com/slok/kubewebhook/v2/pkg/model"
14 | "github.com/slok/kubewebhook/v2/pkg/webhook/validating"
15 | )
16 |
17 | // IngressHostValidatingWebhook shows how you would create a ingress validating webhook that checks
18 | // if an ingress has any rule with an invalid host that doesn't match the valid host regex and if is invalid
19 | // will not accept the ingress.
20 | func ExampleValidator_ingressHostValidatingWebhook() {
21 | // Create the regex to validate the hosts.
22 | validHost := regexp.MustCompile(`^.*\.batman\.best\.superhero\.io$`)
23 |
24 | // Create our validator that will check the host on each rule of the received ingress to
25 | // allow or disallow the ingress.
26 | ivh := validating.ValidatorFunc(func(_ context.Context, _ *model.AdmissionReview, obj metav1.Object) (*validating.ValidatorResult, error) {
27 | ingress, ok := obj.(*extensionsv1beta1.Ingress)
28 | if !ok {
29 | return &validating.ValidatorResult{Valid: true}, fmt.Errorf("not an ingress")
30 | }
31 |
32 | for _, r := range ingress.Spec.Rules {
33 | if !validHost.MatchString(r.Host) {
34 | return &validating.ValidatorResult{
35 | Valid: false,
36 | Message: fmt.Sprintf("%s ingress host doesn't match %s regex", r.Host, validHost),
37 | }, nil
38 | }
39 | }
40 |
41 | return &validating.ValidatorResult{
42 | Valid: true,
43 | Message: "all hosts in the ingress are valid",
44 | }, nil
45 | })
46 |
47 | // Create webhook (usage of webhook not in this example).
48 | _, _ = validating.NewWebhook(validating.WebhookConfig{
49 | ID: "example",
50 | Obj: &extensionsv1beta1.Ingress{},
51 | Validator: ivh,
52 | })
53 | }
54 |
55 | // chainValidatingWebhook shows how you would create a validating chain.
56 | func ExampleValidator_chainValidatingWebhook() {
57 | fakeVal := validating.ValidatorFunc(func(_ context.Context, _ *model.AdmissionReview, obj metav1.Object) (*validating.ValidatorResult, error) {
58 | return &validating.ValidatorResult{Valid: true}, nil
59 | })
60 |
61 | fakeVal2 := validating.ValidatorFunc(func(_ context.Context, _ *model.AdmissionReview, obj metav1.Object) (*validating.ValidatorResult, error) {
62 | return &validating.ValidatorResult{Valid: true}, nil
63 | })
64 |
65 | fakeVal3 := validating.ValidatorFunc(func(_ context.Context, _ *model.AdmissionReview, obj metav1.Object) (*validating.ValidatorResult, error) {
66 | return &validating.ValidatorResult{Valid: true}, nil
67 | })
68 |
69 | // Create our webhook using a validator chain.
70 | _, _ = validating.NewWebhook(validating.WebhookConfig{
71 | ID: "podWebhook",
72 | Obj: &corev1.Pod{},
73 | Validator: validating.NewChain(log.Noop, fakeVal, fakeVal2, fakeVal3),
74 | })
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/pkg/webhook/validating/validatingmock/validator.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v2.12.2. DO NOT EDIT.
2 |
3 | package validatingmock
4 |
5 | import (
6 | context "context"
7 |
8 | model "github.com/slok/kubewebhook/v2/pkg/model"
9 | mock "github.com/stretchr/testify/mock"
10 |
11 | testing "testing"
12 |
13 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14 |
15 | validating "github.com/slok/kubewebhook/v2/pkg/webhook/validating"
16 | )
17 |
18 | // Validator is an autogenerated mock type for the Validator type
19 | type Validator struct {
20 | mock.Mock
21 | }
22 |
23 | // Validate provides a mock function with given fields: ctx, ar, obj
24 | func (_m *Validator) Validate(ctx context.Context, ar *model.AdmissionReview, obj v1.Object) (*validating.ValidatorResult, error) {
25 | ret := _m.Called(ctx, ar, obj)
26 |
27 | var r0 *validating.ValidatorResult
28 | if rf, ok := ret.Get(0).(func(context.Context, *model.AdmissionReview, v1.Object) *validating.ValidatorResult); ok {
29 | r0 = rf(ctx, ar, obj)
30 | } else {
31 | if ret.Get(0) != nil {
32 | r0 = ret.Get(0).(*validating.ValidatorResult)
33 | }
34 | }
35 |
36 | var r1 error
37 | if rf, ok := ret.Get(1).(func(context.Context, *model.AdmissionReview, v1.Object) error); ok {
38 | r1 = rf(ctx, ar, obj)
39 | } else {
40 | r1 = ret.Error(1)
41 | }
42 |
43 | return r0, r1
44 | }
45 |
46 | // NewValidator creates a new instance of Validator. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
47 | func NewValidator(t testing.TB) *Validator {
48 | mock := &Validator{}
49 | mock.Mock.Test(t)
50 |
51 | t.Cleanup(func() { mock.AssertExpectations(t) })
52 |
53 | return mock
54 | }
55 |
--------------------------------------------------------------------------------
/pkg/webhook/validating/validator.go:
--------------------------------------------------------------------------------
1 | package validating
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8 |
9 | "github.com/slok/kubewebhook/v2/pkg/log"
10 | "github.com/slok/kubewebhook/v2/pkg/model"
11 | )
12 |
13 | // ValidatorResult is the result of a validator.
14 | type ValidatorResult struct {
15 | // StopChain will stop the chain of validators in case there is a chain set.
16 | StopChain bool
17 | // Valid tells the apiserver that the resource is correct and it should allow or not.
18 | Valid bool
19 | // Message will be used by the apiserver to give more information in case the resource is not valid.
20 | Message string
21 | // Warnings are special messages that can be set to warn the user (e.g deprecation messages, almost invalid resources...).
22 | Warnings []string
23 | }
24 |
25 | // Validator knows how to validate the received kubernetes object.
26 | type Validator interface {
27 | // Validate receives a Kubernetes resource object to be validated, it must
28 | // return an error or a validation result.
29 | // Also receives the webhook admission review in case it wants more context and
30 | // information of the review.
31 | // Validators can be grouped in chains, that's why we have a `StopChain` boolean
32 | // in the result, to stop executing the validators chain.
33 | Validate(ctx context.Context, ar *model.AdmissionReview, obj metav1.Object) (result *ValidatorResult, err error)
34 | }
35 |
36 | //go:generate mockery --case underscore --output validatingmock --outpkg validatingmock --name Validator
37 |
38 | // ValidatorFunc is a helper type to create validators from functions.
39 | type ValidatorFunc func(context.Context, *model.AdmissionReview, metav1.Object) (result *ValidatorResult, err error)
40 |
41 | // Validate satisfies Validator interface.
42 | func (f ValidatorFunc) Validate(ctx context.Context, ar *model.AdmissionReview, obj metav1.Object) (result *ValidatorResult, err error) {
43 | return f(ctx, ar, obj)
44 | }
45 |
46 | type chain struct {
47 | validators []Validator
48 | logger log.Logger
49 | }
50 |
51 | // NewChain returns a new chain of validators.
52 | // - If any of the validators returns an error, the chain will end.
53 | // - If any of the validators returns an stopChain == true, the chain will end.
54 | // - If any of the validators returns as no valid, the chain will end.
55 | func NewChain(logger log.Logger, validators ...Validator) Validator {
56 | return chain{
57 | validators: validators,
58 | logger: logger,
59 | }
60 | }
61 |
62 | // Validate will execute all the validation chain.
63 | func (c chain) Validate(ctx context.Context, ar *model.AdmissionReview, obj metav1.Object) (*ValidatorResult, error) {
64 | var warnings []string
65 | for _, vl := range c.validators {
66 | select {
67 | case <-ctx.Done():
68 | return nil, fmt.Errorf("validator chain not finished correctly, context done")
69 | default:
70 | res, err := vl.Validate(ctx, ar, obj)
71 | if err != nil {
72 | return nil, err
73 | }
74 |
75 | if res == nil {
76 | return nil, fmt.Errorf("validator result can't be `nil`")
77 | }
78 |
79 | // Don't lose the warnings through the chain.
80 | warnings = append(warnings, res.Warnings...)
81 |
82 | if res.StopChain || !res.Valid {
83 | res.Warnings = warnings
84 | return res, nil
85 | }
86 | }
87 | }
88 |
89 | return &ValidatorResult{
90 | Valid: true,
91 | Warnings: warnings,
92 | }, nil
93 | }
94 |
--------------------------------------------------------------------------------
/pkg/webhook/validating/webhook.go:
--------------------------------------------------------------------------------
1 | package validating
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8 |
9 | "github.com/slok/kubewebhook/v2/pkg/log"
10 | "github.com/slok/kubewebhook/v2/pkg/model"
11 | "github.com/slok/kubewebhook/v2/pkg/webhook"
12 | "github.com/slok/kubewebhook/v2/pkg/webhook/internal/helpers"
13 | )
14 |
15 | // WebhookConfig is the Validating webhook configuration.
16 | type WebhookConfig struct {
17 | // ID is the id of the webhook.
18 | ID string
19 | // Object is the object of the webhook, to use multiple types on the same webhook or
20 | // type inference, don't set this field (will be `nil`).
21 | Obj metav1.Object
22 | // Validator is the webhook validator.
23 | Validator Validator
24 | // Logger is the app logger.
25 | Logger log.Logger
26 | }
27 |
28 | func (c *WebhookConfig) defaults() error {
29 | if c.ID == "" {
30 | return fmt.Errorf("id is required")
31 | }
32 |
33 | if c.Validator == nil {
34 | return fmt.Errorf("validator is required")
35 | }
36 |
37 | if c.Logger == nil {
38 | c.Logger = log.Noop
39 | }
40 | c.Logger = c.Logger.WithValues(log.Kv{"webhook-id": c.ID, "webhook-type": "validating"})
41 |
42 | return nil
43 | }
44 |
45 | // NewWebhook is a validating webhook and will return a webhook ready for a type of resource
46 | // it will validate the received resources.
47 | func NewWebhook(cfg WebhookConfig) (webhook.Webhook, error) {
48 | if err := cfg.defaults(); err != nil {
49 | return nil, fmt.Errorf("invalid configuration: %w", err)
50 | }
51 |
52 | // If we don't have the type of the object create a dynamic object creator that will
53 | // infer the type.
54 | var oc helpers.ObjectCreator
55 | if cfg.Obj != nil {
56 | oc = helpers.NewStaticObjectCreator(cfg.Obj)
57 | } else {
58 | oc = helpers.NewDynamicObjectCreator()
59 | }
60 |
61 | // Create our webhook and wrap for instrumentation (metrics and tracing).
62 | return &validatingWebhook{
63 | id: cfg.ID,
64 | objectCreator: oc,
65 | validator: cfg.Validator,
66 | cfg: cfg,
67 | logger: cfg.Logger,
68 | }, nil
69 | }
70 |
71 | type validatingWebhook struct {
72 | id string
73 | objectCreator helpers.ObjectCreator
74 | validator Validator
75 | cfg WebhookConfig
76 | logger log.Logger
77 | }
78 |
79 | func (w validatingWebhook) ID() string { return w.id }
80 |
81 | func (w validatingWebhook) Kind() model.WebhookKind { return model.WebhookKindValidating }
82 |
83 | func (w validatingWebhook) Review(ctx context.Context, ar model.AdmissionReview) (model.AdmissionResponse, error) {
84 | // Delete operations don't have body because should be gone on the deletion, instead they have the body
85 | // of the object we want to delete as an old object.
86 | raw := ar.NewObjectRaw
87 | if ar.Operation == model.OperationDelete {
88 | raw = ar.OldObjectRaw
89 | }
90 |
91 | // Create a new object from the raw type.
92 | runtimeObj, err := w.objectCreator.NewObject(raw)
93 | if err != nil {
94 | return nil, fmt.Errorf("could not create object from raw: %w", err)
95 | }
96 |
97 | validatingObj, ok := runtimeObj.(metav1.Object)
98 | // Get the object.
99 | if !ok {
100 | return nil, fmt.Errorf("impossible to type assert the deep copy to metav1.Object")
101 | }
102 |
103 | res, err := w.validator.Validate(ctx, &ar, validatingObj)
104 | if err != nil {
105 | return nil, fmt.Errorf("validator error: %w", err)
106 | }
107 |
108 | if res == nil {
109 | return nil, fmt.Errorf("result is required, validator result is nil")
110 | }
111 |
112 | w.logger.WithCtxValues(ctx).WithValues(log.Kv{"valid": res.Valid}).Debugf("Webhook validating review finished with '%t' result", res.Valid)
113 |
114 | // Forge response.
115 | return &model.ValidatingAdmissionResponse{
116 | ID: ar.ID,
117 | Allowed: res.Valid,
118 | Message: res.Message,
119 | Warnings: res.Warnings,
120 | }, nil
121 | }
122 |
--------------------------------------------------------------------------------
/pkg/webhook/webhook.go:
--------------------------------------------------------------------------------
1 | package webhook
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/slok/kubewebhook/v2/pkg/model"
7 | )
8 |
9 | // Webhook knows how to handle the admission reviews, in other words Webhook is a dynamic
10 | // admission webhook for Kubernetes.
11 | type Webhook interface {
12 | // The id of the webhook.
13 | ID() string
14 | // The kind of the webhook.
15 | Kind() model.WebhookKind
16 | // Review will handle the admission review and return the AdmissionResponse with the result of the admission
17 | // error, mutation...
18 | Review(ctx context.Context, ar model.AdmissionReview) (model.AdmissionResponse, error)
19 | }
20 |
21 | //go:generate mockery --case underscore --output webhookmock --outpkg webhookmock --name Webhook
22 |
--------------------------------------------------------------------------------
/pkg/webhook/webhookmock/webhook.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v2.12.2. DO NOT EDIT.
2 |
3 | package webhookmock
4 |
5 | import (
6 | context "context"
7 |
8 | model "github.com/slok/kubewebhook/v2/pkg/model"
9 | mock "github.com/stretchr/testify/mock"
10 |
11 | testing "testing"
12 | )
13 |
14 | // Webhook is an autogenerated mock type for the Webhook type
15 | type Webhook struct {
16 | mock.Mock
17 | }
18 |
19 | // ID provides a mock function with given fields:
20 | func (_m *Webhook) ID() string {
21 | ret := _m.Called()
22 |
23 | var r0 string
24 | if rf, ok := ret.Get(0).(func() string); ok {
25 | r0 = rf()
26 | } else {
27 | r0 = ret.Get(0).(string)
28 | }
29 |
30 | return r0
31 | }
32 |
33 | // Kind provides a mock function with given fields:
34 | func (_m *Webhook) Kind() model.WebhookKind {
35 | ret := _m.Called()
36 |
37 | var r0 model.WebhookKind
38 | if rf, ok := ret.Get(0).(func() model.WebhookKind); ok {
39 | r0 = rf()
40 | } else {
41 | r0 = ret.Get(0).(model.WebhookKind)
42 | }
43 |
44 | return r0
45 | }
46 |
47 | // Review provides a mock function with given fields: ctx, ar
48 | func (_m *Webhook) Review(ctx context.Context, ar model.AdmissionReview) (model.AdmissionResponse, error) {
49 | ret := _m.Called(ctx, ar)
50 |
51 | var r0 model.AdmissionResponse
52 | if rf, ok := ret.Get(0).(func(context.Context, model.AdmissionReview) model.AdmissionResponse); ok {
53 | r0 = rf(ctx, ar)
54 | } else {
55 | if ret.Get(0) != nil {
56 | r0 = ret.Get(0).(model.AdmissionResponse)
57 | }
58 | }
59 |
60 | var r1 error
61 | if rf, ok := ret.Get(1).(func(context.Context, model.AdmissionReview) error); ok {
62 | r1 = rf(ctx, ar)
63 | } else {
64 | r1 = ret.Error(1)
65 | }
66 |
67 | return r0, r1
68 | }
69 |
70 | // NewWebhook creates a new instance of Webhook. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
71 | func NewWebhook(t testing.TB) *Webhook {
72 | mock := &Webhook{}
73 | mock.Mock.Test(t)
74 |
75 | t.Cleanup(func() { mock.AssertExpectations(t) })
76 |
77 | return mock
78 | }
79 |
--------------------------------------------------------------------------------
/test/Makefile:
--------------------------------------------------------------------------------
1 | # Creates test certificates for the application.
2 | .PHONY: create-manual-test-certs
3 | create-manual-test-certs:
4 | ${PWD}/create-manual-certs.sh 0.tcp.ngrok.io ${PWD}/manual/certs
5 |
6 | .PHONY: run-example
7 | run-example:
8 | go run ${PWD}/../examples/multiwebhook/cmd/multiwebhook/* -tls-cert-file=${PWD}/manual/certs/cert.pem -tls-key-file=${PWD}/manual/certs/key.pem --debug
9 |
--------------------------------------------------------------------------------
/test/integration/certs/cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIEEzCCAnugAwIBAgIQdpFCvidfSshoG3qpxQeoWDANBgkqhkiG9w0BAQsFADBX
3 | MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExFjAUBgNVBAsMDXNsb2tA
4 | Y29zb3dvcmsxHTAbBgNVBAMMFG1rY2VydCBzbG9rQGNvc293b3JrMB4XDTI0MDMz
5 | MTE1NTMwNFoXDTI2MDcwMTE1NTMwNFowQTEnMCUGA1UEChMebWtjZXJ0IGRldmVs
6 | b3BtZW50IGNlcnRpZmljYXRlMRYwFAYDVQQLDA1zbG9rQGNvc293b3JrMIIBIjAN
7 | BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyjTAwtUqkHs0fO1UAPGGYrf3WMJy
8 | E66/fJzP0wseMnkLaCTBn/DgXzzRhBJ7RjKPnOu2yYKF2Xl2Vs2QJo0bxBVT04L3
9 | Kb5vrOWWOQJX9vCbWQnCreGHaT4JxFqLhGShjbO3TiLcuztGmT2d5khsw5VB/33V
10 | iUhSqI0wgQQrWtJlDTGxNMOXbKFvNKt5YCc6Z8i8WpyfW6LD8HOf3JW8/F4xAdZA
11 | /OU/muZNGFBGLIXhEB5bgJsWeWhJleUUADm405lqjv7pgr31ZWNLE+/YSWs5E13l
12 | wP0SYPVxKQL1n5cqNTdrPP5gqweKi+K/ITdTGe2dVZjfNbsJt/3dLgKZOwIDAQAB
13 | o3EwbzAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0j
14 | BBgwFoAUJKxbSiBv5OwUKGe8fws3pfl3/p8wJwYDVR0RBCAwHoIOKi50Y3Aubmdy
15 | b2suaW+CDHRjcC5uZ3Jvay5pbzANBgkqhkiG9w0BAQsFAAOCAYEAf9d7icRKcORO
16 | Yo69A8DnSMePWS8kdz5WJpPqO4acLDjW2HPx3oe06roV9DVEtdGm0Wjd1bpctvcL
17 | r/tlSLhcAp4ij+izWnlr0LXbv2O/bmWZ0NCWpdHv7M0lFFhocZduuiYawLXv9e0K
18 | T+DsOPQOQeRw+GnOTVL9D4hr7n7c0UisYyuyYqKTzyf9kQgsSNxMm/VaUryA3Lga
19 | WaC50VK5geeV9iuV5BnLnAUW+Pk2j6WrslLSCJayv6d0IQ47PlpxJpWRQhN7xY4G
20 | jSAx8xJiEhUr6YCdNjUIK6Pyj1TlLsaslpEgN3A0+b8bukhGP/zQDxH8zVQKu4vB
21 | ZQg5jNHj+Pwv6rQ8Tho18/mmkAgmakpGlnFqaydUiYDhfIiYpDksAkQ/sOso03t7
22 | 9dyeO4SyZuyV0HoBCmpCvzqH7YHbUXzqmklgnjM4CBQ+uqVwNiths1c8rgo3q72N
23 | C2kqCd7YLnk6rYNU+ozPA9hYuma0G20kilIupoKH5TW27xE5iZwh
24 | -----END CERTIFICATE-----
25 |
--------------------------------------------------------------------------------
/test/integration/certs/key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDKNMDC1SqQezR8
3 | 7VQA8YZit/dYwnITrr98nM/TCx4yeQtoJMGf8OBfPNGEEntGMo+c67bJgoXZeXZW
4 | zZAmjRvEFVPTgvcpvm+s5ZY5Alf28JtZCcKt4YdpPgnEWouEZKGNs7dOIty7O0aZ
5 | PZ3mSGzDlUH/fdWJSFKojTCBBCta0mUNMbE0w5dsoW80q3lgJzpnyLxanJ9bosPw
6 | c5/clbz8XjEB1kD85T+a5k0YUEYsheEQHluAmxZ5aEmV5RQAObjTmWqO/umCvfVl
7 | Y0sT79hJazkTXeXA/RJg9XEpAvWflyo1N2s8/mCrB4qL4r8hN1MZ7Z1VmN81uwm3
8 | /d0uApk7AgMBAAECggEABCi5nkhMK9Sc68Tl6W8OWJF4IPc+6XC6t5FyJOEhqeAb
9 | f/ThlqyZsNvLY3AN4Q/BLHUcuWBZ6HM7H+XyhRh57bqSktMqyk0EdwXx5RJLROUG
10 | DPrKalEtO9ju0n8aR4raV9POfWjyKVe6yAQgb1AmDI/RX7Py5HP8X0MoMD8ptSB9
11 | X405DmUI2HQbJAAuKarznF1rS/9qMsDbFcFCnrIgFcD9KvXnX/IYAhNZ3uTasb0x
12 | gm1CqKoVDA6vAiGtwOUMmpljoVwI1uL7PoiihH3VZDTfSvEuKhCG+WZWkY4FHBnm
13 | soU9Kl61pop8h5bcaAyvbWujsPfbAlZJi7RTCdvzwQKBgQDjOo6phIFCHagSWyGM
14 | LwRLrdhidbNEgu7QlYF+5E+nKpB0lws3zoqMDQtZAqXzDtX5l3AorWjA2EDIww72
15 | N+JQ1ya+TaowlLTpKXYyKWJ2/dLgaJN/B7hZch7TepocO2/vxaX9Znb3Glpqrys8
16 | tdZwfRbo9pYAraLcLwpAfXd/OQKBgQDjzxrwNlGroBNEhVjmARE3yC0v2D4EmFAW
17 | kA4qGAmLllj0o/FVjcpguIRdP7AZpnHBj7QRy3WeJiSkXz1zIjTwB3bOrUOXtdsQ
18 | gROjt5bl2HV/cbo9FjGItZ2mDsXHhD8BYegWjaB6CjF6oA+hAxmT+CxVTN2mqUXH
19 | XUv4MR1oEwKBgQDAIkh1ECSXuinec443UZ4pO7Mm9e0CNlAmsPQRTo4HhJcm7ny+
20 | 0HcYGwOIXRPjJJ1LcYCV9KMJxRFqcHzbNr+3qWUKLvOEJomeSb+2hIXMpa1EsUhe
21 | djGr+DIWkalzy9JHhPFpBdX79R2U7c76g6rx7OONUsvdgPSfh2r8wtpjCQKBgQCw
22 | UKPV1QTC44LmHprecWcFGG44wJHHdqdNvzLnS3Ff0v6IYbawc6x1zXnMvjUqtRMI
23 | L5O7zg/7ViQ3/+qMiKYWPICsl5df/QVOscgkhzxIKo9OExSEoP+3gnFAi+Bxeh5V
24 | kJRTmEvjCK6g7O8LvF14k7SkVHicvBhgpAnfTwwmdwKBgQDgErKWQLbQMzf9BS/D
25 | ck394hjR82HW0GwtG8al3P6fV8ZCpCrbl2OBCC0uCQTxg9/iULm7AH2ryRX4qNc8
26 | GtxGDRUP/ZTZcEXDgTC/tGHPAzr+aIq2OLr0+mhYVgS8UKAAMU5mjE7tdxJFd77c
27 | ZJfGwEo4iZzNDcGVj0BIquxwgw==
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/test/integration/crd/apis/building/register.go:
--------------------------------------------------------------------------------
1 | package building
2 |
3 | // GroupName is the name of the group.
4 | const GroupName = "building.kubewebhook.slok.dev"
5 |
--------------------------------------------------------------------------------
/test/integration/crd/apis/building/v1/doc.go:
--------------------------------------------------------------------------------
1 | // +k8s:deepcopy-gen=package
2 | // +groupName=building.kubewebhook.slok.dev
3 |
4 | // Package v1 is the v1 version of the API.
5 | package v1
6 |
--------------------------------------------------------------------------------
/test/integration/crd/apis/building/v1/register.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
5 | "k8s.io/apimachinery/pkg/runtime"
6 | "k8s.io/apimachinery/pkg/runtime/schema"
7 |
8 | "github.com/slok/kubewebhook/v2/test/integration/crd/apis/building"
9 | )
10 |
11 | const version = "v1"
12 |
13 | // SchemeGroupVersion is group version used to register these objects.
14 | var SchemeGroupVersion = schema.GroupVersion{Group: building.GroupName, Version: version}
15 |
16 | // Kind takes an unqualified kind and returns back a Group qualified GroupKind.
17 | func Kind(kind string) schema.GroupKind {
18 | return VersionKind(kind).GroupKind()
19 | }
20 |
21 | // VersionKind takes an unqualified kind and returns back a Group qualified GroupVersionKind.
22 | func VersionKind(kind string) schema.GroupVersionKind {
23 | return SchemeGroupVersion.WithKind(kind)
24 | }
25 |
26 | // Resource takes an unqualified resource and returns a Group qualified GroupResource.
27 | func Resource(resource string) schema.GroupResource {
28 | return SchemeGroupVersion.WithResource(resource).GroupResource()
29 | }
30 |
31 | var (
32 | // SchemeBuilder is used to register the type to the Kubernetes CRD APIs.
33 | SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
34 | // AddToScheme is used to register the type to the Kubernetes CRD APIs.
35 | AddToScheme = SchemeBuilder.AddToScheme
36 | )
37 |
38 | // Adds the list of known types to Scheme.
39 | func addKnownTypes(scheme *runtime.Scheme) error {
40 | scheme.AddKnownTypes(SchemeGroupVersion,
41 | &House{},
42 | &HouseList{},
43 | )
44 | metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
45 | return nil
46 | }
47 |
--------------------------------------------------------------------------------
/test/integration/crd/apis/building/v1/types.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
5 | )
6 |
7 | // +genclient
8 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
9 |
10 | // House represents a house.
11 | type House struct {
12 | metav1.TypeMeta `json:",inline"`
13 | metav1.ObjectMeta `json:"metadata,omitempty"`
14 |
15 | Spec HouseSpec `json:"spec,omitempty"`
16 | }
17 |
18 | // HouseSpec is the spec for a Team resource.
19 | type HouseSpec struct {
20 | Name string `json:"name"`
21 | Address string `json:"address"`
22 | Active *bool `json:"active,omitempty"`
23 | // +listType=map
24 | // +listMapKey=name
25 | Owners []User `json:"owners,omitempty"`
26 | }
27 |
28 | // User is an user.
29 | type User struct {
30 | Name string `json:"name"`
31 | Email string `json:"email"`
32 | }
33 |
34 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
35 |
36 | // HouseList is a list of House resources.
37 | type HouseList struct {
38 | metav1.TypeMeta `json:",inline"`
39 | metav1.ListMeta `json:"metadata"`
40 |
41 | Items []House `json:"items"`
42 | }
43 |
--------------------------------------------------------------------------------
/test/integration/crd/apis/building/v1/zz_generated.deepcopy.go:
--------------------------------------------------------------------------------
1 | //go:build !ignore_autogenerated
2 | // +build !ignore_autogenerated
3 |
4 | // Code generated by deepcopy-gen. DO NOT EDIT.
5 |
6 | package v1
7 |
8 | import (
9 | runtime "k8s.io/apimachinery/pkg/runtime"
10 | )
11 |
12 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
13 | func (in *House) DeepCopyInto(out *House) {
14 | *out = *in
15 | out.TypeMeta = in.TypeMeta
16 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
17 | in.Spec.DeepCopyInto(&out.Spec)
18 | return
19 | }
20 |
21 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new House.
22 | func (in *House) DeepCopy() *House {
23 | if in == nil {
24 | return nil
25 | }
26 | out := new(House)
27 | in.DeepCopyInto(out)
28 | return out
29 | }
30 |
31 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
32 | func (in *House) DeepCopyObject() runtime.Object {
33 | if c := in.DeepCopy(); c != nil {
34 | return c
35 | }
36 | return nil
37 | }
38 |
39 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
40 | func (in *HouseList) DeepCopyInto(out *HouseList) {
41 | *out = *in
42 | out.TypeMeta = in.TypeMeta
43 | in.ListMeta.DeepCopyInto(&out.ListMeta)
44 | if in.Items != nil {
45 | in, out := &in.Items, &out.Items
46 | *out = make([]House, len(*in))
47 | for i := range *in {
48 | (*in)[i].DeepCopyInto(&(*out)[i])
49 | }
50 | }
51 | return
52 | }
53 |
54 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HouseList.
55 | func (in *HouseList) DeepCopy() *HouseList {
56 | if in == nil {
57 | return nil
58 | }
59 | out := new(HouseList)
60 | in.DeepCopyInto(out)
61 | return out
62 | }
63 |
64 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
65 | func (in *HouseList) DeepCopyObject() runtime.Object {
66 | if c := in.DeepCopy(); c != nil {
67 | return c
68 | }
69 | return nil
70 | }
71 |
72 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
73 | func (in *HouseSpec) DeepCopyInto(out *HouseSpec) {
74 | *out = *in
75 | if in.Active != nil {
76 | in, out := &in.Active, &out.Active
77 | *out = new(bool)
78 | **out = **in
79 | }
80 | if in.Owners != nil {
81 | in, out := &in.Owners, &out.Owners
82 | *out = make([]User, len(*in))
83 | copy(*out, *in)
84 | }
85 | return
86 | }
87 |
88 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HouseSpec.
89 | func (in *HouseSpec) DeepCopy() *HouseSpec {
90 | if in == nil {
91 | return nil
92 | }
93 | out := new(HouseSpec)
94 | in.DeepCopyInto(out)
95 | return out
96 | }
97 |
98 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
99 | func (in *User) DeepCopyInto(out *User) {
100 | *out = *in
101 | return
102 | }
103 |
104 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new User.
105 | func (in *User) DeepCopy() *User {
106 | if in == nil {
107 | return nil
108 | }
109 | out := new(User)
110 | in.DeepCopyInto(out)
111 | return out
112 | }
113 |
--------------------------------------------------------------------------------
/test/integration/crd/client/clientset/versioned/clientset.go:
--------------------------------------------------------------------------------
1 | // Code generated by client-gen. DO NOT EDIT.
2 |
3 | package versioned
4 |
5 | import (
6 | "fmt"
7 | "net/http"
8 |
9 | buildingv1 "github.com/slok/kubewebhook/v2/test/integration/crd/client/clientset/versioned/typed/building/v1"
10 | discovery "k8s.io/client-go/discovery"
11 | rest "k8s.io/client-go/rest"
12 | flowcontrol "k8s.io/client-go/util/flowcontrol"
13 | )
14 |
15 | type Interface interface {
16 | Discovery() discovery.DiscoveryInterface
17 | BuildingV1() buildingv1.BuildingV1Interface
18 | }
19 |
20 | // Clientset contains the clients for groups.
21 | type Clientset struct {
22 | *discovery.DiscoveryClient
23 | buildingV1 *buildingv1.BuildingV1Client
24 | }
25 |
26 | // BuildingV1 retrieves the BuildingV1Client
27 | func (c *Clientset) BuildingV1() buildingv1.BuildingV1Interface {
28 | return c.buildingV1
29 | }
30 |
31 | // Discovery retrieves the DiscoveryClient
32 | func (c *Clientset) Discovery() discovery.DiscoveryInterface {
33 | if c == nil {
34 | return nil
35 | }
36 | return c.DiscoveryClient
37 | }
38 |
39 | // NewForConfig creates a new Clientset for the given config.
40 | // If config's RateLimiter is not set and QPS and Burst are acceptable,
41 | // NewForConfig will generate a rate-limiter in configShallowCopy.
42 | // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient),
43 | // where httpClient was generated with rest.HTTPClientFor(c).
44 | func NewForConfig(c *rest.Config) (*Clientset, error) {
45 | configShallowCopy := *c
46 |
47 | if configShallowCopy.UserAgent == "" {
48 | configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent()
49 | }
50 |
51 | // share the transport between all clients
52 | httpClient, err := rest.HTTPClientFor(&configShallowCopy)
53 | if err != nil {
54 | return nil, err
55 | }
56 |
57 | return NewForConfigAndClient(&configShallowCopy, httpClient)
58 | }
59 |
60 | // NewForConfigAndClient creates a new Clientset for the given config and http client.
61 | // Note the http client provided takes precedence over the configured transport values.
62 | // If config's RateLimiter is not set and QPS and Burst are acceptable,
63 | // NewForConfigAndClient will generate a rate-limiter in configShallowCopy.
64 | func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) {
65 | configShallowCopy := *c
66 | if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 {
67 | if configShallowCopy.Burst <= 0 {
68 | return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0")
69 | }
70 | configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst)
71 | }
72 |
73 | var cs Clientset
74 | var err error
75 | cs.buildingV1, err = buildingv1.NewForConfigAndClient(&configShallowCopy, httpClient)
76 | if err != nil {
77 | return nil, err
78 | }
79 |
80 | cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient)
81 | if err != nil {
82 | return nil, err
83 | }
84 | return &cs, nil
85 | }
86 |
87 | // NewForConfigOrDie creates a new Clientset for the given config and
88 | // panics if there is an error in the config.
89 | func NewForConfigOrDie(c *rest.Config) *Clientset {
90 | cs, err := NewForConfig(c)
91 | if err != nil {
92 | panic(err)
93 | }
94 | return cs
95 | }
96 |
97 | // New creates a new Clientset for the given RESTClient.
98 | func New(c rest.Interface) *Clientset {
99 | var cs Clientset
100 | cs.buildingV1 = buildingv1.New(c)
101 |
102 | cs.DiscoveryClient = discovery.NewDiscoveryClient(c)
103 | return &cs
104 | }
105 |
--------------------------------------------------------------------------------
/test/integration/crd/client/clientset/versioned/fake/clientset_generated.go:
--------------------------------------------------------------------------------
1 | // Code generated by client-gen. DO NOT EDIT.
2 |
3 | package fake
4 |
5 | import (
6 | clientset "github.com/slok/kubewebhook/v2/test/integration/crd/client/clientset/versioned"
7 | buildingv1 "github.com/slok/kubewebhook/v2/test/integration/crd/client/clientset/versioned/typed/building/v1"
8 | fakebuildingv1 "github.com/slok/kubewebhook/v2/test/integration/crd/client/clientset/versioned/typed/building/v1/fake"
9 | "k8s.io/apimachinery/pkg/runtime"
10 | "k8s.io/apimachinery/pkg/watch"
11 | "k8s.io/client-go/discovery"
12 | fakediscovery "k8s.io/client-go/discovery/fake"
13 | "k8s.io/client-go/testing"
14 | )
15 |
16 | // NewSimpleClientset returns a clientset that will respond with the provided objects.
17 | // It's backed by a very simple object tracker that processes creates, updates and deletions as-is,
18 | // without applying any field management, validations and/or defaults. It shouldn't be considered a replacement
19 | // for a real clientset and is mostly useful in simple unit tests.
20 | //
21 | // DEPRECATED: NewClientset replaces this with support for field management, which significantly improves
22 | // server side apply testing. NewClientset is only available when apply configurations are generated (e.g.
23 | // via --with-applyconfig).
24 | func NewSimpleClientset(objects ...runtime.Object) *Clientset {
25 | o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder())
26 | for _, obj := range objects {
27 | if err := o.Add(obj); err != nil {
28 | panic(err)
29 | }
30 | }
31 |
32 | cs := &Clientset{tracker: o}
33 | cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake}
34 | cs.AddReactor("*", "*", testing.ObjectReaction(o))
35 | cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) {
36 | gvr := action.GetResource()
37 | ns := action.GetNamespace()
38 | watch, err := o.Watch(gvr, ns)
39 | if err != nil {
40 | return false, nil, err
41 | }
42 | return true, watch, nil
43 | })
44 |
45 | return cs
46 | }
47 |
48 | // Clientset implements clientset.Interface. Meant to be embedded into a
49 | // struct to get a default implementation. This makes faking out just the method
50 | // you want to test easier.
51 | type Clientset struct {
52 | testing.Fake
53 | discovery *fakediscovery.FakeDiscovery
54 | tracker testing.ObjectTracker
55 | }
56 |
57 | func (c *Clientset) Discovery() discovery.DiscoveryInterface {
58 | return c.discovery
59 | }
60 |
61 | func (c *Clientset) Tracker() testing.ObjectTracker {
62 | return c.tracker
63 | }
64 |
65 | var (
66 | _ clientset.Interface = &Clientset{}
67 | _ testing.FakeClient = &Clientset{}
68 | )
69 |
70 | // BuildingV1 retrieves the BuildingV1Client
71 | func (c *Clientset) BuildingV1() buildingv1.BuildingV1Interface {
72 | return &fakebuildingv1.FakeBuildingV1{Fake: &c.Fake}
73 | }
74 |
--------------------------------------------------------------------------------
/test/integration/crd/client/clientset/versioned/fake/doc.go:
--------------------------------------------------------------------------------
1 | // Code generated by client-gen. DO NOT EDIT.
2 |
3 | // This package has the automatically generated fake clientset.
4 | package fake
5 |
--------------------------------------------------------------------------------
/test/integration/crd/client/clientset/versioned/fake/register.go:
--------------------------------------------------------------------------------
1 | // Code generated by client-gen. DO NOT EDIT.
2 |
3 | package fake
4 |
5 | import (
6 | buildingv1 "github.com/slok/kubewebhook/v2/test/integration/crd/apis/building/v1"
7 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8 | runtime "k8s.io/apimachinery/pkg/runtime"
9 | schema "k8s.io/apimachinery/pkg/runtime/schema"
10 | serializer "k8s.io/apimachinery/pkg/runtime/serializer"
11 | utilruntime "k8s.io/apimachinery/pkg/util/runtime"
12 | )
13 |
14 | var scheme = runtime.NewScheme()
15 | var codecs = serializer.NewCodecFactory(scheme)
16 |
17 | var localSchemeBuilder = runtime.SchemeBuilder{
18 | buildingv1.AddToScheme,
19 | }
20 |
21 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition
22 | // of clientsets, like in:
23 | //
24 | // import (
25 | // "k8s.io/client-go/kubernetes"
26 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme"
27 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
28 | // )
29 | //
30 | // kclientset, _ := kubernetes.NewForConfig(c)
31 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
32 | //
33 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
34 | // correctly.
35 | var AddToScheme = localSchemeBuilder.AddToScheme
36 |
37 | func init() {
38 | v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"})
39 | utilruntime.Must(AddToScheme(scheme))
40 | }
41 |
--------------------------------------------------------------------------------
/test/integration/crd/client/clientset/versioned/scheme/doc.go:
--------------------------------------------------------------------------------
1 | // Code generated by client-gen. DO NOT EDIT.
2 |
3 | // This package contains the scheme of the automatically generated clientset.
4 | package scheme
5 |
--------------------------------------------------------------------------------
/test/integration/crd/client/clientset/versioned/scheme/register.go:
--------------------------------------------------------------------------------
1 | // Code generated by client-gen. DO NOT EDIT.
2 |
3 | package scheme
4 |
5 | import (
6 | buildingv1 "github.com/slok/kubewebhook/v2/test/integration/crd/apis/building/v1"
7 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8 | runtime "k8s.io/apimachinery/pkg/runtime"
9 | schema "k8s.io/apimachinery/pkg/runtime/schema"
10 | serializer "k8s.io/apimachinery/pkg/runtime/serializer"
11 | utilruntime "k8s.io/apimachinery/pkg/util/runtime"
12 | )
13 |
14 | var Scheme = runtime.NewScheme()
15 | var Codecs = serializer.NewCodecFactory(Scheme)
16 | var ParameterCodec = runtime.NewParameterCodec(Scheme)
17 | var localSchemeBuilder = runtime.SchemeBuilder{
18 | buildingv1.AddToScheme,
19 | }
20 |
21 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition
22 | // of clientsets, like in:
23 | //
24 | // import (
25 | // "k8s.io/client-go/kubernetes"
26 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme"
27 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
28 | // )
29 | //
30 | // kclientset, _ := kubernetes.NewForConfig(c)
31 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
32 | //
33 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
34 | // correctly.
35 | var AddToScheme = localSchemeBuilder.AddToScheme
36 |
37 | func init() {
38 | v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"})
39 | utilruntime.Must(AddToScheme(Scheme))
40 | }
41 |
--------------------------------------------------------------------------------
/test/integration/crd/client/clientset/versioned/typed/building/v1/building_client.go:
--------------------------------------------------------------------------------
1 | // Code generated by client-gen. DO NOT EDIT.
2 |
3 | package v1
4 |
5 | import (
6 | "net/http"
7 |
8 | v1 "github.com/slok/kubewebhook/v2/test/integration/crd/apis/building/v1"
9 | "github.com/slok/kubewebhook/v2/test/integration/crd/client/clientset/versioned/scheme"
10 | rest "k8s.io/client-go/rest"
11 | )
12 |
13 | type BuildingV1Interface interface {
14 | RESTClient() rest.Interface
15 | HousesGetter
16 | }
17 |
18 | // BuildingV1Client is used to interact with features provided by the building.kubewebhook.slok.dev group.
19 | type BuildingV1Client struct {
20 | restClient rest.Interface
21 | }
22 |
23 | func (c *BuildingV1Client) Houses(namespace string) HouseInterface {
24 | return newHouses(c, namespace)
25 | }
26 |
27 | // NewForConfig creates a new BuildingV1Client for the given config.
28 | // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient),
29 | // where httpClient was generated with rest.HTTPClientFor(c).
30 | func NewForConfig(c *rest.Config) (*BuildingV1Client, error) {
31 | config := *c
32 | if err := setConfigDefaults(&config); err != nil {
33 | return nil, err
34 | }
35 | httpClient, err := rest.HTTPClientFor(&config)
36 | if err != nil {
37 | return nil, err
38 | }
39 | return NewForConfigAndClient(&config, httpClient)
40 | }
41 |
42 | // NewForConfigAndClient creates a new BuildingV1Client for the given config and http client.
43 | // Note the http client provided takes precedence over the configured transport values.
44 | func NewForConfigAndClient(c *rest.Config, h *http.Client) (*BuildingV1Client, error) {
45 | config := *c
46 | if err := setConfigDefaults(&config); err != nil {
47 | return nil, err
48 | }
49 | client, err := rest.RESTClientForConfigAndClient(&config, h)
50 | if err != nil {
51 | return nil, err
52 | }
53 | return &BuildingV1Client{client}, nil
54 | }
55 |
56 | // NewForConfigOrDie creates a new BuildingV1Client for the given config and
57 | // panics if there is an error in the config.
58 | func NewForConfigOrDie(c *rest.Config) *BuildingV1Client {
59 | client, err := NewForConfig(c)
60 | if err != nil {
61 | panic(err)
62 | }
63 | return client
64 | }
65 |
66 | // New creates a new BuildingV1Client for the given RESTClient.
67 | func New(c rest.Interface) *BuildingV1Client {
68 | return &BuildingV1Client{c}
69 | }
70 |
71 | func setConfigDefaults(config *rest.Config) error {
72 | gv := v1.SchemeGroupVersion
73 | config.GroupVersion = &gv
74 | config.APIPath = "/apis"
75 | config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
76 |
77 | if config.UserAgent == "" {
78 | config.UserAgent = rest.DefaultKubernetesUserAgent()
79 | }
80 |
81 | return nil
82 | }
83 |
84 | // RESTClient returns a RESTClient that is used to communicate
85 | // with API server by this client implementation.
86 | func (c *BuildingV1Client) RESTClient() rest.Interface {
87 | if c == nil {
88 | return nil
89 | }
90 | return c.restClient
91 | }
92 |
--------------------------------------------------------------------------------
/test/integration/crd/client/clientset/versioned/typed/building/v1/doc.go:
--------------------------------------------------------------------------------
1 | // Code generated by client-gen. DO NOT EDIT.
2 |
3 | // This package has the automatically generated typed clients.
4 | package v1
5 |
--------------------------------------------------------------------------------
/test/integration/crd/client/clientset/versioned/typed/building/v1/fake/doc.go:
--------------------------------------------------------------------------------
1 | // Code generated by client-gen. DO NOT EDIT.
2 |
3 | // Package fake has the automatically generated clients.
4 | package fake
5 |
--------------------------------------------------------------------------------
/test/integration/crd/client/clientset/versioned/typed/building/v1/fake/fake_building_client.go:
--------------------------------------------------------------------------------
1 | // Code generated by client-gen. DO NOT EDIT.
2 |
3 | package fake
4 |
5 | import (
6 | v1 "github.com/slok/kubewebhook/v2/test/integration/crd/client/clientset/versioned/typed/building/v1"
7 | rest "k8s.io/client-go/rest"
8 | testing "k8s.io/client-go/testing"
9 | )
10 |
11 | type FakeBuildingV1 struct {
12 | *testing.Fake
13 | }
14 |
15 | func (c *FakeBuildingV1) Houses(namespace string) v1.HouseInterface {
16 | return &FakeHouses{c, namespace}
17 | }
18 |
19 | // RESTClient returns a RESTClient that is used to communicate
20 | // with API server by this client implementation.
21 | func (c *FakeBuildingV1) RESTClient() rest.Interface {
22 | var ret *rest.RESTClient
23 | return ret
24 | }
25 |
--------------------------------------------------------------------------------
/test/integration/crd/client/clientset/versioned/typed/building/v1/fake/fake_house.go:
--------------------------------------------------------------------------------
1 | // Code generated by client-gen. DO NOT EDIT.
2 |
3 | package fake
4 |
5 | import (
6 | "context"
7 |
8 | v1 "github.com/slok/kubewebhook/v2/test/integration/crd/apis/building/v1"
9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10 | labels "k8s.io/apimachinery/pkg/labels"
11 | types "k8s.io/apimachinery/pkg/types"
12 | watch "k8s.io/apimachinery/pkg/watch"
13 | testing "k8s.io/client-go/testing"
14 | )
15 |
16 | // FakeHouses implements HouseInterface
17 | type FakeHouses struct {
18 | Fake *FakeBuildingV1
19 | ns string
20 | }
21 |
22 | var housesResource = v1.SchemeGroupVersion.WithResource("houses")
23 |
24 | var housesKind = v1.SchemeGroupVersion.WithKind("House")
25 |
26 | // Get takes name of the house, and returns the corresponding house object, and an error if there is any.
27 | func (c *FakeHouses) Get(ctx context.Context, name string, options metav1.GetOptions) (result *v1.House, err error) {
28 | emptyResult := &v1.House{}
29 | obj, err := c.Fake.
30 | Invokes(testing.NewGetActionWithOptions(housesResource, c.ns, name, options), emptyResult)
31 |
32 | if obj == nil {
33 | return emptyResult, err
34 | }
35 | return obj.(*v1.House), err
36 | }
37 |
38 | // List takes label and field selectors, and returns the list of Houses that match those selectors.
39 | func (c *FakeHouses) List(ctx context.Context, opts metav1.ListOptions) (result *v1.HouseList, err error) {
40 | emptyResult := &v1.HouseList{}
41 | obj, err := c.Fake.
42 | Invokes(testing.NewListActionWithOptions(housesResource, housesKind, c.ns, opts), emptyResult)
43 |
44 | if obj == nil {
45 | return emptyResult, err
46 | }
47 |
48 | label, _, _ := testing.ExtractFromListOptions(opts)
49 | if label == nil {
50 | label = labels.Everything()
51 | }
52 | list := &v1.HouseList{ListMeta: obj.(*v1.HouseList).ListMeta}
53 | for _, item := range obj.(*v1.HouseList).Items {
54 | if label.Matches(labels.Set(item.Labels)) {
55 | list.Items = append(list.Items, item)
56 | }
57 | }
58 | return list, err
59 | }
60 |
61 | // Watch returns a watch.Interface that watches the requested houses.
62 | func (c *FakeHouses) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
63 | return c.Fake.
64 | InvokesWatch(testing.NewWatchActionWithOptions(housesResource, c.ns, opts))
65 |
66 | }
67 |
68 | // Create takes the representation of a house and creates it. Returns the server's representation of the house, and an error, if there is any.
69 | func (c *FakeHouses) Create(ctx context.Context, house *v1.House, opts metav1.CreateOptions) (result *v1.House, err error) {
70 | emptyResult := &v1.House{}
71 | obj, err := c.Fake.
72 | Invokes(testing.NewCreateActionWithOptions(housesResource, c.ns, house, opts), emptyResult)
73 |
74 | if obj == nil {
75 | return emptyResult, err
76 | }
77 | return obj.(*v1.House), err
78 | }
79 |
80 | // Update takes the representation of a house and updates it. Returns the server's representation of the house, and an error, if there is any.
81 | func (c *FakeHouses) Update(ctx context.Context, house *v1.House, opts metav1.UpdateOptions) (result *v1.House, err error) {
82 | emptyResult := &v1.House{}
83 | obj, err := c.Fake.
84 | Invokes(testing.NewUpdateActionWithOptions(housesResource, c.ns, house, opts), emptyResult)
85 |
86 | if obj == nil {
87 | return emptyResult, err
88 | }
89 | return obj.(*v1.House), err
90 | }
91 |
92 | // Delete takes name of the house and deletes it. Returns an error if one occurs.
93 | func (c *FakeHouses) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error {
94 | _, err := c.Fake.
95 | Invokes(testing.NewDeleteActionWithOptions(housesResource, c.ns, name, opts), &v1.House{})
96 |
97 | return err
98 | }
99 |
100 | // DeleteCollection deletes a collection of objects.
101 | func (c *FakeHouses) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
102 | action := testing.NewDeleteCollectionActionWithOptions(housesResource, c.ns, opts, listOpts)
103 |
104 | _, err := c.Fake.Invokes(action, &v1.HouseList{})
105 | return err
106 | }
107 |
108 | // Patch applies the patch and returns the patched house.
109 | func (c *FakeHouses) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.House, err error) {
110 | emptyResult := &v1.House{}
111 | obj, err := c.Fake.
112 | Invokes(testing.NewPatchSubresourceActionWithOptions(housesResource, c.ns, name, pt, data, opts, subresources...), emptyResult)
113 |
114 | if obj == nil {
115 | return emptyResult, err
116 | }
117 | return obj.(*v1.House), err
118 | }
119 |
--------------------------------------------------------------------------------
/test/integration/crd/client/clientset/versioned/typed/building/v1/generated_expansion.go:
--------------------------------------------------------------------------------
1 | // Code generated by client-gen. DO NOT EDIT.
2 |
3 | package v1
4 |
5 | type HouseExpansion interface{}
6 |
--------------------------------------------------------------------------------
/test/integration/crd/client/clientset/versioned/typed/building/v1/house.go:
--------------------------------------------------------------------------------
1 | // Code generated by client-gen. DO NOT EDIT.
2 |
3 | package v1
4 |
5 | import (
6 | "context"
7 |
8 | v1 "github.com/slok/kubewebhook/v2/test/integration/crd/apis/building/v1"
9 | scheme "github.com/slok/kubewebhook/v2/test/integration/crd/client/clientset/versioned/scheme"
10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11 | types "k8s.io/apimachinery/pkg/types"
12 | watch "k8s.io/apimachinery/pkg/watch"
13 | gentype "k8s.io/client-go/gentype"
14 | )
15 |
16 | // HousesGetter has a method to return a HouseInterface.
17 | // A group's client should implement this interface.
18 | type HousesGetter interface {
19 | Houses(namespace string) HouseInterface
20 | }
21 |
22 | // HouseInterface has methods to work with House resources.
23 | type HouseInterface interface {
24 | Create(ctx context.Context, house *v1.House, opts metav1.CreateOptions) (*v1.House, error)
25 | Update(ctx context.Context, house *v1.House, opts metav1.UpdateOptions) (*v1.House, error)
26 | Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error
27 | DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error
28 | Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.House, error)
29 | List(ctx context.Context, opts metav1.ListOptions) (*v1.HouseList, error)
30 | Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error)
31 | Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.House, err error)
32 | HouseExpansion
33 | }
34 |
35 | // houses implements HouseInterface
36 | type houses struct {
37 | *gentype.ClientWithList[*v1.House, *v1.HouseList]
38 | }
39 |
40 | // newHouses returns a Houses
41 | func newHouses(c *BuildingV1Client, namespace string) *houses {
42 | return &houses{
43 | gentype.NewClientWithList[*v1.House, *v1.HouseList](
44 | "houses",
45 | c.RESTClient(),
46 | scheme.ParameterCodec,
47 | namespace,
48 | func() *v1.House { return &v1.House{} },
49 | func() *v1.HouseList { return &v1.HouseList{} }),
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/test/integration/crd/client/informers/externalversions/building/interface.go:
--------------------------------------------------------------------------------
1 | // Code generated by informer-gen. DO NOT EDIT.
2 |
3 | package building
4 |
5 | import (
6 | v1 "github.com/slok/kubewebhook/v2/test/integration/crd/client/informers/externalversions/building/v1"
7 | internalinterfaces "github.com/slok/kubewebhook/v2/test/integration/crd/client/informers/externalversions/internalinterfaces"
8 | )
9 |
10 | // Interface provides access to each of this group's versions.
11 | type Interface interface {
12 | // V1 provides access to shared informers for resources in V1.
13 | V1() v1.Interface
14 | }
15 |
16 | type group struct {
17 | factory internalinterfaces.SharedInformerFactory
18 | namespace string
19 | tweakListOptions internalinterfaces.TweakListOptionsFunc
20 | }
21 |
22 | // New returns a new Interface.
23 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
24 | return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
25 | }
26 |
27 | // V1 returns a new v1.Interface.
28 | func (g *group) V1() v1.Interface {
29 | return v1.New(g.factory, g.namespace, g.tweakListOptions)
30 | }
31 |
--------------------------------------------------------------------------------
/test/integration/crd/client/informers/externalversions/building/v1/house.go:
--------------------------------------------------------------------------------
1 | // Code generated by informer-gen. DO NOT EDIT.
2 |
3 | package v1
4 |
5 | import (
6 | "context"
7 | time "time"
8 |
9 | buildingv1 "github.com/slok/kubewebhook/v2/test/integration/crd/apis/building/v1"
10 | versioned "github.com/slok/kubewebhook/v2/test/integration/crd/client/clientset/versioned"
11 | internalinterfaces "github.com/slok/kubewebhook/v2/test/integration/crd/client/informers/externalversions/internalinterfaces"
12 | v1 "github.com/slok/kubewebhook/v2/test/integration/crd/client/listers/building/v1"
13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14 | runtime "k8s.io/apimachinery/pkg/runtime"
15 | watch "k8s.io/apimachinery/pkg/watch"
16 | cache "k8s.io/client-go/tools/cache"
17 | )
18 |
19 | // HouseInformer provides access to a shared informer and lister for
20 | // Houses.
21 | type HouseInformer interface {
22 | Informer() cache.SharedIndexInformer
23 | Lister() v1.HouseLister
24 | }
25 |
26 | type houseInformer struct {
27 | factory internalinterfaces.SharedInformerFactory
28 | tweakListOptions internalinterfaces.TweakListOptionsFunc
29 | namespace string
30 | }
31 |
32 | // NewHouseInformer constructs a new informer for House type.
33 | // Always prefer using an informer factory to get a shared informer instead of getting an independent
34 | // one. This reduces memory footprint and number of connections to the server.
35 | func NewHouseInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
36 | return NewFilteredHouseInformer(client, namespace, resyncPeriod, indexers, nil)
37 | }
38 |
39 | // NewFilteredHouseInformer constructs a new informer for House type.
40 | // Always prefer using an informer factory to get a shared informer instead of getting an independent
41 | // one. This reduces memory footprint and number of connections to the server.
42 | func NewFilteredHouseInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
43 | return cache.NewSharedIndexInformer(
44 | &cache.ListWatch{
45 | ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
46 | if tweakListOptions != nil {
47 | tweakListOptions(&options)
48 | }
49 | return client.BuildingV1().Houses(namespace).List(context.TODO(), options)
50 | },
51 | WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
52 | if tweakListOptions != nil {
53 | tweakListOptions(&options)
54 | }
55 | return client.BuildingV1().Houses(namespace).Watch(context.TODO(), options)
56 | },
57 | },
58 | &buildingv1.House{},
59 | resyncPeriod,
60 | indexers,
61 | )
62 | }
63 |
64 | func (f *houseInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
65 | return NewFilteredHouseInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
66 | }
67 |
68 | func (f *houseInformer) Informer() cache.SharedIndexInformer {
69 | return f.factory.InformerFor(&buildingv1.House{}, f.defaultInformer)
70 | }
71 |
72 | func (f *houseInformer) Lister() v1.HouseLister {
73 | return v1.NewHouseLister(f.Informer().GetIndexer())
74 | }
75 |
--------------------------------------------------------------------------------
/test/integration/crd/client/informers/externalversions/building/v1/interface.go:
--------------------------------------------------------------------------------
1 | // Code generated by informer-gen. DO NOT EDIT.
2 |
3 | package v1
4 |
5 | import (
6 | internalinterfaces "github.com/slok/kubewebhook/v2/test/integration/crd/client/informers/externalversions/internalinterfaces"
7 | )
8 |
9 | // Interface provides access to all the informers in this group version.
10 | type Interface interface {
11 | // Houses returns a HouseInformer.
12 | Houses() HouseInformer
13 | }
14 |
15 | type version struct {
16 | factory internalinterfaces.SharedInformerFactory
17 | namespace string
18 | tweakListOptions internalinterfaces.TweakListOptionsFunc
19 | }
20 |
21 | // New returns a new Interface.
22 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
23 | return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
24 | }
25 |
26 | // Houses returns a HouseInformer.
27 | func (v *version) Houses() HouseInformer {
28 | return &houseInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
29 | }
30 |
--------------------------------------------------------------------------------
/test/integration/crd/client/informers/externalversions/generic.go:
--------------------------------------------------------------------------------
1 | // Code generated by informer-gen. DO NOT EDIT.
2 |
3 | package externalversions
4 |
5 | import (
6 | "fmt"
7 |
8 | v1 "github.com/slok/kubewebhook/v2/test/integration/crd/apis/building/v1"
9 | schema "k8s.io/apimachinery/pkg/runtime/schema"
10 | cache "k8s.io/client-go/tools/cache"
11 | )
12 |
13 | // GenericInformer is type of SharedIndexInformer which will locate and delegate to other
14 | // sharedInformers based on type
15 | type GenericInformer interface {
16 | Informer() cache.SharedIndexInformer
17 | Lister() cache.GenericLister
18 | }
19 |
20 | type genericInformer struct {
21 | informer cache.SharedIndexInformer
22 | resource schema.GroupResource
23 | }
24 |
25 | // Informer returns the SharedIndexInformer.
26 | func (f *genericInformer) Informer() cache.SharedIndexInformer {
27 | return f.informer
28 | }
29 |
30 | // Lister returns the GenericLister.
31 | func (f *genericInformer) Lister() cache.GenericLister {
32 | return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource)
33 | }
34 |
35 | // ForResource gives generic access to a shared informer of the matching type
36 | // TODO extend this to unknown resources with a client pool
37 | func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) {
38 | switch resource {
39 | // Group=building.kubewebhook.slok.dev, Version=v1
40 | case v1.SchemeGroupVersion.WithResource("houses"):
41 | return &genericInformer{resource: resource.GroupResource(), informer: f.Building().V1().Houses().Informer()}, nil
42 |
43 | }
44 |
45 | return nil, fmt.Errorf("no informer found for %v", resource)
46 | }
47 |
--------------------------------------------------------------------------------
/test/integration/crd/client/informers/externalversions/internalinterfaces/factory_interfaces.go:
--------------------------------------------------------------------------------
1 | // Code generated by informer-gen. DO NOT EDIT.
2 |
3 | package internalinterfaces
4 |
5 | import (
6 | time "time"
7 |
8 | versioned "github.com/slok/kubewebhook/v2/test/integration/crd/client/clientset/versioned"
9 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10 | runtime "k8s.io/apimachinery/pkg/runtime"
11 | cache "k8s.io/client-go/tools/cache"
12 | )
13 |
14 | // NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer.
15 | type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer
16 |
17 | // SharedInformerFactory a small interface to allow for adding an informer without an import cycle
18 | type SharedInformerFactory interface {
19 | Start(stopCh <-chan struct{})
20 | InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer
21 | }
22 |
23 | // TweakListOptionsFunc is a function that transforms a v1.ListOptions.
24 | type TweakListOptionsFunc func(*v1.ListOptions)
25 |
--------------------------------------------------------------------------------
/test/integration/crd/client/listers/building/v1/expansion_generated.go:
--------------------------------------------------------------------------------
1 | // Code generated by lister-gen. DO NOT EDIT.
2 |
3 | package v1
4 |
5 | // HouseListerExpansion allows custom methods to be added to
6 | // HouseLister.
7 | type HouseListerExpansion interface{}
8 |
9 | // HouseNamespaceListerExpansion allows custom methods to be added to
10 | // HouseNamespaceLister.
11 | type HouseNamespaceListerExpansion interface{}
12 |
--------------------------------------------------------------------------------
/test/integration/crd/client/listers/building/v1/house.go:
--------------------------------------------------------------------------------
1 | // Code generated by lister-gen. DO NOT EDIT.
2 |
3 | package v1
4 |
5 | import (
6 | v1 "github.com/slok/kubewebhook/v2/test/integration/crd/apis/building/v1"
7 | "k8s.io/apimachinery/pkg/labels"
8 | "k8s.io/client-go/listers"
9 | "k8s.io/client-go/tools/cache"
10 | )
11 |
12 | // HouseLister helps list Houses.
13 | // All objects returned here must be treated as read-only.
14 | type HouseLister interface {
15 | // List lists all Houses in the indexer.
16 | // Objects returned here must be treated as read-only.
17 | List(selector labels.Selector) (ret []*v1.House, err error)
18 | // Houses returns an object that can list and get Houses.
19 | Houses(namespace string) HouseNamespaceLister
20 | HouseListerExpansion
21 | }
22 |
23 | // houseLister implements the HouseLister interface.
24 | type houseLister struct {
25 | listers.ResourceIndexer[*v1.House]
26 | }
27 |
28 | // NewHouseLister returns a new HouseLister.
29 | func NewHouseLister(indexer cache.Indexer) HouseLister {
30 | return &houseLister{listers.New[*v1.House](indexer, v1.Resource("house"))}
31 | }
32 |
33 | // Houses returns an object that can list and get Houses.
34 | func (s *houseLister) Houses(namespace string) HouseNamespaceLister {
35 | return houseNamespaceLister{listers.NewNamespaced[*v1.House](s.ResourceIndexer, namespace)}
36 | }
37 |
38 | // HouseNamespaceLister helps list and get Houses.
39 | // All objects returned here must be treated as read-only.
40 | type HouseNamespaceLister interface {
41 | // List lists all Houses in the indexer for a given namespace.
42 | // Objects returned here must be treated as read-only.
43 | List(selector labels.Selector) (ret []*v1.House, err error)
44 | // Get retrieves the House from the indexer for a given namespace and name.
45 | // Objects returned here must be treated as read-only.
46 | Get(name string) (*v1.House, error)
47 | HouseNamespaceListerExpansion
48 | }
49 |
50 | // houseNamespaceLister implements the HouseNamespaceLister
51 | // interface.
52 | type houseNamespaceLister struct {
53 | listers.ResourceIndexer[*v1.House]
54 | }
55 |
--------------------------------------------------------------------------------
/test/integration/crd/manifests/building.kubewebhook.slok.dev_houses.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: apiextensions.k8s.io/v1
3 | kind: CustomResourceDefinition
4 | metadata:
5 | annotations:
6 | controller-gen.kubebuilder.io/version: (devel)
7 | name: houses.building.kubewebhook.slok.dev
8 | spec:
9 | group: building.kubewebhook.slok.dev
10 | names:
11 | kind: House
12 | listKind: HouseList
13 | plural: houses
14 | singular: house
15 | scope: Namespaced
16 | versions:
17 | - name: v1
18 | schema:
19 | openAPIV3Schema:
20 | description: House represents a house.
21 | properties:
22 | apiVersion:
23 | description: |-
24 | APIVersion defines the versioned schema of this representation of an object.
25 | Servers should convert recognized schemas to the latest internal value, and
26 | may reject unrecognized values.
27 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
28 | type: string
29 | kind:
30 | description: |-
31 | Kind is a string value representing the REST resource this object represents.
32 | Servers may infer this from the endpoint the client submits requests to.
33 | Cannot be updated.
34 | In CamelCase.
35 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
36 | type: string
37 | metadata:
38 | type: object
39 | spec:
40 | description: HouseSpec is the spec for a Team resource.
41 | properties:
42 | active:
43 | type: boolean
44 | address:
45 | type: string
46 | name:
47 | type: string
48 | owners:
49 | items:
50 | description: User is an user.
51 | properties:
52 | email:
53 | type: string
54 | name:
55 | type: string
56 | required:
57 | - email
58 | - name
59 | type: object
60 | type: array
61 | x-kubernetes-list-map-keys:
62 | - name
63 | x-kubernetes-list-type: map
64 | required:
65 | - address
66 | - name
67 | type: object
68 | type: object
69 | served: true
70 | storage: true
71 |
--------------------------------------------------------------------------------
/test/integration/create-certs.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 |
4 | DOMAINS="*.tcp.ngrok.io tcp.ngrok.io"
5 |
6 | OUTPATH=./test/integration/certs
7 | OUTCERT="${OUTPATH}/cert.pem"
8 | OUTKEY="${OUTPATH}/key.pem"
9 |
10 | # Create certs for our webhook
11 | mkdir -p "${OUTPATH}"
12 | set -f
13 | mkcert \
14 | -cert-file "${OUTCERT}" \
15 | -key-file "${OUTKEY}" \
16 | ${DOMAINS}
17 | set +f
18 |
--------------------------------------------------------------------------------
/test/integration/gen-crd.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -euxo pipefail
4 |
5 | docker run -it --rm -v ${PWD}:/app ghcr.io/slok/kube-code-generator:v0.3.1 \
6 | --apis-in ./test/integration/crd/apis \
7 | --go-gen-out ./test/integration/crd/client \
8 | --crd-gen-out ./test/integration/crd/manifests
9 |
--------------------------------------------------------------------------------
/test/integration/helper/cli/cli.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "os"
7 | "path/filepath"
8 |
9 | "k8s.io/client-go/kubernetes"
10 | "k8s.io/client-go/rest"
11 | "k8s.io/client-go/tools/clientcmd"
12 | "k8s.io/client-go/util/homedir"
13 |
14 | kubewebhookcrd "github.com/slok/kubewebhook/v2/test/integration/crd/client/clientset/versioned"
15 | )
16 |
17 | // GetK8sSTDClients returns a all k8s clients.
18 | func GetK8sSTDClients(kubehome string, warningWriter io.Writer) (kubernetes.Interface, error) {
19 | // Try fallbacks.
20 | if kubehome == "" {
21 | if kubehome = os.Getenv("KUBECONFIG"); kubehome == "" {
22 | kubehome = filepath.Join(homedir.HomeDir(), ".kube", "config")
23 | }
24 | }
25 |
26 | // Load kubernetes local connection.
27 | config, err := clientcmd.BuildConfigFromFlags("", kubehome)
28 | if err != nil {
29 | return nil, fmt.Errorf("could not load configuration: %s", err)
30 | }
31 |
32 | if warningWriter != nil {
33 | config.WarningHandler = rest.NewWarningWriter(warningWriter, rest.WarningWriterOptions{Deduplicate: true})
34 | }
35 |
36 | // Get the client.
37 | k8sCli, err := kubernetes.NewForConfig(config)
38 | if err != nil {
39 | return nil, err
40 | }
41 |
42 | return k8sCli, nil
43 | }
44 |
45 | // GetK8sCRDClients returns a all k8s clients.
46 | func GetK8sCRDClients(kubehome string, warningWriter io.Writer) (kubewebhookcrd.Interface, error) {
47 | // Try fallbacks.
48 | if kubehome == "" {
49 | if kubehome = os.Getenv("KUBECONFIG"); kubehome == "" {
50 | kubehome = filepath.Join(homedir.HomeDir(), ".kube", "config")
51 | }
52 | }
53 |
54 | // Load kubernetes local connection.
55 | config, err := clientcmd.BuildConfigFromFlags("", kubehome)
56 | if err != nil {
57 | return nil, fmt.Errorf("could not load configuration: %s", err)
58 | }
59 |
60 | if warningWriter != nil {
61 | config.WarningHandler = rest.NewWarningWriter(warningWriter, rest.WarningWriterOptions{Deduplicate: true})
62 | }
63 |
64 | // Get the client.
65 | k8sCli, err := kubewebhookcrd.NewForConfig(config)
66 | if err != nil {
67 | return nil, fmt.Errorf("could not create crd client: %s", err)
68 | }
69 |
70 | return k8sCli, nil
71 | }
72 |
--------------------------------------------------------------------------------
/test/integration/helper/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "testing"
8 |
9 | "k8s.io/client-go/util/homedir"
10 | )
11 |
12 | const (
13 | envVarWebhookURL = "TEST_WEBHOOK_URL"
14 | envVarListenPort = "TEST_LISTEN_PORT"
15 | envVarCertPath = "TEST_CERT_PATH"
16 | envVarCertKeyPath = "TEST_CERT_KEY_PATH"
17 | envKubeConfig = "KUBECONFIG"
18 | )
19 |
20 | // TestEnvConfig has the integration tests environment configuration.
21 | type TestEnvConfig struct {
22 | WebhookURL string
23 | ListenAddress string
24 | KubeConfigPath string
25 | WebhookCertPath string
26 | WebhookCertKeyPath string
27 | WebhookCert string
28 | }
29 |
30 | func (c *TestEnvConfig) defaults() error {
31 | if c.WebhookCertPath == "" {
32 | c.WebhookCertPath = "../certs/cert.pem"
33 | }
34 |
35 | if c.WebhookCertKeyPath == "" {
36 | c.WebhookCertKeyPath = "../certs/key.pem"
37 | }
38 |
39 | // Load certificate data.
40 | if c.WebhookCert == "" {
41 | cert, err := os.ReadFile(c.WebhookCertPath)
42 | if err != nil {
43 | return fmt.Errorf("error loading cert: %s", err)
44 | }
45 | c.WebhookCert = string(cert)
46 | }
47 |
48 | if c.ListenAddress == "" || c.ListenAddress == ":" {
49 | c.ListenAddress = ":8080"
50 | }
51 |
52 | if c.KubeConfigPath == "" {
53 | c.KubeConfigPath = filepath.Join(homedir.HomeDir(), ".kube", "config")
54 | }
55 |
56 | // To create a local testing development env you could do:
57 | // - `kind create cluster`
58 | // - `ssh -R 0:localhost:8080 tunnel.us.ngrok.com tcp 22`
59 | // Use the `https://0.tcp.ngrok.io:17661` url style as `TEST_WEBHOOK_URL` env var.
60 | if c.WebhookURL == "" {
61 | return fmt.Errorf("webhook url is required")
62 | }
63 |
64 | return nil
65 | }
66 |
67 | // GetTestEnvConfig returns the configuration that should have the environment
68 | // so the integration tests can be run.
69 | func GetTestEnvConfig(t *testing.T) TestEnvConfig {
70 | cfg := TestEnvConfig{
71 | WebhookURL: os.Getenv(envVarWebhookURL),
72 | ListenAddress: fmt.Sprintf(":%s", os.Getenv(envVarListenPort)),
73 | KubeConfigPath: os.Getenv(envKubeConfig),
74 | WebhookCertPath: os.Getenv(envVarCertPath),
75 | WebhookCertKeyPath: os.Getenv(envVarCertKeyPath),
76 | }
77 |
78 | err := cfg.defaults()
79 | if err != nil {
80 | t.Fatalf("could not load integration tests configuration: %s", err)
81 | }
82 |
83 | return cfg
84 | }
85 |
--------------------------------------------------------------------------------
/test/integration/webhook/doc.go:
--------------------------------------------------------------------------------
1 | package webhook
2 |
--------------------------------------------------------------------------------
/test/integration/webhook/helper_test.go:
--------------------------------------------------------------------------------
1 | package webhook_test
2 |
3 | import (
4 | "testing"
5 |
6 | arv1 "k8s.io/api/admissionregistration/v1"
7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8 |
9 | helperconfig "github.com/slok/kubewebhook/v2/test/integration/helper/config"
10 | )
11 |
12 | func getValidatingWebhookConfig(t *testing.T, cfg helperconfig.TestEnvConfig, rules []arv1.RuleWithOperations, versions []string) *arv1.ValidatingWebhookConfiguration {
13 | whSideEffect := arv1.SideEffectClassNone
14 | whFailurePolicy := arv1.Fail
15 | var timeoutSecs int32 = 30
16 | return &arv1.ValidatingWebhookConfiguration{
17 | ObjectMeta: metav1.ObjectMeta{
18 | Name: "integration-test-webhook",
19 | },
20 | Webhooks: []arv1.ValidatingWebhook{
21 | {
22 | Name: "test.slok.dev",
23 | AdmissionReviewVersions: versions,
24 | FailurePolicy: &whFailurePolicy,
25 | TimeoutSeconds: &timeoutSecs,
26 | SideEffects: &whSideEffect,
27 | ClientConfig: arv1.WebhookClientConfig{
28 | URL: &cfg.WebhookURL,
29 | CABundle: []byte(cfg.WebhookCert),
30 | },
31 | Rules: rules,
32 | },
33 | },
34 | }
35 | }
36 |
37 | func getMutatingWebhookConfig(t *testing.T, cfg helperconfig.TestEnvConfig, rules []arv1.RuleWithOperations, versions []string) *arv1.MutatingWebhookConfiguration {
38 | whSideEffect := arv1.SideEffectClassNone
39 | var timeoutSecs int32 = 30
40 | return &arv1.MutatingWebhookConfiguration{
41 | ObjectMeta: metav1.ObjectMeta{
42 | Name: "integration-test-webhook",
43 | },
44 | Webhooks: []arv1.MutatingWebhook{
45 | {
46 | Name: "test.slok.dev",
47 | AdmissionReviewVersions: versions,
48 | TimeoutSeconds: &timeoutSecs,
49 | SideEffects: &whSideEffect,
50 | ClientConfig: arv1.WebhookClientConfig{
51 | URL: &cfg.WebhookURL,
52 | CABundle: []byte(cfg.WebhookCert),
53 | },
54 | Rules: rules,
55 | },
56 | },
57 | }
58 | }
59 |
60 | var (
61 | webhookRulesPod = arv1.RuleWithOperations{
62 | Operations: []arv1.OperationType{"CREATE"},
63 | Rule: arv1.Rule{
64 | APIGroups: []string{""},
65 | APIVersions: []string{"v1"},
66 | Resources: []string{"pods"},
67 | },
68 | }
69 |
70 | webhookRulesDeletePod = arv1.RuleWithOperations{
71 | Operations: []arv1.OperationType{"DELETE"},
72 | Rule: arv1.Rule{
73 | APIGroups: []string{""},
74 | APIVersions: []string{"v1"},
75 | Resources: []string{"pods"},
76 | },
77 | }
78 |
79 | webhookRulesHouseCRD = arv1.RuleWithOperations{
80 | Operations: []arv1.OperationType{"CREATE"},
81 | Rule: arv1.Rule{
82 | APIGroups: []string{"building.kubewebhook.slok.dev"},
83 | APIVersions: []string{"v1"},
84 | Resources: []string{"houses"},
85 | },
86 | }
87 | )
88 |
--------------------------------------------------------------------------------
/test/manual/certs/ca-bundle.b64:
--------------------------------------------------------------------------------
1 | LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVFekNDQW51Z0F3SUJBZ0lRZFpXSmIvQlpsMTZ4eHMrMWZ6Ulp5REFOQmdrcWhraUc5dzBCQVFzRkFEQlgKTVI0d0hBWURWUVFLRXhWdGEyTmxjblFnWkdWMlpXeHZjRzFsYm5RZ1EwRXhGakFVQmdOVkJBc01EWE5zYjJ0QQpibUYxZEdsc2RYTXhIVEFiQmdOVkJBTU1GRzFyWTJWeWRDQnpiRzlyUUc1aGRYUnBiSFZ6TUI0WERUSXdNVEl4Ck5EQTNNamt5TmxvWERUSXpNRE14TkRBM01qa3lObG93UVRFbk1DVUdBMVVFQ2hNZWJXdGpaWEowSUdSbGRtVnMKYjNCdFpXNTBJR05sY25ScFptbGpZWFJsTVJZd0ZBWURWUVFMREExemJHOXJRRzVoZFhScGJIVnpNSUlCSWpBTgpCZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUEwZjhxT1NSZ1J1RldMTkhvczBpYVB2Z3B1b0JrCm5yUzk4dDBqUzVkQ0QwZmVLL3pnRHJUb243K1dSR2pDUUFnK0NRcGN1RWloT0N4MXI0dk9wdDhtd2RmbDV6MGoKNXNzZFZIRWdUeC9TSWJYVU9LbHBqZVhMdlpULzdBKzdnbkhOUVFpbGlHUXp4b1c0OHdweTNRWWd6ZmVwRGdvcQpCSTFUWHk1b2VBUmNVRGI1eE9TVTBIOHNMN1RQOXMxbytic1RpYW91NzRKcmZqeFZBbVpIYUNsWE9LdlRocWtICm4reGpVVXZMdlEraHlVVEVmTVhyMUQ0Zkh0dFRuZWNycHRrS3c1YnAyT2c4NDNrekRCYlFkcnJ6dUpVMGs4bkQKcjNPVGExUEJ2cEZBZDZkVTJDMnRXb0RRa3dIaDFJMUgyRzBDU2dub2dNY1F1SmFJclk0Ry9BcnlGUUlEQVFBQgpvM0V3YnpBT0JnTlZIUThCQWY4RUJBTUNCYUF3RXdZRFZSMGxCQXd3Q2dZSUt3WUJCUVVIQXdFd0h3WURWUjBqCkJCZ3dGb0FVdlJzaUZmL05ZY0poOWJnT0w1aWxqc1NMY3Ywd0p3WURWUjBSQkNBd0hvSU9LaTUwWTNBdWJtZHkKYjJzdWFXK0NESFJqY0M1dVozSnZheTVwYnpBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVlFQU1zMmhSMDd3SzloNApvczBUcFQ2TFBlRHRxckZaUFZWeG1Nck4wUlk2azhjU0puMERUTmdOampJaTFFU2lhdG9FRWN4ZG8xajFGNHZwCjd5QWt1SEZkU3VtNTRna2dEekVtL05iQ0t0S0drU3VVcENKTVZBa1NiTGZiRVE5NjBuY3dVL3pHV0N4YmI2QTQKQWFGOTVUYUp0clVxTU1maWxnLytWVUFMb2tHR0hEKzhIOC93L284aWhxV2RkMFBoc2lxNmJuVVp3NE1EalZVTgpMbEgzRExXRStzZnJ4WGtBbktBODhIckhFZkFYMXpQWm5QR1c1R2JxK1ZNUnNhUmlSSDlKR3FCYzUzZzdySHJ4CmFKR282VkVFZmlBUGZ2cTRkdS9PODN2QmtQN3VkTmNSTjdOMVFQUmlFQXFUdWpaQmJkOU9tSUVIMkNVUXZPSzQKSTJWUmxqczM1dk56dHl4UEc4cE5McU1NUkxyMXFBNjd0d1YyVFE4cEJhRDNmZi9ETm9PYVJ3VTBFUnRxU2VEYQo0aXZZUXZHNWRIa1dpdlphaklQNE1GNWNKM3oyclhRNEZEY0hhZlNkQ09GMGFXaXpOcjlKY0x6M0EvWlNPUG1XCkZyd2xzS0FZZlprL3FsNTVRelJPay9NU3ZkRmxDYlBtK2xPRnhxN01QN1VvQlNybmU2RTIKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
--------------------------------------------------------------------------------
/test/manual/certs/cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIEEzCCAnugAwIBAgIQdZWJb/BZl16xxs+1fzRZyDANBgkqhkiG9w0BAQsFADBX
3 | MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExFjAUBgNVBAsMDXNsb2tA
4 | bmF1dGlsdXMxHTAbBgNVBAMMFG1rY2VydCBzbG9rQG5hdXRpbHVzMB4XDTIwMTIx
5 | NDA3MjkyNloXDTIzMDMxNDA3MjkyNlowQTEnMCUGA1UEChMebWtjZXJ0IGRldmVs
6 | b3BtZW50IGNlcnRpZmljYXRlMRYwFAYDVQQLDA1zbG9rQG5hdXRpbHVzMIIBIjAN
7 | BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0f8qOSRgRuFWLNHos0iaPvgpuoBk
8 | nrS98t0jS5dCD0feK/zgDrTon7+WRGjCQAg+CQpcuEihOCx1r4vOpt8mwdfl5z0j
9 | 5ssdVHEgTx/SIbXUOKlpjeXLvZT/7A+7gnHNQQiliGQzxoW48wpy3QYgzfepDgoq
10 | BI1TXy5oeARcUDb5xOSU0H8sL7TP9s1o+bsTiaou74JrfjxVAmZHaClXOKvThqkH
11 | n+xjUUvLvQ+hyUTEfMXr1D4fHttTnecrptkKw5bp2Og843kzDBbQdrrzuJU0k8nD
12 | r3OTa1PBvpFAd6dU2C2tWoDQkwHh1I1H2G0CSgnogMcQuJaIrY4G/AryFQIDAQAB
13 | o3EwbzAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0j
14 | BBgwFoAUvRsiFf/NYcJh9bgOL5iljsSLcv0wJwYDVR0RBCAwHoIOKi50Y3Aubmdy
15 | b2suaW+CDHRjcC5uZ3Jvay5pbzANBgkqhkiG9w0BAQsFAAOCAYEAMs2hR07wK9h4
16 | os0TpT6LPeDtqrFZPVVxmMrN0RY6k8cSJn0DTNgNjjIi1ESiatoEEcxdo1j1F4vp
17 | 7yAkuHFdSum54gkgDzEm/NbCKtKGkSuUpCJMVAkSbLfbEQ960ncwU/zGWCxbb6A4
18 | AaF95TaJtrUqMMfilg/+VUALokGGHD+8H8/w/o8ihqWdd0Phsiq6bnUZw4MDjVUN
19 | LlH3DLWE+sfrxXkAnKA88HrHEfAX1zPZnPGW5Gbq+VMRsaRiRH9JGqBc53g7rHrx
20 | aJGo6VEEfiAPfvq4du/O83vBkP7udNcRN7N1QPRiEAqTujZBbd9OmIEH2CUQvOK4
21 | I2VRljs35vNztyxPG8pNLqMMRLr1qA67twV2TQ8pBaD3ff/DNoOaRwU0ERtqSeDa
22 | 4ivYQvG5dHkWivZajIP4MF5cJ3z2rXQ4FDcHafSdCOF0aWizNr9JcLz3A/ZSOPmW
23 | FrwlsKAYfZk/ql55QzROk/MSvdFlCbPm+lOFxq7MP7UoBSrne6E2
24 | -----END CERTIFICATE-----
25 |
--------------------------------------------------------------------------------
/test/manual/certs/key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDR/yo5JGBG4VYs
3 | 0eizSJo++Cm6gGSetL3y3SNLl0IPR94r/OAOtOifv5ZEaMJACD4JCly4SKE4LHWv
4 | i86m3ybB1+XnPSPmyx1UcSBPH9IhtdQ4qWmN5cu9lP/sD7uCcc1BCKWIZDPGhbjz
5 | CnLdBiDN96kOCioEjVNfLmh4BFxQNvnE5JTQfywvtM/2zWj5uxOJqi7vgmt+PFUC
6 | ZkdoKVc4q9OGqQef7GNRS8u9D6HJRMR8xevUPh8e21Od5yum2QrDlunY6DzjeTMM
7 | FtB2uvO4lTSTycOvc5NrU8G+kUB3p1TYLa1agNCTAeHUjUfYbQJKCeiAxxC4loit
8 | jgb8CvIVAgMBAAECggEACLJCc19YRVcrlGuU8We+S4FHaRvMDu55N0eFIKpA6BUX
9 | 1EaCmNlRENyEQoz8Dl7JAuLU+CS52HOu4/gsNKjlF/3y3WKgy/v5WPfeWKh+sTqw
10 | cTBC2Md9anpzJrl4EGzaDSlogX90zXHYOOhj3VdVoHHzJEuzdcDMhRKM7PtxPzkU
11 | X2Ki11Lcu8dbX3ONOriDMNH8cnaoztzuJqLcqkbL5OBtjOnu1PaKE207iYuAK65G
12 | NscqDtpV3bIYRGcOU9yPDXbP25BfgT03pWvaim+sEkL/ZdlQwIuqqTjh/vXVO5Rs
13 | qjRaBlNURAnF5ZgGbcbTy5cpLRZJBngarUZjaAV0wQKBgQDbjQIc4wC9POfynxn4
14 | hKQHy/7R6Z85xcmNHJu3aEHp1KAafhRmfADPJDgOCVyreeCh/M6suwKPID8jo2fD
15 | 7dYA3icGjGdSwVVvtcPBQUUy7WPcguHJ+AweL5JtaINnC/rin0yE1rEoxlsTIBtS
16 | 4dFWRj8/26M4NlwWvG21U12ZjQKBgQD03BramKDzgotLQAhEqiAcByHn0/qn3IFm
17 | Z91+WP+PAGhPc2fxa9sRcZ9w2PRDKyDLz0IP7yBUMGrz8MBryVdHlUXh8Vz4UZya
18 | 59JnF9JVRMWn+XIzRKogcfDxf2KJHEigYejfEMtzCWTJGuPmdLKKBKULeZzCvhzo
19 | SO/1vzvkqQKBgQC1RfzLmwY+OS6N3Z2U2veQVHdmHA3SpqLedxN+4H8jsOIsXZM8
20 | dwA4B4Cc3k/8aRn2xYRji9j2EbrwEvgXBqWee84fEwgwhN6k6J/jTZ/0B5tfM1V3
21 | 6+0dc0vkN9ne2D5ipQKJ37XQo84IwYat7TNpl4Cbmh5uyDtKrnOqtlVe9QKBgDaa
22 | yyZKeUI0r4mVewKNCeIWORpw7Gn4w6aprPxbLoqeZaSqMNjm25C1TQAmcbp06Lhq
23 | vOm+wu/jaEaEPvUKjns5L79mSvxZftoQrpws9MvtLUL7XttOCb54imngpYG8G/og
24 | O4VbQnOh+abbZ01iYtpilMfNVAcdzGaEtXaRMfMZAoGAJ48gQ4Whup99Zf7m4PWj
25 | 50/wENug4ZLKivZhfZ8zsqaPpOJ6GhHGY3QRzoGO6zJEgsRZCaoGDzzxyaAOexWm
26 | D4n2XGliReFkyi8znk6Z2eSZ/NGvXspJWLp50th5YG+v4hdDKHuw8vbHxjph/WYA
27 | IN89/OMm9U+9x+EN3pdpyhg=
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/test/manual/create-certs.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 |
4 | DOMAINS="*.tcp.ngrok.io tcp.ngrok.io"
5 |
6 | OUTPATH=./test/manual/certs
7 | OUTCERT="${OUTPATH}/cert.pem"
8 | OUTKEY="${OUTPATH}/key.pem"
9 | OUTCABUNDLE64="${OUTPATH}/ca-bundle.b64"
10 |
11 | # Create certs for our webhook
12 | mkdir -p "${OUTPATH}"
13 | set -f
14 | mkcert \
15 | -cert-file "${OUTCERT}" \
16 | -key-file "${OUTKEY}" \
17 | ${DOMAINS}
18 | set +f
19 |
20 | echo "Get webhook config CABundle from "${OUTCABUNDLE64}""
21 | echo -n $(cat ${OUTCERT} | base64 -w0) > "${OUTCABUNDLE64}"
--------------------------------------------------------------------------------
/test/manual/ns.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Namespace
3 | metadata:
4 | name: kubewebhook
5 | labels:
6 | kubewebhook: test
7 |
--------------------------------------------------------------------------------
/test/manual/test.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: nginx-test
5 | namespace: kubewebhook
6 | spec:
7 | replicas: 10
8 | selector:
9 | matchLabels:
10 | app: nginx
11 | template:
12 | metadata:
13 | labels:
14 | app: nginx
15 | spec:
16 | containers:
17 | - name: nginx
18 | image: nginx
--------------------------------------------------------------------------------
/test/manual/test2.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: nginx-min-test
5 | namespace: kubewebhook
6 | spec:
7 | replicas: 0
8 | selector:
9 | matchLabels:
10 | app: nginx
11 | template:
12 | metadata:
13 | labels:
14 | app: nginx
15 | spec:
16 | containers:
17 | - name: nginx
18 | image: nginx
19 | ---
20 | apiVersion: apps/v1
21 | kind: Deployment
22 | metadata:
23 | name: nginx-max-test
24 | namespace: kubewebhook
25 | spec:
26 | replicas: 15
27 | selector:
28 | matchLabels:
29 | app: nginx
30 | template:
31 | metadata:
32 | labels:
33 | app: nginx
34 | spec:
35 | containers:
36 | - name: nginx
37 | image: nginx
38 |
--------------------------------------------------------------------------------