Repository: slackhq/simple-kubernetes-webhook Branch: main Commit: 10254f1be879 Files: 38 Total size: 83.0 KB Directory structure: gitextract_3a9pmxal/ ├── .github/ │ ├── CODE_OF_CONDUCT.md │ ├── CONTRIBUTING.md │ ├── ISSUE_TEMPLATE.md │ ├── PULL_REQUEST_TEMPLATE.md │ └── maintainers_guide.md ├── CODEOWNERS ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── dev/ │ ├── gen-certs.sh │ └── manifests/ │ ├── cluster-config/ │ │ ├── apps.ns.yaml │ │ ├── mutating.config.yaml │ │ └── validating.config.yaml │ ├── kind/ │ │ └── kind.cluster.yaml │ ├── pods/ │ │ ├── bad-name.pod.yaml │ │ ├── lifespan-seven.pod.yaml │ │ ├── lifespan-three.pod.yaml │ │ ├── no-lifespan-label.deploy.yaml │ │ └── no-lifespan-label.pod.yaml │ └── webhook/ │ ├── webhook.deploy.yaml │ ├── webhook.svc.yaml │ └── webhook.tls.secret.yaml ├── go.mod ├── go.sum ├── main.go └── pkg/ ├── admission/ │ ├── admission.go │ └── admission_test.go ├── mutation/ │ ├── inject_env.go │ ├── inject_env_test.go │ ├── minimum_lifespan.go │ ├── minimum_lifespan_test.go │ ├── mutation.go │ └── mutation_test.go └── validation/ ├── name_validator.go ├── name_validator_test.go ├── validation.go └── validation_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ # Code of Conduct ## Introduction Diversity and inclusion make our community strong. We encourage participation from the most varied and diverse backgrounds possible and want to be very clear about where we stand. Our goal is to maintain a safe, helpful and friendly community for everyone, regardless of experience, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other defining characteristic. This code and related procedures also apply to unacceptable behavior occurring outside the scope of community activities, in all community venues (online and in-person) as well as in all one-on-one communications, and anywhere such behavior has the potential to adversely affect the safety and well-being of community members. For more information on our code of conduct, please visit [https://slackhq.github.io/code-of-conduct](https://slackhq.github.io/code-of-conduct) ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributors Guide Interested in contributing? Awesome! Before you do though, please read our [Code of Conduct](https://slackhq.github.io/code-of-conduct). We take it very seriously, and expect that you will as well. There are many ways you can contribute! :heart: ### Bug Reports and Fixes :bug: - If you find a bug, please search for it in the [Issues](https://github.com/slackhq/simple-kubernetes-webhook/issues), and if it isn't already tracked, [create a new issue](https://github.com/slackhq/simple-kubernetes-webhook/issues/new). Fill out the "Bug Report" section of the issue template. Even if an Issue is closed, feel free to comment and add details, it will still be reviewed. - Issues that have already been identified as a bug (note: able to reproduce) will be labelled `bug`. - If you'd like to submit a fix for a bug, [send a Pull Request](#creating_a_pull_request) and mention the Issue number. - Include tests that isolate the bug and verifies that it was fixed. ### New Features :bulb: - If you'd like to add new functionality to this project, describe the problem you want to solve in a [new Issue](https://github.com/slackhq/simple-kubernetes-webhook/issues/new). - Issues that have been identified as a feature request will be labelled `enhancement`. - If you'd like to implement the new feature, please wait for feedback from the project maintainers before spending too much time writing the code. In some cases, `enhancement`s may not align well with the project objectives at the time. ### Tests :mag:, Documentation :books:, Miscellaneous :sparkles: - If you'd like to improve the tests, you want to make the documentation clearer, you have an alternative implementation of something that may have advantages over the way its currently done, or you have any other change, we would be happy to hear about it! - If its a trivial change, go ahead and [send a Pull Request](#creating_a_pull_request) with the changes you have in mind. - If not, [open an Issue](https://github.com/slackhq/simple-kubernetes-webhook/issues/new) to discuss the idea first. If you're new to our project and looking for some way to make your first contribution, look for Issues labelled `good first contribution`. ## Requirements For your contribution to be accepted: - [x] You must have signed the [Contributor License Agreement (CLA)](https://cla-assistant.io/slackhq/simple-kubernetes-webhook). - [x] The test suite must be complete and pass. - [x] The changes must be approved by code review. - [x] Commits should be atomic and messages must be descriptive. Related issues should be mentioned by Issue number. If the contribution doesn't meet the above criteria, you may fail our automated checks or a maintainer will discuss it with you. You can continue to improve a Pull Request by adding commits to the branch from which the PR was created. [Interested in knowing more about about pull requests at Slack?](https://slack.engineering/on-empathy-pull-requests-979e4257d158#.awxtvmb2z) ## Creating a Pull Request 1. :fork_and_knife: Fork the repository on GitHub. 2. :runner: Clone/fetch your fork to your local development machine. It's a good idea to run the tests just to make sure everything is in order. 3. :herb: Create a new branch and check it out. 4. :crystal_ball: Make your changes and commit them locally. Magic happens here! 5. :arrow_heading_up: Push your new branch to your fork. (e.g. `git push username fix-issue-16`). 6. :inbox_tray: Open a Pull Request on github.com from your new branch on your fork to `master` in this repository. ## Maintainers There are more details about processes and workflow in the [Maintainer's Guide](./maintainers_guide.md). ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ ### Description Describe your issue here. ### What type of issue is this? (place an `x` in one of the `[ ]`) - [ ] bug - [ ] enhancement (feature request) - [ ] question - [ ] documentation related - [ ] testing related - [ ] discussion ### Requirements (place an `x` in each of the `[ ]`) * [ ] I've read and understood the [Contributing guidelines](https://github.com/slackhq/simple-kubernetes-webhook/blob/master/.github/contributing.md) and have done my best effort to follow them. * [ ] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct). * [ ] I've searched for any related issues and avoided creating a duplicate issue. --- ### Bug Report Filling out the following details about bugs will help us solve your issue sooner. #### Reproducible in: simple-kubernetes-webhook version: Go version: OS version(s): #### Steps to reproduce: 1. 2. 3. #### Expected result: What you expected to happen #### Actual result: What actually happened #### Attachments: Logs, screenshots, screencast, sample project, funny gif, etc. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### Summary Describe the goal of this PR. Mention any related Issue numbers. ### Requirements (place an `x` in each `[ ]`) * [ ] I've read and understood the [Contributing Guidelines](https://github.com/slackhq/simple-kubernetes-webhook/blob/master/.github/contributing.md) and have done my best effort to follow them. * [ ] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct). > The following point can be removed after setting up CI (such as Travis) with coverage reports (such as Codecov) * [ ] I've written tests to cover the new code and functionality included in this PR. > The following point can be removed after setting up a CLA reporting tool such as cla-assistant.io * [ ] I've read, agree to, and signed the [Contributor License Agreement (CLA)](https://cla-assistant.io/slackhq/simple-kubernetes-webhook). ================================================ FILE: .github/maintainers_guide.md ================================================ # Maintainers Guide This document describes tools, tasks and workflow that one needs to be familiar with in order to effectively maintain this project. If you use this package within your own software as is but don't plan on modifying it, this guide is **not** for you. ## Tools (optional) Tools, dependencies, or other programs someone maintaining this project needs to be familiar with: * Kubernetes * Go * Docker * Kind ## Tasks ### Testing Unit can be run like so: ``` ❯ go test ./... ? github.com/slackhq/simple-kubernetes-webhook [no test files] ok github.com/slackhq/simple-kubernetes-webhook/pkg/admission 0.743s ok github.com/slackhq/simple-kubernetes-webhook/pkg/mutation 1.065s ok github.com/slackhq/simple-kubernetes-webhook/pkg/validation 0.413s ``` ### TLS certificate Kubernetes only allows admission webhooks running with `https`. To generate a TLS secret, run [`./dev/gen-certs.sh`](/dev/gen-certs.sh). The base64 caBundle needs to be manually copied and pasted in the `MutatingWebhookConfiguration` and `ValidattingWebhookConfiguration` at [`./dev/manifests/cluster-config/`](./dev/manifests/cluster-config/) ### Logs The logs level defaults to `debug` and can be set with the env var: ``` LOG_LEVEL=info ``` The logs format defaults to `text` and can be set to `json` with the env var: ``` LOG_JSON=true ``` ### Releasing N/A: this demo project is not released ## Workflow ### Versioning and Tags N/A: this demo project is not released ### Branches The `main` branch is where active development occurs, feel free to name your feature / bug fix branch what your heart desires. ### Issue Management Labels are used to run issues through an organized workflow. Here are the basic definitions: * `bug`: A confirmed bug report. A bug is considered confirmed when reproduction steps have been documented and the issue has been reproduced. * `enhancement`: A feature request for something this package might not already do. * `docs`: An issue that is purely about documentation work. * `tests`: An issue that is purely about testing work. * `needs feedback`: An issue that may have claimed to be a bug but was not reproducible, or was otherwise missing some information. * `discussion`: An issue that is purely meant to hold a discussion. Typically the maintainers are looking for feedback in this issues. * `question`: An issue that is like a support request because the user's usage was not correct. * `semver:major|minor|patch`: Metadata about how resolving this issue would affect the version number. * `security`: An issue that has special consideration for security reasons. * `good first contribution`: An issue that has a well-defined relatively-small scope, with clear expectations. It helps when the testing approach is also known. * `duplicate`: An issue that is functionally the same as another issue. Apply this only if you've linked the other issue by number. > You may want to add more labels for subsystems of your project, depending on how complex it is. **Triage** is the process of taking new issues that aren't yet "seen" and marking them with a basic level of information with labels. An issue should have **one** of the following labels applied: `bug`, `enhancement`, `question`, `needs feedback`, `docs`, `tests`, or `discussion`. Issues are closed when a resolution has been reached. If for any reason a closed issue seems relevant once again, reopening is great and better than creating a duplicate issue. ## Everything else When in doubt, find the other maintainers and ask. ================================================ FILE: CODEOWNERS ================================================ # Comment line immediately above ownership line is reserved for related other information. Please be careful while editing. #ECCN:Open Source #GUSINFO:Open Source,Open Source Workflow ================================================ FILE: Dockerfile ================================================ # syntax=docker/dockerfile:experimental # --- FROM golang:1.16 AS build ENV GOOS=linux ENV GOARCH=amd64 ENV CGO_ENABLED=0 WORKDIR /work COPY . /work # Build admission-webhook RUN --mount=type=cache,target=/root/.cache/go-build,sharing=private \ go build -o bin/admission-webhook . # --- FROM scratch AS run COPY --from=build /work/bin/admission-webhook /usr/local/bin/ CMD ["admission-webhook"] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) Slack Technologies, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ .PHONY: test test: @echo "\n🛠️ Running unit tests..." go test ./... .PHONY: build build: @echo "\n🔧 Building Go binaries..." GOOS=darwin GOARCH=amd64 go build -o bin/admission-webhook-darwin-amd64 . GOOS=linux GOARCH=amd64 go build -o bin/admission-webhook-linux-amd64 . .PHONY: docker-build docker-build: @echo "\n📦 Building simple-kubernetes-webhook Docker image..." docker build -t simple-kubernetes-webhook:latest . # From this point `kind` is required .PHONY: cluster cluster: @echo "\n🔧 Creating Kubernetes cluster..." kind create cluster --config dev/manifests/kind/kind.cluster.yaml .PHONY: delete-cluster delete-cluster: @echo "\n♻️ Deleting Kubernetes cluster..." kind delete cluster .PHONY: push push: docker-build @echo "\n📦 Pushing admission-webhook image into Kind's Docker daemon..." kind load docker-image simple-kubernetes-webhook:latest .PHONY: deploy-config deploy-config: @echo "\n⚙️ Applying cluster config..." kubectl apply -f dev/manifests/cluster-config/ .PHONY: delete-config delete-config: @echo "\n♻️ Deleting Kubernetes cluster config..." kubectl delete -f dev/manifests/cluster-config/ .PHONY: deploy deploy: push delete deploy-config @echo "\n🚀 Deploying simple-kubernetes-webhook..." kubectl apply -f dev/manifests/webhook/ .PHONY: delete delete: @echo "\n♻️ Deleting simple-kubernetes-webhook deployment if existing..." kubectl delete -f dev/manifests/webhook/ || true .PHONY: pod pod: @echo "\n🚀 Deploying test pod..." kubectl apply -f dev/manifests/pods/lifespan-seven.pod.yaml .PHONY: delete-pod delete-pod: @echo "\n♻️ Deleting test pod..." kubectl delete -f dev/manifests/pods/lifespan-seven.pod.yaml .PHONY: bad-pod bad-pod: @echo "\n🚀 Deploying \"bad\" pod..." kubectl apply -f dev/manifests/pods/bad-name.pod.yaml .PHONY: delete-bad-pod delete-bad-pod: @echo "\n🚀 Deleting \"bad\" pod..." kubectl delete -f dev/manifests/pods/bad-name.pod.yaml .PHONY: taint taint: @echo "\n🎨 Taining Kubernetes node.." kubectl taint nodes kind-control-plane "acme.com/lifespan-remaining"=4:NoSchedule .PHONY: logs logs: @echo "\n🔍 Streaming simple-kubernetes-webhook logs..." kubectl logs -l app=simple-kubernetes-webhook -f .PHONY: delete-all delete-all: delete delete-config delete-pod delete-bad-pod ================================================ FILE: README.md ================================================ # simple-kubernetes-webhook This is a simple [Kubernetes admission webhook](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/). It is meant to be used as a validating and mutating admission webhook only and does not support any controller logic. It has been developed as a simple Go web service without using any framework or boilerplate such as kubebuilder. This project is aimed at illustrating how to build a fully functioning admission webhook in the simplest way possible. Most existing examples found on the web rely on heavy machinery using powerful frameworks, yet fail to illustrate how to implement a lightweight webhook that can do much needed actions such as rejecting a pod for compliance reasons, or inject helpful environment variables. For readability, this project has been stripped of the usual production items such as: observability instrumentation, release scripts, redundant deployment configurations, etc. As such, it is not meant to use as-is in a production environment. This project is, in fact, a simplified fork of a system used accross all Kubernetes production environments at Slack. ## Installation This project can fully run locally and includes automation to deploy a local Kubernetes cluster (using Kind). ### Requirements * Docker * kubectl * [Kind](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) * Go >=1.16 (optional) ## Usage ### Create Cluster First, we need to create a Kubernetes cluster: ``` ❯ make cluster 🔧 Creating Kubernetes cluster... kind create cluster --config dev/manifests/kind/kind.cluster.yaml Creating cluster "kind" ... ✓ Ensuring node image (kindest/node:v1.21.1) 🖼 ✓ Preparing nodes 📦 ✓ Writing configuration 📜 ✓ Starting control-plane 🕹️ ✓ Installing CNI 🔌 ✓ Installing StorageClass 💾 Set kubectl context to "kind-kind" You can now use your cluster with: kubectl cluster-info --context kind-kind Have a nice day! 👋 ``` Make sure that the Kubernetes node is ready: ``` ❯ kubectl get nodes NAME STATUS ROLES AGE VERSION kind-control-plane Ready control-plane,master 3m25s v1.21.1 ``` And that system pods are running happily: ``` ❯ kubectl -n kube-system get pods NAME READY STATUS RESTARTS AGE coredns-558bd4d5db-thwvj 1/1 Running 0 3m39s coredns-558bd4d5db-w85ks 1/1 Running 0 3m39s etcd-kind-control-plane 1/1 Running 0 3m56s kindnet-84slq 1/1 Running 0 3m40s kube-apiserver-kind-control-plane 1/1 Running 0 3m54s kube-controller-manager-kind-control-plane 1/1 Running 0 3m56s kube-proxy-4h6sj 1/1 Running 0 3m40s kube-scheduler-kind-control-plane 1/1 Running 0 3m54s ``` ### Deploy Admission Webhook To configure the cluster to use the admission webhook and to deploy said webhook, simply run: ``` ❯ make deploy 📦 Building simple-kubernetes-webhook Docker image... docker build -t simple-kubernetes-webhook:latest . [+] Building 14.3s (13/13) FINISHED ... 📦 Pushing admission-webhook image into Kind's Docker daemon... kind load docker-image simple-kubernetes-webhook:latest Image: "simple-kubernetes-webhook:latest" with ID "sha256:46b8603bcc11a8fa1825190d3ed99c099096395b22a709e13ec6e7ae2f54014d" not yet present on node "kind-control-plane", loading... ⚙️ Applying cluster config... kubectl apply -f dev/manifests/cluster-config/ namespace/apps created mutatingwebhookconfiguration.admissionregistration.k8s.io/simple-kubernetes-webhook.acme.com created validatingwebhookconfiguration.admissionregistration.k8s.io/simple-kubernetes-webhook.acme.com created 🚀 Deploying simple-kubernetes-webhook... kubectl apply -f dev/manifests/webhook/ deployment.apps/simple-kubernetes-webhook created service/simple-kubernetes-webhook created secret/simple-kubernetes-webhook-tls created ``` Then, make sure the admission webhook pod is running (in the `default` namespace): ``` ❯ kubectl get pods NAME READY STATUS RESTARTS AGE simple-kubernetes-webhook-77444566b7-wzwmx 1/1 Running 0 2m21s ``` You can stream logs from it: ``` ❯ make logs 🔍 Streaming simple-kubernetes-webhook logs... kubectl logs -l app=simple-kubernetes-webhook -f time="2021-09-03T04:59:10Z" level=info msg="Listening on port 443..." time="2021-09-03T05:02:21Z" level=debug msg=healthy uri=/health ``` And hit it's health endpoint from your local machine: ``` ❯ curl -k https://localhost:8443/health OK ``` ### Deploying pods Deploy a valid test pod that gets succesfully created: ``` ❯ make pod 🚀 Deploying test pod... kubectl apply -f dev/manifests/pods/lifespan-seven.pod.yaml pod/lifespan-seven created ``` You should see in the admission webhook logs that the pod got mutated and validated. Deploy a non valid pod that gets rejected: ``` ❯ make bad-pod 🚀 Deploying "bad" pod... kubectl apply -f dev/manifests/pods/bad-name.pod.yaml Error from server: error when creating "dev/manifests/pods/bad-name.pod.yaml": admission webhook "simple-kubernetes-webhook.acme.com" denied the request: pod name contains "offensive" ``` You should see in the admission webhook logs that the pod validation failed. It's possible you will also see that the pod was mutated, as webhook configurations are not ordered. ## Testing Unit tests can be run with the following command: ``` $ make test go test ./... ? github.com/slackhq/simple-kubernetes-webhook [no test files] ok github.com/slackhq/simple-kubernetes-webhook/pkg/admission 0.611s ok github.com/slackhq/simple-kubernetes-webhook/pkg/mutation 1.064s ok github.com/slackhq/simple-kubernetes-webhook/pkg/validation 0.749s ``` ## Admission Logic A set of validations and mutations are implemented in an extensible framework. Those happen on the fly when a pod is deployed and no further resources are tracked and updated (ie. no controller logic). ### Validating Webhooks #### Implemented - [name validation](pkg/validation/name_validator.go): validates that a pod name doesn't contain any offensive string #### How to add a new pod validation To add a new pod mutation, create a file `pkg/validation/MUTATION_NAME.go`, then create a new struct implementing the `validation.podValidator` interface. ### Mutating Webhooks #### Implemented - [inject env](pkg/mutation/inject_env.go): inject environment variables into the pod such as `KUBE: true` - [minimum pod lifespan](pkg/mutation/minimum_lifespan.go): inject a set of tolerations used to match pods to nodes of a certain age, the tolerations injected are controlled via the `acme.com/lifespan-requested` pod label. #### How to add a new pod mutation To add a new pod mutation, create a file `pkg/mutation/MUTATION_NAME.go`, then create a new struct implementing the `mutation.podMutator` interface. ================================================ FILE: dev/gen-certs.sh ================================================ #!/bin/bash openssl genrsa -out ca.key 2048 openssl req -new -x509 -days 365 -key ca.key \ -subj "/C=AU/CN=simple-kubernetes-webhook"\ -out ca.crt openssl req -newkey rsa:2048 -nodes -keyout server.key \ -subj "/C=AU/CN=simple-kubernetes-webhook" \ -out server.csr openssl x509 -req \ -extfile <(printf "subjectAltName=DNS:simple-kubernetes-webhook.default.svc") \ -days 365 \ -in server.csr \ -CA ca.crt -CAkey ca.key -CAcreateserial \ -out server.crt echo echo ">> Generating kube secrets..." kubectl create secret tls simple-kubernetes-webhook-tls \ --cert=server.crt \ --key=server.key \ --dry-run=client -o yaml \ > ./manifests/webhook/webhook.tls.secret.yaml echo echo ">> MutatingWebhookConfiguration caBundle:" cat ca.crt | base64 | fold rm ca.crt ca.key ca.srl server.crt server.csr server.key ================================================ FILE: dev/manifests/cluster-config/apps.ns.yaml ================================================ apiVersion: v1 kind: Namespace metadata: name: apps labels: admission-webhook: enabled ================================================ FILE: dev/manifests/cluster-config/mutating.config.yaml ================================================ apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: name: "simple-kubernetes-webhook.acme.com" webhooks: - name: "simple-kubernetes-webhook.acme.com" namespaceSelector: matchLabels: admission-webhook: enabled rules: - apiGroups: [""] apiVersions: ["v1"] operations: ["CREATE"] resources: ["pods"] scope: "*" clientConfig: service: namespace: default name: simple-kubernetes-webhook path: /mutate-pods port: 443 caBundle: | LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMzakNDQWNZQ0NRRFlHcU05a0ZZUjJqQU5CZ2tx aGtpRzl3MEJBUXNGQURBeE1Rc3dDUVlEVlFRR0V3SkIKVlRFaU1DQUdBMVVFQXd3WmMybHRjR3hsTFd0 MVltVnlibVYwWlhNdGQyVmlhRzl2YXpBZUZ3MHlNVEV3TVRRdwpPREEyTkRCYUZ3MHlNakV3TVRRd09E QTJOREJhTURFeEN6QUpCZ05WQkFZVEFrRlZNU0l3SUFZRFZRUUREQmx6CmFXMXdiR1V0YTNWaVpYSnVa WFJsY3kxM1pXSm9iMjlyTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEEKTUlJQkNnS0NBUUVB M1piR3NzSk9GZ2JkTlBDMUJjZVdaeGN4RDVoRkc0M0YxTXRwTXdzeDUrTFlJejQ3M0pPTgo0RGh6Snlr V3huTVZEOEd4UElYYzNWUGNsVHp0V3dvdjdyOVo4dUxDRWdFakwyRWJFbjBKVzVTK2s2NkYwK0ZaCjI1 Y1lQNWVqMjVOd1Iwb3ZpbU9VZUpFelcyQktCT3ZGTTlPcmlhN0tkYkdRTWxRSkVFK3JMNXQxYWZmamhu SVEKdk80MFZwblBFMkQvdmZzaTlEdmVyaTZFOFc2OWJxMEJ4NXRkZUZBalN1Q0FOWldLNjhjOEhIQ3Er U3FjQ2ZaeAp5YVRmd09xQmsvYWkrMGE3a0RpUXRELzBiY0xyNkRnS3ZkckxRSmZveUlidHE1SklMamtu U2VhNFJPazRMYS9xCmN3KytpNFZpVWtOS3pUSTVUWWV0c0NKWDFhZFdBMXYvQ3dJREFRQUJNQTBHQ1Nx R1NJYjNEUUVCQ3dVQUE0SUIKQVFEWXMrNDRuWFc0STZLeSs2VGlGVjZveTErc3lMN2pFNlVONE1oM1JD eWY4Y1Q0MEVBM3VEcTlZYjVmK3BySQpMbXZpd2RLbm1CbzhHR24zN1N1YWNtYmdMOUlxVlJUZ0hlSGZw dElsblMwRklDNFVlM1hKOVRxSkNqbDBGbjgyCm9jK05FSytITjNkcldyMjMrdnZObnVlRzI4djhNenpD V2JjZk9pd0I1TGQxZ0RDbEhIc2RhSHpJZFVjdkk1dGUKbFdzM3U0aXFyYkJDdWFUOWV6OUk5RTdqdHdr R0hwVVpFV2tiNVhLcEt4SlNXQVRyWm5sTGRtTWxDb2FqM2grawpvbkNSd3R6L2d1aFc3dVJaWlQ4NGtE MS9SWGo5d3VySE4zZ1NsVDAyVkhFeHpFUUoxM21aVS82V2p3dE05NWVmCmt6NzZiY2VoR05MU0hPU2lE U1V5b0tBUQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== admissionReviewVersions: ["v1"] sideEffects: None timeoutSeconds: 2 ================================================ FILE: dev/manifests/cluster-config/validating.config.yaml ================================================ apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: name: "simple-kubernetes-webhook.acme.com" webhooks: - name: "simple-kubernetes-webhook.acme.com" namespaceSelector: matchLabels: admission-webhook: enabled rules: - apiGroups: [""] apiVersions: ["v1"] operations: ["CREATE"] resources: ["pods"] scope: "*" clientConfig: service: namespace: default name: simple-kubernetes-webhook path: /validate-pods port: 443 caBundle: | LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMzakNDQWNZQ0NRRFlHcU05a0ZZUjJqQU5CZ2tx aGtpRzl3MEJBUXNGQURBeE1Rc3dDUVlEVlFRR0V3SkIKVlRFaU1DQUdBMVVFQXd3WmMybHRjR3hsTFd0 MVltVnlibVYwWlhNdGQyVmlhRzl2YXpBZUZ3MHlNVEV3TVRRdwpPREEyTkRCYUZ3MHlNakV3TVRRd09E QTJOREJhTURFeEN6QUpCZ05WQkFZVEFrRlZNU0l3SUFZRFZRUUREQmx6CmFXMXdiR1V0YTNWaVpYSnVa WFJsY3kxM1pXSm9iMjlyTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEEKTUlJQkNnS0NBUUVB M1piR3NzSk9GZ2JkTlBDMUJjZVdaeGN4RDVoRkc0M0YxTXRwTXdzeDUrTFlJejQ3M0pPTgo0RGh6Snlr V3huTVZEOEd4UElYYzNWUGNsVHp0V3dvdjdyOVo4dUxDRWdFakwyRWJFbjBKVzVTK2s2NkYwK0ZaCjI1 Y1lQNWVqMjVOd1Iwb3ZpbU9VZUpFelcyQktCT3ZGTTlPcmlhN0tkYkdRTWxRSkVFK3JMNXQxYWZmamhu SVEKdk80MFZwblBFMkQvdmZzaTlEdmVyaTZFOFc2OWJxMEJ4NXRkZUZBalN1Q0FOWldLNjhjOEhIQ3Er U3FjQ2ZaeAp5YVRmd09xQmsvYWkrMGE3a0RpUXRELzBiY0xyNkRnS3ZkckxRSmZveUlidHE1SklMamtu U2VhNFJPazRMYS9xCmN3KytpNFZpVWtOS3pUSTVUWWV0c0NKWDFhZFdBMXYvQ3dJREFRQUJNQTBHQ1Nx R1NJYjNEUUVCQ3dVQUE0SUIKQVFEWXMrNDRuWFc0STZLeSs2VGlGVjZveTErc3lMN2pFNlVONE1oM1JD eWY4Y1Q0MEVBM3VEcTlZYjVmK3BySQpMbXZpd2RLbm1CbzhHR24zN1N1YWNtYmdMOUlxVlJUZ0hlSGZw dElsblMwRklDNFVlM1hKOVRxSkNqbDBGbjgyCm9jK05FSytITjNkcldyMjMrdnZObnVlRzI4djhNenpD V2JjZk9pd0I1TGQxZ0RDbEhIc2RhSHpJZFVjdkk1dGUKbFdzM3U0aXFyYkJDdWFUOWV6OUk5RTdqdHdr R0hwVVpFV2tiNVhLcEt4SlNXQVRyWm5sTGRtTWxDb2FqM2grawpvbkNSd3R6L2d1aFc3dVJaWlQ4NGtE MS9SWGo5d3VySE4zZ1NsVDAyVkhFeHpFUUoxM21aVS82V2p3dE05NWVmCmt6NzZiY2VoR05MU0hPU2lE U1V5b0tBUQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== admissionReviewVersions: ["v1"] sideEffects: None timeoutSeconds: 2 ================================================ FILE: dev/manifests/kind/kind.cluster.yaml ================================================ kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 nodes: - role: control-plane image: kindest/node:v1.21.1 extraPortMappings: - containerPort: 30100 hostPort: 8443 listenAddress: "0.0.0.0" protocol: TCP ================================================ FILE: dev/manifests/pods/bad-name.pod.yaml ================================================ apiVersion: v1 kind: Pod metadata: name: offensive-pod namespace: apps spec: containers: - args: - sleep - "3600" image: busybox name: lifespan-offensive restartPolicy: Always ================================================ FILE: dev/manifests/pods/lifespan-seven.pod.yaml ================================================ apiVersion: v1 kind: Pod metadata: labels: acme.com/lifespan-requested: "7" name: lifespan-seven namespace: apps spec: containers: - args: - sleep - "3600" image: busybox name: lifespan-seven restartPolicy: Always ================================================ FILE: dev/manifests/pods/lifespan-three.pod.yaml ================================================ apiVersion: v1 kind: Pod metadata: labels: acme.com/lifespan-requested: "3" name: lifespan-three namespace: apps spec: containers: - args: - sleep - "3600" image: busybox name: lifespan-three restartPolicy: Always ================================================ FILE: dev/manifests/pods/no-lifespan-label.deploy.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: app: deploy name: deploy namespace: apps spec: replicas: 1 selector: matchLabels: app: deploy template: metadata: labels: app: deploy spec: containers: - command: - sleep - "3600" image: busybox name: busybox ================================================ FILE: dev/manifests/pods/no-lifespan-label.pod.yaml ================================================ apiVersion: v1 kind: Pod metadata: name: no-labels namespace: apps spec: containers: - args: - sleep - "3600" image: busybox name: no-labels restartPolicy: Always ================================================ FILE: dev/manifests/webhook/webhook.deploy.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: app: simple-kubernetes-webhook name: simple-kubernetes-webhook namespace: default spec: replicas: 1 selector: matchLabels: app: simple-kubernetes-webhook template: metadata: labels: app: simple-kubernetes-webhook spec: tolerations: - key: acme.com/lifespan-remaining operator: Exists effect: NoSchedule containers: - image: simple-kubernetes-webhook:latest imagePullPolicy: Never name: simple-kubernetes-webhook env: - name: TLS value: "true" - name: LOG_LEVEL value: "trace" - name: LOG_JSON value: "false" volumeMounts: - name: tls mountPath: "/etc/admission-webhook/tls" readOnly: true volumes: - name: tls secret: secretName: simple-kubernetes-webhook-tls ================================================ FILE: dev/manifests/webhook/webhook.svc.yaml ================================================ --- apiVersion: v1 kind: Service metadata: labels: app: simple-kubernetes-webhook name: simple-kubernetes-webhook namespace: default spec: type: NodePort ports: - port: 443 protocol: TCP targetPort: 443 nodePort: 30100 selector: app: simple-kubernetes-webhook ================================================ FILE: dev/manifests/webhook/webhook.tls.secret.yaml ================================================ apiVersion: v1 data: tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURHVENDQWdHZ0F3SUJBZ0lKQUxLcHI1S3RFeG5sTUEwR0NTcUdTSWIzRFFFQkJRVUFNREV4Q3pBSkJnTlYKQkFZVEFrRlZNU0l3SUFZRFZRUUREQmx6YVcxd2JHVXRhM1ZpWlhKdVpYUmxjeTEzWldKb2IyOXJNQjRYRFRJeApNVEF4TkRBNE1EWTBNRm9YRFRJeU1UQXhOREE0TURZME1Gb3dNVEVMTUFrR0ExVUVCaE1DUVZVeElqQWdCZ05WCkJBTU1HWE5wYlhCc1pTMXJkV0psY201bGRHVnpMWGRsWW1odmIyc3dnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUEKQTRJQkR3QXdnZ0VLQW9JQkFRREhvb0pCVGxuZTRYcE1sQWJWODBDeVJPMVJ3blM1ekFKY3BZdHMxNkc2NkpSSgpTTmJQQWt3YzY0dUNhYlJHNkVhM3Bva3dZVURETUxzQng0QVFUMHFrdUlJdFJYMFBCTjAzSmMxeFo0ZmtmekhICkMycG1tUFpnd3JGWGhOYUlNWlBBbTBqMnc0dmFPcFZObldKR3NRMkNiUUd2NGpWZS9DZHBkaXQxY3BRVWRTamsKZlFwMUJ1Mm95bFR3V1g1NXRjNXhRK0J2NDZVME5pK2c2elZHUUpmQm9JVGVTa3FqU1Q3SEtpM1F6NDBQUjJHRQpXZzZCaS9UWU5GclpxUFR4QmZkMnRMOWY2b1daQ1Q4MVFiS3FDeXB6RExJWkVoQktRSGNRTFpvMlJGNmlSWE9YCmZBY2tDOWNqcldsZ2Z3WHNyQVBHVkxTcVlyaGptK050b0svNFRTYUJBZ01CQUFHak5EQXlNREFHQTFVZEVRUXAKTUNlQ0pYTnBiWEJzWlMxcmRXSmxjbTVsZEdWekxYZGxZbWh2YjJzdVpHVm1ZWFZzZEM1emRtTXdEUVlKS29aSQpodmNOQVFFRkJRQURnZ0VCQURvSExWcGttR2d4NEZUekt1WXI4MGxjbUV2bVFBaG5GcWpWVjBEcmFoMGxId2NqClk1WVZPaWFYOGNBQ2lTYjFabFh4dVR2QzdQaE96SFg4MlphdjhES3Y5SzhkNjVuK3NZb1B4aFNpREdCOTZ3TUgKalB1OTcvck5VSWpjdGduZDlBZk8rZm8rQTJKRmltSWV6WFl2cGhuc2R0cXpxMzRwTkhLYnB6ZXJDZ3FmRkZpdwpaRngwd2svNkdVMlNkT2xxbXJxOEN0VkcwRVVDZG9Pd2xSL1RDUjgyUnQxbUUvSklhcWJ6eDc3dkg3bnk2L05ZCnNNVWgwbER0TVBTQVo2MkNLRnFTZnB4UTZlMXNlNmdVcFZZU2xMWDRZY3pxUy91YWR2VEQvR1lzQWhQbGd2YTYKRUxYaWwwck4wRDc3eTJ3bVMzb3JRK2hxUG9BM0Vja0N5Tm1ySFo0PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2QUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktZd2dnU2lBZ0VBQW9JQkFRREhvb0pCVGxuZTRYcE0KbEFiVjgwQ3lSTzFSd25TNXpBSmNwWXRzMTZHNjZKUkpTTmJQQWt3YzY0dUNhYlJHNkVhM3Bva3dZVURETUxzQgp4NEFRVDBxa3VJSXRSWDBQQk4wM0pjMXhaNGZrZnpISEMycG1tUFpnd3JGWGhOYUlNWlBBbTBqMnc0dmFPcFZOCm5XSkdzUTJDYlFHdjRqVmUvQ2RwZGl0MWNwUVVkU2prZlFwMUJ1Mm95bFR3V1g1NXRjNXhRK0J2NDZVME5pK2cKNnpWR1FKZkJvSVRlU2txalNUN0hLaTNRejQwUFIyR0VXZzZCaS9UWU5GclpxUFR4QmZkMnRMOWY2b1daQ1Q4MQpRYktxQ3lwekRMSVpFaEJLUUhjUUxabzJSRjZpUlhPWGZBY2tDOWNqcldsZ2Z3WHNyQVBHVkxTcVlyaGptK050Cm9LLzRUU2FCQWdNQkFBRUNnZ0VBVWV4QVk2aFJmUU11ZXVwcis3UjlJaXJpOEtCSjRrenoweTBrRUNCVkFDeWQKWFkyRWlTSzZOVXY3emlLdWxrS1BjcUhtdm5IS2I4ODVqcnRkdEZPMW4rOFBqS0J0ZDVKWmJWNFg5cWV6dm5Mcgo3SENrMDBHR0thTDd2NXlGcFJJalBmRDdlamc0MWU4Z2dkOUtDeFJ4Sk1xeTNJaUp1bGJqbllXZXcrMm5FdFZlCnZZN2NjUlFZTWZMdjdROC90N0tQblBpR1FvbE93RFpuYUkvbWkvV2xZQk5TQ1o1Y0E0NXIveldQOGpxaVVLY0cKN1Jrb1F1WEZUQmJHcUVuU1hYOWZOaEcwOWpFQ09MK1gwd0J2VkF1SE9lamdNOS9HUHRyTnl1aW1QUTdSM2d6VwpwQ2tiRk9zR3o2aHcyM2Y5S0ZaOHJaRTBqRTIzWm43YTFjcVJGdklZZ1FLQmdRRDdtS0UrNnlKWjJzR3FQMmdFCk5mcThZUFg0TWQ1Z0xJQzlzRk05b1llU0h0OGZUV1lYa1ZDVURiM1dTVDRmekhDOFR1a2NMN3dZcU93bWlsRDIKOXUrUk40ZVp6RDEzU2ZHR3AxMmNUWVJkZ1QrbTI3M1B3aFNBNXFNNVZYR2xlVmY0aGdjY2UwbGErOUJWOXFiSAo3dmlSL1J2Z29iaVJjVG1YY1NBYldSYzJTUUtCZ1FETElRdnNJYnVGRzNmc25taU5FSmZESG1qeXMvRzVoc0ZpCnBMSm1jYmdKMDZNOWJQSEc0VFl1bFR5YWQ5WmdUWjY4VWIvZEt3MEd3Und6ZXk3WWJBZlpES2tjU1pUZnc1cnMKWG9YUE8yU1BCUzRzVnNoaGhLMkdxVUtpS1VvalV5cGxFZlEvZmJJRkd6K3F6VmpDZllZQ3h0TUlWY2lzSWZBTwoxTUhhT25pT2VRS0JnQW1JUDl1MVp1akdtLzNLUnpPWm8vVk5LeVNMSnlTM3F1MEU2REoya3o5YkFoTWFpSnF0Cis4S1FQcmdHc0Y3ZURRdGxaZm1XYVdiNXgzQ3lYdHpzZ0NrZFZIcmtQUlB1N2tLdXhxSXNZYTUxUGljaFBqREgKNXFUM21BbU5EakE1eDdaM3hYOHp3SlM4NDZqT0hvV0dyVTVDcTdLNERka2MxQlREeVhhZnlueFpBb0dBTmVDNwpEOVBXc0RTYjk0Z0F4VUhjYnlXV3dxRldBVmFyM3FVK3FJdUxQQmdGbVZwWE91QXJoZW1SbklzaXNvS0VFd0UvCitjTGNmcWtqK01lNG9qRHRWL1hTdVMwUEx0YnNOYnZRbENuMXZ6V3BqSnNzSlNtUytUL1Y2N3MxN2U2Mk5QNngKSVZJT3NPb01WaHFIYTNidDM3aXE2dkFOL1JJM1lVZXZiMW5JOWtrQ2dZQkplQ2ZMU1hUd2tqQkYrQTNqY2dhQQowV0ZYeHlobzVzYVg3NGhqeDVzRnRBY2U4YVpBTXpmYTlEN1B3OVBobUE3NWE2b09EV2hJQzZNaklXV1ZlL0VrCnpnckRxZGhpV1BmNFUzNEVpZkUxRy9SdDJyV2dJb2tSWWcxVnIxMW04RjM0a2tZZEVBUGhISDBmak82SFRlZzMKUUFvTGhESjZzZm9FMU94aDZsU2Z6Zz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K kind: Secret metadata: creationTimestamp: null name: simple-kubernetes-webhook-tls type: kubernetes.io/tls ================================================ FILE: go.mod ================================================ module github.com/slackhq/simple-kubernetes-webhook go 1.16 require ( github.com/google/go-cmp v0.5.5 // indirect github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.6.1 github.com/wI2L/jsondiff v0.1.0 golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect k8s.io/api v0.21.3 k8s.io/apimachinery v0.21.3 ) ================================================ FILE: go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/wI2L/jsondiff v0.1.0 h1:j8KVhKey+qbyBy3VL8l3ZrxE907DTVTXcV/BvEeQAeM= github.com/wI2L/jsondiff v0.1.0/go.mod h1:KGXeexPwd48QqbM0XI+cuQDiZXI4n2Ceqs/5z+7WE4s= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/api v0.19.4/go.mod h1:SbtJ2aHCItirzdJ36YslycFNzWADYH3tgOhvBEFtZAk= k8s.io/api v0.21.3 h1:cblWILbLO8ar+Fj6xdDGr603HRsf8Wu9E9rngJeprZQ= k8s.io/api v0.21.3/go.mod h1:hUgeYHUbBp23Ue4qdX9tR8/ANi/g3ehylAqDn9NWVOg= k8s.io/apimachinery v0.19.4/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA= k8s.io/apimachinery v0.21.3 h1:3Ju4nvjCngxxMYby0BimUk+pQHPOQp3eCGChk5kfVII= k8s.io/apimachinery v0.21.3/go.mod h1:H/IM+5vH9kZRNJ4l3x/fXP/5bOPJaVP/guptnZPeCFI= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.8.0 h1:Q3gmuM9hKEjefWFFYF0Mat+YyFJvsUyYuwyNNJ5C9Ts= k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno= sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= ================================================ FILE: main.go ================================================ package main import ( "bytes" "encoding/json" "fmt" "net/http" "os" "github.com/sirupsen/logrus" "github.com/slackhq/simple-kubernetes-webhook/pkg/admission" admissionv1 "k8s.io/api/admission/v1" ) func main() { setLogger() // handle our core application http.HandleFunc("/validate-pods", ServeValidatePods) http.HandleFunc("/mutate-pods", ServeMutatePods) http.HandleFunc("/health", ServeHealth) // start the server // listens to clear text http on port 8080 unless TLS env var is set to "true" if os.Getenv("TLS") == "true" { cert := "/etc/admission-webhook/tls/tls.crt" key := "/etc/admission-webhook/tls/tls.key" logrus.Print("Listening on port 443...") logrus.Fatal(http.ListenAndServeTLS(":443", cert, key, nil)) } else { logrus.Print("Listening on port 8080...") logrus.Fatal(http.ListenAndServe(":8080", nil)) } } // ServeHealth returns 200 when things are good func ServeHealth(w http.ResponseWriter, r *http.Request) { logrus.WithField("uri", r.RequestURI).Debug("healthy") fmt.Fprint(w, "OK") } // ServeValidatePods validates an admission request and then writes an admission // review to `w` func ServeValidatePods(w http.ResponseWriter, r *http.Request) { logger := logrus.WithField("uri", r.RequestURI) logger.Debug("received validation request") in, err := parseRequest(*r) if err != nil { logger.Error(err) http.Error(w, err.Error(), http.StatusBadRequest) return } adm := admission.Admitter{ Logger: logger, Request: in.Request, } out, err := adm.ValidatePodReview() if err != nil { e := fmt.Sprintf("could not generate admission response: %v", err) logger.Error(e) http.Error(w, e, http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") jout, err := json.Marshal(out) if err != nil { e := fmt.Sprintf("could not parse admission response: %v", err) logger.Error(e) http.Error(w, e, http.StatusInternalServerError) return } logger.Debug("sending response") logger.Debugf("%s", jout) fmt.Fprintf(w, "%s", jout) } // ServeMutatePods returns an admission review with pod mutations as a json patch // in the review response func ServeMutatePods(w http.ResponseWriter, r *http.Request) { logger := logrus.WithField("uri", r.RequestURI) logger.Debug("received mutation request") in, err := parseRequest(*r) if err != nil { logger.Error(err) http.Error(w, err.Error(), http.StatusBadRequest) return } adm := admission.Admitter{ Logger: logger, Request: in.Request, } out, err := adm.MutatePodReview() if err != nil { e := fmt.Sprintf("could not generate admission response: %v", err) logger.Error(e) http.Error(w, e, http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") jout, err := json.Marshal(out) if err != nil { e := fmt.Sprintf("could not parse admission response: %v", err) logger.Error(e) http.Error(w, e, http.StatusInternalServerError) return } logger.Debug("sending response") logger.Debugf("%s", jout) fmt.Fprintf(w, "%s", jout) } // setLogger sets the logger using env vars, it defaults to text logs on // debug level unless otherwise specified func setLogger() { logrus.SetLevel(logrus.DebugLevel) lev := os.Getenv("LOG_LEVEL") if lev != "" { llev, err := logrus.ParseLevel(lev) if err != nil { logrus.Fatalf("cannot set LOG_LEVEL to %q", lev) } logrus.SetLevel(llev) } if os.Getenv("LOG_JSON") == "true" { logrus.SetFormatter(&logrus.JSONFormatter{}) } } // parseRequest extracts an AdmissionReview from an http.Request if possible func parseRequest(r http.Request) (*admissionv1.AdmissionReview, error) { if r.Header.Get("Content-Type") != "application/json" { return nil, fmt.Errorf("Content-Type: %q should be %q", r.Header.Get("Content-Type"), "application/json") } bodybuf := new(bytes.Buffer) bodybuf.ReadFrom(r.Body) body := bodybuf.Bytes() if len(body) == 0 { return nil, fmt.Errorf("admission request body is empty") } var a admissionv1.AdmissionReview if err := json.Unmarshal(body, &a); err != nil { return nil, fmt.Errorf("could not parse admission review request: %v", err) } if a.Request == nil { return nil, fmt.Errorf("admission review can't be used: Request field is nil") } return &a, nil } ================================================ FILE: pkg/admission/admission.go ================================================ // Package admission handles kubernetes admissions, // it takes admission requests and returns admission reviews; // for example, to mutate or validate pods package admission import ( "encoding/json" "fmt" "net/http" "github.com/sirupsen/logrus" "github.com/slackhq/simple-kubernetes-webhook/pkg/mutation" "github.com/slackhq/simple-kubernetes-webhook/pkg/validation" admissionv1 "k8s.io/api/admission/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" ) // Admitter is a container for admission business type Admitter struct { Logger *logrus.Entry Request *admissionv1.AdmissionRequest } // MutatePodReview takes an admission request and mutates the pod within, // it returns an admission review with mutations as a json patch (if any) func (a Admitter) MutatePodReview() (*admissionv1.AdmissionReview, error) { pod, err := a.Pod() if err != nil { e := fmt.Sprintf("could not parse pod in admission review request: %v", err) return reviewResponse(a.Request.UID, false, http.StatusBadRequest, e), err } m := mutation.NewMutator(a.Logger) patch, err := m.MutatePodPatch(pod) if err != nil { e := fmt.Sprintf("could not mutate pod: %v", err) return reviewResponse(a.Request.UID, false, http.StatusBadRequest, e), err } return patchReviewResponse(a.Request.UID, patch) } // MutatePodReview takes an admission request and validates the pod within // it returns an admission review func (a Admitter) ValidatePodReview() (*admissionv1.AdmissionReview, error) { pod, err := a.Pod() if err != nil { e := fmt.Sprintf("could not parse pod in admission review request: %v", err) return reviewResponse(a.Request.UID, false, http.StatusBadRequest, e), err } v := validation.NewValidator(a.Logger) val, err := v.ValidatePod(pod) if err != nil { e := fmt.Sprintf("could not validate pod: %v", err) return reviewResponse(a.Request.UID, false, http.StatusBadRequest, e), err } if !val.Valid { return reviewResponse(a.Request.UID, false, http.StatusForbidden, val.Reason), nil } return reviewResponse(a.Request.UID, true, http.StatusAccepted, "valid pod"), nil } // Pod extracts a pod from an admission request func (a Admitter) Pod() (*corev1.Pod, error) { if a.Request.Kind.Kind != "Pod" { return nil, fmt.Errorf("only pods are supported here") } p := corev1.Pod{} if err := json.Unmarshal(a.Request.Object.Raw, &p); err != nil { return nil, err } return &p, nil } // reviewResponse TODO: godoc func reviewResponse(uid types.UID, allowed bool, httpCode int32, reason string) *admissionv1.AdmissionReview { return &admissionv1.AdmissionReview{ TypeMeta: metav1.TypeMeta{ Kind: "AdmissionReview", APIVersion: "admission.k8s.io/v1", }, Response: &admissionv1.AdmissionResponse{ UID: uid, Allowed: allowed, Result: &metav1.Status{ Code: httpCode, Message: reason, }, }, } } // patchReviewResponse builds an admission review with given json patch func patchReviewResponse(uid types.UID, patch []byte) (*admissionv1.AdmissionReview, error) { patchType := admissionv1.PatchTypeJSONPatch return &admissionv1.AdmissionReview{ TypeMeta: metav1.TypeMeta{ Kind: "AdmissionReview", APIVersion: "admission.k8s.io/v1", }, Response: &admissionv1.AdmissionResponse{ UID: uid, Allowed: true, PatchType: &patchType, Patch: patch, }, }, nil } ================================================ FILE: pkg/admission/admission_test.go ================================================ package admission import ( "encoding/json" "net/http" "testing" "github.com/stretchr/testify/assert" admissionv1 "k8s.io/api/admission/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ) func TestPod(t *testing.T) { want := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "lifespan", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{{ Name: "lifespan", Image: "busybox", }}, }, } raw, err := json.Marshal(want) if err != nil { t.Fatal(err) } admreq := &admissionv1.AdmissionRequest{ UID: types.UID("test"), Kind: metav1.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, Object: runtime.RawExtension{ Raw: raw, Object: runtime.Object(nil), }, } a := Admitter{Request: admreq} got, err := a.Pod() if err != nil { t.Fatal(err) } assert.Equal(t, want, got) } func TestReviewResponse(t *testing.T) { uid := types.UID("test") reason := "fail!" want := &admissionv1.AdmissionReview{ TypeMeta: metav1.TypeMeta{ Kind: "AdmissionReview", APIVersion: "admission.k8s.io/v1", }, Response: &admissionv1.AdmissionResponse{ UID: uid, Allowed: false, Result: &metav1.Status{ Code: 418, Message: reason, }, }, } got := reviewResponse(uid, false, http.StatusTeapot, reason) assert.Equal(t, want, got) } func TestPatchReviewResponse(t *testing.T) { uid := types.UID("test") patchType := admissionv1.PatchTypeJSONPatch patch := []byte(`not quite a real patch`) want := &admissionv1.AdmissionReview{ TypeMeta: metav1.TypeMeta{ Kind: "AdmissionReview", APIVersion: "admission.k8s.io/v1", }, Response: &admissionv1.AdmissionResponse{ UID: uid, Allowed: true, PatchType: &patchType, Patch: patch, }, } got, err := patchReviewResponse(uid, patch) if err != nil { t.Fatal(err) } assert.Equal(t, want, got) } ================================================ FILE: pkg/mutation/inject_env.go ================================================ package mutation import ( "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" ) // injectEnv is a container for the mutation injecting environment vars type injectEnv struct { Logger logrus.FieldLogger } // injectEnv implements the podMutator interface var _ podMutator = (*injectEnv)(nil) // Name returns the struct name func (se injectEnv) Name() string { return "inject_env" } // Mutate returns a new mutated pod according to set env rules func (se injectEnv) Mutate(pod *corev1.Pod) (*corev1.Pod, error) { se.Logger = se.Logger.WithField("mutation", se.Name()) mpod := pod.DeepCopy() // build out env var slice envVars := []corev1.EnvVar{{ Name: "KUBE", Value: "true", }} // inject env vars into pod for _, envVar := range envVars { se.Logger.Debugf("pod env injected %s", envVar) injectEnvVar(mpod, envVar) } return mpod, nil } // injectEnvVar injects a var in both containers and init containers of a pod func injectEnvVar(pod *corev1.Pod, envVar corev1.EnvVar) { for i, container := range pod.Spec.Containers { if !HasEnvVar(container, envVar) { pod.Spec.Containers[i].Env = append(container.Env, envVar) } } for i, container := range pod.Spec.InitContainers { if !HasEnvVar(container, envVar) { pod.Spec.InitContainers[i].Env = append(container.Env, envVar) } } } // HasEnvVar returns true if environment variable exists false otherwise func HasEnvVar(container corev1.Container, checkEnvVar corev1.EnvVar) bool { for _, envVar := range container.Env { if envVar.Name == checkEnvVar.Name { return true } } return false } ================================================ FILE: pkg/mutation/inject_env_test.go ================================================ package mutation import ( "testing" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestInjectEnvMutate(t *testing.T) { want := &corev1.Pod{ ObjectMeta: v1.ObjectMeta{ Name: "test", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{{ Name: "test", Env: []corev1.EnvVar{ { Name: "KUBE", Value: "true", }, }, }}, InitContainers: []corev1.Container{{ Name: "inittest", Env: []corev1.EnvVar{ { Name: "KUBE", Value: "true", }, }, }}, }, } pod := &corev1.Pod{ ObjectMeta: v1.ObjectMeta{ Name: "test", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{{ Name: "test", }}, InitContainers: []corev1.Container{{ Name: "inittest", }}, }, } got, err := injectEnv{Logger: logger()}.Mutate(pod) if err != nil { t.Fatal(err) } assert.Equal(t, want, got) } func TestHasEnvVar(t *testing.T) { ey := corev1.EnvVar{ Name: "foo", Value: "sball", } en := corev1.EnvVar{ Name: "the_pope", Value: "of_nope", } c := corev1.Container{ Name: "test", Env: []corev1.EnvVar{ey}, } assert.True(t, HasEnvVar(c, ey)) assert.False(t, HasEnvVar(c, en)) } ================================================ FILE: pkg/mutation/minimum_lifespan.go ================================================ package mutation import ( "fmt" "reflect" "strconv" "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" ) // minLifespanTolerations is a container for mininum lifespan mutation type minLifespanTolerations struct { Logger logrus.FieldLogger } // minLifespanTolerations implements the podMutator interface var _ podMutator = (*minLifespanTolerations)(nil) // Name returns the minLifespanTolerations short name func (mpl minLifespanTolerations) Name() string { return "min_lifespan" } // Mutate returns a new mutated pod according to lifespan tolerations rules func (mpl minLifespanTolerations) Mutate(pod *corev1.Pod) (*corev1.Pod, error) { const ( lifespanLabel = "acme.com/lifespan-requested" taintKey = "acme.com/lifespan-remaining" taintMaxAge = 14 ) mpl.Logger = mpl.Logger.WithField("mutation", mpl.Name()) mpod := pod.DeepCopy() if pod.Labels == nil || pod.Labels[lifespanLabel] == "" { mpl.Logger.WithField("min_lifespan", 0). Printf("no lifespan label found, applying default lifespan toleration") tn := []corev1.Toleration{{ Key: taintKey, Operator: corev1.TolerationOpExists, Effect: corev1.TaintEffectNoSchedule, }} mpod.Spec.Tolerations = appendTolerations(tn, mpod.Spec.Tolerations) return mpod, nil } ts := pod.Labels[lifespanLabel] minAge, err := strconv.Atoi(ts) if err != nil { return nil, fmt.Errorf("pod lifespan label %q is not an integer: %v", ts, err) } mpl.Logger.WithField("min_lifespan", ts).Printf("setting lifespan tolerations") t := []corev1.Toleration{} for i := taintMaxAge; i >= minAge; i-- { t = append(t, corev1.Toleration{ Key: taintKey, Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule, Value: fmt.Sprint(i), }) } mpod.Spec.Tolerations = appendTolerations(t, mpod.Spec.Tolerations) return mpod, nil } // appendTolerations appends existing to new without duplicating any tolerations func appendTolerations(new, existing []corev1.Toleration) []corev1.Toleration { var toAppend []corev1.Toleration for _, n := range new { found := false for _, e := range existing { if reflect.DeepEqual(n, e) { found = true } } if !found { toAppend = append(toAppend, n) } } return append(existing, toAppend...) } ================================================ FILE: pkg/mutation/minimum_lifespan_test.go ================================================ package mutation import ( "testing" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestMinLifespanTolerationsNoLabel(t *testing.T) { want := &corev1.Pod{ ObjectMeta: v1.ObjectMeta{ Name: "lifespan", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{{ Name: "lifespan", Image: "busybox", }}, Tolerations: []corev1.Toleration{ { Key: "acme.com/lifespan-remaining", Operator: corev1.TolerationOpExists, Effect: corev1.TaintEffectNoSchedule, }, }, }, } pod := &corev1.Pod{ ObjectMeta: v1.ObjectMeta{ Name: "lifespan", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{{ Name: "lifespan", Image: "busybox", }}, }, } got, err := minLifespanTolerations{logger()}.Mutate(pod) if err != nil { t.Fatal(err) } assert.Equal(t, want, got) } func TestMinLifespanTolerationsLabel(t *testing.T) { want := &corev1.Pod{ ObjectMeta: v1.ObjectMeta{ Name: "lifespan", Labels: map[string]string{ "acme.com/lifespan-requested": "7", }, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{{ Name: "lifespan", Image: "busybox", }}, Tolerations: []corev1.Toleration{ { Key: "something-unrelated", Operator: corev1.TolerationOpExists, Effect: corev1.TaintEffectNoSchedule, }, { Key: "acme.com/lifespan-remaining", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule, Value: "14", }, { Key: "acme.com/lifespan-remaining", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule, Value: "13", }, { Key: "acme.com/lifespan-remaining", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule, Value: "12", }, { Key: "acme.com/lifespan-remaining", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule, Value: "11", }, { Key: "acme.com/lifespan-remaining", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule, Value: "10", }, { Key: "acme.com/lifespan-remaining", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule, Value: "9", }, { Key: "acme.com/lifespan-remaining", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule, Value: "8", }, { Key: "acme.com/lifespan-remaining", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule, Value: "7", }, }, }, } pod := &corev1.Pod{ ObjectMeta: v1.ObjectMeta{ Name: "lifespan", Labels: map[string]string{ "acme.com/lifespan-requested": "7", }, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{{ Name: "lifespan", Image: "busybox", }}, Tolerations: []corev1.Toleration{ { Key: "something-unrelated", Operator: corev1.TolerationOpExists, Effect: corev1.TaintEffectNoSchedule, }, }, }, } got, err := minLifespanTolerations{logger()}.Mutate(pod) if err != nil { t.Fatal(err) } assert.Equal(t, want, got) } func TestMinLifespanTolerationsIdempotence(t *testing.T) { want := &corev1.Pod{ ObjectMeta: v1.ObjectMeta{ Name: "lifespan", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{{ Name: "lifespan", Image: "busybox", }}, Tolerations: []corev1.Toleration{ { Key: "acme.com/lifespan-remaining", Operator: corev1.TolerationOpExists, Effect: corev1.TaintEffectNoSchedule, }, { Key: "something-unrelated", Operator: corev1.TolerationOpExists, Effect: corev1.TaintEffectNoSchedule, }, }, }, } got, err := minLifespanTolerations{logger()}.Mutate(want.DeepCopy()) if err != nil { t.Fatal(err) } assert.Equal(t, want, got) } ================================================ FILE: pkg/mutation/mutation.go ================================================ package mutation import ( "encoding/json" "github.com/sirupsen/logrus" "github.com/wI2L/jsondiff" corev1 "k8s.io/api/core/v1" ) // Mutator is a container for mutation type Mutator struct { Logger *logrus.Entry } // NewMutator returns an initialised instance of Mutator func NewMutator(logger *logrus.Entry) *Mutator { return &Mutator{Logger: logger} } // podMutators is an interface used to group functions mutating pods type podMutator interface { Mutate(*corev1.Pod) (*corev1.Pod, error) Name() string } // MutatePodPatch returns a json patch containing all the mutations needed for // a given pod func (m *Mutator) MutatePodPatch(pod *corev1.Pod) ([]byte, error) { var podName string if pod.Name != "" { podName = pod.Name } else { if pod.ObjectMeta.GenerateName != "" { podName = pod.ObjectMeta.GenerateName } } log := logrus.WithField("pod_name", podName) // list of all mutations to be applied to the pod mutations := []podMutator{ minLifespanTolerations{Logger: log}, injectEnv{Logger: log}, } mpod := pod.DeepCopy() // apply all mutations for _, m := range mutations { var err error mpod, err = m.Mutate(mpod) if err != nil { return nil, err } } // generate json patch patch, err := jsondiff.Compare(pod, mpod) if err != nil { return nil, err } patchb, err := json.Marshal(patch) if err != nil { return nil, err } return patchb, nil } ================================================ FILE: pkg/mutation/mutation_test.go ================================================ package mutation import ( "io/ioutil" "strings" "testing" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestMutatePodPatch(t *testing.T) { m := NewMutator(logger()) got, err := m.MutatePodPatch(pod()) if err != nil { t.Fatal(err) } p := patch() g := string(got) assert.Equal(t, p, g) } func BenchmarkMutatePodPatch(b *testing.B) { m := NewMutator(logger()) pod := pod() for i := 0; i < b.N; i++ { _, err := m.MutatePodPatch(pod) if err != nil { b.Fatal(err) } } } func pod() *corev1.Pod { return &corev1.Pod{ ObjectMeta: v1.ObjectMeta{ Name: "lifespan", Labels: map[string]string{ "acme.com/lifespan-requested": "7", }, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{{ Name: "lifespan", Image: "busybox", }}, }, } } func patch() string { patch := `[ {"op":"add","path":"/spec/containers/0/env","value":[ {"name":"KUBE","value":"true"} ]}, {"op":"add","path":"/spec/tolerations","value":[ {"effect":"NoSchedule","key":"acme.com/lifespan-remaining","operator":"Equal","value":"14"}, {"effect":"NoSchedule","key":"acme.com/lifespan-remaining","operator":"Equal","value":"13"}, {"effect":"NoSchedule","key":"acme.com/lifespan-remaining","operator":"Equal","value":"12"}, {"effect":"NoSchedule","key":"acme.com/lifespan-remaining","operator":"Equal","value":"11"}, {"effect":"NoSchedule","key":"acme.com/lifespan-remaining","operator":"Equal","value":"10"}, {"effect":"NoSchedule","key":"acme.com/lifespan-remaining","operator":"Equal","value":"9"}, {"effect":"NoSchedule","key":"acme.com/lifespan-remaining","operator":"Equal","value":"8"}, {"effect":"NoSchedule","key":"acme.com/lifespan-remaining","operator":"Equal","value":"7"} ]} ]` patch = strings.ReplaceAll(patch, "\n", "") patch = strings.ReplaceAll(patch, "\t", "") patch = strings.ReplaceAll(patch, " ", "") return patch } func logger() *logrus.Entry { mute := logrus.StandardLogger() mute.Out = ioutil.Discard return mute.WithField("logger", "test") } ================================================ FILE: pkg/validation/name_validator.go ================================================ package validation import ( "fmt" "strings" "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" ) // nameValidator is a container for validating the name of pods type nameValidator struct { Logger logrus.FieldLogger } // nameValidator implements the podValidator interface var _ podValidator = (*nameValidator)(nil) // Name returns the name of nameValidator func (n nameValidator) Name() string { return "name_validator" } // Validate inspects the name of a given pod and returns validation. // The returned validation is only valid if the pod name does not contain some // bad string. func (n nameValidator) Validate(pod *corev1.Pod) (validation, error) { badString := "offensive" if strings.Contains(pod.Name, badString) { v := validation{ Valid: false, Reason: fmt.Sprintf("pod name contains %q", badString), } return v, nil } return validation{Valid: true, Reason: "valid name"}, nil } ================================================ FILE: pkg/validation/name_validator_test.go ================================================ package validation import ( "testing" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestNameValidatorValidate(t *testing.T) { t.Run("good name", func(t *testing.T) { pod := &corev1.Pod{ ObjectMeta: v1.ObjectMeta{ Name: "lifespan", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{{ Name: "lifespan", Image: "busybox", }}, }, } v, err := nameValidator{logger()}.Validate(pod) assert.Nil(t, err) assert.True(t, v.Valid) }) t.Run("bad name", func(t *testing.T) { pod := &corev1.Pod{ ObjectMeta: v1.ObjectMeta{ Name: "lifespan-offensive", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{{ Name: "lifespan", Image: "busybox", }}, }, } v, err := nameValidator{logger()}.Validate(pod) assert.Nil(t, err) assert.False(t, v.Valid) }) } ================================================ FILE: pkg/validation/validation.go ================================================ package validation import ( "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" ) // Validator is a container for mutation type Validator struct { Logger *logrus.Entry } // NewValidator returns an initialised instance of Validator func NewValidator(logger *logrus.Entry) *Validator { return &Validator{Logger: logger} } // podValidators is an interface used to group functions mutating pods type podValidator interface { Validate(*corev1.Pod) (validation, error) Name() string } type validation struct { Valid bool Reason string } // ValidatePod returns true if a pod is valid func (v *Validator) ValidatePod(pod *corev1.Pod) (validation, error) { var podName string if pod.Name != "" { podName = pod.Name } else { if pod.ObjectMeta.GenerateName != "" { podName = pod.ObjectMeta.GenerateName } } log := logrus.WithField("pod_name", podName) log.Print("delete me") // list of all validations to be applied to the pod validations := []podValidator{ nameValidator{v.Logger}, } // apply all validations for _, v := range validations { var err error vp, err := v.Validate(pod) if err != nil { return validation{Valid: false, Reason: err.Error()}, err } if !vp.Valid { return validation{Valid: false, Reason: vp.Reason}, err } } return validation{Valid: true, Reason: "valid pod"}, nil } ================================================ FILE: pkg/validation/validation_test.go ================================================ package validation import ( "io/ioutil" "testing" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestValidatePod(t *testing.T) { v := NewValidator(logger()) pod := &corev1.Pod{ ObjectMeta: v1.ObjectMeta{ Name: "lifespan", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{{ Name: "lifespan", Image: "busybox", }}, }, } val, err := v.ValidatePod(pod) assert.Nil(t, err) assert.True(t, val.Valid) } func logger() *logrus.Entry { mute := logrus.StandardLogger() mute.Out = ioutil.Discard return mute.WithField("logger", "test") }