main 10254f1be879 cached
38 files
83.0 KB
33.9k tokens
49 symbols
1 requests
Download .txt
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")
}
Download .txt
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
Download .txt
SYMBOL INDEX (49 symbols across 13 files)

FILE: main.go
  function main (line 15) | func main() {
  function ServeHealth (line 37) | func ServeHealth(w http.ResponseWriter, r *http.Request) {
  function ServeValidatePods (line 44) | func ServeValidatePods(w http.ResponseWriter, r *http.Request) {
  function ServeMutatePods (line 84) | func ServeMutatePods(w http.ResponseWriter, r *http.Request) {
  function setLogger (line 124) | func setLogger() {
  function parseRequest (line 142) | func parseRequest(r http.Request) (*admissionv1.AdmissionReview, error) {

FILE: pkg/admission/admission.go
  type Admitter (line 21) | type Admitter struct
    method MutatePodReview (line 28) | func (a Admitter) MutatePodReview() (*admissionv1.AdmissionReview, err...
    method ValidatePodReview (line 47) | func (a Admitter) ValidatePodReview() (*admissionv1.AdmissionReview, e...
    method Pod (line 69) | func (a Admitter) Pod() (*corev1.Pod, error) {
  function reviewResponse (line 83) | func reviewResponse(uid types.UID, allowed bool, httpCode int32,
  function patchReviewResponse (line 102) | func patchReviewResponse(uid types.UID, patch []byte) (*admissionv1.Admi...

FILE: pkg/admission/admission_test.go
  function TestPod (line 16) | func TestPod(t *testing.T) {
  function TestReviewResponse (line 52) | func TestReviewResponse(t *testing.T) {
  function TestPatchReviewResponse (line 75) | func TestPatchReviewResponse(t *testing.T) {

FILE: pkg/mutation/inject_env.go
  type injectEnv (line 9) | type injectEnv struct
    method Name (line 17) | func (se injectEnv) Name() string {
    method Mutate (line 22) | func (se injectEnv) Mutate(pod *corev1.Pod) (*corev1.Pod, error) {
  function injectEnvVar (line 42) | func injectEnvVar(pod *corev1.Pod, envVar corev1.EnvVar) {
  function HasEnvVar (line 56) | func HasEnvVar(container corev1.Container, checkEnvVar corev1.EnvVar) bo...

FILE: pkg/mutation/inject_env_test.go
  function TestInjectEnvMutate (line 11) | func TestInjectEnvMutate(t *testing.T) {
  function TestHasEnvVar (line 60) | func TestHasEnvVar(t *testing.T) {

FILE: pkg/mutation/minimum_lifespan.go
  type minLifespanTolerations (line 13) | type minLifespanTolerations struct
    method Name (line 21) | func (mpl minLifespanTolerations) Name() string {
    method Mutate (line 26) | func (mpl minLifespanTolerations) Mutate(pod *corev1.Pod) (*corev1.Pod...
  function appendTolerations (line 73) | func appendTolerations(new, existing []corev1.Toleration) []corev1.Toler...

FILE: pkg/mutation/minimum_lifespan_test.go
  function TestMinLifespanTolerationsNoLabel (line 11) | func TestMinLifespanTolerationsNoLabel(t *testing.T) {
  function TestMinLifespanTolerationsLabel (line 51) | func TestMinLifespanTolerationsLabel(t *testing.T) {
  function TestMinLifespanTolerationsIdempotence (line 150) | func TestMinLifespanTolerationsIdempotence(t *testing.T) {

FILE: pkg/mutation/mutation.go
  type Mutator (line 12) | type Mutator struct
    method MutatePodPatch (line 29) | func (m *Mutator) MutatePodPatch(pod *corev1.Pod) ([]byte, error) {
  function NewMutator (line 17) | func NewMutator(logger *logrus.Entry) *Mutator {
  type podMutator (line 22) | type podMutator interface

FILE: pkg/mutation/mutation_test.go
  function TestMutatePodPatch (line 14) | func TestMutatePodPatch(t *testing.T) {
  function BenchmarkMutatePodPatch (line 26) | func BenchmarkMutatePodPatch(b *testing.B) {
  function pod (line 38) | func pod() *corev1.Pod {
  function patch (line 55) | func patch() string {
  function logger (line 79) | func logger() *logrus.Entry {

FILE: pkg/validation/name_validator.go
  type nameValidator (line 12) | type nameValidator struct
    method Name (line 20) | func (n nameValidator) Name() string {
    method Validate (line 27) | func (n nameValidator) Validate(pod *corev1.Pod) (validation, error) {

FILE: pkg/validation/name_validator_test.go
  function TestNameValidatorValidate (line 11) | func TestNameValidatorValidate(t *testing.T) {

FILE: pkg/validation/validation.go
  type Validator (line 9) | type Validator struct
    method ValidatePod (line 30) | func (v *Validator) ValidatePod(pod *corev1.Pod) (validation, error) {
  function NewValidator (line 14) | func NewValidator(logger *logrus.Entry) *Validator {
  type podValidator (line 19) | type podValidator interface
  type validation (line 24) | type validation struct

FILE: pkg/validation/validation_test.go
  function TestValidatePod (line 13) | func TestValidatePod(t *testing.T) {
  function logger (line 33) | func logger() *logrus.Entry {
Condensed preview — 38 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (92K chars).
[
  {
    "path": ".github/CODE_OF_CONDUCT.md",
    "chars": 969,
    "preview": "# Code of Conduct\n\n## Introduction\n\nDiversity and inclusion make our community strong. We encourage participation from t"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "chars": 3739,
    "preview": "# Contributors Guide\n\nInterested in contributing? Awesome! Before you do though, please read our\n[Code of Conduct](https"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "chars": 1076,
    "preview": "### Description\n\nDescribe your issue here.\n\n### What type of issue is this? (place an `x` in one of the `[ ]`)\n- [ ] bug"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "chars": 863,
    "preview": "###  Summary\n\nDescribe the goal of this PR. Mention any related Issue numbers.\n\n### Requirements (place an `x` in each `"
  },
  {
    "path": ".github/maintainers_guide.md",
    "chars": 3559,
    "preview": "# Maintainers Guide\n\nThis document describes tools, tasks and workflow that one needs to be familiar with in order to ef"
  },
  {
    "path": "CODEOWNERS",
    "chars": 184,
    "preview": "# Comment line immediately above ownership line is reserved for related other information. Please be careful while editi"
  },
  {
    "path": "Dockerfile",
    "chars": 403,
    "preview": "# syntax=docker/dockerfile:experimental\n# ---\nFROM golang:1.16 AS build\n\nENV GOOS=linux\nENV GOARCH=amd64\nENV CGO_ENABLED"
  },
  {
    "path": "LICENSE",
    "chars": 1075,
    "preview": "MIT License\n\nCopyright (c) Slack Technologies, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaini"
  },
  {
    "path": "Makefile",
    "chars": 2282,
    "preview": ".PHONY: test\ntest:\n\t@echo \"\\n🛠️  Running unit tests...\"\n\tgo test ./...\n\n.PHONY: build\nbuild:\n\t@echo \"\\n🔧  Building Go bi"
  },
  {
    "path": "README.md",
    "chars": 7042,
    "preview": "# simple-kubernetes-webhook\n\nThis is a simple [Kubernetes admission webhook](https://kubernetes.io/docs/reference/access"
  },
  {
    "path": "dev/gen-certs.sh",
    "chars": 836,
    "preview": "#!/bin/bash\n\nopenssl genrsa -out ca.key 2048\n\nopenssl req -new -x509 -days 365 -key ca.key \\\n  -subj \"/C=AU/CN=simple-ku"
  },
  {
    "path": "dev/manifests/cluster-config/apps.ns.yaml",
    "chars": 95,
    "preview": "apiVersion: v1\nkind: Namespace\nmetadata:\n  name: apps\n  labels:\n    admission-webhook: enabled\n"
  },
  {
    "path": "dev/manifests/cluster-config/mutating.config.yaml",
    "chars": 2222,
    "preview": "apiVersion: admissionregistration.k8s.io/v1\nkind: MutatingWebhookConfiguration\nmetadata:\n  name: \"simple-kubernetes-webh"
  },
  {
    "path": "dev/manifests/cluster-config/validating.config.yaml",
    "chars": 2226,
    "preview": "apiVersion: admissionregistration.k8s.io/v1\nkind: ValidatingWebhookConfiguration\nmetadata:\n  name: \"simple-kubernetes-we"
  },
  {
    "path": "dev/manifests/kind/kind.cluster.yaml",
    "chars": 242,
    "preview": "kind: Cluster\napiVersion: kind.x-k8s.io/v1alpha4\nnodes:\n  - role: control-plane\n    image: kindest/node:v1.21.1\n    extr"
  },
  {
    "path": "dev/manifests/pods/bad-name.pod.yaml",
    "chars": 216,
    "preview": "apiVersion: v1\nkind: Pod\nmetadata:\n  name: offensive-pod\n  namespace: apps\nspec:\n  containers:\n    - args:\n        - sle"
  },
  {
    "path": "dev/manifests/pods/lifespan-seven.pod.yaml",
    "chars": 260,
    "preview": "apiVersion: v1\nkind: Pod\nmetadata:\n  labels:\n    acme.com/lifespan-requested: \"7\"\n  name: lifespan-seven\n  namespace: ap"
  },
  {
    "path": "dev/manifests/pods/lifespan-three.pod.yaml",
    "chars": 260,
    "preview": "apiVersion: v1\nkind: Pod\nmetadata:\n  labels:\n    acme.com/lifespan-requested: \"3\"\n  name: lifespan-three\n  namespace: ap"
  },
  {
    "path": "dev/manifests/pods/no-lifespan-label.deploy.yaml",
    "chars": 370,
    "preview": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  labels:\n    app: deploy\n  name: deploy\n  namespace: apps\nspec:\n  replic"
  },
  {
    "path": "dev/manifests/pods/no-lifespan-label.pod.yaml",
    "chars": 203,
    "preview": "apiVersion: v1\nkind: Pod\nmetadata:\n  name: no-labels\n  namespace: apps\nspec:\n  containers:\n    - args:\n        - sleep\n "
  },
  {
    "path": "dev/manifests/webhook/webhook.deploy.yaml",
    "chars": 1005,
    "preview": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  labels:\n    app: simple-kubernetes-webhook\n  name: simple-kubernetes-we"
  },
  {
    "path": "dev/manifests/webhook/webhook.svc.yaml",
    "chars": 302,
    "preview": "---\napiVersion: v1\nkind: Service\nmetadata:\n  labels:\n    app: simple-kubernetes-webhook\n  name: simple-kubernetes-webhoo"
  },
  {
    "path": "dev/manifests/webhook/webhook.tls.secret.yaml",
    "chars": 3944,
    "preview": "apiVersion: v1\ndata:\n  tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURHVENDQWdHZ0F3SUJBZ0lKQUxLcHI1S3RFeG5sTUEwR0NT"
  },
  {
    "path": "go.mod",
    "chars": 467,
    "preview": "module github.com/slackhq/simple-kubernetes-webhook\n\ngo 1.16\n\nrequire (\n\tgithub.com/google/go-cmp v0.5.5 // indirect\n\tgi"
  },
  {
    "path": "go.sum",
    "chars": 24862,
    "preview": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ngithub.com/BurntSushi/toml v0.3.1/go."
  },
  {
    "path": "main.go",
    "chars": 4300,
    "preview": "package main\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/sl"
  },
  {
    "path": "pkg/admission/admission.go",
    "chars": 3444,
    "preview": "// Package admission handles kubernetes admissions,\n// it takes admission requests and returns admission reviews;\n// for"
  },
  {
    "path": "pkg/admission/admission_test.go",
    "chars": 1981,
    "preview": "package admission\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\tadmissionv1 "
  },
  {
    "path": "pkg/mutation/inject_env.go",
    "chars": 1594,
    "preview": "package mutation\n\nimport (\n\t\"github.com/sirupsen/logrus\"\n\tcorev1 \"k8s.io/api/core/v1\"\n)\n\n// injectEnv is a container for"
  },
  {
    "path": "pkg/mutation/inject_env_test.go",
    "chars": 1270,
    "preview": "package mutation\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\tv1 \"k8s.io/ap"
  },
  {
    "path": "pkg/mutation/minimum_lifespan.go",
    "chars": 2295,
    "preview": "package mutation\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"strconv\"\n\n\t\"github.com/sirupsen/logrus\"\n\tcorev1 \"k8s.io/api/core/v1\"\n)\n\n/"
  },
  {
    "path": "pkg/mutation/minimum_lifespan_test.go",
    "chars": 4046,
    "preview": "package mutation\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\tv1 \"k8s.io/ap"
  },
  {
    "path": "pkg/mutation/mutation.go",
    "chars": 1411,
    "preview": "package mutation\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/wI2L/jsondiff\"\n\tcorev1 \"k8s.io/a"
  },
  {
    "path": "pkg/mutation/mutation_test.go",
    "chars": 2137,
    "preview": "package mutation\n\nimport (\n\t\"io/ioutil\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/stretchr/testi"
  },
  {
    "path": "pkg/validation/name_validator.go",
    "chars": 926,
    "preview": "package validation\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\tcorev1 \"k8s.io/api/core/v1\"\n)\n\n// nameVal"
  },
  {
    "path": "pkg/validation/name_validator_test.go",
    "chars": 914,
    "preview": "package validation\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\tv1 \"k8s.io/"
  },
  {
    "path": "pkg/validation/validation.go",
    "chars": 1347,
    "preview": "package validation\n\nimport (\n\t\"github.com/sirupsen/logrus\"\n\tcorev1 \"k8s.io/api/core/v1\"\n)\n\n// Validator is a container f"
  },
  {
    "path": "pkg/validation/validation_test.go",
    "chars": 676,
    "preview": "package validation\n\nimport (\n\t\"io/ioutil\"\n\t\"testing\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/stretchr/testify/assert"
  }
]

About this extraction

This page contains the full source code of the slackhq/simple-kubernetes-webhook GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 38 files (83.0 KB), approximately 33.9k tokens, and a symbol index with 49 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!