├── .dockerignore ├── .github └── workflows │ ├── CHANGELOG.md │ ├── build.yml │ ├── codeql-analysis.yml │ └── release-new-version.yaml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── cmd ├── connect.go ├── const.go ├── constant.go ├── core.go ├── dd.go ├── find.go ├── list.go ├── root.go └── struct.go ├── core ├── core.go ├── core_test.go ├── loggin.go ├── struct.go └── utils.go ├── go.mod ├── go.sum ├── main.go ├── port.yml ├── provider ├── aws.go ├── aws_test.go ├── azure.go ├── constant.go ├── core.go ├── core_test.go ├── gcp.go ├── interfaces.go ├── kube_merge.go └── structs.go ├── sonar-project.properties └── test ├── config └── credentials /.dockerignore: -------------------------------------------------------------------------------- 1 | test/ 2 | .github/ 3 | .devcontainer/ 4 | .gitignore 5 | CODE_OF_CONDUCT.md 6 | sonar-project.properties -------------------------------------------------------------------------------- /.github/workflows/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # k8f 2 | 3 | ## Release Notes 4 | 5 | ### Changes 6 | 7 | - moved to Go 1.24 8 | ### Known Issues 9 | 10 | 11 | ## Bugfix 12 | 13 | - fixed no INI in path failes the task 14 | - updated packages with security issues. 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - master # or the name of your main branch 6 | - main # or the name of your main branch 7 | - feature/* 8 | - fix/* 9 | - issue/* 10 | jobs: 11 | build: 12 | name: Build 13 | runs-on: ubuntu-20.04 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | - name: Setup Go 19 | uses: actions/setup-go@v3 20 | with: 21 | go-version: '1.24' 22 | check-latest: true 23 | - name: Run test coverage 24 | run: go test ./... -coverprofile=coverage.out 25 | - name: Run test report 26 | run: go test ./... -json > report.json 27 | - uses: sonarsource/sonarqube-scan-action@master 28 | env: 29 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 30 | SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} 31 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '16 14 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | - name: Setup Go 43 | uses: actions/setup-go@v3 44 | with: 45 | go-version: '1.24' 46 | check-latest: true 47 | # Initializes the CodeQL tools for scanning. 48 | - name: Initialize CodeQL 49 | uses: github/codeql-action/init@v2 50 | with: 51 | languages: ${{ matrix.language }} 52 | # If you wish to specify custom queries, you can do so here or in a config file. 53 | # By default, queries listed here will override any specified in a config file. 54 | # Prefix the list here with "+" to use these queries and those in the config file. 55 | 56 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 57 | # queries: security-extended,security-and-quality 58 | 59 | 60 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 61 | # If this step fails, then you should remove it and run the build manually (see below) 62 | #- name: Autobuild 63 | # uses: github/codeql-action/autobuild@v2 64 | 65 | # ℹ️ Command-line programs to run using the OS shell. 66 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 67 | 68 | # If the Autobuild fails above, remove it and uncomment the following three lines. 69 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 70 | 71 | - run: | 72 | echo "Run, Build Application using script" 73 | go build . 74 | 75 | - name: Perform CodeQL Analysis 76 | uses: github/codeql-action/analyze@v2 77 | with: 78 | category: "/language:${{matrix.language}}" 79 | -------------------------------------------------------------------------------- /.github/workflows/release-new-version.yaml: -------------------------------------------------------------------------------- 1 | # workflow name 2 | name: Generate release-artifacts 3 | 4 | # on events 5 | on: 6 | push: 7 | tags: 8 | - '*' 9 | 10 | # workflow tasks 11 | #https://trstringer.com/github-actions-create-release-upload-artifacts/ 12 | jobs: 13 | generate: 14 | name: Generate cross-platform builds 15 | runs-on: ubuntu-20.04 16 | steps: 17 | - name: Checkout the repository 18 | uses: actions/checkout@v3 19 | - name: Setup Go 20 | uses: actions/setup-go@v3 21 | with: 22 | go-version: '1.24' 23 | check-latest: true 24 | - name: version 25 | run: echo "::set-output name=version::$(git describe --tags --always --abbrev=0 --match='[0-9]*.[0-9]*.[0-9]*' 2> /dev/null)" 26 | id: version 27 | - name: Set up QEMU 28 | uses: docker/setup-qemu-action@v1 29 | 30 | - name: Set up Docker Buildx 31 | uses: docker/setup-buildx-action@v1 32 | 33 | - name: Login to DockerHub 34 | uses: docker/login-action@v1 35 | with: 36 | username: ${{ secrets.DOCKERHUB_USERNAME }} 37 | password: ${{ secrets.DOCKERHUB_TOKEN }} 38 | - name: Run Linux Build 39 | run: go build -ldflags="-X 'k8f/cmd.tversion=${{ steps.version.outputs.version }}'" . 40 | - name: Run Windows Build 41 | run: env GOOS=windows GOARCH=amd64 go build -ldflags="-X 'k8f/cmd.tversion=${{ steps.version.outputs.version }}'" -o ./k8f.exe . 42 | - name: Run Mac Amd arch Build 43 | run: env GOOS=darwin GOARCH=amd64 go build -ldflags="-X 'k8f/cmd.tversion=${{ steps.version.outputs.version }}'" -o ./k8f_darwin_amd . 44 | - name: Run Mac Arm arch Build 45 | run: env GOOS=darwin GOARCH=arm64 go build -ldflags="-X 'k8f/cmd.tversion=${{ steps.version.outputs.version }}'" -o ./k8f_darwin_arm . 46 | - name: Build and push 47 | uses: docker/build-push-action@v2 48 | with: 49 | context: . 50 | platforms: linux/amd64,linux/arm64 51 | push: true 52 | tags: | 53 | unsoop/k8f:latest 54 | unsoop/k8f:${{ steps.version.outputs.version }} 55 | - name: release 56 | uses: actions/create-release@v1 57 | id: create_release 58 | with: 59 | draft: false 60 | prerelease: true 61 | release_name: ${{ steps.version.outputs.version }} 62 | tag_name: ${{ github.ref }} 63 | body_path: ./.github/workflows/CHANGELOG.md 64 | env: 65 | GITHUB_TOKEN: ${{ github.token }} 66 | - name: upload linux artifact 67 | uses: actions/upload-release-asset@v1 68 | env: 69 | GITHUB_TOKEN: ${{ github.token }} 70 | with: 71 | upload_url: ${{ steps.create_release.outputs.upload_url }} 72 | asset_path: ./k8f 73 | asset_name: k8f 74 | asset_content_type: text/plain 75 | - name: upload Windows artifact 76 | uses: actions/upload-release-asset@v1 77 | env: 78 | GITHUB_TOKEN: ${{ github.token }} 79 | with: 80 | upload_url: ${{ steps.create_release.outputs.upload_url }} 81 | asset_path: ./k8f.exe 82 | asset_name: k8f.exe 83 | asset_content_type: text/plain 84 | - name: upload Arm Mac artifact 85 | uses: actions/upload-release-asset@v1 86 | env: 87 | GITHUB_TOKEN: ${{ github.token }} 88 | with: 89 | upload_url: ${{ steps.create_release.outputs.upload_url }} 90 | asset_path: ./k8f_darwin_arm 91 | asset_name: k8f_darwin-arm64 92 | asset_content_type: text/plain 93 | - name: upload Amd Mac artifact 94 | uses: actions/upload-release-asset@v1 95 | env: 96 | GITHUB_TOKEN: ${{ github.token }} 97 | with: 98 | upload_url: ${{ steps.create_release.outputs.upload_url }} 99 | asset_path: ./k8f_darwin_amd 100 | asset_name: k8f_darwin-amd64 101 | asset_content_type: text/plain 102 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | local.prop 2 | testAws.go.not 3 | testfiles/ 4 | testconfig/ 5 | .ssh/ 6 | .vscode/ 7 | .mirrord/ 8 | k8f* 9 | !k8f-list.jpg 10 | .devcontainer/ 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | adam.russak@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM golang:1.24-alpine AS build 3 | 4 | COPY ./.git /tmp/build/.git 5 | COPY ./cmd /tmp/build/cmd 6 | COPY ./core /tmp/build/core 7 | COPY ./provider /tmp/build/provider 8 | COPY ./provider /tmp/build/provider 9 | COPY *.go /tmp/build/ 10 | COPY go.mod /tmp/build/ 11 | COPY go.sum /tmp/build/ 12 | WORKDIR /tmp/build 13 | RUN ls -alh . 14 | RUN apk update \ 15 | && apk add --no-cache \ 16 | git 17 | RUN version=$(git describe --tags --always --abbrev=0 --match='[0-9]*.[0-9]*.[0-9]*' 2> /dev/null) 18 | RUN echo $version 19 | RUN arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) && echo ${arch} && CGO_ENABLED=0 GOOS=linux GOARCH=${arch} go build -ldflags="-X 'k8f/cmd.tversion=${version}'" . 20 | 21 | FROM alpine:3.18.2 22 | RUN addgroup -S nonroot \ 23 | && adduser -S nonroot -G nonroot 24 | USER nonroot 25 | RUN mkdir -p ~/.aws 26 | COPY --from=build /tmp/build/k8f . 27 | ENTRYPOINT ["./k8f" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # :sun_behind_large_cloud: k8f :sun_behind_large_cloud: 4 | 5 |
6 | 7 | **k8f** is a command-line tool designed to simplify and streamline Kubernetes cluster operations.
8 | It provides a collection of useful commands and features that assist in managing and interacting with Kubernetes clusters efficiently.
9 | The tool was designed to scan all you're Azure and/or AWS Accounts for Kubernetes with a single command.
10 | 11 | **What can it do??**
12 | you can **Add** or **Update** EKS/AKS to your kubeconfig file.
13 | you can get you're EKS/AKS output with **k8s name**, **account**, **region**, **version**, and **upgrade status**. 14 | 15 | ## Table of Contents 16 | 17 | - [:sun\_behind\_large\_cloud: k8f :sun\_behind\_large\_cloud:](#sun_behind_large_cloud-k8f-sun_behind_large_cloud) 18 | - [Table of Contents](#table-of-contents) 19 | - [prerequisite](#prerequisite) 20 | - [Supported Platform](#supported-platform) 21 | - [Known issues](#known-issues) 22 | - [Commands](#commands) 23 | - [list](#list) 24 | - [connect](#connect) 25 | - [find](#find) 26 | - [How to install](#how-to-install) 27 | - [Windows](#windows) 28 | - [Linux](#linux) 29 | - [MacOS](#macos) 30 | - [Arm Processor](#arm-processor) 31 | - [Intel Processor](#intel-processor) 32 | - [Container](#container) 33 | 34 | 35 | 36 | 37 | > image created Using MidJorny
38 | 39 | > 40 | [![CodeQL](https://github.com/AdamRussak/k8f/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/AdamRussak/k8f/actions/workflows/codeql-analysis.yml) [![release-artifacts](https://github.com/AdamRussak/k8f/actions/workflows/release-new-version.yaml/badge.svg)](https://github.com/AdamRussak/k8f/actions/workflows/release-new-version.yaml) ![GitHub](https://img.shields.io/github/license/AdamRussak/k8f) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/AdamRussak/k8f) ![GitHub all releases](https://img.shields.io/github/downloads/AdamRussak/k8f/total) [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=AdamRussak_k8f&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=AdamRussak_k8f) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=AdamRussak_k8f&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=AdamRussak_k8f) 41 | [![Docker Size](https://img.shields.io/docker/image-size/unsoop/k8f?label=Size&logo=Docker&color=0aa8d2&logoColor=fff)](https://hub.docker.com/r/unsoop/k8f) 42 | 43 | Issues 44 | Stars 45 | 46 | [badge-info]: https://raw.githubusercontent.com/AdamRussak/public-images/main/badges/info.svg 'Info' 47 | 48 | > ![badge-info][badge-info]
49 | > Tested with:
50 | > AWS CLI: 2.9.17
51 | > AZ CLI: 2.44.1
52 | > Kubectl: v1.26.1
53 | 54 | ## prerequisite 55 | - for Azure: installed and logged in azure cli 56 | - for AWS: install AWS cli and Profiles for each Account at `~/.aws/credentials` and `~/.aws/config` 57 | - for GCP: Installed gcloud cli and logged in 58 | 59 | ## Supported Platform 60 | 61 | 62 | | Provider | CLI | Docker | 63 | |----------|----------|----------| 64 | | AWS | ☑ | ☑ | 65 | | Azure | ☑ | | 66 | | GCP | ☑ | | 67 | 68 | ### Known issues 69 | * GCP currently only supports the List command 70 | * Azure accounts with MFA enabled can cause failure 71 | 72 | ## Commands 73 | 74 | ### list 75 | ```sh 76 | List all K8S in Azure/AWS or Both 77 | 78 | Usage: 79 | k8f list [flags] 80 | 81 | Examples: 82 | k8f list {aws/azure/all} 83 | 84 | Flags: 85 | -h, --help help for list 86 | -o, --output string Set output type(json or yaml) (default "json") 87 | -p, --path string Set output path (default "./output") 88 | --profile-select Get UI to select single profile to connect 89 | -s, --save Get UI to select single profile to connect 90 | 91 | Global Flags: 92 | --aws-region string Set Default AWS Region (default "eu-west-1") 93 | --validate Fail on validation of the AWS credentals before running 94 | -v, --verbose verbose logging 95 | ``` 96 | 97 | List Command Sample Output: 98 | 99 | [![Sample of List command output](https://raw.githubusercontent.com/AdamRussak/public-images/main/k8f/k8f-list.jpg "Sample of List command output")](https://raw.githubusercontent.com/AdamRussak/public-images/main/k8f/k8f-list.jpg "Sample of List command output") 100 | 101 | ### connect 102 | ```sh 103 | Connect to all the clusters of a provider or all Supported Providers 104 | 105 | Usage: 106 | k8f connect [flags] 107 | 108 | Examples: 109 | k8f connect aws -p ./testfiles/config --backup -v 110 | k8f connect aws --isEnv -p ./testfiles/config --overwrite --backup --role-name "test role" -v 111 | 112 | Flags: 113 | --auth change from AWS CLI Auth to AWS IAM Authenticator, Default set to AWS CLI 114 | --backup If true, backup config file to $HOME/.kube/config.bk 115 | --dry-run If true, only run a dry-run with cli output 116 | --force-merge If set, all duplication will be merged without prompt, default is interactive 117 | -h, --help help for connect 118 | --isEnv Add AWS Profile as Env to the Kubeconfig 119 | --merge If true, add new K8s to the existing kubeconfig path 120 | -o, --output string kubeconfig output type format(json or yaml) (default "yaml") 121 | --overwrite If true, force overwrite kubeconfig 122 | -p, --path string Set output path (default "/home//.kube/config") 123 | --profile-select provides a UI to select a single profile to scan 124 | --role-name string Set Role Name (Example: 'myRoleName') 125 | -s, --short-name shorten EKS name from :: to : 126 | 127 | Global Flags: 128 | --aws-region string Set Default AWS Region (default "eu-west-1") 129 | --validate Fail on validation of the AWS credentals before running 130 | -v, --verbose verbose logging 131 | ``` 132 | 133 | ### find 134 | ```sh 135 | Find if a specific K8S exist in Azure or AWS 136 | 137 | Usage: 138 | k8f find [flags] 139 | 140 | Examples: 141 | k8f find {aws/azure/all} my-k8s-cluster 142 | 143 | Flags: 144 | -h, --help help for find 145 | 146 | Global Flags: 147 | --aws-region string Set Default AWS Region (default "eu-west-1") 148 | --validate Fail on validation of the AWS credentals before running 149 | -v, --verbose verbose logging 150 | ``` 151 | 152 | ## How to install 153 | 154 | ### Windows 155 | Latest: 156 | ```ps 157 | $downloads = "$env:USERPROFILE\Downloads" 158 | $source = "$downloads\k8f.exe" 159 | $destination = "C:\tool" 160 | Invoke-WebRequest -Uri "https://github.com/AdamRussak/k8f/releases/latest/download/k8f.exe" -OutFile $source 161 | New-Item -ItemType Directory -Path $destination 162 | Copy-Item -Path $source -Destination $destination 163 | [Environment]::SetEnvironmentVariable("Path", "$env:Path;$destination\k8f.exe", "Machine") 164 | ``` 165 | Version: 166 | ```ps 167 | $downloads = "$env:USERPROFILE\Downloads" 168 | $source = "$downloads\k8f.exe" 169 | $destination = "C:\tool" 170 | $version = "0.3.1" 171 | Invoke-WebRequest -Uri "https://github.com/AdamRussak/k8f/releases/download/$version/k8f.exe" -OutFile "$downloads\k8f.exe" 172 | New-Item -ItemType Directory -Path $destination 173 | Copy-Item -Path $source -Destination $destination 174 | [Environment]::SetEnvironmentVariable("Path", "$env:Path;$destination\k8f.exe", "Machine") 175 | ``` 176 | ### Linux 177 | Latest: 178 | ```sh 179 | cd ~ && wget https://github.com/AdamRussak/k8f/releases/latest/download/k8f 180 | sudo cp ~/k8f /usr/local/bin/k8f 181 | sudo chmod 755 /usr/local/bin/k8f 182 | ``` 183 | Version: 184 | ```sh 185 | cd ~ && wget https://github.com/AdamRussak/k8f/releases/download//k8f 186 | sudo cp ~/k8f /usr/local/bin/k8f 187 | sudo chmod 755 /usr/local/bin/k8f 188 | ``` 189 | ### MacOS 190 | #### Arm processor 191 | Latest: 192 | ```sh 193 | cd ~ && wget https://github.com/AdamRussak/k8f/releases/latest/download/k8f_darwin-arm64 194 | mv k8f_darwin-arm64 ./k8f 195 | sudo cp ~/k8f /usr/local/bin/k8f 196 | sudo chmod 755 /usr/local/bin/k8f 197 | ``` 198 | Version: 199 | ```sh 200 | cd ~ && wget https://github.com/AdamRussak/k8f/releases/download//k8f_darwin-arm64 201 | mv k8f_darwin-arm64 ./k8f 202 | sudo cp ~/k8f /usr/local/bin/k8f 203 | sudo chmod 755 /usr/local/bin/k8f 204 | ``` 205 | #### Intel processor 206 | Latest: 207 | ```sh 208 | cd ~ && wget https://github.com/AdamRussak/k8f/releases/latest/download/k8f_darwin-amd64 209 | mv k8f_darwin-amd64 ./k8f 210 | sudo cp ~/k8f /usr/local/bin/k8f 211 | sudo chmod 755 /usr/local/bin/k8f 212 | ``` 213 | Version: 214 | ```sh 215 | cd ~ && wget https://github.com/AdamRussak/k8f/releases/download//k8f_darwin-amd64 216 | mv k8f_darwin-amd64 ./k8f 217 | sudo cp ~/k8f /usr/local/bin/k8f 218 | sudo chmod 755 /usr/local/bin/k8f 219 | ``` 220 | 221 | ### Container 222 | ```sh 223 | # Basic 224 | docker run -v {path to .aws directory}:/home/nonroot/.aws/:ro unsoop/k8f: 225 | 226 | # Automation Queryable output 227 | OUTPUT=$(docker run -v {path to .aws directory}:/home/nonroot/.aws/:ro unsoop/k8f: 2> /dev/null | grep -o '{.*}') 228 | 229 | ``` 230 | -------------------------------------------------------------------------------- /cmd/connect.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 NAME HERE 3 | */ 4 | package cmd 5 | 6 | import ( 7 | "errors" 8 | "k8f/core" 9 | "k8f/provider" 10 | "os" 11 | 12 | log "github.com/sirupsen/logrus" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // FEATURE: add flag to support aws commands per account/profile (for example: ask per-account what to use) 17 | // connectCmd represents the connect command 18 | var ( 19 | connectCmd = &cobra.Command{ 20 | Use: connectCMD, 21 | Short: connectShort, 22 | Example: connectExample, 23 | Run: func(cmd *cobra.Command, args []string) { 24 | var options provider.CommandOptions = newCommandStruct(cmd, o, args) 25 | log.WithField("CommandOptions", log.Fields{"struct": core.DebugWithInfo(options)}).Debug("CommandOptions Struct Keys and Values: ") 26 | if !options.Overwrite && core.Exists(options.Path) && !options.DryRun && !options.Backup && !options.Merge { 27 | core.FailOnError(errors.New("flags error"), "Cant Run command as path exist and Overwrite is set to FALSE") 28 | } 29 | if !core.Exists(options.Path) { 30 | core.CreateDirectory(options.Path) 31 | log.Warn("Path directorys were created") 32 | } 33 | if !core.Exists(options.Path) && (options.Backup || options.Merge) { 34 | fileCreateConfirm := provider.BoolUI("--backup/--merge option was used but "+options.Path+ 35 | " does not exist. Choose True to create empty file, if False is chosen backup/merge will fail", options) 36 | if fileCreateConfirm == "True" { 37 | err := os.WriteFile(options.Path, []byte(""), 0666) 38 | core.FailOnError(err, "failed to save config") 39 | } else { 40 | core.FailOnError(errors.New("flags error"), "Cant Run command as path does not exist and Backup or Merge is set to True") 41 | } 42 | } 43 | if args[0] == "azure" { 44 | options.ConnectAllAks() 45 | } else if args[0] == "aws" { 46 | options.ConnectAllEks() 47 | } else if args[0] == "all" { 48 | log.Info("Supported Platform are:" + core.PrintOutStirng(supportedProvider)) 49 | options.FullCloudConfig() 50 | } else { 51 | core.FailOnError(errors.New("no Provider Selected"), "Selected Provider Not avilable (yet)") 52 | } 53 | }, 54 | } 55 | ) 56 | 57 | func init() { 58 | connectCmd.Flags().StringVarP(&o.Output, "output", "o", defaultYAMLoutput, "kubeconfig output type format(json or yaml)") 59 | connectCmd.Flags().StringVarP(&o.Path, "path", "p", confPath, "Set output path") 60 | connectCmd.Flags().BoolVar(&o.ProfileSelector, "profile-select", false, "provides a UI to select a single profile to scan") 61 | connectCmd.Flags().BoolVar(&o.Overwrite, "overwrite", false, "If true, force overwrite kubeconfig") 62 | connectCmd.Flags().BoolVar(&o.DryRun, DryRun, false, "If true, only run a dry-run with cli output") 63 | connectCmd.Flags().BoolVar(&o.Backup, "backup", false, "If true, backup config file to $HOME/.kube/config.bk") 64 | connectCmd.Flags().BoolVar(&o.Merge, "merge", false, "If true, add new K8s to the existing kubeconfig path") 65 | connectCmd.Flags().BoolVar(&o.ForceMerge, "force-merge", false, "If set, all duplication will be merged without prompt, default is interactive") 66 | connectCmd.Flags().BoolVar(&o.AwsAuth, "auth", false, "change from AWS CLI Auth to AWS IAM Authenticator, Default set to AWS CLI") 67 | connectCmd.Flags().BoolVar(&o.AwsEnvProfile, "isEnv", false, "Add AWS Profile as Env to the Kubeconfig") 68 | connectCmd.Flags().BoolVarP(&o.AwsClusterName, "short-name", "s", false, "shorten EKS name from :: to :") 69 | connectCmd.Flags().StringVar(&o.AwsRoleString, "role-name", "", "Set Role Name (Example: 'myRoleName')") 70 | connectCmd.MarkFlagsMutuallyExclusive(DryRun, "overwrite", "backup", "merge") 71 | rootCmd.AddCommand(connectCmd) 72 | } 73 | -------------------------------------------------------------------------------- /cmd/const.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | const ( 4 | // global 5 | providerError = "requires cloud provider" 6 | providerListError = "invalid cloud provider specified: %s" 7 | // connect 8 | connectCMD = "connect" 9 | connectShort = "Connect to all the clusters of a provider or all Supported Providers" 10 | connectExample = `k8f connect aws -p ./testfiles/config --backup -v 11 | k8f connect aws --isEnv -p ./testfiles/config --overwrite --backup --role-name "test role" -v` 12 | 13 | //find 14 | findCMD = "find" 15 | findShort = "Find if a specific K8S exist in Azure or AWS" 16 | findExample = `k8f find {aws/azure/all} my-k8s-cluster` 17 | //list 18 | listCMD = "list" 19 | listShort = "List all K8S in Azure/AWS or Both" 20 | listExample = `k8f list {aws/azure/all}` 21 | ) 22 | -------------------------------------------------------------------------------- /cmd/constant.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | const DryRun = "dry-run" 4 | const triggered = "triggered " 5 | const validationError = "validation failed" 6 | -------------------------------------------------------------------------------- /cmd/core.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "k8f/core" 7 | "k8f/provider" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func newCommandStruct(cmd *cobra.Command, o FlagsOptions, args []string) provider.CommandOptions { 13 | var commandPath string 14 | var commandOutput string 15 | switch cmd.Use { 16 | case "list": 17 | commandPath = o.ListPath 18 | commandOutput = o.ListOutput 19 | default: 20 | commandPath = o.Path 21 | commandOutput = o.Output 22 | } 23 | var commandOptions provider.CommandOptions = provider.CommandOptions{ 24 | AwsRegion: AwsRegion, 25 | ForceMerge: o.ForceMerge, 26 | UiSize: o.UiSize, 27 | Path: commandPath, 28 | Output: commandOutput, 29 | Overwrite: o.Overwrite, 30 | Combined: core.IfXinY(args[0], supportedProvider), 31 | Merge: o.Merge, 32 | Backup: o.Backup, 33 | DryRun: o.DryRun, 34 | AwsAuth: o.AwsAuth, 35 | AwsRoleString: o.AwsRoleString, 36 | AwsEnvProfile: o.AwsEnvProfile, 37 | AwsClusterName: o.AwsClusterName, 38 | ProfileSelector: o.ProfileSelector, 39 | SaveOutput: o.SaveOutput, 40 | Validate: o.Validate, 41 | } 42 | 43 | return commandOptions 44 | } 45 | 46 | func argValidator(cmd *cobra.Command, args []string) error { 47 | var err error 48 | err = checkArgsCount(args) 49 | core.FailOnError(err, validationError) 50 | err = providerValidator(args, cmd) 51 | core.FailOnError(err, validationError) 52 | err = uiSelectValidator(args) 53 | core.FailOnError(err, validationError) 54 | return err 55 | } 56 | 57 | // check amounts of args in the command 58 | func checkArgsCount(args []string) error { 59 | if len(args) < 1 { 60 | return errors.New(providerError) 61 | } 62 | return nil 63 | } 64 | 65 | // check the args for supported Provider 66 | func providerValidator(args []string, cmd *cobra.Command) error { 67 | argouments = append(argouments, supportedProvider...) 68 | switch cmd.Use { 69 | case "find": 70 | if !core.IfXinY(args[0], argouments) { 71 | return fmt.Errorf(providerListError, args[0]) 72 | } 73 | default: 74 | if len(args) > 0 && len(args) <= len(argouments) { 75 | for a := range args { 76 | if !core.IfXinY(args[a], argouments) { 77 | return fmt.Errorf(providerListError, args[a]) 78 | } 79 | } 80 | } 81 | } 82 | return nil 83 | } 84 | 85 | // check if aws UI select was set 86 | func uiSelectValidator(args []string) error { 87 | if o.ProfileSelector && args[0] != "aws" { 88 | return fmt.Errorf("profile selector supports only AWS and is not supporting - %s", args[0]) 89 | } 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /cmd/dd.go: -------------------------------------------------------------------------------- 1 | // /* 2 | // Copyright © 2022 NAME HERE 3 | 4 | // */ 5 | package cmd 6 | 7 | // import ( 8 | // "fmt" 9 | // "k8f/tools" 10 | 11 | // "github.com/spf13/cobra" 12 | // ) 13 | 14 | // // ddCmd represents the dd command 15 | // var apikey string 16 | // var appkey string 17 | // var ddCmd = &cobra.Command{ 18 | // Use: "dd", 19 | // Short: "Send Metrics to Data Dog", 20 | // // Args: func(cmd *cobra.Command, args []string) error { 21 | // // if len(args) < 1 { 22 | // // return errors.New("requires cloud provider") 23 | // // } 24 | // // argouments = append(argouments, supportedProvider...) 25 | 26 | // // if core.IfXinY(args[0], argouments) { 27 | // // return nil 28 | // // } 29 | // // return fmt.Errorf("invalid cloud provider specified: %s", args[0]) 30 | // // }, 31 | // Run: func(cmd *cobra.Command, args []string) { 32 | // fmt.Println("apikey: " + apikey) 33 | // tools.DdMain(apikey) 34 | // fmt.Println("dd called") 35 | // }, 36 | // } 37 | 38 | // func init() { 39 | // ddCmd.Flags().StringVar(&apikey, "apikey", "", "Set API Key for Datadog") 40 | // rootCmd.AddCommand(ddCmd) 41 | // } 42 | -------------------------------------------------------------------------------- /cmd/find.go: -------------------------------------------------------------------------------- 1 | // /* 2 | // Copyright © 2022 NAME HERE 3 | 4 | // */ 5 | package cmd 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "k8f/core" 11 | "k8f/provider" 12 | 13 | log "github.com/sirupsen/logrus" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | // findCmd represents the find command 18 | var findCmd = &cobra.Command{ 19 | Use: findCMD, 20 | Short: findShort, 21 | Example: findExample, 22 | Run: func(cmd *cobra.Command, args []string) { 23 | var p provider.Cluster 24 | var options provider.CommandOptions = newCommandStruct(cmd, o, args) 25 | log.WithField("CommandOptions", log.Fields{"struct": core.DebugWithInfo(options)}).Debug("CommandOptions Struct Keys and Values: ") 26 | log.Info("find called") 27 | if args[0] == "azure" { 28 | p = options.GetSingleAzureCluster(args[1]) 29 | } else if args[0] == "aws" { 30 | p = options.GetSingleAWSCluster(args[1]) 31 | // TODO: add find single cluster to all (in case we know name but not platform) 32 | } else if args[0] == "all" { 33 | log.Info("Supported Platform are:" + core.PrintOutStirng(supportedProvider)) 34 | // p = options.FullCloudConfig() 35 | } else { 36 | core.FailOnError(errors.New("no Provider Selected"), "Selected Provider Not avilable (yet)") 37 | } 38 | log.Debug(string("Outputing List as " + options.Output + " Format")) 39 | var output, err = options.PrintoutResults(p) 40 | core.FailOnError(err, "failed to print results") 41 | fmt.Println(output) 42 | }, 43 | } 44 | 45 | func init() { 46 | rootCmd.AddCommand(findCmd) 47 | } 48 | -------------------------------------------------------------------------------- /cmd/list.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 NAME HERE 3 | */ 4 | package cmd 5 | 6 | import ( 7 | "fmt" 8 | "k8f/core" 9 | "k8f/provider" 10 | 11 | log "github.com/sirupsen/logrus" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // listCmd represents the list command 16 | 17 | var listCmd = &cobra.Command{ 18 | Use: listCMD, 19 | Short: listShort, 20 | Example: listExample, 21 | Run: func(cmd *cobra.Command, args []string) { 22 | var list []provider.Provider 23 | var p interface{} 24 | var options provider.CommandOptions = newCommandStruct(cmd, o, args) 25 | log.WithField("CommandOptions", log.Fields{"struct": core.DebugWithInfo(options)}).Debug("CommandOptions Struct Keys and Values: ") 26 | log.Debug("CommandOptions Used") 27 | if len(args) == 1 && args[0] == "azure" { 28 | log.Debug("Starting Azure List") 29 | p = options.FullAzureList() 30 | } else if len(args) == 1 && args[0] == "aws" { 31 | log.Debug("Starting AWS List") 32 | p = options.FullAwsList() 33 | } else if len(args) == 1 && args[0] == "gcp" { 34 | log.Debug("Starting GCP List") 35 | p = options.GcpMain() 36 | } else if len(args) == 1 && args[0] == "all" { 37 | log.Debug("Starting All List") 38 | log.Info("Supported Platform are:" + core.PrintOutStirng(supportedProvider)) 39 | 40 | var c0 = make(chan provider.Provider) 41 | for _, s := range supportedProvider { 42 | log.Debug(string("Starting " + s + " Provider")) 43 | go runAll(c0, options, s) 44 | } 45 | for i := 0; i < len(supportedProvider); i++ { 46 | var res provider.Provider = <-c0 47 | log.Trace(string("Recived A response from: " + supportedProvider[i])) 48 | list = append(list, res) 49 | } 50 | p = list 51 | } else { 52 | log.Debug("Starting All List") 53 | log.Info("Supported Platform are:" + core.PrintOutStirng(supportedProvider)) 54 | var c0 = make(chan provider.Provider) 55 | for _, s := range args { 56 | log.Debug(string("Starting " + s + " Provider")) 57 | go runAll(c0, options, s) 58 | } 59 | for i := 0; i < len(args); i++ { 60 | var res provider.Provider = <-c0 61 | log.Trace(string("Recived A response from: " + args[i])) 62 | list = append(list, res) 63 | } 64 | p = list 65 | } 66 | log.Debug(string("Outputing List as " + options.Output + " Format")) 67 | if options.SaveOutput { 68 | options.StructOutput(p) 69 | } else { 70 | var output, err = options.PrintoutResults(p) 71 | core.FailOnError(err, "failed to get printout result") 72 | fmt.Println(output) 73 | } 74 | }, 75 | } 76 | 77 | func init() { 78 | listCmd.Flags().BoolVar(&o.ProfileSelector, "profile-select", false, "Get UI to select single profile to connect") 79 | listCmd.Flags().BoolVarP(&o.SaveOutput, "save", "s", false, "Get UI to select single profile to connect") 80 | listCmd.Flags().StringVarP(&o.ListOutput, "output", "o", defaultJSONoutput, "Set output type(json or yaml)") 81 | listCmd.Flags().StringVarP(&o.ListPath, "path", "p", listPath, "Set output path") 82 | rootCmd.AddCommand(listCmd) 83 | 84 | } 85 | 86 | func runAll(c0 chan provider.Provider, options provider.CommandOptions, s string) { 87 | var r provider.Provider 88 | if s == "azure" { 89 | log.Trace(string(triggered + s)) 90 | r = options.FullAzureList() 91 | } else if s == "aws" { 92 | log.Trace(string(triggered + s)) 93 | r = options.FullAwsList() 94 | } else if s == "gcp" { 95 | log.Trace(string(triggered + s)) 96 | r = options.GcpMain() 97 | } 98 | c0 <- r 99 | } 100 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 NAME HERE 3 | */ 4 | package cmd 5 | 6 | import ( 7 | "k8f/core" 8 | 9 | "github.com/spf13/cobra" 10 | "k8s.io/client-go/tools/clientcmd" 11 | ) 12 | 13 | var tversion string 14 | 15 | // rootCmd represents the base command when called without any subcommands 16 | var ( 17 | o FlagsOptions 18 | supportedProvider = []string{"azure", "aws", "gcp"} 19 | argouments = []string{"all"} 20 | AwsRegion = "eu-west-1" 21 | defaultYAMLoutput = "yaml" 22 | defaultJSONoutput = "json" 23 | version = tversion 24 | confPath = clientcmd.RecommendedHomeFile 25 | listPath = "./output" 26 | rootCmd = &cobra.Command{ 27 | Version: version, 28 | Use: "k8f", 29 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 30 | core.ToggleDebug() 31 | var err error = argValidator(cmd, args) 32 | core.FailOnError(err, "Validation failed") 33 | return err 34 | }, 35 | Short: "A CLI tool to List, Connect, Search and check version for K8S Clusters in all your resources at once", 36 | Long: `A CLI tool to find, list, connect, search and check version for K8S Clusters in all your resources at once, 37 | this tool supports Azure AKS and AWS EKS. For example: 38 | to get List of all EKS: 39 | k8f list aws 40 | to connect to all K8S: 41 | k8f connect all`, 42 | } 43 | ) 44 | 45 | // Execute adds all child commands to the root command and sets flags appropriately. 46 | // This is called by main.main(). It only needs to happen once to the rootCmd. 47 | func Execute() { 48 | var err error = rootCmd.Execute() 49 | core.FailOnError(err, "error executing command") 50 | } 51 | 52 | func init() { 53 | rootCmd.Flags().BoolVar(&o.DryRun, "dry-run", false, "Run the task as Dry-run, no action is done") 54 | rootCmd.PersistentFlags().BoolVarP(&core.Verbosity, "verbose", "v", false, "verbose logging (default false)") 55 | rootCmd.PersistentFlags().BoolVarP(&core.ErrorLevel, "quit", "q", false, "error-level logging, only errors are shown, very useful for scripts and automation (default false)") 56 | rootCmd.PersistentFlags().BoolVar(&o.Validate, "validate", false, "Fail on validation of the AWS credentals before running the command (default false)") 57 | rootCmd.PersistentFlags().StringVar(&AwsRegion, "aws-region", AwsRegion, "Set Default AWS Region") 58 | rootCmd.Flags().IntVar(&o.UiSize, "ui-size", 4, "number of list items to show in menu at once") 59 | rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 60 | rootCmd.MarkFlagsMutuallyExclusive("verbose", "quit") 61 | } 62 | -------------------------------------------------------------------------------- /cmd/struct.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | type FlagsOptions struct { 4 | Path string `json:"path,omitempty"` 5 | ListPath string `json:"list_path,omitempty"` 6 | Output string `json:"output,omitempty"` 7 | ListOutput string `json:"list_output,omitempty"` 8 | Overwrite bool `json:"overwrite,omitempty"` 9 | Backup bool `json:"backup,omitempty"` 10 | Merge bool `json:"merge,omitempty"` 11 | ForceMerge bool `json:"force_merge,omitempty"` 12 | UiSize int `json:"uiSize,omitempty"` 13 | DryRun bool `json:"dry-run,omitempty"` 14 | AwsAuth bool `json:"aws_auth,omitempty"` 15 | AwsAssumeRole bool `json:"aws_assume_role,omitempty"` 16 | AwsRoleString string `json:"aws_role_string,omitempty"` 17 | AwsEnvProfile bool `json:"aws_profile,omitempty"` 18 | AwsClusterName bool `json:"aws_cluster_name,omitempty"` 19 | ProfileSelector bool `json:"profile_selector,omitempty"` 20 | SaveOutput bool `json:"save_output,omitempty"` 21 | Validate bool `json:"validate,omitempty"` 22 | } 23 | -------------------------------------------------------------------------------- /core/core.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | log "github.com/sirupsen/logrus" 12 | "gopkg.in/ini.v1" 13 | ) 14 | 15 | func FailOnError(err error, message string) { 16 | if err != nil { 17 | log.Errorf("%s: %s\n", message, err) 18 | os.Exit(1) 19 | } 20 | } 21 | 22 | // getEnvVarOrExit returns the value of specified environment variable or terminates if it's not defined. 23 | func CheckEnvVarOrSitIt(varName string, varKey string) { 24 | val, present := os.LookupEnv(varName) 25 | if present { 26 | log.Debug("Variable " + varName + " Was Pre-set with Value: " + val) 27 | } else { 28 | err := os.Setenv(varName, varKey) 29 | val = os.Getenv(varName) 30 | log.Debug("Variable " + varName + " is Set with Value: " + val) 31 | FailOnError(err, "Issue setting the 'AWS_REGION' Enviroment Variable") 32 | } 33 | } 34 | 35 | func PrintOutStirng(arrayOfStrings []string) string { 36 | var s string 37 | for _, t := range arrayOfStrings { 38 | s = s + " " + t 39 | } 40 | return s 41 | } 42 | 43 | func IfXinY(x string, y []string) bool { 44 | for _, t := range y { 45 | if x == t { 46 | return true 47 | } 48 | } 49 | return false 50 | } 51 | 52 | func Exists(path string) bool { 53 | log.Trace("Start Checking if Path Exist") 54 | _, err := os.Stat(path) 55 | if err == nil { 56 | log.Debug("Path Exist") 57 | return true 58 | } 59 | if os.IsNotExist(err) { 60 | log.Debug("Path Dose NOT Exist") 61 | return false 62 | } 63 | return false 64 | } 65 | 66 | func CreateDirectory(path string) { 67 | // parts := strings.Split(path, string(os.PathSeparator)) 68 | dir := filepath.Dir(path) 69 | var create string 70 | if Exists(dir) { 71 | log.Trace(dir + " Path Exist") 72 | } else { 73 | log.Debug("Createing Directory: " + filepath.Dir(path)) 74 | if !filepath.IsAbs(dir) { 75 | log.Debug(dir + " Path is Not Absolute") 76 | create = "./" + dir 77 | } else { 78 | create = dir 79 | } 80 | err := os.MkdirAll(create, 0777) 81 | FailOnError(err, "Failed to Create Directory") 82 | } 83 | } 84 | 85 | func MergeINIFiles(inputPaths []string) (*bytes.Reader, error) { 86 | // Create a buffer to store the merged result 87 | outputBuffer := bytes.Buffer{} 88 | // Iterate over input INI files 89 | for _, inputPath := range inputPaths { 90 | if !Exists(inputPath) && strings.Contains(inputPath, "credentials") { 91 | FailOnError(errors.New("https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html"), "AWS File `"+inputPath+"` is miss-configured, please fix it and run again") 92 | } else if !Exists(inputPath) { 93 | log.Warning("\tAWS File `"+inputPath+"` is miss-configured, some of your account might not show.\n\t please fix it and run again: ", "https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html") 94 | continue 95 | } 96 | // Open the input INI file 97 | inputFile, err := ini.InsensitiveLoad(inputPath) 98 | FailOnError(err, "failed to load INI") 99 | for _, section := range inputFile.Sections() { 100 | if !checkIfConfigExist(section.Name(), outputBuffer) && len(section.Keys()) != 0 { 101 | outputBuffer.WriteString(fmt.Sprintf("[%s]\n", section.Name())) 102 | // Iterate over keys in the section 103 | for _, key := range section.Keys() { 104 | outputBuffer.WriteString(fmt.Sprintf("%s = %s\n", key.Name(), key.Value())) 105 | } 106 | outputBuffer.WriteString("\n") 107 | } 108 | } 109 | } 110 | return bytes.NewReader(outputBuffer.Bytes()), nil 111 | } 112 | 113 | func checkIfConfigExist(sectionName string, mergingConfig bytes.Buffer) bool { 114 | if len(mergingConfig.Bytes()) == 0 { 115 | return false 116 | } 117 | wordToRemove := "profile " 118 | currentConf := strings.Replace(sectionName, wordToRemove, "", -1) 119 | inputFile, err := ini.InsensitiveLoad(bytes.NewReader(mergingConfig.Bytes())) 120 | FailOnError(err, "failed to load INI") 121 | for _, section := range inputFile.Sections() { 122 | inConfString := strings.Replace(section.Name(), wordToRemove, "", -1) 123 | if currentConf == inConfString { 124 | return true 125 | } 126 | } 127 | return false 128 | } 129 | -------------------------------------------------------------------------------- /core/core_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCheckEnvVarOrSitIt(t *testing.T) { 12 | testCases := []struct { 13 | name string 14 | key string 15 | value string 16 | expected string 17 | }{ 18 | { 19 | name: "Test_CheckEnvVarOrSitIt_EnvVarSet", 20 | key: "TEST", 21 | value: "updated", 22 | expected: "SET", 23 | }, 24 | { 25 | name: "Test_CheckEnvVarOrSitIt_EnvVarNotSet", 26 | key: "TEST", 27 | value: "updated", 28 | expected: "updated", 29 | }, 30 | { 31 | name: "Test_CheckEnvVarOrSitIt_EnvVarSetAndNoChangeNeeded", 32 | key: "TEST", 33 | value: "SET", 34 | expected: "SET", 35 | }, 36 | } 37 | for _, tc := range testCases { 38 | t.Run(tc.name, func(t *testing.T) { 39 | var origVal = "SET" 40 | if tc.expected == origVal { 41 | t.Setenv(tc.key, origVal) 42 | } 43 | CheckEnvVarOrSitIt(tc.key, tc.value) 44 | val, present := os.LookupEnv(tc.key) 45 | if !present || val != tc.expected { 46 | t.Fatalf(`Var(%s) = %s,should have been %s`, tc.key, val, tc.expected) 47 | } 48 | }) 49 | } 50 | } 51 | 52 | func TestPrintOutStirng(t *testing.T) { 53 | testCases := []struct { 54 | name string 55 | input []string 56 | expected string 57 | }{ 58 | { 59 | name: "EmptyArray", 60 | input: []string{}, 61 | expected: "", 62 | }, 63 | { 64 | name: "OneItemArray", 65 | input: []string{"Test"}, 66 | expected: " Test", 67 | }, 68 | { 69 | name: "TwoItemArray", 70 | input: []string{"Test", "Test"}, 71 | expected: " Test Test", 72 | }, 73 | { 74 | name: "ThreeItemArray", 75 | input: []string{"Test", "Test", "Test"}, 76 | expected: " Test Test Test", 77 | }, 78 | { 79 | name: "FourItemArray", 80 | input: []string{"Test", "Test", "Test", "Test"}, 81 | expected: " Test Test Test Test", 82 | }, 83 | { 84 | name: "FiveItemArray", 85 | input: []string{"Test", "Test", "Test", "Test", "Test"}, 86 | expected: " Test Test Test Test Test", 87 | }, 88 | { 89 | name: "SixItemArray", 90 | input: []string{"Test", "Test", "Test", "Test", "Test", "Test"}, 91 | expected: " Test Test Test Test Test Test", 92 | }, 93 | { 94 | name: "SevenItemArray", 95 | input: []string{"Test", "Test", "Test", "Test", "Test", "Test", "Test"}, 96 | expected: " Test Test Test Test Test Test Test", 97 | }, 98 | { 99 | name: "EightItemArray", 100 | input: []string{"Test", "Test", "Test", "Test", "Test", "Test", "Test", "Test"}, 101 | expected: " Test Test Test Test Test Test Test Test", 102 | }, 103 | { 104 | name: "NineItemArray", 105 | input: []string{"Test", "Test", "Test", "Test", "Test", "Test", "Test", "Test", "Test"}, 106 | expected: " Test Test Test Test Test Test Test Test Test", 107 | }, 108 | { 109 | name: "TenItemArray", 110 | input: []string{"Test", "Test", "Test", "Test", "Test", "Test", "Test", "Test", "Test", "Test"}, 111 | expected: " Test Test Test Test Test Test Test Test Test Test", 112 | }, 113 | } 114 | for _, tc := range testCases { 115 | t.Run(tc.name, func(t *testing.T) { 116 | result := PrintOutStirng(tc.input) 117 | if result != tc.expected { 118 | t.Fatalf(`PrintOutStirng(%s) = %s,should have been %s`, tc.input, result, tc.expected) 119 | } 120 | }) 121 | } 122 | } 123 | 124 | func TestIfXinY(t *testing.T) { 125 | testCases := []struct { 126 | name string 127 | x string 128 | y []string 129 | expected bool 130 | }{ 131 | { 132 | name: "StringExistsInSlice", 133 | x: "apple", 134 | y: []string{"apple", "banana", "orange"}, 135 | expected: true, 136 | }, 137 | { 138 | name: "StringDoesNotExistInSlice", 139 | x: "grape", 140 | y: []string{"apple", "banana", "orange"}, 141 | expected: false, 142 | }, 143 | { 144 | name: "EmptySlice", 145 | x: "apple", 146 | y: []string{}, 147 | expected: false, 148 | }, 149 | { 150 | name: "EmptyString", 151 | x: "", 152 | y: []string{"apple", "banana", "orange"}, 153 | expected: false, 154 | }, 155 | { 156 | name: "EmptyStringAndEmptySlice", 157 | x: "", 158 | y: []string{}, 159 | expected: false, 160 | }, 161 | } 162 | for _, tc := range testCases { 163 | t.Run(tc.name, func(t *testing.T) { 164 | var result bool = IfXinY(tc.x, tc.y) 165 | if result != tc.expected { 166 | t.Errorf("Expected %q in %q to be %v, but got %v", tc.x, tc.y, tc.expected, result) 167 | } 168 | }) 169 | } 170 | } 171 | 172 | func TestExists(t *testing.T) { 173 | testCases := []struct { 174 | name string 175 | path string 176 | expected bool 177 | }{ 178 | { 179 | name: "PathExists", 180 | path: "existing_file.txt", 181 | expected: true, 182 | }, 183 | { 184 | name: "PathDoesNotExist", 185 | path: "nonexistent_file.txt", 186 | expected: false, 187 | }, 188 | { 189 | name: "PermissionDenied", 190 | path: "/root/somefile.txt", 191 | expected: false, 192 | }, 193 | } 194 | for _, tc := range testCases { 195 | t.Run(tc.name, func(t *testing.T) { 196 | //create a temporary file 197 | if tc.expected { 198 | file, err := os.Create(tc.path) 199 | if err != nil { 200 | t.Fatal(err) 201 | } 202 | defer func() { 203 | file.Close() 204 | os.Remove(tc.path) 205 | }() 206 | } else { 207 | os.Remove(tc.path) 208 | } 209 | var exists bool = Exists(tc.path) 210 | if exists != tc.expected { 211 | t.Errorf("Exists(%q) = %v; expected %v", tc.path, exists, tc.expected) 212 | } 213 | }) 214 | } 215 | } 216 | 217 | func TestCreateDirectory(t *testing.T) { 218 | const conf string = "/config" 219 | testCases := []struct { 220 | name string 221 | MainPath string 222 | SecondaryPath string 223 | expected bool 224 | }{ 225 | { 226 | name: "DirectoryDoesNotExist", 227 | MainPath: "new_directory", 228 | SecondaryPath: conf, 229 | expected: true, 230 | }, 231 | { 232 | name: "DirectoryExists", 233 | MainPath: "existing_directory", 234 | SecondaryPath: conf, 235 | expected: true, 236 | }, 237 | { 238 | name: "PermissionDenied", 239 | MainPath: "/root", 240 | SecondaryPath: "/somefile.txt", 241 | expected: false, 242 | }, 243 | } 244 | for _, tc := range testCases { 245 | t.Run(tc.name, func(t *testing.T) { 246 | //create a temporary file 247 | if tc.expected { 248 | err := os.MkdirAll(tc.MainPath+tc.SecondaryPath, 0777) 249 | if err != nil { 250 | t.Fatal(err) 251 | } 252 | } else { 253 | os.RemoveAll(tc.MainPath + tc.SecondaryPath) 254 | os.RemoveAll(tc.MainPath) 255 | } 256 | 257 | var exists bool = Exists(tc.MainPath + tc.SecondaryPath) 258 | if exists != tc.expected { 259 | t.Errorf("Exists(%q) = %v; expected %v", tc.MainPath+tc.SecondaryPath, exists, tc.expected) 260 | } else if tc.expected { 261 | os.RemoveAll(tc.MainPath + tc.SecondaryPath) 262 | os.RemoveAll(tc.MainPath) 263 | } 264 | }) 265 | } 266 | } 267 | 268 | func TestMergeINIFiles(t *testing.T) { 269 | testCases := []struct { 270 | name string 271 | inputPaths []string 272 | expectedData string 273 | }{ 274 | { 275 | name: "Creds first and then configuration", 276 | inputPaths: []string{"../test/credentials", "../test/config"}, 277 | expectedData: "[default]\naws_access_key_id = myKey\naws_secret_access_key = myAccessKey\n\n[account1]\naws_access_key_id = account1Key\naws_secret_access_key = account1AccessKey\n\n[account2]\naws_access_key_id = account2Key\naws_secret_access_key = account2AccessKey\n\n", 278 | }, 279 | { 280 | name: "Configuration and then Creds", 281 | inputPaths: []string{"../test/config", "../test/credentials"}, 282 | expectedData: "[default]\nregion = eu-west-1\n\n[profile account1]\nrole_arn = arn:aws:iam::123456789012:role/my-role\nsource_profile = default\n\n[profile account2]\nrole_arn = arn:aws:iam::125456389012:role/my-role\nsource_profile = default\n\n", 283 | }, 284 | } 285 | 286 | for _, tc := range testCases { 287 | t.Run(tc.name, func(t *testing.T) { 288 | result, err := MergeINIFiles(tc.inputPaths) 289 | if err != nil { 290 | t.Fatalf("Error occurred: %v", err) 291 | } 292 | var actualDataBuf bytes.Buffer 293 | if _, err := actualDataBuf.ReadFrom(result); err != nil { 294 | t.Fatalf("Error reading merged data: %v", err) 295 | } 296 | actualData := actualDataBuf.String() 297 | if actualData != tc.expectedData { 298 | t.Errorf("Test case '%s' failed.\nExpected:\n%s\nGot:\n%s", tc.name, tc.expectedData, actualData) 299 | } 300 | }) 301 | } 302 | } 303 | 304 | func TestCheckIfConfigExist(t *testing.T) { 305 | testCases := []struct { 306 | name string 307 | inputString string 308 | newKey string 309 | expectedData bool 310 | }{ 311 | { 312 | name: "section dose not exist", 313 | inputString: `[Section1] 314 | key1 = value1 315 | [Section2] 316 | key2 = value2 317 | `, 318 | newKey: "newAAccount3", 319 | expectedData: false, 320 | }, 321 | { 322 | name: "section exist", 323 | inputString: `[Section1] 324 | key1 = value1 325 | [Section2] 326 | key2 = value2 327 | `, 328 | newKey: "section1", 329 | expectedData: true, 330 | }, 331 | { 332 | name: "No file exist", 333 | inputString: ``, 334 | newKey: "section1", 335 | expectedData: false, 336 | }, 337 | } 338 | 339 | for _, tc := range testCases { 340 | t.Run(tc.name, func(t *testing.T) { 341 | existingIni := bytes.NewBufferString(tc.inputString) 342 | testIfExist := checkIfConfigExist(tc.newKey, *existingIni) 343 | assert.True(t, testIfExist == tc.expectedData, "Unexpected result for "+tc.name) 344 | }) 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /core/loggin.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/fatih/structs" 8 | "github.com/shiena/ansicolor" 9 | log "github.com/sirupsen/logrus" 10 | prefixed "github.com/x-cray/logrus-prefixed-formatter" 11 | ) 12 | 13 | var Verbosity bool 14 | var ErrorLevel bool 15 | 16 | func (f *PlainFormatter) Format(entry *log.Entry) ([]byte, error) { 17 | return []byte(fmt.Sprintf("%s\n", entry.Message)), nil 18 | } 19 | 20 | func ToggleDebug() { 21 | formatter := new(prefixed.TextFormatter) 22 | // Set specific colors for prefix and timestamp 23 | formatter.SetColorScheme(&prefixed.ColorScheme{ 24 | InfoLevelStyle: "green", 25 | WarnLevelStyle: "yellow", 26 | ErrorLevelStyle: "red", 27 | FatalLevelStyle: "red", 28 | PanicLevelStyle: "red", 29 | DebugLevelStyle: "blue", 30 | PrefixStyle: "cyan", 31 | TimestampStyle: "black+h", 32 | }) 33 | formatter.DisableTimestamp = true 34 | formatter.DisableUppercase = true 35 | formatter.ForceColors = true 36 | formatter.ForceFormatting = true 37 | log.SetFormatter(formatter) 38 | log.SetOutput(ansicolor.NewAnsiColorWriter(os.Stdout)) 39 | if Verbosity { 40 | log.Info("Debug logs enabled") 41 | log.SetLevel(log.TraceLevel) 42 | } else if ErrorLevel { 43 | log.SetLevel(log.ErrorLevel) 44 | } else { 45 | log.SetLevel(log.InfoLevel) 46 | } 47 | } 48 | 49 | func DebugWithInfo(input interface{}) map[string]interface{} { 50 | return structs.Map(input) 51 | } 52 | -------------------------------------------------------------------------------- /core/struct.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | type PlainFormatter struct { 4 | } 5 | -------------------------------------------------------------------------------- /core/utils.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/manifoldco/promptui" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // PromptUI output prompt ui 11 | func PromptUI(label string, name string) string { 12 | validate := func(input string) error { 13 | if len(input) < 1 { 14 | return errors.New("context name must have more than 1 characters") 15 | } 16 | return nil 17 | } 18 | prompt := promptui.Prompt{ 19 | Label: label, 20 | Validate: validate, 21 | Default: name, 22 | } 23 | result, err := prompt.Run() 24 | 25 | if err != nil { 26 | log.Fatalf("Prompt failed %v\n", err) 27 | } 28 | return result 29 | } 30 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module k8f 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 9 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice v1.0.0 10 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.2.0 11 | github.com/aws/aws-sdk-go-v2 v1.18.1 12 | github.com/aws/aws-sdk-go-v2/credentials v1.13.26 13 | github.com/aws/aws-sdk-go-v2/service/ec2 v1.100.1 14 | github.com/aws/aws-sdk-go-v2/service/eks v1.27.14 15 | github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 16 | github.com/fatih/structs v1.1.0 17 | github.com/hashicorp/go-version v1.6.0 18 | github.com/imdario/mergo v0.3.16 19 | github.com/manifoldco/promptui v0.9.0 20 | github.com/stretchr/testify v1.9.0 21 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 22 | gopkg.in/yaml.v2 v2.4.0 23 | ) 24 | 25 | require ( 26 | cloud.google.com/go/compute v1.20.0 // indirect 27 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 28 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 // indirect 29 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 // indirect 30 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect 31 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 // indirect 32 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 // indirect 33 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 // indirect 34 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 // indirect 35 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 // indirect 36 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 // indirect 37 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 // indirect 38 | github.com/aws/smithy-go v1.13.5 // indirect 39 | github.com/chzyer/readline v1.5.1 // indirect 40 | github.com/davecgh/go-spew v1.1.1 // indirect 41 | github.com/go-logr/logr v1.2.4 // indirect 42 | github.com/gogo/protobuf v1.3.2 // indirect 43 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 44 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 45 | github.com/golang/protobuf v1.5.3 // indirect 46 | github.com/google/gofuzz v1.2.0 // indirect 47 | github.com/google/s2a-go v0.1.4 // indirect 48 | github.com/google/uuid v1.6.0 // indirect 49 | github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect 50 | github.com/googleapis/gax-go/v2 v2.11.0 // indirect 51 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 52 | github.com/json-iterator/go v1.1.12 // indirect 53 | github.com/kylelemons/godebug v1.1.0 // indirect 54 | github.com/mattn/go-colorable v0.1.13 // indirect 55 | github.com/mattn/go-isatty v0.0.19 // indirect 56 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 57 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 58 | github.com/modern-go/reflect2 v1.0.2 // indirect 59 | github.com/onsi/ginkgo v1.16.5 // indirect 60 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 61 | github.com/pmezard/go-difflib v1.0.0 // indirect 62 | github.com/spf13/pflag v1.0.5 // indirect 63 | go.opencensus.io v0.24.0 // indirect 64 | golang.org/x/crypto v0.35.0 // indirect 65 | golang.org/x/net v0.36.0 // indirect 66 | golang.org/x/oauth2 v0.9.0 // indirect 67 | golang.org/x/sys v0.30.0 // indirect 68 | golang.org/x/term v0.29.0 // indirect 69 | golang.org/x/text v0.22.0 // indirect 70 | golang.org/x/time v0.3.0 // indirect 71 | google.golang.org/appengine v1.6.7 // indirect 72 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect 73 | google.golang.org/grpc v1.56.3 // indirect 74 | google.golang.org/protobuf v1.33.0 // indirect 75 | gopkg.in/inf.v0 v0.9.1 // indirect 76 | gopkg.in/yaml.v3 v3.0.1 // indirect 77 | k8s.io/apimachinery v0.27.3 // indirect 78 | k8s.io/klog/v2 v2.100.1 // indirect 79 | k8s.io/utils v0.0.0-20230505201702-9f6742963106 // indirect 80 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 81 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 82 | sigs.k8s.io/yaml v1.3.0 // indirect 83 | ) 84 | 85 | require ( 86 | github.com/aws/aws-sdk-go-v2/config v1.18.27 87 | github.com/jmespath/go-jmespath v0.4.0 // indirect 88 | github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02 89 | github.com/sirupsen/logrus v1.9.3 90 | github.com/spf13/cobra v1.7.0 91 | google.golang.org/api v0.128.0 92 | gopkg.in/ini.v1 v1.67.0 93 | k8s.io/client-go v0.27.3 94 | ) 95 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go/compute v1.20.0 h1:cUOcywWuowO9It2i1KX1lIb0HH7gLv6nENKuZGnlcSo= 4 | cloud.google.com/go/compute v1.20.0/go.mod h1:kn5BhC++qUWR/AM3Dn21myV7QbgqejW04cAOrtppaQI= 5 | cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= 6 | cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= 7 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= 8 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= 9 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg= 10 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= 11 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo= 12 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg= 13 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice v1.0.0 h1:figxyQZXzZQIcP3njhC68bYUiTw45J8/SsHaLW8Ax0M= 14 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice v1.0.0/go.mod h1:TmlMW4W5OvXOmOyKNnor8nlMMiO1ctIyzmHme/VHsrA= 15 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2 h1:mLY+pNLjCUeKhgnAJWAKhEUQM+RJQo2H1fuGSw1Ky1E= 16 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2/go.mod h1:FbdwsQ2EzwvXxOPcMFYO8ogEc9uMMIj3YkmCdXdAFmk= 17 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 h1:ECsQtyERDVz3NP3kvDOTLvbQhqWp/x9EsGKtb4ogUr8= 18 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0/go.mod h1:s1tW/At+xHqjNFvWU4G0c0Qv33KOhvbGNj0RCTQDV8s= 19 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.2.0 h1:Pmy0+3ox1IC3sp6musv87BFPIdQbqyPFjn7I8I0o2Js= 20 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.2.0/go.mod h1:ThfyMjs6auYrWPnYJjI3H4H++oVPrz01pizpu8lfl3A= 21 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= 22 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 23 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 24 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 25 | github.com/aws/aws-sdk-go-v2 v1.18.1 h1:+tefE750oAb7ZQGzla6bLkOwfcQCEtC5y2RqoqCeqKo= 26 | github.com/aws/aws-sdk-go-v2 v1.18.1/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= 27 | github.com/aws/aws-sdk-go-v2/config v1.18.27 h1:Az9uLwmssTE6OGTpsFqOnaGpLnKDqNYOJzWuC6UAYzA= 28 | github.com/aws/aws-sdk-go-v2/config v1.18.27/go.mod h1:0My+YgmkGxeqjXZb5BYme5pc4drjTnM+x1GJ3zv42Nw= 29 | github.com/aws/aws-sdk-go-v2/credentials v1.13.26 h1:qmU+yhKmOCyujmuPY7tf5MxR/RKyZrOPO3V4DobiTUk= 30 | github.com/aws/aws-sdk-go-v2/credentials v1.13.26/go.mod h1:GoXt2YC8jHUBbA4jr+W3JiemnIbkXOfxSXcisUsZ3os= 31 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 h1:LxK/bitrAr4lnh9LnIS6i7zWbCOdMsfzKFBI6LUCS0I= 32 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4/go.mod h1:E1hLXN/BL2e6YizK1zFlYd8vsfi2GTjbjBazinMmeaM= 33 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 h1:A5UqQEmPaCFpedKouS4v+dHCTUo2sKqhoKO9U5kxyWo= 34 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34/go.mod h1:wZpTEecJe0Btj3IYnDx/VlUzor9wm3fJHyvLpQF0VwY= 35 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 h1:srIVS45eQuewqz6fKKu6ZGXaq6FuFg5NzgQBAM6g8Y4= 36 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28/go.mod h1:7VRpKQQedkfIEXb4k52I7swUnZP0wohVajJMRn3vsUw= 37 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 h1:LWA+3kDM8ly001vJ1X1waCuLJdtTl48gwkPKWy9sosI= 38 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35/go.mod h1:0Eg1YjxE0Bhn56lx+SHJwCzhW+2JGtizsrx+lCqrfm0= 39 | github.com/aws/aws-sdk-go-v2/service/ec2 v1.100.1 h1:AsLoN1zlf+PJ5DRzoegd8k/Zk9f/fBCMKxrZ4sXSE5k= 40 | github.com/aws/aws-sdk-go-v2/service/ec2 v1.100.1/go.mod h1:tIctCeX9IbzsUTKHt53SVEcgyfxV2ElxJeEB+QUbc4M= 41 | github.com/aws/aws-sdk-go-v2/service/eks v1.27.14 h1:47HQVuJXgwvuoc4AT3rVdm77H0qGFbFnsuE4PRT+xX0= 42 | github.com/aws/aws-sdk-go-v2/service/eks v1.27.14/go.mod h1:QxuWcm9rlLkW3aEV8tiDzqZewnNSNUZfnqJvo1Nv9A0= 43 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 h1:bkRyG4a929RCnpVSTvLM2j/T4ls015ZhhYApbmYs15s= 44 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28/go.mod h1:jj7znCIg05jXlaGBlFMGP8+7UN3VtCkRBG2spnmRQkU= 45 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 h1:nneMBM2p79PGWBQovYO/6Xnc2ryRMw3InnDJq1FHkSY= 46 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.12/go.mod h1:HuCOxYsF21eKrerARYO6HapNeh9GBNq7fius2AcwodY= 47 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 h1:2qTR7IFk7/0IN/adSFhYu9Xthr0zVFTgBrmPldILn80= 48 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12/go.mod h1:E4VrHCPzmVB/KFXtqBGKb3c8zpbNBgKe3fisDNLAW5w= 49 | github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 h1:XFJ2Z6sNUUcAz9poj+245DMkrHE4h2j5I9/xD50RHfE= 50 | github.com/aws/aws-sdk-go-v2/service/sts v1.19.2/go.mod h1:dp0yLPsLBOi++WTxzCjA/oZqi6NPIhoR+uF7GeMU9eg= 51 | github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= 52 | github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= 53 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 54 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 55 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 56 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 57 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 58 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 59 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 60 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 61 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 62 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 63 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 64 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 65 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 66 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 67 | github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= 68 | github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 69 | github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 70 | github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 71 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 72 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 73 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 74 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 75 | github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= 76 | github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 77 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 78 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 79 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 80 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 81 | github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= 82 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 83 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 84 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 85 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 86 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 87 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 88 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 89 | github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 90 | github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= 91 | github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 92 | github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= 93 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 94 | github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8= 95 | github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 96 | github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= 97 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 98 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 99 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 100 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 101 | github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 102 | github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 103 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 104 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 105 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 106 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 107 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 108 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 109 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 110 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 111 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 112 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 113 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 114 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 115 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 116 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 117 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 118 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 119 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 120 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 121 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 122 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 123 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 124 | github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= 125 | github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= 126 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 127 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 128 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 129 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 130 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 131 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 132 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 133 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 134 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 135 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 136 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 137 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 138 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 139 | github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= 140 | github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= 141 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 142 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 143 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 144 | github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvkiqTYKBCKLNmlge2eVjoZfySzM= 145 | github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w= 146 | github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4= 147 | github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= 148 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 149 | github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= 150 | github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 151 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 152 | github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= 153 | github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= 154 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 155 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 156 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 157 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 158 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 159 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 160 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 161 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 162 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 163 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 164 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 165 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 166 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 167 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 168 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 169 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 170 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 171 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 172 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 173 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 174 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 175 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 176 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 177 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 178 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 179 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 180 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 181 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= 182 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 183 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 184 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 185 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 186 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 187 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 188 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 189 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 190 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 191 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 192 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 193 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 194 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 195 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 196 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 197 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 198 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 199 | github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E= 200 | github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ= 201 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= 202 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= 203 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 204 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 205 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 206 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 207 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 208 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 209 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 210 | github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02 h1:v9ezJDHA1XGxViAUSIoO/Id7Fl63u6d0YmsAm+/p2hs= 211 | github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02/go.mod h1:RF16/A3L0xSa0oSERcnhd8Pu3IXSDZSK2gmGIMsttFE= 212 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 213 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 214 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 215 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 216 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 217 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 218 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 219 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 220 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 221 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 222 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 223 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 224 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 225 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 226 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 227 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 228 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 229 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= 230 | github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= 231 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 232 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 233 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 234 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 235 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 236 | go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= 237 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 238 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 239 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 240 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 241 | golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 242 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 243 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 244 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 245 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 246 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 247 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 248 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 249 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 250 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 251 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 252 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 253 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 254 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 255 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 256 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 257 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 258 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 259 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 260 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 261 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 262 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 263 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 264 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 265 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 266 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 267 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 268 | golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= 269 | golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= 270 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 271 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 272 | golang.org/x/oauth2 v0.9.0 h1:BPpt2kU7oMRq3kCHAA1tbSEshXRw1LpG2ztgDwrzuAs= 273 | golang.org/x/oauth2 v0.9.0/go.mod h1:qYgFZaFiu6Wg24azG8bdV52QJXJGbZzIIsRCdVKzbLw= 274 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 275 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 276 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 277 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 278 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 279 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 280 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 281 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 282 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 283 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 284 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 285 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 286 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 287 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 288 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 289 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 290 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 291 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 292 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 293 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 294 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 295 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 296 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 297 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 298 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 299 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 300 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 301 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 302 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 303 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 304 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 305 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 306 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 307 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 308 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 309 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 310 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 311 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 312 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 313 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 314 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 315 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 316 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 317 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 318 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 319 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 320 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 321 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 322 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 323 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 324 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 325 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 326 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 327 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 328 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 329 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 330 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 331 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 332 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 333 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 334 | google.golang.org/api v0.128.0 h1:RjPESny5CnQRn9V6siglged+DZCgfu9l6mO9dkX9VOg= 335 | google.golang.org/api v0.128.0/go.mod h1:Y611qgqaE92On/7g65MQgxYul3c0rEB894kniWLY750= 336 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 337 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 338 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 339 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 340 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 341 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 342 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 343 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 344 | google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao= 345 | google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= 346 | google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM= 347 | google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= 348 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc= 349 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= 350 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 351 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 352 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 353 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 354 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 355 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 356 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 357 | google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= 358 | google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= 359 | google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= 360 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 361 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 362 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 363 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 364 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 365 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 366 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 367 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 368 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 369 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 370 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 371 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 372 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 373 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 374 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 375 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 376 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 377 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 378 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 379 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 380 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 381 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 382 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 383 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 384 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 385 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 386 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 387 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 388 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 389 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 390 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 391 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 392 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 393 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 394 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 395 | k8s.io/api v0.27.3 h1:yR6oQXXnUEBWEWcvPWS0jQL575KoAboQPfJAuKNrw5Y= 396 | k8s.io/api v0.27.3/go.mod h1:C4BNvZnQOF7JA/0Xed2S+aUyJSfTGkGFxLXz9MnpIpg= 397 | k8s.io/apimachinery v0.27.3 h1:Ubye8oBufD04l9QnNtW05idcOe9Z3GQN8+7PqmuVcUM= 398 | k8s.io/apimachinery v0.27.3/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E= 399 | k8s.io/client-go v0.27.3 h1:7dnEGHZEJld3lYwxvLl7WoehK6lAq7GvgjxpA3nv1E8= 400 | k8s.io/client-go v0.27.3/go.mod h1:2MBEKuTo6V1lbKy3z1euEGnhPfGZLKTS9tiJ2xodM48= 401 | k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= 402 | k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= 403 | k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg= 404 | k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg= 405 | k8s.io/utils v0.0.0-20230505201702-9f6742963106 h1:EObNQ3TW2D+WptiYXlApGNLVy0zm/JIBVY9i+M4wpAU= 406 | k8s.io/utils v0.0.0-20230505201702-9f6742963106/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 407 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 408 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 409 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= 410 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= 411 | sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= 412 | sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= 413 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 NAME HERE 3 | 4 | */ 5 | // TODO: cli commands to be used with DD & Nagios (or other monitoring system) 6 | // TODO: add output flag for: json,csv,toTerminal,pdf 7 | // testing GPG 8 | package main 9 | 10 | import ( 11 | "k8f/cmd" 12 | ) 13 | 14 | func main() { 15 | cmd.Execute() 16 | } 17 | -------------------------------------------------------------------------------- /port.yml: -------------------------------------------------------------------------------- 1 | - identifier: AdamRussak_k8f_AYi6D0uVa-POq_Tc4jQY 2 | title: k8f 3 | team: [] 4 | icon: DefaultBlueprint 5 | blueprint: sonarQubeProject 6 | properties: 7 | organization: null 8 | link: https://sonarqube.mylab.geekgalaxy.com/dashboard?id=AdamRussak_k8f_AYi6D0uVa-POq_Tc4jQY 9 | lastAnalysisDate: '2025-03-13T06:52:01.299Z' 10 | numberOfBugs: 0 11 | numberOfCodeSmells: 0 12 | numberOfVulnerabilities: 0 13 | numberOfHotSpots: 0 14 | numberOfDuplications: 0 15 | coverage: 0 16 | mainBranch: main 17 | tags: [] 18 | revision: string 19 | managed: true 20 | qualityGateStatus: OK 21 | -------------------------------------------------------------------------------- /provider/aws.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "k8f/core" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/aws/aws-sdk-go-v2/aws" 12 | "github.com/aws/aws-sdk-go-v2/config" 13 | "github.com/aws/aws-sdk-go-v2/credentials/stscreds" 14 | "github.com/aws/aws-sdk-go-v2/service/ec2" 15 | "github.com/aws/aws-sdk-go-v2/service/eks" 16 | "github.com/aws/aws-sdk-go-v2/service/eks/types" 17 | "github.com/aws/aws-sdk-go-v2/service/sts" 18 | log "github.com/sirupsen/logrus" 19 | "gopkg.in/ini.v1" 20 | ) 21 | 22 | // to check if the profile is valid or not we need to load the []AwsProfiles and check if the profile is a Assume role or credentials, and do a simple call to aws to validate it works 23 | func validateCredentials(creds []AwsProfiles) (string, error) { 24 | log.Info("Validating AWS Credentials") 25 | for _, profile := range creds { 26 | var svc *sts.Client 27 | var conf aws.Config 28 | var err error 29 | conf, err = config.LoadDefaultConfig(context.TODO(), config.WithSharedConfigProfile(profile.Name)) 30 | if err != nil { 31 | return profile.Name, err 32 | } 33 | if profile.IsRole { 34 | conf.Credentials = stsAssumeRole(profile) 35 | svc = sts.NewFromConfig(conf) 36 | } else { 37 | svc = sts.NewFromConfig(conf) 38 | } 39 | var input *sts.GetCallerIdentityInput = &sts.GetCallerIdentityInput{} 40 | _, err = svc.GetCallerIdentity(context.Background(), input) 41 | if err != nil { 42 | return profile.Name, err 43 | } 44 | } 45 | log.Info("AWS Credentials Validated") 46 | return "", nil 47 | } 48 | 49 | // https://docs.aws.amazon.com/sdk-for-go/api/aws/credentials/stscreds/#:~:text=or%20service%20clients.-,Assume%20Role,-To%20assume%20an 50 | func (c CommandOptions) FullAwsList() Provider { 51 | var f []Account 52 | core.CheckEnvVarOrSitIt("AWS_REGION", c.AwsRegion) 53 | profiles := c.GetLocalAwsProfiles() 54 | if len(profiles) == 0 { 55 | core.FailOnError(errors.New("no profiles to run with"), "process failed with Error") 56 | } 57 | addOnVersion := profiles[0].getVersion() 58 | l := getLatestEKS(getEKSversionsList(addOnVersion)) 59 | log.Trace(profiles) 60 | c0 := make(chan Account) 61 | for _, profile := range profiles { 62 | go func(c0 chan Account, profile AwsProfiles, l string) { 63 | var re []Cluster 64 | log.Info(string("Using AWS profile: " + profile.Name)) 65 | regions := profile.listRegions() 66 | c2 := make(chan []Cluster) 67 | for _, reg := range regions { 68 | go printOutResult(reg, l, profile, addOnVersion, c2) 69 | } 70 | for i := 0; i < len(regions); i++ { 71 | aRegion := <-c2 72 | if len(aRegion) > 0 { 73 | re = append(re, aRegion...) 74 | } 75 | } 76 | c0 <- Account{profile.Name, re, len(re), ""} 77 | }(c0, profile, l) 78 | } 79 | for i := 0; i < len(profiles); i++ { 80 | res := <-c0 81 | if len(res.Clusters) != 0 { 82 | f = append(f, res) 83 | } 84 | } 85 | return Provider{"aws", f, countTotal(f)} 86 | } 87 | 88 | // get Addons Supported EKS versions 89 | func (p AwsProfiles) getVersion() *eks.DescribeAddonVersionsOutput { 90 | var svc *eks.Client 91 | conf, err := config.LoadDefaultConfig(context.TODO(), config.WithSharedConfigProfile(p.ConfProfile)) 92 | core.FailOnError(err, "Failed to get Version") 93 | if p.IsRole { 94 | conf.Credentials = stsAssumeRole(p) 95 | svc = eks.NewFromConfig(conf) 96 | } else { 97 | svc = eks.NewFromConfig(conf) 98 | } 99 | input2 := &eks.DescribeAddonVersionsInput{} 100 | r, err := svc.DescribeAddonVersions(context.TODO(), input2) 101 | core.FailOnError(err, "Failed to get Describe Version with profile: "+p.ConfProfile) 102 | return r 103 | } 104 | 105 | // gets the latest form suppported Addons 106 | func getLatestEKS(addons []string) string { 107 | return evaluateVersion(addons) 108 | } 109 | 110 | // create Version list 111 | func getEKSversionsList(addons *eks.DescribeAddonVersionsOutput) []string { 112 | var supportList []string 113 | for _, a := range addons.Addons { 114 | for _, c := range a.AddonVersions { 115 | for _, v := range c.Compatibilities { 116 | if !core.IfXinY(*v.ClusterVersion, supportList) { 117 | supportList = append(supportList, *v.ClusterVersion) 118 | } 119 | } 120 | } 121 | } 122 | return supportList 123 | } 124 | 125 | // get installed Version on existing Clusters 126 | func (p AwsProfiles) getEksCurrentVersion(cluster string, profile AwsProfiles, reg string, c3 chan []string) { 127 | var svc *eks.Client 128 | var conf aws.Config 129 | var err error 130 | conf, err = config.LoadDefaultConfig(context.TODO(), config.WithSharedConfigProfile(profile.ConfProfile)) 131 | core.FailOnError(err, awsErrorMessage) 132 | if p.IsRole { 133 | conf.Credentials = stsAssumeRole(p) 134 | svc = eks.NewFromConfig(conf, func(o *eks.Options) { 135 | o.Region = reg 136 | }) 137 | } else { 138 | svc = eks.NewFromConfig(conf, func(o *eks.Options) { 139 | o.Region = reg 140 | }) 141 | } 142 | input := &eks.DescribeClusterInput{ 143 | Name: aws.String(cluster), 144 | } 145 | result, err := svc.DescribeCluster(context.TODO(), input) 146 | core.FailOnError(err, "Failed to Get Cluster Info") 147 | c3 <- []string{cluster, *result.Cluster.Version} 148 | } 149 | 150 | // get all Regions avilable 151 | func (p AwsProfiles) listRegions() []string { 152 | core.CheckEnvVarOrSitIt("AWS_REGION", Kregion) 153 | var reg []string 154 | var svc *ec2.Client 155 | var conf aws.Config 156 | var err error 157 | conf, err = config.LoadDefaultConfig(context.TODO(), config.WithSharedConfigProfile(p.Name)) 158 | core.FailOnError(err, awsErrorMessage) 159 | if p.IsRole { 160 | conf.Credentials = stsAssumeRole(p) 161 | svc = ec2.NewFromConfig(conf) 162 | } else { 163 | svc = ec2.NewFromConfig(conf) 164 | } 165 | input := &ec2.DescribeRegionsInput{} 166 | result, err := svc.DescribeRegions(context.TODO(), input) 167 | log.Debugf("Using profile: %s, ARN: %s, IsRole:%t", p.Name, p.Arn, p.IsRole) 168 | core.FailOnError(err, "Failed Get Region info") 169 | for _, r := range result.Regions { 170 | reg = append(reg, *r.RegionName) 171 | } 172 | return reg 173 | } 174 | 175 | func printOutResult(reg string, latest string, profile AwsProfiles, addons *eks.DescribeAddonVersionsOutput, c chan []Cluster) { 176 | var loc []Cluster 177 | var svc *eks.Client 178 | var conf aws.Config 179 | var err error 180 | conf, err = config.LoadDefaultConfig(context.TODO(), config.WithSharedConfigProfile(profile.ConfProfile)) 181 | core.FailOnError(err, awsErrorMessage) 182 | if profile.IsRole { 183 | conf.Credentials = stsAssumeRole(profile) 184 | svc = eks.NewFromConfig(conf, func(o *eks.Options) { 185 | o.Region = reg 186 | }) 187 | } else { 188 | svc = eks.NewFromConfig(conf, func(o *eks.Options) { 189 | o.Region = reg 190 | }) 191 | } 192 | input := &eks.ListClustersInput{} 193 | result, err := svc.ListClusters(context.TODO(), input) 194 | core.FailOnError(err, "Failed to list Clusters") 195 | log.Debug(string("We are In Region: " + reg + " Profile " + profile.Name)) 196 | if len(result.Clusters) > 0 { 197 | c3 := make(chan []string) 198 | for _, element := range result.Clusters { 199 | go profile.getEksCurrentVersion(element, profile, reg, c3) 200 | } 201 | for i := 0; i < len(result.Clusters); i++ { 202 | res := <-c3 203 | loc = append(loc, Cluster{res[0], res[1], latest, reg, "", "", HowManyVersionsBack(getEKSversionsList(addons), res[1])}) 204 | } 205 | } 206 | c <- loc 207 | } 208 | 209 | func (c CommandOptions) GetLocalAwsProfiles() []AwsProfiles { 210 | var arr []AwsProfiles 211 | mergeconf, err := core.MergeINIFiles([]string{config.DefaultSharedConfigFilename(), config.DefaultSharedCredentialsFilename()}) 212 | core.FailOnError(err, "failed to merge INI") 213 | creds, err := ini.Load(mergeconf) 214 | core.FailOnError(err, "Failed to load profile from creds") 215 | for _, p := range creds.Sections() { 216 | if len(p.Keys()) != 0 { 217 | profileName := removeString("profile", p.Name()) 218 | _, isInArray := XinAwsProfiles(profileName, arr) 219 | kbool, karn := checkIfItsAssumeRole(p.Keys()) 220 | if kbool && !isInArray { 221 | arr = append(arr, AwsProfiles{Name: profileName, IsRole: true, Arn: karn, ConfProfile: p.Name()}) 222 | } else { 223 | arr = append(arr, AwsProfiles{Name: profileName, IsRole: false, ConfProfile: p.Name()}) 224 | } 225 | } 226 | } 227 | // add a if statement to check if to fail on validation error or not 228 | fultyProfile, err := validateCredentials(arr) 229 | arr = c.finalizeValidation(arr, fultyProfile, err) 230 | kJson, _ := json.Marshal(arr) 231 | log.Debugf("profile in use: %s", string(kJson)) 232 | return (arr) // Create JSON string response 233 | } 234 | 235 | func (c CommandOptions) finalizeValidation(arr []AwsProfiles, fultyProfile string, err error) []AwsProfiles { 236 | if c.Validate { 237 | core.FailOnError(err, credentialsValidationError+fultyProfile) 238 | } else { 239 | if err != nil { 240 | log.Error(credentialsValidationError + fultyProfile) 241 | for i, v := range arr { 242 | if v.Name == fultyProfile { 243 | arr = append(arr[:i], arr[i+1:]...) 244 | } 245 | } 246 | log.Warn(fultyProfile + " was removed from the list of profiles") 247 | } 248 | } 249 | return arr 250 | } 251 | 252 | // Connect Logic 253 | func (c CommandOptions) ConnectAllEks() AllConfig { 254 | var auth []Users 255 | var contexts []Contexts 256 | var clusters []Clusters 257 | var arnContext string 258 | var distinctArnContexts []string 259 | core.CheckEnvVarOrSitIt("AWS_REGION", c.AwsRegion) 260 | p := c.FullAwsList() 261 | awsProfiles := c.GetLocalAwsProfiles() 262 | for _, a := range p.Accounts { 263 | r := make(chan LocalConfig) 264 | for _, clus := range a.Clusters { 265 | go func(r chan LocalConfig, clus Cluster, a Account, commandOptions CommandOptions, awsProfiles []AwsProfiles) { 266 | var eksSvc *eks.Client 267 | var conf aws.Config 268 | var err error 269 | inProfile, _ := XinAwsProfiles(a.Name, awsProfiles) 270 | log.Infof("The Profile used is %s, and region is %s", awsProfiles[inProfile].Name, clus.Region) 271 | conf, err = config.LoadDefaultConfig(context.TODO(), config.WithSharedConfigProfile(awsProfiles[inProfile].Name)) 272 | core.FailOnError(err, awsErrorMessage) 273 | if awsProfiles[inProfile].IsRole { 274 | conf.Credentials = stsAssumeRole(awsProfiles[inProfile]) 275 | eksSvc = eks.NewFromConfig(conf, func(o *eks.Options) { 276 | o.Region = clus.Region 277 | }) 278 | } else { 279 | eksSvc = eks.NewFromConfig(conf, func(o *eks.Options) { 280 | o.Region = clus.Region 281 | }) 282 | } 283 | input := &eks.DescribeClusterInput{ 284 | Name: aws.String(clus.Name), 285 | } 286 | result, err := eksSvc.DescribeCluster(context.TODO(), input) 287 | log.Debugf("the Profile that is used is: %s", awsProfiles[inProfile].Name) 288 | core.FailOnError(err, "Error calling DescribeCluster") 289 | r <- GenerateKubeConfiguration(result.Cluster, clus.Region, a, commandOptions) 290 | }(r, clus, a, c, awsProfiles) 291 | } 292 | for i := 0; i < len(a.Clusters); i++ { 293 | result := <-r 294 | duplicationFound := false 295 | // below check ensures we do not let duplicate clusters/contexts/users 296 | // get into the final configuration in case if there are different 297 | // aws profiles configured to assume the same role, because any kind of 298 | // a duplicate (cluster, context, user) makes kubectl crash 299 | for _, s := range distinctArnContexts { 300 | if s == result.Context.Cluster { 301 | duplicationFound = true 302 | break 303 | } 304 | } 305 | if duplicationFound { 306 | continue 307 | } 308 | arnContext = result.Context.Cluster 309 | distinctArnContexts = append(distinctArnContexts, result.Context.Cluster) 310 | auth = append(auth, Users{Name: arnContext, User: result.Authinfo}) 311 | contexts = append(contexts, Contexts{Name: arnContext, Context: result.Context}) 312 | clusters = append(clusters, Clusters{Name: arnContext, Cluster: result.Cluster}) 313 | } 314 | } 315 | if c.Combined { 316 | log.Println("Started aws only config creation") 317 | c.CombineConfigs(AllConfig{auth, contexts, clusters}, arnContext) 318 | return AllConfig{} 319 | } 320 | log.Println("Started aws combined config creation") 321 | return AllConfig{auth, contexts, clusters} 322 | } 323 | 324 | // Create AWS Config 325 | func GenerateKubeConfiguration(cluster *types.Cluster, r string, a Account, c CommandOptions) LocalConfig { 326 | clusterName := c.SetClusterName(cluster.Arn) 327 | clusters := CCluster{ 328 | Server: *cluster.Endpoint, 329 | CertificateAuthorityData: *cluster.CertificateAuthority.Data, 330 | } 331 | contexts := Context{ 332 | Cluster: clusterName, 333 | User: clusterName, 334 | } 335 | 336 | authinfos := User{ 337 | Exec: Exec{ 338 | APIVersion: "client.authentication.k8s.io/v1beta1", 339 | Args: c.AwsArgs(r, *cluster.Name, *cluster.Arn), 340 | Env: c.AwsEnvs(a.Name), 341 | Command: c.setCommand(), 342 | }, 343 | } 344 | return LocalConfig{authinfos, contexts, clusters} 345 | } 346 | 347 | func (c CommandOptions) SetClusterName(arn *string) string { 348 | split := strings.Split(*arn, ":") 349 | clusterName := strings.TrimPrefix(split[5], "cluster/") 350 | const breaker = ":" 351 | switch c.AwsClusterName { 352 | case false: 353 | outputName := split[4] + breaker + split[3] + breaker + clusterName 354 | return outputName 355 | case true: 356 | outputName := split[3] + breaker + clusterName 357 | return outputName 358 | default: 359 | return *arn 360 | } 361 | } 362 | 363 | func (c CommandOptions) setCommand() string { 364 | if c.AwsAuth { 365 | return "aws-iam-authenticator" 366 | } 367 | return "aws" 368 | } 369 | 370 | func (c CommandOptions) AwsArgs(region string, clusterName string, arn string) []string { 371 | var args []string 372 | if c.AwsRoleString != "" && !c.AwsAuth { 373 | args = []string{"--region", region, "eks", "get-token", "--cluster-name", clusterName, "--role-arn", "arn:aws:iam::" + SplitAzIDAndGiveItem(arn, ":", 4) + ":role/" + c.AwsRoleString} 374 | } else if c.AwsRoleString != "" && c.AwsAuth { 375 | args = []string{"token", "-i", clusterName, "--role-arn", "arn:aws:iam::" + SplitAzIDAndGiveItem(arn, ":", 4) + ":role/" + c.AwsRoleString} 376 | } else { 377 | args = []string{"--region", region, "eks", "get-token", "--cluster-name", clusterName} 378 | } 379 | return args 380 | } 381 | 382 | func (c CommandOptions) AwsEnvs(profile string) interface{} { 383 | if c.AwsEnvProfile { 384 | var envArray []Env 385 | envArray = append(envArray, Env{Name: "AWS_PROFILE", Value: profile}) 386 | return envArray 387 | } 388 | return nil 389 | } 390 | 391 | func (c CommandOptions) GetSingleAWSCluster(clusterToFind string) Cluster { 392 | log.Info("Starting AWS find cluster named: " + clusterToFind) 393 | core.CheckEnvVarOrSitIt("AWS_REGION", c.AwsRegion) 394 | //get Profiles//search this name in account 395 | var f Cluster 396 | profiles := c.GetLocalAwsProfiles() 397 | c0 := make(chan Cluster) 398 | // search each profile 399 | for _, profile := range profiles { 400 | go c.getAwsClusters(c0, profile, clusterToFind) 401 | } 402 | for i := 0; i < len(profiles); i++ { 403 | res := <-c0 404 | if res.Name == clusterToFind { 405 | f = res 406 | } 407 | } 408 | return f 409 | //search this name in region 410 | //once it is found erturn info to the user 411 | } 412 | 413 | func (c CommandOptions) getAwsClusters(c0 chan Cluster, profile AwsProfiles, clusterToFind string) { 414 | var re Cluster 415 | log.Info(string("Using AWS profile: " + profile.Name)) 416 | regions := profile.listRegions() 417 | profiles := c.GetLocalAwsProfiles() 418 | addOnVersion := profiles[0].getVersion() 419 | c2 := make(chan []Cluster) 420 | for _, reg := range regions { 421 | go printOutResult(reg, clusterToFind, profile, addOnVersion, c2) 422 | } 423 | for i := 0; i < len(regions); i++ { 424 | aRegion := <-c2 425 | if len(aRegion) > 0 { 426 | for _, cluster := range aRegion { 427 | if cluster.Name == clusterToFind { 428 | re = cluster 429 | } 430 | } 431 | } 432 | } 433 | c0 <- re 434 | } 435 | 436 | func checkIfItsAssumeRole(keys []*ini.Key) (bool, string) { 437 | var ARNRegexp = regexp.MustCompile(`^arn:(\w|-)*:iam::\d+:role\/?(\w+|-|\/|\.)*$`) 438 | for _, a := range keys { 439 | if ARNRegexp.MatchString(a.String()) { 440 | log.Debug("Is ARN: " + a.String()) 441 | return true, a.String() 442 | } 443 | } 444 | return false, "" 445 | } 446 | 447 | func stsAssumeRole(awsProfile AwsProfiles) *aws.CredentialsCache { 448 | roleSession := "default" 449 | conf, err := config.LoadDefaultConfig(context.TODO(), 450 | config.WithRegion("us-east-1"), 451 | config.WithSharedConfigProfile(roleSession)) 452 | core.FailOnError(err, awsErrorMessage) 453 | appCreds := stscreds.NewAssumeRoleProvider(sts.NewFromConfig(conf), awsProfile.Arn) 454 | creds := aws.NewCredentialsCache(appCreds) 455 | log.Debugf("Succsefully triggered stsAssumeRole for %s", awsProfile.Name) 456 | return creds 457 | } 458 | 459 | func XinAwsProfiles(x string, y []AwsProfiles) (int, bool) { 460 | for t := range y { 461 | if strings.Contains(y[t].ConfProfile, x) { 462 | log.Debugf("profile %s is the %x in list", x, t) 463 | return t, true 464 | } 465 | } 466 | return 0, false 467 | } 468 | 469 | func removeString(word, arn string) string { 470 | if !strings.Contains("default", arn) && strings.Contains(arn, word) { 471 | log.Debugf("Cutting %s from %s", word, arn) 472 | split := strings.Split(arn, " ") 473 | log.Debugf("length is %x and the profile name will be: %s", len(split), split[1]) 474 | return split[1] 475 | } else if strings.Contains(word, arn) { 476 | return "" 477 | } else { 478 | return arn 479 | } 480 | } 481 | -------------------------------------------------------------------------------- /provider/aws_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | const testErrorMessage = "Unexpected result for " 10 | 11 | func TestAwsArgs(t *testing.T) { 12 | const testRegion = "eu-west-1" 13 | const testRegionFlag = "--region" 14 | const testGetTokenFlag = "get-token" 15 | const testClusterArn = "arn:aws:eks:eu-west-1:123456789:cluster/testCluster" 16 | const testClusterNameFlag = "--cluster-name" 17 | const testClusterName = "testCluster" 18 | // the testcase should have the CommandOptions struct, the region string, the clustername string and the arn string and expected array 19 | testCases := []struct { 20 | name string 21 | Command CommandOptions 22 | region string 23 | clusterName string 24 | arn string 25 | expected []string 26 | }{ 27 | { 28 | name: "Test_AwsArgs_only_role", 29 | Command: CommandOptions{AwsRoleString: "testRole", AwsAuth: false}, 30 | region: testRegion, 31 | clusterName: testClusterName, 32 | arn: testClusterArn, 33 | expected: []string{testRegionFlag, testRegion, "eks", testGetTokenFlag, testClusterNameFlag, testClusterName, "--role-arn", "arn:aws:iam::123456789:role/testRole"}, 34 | }, 35 | { 36 | name: "Test_AwsArgs_only_role_with_aws_auth", 37 | Command: CommandOptions{AwsRoleString: "testRole", AwsAuth: true}, 38 | region: testRegion, 39 | clusterName: testClusterName, 40 | arn: testClusterArn, 41 | expected: []string{"token", "-i", testClusterName, "--role-arn", "arn:aws:iam::123456789:role/testRole"}, 42 | }, 43 | { 44 | name: "Test_AwsArgs_without_role", 45 | Command: CommandOptions{AwsRoleString: "", AwsAuth: false}, 46 | region: testRegion, 47 | clusterName: testClusterName, 48 | arn: testClusterArn, 49 | expected: []string{testRegionFlag, testRegion, "eks", testGetTokenFlag, testClusterNameFlag, testClusterName}, 50 | }, 51 | { 52 | name: "Test_AwsArgs_without_role_with_aws_auth", 53 | Command: CommandOptions{AwsRoleString: "", AwsAuth: true}, 54 | region: testRegion, 55 | clusterName: testClusterName, 56 | arn: testClusterArn, 57 | expected: []string{testRegionFlag, testRegion, "eks", testGetTokenFlag, testClusterNameFlag, testClusterName}, 58 | }, 59 | } 60 | for _, tc := range testCases { 61 | t.Run(tc.name, func(t *testing.T) { 62 | var c CommandOptions = CommandOptions{AwsRoleString: tc.Command.AwsRoleString, AwsAuth: tc.Command.AwsAuth} 63 | args := c.AwsArgs(tc.region, tc.clusterName, tc.arn) 64 | assert.Equal(t, tc.expected, args, testErrorMessage+tc.name) 65 | }) 66 | } 67 | } 68 | 69 | func TestSetClusterName(t *testing.T) { 70 | testCases := []struct { 71 | name string 72 | AwsClusterName bool 73 | ClusterName string 74 | expected string 75 | }{ 76 | { 77 | name: "Full_Cluster_output", 78 | AwsClusterName: false, 79 | ClusterName: "arn:aws:eks:ap-southeast-2:234432434234:cluster/testCLuster01", 80 | expected: "234432434234:ap-southeast-2:testCLuster01", 81 | }, 82 | { 83 | name: "Short_ARN_output", 84 | AwsClusterName: true, 85 | ClusterName: "arn:aws:eks:ap-southeast-2:234432434234:cluster/testCLuster02", 86 | expected: "ap-southeast-2:testCLuster02", 87 | }, 88 | } 89 | 90 | for _, tc := range testCases { 91 | t.Run(tc.name, func(t *testing.T) { 92 | c := CommandOptions{AwsClusterName: tc.AwsClusterName} 93 | result := c.SetClusterName(&tc.ClusterName) 94 | assert.Equal(t, tc.expected, result, testErrorMessage+tc.name) 95 | }) 96 | } 97 | } 98 | 99 | func TestRemoveString(t *testing.T) { 100 | testCases := []struct { 101 | name string 102 | fullString string 103 | whatToRemove string 104 | expected string 105 | }{ 106 | { 107 | name: "Test_proifle_remove", 108 | fullString: "profile test", 109 | whatToRemove: "profile", 110 | expected: "test", 111 | }, { 112 | name: "Test_default_without_profile", 113 | fullString: "default", 114 | whatToRemove: "profile", 115 | expected: "default", 116 | }, 117 | } 118 | for _, tc := range testCases { 119 | t.Run(tc.name, func(t *testing.T) { 120 | result := removeString(tc.whatToRemove, tc.fullString) 121 | assert.Equal(t, tc.expected, result, testErrorMessage+tc.name) 122 | }) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /provider/azure.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "k8f/core" 6 | 7 | "github.com/Azure/azure-sdk-for-go/sdk/azidentity" 8 | "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice" 9 | "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions" 10 | log "github.com/sirupsen/logrus" 11 | "gopkg.in/yaml.v2" 12 | ) 13 | 14 | var ( 15 | ctx = context.Background() 16 | ) 17 | 18 | func (c CommandOptions) FullAzureList() Provider { 19 | log.Info("Starting Azure Full List") 20 | var list []Account 21 | c0 := make(chan string) 22 | tenant := GetTenentList() 23 | for _, t := range tenant { 24 | log.Info("Start Tenanat: " + *t.DisplayName) 25 | go func(c0 chan string, t armsubscriptions.TenantIDDescription) { 26 | subs := listSubscriptions(*t.TenantID) 27 | c1 := make(chan Account) 28 | for _, s := range subs { 29 | log.Info("Start Subscription: " + s.Name) 30 | go getAllAKS(s, c1, *t.TenantID) 31 | } 32 | for i := 0; i < len(subs); i++ { 33 | res := <-c1 34 | list = append(list, res) 35 | log.Debug("Finished Subscription: " + subs[i].Name) 36 | } 37 | c0 <- "Finished Tenanat:" 38 | }(c0, t) 39 | 40 | } 41 | for i := 0; i < len(tenant); i++ { 42 | res := <-c0 43 | log.Debug(res + " " + *tenant[i].DisplayName) 44 | } 45 | return Provider{"azure", list, countTotal(list)} 46 | } 47 | 48 | func auth(tenantid string) *azidentity.AzureCLICredential { 49 | log.Debug("Start Authentication for tenant ID: " + tenantid) 50 | cred, err := azidentity.NewAzureCLICredential(&azidentity.AzureCLICredentialOptions{TenantID: tenantid}) 51 | core.FailOnError(err, "Authentication Failed") 52 | log.Debug("Finished Authentication for tenant ID: " + tenantid) 53 | return cred 54 | } 55 | 56 | // get full list of tenants user got permissions to. 57 | func GetTenentList() []armsubscriptions.TenantIDDescription { 58 | log.Debug("Start getting tenant list") 59 | var res []armsubscriptions.TenantIDDescription 60 | tenants, err := armsubscriptions.NewTenantsClient(auth(""), nil) 61 | core.FailOnError(err, "Failed to get Tenants") 62 | tenant := tenants.NewListPager(nil) 63 | for tenant.More() { 64 | nextResult, err := tenant.NextPage(ctx) 65 | core.FailOnError(err, azureErrorMessage) 66 | for _, v := range nextResult.Value { 67 | res = append(res, *v) 68 | } 69 | } 70 | log.Debug("Finished getting tenant list") 71 | return res 72 | } 73 | 74 | func listSubscriptions(id string) []subs { 75 | log.Debug("Start getting Subscription list") 76 | var res []subs 77 | client, err := armsubscriptions.NewClient(auth(id), nil) 78 | core.FailOnError(err, "Failed to Auth") 79 | r := client.NewListPager(nil) 80 | for r.More() { 81 | nextResult, err := r.NextPage(ctx) 82 | core.FailOnError(err, azureErrorMessage) 83 | for _, v := range nextResult.Value { 84 | res = append(res, subs{*v.DisplayName, *v.SubscriptionID}) 85 | 86 | } 87 | } 88 | log.Debug("Finished getting Subscription list") 89 | return res 90 | } 91 | 92 | // this is the only path we need to get the aks, now need to get latest version. 93 | func getAllAKS(subscription subs, c1 chan Account, id string) { 94 | var r []Cluster 95 | client, err := armcontainerservice.NewManagedClustersClient(subscription.Id, auth(id), nil) 96 | core.FailOnError(err, "failed to create client") 97 | pager := client.NewListPager(nil) 98 | for pager.More() { 99 | nextResult, err := pager.NextPage(ctx) 100 | core.FailOnError(err, azureErrorMessage) 101 | for _, v := range nextResult.Value { 102 | supportedAKS := findSupportedAksVersions(SplitAzIDAndGiveItem(*v.ID, "/", 4), *v.Name, subscription.Id, id) 103 | l := getAksConfig(supportedAKS) 104 | log.Debug("current Version is: " + *v.Properties.KubernetesVersion) 105 | r = append(r, Cluster{*v.Name, *v.Properties.KubernetesVersion, l, *v.Location, *v.ID, "", microsoftSupportedVersion(l, *v.Properties.KubernetesVersion)}) 106 | } 107 | } 108 | c1 <- Account{subscription.Name, r, len(r), id} 109 | } 110 | 111 | // Getting AKS Config 112 | func getAksConfig(supportedList []string) string { 113 | return evaluateVersion(supportedList) 114 | } 115 | 116 | // Getting AKS Supported K8S versions 117 | func findSupportedAksVersions(resourceGroup string, resourceName string, subscription string, id string) []string { 118 | var supportList []string 119 | log.WithField("CommandOptions", log.Fields{"subscription": subscription, "tenantID": id, "resourceName": resourceName}).Debug("getAksConfig Variables and Values: ") 120 | client, err := armcontainerservice.NewManagedClustersClient(subscription, auth(id), nil) 121 | core.FailOnError(err, "Create Client Failed") 122 | profile, err := client.GetUpgradeProfile(ctx, resourceGroup, resourceName, nil) 123 | core.FailOnError(err, "Update Profile Failed") 124 | for _, a := range profile.Properties.ControlPlaneProfile.Upgrades { 125 | supportList = append(supportList, *a.KubernetesVersion) 126 | } 127 | log.Debug("List of Supported Versions") 128 | log.Debug(supportList) 129 | return supportList 130 | } 131 | 132 | // Getting AKS profile (contains most of the information needed for list and Connect commnands) 133 | func getAksProfile(client *armcontainerservice.ManagedClustersClient, resourceGroupName string, resourceName string) AllConfig { 134 | log.WithField("CommandOptions", log.Fields{"struct": core.DebugWithInfo(client), "resourceGroupName": resourceGroupName, "resourceName": resourceName}).Debug("getAksProfile Variables and Values: ") 135 | l, err := client.ListClusterUserCredentials(ctx, resourceGroupName, resourceName, nil) 136 | core.FailOnError(err, "get user creds Failed") 137 | y := Config{} 138 | for _, c := range l.Kubeconfigs { 139 | b64 := c.Value 140 | err := yaml.Unmarshal(b64, &y) 141 | core.FailOnError(err, "Failed To Unmarshal Config") 142 | } 143 | return AllConfig{auth: y.Users, context: y.Contexts, clusters: y.Clusters} 144 | } 145 | 146 | // create the KubeConfig info needed for AKS 147 | func (c CommandOptions) ConnectAllAks() AllConfig { 148 | var authe []Users 149 | var context []Contexts 150 | var clusters []Clusters 151 | var arnContext string 152 | p := c.FullAzureList() 153 | for _, a := range p.Accounts { 154 | chanel := make(chan AllConfig) 155 | for _, c := range a.Clusters { 156 | go func(chanel chan AllConfig, c Cluster, a Account) { 157 | log.WithField("Cluster Struct", log.Fields{"struct": core.DebugWithInfo(c), "tenentAuth": core.DebugWithInfo(a)}).Debug("Creating NewManagedClustersClient") 158 | client, err := armcontainerservice.NewManagedClustersClient(SplitAzIDAndGiveItem(c.Id, "/", 2), auth(a.Tenanat), nil) 159 | core.FailOnError(err, "get user creds Failed") 160 | chanel <- getAksProfile(client, SplitAzIDAndGiveItem(c.Id, "/", 4), c.Name) 161 | }(chanel, c, a) 162 | } 163 | for i := 0; i < len(a.Clusters); i++ { 164 | response := <-chanel 165 | arnContext = response.context[0].Context.User 166 | authe = append(authe, response.auth...) 167 | context = append(context, response.context...) 168 | clusters = append(clusters, response.clusters...) 169 | } 170 | } 171 | if c.Combined { 172 | log.Debug("Started azure only config creation") 173 | c.CombineConfigs(AllConfig{authe, context, clusters}, arnContext) 174 | return AllConfig{} 175 | } else { 176 | log.Debug("Started azure combined config creation") 177 | return AllConfig{authe, context, clusters} 178 | } 179 | 180 | } 181 | 182 | func (c CommandOptions) GetSingleAzureCluster(clusterToFind string) Cluster { 183 | log.Info("Starting Azure find cluster named: " + clusterToFind) 184 | c0 := make(chan Cluster) 185 | tenant := GetTenentList() 186 | for _, t := range tenant { 187 | log.Info("Start Tenanat: " + *t.DisplayName) 188 | go getAzureClusters(c0, t, clusterToFind) 189 | } 190 | for i := 0; i < len(tenant); i++ { 191 | res := <-c0 192 | if res.Name == clusterToFind { 193 | return res 194 | } 195 | } 196 | return Cluster{} 197 | } 198 | func getAzureClusters(c0 chan Cluster, t armsubscriptions.TenantIDDescription, clusterToFind string) { 199 | var list Cluster 200 | subs := listSubscriptions(*t.TenantID) 201 | c1 := make(chan Account) 202 | for _, s := range subs { 203 | log.Info("Start Subscription: " + s.Name) 204 | go getAllAKS(s, c1, *t.TenantID) 205 | } 206 | for i := 0; i < len(subs); i++ { 207 | res := <-c1 208 | for a := range res.Clusters { 209 | if res.Clusters[a].Name == clusterToFind { 210 | list = res.Clusters[a] 211 | break 212 | } 213 | } 214 | log.Debug("Finished Subscription: " + subs[i].Name) 215 | } 216 | c0 <- list 217 | } 218 | -------------------------------------------------------------------------------- /provider/constant.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | const Kregion = "eu-west-1" 4 | const awsErrorMessage = "Failed to create new session" 5 | const azureErrorMessage = "failed to advance page" 6 | const decodeError = "failed to decode base64" 7 | const backupExtnesion = ".bak" 8 | const filedtoCopyToTarget = "failed to Copy target file" 9 | const credentialsValidationError = "credentials validation error for profile " 10 | -------------------------------------------------------------------------------- /provider/core.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "k8f/core" 8 | "os" 9 | "path/filepath" 10 | "reflect" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/hashicorp/go-version" 15 | log "github.com/sirupsen/logrus" 16 | "gopkg.in/yaml.v2" 17 | ) 18 | 19 | func returnMinorDiff(splitcurernt, splitLatest []string) int { 20 | latestMinor, err := strconv.Atoi(splitLatest[1]) 21 | core.FailOnError(err, "faild to convert string to int") 22 | currentMinor, err := strconv.Atoi(splitcurernt[1]) 23 | core.FailOnError(err, "faild to convert string to int") 24 | getStatus := latestMinor - currentMinor 25 | return getStatus 26 | } 27 | 28 | // evaluate latest version from addon version list 29 | func evaluateVersion(list []string) string { 30 | var latest string 31 | for _, v := range list { 32 | var lt *version.Version 33 | var err error 34 | v1, err := version.NewVersion(v) 35 | core.FailOnError(err, "Error Evaluating Version") 36 | if latest == "" { 37 | lt, err = version.NewVersion("0.0") 38 | } else { 39 | lt, err = version.NewVersion(latest) 40 | } 41 | core.FailOnError(err, "Error Evaluating Version") 42 | // Options availabe 43 | if v1.GreaterThan(lt) { 44 | latest = v 45 | } // GreaterThen 46 | } 47 | return latest 48 | } 49 | 50 | // Microsoft Compliance 51 | func microsoftSupportedVersion(latest string, current string) string { 52 | //IMPORTANT: only supports same major at the moment!!! 53 | splitLatest := strings.Split(latest, ".") 54 | splitcurernt := strings.Split(current, ".") 55 | // make sure its the same major 56 | if splitLatest[0] == splitcurernt[0] { 57 | getStatus := returnMinorDiff(splitcurernt, splitLatest) 58 | //if its latest minor or -1, mark as ok 59 | if getStatus <= 1 { 60 | return "OK" 61 | //if its minor -2 show warning 62 | } else if getStatus > 1 && getStatus <= 2 { 63 | return "Warning" 64 | // if its minor > -2 show Critical 65 | } else { 66 | return "Critical" 67 | } 68 | 69 | } 70 | return "Unknown" 71 | } 72 | 73 | // provide version compare 74 | func HowManyVersionsBack(versionsList []string, currentVersion string) string { 75 | log.Debug("current version is: " + currentVersion) 76 | latest := evaluateVersion(versionsList) 77 | splitcurernt := strings.Split(currentVersion, ".") 78 | splitLatest := strings.Split(latest, ".") 79 | //check if same major 80 | if splitLatest[0] == splitcurernt[0] { 81 | getStatus := returnMinorDiff(splitcurernt, splitLatest) 82 | if getStatus <= 1 { 83 | return "Perfect" 84 | } else if getStatus <= 3 { 85 | return "OK" 86 | } else { 87 | return "Warning" 88 | } 89 | } 90 | return "Critical" 91 | } 92 | 93 | func (c CommandOptions) getYamlOrJsonOutput(p interface{}) ([]byte, error) { 94 | var kJson []byte 95 | var err error 96 | log.Debug("start RunResult Func") 97 | if c.Output == "json" { 98 | log.Info("start Json Marshal") 99 | kJson, _ = json.Marshal(p) 100 | } else if c.Output == "yaml" { 101 | log.Info("start YAML Marshal") 102 | kJson, _ = yaml.Marshal(p) 103 | } else { 104 | err = fmt.Errorf("requested Output is not supported") 105 | } 106 | log.Info("returning Output Marshal") 107 | return kJson, err 108 | } 109 | 110 | // printout format selection 111 | func (c CommandOptions) PrintoutResults(p interface{}) (string, error) { 112 | data, err := c.getYamlOrJsonOutput(p) 113 | return string(data), err 114 | } 115 | 116 | // func to count ammount of Cluster in an account 117 | func countTotal(f []Account) int { 118 | var count int 119 | for _, a := range f { 120 | count = count + a.TotalCount 121 | } 122 | return count 123 | } 124 | 125 | // func to merge kubeconfig output to singe config file 126 | func (c CommandOptions) CombineConfigs(configs AllConfig, arn string) { 127 | var y []byte 128 | var err error 129 | clientConfig := Config{ 130 | Kind: "Config", 131 | APIVersion: "v1", 132 | Clusters: configs.clusters, 133 | Contexts: configs.context, 134 | CurrentContext: arn, 135 | Preferences: Preferences{}, 136 | Users: configs.auth, 137 | } 138 | if c.DryRun { 139 | log.Debugf("Dry-run will Output a %s Output", c.Output) 140 | output, err := c.PrintoutResults(clientConfig) 141 | core.FailOnError(err, "failed to printout results") 142 | fmt.Println(output) 143 | } else { 144 | if c.Backup { 145 | y, _ = yaml.Marshal(clientConfig) 146 | log.Debug("calling copy file to bak") 147 | c.Configcopy() 148 | } 149 | if c.Merge { 150 | y, err = c.runMerge(clientConfig) 151 | core.FailOnError(err, "failed to merge configs") 152 | c.cleanFile() 153 | } else { 154 | y, _ = yaml.Marshal(clientConfig) 155 | } 156 | err := os.WriteFile(c.Path, y, 0666) 157 | core.FailOnError(err, "failed to save config") 158 | log.Infof("「 %s 」 write successful!\n", c.Path) 159 | } 160 | } 161 | 162 | func (c CommandOptions) FullCloudConfig() { 163 | var auth []Users 164 | var context []Contexts 165 | var clusters []Clusters 166 | r := make(chan AllConfig) 167 | for _, cloud := range []string{"azure", "aws"} { 168 | go func(cloud string, r chan AllConfig, c CommandOptions) { 169 | var res AllConfig 170 | if cloud == "azure" { 171 | res = c.ConnectAllAks() 172 | } else if cloud == "aws" { 173 | res = c.ConnectAllEks() 174 | } 175 | r <- res 176 | }(cloud, r, c) 177 | } 178 | for i := 0; i < len([]string{"azure", "aws"}); i++ { 179 | response := <-r 180 | auth = append(auth, response.auth...) 181 | context = append(context, response.context...) 182 | clusters = append(clusters, response.clusters...) 183 | } 184 | c.CombineConfigs(AllConfig{auth: auth, context: context, clusters: clusters}, context[0].Context.User) 185 | } 186 | 187 | func (c CommandOptions) Configcopy() { 188 | sourceFileStat, err := os.Stat(c.Path) 189 | core.FailOnError(err, "Issue Findign the Files in the path: "+c.Path) 190 | if !sourceFileStat.Mode().IsRegular() { 191 | core.FailOnError(err, c.Path+" is not a regular file") 192 | } 193 | source, err := os.Open(c.Path) 194 | core.FailOnError(err, "failed to Open target file") 195 | defer source.Close() 196 | var destination *os.File 197 | backupVersion := c.GetBackupFileVersion() 198 | if backupVersion >= 1 { 199 | destination, err = os.Create(c.Path + backupExtnesion + "." + fmt.Sprint(backupVersion)) 200 | log.Debug(c.Path + backupExtnesion + "." + fmt.Sprint(backupVersion)) 201 | core.FailOnError(err, filedtoCopyToTarget) 202 | } else { 203 | destination, err = os.Create(c.Path + backupExtnesion) 204 | core.FailOnError(err, filedtoCopyToTarget) 205 | } 206 | 207 | defer destination.Close() 208 | _, err = io.Copy(destination, source) 209 | core.FailOnError(err, filedtoCopyToTarget) 210 | } 211 | 212 | func (c CommandOptions) GetBackupFileVersion() int { 213 | dir := filepath.Dir(c.Path) 214 | dirFiles, err := os.ReadDir(dir) 215 | core.FailOnError(err, "failed to list all files in directory") 216 | var countBackups []string 217 | for _, file := range dirFiles { 218 | if strings.Contains(file.Name(), backupExtnesion) { 219 | countBackups = append(countBackups, file.Name()) 220 | log.Debug(file.Name(), " ", file.IsDir()) 221 | } 222 | } 223 | return len(countBackups) 224 | } 225 | func SplitAzIDAndGiveItem(input string, seperator string, out int) string { 226 | s := strings.Split(input, seperator) 227 | log.Debug("Split output") 228 | return s[out] 229 | } 230 | 231 | func (c CommandOptions) cleanFile() { 232 | // Open the file with write only mode and set the file mode to 0644 233 | file, err := os.OpenFile(c.Path, os.O_WRONLY|os.O_TRUNC, 0644) 234 | if err != nil { 235 | panic(err) 236 | } 237 | defer file.Close() 238 | 239 | // Truncate the file content to 0 240 | if err := file.Truncate(0); err != nil { 241 | panic(err) 242 | } 243 | 244 | // Get the file info to print the file size 245 | fileStat, err := file.Stat() 246 | if err != nil { 247 | panic(err) 248 | } 249 | 250 | // Print the file size after cleaning the file 251 | log.Debug("File size after cleaning:", fileStat.Size()) 252 | } 253 | 254 | func checkIfStructInit(u interface{}, key string) bool { 255 | v := reflect.ValueOf(u) 256 | t := v.Type() 257 | 258 | for i := 0; i < v.NumField(); i++ { 259 | field := v.Field(i) 260 | tag := t.Field(i).Tag.Get("yaml") 261 | if tag == key+",omitempty" { 262 | // Check if the field is set to its zero value 263 | if reflect.DeepEqual(field.Interface(), reflect.Zero(field.Type()).Interface()) { 264 | continue 265 | } 266 | // If any field has a non-zero value, return true 267 | return true 268 | } 269 | } 270 | return false 271 | } 272 | 273 | func (c CommandOptions) StructOutput(inpoutInfo interface{}) { 274 | data, err := c.getYamlOrJsonOutput(inpoutInfo) 275 | core.FailOnError(err, "failed to get struct as "+c.Output) 276 | err = os.WriteFile(c.Path+"."+c.Output, data, 0666) 277 | core.FailOnError(err, "failed to save config") 278 | } 279 | -------------------------------------------------------------------------------- /provider/core_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "k8f/core" 8 | "os" 9 | "testing" 10 | 11 | log "github.com/sirupsen/logrus" 12 | "github.com/stretchr/testify/assert" 13 | "gopkg.in/yaml.v2" 14 | ) 15 | 16 | func TestReturnMinorDiff(t *testing.T) { 17 | testCases := []struct { 18 | name string 19 | currentVersion []string 20 | latestVersion []string 21 | expected int 22 | }{ 23 | { 24 | name: "PerfectMatch", 25 | currentVersion: []string{"1", "27"}, 26 | latestVersion: []string{"1", "27"}, 27 | expected: 0, 28 | }, { 29 | name: "PerfectMatch-1", 30 | currentVersion: []string{"1", "26"}, 31 | latestVersion: []string{"1", "27"}, 32 | expected: 1, 33 | }, { 34 | name: "WarningMatch", 35 | currentVersion: []string{"1", "22"}, 36 | latestVersion: []string{"1", "27"}, 37 | expected: 5, 38 | }, { 39 | name: "CriticalMatch", 40 | currentVersion: []string{"1", "0"}, 41 | latestVersion: []string{"1", "27"}, 42 | expected: 27, 43 | }, 44 | } 45 | for _, tc := range testCases { 46 | t.Run(tc.name, func(t *testing.T) { 47 | // Test case: Perfect match, current version is the latest 48 | result := returnMinorDiff(tc.currentVersion, tc.latestVersion) 49 | assert.Equal(t, tc.expected, result, "Unexpected result for "+tc.name) 50 | 51 | }) 52 | } 53 | } 54 | func TestEvaluateVersion(t *testing.T) { 55 | t.Run("EmptyList", func(t *testing.T) { 56 | // Test case: Empty list 57 | list := []string{} 58 | result := evaluateVersion(list) 59 | expected := "" 60 | assert.Equal(t, expected, result, "Unexpected result for empty list") 61 | }) 62 | 63 | t.Run("SingleVersion", func(t *testing.T) { 64 | // Test case: Single version in the list 65 | list := []string{"1.2.3"} 66 | result := evaluateVersion(list) 67 | expected := "1.2.3" 68 | assert.Equal(t, expected, result, "Unexpected result for single version") 69 | }) 70 | 71 | t.Run("MultipleVersions", func(t *testing.T) { 72 | // Test case: Multiple versions in the list 73 | list := []string{"1.0.0", "1.2.3", "2.0.0", "0.1.0"} 74 | result := evaluateVersion(list) 75 | expected := "2.0.0" 76 | assert.Equal(t, expected, result, "Unexpected result for multiple versions") 77 | }) 78 | // Add more test cases for other scenarios 79 | } 80 | 81 | func TestMicrosoftSupportedVersion(t *testing.T) { 82 | setCurrent := "10.0.0" 83 | testCases := []struct { 84 | name string 85 | latest string 86 | current string 87 | expected string 88 | }{ 89 | { 90 | name: "SameMajorVersion", 91 | latest: setCurrent, 92 | current: setCurrent, 93 | expected: "OK", 94 | }, 95 | { 96 | name: "MinorVersionDifference", 97 | latest: "10.1.0", 98 | current: setCurrent, 99 | expected: "OK", 100 | }, 101 | { 102 | name: "MinorVersionWarning", 103 | latest: "10.10.0", 104 | current: "10.8.0", 105 | expected: "Warning", 106 | }, 107 | { 108 | name: "MinorVersionCritical", 109 | latest: "10.6.0", 110 | current: setCurrent, 111 | expected: "Critical", 112 | }, 113 | { 114 | name: "DifferentMajorVersion", 115 | latest: "11.0.0", 116 | current: "10.1.0", 117 | expected: "Unknown", 118 | }, 119 | // Add more test cases for other scenarios 120 | } 121 | 122 | for _, tc := range testCases { 123 | t.Run(tc.name, func(t *testing.T) { 124 | result := microsoftSupportedVersion(tc.latest, tc.current) 125 | assert.Equal(t, tc.expected, result, "Unexpected result for "+tc.name) 126 | }) 127 | } 128 | } 129 | 130 | func TestHowManyVersionsBack(t *testing.T) { 131 | versionsList := []string{"1.24", "1.23", "1.22", "1.21", "1.20", "1.27", "1.26", "1.25"} 132 | testCases := []struct { 133 | name string 134 | currentVersion string 135 | expected string 136 | }{ 137 | { 138 | name: "PerfectMatch", 139 | currentVersion: "1.27", 140 | expected: "Perfect", 141 | }, { 142 | name: "PerfectMatch-1", 143 | currentVersion: "1.26", 144 | expected: "Perfect", 145 | }, { 146 | name: "WarningMatch", 147 | currentVersion: "1.22", 148 | expected: "Warning", 149 | }, { 150 | name: "CriticalMatch", 151 | currentVersion: "0.5", 152 | expected: "Critical", 153 | }, 154 | } 155 | for _, tc := range testCases { 156 | t.Run(tc.name, func(t *testing.T) { 157 | // Test case: Perfect match, current version is the latest 158 | result := HowManyVersionsBack(versionsList, tc.currentVersion) 159 | assert.Equal(t, tc.expected, result, "Unexpected result for "+tc.name) 160 | 161 | }) 162 | } 163 | } 164 | 165 | func TestPrintoutResults(t *testing.T) { 166 | data := struct { 167 | Name string `json:"name"` 168 | Age int `json:"age"` 169 | }{ 170 | Name: "John Doe", 171 | Age: 30, 172 | } 173 | 174 | t.Run("JSONOutput", func(t *testing.T) { 175 | // Test case: JSON output 176 | c := CommandOptions{Output: "json"} 177 | result, err := c.PrintoutResults(data) 178 | if err != nil { 179 | t.Errorf("Didnt expected an error, but got none.") 180 | } 181 | expected, _ := json.Marshal(data) 182 | assert.Equal(t, string(expected), result, "Unexpected result for JSON output") 183 | }) 184 | 185 | t.Run("YAMLOutput", func(t *testing.T) { 186 | // Test case: YAML output 187 | c := CommandOptions{Output: "yaml"} 188 | result, err := c.PrintoutResults(data) 189 | if err != nil { 190 | t.Errorf("Didnt expected an error, but got none.") 191 | } 192 | expected, _ := yaml.Marshal(data) 193 | assert.Equal(t, string(expected), result, "Unexpected result for YAML output") 194 | }) 195 | 196 | t.Run("UnsupportedOutput", func(t *testing.T) { 197 | // Test case: Unsupported output type 198 | c := CommandOptions{Output: "csv"} 199 | _, err := c.PrintoutResults(data) 200 | if assert.Error(t, err) { 201 | expectedError := "requested Output is not supported" 202 | assert.Equal(t, expectedError, err.Error()) 203 | } 204 | // expected := "Requested Output is not supported" 205 | // assert.Equal(t, expected, result, "Unexpected result for unsupported output type") 206 | }) 207 | // Add more test cases for other scenarios 208 | } 209 | 210 | func TestCountTotal(t *testing.T) { 211 | accounts := []Account{ 212 | {TotalCount: 10}, 213 | {TotalCount: 20}, 214 | {TotalCount: 30}, 215 | } 216 | 217 | t.Run("MultipleAccounts", func(t *testing.T) { 218 | // Test case: Multiple accounts 219 | result := countTotal(accounts) 220 | expected := 60 221 | assert.Equal(t, expected, result, "Unexpected total count for multiple accounts") 222 | }) 223 | 224 | t.Run("EmptyAccounts", func(t *testing.T) { 225 | // Test case: Empty accounts 226 | result := countTotal([]Account{}) 227 | expected := 0 228 | assert.Equal(t, expected, result, "Unexpected total count for empty accounts") 229 | }) 230 | // Add more test cases for other scenarios 231 | } 232 | 233 | func TestCheckIfStructInit(t *testing.T) { 234 | t.Run("FieldExists", func(t *testing.T) { 235 | // Test case: Field exists in the struct 236 | user := User{ 237 | Exec: Exec{APIVersion: "1", Args: []string{"1", "2"}, Command: "noting", Env: "dev", ProvideClusterInfo: true}, 238 | ClientCertificateData: "certData", 239 | ClientKeyData: "clientKeyData", 240 | Token: "veryComplicatedToken", 241 | } 242 | 243 | result := checkIfStructInit(user, "exec") 244 | expected := true 245 | assert.Equal(t, expected, result, "Unexpected result for field existence") 246 | }) 247 | 248 | t.Run("FieldOmitted", func(t *testing.T) { 249 | // Test case: Field is omitted in the struct 250 | user := User{} 251 | 252 | result := checkIfStructInit(user, "exec") 253 | expected := false 254 | assert.Equal(t, expected, result, "Unexpected result for field omission") 255 | }) 256 | // Add more test cases for other scenarios 257 | } 258 | 259 | func TestCleanFile(t *testing.T) { 260 | // Create a temporary file for testing 261 | tmpfile, err := os.CreateTemp("", "testfile") 262 | if err != nil { 263 | t.Fatal(err) 264 | } 265 | defer os.Remove(tmpfile.Name()) 266 | // Write some content to the file 267 | content := []byte("Test content") 268 | _, err = tmpfile.Write(content) 269 | if err != nil { 270 | t.Fatal(err) 271 | } 272 | // Close the file before cleaning it 273 | tmpfile.Close() 274 | 275 | // Create a CommandOptions instance with the temporary file path 276 | c := CommandOptions{Path: tmpfile.Name()} 277 | 278 | // Call the cleanFile method 279 | c.cleanFile() 280 | // Open the file again to check if it's empty 281 | file, err := os.Open(tmpfile.Name()) 282 | if err != nil { 283 | t.Fatal(err) 284 | } 285 | defer file.Close() 286 | // Read the file content 287 | _, err = io.ReadAll(file) 288 | if err != nil { 289 | t.Fatal(err) 290 | } 291 | // Check if the file is empty 292 | fileStat, err := file.Stat() 293 | if err != nil { 294 | t.Fatal(err) 295 | } 296 | // Verify that the file size is 0 after cleaning 297 | if fileStat.Size() != 0 { 298 | t.Errorf("Expected file size after cleaning: 0, got: %d", fileStat.Size()) 299 | } 300 | } 301 | 302 | func TestConfigCopy(t *testing.T) { 303 | t.Run("RegularFile", func(t *testing.T) { 304 | // Create a temporary file for testing 305 | tmpfile, err := os.CreateTemp("", "testfile") 306 | if err != nil { 307 | t.Fatal(err) 308 | } 309 | defer os.RemoveAll(tmpfile.Name()) 310 | 311 | // Create a CommandOptions instance with the temporary file path 312 | c := CommandOptions{Path: tmpfile.Name()} 313 | 314 | // Call the Configcopy method 315 | c.Configcopy() 316 | 317 | // Check if the backup file exists 318 | _, err = os.Stat(tmpfile.Name() + ".bak") 319 | if err != nil { 320 | t.Errorf("Expected backup file to exist, got error: %v", err) 321 | } 322 | os.RemoveAll(tmpfile.Name() + ".bak") 323 | }) 324 | } 325 | 326 | func TestSplitAzIDAndGiveItem(t *testing.T) { 327 | t.Run("ValidInput", func(t *testing.T) { 328 | input := "item1-item2-item3" 329 | separator := "-" 330 | index := 1 331 | expected := "item2" 332 | 333 | result := SplitAzIDAndGiveItem(input, separator, index) 334 | 335 | if result != expected { 336 | t.Errorf("Expected result: %s, got: %s", expected, result) 337 | } 338 | }) 339 | } 340 | 341 | func TestGetBackupFileVersion(t *testing.T) { 342 | testCases := []struct { 343 | name string 344 | current int 345 | expected int 346 | }{ 347 | {name: "NoBackups", 348 | current: 0, 349 | expected: 0}, 350 | {name: "OneBackups", 351 | current: 1, 352 | expected: 1}, 353 | {name: "FiveBackups", 354 | current: 4, 355 | expected: 5}, 356 | } 357 | for _, tc := range testCases { 358 | directory := t.TempDir() 359 | for i := 0; i < tc.current; i++ { 360 | _, err := os.Create(directory + backupExtnesion + "." + fmt.Sprint(i)) 361 | log.Debug(directory + backupExtnesion + "." + fmt.Sprint(i)) 362 | core.FailOnError(err, filedtoCopyToTarget) 363 | } 364 | t.Run(tc.name, func(t *testing.T) { 365 | c := CommandOptions{Path: directory} 366 | // Test case: Perfect match, current version is the latest 367 | result := c.GetBackupFileVersion() 368 | assert.Equal(t, tc.expected, result, "Unexpected result for "+tc.name) 369 | }) 370 | } 371 | } 372 | 373 | func TestStructOutput(t *testing.T) { 374 | outputStruct := Provider{Provider: "aws", Accounts: []Account{{Name: "my-account", TotalCount: 1, Clusters: []Cluster{{Name: "my-cluster", Version: "1.21", Latest: "1.27", Region: "eu-west-1", Status: "Warning"}}}}} 375 | testCases := []struct { 376 | name string 377 | inpoutInfo interface{} 378 | Command CommandOptions 379 | Path string 380 | Output string 381 | }{ 382 | { 383 | name: "json file Saved in current directory", 384 | inpoutInfo: outputStruct, 385 | Command: CommandOptions{Output: "json", Path: "./output"}, 386 | }, 387 | { 388 | name: "Yaml file Saved in current directory", 389 | inpoutInfo: outputStruct, 390 | Command: CommandOptions{Output: "yaml", Path: "./output"}, 391 | }, 392 | } 393 | 394 | for _, tc := range testCases { 395 | t.Run(tc.name, func(t *testing.T) { 396 | var c CommandOptions = CommandOptions(tc.Command) 397 | filename := c.Path + "." + c.Output 398 | c.StructOutput(tc.inpoutInfo) 399 | _, err := os.Stat(filename) 400 | if err != nil { 401 | t.Errorf("Expected file '%s' to be created, but got an error: %v", filename, err) 402 | } 403 | err = os.Remove(filename) 404 | if err != nil { 405 | fmt.Printf("Error deleting the file: %s\n", err) 406 | } 407 | }) 408 | } 409 | } 410 | 411 | func TestGetYamlOrJsonOutput(t *testing.T) { 412 | inputInfo := Provider{Provider: "aws", Accounts: []Account{{Name: "a", Clusters: []Cluster{{Name: "1", Version: "1.23", Latest: "1.27", Region: "eu-west-1"}}, TotalCount: 1}}} 413 | testCases := []struct { 414 | name string 415 | inpoutInfo interface{} 416 | Command CommandOptions 417 | Expecterd string 418 | }{ 419 | { 420 | name: "a valid yaml struct", 421 | inpoutInfo: inputInfo, 422 | Command: CommandOptions{Output: "yaml"}, 423 | }, 424 | { 425 | name: "a valid json struct", 426 | inpoutInfo: inputInfo, 427 | Command: CommandOptions{Output: "json"}, 428 | }, 429 | { 430 | name: "Not a valid csv struct", 431 | inpoutInfo: inputInfo, 432 | Command: CommandOptions{Output: "csv"}, 433 | }, 434 | } 435 | 436 | for _, tc := range testCases { 437 | t.Run(tc.name, func(t *testing.T) { 438 | c := CommandOptions(tc.Command) 439 | data, err := c.getYamlOrJsonOutput(tc.inpoutInfo) 440 | if err != nil { 441 | expectedError := "requested Output is not supported" 442 | assert.Equal(t, expectedError, err.Error()) 443 | } else { 444 | assert.IsType(t, []byte{}, data) 445 | } 446 | }) 447 | } 448 | } 449 | -------------------------------------------------------------------------------- /provider/gcp.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "k8f/core" 7 | "strings" 8 | 9 | log "github.com/sirupsen/logrus" 10 | "google.golang.org/api/cloudresourcemanager/v1" 11 | "google.golang.org/api/container/v1" 12 | "google.golang.org/api/option" 13 | ) 14 | 15 | // main process for List Command 16 | func (c CommandOptions) GcpMain() Provider { 17 | log.Info("Starting GCP List") 18 | var clusters []Account 19 | Projects := gcpProjects() 20 | chanel := make(chan Account) 21 | for _, p := range Projects { 22 | go func(chanel chan Account, p subs) { 23 | log.Info("Starting GCP project: " + p.Id) 24 | var err error 25 | projclusters, err := c.getK8sClusterConfigs(context.Background(), p.Id) 26 | chanel <- Account{Name: p.Id, Clusters: projclusters, TotalCount: len(projclusters)} 27 | log.Error(err) 28 | }(chanel, p) 29 | 30 | } 31 | for i := 0; i < len(Projects); i++ { 32 | accoutn := <-chanel 33 | clusters = append(clusters, accoutn) 34 | } 35 | return Provider{Provider: "gcp", Accounts: clusters, TotalCount: countTotal(clusters)} 36 | } 37 | 38 | // lists all GCP projects in current orgenization 39 | func gcpProjects() []subs { 40 | // resource manager auth 41 | var projStruct []subs 42 | cloudresourcemanagerService, err := cloudresourcemanager.NewService(ctx, option.WithScopes(cloudresourcemanager.CloudPlatformReadOnlyScope)) 43 | core.FailOnError(err, "Failed to create Auth client") 44 | // get list of orginization Projects 45 | projList := cloudresourcemanagerService.Projects.List() 46 | resp, err := projList.Do() 47 | core.FailOnError(err, "Failed to get projects list") 48 | for _, a := range resp.Projects { 49 | projStruct = append(projStruct, subs{Name: a.Name, Id: a.ProjectId}) 50 | } 51 | return projStruct 52 | } 53 | 54 | func (c CommandOptions) getK8sClusterConfigs(ctx context.Context, projectId string) ([]Cluster, error) { 55 | var clustserss []Cluster 56 | svc, err := container.NewService(ctx) 57 | if err != nil { 58 | return []Cluster{}, fmt.Errorf("container.NewService: %w", err) 59 | } 60 | 61 | // Ask Google for a list of all kube clusters in the given project. 62 | 63 | resp, err := svc.Projects.Zones.Clusters.List(projectId, "-").Context(ctx).Do() 64 | if err != nil { 65 | return []Cluster{}, fmt.Errorf("clusters list project=%s: %w", projectId, err) 66 | } 67 | 68 | for _, a := range resp.Clusters { 69 | log.Info("the Cluster name is: " + a.Name + " and its in zone " + a.Zone) 70 | clustserss = append(clustserss, Cluster{Name: a.Name, Version: a.CurrentMasterVersion, Region: a.Zone, Latest: c.latestGCP(a)}) 71 | } 72 | return clustserss, nil 73 | } 74 | 75 | // func to get latest version 76 | func (c CommandOptions) latestGCP(k *container.Cluster) string { 77 | svc, err := container.NewService(context.Background()) 78 | core.FailOnError(err, "failed to create container service") 79 | output := svc.Projects.Zones.GetServerconfig(k.Name, k.Zone) 80 | ver, err := output.Do() 81 | core.FailOnError(err, "failed to get versions") 82 | for _, v := range ver.Channels { 83 | if strings.Contains(k.ReleaseChannel.Channel, v.Channel) { 84 | return v.ValidVersions[0] 85 | } 86 | } 87 | return "" 88 | } 89 | -------------------------------------------------------------------------------- /provider/interfaces.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-sdk-go-v2/service/sts" 7 | ) 8 | 9 | type STSAssumeRoleAPI interface { 10 | AssumeRole(ctx context.Context, 11 | params *sts.AssumeRoleInput, 12 | optFns ...func(*sts.Options)) (*sts.AssumeRoleOutput, error) 13 | } 14 | -------------------------------------------------------------------------------- /provider/kube_merge.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/base64" 6 | "errors" 7 | "fmt" 8 | "k8f/core" 9 | "strings" 10 | 11 | "github.com/imdario/mergo" 12 | "github.com/manifoldco/promptui" 13 | log "github.com/sirupsen/logrus" 14 | "github.com/spf13/cobra" 15 | "k8s.io/client-go/tools/clientcmd" 16 | clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 17 | ) 18 | 19 | // MergeCommand merge cmd struct 20 | type MergeCommand struct { 21 | cobra.Command 22 | } 23 | 24 | // KubeConfigOption kubeConfig option 25 | type KubeConfigOption struct { 26 | config *clientcmdapi.Config 27 | fileName string 28 | } 29 | 30 | func (mc CommandOptions) runMerge(newConf Config) ([]byte, error) { 31 | var kconfigs []*clientcmdapi.Config 32 | confToupdate, err := toClientConfig(&newConf) 33 | kconfigs = append(kconfigs, confToupdate) 34 | core.FailOnError(err, "failed to convert Config struct to clientcmdapi.Config") 35 | outConfigs := clientcmdapi.NewConfig() 36 | log.Infof("Loading KubeConfig file: %s\n", mc.Path) 37 | loadConfig, err := loadKubeConfig(mc.Path) 38 | core.FailOnError(err, "File "+mc.Path+" is not kubeconfig\n") 39 | kconfigs = append(kconfigs, loadConfig) 40 | for _, conf := range kconfigs { 41 | kco := &KubeConfigOption{ 42 | config: conf, 43 | fileName: getFileName(mc.Path), 44 | } 45 | outConfigs, err = kco.handleContexts(outConfigs, mc) 46 | if err != nil { 47 | return nil, err 48 | } 49 | } 50 | outConfigs.APIVersion = "v1" 51 | outConfigs.Kind = "Config" 52 | var contxetcItem *clientcmdapi.Context 53 | for _, value := range outConfigs.Contexts { 54 | contxetcItem = value 55 | break 56 | } 57 | outConfigs.CurrentContext = contxetcItem.Cluster 58 | confByte, err := clientcmd.Write(*outConfigs) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return confByte, nil 63 | } 64 | 65 | func loadKubeConfig(yaml string) (*clientcmdapi.Config, error) { 66 | loadConfig, err := clientcmd.LoadFromFile(yaml) 67 | if err != nil { 68 | return nil, err 69 | } 70 | return loadConfig, err 71 | } 72 | 73 | func getFileName(path string) string { 74 | n := strings.Split(path, "/") 75 | result := strings.Split(n[len(n)-1], ".") 76 | return result[0] 77 | } 78 | 79 | func (kc *KubeConfigOption) handleContexts(oldConfig *clientcmdapi.Config, mc CommandOptions) (*clientcmdapi.Config, error) { 80 | newConfig := clientcmdapi.NewConfig() 81 | for name, ctx := range kc.config.Contexts { 82 | var newName string 83 | if len(kc.config.Contexts) >= 1 { 84 | newName = name 85 | } else { 86 | newName = kc.fileName 87 | } 88 | if checkContextName(newName, oldConfig) && !mc.ForceMerge { 89 | nameConfirm := BoolUI(fmt.Sprintf("「%s」 Name already exists, do you want to rename it. (If you select `False`, this context will not be merged)", newName), mc) 90 | if nameConfirm == "True" { 91 | newName = core.PromptUI("Rename", newName) 92 | if newName == kc.fileName { 93 | return nil, errors.New("need to rename") 94 | } 95 | } else { 96 | continue 97 | } 98 | } 99 | itemConfig := kc.handleContext(oldConfig, newName, ctx) 100 | newConfig = appendConfig(newConfig, itemConfig) 101 | log.Infof("Add Context: %s \n", newName) 102 | } 103 | outConfig := appendConfig(oldConfig, newConfig) 104 | return outConfig, nil 105 | } 106 | func checkContextName(name string, oldConfig *clientcmdapi.Config) bool { 107 | if _, ok := oldConfig.Contexts[name]; ok { 108 | return true 109 | } 110 | return false 111 | } 112 | func BoolUI(label string, mc CommandOptions) string { 113 | templates := &promptui.SelectTemplates{ 114 | Label: "{{ . }}", 115 | Active: "\U0001F37A {{ . | red }}", 116 | Inactive: " {{ . | cyan }}", 117 | Selected: "\U0001F47B {{ . | green }}", 118 | } 119 | prompt := promptui.Select{ 120 | Label: label, 121 | Items: []string{"False", "True"}, 122 | Templates: templates, 123 | Size: mc.UiSize, 124 | } 125 | _, obj, err := prompt.Run() 126 | if err != nil { 127 | log.Fatalf("Prompt failed %v\n", err) 128 | } 129 | return obj 130 | } 131 | 132 | // HashSufString return the string of HashSuf. 133 | func HashSufString(data string) string { 134 | sum, _ := hEncode(Hash(data)) 135 | return sum 136 | } 137 | func (kc *KubeConfigOption) handleContext(oldConfig *clientcmdapi.Config, 138 | name string, ctx *clientcmdapi.Context) *clientcmdapi.Config { 139 | 140 | var ( 141 | clusterNameSuffix string 142 | userNameSuffix string 143 | ) 144 | 145 | isClusterNameExist, isUserNameExist := checkClusterAndUserName(oldConfig, ctx.Cluster, ctx.AuthInfo) 146 | newConfig := clientcmdapi.NewConfig() 147 | suffix := HashSufString(name) 148 | 149 | if isClusterNameExist { 150 | clusterNameSuffix = "-" + suffix 151 | } 152 | if isUserNameExist { 153 | userNameSuffix = "-" + suffix 154 | } 155 | 156 | userName := fmt.Sprintf("%v%v", ctx.AuthInfo, userNameSuffix) 157 | clusterName := fmt.Sprintf("%v%v", ctx.Cluster, clusterNameSuffix) 158 | newCtx := ctx.DeepCopy() 159 | newConfig.AuthInfos[userName] = kc.config.AuthInfos[newCtx.AuthInfo] 160 | newConfig.Clusters[clusterName] = kc.config.Clusters[newCtx.Cluster] 161 | newConfig.Contexts[name] = newCtx 162 | newConfig.Contexts[name].AuthInfo = userName 163 | newConfig.Contexts[name].Cluster = clusterName 164 | 165 | return newConfig 166 | } 167 | func checkClusterAndUserName(oldConfig *clientcmdapi.Config, newClusterName, newUserName string) (bool, bool) { 168 | var ( 169 | isClusterNameExist bool 170 | isUserNameExist bool 171 | ) 172 | 173 | for _, ctx := range oldConfig.Contexts { 174 | if ctx.Cluster == newClusterName { 175 | isClusterNameExist = true 176 | } 177 | if ctx.AuthInfo == newUserName { 178 | isUserNameExist = true 179 | } 180 | } 181 | 182 | return isClusterNameExist, isUserNameExist 183 | } 184 | 185 | // Copied from https://github.com/kubernetes/kubernetes 186 | // /blob/master/pkg/kubectl/util/hash/hash.go 187 | func hEncode(hex string) (string, error) { 188 | if len(hex) < 10 { 189 | return "", fmt.Errorf( 190 | "input length must be at least 10") 191 | } 192 | enc := []rune(hex[:10]) 193 | for i := range enc { 194 | switch enc[i] { 195 | case '0': 196 | enc[i] = 'g' 197 | case '1': 198 | enc[i] = 'h' 199 | case '3': 200 | enc[i] = 'k' 201 | case 'a': 202 | enc[i] = 'm' 203 | case 'e': 204 | enc[i] = 't' 205 | } 206 | } 207 | return string(enc), nil 208 | } 209 | 210 | func appendConfig(c1, c2 *clientcmdapi.Config) *clientcmdapi.Config { 211 | config := clientcmdapi.NewConfig() 212 | _ = mergo.Merge(config, c1) 213 | _ = mergo.Merge(config, c2) 214 | return config 215 | } 216 | 217 | // Hash returns the hex form of the sha256 of the argument. 218 | func Hash(data string) string { 219 | return fmt.Sprintf("%x", sha256.Sum256([]byte(data))) 220 | } 221 | 222 | // Convert merged config to clientcmdapi.Config 223 | func toClientConfig(cfg *Config) (*clientcmdapi.Config, error) { 224 | clientConfig := clientcmdapi.NewConfig() 225 | 226 | // Set API version 227 | clientConfig.APIVersion = cfg.APIVersion 228 | 229 | // Set current context 230 | clientConfig.CurrentContext = cfg.CurrentContext 231 | 232 | // Set clusters 233 | for _, c := range cfg.Clusters { 234 | decodedBytes, err := base64.StdEncoding.DecodeString(c.Cluster.CertificateAuthorityData) 235 | core.FailOnError(err, decodeError) 236 | cluster := clientcmdapi.Cluster{ 237 | Server: c.Cluster.Server, 238 | CertificateAuthorityData: decodedBytes, 239 | } 240 | clientConfig.Clusters[c.Name] = &cluster 241 | } 242 | 243 | // Set users 244 | for _, u := range cfg.Users { 245 | user := getUserForCluster(u) 246 | clientConfig.AuthInfos[u.Name] = &user 247 | } 248 | 249 | // Set contexts 250 | for _, c := range cfg.Contexts { 251 | context := clientcmdapi.Context{ 252 | Cluster: c.Context.Cluster, 253 | AuthInfo: c.Context.User, 254 | } 255 | clientConfig.Contexts[c.Name] = &context 256 | } 257 | 258 | return clientConfig, nil 259 | } 260 | 261 | func getUserForCluster(u Users) clientcmdapi.AuthInfo { 262 | var user clientcmdapi.AuthInfo 263 | if !checkIfStructInit(u.User, "exec") { 264 | clientCertificateDataBytes, err := base64.StdEncoding.DecodeString(u.User.ClientCertificateData) 265 | core.FailOnError(err, decodeError) 266 | ClientKeyDataBytes, err := base64.StdEncoding.DecodeString(u.User.ClientKeyData) 267 | core.FailOnError(err, decodeError) 268 | user = clientcmdapi.AuthInfo{ 269 | ClientCertificateData: []byte(clientCertificateDataBytes), 270 | ClientKeyData: []byte(ClientKeyDataBytes), 271 | Token: u.User.Token, 272 | } 273 | } else { 274 | user = clientcmdapi.AuthInfo{ 275 | Exec: &clientcmdapi.ExecConfig{ 276 | APIVersion: u.User.Exec.APIVersion, 277 | Command: u.User.Exec.Command, 278 | Args: u.User.Exec.Args, 279 | Env: []clientcmdapi.ExecEnvVar{}, 280 | }, 281 | } 282 | envs, _ := u.User.Exec.Env.([]Env) 283 | for _, env := range envs { 284 | user.Exec.Env = append(user.Exec.Env, clientcmdapi.ExecEnvVar{Name: env.Name, Value: env.Value}) 285 | } 286 | } 287 | return user 288 | } 289 | -------------------------------------------------------------------------------- /provider/structs.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | type AwsProfiles struct { 4 | Name string 5 | IsRole bool 6 | Arn string `json:"arn,omitempty"` 7 | ConfProfile string 8 | SessionName string 9 | } 10 | 11 | type CommandOptions struct { 12 | AwsRegion string 13 | Path string 14 | Output string 15 | Overwrite bool 16 | Combined bool 17 | Backup bool 18 | Merge bool 19 | ForceMerge bool 20 | UiSize int 21 | DryRun bool 22 | AwsAuth bool 23 | AwsRoleString string 24 | AwsEnvProfile bool 25 | AwsClusterName bool 26 | ProfileName string 27 | ProfileSelector bool 28 | SaveOutput bool 29 | Validate bool 30 | } 31 | 32 | // Azure /GCP 33 | type subs struct { 34 | Name string `json:"name,omitempty"` 35 | Id string `json:"id,omitempty"` 36 | } 37 | 38 | // Standard of Cluster Info Output 39 | type Cluster struct { 40 | Name string `json:"name,omitempty"` 41 | Version string `json:"version,omitempty"` 42 | Latest string `json:"latest,omitempty"` 43 | Region string `json:"region,omitempty"` 44 | Id string `json:"id,omitempty"` 45 | CluserChannel string `json:"channel,omitempty"` 46 | Status string `json:"status,omitempty"` 47 | } 48 | 49 | type Account struct { 50 | Name string `json:"name,omitempty"` 51 | Clusters []Cluster `json:"clusters,omitempty"` 52 | TotalCount int `json:"totalCount,omitempty"` 53 | Tenanat string `json:"tenant,omitempty"` 54 | } 55 | 56 | type Provider struct { 57 | Provider string `json:"provider,omitempty"` 58 | Accounts []Account `json:"accounts,omitempty"` 59 | TotalCount int `json:"totalCount,omitempty"` 60 | } 61 | 62 | // AWS Kubeconfig 63 | type LocalConfig struct { 64 | Authinfo User `json:"authinfo,omitempty"` 65 | Context Context `json:"context,omitempty"` 66 | Cluster CCluster `json:"cluster,omitempty"` 67 | } 68 | 69 | type AllConfig struct { 70 | auth []Users 71 | context []Contexts 72 | clusters []Clusters 73 | } 74 | 75 | // merged config struct 76 | type Config struct { 77 | APIVersion string `yaml:"apiVersion,omitempty"` 78 | Clusters []Clusters `yaml:"clusters,omitempty"` 79 | Contexts []Contexts `yaml:"contexts,omitempty"` 80 | CurrentContext string `yaml:"current-context,omitempty"` 81 | Kind string `yaml:"kind,omitempty"` 82 | Preferences Preferences `yaml:"preferences,omitempty"` 83 | Users []Users `yaml:"users,omitempty"` 84 | } 85 | 86 | type Clusters struct { 87 | Cluster CCluster `yaml:"cluster,omitempty"` 88 | Name string `yaml:"name,omitempty"` 89 | } 90 | 91 | type CCluster struct { 92 | CertificateAuthorityData string `yaml:"certificate-authority-data,omitempty"` 93 | Server string `yaml:"server,omitempty"` 94 | } 95 | 96 | type Context struct { 97 | Cluster string `yaml:"cluster,omitempty"` 98 | User string `yaml:"user,omitempty"` 99 | } 100 | 101 | type Contexts struct { 102 | Context Context `yaml:"context,omitempty"` 103 | Name string `yaml:"name,omitempty"` 104 | } 105 | 106 | type Preferences struct { 107 | } 108 | 109 | type Exec struct { 110 | APIVersion string `yaml:"apiVersion,omitempty"` 111 | Args []string `yaml:"args,omitempty"` 112 | Command string `yaml:"command,omitempty"` 113 | Env interface{} `yaml:"env,omitempty"` 114 | ProvideClusterInfo bool `yaml:"provideClusterInfo,omitempty"` 115 | } 116 | 117 | type User struct { 118 | Exec Exec `yaml:"exec,omitempty"` 119 | ClientCertificateData string `yaml:"client-certificate-data,omitempty"` 120 | ClientKeyData string `yaml:"client-key-data,omitempty"` 121 | Token string `yaml:"token,omitempty"` 122 | } 123 | 124 | type Users struct { 125 | Name string `yaml:"name,omitempty"` 126 | User User `yaml:"user,omitempty"` 127 | } 128 | 129 | type Env struct { 130 | Name string `yaml:"name,omitempty"` 131 | Value string `yaml:"value,omitempty"` 132 | } 133 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | # ===================================================== 2 | # Standard properties 3 | # ===================================================== 4 | sonar.projectKey=AdamRussak_k8f_AYi6D0uVa-POq_Tc4jQY 5 | sonar.sources=. 6 | sonar.exclusions=**/*_test.go 7 | sonar.tests=. 8 | sonar.test.inclusions=**/*_test.go 9 | # ===================================================== 10 | # Meta-data for the project 11 | # ===================================================== 12 | sonar.links.homepage=https://github.com/AdamRussak/k8f 13 | sonar.links.scm=https://github.com/AdamRussak/k8f 14 | sonar.links.issue=https://github.com/AdamRussak/k8f/issues 15 | 16 | # ===================================================== 17 | # Properties specific to Go 18 | # ===================================================== 19 | # sonar.go.gometalinter.reportPaths=gometalinter-report.out 20 | sonar.go.tests.reportPaths=report.json 21 | sonar.go.coverage.reportPaths=coverage.out -------------------------------------------------------------------------------- /test/config: -------------------------------------------------------------------------------- 1 | [default] 2 | region = eu-west-1 3 | [profile account1] 4 | role_arn = arn:aws:iam::123456789012:role/my-role 5 | source_profile = default 6 | [profile account2] 7 | role_arn = arn:aws:iam::125456389012:role/my-role 8 | source_profile = default 9 | 10 | -------------------------------------------------------------------------------- /test/credentials: -------------------------------------------------------------------------------- 1 | [default] 2 | aws_access_key_id = myKey 3 | aws_secret_access_key = myAccessKey 4 | [account1] 5 | aws_access_key_id = account1Key 6 | aws_secret_access_key = account1AccessKey 7 | [account2] 8 | aws_access_key_id = account2Key 9 | aws_secret_access_key = account2AccessKey 10 | --------------------------------------------------------------------------------