├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── dependabot.yml └── workflows │ ├── lint.yml │ ├── push.yaml │ ├── release.yaml │ └── unit-test.yaml ├── .gitignore ├── .goreleaser.yaml ├── .krew.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd └── cli │ ├── get.go │ ├── remove.go │ ├── root.go │ ├── upsert.go │ └── version.go ├── example └── example.go ├── go.mod ├── go.sum ├── main.go └── pkg └── mapper ├── configmaps.go ├── configmaps_test.go ├── get.go ├── get_test.go ├── remove.go ├── remove_test.go ├── types.go ├── upsert.go └── upsert_test.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Each line is a file pattern followed by one or more owners. 2 | 3 | # These owners will be the default owners for everything in 4 | # the repo. Unless a later match takes precedence, 5 | # review when someone opens a pull request. 6 | * @keikoproj/authorized-approvers 7 | 8 | # Admins own root and CI. 9 | .github/** @keikoproj/keiko-admins @keikoproj/keiko-maintainers 10 | /* @keikoproj/keiko-admins @keikoproj/keiko-maintainers 11 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We welcome participation from individuals and groups of all backgrounds who want to benefit the broader open source community 4 | through participation in this project. We are dedicated to ensuring a productive, safe and educational experience for all. 5 | 6 | ## Guidelines 7 | 8 | Be welcoming 9 | 10 | * Make it easy for new members to learn and contribute. Help them along the path. Don't make them jump through hoops. 11 | 12 | Be considerate 13 | 14 | * There is a live person at the other end of the Internet. Consider how your comments will affect them. It is often better to give a quick but useful reply than to delay to compose a more thorough reply. 15 | 16 | Be respectful 17 | 18 | * Not everyone is Linus Torvalds, and this is probably a good thing :) but everyone is deserving of respect and consideration for wanting to benefit the broader community. Criticize ideas but respect the person. Saying something positive before you criticize lets the other person know that your criticism is not personal. 19 | 20 | Be patient 21 | 22 | * We have diverse backgrounds. It will take time and effort to understand each others' points of view. Some of us have day jobs and other responsibilities and may take time to respond to requests. 23 | 24 | ## Relevant References 25 | 26 | * 27 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | 1. Fork it () 4 | 2. Open an issue and discuss the feature / bug 5 | 3. Create your feature branch (`git checkout -b feature/fooBar`) 6 | 4. Commit your changes (`git commit -am 'Add some fooBar'`) 7 | 5. Push to the branch (`git push origin feature/fooBar`) 8 | 6. Make sure unit tests are passing 9 | 7. Create a new Pull Request 10 | 11 | ## How to report a bug 12 | 13 | * What did you do? (how to reproduce) 14 | * What did you see? (include logs and screenshots as appropriate) 15 | * What did you expect? 16 | 17 | ## How to contribute a bug fix 18 | 19 | * Open an issue and discuss it. 20 | * Create a pull request for your fix. 21 | 22 | ## How to suggest a new feature 23 | 24 | * Open an issue and discuss it. 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | ignore: 13 | - dependency-name: "k8s.io*" ## K8s module version updates should be done explicitly 14 | update-types: ["version-update:semver-major", "version-update:semver-minor"] 15 | - dependency-name: "sigs.k8s.io*" ## K8s module version updates should be done explicitly 16 | update-types: ["version-update:semver-major", "version-update:semver-minor"] 17 | - dependency-name: "*" ## Major version updates should be done explicitly 18 | update-types: ["version-update:semver-major"] 19 | 20 | - package-ecosystem: "github-actions" 21 | directory: "/" 22 | schedule: 23 | interval: "monthly" 24 | 25 | - package-ecosystem: "docker" 26 | directory: "/" 27 | schedule: 28 | interval: "monthly" 29 | ignore: 30 | - dependency-name: "golang" ## Golang version should be done explicitly -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Scan 2 | 3 | on: 4 | # Only run on PRs targeting master 5 | pull_request: 6 | branches: [ master ] 7 | types: [opened, synchronize, reopened] 8 | # For direct pushes to master only 9 | push: 10 | branches: [ master ] 11 | paths-ignore: 12 | - '**.md' 13 | - 'docs/**' 14 | - '.github/**' 15 | - '!.github/workflows/lint.yml' 16 | 17 | # Prevent duplicate workflow runs 18 | concurrency: 19 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | golangci: 24 | name: Go Linting 25 | runs-on: ubuntu-latest 26 | # Allow job to succeed even with lint issues for now 27 | continue-on-error: true 28 | steps: 29 | - name: Check out code 30 | uses: actions/checkout@v4 31 | 32 | - name: Set up Go 33 | uses: actions/setup-go@v5 34 | with: 35 | go-version: 1.24.x 36 | cache: true 37 | 38 | # Simple linting first using standard go tools 39 | - name: Run go fmt 40 | run: | 41 | go fmt ./... 42 | 43 | - name: Run go vet 44 | run: | 45 | go vet ./... 46 | 47 | - name: Run golangci-lint 48 | id: lint 49 | uses: golangci/golangci-lint-action@v7 50 | with: 51 | version: latest 52 | 53 | gosec-issues: 54 | name: Security Scan Issues 55 | runs-on: ubuntu-latest 56 | steps: 57 | - name: Check out code 58 | uses: actions/checkout@v4 59 | 60 | # Fail only on high severity issues 61 | - name: Run gosec security scan 62 | uses: securego/gosec@master 63 | with: 64 | args: -exclude-generated -fmt=json -out=results.json ./... 65 | 66 | - name: Check for high severity issues 67 | run: | 68 | if [ ! -f results.json ]; then 69 | echo "Error: gosec scan results not found" 70 | exit 1 71 | fi 72 | 73 | # Check if any high severity issues exist (level 3) 74 | HIGH_ISSUES=$(cat results.json | grep -c '"severity":"HIGH"' || true) 75 | if [ "$HIGH_ISSUES" -gt 0 ]; then 76 | echo "Found $HIGH_ISSUES high severity security issues!" 77 | cat results.json | grep -A 5 -B 5 '"severity":"HIGH"' 78 | exit 1 79 | else 80 | echo "No high severity security issues found." 81 | fi 82 | 83 | - name: Upload security scan results 84 | if: always() # Run even if previous steps failed 85 | uses: actions/upload-artifact@v4 86 | with: 87 | name: gosec-results 88 | path: results.json 89 | retention-days: 7 90 | if-no-files-found: warn 91 | 92 | license-check: 93 | name: License Compliance 94 | runs-on: ubuntu-latest 95 | steps: 96 | - name: Check out code 97 | uses: actions/checkout@v4 98 | 99 | - name: Set up Go 100 | uses: actions/setup-go@v5 101 | with: 102 | go-version: 1.24.x 103 | 104 | - name: Check License Headers 105 | run: | 106 | # Only check Go files that aren't in vendor or generated 107 | echo "Checking for Apache License headers in Go files..." 108 | # Store files missing license in a variable 109 | MISSING_LICENSE=$(find . -name "*.go" -type f -not -path "*/vendor/*" -not -path "*/mocks/*" | xargs grep -L "Licensed under the Apache License" || true) 110 | 111 | # If any files are missing license headers, report and exit with error 112 | if [ -n "$MISSING_LICENSE" ]; then 113 | echo "ERROR: The following files are missing Apache License headers:" 114 | echo "$MISSING_LICENSE" 115 | echo "License check failed. Please add the appropriate license headers." 116 | exit 1 117 | else 118 | echo "License check passed. All files have proper license headers." 119 | fi 120 | -------------------------------------------------------------------------------- /.github/workflows/push.yaml: -------------------------------------------------------------------------------- 1 | name: push 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | tags: 7 | - '*' 8 | 9 | jobs: 10 | push: 11 | name: push 12 | if: github.repository_owner == 'keikoproj' 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Docker Buildx 20 | id: buildx 21 | uses: docker/setup-buildx-action@v3 22 | with: 23 | install: true 24 | version: latest 25 | 26 | - name: Set up QEMU 27 | id: qemu 28 | uses: docker/setup-qemu-action@v3 29 | with: 30 | image: tonistiigi/binfmt:latest 31 | platforms: all 32 | 33 | - name: Login to DockerHub 34 | uses: docker/login-action@v3 35 | with: 36 | username: ${{ secrets.DOCKERHUB_USERNAME }} 37 | password: ${{ secrets.DOCKERHUB_TOKEN }} 38 | 39 | - name: Docker meta 40 | id: docker_meta 41 | uses: docker/metadata-action@v5 42 | with: 43 | images: ${{ github.repository_owner }}/aws-auth 44 | tags: | 45 | type=ref,event=branch 46 | type=ref,event=pr 47 | type=semver,pattern={{version}} 48 | 49 | - name: Build and push 50 | uses: docker/build-push-action@v6 51 | with: 52 | context: . 53 | file: ./Dockerfile 54 | platforms: linux/amd64,linux/arm64 55 | push: true 56 | tags: ${{ steps.docker_meta.outputs.tags }} 57 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release aws-auth binaries 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | releases-matrix: 9 | name: Release Go Binary 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@master 14 | 15 | - name: Setup Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: ^1.24 19 | 20 | - name: GoReleaser 21 | uses: goreleaser/goreleaser-action@v6 22 | with: 23 | version: latest 24 | args: release 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | # TODO: Test using krew-release-bot later 29 | 30 | - name: Update new version in krew-index 31 | uses: rajatjindal/krew-release-bot@v0.0.47 -------------------------------------------------------------------------------- /.github/workflows/unit-test.yaml: -------------------------------------------------------------------------------- 1 | name: unit-test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | unit-test: 11 | if: github.repository_owner == 'keikoproj' 12 | name: unit-test 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Set up Go 1.x 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: ^1.24 19 | 20 | - name: Check out code into the Go module directory 21 | uses: actions/checkout@v4 22 | 23 | - name: Test 24 | run: | 25 | go test -v ./... -coverprofile ./coverage.txt 26 | make docker-build 27 | 28 | - name: Upload coverage reports to Codecov 29 | uses: codecov/codecov-action@v5 30 | with: 31 | files: ./coverage.out 32 | token: ${{ secrets.CODECOV_TOKEN }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/* 2 | coverage.txt 3 | coverage.html 4 | results.json 5 | dist/ 6 | .windsurfrules 7 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | before: 4 | hooks: 5 | - go mod tidy 6 | - go generate ./... 7 | builds: 8 | - id: aws-auth 9 | main: ./ 10 | binary: aws-auth 11 | env: 12 | - CGO_ENABLED=0 13 | goos: 14 | - linux 15 | - windows 16 | - darwin 17 | ldflags: 18 | - -s 19 | - -w 20 | - -X github.com/keikoproj/aws-auth/cmd/cli.gitCommit={{.Commit}} 21 | - -X github.com/keikoproj/aws-auth/cmd/cli.buildDate={{.Date}} 22 | - -X github.com/keikoproj/aws-auth/cmd/cli.pkgVersion={{.Version}} 23 | archives: 24 | - builds: 25 | - aws-auth 26 | name_template: "{{ .ProjectName }}_{{ .Tag }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 27 | wrap_in_directory: false 28 | format: tar.gz 29 | files: 30 | - LICENSE 31 | -------------------------------------------------------------------------------- /.krew.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: krew.googlecontainertools.github.com/v1alpha2 2 | kind: Plugin 3 | metadata: 4 | name: aws-auth 5 | spec: 6 | version: {{ .TagName }} 7 | homepage: https://github.com/keikoproj/aws-auth 8 | shortDescription: Manage aws-auth ConfigMap 9 | description: | 10 | This plugin allows upserting and removing IAM mappings from the 11 | aws-auth configmap in order to manage access to EKS clusters for 12 | roles or users. 13 | platforms: 14 | - selector: 15 | matchLabels: 16 | os: darwin 17 | arch: amd64 18 | {{addURIAndSha "https://github.com/keikoproj/aws-auth/releases/download/{{ .TagName }}/aws-auth_{{ .TagName }}_darwin_amd64.tar.gz" .TagName }} 19 | bin: aws-auth 20 | - selector: 21 | matchLabels: 22 | os: darwin 23 | arch: arm64 24 | {{addURIAndSha "https://github.com/keikoproj/aws-auth/releases/download/{{ .TagName }}/aws-auth_{{ .TagName }}_darwin_arm64.tar.gz" .TagName }} 25 | bin: aws-auth 26 | - selector: 27 | matchLabels: 28 | os: linux 29 | arch: amd64 30 | {{addURIAndSha "https://github.com/keikoproj/aws-auth/releases/download/{{ .TagName }}/aws-auth_{{ .TagName }}_linux_amd64.tar.gz" .TagName }} 31 | bin: aws-auth 32 | - selector: 33 | matchLabels: 34 | os: linux 35 | arch: arm64 36 | {{addURIAndSha "https://github.com/keikoproj/aws-auth/releases/download/{{ .TagName }}/aws-auth_{{ .TagName }}_linux_arm64.tar.gz" .TagName }} 37 | bin: aws-auth 38 | - selector: 39 | matchLabels: 40 | os: windows 41 | arch: amd64 42 | {{ addURIAndSha "https://github.com/keikoproj/aws-auth/releases/download/{{ .TagName }}/aws-auth_{{ .TagName }}_windows_amd64.tar.gz" .TagName }} 43 | bin: aws-auth.exe 44 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine as build 2 | 3 | RUN apk add --update --no-cache \ 4 | curl \ 5 | build-base \ 6 | git \ 7 | python3 \ 8 | py3-pip 9 | # Install AWS CLI using Alpine package manager or setup a virtual environment 10 | RUN python3 -m venv /tmp/venv && \ 11 | . /tmp/venv/bin/activate && \ 12 | pip install awscli && \ 13 | cp -r /tmp/venv/bin/aws* /usr/local/bin/ 14 | 15 | WORKDIR /go/src/github.com/keikoproj/aws-auth 16 | COPY . . 17 | RUN git rev-parse HEAD 18 | RUN date +%FT%T%z 19 | RUN make build 20 | RUN chmod +x ./bin/aws-auth 21 | 22 | # Now copy it into our base image. 23 | FROM gcr.io/distroless/base-debian11 24 | COPY --from=build /go/src/github.com/keikoproj/aws-auth/bin/aws-auth /bin/aws-auth 25 | 26 | ENV HOME /root 27 | ENTRYPOINT ["/bin/aws-auth"] 28 | CMD ["help"] 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2017-2018 The Keiko Authors 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | .PHONY: test docker clean all 3 | 4 | COMMIT=`git rev-parse HEAD` 5 | BUILD=`date +%FT%T%z` 6 | LDFLAG_LOCATION=github.com/keikoproj/aws-auth/cmd/cli 7 | 8 | LDFLAGS=-ldflags "-X ${LDFLAG_LOCATION}.buildDate=${BUILD} -X ${LDFLAG_LOCATION}.gitCommit=${COMMIT}" 9 | 10 | GIT_TAG=$(shell git rev-parse --short HEAD) 11 | IMAGE ?= aws-auth:latest 12 | 13 | all: lint test build 14 | 15 | build: 16 | CGO_ENABLED=0 go build ${LDFLAGS} -o bin/aws-auth github.com/keikoproj/aws-auth 17 | chmod +x bin/aws-auth 18 | 19 | test: fmt vet 20 | go test -v ./... -coverprofile coverage.txt 21 | go tool cover -html=coverage.txt -o coverage.html 22 | 23 | # Run go fmt against code 24 | fmt: 25 | go fmt ./... 26 | 27 | # Run go vet against code 28 | vet: 29 | go vet ./... 30 | 31 | docker-build: 32 | docker build -t $(IMAGE) . 33 | 34 | docker-push: 35 | docker push ${IMAGE} 36 | 37 | LOCALBIN = $(shell pwd)/bin 38 | $(LOCALBIN): 39 | mkdir -p $(LOCALBIN) 40 | 41 | GOLANGCI_LINT_VERSION := v2.1.1 42 | GOLANGCI_LINT = $(shell pwd)/bin/golangci-lint 43 | .PHONY: golangci-lint 44 | $(GOLANGCI_LINT): $(LOCALBIN) 45 | GOBIN=$(LOCALBIN) go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) 46 | 47 | .PHONY: lint 48 | lint: $(GOLANGCI_LINT) 49 | @echo "Running golangci-lint" 50 | $(GOLANGCI_LINT) run ./... 51 | 52 | .PHONY: clean 53 | clean: 54 | @rm -rf ./bin 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # aws-auth 3 | [![unit-test](https://github.com/keikoproj/aws-auth/actions/workflows/unit-test.yaml/badge.svg?branch=master)](https://github.com/keikoproj/aws-auth/actions/workflows/unit-test.yaml) 4 | [![codecov](https://codecov.io/gh/keikoproj/aws-auth/branch/master/graph/badge.svg)](https://codecov.io/gh/keikoproj/aws-auth) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/keikoproj/aws-auth)](https://goreportcard.com/report/github.com/keikoproj/aws-auth) 6 | 7 | 8 | The `aws-auth` utility and library makes the management of the `aws-auth` ConfigMap for EKS Kubernetes clusters easier and safer. 9 | 10 | ## Use cases 11 | 12 | - make bootstrapping a node group or removing/adding user access on EKS fast and easy 13 | 14 | - useful for automation purposes, any workflow that needs to grant IAM access to an EKS cluster can use this library to modify the config map. 15 | 16 | - run as part of a workflow on kubernetes using a docker image 17 | 18 | The `aws-auth` tool is referenced in the AWS EKS best practices documentation [here](https://aws.github.io/aws-eks-best-practices/security/docs/iam/#use-tools-to-make-changes-to-the-aws-auth-configmap). 19 | 20 | ## Install 21 | 22 | `aws-auth` includes both a CLI and a [go library](#usage-as-a-library). You can install the CLI via `go get` or as a kubectl plugin via [Krew](https://krew.sigs.k8s.io/) or by downloading a binary from the [releases page](https://github.com/keikoproj/aws-auth/releases). 23 | 24 | ### go get 25 | 26 | ```text 27 | go get github.com/keikoproj/aws-auth 28 | aws-auth help 29 | ``` 30 | 31 | ### kubectl krew 32 | 33 | Alternatively, install aws-auth with the krew plugin manager for kubectl. 34 | 35 | ``` 36 | kubectl krew install aws-auth 37 | kubectl aws-auth 38 | ``` 39 | 40 | ### Download release artifact 41 | 42 | The latest release artifacts can be downloaded from the [GitHub releases page](https://github.com/keikoproj/aws-auth/releases/latest). 43 | 44 | Or you can use the following command to download the latest release artifact for your platform: 45 | 46 | ``` bash 47 | curl -s https://api.github.com/repos/keikoproj/aws-auth/releases/latest 48 | | grep "browser_download_url" \ 49 | | grep $(go env GOARCH) | grep $(go env GOOS) \ 50 | | cut -d : -f 2,3 \ 51 | | tr -d \" \ 52 | | wget -qi - 53 | ``` 54 | 55 | ## Usage from command line or Krew 56 | 57 | Either download/install a released binary or add as a plugin to kubectl via Krew 58 | 59 | ```text 60 | $ kubectl krew update 61 | $ kubectl krew install aws-auth 62 | Installing plugin: aws-auth 63 | Installed plugin: aws-auth 64 | 65 | $ kubectl krew aws-auth 66 | aws-auth modifies the aws-auth configmap on eks clusters 67 | 68 | Usage: 69 | aws-auth [command] 70 | 71 | Available Commands: 72 | help Help about any command 73 | remove remove removes a user or role from the aws-auth configmap 74 | remove-by-username remove-by-username removes all map roles and map users from the aws-auth configmap 75 | upsert upsert updates or inserts a user or role to the aws-auth configmap 76 | version Version of aws-auth 77 | 78 | Flags: 79 | -h, --help help for aws-auth 80 | 81 | Use "aws-auth [command] --help" for more information about a command. 82 | ``` 83 | 84 | Given a config map with the following data: 85 | 86 | ```text 87 | $ kubectl get configmap aws-auth -n kube-system -o yaml 88 | apiVersion: v1 89 | kind: ConfigMap 90 | metadata: 91 | name: aws-auth 92 | namespace: kube-system 93 | data: 94 | mapRoles: | 95 | - rolearn: arn:aws:iam::555555555555:role/devel-worker-nodes-NodeInstanceRole-74RF4UBDUKL6 96 | username: system:node:{{EC2PrivateDNSName}} 97 | groups: 98 | - system:bootstrappers 99 | - system:nodes 100 | - rolearn: arn:aws:iam::555555555555:role/abc 101 | username: ops-user 102 | groups: 103 | - system:masters 104 | mapUsers: | 105 | - userarn: arn:aws:iam::555555555555:user/a-user 106 | username: admin 107 | groups: 108 | - system:masters 109 | - userarn: arn:aws:iam::555555555555:user/a-user 110 | username: ops-user 111 | groups: 112 | - system:masters 113 | ``` 114 | 115 | Remove all access belonging to an ARN (both mapUser roles will be removed) 116 | 117 | ```text 118 | $ aws-auth remove --mapusers --userarn arn:aws:iam::555555555555:user/a-user 119 | removed arn:aws:iam::555555555555:user/a-user from aws-auth 120 | ``` 121 | 122 | Remove by full match (only `mapUsers[0]` will be removed) 123 | 124 | ```text 125 | $ aws-auth remove --mapusers --userarn arn:aws:iam::555555555555:user/a-user --username admin --groups system:masters 126 | removed arn:aws:iam::555555555555:user/a-user from aws-auth 127 | ``` 128 | 129 | Remove based on a username 130 | 131 | This command removes all map roles and map users that have matching input username. In the above configmap, map role for roleARN *arn:aws:iam::555555555555:role/abc* and mapUser for userARN *arn:aws:iam::555555555555:user/a-user* will be removed. 132 | 133 | ```text 134 | $ aws-auth remove-by-username --username ops-user 135 | ``` 136 | 137 | 138 | Bootstrap a new node group role 139 | 140 | ```text 141 | $ aws-auth upsert --maproles --rolearn arn:aws:iam::555555555555:role/my-new-node-group-NodeInstanceRole-74RF4UBDUKL6 --username system:node:{{EC2PrivateDNSName}} --groups system:bootstrappers system:nodes 142 | added arn:aws:iam::555555555555:role/my-new-node-group-NodeInstanceRole-74RF4UBDUKL6 to aws-auth 143 | ``` 144 | 145 | You can also add retries with exponential backoff 146 | 147 | ```text 148 | $ aws-auth upsert --maproles --rolearn arn:aws:iam::555555555555:role/my-new-node-group-NodeInstanceRole-74RF4UBDUKL6 --username system:node:{{EC2PrivateDNSName}} --groups system:bootstrappers system:nodes --retry 149 | ``` 150 | 151 | Retries are configurable using the following flags 152 | 153 | ```text 154 | --retry Retry on failure with exponential backoff 155 | --retry-max-count int Maximum number of retries before giving up (default 12) 156 | --retry-max-time duration Maximum wait interval (default 30s) 157 | --retry-min-time duration Minimum wait interval (default 200ms) 158 | ``` 159 | 160 | Append groups to mapping instead of overwriting by using --append 161 | 162 | ``` 163 | $ aws-auth upsert --maproles --rolearn arn:aws:iam::00000000000:role/test --username test --groups test --append 164 | ``` 165 | 166 | Avoid overwriting username by using --update-username=false 167 | 168 | ``` 169 | $ aws-auth upsert --maproles --rolearn arn:aws:iam::00000000000:role/test --username test2 --groups test --update-username=false 170 | ``` 171 | 172 | Use the `get` command to get a detailed view of mappings 173 | 174 | ``` 175 | $ aws-auth get 176 | 177 | TYPE ARN USERNAME GROUPS 178 | Role Mapping arn:aws:iam::555555555555:role/my-new-node-group system:node:{{EC2PrivateDNSName}} system:bootstrappers, system:nodes 179 | ``` 180 | 181 | use impersonate 182 | ``` 183 | aws-auth get|update|remove --as --as-group 184 | ``` 185 | 186 | ## Usage as a library 187 | 188 | ```go 189 | 190 | 191 | package main 192 | 193 | import ( 194 | awsauth "github.com/keikoproj/aws-auth/pkg/mapper" 195 | ) 196 | 197 | func someFunc(client kubernetes.Interface) error { 198 | awsAuth := awsauth.New(client, false) 199 | myUpsertRole := &awsauth.MapperArguments{ 200 | MapRoles: true, 201 | RoleARN: "arn:aws:iam::555555555555:role/my-new-node-group-NodeInstanceRole-74RF4UBDUKL6", 202 | Username: "system:node:{{EC2PrivateDNSName}}", 203 | Groups: []string{ 204 | "system:bootstrappers", 205 | "system:nodes", 206 | }, 207 | WithRetries: true, 208 | MinRetryTime: time.Millisecond * 100, 209 | MaxRetryTime: time.Second * 30, 210 | MaxRetryCount: 12, 211 | } 212 | 213 | err = awsAuth.Upsert(myUpsertRole) 214 | if err != nil { 215 | return err 216 | } 217 | } 218 | 219 | ``` 220 | 221 | ## Run in a container 222 | 223 | ```shell 224 | $ docker run \ 225 | -v ~/.kube/:/root/.kube/ \ 226 | -v ~/.aws/:/root/.aws/ \ 227 | keikoproj/aws-auth:latest \ 228 | aws-auth upsert --mapusers \ 229 | --userarn arn:aws:iam::555555555555:user/a-user \ 230 | --username admin \ 231 | --groups system:masters 232 | ``` 233 | -------------------------------------------------------------------------------- /cmd/cli/get.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package cli 17 | 18 | import ( 19 | "log" 20 | "os" 21 | "strings" 22 | 23 | "github.com/keikoproj/aws-auth/pkg/mapper" 24 | "github.com/olekukonko/tablewriter" 25 | "github.com/spf13/cobra" 26 | ) 27 | 28 | var getArgs = &mapper.MapperArguments{ 29 | OperationType: mapper.OperationGet, 30 | IsGlobal: true, 31 | } 32 | 33 | // getCmd represents the base view command when run without subcommands 34 | var getCmd = &cobra.Command{ 35 | Use: "get", 36 | Short: "get provides a detailed summary of the configmap", 37 | Long: `get allows a user to output the aws-auth configmap entires in various formats`, 38 | Run: func(cmd *cobra.Command, args []string) { 39 | options := kubeOptions{ 40 | AsUser: upsertArgs.AsUser, 41 | AsGroups: upsertArgs.AsGroups, 42 | } 43 | 44 | k, err := getKubernetesClient(getArgs.KubeconfigPath, options) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | 49 | worker := mapper.New(k, true) 50 | 51 | d, err := worker.Get(getArgs) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | 56 | table := tablewriter.NewWriter(os.Stdout) 57 | table.SetHeader([]string{"Type", "ARN", "Username", "Groups"}) 58 | table.SetAutoWrapText(false) 59 | table.SetAutoFormatHeaders(true) 60 | table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 61 | table.SetAlignment(tablewriter.ALIGN_LEFT) 62 | table.SetCenterSeparator("") 63 | table.SetColumnSeparator("") 64 | table.SetRowSeparator("") 65 | table.SetHeaderLine(false) 66 | table.SetBorder(false) 67 | table.SetTablePadding("\t") 68 | table.SetNoWhiteSpace(true) 69 | data := make([][]string, 0) 70 | 71 | for _, row := range d.MapRoles { 72 | data = append(data, []string{"Role Mapping", row.RoleARN, row.Username, strings.Join(row.Groups, ", ")}) 73 | } 74 | 75 | for _, row := range d.MapUsers { 76 | data = append(data, []string{"User Mapping", row.UserARN, row.Username, strings.Join(row.Groups, ", ")}) 77 | } 78 | 79 | table.AppendBulk(data) 80 | table.Render() 81 | }, 82 | } 83 | 84 | func init() { 85 | rootCmd.AddCommand(getCmd) 86 | getCmd.Flags().StringVar(&getArgs.KubeconfigPath, "kubeconfig", "", "Path to kubeconfig") 87 | getCmd.Flags().StringVar(&getArgs.Format, "format", "table", "The format in which to display results (currently only 'table' supported)") 88 | getCmd.Flags().StringVar(&upsertArgs.AsUser, "as", "", "Username to impersonate for the operation") 89 | getCmd.Flags().StringSliceVar(&upsertArgs.AsGroups, "as-group", []string{}, "Group to impersonate for the operation, this flag can be repeated to specify multiple groups") 90 | } 91 | -------------------------------------------------------------------------------- /cmd/cli/remove.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package cli 17 | 18 | import ( 19 | "log" 20 | "time" 21 | 22 | "github.com/spf13/cobra" 23 | 24 | "github.com/keikoproj/aws-auth/pkg/mapper" 25 | ) 26 | 27 | var removeArgs = &mapper.MapperArguments{ 28 | OperationType: mapper.OperationRemove, 29 | } 30 | 31 | // deleteCmd represents the base command when called without any subcommands 32 | var removeCmd = &cobra.Command{ 33 | Use: "remove", 34 | Short: "remove removes a user or role from the aws-auth configmap", 35 | Long: `remove removes a user or role from the aws-auth configmap`, 36 | Run: func(cmd *cobra.Command, args []string) { 37 | options := kubeOptions{ 38 | AsUser: upsertArgs.AsUser, 39 | AsGroups: upsertArgs.AsGroups, 40 | } 41 | 42 | k, err := getKubernetesClient(getArgs.KubeconfigPath, options) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | 47 | worker := mapper.New(k, true) 48 | if err := worker.Remove(removeArgs); err != nil { 49 | log.Fatal(err) 50 | } 51 | }, 52 | } 53 | 54 | // removeByUsernameCmd removes all map roles and map users in an auth cm based on the input username 55 | func removeByUsernameCmd() *cobra.Command { 56 | var removeArgs = &mapper.MapperArguments{} 57 | var command = &cobra.Command{ 58 | Use: "remove-by-username", 59 | Short: "remove-by-username removes all map roles and map users from the aws-auth configmap", 60 | Run: func(cmd *cobra.Command, args []string) { 61 | options := kubeOptions{ 62 | AsUser: upsertArgs.AsUser, 63 | AsGroups: upsertArgs.AsGroups, 64 | } 65 | 66 | k, err := getKubernetesClient(getArgs.KubeconfigPath, options) 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | 71 | worker := mapper.New(k, true) 72 | 73 | if err := worker.RemoveByUsername(removeArgs); err != nil { 74 | log.Fatal(err) 75 | } 76 | }, 77 | } 78 | 79 | command.Flags().StringVar(&removeArgs.KubeconfigPath, "kubeconfig", "", "Kubeconfig path") 80 | command.Flags().StringVar(&removeArgs.Username, "username", "", "Username to remove") 81 | command.Flags().BoolVar(&removeArgs.WithRetries, "retry", false, "Retry on failure with exponential backoff") 82 | command.Flags().DurationVar(&removeArgs.MinRetryTime, "retry-min-time", time.Millisecond*200, "Minimum wait interval") 83 | command.Flags().DurationVar(&removeArgs.MaxRetryTime, "retry-max-time", time.Second*30, "Maximum wait interval") 84 | command.Flags().IntVar(&removeArgs.MaxRetryCount, "retry-max-count", 12, "Maximum number of retries before giving up") 85 | return command 86 | } 87 | 88 | func init() { 89 | rootCmd.AddCommand(removeCmd) 90 | rootCmd.AddCommand(removeByUsernameCmd()) 91 | removeCmd.Flags().StringVar(&removeArgs.KubeconfigPath, "kubeconfig", "", "Kubeconfig path") 92 | removeCmd.Flags().StringVar(&removeArgs.Username, "username", "", "Username to remove") 93 | removeCmd.Flags().StringVar(&removeArgs.RoleARN, "rolearn", "", "Role ARN to remove") 94 | removeCmd.Flags().StringVar(&removeArgs.UserARN, "userarn", "", "User ARN to remove") 95 | removeCmd.Flags().StringSliceVar(&removeArgs.Groups, "groups", []string{}, "Groups to remove") 96 | removeCmd.Flags().BoolVar(&removeArgs.Force, "force", false, "Ignores not found errors") 97 | removeCmd.Flags().BoolVar(&removeArgs.MapRoles, "maproles", false, "Removes a role") 98 | removeCmd.Flags().BoolVar(&removeArgs.MapUsers, "mapusers", false, "Removes a user") 99 | removeCmd.Flags().BoolVar(&removeArgs.WithRetries, "retry", false, "Retry on failure with exponential backoff") 100 | removeCmd.Flags().DurationVar(&removeArgs.MinRetryTime, "retry-min-time", time.Millisecond*200, "Minimum wait interval") 101 | removeCmd.Flags().DurationVar(&removeArgs.MaxRetryTime, "retry-max-time", time.Second*30, "Maximum wait interval") 102 | removeCmd.Flags().IntVar(&removeArgs.MaxRetryCount, "retry-max-count", 12, "Maximum number of retries before giving up") 103 | removeCmd.Flags().StringVar(&upsertArgs.AsUser, "as", "", "Username to impersonate for the operation") 104 | removeCmd.Flags().StringSliceVar(&upsertArgs.AsGroups, "as-group", []string{}, "Group to impersonate for the operation, this flag can be repeated to specify multiple groups") 105 | } 106 | -------------------------------------------------------------------------------- /cmd/cli/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package cli 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | 22 | "github.com/spf13/cobra" 23 | "k8s.io/client-go/kubernetes" 24 | "k8s.io/client-go/rest" 25 | "k8s.io/client-go/tools/clientcmd" 26 | ) 27 | 28 | type kubeOptions struct { 29 | AsUser string 30 | AsGroups []string 31 | } 32 | 33 | // rootCmd represents the base command when called without any subcommands 34 | var rootCmd = &cobra.Command{ 35 | Use: "aws-auth", 36 | Short: "aws-auth modifies the aws-auth configmap on eks clusters", 37 | Long: `aws-auth modifies the aws-auth configmap on eks clusters`, 38 | } 39 | 40 | // Execute adds all child commands to the root command and sets flags appropriately. 41 | // This is called by main.main(). It only needs to happen once to the rootCmd. 42 | func Execute() { 43 | if err := rootCmd.Execute(); err != nil { 44 | fmt.Println(err) 45 | os.Exit(1) 46 | } 47 | } 48 | 49 | func getKubernetesConfig() (*rest.Config, error) { 50 | var config *rest.Config 51 | config, err := rest.InClusterConfig() 52 | if err != nil { 53 | return getKubernetesLocalConfig() 54 | } 55 | return config, nil 56 | } 57 | 58 | func getKubernetesLocalConfig() (*rest.Config, error) { 59 | loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 60 | clientCfg := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{}) 61 | return clientCfg.ClientConfig() 62 | } 63 | 64 | func getKubernetesClient(kubePath string, options kubeOptions) (kubernetes.Interface, error) { 65 | var ( 66 | config *rest.Config 67 | err error 68 | ) 69 | 70 | // if kubeconfig path is not provided, try to auto detect 71 | if kubePath == "" { 72 | config, err = getKubernetesConfig() 73 | if err != nil { 74 | return nil, err 75 | } 76 | } else { 77 | config, err = clientcmd.BuildConfigFromFlags("", kubePath) 78 | if err != nil { 79 | return nil, err 80 | } 81 | } 82 | 83 | config.Impersonate.UserName = options.AsUser 84 | config.Impersonate.Groups = options.AsGroups 85 | 86 | client, err := kubernetes.NewForConfig(config) 87 | if err != nil { 88 | return nil, err 89 | } 90 | return client, nil 91 | } 92 | -------------------------------------------------------------------------------- /cmd/cli/upsert.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package cli 17 | 18 | import ( 19 | "log" 20 | "time" 21 | 22 | "github.com/keikoproj/aws-auth/pkg/mapper" 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | var upsertArgs = &mapper.MapperArguments{ 27 | OperationType: mapper.OperationUpsert, 28 | UpdateUsername: &mapper.UpdateUsernameDefaultValue, 29 | } 30 | 31 | // upsertCmd represents the base command when called without any subcommands 32 | var upsertCmd = &cobra.Command{ 33 | Use: "upsert", 34 | Short: "upsert updates or inserts a user or role to the aws-auth configmap", 35 | Long: `upsert updates or inserts a user or role to the aws-auth configmap`, 36 | Run: func(cmd *cobra.Command, args []string) { 37 | options := kubeOptions{ 38 | AsUser: upsertArgs.AsUser, 39 | AsGroups: upsertArgs.AsGroups, 40 | } 41 | 42 | k, err := getKubernetesClient(getArgs.KubeconfigPath, options) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | 47 | worker := mapper.New(k, true) 48 | if err := worker.Upsert(upsertArgs); err != nil { 49 | log.Fatal(err) 50 | } 51 | }, 52 | } 53 | 54 | func init() { 55 | rootCmd.AddCommand(upsertCmd) 56 | upsertCmd.Flags().StringVar(&upsertArgs.KubeconfigPath, "kubeconfig", "", "Path to kubeconfig") 57 | upsertCmd.Flags().StringVar(&upsertArgs.Username, "username", "", "Username to upsert") 58 | upsertCmd.Flags().StringVar(&upsertArgs.RoleARN, "rolearn", "", "Role ARN to upsert") 59 | upsertCmd.Flags().StringVar(&upsertArgs.UserARN, "userarn", "", "User ARN to upsert") 60 | upsertCmd.Flags().StringSliceVar(&upsertArgs.Groups, "groups", []string{}, "Groups to upsert") 61 | upsertCmd.Flags().BoolVar(&upsertArgs.MapRoles, "maproles", false, "Upsert a role") 62 | upsertCmd.Flags().BoolVar(&upsertArgs.MapUsers, "mapusers", false, "Upsert a user") 63 | upsertCmd.Flags().BoolVar(&upsertArgs.WithRetries, "retry", false, "Retry on failure with exponential backoff") 64 | upsertCmd.Flags().DurationVar(&upsertArgs.MinRetryTime, "retry-min-time", time.Millisecond*200, "Minimum wait interval") 65 | upsertCmd.Flags().DurationVar(&upsertArgs.MaxRetryTime, "retry-max-time", time.Second*30, "Maximum wait interval") 66 | upsertCmd.Flags().IntVar(&upsertArgs.MaxRetryCount, "retry-max-count", 12, "Maximum number of retries before giving up") 67 | upsertCmd.Flags().BoolVar(&upsertArgs.Append, "append", false, "append to a existing group list") 68 | upsertCmd.Flags().BoolVar(upsertArgs.UpdateUsername, "update-username", true, "set to false to not overwite username") 69 | upsertCmd.Flags().StringVar(&upsertArgs.AsUser, "as", "", "Username to impersonate for the operation") 70 | upsertCmd.Flags().StringSliceVar(&upsertArgs.AsGroups, "as-group", []string{}, "Group to impersonate for the operation, this flag can be repeated to specify multiple groups") 71 | } 72 | -------------------------------------------------------------------------------- /cmd/cli/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package cli 17 | 18 | import ( 19 | "fmt" 20 | "runtime" 21 | 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | var ( 26 | // gitCommit is a constant representing the source version that 27 | // generated this build. It should be set during build via -ldflags. 28 | gitCommit string 29 | // buildDate in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ') 30 | //It should be set during build via -ldflags. 31 | buildDate string 32 | // version is the aws-auth package version 33 | pkgVersion string = "dev" 34 | ) 35 | 36 | // Info holds the information related to descheduler app version. 37 | type Info struct { 38 | PackageVersion string `json:"pkgVersion"` 39 | GitCommit string `json:"gitCommit"` 40 | BuildDate string `json:"buildDate"` 41 | GoVersion string `json:"goVersion"` 42 | Compiler string `json:"compiler"` 43 | Platform string `json:"platform"` 44 | } 45 | 46 | // Get returns the overall codebase version. It's for detecting 47 | // what code a binary was built from. 48 | func Get() Info { 49 | return Info{ 50 | GitCommit: gitCommit, 51 | BuildDate: buildDate, 52 | GoVersion: runtime.Version(), 53 | Compiler: runtime.Compiler, 54 | Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), 55 | PackageVersion: pkgVersion, 56 | } 57 | } 58 | 59 | var versionCmd = &cobra.Command{ 60 | Use: "version", 61 | Short: "Version of aws-auth", 62 | Long: `Prints the version of aws-auth.`, 63 | Run: func(cmd *cobra.Command, args []string) { 64 | fmt.Printf("aws-auth version %+v\n", Get()) 65 | }, 66 | } 67 | 68 | func init() { 69 | rootCmd.AddCommand(versionCmd) 70 | } 71 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | 22 | awsauth "github.com/keikoproj/aws-auth/pkg/mapper" 23 | "k8s.io/client-go/kubernetes" 24 | "k8s.io/client-go/tools/clientcmd" 25 | ) 26 | 27 | func main() { 28 | 29 | kubePath := os.Getenv("KUBECONFIG") 30 | config, err := clientcmd.BuildConfigFromFlags("", kubePath) 31 | if err != nil { 32 | os.Exit(1) 33 | } 34 | 35 | client, err := kubernetes.NewForConfig(config) 36 | if err != nil { 37 | os.Exit(1) 38 | } 39 | 40 | awsAuth := awsauth.New(client, false) 41 | myUpsertRole := &awsauth.MapperArguments{ 42 | MapRoles: true, 43 | RoleARN: "arn:aws:iam::555555555555:role/my-new-node-group-NodeInstanceRole-74RF4UBDUKL6", 44 | Username: "system:node:{{EC2PrivateDNSName}}", 45 | Groups: []string{ 46 | "system:bootstrappers", 47 | "system:nodes", 48 | }, 49 | } 50 | 51 | err = awsAuth.Upsert(myUpsertRole) 52 | if err != nil { 53 | fmt.Println(err) 54 | os.Exit(1) 55 | } 56 | 57 | myDeleteRole := &awsauth.MapperArguments{ 58 | MapRoles: true, 59 | RoleARN: "arn:aws:iam::555555555555:role/my-new-node-group-NodeInstanceRole-74RF4UBDUKL6", 60 | Username: "system:node:{{EC2PrivateDNSName}}", 61 | Groups: []string{ 62 | "system:bootstrappers", 63 | "system:nodes", 64 | }, 65 | } 66 | 67 | err = awsAuth.Remove(myDeleteRole) 68 | if err != nil { 69 | fmt.Println(err) 70 | os.Exit(1) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/keikoproj/aws-auth 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/jpillora/backoff v1.0.0 7 | github.com/olekukonko/tablewriter v0.0.5 8 | github.com/onsi/gomega v1.37.0 9 | github.com/pkg/errors v0.9.1 10 | github.com/spf13/cobra v1.9.1 11 | gopkg.in/yaml.v2 v2.4.0 12 | k8s.io/api v0.32.5 13 | k8s.io/apimachinery v0.32.5 14 | k8s.io/client-go v0.32.5 15 | ) 16 | 17 | require ( 18 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 19 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 20 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 21 | github.com/go-logr/logr v1.4.2 // indirect 22 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 23 | github.com/go-openapi/jsonreference v0.21.0 // indirect 24 | github.com/go-openapi/swag v0.23.1 // indirect 25 | github.com/gogo/protobuf v1.3.2 // indirect 26 | github.com/golang/protobuf v1.5.4 // indirect 27 | github.com/google/gnostic-models v0.6.9 // indirect 28 | github.com/google/go-cmp v0.7.0 // indirect 29 | github.com/google/gofuzz v1.2.0 // indirect 30 | github.com/google/uuid v1.6.0 // indirect 31 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 32 | github.com/josharian/intern v1.0.0 // indirect 33 | github.com/json-iterator/go v1.1.12 // indirect 34 | github.com/mailru/easyjson v0.9.0 // indirect 35 | github.com/mattn/go-runewidth v0.0.16 // indirect 36 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 37 | github.com/modern-go/reflect2 v1.0.2 // indirect 38 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 39 | github.com/rivo/uniseg v0.4.7 // indirect 40 | github.com/spf13/pflag v1.0.6 // indirect 41 | github.com/x448/float16 v0.8.4 // indirect 42 | golang.org/x/net v0.38.0 // indirect 43 | golang.org/x/oauth2 v0.28.0 // indirect 44 | golang.org/x/sys v0.31.0 // indirect 45 | golang.org/x/term v0.30.0 // indirect 46 | golang.org/x/text v0.23.0 // indirect 47 | golang.org/x/time v0.11.0 // indirect 48 | google.golang.org/protobuf v1.36.6 // indirect 49 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 50 | gopkg.in/inf.v0 v0.9.1 // indirect 51 | gopkg.in/yaml.v3 v3.0.1 // indirect 52 | k8s.io/klog/v2 v2.130.1 // indirect 53 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 54 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect 55 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 56 | sigs.k8s.io/randfill v1.0.0 // indirect 57 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 58 | sigs.k8s.io/yaml v1.4.0 // indirect 59 | ) 60 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 5 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= 7 | github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 8 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 9 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 10 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 11 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 12 | github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= 13 | github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= 14 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 15 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 16 | github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= 17 | github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= 18 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 19 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 20 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 21 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 22 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 23 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 24 | github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 25 | github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 26 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 27 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 28 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 29 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 30 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 31 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 32 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= 33 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 34 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 35 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 36 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 37 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 38 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 39 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 40 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= 41 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 42 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 43 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 44 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 45 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 46 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 47 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 48 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 49 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 50 | github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= 51 | github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 52 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 53 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 54 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 55 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 56 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 57 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 58 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 59 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 60 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 61 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 62 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 63 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 64 | github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0= 65 | github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= 66 | github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= 67 | github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= 68 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 69 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 70 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 71 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 72 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 73 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 74 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 75 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 76 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 77 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 78 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 79 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 80 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 81 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 82 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 83 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 84 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 85 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 86 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 87 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 88 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 89 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 90 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 91 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 92 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 93 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 94 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 95 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 96 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 97 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 98 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 99 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 100 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 101 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 102 | golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= 103 | golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 104 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 105 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 106 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 107 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 108 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 109 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 110 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 111 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 112 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 113 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 114 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 115 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 116 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 117 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 118 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 119 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 120 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 121 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 122 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 123 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 124 | golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= 125 | golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= 126 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 127 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 128 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 129 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 130 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 131 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 132 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 133 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 134 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 135 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 136 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 137 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 138 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 139 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 140 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 141 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 142 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 143 | k8s.io/api v0.32.4 h1:kw8Y/G8E7EpNy7gjB8gJZl3KJkNz8HM2YHrZPtAZsF4= 144 | k8s.io/api v0.32.4/go.mod h1:5MYFvLvweRhyKylM3Es/6uh/5hGp0dg82vP34KifX4g= 145 | k8s.io/api v0.32.5 h1:uqjjsYo1kTJr5NIcoIaP9F+TgXgADH7nKQx91FDAhtk= 146 | k8s.io/api v0.32.5/go.mod h1:bXXFU3fGCZ/eFMZvfHZC69PeGbXEL4zzjuPVzOxHF64= 147 | k8s.io/apimachinery v0.32.4 h1:8EEksaxA7nd7xWJkkwLDN4SvWS5ot9g6Z/VZb3ju25I= 148 | k8s.io/apimachinery v0.32.4/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= 149 | k8s.io/apimachinery v0.32.5 h1:6We3aJ6crC0ap8EhsEXcgX3LpI6SEjubpiOMXLROwPM= 150 | k8s.io/apimachinery v0.32.5/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= 151 | k8s.io/client-go v0.32.4 h1:zaGJS7xoYOYumoWIFXlcVrsiYioRPrXGO7dBfVC5R6M= 152 | k8s.io/client-go v0.32.4/go.mod h1:k0jftcyYnEtwlFW92xC7MTtFv5BNcZBr+zn9jPlT9Ic= 153 | k8s.io/client-go v0.32.5 h1:huFmQMzgWu0z4kbWsuZci+Gt4Fo72I4CcrvhToZ/Qp0= 154 | k8s.io/client-go v0.32.5/go.mod h1:Qchw6f9WIVrur7DKojAHpRgGLcANT0RLIvF39Jz58xA= 155 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 156 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 157 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= 158 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= 159 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= 160 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 161 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= 162 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 163 | sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 164 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 165 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 166 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= 167 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= 168 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 169 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 170 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package main 17 | 18 | import "github.com/keikoproj/aws-auth/cmd/cli" 19 | 20 | func main() { 21 | cli.Execute() 22 | } 23 | -------------------------------------------------------------------------------- /pkg/mapper/configmaps.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package mapper 17 | 18 | import ( 19 | "context" 20 | 21 | yaml "gopkg.in/yaml.v2" 22 | v1 "k8s.io/api/core/v1" 23 | "k8s.io/apimachinery/pkg/api/errors" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/client-go/kubernetes" 26 | ) 27 | 28 | const ( 29 | AwsAuthNamespace = "kube-system" 30 | AwsAuthName = "aws-auth" 31 | ) 32 | 33 | // ReadAuthMap reads the aws-auth config map and returns an AwsAuthData and the actually ConfigMap objects 34 | func ReadAuthMap(k kubernetes.Interface) (AwsAuthData, *v1.ConfigMap, error) { 35 | var authData AwsAuthData 36 | 37 | cm, err := k.CoreV1().ConfigMaps(AwsAuthNamespace).Get(context.Background(), AwsAuthName, metav1.GetOptions{}) 38 | if err != nil { 39 | if errors.IsNotFound(err) { 40 | cm, err = CreateAuthMap(k) 41 | if err != nil { 42 | return authData, cm, err 43 | } 44 | } else { 45 | return authData, cm, err 46 | } 47 | } 48 | 49 | err = yaml.Unmarshal([]byte(cm.Data["mapRoles"]), &authData.MapRoles) 50 | if err != nil { 51 | return authData, cm, err 52 | } 53 | 54 | err = yaml.Unmarshal([]byte(cm.Data["mapUsers"]), &authData.MapUsers) 55 | if err != nil { 56 | return authData, cm, err 57 | } 58 | 59 | return authData, cm, nil 60 | } 61 | 62 | func CreateAuthMap(k kubernetes.Interface) (*v1.ConfigMap, error) { 63 | configMapObject := &v1.ConfigMap{ 64 | ObjectMeta: metav1.ObjectMeta{ 65 | Name: AwsAuthName, 66 | Namespace: AwsAuthNamespace, 67 | }, 68 | } 69 | configMap, err := k.CoreV1().ConfigMaps(AwsAuthNamespace).Create(context.Background(), configMapObject, metav1.CreateOptions{}) 70 | if err != nil { 71 | return configMap, err 72 | } 73 | return configMap, nil 74 | } 75 | 76 | // UpdateAuthMap updates a given ConfigMap 77 | func UpdateAuthMap(k kubernetes.Interface, authData AwsAuthData, cm *v1.ConfigMap) error { 78 | 79 | mapRoles, err := yaml.Marshal(authData.MapRoles) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | mapUsers, err := yaml.Marshal(authData.MapUsers) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | cm.Data = map[string]string{ 90 | "mapRoles": string(mapRoles), 91 | "mapUsers": string(mapUsers), 92 | } 93 | 94 | _, err = k.CoreV1().ConfigMaps(AwsAuthNamespace).Update(context.Background(), cm, metav1.UpdateOptions{}) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /pkg/mapper/configmaps_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package mapper 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "testing" 22 | 23 | "github.com/onsi/gomega" 24 | v1 "k8s.io/api/core/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/client-go/kubernetes" 27 | "k8s.io/client-go/kubernetes/fake" 28 | ) 29 | 30 | func create_MockConfigMap(client kubernetes.Interface) { 31 | role := NewRolesAuthMap("arn:aws:iam::00000000000:role/node-1", 32 | "system:node:{{EC2PrivateDNSName}}", 33 | []string{"system:bootstrappers", "system:nodes"}) 34 | 35 | user := NewUsersAuthMap("arn:aws:iam::00000000000:user/user-1", 36 | "admin", 37 | []string{"system:masters"}) 38 | 39 | configMap := &v1.ConfigMap{ 40 | ObjectMeta: metav1.ObjectMeta{ 41 | Name: AwsAuthName, 42 | Namespace: AwsAuthNamespace, 43 | }, 44 | Data: map[string]string{ 45 | "mapRoles": role.String(), 46 | "mapUsers": user.String(), 47 | }, 48 | } 49 | _, err := client.CoreV1().ConfigMaps(AwsAuthNamespace).Create(context.Background(), configMap, metav1.CreateOptions{}) 50 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 51 | } 52 | 53 | func create_MockMalformedConfigMap(client kubernetes.Interface) { 54 | configMap := &v1.ConfigMap{ 55 | ObjectMeta: metav1.ObjectMeta{ 56 | Name: AwsAuthName, 57 | Namespace: AwsAuthNamespace, 58 | }, 59 | Data: map[string]string{ 60 | "mapRoles": "''", 61 | "mapUsers": "''", 62 | }, 63 | } 64 | _, err := client.CoreV1().ConfigMaps(AwsAuthNamespace).Create(context.Background(), configMap, metav1.CreateOptions{}) 65 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 66 | 67 | } 68 | 69 | func TestConfigMaps_Update(t *testing.T) { 70 | g := gomega.NewWithT(t) 71 | gomega.RegisterTestingT(t) 72 | client := fake.NewSimpleClientset() 73 | create_MockConfigMap(client) 74 | 75 | auth, cm, err := ReadAuthMap(client) 76 | g.Expect(err).NotTo(gomega.HaveOccurred()) 77 | 78 | role := NewRolesAuthMap("arn:aws:iam::00000000000:role/node-2", 79 | "system:node:{{EC2PrivateDNSName}}", 80 | []string{"system:bootstrappers", "system:nodes"}) 81 | user := NewUsersAuthMap("arn:aws:iam::00000000000:user/user-2", 82 | "ops-user", 83 | []string{"system:masters"}) 84 | 85 | auth.MapRoles = append(auth.MapRoles, role) 86 | auth.MapUsers = append(auth.MapUsers, user) 87 | 88 | err = UpdateAuthMap(client, auth, cm) 89 | g.Expect(err).NotTo(gomega.HaveOccurred()) 90 | 91 | auth, _, err = ReadAuthMap(client) 92 | g.Expect(err).NotTo(gomega.HaveOccurred()) 93 | 94 | fmt.Println(auth.MapRoles[0]) 95 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(2)) 96 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(2)) 97 | } 98 | 99 | func TestConfigMaps_Read(t *testing.T) { 100 | g := gomega.NewWithT(t) 101 | gomega.RegisterTestingT(t) 102 | client := fake.NewSimpleClientset() 103 | create_MockConfigMap(client) 104 | 105 | auth, _, err := ReadAuthMap(client) 106 | g.Expect(err).NotTo(gomega.HaveOccurred()) 107 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(1)) 108 | g.Expect(auth.MapRoles[0].RoleARN).To(gomega.Equal("arn:aws:iam::00000000000:role/node-1")) 109 | g.Expect(auth.MapRoles[0].Username).To(gomega.Equal("system:node:{{EC2PrivateDNSName}}")) 110 | g.Expect(auth.MapRoles[0].Groups).To(gomega.Equal([]string{"system:bootstrappers", "system:nodes"})) 111 | 112 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(1)) 113 | g.Expect(auth.MapUsers[0].UserARN).To(gomega.Equal("arn:aws:iam::00000000000:user/user-1")) 114 | g.Expect(auth.MapUsers[0].Username).To(gomega.Equal("admin")) 115 | g.Expect(auth.MapUsers[0].Groups).To(gomega.Equal([]string{"system:masters"})) 116 | } 117 | 118 | func TestConfigMaps_Create(t *testing.T) { 119 | g := gomega.NewWithT(t) 120 | gomega.RegisterTestingT(t) 121 | client := fake.NewSimpleClientset() 122 | 123 | auth, _, err := ReadAuthMap(client) 124 | g.Expect(err).NotTo(gomega.HaveOccurred()) 125 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(0)) 126 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(0)) 127 | } 128 | -------------------------------------------------------------------------------- /pkg/mapper/get.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package mapper 17 | 18 | // Upsert update or inserts by rolearn 19 | func (b *AuthMapper) Get(args *MapperArguments) (AwsAuthData, error) { 20 | args.IsGlobal = true 21 | args.Validate() 22 | 23 | if args.WithRetries { 24 | out, err := WithRetry(func() (interface{}, error) { 25 | return b.getAuth() 26 | }, args) 27 | if err != nil { 28 | return AwsAuthData{}, err 29 | } 30 | return out.(AwsAuthData), nil 31 | } 32 | 33 | return b.getAuth() 34 | } 35 | 36 | func (b *AuthMapper) getAuth() (AwsAuthData, error) { 37 | 38 | // Read the config map and return an AuthMap 39 | authData, _, err := ReadAuthMap(b.KubernetesClient) 40 | if err != nil { 41 | return AwsAuthData{}, err 42 | } 43 | 44 | return authData, nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/mapper/get_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package mapper 17 | 18 | import ( 19 | "testing" 20 | "time" 21 | 22 | "github.com/onsi/gomega" 23 | "k8s.io/client-go/kubernetes/fake" 24 | ) 25 | 26 | func TestMapper_Get(t *testing.T) { 27 | g := gomega.NewWithT(t) 28 | gomega.RegisterTestingT(t) 29 | client := fake.NewSimpleClientset() 30 | mapper := New(client, true) 31 | create_MockConfigMap(client) 32 | 33 | err := mapper.Upsert(&MapperArguments{ 34 | MapRoles: true, 35 | RoleARN: "arn:aws:iam::00000000000:role/node-2", 36 | Username: "system:node:{{EC2PrivateDNSName}}", 37 | Groups: []string{"system:bootstrappers", "system:nodes"}, 38 | }) 39 | g.Expect(err).NotTo(gomega.HaveOccurred()) 40 | 41 | err = mapper.Upsert(&MapperArguments{ 42 | MapUsers: true, 43 | UserARN: "arn:aws:iam::00000000000:user/user-2", 44 | Username: "admin", 45 | Groups: []string{"system:masters"}, 46 | }) 47 | g.Expect(err).NotTo(gomega.HaveOccurred()) 48 | 49 | data, err := mapper.Get(&MapperArguments{}) 50 | g.Expect(err).NotTo(gomega.HaveOccurred()) 51 | g.Expect(data).NotTo(gomega.Equal(AwsAuthData{})) 52 | 53 | auth, _, err := ReadAuthMap(client) 54 | g.Expect(err).NotTo(gomega.HaveOccurred()) 55 | g.Expect(auth).To(gomega.Equal(data)) 56 | } 57 | 58 | func TestMapper_GetRetry(t *testing.T) { 59 | g := gomega.NewWithT(t) 60 | gomega.RegisterTestingT(t) 61 | client := fake.NewSimpleClientset() 62 | mapper := New(client, true) 63 | 64 | data, err := mapper.Get(&MapperArguments{ 65 | WithRetries: true, 66 | MinRetryTime: 1 * time.Second, 67 | MaxRetryTime: 2 * time.Second, 68 | MaxRetryCount: 5, 69 | }) 70 | g.Expect(err).NotTo(gomega.HaveOccurred()) 71 | g.Expect(data).To(gomega.Equal(AwsAuthData{})) 72 | 73 | auth, _, err := ReadAuthMap(client) 74 | g.Expect(err).NotTo(gomega.HaveOccurred()) 75 | g.Expect(auth).To(gomega.Equal(data)) 76 | } 77 | 78 | func TestMapper_GetRetryFail(t *testing.T) { 79 | g := gomega.NewWithT(t) 80 | gomega.RegisterTestingT(t) 81 | client := fake.NewSimpleClientset() 82 | mapper := New(client, true) 83 | create_MockMalformedConfigMap(client) 84 | 85 | data, err := mapper.Get(&MapperArguments{ 86 | WithRetries: true, 87 | MinRetryTime: 100 * time.Millisecond, 88 | MaxRetryTime: 200 * time.Millisecond, 89 | MaxRetryCount: 5, 90 | }) 91 | 92 | g.Expect(err).To(gomega.HaveOccurred()) 93 | g.Expect(err.Error()).To(gomega.ContainSubstring("waiter timed out")) 94 | g.Expect(data).To(gomega.Equal(AwsAuthData{})) 95 | 96 | _, _, err = ReadAuthMap(client) 97 | g.Expect(err).To(gomega.HaveOccurred()) 98 | g.Expect(err.Error()).To(gomega.ContainSubstring("cannot unmarshal")) 99 | 100 | } 101 | -------------------------------------------------------------------------------- /pkg/mapper/remove.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package mapper 17 | 18 | import ( 19 | "errors" 20 | "fmt" 21 | "log" 22 | "reflect" 23 | ) 24 | 25 | // Remove removes by match of provided arguments 26 | func (b *AuthMapper) Remove(args *MapperArguments) error { 27 | args.Validate() 28 | 29 | if args.WithRetries { 30 | _, err := WithRetry(func() (interface{}, error) { 31 | return nil, b.removeAuth(args) 32 | }, args) 33 | return err 34 | } 35 | return b.removeAuth(args) 36 | } 37 | 38 | // RemoveByUsername removes all map roles and map users that match provided username 39 | func (b *AuthMapper) RemoveByUsername(args *MapperArguments) error { 40 | args.IsGlobal = true 41 | args.Validate() 42 | if args.WithRetries { 43 | _, err := WithRetry(func() (interface{}, error) { 44 | return nil, b.removeAuthByUser(args) 45 | }, args) 46 | return err 47 | } 48 | return b.removeAuthByUser(args) 49 | } 50 | 51 | func (b *AuthMapper) removeAuthByUser(args *MapperArguments) error { 52 | // Read the config map and return an AuthMap 53 | authData, configMap, err := ReadAuthMap(b.KubernetesClient) 54 | if err != nil { 55 | return err 56 | } 57 | removed := false 58 | 59 | var newRolesAuthMap []*RolesAuthMap 60 | 61 | for _, mapRole := range authData.MapRoles { 62 | // Add all other members except the matched 63 | if args.Username != mapRole.Username { 64 | newRolesAuthMap = append(newRolesAuthMap, mapRole) 65 | } else { 66 | removed = true 67 | } 68 | } 69 | 70 | var newUsersAuthMap []*UsersAuthMap 71 | 72 | for _, mapUser := range authData.MapUsers { 73 | // Add all other members except the matched 74 | if args.Username != mapUser.Username { 75 | newUsersAuthMap = append(newUsersAuthMap, mapUser) 76 | } else { 77 | removed = true 78 | } 79 | } 80 | 81 | if !removed { 82 | msg := fmt.Sprintf("failed to remove based on username %v, found zero matches\n", args.Username) 83 | log.Print(msg) 84 | if args.Force { 85 | return nil 86 | } 87 | return errors.New(msg) 88 | } 89 | 90 | authData.SetMapRoles(newRolesAuthMap) 91 | authData.SetMapUsers(newUsersAuthMap) 92 | 93 | return UpdateAuthMap(b.KubernetesClient, authData, configMap) 94 | } 95 | 96 | func (b *AuthMapper) removeAuth(args *MapperArguments) error { 97 | // Read the config map and return an AuthMap 98 | authData, configMap, err := ReadAuthMap(b.KubernetesClient) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | if args.MapRoles { 104 | var rolesResource = NewRolesAuthMap(args.RoleARN, args.Username, args.Groups) 105 | newMap, ok := removeRole(authData.MapRoles, rolesResource) 106 | 107 | if !ok { 108 | log.Printf("failed to remove %v, could not find exact match\n", rolesResource.RoleARN) 109 | if args.Force { 110 | return nil 111 | } 112 | return errors.New("could not find rolemap") 113 | } 114 | log.Printf("removed %v from aws-auth\n", rolesResource.RoleARN) 115 | authData.SetMapRoles(newMap) 116 | } 117 | 118 | if args.MapUsers { 119 | var usersResource = NewUsersAuthMap(args.UserARN, args.Username, args.Groups) 120 | newMap, ok := removeUser(authData.MapUsers, usersResource) 121 | 122 | if !ok { 123 | log.Printf("failed to remove %v, could not find exact match\n", usersResource.UserARN) 124 | if args.Force { 125 | return nil 126 | } 127 | return errors.New("could not find rolemap") 128 | } 129 | log.Printf("removed %v from aws-auth\n", usersResource.UserARN) 130 | authData.SetMapUsers(newMap) 131 | } 132 | 133 | return UpdateAuthMap(b.KubernetesClient, authData, configMap) 134 | } 135 | 136 | func removeRole(authMaps []*RolesAuthMap, targetMap *RolesAuthMap) ([]*RolesAuthMap, bool) { 137 | var newMap []*RolesAuthMap 138 | var match bool 139 | var removed bool 140 | 141 | for _, existingMap := range authMaps { 142 | match = false 143 | if existingMap.RoleARN == targetMap.RoleARN { 144 | match = true 145 | if len(targetMap.Groups) != 0 { 146 | if reflect.DeepEqual(existingMap.Groups, targetMap.Groups) { 147 | match = true 148 | } else { 149 | match = false 150 | } 151 | } 152 | if targetMap.Username != "" { 153 | if existingMap.Username == targetMap.Username { 154 | match = true 155 | } else { 156 | match = false 157 | } 158 | } 159 | } 160 | if match { 161 | removed = true 162 | } else { 163 | newMap = append(newMap, existingMap) 164 | } 165 | } 166 | return newMap, removed 167 | } 168 | 169 | func removeUser(authMaps []*UsersAuthMap, targetMap *UsersAuthMap) ([]*UsersAuthMap, bool) { 170 | var newMap []*UsersAuthMap 171 | var match bool 172 | var removed bool 173 | 174 | for _, existingMap := range authMaps { 175 | match = false 176 | if existingMap.UserARN == targetMap.UserARN { 177 | match = true 178 | if len(targetMap.Groups) != 0 { 179 | if reflect.DeepEqual(existingMap.Groups, targetMap.Groups) { 180 | match = true 181 | } else { 182 | match = false 183 | } 184 | } 185 | if targetMap.Username != "" { 186 | if existingMap.Username == targetMap.Username { 187 | match = true 188 | } else { 189 | match = false 190 | } 191 | } 192 | } 193 | if match { 194 | removed = true 195 | } else { 196 | newMap = append(newMap, existingMap) 197 | } 198 | } 199 | return newMap, removed 200 | } 201 | -------------------------------------------------------------------------------- /pkg/mapper/remove_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package mapper 17 | 18 | import ( 19 | "testing" 20 | "time" 21 | 22 | "github.com/onsi/gomega" 23 | "k8s.io/client-go/kubernetes/fake" 24 | ) 25 | 26 | func TestMapper_Remove(t *testing.T) { 27 | g := gomega.NewWithT(t) 28 | gomega.RegisterTestingT(t) 29 | client := fake.NewSimpleClientset() 30 | mapper := New(client, true) 31 | create_MockConfigMap(client) 32 | 33 | err := mapper.Remove(&MapperArguments{ 34 | MapRoles: true, 35 | RoleARN: "arn:aws:iam::00000000000:role/node-1", 36 | Username: "system:node:{{EC2PrivateDNSName}}", 37 | Groups: []string{"system:bootstrappers", "system:nodes"}, 38 | }) 39 | g.Expect(err).NotTo(gomega.HaveOccurred()) 40 | 41 | err = mapper.Remove(&MapperArguments{ 42 | MapUsers: true, 43 | UserARN: "arn:aws:iam::00000000000:user/user-1", 44 | Username: "admin", 45 | Groups: []string{"system:masters"}, 46 | }) 47 | g.Expect(err).NotTo(gomega.HaveOccurred()) 48 | 49 | auth, _, err := ReadAuthMap(client) 50 | g.Expect(err).NotTo(gomega.HaveOccurred()) 51 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(0)) 52 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(0)) 53 | } 54 | 55 | func TestMapper_RemoveByARN(t *testing.T) { 56 | g := gomega.NewWithT(t) 57 | gomega.RegisterTestingT(t) 58 | client := fake.NewSimpleClientset() 59 | mapper := New(client, true) 60 | create_MockConfigMap(client) 61 | 62 | err := mapper.Remove(&MapperArguments{ 63 | MapRoles: true, 64 | RoleARN: "arn:aws:iam::00000000000:role/node-1", 65 | }) 66 | g.Expect(err).NotTo(gomega.HaveOccurred()) 67 | 68 | err = mapper.Remove(&MapperArguments{ 69 | MapUsers: true, 70 | UserARN: "arn:aws:iam::00000000000:user/user-1", 71 | }) 72 | g.Expect(err).NotTo(gomega.HaveOccurred()) 73 | 74 | auth, _, err := ReadAuthMap(client) 75 | g.Expect(err).NotTo(gomega.HaveOccurred()) 76 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(0)) 77 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(0)) 78 | } 79 | 80 | func TestMapper_RemoveNotFound(t *testing.T) { 81 | g := gomega.NewWithT(t) 82 | gomega.RegisterTestingT(t) 83 | client := fake.NewSimpleClientset() 84 | mapper := New(client, true) 85 | create_MockConfigMap(client) 86 | 87 | err := mapper.Remove(&MapperArguments{ 88 | MapRoles: true, 89 | RoleARN: "arn:aws:iam::00000000000:role/node-2", 90 | }) 91 | g.Expect(err).To(gomega.HaveOccurred()) 92 | 93 | err = mapper.Remove(&MapperArguments{ 94 | MapUsers: true, 95 | UserARN: "arn:aws:iam::00000000000:user/user-2", 96 | }) 97 | g.Expect(err).To(gomega.HaveOccurred()) 98 | 99 | auth, _, err := ReadAuthMap(client) 100 | g.Expect(err).NotTo(gomega.HaveOccurred()) 101 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(1)) 102 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(1)) 103 | } 104 | 105 | func TestMapper_RemoveNotFoundForce(t *testing.T) { 106 | g := gomega.NewWithT(t) 107 | gomega.RegisterTestingT(t) 108 | client := fake.NewSimpleClientset() 109 | mapper := New(client, true) 110 | create_MockConfigMap(client) 111 | 112 | err := mapper.Remove(&MapperArguments{ 113 | Force: true, 114 | MapRoles: true, 115 | RoleARN: "arn:aws:iam::00000000000:role/node-2", 116 | }) 117 | g.Expect(err).NotTo(gomega.HaveOccurred()) 118 | 119 | err = mapper.Remove(&MapperArguments{ 120 | Force: true, 121 | MapUsers: true, 122 | UserARN: "arn:aws:iam::00000000000:user/user-2", 123 | }) 124 | g.Expect(err).NotTo(gomega.HaveOccurred()) 125 | 126 | auth, _, err := ReadAuthMap(client) 127 | g.Expect(err).NotTo(gomega.HaveOccurred()) 128 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(1)) 129 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(1)) 130 | } 131 | 132 | func TestMapper_RemoveByUsername(t *testing.T) { 133 | g := gomega.NewWithT(t) 134 | gomega.RegisterTestingT(t) 135 | client := fake.NewSimpleClientset() 136 | mapper := New(client, true) 137 | create_MockConfigMap(client) 138 | 139 | err := mapper.RemoveByUsername(&MapperArguments{ 140 | Username: "system:node:{{EC2PrivateDNSName}}", 141 | }) 142 | g.Expect(err).NotTo(gomega.HaveOccurred()) 143 | 144 | auth, _, err := ReadAuthMap(client) 145 | g.Expect(err).NotTo(gomega.HaveOccurred()) 146 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(0)) 147 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(1)) 148 | 149 | err = mapper.RemoveByUsername(&MapperArguments{ 150 | Username: "admin", 151 | }) 152 | g.Expect(err).NotTo(gomega.HaveOccurred()) 153 | 154 | auth, _, err = ReadAuthMap(client) 155 | g.Expect(err).NotTo(gomega.HaveOccurred()) 156 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(0)) 157 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(0)) 158 | } 159 | 160 | func TestMapper_RemoveByUsernameWithRetries(t *testing.T) { 161 | g := gomega.NewWithT(t) 162 | gomega.RegisterTestingT(t) 163 | client := fake.NewSimpleClientset() 164 | mapper := New(client, true) 165 | create_MockConfigMap(client) 166 | 167 | err := mapper.RemoveByUsername(&MapperArguments{ 168 | Username: "system:node:{{EC2PrivateDNSName}}", 169 | WithRetries: true, 170 | MinRetryTime: time.Millisecond * 1, 171 | MaxRetryTime: time.Millisecond * 2, 172 | MaxRetryCount: 3, 173 | }) 174 | g.Expect(err).NotTo(gomega.HaveOccurred()) 175 | 176 | auth, _, err := ReadAuthMap(client) 177 | g.Expect(err).NotTo(gomega.HaveOccurred()) 178 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(0)) 179 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(1)) 180 | 181 | err = mapper.RemoveByUsername(&MapperArguments{ 182 | Username: "admin", 183 | WithRetries: true, 184 | MinRetryTime: time.Millisecond * 1, 185 | MaxRetryTime: time.Millisecond * 2, 186 | MaxRetryCount: 3, 187 | }) 188 | g.Expect(err).NotTo(gomega.HaveOccurred()) 189 | 190 | auth, _, err = ReadAuthMap(client) 191 | g.Expect(err).NotTo(gomega.HaveOccurred()) 192 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(0)) 193 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(0)) 194 | } 195 | 196 | func TestMapper_RemoveByUsernameNotFound(t *testing.T) { 197 | g := gomega.NewWithT(t) 198 | gomega.RegisterTestingT(t) 199 | client := fake.NewSimpleClientset() 200 | mapper := New(client, true) 201 | create_MockConfigMap(client) 202 | 203 | err := mapper.RemoveByUsername(&MapperArguments{ 204 | Username: "", 205 | }) 206 | g.Expect(err).To(gomega.HaveOccurred()) 207 | 208 | auth, _, err := ReadAuthMap(client) 209 | g.Expect(err).NotTo(gomega.HaveOccurred()) 210 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(1)) 211 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(1)) 212 | } 213 | 214 | func TestMapper_RemoveByUsernameNotFoundForce(t *testing.T) { 215 | g := gomega.NewWithT(t) 216 | gomega.RegisterTestingT(t) 217 | client := fake.NewSimpleClientset() 218 | mapper := New(client, true) 219 | create_MockConfigMap(client) 220 | 221 | err := mapper.RemoveByUsername(&MapperArguments{ 222 | Username: "", 223 | Force: true, 224 | }) 225 | g.Expect(err).NotTo(gomega.HaveOccurred()) 226 | 227 | auth, _, err := ReadAuthMap(client) 228 | g.Expect(err).NotTo(gomega.HaveOccurred()) 229 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(1)) 230 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(1)) 231 | } 232 | 233 | func TestMapper_RemoveWithRetries(t *testing.T) { 234 | g := gomega.NewWithT(t) 235 | gomega.RegisterTestingT(t) 236 | client := fake.NewSimpleClientset() 237 | mapper := New(client, true) 238 | create_MockConfigMap(client) 239 | 240 | err := mapper.Remove(&MapperArguments{ 241 | MapRoles: true, 242 | RoleARN: "arn:aws:iam::00000000000:role/node-1", 243 | Username: "system:node:{{EC2PrivateDNSName}}", 244 | Groups: []string{"system:bootstrappers", "system:nodes"}, 245 | WithRetries: true, 246 | MinRetryTime: time.Millisecond * 1, 247 | MaxRetryTime: time.Millisecond * 2, 248 | MaxRetryCount: 3, 249 | }) 250 | g.Expect(err).NotTo(gomega.HaveOccurred()) 251 | 252 | err = mapper.Remove(&MapperArguments{ 253 | MapUsers: true, 254 | UserARN: "arn:aws:iam::00000000000:user/user-1", 255 | Username: "admin", 256 | Groups: []string{"system:masters"}, 257 | WithRetries: true, 258 | MinRetryTime: time.Millisecond * 1, 259 | MaxRetryTime: time.Millisecond * 2, 260 | MaxRetryCount: 3, 261 | }) 262 | g.Expect(err).NotTo(gomega.HaveOccurred()) 263 | 264 | auth, _, err := ReadAuthMap(client) 265 | g.Expect(err).NotTo(gomega.HaveOccurred()) 266 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(0)) 267 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(0)) 268 | } 269 | -------------------------------------------------------------------------------- /pkg/mapper/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package mapper 17 | 18 | import ( 19 | "fmt" 20 | "io" 21 | "log" 22 | "strings" 23 | "time" 24 | 25 | "github.com/jpillora/backoff" 26 | "github.com/pkg/errors" 27 | "k8s.io/client-go/kubernetes" 28 | ) 29 | 30 | func init() { 31 | log.SetFlags(0) 32 | 33 | } 34 | 35 | type AuthMapper struct { 36 | KubernetesClient kubernetes.Interface 37 | LoggingEnabled bool 38 | } 39 | 40 | func New(client kubernetes.Interface, isCommandline bool) *AuthMapper { 41 | var mapper = &AuthMapper{} 42 | mapper.KubernetesClient = client 43 | 44 | if !isCommandline { 45 | log.SetOutput(io.Discard) 46 | } 47 | return mapper 48 | } 49 | 50 | var ( 51 | DefaultRetryerBackoffFactor float64 = 2.0 52 | DefaultRetryerBackoffJitter = true 53 | UpdateUsernameDefaultValue bool = true 54 | ) 55 | 56 | // AwsAuthData represents the data of the aws-auth configmap 57 | type AwsAuthData struct { 58 | MapRoles []*RolesAuthMap `yaml:"mapRoles"` 59 | MapUsers []*UsersAuthMap `yaml:"mapUsers"` 60 | } 61 | 62 | // SetMapRoles sets the MapRoles element 63 | func (m *AwsAuthData) SetMapRoles(authMap []*RolesAuthMap) { 64 | m.MapRoles = authMap 65 | } 66 | 67 | // SetMapUsers sets the MapUsers element 68 | func (m *AwsAuthData) SetMapUsers(authMap []*UsersAuthMap) { 69 | m.MapUsers = authMap 70 | } 71 | 72 | type OperationType string 73 | 74 | const ( 75 | OperationUpsert OperationType = "upsert" 76 | OperationRemove OperationType = "remove" 77 | OperationGet OperationType = "get" 78 | ) 79 | 80 | // MapperArguments are the arguments for removing a mapRole or mapUsers 81 | type MapperArguments struct { 82 | KubeconfigPath string 83 | Format string 84 | OperationType OperationType 85 | MapRoles bool 86 | MapUsers bool 87 | Force bool 88 | Username string 89 | RoleARN string 90 | UserARN string 91 | Groups []string 92 | WithRetries bool 93 | MinRetryTime time.Duration 94 | MaxRetryTime time.Duration 95 | MaxRetryCount int 96 | IsGlobal bool 97 | Append bool 98 | UpdateUsername *bool 99 | 100 | AsUser string 101 | AsGroups []string 102 | } 103 | 104 | func (args *MapperArguments) Validate() { 105 | if args.WithRetries { 106 | if args.MaxRetryCount < 1 { 107 | log.Fatal("error: --retry-max-count is invalid, must be greater than zero") 108 | } 109 | } 110 | 111 | if args.RoleARN == "" && args.MapRoles { 112 | log.Fatal("error: --rolearn not provided") 113 | } 114 | 115 | if args.UserARN == "" && args.MapUsers { 116 | log.Fatal("error: --userarn not provided") 117 | } 118 | 119 | if args.MapUsers && args.MapRoles { 120 | log.Fatal("error: --mapusers and --maproles are mutually exclusive") 121 | } 122 | 123 | if args.OperationType == OperationUpsert && args.Username == "" { 124 | log.Fatal("error: --username not provided") 125 | } 126 | 127 | if args.OperationType == OperationGet && args.Format != "table" { 128 | log.Fatal("error: --format only supports value 'table'") 129 | } 130 | 131 | if !args.MapUsers && !args.MapRoles { 132 | if !args.IsGlobal { 133 | log.Fatal("error: must select --mapusers or --maproles") 134 | } 135 | } 136 | 137 | if args.UpdateUsername == nil { 138 | args.UpdateUsername = &UpdateUsernameDefaultValue 139 | } 140 | 141 | } 142 | 143 | // RolesAuthMap is the basic structure of a mapRoles authentication object 144 | type RolesAuthMap struct { 145 | RoleARN string `yaml:"rolearn"` 146 | Username string `yaml:"username"` 147 | Groups []string `yaml:"groups,omitempty"` 148 | } 149 | 150 | func (r *RolesAuthMap) String() string { 151 | var s strings.Builder 152 | s.WriteString(fmt.Sprintf("- rolearn: %v\n ", r.RoleARN)) 153 | s.WriteString(fmt.Sprintf("username: %v\n ", r.Username)) 154 | s.WriteString("groups:\n") 155 | for _, group := range r.Groups { 156 | s.WriteString(fmt.Sprintf(" - %v\n", group)) 157 | } 158 | return s.String() 159 | } 160 | 161 | // UsersAuthMap is the basic structure of a mapUsers authentication object 162 | type UsersAuthMap struct { 163 | UserARN string `yaml:"userarn"` 164 | Username string `yaml:"username"` 165 | Groups []string `yaml:"groups,omitempty"` 166 | } 167 | 168 | func (r *UsersAuthMap) String() string { 169 | var s strings.Builder 170 | s.WriteString(fmt.Sprintf("- userarn: %v\n ", r.UserARN)) 171 | s.WriteString(fmt.Sprintf("username: %v\n ", r.Username)) 172 | s.WriteString("groups:\n") 173 | for _, group := range r.Groups { 174 | s.WriteString(fmt.Sprintf(" - %v\n", group)) 175 | } 176 | return s.String() 177 | } 178 | 179 | // NewRolesAuthMap returns a new NewRolesAuthMap 180 | func NewRolesAuthMap(rolearn, username string, groups []string) *RolesAuthMap { 181 | return &RolesAuthMap{ 182 | RoleARN: rolearn, 183 | Username: username, 184 | Groups: groups, 185 | } 186 | } 187 | 188 | // NewUsersAuthMap returns a new NewUsersAuthMap 189 | func NewUsersAuthMap(userarn, username string, groups []string) *UsersAuthMap { 190 | return &UsersAuthMap{ 191 | UserARN: userarn, 192 | Username: username, 193 | Groups: groups, 194 | } 195 | } 196 | 197 | // SetUsername sets the Username value 198 | func (r *UsersAuthMap) SetUsername(v string) *UsersAuthMap { 199 | r.Username = v 200 | return r 201 | } 202 | 203 | // SetGroups sets the Groups value 204 | func (r *UsersAuthMap) SetGroups(g []string) *UsersAuthMap { 205 | r.Groups = g 206 | return r 207 | } 208 | 209 | // SetUsername sets the Username value 210 | func (r *RolesAuthMap) SetUsername(v string) *RolesAuthMap { 211 | r.Username = v 212 | return r 213 | } 214 | 215 | // SetGroups sets the Groups value 216 | func (r *RolesAuthMap) SetGroups(g []string) *RolesAuthMap { 217 | r.Groups = g 218 | return r 219 | } 220 | 221 | // AppendGroups sets the Groups value 222 | func (r *UsersAuthMap) AppendGroups(g []string) *UsersAuthMap { 223 | r.Groups = append(r.Groups, g...) 224 | return r 225 | } 226 | 227 | // AppendGroups sets the Groups value 228 | func (r *RolesAuthMap) AppendGroups(g []string) *RolesAuthMap { 229 | r.Groups = append(r.Groups, g...) 230 | return r 231 | } 232 | 233 | type RetriableFunction func() (interface{}, error) 234 | 235 | func WithRetry(fn RetriableFunction, args *MapperArguments) (interface{}, error) { 236 | // Update the config map and return an AuthMap 237 | var ( 238 | counter int 239 | err error 240 | out interface{} 241 | bkoff = &backoff.Backoff{ 242 | Min: args.MinRetryTime, 243 | Max: args.MaxRetryTime, 244 | Factor: DefaultRetryerBackoffFactor, 245 | Jitter: DefaultRetryerBackoffJitter, 246 | } 247 | ) 248 | 249 | for counter < args.MaxRetryCount { 250 | 251 | if out, err = fn(); err != nil { 252 | d := bkoff.Duration() 253 | log.Printf("error: %v: will retry after %v", err, d) 254 | time.Sleep(d) 255 | counter++ 256 | continue 257 | } 258 | return out, nil 259 | } 260 | return out, errors.Wrap(err, "waiter timed out") 261 | } 262 | 263 | type UpsertOptions struct { 264 | Append bool 265 | UpdateUsername bool 266 | } 267 | -------------------------------------------------------------------------------- /pkg/mapper/upsert.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package mapper 17 | 18 | import ( 19 | "log" 20 | "reflect" 21 | ) 22 | 23 | // Upsert update or inserts by rolearn 24 | func (b *AuthMapper) Upsert(args *MapperArguments) error { 25 | args.Validate() 26 | 27 | if args.WithRetries { 28 | _, err := WithRetry(func() (interface{}, error) { 29 | return nil, b.upsertAuth(args) 30 | }, args) 31 | return err 32 | } 33 | 34 | return b.upsertAuth(args) 35 | } 36 | 37 | /** 38 | * UpsertMultiple upserts list of mapRoles and mapUsers into the configmap 39 | * if no changes are required based on new entries, configmap doesn't get updated 40 | */ 41 | func (b *AuthMapper) UpsertMultiple(newMapRoles []*RolesAuthMap, newMapUsers []*UsersAuthMap) error { 42 | updated := false 43 | mapRoles := []*RolesAuthMap{} 44 | mapUsers := []*UsersAuthMap{} 45 | 46 | // Read the config map and return an AuthMap 47 | authData, configMap, err := ReadAuthMap(b.KubernetesClient) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | // Insert all new mapRole entries 53 | for _, newMember := range newMapRoles { 54 | found := false 55 | for _, existing := range authData.MapRoles { 56 | 57 | if existing.RoleARN == newMember.RoleARN { 58 | found = true 59 | } 60 | } 61 | 62 | if !found { 63 | updated = true 64 | mapRoles = append(mapRoles, newMember) 65 | } 66 | } 67 | 68 | // Upsert existing mapRoles 69 | for _, existing := range authData.MapRoles { 70 | 71 | for _, newMember := range newMapRoles { 72 | 73 | if existing.RoleARN != newMember.RoleARN { 74 | continue 75 | } 76 | 77 | if !reflect.DeepEqual(existing.Groups, newMember.Groups) { 78 | existing.SetGroups(newMember.Groups) 79 | updated = true 80 | } 81 | 82 | if existing.Username != newMember.Username { 83 | existing.SetUsername(newMember.Username) 84 | updated = true 85 | } 86 | 87 | } 88 | 89 | mapRoles = append(mapRoles, existing) 90 | } 91 | 92 | // Insert all new mapUser entries 93 | for _, newMember := range newMapUsers { 94 | found := false 95 | for _, existing := range authData.MapUsers { 96 | 97 | if existing.UserARN == newMember.UserARN { 98 | found = true 99 | } 100 | } 101 | 102 | if !found { 103 | updated = true 104 | mapUsers = append(mapUsers, newMember) 105 | } 106 | } 107 | 108 | // Upsert existing mapUsers 109 | for _, existing := range authData.MapUsers { 110 | 111 | for _, newMember := range newMapUsers { 112 | 113 | if existing.UserARN != newMember.UserARN { 114 | continue 115 | } 116 | 117 | if !reflect.DeepEqual(existing.Groups, newMember.Groups) { 118 | existing.SetGroups(newMember.Groups) 119 | updated = true 120 | } 121 | 122 | if existing.Username != newMember.Username { 123 | existing.SetUsername(newMember.Username) 124 | updated = true 125 | } 126 | 127 | } 128 | 129 | mapUsers = append(mapUsers, existing) 130 | } 131 | 132 | if !updated { 133 | log.Printf("found zero changes to update, configmap is not changed \n") 134 | return nil 135 | } 136 | 137 | authData.SetMapRoles(mapRoles) 138 | authData.SetMapUsers((mapUsers)) 139 | 140 | // Update the config map and return an AuthMap 141 | err = UpdateAuthMap(b.KubernetesClient, authData, configMap) 142 | if err != nil { 143 | return err 144 | } 145 | 146 | return nil 147 | } 148 | 149 | func (b *AuthMapper) upsertAuth(args *MapperArguments) error { 150 | // Read the config map and return an AuthMap 151 | authData, configMap, err := ReadAuthMap(b.KubernetesClient) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | opts := &UpsertOptions{ 157 | Append: args.Append, 158 | UpdateUsername: *args.UpdateUsername, 159 | } 160 | 161 | if args.MapRoles { 162 | var roleResource = NewRolesAuthMap(args.RoleARN, args.Username, args.Groups) 163 | 164 | newMap, ok := upsertRole(authData.MapRoles, roleResource, opts) 165 | if ok { 166 | log.Printf("role %v has been updated\n", roleResource.RoleARN) 167 | } else { 168 | log.Printf("no updates needed to %v\n", roleResource.RoleARN) 169 | } 170 | authData.SetMapRoles(newMap) 171 | } 172 | 173 | if args.MapUsers { 174 | var userResource = NewUsersAuthMap(args.UserARN, args.Username, args.Groups) 175 | 176 | newMap, ok := upsertUser(authData.MapUsers, userResource, opts) 177 | if ok { 178 | log.Printf("role %v has been updated\n", userResource.UserARN) 179 | } else { 180 | log.Printf("no updates needed to %v\n", userResource.UserARN) 181 | } 182 | authData.SetMapUsers(newMap) 183 | } 184 | 185 | return UpdateAuthMap(b.KubernetesClient, authData, configMap) 186 | } 187 | 188 | func upsertRole(authMaps []*RolesAuthMap, resource *RolesAuthMap, opts *UpsertOptions) ([]*RolesAuthMap, bool) { 189 | var match bool 190 | var updated bool 191 | for _, existing := range authMaps { 192 | // Update 193 | if existing.RoleARN == resource.RoleARN { 194 | match = true 195 | if !reflect.DeepEqual(existing.Groups, resource.Groups) { 196 | if opts.Append { 197 | existing.AppendGroups(resource.Groups) 198 | } else { 199 | existing.SetGroups(resource.Groups) 200 | } 201 | updated = true 202 | } 203 | if existing.Username != resource.Username { 204 | if opts.UpdateUsername { 205 | existing.SetUsername(resource.Username) 206 | updated = true 207 | } 208 | } 209 | } 210 | } 211 | 212 | // Insert 213 | if !match { 214 | updated = true 215 | authMaps = append(authMaps, resource) 216 | } 217 | return authMaps, updated 218 | } 219 | 220 | func upsertUser(authMaps []*UsersAuthMap, resource *UsersAuthMap, opts *UpsertOptions) ([]*UsersAuthMap, bool) { 221 | var match bool 222 | var updated bool 223 | for _, existing := range authMaps { 224 | // Update 225 | if existing.UserARN == resource.UserARN { 226 | match = true 227 | if !reflect.DeepEqual(existing.Groups, resource.Groups) { 228 | if opts.Append { 229 | existing.AppendGroups(resource.Groups) 230 | } else { 231 | existing.SetGroups(resource.Groups) 232 | } 233 | updated = true 234 | } 235 | if existing.Username != resource.Username { 236 | if opts.UpdateUsername { 237 | existing.SetUsername(resource.Username) 238 | updated = true 239 | } 240 | } 241 | } 242 | } 243 | 244 | // Insert 245 | if !match { 246 | updated = true 247 | authMaps = append(authMaps, resource) 248 | } 249 | return authMaps, updated 250 | } 251 | -------------------------------------------------------------------------------- /pkg/mapper/upsert_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package mapper 17 | 18 | import ( 19 | "testing" 20 | "time" 21 | 22 | "github.com/onsi/gomega" 23 | "k8s.io/client-go/kubernetes/fake" 24 | ) 25 | 26 | func TestMapper_UpsertInsert(t *testing.T) { 27 | g := gomega.NewWithT(t) 28 | gomega.RegisterTestingT(t) 29 | client := fake.NewSimpleClientset() 30 | mapper := New(client, true) 31 | create_MockConfigMap(client) 32 | 33 | err := mapper.Upsert(&MapperArguments{ 34 | MapRoles: true, 35 | RoleARN: "arn:aws:iam::00000000000:role/node-2", 36 | Username: "system:node:{{EC2PrivateDNSName}}", 37 | Groups: []string{"system:bootstrappers", "system:nodes"}, 38 | }) 39 | g.Expect(err).NotTo(gomega.HaveOccurred()) 40 | 41 | err = mapper.Upsert(&MapperArguments{ 42 | MapUsers: true, 43 | UserARN: "arn:aws:iam::00000000000:user/user-2", 44 | Username: "admin", 45 | Groups: []string{"system:masters"}, 46 | }) 47 | g.Expect(err).NotTo(gomega.HaveOccurred()) 48 | 49 | auth, _, err := ReadAuthMap(client) 50 | g.Expect(err).NotTo(gomega.HaveOccurred()) 51 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(2)) 52 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(2)) 53 | } 54 | 55 | func TestMapper_UpsertUpdate(t *testing.T) { 56 | g := gomega.NewWithT(t) 57 | gomega.RegisterTestingT(t) 58 | client := fake.NewSimpleClientset() 59 | mapper := New(client, true) 60 | create_MockConfigMap(client) 61 | 62 | err := mapper.Upsert(&MapperArguments{ 63 | MapRoles: true, 64 | RoleARN: "arn:aws:iam::00000000000:role/node-1", 65 | Username: "this:is:a:test", 66 | Groups: []string{"system:some-role"}, 67 | }) 68 | g.Expect(err).NotTo(gomega.HaveOccurred()) 69 | 70 | err = mapper.Upsert(&MapperArguments{ 71 | MapUsers: true, 72 | UserARN: "arn:aws:iam::00000000000:user/user-1", 73 | Username: "this:is:a:test", 74 | Groups: []string{"system:some-role"}, 75 | }) 76 | g.Expect(err).NotTo(gomega.HaveOccurred()) 77 | 78 | auth, _, err := ReadAuthMap(client) 79 | g.Expect(err).NotTo(gomega.HaveOccurred()) 80 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(1)) 81 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(1)) 82 | g.Expect(auth.MapRoles[0].RoleARN).To(gomega.Equal("arn:aws:iam::00000000000:role/node-1")) 83 | g.Expect(auth.MapRoles[0].Username).To(gomega.Equal("this:is:a:test")) 84 | g.Expect(auth.MapRoles[0].Groups).To(gomega.Equal([]string{"system:some-role"})) 85 | g.Expect(auth.MapUsers[0].UserARN).To(gomega.Equal("arn:aws:iam::00000000000:user/user-1")) 86 | g.Expect(auth.MapUsers[0].Username).To(gomega.Equal("this:is:a:test")) 87 | g.Expect(auth.MapUsers[0].Groups).To(gomega.Equal([]string{"system:some-role"})) 88 | } 89 | 90 | func TestMapper_UpsertNotNeeded(t *testing.T) { 91 | g := gomega.NewWithT(t) 92 | gomega.RegisterTestingT(t) 93 | client := fake.NewSimpleClientset() 94 | mapper := New(client, true) 95 | create_MockConfigMap(client) 96 | 97 | err := mapper.Upsert(&MapperArguments{ 98 | MapRoles: true, 99 | RoleARN: "arn:aws:iam::00000000000:role/node-1", 100 | Username: "system:node:{{EC2PrivateDNSName}}", 101 | Groups: []string{"system:bootstrappers", "system:nodes"}, 102 | }) 103 | g.Expect(err).NotTo(gomega.HaveOccurred()) 104 | 105 | err = mapper.Upsert(&MapperArguments{ 106 | MapUsers: true, 107 | UserARN: "arn:aws:iam::00000000000:user/user-1", 108 | Username: "admin", 109 | Groups: []string{"system:masters"}, 110 | }) 111 | g.Expect(err).NotTo(gomega.HaveOccurred()) 112 | 113 | auth, _, err := ReadAuthMap(client) 114 | g.Expect(err).NotTo(gomega.HaveOccurred()) 115 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(1)) 116 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(1)) 117 | } 118 | 119 | func TestMapper_UpsertWithCreate(t *testing.T) { 120 | g := gomega.NewWithT(t) 121 | gomega.RegisterTestingT(t) 122 | client := fake.NewSimpleClientset() 123 | mapper := New(client, true) 124 | 125 | err := mapper.Upsert(&MapperArguments{ 126 | MapRoles: true, 127 | RoleARN: "arn:aws:iam::00000000000:role/node-1", 128 | Username: "this:is:a:test", 129 | Groups: []string{"system:some-role"}, 130 | }) 131 | g.Expect(err).NotTo(gomega.HaveOccurred()) 132 | 133 | err = mapper.Upsert(&MapperArguments{ 134 | MapUsers: true, 135 | UserARN: "arn:aws:iam::00000000000:user/user-1", 136 | Username: "this:is:a:test", 137 | Groups: []string{"system:some-role"}, 138 | }) 139 | g.Expect(err).NotTo(gomega.HaveOccurred()) 140 | 141 | auth, _, err := ReadAuthMap(client) 142 | g.Expect(err).NotTo(gomega.HaveOccurred()) 143 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(1)) 144 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(1)) 145 | g.Expect(auth.MapRoles[0].RoleARN).To(gomega.Equal("arn:aws:iam::00000000000:role/node-1")) 146 | g.Expect(auth.MapRoles[0].Username).To(gomega.Equal("this:is:a:test")) 147 | g.Expect(auth.MapRoles[0].Groups).To(gomega.Equal([]string{"system:some-role"})) 148 | g.Expect(auth.MapUsers[0].UserARN).To(gomega.Equal("arn:aws:iam::00000000000:user/user-1")) 149 | g.Expect(auth.MapUsers[0].Username).To(gomega.Equal("this:is:a:test")) 150 | g.Expect(auth.MapUsers[0].Groups).To(gomega.Equal([]string{"system:some-role"})) 151 | } 152 | 153 | func TestMapper_UpsertMultipleInsert(t *testing.T) { 154 | g := gomega.NewWithT(t) 155 | gomega.RegisterTestingT(t) 156 | client := fake.NewSimpleClientset() 157 | mapper := New(client, true) 158 | create_MockConfigMap(client) 159 | 160 | role2 := &RolesAuthMap{ 161 | RoleARN: "arn:aws:iam::00000000000:role/node-2", 162 | Username: "system:node:{{EC2PrivateDNSName}}", 163 | Groups: []string{"system:bootstrappers", "system:nodes"}, 164 | } 165 | 166 | role3 := &RolesAuthMap{ 167 | RoleARN: "arn:aws:iam::00000000000:role/node-3", 168 | Username: "system:node:{{EC2PrivateDNSName}}", 169 | Groups: []string{"system:bootstrappers", "system:nodes"}, 170 | } 171 | 172 | err := mapper.UpsertMultiple([]*RolesAuthMap{role2, role3}, []*UsersAuthMap{}) 173 | g.Expect(err).NotTo(gomega.HaveOccurred()) 174 | 175 | auth, _, err := ReadAuthMap(client) 176 | g.Expect(err).NotTo(gomega.HaveOccurred()) 177 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(3)) 178 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(1)) 179 | 180 | mapUser2 := &UsersAuthMap{ 181 | UserARN: "arn:aws:iam::00000000000:user/user-2", 182 | Username: "admin", 183 | Groups: []string{"system:masters"}, 184 | } 185 | 186 | err = mapper.UpsertMultiple([]*RolesAuthMap{}, []*UsersAuthMap{mapUser2}) 187 | g.Expect(err).NotTo(gomega.HaveOccurred()) 188 | 189 | auth, _, err = ReadAuthMap(client) 190 | g.Expect(err).NotTo(gomega.HaveOccurred()) 191 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(3)) 192 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(2)) 193 | } 194 | 195 | func TestMapper_UpsertMultipleUpdate(t *testing.T) { 196 | g := gomega.NewWithT(t) 197 | gomega.RegisterTestingT(t) 198 | client := fake.NewSimpleClientset() 199 | mapper := New(client, true) 200 | create_MockConfigMap(client) 201 | 202 | role1 := &RolesAuthMap{ 203 | RoleARN: "arn:aws:iam::00000000000:role/node-1", 204 | Username: "this:is:a:test", 205 | Groups: []string{"system:some-role"}, 206 | } 207 | 208 | err := mapper.UpsertMultiple([]*RolesAuthMap{role1}, []*UsersAuthMap{}) 209 | g.Expect(err).NotTo(gomega.HaveOccurred()) 210 | 211 | auth, _, err := ReadAuthMap(client) 212 | g.Expect(err).NotTo(gomega.HaveOccurred()) 213 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(1)) 214 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(1)) 215 | g.Expect(auth.MapRoles[0].RoleARN).To(gomega.Equal("arn:aws:iam::00000000000:role/node-1")) 216 | g.Expect(auth.MapRoles[0].Username).To(gomega.Equal("this:is:a:test")) 217 | g.Expect(auth.MapRoles[0].Groups).To(gomega.Equal([]string{"system:some-role"})) 218 | g.Expect(auth.MapUsers[0].UserARN).To(gomega.Equal("arn:aws:iam::00000000000:user/user-1")) 219 | g.Expect(auth.MapUsers[0].Username).To(gomega.Equal("admin")) 220 | g.Expect(auth.MapUsers[0].Groups).To(gomega.Equal([]string{"system:masters"})) 221 | 222 | mapUser1 := &UsersAuthMap{ 223 | UserARN: "arn:aws:iam::00000000000:user/user-1", 224 | Username: "this:is:a:test", 225 | Groups: []string{"system:some-role"}, 226 | } 227 | 228 | err = mapper.UpsertMultiple([]*RolesAuthMap{}, []*UsersAuthMap{mapUser1}) 229 | g.Expect(err).NotTo(gomega.HaveOccurred()) 230 | 231 | auth, _, err = ReadAuthMap(client) 232 | g.Expect(err).NotTo(gomega.HaveOccurred()) 233 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(1)) 234 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(1)) 235 | g.Expect(auth.MapRoles[0].RoleARN).To(gomega.Equal("arn:aws:iam::00000000000:role/node-1")) 236 | g.Expect(auth.MapRoles[0].Username).To(gomega.Equal("this:is:a:test")) 237 | g.Expect(auth.MapRoles[0].Groups).To(gomega.Equal([]string{"system:some-role"})) 238 | g.Expect(auth.MapUsers[0].UserARN).To(gomega.Equal("arn:aws:iam::00000000000:user/user-1")) 239 | g.Expect(auth.MapUsers[0].Username).To(gomega.Equal("this:is:a:test")) 240 | g.Expect(auth.MapUsers[0].Groups).To(gomega.Equal([]string{"system:some-role"})) 241 | } 242 | 243 | func TestMapper_UpsertMultipleNotNeeded(t *testing.T) { 244 | g := gomega.NewWithT(t) 245 | gomega.RegisterTestingT(t) 246 | client := fake.NewSimpleClientset() 247 | mapper := New(client, true) 248 | create_MockConfigMap(client) 249 | 250 | role1 := &RolesAuthMap{ 251 | RoleARN: "arn:aws:iam::00000000000:role/node-1", 252 | Username: "system:node:{{EC2PrivateDNSName}}", 253 | Groups: []string{"system:bootstrappers", "system:nodes"}, 254 | } 255 | 256 | mapUser1 := &UsersAuthMap{ 257 | UserARN: "arn:aws:iam::00000000000:user/user-1", 258 | Username: "admin", 259 | Groups: []string{"system:masters"}, 260 | } 261 | 262 | err := mapper.UpsertMultiple([]*RolesAuthMap{role1}, []*UsersAuthMap{mapUser1}) 263 | g.Expect(err).NotTo(gomega.HaveOccurred()) 264 | 265 | auth, _, err := ReadAuthMap(client) 266 | g.Expect(err).NotTo(gomega.HaveOccurred()) 267 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(1)) 268 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(1)) 269 | } 270 | 271 | func TestMapper_UpsertMultipleWithCreate(t *testing.T) { 272 | g := gomega.NewWithT(t) 273 | gomega.RegisterTestingT(t) 274 | client := fake.NewSimpleClientset() 275 | mapper := New(client, true) 276 | 277 | role1 := &RolesAuthMap{ 278 | RoleARN: "arn:aws:iam::00000000000:role/node-1", 279 | Username: "this:is:a:test", 280 | Groups: []string{"system:some-role"}, 281 | } 282 | 283 | mapUser1 := &UsersAuthMap{ 284 | UserARN: "arn:aws:iam::00000000000:user/user-1", 285 | Username: "this:is:a:test", 286 | Groups: []string{"system:some-role"}, 287 | } 288 | 289 | err := mapper.UpsertMultiple([]*RolesAuthMap{role1}, []*UsersAuthMap{mapUser1}) 290 | g.Expect(err).NotTo(gomega.HaveOccurred()) 291 | 292 | auth, _, err := ReadAuthMap(client) 293 | g.Expect(err).NotTo(gomega.HaveOccurred()) 294 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(1)) 295 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(1)) 296 | g.Expect(auth.MapRoles[0].RoleARN).To(gomega.Equal("arn:aws:iam::00000000000:role/node-1")) 297 | g.Expect(auth.MapRoles[0].Username).To(gomega.Equal("this:is:a:test")) 298 | g.Expect(auth.MapRoles[0].Groups).To(gomega.Equal([]string{"system:some-role"})) 299 | g.Expect(auth.MapUsers[0].UserARN).To(gomega.Equal("arn:aws:iam::00000000000:user/user-1")) 300 | g.Expect(auth.MapUsers[0].Username).To(gomega.Equal("this:is:a:test")) 301 | g.Expect(auth.MapUsers[0].Groups).To(gomega.Equal([]string{"system:some-role"})) 302 | } 303 | 304 | func TestMapper_UpsertEmptyGroups(t *testing.T) { 305 | g := gomega.NewWithT(t) 306 | gomega.RegisterTestingT(t) 307 | client := fake.NewSimpleClientset() 308 | mapper := New(client, true) 309 | 310 | role1 := &RolesAuthMap{ 311 | RoleARN: "arn:aws:iam::00000000000:role/node-1", 312 | Username: "this:is:a:test", 313 | Groups: []string{}, 314 | } 315 | 316 | mapUser1 := &UsersAuthMap{ 317 | UserARN: "arn:aws:iam::00000000000:user/user-1", 318 | Username: "this:is:a:test", 319 | Groups: []string{}, 320 | } 321 | 322 | err := mapper.UpsertMultiple([]*RolesAuthMap{role1}, []*UsersAuthMap{mapUser1}) 323 | g.Expect(err).NotTo(gomega.HaveOccurred()) 324 | 325 | auth, _, err := ReadAuthMap(client) 326 | g.Expect(err).NotTo(gomega.HaveOccurred()) 327 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(1)) 328 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(1)) 329 | g.Expect(auth.MapRoles[0].RoleARN).To(gomega.Equal("arn:aws:iam::00000000000:role/node-1")) 330 | g.Expect(auth.MapRoles[0].Username).To(gomega.Equal("this:is:a:test")) 331 | g.Expect(auth.MapRoles[0].Groups).To(gomega.BeNil()) 332 | g.Expect(auth.MapUsers[0].UserARN).To(gomega.Equal("arn:aws:iam::00000000000:user/user-1")) 333 | g.Expect(auth.MapUsers[0].Username).To(gomega.Equal("this:is:a:test")) 334 | g.Expect(auth.MapUsers[0].Groups).To(gomega.BeNil()) 335 | } 336 | 337 | func TestMapper_UpsertWithRetries(t *testing.T) { 338 | g := gomega.NewWithT(t) 339 | gomega.RegisterTestingT(t) 340 | client := fake.NewSimpleClientset() 341 | mapper := New(client, true) 342 | create_MockConfigMap(client) 343 | 344 | err := mapper.Upsert(&MapperArguments{ 345 | MapRoles: true, 346 | RoleARN: "arn:aws:iam::00000000000:role/node-2", 347 | Username: "system:node:{{EC2PrivateDNSName}}", 348 | Groups: []string{"system:bootstrappers", "system:nodes"}, 349 | WithRetries: true, 350 | MinRetryTime: time.Millisecond * 1, 351 | MaxRetryTime: time.Millisecond * 2, 352 | MaxRetryCount: 3, 353 | }) 354 | g.Expect(err).NotTo(gomega.HaveOccurred()) 355 | 356 | err = mapper.Upsert(&MapperArguments{ 357 | MapUsers: true, 358 | UserARN: "arn:aws:iam::00000000000:user/user-2", 359 | Username: "admin", 360 | Groups: []string{"system:masters"}, 361 | WithRetries: true, 362 | MinRetryTime: time.Millisecond * 1, 363 | MaxRetryTime: time.Millisecond * 2, 364 | MaxRetryCount: 3, 365 | }) 366 | g.Expect(err).NotTo(gomega.HaveOccurred()) 367 | 368 | auth, _, err := ReadAuthMap(client) 369 | g.Expect(err).NotTo(gomega.HaveOccurred()) 370 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(2)) 371 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(2)) 372 | } 373 | 374 | func TestUpsertWithRetries(t *testing.T) { 375 | g := gomega.NewWithT(t) 376 | gomega.RegisterTestingT(t) 377 | client := fake.NewSimpleClientset() 378 | mapper := New(client, true) 379 | create_MockConfigMap(client) 380 | 381 | err := mapper.Upsert(&MapperArguments{ 382 | MapRoles: true, 383 | RoleARN: "arn:aws:iam::00000000000:role/node-1", 384 | Username: "this:is:a:test", 385 | Groups: []string{"system:some-role"}, 386 | WithRetries: true, 387 | MaxRetryCount: 12, 388 | MaxRetryTime: 1 * time.Millisecond, 389 | MinRetryTime: 1 * time.Millisecond, 390 | }) 391 | g.Expect(err).NotTo(gomega.HaveOccurred()) 392 | 393 | err = mapper.Upsert(&MapperArguments{ 394 | MapUsers: true, 395 | UserARN: "arn:aws:iam::00000000000:user/user-1", 396 | Username: "this:is:a:test", 397 | Groups: []string{"system:some-role"}, 398 | }) 399 | g.Expect(err).NotTo(gomega.HaveOccurred()) 400 | 401 | auth, _, err := ReadAuthMap(client) 402 | g.Expect(err).NotTo(gomega.HaveOccurred()) 403 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(1)) 404 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(1)) 405 | g.Expect(auth.MapRoles[0].RoleARN).To(gomega.Equal("arn:aws:iam::00000000000:role/node-1")) 406 | g.Expect(auth.MapRoles[0].Username).To(gomega.Equal("this:is:a:test")) 407 | g.Expect(auth.MapRoles[0].Groups).To(gomega.Equal([]string{"system:some-role"})) 408 | g.Expect(auth.MapUsers[0].UserARN).To(gomega.Equal("arn:aws:iam::00000000000:user/user-1")) 409 | g.Expect(auth.MapUsers[0].Username).To(gomega.Equal("this:is:a:test")) 410 | g.Expect(auth.MapUsers[0].Groups).To(gomega.Equal([]string{"system:some-role"})) 411 | } 412 | 413 | func TestMapper_UpsertUpdateAppendGroups(t *testing.T) { 414 | g := gomega.NewWithT(t) 415 | gomega.RegisterTestingT(t) 416 | client := fake.NewSimpleClientset() 417 | mapper := New(client, true) 418 | create_MockConfigMap(client) 419 | 420 | err := mapper.Upsert(&MapperArguments{ 421 | MapRoles: true, 422 | RoleARN: "arn:aws:iam::00000000000:role/node-1", 423 | Username: "this:is:a:test", 424 | Groups: []string{"appendedGroup"}, 425 | Append: true, 426 | }) 427 | g.Expect(err).NotTo(gomega.HaveOccurred()) 428 | 429 | err = mapper.Upsert(&MapperArguments{ 430 | MapUsers: true, 431 | UserARN: "arn:aws:iam::00000000000:user/user-1", 432 | Username: "this:is:a:test", 433 | Groups: []string{"appendedGroup"}, 434 | Append: true, 435 | }) 436 | g.Expect(err).NotTo(gomega.HaveOccurred()) 437 | 438 | auth, _, err := ReadAuthMap(client) 439 | g.Expect(err).NotTo(gomega.HaveOccurred()) 440 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(1)) 441 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(1)) 442 | g.Expect(auth.MapRoles[0].RoleARN).To(gomega.Equal("arn:aws:iam::00000000000:role/node-1")) 443 | g.Expect(auth.MapRoles[0].Username).To(gomega.Equal("this:is:a:test")) 444 | g.Expect(auth.MapRoles[0].Groups).To(gomega.Equal([]string{"system:bootstrappers", "system:nodes", "appendedGroup"})) 445 | g.Expect(auth.MapUsers[0].UserARN).To(gomega.Equal("arn:aws:iam::00000000000:user/user-1")) 446 | g.Expect(auth.MapUsers[0].Username).To(gomega.Equal("this:is:a:test")) 447 | g.Expect(auth.MapUsers[0].Groups).To(gomega.Equal([]string{"system:masters", "appendedGroup"})) 448 | } 449 | 450 | func TestMapper_UpdateUsername(t *testing.T) { 451 | g := gomega.NewWithT(t) 452 | gomega.RegisterTestingT(t) 453 | client := fake.NewSimpleClientset() 454 | mapper := New(client, true) 455 | create_MockConfigMap(client) 456 | 457 | updateUsername := false 458 | 459 | err := mapper.Upsert(&MapperArguments{ 460 | MapRoles: true, 461 | RoleARN: "arn:aws:iam::00000000000:role/node-1", 462 | Username: "this:is:a:test", 463 | Groups: []string{"system:bootstrappers", "system:nodes"}, 464 | UpdateUsername: &updateUsername, 465 | }) 466 | g.Expect(err).NotTo(gomega.HaveOccurred()) 467 | 468 | err = mapper.Upsert(&MapperArguments{ 469 | MapUsers: true, 470 | UserARN: "arn:aws:iam::00000000000:user/user-1", 471 | Username: "this:is:a:test", 472 | Groups: []string{"system:masters"}, 473 | UpdateUsername: &updateUsername, 474 | }) 475 | g.Expect(err).NotTo(gomega.HaveOccurred()) 476 | 477 | auth, _, err := ReadAuthMap(client) 478 | g.Expect(err).NotTo(gomega.HaveOccurred()) 479 | g.Expect(len(auth.MapRoles)).To(gomega.Equal(1)) 480 | g.Expect(len(auth.MapUsers)).To(gomega.Equal(1)) 481 | g.Expect(auth.MapRoles[0].RoleARN).To(gomega.Equal("arn:aws:iam::00000000000:role/node-1")) 482 | g.Expect(auth.MapRoles[0].Username).To(gomega.Equal("system:node:{{EC2PrivateDNSName}}")) 483 | g.Expect(auth.MapRoles[0].Groups).To(gomega.Equal([]string{"system:bootstrappers", "system:nodes"})) 484 | g.Expect(auth.MapUsers[0].UserARN).To(gomega.Equal("arn:aws:iam::00000000000:user/user-1")) 485 | g.Expect(auth.MapUsers[0].Username).To(gomega.Equal("admin")) 486 | g.Expect(auth.MapUsers[0].Groups).To(gomega.Equal([]string{"system:masters"})) 487 | } 488 | --------------------------------------------------------------------------------