├── .dockerignore ├── .github └── workflows │ ├── chart-release.yml │ ├── ci.yml │ └── publish-docker.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── _dev-env ├── .gitignore ├── Makefile ├── README.md ├── docker-compose.yml ├── kind-config.yml ├── oidc-provider │ ├── csr.conf │ └── hydra.yml ├── test-irsa.yml └── webhook │ ├── csr.conf │ ├── csr.yml │ ├── deploy.sh │ ├── manifests │ ├── auth.yml │ ├── deploy.yml │ └── svc.yml │ ├── mutatingwebhook.tmpl │ └── webhook-tester.yml ├── _doc ├── architecture-diagram ├── architecture-diagram.png ├── example │ ├── k8s │ │ ├── Chart.yaml │ │ ├── templates │ │ │ └── deploy.yml │ │ └── values.yaml │ └── terraform │ │ ├── .gitignore │ │ ├── .terraform.lock.hcl │ │ ├── README.md │ │ ├── main.tf │ │ ├── output.tf │ │ ├── shell.nix │ │ └── variables.tf └── model │ ├── .gitignore │ ├── IrsaOperator.cfg │ ├── IrsaOperator.pdf │ ├── IrsaOperator.tla │ ├── shell.nix │ ├── tlapdf │ └── tlaplus.nix ├── _helm └── chart │ ├── Chart.yaml │ ├── crds │ ├── irsa.voodoo.io_iamroleserviceaccounts.yaml │ ├── irsa.voodoo.io_policies.yaml │ └── irsa.voodoo.io_roles.yaml │ ├── templates │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── rolebindings.yaml │ ├── roles.yaml │ ├── service.yaml │ └── serviceaccount.yaml │ └── values.yaml ├── api └── v1alpha1 │ ├── groupversion_info.go │ ├── iamroleserviceaccount_types.go │ ├── policy_types.go │ ├── role_types.go │ ├── shared_types.go │ └── zz_generated.deepcopy.go ├── aws ├── aws.go ├── aws_test.go ├── suite_test.go ├── types.go └── types_test.go ├── config ├── certmanager │ ├── certificate.yaml │ ├── kustomization.yaml │ └── kustomizeconfig.yaml ├── crd │ ├── bases │ │ ├── irsa.voodoo.io_iamroleserviceaccounts.yaml │ │ ├── irsa.voodoo.io_policies.yaml │ │ └── irsa.voodoo.io_roles.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_iamroleserviceaccounts.yaml │ │ ├── cainjection_in_policies.yaml │ │ ├── cainjection_in_roles.yaml │ │ ├── webhook_in_iamroleserviceaccounts.yaml │ │ ├── webhook_in_policies.yaml │ │ └── webhook_in_roles.yaml ├── default │ ├── kustomization.yaml │ ├── manager_auth_proxy_patch.yaml │ └── manager_config_patch.yaml ├── manager │ ├── controller_manager_config.yaml │ ├── kustomization.yaml │ └── manager.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── rbac │ ├── auth_proxy_client_clusterrole.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_role_binding.yaml │ ├── auth_proxy_service.yaml │ ├── iamroleserviceaccount_editor_role.yaml │ ├── iamroleserviceaccount_viewer_role.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── policy_editor_role.yaml │ ├── policy_viewer_role.yaml │ ├── role.yaml │ ├── role_binding.yaml │ ├── role_editor_role.yaml │ └── role_viewer_role.yaml ├── samples │ ├── irsa_v1alpha1_iamroleserviceaccount.yaml │ ├── irsa_v1alpha1_policy.yaml │ ├── irsa_v1alpha1_role.yaml │ ├── kustomization.yaml │ └── test_deploy.yaml └── scorecard │ ├── bases │ └── config.yaml │ ├── kustomization.yaml │ └── patches │ ├── basic.config.yaml │ └── olm.config.yaml ├── controllers ├── README.md ├── aws.go ├── aws_test.go ├── helper.go ├── iamroleserviceaccount_controller.go ├── iamroleserviceaccount_controller_test.go ├── model_test.go ├── policy_controller.go ├── policy_controller_test.go ├── role_controller.go ├── shared_test.go └── suite_test.go ├── cr.yml ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt ├── main.go ├── shell.nix └── testbin └── setup-envtest.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore all files which are not go type 3 | !**/*.go 4 | !./testbin/ 5 | !**/*.mod 6 | !**/*.sum 7 | _*/** 8 | -------------------------------------------------------------------------------- /.github/workflows/chart-release.yml: -------------------------------------------------------------------------------- 1 | name: Release Charts 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "_helm/chart/Chart.yaml" 9 | 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Fetch history 17 | run: git fetch --prune --unshallow 18 | 19 | - name: Configure Git 20 | run: | 21 | git config user.name "$GITHUB_ACTOR" 22 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 23 | 24 | - name: Install Helm 25 | uses: azure/setup-helm@v1 26 | with: 27 | version: v3.4.0 28 | 29 | - name: Run chart-releaser 30 | uses: helm/chart-releaser-action@v1.2.0 31 | with: 32 | charts_dir: _helm 33 | config: cr.yml 34 | env: 35 | CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths-ignore: 7 | - '_helm/**' 8 | pull_request: 9 | branches: [ main ] 10 | paths-ignore: 11 | - '_helm/**' 12 | 13 | jobs: 14 | 15 | sanity-checks: 16 | name: Sanity checks 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Set up Go 1.15 22 | uses: actions/setup-go@v2 23 | with: 24 | go-version: 1.15 25 | id: go 26 | 27 | - name: Format 28 | run: if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then exit 1; fi 29 | 30 | - name: Lint 31 | uses: golangci/golangci-lint-action@v2 32 | with: 33 | version: v1.29 34 | args: --timeout=5m 35 | 36 | test: 37 | name: Tests 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v2 41 | 42 | - name: Set up Go 1.15 43 | uses: actions/setup-go@v2 44 | with: 45 | go-version: 1.15 46 | id: go 47 | 48 | - name: Generate manifests 49 | run: make generate 50 | 51 | - name: Test 52 | env: 53 | LOCALSTACK_ENDPOINT: ${{ job.services.localstack.ports[4566] }} 54 | shell: bash 55 | run: | 56 | export ENVTEST_ASSETS_DIR=${GITHUB_WORKSPACE}/testbin 57 | source ${ENVTEST_ASSETS_DIR}/setup-envtest.sh 58 | fetch_envtest_tools ${ENVTEST_ASSETS_DIR} 59 | setup_envtest_env ${ENVTEST_ASSETS_DIR} 60 | go test -v ./... -coverprofile cover.out 61 | 62 | services: 63 | localstack: 64 | image: localstack/localstack:0.12.4 65 | env: 66 | SERVICES: 'iam,s3,sts' 67 | -------------------------------------------------------------------------------- /.github/workflows/publish-docker.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | push_to_registry: 9 | name: Push Docker image to GitHub Packages 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out the repo 13 | uses: actions/checkout@v2 14 | - name: Push to GitHub Packages 15 | uses: docker/build-push-action@v1 16 | with: 17 | username: ${{ github.actor }} 18 | password: ${{ secrets.GHCR }} 19 | registry: ghcr.io 20 | repository: voodooteam/irsa-operator 21 | tag_with_ref: true 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | irsa-operator 2 | 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | bin 10 | testbin/bin/* 11 | 12 | # Test binary, build with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Kubernetes Generated files - skip generated files, except for vendored files 19 | 20 | !vendor/**/zz_generated.* 21 | 22 | # editor and IDE paraphernalia 23 | .idea 24 | *.swp 25 | *.swo 26 | *~ 27 | 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.15 as builder 2 | 3 | WORKDIR /workspace 4 | COPY go.mod go.mod 5 | COPY go.sum go.sum 6 | RUN go mod download 7 | 8 | COPY main.go main.go 9 | COPY api/ api/ 10 | COPY controllers/ controllers/ 11 | COPY aws/ aws/ 12 | 13 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o manager main.go 14 | 15 | # Use distroless as minimal base image to package the manager binary 16 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 17 | FROM gcr.io/distroless/static:nonroot 18 | WORKDIR / 19 | COPY --from=builder /workspace/manager . 20 | USER 65532:65532 21 | 22 | ENTRYPOINT ["/manager"] 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2021 Voodoo.io 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Current Operator version 2 | VERSION ?= 0.0.1 3 | # Default bundle image tag 4 | BUNDLE_IMG ?= controller-bundle:$(VERSION) 5 | # Options for 'bundle-build' 6 | ifneq ($(origin CHANNELS), undefined) 7 | BUNDLE_CHANNELS := --channels=$(CHANNELS) 8 | endif 9 | ifneq ($(origin DEFAULT_CHANNEL), undefined) 10 | BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL) 11 | endif 12 | BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL) 13 | 14 | # Image URL to use all building/pushing image targets 15 | IMG ?= controller:latest 16 | # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) 17 | CRD_OPTIONS ?= "crd:trivialVersions=true,preserveUnknownFields=false" 18 | 19 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 20 | ifeq (,$(shell go env GOBIN)) 21 | GOBIN=$(shell go env GOPATH)/bin 22 | else 23 | GOBIN=$(shell go env GOBIN) 24 | endif 25 | 26 | all: manager 27 | 28 | # Run tests 29 | ENVTEST_ASSETS_DIR=$(shell pwd)/testbin 30 | test: generate manifests 31 | mkdir -p ${ENVTEST_ASSETS_DIR} 32 | 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 33 | 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 34 | 35 | testmodel: generate manifests 36 | mkdir -p ${ENVTEST_ASSETS_DIR} 37 | 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 38 | 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 39 | 40 | # Build manager binary 41 | manager: generate fmt vet 42 | go build -o bin/manager main.go 43 | 44 | # Run against the configured Kubernetes cluster in ~/.kube/config 45 | run: generate fmt vet manifests 46 | go run ./main.go 47 | 48 | # Install CRDs into a cluster 49 | install: manifests kustomize 50 | $(KUSTOMIZE) build config/crd | kubectl apply -f - 51 | 52 | # Uninstall CRDs from a cluster 53 | uninstall: manifests kustomize 54 | $(KUSTOMIZE) build config/crd | kubectl delete -f - 55 | 56 | # Deploy controller in the configured Kubernetes cluster in ~/.kube/config 57 | deploy: manifests kustomize 58 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 59 | $(KUSTOMIZE) build config/default | kubectl apply -f - 60 | 61 | #gen-helm: manifests kustomize 62 | # $(KUSTOMIZE) build config/default > config/helm/irsa/templates/irsa-operator.yml 63 | 64 | # UnDeploy controller from the configured Kubernetes cluster in ~/.kube/config 65 | undeploy: 66 | $(KUSTOMIZE) build config/default | kubectl delete -f - 67 | 68 | # Generate manifests e.g. CRD, RBAC etc. 69 | manifests: controller-gen 70 | $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases 71 | rm ./_helm/chart/crds/* 72 | cp config/crd/bases/* ./_helm/chart/crds/ 73 | 74 | # Run go fmt against code 75 | fmt: 76 | go fmt ./... 77 | 78 | # Run go vet against code 79 | vet: 80 | go vet ./... 81 | 82 | # Run golangci-lint against code 83 | lint: 84 | golangci-lint run 85 | 86 | # Generate code 87 | generate: controller-gen 88 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 89 | 90 | # Build the docker image 91 | docker-build: test 92 | DOCKER_BUILDKIT=1 docker build -t ${IMG} . 93 | 94 | # Push the docker image 95 | docker-push: 96 | docker push ${IMG} 97 | 98 | # Download controller-gen locally if necessary 99 | CONTROLLER_GEN = $(shell pwd)/bin/controller-gen 100 | controller-gen: 101 | $(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.4.1) 102 | 103 | # Download kustomize locally if necessary 104 | KUSTOMIZE = $(shell pwd)/bin/kustomize 105 | kustomize: 106 | $(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v3@v3.8.7) 107 | 108 | # go-get-tool will 'go get' any package $2 and install it to $1. 109 | PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) 110 | define go-get-tool 111 | @[ -f $(1) ] || { \ 112 | set -e ;\ 113 | TMP_DIR=$$(mktemp -d) ;\ 114 | cd $$TMP_DIR ;\ 115 | go mod init tmp ;\ 116 | echo "Downloading $(2)" ;\ 117 | GOBIN=$(PROJECT_DIR)/bin go get $(2) ;\ 118 | rm -rf $$TMP_DIR ;\ 119 | } 120 | endef 121 | 122 | # Generate bundle manifests and metadata, then validate generated files. 123 | .PHONY: bundle 124 | bundle: manifests kustomize 125 | operator-sdk generate kustomize manifests -q 126 | cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) 127 | $(KUSTOMIZE) build config/manifests | operator-sdk generate bundle -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) 128 | operator-sdk bundle validate ./bundle 129 | 130 | # Build the bundle image. 131 | .PHONY: bundle-build 132 | bundle-build: 133 | docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) . 134 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: voodoo.io 2 | layout: go.kubebuilder.io/v3 3 | projectName: irsa-operator 4 | repo: github.com/VoodooTeam/irsa-operator 5 | resources: 6 | - crdVersion: v1 7 | group: irsa 8 | kind: IamRoleServiceAccount 9 | version: v1alpha1 10 | - crdVersion: v1 11 | group: irsa 12 | kind: Awspolicy 13 | version: v1alpha1 14 | - crdVersion: v1 15 | group: irsa 16 | kind: Awsrole 17 | version: v1alpha1 18 | - crdVersion: v1 19 | group: irsa 20 | kind: Role 21 | version: v1alpha1 22 | - crdVersion: v1 23 | group: irsa 24 | kind: Policy 25 | version: v1alpha1 26 | version: 3-alpha 27 | plugins: 28 | manifests.sdk.operatorframework.io/v2: {} 29 | scorecard.sdk.operatorframework.io/v2: {} 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # !!! DISCLAIMER: This repository is not maintained anymore !!! 2 | 3 | # IRSA operator 4 | 5 | ![CI](https://github.com/VoodooTeam/irsa-operator/actions/workflows/ci.yml/badge.svg) 6 | 7 | A Kubernetes operator to manage IAM roles & policies needed for IRSA, directly from your EKS cluster 8 | 9 | This project is built using the Kubernetes [operator SDK](https://sdk.operatorframework.io/) 10 | 11 | ## What problem does it solve ? 12 | 13 | When using IRSA in order to scope AWS permissions at the pod-level (instead of the usual node-level) you have to define the "absolute path" of the serviceAccount to give it some rights on AWS resources (ie. declare on AWS the exact namespace and name of the serviceAccount allowed to assume the role). This creates an hidden dependency between AWS and your k8s serviceAccount. For instance, you will break the permissions given by the serviceAccount if you just rename it. 14 | 15 | Also, the steps to get IRSA working on AWS can be a bit cumbersome (create a policy, create a role, create an assume role policy with not-super-obvious fields). 16 | 17 | This operator solves both problems by letting you declare a very simple CRD containing the name of the serviceAccount you want (name of the CRD resource itself) and the permissions you want to give. 18 | It will automatically create the resources needed on AWS and the serviceAccount in the namespace were you created the CR (see example below). 19 | 20 | ## Caveat 21 | - oidc must be enabled on your EKS cluster 22 | 23 | ## Resources 24 | 25 | - [Docker images](https://github.com/orgs/VoodooTeam/packages/container/package/irsa-operator) 26 | - [Helm repo](https://voodooteam.github.io/irsa-operator/index.yaml) 27 | 28 | ## Example 29 | 30 | This CRD will allow any pod using the `serviceAccount` named `s3-get-lister` to `Get` and `List` all objects in the s3 bucket with ARN `arn:aws:s3:::test-irsa-4gkut9fl` 31 | 32 | ``` 33 | apiVersion: irsa.voodoo.io/v1alpha1 34 | kind: IamRoleServiceAccount 35 | metadata: 36 | name: s3-get-lister 37 | spec: 38 | policy: 39 | statement: 40 | - resource: "arn:aws:s3:::test-irsa-4gkut9fl" 41 | action: 42 | - "s3:Get*" 43 | - "s3:List*" 44 | ``` 45 | 46 | What this operator does (from a user point of view) : 47 | - create an IAM Policy with the provided statement 48 | - create an IAM Role with this policy attached to it 49 | - create a serviceAccount named as specified with the IAM Role capabilities 50 | 51 | you can use the serviceAccount created by the irsa-operator by simply setting its name in your pods `spec.serviceAccountName` 52 | 53 | ``` 54 | apiVersion: apps/v1 55 | kind: Deployment 56 | metadata: 57 | labels: 58 | app: irsa-test 59 | name: irsa-test 60 | spec: 61 | selector: 62 | matchLabels: 63 | app: irsa-test 64 | template: 65 | metadata: 66 | labels: 67 | app: irsa-test 68 | spec: 69 | serviceAccountName: s3-get-lister # <- HERE, simply the name of the IamRoleServiceAccount 70 | containers: 71 | - image: amazon/aws-cli 72 | name: aws-cli 73 | command: ["aws", "s3", "ls", "arn:aws:s3:::test-irsa-4gkut9fl"] 74 | ``` 75 | 76 | ## installation of the operator 77 | 78 | An helm chart is available on this repo, you can use it to install the operator in a cluster. 79 | 80 | The operator will use IRSA also to do its job (but you have to do that once per cluster and fields are hardcoded in the helm chart), see see [./\_doc/example/terraform/main.tf](./\_doc/example/terraform/main.tf ) 81 | 82 | - the `clusterName` is used to avoid name collisions between AWS IAM resources created by different EKS running in the same account, you can use whatever value you want (most likely the EKS cluster name) 83 | - the rolearn is the role the operator will use 84 | - the oidcProviderARN is known at cluster creation (`oidc` must be enabled) 85 | 86 | 87 | ## architecture 88 | 89 | Here's how IRSA works and how the irsa-operator interfaces with it 90 | 91 | ![](./_doc/architecture-diagram.png) 92 | 93 | ## model 94 | 95 | TLA+ formal specification of the way this operator works is available : [./\_doc/model/IrsaOperator.pdf](./_doc/model/IrsaOperator.pdf) 96 | 97 | 98 | ## project structure 99 | this project follows the `operator SDK` structure : 100 | - CR types are declared in `./api/`, the `zz_generated...` file is autogenerated based on other CRs using the `make` command 101 | - Controllers (handling reconciliation loops) are in `./controllers/`, one controller per CR. 102 | 103 | ## (manual) installation of the operator 104 | 105 | ### pre-requisites 106 | - kubectl configure to talk to the EKS where you want to install the operator 107 | - a docker registry where the EKS cluster can pull the operator image you'll build 108 | - an IAM role with the ability to create policies, roles, attach policies (use its arn instead of the placeholder ``) 109 | 110 | ### build the docker image of the controller and push it to an ECR 111 | 112 | ``` 113 | make docker-build docker-push IMG= 114 | ``` 115 | 116 | _NB : it will run all the tests before building the image_ 117 | 118 | ### install with Helm 119 | ``` 120 | helm install irsa-operator --set image= --set rolearn= --set oidcProviderARN= --set clusterName= ./config/helm/ 121 | ``` 122 | 123 | #### check 124 | 125 | you can access operator's logs there : 126 | ``` 127 | k logs deploy/irsa-operator-controller-manager -n irsa-operator-system -c manager -f 128 | ``` 129 | 130 | ### deploy a resource that uses the iamroleserviceaccount CRD 131 | 132 | ``` 133 | helm install s3lister --set s3BucketName= ./_doc/example/k8s 134 | ``` 135 | 136 | #### check 137 | you can access logs of your pod 138 | 139 | ``` 140 | kubectl logs --selector=app=s3lister 141 | ``` 142 | 143 | if you see the listing of your s3 ``, congratulations ! the pod has been able to achieve this using the abilities you gave it in your `IamRoleServiceAccount.Spec` ! 144 | 145 | ## Work on the project 146 | ### Dev Dependencies 147 | If you use [nix](https://nixos.org/download.html) you can open a shell with everything you need installed : 148 | ``` 149 | nix-shell 150 | ``` 151 | otherwise, you'll need : 152 | - go 153 | - operator-sdk 154 | - helm 155 | - docker-compose 156 | - kind 157 | - awscli2 158 | - openssl 159 | - curl 160 | - jq 161 | - gnumake 162 | - envsubst 163 | 164 | ### resources 165 | - [kubebuilder](https://book.kubebuilder.io/) 166 | - [kubernetes operator concurrency model](https://openkruise.io/en-us/blog/blog2.html) 167 | 168 | ### tests 169 | - check test coverage in your browser with `go tool cover -html=cover.out` 170 | 171 | ## Release process 172 | ### Publish docker image 173 | - create a release with the name `v` 174 | - it will trigger the `publish-docker` workflow and push the docker image to github artefacts 175 | 176 | ### Publish the helm chart 177 | if the previous step went fine 178 | - update [./_helm/chart/Chart.yaml](./_helm/chart/Chart.yaml) and set the version to 179 | - it will trigger the `chart-release` workflow, publish the helm chart and create a release called `helm-v` 180 | 181 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /_dev-env/.gitignore: -------------------------------------------------------------------------------- 1 | k8s-pki 2 | oidc-provider/tls 3 | webhook/tls 4 | -------------------------------------------------------------------------------- /_dev-env/Makefile: -------------------------------------------------------------------------------- 1 | export AWS_ACCESS_KEY_ID = test 2 | export AWS_SECRET_ACCESS_KEY = test 3 | export AWS_REGION = us-east-1 4 | export AWS = aws --endpoint-url=http://localhost:4566 --no-cli-pager 5 | export DOCKER_USER = USER_ID=$(shell id -u) GROUP_ID=$(shell id -g) 6 | 7 | .PHONY: all start tear_down 8 | .PHONY: start_kind start_docker_compose register_oidc check wait_for_localstack deploy_webhook 9 | .PHONY: docker_build docker_push docker_build_push local_deploy update_operator restart_operator 10 | 11 | all: clean start docker_build_push local_deploy test_irsa 12 | start: start_kind gen_oidc_certs start_docker_compose wait_for_localstack register_oidc deploy_webhook 13 | update_operator: docker_build_push local_deploy restart_operator 14 | 15 | CERT_FOLDER=./oidc-provider/tls 16 | 17 | restart_operator: 18 | kubectl delete po -n irsa-operator-system --all 19 | 20 | test_irsa: 21 | kubectl apply -f ./test-irsa.yml 22 | 23 | start_kind: 24 | $(info == STARTING KIND CLUSTER ==) 25 | sudo rm -rf ./k8s-pki 26 | mkdir ./k8s-pki 27 | kind create cluster --config ./kind-config.yml 28 | sudo chmod 644 ./k8s-pki/sa.* 29 | 30 | gen_oidc_certs: 31 | rm -rf $(CERT_FOLDER) 32 | mkdir $(CERT_FOLDER) 33 | openssl genrsa -out $(CERT_FOLDER)/hydra.local.key 2048 34 | openssl req -new -config ./oidc-provider/csr.conf -key $(CERT_FOLDER)/hydra.local.key -out $(CERT_FOLDER)/hydra.local.csr 35 | openssl x509 -req -days 365 -in $(CERT_FOLDER)/hydra.local.csr -signkey $(CERT_FOLDER)/hydra.local.key -out $(CERT_FOLDER)/hydra.local.crt 36 | 37 | start_docker_compose: 38 | $(info == STARTING DOCKER-COMPOSE ==) 39 | $(DOCKER_USER) docker-compose up -d 40 | 41 | register_oidc: 42 | $(info == REGISTERING OPENID CONNECT PROVIDER ==) 43 | $(AWS) iam \ 44 | create-open-id-connect-provider --url https://hydra.local:4444 --client-id-list sts.amazonaws.com --thumbprint-list \ 45 | $(shell openssl s_client -connect localhost:4444 < /dev/null 2>/dev/null | openssl x509 -fingerprint -noout -in /dev/stdin | sed 's/.*=\|://g') 46 | 47 | check: 48 | $(info == CHECKING OPENID CONNECT PROVIDERS ==) 49 | $(AWS) iam list-open-id-connect-providers 50 | 51 | wait_for_localstack: 52 | $(info == WAITING FOR LOCALSTACK ==) 53 | until $(AWS) sts get-caller-identity > /dev/null 2>&1; do \ 54 | sleep 1; \ 55 | done 56 | 57 | docker_build: 58 | $(info == BUILDING OPERATOR DOCKER IMAGE ==) 59 | docker build -t localhost:5000/irsa-operator .. 60 | 61 | docker_push: 62 | $(info == PUSHING OPERATOR IMAGE TO LOCAL REGISTRY ==) 63 | docker push localhost:5000/irsa-operator 64 | 65 | docker_build_push: docker_build docker_push 66 | 67 | deploy_webhook: 68 | ./webhook/deploy.sh 69 | 70 | local_deploy: 71 | OIDC=$$($(AWS) iam list-open-id-connect-providers | jq -r '.OpenIDConnectProviderList[0].Arn'); \ 72 | kustomize build ../config/default | \ 73 | CONTROLLER_IMG=localhost:5000/irsa-operator:latest \ 74 | ROLE_ARN=not-applicable \ 75 | CLUSTER_NAME=kind-cluster \ 76 | LOCALSTACK_ENDPOINT=http://aws-local:4566 \ 77 | OIDC_PROVIDER_ARN=$$OIDC envsubst | kubectl apply -f - 78 | 79 | clean: 80 | $(info == TEARING DOWN ==) 81 | kind delete clusters irsa-operator 82 | $(DOCKER_USER) docker-compose down 83 | sudo rm -rf ./k8s-pki 84 | -------------------------------------------------------------------------------- /_dev-env/README.md: -------------------------------------------------------------------------------- 1 | # dev env 2 | 3 | ## dependencies 4 | - docker 5 | - kind 6 | - kubectl (>= 1.20) 7 | - kustomize 8 | - awscli v2 9 | - gnumake 10 | - jq 11 | - openssl 12 | - envsubst 13 | 14 | ## useful commands 15 | 16 | start the dev env 17 | ``` 18 | make 19 | ``` 20 | 21 | update the operator 22 | ``` 23 | make update_operator 24 | ``` 25 | 26 | tear down the dev env 27 | ``` 28 | make clean 29 | ``` 30 | 31 | # what it does (when it starts) 32 | - create the kind cluster 33 | - start localstack 34 | - start hydra (OIDC provider) 35 | - register the oidc provider on AWS 36 | - deploy the admission webhook (will use hydra) 37 | - install the IRSA-operator CRDs 38 | - deploy the irsa-operator 39 | - deploy a test IamRoleServiceAccount CR and a deployment that uses it 40 | 41 | ## resources 42 | 43 | https://blog.mikesir87.io/2020/09/eks-pod-identity-webhook-deep-dive/ 44 | 45 | https://www.eksworkshop.com/beginner/110_irsa/ 46 | 47 | https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html 48 | -------------------------------------------------------------------------------- /_dev-env/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | local-registry: 5 | image: registry:2 6 | ports: 7 | - "5000:5000" 8 | restart: unless-stopped 9 | 10 | # AWS 11 | aws-local: 12 | image: localstack/localstack:0.12.12 13 | ports: 14 | - "4566:4566" 15 | environment: 16 | - SERVICES=iam,s3,sts 17 | - DEBUG=1 18 | 19 | # OIDC 20 | hydra.local: 21 | image: oryd/hydra:v1.9.0-alpha.3-sqlite 22 | ports: 23 | - "4444:4444" # Public port 24 | - "4445:4445" # Admin port 25 | - "5555:5555" # Port for hydra token user 26 | environment: 27 | - DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true 28 | - SERVE_TLS_KEY_PATH=/etc/config/certs/hydra.local.key 29 | - SERVE_TLS_CERT_PATH=/etc/config/certs/hydra.local.crt 30 | user: "${USER_ID}:${GROUP_ID}" 31 | command: 32 | serve -c /etc/config/hydra.yml all 33 | volumes: 34 | - type: volume 35 | source: hydra-sqlite 36 | target: /var/lib/sqlite 37 | read_only: false 38 | - type: bind 39 | source: ./oidc-provider/hydra.yml 40 | target: /etc/config/hydra.yml 41 | - type: bind 42 | source: ./oidc-provider/tls 43 | target: /etc/config/certs 44 | restart: unless-stopped 45 | depends_on: 46 | - hydra-migrate-db 47 | 48 | hydra-migrate-db: 49 | image: oryd/hydra:v1.9.0-alpha.3-sqlite 50 | environment: 51 | - DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true 52 | user: "${USER_ID}:${GROUP_ID}" 53 | command: 54 | migrate -c /etc/config/hydra.yml sql -e --yes 55 | volumes: 56 | - type: volume 57 | source: hydra-sqlite 58 | target: /var/lib/sqlite 59 | read_only: false 60 | - type: bind 61 | source: ./oidc-provider/hydra.yml 62 | target: /etc/config/hydra.yml 63 | restart: on-failure 64 | 65 | hydra-add-keys: 66 | image: oryd/hydra:v1.9.0-alpha.3-sqlite 67 | environment: 68 | - DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true 69 | - HYDRA_ADMIN_URL=https://hydra.local:4445 70 | user: "${USER_ID}:${GROUP_ID}" 71 | command: 72 | keys import my-set /etc/pki/sa.key /etc/pki/sa.pub --skip-tls-verify 73 | volumes: 74 | - type: bind 75 | source: ./k8s-pki 76 | target: /etc/pki 77 | restart: on-failure 78 | depends_on: 79 | - hydra.local 80 | 81 | 82 | volumes: 83 | hydra-sqlite: 84 | 85 | networks: 86 | default: 87 | external: 88 | name: kind 89 | -------------------------------------------------------------------------------- /_dev-env/kind-config.yml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | name: irsa-operator 4 | kubeadmConfigPatches: 5 | - | 6 | kind: ClusterConfiguration 7 | apiServer: 8 | extraArgs: 9 | service-account-issuer: "https://hydra.local:4444" 10 | service-account-key-file: "/etc/kubernetes/pki/sa.pub" 11 | service-account-signing-key-file: "/etc/kubernetes/pki/sa.key" 12 | api-audiences: "sts.amazonaws.com" 13 | 14 | containerdConfigPatches: 15 | - |- 16 | [plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:5000"] 17 | endpoint = ["http://local-registry:5000"] 18 | 19 | nodes: 20 | - role: control-plane 21 | image: kindest/node:v1.20.7 22 | extraMounts: 23 | - hostPath: ./k8s-pki/ 24 | containerPath: /etc/kubernetes/pki 25 | -------------------------------------------------------------------------------- /_dev-env/oidc-provider/csr.conf: -------------------------------------------------------------------------------- 1 | [ req ] 2 | prompt = no 3 | distinguished_name = dn 4 | req_extensions = req_ext 5 | 6 | [ dn ] 7 | CN = hydra 8 | emailAddress = matthieuj@voodoo.io 9 | O = Voodoo 10 | OU = Infra 11 | L = Brest 12 | ST = Brittany 13 | C = FR 14 | 15 | [ req_ext ] 16 | subjectAltName = DNS: hydra.local 17 | -------------------------------------------------------------------------------- /_dev-env/oidc-provider/hydra.yml: -------------------------------------------------------------------------------- 1 | serve: 2 | cookies: 3 | same_site_mode: Lax 4 | 5 | urls: 6 | self: 7 | issuer: https://hydra.local:4444 8 | 9 | secrets: 10 | system: 11 | - youReallyNeedToChangeThis 12 | 13 | oidc: 14 | subject_identifiers: 15 | supported_types: 16 | - pairwise 17 | - public 18 | pairwise: 19 | salt: youReallyNeedToChangeThis 20 | 21 | webfinger: 22 | oidc_discovery: 23 | supported_claims: 24 | - sub 25 | - iss 26 | -------------------------------------------------------------------------------- /_dev-env/test-irsa.yml: -------------------------------------------------------------------------------- 1 | apiVersion: irsa.voodoo.io/v1alpha1 2 | kind: IamRoleServiceAccount 3 | metadata: 4 | name: s3put 5 | spec: 6 | policy: 7 | statement: 8 | - resource: "arn:aws:s3:::test-irsa" 9 | action: 10 | - "s3:Get*" 11 | - "s3:List*" 12 | --- 13 | apiVersion: apps/v1 14 | kind: Deployment 15 | metadata: 16 | name: s3test 17 | spec: 18 | selector: 19 | matchLabels: 20 | app: s3test 21 | template: 22 | metadata: 23 | labels: 24 | app: s3test 25 | spec: 26 | serviceAccountName: s3put 27 | containers: 28 | - image: amazon/aws-cli 29 | name: aws-cli 30 | command: ["bash", "-c", "aws --endpoint-url=http://aws-local:4566 sts assume-role-with-web-identity --role-arn $AWS_ROLE_ARN --role-session-name test --web-identity-token file://$AWS_WEB_IDENTITY_TOKEN_FILE"] 31 | -------------------------------------------------------------------------------- /_dev-env/webhook/csr.conf: -------------------------------------------------------------------------------- 1 | [req] 2 | prompt = no 3 | req_extensions = v3_req 4 | distinguished_name = req_distinguished_name 5 | 6 | [req_distinguished_name] 7 | CN = system:node:webhook 8 | O = system:nodes 9 | 10 | [ v3_req ] 11 | basicConstraints = CA:FALSE 12 | keyUsage = digitalSignature, keyEncipherment 13 | extendedKeyUsage = serverAuth 14 | subjectAltName = @alt_names 15 | [alt_names] 16 | DNS.1 = pod-identity-webhook 17 | DNS.2 = pod-identity-webhook.default 18 | DNS.3 = pod-identity-webhook.default.svc 19 | -------------------------------------------------------------------------------- /_dev-env/webhook/csr.yml: -------------------------------------------------------------------------------- 1 | apiVersion: certificates.k8s.io/v1 2 | kind: CertificateSigningRequest 3 | metadata: 4 | name: ${CSR_NAME} 5 | spec: 6 | signerName: kubernetes.io/kubelet-serving 7 | groups: 8 | - system:authenticated 9 | request: ${CSR_REQ} 10 | usages: 11 | - key encipherment 12 | - digital signature 13 | - server auth 14 | -------------------------------------------------------------------------------- /_dev-env/webhook/deploy.sh: -------------------------------------------------------------------------------- 1 | export SCRIPT_DIR=$(dirname "$0") 2 | export APP="pod-identity-webhook" 3 | export NAMESPACE="default" 4 | export CSR_NAME="${APP}.${NAMESPACE}.svc" 5 | export CERT_FOLDER=${SCRIPT_DIR}/tls 6 | 7 | # initial cleanup 8 | rm -rf ${CERT_FOLDER} 9 | mkdir ${CERT_FOLDER} 10 | 11 | # we generate the private key and the CSR 12 | openssl genrsa -out ${CERT_FOLDER}/webhook.key 2048 13 | openssl req -new -config ${SCRIPT_DIR}/csr.conf -key ${CERT_FOLDER}/webhook.key -out ${CERT_FOLDER}/webhook.csr 14 | 15 | # we create or replace the k8s CSR 16 | kubectl delete csr ${CSR_NAME} || true 17 | export CSR_REQ=$(cat ${CERT_FOLDER}/webhook.csr | base64 | tr -d '\n') 18 | cat ${SCRIPT_DIR}/csr.yml | envsubst | kubectl apply -f - 19 | 20 | # we wait for the CSR to be created in k8s 21 | while true; do 22 | kubectl get csr ${CSR_NAME} > /dev/null 2>&1 23 | if [ "$?" -eq 0 ]; then 24 | break 25 | fi 26 | echo "Waiting for CSR to be created" 27 | sleep 1 28 | done 29 | 30 | # we approve it 31 | kubectl certificate approve ${CSR_NAME} 32 | 33 | # we wait for the corresponding certificate to be issued 34 | while true; do 35 | TLS_CERT=$(kubectl get csr ${CSR_NAME} -o jsonpath='{.status.certificate}') 36 | if [[ $TLS_CERT != "" ]]; then 37 | break 38 | fi 39 | echo "Waiting for certificate to be issued" 40 | sleep 1 41 | done 42 | 43 | # we set the certificate and the private key as TLS secret on k8s 44 | echo ${TLS_CERT} | openssl base64 -d -A -out ${CERT_FOLDER}/webhook.pem 45 | kubectl delete secret webhook-tls-cert || true 46 | kubectl create secret tls webhook-tls-cert --cert=${CERT_FOLDER}/webhook.pem --key=${CERT_FOLDER}/webhook.key 47 | 48 | # grab the CA from k8s add it to the mutatingwebhook and create all the webhook related resources 49 | export CA_BUNDLE=$(kubectl config view --raw --minify --flatten -o jsonpath='{.clusters[].cluster.certificate-authority-data}') 50 | cat ${SCRIPT_DIR}/mutatingwebhook.tmpl | envsubst | kubectl apply -f - 51 | kubectl apply -f ${SCRIPT_DIR}/manifests/ 52 | 53 | # we restart the webhook (if any) 54 | kubectl delete po -l app=pod-identity-webhook 55 | -------------------------------------------------------------------------------- /_dev-env/webhook/manifests/auth.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: pod-identity-webhook 5 | namespace: default 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: Role 9 | metadata: 10 | name: pod-identity-webhook 11 | namespace: default 12 | rules: 13 | - apiGroups: 14 | - "" 15 | resources: 16 | - secrets 17 | verbs: 18 | - create 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - secrets 23 | verbs: 24 | - get 25 | - update 26 | - patch 27 | resourceNames: 28 | - "webhook-tls-cert" 29 | --- 30 | apiVersion: rbac.authorization.k8s.io/v1 31 | kind: RoleBinding 32 | metadata: 33 | name: pod-identity-webhook 34 | namespace: default 35 | roleRef: 36 | apiGroup: rbac.authorization.k8s.io 37 | kind: Role 38 | name: pod-identity-webhook 39 | subjects: 40 | - kind: ServiceAccount 41 | name: pod-identity-webhook 42 | namespace: default 43 | --- 44 | apiVersion: rbac.authorization.k8s.io/v1 45 | kind: ClusterRole 46 | metadata: 47 | name: pod-identity-webhook 48 | rules: 49 | - apiGroups: 50 | - "" 51 | resources: 52 | - serviceaccounts 53 | verbs: 54 | - get 55 | - watch 56 | - list 57 | - apiGroups: 58 | - certificates.k8s.io 59 | resources: 60 | - certificatesigningrequests 61 | verbs: 62 | - create 63 | - get 64 | - list 65 | - watch 66 | --- 67 | apiVersion: rbac.authorization.k8s.io/v1 68 | kind: ClusterRoleBinding 69 | metadata: 70 | name: pod-identity-webhook 71 | roleRef: 72 | apiGroup: rbac.authorization.k8s.io 73 | kind: ClusterRole 74 | name: pod-identity-webhook 75 | subjects: 76 | - kind: ServiceAccount 77 | name: pod-identity-webhook 78 | namespace: default 79 | -------------------------------------------------------------------------------- /_dev-env/webhook/manifests/deploy.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: pod-identity-webhook 5 | namespace: default 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: pod-identity-webhook 10 | template: 11 | metadata: 12 | labels: 13 | app: pod-identity-webhook 14 | spec: 15 | serviceAccountName: pod-identity-webhook 16 | containers: 17 | - name: pod-identity-webhook 18 | image: err0r500/eks-pod-identity-webhook 19 | imagePullPolicy: Always 20 | command: 21 | - /webhook 22 | - --in-cluster 23 | - --namespace=default 24 | - --service-name=pod-identity-webhook 25 | - --tls-secret=webhook-tls-cert 26 | - --annotation-prefix=eks.amazonaws.com 27 | - --token-audience=sts.amazonaws.com 28 | - --logtostderr 29 | - --enable-debugging-handlers 30 | volumeMounts: 31 | - name: webhook-certs 32 | mountPath: /var/run/app/certs 33 | readOnly: false 34 | volumes: 35 | - name: webhook-certs 36 | emptyDir: {} 37 | -------------------------------------------------------------------------------- /_dev-env/webhook/manifests/svc.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: pod-identity-webhook 5 | namespace: default 6 | spec: 7 | ports: 8 | - port: 443 9 | selector: 10 | app: pod-identity-webhook 11 | -------------------------------------------------------------------------------- /_dev-env/webhook/mutatingwebhook.tmpl: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: MutatingWebhookConfiguration 3 | metadata: 4 | name: pod-identity-webhook 5 | namespace: default 6 | webhooks: 7 | - name: pod-identity-webhook.amazonaws.com 8 | admissionReviewVersions: ["v1beta1"] 9 | failurePolicy: Ignore 10 | sideEffects: None 11 | clientConfig: 12 | service: 13 | name: pod-identity-webhook 14 | namespace: default 15 | path: "/mutate" 16 | caBundle: ${CA_BUNDLE} 17 | rules: 18 | - operations: [ "CREATE" ] 19 | apiGroups: [""] 20 | apiVersions: ["v1"] 21 | resources: ["pods"] 22 | -------------------------------------------------------------------------------- /_dev-env/webhook/webhook-tester.yml: -------------------------------------------------------------------------------- 1 | # when you create these resources, AWS related env vars must be injected in the pod automatically 2 | # otherwise it means the webhook doesn't work (it's another project, but the irsa-operator relies on this mechanism) 3 | apiVersion: v1 4 | kind: ServiceAccount 5 | metadata: 6 | name: webhook-tester-sa 7 | annotations: 8 | eks.amazonaws.com/role-arn: arn:aws:iam::000000000000:role/my-app-role 9 | --- 10 | apiVersion: v1 11 | kind: Pod 12 | metadata: 13 | labels: 14 | run: webhook-tester 15 | name: webhook-tester 16 | spec: 17 | serviceAccountName: webhook-tester-sa 18 | containers: 19 | - image: praqma/network-multitool 20 | name: webhook-tester 21 | restartPolicy: Always 22 | 23 | -------------------------------------------------------------------------------- /_doc/architecture-diagram: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /_doc/architecture-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VoodooTeam/irsa-operator/572794197ed4f3c71e4bcba49f05d65149276068/_doc/architecture-diagram.png -------------------------------------------------------------------------------- /_doc/example/k8s/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: "v1" 2 | name: test-irsa-deploy 3 | version: 0.0.1 4 | -------------------------------------------------------------------------------- /_doc/example/k8s/templates/deploy.yml: -------------------------------------------------------------------------------- 1 | apiVersion: irsa.voodoo.io/v1alpha1 2 | kind: IamRoleServiceAccount 3 | metadata: 4 | name: iamroleserviceaccount-test-sample 5 | spec: 6 | serviceAccountName: s3get 7 | policy: 8 | statement: 9 | - resource: "arn:aws:s3:::{{ .Values.s3BucketName }}" 10 | action: 11 | - "s3:Get*" 12 | - "s3:List*" 13 | 14 | --- 15 | apiVersion: apps/v1 16 | kind: Deployment 17 | metadata: 18 | labels: 19 | app: s3lister 20 | name: s3lister 21 | spec: 22 | replicas: 1 23 | selector: 24 | matchLabels: 25 | app: s3lister 26 | template: 27 | metadata: 28 | labels: 29 | app: s3lister 30 | spec: 31 | serviceAccountName: s3get 32 | containers: 33 | - image: amazon/aws-cli 34 | name: aws-cli 35 | command: ["aws", "s3", "ls", "{{ .Values.s3BucketName }}"] 36 | 37 | -------------------------------------------------------------------------------- /_doc/example/k8s/values.yaml: -------------------------------------------------------------------------------- 1 | s3BucketName: 2 | -------------------------------------------------------------------------------- /_doc/example/terraform/.gitignore: -------------------------------------------------------------------------------- 1 | .terraform/ 2 | terraform* 3 | kubeconfig* 4 | -------------------------------------------------------------------------------- /_doc/example/terraform/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "3.23.0" 6 | constraints = ">= 2.23.0, >= 2.28.1, >= 2.68.0, >= 3.0.0, >= 3.3.0" 7 | hashes = [ 8 | "h1:GugGr7igctZkUUt0im9b0CbdinTRxb4dNXvmGuN2gZ8=", 9 | ] 10 | } 11 | 12 | provider "registry.terraform.io/hashicorp/kubernetes" { 13 | version = "1.13.3" 14 | constraints = "~> 1.11, >= 1.11.1" 15 | hashes = [ 16 | "h1:whoGs/NeucMF8U/urPaeXdQUb+ppaO1Ae4r5aJRhfrU=", 17 | ] 18 | } 19 | 20 | provider "registry.terraform.io/hashicorp/local" { 21 | version = "1.4.0" 22 | constraints = "~> 1.2, >= 1.4.0" 23 | hashes = [ 24 | "h1:P3mtBQSRp/KhVLJgwdHZRTWaYsT6A9nSwrmKrRZwsW8=", 25 | ] 26 | } 27 | 28 | provider "registry.terraform.io/hashicorp/null" { 29 | version = "2.1.2" 30 | constraints = ">= 2.1.0, ~> 2.1" 31 | hashes = [ 32 | "h1:CFnENdqQu4g3LJNevA32aDxcUz2qGkRGQpFfkI8TCdE=", 33 | ] 34 | } 35 | 36 | provider "registry.terraform.io/hashicorp/random" { 37 | version = "2.3.1" 38 | constraints = ">= 2.1.0, ~> 2.1" 39 | hashes = [ 40 | "h1:bPBDLMpQzOjKhDlP9uH2UPIz9tSjcbCtLdiJ5ASmCx4=", 41 | ] 42 | } 43 | 44 | provider "registry.terraform.io/hashicorp/template" { 45 | version = "2.2.0" 46 | constraints = ">= 2.1.0, ~> 2.1" 47 | hashes = [ 48 | "h1:94qn780bi1qjrbC3uQtjJh3Wkfwd5+tTtJHOb7KTg9w=", 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /_doc/example/terraform/README.md: -------------------------------------------------------------------------------- 1 | create the infrastructure 2 | ``` 3 | terraform apply 4 | ``` 5 | -------------------------------------------------------------------------------- /_doc/example/terraform/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 0.12.0" 3 | } 4 | 5 | provider "aws" { 6 | version = ">= 2.28.1" 7 | region = var.region 8 | } 9 | 10 | provider "random" { 11 | version = "~> 2.1" 12 | } 13 | 14 | provider "local" { 15 | version = "~> 1.2" 16 | } 17 | 18 | provider "null" { 19 | version = "~> 2.1" 20 | } 21 | 22 | provider "template" { 23 | version = "~> 2.1" 24 | } 25 | 26 | data "aws_eks_cluster" "cluster" { 27 | name = module.eks.cluster_id 28 | } 29 | 30 | data "aws_eks_cluster_auth" "cluster" { 31 | name = module.eks.cluster_id 32 | } 33 | 34 | provider "kubernetes" { 35 | host = data.aws_eks_cluster.cluster.endpoint 36 | cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority.0.data) 37 | token = data.aws_eks_cluster_auth.cluster.token 38 | load_config_file = false 39 | version = "~> 1.11" 40 | } 41 | 42 | data "aws_availability_zones" "available" {} 43 | 44 | locals { 45 | cluster_name = "test-eks-${random_string.suffix.result}" 46 | } 47 | 48 | resource "random_string" "suffix" { 49 | length = 8 50 | special = false 51 | } 52 | 53 | module "vpc" { 54 | source = "terraform-aws-modules/vpc/aws" 55 | version = "2.64.0" 56 | 57 | name = "test-irsa" 58 | cidr = "10.0.0.0/16" 59 | azs = data.aws_availability_zones.available.names 60 | private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] 61 | public_subnets = ["10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"] 62 | enable_nat_gateway = true 63 | single_nat_gateway = true 64 | enable_dns_hostnames = true 65 | 66 | public_subnet_tags = { 67 | "kubernetes.io/cluster/${local.cluster_name}" = "shared" 68 | "kubernetes.io/role/elb" = "1" 69 | } 70 | 71 | private_subnet_tags = { 72 | "kubernetes.io/cluster/${local.cluster_name}" = "shared" 73 | "kubernetes.io/role/internal-elb" = "1" 74 | } 75 | } 76 | 77 | module "eks" { 78 | source = "terraform-aws-modules/eks/aws" 79 | 80 | cluster_name = local.cluster_name 81 | cluster_version = "1.18" 82 | subnets = module.vpc.private_subnets 83 | enable_irsa = true 84 | 85 | tags = { 86 | Environment = "test-irsa" 87 | } 88 | 89 | vpc_id = module.vpc.vpc_id 90 | 91 | worker_groups = [ 92 | { 93 | name = "test-irsa" 94 | instance_type = "t2.small" 95 | asg_desired_capacity = 1 96 | } 97 | ] 98 | } 99 | 100 | module "iam_assumable_role_admin" { 101 | source = "terraform-aws-modules/iam/aws//modules/iam-assumable-role-with-oidc" 102 | version = "3.6.0" 103 | create_role = true 104 | role_name = "irsa-operator" 105 | provider_url = replace(module.eks.cluster_oidc_issuer_url, "https://", "") 106 | role_policy_arns = [aws_iam_policy.irsa.arn] 107 | oidc_fully_qualified_subjects = ["system:serviceaccount:irsa-operator-system:irsa-operator-oidc-sa"] # these fields are hardcoded in the helm chart 108 | } 109 | 110 | resource "aws_iam_policy" "irsa" { 111 | name_prefix = "irsa-operator" 112 | description = "irsa operator" 113 | policy = data.aws_iam_policy_document.irsa.json 114 | } 115 | 116 | data "aws_iam_policy_document" "irsa" { 117 | statement { 118 | sid = "irsaIam" 119 | effect = "Allow" 120 | 121 | actions = [ 122 | "iam:*" 123 | ] 124 | 125 | resources = ["*"] 126 | } 127 | } 128 | 129 | module "s3_bucket" { 130 | source = "terraform-aws-modules/s3-bucket/aws" 131 | version = "v1.17.0" 132 | 133 | bucket = "test-irsa-${lower(random_string.suffix.result)}" 134 | acl = "private" 135 | } 136 | 137 | resource "aws_s3_bucket_object" "hello" { 138 | bucket = module.s3_bucket.this_s3_bucket_id 139 | key = "/hello/" 140 | } 141 | 142 | resource "aws_s3_bucket_object" "irsa" { 143 | bucket = module.s3_bucket.this_s3_bucket_id 144 | key = "/irsa/" 145 | } 146 | 147 | resource "aws_ecr_repository" "this" { 148 | name = "irsa" 149 | } 150 | -------------------------------------------------------------------------------- /_doc/example/terraform/output.tf: -------------------------------------------------------------------------------- 1 | output "s3_name" { 2 | description = "s3 bucket name" 3 | value = module.s3_bucket.this_s3_bucket_id 4 | } 5 | 6 | output "ecr_url" { 7 | description = "ecr url" 8 | value = aws_ecr_repository.this.repository_url 9 | } 10 | 11 | output "oidc_arn" { 12 | description = "oidc server arn" 13 | value = module.eks.oidc_provider_arn 14 | } 15 | 16 | -------------------------------------------------------------------------------- /_doc/example/terraform/shell.nix: -------------------------------------------------------------------------------- 1 | let 2 | stable = import (builtins.fetchTarball { 3 | name = "nixos-20.09"; 4 | url = "https://github.com/NixOS/nixpkgs/archive/20.09.tar.gz"; 5 | sha256 = "1wg61h4gndm3vcprdcg7rc4s1v3jkm5xd7lw8r2f67w502y94gcy"; 6 | }) {}; 7 | 8 | voodoo = import (builtins.fetchGit { 9 | url = "git@github.com:VoodooTeam/nix-pkgs.git"; 10 | ref = "master"; 11 | }) stable; 12 | 13 | in 14 | stable.mkShell { 15 | buildInputs = 16 | [ 17 | voodoo.terraform_0_14_3 18 | voodoo.kubectl_1_19_4 19 | ]; 20 | } 21 | 22 | 23 | -------------------------------------------------------------------------------- /_doc/example/terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" { 2 | default = "eu-west-3" 3 | } 4 | -------------------------------------------------------------------------------- /_doc/model/.gitignore: -------------------------------------------------------------------------------- 1 | *.dvi 2 | *.tex 3 | states/ 4 | result 5 | -------------------------------------------------------------------------------- /_doc/model/IrsaOperator.cfg: -------------------------------------------------------------------------------- 1 | SPECIFICATION Spec 2 | 3 | CONSTANTS 4 | NULL = "NULL" 5 | _workers = {"wa", "wb"} 6 | 7 | INVARIANTS 8 | TypeOk 9 | 10 | PROPERTIES 11 | NoConcurrentProcessingOfSameResource 12 | TerminationIsTheLastAction -------------------------------------------------------------------------------- /_doc/model/IrsaOperator.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VoodooTeam/irsa-operator/572794197ed4f3c71e4bcba49f05d65149276068/_doc/model/IrsaOperator.pdf -------------------------------------------------------------------------------- /_doc/model/shell.nix: -------------------------------------------------------------------------------- 1 | let 2 | pkgs = import (builtins.fetchTarball { 3 | name = "nixos-20.09"; 4 | url = "https://github.com/NixOS/nixpkgs/archive/20.09.tar.gz"; 5 | sha256 = "1wg61h4gndm3vcprdcg7rc4s1v3jkm5xd7lw8r2f67w502y94gcy"; 6 | }) {}; 7 | 8 | tlatools = with pkgs; 9 | import ./tlaplus.nix { 10 | inherit stdenv fetchFromGitHub makeWrapper adoptopenjdk-bin jre ant; }; 11 | in 12 | pkgs.mkShell { 13 | buildInputs = 14 | [ 15 | tlatools 16 | pkgs.adoptopenjdk-bin 17 | pkgs.texlive.combined.scheme-basic 18 | ]; 19 | } 20 | 21 | -------------------------------------------------------------------------------- /_doc/model/tlapdf: -------------------------------------------------------------------------------- 1 | # usage : ./tlapdf ./IrsaOperator 2 | 3 | tla2tex -textwidth 470 -hoffset -70 -textheight 630 -voffset -50 -shade $1.tla \ 4 | && pdflatex $1.tex \ 5 | && rm $1.log $1.aux $1.dvi $1.tex $1.ps -------------------------------------------------------------------------------- /_doc/model/tlaplus.nix: -------------------------------------------------------------------------------- 1 | { stdenv, fetchFromGitHub, makeWrapper 2 | , adoptopenjdk-bin, jre, ant 3 | }: 4 | 5 | stdenv.mkDerivation rec { 6 | pname = "tlaplus"; 7 | version = "1.7.1"; 8 | 9 | src = fetchFromGitHub { 10 | owner = "tlaplus"; 11 | repo = "tlaplus"; 12 | rev = "refs/tags/v${version}"; 13 | sha256 = "1mm6r9bq79zks50yk0agcpdkw9yy994m38ibmgpb3bi3wkpq9891"; 14 | }; 15 | 16 | buildInputs = [ makeWrapper adoptopenjdk-bin ant ]; 17 | 18 | buildPhase = "ant -f tlatools/org.lamport.tlatools/customBuild.xml compile dist"; 19 | installPhase = '' 20 | mkdir -p $out/share/java $out/bin 21 | cp tlatools/org.lamport.tlatools/dist/*.jar $out/share/java 22 | makeWrapper ${jre}/bin/java $out/bin/tlc2 \ 23 | --add-flags "-cp $out/share/java/tla2tools.jar tlc2.TLC" 24 | makeWrapper ${jre}/bin/java $out/bin/tla2sany \ 25 | --add-flags "-cp $out/share/java/tla2tools.jar tla2sany.SANY" 26 | makeWrapper ${jre}/bin/java $out/bin/pcal \ 27 | --add-flags "-cp $out/share/java/tla2tools.jar pcal.trans" 28 | makeWrapper ${jre}/bin/java $out/bin/tla2tex \ 29 | --add-flags "-cp $out/share/java/tla2tools.jar tla2tex.TLA" 30 | ''; 31 | 32 | meta = { 33 | description = "An algorithm specification language with model checking tools"; 34 | homepage = "http://lamport.azurewebsites.net/tla/tla.html"; 35 | license = stdenv.lib.licenses.mit; 36 | platforms = stdenv.lib.platforms.unix; 37 | maintainers = [ stdenv.lib.maintainers.thoughtpolice ]; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /_helm/chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: irsa-operator 3 | description: A Helm chart for Kubernetes to install the irsa-operator 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.6 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "v0.1.1" 25 | -------------------------------------------------------------------------------- /_helm/chart/crds/irsa.voodoo.io_iamroleserviceaccounts.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | controller-gen.kubebuilder.io/version: v0.4.1 8 | creationTimestamp: null 9 | name: iamroleserviceaccounts.irsa.voodoo.io 10 | spec: 11 | group: irsa.voodoo.io 12 | names: 13 | kind: IamRoleServiceAccount 14 | listKind: IamRoleServiceAccountList 15 | plural: iamroleserviceaccounts 16 | singular: iamroleserviceaccount 17 | scope: Namespaced 18 | versions: 19 | - name: v1alpha1 20 | schema: 21 | openAPIV3Schema: 22 | description: IamRoleServiceAccount is the Schema for the iamroleserviceaccounts 23 | API 24 | properties: 25 | apiVersion: 26 | description: 'APIVersion defines the versioned schema of this representation 27 | of an object. Servers should convert recognized schemas to the latest 28 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 29 | type: string 30 | kind: 31 | description: 'Kind is a string value representing the REST resource this 32 | object represents. Servers may infer this from the endpoint the client 33 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 34 | type: string 35 | metadata: 36 | type: object 37 | spec: 38 | description: IamRoleServiceAccountSpec defines the desired state of IamRoleServiceAccount 39 | properties: 40 | policy: 41 | description: PolicySpec describes the policy that must be present 42 | on AWS 43 | properties: 44 | arn: 45 | type: string 46 | statement: 47 | items: 48 | description: StatementSpec defines an aws statement (Sid is 49 | autogenerated & Effect is always "allow") 50 | properties: 51 | action: 52 | items: 53 | type: string 54 | type: array 55 | resource: 56 | type: string 57 | required: 58 | - action 59 | - resource 60 | type: object 61 | type: array 62 | required: 63 | - statement 64 | type: object 65 | required: 66 | - policy 67 | type: object 68 | status: 69 | description: IamRoleServiceAccountStatus defines the observed state of 70 | IamRoleServiceAccount 71 | properties: 72 | condition: 73 | type: string 74 | reason: 75 | type: string 76 | required: 77 | - condition 78 | type: object 79 | type: object 80 | served: true 81 | storage: true 82 | subresources: 83 | status: {} 84 | status: 85 | acceptedNames: 86 | kind: "" 87 | plural: "" 88 | conditions: [] 89 | storedVersions: [] 90 | -------------------------------------------------------------------------------- /_helm/chart/crds/irsa.voodoo.io_policies.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | controller-gen.kubebuilder.io/version: v0.4.1 8 | creationTimestamp: null 9 | name: policies.irsa.voodoo.io 10 | spec: 11 | group: irsa.voodoo.io 12 | names: 13 | kind: Policy 14 | listKind: PolicyList 15 | plural: policies 16 | singular: policy 17 | scope: Namespaced 18 | versions: 19 | - name: v1alpha1 20 | schema: 21 | openAPIV3Schema: 22 | description: Policy is the Schema for the awspolicies API 23 | properties: 24 | apiVersion: 25 | description: 'APIVersion defines the versioned schema of this representation 26 | of an object. Servers should convert recognized schemas to the latest 27 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 28 | type: string 29 | kind: 30 | description: 'Kind is a string value representing the REST resource this 31 | object represents. Servers may infer this from the endpoint the client 32 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 33 | type: string 34 | metadata: 35 | type: object 36 | spec: 37 | description: PolicySpec describes the policy that must be present on AWS 38 | properties: 39 | arn: 40 | type: string 41 | statement: 42 | items: 43 | description: StatementSpec defines an aws statement (Sid is autogenerated 44 | & Effect is always "allow") 45 | properties: 46 | action: 47 | items: 48 | type: string 49 | type: array 50 | resource: 51 | type: string 52 | required: 53 | - action 54 | - resource 55 | type: object 56 | type: array 57 | required: 58 | - statement 59 | type: object 60 | status: 61 | description: PolicyStatus defines the observed state of Policy 62 | properties: 63 | condition: 64 | description: poorman's golang enum 65 | type: string 66 | reason: 67 | type: string 68 | required: 69 | - condition 70 | type: object 71 | type: object 72 | served: true 73 | storage: true 74 | subresources: 75 | status: {} 76 | status: 77 | acceptedNames: 78 | kind: "" 79 | plural: "" 80 | conditions: [] 81 | storedVersions: [] 82 | -------------------------------------------------------------------------------- /_helm/chart/crds/irsa.voodoo.io_roles.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | controller-gen.kubebuilder.io/version: v0.4.1 8 | creationTimestamp: null 9 | name: roles.irsa.voodoo.io 10 | spec: 11 | group: irsa.voodoo.io 12 | names: 13 | kind: Role 14 | listKind: RoleList 15 | plural: roles 16 | singular: role 17 | scope: Namespaced 18 | versions: 19 | - name: v1alpha1 20 | schema: 21 | openAPIV3Schema: 22 | description: Role is the Schema for the awsroles API 23 | properties: 24 | apiVersion: 25 | description: 'APIVersion defines the versioned schema of this representation 26 | of an object. Servers should convert recognized schemas to the latest 27 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 28 | type: string 29 | kind: 30 | description: 'Kind is a string value representing the REST resource this 31 | object represents. Servers may infer this from the endpoint the client 32 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 33 | type: string 34 | metadata: 35 | type: object 36 | spec: 37 | description: RoleSpec defines the desired state of Role 38 | properties: 39 | permissionsBoundariesPolicyARN: 40 | type: string 41 | policyarn: 42 | type: string 43 | rolearn: 44 | type: string 45 | serviceAccountName: 46 | type: string 47 | required: 48 | - serviceAccountName 49 | type: object 50 | status: 51 | description: RoleStatus defines the observed state of Role 52 | properties: 53 | condition: 54 | description: poorman's golang enum 55 | type: string 56 | reason: 57 | type: string 58 | required: 59 | - condition 60 | type: object 61 | type: object 62 | served: true 63 | storage: true 64 | subresources: 65 | status: {} 66 | status: 67 | acceptedNames: 68 | kind: "" 69 | plural: "" 70 | conditions: [] 71 | storedVersions: [] 72 | -------------------------------------------------------------------------------- /_helm/chart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "irsa-operator.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "irsa-operator.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "irsa-operator.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "irsa-operator.labels" -}} 37 | helm.sh/chart: {{ include "irsa-operator.chart" . }} 38 | {{ include "irsa-operator.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "irsa-operator.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "irsa-operator.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "irsa-operator.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "irsa-operator.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /_helm/chart/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "irsa-operator.fullname" . }} 5 | labels: 6 | {{- include "irsa-operator.labels" . | nindent 4 }} 7 | spec: 8 | replicas: {{ .Values.replicaCount }} 9 | selector: 10 | matchLabels: 11 | {{- include "irsa-operator.selectorLabels" . | nindent 6 }} 12 | template: 13 | metadata: 14 | {{- with .Values.podAnnotations }} 15 | annotations: 16 | {{- toYaml . | nindent 8 }} 17 | {{- end }} 18 | labels: 19 | {{- include "irsa-operator.selectorLabels" . | nindent 8 }} 20 | spec: 21 | {{- with .Values.imagePullSecrets }} 22 | imagePullSecrets: 23 | {{- toYaml . | nindent 8 }} 24 | {{- end }} 25 | serviceAccountName: {{ include "irsa-operator.serviceAccountName" . }} 26 | securityContext: 27 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 28 | containers: 29 | - name: {{ .Chart.Name }} 30 | securityContext: 31 | {{- toYaml .Values.securityContext | nindent 12 }} 32 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 33 | imagePullPolicy: {{ .Values.image.pullPolicy }} 34 | command: 35 | - /manager 36 | args: 37 | - --health-probe-bind-address=:8081 38 | - --metrics-bind-address=:8080 39 | - --leader-elect 40 | - --cluster-name={{ required "clusterName is required, used to avoid collision in (deterministic) IAM resources names" .Values.clusterName }} 41 | - --oidc-provider-arn={{ required "oidcProviderARN is required" .Values.oidcProviderARN }} 42 | - --permissions-boundaries-policy-arn={{ .Values.permissionsBoundariesPolicyARN }} 43 | ports: 44 | - name: metrics 45 | containerPort: 8080 46 | protocol: TCP 47 | - name: health 48 | containerPort: 8081 49 | protocol: TCP 50 | {{- if .Values.localstackEndpoint }} 51 | env: 52 | - value: {{ .Values.localstackEndpoint }} 53 | name: LOCALSTACK_ENDPOINT 54 | {{- end }} 55 | livenessProbe: 56 | httpGet: 57 | path: /healthz 58 | port: 8081 59 | initialDelaySeconds: 15 60 | periodSeconds: 20 61 | readinessProbe: 62 | httpGet: 63 | path: /readyz 64 | port: 8081 65 | initialDelaySeconds: 5 66 | periodSeconds: 10 67 | resources: 68 | {{- toYaml .Values.resources | nindent 12 }} 69 | {{- with .Values.nodeSelector }} 70 | nodeSelector: 71 | {{- toYaml . | nindent 8 }} 72 | {{- end }} 73 | {{- with .Values.affinity }} 74 | affinity: 75 | {{- toYaml . | nindent 8 }} 76 | {{- end }} 77 | {{- with .Values.tolerations }} 78 | tolerations: 79 | {{- toYaml . | nindent 8 }} 80 | {{- end }} 81 | -------------------------------------------------------------------------------- /_helm/chart/templates/rolebindings.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: RoleBinding 4 | metadata: 5 | name: {{ include "irsa-operator.fullname" . }}-leader-election 6 | labels: 7 | {{- include "irsa-operator.labels" . | nindent 4 }} 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: Role 11 | name: {{ include "irsa-operator.fullname" . }}-leader-election 12 | subjects: 13 | - kind: ServiceAccount 14 | name: {{ include "irsa-operator.serviceAccountName" . }} 15 | namespace: {{ .Release.Namespace | quote }} 16 | --- 17 | apiVersion: rbac.authorization.k8s.io/v1 18 | kind: ClusterRoleBinding 19 | metadata: 20 | name: {{ include "irsa-operator.fullname" . }}-manager 21 | roleRef: 22 | apiGroup: rbac.authorization.k8s.io 23 | kind: ClusterRole 24 | name: {{ include "irsa-operator.fullname" . }}-manager 25 | subjects: 26 | - kind: ServiceAccount 27 | name: {{ include "irsa-operator.serviceAccountName" . }} 28 | namespace: {{ .Release.Namespace | quote }} 29 | -------------------------------------------------------------------------------- /_helm/chart/templates/roles.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: {{ include "irsa-operator.fullname" . }}-leader-election 6 | labels: 7 | {{- include "irsa-operator.labels" . | nindent 4 }} 8 | rules: 9 | - apiGroups: 10 | - "" 11 | - coordination.k8s.io 12 | resources: 13 | - configmaps 14 | - leases 15 | verbs: 16 | - get 17 | - list 18 | - watch 19 | - create 20 | - update 21 | - patch 22 | - delete 23 | - apiGroups: 24 | - "" 25 | resources: 26 | - events 27 | verbs: 28 | - create 29 | - patch 30 | --- 31 | apiVersion: rbac.authorization.k8s.io/v1 32 | kind: ClusterRole 33 | metadata: 34 | name: {{ include "irsa-operator.fullname" . }}-manager 35 | labels: 36 | {{- include "irsa-operator.labels" . | nindent 4 }} 37 | rules: 38 | - apiGroups: 39 | - "" 40 | resources: 41 | - serviceaccounts 42 | verbs: 43 | - create 44 | - delete 45 | - get 46 | - list 47 | - watch 48 | - apiGroups: 49 | - irsa.voodoo.io 50 | resources: 51 | - iamroleserviceaccounts 52 | verbs: 53 | - create 54 | - delete 55 | - get 56 | - list 57 | - update 58 | - watch 59 | - apiGroups: 60 | - irsa.voodoo.io 61 | resources: 62 | - iamroleserviceaccounts/finalizers 63 | verbs: 64 | - update 65 | - apiGroups: 66 | - irsa.voodoo.io 67 | resources: 68 | - iamroleserviceaccounts/status 69 | verbs: 70 | - get 71 | - update 72 | - apiGroups: 73 | - irsa.voodoo.io 74 | resources: 75 | - policies 76 | verbs: 77 | - create 78 | - delete 79 | - get 80 | - list 81 | - patch 82 | - update 83 | - watch 84 | - apiGroups: 85 | - irsa.voodoo.io 86 | resources: 87 | - policies/finalizers 88 | verbs: 89 | - update 90 | - apiGroups: 91 | - irsa.voodoo.io 92 | resources: 93 | - policies/status 94 | verbs: 95 | - get 96 | - patch 97 | - update 98 | - apiGroups: 99 | - irsa.voodoo.io 100 | resources: 101 | - roles 102 | verbs: 103 | - create 104 | - delete 105 | - get 106 | - list 107 | - patch 108 | - update 109 | - watch 110 | - apiGroups: 111 | - irsa.voodoo.io 112 | resources: 113 | - roles/finalizers 114 | verbs: 115 | - update 116 | - apiGroups: 117 | - irsa.voodoo.io 118 | resources: 119 | - roles/status 120 | verbs: 121 | - get 122 | - patch 123 | - update 124 | -------------------------------------------------------------------------------- /_helm/chart/templates/service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: {{ include "irsa-operator.fullname" . }}-metrics 6 | labels: 7 | {{- include "irsa-operator.labels" . | nindent 4 }} 8 | spec: 9 | type: {{ .Values.service.type }} 10 | ports: 11 | - port: {{ .Values.service.port }} 12 | targetPort: metrics 13 | protocol: TCP 14 | name: metrics 15 | selector: 16 | {{- include "irsa-operator.selectorLabels" . | nindent 4 }} 17 | -------------------------------------------------------------------------------- /_helm/chart/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "irsa-operator.serviceAccountName" . }} 6 | labels: 7 | {{- include "irsa-operator.labels" . | nindent 4 }} 8 | annotations: 9 | eks.amazonaws.com/role-arn: {{ required "roleARN is required, used to grant irsa-operator the right to create IAM resources" .Values.roleARN }} 10 | {{- range $key, $val := .Values.serviceAccount.annotations }} 11 | {{ $key }}: {{ $val | quote }} 12 | {{- end }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /_helm/chart/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for irsa-operator. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | clusterName: 8 | roleARN: 9 | oidcProviderARN: 10 | permissionsBoundariesPolicyARN: "" 11 | 12 | # for local deployments only : 13 | localstackEndpoint: 14 | 15 | image: 16 | repository: ghcr.io/voodooteam/irsa-operator 17 | pullPolicy: Always 18 | # Overrides the image tag whose default is the chart appVersion. 19 | tag: "" 20 | 21 | imagePullSecrets: [] 22 | nameOverride: "" 23 | fullnameOverride: "" 24 | 25 | serviceAccount: 26 | # Specifies whether a service account should be created 27 | create: true 28 | # Annotations to add to the service account 29 | annotations: {} 30 | # The name of the service account to use. 31 | # If not set and create is true, a name is generated using the fullname template 32 | name: "" 33 | 34 | podAnnotations: {} 35 | 36 | podSecurityContext: {} 37 | # fsGroup: 2000 38 | 39 | securityContext: {} 40 | # capabilities: 41 | # drop: 42 | # - ALL 43 | # readOnlyRootFilesystem: true 44 | # runAsNonRoot: true 45 | # runAsUser: 1000 46 | 47 | service: 48 | type: ClusterIP 49 | port: 80 50 | 51 | resources: {} 52 | # We usually recommend not to specify default resources and to leave this as a conscious 53 | # choice for the user. This also increases chances charts run on environments with little 54 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 55 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 56 | # limits: 57 | # cpu: 100m 58 | # memory: 128Mi 59 | # requests: 60 | # cpu: 100m 61 | # memory: 128Mi 62 | 63 | nodeSelector: {} 64 | 65 | tolerations: [] 66 | 67 | affinity: {} 68 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | // Package v1alpha1 contains API Schema definitions for the irsa v1alpha1 API group 2 | // +kubebuilder:object:generate=true 3 | // +groupName=irsa.voodoo.io 4 | package v1alpha1 5 | 6 | import ( 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | "sigs.k8s.io/controller-runtime/pkg/scheme" 9 | ) 10 | 11 | var ( 12 | // GroupVersion is group version used to register these objects 13 | GroupVersion = schema.GroupVersion{Group: "irsa.voodoo.io", Version: "v1alpha1"} 14 | 15 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 16 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 17 | 18 | // AddToScheme adds the types in this group-version to the given scheme. 19 | AddToScheme = SchemeBuilder.AddToScheme 20 | ) 21 | -------------------------------------------------------------------------------- /api/v1alpha1/iamroleserviceaccount_types.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "fmt" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ) 8 | 9 | // NewIamRoleServiceAccount is the IamRoleServiceAccount constructor 10 | func NewIamRoleServiceAccount(name, ns string, policyspec PolicySpec) *IamRoleServiceAccount { 11 | return &IamRoleServiceAccount{ 12 | TypeMeta: metav1.TypeMeta{ 13 | APIVersion: "irsa.voodoo.io/v1alpha1", 14 | Kind: "IamRoleServiceAccount", 15 | }, 16 | ObjectMeta: metav1.ObjectMeta{ 17 | Name: name, 18 | Namespace: ns, 19 | }, 20 | Spec: IamRoleServiceAccountSpec{ 21 | Policy: policyspec, 22 | }, 23 | } 24 | } 25 | 26 | func (irsa IamRoleServiceAccount) FullName() string { 27 | return irsa.ObjectMeta.Namespace + "/" + irsa.ObjectMeta.Name 28 | } 29 | 30 | // HasStatus is used in tests, should be moved there 31 | func (irsa IamRoleServiceAccount) HasStatus(st fmt.Stringer) bool { 32 | return irsa.Status.Condition.String() == st.String() 33 | } 34 | 35 | // IsPendingDeletion helps us to detect if the resource should be deleted 36 | func (irsa IamRoleServiceAccount) IsPendingDeletion() bool { 37 | return !irsa.ObjectMeta.DeletionTimestamp.IsZero() 38 | } 39 | 40 | // Validate returns an error if the IamRoleServiceAccountSpec is not valid 41 | func (irsa IamRoleServiceAccount) Validate() error { 42 | return irsa.Spec.Policy.Validate() 43 | } 44 | 45 | // IamRoleServiceAccountSpec defines the desired state of IamRoleServiceAccount 46 | type IamRoleServiceAccountSpec struct { 47 | Policy PolicySpec `json:"policy"` 48 | } 49 | 50 | // IamRoleServiceAccountStatus defines the observed state of IamRoleServiceAccount 51 | type IamRoleServiceAccountStatus struct { 52 | Condition IrsaCondition `json:"condition"` 53 | Reason string `json:"reason,omitempty"` 54 | } 55 | 56 | type IrsaCondition string 57 | 58 | var ( 59 | IrsaSubmitted IrsaCondition = "" 60 | IrsaPending IrsaCondition = "pending" 61 | IrsaSaNameConflict IrsaCondition = "saNameConflict" 62 | IrsaForbidden IrsaCondition = "forbidden" 63 | IrsaFailed IrsaCondition = "failed" 64 | IrsaProgressing IrsaCondition = "progressing" 65 | IrsaOK IrsaCondition = "created" 66 | ) 67 | 68 | // String is just used for comparison in HasStatus 69 | func (i IrsaCondition) String() string { 70 | return string(i) 71 | } 72 | 73 | // +kubebuilder:object:root=true 74 | // +kubebuilder:subresource:status 75 | 76 | // IamRoleServiceAccount is the Schema for the iamroleserviceaccounts API 77 | type IamRoleServiceAccount struct { 78 | metav1.TypeMeta `json:",inline"` 79 | metav1.ObjectMeta `json:"metadata,omitempty"` 80 | 81 | Spec IamRoleServiceAccountSpec `json:"spec,omitempty"` 82 | Status IamRoleServiceAccountStatus `json:"status,omitempty"` 83 | } 84 | 85 | // +kubebuilder:object:root=true 86 | 87 | // IamRoleServiceAccountList contains a list of IamRoleServiceAccount 88 | type IamRoleServiceAccountList struct { 89 | metav1.TypeMeta `json:",inline"` 90 | metav1.ListMeta `json:"metadata,omitempty"` 91 | Items []IamRoleServiceAccount `json:"items"` 92 | } 93 | 94 | func init() { 95 | SchemeBuilder.Register(&IamRoleServiceAccount{}, &IamRoleServiceAccountList{}) 96 | } 97 | -------------------------------------------------------------------------------- /api/v1alpha1/policy_types.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/aws/aws-sdk-go/aws/arn" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | // NewPolicy constructs a Policy, setting mandatory fields for us 12 | func NewPolicy(name, ns string, stm []StatementSpec) *Policy { 13 | return &Policy{ 14 | TypeMeta: metav1.TypeMeta{ 15 | APIVersion: "irsa.voodoo.io/v1alpha1", 16 | Kind: "Policy", 17 | }, 18 | ObjectMeta: metav1.ObjectMeta{ 19 | Name: name, 20 | Namespace: ns, 21 | }, 22 | Spec: PolicySpec{ 23 | Statement: stm, 24 | }, 25 | } 26 | } 27 | 28 | func (p Policy) FullName() string { 29 | return p.ObjectMeta.Namespace + "/" + p.ObjectMeta.Name 30 | } 31 | 32 | // HasStatus is used in tests, should be moved there 33 | func (p Policy) HasStatus(st fmt.Stringer) bool { 34 | return p.Status.Condition.String() == st.String() 35 | } 36 | 37 | // IsPendingDeletion helps us to detect if a Policy should be deleted 38 | func (p Policy) IsPendingDeletion() bool { 39 | return !p.ObjectMeta.DeletionTimestamp.IsZero() 40 | } 41 | 42 | // PolicySpec describes the policy that must be present on AWS 43 | type PolicySpec struct { 44 | ARN string `json:"arn,omitempty"` // the ARN of the aws policy 45 | Statement []StatementSpec `json:"statement"` 46 | } 47 | 48 | // Validate returns an error if the PolicySpec is not valid 49 | func (spec PolicySpec) Validate() error { 50 | if len(spec.Statement) == 0 { 51 | return errors.New("empty Policy.spec.statement") 52 | } 53 | 54 | for i, stm := range spec.Statement { 55 | if err := stm.Validate(); err != nil { 56 | return fmt.Errorf("statement :%d : %s", i, err.Error()) 57 | } 58 | } 59 | 60 | return nil 61 | } 62 | 63 | // StatementSpec defines an aws statement (Sid is autogenerated & Effect is always "allow") 64 | type StatementSpec struct { 65 | Resource string `json:"resource"` // ARN of the target aws resource 66 | Action []string `json:"action"` // the list of requested permissions on the aws resource above 67 | } 68 | 69 | // Validate returns an error if the StatementSpec is not valid 70 | func (spec StatementSpec) Validate() error { 71 | if !arn.IsARN(spec.Resource) { 72 | return fmt.Errorf("%s is an invalid ARN", spec.Resource) 73 | } 74 | 75 | if len(spec.Action) == 0 { 76 | return errors.New("empty action array provided") 77 | } 78 | 79 | for i, a := range spec.Action { 80 | if a == "" { 81 | return fmt.Errorf("action #%d: empty action provided", i) 82 | } 83 | } 84 | 85 | return nil 86 | } 87 | 88 | // IsSame is used to detect meaningful difference between 2 StatementSpec 89 | // ie : order of .Action elements is not taken into account 90 | func (a StatementSpec) IsSame(b StatementSpec) bool { 91 | if a.Resource != b.Resource { 92 | return false 93 | } 94 | 95 | if len(a.Action) != len(b.Action) { 96 | return false 97 | } 98 | 99 | // we must ignore actions order 100 | for _, sA := range a.Action { 101 | diff := true 102 | for _, sB := range b.Action { 103 | if sA == sB { 104 | diff = false 105 | break 106 | } 107 | } 108 | if diff { 109 | return false 110 | } 111 | } 112 | return true 113 | } 114 | 115 | // StatementEquals is used to detect meaningful difference between 2 StatementSpec slices 116 | // ie : order of elements is not taken into account 117 | func StatementEquals(a, b []StatementSpec) bool { 118 | if len(a) != len(b) { 119 | return false 120 | } 121 | 122 | for _, sA := range a { 123 | diff := true 124 | for _, sB := range b { 125 | if sA.IsSame(sB) { 126 | diff = false 127 | break 128 | } 129 | } 130 | if diff { 131 | return false 132 | } 133 | } 134 | return true 135 | } 136 | 137 | // PolicyStatus defines the observed state of Policy 138 | type PolicyStatus struct { 139 | Condition CrCondition `json:"condition"` 140 | Reason string `json:"reason,omitempty"` 141 | } 142 | 143 | func NewPolicyStatus(condition CrCondition, reason string) PolicyStatus { 144 | return PolicyStatus{ 145 | Condition: condition, 146 | Reason: reason, 147 | } 148 | } 149 | 150 | // +kubebuilder:object:root=true 151 | // +kubebuilder:subresource:status 152 | 153 | // Policy is the Schema for the awspolicies API 154 | type Policy struct { 155 | metav1.TypeMeta `json:",inline"` 156 | metav1.ObjectMeta `json:"metadata,omitempty"` 157 | 158 | Spec PolicySpec `json:"spec,omitempty"` 159 | Status PolicyStatus `json:"status,omitempty"` 160 | } 161 | 162 | // Validate returns an error if the Policy is not valid 163 | func (p Policy) Validate(cN string) error { 164 | if err := p.Spec.Validate(); err != nil { 165 | return err 166 | } 167 | 168 | awsName := p.AwsName(cN) 169 | if len(awsName) > 64 { 170 | return fmt.Errorf("genereated uniqueName ( `%s` ) is too long ", awsName) 171 | } 172 | 173 | return nil 174 | } 175 | 176 | // AwsName is the name the resource will have on AWS 177 | // It must be unique per AWS account thus the naming convention 178 | func (p Policy) AwsName(cN string) string { 179 | return fmt.Sprintf("irsa-op-%s-%s-%s", cN, p.ObjectMeta.Namespace, p.ObjectMeta.Name) 180 | } 181 | 182 | // PathPrefix is the "directory" where the policy will be available 183 | // It's used to retrieved a policy on AWS 184 | func (p Policy) PathPrefix(cN string) string { 185 | return fmt.Sprintf("/irsa-operator/%s/%s/%s/", cN, p.ObjectMeta.Namespace, p.ObjectMeta.Name) 186 | } 187 | 188 | // Path is the "file" where the policy will be available 189 | func (p Policy) Path(cN string) string { 190 | return fmt.Sprintf("%spolicy/", p.PathPrefix(cN)) 191 | } 192 | 193 | // +kubebuilder:object:root=true 194 | 195 | // PolicyList contains a list of Policy 196 | type PolicyList struct { 197 | metav1.TypeMeta `json:",inline"` 198 | metav1.ListMeta `json:"metadata,omitempty"` 199 | Items []Policy `json:"items"` 200 | } 201 | 202 | func init() { 203 | SchemeBuilder.Register(&Policy{}, &PolicyList{}) 204 | } 205 | -------------------------------------------------------------------------------- /api/v1alpha1/role_types.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ) 9 | 10 | // NewRole constructs a Role, setting mandatory fields for us 11 | func NewRole(name, ns string) *Role { 12 | return &Role{ 13 | TypeMeta: metav1.TypeMeta{ 14 | APIVersion: "irsa.voodoo.io/v1alpha1", 15 | Kind: "Role", 16 | }, 17 | ObjectMeta: metav1.ObjectMeta{ 18 | Name: name, 19 | Namespace: ns, 20 | }, 21 | Spec: RoleSpec{ 22 | ServiceAccountName: name, 23 | }, 24 | } 25 | } 26 | 27 | func (r Role) FullName() string { 28 | return r.ObjectMeta.Namespace + "/" + r.ObjectMeta.Name 29 | } 30 | 31 | // HasStatus is used in tests, should be moved there 32 | func (r Role) HasStatus(st fmt.Stringer) bool { 33 | return r.Status.Condition.String() == st.String() 34 | } 35 | 36 | // Validate returns an error if the Policy is not valid 37 | func (r Role) Validate(cN string) error { 38 | if err := r.Spec.Validate(); err != nil { 39 | return err 40 | } 41 | 42 | awsName := r.AwsName(cN) 43 | if len(awsName) > 64 { 44 | return fmt.Errorf("aws name is too long : %s", awsName) 45 | } 46 | 47 | return nil 48 | } 49 | 50 | // AwsName is the name the resource will have on AWS 51 | // It must be unique per AWS account thus the naming convention 52 | func (r Role) AwsName(cN string) string { 53 | return fmt.Sprintf("irsa-op-%s-%s-%s", cN, r.ObjectMeta.Namespace, r.ObjectMeta.Name) 54 | } 55 | 56 | // IsPendingDeletion helps us to detect if the resource should be deleted 57 | func (r Role) IsPendingDeletion() bool { 58 | return !r.ObjectMeta.DeletionTimestamp.IsZero() 59 | } 60 | 61 | // RoleSpec defines the desired state of Role 62 | type RoleSpec struct { 63 | ServiceAccountName string `json:"serviceAccountName"` 64 | PolicyARN string `json:"policyarn,omitempty"` 65 | RoleARN string `json:"rolearn,omitempty"` 66 | PermissionsBoundariesPolicyArn string `json:"permissionsBoundariesPolicyARN,omitempty"` 67 | } 68 | 69 | // Validate returns an error if the RoleSpec is not valid 70 | func (spec RoleSpec) Validate() error { 71 | if spec.ServiceAccountName == "" { 72 | return errors.New("empty string provided as spec.ServiceAccountName") 73 | } 74 | 75 | return nil 76 | } 77 | 78 | // RoleStatus defines the observed state of Role 79 | type RoleStatus struct { 80 | Condition CrCondition `json:"condition"` 81 | Reason string `json:"reason,omitempty"` 82 | } 83 | 84 | func NewRoleStatus(condition CrCondition, reason string) RoleStatus { 85 | return RoleStatus{ 86 | Condition: condition, 87 | Reason: reason, 88 | } 89 | } 90 | 91 | // +kubebuilder:object:root=true 92 | // +kubebuilder:subresource:status 93 | 94 | // Role is the Schema for the awsroles API 95 | type Role struct { 96 | metav1.TypeMeta `json:",inline"` 97 | metav1.ObjectMeta `json:"metadata,omitempty"` 98 | 99 | Spec RoleSpec `json:"spec,omitempty"` 100 | Status RoleStatus `json:"status,omitempty"` 101 | } 102 | 103 | // +kubebuilder:object:root=true 104 | 105 | // RoleList contains a list of Role 106 | type RoleList struct { 107 | metav1.TypeMeta `json:",inline"` 108 | metav1.ListMeta `json:"metadata,omitempty"` 109 | Items []Role `json:"items"` 110 | } 111 | 112 | func init() { 113 | SchemeBuilder.Register(&Role{}, &RoleList{}) 114 | } 115 | -------------------------------------------------------------------------------- /api/v1alpha1/shared_types.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | // poorman's golang enum 4 | type CrCondition string 5 | 6 | var ( 7 | CrSubmitted CrCondition = "" 8 | CrPending CrCondition = "pending" 9 | CrProgressing CrCondition = "progressing" 10 | CrOK CrCondition = "created" 11 | CrDeleting CrCondition = "deleting" 12 | CrError CrCondition = "error" 13 | ) 14 | 15 | func (i CrCondition) String() string { 16 | return string(i) 17 | } 18 | -------------------------------------------------------------------------------- /api/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | /* 4 | Copyright 2020. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package v1alpha1 22 | 23 | import ( 24 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | ) 26 | 27 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 28 | func (in *IamRoleServiceAccount) DeepCopyInto(out *IamRoleServiceAccount) { 29 | *out = *in 30 | out.TypeMeta = in.TypeMeta 31 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 32 | in.Spec.DeepCopyInto(&out.Spec) 33 | out.Status = in.Status 34 | } 35 | 36 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IamRoleServiceAccount. 37 | func (in *IamRoleServiceAccount) DeepCopy() *IamRoleServiceAccount { 38 | if in == nil { 39 | return nil 40 | } 41 | out := new(IamRoleServiceAccount) 42 | in.DeepCopyInto(out) 43 | return out 44 | } 45 | 46 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 47 | func (in *IamRoleServiceAccount) DeepCopyObject() runtime.Object { 48 | if c := in.DeepCopy(); c != nil { 49 | return c 50 | } 51 | return nil 52 | } 53 | 54 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 55 | func (in *IamRoleServiceAccountList) DeepCopyInto(out *IamRoleServiceAccountList) { 56 | *out = *in 57 | out.TypeMeta = in.TypeMeta 58 | in.ListMeta.DeepCopyInto(&out.ListMeta) 59 | if in.Items != nil { 60 | in, out := &in.Items, &out.Items 61 | *out = make([]IamRoleServiceAccount, len(*in)) 62 | for i := range *in { 63 | (*in)[i].DeepCopyInto(&(*out)[i]) 64 | } 65 | } 66 | } 67 | 68 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IamRoleServiceAccountList. 69 | func (in *IamRoleServiceAccountList) DeepCopy() *IamRoleServiceAccountList { 70 | if in == nil { 71 | return nil 72 | } 73 | out := new(IamRoleServiceAccountList) 74 | in.DeepCopyInto(out) 75 | return out 76 | } 77 | 78 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 79 | func (in *IamRoleServiceAccountList) DeepCopyObject() runtime.Object { 80 | if c := in.DeepCopy(); c != nil { 81 | return c 82 | } 83 | return nil 84 | } 85 | 86 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 87 | func (in *IamRoleServiceAccountSpec) DeepCopyInto(out *IamRoleServiceAccountSpec) { 88 | *out = *in 89 | in.Policy.DeepCopyInto(&out.Policy) 90 | } 91 | 92 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IamRoleServiceAccountSpec. 93 | func (in *IamRoleServiceAccountSpec) DeepCopy() *IamRoleServiceAccountSpec { 94 | if in == nil { 95 | return nil 96 | } 97 | out := new(IamRoleServiceAccountSpec) 98 | in.DeepCopyInto(out) 99 | return out 100 | } 101 | 102 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 103 | func (in *IamRoleServiceAccountStatus) DeepCopyInto(out *IamRoleServiceAccountStatus) { 104 | *out = *in 105 | } 106 | 107 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IamRoleServiceAccountStatus. 108 | func (in *IamRoleServiceAccountStatus) DeepCopy() *IamRoleServiceAccountStatus { 109 | if in == nil { 110 | return nil 111 | } 112 | out := new(IamRoleServiceAccountStatus) 113 | in.DeepCopyInto(out) 114 | return out 115 | } 116 | 117 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 118 | func (in *Policy) DeepCopyInto(out *Policy) { 119 | *out = *in 120 | out.TypeMeta = in.TypeMeta 121 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 122 | in.Spec.DeepCopyInto(&out.Spec) 123 | out.Status = in.Status 124 | } 125 | 126 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Policy. 127 | func (in *Policy) DeepCopy() *Policy { 128 | if in == nil { 129 | return nil 130 | } 131 | out := new(Policy) 132 | in.DeepCopyInto(out) 133 | return out 134 | } 135 | 136 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 137 | func (in *Policy) DeepCopyObject() runtime.Object { 138 | if c := in.DeepCopy(); c != nil { 139 | return c 140 | } 141 | return nil 142 | } 143 | 144 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 145 | func (in *PolicyList) DeepCopyInto(out *PolicyList) { 146 | *out = *in 147 | out.TypeMeta = in.TypeMeta 148 | in.ListMeta.DeepCopyInto(&out.ListMeta) 149 | if in.Items != nil { 150 | in, out := &in.Items, &out.Items 151 | *out = make([]Policy, len(*in)) 152 | for i := range *in { 153 | (*in)[i].DeepCopyInto(&(*out)[i]) 154 | } 155 | } 156 | } 157 | 158 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicyList. 159 | func (in *PolicyList) DeepCopy() *PolicyList { 160 | if in == nil { 161 | return nil 162 | } 163 | out := new(PolicyList) 164 | in.DeepCopyInto(out) 165 | return out 166 | } 167 | 168 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 169 | func (in *PolicyList) DeepCopyObject() runtime.Object { 170 | if c := in.DeepCopy(); c != nil { 171 | return c 172 | } 173 | return nil 174 | } 175 | 176 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 177 | func (in *PolicySpec) DeepCopyInto(out *PolicySpec) { 178 | *out = *in 179 | if in.Statement != nil { 180 | in, out := &in.Statement, &out.Statement 181 | *out = make([]StatementSpec, len(*in)) 182 | for i := range *in { 183 | (*in)[i].DeepCopyInto(&(*out)[i]) 184 | } 185 | } 186 | } 187 | 188 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicySpec. 189 | func (in *PolicySpec) DeepCopy() *PolicySpec { 190 | if in == nil { 191 | return nil 192 | } 193 | out := new(PolicySpec) 194 | in.DeepCopyInto(out) 195 | return out 196 | } 197 | 198 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 199 | func (in *PolicyStatus) DeepCopyInto(out *PolicyStatus) { 200 | *out = *in 201 | } 202 | 203 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicyStatus. 204 | func (in *PolicyStatus) DeepCopy() *PolicyStatus { 205 | if in == nil { 206 | return nil 207 | } 208 | out := new(PolicyStatus) 209 | in.DeepCopyInto(out) 210 | return out 211 | } 212 | 213 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 214 | func (in *Role) DeepCopyInto(out *Role) { 215 | *out = *in 216 | out.TypeMeta = in.TypeMeta 217 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 218 | out.Spec = in.Spec 219 | out.Status = in.Status 220 | } 221 | 222 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Role. 223 | func (in *Role) DeepCopy() *Role { 224 | if in == nil { 225 | return nil 226 | } 227 | out := new(Role) 228 | in.DeepCopyInto(out) 229 | return out 230 | } 231 | 232 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 233 | func (in *Role) DeepCopyObject() runtime.Object { 234 | if c := in.DeepCopy(); c != nil { 235 | return c 236 | } 237 | return nil 238 | } 239 | 240 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 241 | func (in *RoleList) DeepCopyInto(out *RoleList) { 242 | *out = *in 243 | out.TypeMeta = in.TypeMeta 244 | in.ListMeta.DeepCopyInto(&out.ListMeta) 245 | if in.Items != nil { 246 | in, out := &in.Items, &out.Items 247 | *out = make([]Role, len(*in)) 248 | for i := range *in { 249 | (*in)[i].DeepCopyInto(&(*out)[i]) 250 | } 251 | } 252 | } 253 | 254 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleList. 255 | func (in *RoleList) DeepCopy() *RoleList { 256 | if in == nil { 257 | return nil 258 | } 259 | out := new(RoleList) 260 | in.DeepCopyInto(out) 261 | return out 262 | } 263 | 264 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 265 | func (in *RoleList) DeepCopyObject() runtime.Object { 266 | if c := in.DeepCopy(); c != nil { 267 | return c 268 | } 269 | return nil 270 | } 271 | 272 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 273 | func (in *RoleSpec) DeepCopyInto(out *RoleSpec) { 274 | *out = *in 275 | } 276 | 277 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleSpec. 278 | func (in *RoleSpec) DeepCopy() *RoleSpec { 279 | if in == nil { 280 | return nil 281 | } 282 | out := new(RoleSpec) 283 | in.DeepCopyInto(out) 284 | return out 285 | } 286 | 287 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 288 | func (in *RoleStatus) DeepCopyInto(out *RoleStatus) { 289 | *out = *in 290 | } 291 | 292 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleStatus. 293 | func (in *RoleStatus) DeepCopy() *RoleStatus { 294 | if in == nil { 295 | return nil 296 | } 297 | out := new(RoleStatus) 298 | in.DeepCopyInto(out) 299 | return out 300 | } 301 | 302 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 303 | func (in *StatementSpec) DeepCopyInto(out *StatementSpec) { 304 | *out = *in 305 | if in.Action != nil { 306 | in, out := &in.Action, &out.Action 307 | *out = make([]string, len(*in)) 308 | copy(*out, *in) 309 | } 310 | } 311 | 312 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StatementSpec. 313 | func (in *StatementSpec) DeepCopy() *StatementSpec { 314 | if in == nil { 315 | return nil 316 | } 317 | out := new(StatementSpec) 318 | in.DeepCopyInto(out) 319 | return out 320 | } 321 | -------------------------------------------------------------------------------- /aws/aws_test.go: -------------------------------------------------------------------------------- 1 | package aws_test 2 | 3 | import ( 4 | api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | var ( 10 | validPolicy = api.NewPolicy("name", "testns", []api.StatementSpec{ 11 | {Resource: "arn:aws:s3:::my_corporate_bucket/exampleobject.png", Action: []string{"an:action"}}, 12 | }) 13 | ) 14 | 15 | var _ = Describe("policy", func() { 16 | It("given a valid policy", func() { 17 | 18 | By("creating the policy it without error") 19 | err := awsmngr.CreatePolicy(*validPolicy) 20 | Expect(err).NotTo(HaveOccurred()) 21 | 22 | By("ensuring the creation is idempotent") 23 | err = awsmngr.CreatePolicy(*validPolicy) 24 | Expect(err).NotTo(HaveOccurred()) 25 | 26 | By("retrieving the policy ARN") 27 | policyARN, err := awsmngr.GetPolicyARN(validPolicy.PathPrefix(clusterName), validPolicy.AwsName(clusterName)) 28 | Expect(err).NotTo(HaveOccurred()) 29 | Expect(policyARN).NotTo(BeEmpty()) 30 | 31 | By("creating new policy versions 5 times") 32 | validPolicy.Spec.ARN = policyARN 33 | 34 | for i := 0; i < 5; i++ { 35 | err = awsmngr.UpdatePolicy(*validPolicy) 36 | Expect(err).ToNot(HaveOccurred()) 37 | } 38 | 39 | By("deleting it") 40 | Expect(policyARN).NotTo(BeEmpty()) 41 | err = awsmngr.DeletePolicy(policyARN) 42 | Expect(err).NotTo(HaveOccurred()) 43 | 44 | By("ensuring deletion is also idempotent") 45 | Expect(policyARN).NotTo(BeEmpty()) 46 | err = awsmngr.DeletePolicy(policyARN) 47 | Expect(err).NotTo(HaveOccurred()) 48 | }) 49 | }) 50 | 51 | var _ = Describe("role", func() { 52 | role := api.NewRole("name", "testns") 53 | 54 | Context("given a valid role", func() { 55 | It("doesn't exist yet", func() { 56 | exists, err := awsmngr.RoleExists(role.AwsName(clusterName)) 57 | Expect(err).NotTo(HaveOccurred()) 58 | Expect(exists).To(BeFalse()) 59 | }) 60 | 61 | Context("creation", func() { 62 | It("can create it without error without permissionsBoundariesPolicyARN", func() { 63 | err := awsmngr.CreateRole(*role, "") 64 | Expect(err).NotTo(HaveOccurred()) 65 | }) 66 | }) 67 | 68 | Context("creation", func() { 69 | permissionsBoundariesPolicyARN := "arn:aws:iam::123456789012:policy/UsersManageOwnCredentials" 70 | 71 | It("can create it without error", func() { 72 | err := awsmngr.CreateRole(*role, permissionsBoundariesPolicyARN) 73 | Expect(err).NotTo(HaveOccurred()) 74 | }) 75 | 76 | Context("idempotency", func() { 77 | It("creation is idempotent", func() { 78 | err := awsmngr.CreateRole(*role, permissionsBoundariesPolicyARN) 79 | Expect(err).NotTo(HaveOccurred()) 80 | }) 81 | 82 | Context("exists check", func() { 83 | It("can be checked for existing", func() { 84 | exists, err := awsmngr.RoleExists(role.AwsName(clusterName)) 85 | Expect(err).NotTo(HaveOccurred()) 86 | Expect(exists).To(BeTrue()) 87 | }) 88 | 89 | Context("policies can be attached", func() { 90 | policyARN := "" 91 | It("the policy must exist first", func() { 92 | var err error 93 | err = awsmngr.CreatePolicy(*validPolicy) 94 | Expect(err).NotTo(HaveOccurred()) 95 | 96 | policyARN, err = awsmngr.GetPolicyARN(validPolicy.PathPrefix(clusterName), validPolicy.AwsName(clusterName)) 97 | Expect(err).NotTo(HaveOccurred()) 98 | Expect(policyARN).NotTo(BeEmpty()) 99 | }) 100 | 101 | Context("when done", func() { 102 | It("actually can be attached", func() { 103 | err := awsmngr.AttachRolePolicy(role.AwsName(clusterName), policyARN) 104 | Expect(err).NotTo(HaveOccurred()) 105 | }) 106 | 107 | It("and retrieved", func() { 108 | attached, err := awsmngr.GetAttachedRolePoliciesARNs(role.AwsName(clusterName)) 109 | Expect(err).NotTo(HaveOccurred()) 110 | Expect(len(attached)).To(Equal(1)) 111 | Expect(attached[0]).To(Equal(policyARN)) 112 | }) 113 | 114 | Context("delete attached policy", func() { 115 | It("the role can be deleted without error", func() { 116 | err := awsmngr.DeleteRole(role.AwsName(clusterName)) 117 | Expect(err).NotTo(HaveOccurred()) 118 | }) 119 | }) 120 | 121 | Context("delete attached policy", func() { 122 | It("can be done without error", func() { 123 | err := awsmngr.DeletePolicy(policyARN) 124 | Expect(err).NotTo(HaveOccurred()) 125 | }) 126 | 127 | // this doesn't seem to work with localstack 128 | // todo reproduce with the aws cli : 129 | // nothing attached returned when calling ListEntitiesForPolicy against localstack 130 | // I guess something is missing 131 | // 132 | //Context("the policy now should be detached", func() { 133 | // It("and doesn't cause error to attempt to retrieve it", func() { 134 | // attached, err := awsmngr.GetAttachedRolePoliciesARNs(role.AwsName()) 135 | // Expect(err).NotTo(HaveOccurred()) 136 | // Expect(attached).To(BeEmpty()) 137 | // }) 138 | //}) 139 | }) 140 | }) 141 | }) 142 | 143 | Context("deletion", func() { 144 | It("can be deleted without error", func() { 145 | err := awsmngr.DeleteRole(role.AwsName(clusterName)) 146 | Expect(err).NotTo(HaveOccurred()) 147 | }) 148 | 149 | Context("idempotency", func() { 150 | It("deletion is idempotent", func() { 151 | err := awsmngr.DeleteRole(role.AwsName(clusterName)) 152 | Expect(err).NotTo(HaveOccurred()) 153 | }) 154 | }) 155 | }) 156 | }) 157 | }) 158 | }) 159 | }) 160 | }) 161 | -------------------------------------------------------------------------------- /aws/suite_test.go: -------------------------------------------------------------------------------- 1 | package aws_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "testing" 9 | 10 | irsaws "github.com/VoodooTeam/irsa-operator/aws" 11 | "github.com/VoodooTeam/irsa-operator/controllers" 12 | "github.com/aws/aws-sdk-go/aws" 13 | "github.com/aws/aws-sdk-go/aws/credentials" 14 | "github.com/aws/aws-sdk-go/aws/endpoints" 15 | "github.com/aws/aws-sdk-go/aws/session" 16 | "github.com/go-logr/stdr" 17 | dockertest "github.com/ory/dockertest/v3" 18 | "sigs.k8s.io/controller-runtime/pkg/envtest/printer" 19 | 20 | . "github.com/onsi/ginkgo" 21 | . "github.com/onsi/gomega" 22 | ) 23 | 24 | var resource *dockertest.Resource 25 | var pool *dockertest.Pool 26 | var awsmngr controllers.AwsManager 27 | var clusterName string 28 | 29 | func TestTypes(t *testing.T) { 30 | RegisterFailHandler(Fail) 31 | 32 | RunSpecsWithDefaultAndCustomReporters(t, 33 | "Aws Suite", 34 | []Reporter{printer.NewlineReporter{}}) 35 | } 36 | 37 | var _ = BeforeSuite(func() { 38 | localStackEndpoint := os.Getenv("LOCALSTACK_ENDPOINT") 39 | if localStackEndpoint == "" { 40 | localStackEndpoint = setupLocalStack() 41 | Expect(pool).NotTo(BeNil()) 42 | Expect(resource).NotTo(BeNil()) 43 | } else if _, err := http.Get(localStackEndpoint); err != nil { 44 | log.Fatal("can't reach localstack on ", localStackEndpoint) 45 | } 46 | 47 | clusterName = "clustername" 48 | awsmngr = irsaws.NewAwsManager( 49 | session.Must(session.NewSession(&aws.Config{ 50 | Credentials: credentials.NewStaticCredentials("test", "test", ""), 51 | DisableSSL: aws.Bool(true), 52 | Region: aws.String(endpoints.UsWest1RegionID), 53 | Endpoint: &localStackEndpoint, 54 | })), 55 | stdr.New(log.New(os.Stderr, "", log.LstdFlags)), 56 | clusterName, 57 | "oidcprovider.url", 58 | ) 59 | Expect(awsmngr).NotTo(BeNil()) 60 | }) 61 | 62 | var _ = AfterSuite(func() { 63 | if os.Getenv("LOCALSTACK_ENDPOINT") == "" { 64 | err := pool.Purge(resource) 65 | Expect(err).NotTo(HaveOccurred()) 66 | } 67 | }) 68 | 69 | func setupLocalStack() string { 70 | var err error 71 | pool, err = dockertest.NewPool("") 72 | if err != nil { 73 | log.Fatalf("Could not connect to docker: %s", err) 74 | } 75 | 76 | resource, err = pool.Run("localstack/localstack", "0.12.4", []string{"SERVICES=iam,s3,sts"}) 77 | if err != nil { 78 | log.Fatalf("Could not start resource: %s", err) 79 | } 80 | 81 | localStackEndpoint := fmt.Sprintf("http://localhost:%s", resource.GetPort("4566/tcp")) 82 | if err = pool.Retry(func() error { 83 | _, err := http.Get(localStackEndpoint) 84 | return err 85 | }); err != nil { 86 | log.Fatalf("Could not connect to localstack container: %s", err) 87 | } 88 | 89 | return localStackEndpoint 90 | } 91 | -------------------------------------------------------------------------------- /aws/types.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "regexp" 7 | 8 | api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" 9 | ) 10 | 11 | type PolicyDocument struct { 12 | Version string 13 | Statement []Statement 14 | } 15 | 16 | type Statement struct { 17 | Effect StatementEffect 18 | Action []string 19 | Resource string 20 | } 21 | 22 | func (s Statement) ToSpec() api.StatementSpec { 23 | return api.StatementSpec{ 24 | Resource: s.Resource, 25 | Action: s.Action, 26 | } 27 | } 28 | 29 | type StatementEffect string 30 | 31 | // todo : remove this 32 | const ( 33 | StatementAllow StatementEffect = "Allow" 34 | StatementDeny StatementEffect = "Deny" 35 | ) 36 | 37 | func NewPolicyDocumentString(p api.PolicySpec) (string, error) { 38 | stmt := []Statement{} 39 | 40 | for _, s := range p.Statement { 41 | stmt = append(stmt, Statement{ 42 | Effect: StatementAllow, 43 | Action: s.Action, 44 | Resource: s.Resource, 45 | }) 46 | } 47 | 48 | policy := PolicyDocument{ 49 | Version: "2012-10-17", 50 | Statement: stmt, 51 | } 52 | 53 | bytes, err := json.Marshal(policy) 54 | if err != nil { 55 | return "", err 56 | } 57 | 58 | return string(bytes), nil 59 | } 60 | 61 | type RoleDocument struct { 62 | Version string 63 | Statement []RoleStatement 64 | } 65 | 66 | type RoleStatement struct { 67 | Effect StatementEffect 68 | Principal struct { 69 | Federated string 70 | } `json:"Principal"` 71 | Action string 72 | Condition struct { 73 | StringEquals map[string]string 74 | } 75 | } 76 | 77 | func NewAssumeRolePolicyDoc(r api.Role, oidcProviderArn string) (string, error) { 78 | // resource : https://aws.amazon.com/blogs/opensource/introducing-fine-grained-iam-roles-service-accounts 79 | 80 | // we extract the issuerHostpath from the oidcProviderARN (needed in the condition field) 81 | issuerHostpath := oidcProviderArn 82 | submatches := regexp.MustCompile(`(?s)/(.*)`).FindStringSubmatch(issuerHostpath) 83 | if len(submatches) == 2 { 84 | issuerHostpath = submatches[1] 85 | } 86 | 87 | // then create the json formatted Trust policy 88 | bytes, err := json.Marshal( 89 | RoleDocument{ 90 | Version: "2012-10-17", 91 | Statement: []RoleStatement{ 92 | { 93 | Effect: StatementAllow, 94 | Principal: struct{ Federated string }{ 95 | Federated: string(oidcProviderArn), 96 | }, 97 | Action: "sts:AssumeRoleWithWebIdentity", 98 | Condition: struct { 99 | StringEquals map[string]string 100 | }{ 101 | StringEquals: map[string]string{ 102 | fmt.Sprintf("%s:sub", issuerHostpath): fmt.Sprintf("system:serviceaccount:%s:%s", r.ObjectMeta.Namespace, r.Spec.ServiceAccountName)}, 103 | }, 104 | }, 105 | }, 106 | }, 107 | ) 108 | if err != nil { 109 | return "", err 110 | } 111 | 112 | return string(bytes), nil 113 | } 114 | -------------------------------------------------------------------------------- /aws/types_test.go: -------------------------------------------------------------------------------- 1 | package aws_test 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | 9 | api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" 10 | irsaws "github.com/VoodooTeam/irsa-operator/aws" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | ) 13 | 14 | var _ = Describe("PolicyDocument creation", func() { 15 | Context("given a valid policySpec", func() { 16 | validPolicy := api.PolicySpec{ 17 | Statement: []api.StatementSpec{ 18 | {Resource: "bla", Action: []string{"act1"}}, 19 | }, 20 | } 21 | 22 | It("generates a valid policy document", func() { 23 | expectedPolicyDocument := irsaws.PolicyDocument{ 24 | Version: "2012-10-17", 25 | Statement: []irsaws.Statement{ 26 | { 27 | Effect: irsaws.StatementAllow, 28 | Resource: "bla", 29 | Action: []string{"act1"}, 30 | }, 31 | }, 32 | } 33 | policyJSON, err := irsaws.NewPolicyDocumentString(validPolicy) 34 | Expect(err).NotTo(HaveOccurred()) 35 | 36 | genPolicy := &irsaws.PolicyDocument{} 37 | err = json.Unmarshal([]byte(policyJSON), genPolicy) 38 | Expect(err).NotTo(HaveOccurred()) 39 | Expect(*genPolicy).Should(Equal(expectedPolicyDocument)) 40 | }) 41 | }) 42 | 43 | Context("given a valid role", func() { 44 | expectedRoleDoc := irsaws.RoleDocument{ 45 | Version: "2012-10-17", 46 | Statement: []irsaws.RoleStatement{ 47 | { 48 | Effect: irsaws.StatementAllow, 49 | Principal: struct{ Federated string }{ 50 | Federated: "arn:aws.iam::111122223333:oidc-provider/oidc.REGION.eks.amazonaws.com/CLUSTER_ID", 51 | }, 52 | Action: "sts:AssumeRoleWithWebIdentity", 53 | Condition: struct { 54 | StringEquals map[string]string 55 | }{ 56 | StringEquals: map[string]string{"oidc.REGION.eks.amazonaws.com/CLUSTER_ID:sub": "system:serviceaccount:namespace:serviceAccountName"}, 57 | }, 58 | }, 59 | }, 60 | } 61 | 62 | Context("role document", func() { 63 | It("generates a valid role document", func() { 64 | r := api.Role{ 65 | ObjectMeta: metav1.ObjectMeta{ 66 | Namespace: "namespace", 67 | }, 68 | Spec: api.RoleSpec{ 69 | ServiceAccountName: "serviceAccountName", 70 | PolicyARN: "not used here", 71 | }, 72 | } 73 | 74 | roleJSON, err := irsaws.NewAssumeRolePolicyDoc(r, "arn:aws.iam::111122223333:oidc-provider/oidc.REGION.eks.amazonaws.com/CLUSTER_ID") 75 | Expect(err).NotTo(HaveOccurred()) 76 | 77 | genPolicy := &irsaws.RoleDocument{} 78 | err = json.Unmarshal([]byte(roleJSON), genPolicy) 79 | Expect(*genPolicy).Should(Equal(expectedRoleDoc)) 80 | Expect(err).NotTo(HaveOccurred()) 81 | }) 82 | }) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /config/certmanager/certificate.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | # WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. 4 | apiVersion: cert-manager.io/v1 5 | kind: Issuer 6 | metadata: 7 | name: selfsigned-issuer 8 | namespace: system 9 | spec: 10 | selfSigned: {} 11 | --- 12 | apiVersion: cert-manager.io/v1 13 | kind: Certificate 14 | metadata: 15 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 16 | namespace: system 17 | spec: 18 | # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize 19 | dnsNames: 20 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc 21 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local 22 | issuerRef: 23 | kind: Issuer 24 | name: selfsigned-issuer 25 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 26 | -------------------------------------------------------------------------------- /config/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /config/certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is for teaching kustomize how to update name ref and var substitution 2 | nameReference: 3 | - kind: Issuer 4 | group: cert-manager.io 5 | fieldSpecs: 6 | - kind: Certificate 7 | group: cert-manager.io 8 | path: spec/issuerRef/name 9 | 10 | varReference: 11 | - kind: Certificate 12 | group: cert-manager.io 13 | path: spec/commonName 14 | - kind: Certificate 15 | group: cert-manager.io 16 | path: spec/dnsNames 17 | -------------------------------------------------------------------------------- /config/crd/bases/irsa.voodoo.io_iamroleserviceaccounts.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | controller-gen.kubebuilder.io/version: v0.4.1 8 | creationTimestamp: null 9 | name: iamroleserviceaccounts.irsa.voodoo.io 10 | spec: 11 | group: irsa.voodoo.io 12 | names: 13 | kind: IamRoleServiceAccount 14 | listKind: IamRoleServiceAccountList 15 | plural: iamroleserviceaccounts 16 | singular: iamroleserviceaccount 17 | scope: Namespaced 18 | versions: 19 | - name: v1alpha1 20 | schema: 21 | openAPIV3Schema: 22 | description: IamRoleServiceAccount is the Schema for the iamroleserviceaccounts 23 | API 24 | properties: 25 | apiVersion: 26 | description: 'APIVersion defines the versioned schema of this representation 27 | of an object. Servers should convert recognized schemas to the latest 28 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 29 | type: string 30 | kind: 31 | description: 'Kind is a string value representing the REST resource this 32 | object represents. Servers may infer this from the endpoint the client 33 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 34 | type: string 35 | metadata: 36 | type: object 37 | spec: 38 | description: IamRoleServiceAccountSpec defines the desired state of IamRoleServiceAccount 39 | properties: 40 | policy: 41 | description: PolicySpec describes the policy that must be present 42 | on AWS 43 | properties: 44 | arn: 45 | type: string 46 | statement: 47 | items: 48 | description: StatementSpec defines an aws statement (Sid is 49 | autogenerated & Effect is always "allow") 50 | properties: 51 | action: 52 | items: 53 | type: string 54 | type: array 55 | resource: 56 | type: string 57 | required: 58 | - action 59 | - resource 60 | type: object 61 | type: array 62 | required: 63 | - statement 64 | type: object 65 | required: 66 | - policy 67 | type: object 68 | status: 69 | description: IamRoleServiceAccountStatus defines the observed state of 70 | IamRoleServiceAccount 71 | properties: 72 | condition: 73 | type: string 74 | reason: 75 | type: string 76 | required: 77 | - condition 78 | type: object 79 | type: object 80 | served: true 81 | storage: true 82 | subresources: 83 | status: {} 84 | status: 85 | acceptedNames: 86 | kind: "" 87 | plural: "" 88 | conditions: [] 89 | storedVersions: [] 90 | -------------------------------------------------------------------------------- /config/crd/bases/irsa.voodoo.io_policies.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | controller-gen.kubebuilder.io/version: v0.4.1 8 | creationTimestamp: null 9 | name: policies.irsa.voodoo.io 10 | spec: 11 | group: irsa.voodoo.io 12 | names: 13 | kind: Policy 14 | listKind: PolicyList 15 | plural: policies 16 | singular: policy 17 | scope: Namespaced 18 | versions: 19 | - name: v1alpha1 20 | schema: 21 | openAPIV3Schema: 22 | description: Policy is the Schema for the awspolicies API 23 | properties: 24 | apiVersion: 25 | description: 'APIVersion defines the versioned schema of this representation 26 | of an object. Servers should convert recognized schemas to the latest 27 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 28 | type: string 29 | kind: 30 | description: 'Kind is a string value representing the REST resource this 31 | object represents. Servers may infer this from the endpoint the client 32 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 33 | type: string 34 | metadata: 35 | type: object 36 | spec: 37 | description: PolicySpec describes the policy that must be present on AWS 38 | properties: 39 | arn: 40 | type: string 41 | statement: 42 | items: 43 | description: StatementSpec defines an aws statement (Sid is autogenerated 44 | & Effect is always "allow") 45 | properties: 46 | action: 47 | items: 48 | type: string 49 | type: array 50 | resource: 51 | type: string 52 | required: 53 | - action 54 | - resource 55 | type: object 56 | type: array 57 | required: 58 | - statement 59 | type: object 60 | status: 61 | description: PolicyStatus defines the observed state of Policy 62 | properties: 63 | condition: 64 | description: poorman's golang enum 65 | type: string 66 | reason: 67 | type: string 68 | required: 69 | - condition 70 | type: object 71 | type: object 72 | served: true 73 | storage: true 74 | subresources: 75 | status: {} 76 | status: 77 | acceptedNames: 78 | kind: "" 79 | plural: "" 80 | conditions: [] 81 | storedVersions: [] 82 | -------------------------------------------------------------------------------- /config/crd/bases/irsa.voodoo.io_roles.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | controller-gen.kubebuilder.io/version: v0.4.1 8 | creationTimestamp: null 9 | name: roles.irsa.voodoo.io 10 | spec: 11 | group: irsa.voodoo.io 12 | names: 13 | kind: Role 14 | listKind: RoleList 15 | plural: roles 16 | singular: role 17 | scope: Namespaced 18 | versions: 19 | - name: v1alpha1 20 | schema: 21 | openAPIV3Schema: 22 | description: Role is the Schema for the awsroles API 23 | properties: 24 | apiVersion: 25 | description: 'APIVersion defines the versioned schema of this representation 26 | of an object. Servers should convert recognized schemas to the latest 27 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 28 | type: string 29 | kind: 30 | description: 'Kind is a string value representing the REST resource this 31 | object represents. Servers may infer this from the endpoint the client 32 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 33 | type: string 34 | metadata: 35 | type: object 36 | spec: 37 | description: RoleSpec defines the desired state of Role 38 | properties: 39 | permissionsBoundariesPolicyARN: 40 | type: string 41 | policyarn: 42 | type: string 43 | rolearn: 44 | type: string 45 | serviceAccountName: 46 | type: string 47 | required: 48 | - serviceAccountName 49 | type: object 50 | status: 51 | description: RoleStatus defines the observed state of Role 52 | properties: 53 | condition: 54 | description: poorman's golang enum 55 | type: string 56 | reason: 57 | type: string 58 | required: 59 | - condition 60 | type: object 61 | type: object 62 | served: true 63 | storage: true 64 | subresources: 65 | status: {} 66 | status: 67 | acceptedNames: 68 | kind: "" 69 | plural: "" 70 | conditions: [] 71 | storedVersions: [] 72 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/irsa.voodoo.io_iamroleserviceaccounts.yaml 6 | - bases/irsa.voodoo.io_roles.yaml 7 | - bases/irsa.voodoo.io_policies.yaml 8 | # +kubebuilder:scaffold:crdkustomizeresource 9 | 10 | patchesStrategicMerge: 11 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 12 | # patches here are for enabling the conversion webhook for each CRD 13 | #- patches/webhook_in_iamroleserviceaccounts.yaml 14 | #- patches/webhook_in_roles.yaml 15 | #- patches/webhook_in_policies.yaml 16 | # +kubebuilder:scaffold:crdkustomizewebhookpatch 17 | 18 | # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. 19 | # patches here are for enabling the CA injection for each CRD 20 | #- patches/cainjection_in_iamroleserviceaccounts.yaml 21 | #- patches/cainjection_in_roles.yaml 22 | #- patches/cainjection_in_policies.yaml 23 | # +kubebuilder:scaffold:crdkustomizecainjectionpatch 24 | 25 | # the following config is for teaching kustomize how to do kustomization for CRDs. 26 | configurations: 27 | - kustomizeconfig.yaml 28 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_iamroleserviceaccounts.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: iamroleserviceaccounts.irsa.voodoo.io 8 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_policies.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: policies.irsa.voodoo.io 8 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_roles.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: roles.irsa.voodoo.io 8 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_iamroleserviceaccounts.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: iamroleserviceaccounts.irsa.voodoo.io 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_policies.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: policies.irsa.voodoo.io 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_roles.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: roles.irsa.voodoo.io 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | namespace: irsa-operator-system 2 | namePrefix: irsa-operator- 3 | 4 | bases: 5 | - ../crd 6 | - ../rbac 7 | - ../manager 8 | 9 | patchesStrategicMerge: 10 | - manager_auth_proxy_patch.yaml 11 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 14 | args: 15 | - "--secure-listen-address=0.0.0.0:8443" 16 | - "--upstream=http://127.0.0.1:8080/" 17 | - "--logtostderr=true" 18 | - "--v=10" 19 | ports: 20 | - containerPort: 8443 21 | name: https 22 | - name: manager 23 | args: 24 | - "--health-probe-bind-address=:8081" 25 | - "--metrics-bind-address=127.0.0.1:8080" 26 | - "--leader-elect" 27 | - --cluster-name=$CLUSTER_NAME 28 | - "--oidc-provider-arn=$OIDC_PROVIDER_ARN" 29 | - "--permissions-boundaries-policy-arn=" 30 | -------------------------------------------------------------------------------- /config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | args: 12 | - "--config=controller_manager_config.yaml" 13 | volumeMounts: 14 | - name: manager-config 15 | mountPath: /controller_manager_config.yaml 16 | subPath: controller_manager_config.yaml 17 | volumes: 18 | - name: manager-config 19 | configMap: 20 | name: manager-config 21 | -------------------------------------------------------------------------------- /config/manager/controller_manager_config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 2 | kind: ControllerManagerConfig 3 | health: 4 | healthProbeBindAddress: :8081 5 | metrics: 6 | bindAddress: 127.0.0.1:8080 7 | webhook: 8 | port: 9443 9 | leaderElection: 10 | leaderElect: true 11 | resourceName: d8e70b98.voodoo.io 12 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - manager.yaml 6 | 7 | generatorOptions: 8 | disableNameSuffixHash: true 9 | 10 | configMapGenerator: 11 | - files: 12 | - controller_manager_config.yaml 13 | name: manager-config 14 | 15 | images: 16 | - name: controller 17 | newName: localhost:5000/irsa-operator 18 | newTag: latest 19 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: system 7 | --- 8 | apiVersion: v1 9 | kind: ServiceAccount 10 | metadata: 11 | name: oidc-sa 12 | annotations: 13 | eks.amazonaws.com/role-arn: $ROLE_ARN 14 | --- 15 | apiVersion: apps/v1 16 | kind: Deployment 17 | metadata: 18 | name: controller-manager 19 | namespace: system 20 | labels: 21 | control-plane: controller-manager 22 | spec: 23 | selector: 24 | matchLabels: 25 | control-plane: controller-manager 26 | replicas: 1 27 | template: 28 | metadata: 29 | labels: 30 | control-plane: controller-manager 31 | spec: 32 | serviceAccountName: irsa-operator-oidc-sa 33 | securityContext: 34 | #runAsUser: 65532 35 | fsGroup: 1234 36 | containers: 37 | - command: 38 | - /manager 39 | args: 40 | - --leader-elect 41 | image: $CONTROLLER_IMG 42 | name: manager 43 | imagePullPolicy: Always 44 | env: 45 | - value: $LOCALSTACK_ENDPOINT 46 | name: LOCALSTACK_ENDPOINT 47 | securityContext: 48 | allowPrivilegeEscalation: false 49 | livenessProbe: 50 | httpGet: 51 | path: /healthz 52 | port: 8081 53 | initialDelaySeconds: 15 54 | periodSeconds: 20 55 | readinessProbe: 56 | httpGet: 57 | path: /readyz 58 | port: 8081 59 | initialDelaySeconds: 5 60 | periodSeconds: 10 61 | resources: 62 | limits: 63 | cpu: 100m 64 | memory: 30Mi 65 | requests: 66 | cpu: 100m 67 | memory: 20Mi 68 | terminationGracePeriodSeconds: 10 69 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | name: controller-manager-metrics-monitor 9 | namespace: system 10 | spec: 11 | endpoints: 12 | - path: /metrics 13 | port: https 14 | selector: 15 | matchLabels: 16 | control-plane: controller-manager 17 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: ["/metrics"] 7 | verbs: ["get"] 8 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: ["authentication.k8s.io"] 7 | resources: 8 | - tokenreviews 9 | verbs: ["create"] 10 | - apiGroups: ["authorization.k8s.io"] 11 | resources: 12 | - subjectaccessreviews 13 | verbs: ["create"] 14 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: irsa-operator-oidc-sa 12 | namespace: irsa-operator-system 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: controller-manager-metrics-service 7 | namespace: system 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | targetPort: https 13 | selector: 14 | control-plane: controller-manager 15 | -------------------------------------------------------------------------------- /config/rbac/iamroleserviceaccount_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit iamroleserviceaccounts. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: iamroleserviceaccount-editor-role 6 | rules: 7 | - apiGroups: 8 | - irsa.voodoo.io 9 | resources: 10 | - iamroleserviceaccounts 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - irsa.voodoo.io 21 | resources: 22 | - iamroleserviceaccounts/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/iamroleserviceaccount_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view iamroleserviceaccounts. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: iamroleserviceaccount-viewer-role 6 | rules: 7 | - apiGroups: 8 | - irsa.voodoo.io 9 | resources: 10 | - iamroleserviceaccounts 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - irsa.voodoo.io 17 | resources: 18 | - iamroleserviceaccounts/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - role.yaml 3 | - role_binding.yaml 4 | - leader_election_role.yaml 5 | - leader_election_role_binding.yaml 6 | # Comment the following 4 lines if you want to disable 7 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 8 | # which protects your /metrics endpoint. 9 | - auth_proxy_service.yaml 10 | - auth_proxy_role.yaml 11 | - auth_proxy_role_binding.yaml 12 | - auth_proxy_client_clusterrole.yaml 13 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | - coordination.k8s.io 10 | resources: 11 | - configmaps 12 | - leases 13 | verbs: 14 | - get 15 | - list 16 | - watch 17 | - create 18 | - update 19 | - patch 20 | - delete 21 | - apiGroups: 22 | - "" 23 | resources: 24 | - events 25 | verbs: 26 | - create 27 | - patch 28 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: irsa-operator-oidc-sa 12 | namespace: irsa-operator-system 13 | -------------------------------------------------------------------------------- /config/rbac/policy_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit policies. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: policy-editor-role 6 | rules: 7 | - apiGroups: 8 | - irsa.voodoo.io 9 | resources: 10 | - policies 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - irsa.voodoo.io 21 | resources: 22 | - policies/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/policy_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view policies. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: policy-viewer-role 6 | rules: 7 | - apiGroups: 8 | - irsa.voodoo.io 9 | resources: 10 | - policies 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - irsa.voodoo.io 17 | resources: 18 | - policies/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRole 5 | metadata: 6 | creationTimestamp: null 7 | name: manager-role 8 | rules: 9 | - apiGroups: 10 | - "" 11 | resources: 12 | - serviceaccounts 13 | verbs: 14 | - create 15 | - delete 16 | - get 17 | - list 18 | - watch 19 | - apiGroups: 20 | - irsa.voodoo.io 21 | resources: 22 | - iamroleserviceaccounts 23 | verbs: 24 | - create 25 | - delete 26 | - get 27 | - list 28 | - update 29 | - watch 30 | - apiGroups: 31 | - irsa.voodoo.io 32 | resources: 33 | - iamroleserviceaccounts/finalizers 34 | verbs: 35 | - update 36 | - apiGroups: 37 | - irsa.voodoo.io 38 | resources: 39 | - iamroleserviceaccounts/status 40 | verbs: 41 | - get 42 | - update 43 | - apiGroups: 44 | - irsa.voodoo.io 45 | resources: 46 | - policies 47 | verbs: 48 | - create 49 | - delete 50 | - get 51 | - list 52 | - patch 53 | - update 54 | - watch 55 | - apiGroups: 56 | - irsa.voodoo.io 57 | resources: 58 | - policies/finalizers 59 | verbs: 60 | - update 61 | - apiGroups: 62 | - irsa.voodoo.io 63 | resources: 64 | - policies/status 65 | verbs: 66 | - get 67 | - patch 68 | - update 69 | - apiGroups: 70 | - irsa.voodoo.io 71 | resources: 72 | - roles 73 | verbs: 74 | - create 75 | - delete 76 | - get 77 | - list 78 | - patch 79 | - update 80 | - watch 81 | - apiGroups: 82 | - irsa.voodoo.io 83 | resources: 84 | - roles/finalizers 85 | verbs: 86 | - update 87 | - apiGroups: 88 | - irsa.voodoo.io 89 | resources: 90 | - roles/status 91 | verbs: 92 | - get 93 | - patch 94 | - update 95 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: manager-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: irsa-operator-oidc-sa 12 | namespace: irsa-operator-system 13 | -------------------------------------------------------------------------------- /config/rbac/role_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit roles. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: role-editor-role 6 | rules: 7 | - apiGroups: 8 | - irsa.voodoo.io 9 | resources: 10 | - roles 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - irsa.voodoo.io 21 | resources: 22 | - roles/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/role_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view roles. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: role-viewer-role 6 | rules: 7 | - apiGroups: 8 | - irsa.voodoo.io 9 | resources: 10 | - roles 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - irsa.voodoo.io 17 | resources: 18 | - roles/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/samples/irsa_v1alpha1_iamroleserviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: irsa.voodoo.io/v1alpha1 2 | kind: IamRoleServiceAccount 3 | metadata: 4 | name: s3put 5 | spec: 6 | policy: 7 | statement: 8 | - resource: "arn:aws:s3:::test-irsa-4gkut9fl" 9 | action: 10 | - "s3:Get*" 11 | - "s3:List*" 12 | -------------------------------------------------------------------------------- /config/samples/irsa_v1alpha1_policy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: irsa.voodoo.io/v1alpha1 2 | kind: Policy 3 | metadata: 4 | name: policy-sample 5 | spec: 6 | # Add fields here 7 | foo: bar 8 | -------------------------------------------------------------------------------- /config/samples/irsa_v1alpha1_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: irsa.voodoo.io/v1alpha1 2 | kind: Role 3 | metadata: 4 | name: role-sample 5 | spec: 6 | # Add fields here 7 | foo: bar 8 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples you want in your CSV to this file as resources ## 2 | resources: 3 | - irsa_v1alpha1_iamroleserviceaccount.yaml 4 | - irsa_v1alpha1_role.yaml 5 | - irsa_v1alpha1_policy.yaml 6 | # +kubebuilder:scaffold:manifestskustomizesamples 7 | -------------------------------------------------------------------------------- /config/samples/test_deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: s3test 6 | name: s3test 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: s3test 12 | template: 13 | metadata: 14 | labels: 15 | app: s3test 16 | spec: 17 | serviceAccountName: s3put 18 | containers: 19 | - image: amazon/aws-cli 20 | name: aws-cli 21 | command: ["aws", "s3", "ls", "test-irsa-4gkut9fl"] 22 | 23 | -------------------------------------------------------------------------------- /config/scorecard/bases/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scorecard.operatorframework.io/v1alpha3 2 | kind: Configuration 3 | metadata: 4 | name: config 5 | stages: 6 | - parallel: true 7 | tests: [] 8 | -------------------------------------------------------------------------------- /config/scorecard/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - bases/config.yaml 3 | patchesJson6902: 4 | - path: patches/basic.config.yaml 5 | target: 6 | group: scorecard.operatorframework.io 7 | version: v1alpha3 8 | kind: Configuration 9 | name: config 10 | - path: patches/olm.config.yaml 11 | target: 12 | group: scorecard.operatorframework.io 13 | version: v1alpha3 14 | kind: Configuration 15 | name: config 16 | # +kubebuilder:scaffold:patchesJson6902 17 | -------------------------------------------------------------------------------- /config/scorecard/patches/basic.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - basic-check-spec 7 | image: quay.io/operator-framework/scorecard-test:v1.3.0 8 | labels: 9 | suite: basic 10 | test: basic-check-spec-test 11 | -------------------------------------------------------------------------------- /config/scorecard/patches/olm.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - olm-bundle-validation 7 | image: quay.io/operator-framework/scorecard-test:v1.3.0 8 | labels: 9 | suite: olm 10 | test: olm-bundle-validation-test 11 | - op: add 12 | path: /stages/0/tests/- 13 | value: 14 | entrypoint: 15 | - scorecard-test 16 | - olm-crds-have-validation 17 | image: quay.io/operator-framework/scorecard-test:v1.3.0 18 | labels: 19 | suite: olm 20 | test: olm-crds-have-validation-test 21 | - op: add 22 | path: /stages/0/tests/- 23 | value: 24 | entrypoint: 25 | - scorecard-test 26 | - olm-crds-have-resources 27 | image: quay.io/operator-framework/scorecard-test:v1.3.0 28 | labels: 29 | suite: olm 30 | test: olm-crds-have-resources-test 31 | - op: add 32 | path: /stages/0/tests/- 33 | value: 34 | entrypoint: 35 | - scorecard-test 36 | - olm-spec-descriptors 37 | image: quay.io/operator-framework/scorecard-test:v1.3.0 38 | labels: 39 | suite: olm 40 | test: olm-spec-descriptors-test 41 | - op: add 42 | path: /stages/0/tests/- 43 | value: 44 | entrypoint: 45 | - scorecard-test 46 | - olm-status-descriptors 47 | image: quay.io/operator-framework/scorecard-test:v1.3.0 48 | labels: 49 | suite: olm 50 | test: olm-status-descriptors-test 51 | -------------------------------------------------------------------------------- /controllers/README.md: -------------------------------------------------------------------------------- 1 | # testing 2 | 3 | hack : 4 | - in order not to miss a resource state with the find function (resources are polled on an `interval` basis), 5 | - a `testingDelay` variable can be set in each controller so it delays event processing by an amount of time longer than interval (`interval + 100ms`) 6 | -------------------------------------------------------------------------------- /controllers/aws.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" 5 | ) 6 | 7 | type AwsManager interface { 8 | AwsPolicyManager 9 | AwsRoleManager 10 | } 11 | 12 | type AwsPolicyManager interface { 13 | PolicyExists(arn string) (bool, error) 14 | GetStatement(arn string) ([]api.StatementSpec, error) 15 | GetPolicyARN(pathPrefix, uniqueName string) (string, error) 16 | CreatePolicy(api.Policy) error 17 | UpdatePolicy(api.Policy) error 18 | DeletePolicy(policyARN string) error 19 | } 20 | 21 | type AwsRoleManager interface { 22 | RoleExists(roleName string) (bool, error) 23 | CreateRole(role api.Role, permissionsBoundariesPolicyARN string) error 24 | DeleteRole(roleName string) error 25 | AttachRolePolicy(roleName, policyARN string) error 26 | GetAttachedRolePoliciesARNs(roleName string) ([]string, error) 27 | GetRoleARN(roleName string) (string, error) 28 | DetachRolePolicy(roleName, policyARN string) error 29 | } 30 | -------------------------------------------------------------------------------- /controllers/aws_test.go: -------------------------------------------------------------------------------- 1 | package controllers_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "strings" 8 | "sync" 9 | 10 | api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" 11 | "github.com/VoodooTeam/irsa-operator/aws" 12 | ) 13 | 14 | func newAwsFake() *awsFake { 15 | return &awsFake{ 16 | stacks: &sync.Map{}, 17 | } 18 | } 19 | 20 | type awsFake struct { 21 | stacks *sync.Map // used as : map[resourceName(string)]stack(awsStack) 22 | } 23 | 24 | type awsStack struct { 25 | policy aws.AwsPolicy 26 | role awsRole 27 | errors map[awsMethod]struct{} 28 | events []string 29 | } 30 | 31 | type awsRole struct { 32 | name string 33 | arn string 34 | attachedPolicies []string 35 | permissionsBoundariesPolicyARN string 36 | } 37 | 38 | type awsMethod string 39 | 40 | const ( 41 | policyExists awsMethod = "policyExists" 42 | getStatement awsMethod = "getStatement" 43 | updatePolicy awsMethod = "updatePolicy" 44 | createPolicy awsMethod = "createPolicy" 45 | deletePolicy awsMethod = "deletePolicy" 46 | getPolicyARN awsMethod = "getPolicyARN" 47 | createRole awsMethod = "createRole" 48 | attachRolePolicy awsMethod = "attachRolePolicy" 49 | detachRolePolicy awsMethod = "detachRolePolicy" 50 | deleteRole awsMethod = "deleteRole" 51 | roleExists awsMethod = "roleExists" 52 | getRoleARN awsMethod = "getRoleARN" 53 | getAttachedRolePoliciesARNs awsMethod = "getAttachedRolePoliciesARNs" 54 | ) 55 | 56 | func (s *awsFake) PolicyExists(arn string) (bool, error) { 57 | cN := getResourceName(arn) 58 | if err := s.shouldFailAt(cN, policyExists); err != nil { 59 | return false, err 60 | } 61 | 62 | stack, ok := s.stacks.Load(cN) 63 | if !ok { 64 | return false, nil 65 | } 66 | 67 | return stack.(awsStack).policy.ARN != "", nil 68 | } 69 | 70 | func (s *awsFake) CreatePolicy(policy api.Policy) error { 71 | n := policy.ObjectMeta.Name 72 | if err := s.shouldFailAt(n, createPolicy); err != nil { 73 | return err 74 | } 75 | 76 | // todo abstract away this step (same code in all methods) 77 | raw, ok := s.stacks.Load(n) 78 | if !ok { 79 | return errors.New("policy doesn't exists") 80 | } 81 | stack := raw.(awsStack) 82 | 83 | stack.policy = aws.AwsPolicy{ARN: policyARN(policy), Statement: policy.Spec.Statement} 84 | s.stacks.Store(n, stack) 85 | return nil 86 | } 87 | 88 | func (s *awsFake) UpdatePolicy(policy api.Policy) error { 89 | n := policy.ObjectMeta.Name 90 | if err := s.shouldFailAt(n, updatePolicy); err != nil { 91 | return err 92 | } 93 | raw, ok := s.stacks.Load(n) 94 | if !ok { 95 | return errors.New("policy doesn't exists") 96 | } 97 | 98 | stack := raw.(awsStack) 99 | stack.policy.Statement = policy.Spec.Statement 100 | s.stacks.Store(n, stack) 101 | return nil 102 | } 103 | 104 | func (s *awsFake) DeletePolicy(arn string) error { 105 | cN := getResourceName(arn) 106 | if err := s.shouldFailAt(cN, deletePolicy); err != nil { 107 | return err 108 | } 109 | 110 | raw, ok := s.stacks.Load(cN) 111 | if !ok { 112 | return errors.New("stack doesn't exists") 113 | } 114 | 115 | stack := raw.(awsStack) 116 | stack.policy = aws.AwsPolicy{} 117 | s.stacks.Store(cN, stack) 118 | return nil 119 | } 120 | 121 | func (s *awsFake) GetPolicyARN(pathPrefix, awsName string) (string, error) { 122 | cN := getPolicyNameFromAwsName(awsName) 123 | if err := s.shouldFailAt(cN, getPolicyARN); err != nil { 124 | return "", err 125 | } 126 | 127 | stack, ok := s.stacks.Load(cN) 128 | if !ok { 129 | return "", errors.New("stack doesn't exists") 130 | } 131 | 132 | return stack.(awsStack).policy.ARN, nil 133 | } 134 | 135 | func (s *awsFake) GetStatement(arn string) ([]api.StatementSpec, error) { 136 | n := getResourceName(arn) 137 | if err := s.shouldFailAt(n, getStatement); err != nil { 138 | return nil, err 139 | } 140 | 141 | stack, ok := s.stacks.Load(n) 142 | if !ok { 143 | return nil, errors.New("stack doesn't exists") 144 | } 145 | return stack.(awsStack).policy.Statement, nil 146 | } 147 | 148 | func (s *awsFake) CreateRole(r api.Role, permissionsBoundariesPolicyARN string) error { 149 | n := r.ObjectMeta.Name 150 | if err := s.shouldFailAt(n, createRole); err != nil { 151 | return err 152 | } 153 | 154 | raw, ok := s.stacks.Load(n) 155 | if !ok { 156 | return errors.New("policy doesn't exists") 157 | } 158 | 159 | stack := raw.(awsStack) 160 | stack.role = awsRole{name: roleName(r), arn: roleArn(r), attachedPolicies: []string{}, permissionsBoundariesPolicyARN: permissionsBoundariesPolicyARN} 161 | s.stacks.Store(n, stack) 162 | return nil 163 | } 164 | 165 | func (s *awsFake) DeleteRole(roleName string) error { 166 | cN := getResourceName(roleName) 167 | if err := s.shouldFailAt(cN, deleteRole); err != nil { 168 | return err 169 | } 170 | 171 | raw, ok := s.stacks.Load(cN) 172 | if !ok { 173 | return errors.New("stack doesn't exists") 174 | } 175 | 176 | stack := raw.(awsStack) 177 | stack.role = awsRole{} 178 | s.stacks.Store(cN, stack) 179 | return nil 180 | } 181 | 182 | func (s *awsFake) RoleExists(roleName string) (bool, error) { 183 | cN := getClusterNameFromRoleName(roleName) 184 | if err := s.shouldFailAt(cN, roleExists); err != nil { 185 | return false, err 186 | } 187 | 188 | raw, ok := s.stacks.Load(cN) 189 | if !ok { 190 | return false, errors.New("stack doesn't exists") 191 | } 192 | 193 | stack := raw.(awsStack) 194 | return stack.role.name != "", nil 195 | } 196 | 197 | func (s *awsFake) GetRoleARN(roleName string) (string, error) { 198 | cN := getClusterNameFromRoleName(roleName) 199 | if err := s.shouldFailAt(cN, getRoleARN); err != nil { 200 | return "", err 201 | } 202 | 203 | raw, ok := s.stacks.Load(cN) 204 | if !ok { 205 | return "", errors.New("stack doesn't exists") 206 | } 207 | 208 | stack := raw.(awsStack) 209 | return stack.role.arn, nil 210 | } 211 | 212 | func (s *awsFake) GetAttachedRolePoliciesARNs(roleName string) ([]string, error) { 213 | cN := getClusterNameFromRoleName(roleName) 214 | if err := s.shouldFailAt(cN, getAttachedRolePoliciesARNs); err != nil { 215 | return nil, err 216 | } 217 | 218 | raw, ok := s.stacks.Load(cN) 219 | if !ok { 220 | return nil, errors.New("stack doesn't exists") 221 | } 222 | 223 | stack := raw.(awsStack) 224 | return stack.role.attachedPolicies, nil 225 | } 226 | 227 | func (s *awsFake) AttachRolePolicy(roleName, policyARN string) error { 228 | cN := getClusterNameFromRoleName(roleName) 229 | if err := s.shouldFailAt(cN, attachRolePolicy); err != nil { 230 | return err 231 | } 232 | 233 | raw, ok := s.stacks.Load(cN) 234 | if !ok { 235 | return errors.New("stack doesn't exists") 236 | } 237 | 238 | stack := raw.(awsStack) 239 | stack.role.attachedPolicies = append(stack.role.attachedPolicies, policyARN) 240 | s.stacks.Store(cN, stack) 241 | return nil 242 | } 243 | 244 | func (s *awsFake) DetachRolePolicy(roleName, policyARN string) error { 245 | cN := getClusterNameFromRoleName(roleName) 246 | if err := s.shouldFailAt(cN, detachRolePolicy); err != nil { 247 | return err 248 | } 249 | 250 | raw, ok := s.stacks.Load(cN) 251 | if !ok { 252 | return errors.New("stack doesn't exists") 253 | } 254 | 255 | stack := raw.(awsStack) 256 | newAttachedPolicies := []string{} 257 | for _, p := range stack.role.attachedPolicies { 258 | if p != policyARN { 259 | newAttachedPolicies = append(newAttachedPolicies, p) 260 | } 261 | } 262 | stack.role.attachedPolicies = newAttachedPolicies 263 | s.stacks.Store(cN, stack) 264 | return nil 265 | } 266 | 267 | // shouldFailAt does 2 (!) things : 268 | // - abstract the error mechanism 269 | // - toggle the next result that will be returned 270 | func (s *awsFake) shouldFailAt(n string, m awsMethod) error { 271 | raw, found := s.stacks.Load(n) 272 | if !found { 273 | log.Fatal("stack not found :", n, ",", string(m)) 274 | } 275 | stack := raw.(awsStack) 276 | 277 | // an error exists for method key 278 | // delete the error 279 | // we add this event 280 | // store the new stack[clusterName] 281 | if _, found := stack.errors[m]; found { 282 | delete(stack.errors, m) 283 | stack.events = append(stack.events, fmt.Sprintf("failure : %s", string(m))) 284 | s.stacks.Store(n, stack) 285 | return errors.New(string(m)) 286 | } 287 | 288 | // otherwise 289 | // we store anything at method key 290 | // add the event 291 | // store the new stack[clusterName] 292 | stack.events = append(stack.events, fmt.Sprintf("success : %s", string(m))) 293 | s.stacks.Store(n, stack) 294 | return nil 295 | } 296 | 297 | func policyARN(p api.Policy) string { 298 | arn := genUniqueName(p.Namespace, p.Name) 299 | return arn 300 | } 301 | 302 | func roleName(r api.Role) string { 303 | rN := genUniqueName(r.Namespace, r.Name) 304 | return rN 305 | } 306 | 307 | func roleArn(r api.Role) string { 308 | rN := genUniqueName(r.Namespace, r.Name) 309 | return "arn:" + rN 310 | } 311 | 312 | func genUniqueName(ns, n string) string { 313 | // we don't have to build something realistic, just something that is convenient for testing 314 | return fmt.Sprintf("%s-%s", ns, n) 315 | } 316 | 317 | func getResourceName(roleNameOrPolicyARN string) string { 318 | return strings.Split(roleNameOrPolicyARN, "-")[1] 319 | } 320 | 321 | func getPolicyNameFromAwsName(name string) string { 322 | return strings.Split(name, "-")[4] 323 | } 324 | func getClusterNameFromRoleName(name string) string { 325 | return strings.Split(name, "-")[4] 326 | } 327 | -------------------------------------------------------------------------------- /controllers/helper.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | // Helper functions to check and remove string from a slice of strings. 4 | func containsString(slice []string, s string) bool { 5 | for _, item := range slice { 6 | if item == s { 7 | return true 8 | } 9 | } 10 | return false 11 | } 12 | 13 | func removeString(slice []string, s string) (result []string) { 14 | for _, item := range slice { 15 | if item == s { 16 | continue 17 | } 18 | result = append(result, item) 19 | } 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /controllers/iamroleserviceaccount_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers_test 2 | 3 | import ( 4 | api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | var _ = Describe("IamRoleServiceAccount validity check", func() { 10 | Context("if the spec.policy is empty", func() { 11 | invalidPolicySpec := api.PolicySpec{} 12 | 13 | It("fails at submission", func() { 14 | Expect( 15 | api.NewIamRoleServiceAccount(validName(), testns, invalidPolicySpec).Validate(), 16 | ).ShouldNot(Succeed()) 17 | }) 18 | }) 19 | 20 | Context("if the spec.policy is ok", func() { 21 | validPolicy := api.PolicySpec{ 22 | Statement: []api.StatementSpec{ 23 | {Resource: "arn:aws:s3:::my_corporate_bucket/exampleobject.png", Action: []string{"act1"}}, 24 | }, 25 | } 26 | 27 | Context("if everything else is also ok", func() { 28 | irsa := api.NewIamRoleServiceAccount(validName(), testns, validPolicy) 29 | It("it passes validation", func() { 30 | Expect(irsa.Validate()).Should(Succeed()) 31 | }) 32 | }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /controllers/model_test.go: -------------------------------------------------------------------------------- 1 | package controllers_test 2 | 3 | import ( 4 | "log" 5 | "math/rand" 6 | "sync" 7 | 8 | api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" 9 | "github.com/VoodooTeam/irsa-operator/aws" 10 | "github.com/davecgh/go-spew/spew" 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | var _ = Describe("cluster state", func() { 16 | // generate n awsState and clusterStates ? 17 | // define the converged state (everything created & k8s resources at ok) 18 | // expect the converged state to be reached before timeout 19 | var names []string 20 | 21 | It("must converge", func() { 22 | count := 100 23 | log.Println("will run against ", count, " different envs") 24 | for i := 0; i < count; i++ { 25 | names = append(names, validName()) 26 | } 27 | 28 | var wg sync.WaitGroup 29 | for _, n := range names { 30 | wg.Add(1) 31 | go run(n, &wg) 32 | } 33 | wg.Wait() 34 | }) 35 | }) 36 | 37 | func run(irsaName string, wg *sync.WaitGroup) { 38 | defer wg.Done() 39 | var stack awsStack 40 | initialErrors := getInitialErrs() 41 | 42 | // will log in case of failure the final state and the events that lead there 43 | defer func(iE map[awsMethod]struct{}) { 44 | if recover() != nil { 45 | log.Println(">>>") 46 | log.Println("inital errors :") 47 | spew.Dump(iE) 48 | log.Println("<<<") 49 | 50 | log.Println("ended up with stack state :") 51 | log.Println(">>>") 52 | spew.Dump(stack) 53 | log.Println("<<<") 54 | 55 | log.Println("k8s resources") 56 | log.Println(">>>") 57 | spew.Dump(getRole(irsaName, testns)) 58 | spew.Dump(getPolicy(irsaName, testns)) 59 | spew.Dump(getIrsa(irsaName, testns)) 60 | log.Println("<<<") 61 | 62 | GinkgoT().FailNow() 63 | } 64 | }(copyErrs(initialErrors)) 65 | 66 | // ensure the stack doesnt exists yet 67 | raw, ok := st.stacks.Load(irsaName) 68 | Expect(raw).To(BeNil()) 69 | Expect(ok).To(BeFalse()) 70 | 71 | // initialize the awsStack for the current cluster 72 | st.stacks.Store(irsaName, awsStack{ 73 | policy: aws.AwsPolicy{}, 74 | role: awsRole{}, 75 | errors: initialErrors, 76 | events: []string{}, 77 | }) 78 | 79 | submittedPolicy := api.PolicySpec{ 80 | Statement: []api.StatementSpec{ 81 | {Resource: "arn:aws:s3:::my_corporate_bucket/exampleobject.png", Action: []string{"act1"}}, 82 | }, 83 | } 84 | 85 | { // k8s 86 | { 87 | // we submit the iamroleserviceaccount Spec to k8s 88 | createResource( 89 | api.NewIamRoleServiceAccount(irsaName, testns, submittedPolicy), 90 | ).Should(Succeed()) 91 | } 92 | { // every CR must eventually reach an OK status & serviceAccount has been created 93 | foundPolicyInCondition(irsaName, testns, api.CrOK).Should(BeTrue()) 94 | foundRoleInCondition(irsaName, testns, api.CrOK).Should(BeTrue()) 95 | foundIrsaInCondition(irsaName, testns, api.IrsaOK).Should(BeTrue()) 96 | findSa(irsaName, testns).Should(BeTrue()) 97 | } 98 | } 99 | 100 | { // aws resources checks 101 | // once the cluster is ok, we check what it did to the aws stack 102 | raw, ok := st.stacks.Load(irsaName) 103 | Expect(raw).ShouldNot(BeNil()) 104 | Expect(ok).Should(BeTrue()) 105 | stack = raw.(awsStack) 106 | 107 | // policy 108 | Expect(stack.policy).ShouldNot(BeNil()) 109 | Expect(stack.policy.Statement).To(Equal(submittedPolicy.Statement)) 110 | 111 | // role 112 | Expect(stack.role).ShouldNot(BeNil()) 113 | Expect(len(stack.role.attachedPolicies)).To(Equal(1)) 114 | Expect(stack.role.attachedPolicies[0]).To(Equal(stack.policy.ARN)) 115 | 116 | { 117 | iamrsa := getIrsa(irsaName, testns) 118 | Expect(iamrsa.Spec.Policy.Statement).To(Equal(stack.policy.Statement)) 119 | } 120 | { 121 | role := getRole(irsaName, testns) 122 | Expect(role.Spec.PolicyARN).To(Equal(stack.policy.ARN)) 123 | Expect(role.Spec.RoleARN).To(Equal(stack.role.arn)) 124 | } 125 | { 126 | policy := getPolicy(irsaName, testns) 127 | Expect(policy.Spec.ARN).To(Equal(stack.policy.ARN)) 128 | } 129 | } 130 | } 131 | 132 | func getInitialErrs() map[awsMethod]struct{} { 133 | methods := []awsMethod{ 134 | policyExists, 135 | getStatement, 136 | updatePolicy, 137 | createPolicy, 138 | deletePolicy, 139 | getPolicyARN, 140 | createRole, 141 | attachRolePolicy, 142 | detachRolePolicy, 143 | deleteRole, 144 | roleExists, 145 | getRoleARN, 146 | getAttachedRolePoliciesARNs, 147 | } 148 | 149 | errs := make(map[awsMethod]struct{}) 150 | for _, m := range methods { 151 | if rand.Float32() < 0.5 { 152 | errs[m] = struct{}{} 153 | } 154 | } 155 | return errs 156 | } 157 | 158 | func copyErrs(in map[awsMethod]struct{}) map[awsMethod]struct{} { 159 | out := make(map[awsMethod]struct{}) 160 | for k, v := range in { 161 | out[k] = v 162 | } 163 | return out 164 | } 165 | -------------------------------------------------------------------------------- /controllers/policy_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/go-logr/logr" 8 | "k8s.io/apimachinery/pkg/api/errors" 9 | k8serrors "k8s.io/apimachinery/pkg/api/errors" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | ctrl "sigs.k8s.io/controller-runtime" 12 | "sigs.k8s.io/controller-runtime/pkg/client" 13 | "sigs.k8s.io/controller-runtime/pkg/controller" 14 | 15 | api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" 16 | ) 17 | 18 | func NewPolicyReconciler(client client.Client, scheme *runtime.Scheme, awspm AwsPolicyManager, logger logr.Logger, cN string) *PolicyReconciler { 19 | return &PolicyReconciler{ 20 | Client: client, 21 | log: logger, 22 | scheme: scheme, 23 | awsPM: awspm, 24 | finalizerID: "policy.irsa.voodoo.io", 25 | clusterName: cN, 26 | } 27 | } 28 | 29 | // PolicyReconciler reconciles a Policy object 30 | type PolicyReconciler struct { 31 | client.Client 32 | scheme *runtime.Scheme 33 | awsPM AwsPolicyManager 34 | log logr.Logger 35 | 36 | finalizerID string 37 | clusterName string 38 | } 39 | 40 | // +kubebuilder:rbac:groups=irsa.voodoo.io,resources=policies,verbs=get;list;watch;create;update;patch;delete 41 | // +kubebuilder:rbac:groups=irsa.voodoo.io,resources=policies/status,verbs=get;update;patch 42 | // +kubebuilder:rbac:groups=irsa.voodoo.io,resources=policies/finalizers,verbs=update 43 | 44 | // Reconcile is called each time an event occurs on an api.Policy resource 45 | func (r *PolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 46 | var policy *api.Policy 47 | { // extract policy from the request 48 | var ok bool 49 | policy, ok = r.getPolicyFromReq(ctx, req) 50 | if !ok { 51 | // didn't complete, requeing 52 | return ctrl.Result{Requeue: true}, nil 53 | } 54 | if policy == nil { 55 | // not found, has been deleted 56 | return ctrl.Result{}, nil 57 | } 58 | } 59 | 60 | { // finalizer registration & execution 61 | if policy.IsPendingDeletion() { 62 | if ok := r.executeFinalizerIfPresent(ctx, policy); !ok { 63 | return ctrl.Result{Requeue: true}, nil 64 | } 65 | // ok, no requeue 66 | return ctrl.Result{}, nil 67 | } else { 68 | if ok := r.registerFinalizerIfNeeded(policy); !ok { 69 | return ctrl.Result{Requeue: true}, nil 70 | } 71 | } 72 | } 73 | 74 | // the resource has just been created 75 | if policy.Status.Condition == api.CrSubmitted { 76 | return r.admissionStep(ctx, policy) 77 | } 78 | 79 | // for whatever condition we'll try to check the aws policy needs to be created or updated 80 | return r.reconcilerRoutine(ctx, policy) 81 | } 82 | 83 | // SetupWithManager sets up the controller with the Manager. 84 | func (r *PolicyReconciler) SetupWithManager(mgr ctrl.Manager) error { 85 | return ctrl.NewControllerManagedBy(mgr). 86 | For(&api.Policy{}). 87 | WithOptions(controller.Options{ 88 | MaxConcurrentReconciles: 10, 89 | }). 90 | Complete(r) 91 | } 92 | 93 | // 94 | // privates 95 | // 96 | 97 | // admissionStep does spec validation 98 | func (r *PolicyReconciler) admissionStep(ctx context.Context, p *api.Policy) (ctrl.Result, error) { 99 | if err := p.Validate(r.clusterName); err != nil { // the policy spec is not valid 100 | ok := r.updateStatus(ctx, p, api.NewPolicyStatus(api.CrError, err.Error())) 101 | return ctrl.Result{Requeue: !ok}, nil 102 | } 103 | 104 | // update the role status to "progressing" 105 | ok := r.updateStatus(ctx, p, api.NewPolicyStatus(api.CrProgressing, "passed validation")) 106 | return ctrl.Result{Requeue: !ok}, nil 107 | } 108 | 109 | // reconcilerRoutine is an infinite loop attempting to make the aws IAM policy converge to the policy.Spec 110 | func (r *PolicyReconciler) reconcilerRoutine(ctx context.Context, policy *api.Policy) (ctrl.Result, error) { 111 | if policy.Spec.ARN == "" { // no arn in spec 112 | foundARN, err := r.awsPM.GetPolicyARN(policy.PathPrefix(r.clusterName), policy.AwsName(r.clusterName)) 113 | if err != nil { 114 | r.updateStatus(ctx, policy, api.NewPolicyStatus(api.CrError, err.Error())) 115 | return ctrl.Result{Requeue: true}, nil 116 | } 117 | 118 | if foundARN == "" { // no policy on aws, let's create it 119 | if err := r.awsPM.CreatePolicy(*policy); err != nil { // creation failed 120 | r.updateStatus(ctx, policy, api.NewPolicyStatus(api.CrError, "failed to create policy on AWS : "+err.Error())) 121 | } else { // creation succeeded 122 | r.updateStatus(ctx, policy, api.NewPolicyStatus(api.CrProgressing, "policy created on AWS")) 123 | } 124 | return ctrl.Result{Requeue: true}, nil 125 | } 126 | 127 | // a policy already exists on aws 128 | r.setPolicyArnField(ctx, foundARN, policy) // we set the policyARN field 129 | r.updateStatus(ctx, policy, api.NewPolicyStatus(api.CrProgressing, "policy found on AWS")) 130 | return ctrl.Result{}, nil // modifying the policyARN field will generate a new event 131 | 132 | } else { // policy ARN in spec 133 | policyStatement, err := r.awsPM.GetStatement(policy.Spec.ARN) 134 | if err != nil { 135 | r.updateStatus(ctx, policy, api.NewPolicyStatus(api.CrError, "get policyStatement on AWS failed : "+err.Error())) 136 | return ctrl.Result{Requeue: true}, nil 137 | } 138 | 139 | if !api.StatementEquals(policy.Spec.Statement, policyStatement) { // policy on aws doesn't correspond to the one in Spec 140 | // we update the aws policy 141 | if err := r.awsPM.UpdatePolicy(*policy); err != nil { 142 | r.updateStatus(ctx, policy, api.NewPolicyStatus(api.CrError, "update policyStatement on AWS failed : "+err.Error())) 143 | return ctrl.Result{Requeue: true}, nil 144 | } 145 | r.updateStatus(ctx, policy, api.NewPolicyStatus(api.CrProgressing, "update policyStatement on AWS succeeded")) 146 | return ctrl.Result{Requeue: true}, nil 147 | } 148 | } 149 | 150 | if policy.Status.Condition != api.CrOK { 151 | r.updateStatus(ctx, policy, api.NewPolicyStatus(api.CrOK, "all done")) 152 | } 153 | 154 | return ctrl.Result{}, nil 155 | } 156 | 157 | func (r *PolicyReconciler) executeFinalizerIfPresent(ctx context.Context, policy *api.Policy) (completed bool) { 158 | if !containsString(policy.ObjectMeta.Finalizers, r.finalizerID) { // no finalizer to execute 159 | return true 160 | } 161 | 162 | if policy.Spec.ARN == "" { // the operator hasn't created the policy yet, all done 163 | return r.removeFinalizer(ctx, policy) 164 | } 165 | 166 | if exists, err := r.awsPM.PolicyExists(policy.Spec.ARN); !exists && err == nil { // policy already deleted, all done 167 | return r.removeFinalizer(ctx, policy) 168 | } 169 | 170 | // delete the policy on AWS 171 | if err := r.awsPM.DeletePolicy(policy.Spec.ARN); err != nil { // deletion failed 172 | r.updateStatus(ctx, policy, api.NewPolicyStatus(api.CrError, "delete Policy on AWS failed : "+err.Error())) 173 | return false 174 | } 175 | 176 | { // let's delete the policy (k8s resource) itself 177 | if err := r.Delete(ctx, policy); err != nil && !k8serrors.IsNotFound(err) { 178 | r.controllerErrLog(policy, "delete policy", err) 179 | return false 180 | } 181 | } 182 | 183 | return r.removeFinalizer(ctx, policy) 184 | } 185 | 186 | func (r *PolicyReconciler) removeFinalizer(ctx context.Context, p *api.Policy) bool { 187 | p.ObjectMeta.Finalizers = removeString(p.ObjectMeta.Finalizers, r.finalizerID) 188 | return r.Update(ctx, p) == nil 189 | } 190 | 191 | func (r *PolicyReconciler) updateStatus(ctx context.Context, p *api.Policy, status api.PolicyStatus) bool { 192 | p.Status = status 193 | return r.Status().Update(ctx, p) == nil 194 | } 195 | 196 | func (r *PolicyReconciler) registerFinalizerIfNeeded(role *api.Policy) (completed bool) { 197 | if !containsString(role.ObjectMeta.Finalizers, r.finalizerID) { // the finalizer isn't registered yet 198 | // we add it to the role. 199 | role.ObjectMeta.Finalizers = append(role.ObjectMeta.Finalizers, r.finalizerID) 200 | if err := r.Update(context.Background(), role); err != nil { 201 | r.controllerErrLog(role, "setting finalizer", err) 202 | return false 203 | } 204 | } 205 | return true 206 | } 207 | 208 | func (r *PolicyReconciler) controllerErrLog(resource fullNamer, msg string, err error) { 209 | r.log.Info(fmt.Sprintf("[%s] : Failed to %s : %s", resource.FullName(), msg, err)) 210 | } 211 | 212 | func (r *PolicyReconciler) getPolicyFromReq(ctx context.Context, req ctrl.Request) (policy *api.Policy, completed bool) { 213 | p := &api.Policy{} 214 | if err := r.Get(ctx, req.NamespacedName, p); err != nil { 215 | if errors.IsNotFound(err) { 216 | return nil, true 217 | } 218 | 219 | r.controllerErrLog(p, "get resource failed", err) 220 | return nil, false 221 | } 222 | 223 | return p, true 224 | } 225 | 226 | func (r *PolicyReconciler) setPolicyArnField(ctx context.Context, arn string, policy *api.Policy) (completed bool) { 227 | policy.Spec.ARN = arn 228 | if err := r.Update(ctx, policy); err != nil { 229 | r.controllerErrLog(policy, "set policy.Spec.ARN", err) 230 | return false 231 | } 232 | return true 233 | } 234 | -------------------------------------------------------------------------------- /controllers/policy_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" 8 | ) 9 | 10 | var _ = Describe("Awspolicy validity check", func() { 11 | Context("When creating an Awspolicy", func() { 12 | clusterName := randString() 13 | Context("if the spec.statement is nil", func() { 14 | It("fails at submission", func() { 15 | Expect( 16 | api.NewPolicy(validName(), testns, nil).Validate(clusterName), 17 | ).ShouldNot(Succeed()) 18 | }) 19 | }) 20 | 21 | Context("if the spec.statement is an empty array", func() { 22 | name := validName() 23 | It("fails at validation", func() { 24 | Expect( 25 | api.NewPolicy(name, testns, []api.StatementSpec{}).Validate(clusterName), 26 | ).ShouldNot(Succeed()) 27 | }) 28 | }) 29 | 30 | Context("if the spec.statement[*].resource is not a valid ARN", func() { 31 | name := validName() 32 | 33 | It("fails at validation", func() { 34 | Expect( 35 | api.NewPolicy(name, testns, []api.StatementSpec{ 36 | {Resource: "not an arn", Action: []string{"do something"}}, 37 | }).Validate(clusterName), 38 | ).ShouldNot(Succeed()) 39 | }) 40 | }) 41 | 42 | Context("if the spec.statement[*].action is an empty array", func() { 43 | name := validName() 44 | validARN := "arn:aws:s3:::my_corporate_bucket/exampleobject.png" 45 | 46 | It("fails at validation", func() { 47 | Expect( 48 | api.NewPolicy(name, testns, []api.StatementSpec{ 49 | {Resource: validARN, Action: []string{}}, 50 | }).Validate(clusterName), 51 | ).ShouldNot(Succeed()) 52 | }) 53 | }) 54 | 55 | Context("if everything is ok", func() { 56 | name := validName() 57 | validARN := "arn:aws:s3:::my_corporate_bucket/exampleobject.png" 58 | 59 | It("passes the api submission", func() { 60 | Expect( 61 | api.NewPolicy(name, testns, []api.StatementSpec{ 62 | {Resource: validARN, Action: []string{"an:action"}}, 63 | }).Validate(clusterName), 64 | ).Should(Succeed()) 65 | }) 66 | }) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /controllers/role_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/go-logr/logr" 9 | k8serrors "k8s.io/apimachinery/pkg/api/errors" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | "k8s.io/apimachinery/pkg/types" 12 | ctrl "sigs.k8s.io/controller-runtime" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | "sigs.k8s.io/controller-runtime/pkg/controller" 15 | 16 | api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" 17 | ) 18 | 19 | func NewRoleReconciler( 20 | client client.Client, 21 | scheme *runtime.Scheme, 22 | awsrm AwsRoleManager, 23 | logger logr.Logger, 24 | clusterName, 25 | permissionsBoundariesPolicyARN string) *RoleReconciler { 26 | return &RoleReconciler{ 27 | Client: client, 28 | scheme: scheme, 29 | awsRM: awsrm, 30 | log: logger, 31 | finalizerID: "role.irsa.voodoo.io", 32 | clusterName: clusterName, 33 | permissionsBoundariesPolicyARN: permissionsBoundariesPolicyARN, 34 | } 35 | } 36 | 37 | // RoleReconciler reconciles a Role object 38 | type RoleReconciler struct { 39 | client.Client 40 | log logr.Logger 41 | scheme *runtime.Scheme 42 | awsRM AwsRoleManager 43 | finalizerID string 44 | clusterName string 45 | permissionsBoundariesPolicyARN string 46 | } 47 | 48 | // +kubebuilder:rbac:groups=irsa.voodoo.io,resources=roles,verbs=get;list;watch;create;update;patch;delete 49 | // +kubebuilder:rbac:groups=irsa.voodoo.io,resources=roles/status,verbs=get;update;patch 50 | // +kubebuilder:rbac:groups=irsa.voodoo.io,resources=roles/finalizers,verbs=update 51 | 52 | func (r *RoleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 53 | var role *api.Role 54 | { // extract role from the request 55 | var ok bool 56 | role, ok = r.getRoleFromReq(ctx, req) 57 | if !ok { 58 | // didn't complete, requeing 59 | return ctrl.Result{Requeue: true}, nil 60 | } 61 | if role == nil { 62 | // not found, has been deleted 63 | return ctrl.Result{}, nil 64 | } 65 | } 66 | 67 | { // finalizer registration & execution 68 | if role.IsPendingDeletion() { 69 | // deletion requested, execute finalizer 70 | if ok := r.executeFinalizerIfPresent(role); !ok { 71 | return ctrl.Result{Requeue: true}, nil 72 | } 73 | // all done, no requeue 74 | return ctrl.Result{}, nil 75 | } else { 76 | if ok := r.registerFinalizerIfNeeded(role); !ok { 77 | return ctrl.Result{Requeue: true}, nil 78 | } 79 | } 80 | } 81 | 82 | // handlers 83 | if role.Status.Condition == api.CrSubmitted { 84 | return r.admissionStep(ctx, role) 85 | } 86 | 87 | return r.reconcilerRoutine(ctx, role) 88 | } 89 | 90 | // SetupWithManager sets up the controller with the Manager. 91 | func (r *RoleReconciler) SetupWithManager(mgr ctrl.Manager) error { 92 | return ctrl.NewControllerManagedBy(mgr). 93 | For(&api.Role{}). 94 | WithOptions(controller.Options{ 95 | MaxConcurrentReconciles: 10, 96 | }). 97 | Complete(r) 98 | } 99 | 100 | // 101 | // privates 102 | // 103 | 104 | // admissionStep does spec validation 105 | func (r *RoleReconciler) admissionStep(ctx context.Context, role *api.Role) (ctrl.Result, error) { 106 | if err := role.Validate(r.clusterName); err != nil { // the role spec is invalid 107 | ok := r.updateStatus(ctx, role, api.NewRoleStatus(api.CrError, err.Error())) 108 | return ctrl.Result{Requeue: !ok}, nil 109 | } 110 | 111 | // update the role to "progressing" 112 | ok := r.updateStatus(ctx, role, api.NewRoleStatus(api.CrProgressing, "passed validation")) 113 | return ctrl.Result{Requeue: !ok}, nil 114 | } 115 | 116 | // reconcilerRoutine is an infinite loop attempting to make the aws IAM role, with it's attachment converge to the role.Spec 117 | func (r *RoleReconciler) reconcilerRoutine(ctx context.Context, role *api.Role) (ctrl.Result, error) { 118 | if role.Spec.RoleARN == "" { // no arn in spec 119 | roleExistsOnAws, err := r.awsRM.RoleExists(role.AwsName(r.clusterName)) 120 | if err != nil { // failed to check if roles exists on AWS 121 | r.updateStatus(ctx, role, api.NewRoleStatus(api.CrError, "failed to check if role exists on AWS")) 122 | return ctrl.Result{Requeue: true}, nil 123 | } 124 | 125 | if roleExistsOnAws { 126 | r.setRoleArnField(ctx, role) 127 | r.updateStatus(ctx, role, api.NewRoleStatus(api.CrProgressing, "role found on AWS")) 128 | return ctrl.Result{}, nil // updating the role leads to an automatic requeue 129 | } 130 | 131 | if ok := r.createRoleOnAws(ctx, role, r.permissionsBoundariesPolicyARN); !ok { 132 | return ctrl.Result{Requeue: true}, nil 133 | } 134 | r.updateStatus(ctx, role, api.NewRoleStatus(api.CrProgressing, "role created on AWS")) 135 | } 136 | 137 | if role.Spec.PolicyARN == "" { // the role doesn't have the policyARN set in Spec 138 | if ok := r.setPolicyArnFieldIfPossible(ctx, role); !ok { // we try to grab it from the policy resource and set it 139 | return ctrl.Result{Requeue: true}, nil 140 | } 141 | r.updateStatus(ctx, role, api.NewRoleStatus(api.CrProgressing, "policy found on AWS")) 142 | return ctrl.Result{Requeue: true}, nil 143 | } 144 | 145 | // the role already has a policyARN in Spec 146 | if ok := r.attachPolicyToRoleIfNeeded(ctx, role); !ok { // we attach the policy with the role on aws 147 | return ctrl.Result{Requeue: true}, nil 148 | } 149 | 150 | if role.Status.Condition != api.CrOK { 151 | _ = r.updateStatus(ctx, role, api.NewRoleStatus(api.CrOK, "all done")) 152 | } 153 | 154 | return ctrl.Result{}, nil 155 | } 156 | 157 | func (r *RoleReconciler) setRoleArnField(ctx context.Context, role *api.Role) (completed bool) { 158 | // we get the role details from aws 159 | roleArn, err := r.awsRM.GetRoleARN(role.AwsName(r.clusterName)) 160 | if err != nil { 161 | r.updateStatus(ctx, role, api.NewRoleStatus(api.CrError, "failed to get role ARN on AWS : "+err.Error())) 162 | return false 163 | } 164 | 165 | // set the roleArn in spec 166 | role.Spec.RoleARN = roleArn 167 | if err := r.Update(context.Background(), role); err != nil { 168 | r.updateStatus(ctx, role, api.NewRoleStatus(api.CrError, "failed to set roleArn field in role : "+err.Error())) 169 | return false 170 | } 171 | 172 | return true 173 | } 174 | 175 | func (r *RoleReconciler) createRoleOnAws(ctx context.Context, role *api.Role, permissionsBoundariesPolicyARN string) (completed bool) { 176 | if err := r.awsRM.CreateRole(*role, permissionsBoundariesPolicyARN); err != nil { 177 | r.updateStatus(ctx, role, api.NewRoleStatus(api.CrError, "failed to create roleArn on aws : "+err.Error())) 178 | return false 179 | } 180 | return true 181 | } 182 | 183 | func (r *RoleReconciler) attachPolicyToRoleIfNeeded(ctx context.Context, role *api.Role) (completed bool) { 184 | awsRoleName := role.AwsName(r.clusterName) 185 | roleAlreadyCreatedOnAws, err := r.awsRM.RoleExists(awsRoleName) 186 | if err != nil { 187 | r.updateStatus(ctx, role, api.NewRoleStatus(api.CrError, "failed to check if the role exists : "+err.Error())) 188 | return false 189 | } 190 | 191 | if !roleAlreadyCreatedOnAws { 192 | r.updateStatus(ctx, role, api.NewRoleStatus(api.CrError, "role not created on AWS yet : "+err.Error())) 193 | return false 194 | } 195 | 196 | // maybe the policy is already attached to it ? 197 | policiesARNs, err := r.awsRM.GetAttachedRolePoliciesARNs(awsRoleName) 198 | if err != nil { 199 | r.updateStatus(ctx, role, api.NewRoleStatus(api.CrError, "failed to retrieve attached role policies : "+err.Error())) 200 | return false 201 | } 202 | 203 | for _, pARN := range policiesARNs { // iterate over found policies 204 | if pARN == role.Spec.PolicyARN { 205 | return true 206 | } 207 | } 208 | 209 | // the policy is not attached yet 210 | if err := r.awsRM.AttachRolePolicy(awsRoleName, role.Spec.PolicyARN); err != nil { // we attach the policy 211 | r.updateStatus(ctx, role, api.NewRoleStatus(api.CrError, "failed to attach policy to role : "+err.Error())) 212 | return false 213 | } 214 | 215 | r.updateStatus(ctx, role, api.NewRoleStatus(api.CrProgressing, "policy attached to role")) 216 | return true 217 | } 218 | 219 | func (r *RoleReconciler) setPolicyArnFieldIfPossible(ctx context.Context, role *api.Role) (completed bool) { 220 | // we'll try to get it from the policy resource 221 | policy, ok := r.getPolicy(ctx, role.Name, role.Namespace) 222 | if !ok || policy == nil { 223 | // not found 224 | return false 225 | } 226 | 227 | // if its arn field is not set 228 | if policy.Spec.ARN == "" { 229 | return false 230 | } 231 | 232 | role.Spec.PolicyARN = policy.Spec.ARN 233 | if err := r.Update(ctx, role); err != nil { 234 | r.controllerErrLog(policy, "set policyARN in role spec", err) 235 | return false 236 | } 237 | 238 | return true 239 | } 240 | 241 | func (r *RoleReconciler) registerFinalizerIfNeeded(role *api.Role) (completed bool) { 242 | if !containsString(role.ObjectMeta.Finalizers, r.finalizerID) { 243 | // the finalizer isn't registered yet 244 | // we add it to the role. 245 | role.ObjectMeta.Finalizers = append(role.ObjectMeta.Finalizers, r.finalizerID) 246 | if err := r.Update(context.Background(), role); err != nil { 247 | r.controllerErrLog(role, "setting finalizer", err) 248 | return false 249 | } 250 | } 251 | return true 252 | } 253 | 254 | func (r *RoleReconciler) executeFinalizerIfPresent(role *api.Role) (completed bool) { 255 | if !containsString(role.ObjectMeta.Finalizers, r.finalizerID) { // no finalizer to execute 256 | return true 257 | } 258 | 259 | for { // if some policies are attached to the role, wait till they're detached 260 | attachedPoliciesARNs, err := r.awsRM.GetAttachedRolePoliciesARNs(role.AwsName(r.clusterName)) 261 | if err != nil { 262 | r.controllerErrLog(role, "list attached policies", err) 263 | return false 264 | } 265 | 266 | if len(attachedPoliciesARNs) == 0 { // no policy attached, exit the loop 267 | r.updateStatus(context.TODO(), role, api.NewRoleStatus(api.CrDeleting, "no policy attached")) 268 | break 269 | } 270 | 271 | // we found some policies attached 272 | // policy should also try to detach policies on its side 273 | r.updateStatus(context.TODO(), role, api.NewRoleStatus(api.CrDeleting, fmt.Sprintf("%d policies still attached, waiting for them to be detached", len(attachedPoliciesARNs)))) 274 | for _, attachedPolicyARN := range attachedPoliciesARNs { 275 | _ = r.awsRM.DetachRolePolicy(role.AwsName(r.clusterName), attachedPolicyARN) 276 | } 277 | time.Sleep(time.Second * 5) 278 | } 279 | 280 | { // delete the role on AWS 281 | if err := r.awsRM.DeleteRole(role.AwsName(r.clusterName)); err != nil { 282 | r.controllerErrLog(role, "aws role deletion", err) 283 | return false 284 | } 285 | r.updateStatus(context.TODO(), role, api.NewRoleStatus(api.CrDeleting, "role deleted on AWS")) 286 | } 287 | 288 | { // delete the role CR 289 | if err := r.Delete(context.TODO(), role); err != nil && !k8serrors.IsNotFound(err) { 290 | r.controllerErrLog(role, "deletion", err) 291 | return false 292 | } 293 | } 294 | 295 | // remove the finalizer 296 | role.ObjectMeta.Finalizers = removeString(role.ObjectMeta.Finalizers, r.finalizerID) 297 | return r.Update(context.Background(), role) == nil 298 | } 299 | 300 | func (r *RoleReconciler) getPolicy(ctx context.Context, name, ns string) (_ *api.Policy, completed bool) { 301 | policy := &api.Policy{} 302 | if err := r.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, policy); err != nil { 303 | if k8serrors.IsNotFound(err) { 304 | return nil, true 305 | } 306 | 307 | r.controllerErrLog(policy, "get policy", err) 308 | return nil, false 309 | } 310 | 311 | return policy, true 312 | } 313 | 314 | func (r *RoleReconciler) getRoleFromReq(ctx context.Context, req ctrl.Request) (_ *api.Role, completed bool) { 315 | role := &api.Role{} 316 | if err := r.Get(ctx, req.NamespacedName, role); err != nil { 317 | if k8serrors.IsNotFound(err) { 318 | return nil, true 319 | } 320 | 321 | r.controllerErrLog(role, "get resource", err) 322 | return nil, false 323 | } 324 | 325 | return role, true 326 | } 327 | 328 | func (r *RoleReconciler) updateStatus(ctx context.Context, role *api.Role, status api.RoleStatus) bool { 329 | role.Status = status 330 | return r.Status().Update(ctx, role) == nil 331 | } 332 | 333 | func (r *RoleReconciler) controllerErrLog(resource fullNamer, msg string, err error) { 334 | r.log.Info(fmt.Sprintf("[%s] : Failed to %s : %s", resource.FullName(), msg, err)) 335 | } 336 | -------------------------------------------------------------------------------- /controllers/shared_test.go: -------------------------------------------------------------------------------- 1 | package controllers_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "math/rand" 8 | "time" 9 | 10 | api "github.com/VoodooTeam/irsa-operator/api/v1alpha1" 11 | . "github.com/onsi/gomega" 12 | corev1 "k8s.io/api/core/v1" 13 | "k8s.io/apimachinery/pkg/types" 14 | "sigs.k8s.io/controller-runtime/pkg/client" 15 | ) 16 | 17 | func init() { 18 | rand.Seed(time.Now().UnixNano()) 19 | } 20 | 21 | const ( 22 | testns = "default" 23 | resourcePollTimeout = time.Second * 50 24 | resourcePollInterval = time.Millisecond * 500 25 | ) 26 | 27 | // generates a 20 letters string (~5.0e-29 collision probability) 28 | func randString() string { 29 | letterBytes := "abcdefghijklmnopqrstuvwxyz" // must be valid DNS 30 | b := make([]byte, 20) 31 | for i := range b { 32 | b[i] = letterBytes[rand.Intn(len(letterBytes))] 33 | } 34 | return string(b) 35 | } 36 | 37 | // validName is just a more user-friendly name 38 | var validName = randString 39 | 40 | // ObjTester is used to find a k8s resource with a given Status 41 | type ObjTester interface { 42 | client.Object 43 | HasStatus(st fmt.Stringer) bool 44 | } 45 | 46 | // createResource is used to create any k8s resource and expect a result afterwards 47 | func createResource(obj client.Object) GomegaAsyncAssertion { 48 | return Expect(k8sClient.Create(context.Background(), obj)) 49 | } 50 | 51 | func find(name, ns string, status fmt.Stringer, obj ObjTester) GomegaAsyncAssertion { 52 | return Eventually(func() bool { 53 | err := k8sClient.Get( 54 | context.Background(), 55 | types.NamespacedName{Name: name, Namespace: ns}, 56 | obj, 57 | ) 58 | if err != nil { 59 | return false 60 | } 61 | 62 | return obj.HasStatus(status) 63 | }, resourcePollTimeout, resourcePollInterval) 64 | } 65 | 66 | func findSa(name, ns string) GomegaAsyncAssertion { 67 | return Eventually(func() bool { 68 | err := k8sClient.Get( 69 | context.Background(), 70 | types.NamespacedName{Name: name, Namespace: ns}, 71 | &corev1.ServiceAccount{}, 72 | ) 73 | return err == nil 74 | }, resourcePollTimeout, resourcePollInterval) 75 | } 76 | 77 | func foundIrsaInCondition(name, ns string, cond api.IrsaCondition) GomegaAsyncAssertion { 78 | return find(name, ns, cond, &api.IamRoleServiceAccount{}) 79 | } 80 | 81 | func foundPolicyInCondition(name, ns string, cond api.CrCondition) GomegaAsyncAssertion { 82 | return find(name, ns, cond, &api.Policy{}) 83 | } 84 | 85 | func foundRoleInCondition(name, ns string, cond api.CrCondition) GomegaAsyncAssertion { 86 | return find(name, ns, cond, &api.Role{}) 87 | } 88 | 89 | func getRole(name, ns string) api.Role { 90 | obj := &api.Role{} 91 | getOnK8s(name, ns, obj) 92 | return *obj 93 | } 94 | 95 | func getIrsa(name, ns string) api.IamRoleServiceAccount { 96 | obj := &api.IamRoleServiceAccount{} 97 | getOnK8s(name, ns, obj) 98 | return *obj 99 | } 100 | func getPolicy(name, ns string) api.Policy { 101 | obj := &api.Policy{} 102 | getOnK8s(name, ns, obj) 103 | return *obj 104 | } 105 | 106 | func getOnK8s(name, ns string, o client.Object) { 107 | if err := k8sClient.Get( 108 | context.Background(), 109 | types.NamespacedName{Name: name, Namespace: ns}, 110 | o, 111 | ); err != nil { 112 | log.Fatal("failed to get object") 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /controllers/suite_test.go: -------------------------------------------------------------------------------- 1 | package controllers_test 2 | 3 | import ( 4 | "log" 5 | "path/filepath" 6 | "testing" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | "k8s.io/client-go/kubernetes/scheme" 11 | ctrl "sigs.k8s.io/controller-runtime" 12 | "sigs.k8s.io/controller-runtime/pkg/client" 13 | "sigs.k8s.io/controller-runtime/pkg/envtest" 14 | "sigs.k8s.io/controller-runtime/pkg/envtest/printer" 15 | logf "sigs.k8s.io/controller-runtime/pkg/log" 16 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 17 | 18 | irsav1alpha1 "github.com/VoodooTeam/irsa-operator/api/v1alpha1" 19 | irsaCtrl "github.com/VoodooTeam/irsa-operator/controllers" 20 | // +kubebuilder:scaffold:imports 21 | ) 22 | 23 | var k8sClient client.Client 24 | var testEnv *envtest.Environment 25 | var st *awsFake 26 | 27 | func CustomFail(message string, callerSkip ...int) { 28 | log.Println(message) 29 | panic(GINKGO_PANIC) 30 | } 31 | 32 | func TestAPIs(t *testing.T) { 33 | RegisterFailHandler(CustomFail) 34 | 35 | RunSpecsWithDefaultAndCustomReporters(t, 36 | "Controller Suite", 37 | []Reporter{printer.NewlineReporter{}}) 38 | } 39 | 40 | type fakeWriter struct{} 41 | 42 | func (fakeWriter) Write(b []byte) (int, error) { 43 | return 0, nil 44 | } 45 | 46 | var _ = BeforeSuite(func() { 47 | // we disable the standard logging 48 | logf.SetLogger(zap.New(zap.WriteTo(fakeWriter{}))) 49 | 50 | By("bootstrapping test environment") 51 | testEnv = &envtest.Environment{ 52 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 53 | } 54 | 55 | // start the envtest cluster 56 | cfg, err := testEnv.Start() 57 | Expect(err).NotTo(HaveOccurred()) 58 | Expect(cfg).NotTo(BeNil()) 59 | 60 | // import the irsa scheme 61 | err = irsav1alpha1.AddToScheme(scheme.Scheme) 62 | Expect(err).NotTo(HaveOccurred()) 63 | 64 | k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ 65 | Scheme: scheme.Scheme, 66 | }) 67 | Expect(err).ToNot(HaveOccurred()) 68 | 69 | // reconcilers 70 | // irsa reconcilier 71 | iR := irsaCtrl.NewIrsaReconciler( 72 | k8sManager.GetClient(), 73 | scheme.Scheme, 74 | ctrl.Log.WithName("controllers").WithName("irsa"), 75 | ) 76 | err = iR.SetupWithManager(k8sManager) 77 | Expect(err).ToNot(HaveOccurred()) 78 | 79 | // policy reconcilier 80 | clusterName := "clustername" 81 | st = newAwsFake() 82 | pR := irsaCtrl.NewPolicyReconciler( 83 | k8sManager.GetClient(), 84 | scheme.Scheme, 85 | st, 86 | ctrl.Log.WithName("controllers").WithName("policy"), 87 | clusterName, 88 | ) 89 | 90 | err = pR.SetupWithManager(k8sManager) 91 | Expect(err).ToNot(HaveOccurred()) 92 | 93 | // start role reconcilier 94 | rR := irsaCtrl.NewRoleReconciler( 95 | k8sManager.GetClient(), 96 | scheme.Scheme, 97 | st, 98 | ctrl.Log.WithName("controllers").WithName("role"), 99 | clusterName, 100 | "", 101 | ) 102 | err = rR.SetupWithManager(k8sManager) 103 | Expect(err).ToNot(HaveOccurred()) 104 | 105 | go func() { 106 | err = k8sManager.Start(ctrl.SetupSignalHandler()) 107 | Expect(err).ToNot(HaveOccurred()) 108 | }() 109 | 110 | k8sClient = k8sManager.GetClient() 111 | Expect(k8sClient).ToNot(BeNil()) 112 | 113 | }, 60) 114 | 115 | var _ = AfterSuite(func() { 116 | By("tearing down the test environment") 117 | err := testEnv.Stop() 118 | Expect(err).NotTo(HaveOccurred()) 119 | }) 120 | -------------------------------------------------------------------------------- /cr.yml: -------------------------------------------------------------------------------- 1 | release-name-template: "helm-v{{ .Version }}" 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/VoodooTeam/irsa-operator 2 | 3 | go 1.15 4 | 5 | require ( 6 | cloud.google.com/go v0.79.0 // indirect 7 | github.com/Azure/go-autorest/autorest v0.11.18 // indirect 8 | github.com/aws/aws-sdk-go v1.37.29 9 | github.com/cenkalti/backoff/v3 v3.2.2 // indirect 10 | github.com/containerd/continuity v0.0.0-20200228182428-0f16d7a0959c // indirect 11 | github.com/davecgh/go-spew v1.1.1 12 | github.com/go-logr/logr v0.4.0 13 | github.com/go-logr/stdr v0.3.0 14 | github.com/go-logr/zapr v0.4.0 // indirect 15 | github.com/gogo/protobuf v1.3.2 // indirect 16 | github.com/google/gofuzz v1.2.0 // indirect 17 | github.com/google/uuid v1.2.0 // indirect 18 | github.com/googleapis/gnostic v0.5.4 // indirect 19 | github.com/imdario/mergo v0.3.12 // indirect 20 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 21 | github.com/onsi/ginkgo v1.14.1 22 | github.com/onsi/gomega v1.10.3 23 | github.com/ory/dockertest/v3 v3.6.2 24 | github.com/prometheus/client_golang v1.9.1-0.20210211201929-babeb356a51b // indirect 25 | github.com/prometheus/procfs v0.6.0 // indirect 26 | go.uber.org/multierr v1.6.0 // indirect 27 | go.uber.org/zap v1.16.0 // indirect 28 | golang.org/x/oauth2 v0.0.0-20210311163135-5366d9dc1934 // indirect 29 | golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 // indirect 30 | golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect 31 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect 32 | gopkg.in/yaml.v2 v2.4.0 // indirect 33 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 34 | k8s.io/api v0.20.4 35 | k8s.io/apiextensions-apiserver v0.20.4 // indirect 36 | k8s.io/apimachinery v0.20.4 37 | k8s.io/client-go v0.20.4 38 | k8s.io/klog/v2 v2.6.0 // indirect 39 | k8s.io/kube-openapi v0.0.0-20210305164622-f622666832c1 // indirect 40 | k8s.io/utils v0.0.0-20210305010621-2afb4311ab10 // indirect 41 | sigs.k8s.io/controller-runtime v0.8.3 42 | sigs.k8s.io/structured-merge-diff/v4 v4.1.0 // indirect 43 | ) 44 | 45 | replace ( 46 | github.com/apache/thrift/lib/go/thrift => github.com/apache/thrift/lib/go/thrift v0.13.0 47 | github.com/aws/aws-sdk-go => github.com/aws/aws-sdk-go v1.37.29 48 | github.com/miekg/dns => github.com/miekg/dns v1.1.25 49 | github.com/nats-io/nats-server/v2 => github.com/nats-io/nats-server/v2 v2.1.9 50 | golang.org/x/crypto => golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 // CVE-2020-9283 51 | golang.org/x/text => golang.org/x/text v0.3.3 // CVE-2018-1098 52 | ) 53 | 54 | exclude ( 55 | github.com/apache/thrift v0.12.0 56 | github.com/prometheus/common v0.18.0 // indirect 57 | ) 58 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | 10 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 11 | // to ensure that exec-entrypoint and run can make use of them. 12 | _ "k8s.io/client-go/plugin/pkg/client/auth" 13 | 14 | "k8s.io/apimachinery/pkg/runtime" 15 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 16 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 17 | ctrl "sigs.k8s.io/controller-runtime" 18 | "sigs.k8s.io/controller-runtime/pkg/healthz" 19 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 20 | 21 | irsav1alpha1 "github.com/VoodooTeam/irsa-operator/api/v1alpha1" 22 | irsaws "github.com/VoodooTeam/irsa-operator/aws" 23 | "github.com/VoodooTeam/irsa-operator/controllers" 24 | 25 | "github.com/aws/aws-sdk-go/aws" 26 | "github.com/aws/aws-sdk-go/aws/credentials" 27 | "github.com/aws/aws-sdk-go/aws/endpoints" 28 | "github.com/aws/aws-sdk-go/aws/session" 29 | // +kubebuilder:scaffold:imports 30 | ) 31 | 32 | var ( 33 | scheme = runtime.NewScheme() 34 | setupLog = ctrl.Log.WithName("setup") 35 | ) 36 | 37 | func init() { 38 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 39 | utilruntime.Must(irsav1alpha1.AddToScheme(scheme)) 40 | // +kubebuilder:scaffold:scheme 41 | } 42 | 43 | func main() { 44 | var metricsAddr string 45 | var enableLeaderElection bool 46 | var probeAddr string 47 | var clusterName string 48 | var oidcProviderARN string 49 | var permissionsBoundariesPolicyARN string 50 | 51 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 52 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 53 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 54 | "Enable leader election for controller manager. "+ 55 | "Enabling this will ensure there is only one active controller manager.") 56 | 57 | flag.StringVar(&clusterName, "cluster-name", "", "The cluster name, used to avoid name collisions on aws, set this to the name of the eks cluster") 58 | flag.StringVar(&oidcProviderARN, "oidc-provider-arn", "", "The ARN of the oidc provider to use.") 59 | flag.StringVar(&permissionsBoundariesPolicyARN, "permissions-boundaries-policy-arn", "", "The ARN of the policy used as permissions boundaries") 60 | 61 | opts := zap.Options{ 62 | Development: true, 63 | } 64 | opts.BindFlags(flag.CommandLine) 65 | flag.Parse() 66 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 67 | 68 | if clusterName == "" { 69 | setupLog.Error(errors.New("cluster-name not provided"), "unable to start manager") 70 | os.Exit(1) 71 | } 72 | if oidcProviderARN == "" { 73 | setupLog.Error(errors.New("oidc-provider-url not provided"), "unable to start manager") 74 | os.Exit(1) 75 | } 76 | setupLog.Info(fmt.Sprintf("cluster name is : %s", clusterName)) 77 | setupLog.Info(fmt.Sprintf("oidc provider arn is : %s", oidcProviderARN)) 78 | if permissionsBoundariesPolicyARN == "" { 79 | setupLog.Info("no permissions boundaries set, you're granting FullAdmin rights to your k8s admins") 80 | } else { 81 | setupLog.Info(fmt.Sprintf("permissions boundaries policy arn is : %s", permissionsBoundariesPolicyARN)) 82 | } 83 | 84 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 85 | Scheme: scheme, 86 | MetricsBindAddress: metricsAddr, 87 | Port: 9443, 88 | HealthProbeBindAddress: probeAddr, 89 | LeaderElection: enableLeaderElection, 90 | LeaderElectionID: "d8e70b98.voodoo.io", 91 | Namespace: "", // empty string means "watch resources on all namespaces" 92 | }) 93 | if err != nil { 94 | setupLog.Error(err, "unable to start manager") 95 | os.Exit(1) 96 | } 97 | 98 | if err = controllers.NewIrsaReconciler( 99 | mgr.GetClient(), 100 | mgr.GetScheme(), 101 | ctrl.Log.WithName("controllers").WithName("IamRoleServiceAccount"), 102 | ).SetupWithManager(mgr); err != nil { 103 | setupLog.Error(err, "unable to create controller", "controller", "IamRoleServiceAccount") 104 | os.Exit(1) 105 | } 106 | 107 | awsCfg := getAwsConfig() 108 | 109 | if err = controllers.NewPolicyReconciler( 110 | mgr.GetClient(), 111 | mgr.GetScheme(), 112 | irsaws.NewAwsManager( 113 | awsCfg, 114 | ctrl.Log.WithName("aws").WithName("Policy"), clusterName, 115 | oidcProviderARN, 116 | ), 117 | ctrl.Log.WithName("controllers").WithName("Policy"), 118 | clusterName, 119 | ).SetupWithManager(mgr); err != nil { 120 | setupLog.Error(err, "unable to create controller", "controller", "Policy") 121 | os.Exit(1) 122 | } 123 | 124 | if err = controllers.NewRoleReconciler( 125 | mgr.GetClient(), 126 | mgr.GetScheme(), 127 | irsaws.NewAwsManager(awsCfg, ctrl.Log.WithName("controllers").WithName("Aws"), clusterName, oidcProviderARN), 128 | ctrl.Log.WithName("controllers").WithName("Role"), 129 | clusterName, 130 | permissionsBoundariesPolicyARN, 131 | ).SetupWithManager(mgr); err != nil { 132 | setupLog.Error(err, "unable to create controller", "controller", "Role") 133 | os.Exit(1) 134 | } 135 | 136 | // +kubebuilder:scaffold:builder 137 | 138 | if err := mgr.AddHealthzCheck("health", healthz.Ping); err != nil { 139 | setupLog.Error(err, "unable to set up health check") 140 | os.Exit(1) 141 | } 142 | if err := mgr.AddReadyzCheck("check", healthz.Ping); err != nil { 143 | setupLog.Error(err, "unable to set up ready check") 144 | os.Exit(1) 145 | } 146 | 147 | setupLog.Info("starting manager") 148 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 149 | setupLog.Error(err, "problem running manager") 150 | os.Exit(1) 151 | } 152 | } 153 | 154 | func getAwsConfig() *session.Session { 155 | localstackEndpoint := os.Getenv("LOCALSTACK_ENDPOINT") 156 | 157 | if localstackEndpoint != "" { 158 | setupLog.Info(fmt.Sprintf("using localstack at : %s", localstackEndpoint)) 159 | if _, err := http.Get(localstackEndpoint); err != nil { // we check connectivity 160 | panic(err) 161 | } 162 | 163 | return session.Must(session.NewSession(&aws.Config{ 164 | Credentials: credentials.NewStaticCredentials("test", "test", ""), 165 | DisableSSL: aws.Bool(true), 166 | Region: aws.String(endpoints.UsWest1RegionID), 167 | Endpoint: aws.String(localstackEndpoint), 168 | })) 169 | } 170 | 171 | return session.Must(session.NewSession()) 172 | } 173 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | let 2 | pkgs = import 3 | (builtins.fetchTarball { 4 | name = "nixos-21.05"; 5 | url = "https://github.com/NixOS/nixpkgs/archive/21.05.tar.gz"; 6 | sha256 = "1ckzhh24mgz6jd1xhfgx0i9mijk6xjqxwsshnvq789xsavrmsc36"; 7 | }) 8 | { }; 9 | 10 | voodoo = import 11 | (builtins.fetchGit { 12 | url = "git@github.com:VoodooTeam/devops-nix-pkgs.git"; 13 | ref = "v0.1.0"; 14 | }) 15 | { inherit pkgs; system = builtins.currentSystem; }; 16 | in 17 | pkgs.mkShell { 18 | buildInputs = 19 | [ 20 | # go vim 21 | pkgs.go 22 | pkgs.gopls 23 | pkgs.asmfmt 24 | pkgs.errcheck 25 | 26 | # operator-sdk cli 27 | voodoo.operator-sdk_1_3_0 28 | voodoo.helm_3_4_2 29 | 30 | # only for local testing 31 | pkgs.docker-compose 32 | voodoo.kind_0_9_0 33 | pkgs.awscli2 34 | pkgs.openssl 35 | pkgs.curl 36 | pkgs.jq 37 | pkgs.gnumake 38 | pkgs.envsubst 39 | ]; 40 | } 41 | -------------------------------------------------------------------------------- /testbin/setup-envtest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2020 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o pipefail 19 | 20 | # Turn colors in this script off by setting the NO_COLOR variable in your 21 | # environment to any value: 22 | # 23 | # $ NO_COLOR=1 test.sh 24 | NO_COLOR=${NO_COLOR:-""} 25 | if [ -z "$NO_COLOR" ]; then 26 | header=$'\e[1;33m' 27 | reset=$'\e[0m' 28 | else 29 | header='' 30 | reset='' 31 | fi 32 | 33 | function header_text { 34 | echo "$header$*$reset" 35 | } 36 | 37 | function setup_envtest_env { 38 | header_text "setting up env vars" 39 | 40 | # Setup env vars 41 | KUBEBUILDER_ASSETS=${KUBEBUILDER_ASSETS:-""} 42 | if [[ -z "${KUBEBUILDER_ASSETS}" ]]; then 43 | export KUBEBUILDER_ASSETS=$1/bin 44 | fi 45 | } 46 | 47 | # fetch k8s API gen tools and make it available under envtest_root_dir/bin. 48 | # 49 | # Skip fetching and untaring the tools by setting the SKIP_FETCH_TOOLS variable 50 | # in your environment to any value: 51 | # 52 | # $ SKIP_FETCH_TOOLS=1 ./check-everything.sh 53 | # 54 | # If you skip fetching tools, this script will use the tools already on your 55 | # machine. 56 | function fetch_envtest_tools { 57 | SKIP_FETCH_TOOLS=${SKIP_FETCH_TOOLS:-""} 58 | if [ -n "$SKIP_FETCH_TOOLS" ]; then 59 | return 0 60 | fi 61 | 62 | tmp_root=/tmp 63 | envtest_root_dir=$tmp_root/envtest 64 | 65 | k8s_version="${ENVTEST_K8S_VERSION:-1.19.2}" 66 | goarch="$(go env GOARCH)" 67 | goos="$(go env GOOS)" 68 | 69 | if [[ "$goos" != "linux" && "$goos" != "darwin" ]]; then 70 | echo "OS '$goos' not supported. Aborting." >&2 71 | return 1 72 | fi 73 | 74 | local dest_dir="${1}" 75 | 76 | # use the pre-existing version in the temporary folder if it matches our k8s version 77 | if [[ -x "${dest_dir}/bin/kube-apiserver" ]]; then 78 | version=$("${dest_dir}"/bin/kube-apiserver --version) 79 | if [[ $version == *"${k8s_version}"* ]]; then 80 | header_text "Using cached envtest tools from ${dest_dir}" 81 | return 0 82 | fi 83 | fi 84 | 85 | header_text "fetching envtest tools@${k8s_version} (into '${dest_dir}')" 86 | envtest_tools_archive_name="kubebuilder-tools-$k8s_version-$goos-$goarch.tar.gz" 87 | envtest_tools_download_url="https://storage.googleapis.com/kubebuilder-tools/$envtest_tools_archive_name" 88 | 89 | envtest_tools_archive_path="$tmp_root/$envtest_tools_archive_name" 90 | if [ ! -f $envtest_tools_archive_path ]; then 91 | curl -sL ${envtest_tools_download_url} -o "$envtest_tools_archive_path" 92 | fi 93 | 94 | mkdir -p "${dest_dir}" 95 | tar -C "${dest_dir}" --strip-components=1 -zvxf "$envtest_tools_archive_path" 96 | } 97 | --------------------------------------------------------------------------------