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"
+}