diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..77fcbf6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore all files which are not go type +!**/*.go +!./testbin/ +!**/*.mod +!**/*.sum +_*/** diff --git a/.github/workflows/chart-release.yml b/.github/workflows/chart-release.yml new file mode 100644 index 0000000..a8d5098 --- /dev/null +++ b/.github/workflows/chart-release.yml @@ -0,0 +1,38 @@ +name: Release Charts + +#on: +# push: +# branches: [ release ] +on: + push: + paths: + - 'config/helm/irsa/Chart.yaml' + branches: + - main + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Fetch history + run: git fetch --prune --unshallow + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Install Helm + uses: azure/setup-helm@v1 + with: + version: v3.4.0 + + - name: Run chart-releaser + uses: helm/chart-releaser-action@v1.2.0 + with: + charts_dir: config/helm + config: cr.yml + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..342ca88 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,65 @@ +name: CI + +on: + push: + branches: [ main ] + paths-ignore: + - 'config/helm/**' + pull_request: + branches: [ main ] + paths-ignore: + - 'config/helm/**' + +jobs: + + sanity-checks: + name: Sanity checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go 1.15 + uses: actions/setup-go@v2 + with: + go-version: 1.15 + id: go + + - name: Format + run: if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then exit 1; fi + + - name: Lint + uses: golangci/golangci-lint-action@v2 + with: + version: v1.29 + + test: + name: Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go 1.15 + uses: actions/setup-go@v2 + with: + go-version: 1.15 + id: go + + - name: Generate manifests + run: make generate + + - name: Test + env: + LOCALSTACK_ENDPOINT: ${{ job.services.localstack.ports[4566] }} + shell: bash + run: | + export ENVTEST_ASSETS_DIR=${GITHUB_WORKSPACE}/testbin + source ${ENVTEST_ASSETS_DIR}/setup-envtest.sh + fetch_envtest_tools ${ENVTEST_ASSETS_DIR} + setup_envtest_env ${ENVTEST_ASSETS_DIR} + go test -v ./... -coverprofile cover.out + + services: + localstack: + image: localstack/localstack:0.12.4 + env: + SERVICES: 'iam,s3,sts' diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml new file mode 100644 index 0000000..c8e991b --- /dev/null +++ b/.github/workflows/publish-docker.yml @@ -0,0 +1,21 @@ +name: Publish Docker image + +on: + release: + types: [published] + +jobs: + push_to_registry: + name: Push Docker image to GitHub Packages + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v2 + - name: Push to GitHub Packages + uses: docker/build-push-action@v1 + with: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + registry: docker.pkg.github.com + repository: voodooteam/irsa-operator/irsa-operator + tag_with_ref: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb908f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +_dev-env +irsa-operator + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin +testbin/bin/* + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +*.swp +*.swo +*~ + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4beb7f7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM golang:1.15 as builder + +WORKDIR /workspace +COPY go.mod go.mod +COPY go.sum go.sum +RUN go mod download + +COPY main.go main.go +COPY api/ api/ +COPY controllers/ controllers/ +COPY aws/ aws/ + +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o manager main.go + +# Use distroless as minimal base image to package the manager binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/static:nonroot +WORKDIR / +COPY --from=builder /workspace/manager . +USER 65532:65532 + +ENTRYPOINT ["/manager"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1790384 --- /dev/null +++ b/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2021 Voodoo.io + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..58cd9a7 --- /dev/null +++ b/Makefile @@ -0,0 +1,133 @@ +# Current Operator version +VERSION ?= 0.0.1 +# Default bundle image tag +BUNDLE_IMG ?= controller-bundle:$(VERSION) +# Options for 'bundle-build' +ifneq ($(origin CHANNELS), undefined) +BUNDLE_CHANNELS := --channels=$(CHANNELS) +endif +ifneq ($(origin DEFAULT_CHANNEL), undefined) +BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL) +endif +BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL) + +# Image URL to use all building/pushing image targets +IMG ?= controller:latest +# Produce CRDs that work back to Kubernetes 1.11 (no version conversion) +CRD_OPTIONS ?= "crd:trivialVersions=true,preserveUnknownFields=false" + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +all: manager + +# Run tests +ENVTEST_ASSETS_DIR=$(shell pwd)/testbin +test: generate manifests + mkdir -p ${ENVTEST_ASSETS_DIR} + test -f ${ENVTEST_ASSETS_DIR}/setup-envtest.sh || curl -sSLo ${ENVTEST_ASSETS_DIR}/setup-envtest.sh https://raw.githubusercontent.com/kubernetes-sigs/controller-runtime/v0.7.0/hack/setup-envtest.sh + source ${ENVTEST_ASSETS_DIR}/setup-envtest.sh; fetch_envtest_tools $(ENVTEST_ASSETS_DIR); setup_envtest_env $(ENVTEST_ASSETS_DIR); go test -v ./... -coverprofile cover.out + +testmodel: generate manifests + mkdir -p ${ENVTEST_ASSETS_DIR} + test -f ${ENVTEST_ASSETS_DIR}/setup-envtest.sh || curl -sSLo ${ENVTEST_ASSETS_DIR}/setup-envtest.sh https://raw.githubusercontent.com/kubernetes-sigs/controller-runtime/v0.7.0/hack/setup-envtest.sh + source ${ENVTEST_ASSETS_DIR}/setup-envtest.sh; fetch_envtest_tools $(ENVTEST_ASSETS_DIR); setup_envtest_env $(ENVTEST_ASSETS_DIR); go test -v ./controllers/... -- -ginkgo.failfast + +# Build manager binary +manager: generate fmt vet + go build -o bin/manager main.go + +# Run against the configured Kubernetes cluster in ~/.kube/config +run: generate fmt vet manifests + go run ./main.go + +# Install CRDs into a cluster +install: manifests kustomize + $(KUSTOMIZE) build config/crd | kubectl apply -f - + +# Uninstall CRDs from a cluster +uninstall: manifests kustomize + $(KUSTOMIZE) build config/crd | kubectl delete -f - + +# Deploy controller in the configured Kubernetes cluster in ~/.kube/config +deploy: manifests kustomize + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default | kubectl apply -f - + +gen-helm: manifests kustomize + $(KUSTOMIZE) build config/default > config/helm/irsa/templates/irsa-operator.yml + #echo version: ${CHART_VERSION} >> config/helm/Chart.yaml + #echo image: ${DOCKER_IMAGE}:${CHART_VERSION} >> config/helm/values.yaml + +# UnDeploy controller from the configured Kubernetes cluster in ~/.kube/config +undeploy: + $(KUSTOMIZE) build config/default | kubectl delete -f - + +# Generate manifests e.g. CRD, RBAC etc. +manifests: controller-gen + $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases + +# Run go fmt against code +fmt: + go fmt ./... + +# Run go vet against code +vet: + go vet ./... + +# Run golangci-lint against code +lint: + golangci-lint run + +# Generate code +generate: controller-gen + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + +# Build the docker image +docker-build: test + DOCKER_BUILDKIT=1 docker build -t ${IMG} . + +# Push the docker image +docker-push: + docker push ${IMG} + +# Download controller-gen locally if necessary +CONTROLLER_GEN = $(shell pwd)/bin/controller-gen +controller-gen: + $(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.4.1) + +# Download kustomize locally if necessary +KUSTOMIZE = $(shell pwd)/bin/kustomize +kustomize: + $(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v3@v3.8.7) + +# go-get-tool will 'go get' any package $2 and install it to $1. +PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) +define go-get-tool +@[ -f $(1) ] || { \ +set -e ;\ +TMP_DIR=$$(mktemp -d) ;\ +cd $$TMP_DIR ;\ +go mod init tmp ;\ +echo "Downloading $(2)" ;\ +GOBIN=$(PROJECT_DIR)/bin go get $(2) ;\ +rm -rf $$TMP_DIR ;\ +} +endef + +# Generate bundle manifests and metadata, then validate generated files. +.PHONY: bundle +bundle: manifests kustomize + operator-sdk generate kustomize manifests -q + cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) + $(KUSTOMIZE) build config/manifests | operator-sdk generate bundle -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) + operator-sdk bundle validate ./bundle + +# Build the bundle image. +.PHONY: bundle-build +bundle-build: + docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) . diff --git a/PROJECT b/PROJECT new file mode 100644 index 0000000..478d0a3 --- /dev/null +++ b/PROJECT @@ -0,0 +1,29 @@ +domain: voodoo.io +layout: go.kubebuilder.io/v3 +projectName: irsa-operator +repo: github.com/VoodooTeam/irsa-operator +resources: +- crdVersion: v1 + group: irsa + kind: IamRoleServiceAccount + version: v1alpha1 +- crdVersion: v1 + group: irsa + kind: Awspolicy + version: v1alpha1 +- crdVersion: v1 + group: irsa + kind: Awsrole + version: v1alpha1 +- crdVersion: v1 + group: irsa + kind: Role + version: v1alpha1 +- crdVersion: v1 + group: irsa + kind: Policy + version: v1alpha1 +version: 3-alpha +plugins: + manifests.sdk.operatorframework.io/v2: {} + scorecard.sdk.operatorframework.io/v2: {} diff --git a/README.md b/README.md index e87026d..4d75400 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # IRSA operator -![CI](https://github.com/matthieuJacquot-voodoo/test-github-actions/actions/workflows/ci.yml/badge.svg) +![CI](https://github.com/VoodooTeam/irsa-operator/actions/workflows/ci.yml/badge.svg) A Kubernetes operator to manage IAM roles & policies needed for IRSA, directly from your EKS cluster diff --git a/_doc/architecture-diagram b/_doc/architecture-diagram new file mode 100644 index 0000000..fe55950 --- /dev/null +++ b/_doc/architecture-diagram @@ -0,0 +1 @@ +7V3rc6I6G/9rnDnvBzvc0Y+ttWc7e9/2zJ6+XzoIUVORuIC17l9/EiAqSURULrGz3ZmuhkDhuef3PHno6IP529+hs5h+Rh7wO5rivXX0246maT2zj/8jI+t0pNdX0oFJCL10SN0OPMDfIBuk05bQA1FuYoyQH8NFftBFQQDcODfmhCFa5aeNkZ//qwtnAriBB9fx+dGf0Iun2VOYynb8A4CTKf3LqpIdmTt0cjYQTR0PrXaG9GFHH4QIxemn+dsA+IR4lC7peXd7jm5uLARBXOaEl6nx8g+Ig+FIffnHmNja691TN7vKq+MvsweOnKsZWGe3HK8pHUK0DDxALqV29JvVFMbgYeG45OgKcx6PTeO5nx0eQ98fIB+Fybm654De2MXjURyiGdg5Yrk9MBrjI/zD0DsDYQzedoayh/sboDmIQ3yjSnZUtzNCr6kEZd9XO3zLhqY7LDOyMSeTlMnmylti4g8ZPY+grW5yxE0oix8ThhyBgYflLvuKwniKJihw/OF29GbLAgV/2875hNAiI/wLiON1pkTOMkZ5toA3GP+78/mJXOrKzL7dvmVXTr6s6ZcAk+Lf3S87Z5Gv29OSb/S8KHbC+JooIB4Y+cid0cE76NMb2sv1CC1DFxTQVstMgRNOQHxYvglxC2UoBL4Tw9e80lcuEZpI2xbL0QVqm9pvT9sM6A972sp/Rd/7/vq78vvtu9VVzVZUaqMeW414yimEWD1Ol3ydl3wxPfS2RF98OxYn+x5wfUyAjmb5+EFuRtgmWhPyaQHCOYwiiIIocfdCxn5yRjjUyDHD8eEkwJ9dTFSAZf6GiDPEvvw6OzCHnpfyHUTwtzNKrkfYsUAwiJNHNm865q2QQYVSxynOJiLJ/krO6YsUSrlSlF7Gs9Isya72jdz+zhQ0HkdYOFiebf7oGT6N4+IC+dBd80z8i5DjOnkYxyPs0/9XqZkbj4HlCs2cZ/dHSqGSHWHmdDNv5/S2owo+qCAKoqymDpGAjn7XZmShXNk0nHjaOXQotKjBdj49fvv8qt70B7fRF/XbDerHv4Zdo2TUULnpTE7F0ZCz3pmQGR1ejTeyp+Vljwra3d4T+oUn4A/pPVRqEux9AolkkEdJxFFIubLiaEkVxNKV+Q7DU167U+DOsDxbzpyY7mAULRLysJ4hxuY6wPO/fmxPQOpnWl8qphm8kobLKO6GyAdd6sIvzzurPcm8s8YDKgscbILETMPgjy08YOMOqhV1iZLoFb+qYe1fBMJX6IJr18WMJFIwXvp+4MxBtfpmkn/CRX/yQ85AQbwznv5Uo4c9Pa+Ghs2roao2qYc9ji8Iei55zhC9Qg+EvFvy0NyBwZeqOQNUzwS2iDN9y9YdqxoOmCoDx1gtG0K63m/J1p2EcOZtnVqbreuXtHWqIZWtU/lIHwTRUoTghImEYVJg4wfEWHfj+E2hmJ6P33SVK82y7Lwd7MgD5wjhK41HpIlIYsItRzgmxB+SjAXL3pfVLHpehlCecEYpq+L2UfFMLosRoAB08kmMZuyALlcWo8/JTAgmMMIqCwjQxznai1tW9PXDqwrVaNSb8qif4/skuZ3i4wo2ku4SM5+PXaReTKs0pX9prrB3cdGNmrN7B/O3DfC0JxdLebM2RlKHLnSNU0noojKhi3Ze6EKvrF0Zau66vfwl6stUqTwS8/jpgRCHcGaMeSMwlvIv9CxLtoUeDy0mNU2aMiDZv3i6nI8WYSIVF09srW1iazz2/rKKkyAgwdTfLaJk9ttGlDQe6puBBaH9coF/jXj8/A+2S0mnlYwINLmiPJN3ITB4wY6VhN1pup1bGrshwHeGSH4rhgIo8T2X+e1ZIIPAYwsB8VA1ZYBlJcuQI6VvKAxQ3i/O6LPzbbXahP7/59Gnj6H/YTB0htb9ozsYqF1BTmMCAhDieClq1cZJjemWt3FlJXEbnFuGli1OpEAShULT56UmDuFkkoFC7brHJixhtaIjpvH7MGJGxUZMXJh5efLWpqkSZwekEDe2qO2QuLHz6VJi33xLM4vm11M0R3mwW0QFRlOEZoSpyOOk96il3F7ZOL2+X5TSb3bpy5ceDx5+tE6nzZKc5gp6bdOJx2O2kuWBhY/Wc3AuGlODgJmtCxgPANOKRiVyKMZCvi0jGEwEFe/dblbs03XSap/uDKy7Y+gDvvS9xb0pNXoWm/csRRuhJFnnU6zpT7BwBksrz+bs8e5G3mxoNmMP0hvNzjprnSOWFd5xe8AHkySbsGMnyBc4CRJDkaRoS1gLGEVLEIpthRw5J6opleScaO1NNcUy9LqGkT+lxr1Q7SaBG4B86y1/EZmW4OPsI9Dt74tBPP/dNYcfoK1ItolXF8QJju+TwCCBfZPoXdniwIe2Q/w1X8Z4amIoHC/b+EiCHhyySW0LKs0/K718/lmtxBoY/fxF67ANQqiGL5Uk3qBd/PRKkzpJVDp2PB5A1fvmSTtrj4UqNCUPJehqMfTArh2LgLEmd/lqvPjOliMcqCzgnoreC2hYwGwV0kx+vblpYtBMfxC9VXNwxLKDSfQPkp+ztJ2WtF5YAw+dB1eWEdhUYRIbvwMUcJ4ezheYY0lCZAbWrTj38qwslNkq+h1gHcy7ZwoLSJHlEm/34isCcJznTpOHTmsC/qwIasz3lzYbrdWWihMpvNiAtwWKWk6on7aDQpNoCVlaHiQDHHl5mK690OFdxtf720Gnps0U7exMNFqG+Sk+JD/YW3MLNUrhS9tybfB7YTZ6Q9Vm7sySqCwEv5YgigWxWBKvXf98SCRBcDyX48H3ubl0G0FbyeCMinYliIxuUoDnTAyGSa9vMOT6ozWDX8ZOksp0zPfnn8Ob5/vb4ZfH+8en58evH4dfnu/uPw07oqLODYiX/s6u8OPrp+Hz9Y8vPMSnjEM0l8e1XyI4XNo6VV/ZeZ514jsPpDLnOgtnBH0Yw8QyySUhpYO/o7KOskhIaxiCuDLNlicAsWVLN4uY2lB7y5MQ3x4T3VpsE+ZD880jW7oxJ9RUPMmnuIQh1jbC4n2mM3FgECWwzCoNxTLmSxtTFWprJTGVrdv53ZC01v7cNFcvd9XmUuBUXXP9QD2OnZe7D81ksgaifWjNLl951VwGEOugvxaGrtDDDw/HMAGdVxDfszTO513uSDLKIlKtNXUUby0QNJgIAhQ7sRSC00S0Wi3PG9rAcZ4t4WFItlvd+zHkzNshhC3qGk3/GnxBe4pHJcwnasfachC8JvNDPm1wwYxhPKzVoIcVVptprRg66fNxRZV5hyHk1vIvQjvMax7myJQESq5Y89r3f6fUbjXW8oey96ADLIvEbGsEbI3pS5Opfc2FXMwy3SzaE1ZTAUKRwgk390Sv7nl+oQJrzu7qaXI7lFDi+IIduZDX0ti8Vn0wW6Si1ZVh1qupTIce09qDp4mP19NAYU/nKa5VSFWNqCrQW6YSdZO0b0ttVd5Ft9uEos56aDEFBD71rNKWLQzZMykMWfPObUaqDry+RNOLpjfic8WcaLVipBJ3IYGYVp6Ea0IkuV4CTXgPjTN9pPje8fioBRv3OC81+SV8tiDcXe9nQ+UzKSLfkxfuCtyPzrQI0Xn3I+q3q9XmfmTrgdS8+zHL6nVr2fXC+95d23tzwZtXoqmzIB+Xc//ajdGuDiSZx28ogskGPv12hOIYrxl4JYkRE4mhZezDAOsffRlxRfphsc0SeP3QGg3P+CqXBfK6abIpXnc3i9K9Ya3SSFjLbBbviUDfZgnHp+9mvSI4twSd2tlOZctG2h6/1E8qDyokbTPt53VmF6EEpOWNavku/8eTuZnCdEM649Dj61XTzeQp/KzAkPSXqFKepUrJScAA3q1xdU9uQgzMAncK5uWqmd5NmM5qTNkttLXF6X0eJqIcIYKbY4P1a4nogW6URNqkQXlv8bY9tsm0zqIrZ+78xkH6KrpyCWB8l7wq0QmD9LRN0Xfyx8ixZ3KMjtKycSoc6d3skY8LTt7qeVdliN6UYtckEcJ6PV4gXPz4pBSRI7v0hU811K8U9XmUeCOQ8Lb5eA9HI6Q9d1Ky1PLLNk9ieG5pX02+9iyGG5Uz/CTQjt1CaB/oAMrOt5poOGvKI25lq/qZ+gC7fYHTpJA33WTiHOUASszMVyvuICskKY9Qku4TxdWacpTU08VkBS0kFNVidhdmEiRFC4kiVcjVXLoheM+1lnaTrwMW7+Jop8PniZHcKVXPp9vmXknb3Jcq+BO83ivp1ouH0jciEWvoxMnbaaW1hFQsKzGFjCU8c2dR/YZQkOfM0ETCrinWt66Pb9wTvmPHu7xXkRxZjVld94eztL70mm9HEJlXCW6uUW/UpipaYdh/9AmNVJwUMWc3RogrrvlvJJOhKmzessHi/sJtw2ySaGd76uVRmV1/mA0W3T49fvv8qt70B7fRF/XbDerHv4aCAsg0daGgBamhELxW9ayEpwl6niGick8b6dY2XSRwsxzh91LZZkCFRrMVQipbrbrAJsrSPCeablRP6IcOOj8h5UrjX2KJqDzCFd5kq20iTmSvfVHs1c9k72lhiqrmA3X1AJqpGozlYU44G14S0pAPQeZO4Exk6lhYT1ujBoVSOG+Pzdmp3jZo+0DZBJWt4iwU1JriaSFN+xcotJdlSo1WgHpOROnbYPaJqM02AT9yvnGgXY9lmEXz6zHVPI5y/+Phuly0fUK39kaibVVh33MvqiyoC0MWkpmvgUt6SK6m5HdHv2vXxjT2bodKzIpR1qycaVXOYjhfX5CUAC2QD921oOFVHDtJTVjVfTPKggjnKRy7k1KrbwtgUkxF6q22VhATYfoZeQSTH/4H \ No newline at end of file diff --git a/_doc/architecture-diagram.png b/_doc/architecture-diagram.png new file mode 100644 index 0000000..0d5d47d Binary files /dev/null and b/_doc/architecture-diagram.png differ diff --git a/_doc/model/.gitignore b/_doc/model/.gitignore new file mode 100644 index 0000000..39113ec --- /dev/null +++ b/_doc/model/.gitignore @@ -0,0 +1,4 @@ +*.dvi +*.tex +states/ +result diff --git a/_doc/model/IrsaOperator.cfg b/_doc/model/IrsaOperator.cfg new file mode 100644 index 0000000..3bc0158 --- /dev/null +++ b/_doc/model/IrsaOperator.cfg @@ -0,0 +1,12 @@ +SPECIFICATION Spec + +CONSTANTS + NULL = "NULL" + _workers = {"wa", "wb"} + +INVARIANTS + TypeOk + +PROPERTIES + NoConcurrentProcessingOfSameResource + TerminationIsTheLastAction \ No newline at end of file diff --git a/_doc/model/IrsaOperator.pdf b/_doc/model/IrsaOperator.pdf new file mode 100644 index 0000000..f80dafe Binary files /dev/null and b/_doc/model/IrsaOperator.pdf differ diff --git a/_doc/model/IrsaOperator.tla b/_doc/model/IrsaOperator.tla new file mode 100644 index 0000000..b4f657c --- /dev/null +++ b/_doc/model/IrsaOperator.tla @@ -0,0 +1,356 @@ +---- MODULE IrsaOperator ---- +EXTENDS TLC, Naturals, Sequences + +(* + Caveat : + + - distributed consensus is not displayed here (operator SDK handles this for us) + - multi CR mechanism is not displayed here (simple scoping is enough to avoid collisions) + - we also assume the specs are valid + +*) + +CONSTANTS + NULL, \* dummy constant + _workers \* the set of reconcile loops + +VARIABLES + irsa, \* the iamroleserviceaccount CR + policy, \* the policy CR + role, \* the role CR + sa, \* the serviceAccount to be created + awsPolicy, \* the IAM policy on aws + awsRole, \* the IAM role on aws + wq, \* the k8s workqueue + workers, \* the workers (concurrent reconcile loops) + modified \* the k8s resources modified during an action (simulates the watch mechanism) + +vars == <> + +(* the different requests *) +iReq == "irsa" +pReq == "policy" +rReq == "role" +saReq == "sa" + + +pendingSt == "pending" + +valid_states == {NULL, pendingSt} + +TypeOk == + /\ irsa.st \in valid_states + /\ policy.st \in valid_states + /\ role.st \in valid_states + /\ sa.st \in valid_states + /\ \A w \in DOMAIN workers: workers[w].req \in {NULL, iReq, pReq, rReq, saReq} + /\ awsRole.arn \in {NULL, "roleARN"} + /\ awsPolicy.arn \in {NULL, "policyARN"} + + +Init == + /\ irsa = [st |-> NULL, + saName |-> "saName", + stmt |-> "statement", + roleARN |-> NULL, + policyARN |-> NULL + ] + /\ policy = [st |-> NULL, + stmt |-> NULL, + awsPolicyArn |-> NULL + ] + /\ role = [st |-> NULL, + saName |-> NULL, + roleArn |-> NULL, + policyArn |-> NULL, + policiesAttached |-> FALSE + \* NB : this last flag is not yet in the implementation. + \* It's needed to avoid missing the attached policies + ] + /\ sa = [ st |-> NULL, + name |-> NULL, + roleArn |-> NULL + ] + /\ awsPolicy = [arn |-> NULL] \* union already created as expected & different + /\ awsRole = [arn |-> NULL, attachedPolicy |-> NULL] \* union already created as expected & different + /\ modified = <> + /\ wq = [dirty |-> {}, processing |-> {}, queue |-> <<>>] \* we start with an IrsaRequest in the dirty set + /\ workers = [w \in _workers |-> [idle |-> TRUE, req |-> NULL]] + + + +(***************************************************************************) +(* k8s workqueue *) +(***************************************************************************) + +Enqueue(r) == \* sequence of modified resources, simulating the watch mechanism + /\ modified' = modified \o r + +\* tla spec of the k8s workqueue algorithm +\* see : https://github.com/kubernetes/client-go/blob/a57d0056dbf1d48baaf3cee876c123bea745591f/util/workqueue/queue.go#L65 +Add == + /\ modified # <<>> + /\ modified' = Tail(modified) + /\ LET e == Head(modified) IN + IF e \in wq.dirty + THEN + /\ UNCHANGED <> + ELSE + /\ IF e \notin wq.processing + THEN wq' = [wq EXCEPT !.dirty = wq.dirty \union {e}, !.queue = Append(wq.queue, e) ] + ELSE wq' = [wq EXCEPT !.dirty = wq.dirty \union {e}] + /\ UNCHANGED <> + + +Get(w) == + /\ workers[w].idle + /\ workers[w].req = NULL + /\ wq.queue # <<>> + /\ LET head == Head(wq.queue) IN + /\ workers' = [workers EXCEPT ![w] = [idle |-> FALSE, req |-> head]] + /\ wq' = [wq EXCEPT !.queue = Tail(wq.queue), !.dirty = wq.dirty \ {head}, !.processing = wq.processing \union {head} ] + /\ UNCHANGED <> + + +Done(w) == + /\ workers[w].idle + /\ workers[w].req # NULL + /\ workers' = [workers EXCEPT ![w] = [idle |-> TRUE, req |-> NULL]] + /\ LET r == workers[w].req IN + IF r \in wq.dirty + THEN wq' = [wq EXCEPT !.processing = wq.processing \ {r}, !.queue = Append(wq.queue, r)] + ELSE wq' = [wq EXCEPT !.processing = wq.processing \ {r} ] + /\ UNCHANGED <> + + + +(***************************************************************************) +(* the expected states when a resource has converged *) +(***************************************************************************) + +IrsaComplete == + /\ policy.st # NULL + /\ role.st # NULL + /\ sa.st # NULL + +policyComplete == + /\ policy.st # NULL + /\ policy.stmt # NULL + /\ policy.awsPolicyArn # NULL + +roleComplete == + /\ role.st # NULL + /\ role.saName # NULL + /\ role.roleArn # NULL + /\ role.policyArn # NULL + /\ role.policiesAttached + +saComplete == + /\ sa.st # NULL + /\ sa.name # NULL + /\ sa.roleArn # NULL + + + +(***************************************************************************) +(* operator specific actions *) +(***************************************************************************) + +\* NB : update policy not displayed yet +CreatePolicy(w) == + \* irsa controller + /\ workers[w].idle = FALSE + /\ workers[w].req = iReq + /\ policy.st = NULL \* policy doesn't exist + /\ policy' = [policy EXCEPT !.st = "pending", !.stmt = irsa.stmt ] + /\ workers' = [workers EXCEPT ![w].idle = TRUE] + /\ Enqueue(<>) + /\ UNCHANGED <> + + +CreateRole(w) == + \* irsa controller + /\ workers[w].idle = FALSE + /\ workers[w].req = iReq + /\ role.st = NULL \* role doesn't exist + /\ role' = [role EXCEPT !.st = "pending", !.saName = irsa.saName ] + /\ workers' = [workers EXCEPT ![w].idle = TRUE] + /\ Enqueue(<>) + /\ UNCHANGED <> + + +\* if it has one, we'll try to update it, not shown yet +PolicyHasNoARN(w) == + \* policy controller + /\ workers[w].idle = FALSE + /\ workers[w].req = pReq + /\ policy.awsPolicyArn = NULL + /\ IF awsPolicy.arn = NULL + THEN /\ awsPolicy' = [awsPolicy EXCEPT !.arn = "policyARN"] + /\ Enqueue(<>) + /\ UNCHANGED <> + ELSE /\ policy.awsPolicyArn = NULL + /\ policy' = [policy EXCEPT !.awsPolicyArn = awsPolicy.arn] + /\ Enqueue(<>) + /\ UNCHANGED <> + + +RoleHasNoRoleARN(w) == + \* role controller + /\ workers[w].idle = FALSE + /\ workers[w].req = rReq + /\ role.roleArn = NULL + /\ IF awsRole.arn = NULL + THEN /\ awsRole' = [awsRole EXCEPT !.arn = "roleARN"] + /\ UNCHANGED <> + ELSE /\ role' = [role EXCEPT !.roleArn = awsRole.arn] + /\ UNCHANGED <> + /\ Enqueue(<>) + + +RoleHasNoPolicyARN(w) == + \* role controller + /\ workers[w].idle = FALSE + /\ workers[w].req = rReq + /\ role.policyArn = NULL + /\ policy.awsPolicyArn # NULL + /\ role' = [role EXCEPT !.policyArn = policy.awsPolicyArn] + /\ Enqueue(<>) + /\ UNCHANGED <> + + +RoleHasPolicyARN(w) == + \* role controller + /\ workers[w].idle = FALSE + /\ workers[w].req = rReq + /\ role.policyArn # NULL + /\ role.roleArn # NULL + /\ ~role.policiesAttached + /\ awsRole.attachedPolicy = NULL + /\ awsRole' = [awsRole EXCEPT !.attachedPolicy = role.policyArn] + /\ role' = [role EXCEPT !.policiesAttached = TRUE] + /\ Enqueue(<>) + /\ UNCHANGED <> + +CreateServiceAccount(w) == + \* irsa controller + /\ workers[w].idle = FALSE + /\ workers[w].req = iReq + /\ sa.st = NULL + /\ roleComplete + /\ policyComplete + /\ sa' = [sa EXCEPT !.st = "pending", !.name = irsa.saName, !.roleArn = role.roleArn ] + /\ Enqueue(<>) + /\ UNCHANGED <> + + +\* the following actions just "swallow" events when there's nothing to do on the resource +IrsaAllDone(w) == + /\ workers[w].idle = FALSE + /\ workers[w].req = iReq + /\ IrsaComplete + /\ workers' = [workers EXCEPT ![w].idle = TRUE] + /\ UNCHANGED <> + +PolicyAllDone(w) == + /\ workers[w].idle = FALSE + /\ workers[w].req = pReq + /\ policyComplete + /\ workers' = [workers EXCEPT ![w].idle = TRUE] + /\ UNCHANGED <> + +RoleAllDone(w) == + /\ workers[w].idle = FALSE + /\ workers[w].req = rReq + /\ roleComplete + /\ workers' = [workers EXCEPT ![w].idle = TRUE] + /\ UNCHANGED <> + +SaAllDone(w) == + /\ workers[w].idle = FALSE + /\ workers[w].req = saReq + /\ saComplete + /\ workers' = [workers EXCEPT ![w].idle = TRUE] + /\ UNCHANGED <> + + +\* the whole state converged +Termination == + /\ \A w \in DOMAIN workers: workers[w].idle = TRUE /\ workers[w].req = NULL + /\ IrsaComplete + /\ roleComplete + /\ policyComplete + /\ saComplete + /\ awsPolicy.arn # NULL + /\ /\ awsRole.arn # NULL + /\ awsRole.attachedPolicy # NULL + /\ UNCHANGED vars + + +(***************************************************************************) +(* Spec *) +(***************************************************************************) + +Actions == + \/ Add + \/ \E w \in _workers: \/ Get(w) + \/ Done(w) + \/ CreatePolicy(w) + \/ CreateRole(w) + \/ CreateServiceAccount(w) + \/ PolicyHasNoARN(w) + \/ RoleHasNoRoleARN(w) + \/ RoleHasNoPolicyARN(w) + \/ RoleHasPolicyARN(w) + \/ IrsaAllDone(w) + \/ PolicyAllDone(w) + \/ RoleAllDone(w) + \/ SaAllDone(w) + + +Fairness == + /\ WF_vars(Add) + /\ WF_vars(Termination) + /\ \A w \in _workers: /\ WF_vars(Get(w)) + /\ WF_vars(Done(w)) + /\ WF_vars(CreatePolicy(w)) + /\ WF_vars(CreateRole(w)) + /\ WF_vars(CreateServiceAccount(w)) + /\ WF_vars(PolicyHasNoARN(w)) + /\ WF_vars(RoleHasNoRoleARN(w)) + /\ WF_vars(RoleHasNoPolicyARN(w)) + /\ WF_vars(RoleHasPolicyARN(w)) + /\ WF_vars(IrsaAllDone(w)) + /\ WF_vars(PolicyAllDone(w)) + /\ WF_vars(RoleAllDone(w)) + /\ WF_vars(SaAllDone(w)) + + +Next == + \/ Actions + \/ Termination + +Spec == + /\ Init + /\ [][ Next ]_vars + /\ []TypeOk + /\ Fairness + +(***************************************************************************) +(* Expectations *) +(***************************************************************************) + +\* Safety +NoConcurrentProcessingOfSameResource == + [] \A w \in DOMAIN workers : \/ workers[w].idle + \/ workers[w].req \notin {workers[x].req: x \in DOMAIN workers \ {w}} + +\* Liveness +TerminationIsTheLastAction == + [] ENABLED Termination ~> /\ ENABLED Termination + /\ ~ENABLED Actions + +THEOREM Spec => NoConcurrentProcessingOfSameResource +THEOREM Spec => TerminationIsTheLastAction + +==== diff --git a/_doc/model/shell.nix b/_doc/model/shell.nix new file mode 100644 index 0000000..527f8e9 --- /dev/null +++ b/_doc/model/shell.nix @@ -0,0 +1,20 @@ +let + pkgs = import (builtins.fetchTarball { + name = "nixos-20.09"; + url = "https://github.com/NixOS/nixpkgs/archive/20.09.tar.gz"; + sha256 = "1wg61h4gndm3vcprdcg7rc4s1v3jkm5xd7lw8r2f67w502y94gcy"; + }) {}; + + tlatools = with pkgs; + import ./tlaplus.nix { + inherit stdenv fetchFromGitHub makeWrapper adoptopenjdk-bin jre ant; }; +in +pkgs.mkShell { + buildInputs = + [ + tlatools + pkgs.adoptopenjdk-bin + pkgs.texlive.combined.scheme-basic + ]; +} + diff --git a/_doc/model/tlapdf b/_doc/model/tlapdf new file mode 100755 index 0000000..1ddbbe9 --- /dev/null +++ b/_doc/model/tlapdf @@ -0,0 +1,5 @@ +# usage : ./tlapdf ./IrsaOperator + +tla2tex -textwidth 470 -hoffset -70 -textheight 630 -voffset -50 -shade $1.tla \ + && pdflatex $1.tex \ + && rm $1.log $1.aux $1.dvi $1.tex $1.ps \ No newline at end of file diff --git a/_doc/model/tlaplus.nix b/_doc/model/tlaplus.nix new file mode 100644 index 0000000..3b21f02 --- /dev/null +++ b/_doc/model/tlaplus.nix @@ -0,0 +1,39 @@ +{ stdenv, fetchFromGitHub, makeWrapper +, adoptopenjdk-bin, jre, ant +}: + +stdenv.mkDerivation rec { + pname = "tlaplus"; + version = "1.7.1"; + + src = fetchFromGitHub { + owner = "tlaplus"; + repo = "tlaplus"; + rev = "refs/tags/v${version}"; + sha256 = "1mm6r9bq79zks50yk0agcpdkw9yy994m38ibmgpb3bi3wkpq9891"; + }; + + buildInputs = [ makeWrapper adoptopenjdk-bin ant ]; + + buildPhase = "ant -f tlatools/org.lamport.tlatools/customBuild.xml compile dist"; + installPhase = '' + mkdir -p $out/share/java $out/bin + cp tlatools/org.lamport.tlatools/dist/*.jar $out/share/java + makeWrapper ${jre}/bin/java $out/bin/tlc2 \ + --add-flags "-cp $out/share/java/tla2tools.jar tlc2.TLC" + makeWrapper ${jre}/bin/java $out/bin/tla2sany \ + --add-flags "-cp $out/share/java/tla2tools.jar tla2sany.SANY" + makeWrapper ${jre}/bin/java $out/bin/pcal \ + --add-flags "-cp $out/share/java/tla2tools.jar pcal.trans" + makeWrapper ${jre}/bin/java $out/bin/tla2tex \ + --add-flags "-cp $out/share/java/tla2tools.jar tla2tex.TLA" + ''; + + meta = { + description = "An algorithm specification language with model checking tools"; + homepage = "http://lamport.azurewebsites.net/tla/tla.html"; + license = stdenv.lib.licenses.mit; + platforms = stdenv.lib.platforms.unix; + maintainers = [ stdenv.lib.maintainers.thoughtpolice ]; + }; +} diff --git a/_example/k8s/Chart.yaml b/_example/k8s/Chart.yaml new file mode 100644 index 0000000..ddaac3b --- /dev/null +++ b/_example/k8s/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: "v1" +name: test-irsa-deploy +version: 0.0.1 diff --git a/_example/k8s/templates/deploy.yml b/_example/k8s/templates/deploy.yml new file mode 100644 index 0000000..7f31958 --- /dev/null +++ b/_example/k8s/templates/deploy.yml @@ -0,0 +1,36 @@ +apiVersion: irsa.voodoo.io/v1alpha1 +kind: IamRoleServiceAccount +metadata: + name: iamroleserviceaccount-test-sample +spec: + serviceAccountName: s3get + policy: + statement: + - resource: "arn:aws:s3:::{{ .Values.s3BucketName }}" + action: + - "s3:Get*" + - "s3:List*" + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: s3lister + name: s3lister +spec: + replicas: 1 + selector: + matchLabels: + app: s3lister + template: + metadata: + labels: + app: s3lister + spec: + serviceAccountName: s3get + containers: + - image: amazon/aws-cli + name: aws-cli + command: ["aws", "s3", "ls", "{{ .Values.s3BucketName }}"] + diff --git a/_example/k8s/values.yaml b/_example/k8s/values.yaml new file mode 100644 index 0000000..d9514cd --- /dev/null +++ b/_example/k8s/values.yaml @@ -0,0 +1 @@ +s3BucketName: diff --git a/_example/terraform/.gitignore b/_example/terraform/.gitignore new file mode 100644 index 0000000..fc08296 --- /dev/null +++ b/_example/terraform/.gitignore @@ -0,0 +1,3 @@ +.terraform/ +terraform* +kubeconfig* diff --git a/_example/terraform/.terraform.lock.hcl b/_example/terraform/.terraform.lock.hcl new file mode 100755 index 0000000..701c99f --- /dev/null +++ b/_example/terraform/.terraform.lock.hcl @@ -0,0 +1,50 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "3.23.0" + constraints = ">= 2.23.0, >= 2.28.1, >= 2.68.0, >= 3.0.0, >= 3.3.0" + hashes = [ + "h1:GugGr7igctZkUUt0im9b0CbdinTRxb4dNXvmGuN2gZ8=", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "1.13.3" + constraints = "~> 1.11, >= 1.11.1" + hashes = [ + "h1:whoGs/NeucMF8U/urPaeXdQUb+ppaO1Ae4r5aJRhfrU=", + ] +} + +provider "registry.terraform.io/hashicorp/local" { + version = "1.4.0" + constraints = "~> 1.2, >= 1.4.0" + hashes = [ + "h1:P3mtBQSRp/KhVLJgwdHZRTWaYsT6A9nSwrmKrRZwsW8=", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "2.1.2" + constraints = ">= 2.1.0, ~> 2.1" + hashes = [ + "h1:CFnENdqQu4g3LJNevA32aDxcUz2qGkRGQpFfkI8TCdE=", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "2.3.1" + constraints = ">= 2.1.0, ~> 2.1" + hashes = [ + "h1:bPBDLMpQzOjKhDlP9uH2UPIz9tSjcbCtLdiJ5ASmCx4=", + ] +} + +provider "registry.terraform.io/hashicorp/template" { + version = "2.2.0" + constraints = ">= 2.1.0, ~> 2.1" + hashes = [ + "h1:94qn780bi1qjrbC3uQtjJh3Wkfwd5+tTtJHOb7KTg9w=", + ] +} diff --git a/_example/terraform/README.md b/_example/terraform/README.md new file mode 100644 index 0000000..522e846 --- /dev/null +++ b/_example/terraform/README.md @@ -0,0 +1,9 @@ +create the infrastructure +``` +terraform apply +``` + +build & push the docker image to the newly created ecr +``` +make docker-build docker-push IMG=/irsa:0.0.1 +``` diff --git a/_example/terraform/main.tf b/_example/terraform/main.tf new file mode 100644 index 0000000..b9d54ad --- /dev/null +++ b/_example/terraform/main.tf @@ -0,0 +1,149 @@ +terraform { + required_version = ">= 0.12.0" +} + +provider "aws" { + version = ">= 2.28.1" + region = var.region +} + +provider "random" { + version = "~> 2.1" +} + +provider "local" { + version = "~> 1.2" +} + +provider "null" { + version = "~> 2.1" +} + +provider "template" { + version = "~> 2.1" +} + +data "aws_eks_cluster" "cluster" { + name = module.eks.cluster_id +} + +data "aws_eks_cluster_auth" "cluster" { + name = module.eks.cluster_id +} + +provider "kubernetes" { + host = data.aws_eks_cluster.cluster.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority.0.data) + token = data.aws_eks_cluster_auth.cluster.token + load_config_file = false + version = "~> 1.11" +} + +data "aws_availability_zones" "available" {} + +locals { + cluster_name = "test-eks-${random_string.suffix.result}" +} + +resource "random_string" "suffix" { + length = 8 + special = false +} + +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + version = "2.64.0" + + name = "test-irsa" + cidr = "10.0.0.0/16" + azs = data.aws_availability_zones.available.names + private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] + public_subnets = ["10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"] + enable_nat_gateway = true + single_nat_gateway = true + enable_dns_hostnames = true + + public_subnet_tags = { + "kubernetes.io/cluster/${local.cluster_name}" = "shared" + "kubernetes.io/role/elb" = "1" + } + + private_subnet_tags = { + "kubernetes.io/cluster/${local.cluster_name}" = "shared" + "kubernetes.io/role/internal-elb" = "1" + } +} + +module "eks" { + source = "terraform-aws-modules/eks/aws" + + cluster_name = local.cluster_name + cluster_version = "1.18" + subnets = module.vpc.private_subnets + enable_irsa = true + + tags = { + Environment = "test-irsa" + } + + vpc_id = module.vpc.vpc_id + + worker_groups = [ + { + name = "test-irsa" + instance_type = "t2.small" + asg_desired_capacity = 1 + } + ] +} + +module "iam_assumable_role_admin" { + source = "terraform-aws-modules/iam/aws//modules/iam-assumable-role-with-oidc" + version = "3.6.0" + create_role = true + role_name = "irsa-operator" + provider_url = replace(module.eks.cluster_oidc_issuer_url, "https://", "") + role_policy_arns = [aws_iam_policy.irsa.arn] + oidc_fully_qualified_subjects = ["system:serviceaccount:irsa-operator-system:irsa-operator-oidc-sa"] +} + +resource "aws_iam_policy" "irsa" { + name_prefix = "irsa-operator" + description = "irsa operator" + policy = data.aws_iam_policy_document.irsa.json +} + +data "aws_iam_policy_document" "irsa" { + statement { + sid = "irsaIam" + effect = "Allow" + + actions = [ + "iam:*" + ] + + resources = ["*"] + } +} + +module "s3_bucket" { + source = "terraform-aws-modules/s3-bucket/aws" + version = "v1.17.0" + + bucket = "test-irsa-${lower(random_string.suffix.result)}" + acl = "private" +} + +resource "aws_s3_bucket_object" "hello" { + bucket = module.s3_bucket.this_s3_bucket_id + key = "/hello/" +} + +resource "aws_s3_bucket_object" "irsa" { + bucket = module.s3_bucket.this_s3_bucket_id + key = "/irsa/" +} + +resource "aws_ecr_repository" "this" { + name = "irsa" +} diff --git a/_example/terraform/output.tf b/_example/terraform/output.tf new file mode 100644 index 0000000..784a23a --- /dev/null +++ b/_example/terraform/output.tf @@ -0,0 +1,15 @@ +output "s3_name" { + description = "s3 bucket name" + value = module.s3_bucket.this_s3_bucket_id +} + +output "ecr_url" { + description = "ecr url" + value = aws_ecr_repository.this.repository_url +} + +output "oidc_arn" { + description = "oidc server arn" + value = module.eks.oidc_provider_arn +} + diff --git a/_example/terraform/shell.nix b/_example/terraform/shell.nix new file mode 100644 index 0000000..fffec77 --- /dev/null +++ b/_example/terraform/shell.nix @@ -0,0 +1,22 @@ +let + stable = import (builtins.fetchTarball { + name = "nixos-20.09"; + url = "https://github.com/NixOS/nixpkgs/archive/20.09.tar.gz"; + sha256 = "1wg61h4gndm3vcprdcg7rc4s1v3jkm5xd7lw8r2f67w502y94gcy"; + }) {}; + + voodoo = import (builtins.fetchGit { + url = "git@github.com:VoodooTeam/nix-pkgs.git"; + ref = "master"; + }) stable; + + in + stable.mkShell { + buildInputs = + [ + voodoo.terraform_0_14_3 + voodoo.kubectl_1_19_4 + ]; + } + + diff --git a/_example/terraform/variables.tf b/_example/terraform/variables.tf new file mode 100644 index 0000000..b69c2e2 --- /dev/null +++ b/_example/terraform/variables.tf @@ -0,0 +1,3 @@ +variable "region" { + default = "eu-west-3" +} diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go new file mode 100644 index 0000000..e8fda24 --- /dev/null +++ b/api/v1alpha1/groupversion_info.go @@ -0,0 +1,20 @@ +// Package v1alpha1 contains API Schema definitions for the irsa v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=irsa.voodoo.io +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "irsa.voodoo.io", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/v1alpha1/iamroleserviceaccount_types.go b/api/v1alpha1/iamroleserviceaccount_types.go new file mode 100644 index 0000000..6bf42cd --- /dev/null +++ b/api/v1alpha1/iamroleserviceaccount_types.go @@ -0,0 +1,98 @@ +package v1alpha1 + +import ( + "errors" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// NewIamRoleServiceAccount is the IamRoleServiceAccount constructor +func NewIamRoleServiceAccount(name, ns, saName string, policyspec PolicySpec) *IamRoleServiceAccount { + return &IamRoleServiceAccount{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "irsa.voodoo.io/v1alpha1", + Kind: "IamRoleServiceAccount", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + }, + Spec: IamRoleServiceAccountSpec{ + ServiceAccountName: saName, + Policy: policyspec, + }, + } +} + +// HasStatus is used in tests, should be moved there +func (irsa IamRoleServiceAccount) HasStatus(st fmt.Stringer) bool { + return irsa.Status.Condition.String() == st.String() +} + +// IsPendingDeletion helps us to detect if the resource should be deleted +func (irsa IamRoleServiceAccount) IsPendingDeletion() bool { + return !irsa.ObjectMeta.DeletionTimestamp.IsZero() +} + +// Validate returns an error if the IamRoleServiceAccountSpec is not valid +func (irsa IamRoleServiceAccount) Validate() error { + if irsa.Spec.ServiceAccountName == "" { + return errors.New("empty serviceAccountName") + } + return irsa.Spec.Policy.Validate() +} + +// IamRoleServiceAccountSpec defines the desired state of IamRoleServiceAccount +type IamRoleServiceAccountSpec struct { + ServiceAccountName string `json:"serviceAccountName"` + Policy PolicySpec `json:"policy"` +} + +// IamRoleServiceAccountStatus defines the observed state of IamRoleServiceAccount +type IamRoleServiceAccountStatus struct { + Condition IrsaCondition `json:"condition"` + Reason string `json:"reason,omitempty"` +} + +type IrsaCondition string + +var ( + IrsaSubmitted IrsaCondition = "" + IrsaPending IrsaCondition = "pending" + IrsaSaNameConflict IrsaCondition = "saNameConflict" + IrsaForbidden IrsaCondition = "forbidden" + IrsaFailed IrsaCondition = "failed" + IrsaProgressing IrsaCondition = "progressing" + IrsaOK IrsaCondition = "created" +) + +// String is just used for comparison in HasStatus +func (i IrsaCondition) String() string { + return string(i) +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// IamRoleServiceAccount is the Schema for the iamroleserviceaccounts API +type IamRoleServiceAccount struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec IamRoleServiceAccountSpec `json:"spec,omitempty"` + Status IamRoleServiceAccountStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// IamRoleServiceAccountList contains a list of IamRoleServiceAccount +type IamRoleServiceAccountList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []IamRoleServiceAccount `json:"items"` +} + +func init() { + SchemeBuilder.Register(&IamRoleServiceAccount{}, &IamRoleServiceAccountList{}) +} diff --git a/api/v1alpha1/policy_types.go b/api/v1alpha1/policy_types.go new file mode 100644 index 0000000..971643b --- /dev/null +++ b/api/v1alpha1/policy_types.go @@ -0,0 +1,193 @@ +package v1alpha1 + +import ( + "errors" + "fmt" + + "github.com/aws/aws-sdk-go/aws/arn" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// NewPolicy constructs a Policy, setting mandatory fields for us +func NewPolicy(name, ns string, stm []StatementSpec) *Policy { + return &Policy{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "irsa.voodoo.io/v1alpha1", + Kind: "Policy", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + }, + Spec: PolicySpec{ + Statement: stm, + }, + } +} + +// HasStatus is used in tests, should be moved there +func (p Policy) HasStatus(st fmt.Stringer) bool { + return p.Status.Condition.String() == st.String() +} + +// IsPendingDeletion helps us to detect if a Policy should be deleted +func (p Policy) IsPendingDeletion() bool { + return !p.ObjectMeta.DeletionTimestamp.IsZero() +} + +// PolicySpec describes the policy that must be present on AWS +type PolicySpec struct { + ARN string `json:"arn,omitempty"` // the ARN of the aws policy + Statement []StatementSpec `json:"statement"` +} + +// Validate returns an error if the PolicySpec is not valid +func (spec PolicySpec) Validate() error { + if len(spec.Statement) == 0 { + return errors.New("empty Policy.spec.statement") + } + + for i, stm := range spec.Statement { + if err := stm.Validate(); err != nil { + return fmt.Errorf("statement :%d : %s", i, err.Error()) + } + } + + return nil +} + +// StatementSpec defines an aws statement (Sid is autogenerated & Effect is always "allow") +type StatementSpec struct { + Resource string `json:"resource"` // ARN of the target aws resource + Action []string `json:"action"` // the list of requested permissions on the aws resource above +} + +// Validate returns an error if the StatementSpec is not valid +func (spec StatementSpec) Validate() error { + if !arn.IsARN(spec.Resource) { + return fmt.Errorf("%s is an invalid ARN", spec.Resource) + } + + if len(spec.Action) == 0 { + return errors.New("empty action array provided") + } + + for i, a := range spec.Action { + if a == "" { + return fmt.Errorf("action #%d: empty action provided", i) + } + } + + return nil +} + +// IsSame is used to detect meaningful difference between 2 StatementSpec +// ie : order of .Action elements is not taken into account +func (a StatementSpec) IsSame(b StatementSpec) bool { + if a.Resource != b.Resource { + return false + } + + if len(a.Action) != len(b.Action) { + return false + } + + // we must ignore actions order + for _, sA := range a.Action { + diff := true + for _, sB := range b.Action { + if sA == sB { + diff = false + break + } + } + if diff { + return false + } + } + return true +} + +// StatementEquals is used to detect meaningful difference between 2 StatementSpec slices +// ie : order of elements is not taken into account +func StatementEquals(a, b []StatementSpec) bool { + if len(a) != len(b) { + return false + } + + for _, sA := range a { + diff := true + for _, sB := range b { + if sA.IsSame(sB) { + diff = false + break + } + } + if diff { + return false + } + } + return true +} + +// PolicyStatus defines the observed state of Policy +type PolicyStatus struct { + Condition CrCondition `json:"condition"` + Reason string `json:"reason,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// Policy is the Schema for the awspolicies API +type Policy struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PolicySpec `json:"spec,omitempty"` + Status PolicyStatus `json:"status,omitempty"` +} + +// Validate returns an error if the Policy is not valid +func (p Policy) Validate(cN string) error { + if err := p.Spec.Validate(); err != nil { + return err + } + + awsName := p.AwsName(cN) + if len(awsName) > 64 { + return fmt.Errorf("genereated uniqueName ( `%s` ) is too long ", awsName) + } + + return nil +} + +// AwsName is the name the resource will have on AWS +// It must be unique per AWS account thus the naming convention +func (p Policy) AwsName(cN string) string { + return fmt.Sprintf("irsa-op-%s-%s-%s", cN, p.ObjectMeta.Namespace, p.ObjectMeta.Name) +} + +// PathPrefix is the "directory" where the policy will be available +// It's used to retrieved a policy on AWS +func (p Policy) PathPrefix(cN string) string { + return fmt.Sprintf("/irsa-operator/%s/%s/%s/", cN, p.ObjectMeta.Namespace, p.ObjectMeta.Name) +} + +// Path is the "file" where the policy will be available +func (p Policy) Path(cN string) string { + return fmt.Sprintf("%spolicy/", p.PathPrefix(cN)) +} + +// +kubebuilder:object:root=true + +// PolicyList contains a list of Policy +type PolicyList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Policy `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Policy{}, &PolicyList{}) +} diff --git a/api/v1alpha1/role_types.go b/api/v1alpha1/role_types.go new file mode 100644 index 0000000..a2d3ba8 --- /dev/null +++ b/api/v1alpha1/role_types.go @@ -0,0 +1,103 @@ +package v1alpha1 + +import ( + "errors" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// NewRole constructs a Role, setting mandatory fields for us +func NewRole(name, ns, serviceAccountName string) *Role { + return &Role{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "irsa.voodoo.io/v1alpha1", + Kind: "Role", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + }, + Spec: RoleSpec{ + ServiceAccountName: serviceAccountName, + }, + } +} + +// HasStatus is used in tests, should be moved there +func (r Role) HasStatus(st fmt.Stringer) bool { + return r.Status.Condition.String() == st.String() +} + +// Validate returns an error if the Policy is not valid +func (r Role) Validate(cN string) error { + if err := r.Spec.Validate(); err != nil { + return err + } + + awsName := r.AwsName(cN) + if len(awsName) > 64 { + return fmt.Errorf("aws name is too long : %s", awsName) + } + + return nil +} + +// AwsName is the name the resource will have on AWS +// It must be unique per AWS account thus the naming convention +func (r Role) AwsName(cN string) string { + return fmt.Sprintf("irsa-op-%s-%s-%s", cN, r.ObjectMeta.Namespace, r.ObjectMeta.Name) +} + +// IsPendingDeletion helps us to detect if the resource should be deleted +func (r Role) IsPendingDeletion() bool { + return !r.ObjectMeta.DeletionTimestamp.IsZero() +} + +// RoleSpec defines the desired state of Role +type RoleSpec struct { + ServiceAccountName string `json:"serviceAccountName"` + + PolicyARN string `json:"policyarn,omitempty"` + RoleARN string `json:"rolearn,omitempty"` +} + +// Validate returns an error if the RoleSpec is not valid +func (spec RoleSpec) Validate() error { + if spec.ServiceAccountName == "" { + return errors.New("empty string provided as spec.ServiceAccountName") + } + + return nil +} + +// RoleStatus defines the observed state of Role +type RoleStatus struct { + Condition CrCondition `json:"condition"` + Reason string `json:"reason,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// Role is the Schema for the awsroles API +type Role struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RoleSpec `json:"spec,omitempty"` + Status RoleStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// RoleList contains a list of Role +type RoleList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Role `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Role{}, &RoleList{}) +} diff --git a/api/v1alpha1/shared_types.go b/api/v1alpha1/shared_types.go new file mode 100644 index 0000000..d04d134 --- /dev/null +++ b/api/v1alpha1/shared_types.go @@ -0,0 +1,16 @@ +package v1alpha1 + +// poorman's golang enum +type CrCondition string + +var ( + CrSubmitted CrCondition = "" + CrPending CrCondition = "pending" + CrForbidden CrCondition = "forbidden" + CrFailed CrCondition = "failed" + CrOK CrCondition = "created" +) + +func (i CrCondition) String() string { + return string(i) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000..b987798 --- /dev/null +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,320 @@ +// +build !ignore_autogenerated + +/* +Copyright 2020. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IamRoleServiceAccount) DeepCopyInto(out *IamRoleServiceAccount) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IamRoleServiceAccount. +func (in *IamRoleServiceAccount) DeepCopy() *IamRoleServiceAccount { + if in == nil { + return nil + } + out := new(IamRoleServiceAccount) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IamRoleServiceAccount) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IamRoleServiceAccountList) DeepCopyInto(out *IamRoleServiceAccountList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]IamRoleServiceAccount, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IamRoleServiceAccountList. +func (in *IamRoleServiceAccountList) DeepCopy() *IamRoleServiceAccountList { + if in == nil { + return nil + } + out := new(IamRoleServiceAccountList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IamRoleServiceAccountList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IamRoleServiceAccountSpec) DeepCopyInto(out *IamRoleServiceAccountSpec) { + *out = *in + in.Policy.DeepCopyInto(&out.Policy) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IamRoleServiceAccountSpec. +func (in *IamRoleServiceAccountSpec) DeepCopy() *IamRoleServiceAccountSpec { + if in == nil { + return nil + } + out := new(IamRoleServiceAccountSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IamRoleServiceAccountStatus) DeepCopyInto(out *IamRoleServiceAccountStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IamRoleServiceAccountStatus. +func (in *IamRoleServiceAccountStatus) DeepCopy() *IamRoleServiceAccountStatus { + if in == nil { + return nil + } + out := new(IamRoleServiceAccountStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Policy) DeepCopyInto(out *Policy) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Policy. +func (in *Policy) DeepCopy() *Policy { + if in == nil { + return nil + } + out := new(Policy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Policy) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PolicyList) DeepCopyInto(out *PolicyList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Policy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicyList. +func (in *PolicyList) DeepCopy() *PolicyList { + if in == nil { + return nil + } + out := new(PolicyList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PolicyList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PolicySpec) DeepCopyInto(out *PolicySpec) { + *out = *in + if in.Statement != nil { + in, out := &in.Statement, &out.Statement + *out = make([]StatementSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicySpec. +func (in *PolicySpec) DeepCopy() *PolicySpec { + if in == nil { + return nil + } + out := new(PolicySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PolicyStatus) DeepCopyInto(out *PolicyStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicyStatus. +func (in *PolicyStatus) DeepCopy() *PolicyStatus { + if in == nil { + return nil + } + out := new(PolicyStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Role) DeepCopyInto(out *Role) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Role. +func (in *Role) DeepCopy() *Role { + if in == nil { + return nil + } + out := new(Role) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Role) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RoleList) DeepCopyInto(out *RoleList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Role, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleList. +func (in *RoleList) DeepCopy() *RoleList { + if in == nil { + return nil + } + out := new(RoleList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RoleList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RoleSpec) DeepCopyInto(out *RoleSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleSpec. +func (in *RoleSpec) DeepCopy() *RoleSpec { + if in == nil { + return nil + } + out := new(RoleSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RoleStatus) DeepCopyInto(out *RoleStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleStatus. +func (in *RoleStatus) DeepCopy() *RoleStatus { + if in == nil { + return nil + } + out := new(RoleStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StatementSpec) DeepCopyInto(out *StatementSpec) { + *out = *in + if in.Action != nil { + in, out := &in.Action, &out.Action + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StatementSpec. +func (in *StatementSpec) DeepCopy() *StatementSpec { + if in == nil { + return nil + } + out := new(StatementSpec) + in.DeepCopyInto(out) + return out +} diff --git a/aws/aws.go b/aws/aws.go new file mode 100644 index 0000000..d5104cf --- /dev/null +++ b/aws/aws.go @@ -0,0 +1,376 @@ +package aws + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + + api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" + "github.com/VoodooTeam/irsa-operator/controllers" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/iam" + + "github.com/go-logr/logr" +) + +type AwsPolicy struct { + ARN string + Statement []api.StatementSpec +} + +type RealAwsManager struct { + Client *iam.IAM + log logr.Logger + clusterName string + oidcProviderArn string +} + +func NewAwsManager(sess *session.Session, logger logr.Logger, cN, oidcProviderArn string) controllers.AwsManager { + return &RealAwsManager{ + Client: iam.New(sess), + log: logger, + clusterName: cN, + oidcProviderArn: oidcProviderArn, + } +} + +var desc = "created by the irsa-operator" + +func (m RealAwsManager) GetStatement(arn string) ([]api.StatementSpec, error) { + // we retrieve the defaultVersionID by getting the policy + res, err := m.Client.GetPolicy(&iam.GetPolicyInput{PolicyArn: &arn}) + if err != nil { + return nil, err + } + + // we get the url-encoded document of the default version of the policy + resPV, err := m.Client.GetPolicyVersion(&iam.GetPolicyVersionInput{PolicyArn: &arn, VersionId: res.Policy.DefaultVersionId}) + if err != nil { + return nil, err + } + + // we decode the document + decodedDoc, err := url.QueryUnescape(*resPV.PolicyVersion.Document) + if err != nil { + return nil, err + } + + // unmarshal the document in our aws specific PolicyDocument struct + doc := &PolicyDocument{} + if err := json.Unmarshal([]byte(decodedDoc), doc); err != nil { + return nil, err + } + + // iterate over all the statements to convert them in statementSpec + stmtSpecs := []api.StatementSpec{} + for _, s := range doc.Statement { + stmtSpecs = append(stmtSpecs, s.ToSpec()) + } + + return stmtSpecs, nil +} + +func (m RealAwsManager) UpdatePolicy(policy api.Policy) error { + policyDoc, err := NewPolicyDocumentString(policy.Spec) + if err != nil { + m.logExtErr(err, "failed at policy serialization") + return err + } + + _, err = m.Client.CreatePolicyVersion(&iam.CreatePolicyVersionInput{PolicyArn: &policy.Spec.ARN, PolicyDocument: &policyDoc, SetAsDefault: aws.Bool(true)}) + if err != nil { + return err + } + + return nil +} + +func (m RealAwsManager) CreatePolicy(policy api.Policy) error { + _ = m.log.WithName("aws").WithName("policy") + + policyDoc, err := NewPolicyDocumentString(policy.Spec) + if err != nil { + m.logExtErr(err, "failed at policy serialization") + return err + } + + pn := policy.AwsName(m.clusterName) + pp := policy.Path(m.clusterName) + input := &iam.CreatePolicyInput{ + PolicyName: &pn, + PolicyDocument: &policyDoc, + Description: &desc, + Path: &pp, + } + + if _, err := m.Client.CreatePolicy(input); err != nil { + if reqErr, ok := err.(awserr.RequestFailure); ok { + if reqErr.StatusCode() == http.StatusConflict { + // already created, nothing to do + m.log.Info("policy already created on aws") + return nil + } + } + + // other error + m.logExtErr(err, "failed at policy creation") + return err + } + + m.log.Info("policy created on aws") + return nil +} + +func (m RealAwsManager) PolicyExists(policyARN string) (bool, error) { + if _, err := m.Client.GetPolicy(&iam.GetPolicyInput{PolicyArn: &policyARN}); err != nil { + if reqErr, ok := err.(awserr.RequestFailure); ok { + if reqErr.StatusCode() == http.StatusNotFound { + m.log.Info("policy doesnt exists on aws") + return false, nil + } + } + return false, err + } + + return true, nil +} + +// Gets an aws policy on aws +func (m RealAwsManager) GetPolicyARN(pathPrefix, uniqueName string) (string, error) { + _ = m.log.WithName("aws").WithName("policy") + + // we list the policies and try to find a match + out, err := m.Client.ListPolicies(&iam.ListPoliciesInput{PathPrefix: &pathPrefix}) + if err != nil { + m.logExtErr(err, "failed to list policies on aws") + return "", nil + } + + for _, p := range out.Policies { + if *p.PolicyName == uniqueName { + return *p.Arn, nil + } + } + + // return nothing + return "", nil +} + +func (m RealAwsManager) DeletePolicy(policyARN string) error { + + // we first ensure the policy isn't already deleted + if _, err := m.Client.GetPolicy(&iam.GetPolicyInput{PolicyArn: &policyARN}); err != nil { + if reqErr, ok := err.(awserr.RequestFailure); ok { + if reqErr.StatusCode() == http.StatusNotFound { + // already created, nothing to do + m.log.Info("policy doesnt exists on aws") + return nil + } + } + } + + m.log.Info("found policy") + + // list what the policy is attached to + listRes, err := m.Client.ListEntitiesForPolicy(&iam.ListEntitiesForPolicyInput{PolicyArn: &policyARN}) + if err != nil { + if reqErr, ok := err.(awserr.RequestFailure); ok { + if reqErr.StatusCode() == http.StatusNotFound { + // already deleted, nothing to do + m.log.Info("policy already deleted on aws") + return nil + } + } + return err + } + m.log.Info("policy found") + + // detach the policy from the role + if len(listRes.PolicyRoles) > 1 { + // should be attached to a single role + // we're conservative and return an error + return errors.New("policy attached to several roles, not supposed to happen") + } + + m.log.Info(fmt.Sprintf("policy attached to %d roles", len(listRes.PolicyRoles))) + for _, r := range listRes.PolicyRoles { + // we ignore the detach errors + _, err := m.Client.DetachRolePolicy(&iam.DetachRolePolicyInput{RoleName: r.RoleName, PolicyArn: &policyARN}) + if err != nil { + m.logExtErr(err, "failed to detach policy from role") + return err + } + } + + m.log.Info("policy will be deleted") + pvv, err := m.Client.ListPolicyVersions(&iam.ListPolicyVersionsInput{PolicyArn: &policyARN}) + if err != nil { + if reqErr, ok := err.(awserr.RequestFailure); ok { + if reqErr.StatusCode() == http.StatusNotFound { + // already deleted, nothing to do + m.log.Info("policy already deleted on aws") + return nil + } + } else { + return err + } + } + + for _, pv := range pvv.Versions { + if *pv.IsDefaultVersion { + continue + } + // we ignore potential errors, if we didn't manage to delete all non-default versions, the DeletePolicy below will fail + _, _ = m.Client.DeletePolicyVersion(&iam.DeletePolicyVersionInput{ + PolicyArn: &policyARN, + VersionId: pv.VersionId, + }) + } + + // actually delete policy + if _, err := m.Client.DeletePolicy(&iam.DeletePolicyInput{PolicyArn: &policyARN}); err != nil { + if reqErr, ok := err.(awserr.RequestFailure); ok { + if reqErr.StatusCode() == http.StatusNotFound { + // already deleted, nothing to do + m.log.Info("policy already deleted on aws") + return nil + } + } + return err + } + + return nil +} + +func (m RealAwsManager) RoleExists(roleName string) (bool, error) { + _ = m.log.WithName("aws").WithName("role") + + res, err := m.Client.GetRole(&iam.GetRoleInput{RoleName: &roleName}) + if err != nil { + if reqErr, ok := err.(awserr.RequestFailure); ok { + if reqErr.StatusCode() == http.StatusNotFound { + // if it failed because it didn't find it + // perfect, it just doesn't exists, not an error + m.log.Info("role not found on aws") + return false, nil + } + } + + // for any other error, let's return it + return false, err + } + + // the role field is supposed to be mandatory, but we just ensure it found something + return res.Role != nil, nil +} + +func (m RealAwsManager) GetRoleARN(roleName string) (string, error) { + _ = m.log.WithName("aws").WithName("role") + + res, err := m.Client.GetRole(&iam.GetRoleInput{RoleName: &roleName}) + if err != nil { + return "", err + } + + // the role field is supposed to be mandatory, but we just ensure it found something + if res.Role == nil { + return "", errors.New("aws returned an empty role without error (they said it shouldn't happen)") + } + return *res.Role.Arn, nil +} + +func (m RealAwsManager) GetAttachedRolePoliciesARNs(roleName string) ([]string, error) { + _ = m.log.WithName("aws").WithName("role") + + // let's get all the policies attached to the given role + res, err := m.Client.ListAttachedRolePolicies(&iam.ListAttachedRolePoliciesInput{RoleName: &roleName}) + if err != nil { + if reqErr, ok := err.(awserr.RequestFailure); ok { + if reqErr.StatusCode() == http.StatusNotFound { + // if it failed because it didn't find it + // perfect, it just doesn't exists, not an error + m.log.Info("role not found on aws") + return nil, nil + } + } + + // for any error, let's return it + m.logExtErr(err, "failed to get the role to find its attached policies on aws") + return nil, err + } + + // otherwise, we aggregate the policies ARNs + arns := []string{} + for _, p := range res.AttachedPolicies { + arns = append(arns, *p.PolicyArn) + } + + return arns, nil +} + +func (m RealAwsManager) CreateRole(role api.Role) error { + _ = m.log.WithName("aws").WithName("role") + + roleDoc, err := NewAssumeRolePolicyDoc(role, m.oidcProviderArn) + if err != nil { + m.logExtErr(err, "failed at trust policy serialization") + return err + } + + rn := role.AwsName(m.clusterName) + if _, err := m.Client.CreateRole(&iam.CreateRoleInput{ + RoleName: &rn, + AssumeRolePolicyDocument: &roleDoc, + Description: &desc, + }); err != nil { + if reqErr, ok := err.(awserr.RequestFailure); ok { + if reqErr.StatusCode() == http.StatusConflict { + // the role already exists, we return without error + m.log.Info("role already created on aws") + return nil + } + } + + m.logExtErr(err, "failed to create trust role policy") + return err + } + + m.log.Info(fmt.Sprintf("successfully created trust role policy (%s) on aws", rn)) + return nil +} + +func (m RealAwsManager) AttachRolePolicy(roleName, policyARN string) error { + _ = m.log.WithName("aws").WithName("role") + + if _, err := m.Client.AttachRolePolicy(&iam.AttachRolePolicyInput{RoleName: &roleName, PolicyArn: &policyARN}); err != nil { + m.logExtErr(err, "failed to attach role policy on aws") + return err + } + + m.log.Info(fmt.Sprintf("successfully attached role (%s) & policy (%s) on aws", roleName, policyARN)) + return nil +} + +func (m RealAwsManager) DeleteRole(roleName string) error { + if _, err := m.Client.DeleteRole(&iam.DeleteRoleInput{RoleName: &roleName}); err != nil { + if reqErr, ok := err.(awserr.RequestFailure); ok { + if reqErr.StatusCode() == http.StatusNotFound || reqErr.StatusCode() == http.StatusConflict { + // already deleted, nothing to do + return nil + } + } + m.logExtErr(err, "role already deleted on aws") + return err + } + + return nil +} + +func (m RealAwsManager) logExtErr(err error, msg string) { + m.log.Info(fmt.Sprintf("%s : %s", msg, err)) +} diff --git a/aws/aws_test.go b/aws/aws_test.go new file mode 100644 index 0000000..6fbc9a3 --- /dev/null +++ b/aws/aws_test.go @@ -0,0 +1,139 @@ +package aws_test + +import ( + api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var validPolicy = api.NewPolicy("name", "testns", []api.StatementSpec{ + {Resource: "arn:aws:s3:::my_corporate_bucket/exampleobject.png", Action: []string{"an:action"}}, +}) +var _ = Describe("policy", func() { + It("given a valid policy", func() { + + By("creating the policy it without error") + err := awsmngr.CreatePolicy(*validPolicy) + Expect(err).NotTo(HaveOccurred()) + + By("ensuring the creation is idempotent") + err = awsmngr.CreatePolicy(*validPolicy) + Expect(err).NotTo(HaveOccurred()) + + By("retrieving the policy ARN") + policyARN, err := awsmngr.GetPolicyARN(validPolicy.PathPrefix(clusterName), validPolicy.AwsName(clusterName)) + Expect(err).NotTo(HaveOccurred()) + Expect(policyARN).NotTo(BeEmpty()) + + By("deleting it") + Expect(policyARN).NotTo(BeEmpty()) + err = awsmngr.DeletePolicy(policyARN) + Expect(err).NotTo(HaveOccurred()) + + By("ensuring deletion is also idempotent") + Expect(policyARN).NotTo(BeEmpty()) + err = awsmngr.DeletePolicy(policyARN) + Expect(err).NotTo(HaveOccurred()) + }) +}) + +var _ = Describe("role", func() { + role := api.NewRole("name", "testns", "serviceaccountname") + Context("given a valid role", func() { + It("doesn't exist yet", func() { + exists, err := awsmngr.RoleExists(role.AwsName(clusterName)) + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(BeFalse()) + }) + + Context("creation", func() { + It("can create it without error", func() { + err := awsmngr.CreateRole(*role) + Expect(err).NotTo(HaveOccurred()) + }) + + Context("idempotency", func() { + It("creation is idempotent", func() { + err := awsmngr.CreateRole(*role) + Expect(err).NotTo(HaveOccurred()) + }) + + Context("exists check", func() { + It("can be checked for existing", func() { + exists, err := awsmngr.RoleExists(role.AwsName(clusterName)) + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(BeTrue()) + }) + + Context("policies can be attached", func() { + policyARN := "" + It("the policy must exist first", func() { + var err error + err = awsmngr.CreatePolicy(*validPolicy) + Expect(err).NotTo(HaveOccurred()) + + policyARN, err = awsmngr.GetPolicyARN(validPolicy.PathPrefix(clusterName), validPolicy.AwsName(clusterName)) + Expect(err).NotTo(HaveOccurred()) + Expect(policyARN).NotTo(BeEmpty()) + }) + + Context("when done", func() { + It("actually can be attached", func() { + err := awsmngr.AttachRolePolicy(role.AwsName(clusterName), policyARN) + Expect(err).NotTo(HaveOccurred()) + }) + + It("and retrieved", func() { + attached, err := awsmngr.GetAttachedRolePoliciesARNs(role.AwsName(clusterName)) + Expect(err).NotTo(HaveOccurred()) + Expect(len(attached)).To(Equal(1)) + Expect(attached[0]).To(Equal(policyARN)) + }) + + Context("delete attached policy", func() { + It("the role can be deleted without error", func() { + err := awsmngr.DeleteRole(role.AwsName(clusterName)) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("delete attached policy", func() { + It("can be done without error", func() { + err := awsmngr.DeletePolicy(policyARN) + Expect(err).NotTo(HaveOccurred()) + }) + + // this doesn't seem to work with localstack + // todo reproduce with the aws cli : + // nothing attached returned when calling ListEntitiesForPolicy against localstack + // I guess something is missing + // + //Context("the policy now should be detached", func() { + // It("and doesn't cause error to attempt to retrieve it", func() { + // attached, err := awsmngr.GetAttachedRolePoliciesARNs(role.AwsName()) + // Expect(err).NotTo(HaveOccurred()) + // Expect(attached).To(BeEmpty()) + // }) + //}) + }) + }) + }) + + Context("deletion", func() { + It("can be deleted without error", func() { + err := awsmngr.DeleteRole(role.AwsName(clusterName)) + Expect(err).NotTo(HaveOccurred()) + }) + + Context("idempotency", func() { + It("deletion is idempotent", func() { + err := awsmngr.DeleteRole(role.AwsName(clusterName)) + Expect(err).NotTo(HaveOccurred()) + }) + }) + }) + }) + }) + }) + }) +}) diff --git a/aws/suite_test.go b/aws/suite_test.go new file mode 100644 index 0000000..4980393 --- /dev/null +++ b/aws/suite_test.go @@ -0,0 +1,90 @@ +package aws_test + +import ( + "fmt" + "log" + "net/http" + "os" + "testing" + + irsaws "github.com/VoodooTeam/irsa-operator/aws" + "github.com/VoodooTeam/irsa-operator/controllers" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/endpoints" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/go-logr/stdr" + dockertest "github.com/ory/dockertest/v3" + "sigs.k8s.io/controller-runtime/pkg/envtest/printer" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var resource *dockertest.Resource +var pool *dockertest.Pool +var awsmngr controllers.AwsManager +var clusterName string + +func TestTypes(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecsWithDefaultAndCustomReporters(t, + "Aws Suite", + []Reporter{printer.NewlineReporter{}}) +} + +var _ = BeforeSuite(func() { + localStackEndpoint := os.Getenv("LOCALSTACK_ENDPOINT") + if localStackEndpoint == "" { + localStackEndpoint = setupLocalStack() + Expect(pool).NotTo(BeNil()) + Expect(resource).NotTo(BeNil()) + } else if _, err := http.Get(localStackEndpoint); err != nil { + log.Fatal("can't reach localstack on ", localStackEndpoint) + } + + clusterName = "clustername" + awsmngr = irsaws.NewAwsManager( + session.Must(session.NewSession(&aws.Config{ + Credentials: credentials.NewStaticCredentials("test", "test", ""), + DisableSSL: aws.Bool(true), + Region: aws.String(endpoints.UsWest1RegionID), + Endpoint: &localStackEndpoint, + })), + stdr.New(log.New(os.Stderr, "", log.LstdFlags)), + clusterName, + "oidcprovider.url", + ) + Expect(awsmngr).NotTo(BeNil()) +}) + +var _ = AfterSuite(func() { + if os.Getenv("LOCALSTACK_ENDPOINT") == "" { + err := pool.Purge(resource) + Expect(err).NotTo(HaveOccurred()) + } +}) + +func setupLocalStack() string { + var err error + pool, err = dockertest.NewPool("") + if err != nil { + log.Fatalf("Could not connect to docker: %s", err) + } + + resource, err = pool.Run("localstack/localstack", "0.12.4", []string{"SERVICES=iam,s3,sts"}) + if err != nil { + log.Fatalf("Could not start resource: %s", err) + } + + localStackEndpoint := fmt.Sprintf("http://localhost:%s", resource.GetPort("4566/tcp")) + if err = pool.Retry(func() error { + _, err := http.Get(localStackEndpoint) + return err + }); err != nil { + log.Fatalf("Could not connect to localstack container: %s", err) + } + + return localStackEndpoint +} diff --git a/aws/types.go b/aws/types.go new file mode 100644 index 0000000..2ee3716 --- /dev/null +++ b/aws/types.go @@ -0,0 +1,112 @@ +package aws + +import ( + "encoding/json" + "fmt" + "regexp" + + api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" +) + +type PolicyDocument struct { + Version string + Statement []Statement +} + +type Statement struct { + Effect StatementEffect + Action []string + Resource string +} + +func (s Statement) ToSpec() api.StatementSpec { + return api.StatementSpec{ + Resource: s.Resource, + Action: s.Action, + } +} + +type StatementEffect string + +const ( + StatementAllow StatementEffect = "Allow" + StatementDeny StatementEffect = "Deny" +) + +func NewPolicyDocumentString(p api.PolicySpec) (string, error) { + stmt := []Statement{} + + for _, s := range p.Statement { + stmt = append(stmt, Statement{ + Effect: StatementAllow, + Action: s.Action, + Resource: s.Resource, + }) + } + + policy := PolicyDocument{ + Version: "2012-10-17", + Statement: stmt, + } + + bytes, err := json.Marshal(policy) + if err != nil { + return "", err + } + + return string(bytes), nil +} + +type RoleDocument struct { + Version string + Statement []RoleStatement +} + +type RoleStatement struct { + Effect StatementEffect + Principal struct { + Federated string + } `json:"Principal"` + Action string + Condition struct { + StringEquals map[string]string + } +} + +func NewAssumeRolePolicyDoc(r api.Role, oidcProviderArn string) (string, error) { + // resource : https://aws.amazon.com/blogs/opensource/introducing-fine-grained-iam-roles-service-accounts + + // we extract the issuerHostpath from the oidcProviderARN (needed in the condition field) + issuerHostpath := oidcProviderArn + submatches := regexp.MustCompile(`(?s)/(.*)`).FindStringSubmatch(issuerHostpath) + if len(submatches) == 2 { + issuerHostpath = submatches[1] + } + + // then create the json formatted Trust policy + bytes, err := json.Marshal( + RoleDocument{ + Version: "2012-10-17", + Statement: []RoleStatement{ + { + Effect: StatementAllow, + Principal: struct{ Federated string }{ + Federated: string(oidcProviderArn), + }, + Action: "sts:AssumeRoleWithWebIdentity", + Condition: struct { + StringEquals map[string]string + }{ + StringEquals: map[string]string{ + fmt.Sprintf("%s:sub", issuerHostpath): fmt.Sprintf("system:serviceaccount:%s:%s", r.ObjectMeta.Namespace, r.Spec.ServiceAccountName)}, + }, + }, + }, + }, + ) + if err != nil { + return "", err + } + + return string(bytes), nil +} diff --git a/aws/types_test.go b/aws/types_test.go new file mode 100644 index 0000000..c5a7fc5 --- /dev/null +++ b/aws/types_test.go @@ -0,0 +1,84 @@ +package aws_test + +import ( + "encoding/json" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" + irsaws "github.com/VoodooTeam/irsa-operator/aws" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("PolicyDocument creation", func() { + Context("given a valid policySpec", func() { + validPolicy := api.PolicySpec{ + Statement: []api.StatementSpec{ + {Resource: "bla", Action: []string{"act1"}}, + }, + } + + It("generates a valid policy document", func() { + expectedPolicyDocument := irsaws.PolicyDocument{ + Version: "2012-10-17", + Statement: []irsaws.Statement{ + { + Effect: irsaws.StatementAllow, + Resource: "bla", + Action: []string{"act1"}, + }, + }, + } + policyJSON, err := irsaws.NewPolicyDocumentString(validPolicy) + Expect(err).NotTo(HaveOccurred()) + + genPolicy := &irsaws.PolicyDocument{} + err = json.Unmarshal([]byte(policyJSON), genPolicy) + Expect(err).NotTo(HaveOccurred()) + Expect(*genPolicy).Should(Equal(expectedPolicyDocument)) + }) + }) + + Context("given a valid role", func() { + expectedRoleDoc := irsaws.RoleDocument{ + Version: "2012-10-17", + Statement: []irsaws.RoleStatement{ + { + Effect: irsaws.StatementAllow, + Principal: struct{ Federated string }{ + Federated: "arn:aws.iam::111122223333:oidc-provider/oidc.REGION.eks.amazonaws.com/CLUSTER_ID", + }, + Action: "sts:AssumeRoleWithWebIdentity", + Condition: struct { + StringEquals map[string]string + }{ + StringEquals: map[string]string{"oidc.REGION.eks.amazonaws.com/CLUSTER_ID:sub": "system:serviceaccount:namespace:serviceAccountName"}, + }, + }, + }, + } + + Context("role document", func() { + It("generates a valid role document", func() { + r := api.Role{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "namespace", + }, + Spec: api.RoleSpec{ + ServiceAccountName: "serviceAccountName", + PolicyARN: "not used here", + }, + } + + roleJSON, err := irsaws.NewAssumeRolePolicyDoc(r, "arn:aws.iam::111122223333:oidc-provider/oidc.REGION.eks.amazonaws.com/CLUSTER_ID") + Expect(err).NotTo(HaveOccurred()) + + genPolicy := &irsaws.RoleDocument{} + err = json.Unmarshal([]byte(roleJSON), genPolicy) + Expect(*genPolicy).Should(Equal(expectedRoleDoc)) + Expect(err).NotTo(HaveOccurred()) + }) + }) + }) +}) diff --git a/config/certmanager/certificate.yaml b/config/certmanager/certificate.yaml new file mode 100644 index 0000000..52d8661 --- /dev/null +++ b/config/certmanager/certificate.yaml @@ -0,0 +1,25 @@ +# The following manifests contain a self-signed issuer CR and a certificate CR. +# More document can be found at https://docs.cert-manager.io +# WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: selfsigned-issuer + namespace: system +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml + namespace: system +spec: + # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize + dnsNames: + - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc + - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local + issuerRef: + kind: Issuer + name: selfsigned-issuer + secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize diff --git a/config/certmanager/kustomization.yaml b/config/certmanager/kustomization.yaml new file mode 100644 index 0000000..bebea5a --- /dev/null +++ b/config/certmanager/kustomization.yaml @@ -0,0 +1,5 @@ +resources: +- certificate.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/config/certmanager/kustomizeconfig.yaml b/config/certmanager/kustomizeconfig.yaml new file mode 100644 index 0000000..90d7c31 --- /dev/null +++ b/config/certmanager/kustomizeconfig.yaml @@ -0,0 +1,16 @@ +# This configuration is for teaching kustomize how to update name ref and var substitution +nameReference: +- kind: Issuer + group: cert-manager.io + fieldSpecs: + - kind: Certificate + group: cert-manager.io + path: spec/issuerRef/name + +varReference: +- kind: Certificate + group: cert-manager.io + path: spec/commonName +- kind: Certificate + group: cert-manager.io + path: spec/dnsNames diff --git a/config/crd/bases/irsa.voodoo.io_iamroleserviceaccounts.yaml b/config/crd/bases/irsa.voodoo.io_iamroleserviceaccounts.yaml new file mode 100644 index 0000000..74f41a6 --- /dev/null +++ b/config/crd/bases/irsa.voodoo.io_iamroleserviceaccounts.yaml @@ -0,0 +1,92 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null + name: iamroleserviceaccounts.irsa.voodoo.io +spec: + group: irsa.voodoo.io + names: + kind: IamRoleServiceAccount + listKind: IamRoleServiceAccountList + plural: iamroleserviceaccounts + singular: iamroleserviceaccount + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: IamRoleServiceAccount is the Schema for the iamroleserviceaccounts + API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: IamRoleServiceAccountSpec defines the desired state of IamRoleServiceAccount + properties: + policy: + description: PolicySpec describes the policy that must be present + on AWS + properties: + arn: + type: string + statement: + items: + description: StatementSpec defines an aws statement (Sid is + autogenerated & Effect is always "allow") + properties: + action: + items: + type: string + type: array + resource: + type: string + required: + - action + - resource + type: object + type: array + required: + - statement + type: object + serviceAccountName: + type: string + required: + - policy + - serviceAccountName + type: object + status: + description: IamRoleServiceAccountStatus defines the observed state of + IamRoleServiceAccount + properties: + condition: + type: string + reason: + type: string + required: + - condition + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/bases/irsa.voodoo.io_policies.yaml b/config/crd/bases/irsa.voodoo.io_policies.yaml new file mode 100644 index 0000000..efb1021 --- /dev/null +++ b/config/crd/bases/irsa.voodoo.io_policies.yaml @@ -0,0 +1,81 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null + name: policies.irsa.voodoo.io +spec: + group: irsa.voodoo.io + names: + kind: Policy + listKind: PolicyList + plural: policies + singular: policy + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Policy is the Schema for the awspolicies API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: PolicySpec describes the policy that must be present on AWS + properties: + arn: + type: string + statement: + items: + description: StatementSpec defines an aws statement (Sid is autogenerated + & Effect is always "allow") + properties: + action: + items: + type: string + type: array + resource: + type: string + required: + - action + - resource + type: object + type: array + required: + - statement + type: object + status: + description: PolicyStatus defines the observed state of Policy + properties: + condition: + description: poorman's golang enum + type: string + reason: + type: string + required: + - condition + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/bases/irsa.voodoo.io_roles.yaml b/config/crd/bases/irsa.voodoo.io_roles.yaml new file mode 100644 index 0000000..73419ff --- /dev/null +++ b/config/crd/bases/irsa.voodoo.io_roles.yaml @@ -0,0 +1,69 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null + name: roles.irsa.voodoo.io +spec: + group: irsa.voodoo.io + names: + kind: Role + listKind: RoleList + plural: roles + singular: role + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Role is the Schema for the awsroles API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: RoleSpec defines the desired state of Role + properties: + policyarn: + type: string + rolearn: + type: string + serviceAccountName: + type: string + required: + - serviceAccountName + type: object + status: + description: RoleStatus defines the observed state of Role + properties: + condition: + description: poorman's golang enum + type: string + reason: + type: string + required: + - condition + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml new file mode 100644 index 0000000..a512a74 --- /dev/null +++ b/config/crd/kustomization.yaml @@ -0,0 +1,27 @@ +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/irsa.voodoo.io_iamroleserviceaccounts.yaml +- bases/irsa.voodoo.io_roles.yaml +- bases/irsa.voodoo.io_policies.yaml +# +kubebuilder:scaffold:crdkustomizeresource + +patchesStrategicMerge: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +#- patches/webhook_in_iamroleserviceaccounts.yaml +#- patches/webhook_in_roles.yaml +#- patches/webhook_in_policies.yaml +# +kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +#- patches/cainjection_in_iamroleserviceaccounts.yaml +#- patches/cainjection_in_roles.yaml +#- patches/cainjection_in_policies.yaml +# +kubebuilder:scaffold:crdkustomizecainjectionpatch + +# the following config is for teaching kustomize how to do kustomization for CRDs. +configurations: +- kustomizeconfig.yaml diff --git a/config/crd/kustomizeconfig.yaml b/config/crd/kustomizeconfig.yaml new file mode 100644 index 0000000..ec5c150 --- /dev/null +++ b/config/crd/kustomizeconfig.yaml @@ -0,0 +1,19 @@ +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/config/crd/patches/cainjection_in_iamroleserviceaccounts.yaml b/config/crd/patches/cainjection_in_iamroleserviceaccounts.yaml new file mode 100644 index 0000000..45fa79a --- /dev/null +++ b/config/crd/patches/cainjection_in_iamroleserviceaccounts.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: iamroleserviceaccounts.irsa.voodoo.io diff --git a/config/crd/patches/cainjection_in_policies.yaml b/config/crd/patches/cainjection_in_policies.yaml new file mode 100644 index 0000000..5d9dbc8 --- /dev/null +++ b/config/crd/patches/cainjection_in_policies.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: policies.irsa.voodoo.io diff --git a/config/crd/patches/cainjection_in_roles.yaml b/config/crd/patches/cainjection_in_roles.yaml new file mode 100644 index 0000000..3749005 --- /dev/null +++ b/config/crd/patches/cainjection_in_roles.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: roles.irsa.voodoo.io diff --git a/config/crd/patches/webhook_in_iamroleserviceaccounts.yaml b/config/crd/patches/webhook_in_iamroleserviceaccounts.yaml new file mode 100644 index 0000000..9349838 --- /dev/null +++ b/config/crd/patches/webhook_in_iamroleserviceaccounts.yaml @@ -0,0 +1,14 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: iamroleserviceaccounts.irsa.voodoo.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/crd/patches/webhook_in_policies.yaml b/config/crd/patches/webhook_in_policies.yaml new file mode 100644 index 0000000..1b82694 --- /dev/null +++ b/config/crd/patches/webhook_in_policies.yaml @@ -0,0 +1,14 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: policies.irsa.voodoo.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/crd/patches/webhook_in_roles.yaml b/config/crd/patches/webhook_in_roles.yaml new file mode 100644 index 0000000..085687f --- /dev/null +++ b/config/crd/patches/webhook_in_roles.yaml @@ -0,0 +1,14 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: roles.irsa.voodoo.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml new file mode 100644 index 0000000..a4dfba9 --- /dev/null +++ b/config/default/kustomization.yaml @@ -0,0 +1,74 @@ +# Adds namespace to all resources. +namespace: irsa-operator-system + +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +namePrefix: irsa-operator- + +# Labels to add to all resources and selectors. +#commonLabels: +# someName: someValue + +bases: +- ../crd +- ../rbac +- ../manager +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- ../webhook +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. +#- ../certmanager +# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. +#- ../prometheus + +patchesStrategicMerge: +# Protect the /metrics endpoint by putting it behind auth. +# If you want your controller-manager to expose the /metrics +# endpoint w/o any authn/z, please comment the following line. +- manager_auth_proxy_patch.yaml + +# Mount the controller config file for loading manager configurations +# through a ComponentConfig type +#- manager_config_patch.yaml + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- manager_webhook_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. +# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. +# 'CERTMANAGER' needs to be enabled to use ca injection +#- webhookcainjection_patch.yaml + +# the following config is for teaching kustomize how to do var substitution +vars: +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. +#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR +# objref: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldref: +# fieldpath: metadata.namespace +#- name: CERTIFICATE_NAME +# objref: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +#- name: SERVICE_NAMESPACE # namespace of the service +# objref: +# kind: Service +# version: v1 +# name: webhook-service +# fieldref: +# fieldpath: metadata.namespace +#- name: SERVICE_NAME +# objref: +# kind: Service +# version: v1 +# name: webhook-service diff --git a/config/default/manager_auth_proxy_patch.yaml b/config/default/manager_auth_proxy_patch.yaml new file mode 100644 index 0000000..c93c8ca --- /dev/null +++ b/config/default/manager_auth_proxy_patch.yaml @@ -0,0 +1,28 @@ +# This patch inject a sidecar container which is a HTTP proxy for the +# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: kube-rbac-proxy + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=10" + ports: + - containerPort: 8443 + name: https + - name: manager + args: + - "--health-probe-bind-address=:8081" + - "--metrics-bind-address=127.0.0.1:8080" + - "--leader-elect" + - "--cluster-name={{ .Values.clusterName }}" + - "--oidc-provider-arn={{ .Values.oidcProviderARN }}" diff --git a/config/default/manager_config_patch.yaml b/config/default/manager_config_patch.yaml new file mode 100644 index 0000000..6c40015 --- /dev/null +++ b/config/default/manager_config_patch.yaml @@ -0,0 +1,20 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + args: + - "--config=controller_manager_config.yaml" + volumeMounts: + - name: manager-config + mountPath: /controller_manager_config.yaml + subPath: controller_manager_config.yaml + volumes: + - name: manager-config + configMap: + name: manager-config diff --git a/config/helm/irsa/Chart.yaml b/config/helm/irsa/Chart.yaml new file mode 100644 index 0000000..2464bb8 --- /dev/null +++ b/config/helm/irsa/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: "v1" +name: irsa-operator +version: 0.0.0 diff --git a/config/helm/irsa/templates/irsa-operator.yml b/config/helm/irsa/templates/irsa-operator.yml new file mode 100644 index 0000000..2731fbe --- /dev/null +++ b/config/helm/irsa/templates/irsa-operator.yml @@ -0,0 +1,526 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + name: irsa-operator-system +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null + name: iamroleserviceaccounts.irsa.voodoo.io +spec: + group: irsa.voodoo.io + names: + kind: IamRoleServiceAccount + listKind: IamRoleServiceAccountList + plural: iamroleserviceaccounts + singular: iamroleserviceaccount + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: IamRoleServiceAccount is the Schema for the iamroleserviceaccounts API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: IamRoleServiceAccountSpec defines the desired state of IamRoleServiceAccount + properties: + policy: + description: PolicySpec describes the policy that must be present on AWS + properties: + arn: + type: string + statement: + items: + description: StatementSpec defines an aws statement (Sid is autogenerated & Effect is always "allow") + properties: + action: + items: + type: string + type: array + resource: + type: string + required: + - action + - resource + type: object + type: array + required: + - statement + type: object + serviceAccountName: + type: string + required: + - policy + - serviceAccountName + type: object + status: + description: IamRoleServiceAccountStatus defines the observed state of IamRoleServiceAccount + properties: + condition: + type: string + reason: + type: string + required: + - condition + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null + name: policies.irsa.voodoo.io +spec: + group: irsa.voodoo.io + names: + kind: Policy + listKind: PolicyList + plural: policies + singular: policy + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Policy is the Schema for the awspolicies API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: PolicySpec describes the policy that must be present on AWS + properties: + arn: + type: string + statement: + items: + description: StatementSpec defines an aws statement (Sid is autogenerated & Effect is always "allow") + properties: + action: + items: + type: string + type: array + resource: + type: string + required: + - action + - resource + type: object + type: array + required: + - statement + type: object + status: + description: PolicyStatus defines the observed state of Policy + properties: + condition: + description: poorman's golang enum + type: string + reason: + type: string + required: + - condition + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null + name: roles.irsa.voodoo.io +spec: + group: irsa.voodoo.io + names: + kind: Role + listKind: RoleList + plural: roles + singular: role + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Role is the Schema for the awsroles API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: RoleSpec defines the desired state of Role + properties: + policyarn: + type: string + rolearn: + type: string + serviceAccountName: + type: string + required: + - serviceAccountName + type: object + status: + description: RoleStatus defines the observed state of Role + properties: + condition: + description: poorman's golang enum + type: string + reason: + type: string + required: + - condition + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: + eks.amazonaws.com/role-arn: '{{ .Values.rolearn }}' + name: irsa-operator-oidc-sa + namespace: irsa-operator-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: irsa-operator-leader-election-role + namespace: irsa-operator-system +rules: +- apiGroups: + - "" + - coordination.k8s.io + resources: + - configmaps + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: irsa-operator-manager-role +rules: +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - create + - delete + - get + - list + - watch +- apiGroups: + - irsa.voodoo.io + resources: + - iamroleserviceaccounts + verbs: + - create + - delete + - get + - list + - update + - watch +- apiGroups: + - irsa.voodoo.io + resources: + - iamroleserviceaccounts/finalizers + verbs: + - update +- apiGroups: + - irsa.voodoo.io + resources: + - iamroleserviceaccounts/status + verbs: + - get + - update +- apiGroups: + - irsa.voodoo.io + resources: + - policies + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - irsa.voodoo.io + resources: + - policies/finalizers + verbs: + - update +- apiGroups: + - irsa.voodoo.io + resources: + - policies/status + verbs: + - get + - patch + - update +- apiGroups: + - irsa.voodoo.io + resources: + - roles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - irsa.voodoo.io + resources: + - roles/finalizers + verbs: + - update +- apiGroups: + - irsa.voodoo.io + resources: + - roles/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: irsa-operator-metrics-reader +rules: +- nonResourceURLs: + - /metrics + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: irsa-operator-proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: irsa-operator-leader-election-rolebinding + namespace: irsa-operator-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: irsa-operator-leader-election-role +subjects: +- kind: ServiceAccount + name: irsa-operator-oidc-sa + namespace: irsa-operator-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: irsa-operator-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: irsa-operator-manager-role +subjects: +- kind: ServiceAccount + name: irsa-operator-oidc-sa + namespace: irsa-operator-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: irsa-operator-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: irsa-operator-proxy-role +subjects: +- kind: ServiceAccount + name: irsa-operator-oidc-sa + namespace: irsa-operator-system +--- +apiVersion: v1 +data: + controller_manager_config.yaml: | + apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 + kind: ControllerManagerConfig + health: + healthProbeBindAddress: :8081 + metrics: + bindAddress: 127.0.0.1:8080 + webhook: + port: 9443 + leaderElection: + leaderElect: true + resourceName: d8e70b98.voodoo.io +kind: ConfigMap +metadata: + name: irsa-operator-manager-config + namespace: irsa-operator-system +--- +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + name: irsa-operator-controller-manager-metrics-service + namespace: irsa-operator-system +spec: + ports: + - name: https + port: 8443 + targetPort: https + selector: + control-plane: controller-manager +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + control-plane: controller-manager + name: irsa-operator-controller-manager + namespace: irsa-operator-system +spec: + replicas: 1 + selector: + matchLabels: + control-plane: controller-manager + template: + metadata: + labels: + control-plane: controller-manager + spec: + containers: + - args: + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --logtostderr=true + - --v=10 + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 + name: kube-rbac-proxy + ports: + - containerPort: 8443 + name: https + - args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=127.0.0.1:8080 + - --leader-elect + - --cluster-name={{ .Values.clusterName }} + - --oidc-provider-arn={{ .Values.oidcProviderARN }} + command: + - /manager + image: | + 'docker.pkg.github.com/voodooteam/irsa-operator/irsa-operator:v{{ .Chart.Version }}' + imagePullPolicy: Always + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 100m + memory: 30Mi + requests: + cpu: 100m + memory: 20Mi + securityContext: + allowPrivilegeEscalation: false + securityContext: + fsGroup: 1234 + runAsUser: 65532 + serviceAccountName: irsa-operator-oidc-sa + terminationGracePeriodSeconds: 10 diff --git a/config/helm/irsa/values.yaml b/config/helm/irsa/values.yaml new file mode 100644 index 0000000..0456cd2 --- /dev/null +++ b/config/helm/irsa/values.yaml @@ -0,0 +1,3 @@ +clusterName: +rolearn: +oidcProviderARN: diff --git a/config/manager/controller_manager_config.yaml b/config/manager/controller_manager_config.yaml new file mode 100644 index 0000000..dfe9306 --- /dev/null +++ b/config/manager/controller_manager_config.yaml @@ -0,0 +1,11 @@ +apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 +kind: ControllerManagerConfig +health: + healthProbeBindAddress: :8081 +metrics: + bindAddress: 127.0.0.1:8080 +webhook: + port: 9443 +leaderElection: + leaderElect: true + resourceName: d8e70b98.voodoo.io diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml new file mode 100644 index 0000000..72b8cb5 --- /dev/null +++ b/config/manager/kustomization.yaml @@ -0,0 +1,10 @@ +resources: +- manager.yaml + +generatorOptions: + disableNameSuffixHash: true + +configMapGenerator: +- files: + - controller_manager_config.yaml + name: manager-config diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml new file mode 100644 index 0000000..6af28b3 --- /dev/null +++ b/config/manager/manager.yaml @@ -0,0 +1,66 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + name: system +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: oidc-sa + annotations: + eks.amazonaws.com/role-arn: "{{ .Values.rolearn }}" +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + control-plane: controller-manager +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + labels: + control-plane: controller-manager + spec: + serviceAccountName: irsa-operator-oidc-sa + securityContext: + runAsUser: 65532 + fsGroup: 1234 + containers: + - command: + - /manager + args: + - --leader-elect + image: | + 'docker.pkg.github.com/voodooteam/irsa-operator/irsa-operator:v{{ .Chart.Version }}' + name: manager + imagePullPolicy: Always + securityContext: + allowPrivilegeEscalation: false + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 100m + memory: 30Mi + requests: + cpu: 100m + memory: 20Mi + terminationGracePeriodSeconds: 10 diff --git a/config/prometheus/kustomization.yaml b/config/prometheus/kustomization.yaml new file mode 100644 index 0000000..ed13716 --- /dev/null +++ b/config/prometheus/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- monitor.yaml diff --git a/config/prometheus/monitor.yaml b/config/prometheus/monitor.yaml new file mode 100644 index 0000000..9b8047b --- /dev/null +++ b/config/prometheus/monitor.yaml @@ -0,0 +1,16 @@ + +# Prometheus Monitor Service (Metrics) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: controller-manager + name: controller-manager-metrics-monitor + namespace: system +spec: + endpoints: + - path: /metrics + port: https + selector: + matchLabels: + control-plane: controller-manager diff --git a/config/rbac/auth_proxy_client_clusterrole.yaml b/config/rbac/auth_proxy_client_clusterrole.yaml new file mode 100644 index 0000000..bd4af13 --- /dev/null +++ b/config/rbac/auth_proxy_client_clusterrole.yaml @@ -0,0 +1,7 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: metrics-reader +rules: +- nonResourceURLs: ["/metrics"] + verbs: ["get"] diff --git a/config/rbac/auth_proxy_role.yaml b/config/rbac/auth_proxy_role.yaml new file mode 100644 index 0000000..618f5e4 --- /dev/null +++ b/config/rbac/auth_proxy_role.yaml @@ -0,0 +1,13 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: proxy-role +rules: +- apiGroups: ["authentication.k8s.io"] + resources: + - tokenreviews + verbs: ["create"] +- apiGroups: ["authorization.k8s.io"] + resources: + - subjectaccessreviews + verbs: ["create"] diff --git a/config/rbac/auth_proxy_role_binding.yaml b/config/rbac/auth_proxy_role_binding.yaml new file mode 100644 index 0000000..83714de --- /dev/null +++ b/config/rbac/auth_proxy_role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: proxy-role +subjects: +- kind: ServiceAccount + name: irsa-operator-oidc-sa + namespace: irsa-operator-system diff --git a/config/rbac/auth_proxy_service.yaml b/config/rbac/auth_proxy_service.yaml new file mode 100644 index 0000000..6cf656b --- /dev/null +++ b/config/rbac/auth_proxy_service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + targetPort: https + selector: + control-plane: controller-manager diff --git a/config/rbac/iamroleserviceaccount_editor_role.yaml b/config/rbac/iamroleserviceaccount_editor_role.yaml new file mode 100644 index 0000000..bcc67fa --- /dev/null +++ b/config/rbac/iamroleserviceaccount_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit iamroleserviceaccounts. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: iamroleserviceaccount-editor-role +rules: +- apiGroups: + - irsa.voodoo.io + resources: + - iamroleserviceaccounts + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - irsa.voodoo.io + resources: + - iamroleserviceaccounts/status + verbs: + - get diff --git a/config/rbac/iamroleserviceaccount_viewer_role.yaml b/config/rbac/iamroleserviceaccount_viewer_role.yaml new file mode 100644 index 0000000..8331cad --- /dev/null +++ b/config/rbac/iamroleserviceaccount_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view iamroleserviceaccounts. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: iamroleserviceaccount-viewer-role +rules: +- apiGroups: + - irsa.voodoo.io + resources: + - iamroleserviceaccounts + verbs: + - get + - list + - watch +- apiGroups: + - irsa.voodoo.io + resources: + - iamroleserviceaccounts/status + verbs: + - get diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml new file mode 100644 index 0000000..66c2833 --- /dev/null +++ b/config/rbac/kustomization.yaml @@ -0,0 +1,12 @@ +resources: +- role.yaml +- role_binding.yaml +- leader_election_role.yaml +- leader_election_role_binding.yaml +# Comment the following 4 lines if you want to disable +# the auth proxy (https://github.com/brancz/kube-rbac-proxy) +# which protects your /metrics endpoint. +- auth_proxy_service.yaml +- auth_proxy_role.yaml +- auth_proxy_role_binding.yaml +- auth_proxy_client_clusterrole.yaml diff --git a/config/rbac/leader_election_role.yaml b/config/rbac/leader_election_role.yaml new file mode 100644 index 0000000..6334cc5 --- /dev/null +++ b/config/rbac/leader_election_role.yaml @@ -0,0 +1,27 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: leader-election-role +rules: +- apiGroups: + - "" + - coordination.k8s.io + resources: + - configmaps + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/config/rbac/leader_election_role_binding.yaml b/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 0000000..de3c5af --- /dev/null +++ b/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: leader-election-role +subjects: +- kind: ServiceAccount + name: irsa-operator-oidc-sa + namespace: irsa-operator-system diff --git a/config/rbac/policy_editor_role.yaml b/config/rbac/policy_editor_role.yaml new file mode 100644 index 0000000..2ddb615 --- /dev/null +++ b/config/rbac/policy_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit policies. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: policy-editor-role +rules: +- apiGroups: + - irsa.voodoo.io + resources: + - policies + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - irsa.voodoo.io + resources: + - policies/status + verbs: + - get diff --git a/config/rbac/policy_viewer_role.yaml b/config/rbac/policy_viewer_role.yaml new file mode 100644 index 0000000..942faa0 --- /dev/null +++ b/config/rbac/policy_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view policies. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: policy-viewer-role +rules: +- apiGroups: + - irsa.voodoo.io + resources: + - policies + verbs: + - get + - list + - watch +- apiGroups: + - irsa.voodoo.io + resources: + - policies/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml new file mode 100644 index 0000000..26e746f --- /dev/null +++ b/config/rbac/role.yaml @@ -0,0 +1,94 @@ + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: manager-role +rules: +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - create + - delete + - get + - list + - watch +- apiGroups: + - irsa.voodoo.io + resources: + - iamroleserviceaccounts + verbs: + - create + - delete + - get + - list + - update + - watch +- apiGroups: + - irsa.voodoo.io + resources: + - iamroleserviceaccounts/finalizers + verbs: + - update +- apiGroups: + - irsa.voodoo.io + resources: + - iamroleserviceaccounts/status + verbs: + - get + - update +- apiGroups: + - irsa.voodoo.io + resources: + - policies + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - irsa.voodoo.io + resources: + - policies/finalizers + verbs: + - update +- apiGroups: + - irsa.voodoo.io + resources: + - policies/status + verbs: + - get + - patch + - update +- apiGroups: + - irsa.voodoo.io + resources: + - roles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - irsa.voodoo.io + resources: + - roles/finalizers + verbs: + - update +- apiGroups: + - irsa.voodoo.io + resources: + - roles/status + verbs: + - get + - patch + - update diff --git a/config/rbac/role_binding.yaml b/config/rbac/role_binding.yaml new file mode 100644 index 0000000..0a2c18f --- /dev/null +++ b/config/rbac/role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: irsa-operator-oidc-sa + namespace: irsa-operator-system diff --git a/config/rbac/role_editor_role.yaml b/config/rbac/role_editor_role.yaml new file mode 100644 index 0000000..31610ec --- /dev/null +++ b/config/rbac/role_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit roles. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: role-editor-role +rules: +- apiGroups: + - irsa.voodoo.io + resources: + - roles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - irsa.voodoo.io + resources: + - roles/status + verbs: + - get diff --git a/config/rbac/role_viewer_role.yaml b/config/rbac/role_viewer_role.yaml new file mode 100644 index 0000000..66f1933 --- /dev/null +++ b/config/rbac/role_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view roles. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: role-viewer-role +rules: +- apiGroups: + - irsa.voodoo.io + resources: + - roles + verbs: + - get + - list + - watch +- apiGroups: + - irsa.voodoo.io + resources: + - roles/status + verbs: + - get diff --git a/config/samples/irsa_v1alpha1_iamroleserviceaccount.yaml b/config/samples/irsa_v1alpha1_iamroleserviceaccount.yaml new file mode 100644 index 0000000..648a0bb --- /dev/null +++ b/config/samples/irsa_v1alpha1_iamroleserviceaccount.yaml @@ -0,0 +1,12 @@ +apiVersion: irsa.voodoo.io/v1alpha1 +kind: IamRoleServiceAccount +metadata: + name: iamroleserviceaccount-test-sample +spec: + serviceAccountName: s3put + policy: + statement: + - resource: "arn:aws:s3:::test-irsa-4gkut9fl" + action: + - "s3:Get*" + - "s3:List*" diff --git a/config/samples/irsa_v1alpha1_policy.yaml b/config/samples/irsa_v1alpha1_policy.yaml new file mode 100644 index 0000000..311780d --- /dev/null +++ b/config/samples/irsa_v1alpha1_policy.yaml @@ -0,0 +1,7 @@ +apiVersion: irsa.voodoo.io/v1alpha1 +kind: Policy +metadata: + name: policy-sample +spec: + # Add fields here + foo: bar diff --git a/config/samples/irsa_v1alpha1_role.yaml b/config/samples/irsa_v1alpha1_role.yaml new file mode 100644 index 0000000..48c0691 --- /dev/null +++ b/config/samples/irsa_v1alpha1_role.yaml @@ -0,0 +1,7 @@ +apiVersion: irsa.voodoo.io/v1alpha1 +kind: Role +metadata: + name: role-sample +spec: + # Add fields here + foo: bar diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml new file mode 100644 index 0000000..31eb4d6 --- /dev/null +++ b/config/samples/kustomization.yaml @@ -0,0 +1,6 @@ +## Append samples you want in your CSV to this file as resources ## +resources: +- irsa_v1alpha1_iamroleserviceaccount.yaml +- irsa_v1alpha1_role.yaml +- irsa_v1alpha1_policy.yaml +# +kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/test_deploy.yaml b/config/samples/test_deploy.yaml new file mode 100644 index 0000000..8076bf8 --- /dev/null +++ b/config/samples/test_deploy.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: s3test + name: s3test +spec: + replicas: 1 + selector: + matchLabels: + app: s3test + template: + metadata: + labels: + app: s3test + spec: + serviceAccountName: s3put + containers: + - image: amazon/aws-cli + name: aws-cli + command: ["aws", "s3", "ls", "test-irsa-4gkut9fl"] + diff --git a/config/scorecard/bases/config.yaml b/config/scorecard/bases/config.yaml new file mode 100644 index 0000000..c770478 --- /dev/null +++ b/config/scorecard/bases/config.yaml @@ -0,0 +1,7 @@ +apiVersion: scorecard.operatorframework.io/v1alpha3 +kind: Configuration +metadata: + name: config +stages: +- parallel: true + tests: [] diff --git a/config/scorecard/kustomization.yaml b/config/scorecard/kustomization.yaml new file mode 100644 index 0000000..d73509e --- /dev/null +++ b/config/scorecard/kustomization.yaml @@ -0,0 +1,16 @@ +resources: +- bases/config.yaml +patchesJson6902: +- path: patches/basic.config.yaml + target: + group: scorecard.operatorframework.io + version: v1alpha3 + kind: Configuration + name: config +- path: patches/olm.config.yaml + target: + group: scorecard.operatorframework.io + version: v1alpha3 + kind: Configuration + name: config +# +kubebuilder:scaffold:patchesJson6902 diff --git a/config/scorecard/patches/basic.config.yaml b/config/scorecard/patches/basic.config.yaml new file mode 100644 index 0000000..d434786 --- /dev/null +++ b/config/scorecard/patches/basic.config.yaml @@ -0,0 +1,10 @@ +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - basic-check-spec + image: quay.io/operator-framework/scorecard-test:v1.3.0 + labels: + suite: basic + test: basic-check-spec-test diff --git a/config/scorecard/patches/olm.config.yaml b/config/scorecard/patches/olm.config.yaml new file mode 100644 index 0000000..890bf05 --- /dev/null +++ b/config/scorecard/patches/olm.config.yaml @@ -0,0 +1,50 @@ +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-bundle-validation + image: quay.io/operator-framework/scorecard-test:v1.3.0 + labels: + suite: olm + test: olm-bundle-validation-test +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-crds-have-validation + image: quay.io/operator-framework/scorecard-test:v1.3.0 + labels: + suite: olm + test: olm-crds-have-validation-test +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-crds-have-resources + image: quay.io/operator-framework/scorecard-test:v1.3.0 + labels: + suite: olm + test: olm-crds-have-resources-test +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-spec-descriptors + image: quay.io/operator-framework/scorecard-test:v1.3.0 + labels: + suite: olm + test: olm-spec-descriptors-test +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-status-descriptors + image: quay.io/operator-framework/scorecard-test:v1.3.0 + labels: + suite: olm + test: olm-status-descriptors-test diff --git a/controllers/README.md b/controllers/README.md new file mode 100644 index 0000000..4ee3d27 --- /dev/null +++ b/controllers/README.md @@ -0,0 +1,5 @@ +# testing + +hack : +- in order not to miss a resource state with the find function (resources are polled on an `interval` basis), +- a `testingDelay` variable can be set in each controller so it delays event processing by an amount of time longer than interval (`interval + 100ms`) diff --git a/controllers/aws.go b/controllers/aws.go new file mode 100644 index 0000000..b7468bd --- /dev/null +++ b/controllers/aws.go @@ -0,0 +1,28 @@ +package controllers + +import ( + api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" +) + +type AwsManager interface { + AwsPolicyManager + AwsRoleManager +} + +type AwsPolicyManager interface { + PolicyExists(arn string) (bool, error) + GetStatement(arn string) ([]api.StatementSpec, error) + GetPolicyARN(pathPrefix, uniqueName string) (string, error) + CreatePolicy(api.Policy) error + UpdatePolicy(api.Policy) error + DeletePolicy(policyARN string) error +} + +type AwsRoleManager interface { + RoleExists(roleName string) (bool, error) + CreateRole(api.Role) error + DeleteRole(roleName string) error + AttachRolePolicy(roleName, policyARN string) error + GetAttachedRolePoliciesARNs(roleName string) ([]string, error) + GetRoleARN(roleName string) (string, error) +} diff --git a/controllers/aws_test.go b/controllers/aws_test.go new file mode 100644 index 0000000..85f4dd7 --- /dev/null +++ b/controllers/aws_test.go @@ -0,0 +1,306 @@ +package controllers_test + +import ( + "errors" + "fmt" + "log" + "strings" + "sync" + + api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" + "github.com/VoodooTeam/irsa-operator/aws" +) + +func newAwsFake() *awsFake { + return &awsFake{ + stacks: &sync.Map{}, + } +} + +type awsFake struct { + stacks *sync.Map // used as : map[resourceName(string)]stack(awsStack) +} + +type awsStack struct { + policy aws.AwsPolicy + role awsRole + errors map[awsMethod]struct{} + events []string +} + +type awsRole struct { + name string + arn string + attachedPolicies []string +} + +type awsMethod string + +const ( + policyExists awsMethod = "policyExists" + getStatement awsMethod = "getStatement" + updatePolicy awsMethod = "updatePolicy" + createPolicy awsMethod = "createPolicy" + deletePolicy awsMethod = "deletePolicy" + getPolicyARN awsMethod = "getPolicyARN" + createRole awsMethod = "createRole" + attachRolePolicy awsMethod = "attachRolePolicy" + deleteRole awsMethod = "deleteRole" + roleExists awsMethod = "roleExists" + getRoleARN awsMethod = "getRoleARN" + getAttachedRolePoliciesARNs awsMethod = "getAttachedRolePoliciesARNs" +) + +func (s *awsFake) PolicyExists(arn string) (bool, error) { + cN := getResourceName(arn) + if err := s.shouldFailAt(cN, policyExists); err != nil { + return false, err + } + + stack, ok := s.stacks.Load(cN) + if !ok { + return false, nil + } + + return stack.(awsStack).policy.ARN != "", nil +} + +func (s *awsFake) CreatePolicy(policy api.Policy) error { + n := policy.ObjectMeta.Name + if err := s.shouldFailAt(n, createPolicy); err != nil { + return err + } + + // todo abstract away this step (same code in all methods) + raw, ok := s.stacks.Load(n) + if !ok { + return errors.New("policy doesn't exists") + } + stack := raw.(awsStack) + + stack.policy = aws.AwsPolicy{ARN: policyARN(policy), Statement: policy.Spec.Statement} + s.stacks.Store(n, stack) + return nil +} + +func (s *awsFake) UpdatePolicy(policy api.Policy) error { + n := policy.ObjectMeta.Name + if err := s.shouldFailAt(n, updatePolicy); err != nil { + return err + } + raw, ok := s.stacks.Load(n) + if !ok { + return errors.New("policy doesn't exists") + } + + stack := raw.(awsStack) + stack.policy.Statement = policy.Spec.Statement + s.stacks.Store(n, stack) + return nil +} + +func (s *awsFake) DeletePolicy(arn string) error { + cN := getResourceName(arn) + if err := s.shouldFailAt(cN, deletePolicy); err != nil { + return err + } + + raw, ok := s.stacks.Load(cN) + if !ok { + return errors.New("stack doesn't exists") + } + + stack := raw.(awsStack) + stack.policy = aws.AwsPolicy{} + s.stacks.Store(cN, stack) + return nil +} + +func (s *awsFake) GetPolicyARN(pathPrefix, awsName string) (string, error) { + cN := getPolicyNameFromAwsName(awsName) + if err := s.shouldFailAt(cN, getPolicyARN); err != nil { + return "", err + } + + stack, ok := s.stacks.Load(cN) + if !ok { + return "", errors.New("stack doesn't exists") + } + + return stack.(awsStack).policy.ARN, nil +} + +func (s *awsFake) GetStatement(arn string) ([]api.StatementSpec, error) { + n := getResourceName(arn) + if err := s.shouldFailAt(n, getStatement); err != nil { + return nil, err + } + + stack, ok := s.stacks.Load(n) + if !ok { + return nil, errors.New("stack doesn't exists") + } + return stack.(awsStack).policy.Statement, nil +} + +func (s *awsFake) CreateRole(r api.Role) error { + n := r.ObjectMeta.Name + if err := s.shouldFailAt(n, createRole); err != nil { + return err + } + + raw, ok := s.stacks.Load(n) + if !ok { + return errors.New("policy doesn't exists") + } + + stack := raw.(awsStack) + stack.role = awsRole{name: roleName(r), arn: roleArn(r), attachedPolicies: []string{}} + s.stacks.Store(n, stack) + return nil +} + +func (s *awsFake) DeleteRole(roleName string) error { + cN := getResourceName(roleName) + if err := s.shouldFailAt(cN, deleteRole); err != nil { + return err + } + + raw, ok := s.stacks.Load(cN) + if !ok { + return errors.New("stack doesn't exists") + } + + stack := raw.(awsStack) + stack.role = awsRole{} + s.stacks.Store(cN, stack) + return nil +} + +func (s *awsFake) RoleExists(roleName string) (bool, error) { + cN := getClusterNameFromRoleName(roleName) + if err := s.shouldFailAt(cN, roleExists); err != nil { + return false, err + } + + raw, ok := s.stacks.Load(cN) + if !ok { + return false, errors.New("stack doesn't exists") + } + + stack := raw.(awsStack) + return stack.role.name != "", nil +} + +func (s *awsFake) GetRoleARN(roleName string) (string, error) { + cN := getClusterNameFromRoleName(roleName) + if err := s.shouldFailAt(cN, getRoleARN); err != nil { + return "", err + } + + raw, ok := s.stacks.Load(cN) + if !ok { + return "", errors.New("stack doesn't exists") + } + + stack := raw.(awsStack) + return stack.role.arn, nil +} + +func (s *awsFake) GetAttachedRolePoliciesARNs(roleName string) ([]string, error) { + cN := getClusterNameFromRoleName(roleName) + if err := s.shouldFailAt(cN, getAttachedRolePoliciesARNs); err != nil { + return nil, err + } + + raw, ok := s.stacks.Load(cN) + if !ok { + return nil, errors.New("stack doesn't exists") + } + + stack := raw.(awsStack) + return stack.role.attachedPolicies, nil +} + +func (s *awsFake) AttachRolePolicy(roleName, policyARN string) error { + cN := getClusterNameFromRoleName(roleName) + if err := s.shouldFailAt(cN, attachRolePolicy); err != nil { + return err + } + + raw, ok := s.stacks.Load(cN) + if !ok { + return errors.New("stack doesn't exists") + } + + stack := raw.(awsStack) + // todo fix roleName inconcistencies (should be awsRoleName ?) + //if stack.role.name != roleName { + // return errors.New("role not found") + //} + + stack.role.attachedPolicies = append(stack.role.attachedPolicies, policyARN) + s.stacks.Store(cN, stack) + return nil +} + +// shouldFailAt does 2 (!) things : +// - abstract the error mechanism +// - toggle the next result that will be returned +func (s *awsFake) shouldFailAt(n string, m awsMethod) error { + raw, found := s.stacks.Load(n) + if !found { + log.Fatal("stack not found :", n, ",", string(m)) + } + stack := raw.(awsStack) + + // an error exists for method key + // delete the error + // we add this event + // store the new stack[clusterName] + if _, found := stack.errors[m]; found { + delete(stack.errors, m) + stack.events = append(stack.events, fmt.Sprintf("failure : %s", string(m))) + s.stacks.Store(n, stack) + return errors.New(string(m)) + } + + // otherwise + // we store anything at method key + // add the event + // store the new stack[clusterName] + stack.events = append(stack.events, fmt.Sprintf("success : %s", string(m))) + s.stacks.Store(n, stack) + return nil +} + +func policyARN(p api.Policy) string { + arn := genUniqueName(p.Namespace, p.Name) + return arn +} + +func roleName(r api.Role) string { + rN := genUniqueName(r.Namespace, r.Name) + return rN +} + +func roleArn(r api.Role) string { + rN := genUniqueName(r.Namespace, r.Name) + return "arn:" + rN +} + +func genUniqueName(ns, n string) string { + // we don't have to build something realistic, just something that is convenient for testing + return fmt.Sprintf("%s-%s", ns, n) +} + +func getResourceName(roleNameOrPolicyARN string) string { + return strings.Split(roleNameOrPolicyARN, "-")[1] +} + +func getPolicyNameFromAwsName(name string) string { + return strings.Split(name, "-")[4] +} +func getClusterNameFromRoleName(name string) string { + return strings.Split(name, "-")[4] +} diff --git a/controllers/helper.go b/controllers/helper.go new file mode 100644 index 0000000..39d0b92 --- /dev/null +++ b/controllers/helper.go @@ -0,0 +1,29 @@ +package controllers + +import "fmt" + +type completed bool + +// Helper functions to check and remove string from a slice of strings. +func containsString(slice []string, s string) bool { + for _, item := range slice { + if item == s { + return true + } + } + return false +} + +func removeString(slice []string, s string) (result []string) { + for _, item := range slice { + if item == s { + continue + } + result = append(result, item) + } + return +} + +func (r *RoleReconciler) logExtErr(err error, msg string) { + r.log.Info(fmt.Sprintf("%s : %s", msg, err)) +} diff --git a/controllers/iamroleserviceaccount_controller.go b/controllers/iamroleserviceaccount_controller.go new file mode 100644 index 0000000..23cf3ac --- /dev/null +++ b/controllers/iamroleserviceaccount_controller.go @@ -0,0 +1,482 @@ +package controllers + +import ( + "context" + + "errors" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + k8s "k8s.io/apimachinery/pkg/api/errors" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + + "time" + + api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func NewIrsaReconciler(client client.Client, scheme *runtime.Scheme, logger logr.Logger) *IamRoleServiceAccountReconciler { + return &IamRoleServiceAccountReconciler{ + Client: client, + scheme: scheme, + log: logger, + finalizerID: "irsa.irsa.voodoo.io", + } +} + +// IamRoleServiceAccountReconciler reconciles a IamRoleServiceAccount object +type IamRoleServiceAccountReconciler struct { + client.Client + log logr.Logger + scheme *runtime.Scheme + finalizerID string + + TestingDelay *time.Duration +} + +// +kubebuilder:rbac:groups=irsa.voodoo.io,resources=iamroleserviceaccounts,verbs=get;list;watch;create;update;delete +// +kubebuilder:rbac:groups=irsa.voodoo.io,resources=iamroleserviceaccounts/status,verbs=get;update +// +kubebuilder:rbac:groups=irsa.voodoo.io,resources=iamroleserviceaccounts/finalizers,verbs=update +// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch;create;delete + +// Reconcile is called each time an event occurs on an api.IamRoleServiceAccount resource +func (r *IamRoleServiceAccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = r.log.WithValues("iamroleserviceaccount", req.NamespacedName) + + { // a processing delay can be set to ensure the testing framework sees every transitionnal state + r.waitIfTesting() + } + + var irsa *api.IamRoleServiceAccount + { // extract role from the request + var ok completed + irsa, ok = r.getIrsaFromReq(ctx, req) + if !ok { // didn't complete, requeing + return ctrl.Result{Requeue: true}, nil + } + if irsa == nil { // not found, has been deleted + return ctrl.Result{}, nil + } + } + + { // finalizer registration & execution + if irsa.IsPendingDeletion() { + if ok := r.executeFinalizerIfPresent(ctx, irsa); !ok { + return ctrl.Result{Requeue: true}, nil + } + // ok, no requeue + return ctrl.Result{}, nil + } else { + if ok := r.registerFinalizerIfNeeded(irsa); !ok { + return ctrl.Result{Requeue: true}, nil + } + } + } + + if irsa.Status.Condition == api.IrsaSubmitted { // the resource has just been created + return r.admissionStep(ctx, irsa) + } + + // for whatever condition we'll try to check the role and policy actually exists + return r.reconcilerRoutine(ctx, irsa) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *IamRoleServiceAccountReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&api.IamRoleServiceAccount{}). + Owns(&api.Role{}). + Owns(&api.Policy{}). + Owns(&corev1.ServiceAccount{}). + WithOptions(controller.Options{ + MaxConcurrentReconciles: 10, + }). + Complete(r) +} + +// +// privates +// + +// admissionStep does spec validation +func (r *IamRoleServiceAccountReconciler) admissionStep(ctx context.Context, irsa *api.IamRoleServiceAccount) (ctrl.Result, error) { + r.log.Info("handling submitted IRSA (checking values, setting defaults)") + + { // we check submitted spec validity + if err := irsa.Validate(); err != nil { + r.log.Error(err, "invalid spec, passing status to failed") + + // we set the status.condition to failed + if err := r.updateStatus(ctx, irsa, api.IamRoleServiceAccountStatus{Condition: api.IrsaFailed, Reason: err.Error()}); err != nil { + r.log.Error(err, "failed to set iamroleserviceaccount status to failed") + return ctrl.Result{Requeue: true}, nil + } + + // and stop here + return ctrl.Result{}, nil + } + } + + { // we check the serviceAccountName doesn't conflict with an existing one + if r.saWithNameExistsInNs(ctx, irsa.Spec.ServiceAccountName, irsa.ObjectMeta.Namespace) { + e := errors.New("service_account already exists") + r.log.Info(e.Error()) + + // if it's the case, we set the status.condition to saNameConflict + if err := r.updateStatus(ctx, irsa, api.IamRoleServiceAccountStatus{Condition: api.IrsaSaNameConflict, Reason: e.Error()}); err != nil { + r.log.Error(err, "failed to set iamroleserviceaccount status to saNameConflict") + return ctrl.Result{Requeue: true}, nil + } + + // and stop here + return ctrl.Result{}, nil + } + } + + { // we update the status to pending + if err := r.updateStatus(ctx, irsa, api.IamRoleServiceAccountStatus{Condition: api.IrsaPending, Reason: "passed validation"}); err != nil { + r.log.Error(err, "failed to set iamroleserviceaccount status to pending") + return ctrl.Result{Requeue: true}, nil + } + } + + // and stop here + return ctrl.Result{}, nil +} + +// reconcilerRoutine is an infinite loop attempting to make the policy, role, service_account converge to the irsa.Spec +func (r *IamRoleServiceAccountReconciler) reconcilerRoutine(ctx context.Context, irsa *api.IamRoleServiceAccount) (ctrl.Result, error) { + r.log.Info("reconciler routine") + + var policyAlreadyExists, roleAlreadyExists, saAlreadyExists bool + + { // Policy creation + var ok completed + policyAlreadyExists, ok = r.policyAlreadyExists(ctx, irsa.ObjectMeta.Name, irsa.ObjectMeta.Namespace) + if !ok { + return ctrl.Result{Requeue: true}, nil + } + + if !policyAlreadyExists { + if ok := r.createPolicy(ctx, irsa); !ok { + return ctrl.Result{Requeue: true}, nil + } + } else { + if ok := r.updatePolicyIfNeeded(ctx, irsa); !ok { + return ctrl.Result{Requeue: true}, nil + } + } + } + + { // Role creation + var ok completed + roleAlreadyExists, ok = r.roleAlreadyExists(ctx, irsa.ObjectMeta.Name, irsa.ObjectMeta.Namespace) + if !ok { + return ctrl.Result{Requeue: true}, nil + } + + if !roleAlreadyExists { + if ok := r.createRole(ctx, irsa); !ok { + return ctrl.Result{Requeue: true}, nil + } + } + } + + { // service_account creation + var ok completed + saAlreadyExists, ok = r.saAlreadyExists(ctx, irsa.Spec.ServiceAccountName, irsa.ObjectMeta.Namespace) + if !ok { + return ctrl.Result{Requeue: true}, nil + } + if !saAlreadyExists { + if r.roleIsOk(ctx, irsa.ObjectMeta.Name, irsa.ObjectMeta.Namespace) && r.policyIsOK(ctx, irsa.ObjectMeta.Name, irsa.ObjectMeta.Namespace) { + if ok := r.createServiceAccount(ctx, irsa); !ok { + return ctrl.Result{Requeue: true}, nil + } + } + } + } + + { // set the status to ok + if policyAlreadyExists && roleAlreadyExists && saAlreadyExists && irsa.Status.Condition != api.IrsaOK { + if err := r.updateStatus(ctx, irsa, api.IamRoleServiceAccountStatus{Condition: api.IrsaOK, Reason: "all resources successfully created"}); err != nil { + r.log.Error(err, "failed to set iamroleserviceaccount status to ok") + return ctrl.Result{Requeue: true}, nil + } + } + } + + // all done, we'll keep watching after missing resources every 20" + return ctrl.Result{RequeueAfter: time.Second * 20}, nil +} + +func (r *IamRoleServiceAccountReconciler) executeFinalizerIfPresent(ctx context.Context, irsa *api.IamRoleServiceAccount) completed { + if !containsString(irsa.ObjectMeta.Finalizers, r.finalizerID) { + // no finalizer to execute + return true + } + + r.log.Info("executing finalizer") + + { // we delete the service account we created, we first need to ensure it is not owned by another operator (since it's a serviceaccount) + sa := &corev1.ServiceAccount{} + if err := r.Get(ctx, types.NamespacedName{Namespace: irsa.Namespace, Name: irsa.Spec.ServiceAccountName}, sa); err != nil { + if !k8serrors.IsNotFound(err) { + r.log.Error(err, "get resource failed") + return false + } + } + + owned := false + for _, or := range sa.GetOwnerReferences() { + if or.UID == irsa.UID { + owned = true + break + } + } + + if owned { // we delete the service account if we own it + if err := r.Delete(ctx, sa); err != nil { + if !k8serrors.IsNotFound(err) { + r.log.Error(err, "get resource failed") + return false + } + } + } + } + + { // we delete the iamroleserviceaccount CR we may have already created + r.log.Info("deleting irsa") + if err := r.Delete(ctx, irsa); err != nil { + if !k8serrors.IsNotFound(err) { + r.log.Error(err, "delete resource failed") + return false + } + } + } + + { // we remove our finalizer from the list and update it. + irsa.ObjectMeta.Finalizers = removeString(irsa.ObjectMeta.Finalizers, r.finalizerID) + if err := r.Update(context.Background(), irsa); err != nil { + r.log.Error(err, "failed to remove the finalizer") + return false + } + } + + return true +} + +func (r *IamRoleServiceAccountReconciler) getIrsaFromReq(ctx context.Context, req ctrl.Request) (*api.IamRoleServiceAccount, completed) { + irsa := &api.IamRoleServiceAccount{} + if err := r.Get(ctx, req.NamespacedName, irsa); err != nil { + if k8serrors.IsNotFound(err) { + r.log.Info("IamRoleServiceAccount deleted") + return nil, true + } + + r.log.Error(err, "get resource failed") + return nil, false + } + + return irsa, true +} + +func (r IamRoleServiceAccountReconciler) roleIsOk(ctx context.Context, name, ns string) bool { + role := &api.Role{} + if err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, role); err != nil { + return false + } + return role.Status.Condition == api.CrOK +} + +func (r IamRoleServiceAccountReconciler) policyIsOK(ctx context.Context, name, ns string) bool { + policy := &api.Policy{} + if err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, policy); err != nil { + return false + } + return policy.Status.Condition == api.CrOK +} + +func (r *IamRoleServiceAccountReconciler) policyAlreadyExists(ctx context.Context, name, ns string) (bool, completed) { + return r.resourceExists(ctx, name, ns, &api.Policy{}) +} + +func (r *IamRoleServiceAccountReconciler) roleAlreadyExists(ctx context.Context, name, ns string) (bool, completed) { + return r.resourceExists(ctx, name, ns, &api.Role{}) +} + +func (r *IamRoleServiceAccountReconciler) saAlreadyExists(ctx context.Context, name, ns string) (bool, completed) { + return r.resourceExists(ctx, name, ns, &corev1.ServiceAccount{}) +} + +func (r *IamRoleServiceAccountReconciler) resourceExists(ctx context.Context, name, ns string, obj client.Object) (bool, completed) { + if err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, obj); err != nil { + if k8s.IsNotFound(err) { + return false, true + } else { + // something went wrong, requeue + r.log.Error(err, "get resource failed") + return false, false + } + } + + return true, true +} + +func (r *IamRoleServiceAccountReconciler) createPolicy(ctx context.Context, irsa *api.IamRoleServiceAccount) completed { + r.log.Info("creating missing policy") + // we instantiate the policy + newPolicy := api.NewPolicy(irsa.ObjectMeta.Name, irsa.ObjectMeta.Namespace, irsa.Spec.Policy.Statement) + + // set this irsa instance as the owner of this role + if err := ctrl.SetControllerReference(irsa, newPolicy, r.scheme); err != nil { + // another resource is already the owner... + r.log.Error(err, "failed to set the controller reference") + return false + } + + if err := r.Client.Create(ctx, newPolicy); err != nil { + // we failed to create it, requeue + r.log.Error(err, "failed to create policy") + return false + } + return true +} + +func (r *IamRoleServiceAccountReconciler) updatePolicyIfNeeded(ctx context.Context, irsa *api.IamRoleServiceAccount) completed { + policy := &api.Policy{} + exists, ok := r.resourceExists(ctx, irsa.ObjectMeta.Name, irsa.ObjectMeta.Namespace, policy) + if !bool(ok) || !exists { + return false + } + + // todo, check if they are actually different + + // we instantiate the policy + policy.Spec.Statement = irsa.Spec.Policy.Statement + if err := r.Client.Update(ctx, policy); err != nil { + // we failed to create it, requeue + r.log.Error(err, "failed to create policy") + return false + } + + return true +} + +func (r *IamRoleServiceAccountReconciler) createRole(ctx context.Context, irsa *api.IamRoleServiceAccount) completed { + r.log.Info("creating role") + + // we initialize a new role + role := api.NewRole( + irsa.ObjectMeta.Name, + irsa.ObjectMeta.Namespace, + irsa.Spec.ServiceAccountName, + ) + + // set this irsa instance as the owner of this role + if err := ctrl.SetControllerReference(irsa, role, r.scheme); err != nil { + // another resource is already the owner... + r.log.Error(err, "failed to set the controller reference") + return false + } + + // then we create the role on k8s + if err := r.Client.Create(ctx, role); err != nil { + r.log.Error(err, "failed to create role") + return false + } + + return true +} + +func (r *IamRoleServiceAccountReconciler) createServiceAccount(ctx context.Context, irsa *api.IamRoleServiceAccount) completed { + r.log.Info("creating service_account") + + role := &api.Role{} + if err := r.Client.Get(ctx, types.NamespacedName{Name: irsa.ObjectMeta.Name, Namespace: irsa.ObjectMeta.Namespace}, role); err != nil { + if k8s.IsNotFound(err) { + return false + } else { + // something went wrong, requeue + r.log.Error(err, "get resource failed") + return false + } + } + + if role.Spec.RoleARN != "" { + r.log.Info("role has arn") + + // we initialize a new serviceAccount + newServiceAccount := &corev1.ServiceAccount{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ServiceAccount", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: irsa.Spec.ServiceAccountName, + Namespace: irsa.ObjectMeta.Namespace, + Annotations: map[string]string{ + "eks.amazonaws.com/role-arn": role.Spec.RoleARN, + }, + }, + } + + // set the current iamroleserviceaccount as the owner + if err := ctrl.SetControllerReference(irsa, newServiceAccount, r.scheme); err != nil { + // another resource is already the owner... + r.log.Error(err, "failed to set the controller reference") + return false + } + + // then actually create the serviceAccount + if err := r.Client.Create(ctx, newServiceAccount); err != nil { + // we failed to create it, requeue + r.log.Error(err, "failed to create serviceaccount") + return false + } + } else { + r.log.Info("role has no RoleArn in spec, waiting") + } + + return true +} + +func (r *IamRoleServiceAccountReconciler) saWithNameExistsInNs(ctx context.Context, name, ns string) bool { + // a bit fragile, don't check errors other than api.NotFound + return r.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, &corev1.ServiceAccount{}) == nil +} + +func (r *IamRoleServiceAccountReconciler) updateStatus(ctx context.Context, obj *api.IamRoleServiceAccount, status api.IamRoleServiceAccountStatus) error { + obj.Status = status + if err := r.Status().Update(ctx, obj); err != nil { + r.log.Error(err, "unable to update IamRoleServiceAccount status") + return err + } + + return nil +} + +func (r *IamRoleServiceAccountReconciler) waitIfTesting() { + if r.TestingDelay != nil { + time.Sleep(*r.TestingDelay) + } +} + +func (r *IamRoleServiceAccountReconciler) registerFinalizerIfNeeded(role *api.IamRoleServiceAccount) completed { + if !containsString(role.ObjectMeta.Finalizers, r.finalizerID) { + // the finalizer isn't registered yet + // we add it to the irsa + role.ObjectMeta.Finalizers = append(role.ObjectMeta.Finalizers, r.finalizerID) + if err := r.Update(context.Background(), role); err != nil { + r.log.Error(err, "setting finalizer failed") + return false + } + } + return true +} diff --git a/controllers/iamroleserviceaccount_controller_test.go b/controllers/iamroleserviceaccount_controller_test.go new file mode 100644 index 0000000..8a3ede7 --- /dev/null +++ b/controllers/iamroleserviceaccount_controller_test.go @@ -0,0 +1,43 @@ +package controllers_test + +import ( + api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("IamRoleServiceAccount validity check", func() { + Context("if the spec.policy is empty", func() { + invalidPolicySpec := api.PolicySpec{} + + It("fails at submission", func() { + Expect( + api.NewIamRoleServiceAccount(validName(), testns, randString(), invalidPolicySpec).Validate(), + ).ShouldNot(Succeed()) + }) + }) + + Context("if the spec.policy is ok", func() { + validPolicy := api.PolicySpec{ + Statement: []api.StatementSpec{ + {Resource: "arn:aws:s3:::my_corporate_bucket/exampleobject.png", Action: []string{"act1"}}, + }, + } + name := validName() + + Context("if the spec.serviceAccountName is an empty string", func() { + invalidSaName := "" + It("doesnt pass validation", func() { + irsa := api.NewIamRoleServiceAccount(name, testns, invalidSaName, validPolicy) + Expect(irsa.Validate()).ShouldNot(Succeed()) + }) + }) + + Context("if everything else is also ok", func() { + irsa := api.NewIamRoleServiceAccount(name, testns, validName(), validPolicy) + It("it passes validation", func() { + Expect(irsa.Validate()).Should(Succeed()) + }) + }) + }) +}) diff --git a/controllers/model_test.go b/controllers/model_test.go new file mode 100644 index 0000000..93274c3 --- /dev/null +++ b/controllers/model_test.go @@ -0,0 +1,151 @@ +package controllers_test + +import ( + "log" + "math/rand" + "sync" + + api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" + "github.com/VoodooTeam/irsa-operator/aws" + "github.com/davecgh/go-spew/spew" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("cluster state", func() { + // generate n awsState and clusterStates ? + // define the converged state (everything created & k8s resources at ok) + // expect the converged state to be reached before timeout + var names []string + + It("must converge", func() { + count := 100 + log.Println("will run against ", count, "different envs") + for i := 0; i < count; i++ { + names = append(names, validName()) + } + + var wg sync.WaitGroup + for _, n := range names { + wg.Add(1) + go run(n, &wg) + } + wg.Wait() + }) +}) + +func run(irsaName string, wg *sync.WaitGroup) { + defer wg.Done() + var stack awsStack + initialErrors := getInitialErrs() + + // will log in case of failure the final state and the events that lead there + defer func(iE map[awsMethod]struct{}) { + if recover() != nil { + log.Println(">>> inital errors :") + spew.Dump(iE) + log.Println(">>> ended up with stack state :") + spew.Dump(stack) + + GinkgoT().FailNow() + } + }(copyErrs(initialErrors)) + + // ensure the stack doesnt exists yet + raw, ok := st.stacks.Load(irsaName) + Expect(raw).To(BeNil()) + Expect(ok).To(BeFalse()) + + // initialize the awsStack for the current cluster + st.stacks.Store(irsaName, awsStack{ + policy: aws.AwsPolicy{}, + role: awsRole{}, + errors: initialErrors, + events: []string{}, + }) + + submittedPolicy := api.PolicySpec{ + Statement: []api.StatementSpec{ + {Resource: "arn:aws:s3:::my_corporate_bucket/exampleobject.png", Action: []string{"act1"}}, + }, + } + + { // k8s + saName := validName() + { + // we submit the iamroleserviceaccount Spec to k8s + createResource( + api.NewIamRoleServiceAccount(irsaName, testns, saName, submittedPolicy), + ).Should(Succeed()) + } + { // every CR must eventually reach an OK status & serviceAccount has been created + foundPolicyInCondition(irsaName, testns, api.CrOK).Should(BeTrue()) + foundRoleInCondition(irsaName, testns, api.CrOK).Should(BeTrue()) + foundIrsaInCondition(irsaName, testns, api.IrsaOK).Should(BeTrue()) + findSa(saName, testns).Should(BeTrue()) + } + } + + { // aws resources checks + // once the cluster is ok, we check what it did to the aws stack + raw, ok := st.stacks.Load(irsaName) + Expect(raw).ShouldNot(BeNil()) + Expect(ok).Should(BeTrue()) + stack = raw.(awsStack) + + // policy + Expect(stack.policy).ShouldNot(BeNil()) + Expect(stack.policy.Statement).To(Equal(submittedPolicy.Statement)) + + // role + Expect(stack.role).ShouldNot(BeNil()) + Expect(len(stack.role.attachedPolicies)).To(Equal(1)) + Expect(stack.role.attachedPolicies[0]).To(Equal(stack.policy.ARN)) + + { + iamrsa := getIrsa(irsaName, testns) + Expect(iamrsa.Spec.Policy.Statement).To(Equal(stack.policy.Statement)) + } + { + role := getRole(irsaName, testns) + Expect(role.Spec.PolicyARN).To(Equal(stack.policy.ARN)) + Expect(role.Spec.RoleARN).To(Equal(stack.role.arn)) + } + { + policy := getPolicy(irsaName, testns) + Expect(policy.Spec.ARN).To(Equal(stack.policy.ARN)) + } + } +} + +func getInitialErrs() map[awsMethod]struct{} { + methods := []awsMethod{ + policyExists, + getStatement, + updatePolicy, + createPolicy, + deletePolicy, + getPolicyARN, + createRole, + attachRolePolicy, + deleteRole, + roleExists, + getRoleARN, + getAttachedRolePoliciesARNs} + + errs := make(map[awsMethod]struct{}) + for _, m := range methods { + if rand.Float32() < 0.5 { + errs[m] = struct{}{} + } + } + return errs +} + +func copyErrs(in map[awsMethod]struct{}) map[awsMethod]struct{} { + out := make(map[awsMethod]struct{}) + for k, v := range in { + out[k] = v + } + return out +} diff --git a/controllers/policy_controller.go b/controllers/policy_controller.go new file mode 100644 index 0000000..b67f50b --- /dev/null +++ b/controllers/policy_controller.go @@ -0,0 +1,263 @@ +package controllers + +import ( + "context" + "fmt" + "time" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/api/errors" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + + api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" +) + +func NewPolicyReconciler(client client.Client, scheme *runtime.Scheme, awspm AwsPolicyManager, logger logr.Logger, cN string) *PolicyReconciler { + return &PolicyReconciler{ + Client: client, + log: logger, + scheme: scheme, + awsPM: awspm, + finalizerID: "role.irsa.voodoo.io", + clusterName: cN, + } +} + +// PolicyReconciler reconciles a Policy object +type PolicyReconciler struct { + client.Client + scheme *runtime.Scheme + awsPM AwsPolicyManager + log logr.Logger + + finalizerID string + clusterName string + + TestingDelay *time.Duration +} + +// +kubebuilder:rbac:groups=irsa.voodoo.io,resources=policies,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=irsa.voodoo.io,resources=policies/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=irsa.voodoo.io,resources=policies/finalizers,verbs=update + +// Reconcile is called each time an event occurs on an api.Policy resource +func (r *PolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = r.log.WithValues("policy", req.NamespacedName) + + { // a processing delay can be set to ensure the testing framework sees every transitionnal state + r.waitIfTesting() + } + + var policy *api.Policy + { // extract policy from the request + var ok completed + policy, ok = r.getPolicyFromReq(ctx, req) + if !ok { + // didn't complete, requeing + return ctrl.Result{Requeue: true}, nil + } + if policy == nil { + // not found, has been deleted + return ctrl.Result{}, nil + } + } + + { // finalizer registration & execution + if policy.IsPendingDeletion() { + if ok := r.executeFinalizerIfPresent(policy); !ok { + return ctrl.Result{Requeue: true}, nil + } + // ok, no requeue + return ctrl.Result{}, nil + } else { + if ok := r.registerFinalizerIfNeeded(policy); !ok { + return ctrl.Result{Requeue: true}, nil + } + } + } + + // the resource has just been created + if policy.Status.Condition == api.CrSubmitted { + return r.admissionStep(ctx, policy) + } + + // for whatever condition we'll try to check the aws policy needs to be created or updated + return r.reconcilerRoutine(ctx, policy) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *PolicyReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&api.Policy{}). + WithOptions(controller.Options{ + MaxConcurrentReconciles: 10, + }). + Complete(r) +} + +// +// privates +// + +// admissionStep does spec validation +func (r *PolicyReconciler) admissionStep(ctx context.Context, policy *api.Policy) (ctrl.Result, error) { + r.log.Info("handling submitted IamPolicy (checking values, setting defaults)") + + if err := policy.Validate(r.clusterName); err != nil { // the policy spec is invalid + r.log.Info("invalid spec, passing status to failed") + if err := r.updateStatus(ctx, policy, api.PolicyStatus{Condition: api.CrFailed, Reason: err.Error()}); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + // update the role to "pending" + if err := r.updateStatus(ctx, policy, api.PolicyStatus{Condition: api.CrPending, Reason: "passed validation"}); err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +// reconcilerRoutine is an infinite loop attempting to make the aws IAM policy converge to the policy.Spec +func (r *PolicyReconciler) reconcilerRoutine(ctx context.Context, policy *api.Policy) (ctrl.Result, error) { + r.log.Info("reconciler routine") + + if policy.Spec.ARN == "" { // no arn in spec, if we find it on aws : we set the spec, otherwise : we create the AWS policy + foundARN, err := r.awsPM.GetPolicyARN(policy.PathPrefix(r.clusterName), policy.AwsName(r.clusterName)) + if err != nil { + r.logExtErr(err, "failed while attempting to find policy on aws") + return ctrl.Result{Requeue: true}, nil + } + + if foundARN == "" { // no policy on aws + if err := r.awsPM.CreatePolicy(*policy); err != nil { // we create it + r.logExtErr(err, "failed to create policy on aws") + return ctrl.Result{Requeue: true}, nil + } + } else { // a policy already exists on aws + if ok := r.setPolicyArnField(ctx, foundARN, policy); !ok { // we set the policyARN field + return ctrl.Result{Requeue: true}, nil + } + } + } else { // policy arn in spec, we may have to update it on aws + policyStatement, err := r.awsPM.GetStatement(policy.Spec.ARN) + if err != nil { + return ctrl.Result{Requeue: true}, nil + } + + if !api.StatementEquals(policy.Spec.Statement, policyStatement) { // policy on aws doesn't correspond to the one in Spec + // we update the aws policy + if err := r.awsPM.UpdatePolicy(*policy); err != nil { + return ctrl.Result{Requeue: true}, nil + } + } + } + + if policy.Status.Condition != api.CrOK { + r.log.Info("passing policy status to OK") + _ = r.updateStatus(ctx, policy, api.PolicyStatus{Condition: api.CrOK}) + } + + return ctrl.Result{RequeueAfter: time.Second * 20}, nil +} + +func (r *PolicyReconciler) executeFinalizerIfPresent(policy *api.Policy) completed { + if !containsString(policy.ObjectMeta.Finalizers, r.finalizerID) { // no finalizer to execute + return true + } + + r.log.Info("executing finalizer : deleting policy on aws") + + arn, err := r.awsPM.GetPolicyARN(policy.PathPrefix(r.clusterName), policy.AwsName(r.clusterName)) + if err != nil { + r.logExtErr(err, "failed to get policy arn") + return false + } + if arn != "" { + // policy found on aws + if err := r.awsPM.DeletePolicy(arn); err != nil { + // it failed for any reason, we requeue + r.logExtErr(err, "failed to delete policy on aws") + return false + } + } + + r.log.Info("deleting policy") + // let's delete the policy itself + if err := r.Delete(context.TODO(), policy); err != nil { + if !k8serrors.IsNotFound(err) { + r.log.Error(err, "delete policy failed") + return false + } + } + + // it succeeded + // we remove our finalizer from the list and update it. + policy.ObjectMeta.Finalizers = removeString(policy.ObjectMeta.Finalizers, r.finalizerID) + if err := r.Update(context.Background(), policy); err != nil { + return false + } + + return true +} +func (r *PolicyReconciler) waitIfTesting() { + if r.TestingDelay != nil { + time.Sleep(*r.TestingDelay) + } +} + +// helper function to update a Policy status +func (r *PolicyReconciler) updateStatus(ctx context.Context, Policy *api.Policy, status api.PolicyStatus) error { + Policy.Status = status + if err := r.Status().Update(ctx, Policy); err != nil { + r.log.Error(err, "unable to update Policy status") + return err + } + + return nil +} + +func (r *PolicyReconciler) registerFinalizerIfNeeded(role *api.Policy) completed { + if !containsString(role.ObjectMeta.Finalizers, r.finalizerID) { + // the finalizer isn't registered yet + // we add it to the role. + role.ObjectMeta.Finalizers = append(role.ObjectMeta.Finalizers, r.finalizerID) + if err := r.Update(context.Background(), role); err != nil { + r.log.Error(err, "setting finalizer failed") + return false + } + } + return true +} + +func (r *PolicyReconciler) logExtErr(err error, msg string) { + r.log.Info(fmt.Sprintf("%s : %s", msg, err)) +} + +func (r *PolicyReconciler) getPolicyFromReq(ctx context.Context, req ctrl.Request) (*api.Policy, completed) { + p := &api.Policy{} + if err := r.Get(ctx, req.NamespacedName, p); err != nil { + if errors.IsNotFound(err) { + return nil, true + } + + r.log.Error(err, "get resource failed") + return nil, false + } + + return p, true +} + +func (r *PolicyReconciler) setPolicyArnField(ctx context.Context, arn string, policy *api.Policy) completed { + policy.Spec.ARN = arn + if err := r.Update(ctx, policy); err != nil { + r.log.Error(err, "failed to set policy.Spec.ARN") + return false + } + return true +} diff --git a/controllers/policy_controller_test.go b/controllers/policy_controller_test.go new file mode 100644 index 0000000..3ced81a --- /dev/null +++ b/controllers/policy_controller_test.go @@ -0,0 +1,68 @@ +package controllers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" +) + +var _ = Describe("Awspolicy validity check", func() { + Context("When creating an Awspolicy", func() { + clusterName := randString() + Context("if the spec.statement is nil", func() { + It("fails at submission", func() { + Expect( + api.NewPolicy(validName(), testns, nil).Validate(clusterName), + ).ShouldNot(Succeed()) + }) + }) + + Context("if the spec.statement is an empty array", func() { + name := validName() + It("fails at validation", func() { + Expect( + api.NewPolicy(name, testns, []api.StatementSpec{}).Validate(clusterName), + ).ShouldNot(Succeed()) + }) + }) + + Context("if the spec.statement[*].resource is not a valid ARN", func() { + name := validName() + + It("fails at validation", func() { + Expect( + api.NewPolicy(name, testns, []api.StatementSpec{ + {Resource: "not an arn", Action: []string{"do something"}}, + }).Validate(clusterName), + ).ShouldNot(Succeed()) + }) + }) + + Context("if the spec.statement[*].action is an empty array", func() { + name := validName() + validARN := "arn:aws:s3:::my_corporate_bucket/exampleobject.png" + + It("fails at validation", func() { + Expect( + api.NewPolicy(name, testns, []api.StatementSpec{ + {Resource: validARN, Action: []string{}}, + }).Validate(clusterName), + ).ShouldNot(Succeed()) + }) + }) + + Context("if everything is ok", func() { + name := validName() + validARN := "arn:aws:s3:::my_corporate_bucket/exampleobject.png" + + It("passes the api submission", func() { + Expect( + api.NewPolicy(name, testns, []api.StatementSpec{ + {Resource: validARN, Action: []string{"an:action"}}, + }).Validate(clusterName), + ).Should(Succeed()) + }) + }) + }) +}) diff --git a/controllers/role_controller.go b/controllers/role_controller.go new file mode 100644 index 0000000..b62340d --- /dev/null +++ b/controllers/role_controller.go @@ -0,0 +1,363 @@ +package controllers + +import ( + "context" + "fmt" + "time" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/api/errors" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + + api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" +) + +func NewRoleReconciler(client client.Client, scheme *runtime.Scheme, awsrm AwsRoleManager, logger logr.Logger, cN string) *RoleReconciler { + return &RoleReconciler{ + Client: client, + scheme: scheme, + awsRM: awsrm, + log: logger, + finalizerID: "role.irsa.voodoo.io", + clusterName: cN, + } +} + +// RoleReconciler reconciles a Role object +type RoleReconciler struct { + client.Client + log logr.Logger + scheme *runtime.Scheme + awsRM AwsRoleManager + finalizerID string + clusterName string + + TestingDelay *time.Duration +} + +// +kubebuilder:rbac:groups=irsa.voodoo.io,resources=roles,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=irsa.voodoo.io,resources=roles/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=irsa.voodoo.io,resources=roles/finalizers,verbs=update + +func (r *RoleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = r.log.WithValues("role", req.NamespacedName) + + { // a processing delay can be set to ensure the testing framework sees every transitionnal state + r.waitIfTesting() + } + + var role *api.Role + { // extract role from the request + var ok completed + role, ok = r.getRoleFromReq(ctx, req) + if !ok { + // didn't complete, requeing + return ctrl.Result{Requeue: true}, nil + } + if role == nil { + // not found, has been deleted + return ctrl.Result{}, nil + } + } + + { // finalizer registration & execution + if role.IsPendingDeletion() { + // deletion requested, execute finalizer + if ok := r.executeFinalizerIfPresent(role); !ok { + return ctrl.Result{Requeue: true}, nil + } + // all done, no requeue + return ctrl.Result{}, nil + } else { + if ok := r.registerFinalizerIfNeeded(role); !ok { + return ctrl.Result{Requeue: true}, nil + } + } + } + + // handlers + if role.Status.Condition == api.CrSubmitted { + return r.admissionStep(ctx, role) + } + + return r.reconcilerRoutine(ctx, role) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *RoleReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&api.Role{}). + WithOptions(controller.Options{ + MaxConcurrentReconciles: 10, + }). + Complete(r) +} + +// +// privates +// + +// admissionStep does spec validation +func (r *RoleReconciler) admissionStep(ctx context.Context, role *api.Role) (ctrl.Result, error) { + r.log.Info("admissionStep") + + if err := role.Validate(r.clusterName); err != nil { // the role spec is invalid + r.log.Info("invalid spec, passing status to failed") + if err := r.updateStatus(ctx, role, api.RoleStatus{Condition: api.CrFailed, Reason: err.Error()}); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + // update the role to "pending" + if err := r.updateStatus(ctx, role, api.RoleStatus{Condition: api.CrPending, Reason: "passed validation"}); err != nil { + return ctrl.Result{}, err + } + + r.log.Info("successfully set role status to pending") + return ctrl.Result{}, nil +} + +// reconcilerRoutine is an infinite loop attempting to make the aws IAM role, with it's attachment converge to the role.Spec +func (r *RoleReconciler) reconcilerRoutine(ctx context.Context, role *api.Role) (ctrl.Result, error) { + r.log.Info("reconciler routine") + + if role.Spec.RoleARN == "" { // no arn in spec, if we find it on aws : we set the spec, otherwise : we create the AWS role + roleExistsOnAws, err := r.awsRM.RoleExists(role.AwsName(r.clusterName)) + if err != nil { + r.logExtErr(err, "failed to check if the role exists") + return ctrl.Result{Requeue: true}, nil + } + + if roleExistsOnAws { + if ok := r.setRoleArnField(ctx, role); !ok { + return ctrl.Result{Requeue: true}, nil + } + } else { + if ok := r.createRoleOnAws(ctx, role); !ok { + return ctrl.Result{Requeue: true}, nil + } + } + } + + if role.Spec.PolicyARN == "" { // the role doesn't have the policyARN set in Spec + if ok := r.setPolicyArnFieldIfPossible(ctx, role); !ok { // we try to grab it from the policy resource and set it + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{Requeue: true}, nil + } + + if role.Spec.PolicyARN != "" { // the role already has a policyARN in Spec + if ok := r.attachPolicyToRoleIfNeeded(ctx, role); !ok { // we attach the policy with the role on aws + return ctrl.Result{Requeue: true}, nil + } + } + + if role.Status.Condition != api.CrOK { + _ = r.updateStatus(ctx, role, api.RoleStatus{Condition: api.CrOK}) + } + + return ctrl.Result{RequeueAfter: time.Second * 20}, nil +} + +func (r *RoleReconciler) setRoleArnField(ctx context.Context, role *api.Role) completed { + // we get the role details from aws + roleArn, err := r.awsRM.GetRoleARN(role.AwsName(r.clusterName)) + if err != nil { + r.logExtErr(err, "failed to get role arn on aws") + return false + } + + // set the roleArn in spec + role.Spec.RoleARN = roleArn + if err := r.Update(context.Background(), role); err != nil { + r.log.Error(err, "failed to set roleArn field in role") + return false + } + + return true +} + +func (r *RoleReconciler) createRoleOnAws(ctx context.Context, role *api.Role) completed { + if err := r.awsRM.CreateRole(*role); err != nil { + r.logExtErr(err, "failed to create role on aws") + return false + } + return true +} + +func (r *RoleReconciler) attachPolicyToRoleIfNeeded(ctx context.Context, role *api.Role) completed { + awsRoleName := role.AwsName(r.clusterName) + roleAlreadyCreatedOnAws, err := r.awsRM.RoleExists(awsRoleName) + if err != nil { + r.logExtErr(err, "failed to check if the role exists") + return false + } + + if !roleAlreadyCreatedOnAws { + return false + } + + // maybe the policy is already attached to it ? + policiesARNs, err := r.awsRM.GetAttachedRolePoliciesARNs(awsRoleName) + if err != nil { + r.logExtErr(err, "failed to retrieve attached role policies") + return false + } + + for _, pARN := range policiesARNs { // iterate over found policies + if pARN == role.Spec.PolicyARN { + return true + } + } + + // the policy is not attached yet + if err := r.awsRM.AttachRolePolicy(awsRoleName, role.Spec.PolicyARN); err != nil { // we attach the policy + r.logExtErr(err, "failed to attach policy to role") + return false + } + + r.log.Info("attached policy to role") + return true +} + +func (r *RoleReconciler) setPolicyArnFieldIfPossible(ctx context.Context, role *api.Role) completed { + r.log.Info("setPolicyArnFieldIfPossible") + + // we'll try to get it from the policy resource + policy, ok := r.getPolicy(ctx, role.Name, role.Namespace) + if !ok || policy == nil { + // not found + return false + } + + // if its arn field is not set + if policy.Spec.ARN == "" { + return false + } + + role.Spec.PolicyARN = policy.Spec.ARN + if err := r.Update(ctx, role); err != nil { + r.log.Error(err, "failed to set policyARN in role spec") + return false + } + + return true +} + +func (r *RoleReconciler) registerFinalizerIfNeeded(role *api.Role) completed { + if !containsString(role.ObjectMeta.Finalizers, r.finalizerID) { + // the finalizer isn't registered yet + // we add it to the role. + role.ObjectMeta.Finalizers = append(role.ObjectMeta.Finalizers, r.finalizerID) + if err := r.Update(context.Background(), role); err != nil { + r.log.Error(err, "setting finalizer failed") + return false + } + } + return true +} + +func (r *RoleReconciler) executeFinalizerIfPresent(role *api.Role) completed { + if !containsString(role.ObjectMeta.Finalizers, r.finalizerID) { + // no finalizer to execute + return true + } + r.log.Info("executing finalizer : deleting role on aws") + + // if some policies are attached to the role + // we'll wait till they're detached + waitForPolicies := true + for waitForPolicies { + attachedPolicies, err := r.awsRM.GetAttachedRolePoliciesARNs(role.AwsName(r.clusterName)) + if err != nil { + r.logExtErr(err, "failed to list attached policies") + return false + } + + // we found some policies attached + // we loop + if len(attachedPolicies) > 0 { + r.log.Info(fmt.Sprintf("%d policies still attached, waiting for them to be detached", len(attachedPolicies))) + time.Sleep(time.Second * 5) + } else { + // no policy attach, we exit the loop + waitForPolicies = false + } + } + + // we delete the role + if err := r.awsRM.DeleteRole(role.AwsName(r.clusterName)); err != nil { + r.logExtErr(err, "failed delete the role") + return false + } + + r.log.Info("deleting role") + // let's delete the role itself + if err := r.Delete(context.TODO(), role); err != nil { + if !k8serrors.IsNotFound(err) { + r.log.Error(err, "role resource failed") + return false + } + } + + // it succeeded + // we remove our finalizer from the list and update it. + role.ObjectMeta.Finalizers = removeString(role.ObjectMeta.Finalizers, r.finalizerID) + if err := r.Update(context.Background(), role); err != nil { + r.log.Error(err, "failed to remove the finalizer") + return false + } + + return true +} + +func (r *RoleReconciler) getPolicy(ctx context.Context, name, ns string) (*api.Policy, completed) { + policy := &api.Policy{} + if err := r.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, policy); err != nil { + if errors.IsNotFound(err) { + return nil, true + } + + r.log.Error(err, "get policy failed") + return nil, false + } + + return policy, true +} + +func (r *RoleReconciler) getRoleFromReq(ctx context.Context, req ctrl.Request) (*api.Role, completed) { + role := &api.Role{} + if err := r.Get(ctx, req.NamespacedName, role); err != nil { + if errors.IsNotFound(err) { + return nil, true + } + + r.log.Error(err, "get resource failed") + return nil, false + } + + return role, true +} + +func (r *RoleReconciler) waitIfTesting() { + if r.TestingDelay != nil { + time.Sleep(*r.TestingDelay) + } +} + +// helper function to update a Policy status +func (r *RoleReconciler) updateStatus(ctx context.Context, role *api.Role, status api.RoleStatus) error { + role.Status = status + if err := r.Status().Update(ctx, role); err != nil { + r.log.Error(err, "failed to update Policy status") + return err + } + + return nil +} diff --git a/controllers/shared_test.go b/controllers/shared_test.go new file mode 100644 index 0000000..81c473e --- /dev/null +++ b/controllers/shared_test.go @@ -0,0 +1,116 @@ +package controllers_test + +import ( + "context" + "fmt" + "log" + "math/rand" + "time" + + api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +const ( + testns = "default" + resourcePollTimeout = time.Second * 50 + resourcePollInterval = time.Millisecond * 500 +) + +// generates a 20 letters string (~5.0e-29 collision probability) +func randString() string { + letterBytes := "abcdefghijklmnopqrstuvwxyz" // must be valid DNS + b := make([]byte, 20) + for i := range b { + b[i] = letterBytes[rand.Intn(len(letterBytes))] + } + return string(b) +} + +// validName is just a more user-friendly name +var validName = randString + +// ObjTester is used to find a k8s resource with a given Status +type ObjTester interface { + client.Object + HasStatus(st fmt.Stringer) bool +} + +// createResource is used to create any k8s resource and expect a result afterwards +func createResource(obj client.Object) GomegaAsyncAssertion { + return Expect(k8sClient.Create(context.Background(), obj)) +} + +func find(name, ns string, status fmt.Stringer, obj ObjTester) GomegaAsyncAssertion { + return Eventually(func() bool { + err := k8sClient.Get( + context.Background(), + types.NamespacedName{Name: name, Namespace: ns}, + obj, + ) + if err != nil { + return false + } + + return obj.HasStatus(status) + }, resourcePollTimeout, resourcePollInterval) +} + +func findSa(name, ns string) GomegaAsyncAssertion { + return Eventually(func() bool { + err := k8sClient.Get( + context.Background(), + types.NamespacedName{Name: name, Namespace: ns}, + &corev1.ServiceAccount{}, + ) + return err == nil + }, resourcePollTimeout, resourcePollInterval) +} + +func foundIrsaInCondition(name, ns string, cond api.IrsaCondition) GomegaAsyncAssertion { + return find(name, ns, cond, &api.IamRoleServiceAccount{}) +} + +func foundPolicyInCondition(name, ns string, cond api.CrCondition) GomegaAsyncAssertion { + obj := &api.Policy{} + return find(name, ns, cond, obj) +} + +func foundRoleInCondition(name, ns string, cond api.CrCondition) GomegaAsyncAssertion { + obj := &api.Role{} + return find(name, ns, cond, obj) +} + +func getRole(name, ns string) api.Role { + obj := &api.Role{} + getOnK8s(name, ns, obj) + return *obj +} + +func getIrsa(name, ns string) api.IamRoleServiceAccount { + obj := &api.IamRoleServiceAccount{} + getOnK8s(name, ns, obj) + return *obj +} +func getPolicy(name, ns string) api.Policy { + obj := &api.Policy{} + getOnK8s(name, ns, obj) + return *obj +} + +func getOnK8s(name, ns string, o client.Object) { + if err := k8sClient.Get( + context.Background(), + types.NamespacedName{Name: name, Namespace: ns}, + o, + ); err != nil { + log.Fatal("failed to get object") + } +} diff --git a/controllers/suite_test.go b/controllers/suite_test.go new file mode 100644 index 0000000..f0005a4 --- /dev/null +++ b/controllers/suite_test.go @@ -0,0 +1,118 @@ +package controllers_test + +import ( + "log" + "path/filepath" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/controller-runtime/pkg/envtest/printer" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + irsav1alpha1 "github.com/VoodooTeam/irsa-operator/api/v1alpha1" + irsaCtrl "github.com/VoodooTeam/irsa-operator/controllers" + // +kubebuilder:scaffold:imports +) + +var k8sClient client.Client +var testEnv *envtest.Environment +var st *awsFake + +func CustomFail(message string, callerSkip ...int) { + log.Println(message) + panic(GINKGO_PANIC) +} + +func TestAPIs(t *testing.T) { + RegisterFailHandler(CustomFail) + + RunSpecsWithDefaultAndCustomReporters(t, + "Controller Suite", + []Reporter{printer.NewlineReporter{}}) +} + +type fakeWriter struct{} + +func (fakeWriter) Write(b []byte) (int, error) { + return 0, nil +} + +var _ = BeforeSuite(func() { + // we disable the standard logging + logf.SetLogger(zap.New(zap.WriteTo(fakeWriter{}))) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, + } + + // start the envtest cluster + cfg, err := testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + // import the irsa scheme + err = irsav1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + }) + Expect(err).ToNot(HaveOccurred()) + + // reconcilers + // irsa reconcilier + iR := irsaCtrl.NewIrsaReconciler( + k8sManager.GetClient(), + scheme.Scheme, + ctrl.Log.WithName("controllers").WithName("irsa"), + ) + err = iR.SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + // policy reconcilier + clusterName := "clustername" + st = newAwsFake() + pR := irsaCtrl.NewPolicyReconciler( + k8sManager.GetClient(), + scheme.Scheme, + st, + ctrl.Log.WithName("controllers").WithName("policy"), + clusterName, + ) + + err = pR.SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + // start role reconcilier + rR := irsaCtrl.NewRoleReconciler( + k8sManager.GetClient(), + scheme.Scheme, + st, + ctrl.Log.WithName("controllers").WithName("role"), + clusterName, + ) + err = rR.SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + go func() { + err = k8sManager.Start(ctrl.SetupSignalHandler()) + Expect(err).ToNot(HaveOccurred()) + }() + + k8sClient = k8sManager.GetClient() + Expect(k8sClient).ToNot(BeNil()) + +}, 60) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/cr.yml b/cr.yml new file mode 100644 index 0000000..4bf8576 --- /dev/null +++ b/cr.yml @@ -0,0 +1 @@ +release-name-template: "helm-v{{ .Version }}" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d29ff16 --- /dev/null +++ b/go.mod @@ -0,0 +1,24 @@ +module github.com/VoodooTeam/irsa-operator + +go 1.15 + +require ( + github.com/aws/aws-sdk-go v1.36.19 + github.com/cenkalti/backoff/v3 v3.2.2 // indirect + github.com/containerd/continuity v0.0.0-20200228182428-0f16d7a0959c // indirect + github.com/davecgh/go-spew v1.1.1 + github.com/go-logr/logr v0.3.0 + github.com/go-logr/stdr v0.3.0 + github.com/google/go-cmp v0.5.4 // indirect + github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/onsi/ginkgo v1.14.1 + github.com/onsi/gomega v1.10.3 + github.com/ory/dockertest/v3 v3.6.2 + github.com/stretchr/testify v1.6.1 // indirect + golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6 // indirect + gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect + k8s.io/api v0.19.2 + k8s.io/apimachinery v0.19.2 + k8s.io/client-go v0.19.2 + sigs.k8s.io/controller-runtime v0.7.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..08eaab6 --- /dev/null +++ b/go.sum @@ -0,0 +1,719 @@ +bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.51.0 h1:PvKAVQWCtlGUSlZkGW3QLelKaWq7KYv/MW1EboG8bfM= +cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest v0.9.6 h1:5YWtOnckcudzIw8lPPBcWOnmIFWMtHci1ZWAZulMSx0= +github.com/Azure/go-autorest/autorest v0.9.6/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/adal v0.8.2 h1:O1X4oexUxnZCaEUGsvMnr8ZGj8HI37tNezwY4npRqA0= +github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/date v0.2.0 h1:yW+Zlqf26583pE43KhfnhFcdmSWlm5Ew6bxipnr/tbM= +github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.3.0 h1:qJumjCaCudz+OcqE9/XtEPfvtOjOmKaui4EOpFI6zZc= +github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= +github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-sdk-go v1.36.19 h1:zbJZKkxeDiYxUYFjymjWxPye+qa1G2gRVyhIzZrB9zA= +github.com/aws/aws-sdk-go v1.36.19/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= +github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= +github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20200228182428-0f16d7a0959c h1:8ahmSVELW1wghbjerVAyuEYD5+Dio66RYvSS0iGfL1M= +github.com/containerd/continuity v0.0.0-20200228182428-0f16d7a0959c/go.mod h1:Dq467ZllaHgAtVp4p1xUQWBrFXR9s/wyoTpG8zOJGkY= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= +github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v0.3.0 h1:q4c+kbcR0d5rSurhBR8dIgieOaYpXtsdTYfx22Cu6rs= +github.com/go-logr/logr v0.3.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/stdr v0.3.0 h1:GzFt/sOHlPMqsh46UTAaIDWPFq3DdNGcF4rwMjhTBVo= +github.com/go-logr/stdr v0.3.0/go.mod h1:NO1vneyJDqKVgJYnxhwXWWmQPOvNM391IG3H8ql3jiA= +github.com/go-logr/zapr v0.2.0 h1:v6Ji8yBW77pva6NkJKQdHLAJKrIJKRHz0RXwPqCHSR4= +github.com/go-logr/zapr v0.2.0/go.mod h1:qhKdvif7YF5GI9NWEpyxTSSBdGmzkNguibrdCNVPunU= +github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7 h1:5ZkaAPbicIKTF2I64qf5Fh8Aa83Q/dnOafMYV0OMwjA= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/googleapis/gnostic v0.5.1 h1:A8Yhf6EtqTv9RMsU6MQTyrtV1TjWlR6xU9BsZIwuTCM= +github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.10 h1:6q5mVkdH/vYmqngx7kZQTjJ5HRsx+ImorDIEQ+beJgc= +github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2 h1:hRGSmZu7j271trc9sneMrpOW7GN5ngLm8YUZIPzf394= +github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= +github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2 h1:SPoLlS9qUUnXcIY4pvA4CTwYjk0Is5f4UPEkeESr53k= +github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2/go.mod h1:TjQg8pa4iejrUrjiz0MCtMV38jdMNW4doKSiBrEvCQQ= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4= +github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs= +github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v1.0.0-rc9 h1:/k06BMULKF5hidyoZymkoDCzdJzltZpz/UU4LguQVtc= +github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/ory/dockertest/v3 v3.6.2 h1:Q3Y8naCMyC1Nw91BHum1bGyEsNQc/UOIYS3ZoPoou0g= +github.com/ory/dockertest/v3 v3.6.2/go.mod h1:EFLcVUOl8qCwp9NyDAcCDtq/QviLtYswW/VbWzUnTNE= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/etcd v0.5.0-alpha.5.0.20200819165624-17cef6e3e9d5/go.mod h1:skWido08r9w6Lq/w70DO5XYIKMu4QFu1+4VsqLQuJy8= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.8.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM= +go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= +golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 h1:5/PjkGUjvEU5Gl6BxmvKRPpqo2uNMv4rcHBMwzk/st8= +golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200616133436-c1934b75d054 h1:HHeAlu5H9b71C+Fx0K+1dGgVFN1DM1/wz4aoGOA5qS8= +golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6 h1:rbvTkL9AkFts1cgI78+gG6Yu1pwaqX6hjSJAatB78E4= +golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.1.0 h1:Phva6wqu+xR//Njw6iorylFFgn/z547tw5Ne3HZPQ+k= +gomodules.xyz/jsonpatch/v2 v2.1.0/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +k8s.io/api v0.19.2 h1:q+/krnHWKsL7OBZg/rxnycsl9569Pud76UJ77MvKXms= +k8s.io/api v0.19.2/go.mod h1:IQpK0zFQ1xc5iNIQPqzgoOwuFugaYHK4iCknlAQP9nI= +k8s.io/apiextensions-apiserver v0.19.2 h1:oG84UwiDsVDu7dlsGQs5GySmQHCzMhknfhFExJMz9tA= +k8s.io/apiextensions-apiserver v0.19.2/go.mod h1:EYNjpqIAvNZe+svXVx9j4uBaVhTB4C94HkY3w058qcg= +k8s.io/apimachinery v0.19.2 h1:5Gy9vQpAGTKHPVOh5c4plE274X8D/6cuEiTO2zve7tc= +k8s.io/apimachinery v0.19.2/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA= +k8s.io/apiserver v0.19.2/go.mod h1:FreAq0bJ2vtZFj9Ago/X0oNGC51GfubKK/ViOKfVAOA= +k8s.io/client-go v0.19.2 h1:gMJuU3xJZs86L1oQ99R4EViAADUPMHHtS9jFshasHSc= +k8s.io/client-go v0.19.2/go.mod h1:S5wPhCqyDNAlzM9CnEdgTGV4OqhsW3jGO1UM1epwfJA= +k8s.io/code-generator v0.19.2/go.mod h1:moqLn7w0t9cMs4+5CQyxnfA/HV8MF6aAVENF+WZZhgk= +k8s.io/component-base v0.19.2 h1:jW5Y9RcZTb79liEhW3XDVTW7MuvEGP0tQZnfSX6/+gs= +k8s.io/component-base v0.19.2/go.mod h1:g5LrsiTiabMLZ40AR6Hl45f088DevyGY+cCE2agEIVo= +k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.2.0 h1:XRvcwJozkgZ1UQJmfMGpvRthQHOvihEhYtDfAaxMz/A= +k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6 h1:+WnxoVtG8TMiudHBSEtrVL1egv36TkkJm+bA8AxicmQ= +k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= +k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20200912215256-4140de9c8800 h1:9ZNvfPvVIEsp/T1ez4GQuzCcCTEQWhovSofhqR73A6g= +k8s.io/utils v0.0.0-20200912215256-4140de9c8800/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.9/go.mod h1:dzAXnQbTRyDlZPJX2SUPEqvnB+j7AJjtlox7PEwigU0= +sigs.k8s.io/controller-runtime v0.7.0 h1:bU20IBBEPccWz5+zXpLnpVsgBYxqclaHu1pVDl/gEt8= +sigs.k8s.io/controller-runtime v0.7.0/go.mod h1:pJ3YBrJiAqMAZKi6UVGuE98ZrroV1p+pIhoHsMm9wdU= +sigs.k8s.io/structured-merge-diff/v4 v4.0.1 h1:YXTMot5Qz/X1iBRJhAt+vI+HVttY0WkSqqhKxQ0xVbA= +sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt new file mode 100644 index 0000000..606681a --- /dev/null +++ b/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright 2020. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..3d5bde5 --- /dev/null +++ b/main.go @@ -0,0 +1,172 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "log" + "net/http" + "os" + + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + _ "k8s.io/client-go/plugin/pkg/client/auth" + + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + irsav1alpha1 "github.com/VoodooTeam/irsa-operator/api/v1alpha1" + irsaws "github.com/VoodooTeam/irsa-operator/aws" + "github.com/VoodooTeam/irsa-operator/controllers" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/endpoints" + "github.com/aws/aws-sdk-go/aws/session" + // +kubebuilder:scaffold:imports +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(irsav1alpha1.AddToScheme(scheme)) + // +kubebuilder:scaffold:scheme +} + +func main() { + var metricsAddr string + var enableLeaderElection bool + var probeAddr string + var clusterName string + var oidcProviderARN string + + flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") + flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.BoolVar(&enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + + flag.StringVar(&clusterName, "cluster-name", "", "The cluster name, used to avoid name collisions on aws, set this to the name of the eks cluster") + flag.StringVar(&oidcProviderARN, "oidc-provider-arn", "", "The ARN of the oidc provider to use.") + + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + if clusterName == "" { + setupLog.Error(errors.New("cluster-name not provided"), "unable to start manager") + os.Exit(1) + } + if oidcProviderARN == "" { + setupLog.Error(errors.New("oidc-provider-url not provided"), "unable to start manager") + os.Exit(1) + } + setupLog.Info(fmt.Sprintf("cluster name is : %s", clusterName)) + setupLog.Info(fmt.Sprintf("oidc provider arn is : %s", oidcProviderARN)) + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + MetricsBindAddress: metricsAddr, + Port: 9443, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "d8e70b98.voodoo.io", + Namespace: "", // empty string means "watch resources on all namespaces" + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + if err = controllers.NewIrsaReconciler( + mgr.GetClient(), + mgr.GetScheme(), + ctrl.Log.WithName("controllers").WithName("IamRoleServiceAccount"), + ).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "IamRoleServiceAccount") + os.Exit(1) + } + + awsCfg := getAwsConfig() + + if err = controllers.NewPolicyReconciler( + mgr.GetClient(), + mgr.GetScheme(), + irsaws.NewAwsManager( + awsCfg, + ctrl.Log.WithName("aws").WithName("Policy"), clusterName, + oidcProviderARN, + ), + ctrl.Log.WithName("controllers").WithName("Policy"), + clusterName, + ).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Policy") + os.Exit(1) + } + + if err = controllers.NewRoleReconciler( + mgr.GetClient(), + mgr.GetScheme(), + irsaws.NewAwsManager(awsCfg, ctrl.Log.WithName("controllers").WithName("Aws"), clusterName, oidcProviderARN), + ctrl.Log.WithName("controllers").WithName("Role"), + clusterName, + ).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Role") + os.Exit(1) + } + + // +kubebuilder:scaffold:builder + + if err := mgr.AddHealthzCheck("health", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("check", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} + +func getAwsConfig() *session.Session { + // todo, outdated + islocalTesting := false + if os.Getenv("LOCAL_TEST") != "" { + islocalTesting = true + } + + if islocalTesting { + log.Println("using localstack, will try to connect at 172.17.0.1:4566") + + localStackEndpoint := "http://172.17.0.1:4566" + _, err := http.Get(localStackEndpoint) + if err != nil { + panic(err) + } + + return session.Must(session.NewSession(&aws.Config{ + Credentials: credentials.NewStaticCredentials("test", "test", ""), + DisableSSL: aws.Bool(true), + Region: aws.String(endpoints.UsWest1RegionID), + Endpoint: aws.String(localStackEndpoint), + })) + } + + return session.Must(session.NewSession()) +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..56825e6 --- /dev/null +++ b/shell.nix @@ -0,0 +1,46 @@ +let + stable = import (builtins.fetchTarball { + name = "nixos-20.09"; + url = "https://github.com/NixOS/nixpkgs/archive/20.09.tar.gz"; + sha256 = "1wg61h4gndm3vcprdcg7rc4s1v3jkm5xd7lw8r2f67w502y94gcy"; + }) {}; + + nightly = import (builtins.fetchTarball { + name = "nixos-nightly-2020-12-02"; + url = "https://github.com/NixOS/nixpkgs/archive/b6bca3d80619f1565ba0ea635b0d38234e41c6bd.tar.gz"; + sha256 = "09d4f6h98rmxnxzm1x07jxgrc81k6mz7fjigq375fkmb41j2kdsi"; + }) {}; + + voodoo = import (builtins.fetchGit { + url = "git@github.com:VoodooTeam/nix-pkgs.git"; + ref = "master"; + }) stable; + in + + stable.mkShell { + buildInputs = + [ + # go vim + stable.go + nightly.gopls + nightly.asmfmt + nightly.errcheck + + # operator-sdk cli + voodoo.operator-sdk_1_3_0 + + voodoo.helm_3_4_2 + + # only for local testing + stable.docker-compose + voodoo.kind_0_9_0 + stable.awscli2 + stable.openssl + stable.curl + stable.jq + stable.gnumake + stable.cfssl + ]; + } + + diff --git a/testbin/setup-envtest.sh b/testbin/setup-envtest.sh new file mode 100644 index 0000000..d099c4b --- /dev/null +++ b/testbin/setup-envtest.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +# Copyright 2020 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o pipefail + +# Turn colors in this script off by setting the NO_COLOR variable in your +# environment to any value: +# +# $ NO_COLOR=1 test.sh +NO_COLOR=${NO_COLOR:-""} +if [ -z "$NO_COLOR" ]; then + header=$'\e[1;33m' + reset=$'\e[0m' +else + header='' + reset='' +fi + +function header_text { + echo "$header$*$reset" +} + +function setup_envtest_env { + header_text "setting up env vars" + + # Setup env vars + KUBEBUILDER_ASSETS=${KUBEBUILDER_ASSETS:-""} + if [[ -z "${KUBEBUILDER_ASSETS}" ]]; then + export KUBEBUILDER_ASSETS=$1/bin + fi +} + +# fetch k8s API gen tools and make it available under envtest_root_dir/bin. +# +# Skip fetching and untaring the tools by setting the SKIP_FETCH_TOOLS variable +# in your environment to any value: +# +# $ SKIP_FETCH_TOOLS=1 ./check-everything.sh +# +# If you skip fetching tools, this script will use the tools already on your +# machine. +function fetch_envtest_tools { + SKIP_FETCH_TOOLS=${SKIP_FETCH_TOOLS:-""} + if [ -n "$SKIP_FETCH_TOOLS" ]; then + return 0 + fi + + tmp_root=/tmp + envtest_root_dir=$tmp_root/envtest + + k8s_version="${ENVTEST_K8S_VERSION:-1.19.2}" + goarch="$(go env GOARCH)" + goos="$(go env GOOS)" + + if [[ "$goos" != "linux" && "$goos" != "darwin" ]]; then + echo "OS '$goos' not supported. Aborting." >&2 + return 1 + fi + + local dest_dir="${1}" + + # use the pre-existing version in the temporary folder if it matches our k8s version + if [[ -x "${dest_dir}/bin/kube-apiserver" ]]; then + version=$("${dest_dir}"/bin/kube-apiserver --version) + if [[ $version == *"${k8s_version}"* ]]; then + header_text "Using cached envtest tools from ${dest_dir}" + return 0 + fi + fi + + header_text "fetching envtest tools@${k8s_version} (into '${dest_dir}')" + envtest_tools_archive_name="kubebuilder-tools-$k8s_version-$goos-$goarch.tar.gz" + envtest_tools_download_url="https://storage.googleapis.com/kubebuilder-tools/$envtest_tools_archive_name" + + envtest_tools_archive_path="$tmp_root/$envtest_tools_archive_name" + if [ ! -f $envtest_tools_archive_path ]; then + curl -sL ${envtest_tools_download_url} -o "$envtest_tools_archive_path" + fi + + mkdir -p "${dest_dir}" + tar -C "${dest_dir}" --strip-components=1 -zvxf "$envtest_tools_archive_path" +}