├── .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 | 3 | 4 | kubewebhook_logo 5 | Created with Sketch. 6 | 7 | 15 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------