├── .dockerignore ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE.txt ├── MAINTAINERS.md ├── Makefile ├── README.md ├── cmd ├── flags.go └── main.go ├── conf └── sidecar-test.yaml ├── docs ├── configmaps.md ├── configuration.md ├── deployment.md ├── design.md ├── hacking.md ├── sidecar-configuration-format.md ├── tls.md └── unit-tests.md ├── entrypoint.sh ├── examples ├── kubernetes │ ├── clusterrole.yaml │ ├── clusterrolebinding.yaml │ ├── configmap-sidecar-test.yaml │ ├── debug-pod.yaml │ ├── deployment.yaml │ ├── mutating-webhook-configuration.yaml │ ├── namespace.yaml │ ├── service-monitor.yaml │ ├── service.yaml │ └── serviceaccount.yaml └── tls │ ├── README.md │ ├── ca.conf │ ├── csr-prod.conf │ └── new-cluster-injector-cert.rb ├── go.mod ├── go.sum ├── internal └── pkg │ ├── config │ ├── config.go │ ├── config_test.go │ └── watcher │ │ ├── config.go │ │ ├── loader_test.go │ │ ├── message.go │ │ ├── watcher.go │ │ └── watcher_test.go │ ├── testing │ ├── config.go │ └── testing.go │ └── version │ └── version.go ├── pkg ├── coalescer │ ├── coalescer.go │ └── coalescer_test.go └── server │ ├── errors.go │ ├── parameters.go │ ├── webhook.go │ └── webhook_test.go └── test └── fixtures ├── gabe-test.yaml ├── k8s ├── admissioncontrol │ ├── patch │ │ ├── env-override.json │ │ ├── service-account-already-set.json │ │ ├── service-account-default-token.json │ │ ├── service-account-set-default.json │ │ ├── service-account.json │ │ ├── sidecar-test-1.json │ │ ├── volumetest-existingvolume.json │ │ └── volumetest.json │ └── request │ │ ├── env-override.yaml │ │ ├── missing-sidecar-config.yaml │ │ ├── service-account-already-set.yaml │ │ ├── service-account-default-token.yaml │ │ ├── service-account-set-default.yaml │ │ ├── service-account.yaml │ │ ├── sidecar-test-1.yaml │ │ ├── volumetest-existingvolume.yaml │ │ └── volumetest.yaml ├── bad-sidecar.yaml ├── configmap-complex-sidecar.yaml ├── configmap-env1.yaml ├── configmap-host-aliases.yaml ├── configmap-hostNetwork-hostPid.yaml ├── configmap-init-containers.yaml ├── configmap-multiple1.yaml ├── configmap-sidecar-test.yaml ├── configmap-volume-mounts.yaml ├── env1.yaml ├── ignored-namespace-pod.yaml ├── object1.yaml ├── object2latest.yaml ├── object2v.yaml ├── object3-missing.yaml ├── object4.yaml ├── object5.yaml ├── object6.yaml ├── object7-badrequestformat.yaml ├── object7-v2.yaml └── object7.yaml └── sidecars ├── bad ├── inheritance-escape.yaml ├── inheritance-filenotfound.yaml ├── init-containers-colons-v3.yaml └── missing-name.yaml ├── complex-sidecar.yaml ├── env1.yaml ├── host-aliases.yaml ├── inheritance-1.yaml ├── inheritance-deep-2.yaml ├── init-containers-v2.yaml ├── init-containers.yaml ├── maxmind.yaml ├── service-account-default-token.yaml ├── service-account-with-inheritance.yaml ├── service-account.yaml ├── sidecar-test.yaml ├── test-network-pid.yaml └── volume-mounts.yaml /.dockerignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.gopath~ 3 | /bin 4 | Jenkinsfile 5 | Dockerfile 6 | README.md 7 | /ci-cd 8 | .dockerignore 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # What's going on? 2 | 3 | > A brief description of your problem, here, please! 4 | 5 | # Expected Behavior 6 | 7 | > What is the expected behavior? How does this diverge from actual behavior? 8 | 9 | # Reproducer 10 | 11 | > Got a reproducer? Please include how you launch the injector, complete with 12 | > sidecar configurations (files or configmaps). 13 | 14 | # Version Deets 15 | 16 | * Kubernetes Version: `x.y.z` 17 | * `k8s-sidecar-injector` Version: `x.y.z-commitish` 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # What and why? 2 | 3 | > Provide a description of what is being changed and why. 4 | > Be descriptive, give context, and avoid one liners. 5 | 6 | # Testing Steps 7 | 8 | Please provide adequate testing steps (including screenshots if necessary). 9 | Include any test fixtures or sample configurations in your commit. 10 | 11 | - [ ] Added unit tests for this feature (`make test`) 12 | 13 | # Reviewers 14 | 15 | Required reviewers: `@byxorna` 16 | Request reviews from other people you want to review this PR in the "Reviewers" section on the right. 17 | 18 | > :warning: this PR must have at least 2 thumbs from the [MAINTAINERS.md](/MAINTAINERS.md) of the project before merging! 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | *.swp 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 15 | /.glide/ 16 | 17 | /.gopath~/ 18 | /vendor/ 19 | /bin/ 20 | /test/tests.xml 21 | /test/tests.output 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.13.x" 5 | - "1.14.x" 6 | - "1.15.x" 7 | 8 | env: 9 | - GO111MODULE=on 10 | 11 | before_install: 12 | - rm go.sum 13 | 14 | install: true # skip for now, due to gomodules being annoying 15 | 16 | script: 17 | - make all 18 | - make test 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GO_VERSION=1.15.0 2 | FROM golang:${GO_VERSION}-alpine 3 | 4 | RUN apk --no-cache add \ 5 | ca-certificates \ 6 | make \ 7 | git 8 | 9 | WORKDIR /src 10 | COPY go.mod go.sum Makefile ./ 11 | # run vendor install and lint, so we have all deps installed 12 | RUN make vendor lint 13 | COPY . . 14 | RUN make test all 15 | 16 | FROM alpine:latest 17 | ENV TLS_PORT=9443 \ 18 | LIFECYCLE_PORT=9000 \ 19 | TLS_CERT_FILE=/var/lib/secrets/cert.crt \ 20 | TLS_KEY_FILE=/var/lib/secrets/cert.key 21 | RUN apk --no-cache add ca-certificates bash 22 | COPY --from=0 /src/bin/k8s-sidecar-injector /bin/k8s-sidecar-injector 23 | COPY ./conf /conf 24 | COPY ./entrypoint.sh /bin/entrypoint.sh 25 | ENTRYPOINT ["entrypoint.sh"] 26 | EXPOSE $TLS_PORT $LIFECYCLE_PORT 27 | CMD [] 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2018 Tumblr, Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # k8s-sidecar-injector 2 | 3 | ## Maintainers 4 | 5 | * `@byxorna` 6 | * `@defect` 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PACKAGE = github.com/tumblr/k8s-sidecar-injector 2 | BINARY = k8s-sidecar-injector 3 | DATE ?= $(shell date +%FT%T%z) 4 | BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) 5 | COMMIT ?= $(shell git rev-parse HEAD) 6 | VERSION ?= $(shell git describe --tags --always --dirty --match=v* 2> /dev/null || \ 7 | cat $(CURDIR)/.version 2> /dev/null || echo v0) 8 | BIN = $(GOPATH)/bin 9 | PKGS = $(or $(PKG),$(shell $(GO) list ./... | grep -v "^$(PACKAGE)/vendor/")) 10 | TESTPKGS = $(shell env $(GO) list -f '{{ if or .TestGoFiles .XTestGoFiles }}{{ .ImportPath }}{{ end }}' $(PKGS)) 11 | DOCKER_IMAGE_BASE = tumblr/k8s-sidecar-injector 12 | DOCKER_TAG = $(VERSION) 13 | 14 | export CGO_ENABLED := 0 15 | 16 | GO = go 17 | GODOC = godoc 18 | TIMEOUT = 15 19 | V = 0 20 | Q = $(if $(filter 1,$V),,@) 21 | M = $(shell printf "\033[34;1m▶\033[0m") 22 | 23 | .PHONY: all 24 | all: fmt lint vendor | ; $(info $(M) building executable…) @ ## Build program binary 25 | $Q $(GO) build \ 26 | -tags release \ 27 | -ldflags '-X $(PACKAGE)/internal/pkg/version.Version=$(VERSION) -X $(PACKAGE)/internal/pkg/version.BuildDate=$(DATE) -X $(PACKAGE)/internal/pkg/version.Package=$(PACKAGE) -X $(PACKAGE)/internal/pkg/version.Commit=$(COMMIT) -X $(PACKAGE)/internal/pkg/version.Branch=$(BRANCH)' \ 28 | -o bin/$(BINARY) $(PACKAGE)/cmd \ 29 | && echo "Built bin/$(BINARY): $(VERSION) $(DATE)" 30 | 31 | # Tools 32 | # 33 | 34 | GOLINT = $(BIN)/golint 35 | $(BIN)/golint: | ; $(info $(M) building golint…) 36 | $Q go get golang.org/x/lint/golint 37 | 38 | GOCOVMERGE = $(BIN)/gocovmerge 39 | $(BIN)/gocovmerge: | ; $(info $(M) building gocovmerge…) 40 | $Q go get github.com/wadey/gocovmerge 41 | 42 | GOCOV = $(BIN)/gocov 43 | $(BIN)/gocov: | ; $(info $(M) building gocov…) 44 | $Q go get github.com/axw/gocov/... 45 | 46 | GOCOVXML = $(BIN)/gocov-xml 47 | $(BIN)/gocov-xml: | ; $(info $(M) building gocov-xml…) 48 | $Q go get github.com/AlekSi/gocov-xml 49 | 50 | GO2XUNIT = $(BIN)/go2xunit 51 | $(BIN)/go2xunit: | ; $(info $(M) building go2xunit…) 52 | $Q go get github.com/tebeka/go2xunit 53 | 54 | # Tests 55 | # 56 | 57 | TEST_TARGETS := test-default test-bench test-short test-verbose test-race 58 | .PHONY: $(TEST_TARGETS) test-xml check test tests 59 | test-bench: ARGS=-run=__absolutelynothing__ -bench=. ## Run benchmarks 60 | test-short: ARGS=-short ## Run only short tests 61 | test-verbose: ARGS=-v ## Run tests in verbose mode with coverage reporting 62 | test-race: ARGS=-race ## Run tests with race detector 63 | $(TEST_TARGETS): NAME=$(MAKECMDGOALS:test-%=%) 64 | $(TEST_TARGETS): test 65 | check test tests: fmt lint vendor | ; $(info $(M) running $(NAME:%=% )tests…) @ ## Run tests 66 | $Q $(GO) test -count=1 -timeout=$(TIMEOUT)s $(ARGS) $(TESTPKGS) 67 | 68 | test-xml: fmt lint vendor | $(GO2XUNIT) ; $(info $(M) running $(NAME:%=% )tests…) @ ## Run tests with xUnit output 69 | $Q 2>&1 $(GO) test -count=1 -timeout=$(TIMEOUT)s -v $(TESTPKGS) | tee test/tests.output 70 | $(GO2XUNIT) -fail -input test/tests.output -output test/tests.xml 71 | 72 | COVERAGE_MODE = atomic 73 | COVERAGE_PROFILE = $(COVERAGE_DIR)/profile.out 74 | COVERAGE_XML = $(COVERAGE_DIR)/coverage.xml 75 | COVERAGE_HTML = $(COVERAGE_DIR)/index.html 76 | .PHONY: test-coverage test-coverage-tools 77 | test-coverage-tools: | $(GOCOVMERGE) $(GOCOV) $(GOCOVXML) 78 | test-coverage: COVERAGE_DIR := $(CURDIR)/test/coverage.$(shell date -u +"%Y-%m-%dT%H:%M:%SZ") 79 | test-coverage: fmt lint vendor test-coverage-tools | ; $(info $(M) running coverage tests…) @ ## Run coverage tests 80 | $Q mkdir -p $(COVERAGE_DIR)/coverage 81 | $Q for pkg in $(TESTPKGS); do \ 82 | $(GO) test \ 83 | -coverpkg=$$($(GO) list -f '{{ join .Deps "\n" }}' $$pkg | \ 84 | grep '^$(PACKAGE)/' | grep -v '^$(PACKAGE)/vendor/' | \ 85 | tr '\n' ',')$$pkg \ 86 | -covermode=$(COVERAGE_MODE) \ 87 | -coverprofile="$(COVERAGE_DIR)/coverage/`echo $$pkg | tr "/" "-"`.cover" $$pkg ;\ 88 | done 89 | $Q $(GOCOVMERGE) $(COVERAGE_DIR)/coverage/*.cover > $(COVERAGE_PROFILE) 90 | $Q $(GO) tool cover -html=$(COVERAGE_PROFILE) -o $(COVERAGE_HTML) 91 | $Q $(GOCOV) convert $(COVERAGE_PROFILE) | $(GOCOVXML) > $(COVERAGE_XML) 92 | 93 | .PHONY: docker 94 | docker: | ; $(info $(M) building docker container…) 95 | docker build -t $(DOCKER_IMAGE_BASE):$(DOCKER_TAG) . 96 | docker build -t $(DOCKER_IMAGE_BASE):latest . 97 | 98 | .PHONY: lint 99 | lint: vendor | $(GOLINT) ; $(info $(M) running golint…) 100 | $Q $(GOLINT) $(PKGS) 101 | 102 | .PHONY: fmt 103 | fmt: ; $(info $(M) running gofmt…) @ 104 | $Q $(GO) fmt $(PKGS) 105 | 106 | # Dependency management 107 | 108 | vendor: go.mod go.sum | ; $(info $(M) retrieving dependencies…) 109 | $Q $(GO) mod download 2>&1 110 | $Q $(GO) mod vendor 111 | 112 | .PHONY: clean 113 | clean: ; $(info $(M) cleaning…) @ ## Cleanup everything 114 | @rm -rf vendor/ 115 | @rm -rf bin 116 | @rm -rf test/tests.* test/coverage.* 117 | 118 | .PHONY: help 119 | help: 120 | @grep -E '^[ a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ 121 | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' 122 | 123 | .PHONY: version 124 | version: 125 | @echo $(VERSION) 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # k8s-sidecar-injector 2 | 3 | Uses MutatingAdmissionWebhook in Kubernetes to inject sidecars into new deployments at resource creation time 4 | 5 | ![GitHub release](https://img.shields.io/github/release/tumblr/k8s-sidecar-injector.svg) ![Travis (.org)](https://img.shields.io/travis/tumblr/k8s-sidecar-injector.svg) ![Docker Automated build](https://img.shields.io/docker/automated/tumblr/k8s-sidecar-injector.svg) ![Docker Build Status](https://img.shields.io/docker/build/tumblr/k8s-sidecar-injector.svg) ![MicroBadger Size](https://img.shields.io/microbadger/image-size/tumblr/k8s-sidecar-injector.svg) ![Docker Pulls](https://img.shields.io/docker/pulls/tumblr/k8s-sidecar-injector.svg) ![Docker Stars](https://img.shields.io/docker/stars/tumblr/k8s-sidecar-injector.svg) [![Godoc](https://godoc.org/github.com/tumblr/k8s-sidecar-injector?status.svg)](http://godoc.org/github.com/tumblr/k8s-sidecar-injector) 6 | 7 | 8 | # What is this? 9 | 10 | At Tumblr, we run some containers that have complicated sidecar setups. A kubernetes pod may run 5+ other containers, with some associated volumes and environment variables. It became clear quickly that keeping these sidecars in line would become an operational hassle; making sure every service uses the correct version of each dependency, updating global environment variable sets as configurations in our DCs change, etc. 11 | 12 | To help solve this, we wrote the `k8s-sidecar-injector`. It is a small service that runs in each Kubernetes cluster, and listens to the Kubernetes API via webhooks. For each pod creation, the injector gets a (mutating admission) webhook, asking whether or not to allow the pod launch, and if allowed, what changes we would like to make to it. For pods that have special annotations on them (i.e. `injector.tumblr.com/request=logger:v1`), we rewrite the pod configuration to include the containers, volumes, volume mounts, host aliases, init-containers and environment variables defined in the sidecar `logger:v1`'s configuration. 13 | 14 | This enabled us to keep sane, centralized configuration for oft-used, but infrequently cared about configuration for our sidecars. 15 | 16 | # Configuration 17 | 18 | See [/docs/configuration.md](/docs/configuration.md) to get started with setting up your sidecar injector's configurations. 19 | 20 | # Deployment 21 | 22 | See [/docs/deployment.md](/docs/deployment.md) to see what a sample deployment may look like for you! 23 | 24 | # How it works 25 | 26 | 1. A pod is created. It has annotation `injector.tumblr.com/request=logger:v1` 27 | 2. K8s webhooks out to this service, asking whether to allow this pod creation, and how to mutate it 28 | 3. If the pod is annotated with `injector.tumblr.com/status=injected`: Do nothing! Return "allowed" to pod creation 29 | 4. Pull the "logger:v1" sidecar config, patch the resource, and return it to k8s 30 | 5. Pod will launch in k8s with the modified configuration 31 | 32 | A crappy ASCII diagram will help :) 33 | 34 | ``` 35 | +-----------------+ 36 | +------------------------------+ +----------------+ | | 37 | | | | | | Sidecar | 38 | | MutatingAdmissionWebhook | | Sidecar | | configuration | 39 | | | | ConfigMaps | | files on disk | 40 | +------------+-----------------+ | | | | 41 | | +--------+-------+ +------+----------+ 42 | discover injector | | | 43 | endpoints | watch ConfigMaps | | load from disk 44 | | | | 45 | +-------v--------+ pod launch +---v----------------v-----+ 46 | | +------------------------> | 47 | | Kubernetes | | k8s-sidecar-injector | 48 | | API Server <------------------------+ | 49 | | | mutated pod spec +--------------------------+ 50 | +----------------+ 51 | ``` 52 | 53 | 54 | # Run 55 | 56 | The image is build and published on the Hub at https://hub.docker.com/r/tumblr/k8s-sidecar-injector/. See [/docs/deployment.md](/docs/deployment.md) for how to run this in Kubernetes. 57 | 58 | ## By hand 59 | 60 | This needs some special configuration surrounding the TLS certs, but if you have already read [docs/configuration.md](./docs/configuration.md), you can run this manually with: 61 | 62 | ```bash 63 | $ ./bin/k8s-sidecar-injector --tls-port=9000 --config-directory=conf/ --tls-cert-file="${TLS_CERT_FILE}" --tls-key-file="${TLS_KEY_FILE}" 64 | ``` 65 | 66 | *NOTE*: this is not a supported method of running in production. You are highly encouraged to read [docs/deployment.md](./docs/deployment.md) to deploy this to Kubernetes in The Supported Way. 67 | 68 | # Hacking 69 | 70 | See [hacking.md](/docs/hacking.md) 71 | 72 | # License 73 | 74 | [Apache 2.0](/LICENSE.txt) 75 | 76 | Copyright 2019, Tumblr, Inc. 77 | -------------------------------------------------------------------------------- /cmd/flags.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // MapStringStringFlag is a flag struct for key=value pairs 9 | type MapStringStringFlag struct { 10 | Values map[string]string 11 | } 12 | 13 | // String implements the flag.Var interface 14 | func (s *MapStringStringFlag) String() string { 15 | z := []string{} 16 | for x, y := range s.Values { 17 | z = append(z, fmt.Sprintf("%s=%s", x, y)) 18 | } 19 | return strings.Join(z, ",") 20 | } 21 | 22 | // Set implements the flag.Var interface 23 | func (s *MapStringStringFlag) Set(value string) error { 24 | if s.Values == nil { 25 | s.Values = map[string]string{} 26 | } 27 | for _, p := range strings.Split(value, ",") { 28 | fields := strings.Split(p, "=") 29 | if len(fields) != 2 { 30 | return fmt.Errorf("%s is incorrectly formatted! should be key=value[,key2=value2]", p) 31 | } 32 | s.Values[fields[0]] = fields[1] 33 | } 34 | return nil 35 | } 36 | 37 | // ToMapStringString returns the underlying representation of the map of key=value pairs 38 | func (s *MapStringStringFlag) ToMapStringString() map[string]string { 39 | return s.Values 40 | } 41 | 42 | // NewMapStringStringFlag creates a new flag var for storing key=value pairs 43 | func NewMapStringStringFlag() MapStringStringFlag { 44 | return MapStringStringFlag{Values: map[string]string{}} 45 | } 46 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "os" 11 | "os/signal" 12 | "runtime" 13 | "syscall" 14 | "time" 15 | 16 | "github.com/golang/glog" 17 | "github.com/gorilla/handlers" 18 | "github.com/gorilla/mux" 19 | "github.com/tumblr/k8s-sidecar-injector/internal/pkg/config" 20 | "github.com/tumblr/k8s-sidecar-injector/internal/pkg/config/watcher" 21 | "github.com/tumblr/k8s-sidecar-injector/internal/pkg/version" 22 | "github.com/tumblr/k8s-sidecar-injector/pkg/coalescer" 23 | "github.com/tumblr/k8s-sidecar-injector/pkg/server" 24 | 25 | "github.com/dyson/certman" 26 | ) 27 | 28 | var ( 29 | // EventCoalesceWindow is the window for coalescing events from ConfigMapWatcher 30 | EventCoalesceWindow = time.Second * 3 31 | ) 32 | 33 | // ShowVersion shows the version of the jawner 34 | func ShowVersion(o io.Writer) { 35 | fmt.Fprintf(o, "k8s-sidecar-injector version:%s (commit:%s branch:%s) built on %s with %s\n", version.Version, version.Commit, version.Branch, version.BuildDate, runtime.Version()) 36 | } 37 | 38 | func init() { 39 | // set the glog sev to a reasonable default 40 | flag.Lookup("logtostderr").Value.Set("true") 41 | // disable logging to disk cause thats strange 42 | flag.Lookup("log_dir").Value.Set("") 43 | flag.Lookup("stderrthreshold").Value.Set("INFO") 44 | 45 | flag.Usage = func() { 46 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 47 | ShowVersion(os.Stderr) 48 | flag.PrintDefaults() 49 | } 50 | } 51 | 52 | func main() { 53 | var ( 54 | parameters server.Parameters 55 | ) 56 | cmWatcherLabels := NewMapStringStringFlag() 57 | watcherConfig := watcher.NewConfig() 58 | 59 | // get command line parameters 60 | flag.IntVar(¶meters.LifecyclePort, "lifecycle-port", 9000, "Metrics and introspection port (metrics, healthchecking, etc)") 61 | flag.IntVar(¶meters.TLSPort, "tls-port", 9443, "Webhook server port for handling k8s webhooks (TLS)") 62 | flag.StringVar(¶meters.CertFile, "tls-cert-file", "/var/lib/secrets/cert.pem", "File containing the x509 Certificate for HTTPS.") 63 | flag.StringVar(¶meters.KeyFile, "tls-key-file", "/var/lib/secrets/cert.key", "File containing the x509 private key to --tls-cert-file.") 64 | flag.StringVar(¶meters.ConfigDirectory, "config-directory", "conf/", "Config directory (will load all .yaml files in this directory)") 65 | flag.StringVar(¶meters.AnnotationNamespace, "annotation-namespace", "injector.tumblr.com", "Override the AnnotationNamespace") 66 | flag.StringVar(&watcherConfig.Namespace, "configmap-namespace", "", "Namespace to search for ConfigMaps to load Injection Configs from (default: current namespace)") 67 | flag.Var(&cmWatcherLabels, "configmap-labels", "Label pairs used to discover ConfigMaps in Kubernetes. These should be key1=value[,key2=val2,...]") 68 | flag.StringVar(&watcherConfig.MasterURL, "master-url", "", "Kubernetes master URL (used for running outside of the cluster)") 69 | flag.StringVar(&watcherConfig.Kubeconfig, "kubeconfig", "", "Kubernetes kubeconfig (used only for running outside of the cluster)") 70 | flag.Parse() 71 | 72 | watcherConfig.ConfigMapLabels = cmWatcherLabels.ToMapStringString() 73 | 74 | glog.Infof("Launching k8s-sidecar-injector version=%s commit=%s branch=%s golang=%s\n", version.Version, version.Commit, version.Branch, runtime.Version()) 75 | 76 | glog.V(2).Infof("Loaded server configuration parameters %+v", parameters) 77 | glog.V(2).Infof("Loaded ConfigMap watcher configuration %+v", watcherConfig) 78 | cfg, err := config.LoadConfigDirectory(parameters.ConfigDirectory) 79 | if err != nil { 80 | glog.Errorf("Failed to load configuration: %v", err) 81 | os.Exit(1) 82 | } 83 | if parameters.AnnotationNamespace != "" { 84 | cfg.AnnotationNamespace = parameters.AnnotationNamespace 85 | } 86 | 87 | // wire this up to cancel the context when we get shutdown signal 88 | ctx, cancelContexts := context.WithCancel(context.Background()) 89 | 90 | glog.Infof("Loaded %d injection configs in annotation namespace %s:", len(cfg.Injections), cfg.AnnotationNamespace) 91 | for _, v := range cfg.Injections { 92 | glog.Infof(" %s", v.String()) 93 | } 94 | 95 | // start up the watcher, and get the first batch of ConfigMaps 96 | // to set in the config. 97 | // make sure to union this with any file configs we loaded from disk 98 | configWatcher, err := watcher.New(*watcherConfig) 99 | if err != nil { 100 | glog.Errorf("Error creating ConfigMap watcher: %s", err.Error()) 101 | os.Exit(1) 102 | } 103 | 104 | go func() { 105 | // watch for reconciliation signals, and grab configmaps, then update the running configuration 106 | // for the server 107 | sigChan := make(chan interface{}, 10) 108 | //debouncedChan := make(chan interface{}, 10) 109 | 110 | // debounce events from sigChan, so we dont hammer apiserver on reconciliation 111 | eventsCh := coalescer.Coalesce(ctx, EventCoalesceWindow, sigChan) 112 | 113 | go func() { 114 | for { 115 | glog.Infof("launching watcher for ConfigMaps") 116 | err := configWatcher.Watch(ctx, sigChan) 117 | if err != nil { 118 | switch err { 119 | case watcher.ErrWatchChannelClosed: 120 | glog.Errorf("watcher got error, try to restart watcher: %s", err.Error()) 121 | default: 122 | glog.Fatalf("error watching for new ConfigMaps (terminating): %s", err.Error()) 123 | } 124 | } 125 | } 126 | }() 127 | 128 | for { 129 | select { 130 | case <-eventsCh: 131 | glog.V(1).Infof("triggering ConfigMap reconciliation") 132 | updatedInjectionConfigs, err := configWatcher.Get(ctx) 133 | if err != nil { 134 | glog.Errorf("error reconciling configmaps: %s", err.Error()) 135 | continue 136 | } 137 | glog.V(1).Infof("got %d updated InjectionConfigs from reconciliation", len(updatedInjectionConfigs)) 138 | 139 | newInjectionConfigs := make([]*config.InjectionConfig, len(updatedInjectionConfigs)+len(cfg.Injections)) 140 | { 141 | i := 0 142 | for k := range cfg.Injections { 143 | newInjectionConfigs[i] = cfg.Injections[k] 144 | i++ 145 | } 146 | for i, watched := range updatedInjectionConfigs { 147 | newInjectionConfigs[i+len(cfg.Injections)] = watched 148 | } 149 | } 150 | 151 | glog.V(1).Infof("updating server with newly loaded configurations (%d loaded from disk, %d loaded from k8s api)", len(cfg.Injections), len(updatedInjectionConfigs)) 152 | cfg.ReplaceInjectionConfigs(newInjectionConfigs) 153 | glog.V(1).Infof("configuration replaced") 154 | } 155 | } 156 | 157 | }() 158 | 159 | // web server listening for healthchecks, metrics requests, etc 160 | lifecycleServer := &http.Server{ 161 | Addr: fmt.Sprintf(":%v", parameters.LifecyclePort), 162 | } 163 | 164 | // web server terminating TLS for handling k8s webhooks 165 | whsvr := &server.WebhookServer{ 166 | Config: cfg, 167 | Server: &http.Server{ 168 | Addr: fmt.Sprintf(":%v", parameters.TLSPort), 169 | }, 170 | } 171 | 172 | if parameters.CertFile != "" && parameters.KeyFile != "" { 173 | cm, err := certman.New(parameters.CertFile, parameters.KeyFile) 174 | if err != nil { 175 | glog.Errorf("Failed to load key pair: %v", err) 176 | os.Exit(1) 177 | } 178 | if err := cm.Watch(); err != nil { 179 | glog.Errorf("Failed to start watcher on key pair: %v", err) 180 | os.Exit(1) 181 | } 182 | whsvr.Server.TLSConfig = &tls.Config{GetCertificate: cm.GetCertificate} 183 | } 184 | 185 | // define secure mux for routing requests that come in over our TLS port 186 | secureMux := mux.NewRouter() 187 | secureMux.Handle("/mutate", whsvr.MutateHandler()) 188 | secureMux.Handle("/health", whsvr.HealthHandler()) 189 | loggedSecureRouter := handlers.CombinedLoggingHandler(os.Stdout, secureMux) 190 | whsvr.Server.Handler = loggedSecureRouter 191 | 192 | // start webhook server in new rountine 193 | glog.Infof("Launching sidecar injector server (http+tls) on :%d", parameters.TLSPort) 194 | go func() { 195 | if parameters.CertFile != "" && parameters.KeyFile != "" { 196 | if err := whsvr.Server.ListenAndServeTLS("", ""); err != nil { 197 | glog.Errorf("Failed to listen and serve webhook server (http+tls): %v", err) 198 | os.Exit(1) 199 | } 200 | } else { 201 | if err := whsvr.Server.ListenAndServe(); err != nil { 202 | glog.Errorf("Failed to listen and serve webhook server (http): %v", err) 203 | os.Exit(1) 204 | } 205 | } 206 | }() 207 | 208 | // define an insecure mux that handles lifecycle requests 209 | insecureMux := mux.NewRouter() 210 | insecureMux.Handle("/metrics", whsvr.MetricsHandler()) 211 | insecureMux.Handle("/health", whsvr.HealthHandler()) 212 | loggedInsecureRouter := handlers.CombinedLoggingHandler(os.Stdout, insecureMux) 213 | lifecycleServer.Handler = loggedInsecureRouter 214 | 215 | // start webhook server in new rountine 216 | glog.Infof("Launching lifecycle server (http) on :%d", parameters.LifecyclePort) 217 | go func() { 218 | if err := lifecycleServer.ListenAndServe(); err != nil { 219 | glog.Errorf("Failed to listen and serve lifecycle http server: %v", err) 220 | os.Exit(1) 221 | } 222 | }() 223 | 224 | // listening OS shutdown singal 225 | signalChan := make(chan os.Signal, 1) 226 | signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) 227 | <-signalChan 228 | 229 | glog.Infof("Got OS shutdown signal, shutting down webhook server gracefully...") 230 | whsvr.Server.Shutdown(ctx) 231 | cancelContexts() 232 | } 233 | -------------------------------------------------------------------------------- /conf/sidecar-test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: sidecar-test 3 | env: 4 | - name: NEW_ENV 5 | value: "injected-new-env" 6 | - name: DATACENTER 7 | value: "from-injector" 8 | containers: 9 | - name: sidecar-nginx 10 | image: nginx:1.12.2 11 | imagePullPolicy: IfNotPresent 12 | env: 13 | - name: DATACENTER 14 | value: "from-sidecar" 15 | ports: 16 | - containerPort: 80 17 | volumeMounts: 18 | - name: nginx-conf 19 | mountPath: /etc/nginx 20 | volumes: 21 | - name: nginx-conf 22 | configMap: 23 | name: nginx-configmap 24 | -------------------------------------------------------------------------------- /docs/configmaps.md: -------------------------------------------------------------------------------- 1 | # ConfigMaps 2 | 3 | The `k8s-sidecar-injector` is able to read sidecar configuration from kubernetes via `ConfigMap`s, in addition to files on disk. This document describes how the mechanism works. 4 | 5 | ## Configuration 6 | 7 | There are 2 flags that control the injector's behavior regarding loading sidecars from configmaps. 8 | 9 | * `--configmap-namespace`: defaults to the namespace the service is running in. This namespace is searched for configmaps to load 10 | * `--configmap-labels=key=value[,key2=value2]`: the labels used to discover configmaps in the API. This is used for a watch, so any new configmaps are loaded when they are created. 11 | 12 | These are controlled by `$CONFIGMAP_LABELS` and `$CONFIGMAP_NAMESPACE` in the default entrypoint and deployment. 13 | 14 | ## ConfigMap Format 15 | 16 | A ConfigMap should look like the following; multiple sidecar configs may live in a single ConfigMap: 17 | 18 | ```yaml 19 | --- 20 | apiVersion: v1 21 | kind: ConfigMap 22 | metadata: 23 | name: test-injectionconfig1 24 | namespace: default 25 | labels 26 | app: k8s-sidecar-injector 27 | data: 28 | sidecar-v1: | 29 | name: sidecar-v1 30 | volumes: [] 31 | containers: [] 32 | env: [] 33 | another-sidecar-v1: | 34 | name: another-sidecar 35 | env: [] 36 | ``` 37 | 38 | Please note, the `labels` must match the provided `$CONFIGMAP_LABELS`, so the injector can discover these ConfigMaps. Additionally, make sure the `namespace` jives with the `$CONFIGMAP_NAMESPACE` (or if omitted, the ConfigMap is in the same namespace as the sidecar injector). 39 | 40 | See [/docs/sidecar-configuration-format.md](/docs/sidecar-configuration-format.md) for more details on the schema for a Sidecar Configuration. 41 | 42 | ## Authentication to read ConfigMaps 43 | 44 | The `k8s-sidecar-injector` uses in-cluster discovery of the API, and `ServiceAccount` authentication, which is controlled by the following flags 45 | 46 | * `--kubeconfig`: which kubeconfig to use. If omitted, uses in-cluster discovery 47 | * `--master-url`: which kubernetes master to use. If omitted, uses in-cluster discovery 48 | 49 | By default, we use `ServiceAccount`s. For this reason, make sure your deployment has 50 | `serviceAccountName: k8s-sidecar-injector`, and you have created the appropriate `ClusterRole`s: 51 | 52 | ```yaml 53 | apiVersion: rbac.authorization.k8s.io/v1 54 | kind: ClusterRole 55 | metadata: 56 | name: k8s-sidecar-injector 57 | rules: 58 | - apiGroups: [""] 59 | resources: ["configmaps"] 60 | verbs: ["get","watch","list"] 61 | $ cat clusterrolebinding.yaml 62 | apiVersion: rbac.authorization.k8s.io/v1 63 | kind: ClusterRoleBinding 64 | metadata: 65 | name: k8s-sidecar-injector 66 | subjects: 67 | - kind: ServiceAccount 68 | name: k8s-sidecar-injector 69 | namespace: kube-system 70 | roleRef: 71 | kind: ClusterRole 72 | name: k8s-sidecar-injector 73 | apiGroup: rbac.authorization.k8s.io 74 | ``` 75 | 76 | ## Watching 77 | 78 | This illustrates the injector discovering a new ConfigMap with matching labels, and hot-loading it into the running server: 79 | 80 | ``` 81 | I1113 15:38:41.140753 1 watcher.go:110] event: ADDED &TypeMeta{Kind:,APIVersion:,} 82 | I1113 15:38:41.140809 1 watcher.go:118] signalling event received from watch channel: ADDED &TypeMeta{Kind:,APIVersion:,} 83 | I1113 15:38:41.140837 1 coalescer.go:27] got reconciliation signal, debouncing for 3s 84 | 10.246.219.242 - - [13/Nov/2018:15:38:41 +0000] "GET /metrics HTTP/1.1" 200 1768 "" "Prometheus/2.2.1" 85 | 10.246.219.236 - - [13/Nov/2018:15:38:43 +0000] "GET /metrics HTTP/1.1" 200 1770 "" "Prometheus/2.2.1" 86 | I1113 15:38:44.140992 1 coalescer.go:21] signalling reconciliation after 3s 87 | I1113 15:38:44.141054 1 main.go:119] triggering ConfigMap reconciliation 88 | I1113 15:38:44.141081 1 watcher.go:141] Fetching ConfigMaps... 89 | I1113 15:38:44.151642 1 watcher.go:148] Fetched 1 ConfigMaps 90 | I1113 15:38:44.151670 1 watcher.go:166] Parsing kube-system/sidecar-test-gabe:test-gabe into InjectionConfig 91 | I1113 15:38:44.151921 1 config.go:139] Loaded injection config env1 sha256sum=a474541e6ea04b5a134f4cf39ee2948484fa9d6c4226514128705d0ba3921c4b 92 | I1113 15:38:44.151951 1 watcher.go:171] Loaded InjectionConfig env1 from ConfigMap sidecar-test-gabe:test-gabe 93 | I1113 15:38:44.151962 1 watcher.go:154] Found 1 InjectionConfigs in sidecar-test-gabe 94 | I1113 15:38:44.151975 1 main.go:125] got 1 updated InjectionConfigs from reconciliation 95 | I1113 15:38:44.151987 1 main.go:139] updating server with newly loaded configurations (5 loaded from disk, 1 loaded from k8s api) 96 | I1113 15:38:44.152004 1 main.go:141] configuration replaced 97 | ``` 98 | 99 | 100 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Runtime Configuration 2 | 3 | The sidecar injector has a few needs to get sorted before being able to start injecting sidecars: 4 | 5 | 1. Generate TLS certs: [/docs/tls.md](/docs/tls.md) 6 | 2. `MutatingWebhookConfiguration` resources - this is dependent on the previous step. See [/docs/deployment.md](/docs/deployment.md) 7 | 3. Sidecar Configurations - Up to you how you manage these; file or ConfigMap - see [/docs/sidecar-configuration-format.md](/docs/sidecar-configuration-format.md) 8 | 4. Deploy to Kubernetes - See [/docs/deployment.md](/docs/deployment.md) 9 | 10 | Once you have these sorted out, you should be ready to rock! 11 | -------------------------------------------------------------------------------- /docs/deployment.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | 3 | Example Kubernetes manifests are provided in [/examples/kubernetes](/examples/kubernetes). You are expected to tailor these to your needs. Specifically, you will need to: 4 | 5 | 1. Generate TLS certs [/docs/tls.md](/docs/tls.md) and update [/examples/kubernetes/mutating-webhook-configuration.yaml](/examples/kubernetes/mutating-webhook-configuration.yaml) with the `caBundle` 6 | 2. Update [/examples/kubernetes/deployment.yaml](/examples/kubernetes/deployment.yaml) with the appropriate version you want to deploy 7 | 3. Specify whatever flags you want in the deployment.yaml 8 | 4. Create a kubernetes secret from the certificates that you generated as a part of [/docs/tls.md](/docs/tls.md). 9 | ``` 10 | kubectl create secret generic k8s-sidecar-injector --from-file=examples/tls/${DEPLOYMENT}/${CLUSTER}/sidecar-injector.crt --from-file=examples/tls/${DEPLOYMENT}/${CLUSTER}/sidecar-injector.key --namespace=kube-system 11 | ``` 12 | 5. Create ConfigMaps (or sidecar config files on disk somewhere) so the injector has some sidecars to inject :) [/docs/configmaps.md](/docs/configmaps.md) 13 | 14 | Once you hack the example Kubernetes manifests to work for your deployment, deploy them to your cluster. The list of manifests you should deploy are below: 15 | 16 | * [clusterrole.yaml](/examples/kubernetes/clusterrole.yaml) 17 | * [clusterrolebinding.yaml](/examples/kubernetes/clusterrolebinding.yaml) 18 | * [service-monitor.yaml](/examples/kubernetes/service-monitor.yaml) 19 | * [serviceaccount.yaml](/examples/kubernetes/serviceaccount.yaml) 20 | * [service.yaml](/examples/kubernetes/service.yaml) 21 | * [deployment.yaml](/examples/kubernetes/deployment.yaml) 22 | * [mutating-webhook-configuration.yaml](/examples/kubernetes/mutating-webhook-configuration.yaml) 23 | 24 | A sample ConfigMap is included to test injections at [/examples/kubernetes/configmap-sidecar-test.yaml](/examples/kubernetes/configmap-sidecar-test.yaml). 25 | 26 | Add it to the cluster, and you should see it show up in the logs for the sidecar injector. 27 | 28 | ```bash 29 | $ kubectl create -f examples/kubernetes/configmap-sidecar-test.yaml 30 | configmap/sidecar-test created 31 | $ kubectl logs --tail=60 -n kube-system -l k8s-app=k8s-sidecar-injector 32 | ... 33 | I1119 16:25:10.782478 1 main.go:124] triggering ConfigMap reconciliation 34 | I1119 16:25:10.782536 1 watcher.go:140] Fetching ConfigMaps... 35 | I1119 16:25:10.792451 1 watcher.go:147] Fetched 1 ConfigMaps 36 | I1119 16:25:10.792757 1 watcher.go:168] Loaded InjectionConfig test1 from ConfigMap sidecar-test:test1 37 | I1119 16:25:10.792778 1 watcher.go:153] Found 1 InjectionConfigs in sidecar-test 38 | I1119 16:25:10.792788 1 main.go:130] got 1 updated InjectionConfigs from reconciliation 39 | I1119 16:25:10.792800 1 main.go:144] updating server with newly loaded configurations (5 loaded from disk, 1 loaded from k8s api) 40 | I1119 16:25:10.792813 1 main.go:146] configuration replaced 41 | ... 42 | ``` 43 | 44 | Now, you are ready to create your first pod that asks for an injection: 45 | 46 | ```bash 47 | $ kubectl create -f examples/kubernetes/debug-pod.yaml 48 | pod/debian-debug created 49 | ``` 50 | 51 | Verify its up and running; note the `injector.tumblr.com/status: injected` label, indicating the pod had its sidecar added successfully, as well as the added environment variables, and additional `sidecar-nginx` container! 52 | 53 | ```bash 54 | $ kubectl describe -f debug-pod.yaml 55 | Name: debian-debug 56 | Namespace: default 57 | ... 58 | Annotations: injector.tumblr.com/status: injected 59 | Status: Running 60 | IP: 10.246.248.115 61 | Containers: 62 | debian-debug: 63 | Image: debian:jessie 64 | Command: 65 | sleep 66 | 3600 67 | State: Running 68 | Started: Mon, 19 Nov 2018 11:28:36 -0500 69 | Ready: True 70 | Restart Count: 0 71 | Environment: 72 | HELLO: world 73 | sidecar-nginx: 74 | Image: nginx:1.12.2 75 | Port: 80/TCP 76 | Host Port: 0/TCP 77 | State: Running 78 | Started: Mon, 19 Nov 2018 11:28:40 -0500 79 | Ready: True 80 | Environment: 81 | HELLO: world 82 | Mounts: 83 | ... 84 | ``` 85 | 86 | 87 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | # Design 2 | 3 | Just some random notes about the design of the sidecar injector 4 | 5 | ## Mixin Injections 6 | 7 | On the note of why the `k8s-sidecar-injector` does not support multiple injections (aka "mixin" injections), https://github.com/tumblr/k8s-sidecar-injector/issues/17 articulates my thought process during design fairly well. Copying the body of the issue for posterity: 8 | 9 | ### Q 10 | 11 | > Hi, I've been testing your sidecar-injector and just wonder if there is a way to request more than one sidecar injection configurations? 12 | > Multiple annotation "injector.tumblr.com/request" with request assigned to the different names won't raise an error but only the last injection will be applied. 13 | 14 | ### A 15 | 16 | > Great question - this was an explicit design decision I made when writing the injector initially. My thinking was that: while a technically possible and useful feature, it made me nervous about understandabiliity. Supporting "mixin" injections would be neat, but it becomes much more difficult to understand what the injected pod would look like when you need to union multiple injection configs in your head with your pod spec. Further, I was not sure there was an intuitive way to handle merging injection configs that have overlap (i.e. overlapping and conflicting env vars, volumes with diff mount flags, etc). 17 | > Ultimately, I wanted to make sure this was an uninteresting, uncomplicated part of our infrastructure, and not overly clever. We have internal uses that could benefit from multiple injections, but we instead maintain separate config files including the duplication. I have found its easier for new engineers to grok behavior with this design. 18 | -------------------------------------------------------------------------------- /docs/hacking.md: -------------------------------------------------------------------------------- 1 | # Hacking 2 | 3 | ## Build 4 | 5 | Want to build this thing yourself? 6 | 7 | ```bash 8 | $ make 9 | 10 | $ ./bin/k8s-sidecar-injector --help 11 | ``` 12 | 13 | Building the docker image is accomplished by `make docker` 14 | 15 | ## Tests 16 | 17 | ```bash 18 | $ make test 19 | ``` 20 | 21 | 22 | 23 | ## Image build 24 | 25 | The image is build and published on the Hub at https://hub.docker.com/r/tumblr/k8s-sidecar-injector/. See [/docs/deployment.md](/docs/deployment.md) for how to run this in Kubernetes. 26 | 27 | ``` 28 | $ make docker 29 | ``` 30 | 31 | ## Run By Hand 32 | 33 | This needs some special configuration surrounding the TLS certs, but if you have already read [docs/configuration.md](./docs/configuration.md), you can run this manually with: 34 | 35 | ```bash 36 | $ ./bin/k8s-sidecar-injector --tls-port=9000 --config-directory=conf/ --tls-cert-file="${TLS_CERT_FILE}" --tls-key-file="${TLS_KEY_FILE}" 37 | ``` 38 | 39 | NOTE: this is not a supported method of running in production. You are highly encouraged to read [docs/deployment.md](./docs/deployment.md) to deploy this to Kubernetes in The Supported Way. 40 | 41 | -------------------------------------------------------------------------------- /docs/sidecar-configuration-format.md: -------------------------------------------------------------------------------- 1 | # Sidecar Configuration Format 2 | 3 | Config is can be loaded from 2 sources: 4 | * `--config-directory`: load all YAML configs that define a sidecar configuration 5 | * Kubernetes ConfigMaps: `--configmap-labels` and `--configmap-namespace` controls how the injector finds ConfigMaps to load sidecar configurations from 6 | 7 | A sidecar configuration looks like: 8 | 9 | ```yaml 10 | --- 11 | # sidecar configs are identified by a requesting 12 | # annotation, like: 13 | # "injector.tumblr.com/request=tumblr-php" 14 | # the "name: tumblr-php" must match a configuration below; 15 | 16 | # "name" identifies this sidecar uniquely to the injector. NOTE: it is an error to load 17 | # 2 configuration with the same name! You may include version information in the name to disambiguate 18 | # between newer versions of the same sidecar. For example: 19 | # name: my-sidecar:v1.2 20 | # indicates "my-sidecar" is version "1.2". A request for `injector.tumblr.com/request: my-sidecar:v1.2` 21 | # will return this configuration. If the version information is omitted, "latest" is assumed. 22 | # `name: "test"` implies `name: test:latest`. 23 | # * `injector.tumblr.com/request: my-sidecar` => `my-sidecar:latest` 24 | # * `injector.tumblr.com/request: my-sidecar:latest` => `my-sidecar:latest` 25 | # * `injector.tumblr.com/request: my-sidecar:v1.2` => `my-sidecar:v1.2` 26 | name: "test:v1.2" 27 | 28 | # Each InjectionConfig is a struct that adheres to kubernetes' volume and containers 29 | # spec. Any volumes injected are scoped to the namespace that the 30 | # resource exists within 31 | 32 | # Optionally, you can inherit from another sidecar configuration. This is useful to reduce 33 | # duplication in your sidecars. Fields that appear in this config will override and replace 34 | # fields in the inherited sidecar. We intelligently merge list fields as well, so top level 35 | # keys are not blindly replaced, but merged instead. 36 | # `inherits` is a file on disk to load the parent config from. 37 | # NOTE: `inherits` is not supported when loading InjectionConfigs from ConfigMap 38 | # NOTE: this is relative to the current file, and does not allow for absolute pathing! 39 | inherits: "some-sidecar.yaml" 40 | 41 | containers: 42 | # we inject a nginx container 43 | - name: sidecar-nginx 44 | image: nginx:1.12.2 45 | imagePullPolicy: IfNotPresent 46 | ports: 47 | - containerPort: 80 48 | volumeMounts: 49 | - name: nginx-conf 50 | mountPath: /etc/nginx 51 | 52 | # serviceAccountName is optional - if specified, it will set (but not overwrite an existing!) 53 | # serviceAccountName field in your pod. Please note, that due to https://github.com/kubernetes/kubernetes/pull/78080 54 | # if you use this feature on k8s < 1.15.0, your sidecars will not get properly initialized with the associated 55 | # secret volume mounts for this serviceaccount, due to the ServiceAccountController running before 56 | # the MutatingWebhookAdmissionController in older versions of k8s, as well as not _rerunning_ after the MWAC to 57 | # populate volumes on containers that were added by the injector. 58 | serviceAccountName: "someserviceaccount" 59 | 60 | volumes: 61 | - name: nginx-conf 62 | configMap: 63 | name: nginx-configmap 64 | - name: some-config 65 | configMap: 66 | name: some-configmap 67 | 68 | # hostAliases are not being merged, only added, as they only add entries to /etc/hosts in the containers. 69 | # Duplicate entries won't throw an error. 70 | # hostAliases are used for the whole pod. 71 | hostAliases: 72 | - ip: 1.2.3.4 73 | hostnames: 74 | - somehost.example.com 75 | - anotherhost.example.com 76 | 77 | # all environment variables defined here will be added to containers _only_ if the .Name 78 | # is not already present (we will not replace an env var, only add them) 79 | # These will be inserted into each container in the pod, including any containers added via 80 | # injection. The same applies to volumeMounts. 81 | env: 82 | - name: DATACENTER 83 | value: "dc01" 84 | 85 | # all volumeMounts defined here will be added to containers, if the .name attribute 86 | # does not already exist in the list of volumeMounts, i.e. no replacement will be done. 87 | # They will be added to each container, including the ones added via injection. 88 | # This behaviour is the same for environment variables. 89 | volumeMounts: 90 | - name: some-config 91 | mountPath: /etc/some-config 92 | 93 | # initContainers will be added, no replacement of existing initContainers with the same names will be done 94 | # this works exactly the same way like adding normal containers does: if you have a conflicting name, 95 | # the server will return an error 96 | initContainers: 97 | - name: some-initcontainer 98 | image: init:1.12.2 99 | imagePullPolicy: IfNotPresent 100 | ``` 101 | 102 | ## Configuring new sidecars 103 | 104 | In order for the injector to know about a sidecar configuration, you need to either give it a yaml file to describe the sidecar, or create ConfigMaps in Kubernetes (that contain t he YAML config for the sidecar). 105 | 106 | 1. Create a new InjectionConfiguration `yaml` 107 | 1. Specify your `name:`. This is what you will request with `injector.tumblr.com/request=$name` 108 | 2. Fill in the `containers`, `volumes`, `volumeMounts`, `hostAliases`, `initContainers`, `serviceAccountName`, and `env` fields with your configuration you want injected 109 | 2. Either bake your yaml into your Docker image you run (in `--config-directory=conf/`), or configure it as a ConfigMap in your k8s cluster. See [/docs/configmaps.md](/docs/configmaps.md) for information on how to configure a ConfigMap. 110 | 3. Deploy a pod with annotation `injector.tumblr.com/request=$name`! 111 | -------------------------------------------------------------------------------- /docs/tls.md: -------------------------------------------------------------------------------- 1 | # Generating TLS Certs 2 | 3 | Certs are needed to setup the sidecar injector. Because these are not needed to be signed by the k8s apiserver, nor by a valid Certificate Authority, we provide some helpful scripts in `examples/tls/` to create some selfsigned certs for use with this application. 4 | 5 | ## Edit your CSR configs 6 | 7 | To generate new certs for a new deployment, first edit the `ca.conf` and `csr-prod.conf` and tweak them to your liking. 8 | 9 | You can use `sed` to do this easily, by setting `DOMAIN=yourdomain.com` and `ORG="Your Org Name, Inc."`: 10 | 11 | ```bash 12 | $ export ORG="..." DOMAIN="..." 13 | $ sed -i '' -e "s|__ORG__|$ORG|g" -e "s|__DOMAIN__|$DOMAIN|g" ca.conf csr-prod.conf 14 | ``` 15 | 16 | ## Generate Certs 17 | 18 | Next, set a reasonable value for your deployment's `DEPLOYMENT=` (This is your deployment zone. This can be a physical DC identifier, availability zone, or whatever you term your identifer for geographic blast zones. i.e. `us-east-1` or `dc01`), and your `CLUSTER=` (cluster identifier. $DEPLOYMENT-$CLUSTER uniquely identifies your deployment of k8s). Then, run the script to generate the certs. 19 | 20 | Lets take `DEPLOYMENT=us-east-1` and `CLUSTER=PRODUCTION` in our example: 21 | 22 | ```bash 23 | $ cd examples/tls/ 24 | $ DEPLOYMENT=us-east-1 CLUSTER=PRODUCTION ./new-cluster-injector-cert.rb 25 | ``` 26 | 27 | This will generate all the files necessary for a new CA, and the k8s-sidecar-injector cert! 28 | 29 | ```bash 30 | $ git ls-files -o 31 | .srl 32 | us-east-1/PRODUCTION/ca.crt 33 | us-east-1/PRODUCTION/ca.key 34 | us-east-1/PRODUCTION/sidecar-injector.crt 35 | us-east-1/PRODUCTION/sidecar-injector.csr 36 | us-east-1/PRODUCTION/sidecar-injector.key 37 | ``` 38 | 39 | Now, see the next section to configure the MutatingWebhookConfiguration with the proper certificate :) 40 | 41 | # MutatingWebhookConfiguration 42 | 43 | The [MutatingWebhookConfiguration](/examples/kubernetes/mutating-webhook-configuration.yaml) needs to know what `ca.crt` is used to sign the certs used to terminate TLS by the service. So, we need to extract the `caBundle` from your generated certificates in the previous step, and set it in [MutatingWebhookConfiguration](/examples/kubernetes/mutating-webhook-configuration.yaml) 44 | 45 | Keeping with our `DEPLOYMENT=us-east-1` and `CLUSTER=PRODUCTION` example: 46 | 47 | ```bash 48 | $ cd examples/tls 49 | $ CABUNDLE_BASE64="$(cat $DEPLOYMENT/$CLUSTER/ca.crt |base64|tr -d '\n')" 50 | $ echo $CABUNDLE_BASE64 51 | LS0tLS1CRUdJTi..........= 52 | ``` 53 | 54 | Now, take this data and set it into the mutating webhook config as the `caBundle:` value. 55 | 56 | ```bash 57 | $ sed -i '' -e "s|__CA_BUNDLE_BASE64__|$CABUNDLE_BASE64|g" examples/kubernetes/mutating-webhook-configuration.yaml 58 | 59 | ``` 60 | 61 | Once this is done, you are ready to head back to [deployment.md](/docs/deployment.md)! 62 | 63 | -------------------------------------------------------------------------------- /docs/unit-tests.md: -------------------------------------------------------------------------------- 1 | # Unit Tests 2 | 3 | To add a new unit test for some behavior, please do the following: 4 | 5 | 1. create a AdmissionRequest in YAML format at `test/fixtures/k8s/admissioncontrol/request/foo.yaml`. This should include the pod spec k8s will send with the request, and the annotation with the desired injected sidecar 6 | 2. create a Patch JSON at `test/fixtures/k8s/admissioncontrol/patch/foo.json`. 7 | 3. register your test in the `pkg/server/webhook_test.go` list `mutationTests` 8 | 9 | Please use the `injector.unittest.com/request` annotation on your `AdmissionRequest` YAML to signal which sidecar you want to be injected. 10 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | LIFECYCLE_PORT="${LIFECYCLE_PORT:-9000}" 4 | TLS_PORT="${TLS_PORT:-9443}" 5 | CONFIG_DIR="${CONFIG_DIR:-/conf}" 6 | TLS_CERT_FILE="${TLS_CERT_FILE:-/var/lib/secrets/cert.crt}" 7 | TLS_KEY_FILE="${TLS_KEY_FILE:-/var/lib/secrets/cert.key}" 8 | ANNOTATION_NAMESPACE="${ANNOTATION_NAMESPACE:-injector.tumblr.com}" 9 | CONFIGMAP_LABELS="${CONFIGMAP_LABELS:-app=k8s-sidecar-injector}" 10 | CONFIGMAP_NAMESPACE="${CONFIGMAP_NAMESPACE:-}" 11 | ANNOTATION_NAMESPACE="${ANNOTATION_NAMESPACE:-injector.tumblr.com}" 12 | LOG_LEVEL="${LOG_LEVEL:-2}" 13 | echo "k8s-sidecar-injector starting at $(date) with TLS_PORT=${TLS_PORT} CONFIG_DIR=${CONFIG_DIR} TLS_CERT_FILE=${TLS_CERT_FILE} TLS_KEY_FILE=${TLS_KEY_FILE}" 14 | set -x 15 | exec k8s-sidecar-injector \ 16 | --v="${LOG_LEVEL}" \ 17 | --lifecycle-port="${LIFECYCLE_PORT}" \ 18 | --tls-port="${TLS_PORT}" \ 19 | --config-directory="${CONFIG_DIR}" \ 20 | --tls-cert-file="${TLS_CERT_FILE}" \ 21 | --tls-key-file="${TLS_KEY_FILE}" \ 22 | --configmap-labels="${CONFIGMAP_LABELS}" \ 23 | --configmap-namespace="${CONFIGMAP_NAMESPACE}" \ 24 | --annotation-namespace="${ANNOTATION_NAMESPACE}" \ 25 | "$@" 26 | -------------------------------------------------------------------------------- /examples/kubernetes/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: k8s-sidecar-injector 5 | rules: 6 | - apiGroups: [""] 7 | resources: ["configmaps"] 8 | verbs: ["get","watch","list"] 9 | -------------------------------------------------------------------------------- /examples/kubernetes/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: k8s-sidecar-injector 5 | subjects: 6 | - kind: ServiceAccount 7 | name: k8s-sidecar-injector 8 | namespace: kube-system 9 | roleRef: 10 | kind: ClusterRole 11 | name: k8s-sidecar-injector 12 | apiGroup: rbac.authorization.k8s.io 13 | -------------------------------------------------------------------------------- /examples/kubernetes/configmap-sidecar-test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: sidecar-test 6 | namespace: kube-system 7 | labels: 8 | app: k8s-sidecar-injector 9 | track: test 10 | data: 11 | test1: | 12 | name: test1 13 | env: 14 | - name: HELLO 15 | value: world 16 | - name: TEST 17 | value: test_that 18 | volumeMounts: 19 | - name: test-vol 20 | mountPath: /tmp/test 21 | volumes: 22 | - name: test-vol 23 | configMap: 24 | name: test-config 25 | containers: 26 | - name: sidecar-nginx 27 | image: nginx:1.12.2 28 | imagePullPolicy: IfNotPresent 29 | ports: 30 | - containerPort: 80 31 | env: 32 | - name: ENV_IN_SIDECAR 33 | value: test-in-sidecar 34 | --- 35 | # configmap to test sharing a volume between sidecar and existing container 36 | apiVersion: v1 37 | kind: ConfigMap 38 | metadata: 39 | name: test-config 40 | namespace: default 41 | data: 42 | test.txt: | 43 | this is some test message shared between containers 44 | -------------------------------------------------------------------------------- /examples/kubernetes/debug-pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: debian-debug 5 | namespace: default 6 | annotations: 7 | injector.tumblr.com/request: test1 8 | spec: 9 | containers: 10 | - image: debian:jessie 11 | command: 12 | - sleep 13 | - "3600" 14 | imagePullPolicy: IfNotPresent 15 | name: debian-debug 16 | restartPolicy: Never 17 | -------------------------------------------------------------------------------- /examples/kubernetes/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: "k8s-sidecar-injector-prod" 5 | namespace: "kube-system" 6 | labels: 7 | k8s-app: "k8s-sidecar-injector" 8 | track: "prod" 9 | spec: 10 | replicas: 2 11 | strategy: 12 | type: RollingUpdate 13 | rollingUpdate: 14 | maxSurge: 1 15 | maxUnavailable: 0 16 | template: 17 | metadata: 18 | labels: 19 | k8s-app: "k8s-sidecar-injector" 20 | track: "prod" 21 | spec: 22 | serviceAccountName: k8s-sidecar-injector 23 | volumes: 24 | - name: secrets 25 | secret: 26 | secretName: k8s-sidecar-injector 27 | containers: 28 | - name: "k8s-sidecar-injector" 29 | imagePullPolicy: Always 30 | image: tumblr/k8s-sidecar-injector:latest 31 | command: ["entrypoint.sh"] 32 | args: [] 33 | ports: 34 | - name: https 35 | containerPort: 9443 36 | - name: http-metrics 37 | containerPort: 9000 38 | volumeMounts: 39 | - name: secrets 40 | mountPath: /var/lib/secrets 41 | livenessProbe: 42 | httpGet: 43 | scheme: HTTPS 44 | path: /health 45 | port: https 46 | initialDelaySeconds: 10 47 | periodSeconds: 10 48 | timeoutSeconds: 3 49 | resources: 50 | requests: 51 | cpu: "0.5" 52 | memory: 1Gi 53 | limits: 54 | cpu: "0.5" 55 | memory: 2Gi 56 | env: 57 | - name: "TLS_CERT_FILE" 58 | value: "/var/lib/secrets/sidecar-injector.crt" 59 | - name: "TLS_KEY_FILE" 60 | value: "/var/lib/secrets/sidecar-injector.key" 61 | - name: "LOG_LEVEL" 62 | value: "2" 63 | - name: "CONFIG_DIR" 64 | value: "conf/" 65 | - name: "CONFIGMAP_LABELS" 66 | value: "app=k8s-sidecar-injector" 67 | -------------------------------------------------------------------------------- /examples/kubernetes/mutating-webhook-configuration.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1beta1 2 | kind: MutatingWebhookConfiguration 3 | metadata: 4 | name: "tumblr-sidecar-injector-webhook" 5 | labels: 6 | app: k8s-sidecar-injector 7 | track: prod 8 | webhooks: 9 | - name: "injector.tumblr.com" 10 | failurePolicy: "Ignore" # we fail "open" if the webhook is down hard 11 | rules: 12 | - operations: [ "CREATE" ] 13 | apiGroups: [""] 14 | apiVersions: ["v1"] 15 | resources: ["pods"] 16 | clientConfig: 17 | # https://github.com/kubernetes/kubernetes/blob/v1.10.0-beta.1/staging/src/k8s.io/api/admissionregistration/v1beta1/types.go#L218 18 | # note: k8s is smart enough to use 443 or the only exposed port on the service 19 | # note: this requires the service to serve TLS directly (not thru ingress) 20 | service: 21 | name: "k8s-sidecar-injector-prod" 22 | namespace: "kube-system" 23 | path: "/mutate" # what /url/slug to send requests at 24 | # See README.md for how this was generated! 25 | caBundle: "__CA_BUNDLE_BASE64__" 26 | -------------------------------------------------------------------------------- /examples/kubernetes/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: "kube-system" 5 | -------------------------------------------------------------------------------- /examples/kubernetes/service-monitor.yaml: -------------------------------------------------------------------------------- 1 | # NOTE: this describes how prometheus finds your Service for your app 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | name: "k8s-sidecar-injector-prod" 6 | namespace: "kube-system" 7 | labels: 8 | k8s-app: "k8s-sidecar-injector" 9 | track: "prod" 10 | spec: 11 | endpoints: 12 | - honorLabels: true 13 | port: http-metrics 14 | path: "/metrics" 15 | interval: 30s 16 | jobLabel: k8s-app 17 | namespaceSelector: 18 | matchNames: 19 | - kube-system 20 | selector: 21 | matchLabels: 22 | k8s-app: "k8s-sidecar-injector" 23 | track: "prod" 24 | -------------------------------------------------------------------------------- /examples/kubernetes/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: k8s-sidecar-injector-prod 5 | namespace: kube-system 6 | labels: 7 | k8s-app: k8s-sidecar-injector 8 | track: prod 9 | spec: 10 | type: ClusterIP 11 | # NOTE(gabe): because of how MutatingWebhookConfigurations work, we MUST set this to have a clusterip 12 | # to avoid the thiccc chains of 13 | # W0802 14:53:36.704545 1 admission.go:253] Failed calling webhook, failing open injector.tumblr.com: failed calling admission webhook "injector.tumblr.com": Post https://k8s-sidecar-injector-prod.sre-sys.svc:443/mutate: cannot route to service with ClusterIP "None" 14 | # E0802 14:53:36.704610 1 admission.go:254] failed calling admission webhook "injector.tumblr.com": Post https://k8s-sidecar-injector-prod.sre-sys.svc:443/mutate: cannot route to service with ClusterIP "None" 15 | #clusterIP: None 16 | ports: 17 | - name: https 18 | port: 443 19 | targetPort: https 20 | protocol: TCP 21 | - name: http-metrics 22 | port: 80 23 | targetPort: http-metrics 24 | protocol: TCP 25 | selector: 26 | k8s-app: k8s-sidecar-injector 27 | track: prod 28 | -------------------------------------------------------------------------------- /examples/kubernetes/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: k8s-sidecar-injector 5 | namespace: kube-system 6 | labels: 7 | kubernetes.io/cluster-service: "true" 8 | addonmanager.kubernetes.io/mode: Reconcile 9 | -------------------------------------------------------------------------------- /examples/tls/README.md: -------------------------------------------------------------------------------- 1 | # TLS Generator 2 | 3 | This is stuff useful for new users who need to generate a new TLS cert for deploying the sidecar injector. 4 | 5 | -------------------------------------------------------------------------------- /examples/tls/ca.conf: -------------------------------------------------------------------------------- 1 | [req] 2 | req_extensions = v3_req 3 | distinguished_name = req_distinguished_name 4 | [ v3_req ] 5 | basicConstraints = CA:TRUE 6 | keyUsage = nonRepudiation, digitalSignature, keyEncipherment 7 | extendedKeyUsage = serverAuth 8 | [req_distinguished_name] 9 | countryName = Country Name (2 letter code) 10 | countryName_default = US 11 | stateOrProvinceName = State or Province Name (full name) 12 | stateOrProvinceName_default = New York 13 | localityName = Locality Name (eg, city) 14 | localityName_default = New York City 15 | organizationName = Organization Name (eg, company) 16 | organizationName_default = __ORG__ 17 | commonName = Common Name (eg, YOUR name) 18 | commonName_default = k8s-sidecar-injector-CA 19 | commonName_max = 64 20 | emailAddress = Email Address 21 | emailAddress_default = null@__DOMAIN__ 22 | -------------------------------------------------------------------------------- /examples/tls/csr-prod.conf: -------------------------------------------------------------------------------- 1 | [ req ] 2 | default_bits = 2048 3 | default_keyfile = sidecar-injector.key 4 | distinguished_name = req_distinguished_name 5 | req_extensions = req_ext # The extentions to add to the self signed cert 6 | 7 | [ req_distinguished_name ] 8 | countryName = Country Name (2 letter code) 9 | countryName_default = US 10 | stateOrProvinceName = State or Province Name (full name) 11 | stateOrProvinceName_default = New York 12 | localityName = Locality Name (eg, city) 13 | localityName_default = NYC 14 | organizationName = Organization Name (eg, company) 15 | organizationName_default = __ORG__ 16 | commonName = Common Name (eg, YOUR name) 17 | commonName_default = k8s-sidecar-injector 18 | commonName_max = 64 19 | 20 | [ req_ext ] 21 | subjectAltName = @alt_names 22 | 23 | [alt_names] 24 | DNS.1 = k8s-sidecar-injector-prod 25 | DNS.2 = k8s-sidecar-injector-prod.kube-system 26 | DNS.3 = k8s-sidecar-injector-prod.kube-system.svc 27 | -------------------------------------------------------------------------------- /examples/tls/new-cluster-injector-cert.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | require 'optparse' 4 | require 'fileutils' 5 | 6 | @options = { 7 | :cluster => nil, 8 | :az => nil, 9 | :ca => { 10 | :key => 'ca.key', 11 | :key_bits => 4096, 12 | :config => 'ca.conf', 13 | :validity_days => 999999, 14 | :cert => 'ca.crt', 15 | }, 16 | :certs => { 17 | :key_bits => 2048, 18 | :key => 'sidecar-injector.key', 19 | :csr => 'sidecar-injector.csr', 20 | :cert => 'sidecar-injector.crt', 21 | :validity_days => 999999, 22 | :csr_config => 'csr-prod.conf', 23 | } 24 | } 25 | 26 | def gen_new_certs(az, cluster) 27 | puts "Generating certs for #{az}-#{cluster}" 28 | workdir = "./#{az}/#{cluster}" 29 | # gen new ca key 30 | puts "Generating a new CA key..." 31 | if !Kernel.system(%[ 32 | openssl \ 33 | genrsa \ 34 | -out #{File.join(workdir, @options[:ca][:key])} \ 35 | #{@options[:ca][:key_bits]} \ 36 | ]) 37 | abort "unable to generate ca key" 38 | end 39 | # # Create and self sign the Root Certificate 40 | puts "Creating and signing the ca.crt (press enter when prompted for input)" 41 | if !Kernel.system(%[openssl req -x509 \ 42 | -config #{@options[:ca][:config]} \ 43 | -new -nodes \ 44 | -key #{File.join(workdir,@options[:ca][:key])} \ 45 | -sha256 -days #{@options[:ca][:validity_days]} \ 46 | -out #{File.join(workdir,@options[:ca][:cert])} \ 47 | ]) 48 | abort "unable to generate and sign ca.crt" 49 | # 50 | end 51 | # ### Create the certificate key 52 | puts "Creating the certificate key" 53 | if !Kernel.system(%[ 54 | openssl genrsa \ 55 | -out #{File.join(workdir, @options[:certs][:key])} \ 56 | #{@options[:certs][:key_bits]} \ 57 | ]) 58 | abort "unable to create certificate key" 59 | end 60 | # #create signing request 61 | puts "Creating signing request (press enter when prompted for input)" 62 | if !Kernel.system(%[ 63 | openssl req -new \ 64 | -key #{File.join(workdir,@options[:certs][:key])} \ 65 | -out #{File.join(workdir,@options[:certs][:csr])} \ 66 | -config #{@options[:certs][:csr_config]} \ 67 | ]) 68 | abort "unable to create CSR" 69 | end 70 | 71 | # ### Check the signing request 72 | # openssl req -text -noout -in sidecar-injector.csr|grep -A4 "Requested Extensions" 73 | # 74 | # ### Generate the certificate using the mydomain csr and key along with the CA Root key 75 | puts "Generate the cert" 76 | if !Kernel.system(%[ 77 | openssl x509 -req \ 78 | -in #{File.join(workdir,@options[:certs][:csr])} \ 79 | -CA #{File.join(workdir, @options[:ca][:cert])} \ 80 | -CAkey #{File.join(workdir, @options[:ca][:key])} \ 81 | -CAcreateserial \ 82 | -out #{File.join(workdir, @options[:certs][:cert])} \ 83 | -days #{@options[:certs][:validity_days]} \ 84 | -sha256 -extensions req_ext \ 85 | -extfile #{@options[:certs][:csr_config]} \ 86 | ]) 87 | abort "Unable to generate and sign the cert!" 88 | end 89 | end 90 | 91 | def main 92 | Dir.chdir(File.dirname(__FILE__)) 93 | if ENV['DEPLOYMENT'].nil? 94 | abort "You must pass DEPLOYMENT= in the environment (i.e. us-east-1 or dc01) for what DEPLOYMENT we are generating a cert for" 95 | end 96 | if ENV['CLUSTER'].nil? 97 | abort "You must pass CLUSTER= in the environment (i.e. PRODUCTION) for what CLUSTER we are generating a cert for" 98 | end 99 | az = ENV['DEPLOYMENT'].downcase 100 | cluster = ENV['CLUSTER'].upcase 101 | if !Dir.exist?("./#{az}/#{cluster}") 102 | puts "Creating ./#{az}/#{cluster}" 103 | FileUtils.mkdir_p("./#{az}/#{cluster}") 104 | end 105 | gen_new_certs(az, cluster) 106 | puts "\n\n\nAll done!\n\nHere are your certs for #{az}-#{cluster}\n\n" 107 | Kernel.system(%[git status]) 108 | puts "Generated new certs for #{az}-#{cluster} for k8s-sidecar-injector" 109 | puts "Please commit these!" 110 | end 111 | 112 | main 113 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tumblr/k8s-sidecar-injector 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/dyson/certman v0.2.1 7 | github.com/ghodss/yaml v1.0.0 8 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b 9 | github.com/gorilla/handlers v1.4.2 10 | github.com/gorilla/mux v1.7.4 11 | github.com/imdario/mergo v0.3.11 // indirect 12 | github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce 13 | github.com/prometheus/client_golang v1.7.1 14 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect 15 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect 16 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect 17 | gopkg.in/yaml.v2 v2.3.0 18 | k8s.io/api v0.18.6 19 | k8s.io/apimachinery v0.18.6 20 | k8s.io/client-go v0.18.6 21 | k8s.io/utils v0.0.0-20200731180307-f00132d28269 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /internal/pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "sync" 11 | 12 | "github.com/ghodss/yaml" 13 | "github.com/golang/glog" 14 | corev1 "k8s.io/api/core/v1" 15 | ) 16 | 17 | const ( 18 | annotationNamespaceDefault = "injector.tumblr.com" 19 | defaultVersion = "latest" 20 | ) 21 | 22 | var ( 23 | // ErrMissingName .. 24 | ErrMissingName = fmt.Errorf(`name field is required for an injection config`) 25 | // ErrNoConfigurationLoaded .. 26 | ErrNoConfigurationLoaded = fmt.Errorf(`at least one config must be present in the --config-directory`) 27 | // ErrCannotMergeNilInjectionConfig indicates an error trying to merge `nil` into an InjectionConfig 28 | ErrCannotMergeNilInjectionConfig = fmt.Errorf("cannot merge nil InjectionConfig") 29 | // ErrUnsupportedNameVersionFormat indicates the format of the name is invalid 30 | ErrUnsupportedNameVersionFormat = fmt.Errorf(`not a valid name or name:version format`) 31 | ) 32 | 33 | // InjectionConfig is a specific instance of a injected config, for a given annotation 34 | type InjectionConfig struct { 35 | Name string `json:"name"` 36 | Inherits string `json:"inherits"` 37 | Containers []corev1.Container `json:"containers"` 38 | Volumes []corev1.Volume `json:"volumes"` 39 | Environment []corev1.EnvVar `json:"env"` 40 | VolumeMounts []corev1.VolumeMount `json:"volumeMounts"` 41 | HostAliases []corev1.HostAlias `json:"hostAliases"` 42 | HostNetwork bool `json:"hostNetwork"` 43 | HostPID bool `json:"hostPID"` 44 | InitContainers []corev1.Container `json:"initContainers"` 45 | ServiceAccountName string `json:"serviceAccountName"` 46 | 47 | version string 48 | } 49 | 50 | // Config is a struct indicating how a given injection should be configured 51 | type Config struct { 52 | sync.RWMutex 53 | AnnotationNamespace string `yaml:"annotationnamespace"` 54 | Injections map[string]*InjectionConfig `yaml:"injections"` 55 | } 56 | 57 | // String returns a string representation of the config 58 | func (c *InjectionConfig) String() string { 59 | inheritsString := "" 60 | if c.Inherits != "" { 61 | inheritsString = fmt.Sprintf(" (inherits %s)", c.Inherits) 62 | } 63 | 64 | saString := "" 65 | if c.ServiceAccountName != "" { 66 | saString = fmt.Sprintf(", serviceAccountName %s", c.ServiceAccountName) 67 | } 68 | return fmt.Sprintf("%s%s: %d containers, %d init containers, %d volumes, %d environment vars, %d volume mounts, %d host aliases%s", 69 | c.FullName(), 70 | inheritsString, 71 | len(c.Containers), 72 | len(c.InitContainers), 73 | len(c.Volumes), 74 | len(c.Environment), 75 | len(c.VolumeMounts), 76 | len(c.HostAliases), 77 | saString) 78 | } 79 | 80 | // Version returns the parsed version of this injection config. If no version is specified, 81 | // "latest" is returned. The version is extracted from the request annotation, i.e. 82 | // injector.tumblr.com/request: my-sidecar:1.2, where "1.2" is the version. 83 | func (c *InjectionConfig) Version() string { 84 | if c.version == "" { 85 | return defaultVersion 86 | } 87 | 88 | return c.version 89 | } 90 | 91 | // FullName returns the full identifier of this sidecar - both the Name, and the Version(), formatted like 92 | // "${.Name}:${.Version}" 93 | func (c *InjectionConfig) FullName() string { 94 | return canonicalizeConfigName(c.Name, c.Version()) 95 | } 96 | 97 | // ReplaceInjectionConfigs will take a list of new InjectionConfigs, and replace the current configuration with them. 98 | // this blocks waiting on being able to update the configs in place. 99 | func (c *Config) ReplaceInjectionConfigs(replacementConfigs []*InjectionConfig) { 100 | c.Lock() 101 | defer c.Unlock() 102 | c.Injections = map[string]*InjectionConfig{} 103 | 104 | for _, r := range replacementConfigs { 105 | c.Injections[r.FullName()] = r 106 | } 107 | } 108 | 109 | // HasInjectionConfig returns bool for whether the config contains a config 110 | // given some key identifier 111 | func (c *Config) HasInjectionConfig(key string) bool { 112 | c.RLock() 113 | defer c.RUnlock() 114 | 115 | name, version, err := configNameFields(key) 116 | if err != nil { 117 | return false 118 | } 119 | fullKey := canonicalizeConfigName(name, version) 120 | 121 | _, ok := c.Injections[fullKey] 122 | 123 | return ok 124 | } 125 | 126 | // GetInjectionConfig returns the InjectionConfig given a requested key 127 | func (c *Config) GetInjectionConfig(key string) (*InjectionConfig, error) { 128 | c.RLock() 129 | defer c.RUnlock() 130 | 131 | name, version, err := configNameFields(key) 132 | if err != nil { 133 | return nil, err 134 | } 135 | fullKey := canonicalizeConfigName(name, version) 136 | 137 | i, ok := c.Injections[fullKey] 138 | if !ok { 139 | return nil, fmt.Errorf("no injection config found for annotation %s", fullKey) 140 | } 141 | 142 | return i, nil 143 | } 144 | 145 | // LoadConfigDirectory loads all configs in a directory and returns the Config 146 | func LoadConfigDirectory(path string) (*Config, error) { 147 | cfg := Config{ 148 | Injections: map[string]*InjectionConfig{}, 149 | } 150 | glob := filepath.Join(path, "*.yaml") 151 | matches, err := filepath.Glob(glob) 152 | 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | for _, p := range matches { 158 | c, err := LoadInjectionConfigFromFilePath(p) 159 | if err != nil { 160 | glog.Errorf("Error reading injection config from %s: %v", p, err) 161 | return nil, err 162 | } 163 | 164 | cfg.Injections[c.FullName()] = c 165 | } 166 | 167 | if len(cfg.Injections) == 0 { 168 | return nil, ErrNoConfigurationLoaded 169 | } 170 | 171 | if cfg.AnnotationNamespace == "" { 172 | cfg.AnnotationNamespace = annotationNamespaceDefault 173 | } 174 | 175 | glog.V(2).Infof("Loaded %d injection configs from %s", len(cfg.Injections), glob) 176 | 177 | return &cfg, nil 178 | } 179 | 180 | // Merge mutates c by merging in fields from child, to create an inheritance 181 | // functionality. 182 | func (c *InjectionConfig) Merge(child *InjectionConfig) error { 183 | if child == nil { 184 | return ErrCannotMergeNilInjectionConfig 185 | } 186 | // for all fields, merge child into c, eventually returning c 187 | c.Name = child.Name 188 | c.version = child.version 189 | c.Inherits = child.Inherits 190 | 191 | // merge containers 192 | for _, cctr := range child.Containers { 193 | contains := false 194 | 195 | for bi, bctr := range c.Containers { 196 | if bctr.Name == cctr.Name { 197 | contains = true 198 | c.Containers[bi] = cctr 199 | } 200 | } 201 | 202 | if !contains { 203 | c.Containers = append(c.Containers, cctr) 204 | } 205 | } 206 | 207 | // merge volumes 208 | for _, cv := range child.Volumes { 209 | contains := false 210 | 211 | for bi, bv := range c.Volumes { 212 | if bv.Name == cv.Name { 213 | contains = true 214 | c.Volumes[bi] = cv 215 | } 216 | } 217 | 218 | if !contains { 219 | c.Volumes = append(c.Volumes, cv) 220 | } 221 | } 222 | 223 | // merge environment 224 | for _, cv := range child.Environment { 225 | contains := false 226 | 227 | for bi, bv := range c.Environment { 228 | if bv.Name == cv.Name { 229 | contains = true 230 | c.Environment[bi] = cv 231 | } 232 | } 233 | 234 | if !contains { 235 | c.Environment = append(c.Environment, cv) 236 | } 237 | } 238 | 239 | // merge volume mounts 240 | for _, cv := range child.VolumeMounts { 241 | contains := false 242 | 243 | for bi, bv := range c.VolumeMounts { 244 | if bv.Name == cv.Name { 245 | contains = true 246 | c.VolumeMounts[bi] = cv 247 | } 248 | } 249 | 250 | if !contains { 251 | c.VolumeMounts = append(c.VolumeMounts, cv) 252 | } 253 | } 254 | 255 | // merge host aliases 256 | // note: we do not need to merge things, as entries are not keyed 257 | c.HostAliases = append(c.HostAliases, child.HostAliases...) 258 | 259 | // merge init containers 260 | for _, cv := range child.InitContainers { 261 | contains := false 262 | 263 | for bi, bv := range c.InitContainers { 264 | if bv.Name == cv.Name { 265 | contains = true 266 | c.InitContainers[bi] = cv 267 | } 268 | } 269 | 270 | if !contains { 271 | c.InitContainers = append(c.InitContainers, cv) 272 | } 273 | } 274 | 275 | // merge serviceAccount settings to the left 276 | if child.ServiceAccountName != "" { 277 | c.ServiceAccountName = child.ServiceAccountName 278 | } 279 | 280 | return nil 281 | } 282 | 283 | // LoadInjectionConfigFromFilePath returns a InjectionConfig given a yaml file on disk 284 | // NOTE: if the InjectionConfig loaded has an Inherits field, we recursively load from Inherits 285 | // and merge the InjectionConfigs to create an inheritance pattern. Inherits is not supported for 286 | // configs loaded via `LoadInjectionConfig` 287 | func LoadInjectionConfigFromFilePath(configFile string) (*InjectionConfig, error) { 288 | f, err := os.Open(configFile) 289 | if err != nil { 290 | return nil, fmt.Errorf("error loading injection config from file %s: %s", configFile, err.Error()) 291 | } 292 | 293 | defer f.Close() 294 | glog.V(3).Infof("Loading injection config from file %s", configFile) 295 | 296 | ic, err := LoadInjectionConfig(f) 297 | if err != nil { 298 | return nil, err 299 | } 300 | 301 | // Support inheritance from an InjectionConfig loaded from a file on disk 302 | if ic.Inherits != "" { 303 | // all Inherits are relative to the directory the current file is in, and are cleaned 304 | // prior to use. 305 | basedir := filepath.Dir(filepath.Clean(f.Name())) 306 | cleanPath := filepath.Join(basedir, ic.Inherits) 307 | glog.V(4).Infof("%s inherits from %s", ic.FullName(), ic.Inherits) 308 | 309 | base, err := LoadInjectionConfigFromFilePath(cleanPath) 310 | if err != nil { 311 | return nil, err 312 | } 313 | 314 | err = base.Merge(ic) 315 | if err != nil { 316 | return nil, err 317 | } 318 | 319 | ic = base 320 | } 321 | 322 | glog.V(3).Infof("Loaded injection config %s version=%s", ic.Name, ic.Version()) 323 | 324 | return ic, nil 325 | } 326 | 327 | // LoadInjectionConfig takes an io.Reader and parses out an injectionconfig 328 | func LoadInjectionConfig(reader io.Reader) (*InjectionConfig, error) { 329 | data, err := ioutil.ReadAll(reader) 330 | if err != nil { 331 | return nil, err 332 | } 333 | 334 | var cfg InjectionConfig 335 | if err := yaml.Unmarshal(data, &cfg); err != nil { 336 | return nil, err 337 | } 338 | 339 | if cfg.Name == "" { 340 | return nil, ErrMissingName 341 | } 342 | 343 | // we need to split the Name field apart into a Name and Version component 344 | cfg.Name, cfg.version, err = configNameFields(cfg.Name) 345 | if err != nil { 346 | return nil, err 347 | } 348 | 349 | return &cfg, nil 350 | } 351 | 352 | // given a name of a config, extract the name and version. Format is "name[:version]" where :version 353 | // is optional, and is assumed to be "latest" if omitted. 354 | func configNameFields(shortName string) (name, version string, err error) { 355 | substrings := strings.Split(shortName, ":") 356 | 357 | switch len(substrings) { 358 | case 1: 359 | return substrings[0], defaultVersion, nil 360 | case 2: 361 | if substrings[1] == "" { 362 | return substrings[0], defaultVersion, nil 363 | } 364 | 365 | return substrings[0], substrings[1], nil 366 | default: 367 | return "", "", ErrUnsupportedNameVersionFormat 368 | } 369 | } 370 | 371 | func canonicalizeConfigName(name, version string) string { 372 | return strings.ToLower(fmt.Sprintf("%s:%s", name, version)) 373 | } 374 | -------------------------------------------------------------------------------- /internal/pkg/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | testhelper "github.com/tumblr/k8s-sidecar-injector/internal/pkg/testing" 8 | ) 9 | 10 | var ( 11 | // location of the fixture sidecar files 12 | fixtureSidecarsDir = "test/fixtures/sidecars" 13 | 14 | testBadConfigs = map[string]testhelper.ConfigExpectation{ 15 | // test that a name with spurious use of ":" errors out on load 16 | "versioned:with:extra:data:v3": testhelper.ConfigExpectation{ 17 | Path: fixtureSidecarsDir + "/bad/init-containers-colons-v3.yaml", 18 | LoadError: ErrUnsupportedNameVersionFormat, 19 | }, 20 | "missing name": testhelper.ConfigExpectation{ 21 | Path: fixtureSidecarsDir + "/bad/missing-name.yaml", 22 | LoadError: ErrMissingName, 23 | }, 24 | "inheritance filenotfound": testhelper.ConfigExpectation{ 25 | Path: fixtureSidecarsDir + "/bad/inheritance-filenotfound.yaml", 26 | LoadError: fmt.Errorf(`error loading injection config from file test/fixtures/sidecars/bad/some-missing-file.yaml: open test/fixtures/sidecars/bad/some-missing-file.yaml: no such file or directory`), 27 | }, 28 | "inheritance escape": testhelper.ConfigExpectation{ 29 | Path: fixtureSidecarsDir + "/bad/inheritance-escape.yaml", 30 | LoadError: fmt.Errorf(`error loading injection config from file test/fixtures/etc/passwd: open test/fixtures/etc/passwd: no such file or directory`), 31 | }, 32 | } 33 | 34 | // test files and expectations 35 | testGoodConfigs = map[string]testhelper.ConfigExpectation{ 36 | "sidecar-test": testhelper.ConfigExpectation{ 37 | Name: "sidecar-test", 38 | Version: "latest", 39 | Path: fixtureSidecarsDir + "/sidecar-test.yaml", 40 | EnvCount: 2, 41 | ContainerCount: 2, 42 | VolumeCount: 1, 43 | VolumeMountCount: 0, 44 | HostAliasCount: 0, 45 | InitContainerCount: 0, 46 | }, 47 | "complex-sidecar": testhelper.ConfigExpectation{ 48 | Name: "complex-sidecar", 49 | Version: "v420.69", 50 | Path: fixtureSidecarsDir + "/complex-sidecar.yaml", 51 | EnvCount: 0, 52 | ContainerCount: 4, 53 | VolumeCount: 1, 54 | VolumeMountCount: 0, 55 | HostAliasCount: 0, 56 | InitContainerCount: 0, 57 | }, 58 | "env1": testhelper.ConfigExpectation{ 59 | Name: "env1", 60 | Version: "latest", 61 | Path: fixtureSidecarsDir + "/env1.yaml", 62 | EnvCount: 3, 63 | ContainerCount: 0, 64 | VolumeCount: 0, 65 | VolumeMountCount: 0, 66 | HostAliasCount: 0, 67 | InitContainerCount: 0, 68 | }, 69 | "volume-mounts": testhelper.ConfigExpectation{ 70 | Name: "volume-mounts", 71 | Version: "latest", 72 | Path: fixtureSidecarsDir + "/volume-mounts.yaml", 73 | EnvCount: 2, 74 | ContainerCount: 3, 75 | VolumeCount: 2, 76 | VolumeMountCount: 1, 77 | HostAliasCount: 0, 78 | InitContainerCount: 0, 79 | }, 80 | "host-aliases": testhelper.ConfigExpectation{ 81 | Name: "host-aliases", 82 | Version: "latest", 83 | Path: fixtureSidecarsDir + "/host-aliases.yaml", 84 | EnvCount: 2, 85 | ContainerCount: 1, 86 | VolumeCount: 0, 87 | VolumeMountCount: 0, 88 | HostAliasCount: 6, 89 | InitContainerCount: 0, 90 | }, 91 | "init-containers": testhelper.ConfigExpectation{ 92 | Name: "init-containers", 93 | Version: "latest", 94 | Path: fixtureSidecarsDir + "/init-containers.yaml", 95 | EnvCount: 0, 96 | ContainerCount: 2, 97 | VolumeCount: 0, 98 | VolumeMountCount: 0, 99 | HostAliasCount: 0, 100 | InitContainerCount: 1, 101 | }, 102 | "versioned1": testhelper.ConfigExpectation{ 103 | Name: "init-containers", 104 | Version: "v2", 105 | Path: fixtureSidecarsDir + "/init-containers-v2.yaml", 106 | EnvCount: 0, 107 | ContainerCount: 2, 108 | VolumeCount: 0, 109 | VolumeMountCount: 0, 110 | HostAliasCount: 0, 111 | InitContainerCount: 1, 112 | }, 113 | // test simple inheritance 114 | "simple inheritance from complex-sidecar": testhelper.ConfigExpectation{ 115 | Name: "inheritance-complex", 116 | Version: "v1", 117 | Path: fixtureSidecarsDir + "/inheritance-1.yaml", 118 | EnvCount: 2, 119 | ContainerCount: 5, 120 | VolumeCount: 2, 121 | VolumeMountCount: 0, 122 | HostAliasCount: 1, 123 | InitContainerCount: 1, 124 | }, 125 | // test deep inheritance 126 | "deep inheritance from inheritance-complex": testhelper.ConfigExpectation{ 127 | Name: "inheritance-deep", 128 | Version: "v2", 129 | Path: fixtureSidecarsDir + "/inheritance-deep-2.yaml", 130 | EnvCount: 3, 131 | ContainerCount: 6, 132 | VolumeCount: 3, 133 | VolumeMountCount: 0, 134 | HostAliasCount: 3, 135 | InitContainerCount: 2, 136 | }, 137 | "service-account": testhelper.ConfigExpectation{ 138 | Name: "service-account", 139 | Version: "latest", 140 | Path: fixtureSidecarsDir + "/service-account.yaml", 141 | EnvCount: 0, 142 | ContainerCount: 0, 143 | VolumeCount: 0, 144 | VolumeMountCount: 0, 145 | HostAliasCount: 0, 146 | InitContainerCount: 0, 147 | ServiceAccount: "someaccount", 148 | }, 149 | // we found that inheritance could cause the loading of the ServiceAccount 150 | // to fail, so we test explicitly for this case. 151 | "service-account-with-inheritance": testhelper.ConfigExpectation{ 152 | Name: "service-account-inherits-env1", 153 | Version: "latest", 154 | Path: fixtureSidecarsDir + "/service-account-with-inheritance.yaml", 155 | EnvCount: 3, 156 | ContainerCount: 0, 157 | VolumeCount: 0, 158 | VolumeMountCount: 0, 159 | HostAliasCount: 0, 160 | InitContainerCount: 0, 161 | ServiceAccount: "someaccount", 162 | }, 163 | // also, if we inject a serviceAccount and any container has a VolumeMount 164 | // with a mountPath of /var/run/secrets/kubernetes.io/serviceaccount, we 165 | // must remove it, to allow the ServiceAccountController to inject the 166 | // appropriate token volume 167 | "service-account-default-token": testhelper.ConfigExpectation{ 168 | Name: "service-account-default-token", 169 | Version: "latest", 170 | Path: fixtureSidecarsDir + "/service-account-default-token.yaml", 171 | EnvCount: 0, 172 | ContainerCount: 0, 173 | VolumeCount: 0, 174 | VolumeMountCount: 0, 175 | HostAliasCount: 0, 176 | InitContainerCount: 0, 177 | ServiceAccount: "someaccount", 178 | }, 179 | "maxmind": testhelper.ConfigExpectation{ 180 | Name: "maxmind", 181 | Version: "latest", 182 | Path: fixtureSidecarsDir + "/maxmind.yaml", 183 | EnvCount: 1, 184 | ContainerCount: 1, 185 | VolumeCount: 2, 186 | VolumeMountCount: 1, 187 | HostAliasCount: 0, 188 | InitContainerCount: 1, 189 | }, 190 | "network-pid": testhelper.ConfigExpectation{ 191 | Name: "test-network-pid", 192 | Version: "latest", 193 | Path: fixtureSidecarsDir + "/test-network-pid.yaml", 194 | HostNetwork: true, 195 | HostPID: true, 196 | }, 197 | } 198 | ) 199 | 200 | func TestConfigsLoadErrors(t *testing.T) { 201 | for _, testConfig := range testBadConfigs { 202 | _, err := LoadInjectionConfigFromFilePath(testConfig.Path) 203 | if err == nil || testConfig.LoadError == nil { 204 | t.Fatal("error was nil or LoadError was nil - this test should only be testing load errors") 205 | } 206 | if testConfig.LoadError.Error() != err.Error() { 207 | t.Fatalf("expected %s load to produce error %v but got %v", testConfig.Path, testConfig.LoadError, err) 208 | } 209 | } 210 | } 211 | 212 | // TestGoodConfigs: load configs from filepath and check if we load what we expected 213 | func TestGoodConfigs(t *testing.T) { 214 | for _, testConfig := range testGoodConfigs { 215 | c, err := LoadInjectionConfigFromFilePath(testConfig.Path) 216 | if testConfig.LoadError != err { 217 | t.Fatalf("expected %s load to produce error %v but got %v", testConfig.Path, testConfig.LoadError, err) 218 | } 219 | if testConfig.LoadError != nil { 220 | // if we expected a load error, and we made it here, continue, because we do not need to test 221 | // anything about the loaded InjectionConfig 222 | continue 223 | } 224 | if c.Name != testConfig.Name { 225 | t.Fatalf("expected %s Name loaded from %s but got %s", testConfig.Name, testConfig.Path, c.Name) 226 | } 227 | if c.Version() != testConfig.Version { 228 | t.Fatalf("expected %s Version() loaded from %s but got %s", testConfig.Version, testConfig.Path, c.Version()) 229 | } 230 | if c.FullName() != testConfig.FullName() { 231 | t.Fatalf("expected FullName() %s loaded from %s but got %s", testConfig.FullName(), testConfig.Path, c.FullName()) 232 | } 233 | if len(c.Environment) != testConfig.EnvCount { 234 | t.Fatalf("expected %d Envs loaded from %s but got %d", testConfig.EnvCount, testConfig.Path, len(c.Environment)) 235 | } 236 | if len(c.Containers) != testConfig.ContainerCount { 237 | t.Fatalf("expected %d Containers loaded from %s but got %d", testConfig.ContainerCount, testConfig.Path, len(c.Containers)) 238 | } 239 | if len(c.Volumes) != testConfig.VolumeCount { 240 | t.Fatalf("expected %d Volumes loaded from %s but got %d", testConfig.VolumeCount, testConfig.Path, len(c.Volumes)) 241 | } 242 | if len(c.VolumeMounts) != testConfig.VolumeMountCount { 243 | t.Fatalf("expected %d VolumeMounts loaded from %s but got %d", testConfig.VolumeMountCount, testConfig.Path, len(c.VolumeMounts)) 244 | } 245 | if len(c.HostAliases) != testConfig.HostAliasCount { 246 | t.Fatalf("expected %d HostAliases loaded from %s but got %d", testConfig.HostAliasCount, testConfig.Path, len(c.HostAliases)) 247 | } 248 | if len(c.InitContainers) != testConfig.InitContainerCount { 249 | t.Fatalf("expected %d InitContainers loaded from %s but got %d", testConfig.InitContainerCount, testConfig.Path, len(c.InitContainers)) 250 | } 251 | if c.ServiceAccountName != testConfig.ServiceAccount { 252 | t.Fatalf("expected ServiceAccountName %s, but got %s", testConfig.ServiceAccount, c.ServiceAccountName) 253 | } 254 | } 255 | } 256 | 257 | // TestLoadConfig: Check if we get all the configs 258 | func TestLoadConfig(t *testing.T) { 259 | expectedNumInjectionsConfig := len(testGoodConfigs) 260 | c, err := LoadConfigDirectory(fixtureSidecarsDir) 261 | if err != nil { 262 | t.Fatal(err) 263 | } 264 | if c.AnnotationNamespace != "injector.tumblr.com" { 265 | t.Fatalf("expected %s AnnotationNamespace but got %s", "injector.tumblr.com", c.AnnotationNamespace) 266 | } 267 | if len(c.Injections) != expectedNumInjectionsConfig { 268 | t.Fatalf("expected %d Injections loaded but got %d", expectedNumInjectionsConfig, len(c.Injections)) 269 | } 270 | } 271 | 272 | // TestFetInjectionConfig: Check if we can properly load a config by name and see if we read the correct values from it 273 | func TestGetInjectionConfig(t *testing.T) { 274 | cfg := testGoodConfigs["sidecar-test"] 275 | c, err := LoadConfigDirectory(fixtureSidecarsDir) 276 | if err != nil { 277 | t.Fatal(err) 278 | } 279 | 280 | i, err := c.GetInjectionConfig(cfg.FullName()) 281 | if err != nil { 282 | t.Fatal(err) 283 | } 284 | 285 | if i.Name != cfg.Name { 286 | t.Fatalf("expected name %s, but got %s", cfg.Name, i.Name) 287 | } 288 | if i.Version() != cfg.Version { 289 | t.Fatalf("expected version %s, but got %s", cfg.Version, i.Version()) 290 | } 291 | if i.FullName() != cfg.FullName() { 292 | t.Fatalf("expected FullName %s, but got %s", cfg.FullName(), i.FullName()) 293 | } 294 | if len(i.Environment) != cfg.EnvCount { 295 | t.Fatalf("expected %d Envs, but got %d", cfg.EnvCount, len(i.Environment)) 296 | } 297 | if len(i.Containers) != cfg.ContainerCount { 298 | t.Fatalf("expected %d container, but got %d", cfg.ContainerCount, len(i.Containers)) 299 | } 300 | if len(i.Volumes) != cfg.VolumeCount { 301 | t.Fatalf("expected %d volume, but got %d", cfg.VolumeCount, len(i.Volumes)) 302 | } 303 | if len(i.VolumeMounts) != cfg.VolumeMountCount { 304 | t.Fatalf("expected %d VolumeMounts, but got %d", cfg.VolumeMountCount, len(i.VolumeMounts)) 305 | } 306 | if len(i.HostAliases) != cfg.HostAliasCount { 307 | t.Fatalf("expected %d HostAliases, but got %d", cfg.HostAliasCount, len(i.HostAliases)) 308 | } 309 | if len(i.InitContainers) != cfg.InitContainerCount { 310 | t.Fatalf("expected %d InitContainers, but got %d", cfg.InitContainerCount, len(i.InitContainers)) 311 | } 312 | if i.ServiceAccountName != cfg.ServiceAccount { 313 | t.Fatalf("expected ServiceAccountName %s, but got %s", cfg.ServiceAccount, i.ServiceAccountName) 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /internal/pkg/config/watcher/config.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | // Config is a configuration struct for the Watcher type 4 | type Config struct { 5 | Namespace string 6 | ConfigMapLabels map[string]string 7 | MasterURL string 8 | Kubeconfig string 9 | } 10 | 11 | // NewConfig returns a new initialized Config 12 | func NewConfig() *Config { 13 | return &Config{ 14 | Namespace: "", 15 | ConfigMapLabels: map[string]string{}, 16 | MasterURL: "", 17 | Kubeconfig: "", 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/pkg/config/watcher/loader_test.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "sort" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/tumblr/k8s-sidecar-injector/internal/pkg/config" 12 | testhelper "github.com/tumblr/k8s-sidecar-injector/internal/pkg/testing" 13 | "gopkg.in/yaml.v2" 14 | "k8s.io/api/core/v1" 15 | ) 16 | 17 | var ( 18 | fixtureSidecarsDir = "test/fixtures/sidecars" 19 | fixtureK8sDir = "test/fixtures/k8s" 20 | 21 | // maps a k8s ConfigMap fixture in test/fixtures/k8s/ => testhelper.ConfigExpectation 22 | ExpectedInjectionConfigFixtures = map[string][]testhelper.ConfigExpectation{ 23 | "configmap-env1": []testhelper.ConfigExpectation{ 24 | testhelper.ConfigExpectation{ 25 | Name: "env1", 26 | Version: "latest", 27 | Path: fixtureSidecarsDir + "/env1.yaml", 28 | VolumeCount: 0, 29 | EnvCount: 3, 30 | ContainerCount: 0, 31 | VolumeMountCount: 0, 32 | HostAliasCount: 0, 33 | InitContainerCount: 0, 34 | }, 35 | }, 36 | "configmap-sidecar-test": []testhelper.ConfigExpectation{ 37 | testhelper.ConfigExpectation{ 38 | Name: "sidecar-test", 39 | Version: "latest", 40 | Path: fixtureSidecarsDir + "/sidecar-test.yaml", 41 | VolumeCount: 1, 42 | EnvCount: 2, 43 | ContainerCount: 2, 44 | VolumeMountCount: 0, 45 | HostAliasCount: 0, 46 | InitContainerCount: 0, 47 | }, 48 | }, 49 | "configmap-complex-sidecar": []testhelper.ConfigExpectation{ 50 | testhelper.ConfigExpectation{ 51 | Name: "complex-sidecar", 52 | Version: "v420.69", 53 | Path: fixtureSidecarsDir + "/complex-sidecar.yaml", 54 | VolumeCount: 1, 55 | EnvCount: 0, 56 | ContainerCount: 4, 57 | VolumeMountCount: 0, 58 | HostAliasCount: 0, 59 | InitContainerCount: 0, 60 | }, 61 | }, 62 | "configmap-multiple1": []testhelper.ConfigExpectation{ 63 | testhelper.ConfigExpectation{ 64 | Name: "env1", 65 | Version: "latest", 66 | Path: fixtureSidecarsDir + "/env1.yaml", 67 | VolumeCount: 0, 68 | EnvCount: 3, 69 | ContainerCount: 0, 70 | VolumeMountCount: 0, 71 | HostAliasCount: 0, 72 | InitContainerCount: 0, 73 | }, 74 | testhelper.ConfigExpectation{ 75 | Name: "sidecar-test", 76 | Version: "latest", 77 | Path: fixtureSidecarsDir + "/sidecar-test.yaml", 78 | VolumeCount: 1, 79 | EnvCount: 2, 80 | ContainerCount: 2, 81 | VolumeMountCount: 0, 82 | HostAliasCount: 0, 83 | InitContainerCount: 0, 84 | }, 85 | }, 86 | "configmap-volume-mounts": []testhelper.ConfigExpectation{ 87 | testhelper.ConfigExpectation{ 88 | Name: "volume-mounts", 89 | Version: "latest", 90 | Path: fixtureSidecarsDir + "/volume-mounts.yaml", 91 | VolumeCount: 2, 92 | EnvCount: 2, 93 | ContainerCount: 3, 94 | VolumeMountCount: 1, 95 | HostAliasCount: 0, 96 | InitContainerCount: 0, 97 | }, 98 | }, 99 | "configmap-host-aliases": []testhelper.ConfigExpectation{ 100 | testhelper.ConfigExpectation{ 101 | Name: "host-aliases", 102 | Version: "latest", 103 | Path: fixtureSidecarsDir + "/host-aliases.yaml", 104 | VolumeCount: 0, 105 | EnvCount: 2, 106 | ContainerCount: 1, 107 | VolumeMountCount: 0, 108 | HostAliasCount: 6, 109 | InitContainerCount: 0, 110 | }, 111 | }, 112 | 113 | "configmap-hostNetwork-hostPid": []testhelper.ConfigExpectation{ 114 | testhelper.ConfigExpectation{ 115 | Name: "test-network-pid", 116 | Version: "latest", 117 | Path: fixtureSidecarsDir + "/test-network-pid.yaml", 118 | HostNetwork: true, 119 | HostPID: true, 120 | }, 121 | }, 122 | 123 | "configmap-init-containers": []testhelper.ConfigExpectation{ 124 | testhelper.ConfigExpectation{ 125 | Name: "init-containers", 126 | Version: "latest", 127 | Path: fixtureSidecarsDir + "/init-containers.yaml", 128 | VolumeCount: 0, 129 | EnvCount: 0, 130 | ContainerCount: 2, 131 | VolumeMountCount: 0, 132 | HostAliasCount: 0, 133 | InitContainerCount: 1, 134 | }, 135 | }, 136 | } 137 | ) 138 | 139 | func k8sFixture(f string) string { 140 | return fmt.Sprintf("%s/%s.yaml", fixtureK8sDir, f) 141 | } 142 | 143 | func TestLoadFromConfigMap(t *testing.T) { 144 | for fixture, expectedFixtures := range ExpectedInjectionConfigFixtures { 145 | fname := k8sFixture(fixture) 146 | t.Logf("loading injection config from %s", fname) 147 | var cm v1.ConfigMap 148 | f, err := os.Open(fname) 149 | if err != nil { 150 | t.Fatal(err) 151 | } 152 | data, err := ioutil.ReadAll(f) 153 | if err != nil { 154 | t.Fatal(err) 155 | } 156 | if err = yaml.Unmarshal(data, &cm); err != nil { 157 | t.Fatal(err) 158 | } 159 | 160 | ics, err := InjectionConfigsFromConfigMap(cm) 161 | if err != nil { 162 | t.Fatal(err) 163 | } 164 | if len(ics) != len(expectedFixtures) { 165 | t.Fatalf("expected %d injection configs loaded from %s, but got %d", len(expectedFixtures), fname, len(ics)) 166 | } 167 | 168 | // make sure all the appropriate names are present 169 | expectedNames := make([]string, len(expectedFixtures)) 170 | for i, f := range expectedFixtures { 171 | expectedNames[i] = f.FullName() 172 | } 173 | sort.Strings(expectedNames) 174 | actualNames := []string{} 175 | for _, x := range ics { 176 | actualNames = append(actualNames, x.FullName()) 177 | } 178 | sort.Strings(actualNames) 179 | if strings.Join(expectedNames, ",") != strings.Join(actualNames, ",") { 180 | t.Fatalf("expected InjectionConfigs loaded with names %v but got %v", expectedNames, actualNames) 181 | } 182 | 183 | for _, expectedICF := range expectedFixtures { 184 | expectedicFile := expectedICF.Path 185 | ic, err := config.LoadInjectionConfigFromFilePath(expectedicFile) 186 | if err != nil { 187 | t.Fatalf("unable to load expected fixture %s: %s", expectedicFile, err.Error()) 188 | } 189 | if ic.HostNetwork != expectedICF.HostNetwork { 190 | t.Fatalf("expected %t hostnetwork variables in %s, but found %t", expectedICF.HostNetwork, expectedICF.Name, ic.HostNetwork) 191 | } 192 | if ic.HostPID != expectedICF.HostPID { 193 | t.Fatalf("expected %t hostpid variables in %s, but found %t", expectedICF.HostPID, expectedICF.Name, ic.HostPID) 194 | } 195 | if ic.Name != expectedICF.Name { 196 | t.Fatalf("expected %s Name in %s, but found %s", expectedICF.Name, expectedICF.Path, ic.Name) 197 | } 198 | if ic.Version() != expectedICF.Version { 199 | t.Fatalf("expected %s Version in %s, but found %s", expectedICF.Version, expectedICF.Path, ic.Version()) 200 | } 201 | if ic.FullName() != expectedICF.FullName() { 202 | t.Fatalf("expected %s FullName() in %s, but found %s", expectedICF.FullName(), expectedICF.Path, ic.FullName()) 203 | } 204 | if len(ic.Environment) != expectedICF.EnvCount { 205 | t.Fatalf("expected %d environment variables in %s, but found %d", expectedICF.EnvCount, expectedICF.Path, len(ic.Environment)) 206 | } 207 | if len(ic.Containers) != expectedICF.ContainerCount { 208 | t.Fatalf("expected %d containers in %s, but found %d", expectedICF.ContainerCount, expectedICF.Path, len(ic.Containers)) 209 | } 210 | if len(ic.Volumes) != expectedICF.VolumeCount { 211 | t.Fatalf("expected %d volumes in %s, but found %d", expectedICF.VolumeCount, expectedICF.Path, len(ic.Volumes)) 212 | } 213 | if len(ic.VolumeMounts) != expectedICF.VolumeMountCount { 214 | t.Fatalf("expected %d volume mounts in %s, but found %d", expectedICF.VolumeMountCount, expectedICF.Path, len(ic.VolumeMounts)) 215 | } 216 | if len(ic.HostAliases) != expectedICF.HostAliasCount { 217 | t.Fatalf("expected %d host aliases in %s, but found %d", expectedICF.HostAliasCount, expectedICF.Path, len(ic.HostAliases)) 218 | } 219 | if len(ic.InitContainers) != expectedICF.InitContainerCount { 220 | t.Fatalf("expected %d init containers in %s, but found %d", expectedICF.InitContainerCount, expectedICF.Path, len(ic.InitContainers)) 221 | } 222 | for _, actualIC := range ics { 223 | if ic.FullName() == actualIC.FullName() { 224 | if ic.String() != actualIC.String() { 225 | t.Fatalf("%s: expected %s to equal %s", expectedICF.Path, ic.String(), actualIC.String()) 226 | } 227 | } 228 | } 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /internal/pkg/config/watcher/message.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "github.com/tumblr/k8s-sidecar-injector/internal/pkg/config" 5 | ) 6 | 7 | // Message is a message that describes a change and payload to a sidecar configuration 8 | type Message struct { 9 | Event Event 10 | InjectionConfig config.InjectionConfig 11 | } 12 | 13 | // Event is what happened to the config (add/delete/update) 14 | type Event uint8 15 | 16 | const ( 17 | // EventAdd is a new ConfigMap 18 | EventAdd Event = iota 19 | // EventUpdate is an Updated ConfigMap 20 | EventUpdate 21 | // EventDelete is a deleted ConfigMap 22 | EventDelete 23 | ) 24 | -------------------------------------------------------------------------------- /internal/pkg/config/watcher/watcher.go: -------------------------------------------------------------------------------- 1 | // Package watcher is a module that handles talking to the k8s api, and watching ConfigMaps for a set of configurations, and emitting them when 2 | // they change. 3 | package watcher 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "fmt" 9 | "io/ioutil" 10 | "strings" 11 | 12 | "github.com/golang/glog" 13 | "github.com/tumblr/k8s-sidecar-injector/internal/pkg/config" 14 | "k8s.io/api/core/v1" 15 | apierrs "k8s.io/apimachinery/pkg/api/errors" 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | "k8s.io/apimachinery/pkg/labels" 18 | "k8s.io/apimachinery/pkg/watch" 19 | "k8s.io/client-go/kubernetes" 20 | k8sv1 "k8s.io/client-go/kubernetes/typed/core/v1" 21 | "k8s.io/client-go/rest" 22 | "k8s.io/client-go/tools/clientcmd" 23 | ) 24 | 25 | const ( 26 | serviceAccountNamespaceFilePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" 27 | ) 28 | 29 | // ErrWatchChannelClosed should restart watcher 30 | var ErrWatchChannelClosed = errors.New("watcher channel has closed") 31 | 32 | // K8sConfigMapWatcher is a struct that connects to the API and collects, parses, and emits sidecar configurations 33 | type K8sConfigMapWatcher struct { 34 | Config 35 | client k8sv1.CoreV1Interface 36 | } 37 | 38 | // New creates a new K8sConfigMapWatcher 39 | func New(cfg Config) (*K8sConfigMapWatcher, error) { 40 | c := K8sConfigMapWatcher{Config: cfg} 41 | if c.Namespace == "" { 42 | // ENHANCEMENT: support downward API/env vars instead? https://github.com/kubernetes/kubernetes/blob/release-1.0/docs/user-guide/downward-api.md 43 | // load from file on disk for serviceaccount: /var/run/secrets/kubernetes.io/serviceaccount/namespace 44 | ns, err := ioutil.ReadFile(serviceAccountNamespaceFilePath) 45 | if err != nil { 46 | return nil, fmt.Errorf("%s: maybe you should specify --configmap-namespace if you are running outside of kubernetes", err.Error()) 47 | } 48 | if string(ns) != "" { 49 | c.Namespace = string(ns) 50 | glog.V(2).Infof("Inferred ConfigMap search namespace=%s from %s", c.Namespace, serviceAccountNamespaceFilePath) 51 | } 52 | } 53 | var ( 54 | err error 55 | k8sConfig *rest.Config 56 | ) 57 | if c.Kubeconfig != "" || c.MasterURL != "" { 58 | glog.V(2).Infof("Creating Kubernetes client from kubeconfig=%s with masterurl=%s", c.Kubeconfig, c.MasterURL) 59 | k8sConfig, err = clientcmd.BuildConfigFromFlags(c.MasterURL, c.Kubeconfig) 60 | if err != nil { 61 | return nil, err 62 | } 63 | } else { 64 | glog.V(2).Infof("Creating Kubernetes client from in-cluster discovery") 65 | k8sConfig, err = rest.InClusterConfig() 66 | if err != nil { 67 | return nil, err 68 | } 69 | } 70 | clientset, err := kubernetes.NewForConfig(k8sConfig) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | c.client = clientset.CoreV1() 76 | err = validate(&c) 77 | if err != nil { 78 | return nil, fmt.Errorf("validation failed for K8sConfigMapWatcher: %s", err.Error()) 79 | } 80 | glog.V(2).Infof("Created ConfigMap watcher: apiserver=%s namespace=%s watchlabels=%v", k8sConfig.Host, c.Namespace, c.ConfigMapLabels) 81 | return &c, nil 82 | } 83 | 84 | func validate(c *K8sConfigMapWatcher) error { 85 | if c == nil { 86 | return fmt.Errorf("configmap watcher was nil") 87 | } 88 | if c.Namespace == "" { 89 | return fmt.Errorf("namespace is empty") 90 | } 91 | if c.ConfigMapLabels == nil { 92 | return fmt.Errorf("configmap labels was an uninitialized map") 93 | } 94 | if c.client == nil { 95 | return fmt.Errorf("k8s client was not setup properly") 96 | } 97 | return nil 98 | } 99 | 100 | // Watch watches for events impacting watched ConfigMaps and emits their events across a channel 101 | func (c *K8sConfigMapWatcher) Watch(ctx context.Context, notifyMe chan<- interface{}) error { 102 | glog.V(3).Infof("Watching for ConfigMaps for changes on namespace=%s with labels=%v", c.Namespace, c.ConfigMapLabels) 103 | watcher, err := c.client.ConfigMaps(c.Namespace).Watch(ctx, metav1.ListOptions{ 104 | LabelSelector: mapStringStringToLabelSelector(c.ConfigMapLabels), 105 | }) 106 | if err != nil { 107 | return fmt.Errorf("unable to create watcher (possible serviceaccount RBAC/ACL failure?): %s", err.Error()) 108 | } 109 | defer watcher.Stop() 110 | for { 111 | select { 112 | case e, ok := <-watcher.ResultChan(): 113 | // channel may closed caused by HTTP timeout, should restart watcher 114 | // detail at https://github.com/kubernetes/client-go/issues/334 115 | if !ok { 116 | glog.Errorf("channel has closed, should restart watcher") 117 | return ErrWatchChannelClosed 118 | } 119 | if e.Type == watch.Error { 120 | return apierrs.FromObject(e.Object) 121 | } 122 | glog.V(3).Infof("event: %s %s", e.Type, e.Object.GetObjectKind()) 123 | switch e.Type { 124 | case watch.Added: 125 | fallthrough 126 | case watch.Modified: 127 | fallthrough 128 | case watch.Deleted: 129 | // signal reconciliation of all InjectionConfigs 130 | glog.V(3).Infof("signalling event received from watch channel: %s %s", e.Type, e.Object.GetObjectKind()) 131 | notifyMe <- struct{}{} 132 | default: 133 | glog.Errorf("got unsupported event %s for %s! skipping", e.Type, e.Object.GetObjectKind()) 134 | } 135 | // events! yay! 136 | case <-ctx.Done(): 137 | glog.V(2).Infof("stopping configmap watcher, context indicated we are done") 138 | // clean up, we cancelled the context, so stop the watch 139 | return nil 140 | } 141 | } 142 | } 143 | 144 | func mapStringStringToLabelSelector(m map[string]string) string { 145 | // https://github.com/kubernetes/apimachinery/issues/47 146 | return labels.Set(m).String() 147 | } 148 | 149 | // Get fetches all matching ConfigMaps 150 | func (c *K8sConfigMapWatcher) Get(ctx context.Context) (cfgs []*config.InjectionConfig, err error) { 151 | glog.V(1).Infof("Fetching ConfigMaps...") 152 | clist, err := c.client.ConfigMaps(c.Namespace).List(ctx, metav1.ListOptions{ 153 | LabelSelector: mapStringStringToLabelSelector(c.ConfigMapLabels), 154 | }) 155 | if err != nil { 156 | return cfgs, err 157 | } 158 | glog.V(1).Infof("Fetched %d ConfigMaps", len(clist.Items)) 159 | for _, cm := range clist.Items { 160 | injectionConfigsForCM, err := InjectionConfigsFromConfigMap(cm) 161 | if err != nil { 162 | return cfgs, fmt.Errorf("error getting ConfigMaps from API: %s", err.Error()) 163 | } 164 | glog.V(1).Infof("Found %d InjectionConfigs in %s", len(injectionConfigsForCM), cm.ObjectMeta.Name) 165 | cfgs = append(cfgs, injectionConfigsForCM...) 166 | } 167 | return cfgs, nil 168 | } 169 | 170 | // InjectionConfigsFromConfigMap parse items in a configmap into a list of InjectionConfigs 171 | func InjectionConfigsFromConfigMap(cm v1.ConfigMap) ([]*config.InjectionConfig, error) { 172 | ics := []*config.InjectionConfig{} 173 | for name, payload := range cm.Data { 174 | glog.V(3).Infof("Parsing %s/%s:%s into InjectionConfig", cm.ObjectMeta.Namespace, cm.ObjectMeta.Name, name) 175 | ic, err := config.LoadInjectionConfig(strings.NewReader(payload)) 176 | if err != nil { 177 | return nil, fmt.Errorf("error parsing ConfigMap %s item %s into injection config: %s", cm.ObjectMeta.Name, name, err.Error()) 178 | } 179 | glog.V(2).Infof("Loaded InjectionConfig %s from ConfigMap %s:%s", ic.Name, cm.ObjectMeta.Name, name) 180 | ics = append(ics, ic) 181 | } 182 | return ics, nil 183 | } 184 | -------------------------------------------------------------------------------- /internal/pkg/config/watcher/watcher_test.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "k8s.io/apimachinery/pkg/watch" 7 | testcore "k8s.io/client-go/testing" 8 | "testing" 9 | 10 | _ "github.com/tumblr/k8s-sidecar-injector/internal/pkg/testing" 11 | "k8s.io/client-go/kubernetes/fake" 12 | ) 13 | 14 | var ( 15 | testConfig = Config{ 16 | Namespace: "default", 17 | ConfigMapLabels: map[string]string{ 18 | "thing": "fake", 19 | }, 20 | } 21 | ) 22 | 23 | func TestGet(t *testing.T) { 24 | w := K8sConfigMapWatcher{ 25 | Config: testConfig, 26 | client: fake.NewSimpleClientset().CoreV1(), 27 | } 28 | 29 | ctx := context.Background() 30 | messages, err := w.Get(ctx) 31 | if err != nil { 32 | t.Fatal(err.Error()) 33 | } 34 | if len(messages) != 0 { 35 | t.Fatalf("expected 0 messages, but got %d", len(messages)) 36 | } 37 | } 38 | 39 | func TestWatcherChannelClose(t *testing.T) { 40 | client := fake.NewSimpleClientset() 41 | watcher := watch.NewEmptyWatch() 42 | client.PrependWatchReactor("configmaps", testcore.DefaultWatchReactor(watcher, nil)) 43 | 44 | w := K8sConfigMapWatcher{ 45 | Config: testConfig, 46 | client: client.CoreV1(), 47 | } 48 | 49 | sigChan := make(chan interface{}, 10) 50 | // background context never canceled, no deadline 51 | ctx := context.Background() 52 | 53 | err := w.Watch(ctx, sigChan) 54 | if err != nil && err != ErrWatchChannelClosed { 55 | t.Errorf("expect catch ErrWatchChannelClosed, but got %s", err) 56 | } 57 | } 58 | 59 | func TestWatcherWatchCreateError(t *testing.T) { 60 | client := fake.NewSimpleClientset() 61 | 62 | client.PrependWatchReactor("configmaps", func(_ testcore.Action) ( 63 | handled bool, 64 | ret watch.Interface, 65 | err error, 66 | ) { 67 | return true, nil, errors.New("did not construct a watcher") 68 | }) 69 | 70 | w := K8sConfigMapWatcher{ 71 | Config: testConfig, 72 | client: client.CoreV1(), 73 | } 74 | 75 | err := w.Watch(context.Background(), make(chan interface{}, 10)) 76 | if err == nil { 77 | t.Error("expected Watch to fail when the watcher couldn't be created") 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /internal/pkg/testing/config.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // ConfigExpectation struct for testing: where to find the file and what we expect to find in it 9 | type ConfigExpectation struct { 10 | // name is not the Name in the loaded config, but only the "some-config" of "some-config:1.2" 11 | Name string 12 | // version is the parsed version string, or "latest" if omitted 13 | Version string 14 | // Path is the path to the YAML to load the sidecar yaml from 15 | Path string 16 | EnvCount int 17 | ContainerCount int 18 | VolumeCount int 19 | VolumeMountCount int 20 | HostAliasCount int 21 | HostNetwork bool 22 | HostPID bool 23 | InitContainerCount int 24 | ServiceAccount string 25 | 26 | // LoadError is an error, if any, that is expected during load 27 | LoadError error 28 | } 29 | 30 | // FullName returns name + version string 31 | func (x *ConfigExpectation) FullName() string { 32 | return strings.ToLower(fmt.Sprintf("%s:%s", x.Name, x.Version)) 33 | } 34 | -------------------------------------------------------------------------------- /internal/pkg/testing/testing.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "runtime" 7 | ) 8 | 9 | // make sure we hop to the project root when imported. This is to make life easier for tests so they can include files from testdata 10 | // without needing to know its relative location in the tree 11 | func init() { 12 | _, filename, _, _ := runtime.Caller(0) 13 | // hop back 2 directories, expecting this is internal/pkg/testing 14 | d := path.Join(path.Dir(filename), "../../..") 15 | err := os.Chdir(d) 16 | if err != nil { 17 | panic(err) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/pkg/version/version.go: -------------------------------------------------------------------------------- 1 | // Package version has variables set at build time by the Makefile 2 | package version 3 | 4 | var ( 5 | // Branch of k8s-sidecar-injector built 6 | Branch = "???" 7 | // Commit of k8s-sidecar-injector built 8 | Commit = "???" 9 | // Version of k8s-sidecar-injector 10 | Version = "???" 11 | // BuildDate of k8s-sidecar-injector 12 | BuildDate = "???" 13 | // Package of k8s-sidecar-injector 14 | Package = "???" 15 | ) 16 | -------------------------------------------------------------------------------- /pkg/coalescer/coalescer.go: -------------------------------------------------------------------------------- 1 | package coalescer 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/golang/glog" 8 | ) 9 | 10 | // Coalesce takes an input chan, and coalesced inputs with a timebound of interval, after which 11 | // it signals on output chan with the last value from input chan 12 | func Coalesce(ctx context.Context, interval time.Duration, input chan interface{}) <-chan interface{} { 13 | output := make(chan interface{}) 14 | go func() { 15 | var ( 16 | signalled bool 17 | inputOpen = true // assume input chan is open before we run our select loop 18 | ) 19 | glog.V(2).Infof("debouncing reconciliation signals with window %s", interval.String()) 20 | for { 21 | doneCh := ctx.Done() 22 | select { 23 | case <-doneCh: 24 | if signalled { 25 | output <- struct{}{} 26 | } 27 | return 28 | case <-time.After(interval): 29 | if signalled { 30 | glog.V(5).Infof("signalling reconciliation after %s", interval.String()) 31 | output <- struct{}{} 32 | signalled = false 33 | } 34 | case _, inputOpen = <-input: 35 | if inputOpen { // only record events if the input channel is still open 36 | glog.V(4).Infof("got reconciliation signal, debouncing for %s", interval.String()) 37 | signalled = true 38 | } 39 | } 40 | // stop running the Coalescer only when all input+output channels are closed! 41 | if !inputOpen { 42 | // input is closed, so lets signal one last time if we have any pending unsignalled events 43 | if signalled { 44 | // send final event, so we dont miss the trailing event after input chan close 45 | output <- struct{}{} 46 | } 47 | glog.V(1).Infof("coalesce routine terminated, input channel is closed") 48 | return 49 | } 50 | } 51 | }() 52 | return output 53 | } 54 | -------------------------------------------------------------------------------- /pkg/coalescer/coalescer_test.go: -------------------------------------------------------------------------------- 1 | package coalescer 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | var ( 12 | payload = struct{}{} 13 | debounceDuration = time.Millisecond * 10 14 | ) 15 | 16 | func TestCoalesceEvents(t *testing.T) { 17 | 18 | var wg sync.WaitGroup 19 | input := make(chan interface{}) 20 | fmt.Printf("Starting coalescer window=%s\n", debounceDuration) 21 | output := Coalesce(context.Background(), debounceDuration, input) 22 | actualEvents := 0 23 | 24 | wg.Add(1) 25 | go func() { 26 | defer wg.Done() 27 | defer close(input) 28 | fmt.Printf("Starting input generator goroutine\n") 29 | // generate some events on input, then chill out 30 | fmt.Printf("sending payload\n") 31 | input <- payload 32 | fmt.Printf("sending payload\n") 33 | input <- payload 34 | fmt.Printf("sending payload\n") 35 | input <- payload 36 | time.Sleep(time.Millisecond * 15) 37 | fmt.Printf("sending payload\n") 38 | input <- payload 39 | time.Sleep(time.Millisecond * 15) 40 | fmt.Printf("sending payload\n") 41 | input <- payload 42 | fmt.Printf("Stopping input generator goroutine\n") 43 | }() 44 | 45 | stop := make(chan struct{}) 46 | go func() { 47 | // read events from output 48 | fmt.Printf("Starting output reader goroutine\n") 49 | for { 50 | select { 51 | case <-output: 52 | //fmt.Printf("output: got event\n") 53 | actualEvents++ 54 | case <-stop: 55 | break 56 | default: 57 | if output == nil { 58 | break 59 | } 60 | } 61 | } 62 | fmt.Printf("Exiting output reader goroutine\n") 63 | }() 64 | t.Log("waiting for all routines to finish") 65 | 66 | wg.Wait() 67 | // wait at least 1 debounce cycle for the final emission of events 68 | time.Sleep(debounceDuration) 69 | stop <- struct{}{} 70 | expectedEvents := 3 71 | if expectedEvents != actualEvents { 72 | t.Errorf("expected %d debounced events, but got %d", expectedEvents, actualEvents) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /pkg/server/errors.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | var ( 8 | // ErrSkipIgnoredNamespace ... 9 | ErrSkipIgnoredNamespace = fmt.Errorf("Skipping pod in ignored namespace") 10 | // ErrSkipAlreadyInjected ... 11 | ErrSkipAlreadyInjected = fmt.Errorf("Skipping pod that has already been injected") 12 | // ErrMissingRequestAnnotation ... 13 | ErrMissingRequestAnnotation = fmt.Errorf("Missing injection request annotation") 14 | // ErrRequestedSidecarNotFound ... 15 | ErrRequestedSidecarNotFound = fmt.Errorf("Requested sidecar not found in configuration") 16 | ) 17 | 18 | // GetErrorReason returns a string description for a given error, for use 19 | // when reporting "reason" in metrics 20 | func GetErrorReason(err error) string { 21 | var reason string 22 | switch err { 23 | case ErrSkipIgnoredNamespace: 24 | reason = "ignored_namespace" 25 | case ErrSkipAlreadyInjected: 26 | reason = "already_injected" 27 | case ErrMissingRequestAnnotation: 28 | reason = "no_annotation" 29 | case ErrRequestedSidecarNotFound: 30 | reason = "missing_config" 31 | case nil: 32 | reason = "" 33 | default: 34 | reason = "unknown_error" 35 | } 36 | return reason 37 | } 38 | -------------------------------------------------------------------------------- /pkg/server/parameters.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | // Parameters parameters 4 | type Parameters struct { 5 | LifecyclePort int // metrics, debugging, health checking port (just http) 6 | TLSPort int // webhook server port (forced TLS) 7 | CertFile string // path to the x509 certificate for https 8 | KeyFile string // path to the x509 private key matching `CertFile` 9 | ConfigDirectory string // path to sidecar injector configuration directory (contains yamls) 10 | AnnotationNamespace string // namespace used to scope annotations 11 | } 12 | -------------------------------------------------------------------------------- /pkg/server/webhook.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "path" 9 | "strings" 10 | 11 | "github.com/golang/glog" 12 | "github.com/prometheus/client_golang/prometheus" 13 | "github.com/prometheus/client_golang/prometheus/promhttp" 14 | "github.com/tumblr/k8s-sidecar-injector/internal/pkg/config" 15 | "k8s.io/api/admission/v1beta1" 16 | admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" 17 | corev1 "k8s.io/api/core/v1" 18 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 | "k8s.io/apimachinery/pkg/runtime" 20 | "k8s.io/apimachinery/pkg/runtime/serializer" 21 | ) 22 | 23 | const ( 24 | // StatusInjected is the annotation value for /status that indicates an injection was already performed on this pod 25 | StatusInjected = "injected" 26 | ) 27 | 28 | var ( 29 | serviceAccountTokenMountPath = "/var/run/secrets/kubernetes.io/serviceaccount" 30 | runtimeScheme = runtime.NewScheme() 31 | codecs = serializer.NewCodecFactory(runtimeScheme) 32 | deserializer = codecs.UniversalDeserializer() 33 | 34 | // (https://github.com/kubernetes/kubernetes/issues/57982) 35 | defaulter = runtime.ObjectDefaulter(runtimeScheme) 36 | 37 | injectionCounter = prometheus.NewCounterVec( 38 | prometheus.CounterOpts{ 39 | Name: "injections", 40 | Help: "Count of mutations/injections into a resource", 41 | }, 42 | []string{"status", "reason", "requested"}, 43 | ) 44 | 45 | httpReqInFlightGauge = prometheus.NewGauge(prometheus.GaugeOpts{ 46 | Name: "http_in_flight_requests", 47 | Help: "A gauge of requests currently being served by the wrapped handler.", 48 | }) 49 | 50 | httpReqCounter = prometheus.NewCounterVec( 51 | prometheus.CounterOpts{ 52 | Name: "http_api_requests_total", 53 | Help: "A counter for requests to the wrapped handler.", 54 | }, 55 | []string{"code", "method"}, 56 | ) 57 | 58 | // duration is partitioned by the HTTP method and handler. It uses custom 59 | // buckets based on the expected request duration. 60 | httpReqDuration = prometheus.NewHistogramVec( 61 | prometheus.HistogramOpts{ 62 | Name: "http_request_duration_seconds", 63 | Help: "A histogram of latencies for requests.", 64 | Buckets: []float64{.001, .01, .05, .1, .5, 1, 5}, 65 | }, 66 | []string{"handler", "method"}, 67 | ) 68 | 69 | // responseSize has no labels, making it a zero-dimensional 70 | // ObserverVec. 71 | httpResResponseSize = prometheus.NewHistogramVec( 72 | prometheus.HistogramOpts{ 73 | Name: "http_response_size_bytes", 74 | Help: "A histogram of response sizes for requests.", 75 | Buckets: []float64{100, 200, 500, 900, 1500, 5000}, 76 | }, 77 | []string{}, 78 | ) 79 | ) 80 | 81 | var ignoredNamespaces = []string{ 82 | metav1.NamespaceSystem, 83 | metav1.NamespacePublic, 84 | } 85 | 86 | // WebhookServer is a server that handles mutating admission webhooks 87 | type WebhookServer struct { 88 | Config *config.Config 89 | Server *http.Server 90 | } 91 | 92 | type patchOperation struct { 93 | Op string `json:"op"` 94 | Path string `json:"path"` 95 | Value interface{} `json:"value,omitempty"` 96 | } 97 | 98 | func init() { 99 | _ = corev1.AddToScheme(runtimeScheme) 100 | _ = admissionregistrationv1beta1.AddToScheme(runtimeScheme) 101 | // defaulting with webhooks: 102 | // https://github.com/kubernetes/kubernetes/issues/57982 103 | _ = corev1.AddToScheme(runtimeScheme) 104 | 105 | // Metrics have to be registered to be exposed: 106 | prometheus.MustRegister(injectionCounter, httpReqInFlightGauge, httpReqCounter, httpReqDuration, httpResResponseSize) 107 | } 108 | 109 | func instrumentHandler(name string, h http.Handler) http.Handler { 110 | return promhttp.InstrumentHandlerInFlight(httpReqInFlightGauge, 111 | promhttp.InstrumentHandlerDuration(httpReqDuration.MustCurryWith(prometheus.Labels{"handler": name}), 112 | promhttp.InstrumentHandlerCounter(httpReqCounter, 113 | promhttp.InstrumentHandlerResponseSize(httpResResponseSize, h), 114 | ), 115 | ), 116 | ) 117 | } 118 | 119 | // (https://github.com/kubernetes/kubernetes/issues/57982) 120 | func applyDefaultsWorkaround(containers []corev1.Container, volumes []corev1.Volume) { 121 | defaulter.Default(&corev1.Pod{ 122 | Spec: corev1.PodSpec{ 123 | Containers: containers, 124 | Volumes: volumes, 125 | }, 126 | }) 127 | } 128 | 129 | func (whsvr *WebhookServer) statusAnnotationKey() string { 130 | return whsvr.Config.AnnotationNamespace + "/status" 131 | } 132 | 133 | func (whsvr *WebhookServer) requestAnnotationKey() string { 134 | return whsvr.Config.AnnotationNamespace + "/request" 135 | } 136 | 137 | // Check whether the target resoured need to be mutated. returns the canonicalized full name of the injection config 138 | // if found, or an error if not. 139 | func (whsvr *WebhookServer) getSidecarConfigurationRequested(ignoredList []string, metadata *metav1.ObjectMeta) (string, error) { 140 | // skip special kubernetes system namespaces 141 | for _, namespace := range ignoredList { 142 | if metadata.Namespace == namespace { 143 | glog.Infof("Pod %s/%s should skip injection due to ignored namespace", metadata.Name, metadata.Namespace) 144 | return "", ErrSkipIgnoredNamespace 145 | } 146 | } 147 | 148 | annotations := metadata.GetAnnotations() 149 | if annotations == nil { 150 | annotations = map[string]string{} 151 | } 152 | 153 | statusAnnotationKey := whsvr.statusAnnotationKey() 154 | requestAnnotationKey := whsvr.requestAnnotationKey() 155 | 156 | status, ok := annotations[statusAnnotationKey] 157 | if ok && strings.ToLower(status) == StatusInjected { 158 | glog.Infof("Pod %s/%s annotation %s=%s indicates injection already satisfied, skipping", metadata.Namespace, metadata.Name, statusAnnotationKey, status) 159 | return "", ErrSkipAlreadyInjected 160 | } 161 | 162 | // determine whether to perform mutation based on annotation for the target resource 163 | requestedInjection, ok := annotations[requestAnnotationKey] 164 | if !ok { 165 | glog.Infof("Pod %s/%s annotation %s is missing, skipping injection", metadata.Namespace, metadata.Name, requestAnnotationKey) 166 | return "", ErrMissingRequestAnnotation 167 | } 168 | ic, err := whsvr.Config.GetInjectionConfig(requestedInjection) 169 | if err != nil { 170 | glog.Errorf("Mutation policy for pod %s/%s: %v", metadata.Namespace, metadata.Name, err) 171 | return "", ErrRequestedSidecarNotFound 172 | } 173 | 174 | glog.Infof("Pod %s/%s annotation %s=%s requesting sidecar config %s", metadata.Namespace, metadata.Name, requestAnnotationKey, requestedInjection, ic.FullName()) 175 | return ic.FullName(), nil 176 | } 177 | 178 | func setEnvironment(target []corev1.Container, addedEnv []corev1.EnvVar, basePath string) (patch []patchOperation) { 179 | var value interface{} 180 | for containerIndex, container := range target { 181 | // for each container in the spec, determine if we want to patch with any env vars 182 | first := len(container.Env) == 0 183 | for _, add := range addedEnv { 184 | path := fmt.Sprintf("%s/%d/env", basePath, containerIndex) 185 | hasKey := false 186 | // make sure we dont override any existing env vars; we only add, dont replace 187 | for _, origEnv := range container.Env { 188 | if origEnv.Name == add.Name { 189 | hasKey = true 190 | break 191 | } 192 | } 193 | if !hasKey { 194 | // make a patch 195 | value = add 196 | if first { 197 | first = false 198 | value = []corev1.EnvVar{add} 199 | } else { 200 | path = path + "/-" 201 | } 202 | patch = append(patch, patchOperation{ 203 | Op: "add", 204 | Path: path, 205 | Value: value, 206 | }) 207 | } 208 | } 209 | } 210 | return patch 211 | } 212 | 213 | func addContainers(target, added []corev1.Container, basePath string) (patch []patchOperation) { 214 | first := len(target) == 0 215 | var value interface{} 216 | for _, add := range added { 217 | value = add 218 | path := basePath 219 | if first { 220 | first = false 221 | value = []corev1.Container{add} 222 | } else { 223 | path = path + "/-" 224 | } 225 | patch = append(patch, patchOperation{ 226 | Op: "add", 227 | Path: path, 228 | Value: value, 229 | }) 230 | } 231 | return patch 232 | } 233 | 234 | func setHostNetwork(target bool, addedHostNetwork bool, basePath string) (patch []patchOperation) { 235 | if addedHostNetwork == true { 236 | patch = append(patch, patchOperation{ 237 | Op: "replace", 238 | Path: basePath, 239 | Value: addedHostNetwork, 240 | }) 241 | } 242 | return patch 243 | } 244 | 245 | func setHostPID(target bool, addedHostPID bool, basePath string) (patch []patchOperation) { 246 | if addedHostPID == true { 247 | patch = append(patch, patchOperation{ 248 | Op: "replace", 249 | Path: basePath, 250 | Value: addedHostPID, 251 | }) 252 | } 253 | return patch 254 | } 255 | 256 | func addVolumes(existing, added []corev1.Volume, basePath string) (patch []patchOperation) { 257 | hasVolume := func(existing []corev1.Volume, add corev1.Volume) bool { 258 | for _, v := range existing { 259 | // if any of the existing volumes have the same name as test.Name, skip 260 | // injecting it 261 | if v.Name == add.Name { 262 | return true 263 | } 264 | } 265 | return false 266 | } 267 | for _, add := range added { 268 | value := add 269 | 270 | if hasVolume(existing, add) { 271 | continue 272 | } 273 | patch = append(patch, patchOperation{ 274 | Op: "add", 275 | Path: basePath + "/-", 276 | Value: value, 277 | }) 278 | } 279 | return patch 280 | } 281 | 282 | func addVolumeMounts(target []corev1.Container, addedVolumeMounts []corev1.VolumeMount, basePath string) (patch []patchOperation) { 283 | var value interface{} 284 | for containerIndex, container := range target { 285 | // for each container in the spec, determine if we want to patch with any volume mounts 286 | first := len(container.VolumeMounts) == 0 287 | for _, add := range addedVolumeMounts { 288 | path := fmt.Sprintf("%s/%d/volumeMounts", basePath, containerIndex) 289 | hasKey := false 290 | // make sure we dont override any existing volume mounts; we only add, dont replace 291 | for _, origVolumeMount := range container.VolumeMounts { 292 | if origVolumeMount.Name == add.Name { 293 | hasKey = true 294 | break 295 | } 296 | } 297 | if !hasKey { 298 | // make a patch 299 | value = add 300 | if first { 301 | first = false 302 | value = []corev1.VolumeMount{add} 303 | } else { 304 | path = path + "/-" 305 | } 306 | patch = append(patch, patchOperation{ 307 | Op: "add", 308 | Path: path, 309 | Value: value, 310 | }) 311 | } 312 | } 313 | } 314 | return patch 315 | } 316 | 317 | func addHostAliases(target, added []corev1.HostAlias, basePath string) (patch []patchOperation) { 318 | first := len(target) == 0 319 | var value interface{} 320 | for _, add := range added { 321 | value = add 322 | path := basePath 323 | if first { 324 | first = false 325 | value = []corev1.HostAlias{add} 326 | } else { 327 | path = path + "/-" 328 | } 329 | patch = append(patch, patchOperation{ 330 | Op: "add", 331 | Path: path, 332 | Value: value, 333 | }) 334 | } 335 | return patch 336 | } 337 | 338 | func setServiceAccount(initContainers []corev1.Container, containers []corev1.Container, sa string, basePath string) (patch []patchOperation) { 339 | patch = append(patch, patchOperation{ 340 | Op: "replace", 341 | Path: path.Join(basePath, "serviceAccountName"), 342 | Value: sa, 343 | }) 344 | 345 | // if we find any pre-existing VolumeMounts that provide the default serviceaccount token, we need to snip 346 | // them out, so the ServiceAccountController will create the correct VolumeMount once we patch this pod 347 | // volumeMounts: 348 | // - name: default-token-wlfz2 349 | // readOnly: true 350 | // mountPath: /var/run/secrets/kubernetes.io/serviceaccount 351 | for icIndex, ic := range initContainers { 352 | for vmIndex, vm := range ic.VolumeMounts { 353 | if vm.MountPath == serviceAccountTokenMountPath { 354 | patch = append(patch, patchOperation{ 355 | Op: "remove", 356 | Path: fmt.Sprintf("%s/initContainers/%d/volumeMounts/%d", basePath, icIndex, vmIndex), 357 | }) 358 | } 359 | } 360 | } 361 | for cIndex, c := range containers { 362 | for vmIndex, vm := range c.VolumeMounts { 363 | if vm.MountPath == serviceAccountTokenMountPath { 364 | patch = append(patch, patchOperation{ 365 | Op: "remove", 366 | Path: fmt.Sprintf("%s/containers/%d/volumeMounts/%d", basePath, cIndex, vmIndex), 367 | }) 368 | } 369 | } 370 | } 371 | return patch 372 | } 373 | 374 | // for containers, add any env vars that are not already defined in the Env list. 375 | // this does _not_ return patches; this is intended to be used only on containers defined 376 | // in the injection config, so the resources do not exist yet in the k8s api (thus no patch needed) 377 | func mergeEnvVars(envs []corev1.EnvVar, containers []corev1.Container) []corev1.Container { 378 | hasEnvVar := func(existing []corev1.EnvVar, add corev1.EnvVar) bool { 379 | for _, v := range existing { 380 | // if any of the existing volumes have the same name as test.Name, skip 381 | // injecting it 382 | if v.Name == add.Name { 383 | return true 384 | } 385 | } 386 | return false 387 | } 388 | mutatedContainers := []corev1.Container{} 389 | for _, c := range containers { 390 | for _, newEnv := range envs { 391 | // check each container for each env var by name. 392 | // if the container has a matching name, dont override! 393 | if hasEnvVar(c.Env, newEnv) { 394 | continue 395 | } 396 | c.Env = append(c.Env, newEnv) 397 | } 398 | mutatedContainers = append(mutatedContainers, c) 399 | } 400 | return mutatedContainers 401 | } 402 | 403 | func mergeVolumeMounts(volumeMounts []corev1.VolumeMount, containers []corev1.Container) []corev1.Container { 404 | mutatedContainers := []corev1.Container{} 405 | for _, c := range containers { 406 | for _, newVolumeMount := range volumeMounts { 407 | // check each container for each volume mount by name. 408 | // if the container has a matching name, dont override! 409 | skip := false 410 | for _, origVolumeMount := range c.VolumeMounts { 411 | if origVolumeMount.Name == newVolumeMount.Name { 412 | skip = true 413 | break 414 | } 415 | } 416 | if !skip { 417 | c.VolumeMounts = append(c.VolumeMounts, newVolumeMount) 418 | } 419 | } 420 | mutatedContainers = append(mutatedContainers, c) 421 | } 422 | return mutatedContainers 423 | } 424 | 425 | func updateAnnotations(target map[string]string, added map[string]string) (patch []patchOperation) { 426 | for key, value := range added { 427 | keyEscaped := strings.Replace(key, "/", "~1", -1) 428 | 429 | if target == nil || target[key] == "" { 430 | target = map[string]string{} 431 | patch = append(patch, patchOperation{ 432 | Op: "add", 433 | Path: path.Join("/metadata/annotations", keyEscaped), 434 | Value: value, 435 | }) 436 | } else { 437 | patch = append(patch, patchOperation{ 438 | Op: "replace", 439 | Path: path.Join("/metadata/annotations", keyEscaped), 440 | Value: value, 441 | }) 442 | } 443 | } 444 | return patch 445 | } 446 | 447 | // create mutation patch for resoures 448 | func createPatch(pod *corev1.Pod, inj *config.InjectionConfig, annotations map[string]string) ([]byte, error) { 449 | var patch []patchOperation 450 | 451 | // be sure to inject the serviceAccountName before adding any volumeMounts, because we must prune out any existing 452 | // volumeMounts that were added to support the default service account. Because this removal is by index, we splice 453 | // them out before appending new volumes at the end. 454 | if inj.ServiceAccountName != "" && (pod.Spec.ServiceAccountName == "" || pod.Spec.ServiceAccountName == "default") { 455 | // only override the serviceaccount name if not set in the pod spec 456 | patch = append(patch, setServiceAccount(pod.Spec.InitContainers, pod.Spec.Containers, inj.ServiceAccountName, "/spec")...) 457 | } 458 | 459 | { // initcontainer injections 460 | // patch all existing InitContainers with the VolumeMounts+EnvVars, and add injected initcontainers 461 | patch = append(patch, setEnvironment(pod.Spec.InitContainers, inj.Environment, "/spec/initContainers")...) 462 | patch = append(patch, addVolumeMounts(pod.Spec.InitContainers, inj.VolumeMounts, "/spec/initContainers")...) 463 | // next, make sure any injected init containers in our config get the EnvVars and VolumeMounts injected 464 | // this mutates inj.InitContainers with our environment vars 465 | mutatedInjectedInitContainers := mergeEnvVars(inj.Environment, inj.InitContainers) 466 | mutatedInjectedInitContainers = mergeVolumeMounts(inj.VolumeMounts, mutatedInjectedInitContainers) 467 | patch = append(patch, addContainers(pod.Spec.InitContainers, mutatedInjectedInitContainers, "/spec/initContainers")...) 468 | } 469 | 470 | { // container injections 471 | // now, patch all existing containers with the env vars and volume mounts, and add injected containers 472 | patch = append(patch, setEnvironment(pod.Spec.Containers, inj.Environment, "/spec/containers")...) 473 | patch = append(patch, addVolumeMounts(pod.Spec.Containers, inj.VolumeMounts, "/spec/containers")...) 474 | // first, make sure any injected containers in our config get the EnvVars and VolumeMounts injected 475 | // this mutates inj.Containers with our environment vars 476 | mutatedInjectedContainers := mergeEnvVars(inj.Environment, inj.Containers) 477 | mutatedInjectedContainers = mergeVolumeMounts(inj.VolumeMounts, mutatedInjectedContainers) 478 | patch = append(patch, addContainers(pod.Spec.Containers, mutatedInjectedContainers, "/spec/containers")...) 479 | } 480 | 481 | { // pod level mutations 482 | // now, add hostAliases and volumes 483 | patch = append(patch, addHostAliases(pod.Spec.HostAliases, inj.HostAliases, "/spec/hostAliases")...) 484 | patch = append(patch, addVolumes(pod.Spec.Volumes, inj.Volumes, "/spec/volumes")...) 485 | } 486 | 487 | { // now, set hostNetwork,hostPID 488 | patch = append(patch, setHostNetwork(pod.Spec.HostNetwork, inj.HostNetwork, "/spec/hostNetwork")...) 489 | patch = append(patch, setHostPID(pod.Spec.HostPID, inj.HostPID, "/spec/hostPID")...) 490 | } 491 | 492 | // last but not least, set annotations 493 | patch = append(patch, updateAnnotations(pod.Annotations, annotations)...) 494 | return json.Marshal(patch) 495 | } 496 | 497 | // main mutation process 498 | func (whsvr *WebhookServer) mutate(req *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { 499 | var pod corev1.Pod 500 | if err := json.Unmarshal(req.Object.Raw, &pod); err != nil { 501 | glog.Errorf("Could not unmarshal raw object: %v", err) 502 | injectionCounter.With(prometheus.Labels{"status": "error", "reason": "unmarshal_error", "requested": ""}).Inc() 503 | return &v1beta1.AdmissionResponse{ 504 | Result: &metav1.Status{ 505 | Message: err.Error(), 506 | }, 507 | } 508 | } 509 | 510 | glog.Infof("AdmissionReview for Kind=%s, Namespace=%s Name=%s (%s) UID=%s patchOperation=%s UserInfo=%s", 511 | req.Kind, req.Namespace, req.Name, pod.Name, req.UID, req.Operation, req.UserInfo) 512 | 513 | // determine whether to perform mutation 514 | injectionKey, err := whsvr.getSidecarConfigurationRequested(ignoredNamespaces, &pod.ObjectMeta) 515 | if err != nil { 516 | glog.Infof("Skipping mutation of %s/%s: %v", pod.Namespace, pod.Name, err) 517 | reason := GetErrorReason(err) 518 | injectionCounter.With(prometheus.Labels{"status": "skipped", "reason": reason, "requested": injectionKey}).Inc() 519 | return &v1beta1.AdmissionResponse{ 520 | Allowed: true, 521 | } 522 | } 523 | 524 | injectionConfig, err := whsvr.Config.GetInjectionConfig(injectionKey) 525 | if err != nil { 526 | glog.Errorf("Error getting injection config %s, permitting launch of pod with no sidecar injected: %s", injectionConfig, err.Error()) 527 | // dont prevent pods from launching! just return allowed 528 | injectionCounter.With(prometheus.Labels{"status": "skipped", "reason": "missing_config", "requested": injectionKey}).Inc() 529 | return &v1beta1.AdmissionResponse{ 530 | Allowed: true, 531 | } 532 | } 533 | 534 | // Workaround: https://github.com/kubernetes/kubernetes/issues/57982 535 | applyDefaultsWorkaround(injectionConfig.Containers, injectionConfig.Volumes) 536 | annotations := map[string]string{} 537 | annotations[whsvr.statusAnnotationKey()] = StatusInjected 538 | patchBytes, err := createPatch(&pod, injectionConfig, annotations) 539 | if err != nil { 540 | injectionCounter.With(prometheus.Labels{"status": "error", "reason": "patching_error", "requested": injectionKey}).Inc() 541 | return &v1beta1.AdmissionResponse{ 542 | Result: &metav1.Status{ 543 | Message: err.Error(), 544 | }, 545 | } 546 | } 547 | 548 | glog.Infof("AdmissionResponse: patch=%v\n", string(patchBytes)) 549 | injectionCounter.With(prometheus.Labels{"status": "success", "reason": "all_groovy", "requested": injectionKey}).Inc() 550 | return &v1beta1.AdmissionResponse{ 551 | Allowed: true, 552 | Patch: patchBytes, 553 | PatchType: func() *v1beta1.PatchType { 554 | pt := v1beta1.PatchTypeJSONPatch 555 | return &pt 556 | }(), 557 | } 558 | } 559 | 560 | // MetricsHandler method for webhook server 561 | func (whsvr *WebhookServer) MetricsHandler() http.Handler { 562 | return instrumentHandler("metrics", promhttp.Handler()) 563 | } 564 | 565 | // HealthHandler returns ok 566 | func (whsvr *WebhookServer) HealthHandler() http.Handler { 567 | return instrumentHandler("health", http.HandlerFunc(whsvr.healthHandler)) 568 | } 569 | 570 | // MutateHandler method for webhook server 571 | func (whsvr *WebhookServer) MutateHandler() http.Handler { 572 | return instrumentHandler("mutate", http.HandlerFunc(whsvr.mutateHandler)) 573 | } 574 | 575 | func (whsvr *WebhookServer) healthHandler(w http.ResponseWriter, r *http.Request) { 576 | fmt.Fprintf(w, "d|-_-|b 🦄") 577 | } 578 | 579 | func (whsvr *WebhookServer) mutateHandler(w http.ResponseWriter, r *http.Request) { 580 | var body []byte 581 | if r.Body != nil { 582 | if data, err := ioutil.ReadAll(r.Body); err == nil { 583 | body = data 584 | } 585 | } 586 | if len(body) == 0 { 587 | glog.Error("empty body") 588 | http.Error(w, "empty body", http.StatusBadRequest) 589 | return 590 | } 591 | 592 | // verify the content type is accurate 593 | contentType := r.Header.Get("Content-Type") 594 | if contentType != "application/json" { 595 | glog.Errorf("Content-Type=%s, expect application/json", contentType) 596 | http.Error(w, "invalid Content-Type, expect `application/json`", http.StatusUnsupportedMediaType) 597 | return 598 | } 599 | 600 | var admissionResponse *v1beta1.AdmissionResponse 601 | ar := v1beta1.AdmissionReview{} 602 | if _, _, err := deserializer.Decode(body, nil, &ar); err != nil { 603 | glog.Errorf("Can't decode body: %v", err) 604 | admissionResponse = &v1beta1.AdmissionResponse{ 605 | Result: &metav1.Status{ 606 | Message: err.Error(), 607 | }, 608 | } 609 | } else { 610 | admissionResponse = whsvr.mutate(ar.Request) 611 | } 612 | 613 | admissionReview := v1beta1.AdmissionReview{} 614 | if admissionResponse != nil { 615 | admissionReview.Response = admissionResponse 616 | if ar.Request != nil { 617 | admissionReview.Response.UID = ar.Request.UID 618 | } 619 | } 620 | 621 | resp, err := json.Marshal(admissionReview) 622 | if err != nil { 623 | glog.Errorf("Can't encode response: %v", err) 624 | http.Error(w, fmt.Sprintf("could not encode response: %v", err), http.StatusInternalServerError) 625 | } 626 | glog.Infof("Ready to write reponse ...") 627 | if _, err := w.Write(resp); err != nil { 628 | glog.Errorf("Can't write response: %v", err) 629 | http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError) 630 | } 631 | } 632 | -------------------------------------------------------------------------------- /pkg/server/webhook_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "testing" 11 | 12 | "github.com/ghodss/yaml" 13 | "github.com/nsf/jsondiff" // for json diffing patches 14 | "github.com/tumblr/k8s-sidecar-injector/internal/pkg/config" 15 | _ "github.com/tumblr/k8s-sidecar-injector/internal/pkg/testing" 16 | "k8s.io/api/admission/v1beta1" 17 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 | ) 19 | 20 | var ( 21 | sidecars = "test/fixtures/sidecars" 22 | jsondiffopts = jsondiff.DefaultConsoleOptions() 23 | 24 | // all these configs are deserialized into metav1.ObjectMeta structs 25 | obj1 = "test/fixtures/k8s/object1.yaml" 26 | obj2latest = "test/fixtures/k8s/object2latest.yaml" 27 | obj2v = "test/fixtures/k8s/object2v.yaml" 28 | env1 = "test/fixtures/k8s/env1.yaml" 29 | obj3Missing = "test/fixtures/k8s/object3-missing.yaml" 30 | obj4 = "test/fixtures/k8s/object4.yaml" 31 | obj5 = "test/fixtures/k8s/object5.yaml" 32 | obj6 = "test/fixtures/k8s/object6.yaml" 33 | obj7 = "test/fixtures/k8s/object7.yaml" 34 | obj7v2 = "test/fixtures/k8s/object7-v2.yaml" 35 | obj7v3 = "test/fixtures/k8s/object7-badrequestformat.yaml" 36 | ignoredNamespace = "test/fixtures/k8s/ignored-namespace-pod.yaml" 37 | badSidecar = "test/fixtures/k8s/bad-sidecar.yaml" 38 | 39 | testIgnoredNamespaces = []string{"ignore-me"} 40 | 41 | // tests to check config loading of sidecars 42 | configTests = []expectedSidecarConfiguration{ 43 | {configuration: obj1, expectedSidecar: "sidecar-test:latest"}, 44 | {configuration: obj2latest, expectedSidecar: "", expectedError: ErrRequestedSidecarNotFound}, 45 | {configuration: obj2v, expectedSidecar: "complex-sidecar:v420.69"}, 46 | {configuration: env1, expectedSidecar: "env1:latest"}, 47 | {configuration: obj3Missing, expectedSidecar: "", expectedError: ErrMissingRequestAnnotation}, // this one is missing any annotations :) 48 | {configuration: obj4, expectedSidecar: "", expectedError: ErrSkipAlreadyInjected}, // this one is already injected, so it should not get injected again 49 | {configuration: obj5, expectedSidecar: "volume-mounts:latest"}, 50 | {configuration: obj6, expectedSidecar: "host-aliases:latest"}, 51 | {configuration: obj7, expectedSidecar: "init-containers:latest"}, 52 | {configuration: obj7v2, expectedSidecar: "init-containers:v2"}, 53 | {configuration: obj7v3, expectedSidecar: "", expectedError: ErrRequestedSidecarNotFound}, 54 | {configuration: ignoredNamespace, expectedSidecar: "", expectedError: ErrSkipIgnoredNamespace}, 55 | {configuration: badSidecar, expectedSidecar: "", expectedError: ErrRequestedSidecarNotFound}, 56 | } 57 | 58 | // tests to check the mutate() function for correct operation 59 | mutationTests = []mutationTest{ 60 | {name: "missing-sidecar-config", allowed: true, patchExpected: false}, 61 | {name: "sidecar-test-1", allowed: true, patchExpected: true}, 62 | {name: "env-override", allowed: true, patchExpected: true}, 63 | {name: "service-account", allowed: true, patchExpected: true}, 64 | {name: "service-account-already-set", allowed: true, patchExpected: true}, 65 | {name: "service-account-set-default", allowed: true, patchExpected: true}, 66 | {name: "service-account-default-token", allowed: true, patchExpected: true}, 67 | {name: "volumetest", allowed: true, patchExpected: true}, 68 | {name: "volumetest-existingvolume", allowed: true, patchExpected: true}, 69 | } 70 | sidecarConfigs, _ = filepath.Glob(path.Join(sidecars, "*.yaml")) 71 | expectedNumInjectionConfigs = len(sidecarConfigs) 72 | ) 73 | 74 | type expectedSidecarConfiguration struct { 75 | configuration string 76 | expectedSidecar string 77 | expectedError error 78 | } 79 | 80 | type mutationTest struct { 81 | // name is a file relative to test/fixtures/k8s/admissioncontrol/request/ ending in .yaml 82 | // which is the v1beta1.AdmissionRequest object passed to mutate 83 | name string 84 | allowed bool 85 | patchExpected bool 86 | } 87 | 88 | func TestLoadConfig(t *testing.T) { 89 | c, err := config.LoadConfigDirectory(sidecars) 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | c.AnnotationNamespace = "injector.unittest.com" 94 | if len(c.Injections) != expectedNumInjectionConfigs { 95 | t.Fatalf("expected %d injection configs to be loaded from %s, but got %d", expectedNumInjectionConfigs, sidecars, len(c.Injections)) 96 | } 97 | if c.AnnotationNamespace != "injector.unittest.com" { 98 | t.Fatalf("expected injector.unittest.com default AnnotationNamespace but got %s", c.AnnotationNamespace) 99 | } 100 | 101 | s := &WebhookServer{ 102 | Config: c, 103 | Server: &http.Server{ 104 | Addr: ":6969", 105 | }, 106 | } 107 | 108 | for _, test := range configTests { 109 | data, err := ioutil.ReadFile(test.configuration) 110 | if err != nil { 111 | t.Fatalf("unable to load object metadata yaml: %v", err) 112 | } 113 | 114 | var obj *metav1.ObjectMeta 115 | if err := yaml.Unmarshal(data, &obj); err != nil { 116 | t.Fatalf("unable to unmarshal object metadata yaml: %v", err) 117 | } 118 | key, err := s.getSidecarConfigurationRequested(testIgnoredNamespaces, obj) 119 | if err != test.expectedError { 120 | t.Fatalf("%s: (expectedSidecar %s) error: %v did not match %v (k %v)", test.configuration, test.expectedSidecar, err, test.expectedError, key) 121 | } 122 | if key != test.expectedSidecar { 123 | t.Fatalf("%s: expected sidecar to be %v but was %v instead", test.configuration, test.expectedSidecar, key) 124 | } 125 | 126 | } 127 | } 128 | 129 | func TestMutation(t *testing.T) { 130 | c, err := config.LoadConfigDirectory(sidecars) 131 | if err != nil { 132 | t.Error(err) 133 | t.Fail() 134 | } 135 | c.AnnotationNamespace = "injector.unittest.com" 136 | 137 | s := &WebhookServer{ 138 | Config: c, 139 | Server: &http.Server{ 140 | Addr: ":6969", 141 | }, 142 | } 143 | 144 | for _, test := range mutationTests { 145 | // now, try to perform the mutation on the k8s object 146 | var req v1beta1.AdmissionRequest 147 | reqFile := fmt.Sprintf("test/fixtures/k8s/admissioncontrol/request/%s.yaml", test.name) 148 | resPatchFile := fmt.Sprintf("test/fixtures/k8s/admissioncontrol/patch/%s.json", test.name) 149 | // load the AdmissionRequest object 150 | reqData, err := ioutil.ReadFile(reqFile) 151 | if err != nil { 152 | t.Fatalf("%s: unable to load AdmissionRequest object: %v", reqFile, err) 153 | } 154 | if err := yaml.Unmarshal(reqData, &req); err != nil { 155 | t.Fatalf("%s: unable to unmarshal AdmissionRequest yaml: %v", reqFile, err) 156 | } 157 | 158 | // stuff the request into mutate, and catch the response 159 | res := s.mutate(&req) 160 | 161 | // extract this field, so we can diff json separate from the AdmissionResponse object 162 | resPatch := res.Patch 163 | res.Patch = nil // zero this field out 164 | 165 | if test.allowed != res.Allowed { 166 | t.Fatalf("expected AdmissionResponse.Allowed=%v differed from received AdmissionResponse.Allowed=%v", test.allowed, res.Allowed) 167 | } 168 | 169 | // diff the JSON patch object with expected JSON loaded from disk 170 | // we do this because this is way easier on the eyes than diffing 171 | // a yaml base64 encoded string 172 | if test.patchExpected { 173 | if _, err := os.Stat(resPatchFile); err != nil { 174 | t.Fatalf("%s: unable to load expected patch JSON response: %v", resPatchFile, err) 175 | } 176 | t.Logf("Loading patch data from %s...", resPatchFile) 177 | expectedPatchData, err := ioutil.ReadFile(resPatchFile) 178 | if err != nil { 179 | t.Error(err) 180 | t.Fail() 181 | } 182 | difference, diffString := jsondiff.Compare(expectedPatchData, resPatch, &jsondiffopts) 183 | if difference != jsondiff.FullMatch { 184 | t.Errorf("Actual patch JSON: %s", string(resPatch)) 185 | t.Fatalf("received AdmissionResponse.patch field differed from expected with %s (%s) (actual on left, expected on right):\n%s", resPatchFile, difference.String(), diffString) 186 | } 187 | } 188 | 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /test/fixtures/gabe-test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: gabe-test 3 | environment: 4 | - name: NEW_ENV 5 | value: "injected-new-env" 6 | - name: ENVIRONMENT 7 | value: "sidecarproduction" 8 | - name: DATACENTER 9 | value: "from-injector" 10 | containers: 11 | - name: sidecar-nginx 12 | image: nginx:1.12.2 13 | imagePullPolicy: IfNotPresent 14 | env: 15 | - name: DATACENTER 16 | value: "set-in-sidecar" 17 | ports: 18 | - containerPort: 80 19 | - name: sidecar-nginx-2 20 | image: nginx:1.12.2 21 | imagePullPolicy: IfNotPresent 22 | -------------------------------------------------------------------------------- /test/fixtures/k8s/admissioncontrol/patch/env-override.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "op": "add", 4 | "path": "/spec/containers/0/env/-", 5 | "value": { 6 | "name": "FOO_BAR", 7 | "value": "something interesting" 8 | } 9 | }, 10 | { 11 | "path": "/spec/containers/0/env/-", 12 | "value": { 13 | "name": "ENVIRONMENT", 14 | "value": "production" 15 | }, 16 | "op": "add" 17 | }, 18 | { 19 | "op": "add", 20 | "path": "/metadata/annotations/injector.unittest.com~1status", 21 | "value": "injected" 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /test/fixtures/k8s/admissioncontrol/patch/service-account-already-set.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "op" : "add", 4 | "path" : "/metadata/annotations/injector.unittest.com~1status", 5 | "value" : "injected" 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /test/fixtures/k8s/admissioncontrol/patch/service-account-default-token.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "op": "replace", 4 | "path": "/spec/serviceAccountName", 5 | "value": "someaccount" 6 | }, 7 | { 8 | "op": "remove", 9 | "path": "/spec/initContainers/0/volumeMounts/0" 10 | }, 11 | { 12 | "op": "remove", 13 | "path": "/spec/containers/1/volumeMounts/1" 14 | }, 15 | { 16 | "op" : "add", 17 | "path" : "/metadata/annotations/injector.unittest.com~1status", 18 | "value" : "injected" 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /test/fixtures/k8s/admissioncontrol/patch/service-account-set-default.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "op": "replace", 4 | "path": "/spec/serviceAccountName", 5 | "value": "someaccount" 6 | }, 7 | { 8 | "op" : "add", 9 | "path" : "/metadata/annotations/injector.unittest.com~1status", 10 | "value" : "injected" 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /test/fixtures/k8s/admissioncontrol/patch/service-account.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "op": "replace", 4 | "path": "/spec/serviceAccountName", 5 | "value": "someaccount" 6 | }, 7 | { 8 | "op" : "add", 9 | "path" : "/metadata/annotations/injector.unittest.com~1status", 10 | "value" : "injected" 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /test/fixtures/k8s/admissioncontrol/patch/sidecar-test-1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "op" : "add", 4 | "path" : "/spec/containers", 5 | "value" : [ 6 | { 7 | "env" : [ 8 | { 9 | "name" : "DATACENTER", 10 | "value" : "bf2" 11 | }, 12 | { 13 | "name" : "FROM_INJECTOR", 14 | "value" : "bar" 15 | } 16 | ], 17 | "image" : "nginx:1.12.2", 18 | "imagePullPolicy" : "IfNotPresent", 19 | "name" : "sidecar-nginx", 20 | "ports" : [ 21 | { 22 | "containerPort" : 80 23 | } 24 | ], 25 | "resources" : {}, 26 | "volumeMounts" : [ 27 | { 28 | "mountPath" : "/etc/nginx", 29 | "name" : "nginx-conf" 30 | } 31 | ] 32 | } 33 | ] 34 | }, 35 | { 36 | "op" : "add", 37 | "path" : "/spec/containers/-", 38 | "value" : { 39 | "env" : [ 40 | { 41 | "name" : "DATACENTER", 42 | "value" : "foo" 43 | }, 44 | { 45 | "name" : "FROM_INJECTOR", 46 | "value" : "bar" 47 | } 48 | ], 49 | "image" : "foo:69", 50 | "name" : "another-sidecar", 51 | "ports" : [ 52 | { 53 | "containerPort" : 420 54 | } 55 | ], 56 | "resources" : {} 57 | } 58 | }, 59 | { 60 | "op" : "add", 61 | "path" : "/spec/volumes/-", 62 | "value" : { 63 | "configMap" : { 64 | "name" : "nginx-configmap" 65 | }, 66 | "name" : "nginx-conf" 67 | } 68 | }, 69 | { 70 | "op" : "add", 71 | "path" : "/metadata/annotations/injector.unittest.com~1status", 72 | "value" : "injected" 73 | } 74 | ] 75 | -------------------------------------------------------------------------------- /test/fixtures/k8s/admissioncontrol/patch/volumetest-existingvolume.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "op" : "add", 4 | "path" : "/spec/initContainers/0/env", 5 | "value" : [ 6 | { 7 | "name" : "EVERYWHERE", 8 | "value" : "injected in all containers" 9 | } 10 | ] 11 | }, 12 | { 13 | "op" : "add", 14 | "path" : "/spec/initContainers/1/env", 15 | "value" : [ 16 | { 17 | "name" : "EVERYWHERE", 18 | "value" : "injected in all containers" 19 | } 20 | ] 21 | }, 22 | { 23 | "op" : "add", 24 | "path" : "/spec/initContainers/0/volumeMounts", 25 | "value" : [ 26 | { 27 | "mountPath" : "/usr/share/GeoIP", 28 | "name" : "maxminddb" 29 | } 30 | ] 31 | }, 32 | { 33 | "op" : "add", 34 | "path" : "/spec/initContainers/-", 35 | "value" : { 36 | "command" : [ 37 | "/bin/init.sh" 38 | ], 39 | "env" : [ 40 | { 41 | "name" : "EVERYWHERE", 42 | "value" : "injected in all containers" 43 | } 44 | ], 45 | "image" : "maxmindimage", 46 | "name" : "maxminddb-init", 47 | "resources" : {}, 48 | "volumeMounts" : [ 49 | { 50 | "mountPath" : "/usr/share/GeoIP", 51 | "name" : "maxminddb" 52 | } 53 | ] 54 | } 55 | }, 56 | { 57 | "op" : "add", 58 | "path" : "/spec/containers/0/env", 59 | "value" : [ 60 | { 61 | "name" : "EVERYWHERE", 62 | "value" : "injected in all containers" 63 | } 64 | ] 65 | }, 66 | { 67 | "op" : "add", 68 | "path" : "/spec/containers/1/env", 69 | "value" : [ 70 | { 71 | "name" : "EVERYWHERE", 72 | "value" : "injected in all containers" 73 | } 74 | ] 75 | }, 76 | { 77 | "op" : "add", 78 | "path" : "/spec/containers/1/volumeMounts", 79 | "value" : [ 80 | { 81 | "mountPath" : "/usr/share/GeoIP", 82 | "name" : "maxminddb" 83 | } 84 | ] 85 | }, 86 | { 87 | "op" : "add", 88 | "path" : "/spec/containers/-", 89 | "value" : { 90 | "env" : [ 91 | { 92 | "name" : "EVERYWHERE", 93 | "value" : "injected in all containers" 94 | } 95 | ], 96 | "image" : "maxmindimage", 97 | "name" : "maxminddb", 98 | "resources" : {}, 99 | "volumeMounts" : [ 100 | { 101 | "mountPath" : "/usr/share/GeoIP", 102 | "name" : "maxminddb" 103 | } 104 | ] 105 | } 106 | }, 107 | { 108 | "op" : "add", 109 | "path" : "/spec/volumes/-", 110 | "value" : { 111 | "emptyDir" : {}, 112 | "name" : "anothervolume" 113 | } 114 | }, 115 | { 116 | "op" : "add", 117 | "path" : "/metadata/annotations/injector.unittest.com~1status", 118 | "value" : "injected" 119 | } 120 | ] 121 | -------------------------------------------------------------------------------- /test/fixtures/k8s/admissioncontrol/patch/volumetest.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "op" : "add", 4 | "path" : "/spec/initContainers/0/env", 5 | "value" : [ 6 | { 7 | "name" : "EVERYWHERE", 8 | "value" : "injected in all containers" 9 | } 10 | ] 11 | }, 12 | { 13 | "op" : "add", 14 | "path" : "/spec/initContainers/1/env", 15 | "value" : [ 16 | { 17 | "name" : "EVERYWHERE", 18 | "value" : "injected in all containers" 19 | } 20 | ] 21 | }, 22 | { 23 | "op" : "add", 24 | "path" : "/spec/initContainers/0/volumeMounts", 25 | "value" : [ 26 | { 27 | "mountPath" : "/usr/share/GeoIP", 28 | "name" : "maxminddb" 29 | } 30 | ] 31 | }, 32 | { 33 | "op" : "add", 34 | "path" : "/spec/initContainers/-", 35 | "value" : { 36 | "command" : [ 37 | "/bin/init.sh" 38 | ], 39 | "env" : [ 40 | { 41 | "name" : "EVERYWHERE", 42 | "value" : "injected in all containers" 43 | } 44 | ], 45 | "image" : "maxmindimage", 46 | "name" : "maxminddb-init", 47 | "resources" : {}, 48 | "volumeMounts" : [ 49 | { 50 | "mountPath" : "/usr/share/GeoIP", 51 | "name" : "maxminddb" 52 | } 53 | ] 54 | } 55 | }, 56 | { 57 | "op" : "add", 58 | "path" : "/spec/containers/0/env", 59 | "value" : [ 60 | { 61 | "name" : "EVERYWHERE", 62 | "value" : "injected in all containers" 63 | } 64 | ] 65 | }, 66 | { 67 | "op" : "add", 68 | "path" : "/spec/containers/1/env", 69 | "value" : [ 70 | { 71 | "name" : "EVERYWHERE", 72 | "value" : "injected in all containers" 73 | } 74 | ] 75 | }, 76 | { 77 | "op" : "add", 78 | "path" : "/spec/containers/1/volumeMounts", 79 | "value" : [ 80 | { 81 | "mountPath" : "/usr/share/GeoIP", 82 | "name" : "maxminddb" 83 | } 84 | ] 85 | }, 86 | { 87 | "op" : "add", 88 | "path" : "/spec/containers/-", 89 | "value" : { 90 | "env" : [ 91 | { 92 | "name" : "EVERYWHERE", 93 | "value" : "injected in all containers" 94 | } 95 | ], 96 | "image" : "maxmindimage", 97 | "name" : "maxminddb", 98 | "resources" : {}, 99 | "volumeMounts" : [ 100 | { 101 | "mountPath" : "/usr/share/GeoIP", 102 | "name" : "maxminddb" 103 | } 104 | ] 105 | } 106 | }, 107 | { 108 | "op" : "add", 109 | "path" : "/spec/volumes/-", 110 | "value" : { 111 | "emptyDir" : {}, 112 | "name" : "maxminddb" 113 | } 114 | }, 115 | { 116 | "op" : "add", 117 | "path" : "/spec/volumes/-", 118 | "value" : { 119 | "emptyDir" : {}, 120 | "name" : "anothervolume" 121 | } 122 | }, 123 | { 124 | "op" : "add", 125 | "path" : "/metadata/annotations/injector.unittest.com~1status", 126 | "value" : "injected" 127 | } 128 | ] 129 | -------------------------------------------------------------------------------- /test/fixtures/k8s/admissioncontrol/request/env-override.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # this is an AdmissionRequest object 3 | # https://godoc.org/k8s.io/api/admission/v1beta1#AdmissionRequest 4 | object: 5 | metadata: 6 | annotations: 7 | injector.unittest.com/request: "env1" 8 | spec: 9 | containers: 10 | - name: something 11 | env: 12 | - name: SOME_VARIABLE 13 | value: dope 14 | - name: DATACENTER 15 | value: definedbypod 16 | -------------------------------------------------------------------------------- /test/fixtures/k8s/admissioncontrol/request/missing-sidecar-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # this is an AdmissionRequest object 3 | # https://godoc.org/k8s.io/api/admission/v1beta1#AdmissionRequest 4 | object: 5 | metadata: 6 | annotations: 7 | injector.unittest.com/request: this-doesnt-exist 8 | spec: 9 | containers: 10 | - name: memory-demo-2-ctr 11 | image: polinux/stress 12 | resources: 13 | requests: 14 | memory: "50Mi" 15 | limits: 16 | memory: "100Mi" 17 | command: ["stress"] 18 | args: ["--vm", "1", "--vm-bytes", "250M", "--vm-hang", "1"] 19 | -------------------------------------------------------------------------------- /test/fixtures/k8s/admissioncontrol/request/service-account-already-set.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # this is an AdmissionRequest object 3 | # https://godoc.org/k8s.io/api/admission/v1beta1#AdmissionRequest 4 | object: 5 | metadata: 6 | annotations: 7 | injector.unittest.com/request: "service-account" 8 | spec: 9 | serviceAccountName: "existing" 10 | containers: [] 11 | -------------------------------------------------------------------------------- /test/fixtures/k8s/admissioncontrol/request/service-account-default-token.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # this is an AdmissionRequest object 3 | # https://godoc.org/k8s.io/api/admission/v1beta1#AdmissionRequest 4 | object: 5 | metadata: 6 | annotations: 7 | injector.unittest.com/request: "service-account-default-token" 8 | spec: 9 | serviceAccountName: "default" # this should get replaced 10 | volumes: 11 | - name: bogusvolume 12 | configMap: 13 | name: config-production 14 | defaultMode: 420 15 | - name: default-token-wlfz2 16 | secret: 17 | secretName: default-token-wlfz2 18 | defaultMode: 420 19 | initContainers: 20 | - name: init-ctr1-with-token 21 | volumeMounts: 22 | # this volume mount must be removed, because 23 | # by default, a serviceAccount will mount its token, 24 | # preventing the injected serviceAccount from settings up its mount 25 | - name: default-token-wlfz2 26 | readOnly: true 27 | mountPath: /var/run/secrets/kubernetes.io/serviceaccount 28 | - name: init-ctr2 29 | volumeMounts: [] 30 | containers: 31 | - name: ctr1 32 | volumeMounts: 33 | - name: bogusvolume 34 | readOnly: true 35 | mountPath: /app/config 36 | - name: ctr2-with-token 37 | volumeMounts: 38 | - name: bogusvolume 39 | readOnly: true 40 | mountPath: /app/config 41 | # this volume mount must be removed, because 42 | # by default, a serviceAccount will mount its token, 43 | # preventing the injected serviceAccount from settings up its mount 44 | - name: default-token-wlfz2 45 | readOnly: true 46 | mountPath: /var/run/secrets/kubernetes.io/serviceaccount 47 | - name: ctr3 48 | volumeMounts: [] 49 | -------------------------------------------------------------------------------- /test/fixtures/k8s/admissioncontrol/request/service-account-set-default.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # this is an AdmissionRequest object 3 | # https://godoc.org/k8s.io/api/admission/v1beta1#AdmissionRequest 4 | object: 5 | metadata: 6 | annotations: 7 | injector.unittest.com/request: "service-account" 8 | spec: 9 | serviceAccountName: "default" # this should get replaced 10 | containers: [] 11 | -------------------------------------------------------------------------------- /test/fixtures/k8s/admissioncontrol/request/service-account.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # this is an AdmissionRequest object 3 | # https://godoc.org/k8s.io/api/admission/v1beta1#AdmissionRequest 4 | object: 5 | metadata: 6 | annotations: 7 | injector.unittest.com/request: "service-account" 8 | spec: 9 | containers: [] 10 | -------------------------------------------------------------------------------- /test/fixtures/k8s/admissioncontrol/request/sidecar-test-1.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # this is an AdmissionRequest object 3 | # https://godoc.org/k8s.io/api/admission/v1beta1#AdmissionRequest 4 | object: 5 | metadata: 6 | annotations: 7 | injector.unittest.com/request: "sidecar-test" 8 | spec: 9 | containers: [] 10 | -------------------------------------------------------------------------------- /test/fixtures/k8s/admissioncontrol/request/volumetest-existingvolume.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # this is an AdmissionRequest object 3 | # https://godoc.org/k8s.io/api/admission/v1beta1#AdmissionRequest 4 | object: 5 | metadata: 6 | annotations: 7 | injector.unittest.com/request: "maxmind" 8 | spec: 9 | initContainers: 10 | - image: maxmindimage 11 | # init2 should get a volumeMount injected 12 | name: init2 13 | command: ["/bin/init.sh"] 14 | - image: maxmindimage 15 | name: already-has-volumemount 16 | command: ["/bin/init.sh"] 17 | volumeMounts: 18 | - name: maxminddb # this should NOT get duplicated 19 | mountPath: /usr/share/GeoIP 20 | containers: 21 | - name: ctr-with-volumemount 22 | volumeMounts: 23 | - name: maxminddb # this should NOT get duplicated 24 | mountPath: /usr/share/GeoIP 25 | - name: ctr-without-volumemount 26 | volumes: 27 | # do not redefine the volume here 28 | - name: maxminddb 29 | emptyDir: {} 30 | 31 | -------------------------------------------------------------------------------- /test/fixtures/k8s/admissioncontrol/request/volumetest.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # this is an AdmissionRequest object 3 | # https://godoc.org/k8s.io/api/admission/v1beta1#AdmissionRequest 4 | object: 5 | metadata: 6 | annotations: 7 | injector.unittest.com/request: "maxmind" 8 | spec: 9 | initContainers: 10 | - image: maxmindimage 11 | # init2 should get a volumeMount injected 12 | name: init2 13 | command: ["/bin/init.sh"] 14 | - image: maxmindimage 15 | name: already-has-volumemount 16 | command: ["/bin/init.sh"] 17 | volumeMounts: 18 | - name: maxminddb # this should NOT get duplicated 19 | mountPath: /usr/share/GeoIP 20 | containers: 21 | - name: ctr-with-volumemount 22 | volumeMounts: 23 | - name: maxminddb # this should NOT get duplicated 24 | mountPath: /usr/share/GeoIP 25 | - name: ctr-without-volumemount 26 | -------------------------------------------------------------------------------- /test/fixtures/k8s/bad-sidecar.yaml: -------------------------------------------------------------------------------- 1 | name: bad-sidecar 2 | namespace: "test-namespace" 3 | annotations: 4 | "injector.unittest.com/request": "this-doesnt-exist" 5 | -------------------------------------------------------------------------------- /test/fixtures/k8s/configmap-complex-sidecar.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: test-injectionconfig1 6 | namespace: default 7 | data: 8 | complex-sidecar: | 9 | name: complex-sidecar:v420.69 10 | volumes: 11 | - name: foo-config 12 | configMap: 13 | name: foo-config 14 | items: 15 | - key: config.yaml 16 | path: twemproxy.yaml 17 | containers: 18 | - name: foo 19 | image: some/container:1.2.3 20 | command: ["entrypoint.sh"] 21 | livenessProbe: 22 | httpGet: 23 | path: / 24 | port: 420 25 | initialDelaySeconds: 3 26 | periodSeconds: 10 27 | volumeMounts: 28 | - name: twem-config 29 | mountPath: /etc/foo/conf/ 30 | readOnly: true 31 | resources: 32 | requests: 33 | cpu: "0.5" 34 | memory: "1Gi" 35 | limits: 36 | cpu: "1.0" 37 | - name: async-worker 38 | image: async-worker:1.5.6 39 | command: ["entrypoint-worker.sh"] 40 | livenessProbe: 41 | exec: 42 | command: ["r-u-ok.sh"] 43 | initialDelaySeconds: 3 44 | periodSeconds: 10 45 | readinessProbe: 46 | exec: 47 | command: ["wut-am-queue.sh"] 48 | initialDelaySeconds: 30 49 | periodSeconds: 10 50 | env: 51 | - name: WORKERS_COUNT 52 | value: "3" 53 | - name: WORKER_QUEUES 54 | value: "x,y,z" 55 | - name: DAEMON_ENDPOINT 56 | value: "some.endpoint.798" 57 | resources: 58 | requests: 59 | cpu: "4.0" 60 | memory: "2Gi" 61 | limits: 62 | cpu: "4.0" 63 | memory: "3Gi" 64 | - name: kafka 65 | image: kafka:0.10-0.1.0 66 | command: ["entrypoint.sh"] 67 | args: [] 68 | ports: 69 | - name: kafka 70 | containerPort: 9092 71 | - name: jmx 72 | containerPort: 9192 73 | livenessProbe: 74 | tcpSocket: 75 | port: kafka 76 | initialDelaySeconds: 3 77 | periodSeconds: 10 78 | resources: 79 | requests: 80 | cpu: "1.0" 81 | memory: "1Gi" 82 | limits: 83 | cpu: "1.0" 84 | memory: "3Gi" 85 | - name: memcached 86 | image: memcached:1.5.8 87 | command: ["entrypoint.sh"] 88 | args: [] 89 | ports: 90 | - name: tcp 91 | containerPort: 11211 92 | env: 93 | - name: MAX_MEMORY_MB 94 | value: "800" 95 | livenessProbe: 96 | tcpSocket: 97 | port: tcp 98 | initialDelaySeconds: 3 99 | periodSeconds: 10 100 | resources: 101 | requests: 102 | cpu: "1.0" 103 | memory: "1Gi" 104 | limits: 105 | cpu: "1.0" 106 | 107 | -------------------------------------------------------------------------------- /test/fixtures/k8s/configmap-env1.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: test-injectionconfig1 6 | namespace: default 7 | data: 8 | test-tumblr1: | 9 | name: env1 10 | env: 11 | - name: FOO_BAR 12 | value: "something interesting" 13 | - name: DATACENTER 14 | value: "from-injection" 15 | - name: ENVIRONMENT 16 | value: "production" 17 | -------------------------------------------------------------------------------- /test/fixtures/k8s/configmap-host-aliases.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: test-host-aliases 6 | namespace: kube-system 7 | data: 8 | test-tumblr1: | 9 | name: host-aliases 10 | hostAliases: 11 | - ip: 1.2.3.4 12 | hostnames: 13 | - some.domain.com 14 | - some.other-domain.com 15 | - ip: 4.3.2.1 16 | hostnames: 17 | - another.domain.com 18 | - ip: 4.3.2.1 19 | hostnames: 20 | - yetanother.domain.com 21 | - ip: 2.3.4.5 22 | hostnames: 23 | - another.domain.com 24 | - ip: 4.3.2.1 25 | - ip: 4.3.2.1 26 | hostnames: 27 | env: 28 | - name: DATACENTER 29 | value: foo 30 | - name: FROM_INJECTOR 31 | value: bar 32 | containers: 33 | - name: sidecar-add-vm 34 | image: nginx:1.12.2 35 | imagePullPolicy: IfNotPresent 36 | env: 37 | - name: DATACENTER 38 | value: bf2 39 | ports: 40 | - containerPort: 80 41 | -------------------------------------------------------------------------------- /test/fixtures/k8s/configmap-hostNetwork-hostPid.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: test-network-pid 6 | namespace: default 7 | data: 8 | hostNetwork-hostPid: | 9 | name: test-network-pid 10 | hostNetwork: true 11 | hostPID: true -------------------------------------------------------------------------------- /test/fixtures/k8s/configmap-init-containers.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: test-init-containers 6 | namespace: default 7 | data: 8 | test-tumblr1: | 9 | name: init-containers 10 | initContainers: 11 | - name: init-container-1 12 | image: foo:bar1 13 | imagePullPolicy: Always 14 | command: 15 | - "bash" 16 | - "-c" 17 | - > 18 | echo "sleep 20" && 19 | sleep 20 20 | containers: 21 | - name: sidecar-add-vm 22 | image: nginx:1.12.2 23 | imagePullPolicy: IfNotPresent 24 | ports: 25 | - containerPort: 80 26 | - name: sidecar-existing-vm 27 | image: foo:69 28 | ports: 29 | - containerPort: 420 30 | -------------------------------------------------------------------------------- /test/fixtures/k8s/configmap-multiple1.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: multiple1 6 | namespace: default 7 | data: 8 | env1: | 9 | name: env1 10 | env: 11 | - name: FOO_BAR 12 | value: "something interesting" 13 | - name: DATACENTER 14 | value: "from-injection" 15 | - name: ENVIRONMENT 16 | value: "production" 17 | test-tumblr1: | 18 | name: sidecar-test 19 | env: 20 | - name: DATACENTER 21 | value: foo 22 | - name: FROM_INJECTOR 23 | value: bar 24 | containers: 25 | - name: sidecar-nginx 26 | image: nginx:1.12.2 27 | imagePullPolicy: IfNotPresent 28 | env: 29 | - name: DATACENTER 30 | value: bf2 31 | ports: 32 | - containerPort: 80 33 | volumeMounts: 34 | - name: nginx-conf 35 | mountPath: /etc/nginx 36 | - name: another-sidecar 37 | image: foo:69 38 | ports: 39 | - containerPort: 420 40 | volumes: 41 | - name: nginx-conf 42 | configMap: 43 | name: nginx-configmap 44 | -------------------------------------------------------------------------------- /test/fixtures/k8s/configmap-sidecar-test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: test-injectionconfig1 6 | namespace: default 7 | data: 8 | test-tumblr1: | 9 | name: sidecar-test 10 | env: 11 | - name: DATACENTER 12 | value: foo 13 | - name: FROM_INJECTOR 14 | value: bar 15 | containers: 16 | - name: sidecar-nginx 17 | image: nginx:1.12.2 18 | imagePullPolicy: IfNotPresent 19 | env: 20 | - name: DATACENTER 21 | value: bf2 22 | ports: 23 | - containerPort: 80 24 | volumeMounts: 25 | - name: nginx-conf 26 | mountPath: /etc/nginx 27 | - name: another-sidecar 28 | image: foo:69 29 | ports: 30 | - containerPort: 420 31 | volumes: 32 | - name: nginx-conf 33 | configMap: 34 | name: nginx-configmap 35 | -------------------------------------------------------------------------------- /test/fixtures/k8s/configmap-volume-mounts.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: test-volume-mounts 6 | namespace: default 7 | data: 8 | test-tumblr1: | 9 | name: volume-mounts 10 | volumeMounts: 11 | - name: test-vol 12 | mountPath: /tmp/test 13 | env: 14 | - name: DATACENTER 15 | value: foo 16 | - name: FROM_INJECTOR 17 | value: bar 18 | containers: 19 | - name: sidecar-add-vm 20 | image: nginx:1.12.2 21 | imagePullPolicy: IfNotPresent 22 | env: 23 | - name: DATACENTER 24 | value: bf2 25 | ports: 26 | - containerPort: 80 27 | volumeMounts: 28 | - name: nginx-conf 29 | mountPath: /etc/nginx 30 | - name: sidecar-existing-vm 31 | image: foo:69 32 | ports: 33 | - containerPort: 420 34 | volumeMounts: 35 | - name: test-vol 36 | mountPath: /tmp/another-dir 37 | - name: sidecar-first-vm 38 | image: bar:42 39 | imagePullPolicy: always 40 | ports: 41 | - containerPort: 43 42 | volumes: 43 | - name: nginx-conf 44 | configMap: 45 | name: nginx-configmap 46 | - name: test-vol 47 | configMap: 48 | name: test-config 49 | -------------------------------------------------------------------------------- /test/fixtures/k8s/env1.yaml: -------------------------------------------------------------------------------- 1 | name: object1 2 | namespace: unittest 3 | annotations: 4 | "hello": "foo" 5 | "injector.unittest.com/request": "env1" 6 | -------------------------------------------------------------------------------- /test/fixtures/k8s/ignored-namespace-pod.yaml: -------------------------------------------------------------------------------- 1 | name: ignored-namespace 2 | namespace: "ignore-me" 3 | annotations: 4 | "injector.unittest.com/request": "volume-mounts" 5 | -------------------------------------------------------------------------------- /test/fixtures/k8s/object1.yaml: -------------------------------------------------------------------------------- 1 | name: object1 2 | namespace: unittest 3 | annotations: 4 | "hello": "foo" 5 | "injector.unittest.com/request": "sidecar-test" 6 | -------------------------------------------------------------------------------- /test/fixtures/k8s/object2latest.yaml: -------------------------------------------------------------------------------- 1 | name: object2 2 | namespace: unittest 3 | annotations: 4 | "injector.unittest.com/request": "complex-sidecar" 5 | -------------------------------------------------------------------------------- /test/fixtures/k8s/object2v.yaml: -------------------------------------------------------------------------------- 1 | name: object2 2 | namespace: unittest 3 | annotations: 4 | "injector.unittest.com/request": "complex-sidecar:v420.69" 5 | -------------------------------------------------------------------------------- /test/fixtures/k8s/object3-missing.yaml: -------------------------------------------------------------------------------- 1 | name: object3 2 | namespace: unittest 3 | -------------------------------------------------------------------------------- /test/fixtures/k8s/object4.yaml: -------------------------------------------------------------------------------- 1 | name: object4 2 | namespace: unittest 3 | # this should not get any injections cause its already got the "status:injected" annotation 4 | annotations: 5 | "injector.unittest.com/status": "injected" 6 | "injector.unittest.com/request": "tumblr-php" 7 | -------------------------------------------------------------------------------- /test/fixtures/k8s/object5.yaml: -------------------------------------------------------------------------------- 1 | name: object5 2 | namespace: unittest 3 | annotations: 4 | "injector.unittest.com/request": "volume-mounts" 5 | -------------------------------------------------------------------------------- /test/fixtures/k8s/object6.yaml: -------------------------------------------------------------------------------- 1 | name: object6 2 | namespace: unittest 3 | annotations: 4 | "injector.unittest.com/request": "host-aliases" 5 | -------------------------------------------------------------------------------- /test/fixtures/k8s/object7-badrequestformat.yaml: -------------------------------------------------------------------------------- 1 | name: object7v3 2 | namespace: unittest 3 | annotations: 4 | "injector.unittest.com/request": "init-containers:extra:data:v3" 5 | -------------------------------------------------------------------------------- /test/fixtures/k8s/object7-v2.yaml: -------------------------------------------------------------------------------- 1 | name: object7v2 2 | namespace: unittest 3 | annotations: 4 | "injector.unittest.com/request": "init-containers:v2" 5 | -------------------------------------------------------------------------------- /test/fixtures/k8s/object7.yaml: -------------------------------------------------------------------------------- 1 | name: object7 2 | namespace: unittest 3 | annotations: 4 | "injector.unittest.com/request": "init-containers" 5 | -------------------------------------------------------------------------------- /test/fixtures/sidecars/bad/inheritance-escape.yaml: -------------------------------------------------------------------------------- 1 | name: inheritance-escape:v1 2 | 3 | # try to escape 4 | inherits: "../../etc/passwd" 5 | -------------------------------------------------------------------------------- /test/fixtures/sidecars/bad/inheritance-filenotfound.yaml: -------------------------------------------------------------------------------- 1 | name: inheritance-filenotfound 2 | 3 | inherits: "some-missing-file.yaml" 4 | 5 | -------------------------------------------------------------------------------- /test/fixtures/sidecars/bad/init-containers-colons-v3.yaml: -------------------------------------------------------------------------------- 1 | name: init-containers:extra:data:v3 2 | initContainers: 3 | - name: init-container-1 4 | image: foo:bar1 5 | imagePullPolicy: Always 6 | command: 7 | - "bash" 8 | - "-c" 9 | - > 10 | echo "sleep 20" && 11 | sleep 20 12 | containers: 13 | - name: sidecar-add-vm 14 | image: nginx:1.12.2 15 | imagePullPolicy: IfNotPresent 16 | ports: 17 | - containerPort: 80 18 | - name: sidecar-existing-vm 19 | image: foo:69 20 | ports: 21 | - containerPort: 420 22 | -------------------------------------------------------------------------------- /test/fixtures/sidecars/bad/missing-name.yaml: -------------------------------------------------------------------------------- 1 | initContainers: 2 | - name: init-container-1 3 | image: foo:bar1 4 | imagePullPolicy: Always 5 | command: 6 | - "bash" 7 | - "-c" 8 | - > 9 | echo "sleep 20" && 10 | sleep 20 11 | containers: 12 | - name: sidecar-add-vm 13 | image: nginx:1.12.2 14 | imagePullPolicy: IfNotPresent 15 | ports: 16 | - containerPort: 80 17 | - name: sidecar-existing-vm 18 | image: foo:69 19 | ports: 20 | - containerPort: 420 21 | -------------------------------------------------------------------------------- /test/fixtures/sidecars/complex-sidecar.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: complex-sidecar:v420.69 3 | volumes: 4 | - name: foo-config 5 | configMap: 6 | name: foo-config 7 | items: 8 | - key: config.yaml 9 | path: twemproxy.yaml 10 | containers: 11 | - name: foo 12 | image: some/container:1.2.3 13 | command: ["entrypoint.sh"] 14 | livenessProbe: 15 | httpGet: 16 | path: / 17 | port: 420 18 | initialDelaySeconds: 3 19 | periodSeconds: 10 20 | volumeMounts: 21 | - name: twem-config 22 | mountPath: /etc/foo/conf/ 23 | readOnly: true 24 | resources: 25 | requests: 26 | cpu: "0.5" 27 | memory: "1Gi" 28 | limits: 29 | cpu: "1.0" 30 | - name: async-worker 31 | image: async-worker:1.5.6 32 | command: ["entrypoint-worker.sh"] 33 | livenessProbe: 34 | exec: 35 | command: ["r-u-ok.sh"] 36 | initialDelaySeconds: 3 37 | periodSeconds: 10 38 | readinessProbe: 39 | exec: 40 | command: ["wut-am-queue.sh"] 41 | initialDelaySeconds: 30 42 | periodSeconds: 10 43 | env: 44 | - name: WORKERS_COUNT 45 | value: "3" 46 | - name: WORKER_QUEUES 47 | value: "x,y,z" 48 | - name: DAEMON_ENDPOINT 49 | value: "some.endpoint.798" 50 | resources: 51 | requests: 52 | cpu: "4.0" 53 | memory: "2Gi" 54 | limits: 55 | cpu: "4.0" 56 | memory: "3Gi" 57 | - name: kafka 58 | image: kafka:0.10-0.1.0 59 | command: ["entrypoint.sh"] 60 | args: [] 61 | ports: 62 | - name: kafka 63 | containerPort: 9092 64 | - name: jmx 65 | containerPort: 9192 66 | livenessProbe: 67 | tcpSocket: 68 | port: kafka 69 | initialDelaySeconds: 3 70 | periodSeconds: 10 71 | resources: 72 | requests: 73 | cpu: "1.0" 74 | memory: "1Gi" 75 | limits: 76 | cpu: "1.0" 77 | memory: "3Gi" 78 | - name: memcached 79 | image: memcached:1.5.8 80 | command: ["entrypoint.sh"] 81 | args: [] 82 | ports: 83 | - name: tcp 84 | containerPort: 11211 85 | env: 86 | - name: MAX_MEMORY_MB 87 | value: "800" 88 | livenessProbe: 89 | tcpSocket: 90 | port: tcp 91 | initialDelaySeconds: 3 92 | periodSeconds: 10 93 | resources: 94 | requests: 95 | cpu: "1.0" 96 | memory: "1Gi" 97 | limits: 98 | cpu: "1.0" 99 | -------------------------------------------------------------------------------- /test/fixtures/sidecars/env1.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: env1 3 | env: 4 | - name: FOO_BAR 5 | value: "something interesting" 6 | - name: DATACENTER 7 | value: "from-injection" 8 | - name: ENVIRONMENT 9 | value: "production" 10 | -------------------------------------------------------------------------------- /test/fixtures/sidecars/host-aliases.yaml: -------------------------------------------------------------------------------- 1 | name: host-aliases 2 | hostAliases: 3 | - ip: 1.2.3.4 4 | hostnames: 5 | - some.domain.com 6 | - some.other-domain.com 7 | - ip: 4.3.2.1 8 | hostnames: 9 | - another.domain.com 10 | - ip: 4.3.2.1 11 | hostnames: 12 | - yetanother.domain.com 13 | - ip: 2.3.4.5 14 | hostnames: 15 | - another.domain.com 16 | - ip: 4.3.2.1 17 | - ip: 4.3.2.1 18 | hostnames: 19 | env: 20 | - name: DATACENTER 21 | value: foo 22 | - name: FROM_INJECTOR 23 | value: bar 24 | containers: 25 | - name: sidecar-add-vm 26 | image: nginx:1.12.2 27 | imagePullPolicy: IfNotPresent 28 | env: 29 | - name: DATACENTER 30 | value: bf2 31 | ports: 32 | - containerPort: 80 33 | -------------------------------------------------------------------------------- /test/fixtures/sidecars/inheritance-1.yaml: -------------------------------------------------------------------------------- 1 | name: inheritance-complex:v1 2 | 3 | inherits: "complex-sidecar.yaml" 4 | 5 | volumes: 6 | # existing volume 7 | - name: foo-config 8 | configMap: 9 | name: foo-config 10 | items: 11 | - key: replace-me.yaml 12 | path: replace-me.yaml 13 | # new volume 14 | - name: new-volume 15 | configMap: 16 | name: foo-config 17 | items: 18 | - key: replace-me.yaml 19 | path: replace-me.yaml 20 | 21 | hostAliases: 22 | - ip: 1.2.3.4 23 | hostnames: 24 | - some.other-domain.com 25 | env: 26 | # existing env var, replace in place 27 | - name: DATACENTER 28 | value: new value, existing envvar 29 | # brand new env var 30 | - name: NEW_VARIABLE 31 | value: test 32 | 33 | containers: 34 | # an existing named image 35 | - name: foo 36 | image: nginx:69.420 37 | - name: a-new-image 38 | image: some-value 39 | 40 | initContainers: 41 | - name: a-new-image 42 | image: some-value 43 | -------------------------------------------------------------------------------- /test/fixtures/sidecars/inheritance-deep-2.yaml: -------------------------------------------------------------------------------- 1 | name: inheritance-deep:v2 2 | 3 | inherits: "inheritance-1.yaml" 4 | 5 | volumes: 6 | # existing volume 7 | - name: foo-config 8 | configMap: 9 | name: foo-config 10 | items: 11 | - key: replace-me.yaml 12 | path: replace-me.yaml 13 | # new volume 14 | - name: new-volume-2 15 | configMap: 16 | name: foo-config 17 | items: 18 | - key: replace-me.yaml 19 | path: replace-me.yaml 20 | 21 | hostAliases: 22 | - ip: 1.2.3.4 23 | hostnames: 24 | - some.other-domain.com 25 | - ip: 1.2.3.4 26 | hostnames: 27 | - some.other-domain.com 28 | env: 29 | # existing env var, replace in place 30 | - name: DATACENTER 31 | value: from-inheritance-deep-2 32 | # brand new env var 33 | - name: NEW_VAR_DEEP_V2 34 | value: from deep v2 35 | 36 | containers: 37 | # an existing named image 38 | - name: foo 39 | image: nginx:69.420 40 | - name: a-new-image-2 41 | image: some-value 42 | 43 | initContainers: 44 | - name: a-new-image-2 45 | image: some-value 46 | -------------------------------------------------------------------------------- /test/fixtures/sidecars/init-containers-v2.yaml: -------------------------------------------------------------------------------- 1 | name: init-containers:v2 2 | initContainers: 3 | - name: init-container-1 4 | image: foo:bar1 5 | imagePullPolicy: Always 6 | command: 7 | - "bash" 8 | - "-c" 9 | - > 10 | echo "sleep 20" && 11 | sleep 20 12 | containers: 13 | - name: sidecar-add-vm 14 | image: nginx:1.12.2 15 | imagePullPolicy: IfNotPresent 16 | ports: 17 | - containerPort: 80 18 | - name: sidecar-existing-vm 19 | image: foo:69 20 | ports: 21 | - containerPort: 420 22 | -------------------------------------------------------------------------------- /test/fixtures/sidecars/init-containers.yaml: -------------------------------------------------------------------------------- 1 | name: init-containers 2 | initContainers: 3 | - name: init-container-1 4 | image: foo:bar1 5 | imagePullPolicy: Always 6 | command: 7 | - "bash" 8 | - "-c" 9 | - > 10 | echo "sleep 20" && 11 | sleep 20 12 | containers: 13 | - name: sidecar-add-vm 14 | image: nginx:1.12.2 15 | imagePullPolicy: IfNotPresent 16 | ports: 17 | - containerPort: 80 18 | - name: sidecar-existing-vm 19 | image: foo:69 20 | ports: 21 | - containerPort: 420 22 | -------------------------------------------------------------------------------- /test/fixtures/sidecars/maxmind.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: maxmind 3 | env: 4 | - name: EVERYWHERE 5 | value: "injected in all containers" 6 | initContainers: 7 | - image: maxmindimage 8 | name: maxminddb-init 9 | command: ["/bin/init.sh"] 10 | containers: 11 | - image: maxmindimage 12 | name: maxminddb 13 | volumes: 14 | - name: maxminddb 15 | emptyDir: {} 16 | - name: anothervolume 17 | emptyDir: {} 18 | volumeMounts: 19 | - name: maxminddb 20 | mountPath: /usr/share/GeoIP 21 | -------------------------------------------------------------------------------- /test/fixtures/sidecars/service-account-default-token.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: service-account-default-token 3 | serviceAccountName: someaccount 4 | -------------------------------------------------------------------------------- /test/fixtures/sidecars/service-account-with-inheritance.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: service-account-inherits-env1 3 | inherits: env1.yaml 4 | serviceAccountName: someaccount 5 | -------------------------------------------------------------------------------- /test/fixtures/sidecars/service-account.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: service-account 3 | serviceAccountName: someaccount 4 | -------------------------------------------------------------------------------- /test/fixtures/sidecars/sidecar-test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: sidecar-test 3 | env: 4 | - name: DATACENTER 5 | value: foo 6 | - name: FROM_INJECTOR 7 | value: bar 8 | containers: 9 | - name: sidecar-nginx 10 | image: nginx:1.12.2 11 | imagePullPolicy: IfNotPresent 12 | env: 13 | - name: DATACENTER 14 | value: bf2 15 | ports: 16 | - containerPort: 80 17 | volumeMounts: 18 | - name: nginx-conf 19 | mountPath: /etc/nginx 20 | - name: another-sidecar 21 | image: foo:69 22 | ports: 23 | - containerPort: 420 24 | volumes: 25 | - name: nginx-conf 26 | configMap: 27 | name: nginx-configmap 28 | -------------------------------------------------------------------------------- /test/fixtures/sidecars/test-network-pid.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: test-network-pid 3 | hostNetwork: true 4 | hostPID: true -------------------------------------------------------------------------------- /test/fixtures/sidecars/volume-mounts.yaml: -------------------------------------------------------------------------------- 1 | name: volume-mounts 2 | volumeMounts: 3 | - name: test-vol 4 | mountPath: /tmp/test 5 | env: 6 | - name: DATACENTER 7 | value: foo 8 | - name: FROM_INJECTOR 9 | value: bar 10 | containers: 11 | - name: sidecar-add-vm 12 | image: nginx:1.12.2 13 | imagePullPolicy: IfNotPresent 14 | env: 15 | - name: DATACENTER 16 | value: bf2 17 | ports: 18 | - containerPort: 80 19 | volumeMounts: 20 | - name: nginx-conf 21 | mountPath: /etc/nginx 22 | - name: sidecar-existing-vm 23 | image: foo:69 24 | ports: 25 | - containerPort: 420 26 | volumeMounts: 27 | - name: test-vol 28 | mountPath: /tmp/another-dir 29 | - name: sidecar-first-vm 30 | image: bar:42 31 | imagePullPolicy: always 32 | ports: 33 | - containerPort: 43 34 | volumes: 35 | - name: nginx-conf 36 | configMap: 37 | name: nginx-configmap 38 | - name: test-vol 39 | configMap: 40 | name: test-config 41 | --------------------------------------------------------------------------------