├── .github └── workflows │ └── build.yaml ├── .gitignore ├── .golicense.json ├── LICENSE ├── README.md ├── cmd ├── gtoken-webhook │ ├── .dockerignore │ ├── .golangci.yaml │ ├── Dockerfile │ ├── Makefile │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── main_test.go └── gtoken │ ├── .dockerignore │ ├── .golangci.yaml │ ├── Dockerfile │ ├── Makefile │ ├── go.mod │ ├── go.sum │ ├── internal │ └── gcp │ │ ├── mock_ServiceAccountInfo.go │ │ ├── mock_Token.go │ │ ├── sainfo.go │ │ └── token.go │ ├── main.go │ └── main_test.go └── deployment ├── clusterrole.yaml ├── clusterrolebinding.yaml ├── deployment.yaml ├── mutatingwebhook.yaml ├── namespace.yaml ├── service-account.yaml ├── service.yaml ├── webhook-create-self-signed-cert.sh ├── webhook-create-signed-cert.sh └── webhook-patch-ca-bundle.sh /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | tags: 8 | - '*' 9 | 10 | jobs: 11 | 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v2 20 | 21 | - name: Decide on tag 22 | id: tagger 23 | run: | 24 | tag=$(echo "${{ github.ref }}" | sed -e 's/^refs\/heads\///g' -e 's/^refs\/tags\///g' -e 's/^refs\/pull\///g' -e 's/\/merge$//g' | sed -e 's/master/latest/g') 25 | echo "::set-output name=tag::${tag}" 26 | echo "::debug::docker image tag ${tag}" 27 | 28 | - name: Login to DockerHub 29 | uses: docker/login-action@v1 30 | with: 31 | username: ${{ secrets.DOCKER_ACCOUNT }} 32 | password: ${{ secrets.DOCKER_TOKEN }} 33 | 34 | - name: Set up QEMU 35 | uses: docker/setup-qemu-action@v1 36 | 37 | - name: Set up Docker buildx 38 | id: buildx 39 | uses: docker/setup-buildx-action@v1 40 | 41 | - name: Cache Docker layers 42 | uses: actions/cache@v2 43 | id: cache 44 | with: 45 | path: /tmp/.buildx-cache 46 | key: ${{ runner.os }}-buildx-${{ github.sha }} 47 | restore-keys: | 48 | ${{ runner.os }}-buildx- 49 | 50 | - name: Build and push gtoken Docker image 51 | if: github.event_name != 'pull_request' 52 | uses: docker/build-push-action@v2 53 | with: 54 | context: cmd/gtoken 55 | cache-from: type=local,src=/tmp/.buildx-cache 56 | cache-to: type=local,dest=/tmp/.buildx-cache 57 | push: true 58 | tags: ${{ secrets.DOCKER_REPOSITORY }}/gtoken:${{ steps.tagger.outputs.tag }} 59 | 60 | - name: Build and push gtoken:alpine Docker image 61 | if: github.event_name != 'pull_request' 62 | uses: docker/build-push-action@v2 63 | with: 64 | context: cmd/gtoken 65 | target: certs 66 | cache-from: type=local,src=/tmp/.buildx-cache 67 | cache-to: type=local,dest=/tmp/.buildx-cache 68 | push: true 69 | tags: ${{ secrets.DOCKER_REPOSITORY }}/gtoken:${{ steps.tagger.outputs.tag }}-alpine 70 | 71 | - name: Build and push gtoken-webhook Docker image 72 | if: github.event_name != 'pull_request' 73 | uses: docker/build-push-action@v2 74 | with: 75 | context: cmd/gtoken-webhook 76 | cache-from: type=local,src=/tmp/.buildx-cache 77 | cache-to: type=local,dest=/tmp/.buildx-cache 78 | push: true 79 | tags: ${{ secrets.DOCKER_REPOSITORY }}/gtoken-webhook:${{ steps.tagger.outputs.tag }} 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cover 2 | **/.cover 3 | .git 4 | .gtm 5 | 6 | .idea 7 | **/.vscode 8 | *.code-workspace 9 | 10 | .bin 11 | **/.bin 12 | **/test 13 | **/*-bundle.yaml 14 | .env 15 | 16 | **/.DS_Store 17 | **/debug 18 | **/debug.test 19 | 20 | gcp-trust-policy.json 21 | 22 | -------------------------------------------------------------------------------- /.golicense.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow": ["MIT", "Apache-2.0", "MPL-2.0", "BSD-3-Clause", "BSD-2-Clause", "ISC"], 3 | "deny": ["LGPL-2.0-or-later", "LGPL-3.0-or-later"], 4 | "override": { 5 | "github.com/russross/blackfriday": "BSD-2-Clause", 6 | "sigs.k8s.io/yaml": "MIT", 7 | "github.com/gogo/protobuf": "BSD-3-Clause", 8 | "google.golang.org/api": "BSD-3-Clause" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://github.com/doitintl/gtoken/workflows/Docker%20Image%20CI/badge.svg)](https://github.com/doitintl/gtoken/actions?query=workflow%3A"Docker+Image+CI") [![Docker Pulls](https://img.shields.io/docker/pulls/doitintl/gtoken.svg?style=popout)](https://hub.docker.com/r/doitintl/gtoken "gtoken image") [![Docker Pulls](https://img.shields.io/docker/pulls/doitintl/gtoken-webhook.svg?style=popout)](https://hub.docker.com/r/doitintl/gtoken-webhook "gtoken-webhook image") [![](https://images.microbadger.com/badges/image/doitintl/gtoken.svg)](https://microbadger.com/images/doitintl/gtoken "gtoken image") [![](https://images.microbadger.com/badges/image/doitintl/gtoken-webhook.svg)](https://microbadger.com/images/doitintl/gtoken-webhook "gtoken-webhook image") 2 | 3 | # Securely access AWS Services from GKE cluster 4 | 5 | Ever wanted to access AWS services from Google Kubernetes cluster (GKE) without using AWS IAM credentials? 6 | 7 | This solution can help you to get and exchange Google OIDC token for temporary AWS IAM security credentials are generated by AWS STS service. This approach allows you to access AWS services form a GKE cluster without pre-generated long-living AWS credentials. 8 | 9 | Read more about this solution on DoiT [Securely Access AWS Services from Google Kubernetes Engine (GKE)](https://blog.doit-intl.com/securely-access-aws-from-gke-dba1c6dbccba?source=friends_link&sk=779821ca975ddb312916e1be732c637f) blog post. 10 | 11 | # `gtoken` tool 12 | 13 | The `gtoken` tool can get Google Cloud ID token when running with under GCP Service Account (for example, GKE Pod with Workload Identity). 14 | 15 | ## `gtoken` command syntax 16 | 17 | ```text 18 | NAME: 19 | gtoken - generate ID token with current Google Cloud service account 20 | 21 | USAGE: 22 | gtoken [global options] command [command options] [arguments...] 23 | 24 | COMMANDS: 25 | help, h Shows a list of commands or help for one command 26 | 27 | GLOBAL OPTIONS: 28 | --refresh auto refresh ID token before it expires (default: true) 29 | --file value write ID token into file (stdout, if not specified) 30 | --help, -h show help (default: false) 31 | --version, -v print the version 32 | ``` 33 | 34 | # `gtoken-webhook` Kubernetes webhook 35 | 36 | The `gtoken-webhook` is a Kubernetes mutating admission webhook, that mutates any K8s Pod running under specially annotated Kubernetes Service Account (see details below). 37 | 38 | ## `gtoken-webhook` mutation 39 | 40 | The `gtoken-webhook` injects a `gtoken` `initContainer` into a target Pod and an additional `gtoken` sidekick container (to refresh an ID OIDC token a moment before expiration), mounts _token volume_ and injects three AWS-specific environment variables. The `gtoken` container generates a valid GCP OIDC ID Token and writes it to the _token volume_. 41 | 42 | Injected AWS environment variables: 43 | 44 | - `AWS_WEB_IDENTITY_TOKEN_FILE` - the path to the web identity token file (OIDC ID token) 45 | - `AWS_ROLE_ARN` - the ARN of the role to assume by Pod containers 46 | - `AWS_ROLE_SESSION_NAME` - the name applied to this assume-role session 47 | 48 | The AWS SDK will automatically make the corresponding `AssumeRoleWithWebIdentity` calls to AWS STS on your behalf. It will handle in memory caching as well as refreshing credentials as needed. 49 | 50 | ### skip injection 51 | 52 | The `gtoken-webhook` can be configured to skip injection for all Pods in the specific Namespace by adding the `admission.gtoken/ignore` label to the Namespace. 53 | 54 | ## `gtoken-webhook` deployment 55 | 56 | 1. Create a new `gtoken` namespace: 57 | 58 | ```sh 59 | kubectl create -f deployment/namespace.yaml 60 | ``` 61 | 62 | ``` 63 | 64 | 1. To deploy the `gtoken-webhook` server, we need to create a webhook service and a deployment in our Kubernetes cluster. It’s pretty straightforward, except one thing, which is the server’s TLS configuration. If you’d care to examine the [deployment.yaml](https://github.com/doitintl/gtoken/blob/master/deployment/deployment.yaml) file, you’ll find that the certificate and corresponding private key files are read from command line arguments, and that the path to these files comes from a volume mount that points to a Kubernetes secret: 65 | 66 | ```yaml 67 | [...] 68 | args: 69 | [...] 70 | - --tls-cert-file=/etc/webhook/certs/cert.pem 71 | - --tls-private-key-file=/etc/webhook/certs/key.pem 72 | volumeMounts: 73 | - name: webhook-certs 74 | mountPath: /etc/webhook/certs 75 | readOnly: true 76 | [...] 77 | volumes: 78 | - name: webhook-certs 79 | secret: 80 | secretName: gtoken-webhook-certs 81 | ``` 82 | 83 | The most important thing to remember is to set the corresponding CA certificate later in the webhook configuration, so the `apiserver` will know that it should be accepted. For now, we’ll reuse the script originally written by the Istio team to generate a certificate signing request. Then we’ll send the request to the Kubernetes API, fetch the certificate, and create the required secret from the result. 84 | 85 | First, run [webhook-create-signed-cert.sh](https://github.com/doitintl/gtoken/blob/master/deployment/webhook-create-signed-cert.sh) script and check if the secret holding the certificate and key has been created: 86 | 87 | ```text 88 | ./deployment/webhook-create-signed-cert.sh 89 | 90 | creating certs in tmpdir /var/folders/vl/gxsw2kf13jsf7s8xrqzcybb00000gp/T/tmp.xsatrckI71 91 | Generating RSA private key, 2048 bit long modulus 92 | .........................+++ 93 | ....................+++ 94 | e is 65537 (0x10001) 95 | certificatesigningrequest.certificates.k8s.io/gtoken-webhook-svc.gtoken created 96 | NAME AGE REQUESTOR CONDITION 97 | gtoken-webhook-svc.gtoken 1s alexei@doit-intl.com Pending 98 | certificatesigningrequest.certificates.k8s.io/gtoken-webhook-svc.gtoken approved 99 | secret/gtoken-webhook-certs configured 100 | ``` 101 | 102 | **Note** For the GKE Autopilot, run the [webhook-create-self-signed-cert.sh](https://github.com/doitintl/gtoken/blob/master/deployment/webhook-create-self-signed-cert.sh) script to generate a self-signed certificate. 103 | 104 | Export CA Bundle as environment variable: 105 | 106 | ```sh 107 | export CA_BUNDLE=[output value of the previous script "Encoded CA:"] 108 | ``` 109 | 110 | Then, we’ll create the webhook service and deployment. 111 | 112 | First, create a Kubernetes Service Account to be used with the `gtoken-webhook`: 113 | 114 | ```sh 115 | kubectl create -f deployment/service-account.yaml 116 | ``` 117 | 118 | Once the secret is created, we can create deployment and service. These are standard Kubernetes deployment and service resources. Up until this point we’ve produced nothing but an HTTP server that’s accepting requests through a service on port 443: 119 | 120 | ```sh 121 | kubectl create -f deployment/deployment.yaml 122 | 123 | kubectl create -f deployment/service.yaml 124 | ``` 125 | 126 | ### configure mutating admission webhook 127 | 128 | Now that our webhook server is running, it can accept requests from the `apiserver`. However, we should create some configuration resources in Kubernetes first. Let’s start with our validating webhook, then we’ll configure the mutating webhook later. If you take a look at the [webhook configuration](https://github.com/doitintl/gtoken/blob/master/deployment/mutatingwebhook.yaml), you’ll notice that it contains a placeholder for `CA_BUNDLE`: 129 | 130 | ```yaml 131 | [...] 132 | service: 133 | name: gtoken-webhook-svc 134 | namespace: gtoken 135 | path: "/pods" 136 | caBundle: ${CA_BUNDLE} 137 | [...] 138 | ``` 139 | 140 | There is a [small script](https://github.com/doitintl/gtoken/blob/master/deployment/webhook-patch-ca-bundle.sh) that substitutes the CA_BUNDLE placeholder in the configuration with this CA. Run this command before creating the validating webhook configuration: 141 | 142 | ```sh 143 | cat ./deployment/mutatingwebhook.yaml | ./deployment/webhook-patch-ca-bundle.sh > ./deployment/mutatingwebhook-bundle.yaml 144 | ``` 145 | 146 | Create mutating webhook configuration: 147 | 148 | ```sh 149 | kubectl create -f deployment/mutatingwebhook-bundle.yaml 150 | ``` 151 | 152 | ### configure RBAC for gtoken-webhook 153 | 154 | Define RBAC permission for webhook service account: 155 | 156 | ```sh 157 | # create a cluster role 158 | kubectl create -f deployment/clusterrole.yaml 159 | # define a cluster role binding 160 | kubectl create -f deployment/clusterrolebinding.yaml 161 | ``` 162 | 163 | ## Configuration Flow 164 | 165 | ### Flow variables 166 | 167 | - `PROJECT_ID` - GCP project ID 168 | - `CLUSTER_NAME` - GKE cluster name 169 | - `CLUSTER_ZONE` - GKE cluster zone 170 | - `GSA_NAME` - Google Cloud Service Account name (choose any) 171 | - `GSA_ID` - Google Cloud Service Account unique ID (generated by Google) 172 | - `KSA_NAME` - Kubernetes Service Account name (choose any) 173 | - `KSA_NAMESPACE` - Kubernetes namespace 174 | - `AWS_ROLE_NAME` - AWS IAM role name (choose any) 175 | - `AWS_POLICY_NAME` - an **existing** AWS IAM policy to assign to IAM role 176 | - `AWS_ROLE_ARN` - AWS IAM Role ARN identifier (generated by AWS) 177 | 178 | ### GCP: Enable GKE Workload Identity 179 | 180 | Create a new GKE cluster with [Workload Identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) enabled: 181 | 182 | ```sh 183 | gcloud container clusters create ${CLUSTER_NAME} \ 184 | --zone=${CLUSTER_ZONE} \ 185 | --workload-pool=${PROJECT_ID}.svc.id.goog 186 | ``` 187 | 188 | or update an existing cluster: 189 | 190 | ```sh 191 | gcloud container clusters update ${CLUSTER_NAME} \ 192 | --zone=${CLUSTER_ZONE} \ 193 | --workload-pool=${PROJECT_ID}.svc.id.goog 194 | ``` 195 | 196 | ### GCP: Configure GCP Service Account 197 | 198 | Create Google Cloud Service Account: 199 | 200 | ```sh 201 | # create GCP Service Account 202 | gcloud iam service-accounts create ${GSA_NAME} 203 | 204 | # get GCP SA UID to be used for AWS Role with Google OIDC Web Identity 205 | GSA_ID=$(gcloud iam service-accounts describe --format json ${GSA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com | jq -r '.uniqueId') 206 | ``` 207 | 208 | Update `GSA_NAME` Google Service Account with following roles: 209 | 210 | - `roles/iam.workloadIdentityUser` - impersonate service accounts from GKE Workloads 211 | - `roles/iam.serviceAccountTokenCreator` - impersonate service accounts to create OAuth2 access tokens, sign blobs, or sign JWTs 212 | 213 | ```sh 214 | gcloud projects add-iam-policy-binding ${PROJECT_ID} \ 215 | --member serviceAccount:${GSA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com \ 216 | --role roles/iam.serviceAccountTokenCreator 217 | 218 | gcloud iam service-accounts add-iam-policy-binding \ 219 | --role roles/iam.workloadIdentityUser \ 220 | --member "serviceAccount:${PROJECT_ID}.svc.id.goog[${K8S_NAMESPACE}/${KSA_NAME}]" \ 221 | ${GSA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com 222 | ``` 223 | 224 | ### AWS: Create AWS IAM Role with Google OIDC Web Identity 225 | 226 | ```sh 227 | # prepare role trust policy document for Google OIDC provider 228 | cat > gcp-trust-policy.json << EOF 229 | { 230 | "Version": "2012-10-17", 231 | "Statement": [ 232 | { 233 | "Effect": "Allow", 234 | "Principal": { 235 | "Federated": "accounts.google.com" 236 | }, 237 | "Action": "sts:AssumeRoleWithWebIdentity", 238 | "Condition": { 239 | "StringEquals": { 240 | "accounts.google.com:sub": "${GSA_ID}" 241 | } 242 | } 243 | } 244 | ] 245 | } 246 | EOF 247 | 248 | # create AWS IAM Rome with Google Web Identity 249 | aws iam create-role --role-name ${AWS_ROLE_NAME} --assume-role-policy-document file://gcp-trust-policy.json 250 | 251 | # assign AWS role desired policies 252 | aws iam attach-role-policy --role-name ${AWS_ROLE_NAME} --policy-arn arn:aws:iam::aws:policy/${AWS_POLICY_NAME} 253 | 254 | # get AWS Role ARN to be used in K8s SA annotation 255 | AWS_ROLE_ARN=$(aws iam get-role --role-name ${AWS_ROLE_NAME} --query Role.Arn --output text) 256 | ``` 257 | 258 | ### GKE: Kubernetes Service Account 259 | 260 | Create K8s namespace: 261 | 262 | ```sh 263 | kubectl create namespace ${K8S_NAMESPACE} 264 | ``` 265 | 266 | Create K8s Service Account: 267 | 268 | ```sh 269 | kubectl create serviceaccount --namespace ${K8S_NAMESPACE} ${KSA_NAME} 270 | ``` 271 | 272 | Annotate K8s Service Account with GKE Workload Identity (GCP Service Account email) 273 | 274 | ```sh 275 | kubectl annotate serviceaccount --namespace ${K8S_NAMESPACE} ${KSA_NAME} \ 276 | iam.gke.io/gcp-service-account=${GSA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com 277 | 278 | ``` 279 | 280 | Annotate K8s Service Account with AWS Role ARN: 281 | 282 | ```sh 283 | kubectl annotate serviceaccount --namespace ${K8S_NAMESPACE} ${KSA_NAME} \ 284 | amazonaws.com/role-arn=${AWS_ROLE_ARN} 285 | ``` 286 | 287 | ### Run demo 288 | 289 | Run a new K8s Pod with K8s ${KSA_NAME} Service Account: 290 | 291 | ```sh 292 | # run a pod (with AWS CLI onboard) in interactive mod 293 | cat << EOF | kubectl apply -f - 294 | apiVersion: v1 295 | kind: Pod 296 | metadata: 297 | name: test-pod 298 | namespace: ${K8S_NAMESPACE} 299 | spec: 300 | serviceAccountName: ${KSA_NAME} 301 | containers: 302 | - name: test-pod 303 | image: mikesir87/aws-cli 304 | command: ["tail", "-f", "/dev/null"] 305 | EOF 306 | 307 | # in Pod shell: check AWS assumed role 308 | aws sts get-caller-identity 309 | 310 | # the output should look similar to below 311 | { 312 | "UserId": "AROA9GB4GPRFFXVHNSLCK:gtoken-webhook-gyaashbbeeqhpvfw", 313 | "Account": "906385953612", 314 | "Arn": "arn:aws:sts::906385953612:assumed-role/bucket-full-gtoken/gtoken-webhook-gyaashbbeeqhpvfw" 315 | } 316 | 317 | ``` 318 | 319 | ## External references 320 | 321 | I've borrowed an initial mutating admission webhook code and deployment guide from [banzaicloud/admission-webhook-example](https://github.com/banzaicloud/admission-webhook-example) repository. Big thanks to Banzai Cloud team! 322 | -------------------------------------------------------------------------------- /cmd/gtoken-webhook/.dockerignore: -------------------------------------------------------------------------------- 1 | # ide 2 | .vscode 3 | 4 | # binaries 5 | .bin 6 | 7 | # test folders 8 | .cover 9 | **/test/coverage* 10 | 11 | # env exposure 12 | .env 13 | 14 | # text files 15 | *.md 16 | *.todo 17 | docs 18 | 19 | Dockerfile 20 | -------------------------------------------------------------------------------- /cmd/gtoken-webhook/.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | # which dirs to skip 3 | skip-dirs: 4 | - mocks 5 | # Timeout for analysis, e.g. 30s, 5m. 6 | # Default: 1m 7 | timeout: 5m 8 | # Exit code when at least one issue was found. 9 | # Default: 1 10 | issues-exit-code: 2 11 | # Include test files or not. 12 | # Default: true 13 | tests: false 14 | 15 | 16 | linters-settings: 17 | govet: 18 | check-shadowing: true 19 | golint: 20 | min-confidence: 0 21 | gocyclo: 22 | min-complexity: 15 23 | maligned: 24 | suggest-new: true 25 | dupl: 26 | threshold: 100 27 | goconst: 28 | min-len: 2 29 | min-occurrences: 2 30 | misspell: 31 | locale: US 32 | lll: 33 | line-length: 160 34 | goimports: 35 | local-prefixes: github.com/golangci/golangci-lint 36 | gocritic: 37 | enabled-tags: 38 | - diagnostic 39 | - experimental 40 | - opinionated 41 | - performance 42 | - style 43 | disabled-checks: 44 | - dupImport # https://github.com/go-critic/go-critic/issues/845 45 | - ifElseChain 46 | - octalLiteral 47 | - rangeValCopy 48 | - unnamedResult 49 | - whyNoLint 50 | - wrapperFunc 51 | funlen: 52 | lines: 100 53 | statements: 50 54 | 55 | linters: 56 | # please, do not use `enable-all`: it's deprecated and will be removed soon. 57 | # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint 58 | disable-all: true 59 | enable: 60 | # - rowserrcheck 61 | - bodyclose 62 | - deadcode 63 | - depguard 64 | - dogsled 65 | - dupl 66 | - errcheck 67 | - funlen 68 | - goconst 69 | - gocritic 70 | - gocyclo 71 | - gofmt 72 | - goimports 73 | - golint 74 | - gosec 75 | - gosimple 76 | - govet 77 | - ineffassign 78 | - interfacer 79 | - lll 80 | - misspell 81 | - nakedret 82 | - scopelint 83 | - staticcheck 84 | - structcheck 85 | - stylecheck 86 | - typecheck 87 | - unconvert 88 | - unparam 89 | - unused 90 | - varcheck 91 | - whitespace 92 | 93 | # don't enable: 94 | # - gochecknoglobals 95 | # - gocognit 96 | # - godox 97 | # - maligned 98 | # - prealloc 99 | 100 | issues: 101 | exclude: 102 | - Using the variable on range scope `tt` in function literal -------------------------------------------------------------------------------- /cmd/gtoken-webhook/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:experimental 2 | 3 | # 4 | # ----- Go Builder Image ------ 5 | # 6 | FROM golang:1.17-alpine AS builder 7 | 8 | # curl git bash 9 | RUN apk add --no-cache curl git bash make 10 | COPY --from=golangci/golangci-lint:v1.45-alpine /usr/bin/golangci-lint /usr/bin 11 | 12 | # 13 | # ----- Build and Test Image ----- 14 | # 15 | FROM builder as build 16 | 17 | # set working directorydoc 18 | RUN mkdir -p /go/src/gtoken-webhook 19 | WORKDIR /go/src/gtoken-webhook 20 | 21 | # load dependency 22 | COPY go.mod . 23 | COPY go.sum . 24 | RUN --mount=type=cache,target=/go/mod go mod download 25 | 26 | # copy sources 27 | COPY . . 28 | 29 | # build 30 | RUN make 31 | 32 | # 33 | # ------ get latest CA certificates 34 | # 35 | FROM alpine:3.15 as certs 36 | RUN apk --update add ca-certificates 37 | 38 | 39 | # 40 | # ------ gtoken release Docker image ------ 41 | # 42 | FROM scratch 43 | 44 | # copy CA certificates 45 | COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 46 | 47 | # this is the last commabd since it's never cached 48 | COPY --from=build /go/src/gtoken-webhook/.bin/github.com/doitintl/gtoken-webhook /gtoken-webhook 49 | 50 | ENTRYPOINT ["/gtoken-webhook"] -------------------------------------------------------------------------------- /cmd/gtoken-webhook/Makefile: -------------------------------------------------------------------------------- 1 | MODULE = $(shell env GO111MODULE=on $(GO) list -m) 2 | DATE ?= $(shell date +%FT%T%z) 3 | VERSION ?= $(shell git describe --tags --always --dirty --match="v*" 2> /dev/null || \ 4 | cat $(CURDIR)/.version 2> /dev/null || echo v0) 5 | PKGS = $(or $(PKG),$(shell env GO111MODULE=on $(GO) list ./...)) 6 | TESTPKGS = $(shell env GO111MODULE=on $(GO) list -f \ 7 | '{{ if or .TestGoFiles .XTestGoFiles }}{{ .ImportPath }}{{ end }}' \ 8 | $(PKGS)) 9 | BIN = $(CURDIR)/.bin 10 | GOLANGCI_LINT_CONFIG = $(CURDIR)/.golangci.yaml 11 | 12 | GO = go 13 | GOLANGCI_LINT = golangci-lint 14 | TIMEOUT = 15 15 | V = 0 16 | Q = $(if $(filter 1,$V),,@) 17 | M = $(shell printf "\033[34;1m▶\033[0m") 18 | 19 | export GO111MODULE=on 20 | export CGO_ENABLED=0 21 | export GOPROXY=https://proxy.golang.org 22 | 23 | .PHONY: all 24 | all: fmt lint test | $(BIN) ; $(info $(M) building executable…) @ ## Build program binary 25 | $Q $(GO) build \ 26 | -tags release \ 27 | -ldflags '-X main.Version=$(VERSION) -X main.BuildDate=$(DATE)' \ 28 | -o $(BIN)/$(basename $(MODULE)) main.go 29 | 30 | # Tools 31 | 32 | $(BIN): 33 | @mkdir -p $@ 34 | $(BIN)/%: | $(BIN) ; $(info $(M) building $(PACKAGE)…) 35 | $Q tmp=$$(mktemp -d); \ 36 | env GO111MODULE=off GOPATH=$$tmp GOBIN=$(BIN) $(GO) get $(PACKAGE) \ 37 | || ret=$$?; \ 38 | rm -rf $$tmp ; exit $$ret 39 | 40 | GOCOV = $(BIN)/gocov 41 | $(BIN)/gocov: PACKAGE=github.com/axw/gocov/... 42 | 43 | GOCOVXML = $(BIN)/gocov-xml 44 | $(BIN)/gocov-xml: PACKAGE=github.com/AlekSi/gocov-xml 45 | 46 | GO2XUNIT = $(BIN)/go2xunit 47 | $(BIN)/go2xunit: PACKAGE=github.com/tebeka/go2xunit 48 | 49 | # Tests 50 | 51 | TEST_TARGETS := test-default test-bench test-short test-verbose test-race 52 | .PHONY: $(TEST_TARGETS) test-xml check test tests 53 | test-bench: ARGS=-run=__absolutelynothing__ -bench=. ## Run benchmarks 54 | test-short: ARGS=-short ## Run only short tests 55 | test-verbose: ARGS=-v ## Run tests in verbose mode with coverage reporting 56 | test-race: ARGS=-race ## Run tests with race detector 57 | $(TEST_TARGETS): NAME=$(MAKECMDGOALS:test-%=%) 58 | $(TEST_TARGETS): test 59 | check test tests: fmt lint ; $(info $(M) running $(NAME:%=% )tests…) @ ## Run tests 60 | $Q $(GO) test -timeout $(TIMEOUT)s $(ARGS) $(TESTPKGS) 61 | 62 | test-xml: fmt lint | $(GO2XUNIT) ; $(info $(M) running xUnit tests…) @ ## Run tests with xUnit output 63 | $Q mkdir -p test 64 | $Q 2>&1 $(GO) test -timeout $(TIMEOUT)s -v $(TESTPKGS) | tee test/tests.output 65 | $(GO2XUNIT) -fail -input test/tests.output -output test/tests.xml 66 | 67 | COVERAGE_MODE = atomic 68 | COVERAGE_PROFILE = $(COVERAGE_DIR)/profile.out 69 | COVERAGE_XML = $(COVERAGE_DIR)/coverage.xml 70 | COVERAGE_HTML = $(COVERAGE_DIR)/index.html 71 | .PHONY: test-coverage test-coverage-tools 72 | test-coverage-tools: | $(GOCOV) $(GOCOVXML) 73 | test-coverage: COVERAGE_DIR := $(CURDIR)/test/coverage.$(shell date -u +"%Y-%m-%dT%H:%M:%SZ") 74 | test-coverage: fmt lint test-coverage-tools ; $(info $(M) running coverage tests…) @ ## Run coverage tests 75 | $Q mkdir -p $(COVERAGE_DIR) 76 | $Q $(GO) test \ 77 | -coverpkg=$$($(GO) list -f '{{ join .Deps "\n" }}' $(TESTPKGS) | \ 78 | grep '^$(MODULE)/' | \ 79 | tr '\n' ',' | sed 's/,$$//') \ 80 | -covermode=$(COVERAGE_MODE) \ 81 | -coverprofile="$(COVERAGE_PROFILE)" $(TESTPKGS) 82 | $Q $(GO) tool cover -html=$(COVERAGE_PROFILE) -o $(COVERAGE_HTML) 83 | $Q $(GOCOV) convert $(COVERAGE_PROFILE) | $(GOCOVXML) > $(COVERAGE_XML) 84 | 85 | .PHONY: lint 86 | lint: | $(info $(M) running golangci-lint…) ## Run golangci-lint 87 | $Q $(GOLANGCI_LINT) run -v -c $(GOLANGCI_LINT_CONFIG) . 88 | 89 | .PHONY: fmt 90 | fmt: ; $(info $(M) running gofmt…) @ ## Run gofmt on all source files 91 | $Q $(GO) fmt $(PKGS) 92 | 93 | # Misc 94 | 95 | .PHONY: clean 96 | clean: ; $(info $(M) cleaning…) @ ## Cleanup everything 97 | @rm -rf $(BIN) 98 | @rm -rf test/tests.* test/coverage.* 99 | 100 | .PHONY: help 101 | help: 102 | @grep -E '^[ a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ 103 | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' 104 | 105 | .PHONY: version 106 | version: 107 | @echo $(VERSION) 108 | -------------------------------------------------------------------------------- /cmd/gtoken-webhook/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/doitintl/gtoken-webhook 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/google/go-cmp v0.5.7 7 | github.com/pkg/errors v0.9.1 8 | github.com/prometheus/client_golang v1.12.1 9 | github.com/sirupsen/logrus v1.8.1 10 | github.com/slok/kubewebhook/v2 v2.0.0 11 | github.com/urfave/cli v1.22.2 12 | k8s.io/api v0.23.5 13 | k8s.io/apimachinery v0.23.5 14 | k8s.io/client-go v0.23.5 15 | sigs.k8s.io/controller-runtime v0.11.1 16 | ) 17 | 18 | require ( 19 | github.com/beorn7/perks v1.0.1 // indirect 20 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 21 | github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect 22 | github.com/davecgh/go-spew v1.1.1 // indirect 23 | github.com/evanphx/json-patch v4.12.0+incompatible // indirect 24 | github.com/go-logr/logr v1.2.0 // indirect 25 | github.com/gogo/protobuf v1.3.2 // indirect 26 | github.com/golang/protobuf v1.5.2 // indirect 27 | github.com/google/gofuzz v1.1.0 // indirect 28 | github.com/googleapis/gnostic v0.5.5 // indirect 29 | github.com/imdario/mergo v0.3.12 // indirect 30 | github.com/json-iterator/go v1.1.12 // indirect 31 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 32 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 33 | github.com/modern-go/reflect2 v1.0.2 // indirect 34 | github.com/prometheus/client_model v0.2.0 // indirect 35 | github.com/prometheus/common v0.32.1 // indirect 36 | github.com/prometheus/procfs v0.7.3 // indirect 37 | github.com/russross/blackfriday/v2 v2.0.1 // indirect 38 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 39 | github.com/spf13/pflag v1.0.5 // indirect 40 | golang.org/x/net v0.0.0-20220325170049-de3da57026de // indirect 41 | golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a // indirect 42 | golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f // indirect 43 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect 44 | golang.org/x/text v0.3.7 // indirect 45 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect 46 | gomodules.xyz/jsonpatch/v3 v3.0.1 // indirect 47 | gomodules.xyz/orderedmap v0.1.0 // indirect 48 | google.golang.org/appengine v1.6.7 // indirect 49 | google.golang.org/protobuf v1.28.0 // indirect 50 | gopkg.in/inf.v0 v0.9.1 // indirect 51 | gopkg.in/yaml.v2 v2.4.0 // indirect 52 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 53 | k8s.io/klog/v2 v2.30.0 // indirect 54 | k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect 55 | k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect 56 | sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect 57 | sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect 58 | sigs.k8s.io/yaml v1.3.0 // indirect 59 | ) 60 | -------------------------------------------------------------------------------- /cmd/gtoken-webhook/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "net/http" 8 | "os" 9 | "runtime" 10 | "strings" 11 | "time" 12 | 13 | "github.com/pkg/errors" 14 | "github.com/prometheus/client_golang/prometheus" 15 | "github.com/prometheus/client_golang/prometheus/promhttp" 16 | log "github.com/sirupsen/logrus" 17 | whhttp "github.com/slok/kubewebhook/v2/pkg/http" 18 | whlogrus "github.com/slok/kubewebhook/v2/pkg/log/logrus" 19 | metrics "github.com/slok/kubewebhook/v2/pkg/metrics/prometheus" 20 | whmodel "github.com/slok/kubewebhook/v2/pkg/model" 21 | wh "github.com/slok/kubewebhook/v2/pkg/webhook" 22 | "github.com/slok/kubewebhook/v2/pkg/webhook/mutating" 23 | "github.com/urfave/cli" 24 | corev1 "k8s.io/api/core/v1" 25 | "k8s.io/apimachinery/pkg/api/resource" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | "k8s.io/client-go/kubernetes" 28 | kubernetesConfig "sigs.k8s.io/controller-runtime/pkg/client/config" 29 | ) 30 | 31 | /* #nosec */ 32 | const ( 33 | // secretsInitContainer is the default gtoken container from which to pull the 'gtoken' binary. 34 | gtokenInitImage = "doitintl/gtoken:latest" 35 | 36 | // tokenVolumeName is the name of the volume where the generated id token will be stored 37 | tokenVolumeName = "gtoken-volume" 38 | 39 | // tokenVolumePath is the mount path where the generated id token will be stored 40 | tokenVolumePath = "/var/run/secrets/aws/token" 41 | 42 | // token file name 43 | tokenFileName = "gtoken" 44 | 45 | // AWS annotation key; used to annotate Kubernetes Service Account with AWS Role ARN 46 | awsRoleArnKey = "amazonaws.com/role-arn" 47 | 48 | // AWS Web Identity Token ENV 49 | awsWebIdentityTokenFile = "AWS_WEB_IDENTITY_TOKEN_FILE" 50 | awsRoleArn = "AWS_ROLE_ARN" 51 | awsRoleSessionName = "AWS_ROLE_SESSION_NAME" 52 | ) 53 | 54 | var ( 55 | // Version contains the current version. 56 | Version = "dev" 57 | // BuildDate contains a string with the build date. 58 | BuildDate = "unknown" 59 | // test mode 60 | testMode = false 61 | ) 62 | 63 | const ( 64 | requestsCPU = "5m" 65 | requestsMemory = "10Mi" 66 | limitsCPU = "20m" 67 | limitsMemory = "50Mi" 68 | ) 69 | 70 | type mutatingWebhook struct { 71 | k8sClient kubernetes.Interface 72 | image string 73 | pullPolicy string 74 | volumeName string 75 | volumePath string 76 | tokenFile string 77 | } 78 | 79 | var logger *log.Logger 80 | 81 | // Returns an int >= min, < max 82 | func randomInt(min, max int) int { 83 | //nolint:gosec 84 | return min + rand.Intn(max-min) 85 | } 86 | 87 | // Generate a random string of a-z chars with len = l 88 | func randomString(l int) string { 89 | if testMode { 90 | return strings.Repeat("0", 16) 91 | } 92 | rand.Seed(time.Now().UnixNano()) 93 | bytes := make([]byte, l) 94 | for i := 0; i < l; i++ { 95 | bytes[i] = byte(randomInt(97, 122)) 96 | } 97 | return string(bytes) 98 | } 99 | 100 | func newK8SClient() (kubernetes.Interface, error) { 101 | kubeConfig, err := kubernetesConfig.GetConfig() 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | return kubernetes.NewForConfig(kubeConfig) 107 | } 108 | 109 | func healthzHandler(w http.ResponseWriter, r *http.Request) { 110 | w.WriteHeader(200) 111 | } 112 | 113 | func serveMetrics(addr string) { 114 | logger.Infof("Telemetry on http://%s", addr) 115 | 116 | mux := http.NewServeMux() 117 | mux.Handle("/metrics", promhttp.Handler()) 118 | err := http.ListenAndServe(addr, mux) 119 | if err != nil { 120 | logger.WithError(err).Fatal("error serving telemetry") 121 | } 122 | } 123 | 124 | func handlerFor(config mutating.WebhookConfig, recorder wh.MetricsRecorder, logger *log.Logger) http.Handler { 125 | webhook, err := mutating.NewWebhook(config) 126 | if err != nil { 127 | logger.WithError(err).Fatal("error creating webhook") 128 | } 129 | 130 | measuredWebhook := wh.NewMeasuredWebhook(recorder, webhook) 131 | 132 | handler, err := whhttp.HandlerFor(whhttp.HandlerConfig{ 133 | Webhook: measuredWebhook, 134 | Logger: whlogrus.NewLogrus(log.NewEntry(logger)), 135 | }) 136 | if err != nil { 137 | logger.WithError(err).Fatalf("error creating webhook") 138 | } 139 | 140 | return handler 141 | } 142 | 143 | // check if K8s Service Account is annotated with AWS role 144 | func (mw *mutatingWebhook) getAwsRoleArn(ctx context.Context, name, ns string) (string, bool, error) { 145 | sa, err := mw.k8sClient.CoreV1().ServiceAccounts(ns).Get(ctx, name, metav1.GetOptions{}) 146 | if err != nil { 147 | logger.WithFields(log.Fields{"service account": name, "namespace": ns}).WithError(err).Fatalf("error getting service account") 148 | return "", false, err 149 | } 150 | roleArn, ok := sa.GetAnnotations()[awsRoleArnKey] 151 | return roleArn, ok, nil 152 | } 153 | 154 | func (mw *mutatingWebhook) mutateContainers(containers []corev1.Container, roleArn string) bool { 155 | if len(containers) == 0 { 156 | return false 157 | } 158 | for i, container := range containers { 159 | // add token volume mount 160 | container.VolumeMounts = append(container.VolumeMounts, []corev1.VolumeMount{ 161 | { 162 | Name: mw.volumeName, 163 | MountPath: mw.volumePath, 164 | }, 165 | }...) 166 | // add AWS Web Identity Token environment variables to container 167 | container.Env = append(container.Env, []corev1.EnvVar{ 168 | { 169 | Name: awsWebIdentityTokenFile, 170 | Value: fmt.Sprintf("%s/%s", mw.volumePath, mw.tokenFile), 171 | }, 172 | { 173 | Name: awsRoleArn, 174 | Value: roleArn, 175 | }, 176 | { 177 | Name: awsRoleSessionName, 178 | Value: fmt.Sprintf("gtoken-webhook-%s", randomString(16)), 179 | }, 180 | }...) 181 | // update containers 182 | containers[i] = container 183 | } 184 | return true 185 | } 186 | 187 | func (mw *mutatingWebhook) mutatePod(ctx context.Context, pod *corev1.Pod, ns string, dryRun bool) error { 188 | // get service account AWS Role ARN annotation 189 | roleArn, ok, err := mw.getAwsRoleArn(ctx, pod.Spec.ServiceAccountName, ns) 190 | if err != nil { 191 | return err 192 | } 193 | if !ok { 194 | logger.Debug("skipping pods with Service Account without AWS Role ARN annotation") 195 | return nil 196 | } 197 | // mutate Pod init containers 198 | initContainersMutated := mw.mutateContainers(pod.Spec.InitContainers, roleArn) 199 | if initContainersMutated { 200 | logger.Debug("successfully mutated pod init containers") 201 | } else { 202 | logger.Debug("no pod init containers were mutated") 203 | } 204 | // mutate Pod containers 205 | containersMutated := mw.mutateContainers(pod.Spec.Containers, roleArn) 206 | if containersMutated { 207 | logger.Debug("successfully mutated pod containers") 208 | } else { 209 | logger.Debug("no pod containers were mutated") 210 | } 211 | 212 | if (initContainersMutated || containersMutated) && !dryRun { 213 | // prepend gtoken init container (as first in it container) 214 | pod.Spec.InitContainers = append([]corev1.Container{getGtokenContainer("generate-gcp-id-token", 215 | mw.image, mw.pullPolicy, mw.volumeName, mw.volumePath, mw.tokenFile, false)}, pod.Spec.InitContainers...) 216 | logger.Debug("successfully prepended pod init containers to spec") 217 | // append sidekick gtoken update container (as last container) 218 | pod.Spec.Containers = append(pod.Spec.Containers, getGtokenContainer("update-gcp-id-token", 219 | mw.image, mw.pullPolicy, mw.volumeName, mw.volumePath, mw.tokenFile, true)) 220 | logger.Debug("successfully prepended pod sidekick containers to spec") 221 | // append empty gtoken volume 222 | pod.Spec.Volumes = append(pod.Spec.Volumes, getGtokenVolume(mw.volumeName)) 223 | logger.Debug("successfully appended pod spec volumes") 224 | } 225 | 226 | return nil 227 | } 228 | 229 | func getGtokenVolume(volumeName string) corev1.Volume { 230 | return corev1.Volume{ 231 | Name: volumeName, 232 | VolumeSource: corev1.VolumeSource{ 233 | EmptyDir: &corev1.EmptyDirVolumeSource{ 234 | Medium: corev1.StorageMediumMemory, 235 | }, 236 | }, 237 | } 238 | } 239 | 240 | func getGtokenContainer(name, image, pullPolicy, volumeName, volumePath, tokenFile string, 241 | refresh bool) corev1.Container { 242 | return corev1.Container{ 243 | Name: name, 244 | Image: image, 245 | ImagePullPolicy: corev1.PullPolicy(pullPolicy), 246 | Command: []string{"/gtoken", fmt.Sprintf("--file=%s/%s", volumePath, tokenFile), fmt.Sprintf("--refresh=%t", refresh)}, 247 | VolumeMounts: []corev1.VolumeMount{ 248 | { 249 | Name: volumeName, 250 | MountPath: volumePath, 251 | }, 252 | }, 253 | Resources: corev1.ResourceRequirements{ 254 | Requests: corev1.ResourceList{ 255 | corev1.ResourceCPU: resource.MustParse(requestsCPU), 256 | corev1.ResourceMemory: resource.MustParse(requestsMemory), 257 | }, 258 | Limits: corev1.ResourceList{ 259 | corev1.ResourceCPU: resource.MustParse(limitsCPU), 260 | corev1.ResourceMemory: resource.MustParse(limitsMemory), 261 | }, 262 | }, 263 | } 264 | } 265 | 266 | func init() { 267 | logger = log.New() 268 | // set log level 269 | logger.SetLevel(log.WarnLevel) 270 | logger.SetFormatter(&log.TextFormatter{}) 271 | } 272 | 273 | func before(c *cli.Context) error { 274 | // set debug log level 275 | switch level := c.GlobalString("log-level"); level { 276 | case "debug", "DEBUG": 277 | logger.SetLevel(log.DebugLevel) 278 | case "info", "INFO": 279 | logger.SetLevel(log.InfoLevel) 280 | case "warning", "WARNING": 281 | logger.SetLevel(log.WarnLevel) 282 | case "error", "ERROR": 283 | logger.SetLevel(log.ErrorLevel) 284 | case "fatal", "FATAL": 285 | logger.SetLevel(log.FatalLevel) 286 | case "panic", "PANIC": 287 | logger.SetLevel(log.PanicLevel) 288 | default: 289 | logger.SetLevel(log.WarnLevel) 290 | } 291 | // set log formatter to JSON 292 | if c.GlobalBool("json") { 293 | logger.SetFormatter(&log.JSONFormatter{}) 294 | } 295 | return nil 296 | } 297 | 298 | func (mw *mutatingWebhook) podMutator(ctx context.Context, ar *whmodel.AdmissionReview, obj metav1.Object) (*mutating.MutatorResult, error) { 299 | switch v := obj.(type) { 300 | case *corev1.Pod: 301 | err := mw.mutatePod(ctx, v, ar.Namespace, ar.DryRun) 302 | if err != nil { 303 | return nil, errors.Wrapf(err, "failed to mutate pod: %s", v.Name) 304 | } 305 | return &mutating.MutatorResult{MutatedObject: v}, nil 306 | default: 307 | return &mutating.MutatorResult{}, nil 308 | } 309 | } 310 | 311 | // mutation webhook server 312 | func runWebhook(c *cli.Context) error { 313 | k8sClient, err := newK8SClient() 314 | if err != nil { 315 | logger.WithError(err).Fatal("error creating k8s client") 316 | } 317 | 318 | webhook := mutatingWebhook{ 319 | k8sClient: k8sClient, 320 | image: c.String("image"), 321 | pullPolicy: c.String("pull-policy"), 322 | volumeName: c.String("volume-name"), 323 | volumePath: c.String("volume-path"), 324 | tokenFile: c.String("token-file"), 325 | } 326 | 327 | mutator := mutating.MutatorFunc(webhook.podMutator) 328 | metricsRecorder, err := metrics.NewRecorder(metrics.RecorderConfig{ 329 | Registry: prometheus.DefaultRegisterer, 330 | }) 331 | if err != nil { 332 | logger.WithError(err).Fatalf("error creating metrics recorder") 333 | } 334 | 335 | podHandler := handlerFor( 336 | mutating.WebhookConfig{ 337 | ID: "init-gtoken-pods", 338 | Obj: &corev1.Pod{}, 339 | Mutator: mutator, 340 | Logger: whlogrus.NewLogrus(log.NewEntry(logger)), 341 | }, 342 | metricsRecorder, 343 | logger, 344 | ) 345 | 346 | mux := http.NewServeMux() 347 | mux.Handle("/pods", podHandler) 348 | mux.Handle("/healthz", http.HandlerFunc(healthzHandler)) 349 | 350 | telemetryAddress := c.String("telemetry-listen-address") 351 | listenAddress := c.String("listen-address") 352 | tlsCertFile := c.String("tls-cert-file") 353 | tlsPrivateKeyFile := c.String("tls-private-key-file") 354 | 355 | if len(telemetryAddress) > 0 { 356 | // Serving metrics without TLS on separated address 357 | go serveMetrics(telemetryAddress) 358 | } else { 359 | mux.Handle("/metrics", promhttp.Handler()) 360 | } 361 | 362 | if tlsCertFile == "" && tlsPrivateKeyFile == "" { 363 | logger.Infof("listening on http://%s", listenAddress) 364 | err = http.ListenAndServe(listenAddress, mux) 365 | } else { 366 | logger.Infof("listening on https://%s", listenAddress) 367 | err = http.ListenAndServeTLS(listenAddress, tlsCertFile, tlsPrivateKeyFile, mux) 368 | } 369 | 370 | if err != nil { 371 | logger.WithError(err).Fatal("error serving webhook") 372 | } 373 | 374 | return nil 375 | } 376 | 377 | func main() { 378 | cli.VersionPrinter = func(c *cli.Context) { 379 | fmt.Printf("version: %s\n", c.App.Version) 380 | fmt.Printf(" build date: %s\n", BuildDate) 381 | fmt.Printf(" built with: %s\n", runtime.Version()) 382 | } 383 | app := cli.NewApp() 384 | app.Name = "gtoken-webhook" 385 | app.Version = Version 386 | app.Authors = []cli.Author{ 387 | { 388 | Name: "Alexei Ledenev", 389 | Email: "alexei.led@gmail.com", 390 | }, 391 | } 392 | app.Usage = "gtoken-webhook is a Kubernetes mutation controller providing a secure access to AWS services from GKE pods" 393 | app.Before = before 394 | app.Flags = []cli.Flag{ 395 | cli.StringFlag{ 396 | Name: "log-level", 397 | Usage: "set log level (debug, info, warning(*), error, fatal, panic)", 398 | Value: "warning", 399 | EnvVar: "LOG_LEVEL", 400 | }, 401 | cli.BoolFlag{ 402 | Name: "json", 403 | Usage: "produce log in JSON format: Logstash and Splunk friendly", 404 | EnvVar: "LOG_JSON", 405 | }, 406 | } 407 | app.Commands = []cli.Command{ 408 | { 409 | Name: "server", 410 | Flags: []cli.Flag{ 411 | cli.StringFlag{ 412 | Name: "listen-address", 413 | Usage: "webhook server listen address", 414 | Value: ":8443", 415 | }, 416 | cli.StringFlag{ 417 | Name: "telemetry-listen-address", 418 | Usage: "specify a dedicated prometheus metrics listen address (using listen-address, if empty)", 419 | }, 420 | cli.StringFlag{ 421 | Name: "tls-cert-file", 422 | Usage: "TLS certificate file", 423 | }, 424 | cli.StringFlag{ 425 | Name: "tls-private-key-file", 426 | Usage: "TLS private key file", 427 | }, 428 | cli.StringFlag{ 429 | Name: "image", 430 | Usage: "Docker image with secrets-init utility on board", 431 | Value: gtokenInitImage, 432 | }, 433 | cli.StringFlag{ 434 | Name: "pull-policy", 435 | Usage: "Docker image pull policy", 436 | Value: string(corev1.PullIfNotPresent), 437 | }, 438 | cli.StringFlag{ 439 | Name: "volume-name", 440 | Usage: "mount volume name", 441 | Value: tokenVolumeName, 442 | }, 443 | cli.StringFlag{ 444 | Name: "volume-path", 445 | Usage: "mount volume path", 446 | Value: tokenVolumePath, 447 | }, 448 | cli.StringFlag{ 449 | Name: "token-file", 450 | Usage: "token file name", 451 | Value: tokenFileName, 452 | }, 453 | }, 454 | Usage: "mutation admission webhook", 455 | Description: "run mutation admission webhook server", 456 | Action: runWebhook, 457 | }, 458 | } 459 | // print version in debug mode 460 | logger.WithField("version", app.Version).Debug("running gtoken-webhook") 461 | 462 | // run main command 463 | if err := app.Run(os.Args); err != nil { 464 | logger.Fatal(err) 465 | } 466 | } 467 | -------------------------------------------------------------------------------- /cmd/gtoken-webhook/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "strings" 7 | "testing" 8 | 9 | cmp "github.com/google/go-cmp/cmp" 10 | corev1 "k8s.io/api/core/v1" 11 | "k8s.io/apimachinery/pkg/api/resource" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/client-go/kubernetes" 14 | fake "k8s.io/client-go/kubernetes/fake" 15 | ) 16 | 17 | func TestMain(m *testing.M) { 18 | testMode = true 19 | os.Exit(m.Run()) 20 | } 21 | 22 | //nolint:funlen 23 | func Test_mutatingWebhook_mutateContainers(t *testing.T) { 24 | type fields struct { 25 | k8sClient kubernetes.Interface 26 | image string 27 | pullPolicy string 28 | volumeName string 29 | volumePath string 30 | tokenFile string 31 | } 32 | type args struct { 33 | containers []corev1.Container 34 | roleArn string 35 | ns string 36 | } 37 | tests := []struct { 38 | name string 39 | fields fields 40 | args args 41 | mutated bool 42 | wantedContainers []corev1.Container 43 | }{ 44 | { 45 | name: "mutate single container", 46 | fields: fields{ 47 | k8sClient: fake.NewSimpleClientset(), 48 | volumeName: "test-volume-name", 49 | volumePath: "/test-volume-path", 50 | tokenFile: "test-token", 51 | }, 52 | args: args{ 53 | containers: []corev1.Container{ 54 | { 55 | Name: "TestContainer", 56 | Image: "test-image", 57 | }, 58 | }, 59 | roleArn: "arn:aws:iam::123456789012:role/testrole", 60 | ns: "test-namespace", 61 | }, 62 | wantedContainers: []corev1.Container{ 63 | { 64 | Name: "TestContainer", 65 | Image: "test-image", 66 | VolumeMounts: []corev1.VolumeMount{{Name: "test-volume-name", MountPath: "/test-volume-path"}}, 67 | Env: []corev1.EnvVar{ 68 | {Name: awsWebIdentityTokenFile, Value: "/test-volume-path/test-token"}, 69 | {Name: awsRoleArn, Value: "arn:aws:iam::123456789012:role/testrole"}, 70 | {Name: awsRoleSessionName, Value: "gtoken-webhook-" + strings.Repeat("0", 16)}, 71 | }, 72 | }, 73 | }, 74 | mutated: true, 75 | }, 76 | { 77 | name: "mutate multiple container", 78 | fields: fields{ 79 | k8sClient: fake.NewSimpleClientset(), 80 | volumeName: "test-volume-name", 81 | volumePath: "/test-volume-path", 82 | tokenFile: "test-token", 83 | }, 84 | args: args{ 85 | containers: []corev1.Container{ 86 | { 87 | Name: "TestContainer1", 88 | Image: "test-image-1", 89 | }, 90 | { 91 | Name: "TestContainer2", 92 | Image: "test-image-2", 93 | }, 94 | }, 95 | roleArn: "arn:aws:iam::123456789012:role/testrole", 96 | ns: "test-namespace", 97 | }, 98 | wantedContainers: []corev1.Container{ 99 | { 100 | Name: "TestContainer1", 101 | Image: "test-image-1", 102 | VolumeMounts: []corev1.VolumeMount{{Name: "test-volume-name", MountPath: "/test-volume-path"}}, 103 | Env: []corev1.EnvVar{ 104 | {Name: awsWebIdentityTokenFile, Value: "/test-volume-path/test-token"}, 105 | {Name: awsRoleArn, Value: "arn:aws:iam::123456789012:role/testrole"}, 106 | {Name: awsRoleSessionName, Value: "gtoken-webhook-" + strings.Repeat("0", 16)}, 107 | }, 108 | }, 109 | { 110 | Name: "TestContainer2", 111 | Image: "test-image-2", 112 | VolumeMounts: []corev1.VolumeMount{{Name: "test-volume-name", MountPath: "/test-volume-path"}}, 113 | Env: []corev1.EnvVar{ 114 | {Name: awsWebIdentityTokenFile, Value: "/test-volume-path/test-token"}, 115 | {Name: awsRoleArn, Value: "arn:aws:iam::123456789012:role/testrole"}, 116 | {Name: awsRoleSessionName, Value: "gtoken-webhook-" + strings.Repeat("0", 16)}, 117 | }, 118 | }, 119 | }, 120 | mutated: true, 121 | }, 122 | { 123 | name: "no containers to mutate", 124 | fields: fields{ 125 | k8sClient: fake.NewSimpleClientset(), 126 | volumeName: "test-volume-name", 127 | volumePath: "/test-volume-path", 128 | }, 129 | args: args{ 130 | roleArn: "arn:aws:iam::123456789012:role/testrole", 131 | ns: "test-namespace", 132 | }, 133 | mutated: false, 134 | }, 135 | } 136 | for _, tt := range tests { 137 | t.Run(tt.name, func(t *testing.T) { 138 | mw := &mutatingWebhook{ 139 | k8sClient: tt.fields.k8sClient, 140 | image: tt.fields.image, 141 | pullPolicy: tt.fields.pullPolicy, 142 | volumeName: tt.fields.volumeName, 143 | volumePath: tt.fields.volumePath, 144 | tokenFile: tt.fields.tokenFile, 145 | } 146 | got := mw.mutateContainers(tt.args.containers, tt.args.roleArn) 147 | if got != tt.mutated { 148 | t.Errorf("mutatingWebhook.mutateContainers() = %v, want %v", got, tt.mutated) 149 | } 150 | if !cmp.Equal(tt.args.containers, tt.wantedContainers) { 151 | t.Errorf("mutatingWebhook.mutateContainers() = diff %v", cmp.Diff(tt.args.containers, tt.wantedContainers)) 152 | } 153 | }) 154 | } 155 | } 156 | 157 | //nolint:funlen 158 | func Test_mutatingWebhook_mutatePod(t *testing.T) { 159 | type fields struct { 160 | image string 161 | pullPolicy string 162 | volumeName string 163 | volumePath string 164 | tokenFile string 165 | } 166 | type args struct { 167 | pod *corev1.Pod 168 | ns string 169 | serviceAccountName string 170 | annotations map[string]string 171 | dryRun bool 172 | } 173 | tests := []struct { 174 | name string 175 | fields fields 176 | args args 177 | wantErr bool 178 | wantedPod *corev1.Pod 179 | }{ 180 | { 181 | name: "mutate pod", 182 | fields: fields{ 183 | image: "doitintl/gtoken:test", 184 | pullPolicy: "Always", 185 | volumeName: "test-volume-name", 186 | volumePath: "/test-volume-path", 187 | tokenFile: "test-token", 188 | }, 189 | args: args{ 190 | pod: &corev1.Pod{ 191 | Spec: corev1.PodSpec{ 192 | Containers: []corev1.Container{ 193 | { 194 | Name: "TestContainer", 195 | Image: "test-image", 196 | }, 197 | }, 198 | ServiceAccountName: "test-sa", 199 | }, 200 | }, 201 | ns: "test-namespace", 202 | serviceAccountName: "test-sa", 203 | annotations: map[string]string{awsRoleArnKey: "arn:aws:iam::123456789012:role/testrole"}, 204 | }, 205 | wantedPod: &corev1.Pod{ 206 | Spec: corev1.PodSpec{ 207 | InitContainers: []corev1.Container{ 208 | { 209 | Name: "generate-gcp-id-token", 210 | Image: "doitintl/gtoken:test", 211 | Command: []string{"/gtoken", "--file=/test-volume-path/test-token", "--refresh=false"}, 212 | Resources: corev1.ResourceRequirements{ 213 | Requests: corev1.ResourceList{ 214 | corev1.ResourceCPU: resource.MustParse(requestsCPU), 215 | corev1.ResourceMemory: resource.MustParse(requestsMemory), 216 | }, 217 | Limits: corev1.ResourceList{ 218 | corev1.ResourceCPU: resource.MustParse(limitsCPU), 219 | corev1.ResourceMemory: resource.MustParse(limitsMemory), 220 | }, 221 | }, 222 | VolumeMounts: []corev1.VolumeMount{ 223 | { 224 | Name: "test-volume-name", 225 | MountPath: "/test-volume-path", 226 | }, 227 | }, 228 | ImagePullPolicy: "Always", 229 | }, 230 | }, 231 | Containers: []corev1.Container{ 232 | { 233 | Name: "TestContainer", 234 | Image: "test-image", 235 | VolumeMounts: []corev1.VolumeMount{{Name: "test-volume-name", MountPath: "/test-volume-path"}}, 236 | Env: []corev1.EnvVar{ 237 | {Name: awsWebIdentityTokenFile, Value: "/test-volume-path/test-token"}, 238 | {Name: awsRoleArn, Value: "arn:aws:iam::123456789012:role/testrole"}, 239 | {Name: awsRoleSessionName, Value: "gtoken-webhook-" + strings.Repeat("0", 16)}, 240 | }, 241 | }, 242 | { 243 | Name: "update-gcp-id-token", 244 | Image: "doitintl/gtoken:test", 245 | Command: []string{"/gtoken", "--file=/test-volume-path/test-token", "--refresh=true"}, 246 | Resources: corev1.ResourceRequirements{ 247 | Requests: corev1.ResourceList{ 248 | corev1.ResourceCPU: resource.MustParse(requestsCPU), 249 | corev1.ResourceMemory: resource.MustParse(requestsMemory), 250 | }, 251 | Limits: corev1.ResourceList{ 252 | corev1.ResourceCPU: resource.MustParse(limitsCPU), 253 | corev1.ResourceMemory: resource.MustParse(limitsMemory), 254 | }, 255 | }, 256 | VolumeMounts: []corev1.VolumeMount{ 257 | { 258 | Name: "test-volume-name", 259 | MountPath: "/test-volume-path", 260 | }, 261 | }, 262 | ImagePullPolicy: "Always", 263 | }, 264 | }, 265 | Volumes: []corev1.Volume{ 266 | { 267 | Name: "test-volume-name", 268 | VolumeSource: corev1.VolumeSource{ 269 | EmptyDir: &corev1.EmptyDirVolumeSource{ 270 | Medium: corev1.StorageMediumMemory, 271 | }, 272 | }, 273 | }, 274 | }, 275 | ServiceAccountName: "test-sa", 276 | }, 277 | }, 278 | }, 279 | } 280 | for _, tt := range tests { 281 | t.Run(tt.name, func(t *testing.T) { 282 | sa := &corev1.ServiceAccount{ 283 | ObjectMeta: metav1.ObjectMeta{ 284 | Name: tt.args.serviceAccountName, 285 | Namespace: tt.args.ns, 286 | Annotations: tt.args.annotations, 287 | }, 288 | } 289 | mw := &mutatingWebhook{ 290 | k8sClient: fake.NewSimpleClientset(sa), 291 | image: tt.fields.image, 292 | pullPolicy: tt.fields.pullPolicy, 293 | volumeName: tt.fields.volumeName, 294 | volumePath: tt.fields.volumePath, 295 | tokenFile: tt.fields.tokenFile, 296 | } 297 | if err := mw.mutatePod(context.TODO(), tt.args.pod, tt.args.ns, tt.args.dryRun); (err != nil) != tt.wantErr { 298 | t.Errorf("mutatingWebhook.mutatePod() error = %v, wantErr %v", err, tt.wantErr) 299 | } 300 | if !cmp.Equal(tt.args.pod, tt.wantedPod) { 301 | t.Errorf("mutatingWebhook.mutateContainers() = diff %v", cmp.Diff(tt.args.pod, tt.wantedPod)) 302 | } 303 | }) 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /cmd/gtoken/.dockerignore: -------------------------------------------------------------------------------- 1 | # ide 2 | .vscode 3 | 4 | # binaries 5 | .bin 6 | 7 | # test folders 8 | .cover 9 | **/test/coverage* 10 | 11 | # env exposure 12 | .env 13 | 14 | # text files 15 | *.md 16 | *.todo 17 | docs 18 | 19 | Dockerfile 20 | -------------------------------------------------------------------------------- /cmd/gtoken/.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | # which dirs to skip 3 | skip-dirs: 4 | - mocks 5 | # Timeout for analysis, e.g. 30s, 5m. 6 | # Default: 1m 7 | timeout: 5m 8 | # Exit code when at least one issue was found. 9 | # Default: 1 10 | issues-exit-code: 2 11 | # Include test files or not. 12 | # Default: true 13 | tests: false 14 | 15 | linters-settings: 16 | govet: 17 | check-shadowing: true 18 | golint: 19 | min-confidence: 0 20 | gocyclo: 21 | min-complexity: 15 22 | maligned: 23 | suggest-new: true 24 | dupl: 25 | threshold: 100 26 | goconst: 27 | min-len: 2 28 | min-occurrences: 2 29 | misspell: 30 | locale: US 31 | lll: 32 | line-length: 140 33 | goimports: 34 | local-prefixes: github.com/golangci/golangci-lint 35 | gocritic: 36 | enabled-tags: 37 | - diagnostic 38 | - experimental 39 | - opinionated 40 | - performance 41 | - style 42 | disabled-checks: 43 | - dupImport # https://github.com/go-critic/go-critic/issues/845 44 | - ifElseChain 45 | - octalLiteral 46 | - rangeValCopy 47 | - unnamedResult 48 | - whyNoLint 49 | - wrapperFunc 50 | funlen: 51 | lines: 100 52 | statements: 50 53 | 54 | linters: 55 | # please, do not use `enable-all`: it's deprecated and will be removed soon. 56 | # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint 57 | disable-all: true 58 | enable: 59 | # - rowserrcheck 60 | - bodyclose 61 | - deadcode 62 | - depguard 63 | - dogsled 64 | - dupl 65 | - errcheck 66 | - funlen 67 | - goconst 68 | - gocritic 69 | - gocyclo 70 | - gofmt 71 | - goimports 72 | - golint 73 | - gosec 74 | - gosimple 75 | - govet 76 | - ineffassign 77 | - interfacer 78 | - lll 79 | - misspell 80 | - nakedret 81 | - scopelint 82 | - staticcheck 83 | - structcheck 84 | - stylecheck 85 | - typecheck 86 | - unconvert 87 | - unparam 88 | - unused 89 | - varcheck 90 | - whitespace 91 | 92 | # don't enable: 93 | # - gochecknoglobals 94 | # - gocognit 95 | # - godox 96 | # - maligned 97 | # - prealloc 98 | 99 | issues: 100 | exclude: 101 | - Using the variable on range scope `tt` in function literal -------------------------------------------------------------------------------- /cmd/gtoken/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:experimental 2 | 3 | # 4 | # ----- Go Builder Image ------ 5 | # 6 | FROM golang:1.17-alpine AS builder 7 | 8 | # curl git bash 9 | RUN apk add --no-cache curl git bash make 10 | COPY --from=golangci/golangci-lint:v1.45-alpine /usr/bin/golangci-lint /usr/bin 11 | 12 | # 13 | # ----- Build and Test Image ----- 14 | # 15 | FROM builder as build 16 | 17 | # set working directorydoc 18 | RUN mkdir -p /go/src/gtoken 19 | WORKDIR /go/src/gtoken 20 | 21 | # load dependency 22 | COPY go.mod . 23 | COPY go.sum . 24 | RUN --mount=type=cache,target=/go/mod go mod download 25 | 26 | # copy sources 27 | COPY . . 28 | 29 | # build 30 | RUN make 31 | 32 | 33 | # 34 | # ------ get latest CA certificates 35 | # 36 | FROM alpine:3.15 as certs 37 | RUN apk --update add ca-certificates 38 | # this is for debug only Alpine image 39 | COPY --from=build /go/src/gtoken/.bin/github.com/doitintl/gtoken /gtoken 40 | CMD ["/gtoken"] 41 | 42 | # 43 | # ------ gtoken release Docker image ------ 44 | # 45 | FROM scratch 46 | 47 | # copy CA certificates 48 | COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 49 | 50 | # this is the last commabd since it's never cached 51 | COPY --from=build /go/src/gtoken/.bin/github.com/doitintl/gtoken /gtoken 52 | 53 | ENTRYPOINT ["/gtoken"] -------------------------------------------------------------------------------- /cmd/gtoken/Makefile: -------------------------------------------------------------------------------- 1 | MODULE = $(shell env GO111MODULE=on $(GO) list -m) 2 | DATE ?= $(shell date +%FT%T%z) 3 | VERSION ?= $(shell git describe --tags --always --dirty --match="v*" 2> /dev/null || \ 4 | cat $(CURDIR)/.version 2> /dev/null || echo v0) 5 | PKGS = $(or $(PKG),$(shell env GO111MODULE=on $(GO) list ./...)) 6 | TESTPKGS = $(shell env GO111MODULE=on $(GO) list -f \ 7 | '{{ if or .TestGoFiles .XTestGoFiles }}{{ .ImportPath }}{{ end }}' \ 8 | $(PKGS)) 9 | BIN = $(CURDIR)/.bin 10 | GOLANGCI_LINT_CONFIG = $(CURDIR)/.golangci.yaml 11 | 12 | GO = go 13 | GOLANGCI_LINT = golangci-lint 14 | TIMEOUT = 15 15 | V = 0 16 | Q = $(if $(filter 1,$V),,@) 17 | M = $(shell printf "\033[34;1m▶\033[0m") 18 | 19 | export GO111MODULE=on 20 | export CGO_ENABLED=0 21 | export GOPROXY=https://proxy.golang.org 22 | 23 | .PHONY: all 24 | all: fmt lint test | $(BIN) ; $(info $(M) building executable…) @ ## Build program binary 25 | $Q $(GO) build \ 26 | -tags release \ 27 | -ldflags '-X main.Version=$(VERSION) -X main.BuildDate=$(DATE)' \ 28 | -o $(BIN)/$(basename $(MODULE)) main.go 29 | 30 | # Tools 31 | 32 | $(BIN): 33 | @mkdir -p $@ 34 | $(BIN)/%: | $(BIN) ; $(info $(M) building $(PACKAGE)…) 35 | $Q tmp=$$(mktemp -d); \ 36 | env GO111MODULE=off GOPATH=$$tmp GOBIN=$(BIN) $(GO) get $(PACKAGE) \ 37 | || ret=$$?; \ 38 | rm -rf $$tmp ; exit $$ret 39 | 40 | GOCOV = $(BIN)/gocov 41 | $(BIN)/gocov: PACKAGE=github.com/axw/gocov/... 42 | 43 | GOCOVXML = $(BIN)/gocov-xml 44 | $(BIN)/gocov-xml: PACKAGE=github.com/AlekSi/gocov-xml 45 | 46 | GO2XUNIT = $(BIN)/go2xunit 47 | $(BIN)/go2xunit: PACKAGE=github.com/tebeka/go2xunit 48 | 49 | GOMOCK = $(BIN)/mockery 50 | $(BIN)/mockery: PACKAGE=github.com/vektra/mockery/.../ 51 | 52 | # Tests 53 | 54 | TEST_TARGETS := test-default test-bench test-short test-verbose test-race 55 | .PHONY: $(TEST_TARGETS) test-xml check test tests 56 | test-bench: ARGS=-run=__absolutelynothing__ -bench=. ## Run benchmarks 57 | test-short: ARGS=-short ## Run only short tests 58 | test-verbose: ARGS=-v ## Run tests in verbose mode with coverage reporting 59 | test-race: ARGS=-race ## Run tests with race detector 60 | $(TEST_TARGETS): NAME=$(MAKECMDGOALS:test-%=%) 61 | $(TEST_TARGETS): test 62 | check test tests: fmt lint ; $(info $(M) running $(NAME:%=% )tests…) @ ## Run tests 63 | $Q $(GO) test -timeout $(TIMEOUT)s $(ARGS) $(TESTPKGS) 64 | 65 | test-xml: fmt lint | $(GO2XUNIT) ; $(info $(M) running xUnit tests…) @ ## Run tests with xUnit output 66 | $Q mkdir -p test 67 | $Q 2>&1 $(GO) test -timeout $(TIMEOUT)s -v $(TESTPKGS) | tee test/tests.output 68 | $(GO2XUNIT) -fail -input test/tests.output -output test/tests.xml 69 | 70 | COVERAGE_MODE = atomic 71 | COVERAGE_PROFILE = $(COVERAGE_DIR)/profile.out 72 | COVERAGE_XML = $(COVERAGE_DIR)/coverage.xml 73 | COVERAGE_HTML = $(COVERAGE_DIR)/index.html 74 | .PHONY: test-coverage test-coverage-tools 75 | test-coverage-tools: | $(GOCOV) $(GOCOVXML) 76 | test-coverage: COVERAGE_DIR := $(CURDIR)/test/coverage.$(shell date -u +"%Y-%m-%dT%H:%M:%SZ") 77 | test-coverage: fmt lint test-coverage-tools ; $(info $(M) running coverage tests…) @ ## Run coverage tests 78 | $Q mkdir -p $(COVERAGE_DIR) 79 | $Q $(GO) test \ 80 | -coverpkg=$$($(GO) list -f '{{ join .Deps "\n" }}' $(TESTPKGS) | \ 81 | grep '^$(MODULE)/' | \ 82 | tr '\n' ',' | sed 's/,$$//') \ 83 | -covermode=$(COVERAGE_MODE) \ 84 | -coverprofile="$(COVERAGE_PROFILE)" $(TESTPKGS) 85 | $Q $(GO) tool cover -html=$(COVERAGE_PROFILE) -o $(COVERAGE_HTML) 86 | $Q $(GOCOV) convert $(COVERAGE_PROFILE) | $(GOCOVXML) > $(COVERAGE_XML) 87 | 88 | .PHONY: lint 89 | lint: | $(info $(M) running golangci-lint…) ## Run golangci-lint 90 | $Q $(GOLANGCI_LINT) run -v -c $(GOLANGCI_LINT_CONFIG) . 91 | 92 | .PHONY: mock 93 | mock: | $(GOMOCK) ; $(info $(M) generating mocks…) @ ## Run golangci-lint 94 | $Q $(GOMOCK) -dir internal/gcp -inpkg -all . 95 | 96 | .PHONY: fmt 97 | fmt: ; $(info $(M) running gofmt…) @ ## Run gofmt on all source files 98 | $Q $(GO) fmt $(PKGS) 99 | 100 | # Misc 101 | 102 | .PHONY: clean 103 | clean: ; $(info $(M) cleaning…) @ ## Cleanup everything 104 | @rm -rf $(BIN) 105 | @rm -rf test/tests.* test/coverage.* 106 | 107 | .PHONY: help 108 | help: 109 | @grep -E '^[ a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ 110 | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' 111 | 112 | .PHONY: version 113 | version: 114 | @echo $(VERSION) 115 | -------------------------------------------------------------------------------- /cmd/gtoken/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/doitintl/gtoken 2 | 3 | go 1.17 4 | 5 | require ( 6 | cloud.google.com/go/compute v0.1.0 7 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 8 | github.com/pkg/errors v0.9.1 9 | github.com/stretchr/testify v1.7.0 10 | github.com/urfave/cli/v2 v2.3.0 11 | golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a 12 | google.golang.org/api v0.63.0 13 | ) 14 | 15 | require ( 16 | github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect 17 | github.com/davecgh/go-spew v1.1.1 // indirect 18 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect 19 | github.com/golang/protobuf v1.5.2 // indirect 20 | github.com/googleapis/gax-go/v2 v2.1.1 // indirect 21 | github.com/pmezard/go-difflib v1.0.0 // indirect 22 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 23 | github.com/stretchr/objx v0.3.0 // indirect 24 | go.opencensus.io v0.23.0 // indirect 25 | golang.org/x/net v0.0.0-20220325170049-de3da57026de // indirect 26 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect 27 | golang.org/x/text v0.3.7 // indirect 28 | google.golang.org/appengine v1.6.7 // indirect 29 | google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c // indirect 30 | google.golang.org/grpc v1.40.1 // indirect 31 | google.golang.org/protobuf v1.28.0 // indirect 32 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 33 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /cmd/gtoken/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 v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 14 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 15 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 16 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 17 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 18 | cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= 19 | cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= 20 | cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= 21 | cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= 22 | cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= 23 | cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= 24 | cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= 25 | cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= 26 | cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= 27 | cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= 28 | cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= 29 | cloud.google.com/go v0.100.2 h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y= 30 | cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= 31 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 32 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 33 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 34 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 35 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 36 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 37 | cloud.google.com/go/compute v0.1.0 h1:rSUBvAyVwNJ5uQCKNJFMwPtTvJkfN38b6Pvb9zZoqJ8= 38 | cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= 39 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 40 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 41 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 42 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 43 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 44 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 45 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 46 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 47 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 48 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 49 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 50 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 51 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 52 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 53 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 54 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 55 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 56 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 57 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 58 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 59 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 60 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 61 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 62 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 63 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 64 | github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 65 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 66 | github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= 67 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 68 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 69 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 70 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 71 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 72 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 73 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 74 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 75 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 76 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 77 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 78 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 79 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= 80 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 81 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 82 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 83 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 84 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 85 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 86 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 87 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 88 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= 89 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 90 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 91 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 92 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 93 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 94 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 95 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 96 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 97 | github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= 98 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 99 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 100 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 101 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 102 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 103 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 104 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 105 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 106 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 107 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 108 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 109 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 110 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 111 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 112 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 113 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 114 | github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= 115 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 116 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 117 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 118 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 119 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 120 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 121 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 122 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 123 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 124 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 125 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 126 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 127 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 128 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 129 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 130 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 131 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 132 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 133 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 134 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 135 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 136 | github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= 137 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 138 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 139 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 140 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 141 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 142 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 143 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 144 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 145 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 146 | github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 147 | github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 148 | github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 149 | github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 150 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 151 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 152 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 153 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 154 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 155 | github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= 156 | github.com/googleapis/gax-go/v2 v2.1.1 h1:dp3bWCh+PPO1zjRRiCSczJav13sBvG4UhNyVTa1KqdU= 157 | github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= 158 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 159 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 160 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 161 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 162 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 163 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 164 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 165 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 166 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 167 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 168 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 169 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 170 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 171 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 172 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 173 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 174 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 175 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 176 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 177 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 178 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 179 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 180 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 181 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 182 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 183 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 184 | github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= 185 | github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 186 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 187 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 188 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 189 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 190 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 191 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 192 | github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= 193 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 194 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 195 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 196 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 197 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 198 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 199 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 200 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 201 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 202 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 203 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 204 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 205 | go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= 206 | go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= 207 | go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= 208 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 209 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 210 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 211 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 212 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 213 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 214 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 215 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 216 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 217 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 218 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 219 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 220 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 221 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 222 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 223 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 224 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 225 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 226 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 227 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 228 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 229 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 230 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 231 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 232 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 233 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 234 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 235 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 236 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 237 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 238 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 239 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 240 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 241 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 242 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 243 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 244 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 245 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 246 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 247 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 248 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 249 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 250 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 251 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 252 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 253 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 254 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 255 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 256 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 257 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 258 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 259 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 260 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 261 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 262 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 263 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 264 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 265 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 266 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 267 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 268 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 269 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 270 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 271 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 272 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 273 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 274 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 275 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 276 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 277 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 278 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 279 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 280 | golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= 281 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 282 | golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 283 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 284 | golang.org/x/net v0.0.0-20220325170049-de3da57026de h1:pZB1TWnKi+o4bENlbzAgLrEbY4RMYmUIRobMcSmfeYc= 285 | golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 286 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 287 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 288 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 289 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 290 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 291 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 292 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 293 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 294 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 295 | golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 296 | golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 297 | golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 298 | golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 299 | golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 300 | golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 301 | golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 302 | golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a h1:qfl7ob3DIEs3Ml9oLuPwY2N04gymzAW04WsUQHIClgM= 303 | golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= 304 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 305 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 306 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 307 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 308 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 309 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 310 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 311 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 312 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 313 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 314 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 315 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 316 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 317 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 318 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 319 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 320 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 321 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 322 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 323 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 324 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 325 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 326 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 327 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 328 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 329 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 330 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 331 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 332 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 333 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 334 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 335 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 336 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 337 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 338 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 339 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 340 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 341 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 342 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 343 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 344 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 345 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 346 | golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 347 | golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 348 | golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 349 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 350 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 351 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 352 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 353 | golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 354 | golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 355 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 356 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 357 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 358 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 359 | golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 360 | golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 361 | golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 362 | golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 363 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= 364 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 365 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 366 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 367 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 368 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 369 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 370 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 371 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 372 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 373 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 374 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 375 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 376 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 377 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 378 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 379 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 380 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 381 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 382 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 383 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 384 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 385 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 386 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 387 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 388 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 389 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 390 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 391 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 392 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 393 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 394 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 395 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 396 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 397 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 398 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 399 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 400 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 401 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 402 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 403 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 404 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 405 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 406 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 407 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 408 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 409 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 410 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 411 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 412 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 413 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 414 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 415 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 416 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 417 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 418 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 419 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 420 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 421 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 422 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 423 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 424 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 425 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 426 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 427 | golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 428 | golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 429 | golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 430 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 431 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 432 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 433 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 434 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 435 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 436 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 437 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 438 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 439 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 440 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 441 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 442 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 443 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 444 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 445 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 446 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 447 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 448 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 449 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 450 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 451 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 452 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 453 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 454 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 455 | google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= 456 | google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= 457 | google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= 458 | google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= 459 | google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= 460 | google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= 461 | google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= 462 | google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= 463 | google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= 464 | google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= 465 | google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= 466 | google.golang.org/api v0.63.0 h1:n2bqqK895ygnBpdPDYetfy23K7fJ22wsrZKCyfuRkkA= 467 | google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= 468 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 469 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 470 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 471 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 472 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 473 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 474 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 475 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 476 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 477 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 478 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 479 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 480 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 481 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 482 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 483 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 484 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 485 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 486 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 487 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 488 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 489 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 490 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 491 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 492 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 493 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 494 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 495 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 496 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 497 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 498 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 499 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 500 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 501 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 502 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 503 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 504 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 505 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 506 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 507 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 508 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 509 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 510 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 511 | google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 512 | google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 513 | google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 514 | google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 515 | google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= 516 | google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= 517 | google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= 518 | google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= 519 | google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= 520 | google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= 521 | google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= 522 | google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= 523 | google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= 524 | google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= 525 | google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= 526 | google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= 527 | google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= 528 | google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= 529 | google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= 530 | google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= 531 | google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= 532 | google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= 533 | google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= 534 | google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= 535 | google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c h1:c5afAQ+F8m49fzDEIKvD7o/D350YjVseBMjtoKL1xsg= 536 | google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= 537 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 538 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 539 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 540 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 541 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 542 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 543 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 544 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 545 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 546 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 547 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 548 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 549 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 550 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 551 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 552 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 553 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 554 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 555 | google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 556 | google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= 557 | google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= 558 | google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= 559 | google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= 560 | google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= 561 | google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= 562 | google.golang.org/grpc v1.40.1 h1:pnP7OclFFFgFi4VHQDQDaoXUVauOFyktqTsqqgzFKbc= 563 | google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= 564 | google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= 565 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 566 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 567 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 568 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 569 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 570 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 571 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 572 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 573 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 574 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 575 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 576 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 577 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 578 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 579 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 580 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 581 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 582 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 583 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 584 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 585 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 586 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 587 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 588 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 589 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 590 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 591 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 592 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 593 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 594 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 595 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 596 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 597 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 598 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 599 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 600 | -------------------------------------------------------------------------------- /cmd/gtoken/internal/gcp/mock_ServiceAccountInfo.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package gcp 4 | 5 | import ( 6 | context "context" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | // MockServiceAccountInfo is an autogenerated mock type for the ServiceAccountInfo type 12 | type MockServiceAccountInfo struct { 13 | mock.Mock 14 | } 15 | 16 | // GetEmail provides a mock function with given fields: 17 | func (_m *MockServiceAccountInfo) GetEmail() (string, error) { 18 | ret := _m.Called() 19 | 20 | var r0 string 21 | if rf, ok := ret.Get(0).(func() string); ok { 22 | r0 = rf() 23 | } else { 24 | r0 = ret.Get(0).(string) 25 | } 26 | 27 | var r1 error 28 | if rf, ok := ret.Get(1).(func() error); ok { 29 | r1 = rf() 30 | } else { 31 | r1 = ret.Error(1) 32 | } 33 | 34 | return r0, r1 35 | } 36 | 37 | // GetID provides a mock function with given fields: _a0 38 | func (_m *MockServiceAccountInfo) GetID(_a0 context.Context) (string, error) { 39 | ret := _m.Called(_a0) 40 | 41 | var r0 string 42 | if rf, ok := ret.Get(0).(func(context.Context) string); ok { 43 | r0 = rf(_a0) 44 | } else { 45 | r0 = ret.Get(0).(string) 46 | } 47 | 48 | var r1 error 49 | if rf, ok := ret.Get(1).(func(context.Context) error); ok { 50 | r1 = rf(_a0) 51 | } else { 52 | r1 = ret.Error(1) 53 | } 54 | 55 | return r0, r1 56 | } 57 | -------------------------------------------------------------------------------- /cmd/gtoken/internal/gcp/mock_Token.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package gcp 4 | 5 | import ( 6 | context "context" 7 | time "time" 8 | 9 | mock "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | // MockToken is an autogenerated mock type for the Token type 13 | type MockToken struct { 14 | mock.Mock 15 | } 16 | 17 | // Generate provides a mock function with given fields: _a0, _a1 18 | func (_m *MockToken) Generate(_a0 context.Context, _a1 string) (string, error) { 19 | ret := _m.Called(_a0, _a1) 20 | 21 | var r0 string 22 | if rf, ok := ret.Get(0).(func(context.Context, string) string); ok { 23 | r0 = rf(_a0, _a1) 24 | } else { 25 | r0 = ret.Get(0).(string) 26 | } 27 | 28 | var r1 error 29 | if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { 30 | r1 = rf(_a0, _a1) 31 | } else { 32 | r1 = ret.Error(1) 33 | } 34 | 35 | return r0, r1 36 | } 37 | 38 | // GetDuration provides a mock function with given fields: _a0 39 | func (_m *MockToken) GetDuration(_a0 string) (time.Duration, error) { 40 | ret := _m.Called(_a0) 41 | 42 | var r0 time.Duration 43 | if rf, ok := ret.Get(0).(func(string) time.Duration); ok { 44 | r0 = rf(_a0) 45 | } else { 46 | r0 = ret.Get(0).(time.Duration) 47 | } 48 | 49 | var r1 error 50 | if rf, ok := ret.Get(1).(func(string) error); ok { 51 | r1 = rf(_a0) 52 | } else { 53 | r1 = ret.Error(1) 54 | } 55 | 56 | return r0, r1 57 | } 58 | 59 | // WriteToFile provides a mock function with given fields: _a0, _a1 60 | func (_m *MockToken) WriteToFile(_a0 string, _a1 string) error { 61 | ret := _m.Called(_a0, _a1) 62 | 63 | var r0 error 64 | if rf, ok := ret.Get(0).(func(string, string) error); ok { 65 | r0 = rf(_a0, _a1) 66 | } else { 67 | r0 = ret.Error(0) 68 | } 69 | 70 | return r0 71 | } 72 | -------------------------------------------------------------------------------- /cmd/gtoken/internal/gcp/sainfo.go: -------------------------------------------------------------------------------- 1 | package gcp 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "log" 7 | "net" 8 | "net/http" 9 | "strings" 10 | "time" 11 | 12 | "cloud.google.com/go/compute/metadata" 13 | "github.com/pkg/errors" 14 | "golang.org/x/oauth2/google" 15 | ) 16 | 17 | // code found on Chromium project, https://github.com/luci/luci-go/blob/master/auth/internal/gce.go 18 | // 19 | // A client with more relaxed timeouts compared to the default one, which was 20 | // observed to timeout often on GKE when using Workload Identities. 21 | var metadataClient = metadata.NewClient(&http.Client{ 22 | Transport: &http.Transport{ 23 | Dial: (&net.Dialer{ 24 | Timeout: 10 * time.Second, 25 | KeepAlive: 30 * time.Second, 26 | }).Dial, 27 | ResponseHeaderTimeout: 15 * time.Second, // default is 2 28 | }, 29 | }) 30 | 31 | type ServiceAccountInfo interface { 32 | GetEmail() (string, error) 33 | GetID(context.Context) (string, error) 34 | } 35 | 36 | type SaInfo struct{} 37 | 38 | func NewSaInfo() ServiceAccountInfo { 39 | return &SaInfo{} 40 | } 41 | 42 | func (sa SaInfo) GetEmail() (string, error) { 43 | // use metadataClient (see above) instead of metadata 44 | // grab an email associated with the account. This must not be failing on 45 | // a healthy VM if the account is present. If it does, the metadata server isd broken. 46 | log.Println("getting email from metadata server") 47 | email, err := metadataClient.Email("") 48 | if err != nil { 49 | return "", errors.Wrap(err, "failed to get default email") 50 | } 51 | return email, nil 52 | } 53 | 54 | func (sa SaInfo) GetID(ctx context.Context) (string, error) { 55 | log.Println("getting service account") 56 | // handle the 'refresh token' command 57 | cx, cancel := context.WithCancel(ctx) 58 | // cancel current context on exit 59 | defer cancel() 60 | 61 | // Ensure the account has requested scopes. Assume 'cloud-platform' scope 62 | // covers all possible scopes. This is important when using GKE Workload 63 | // Identities: the metadata server always reports only 'cloud-platform' scope 64 | // there. Its presence should be enough to cover all scopes used in practice. 65 | // The exception is non-cloud scopes (like gerritcodereview or G Suite). To 66 | // use such scopes, one will have to use impersonation through Cloud IAM APIs, 67 | // which *are* covered by cloud-platform (see ActAsServiceAccount in auth.go). 68 | log.Println("getting scopes") 69 | availableScopes, err := metadataClient.Scopes("") 70 | if err != nil { 71 | log.Printf("failed to get available scopes: %s\n", err) 72 | } 73 | // print scopes and search for cloud-platform scope 74 | var found bool 75 | for _, s := range availableScopes { 76 | log.Printf("scope: %s", s) 77 | if strings.Contains(s, "cloud-platform") { 78 | found = true 79 | } 80 | } 81 | if !found { 82 | log.Println("appending cloud-platform scope") 83 | availableScopes = append(availableScopes, "https://www.googleapis.com/auth/cloud-platform") 84 | } 85 | log.Println("getting credentials") 86 | creds, err := google.FindDefaultCredentials(cx, availableScopes...) 87 | if err != nil { 88 | return "", errors.Wrap(err, "failed to find default credentials") 89 | } 90 | credsMap := make(map[string]interface{}) 91 | err = json.Unmarshal(creds.JSON, &credsMap) 92 | if err != nil { 93 | return "", errors.Wrap(err, "failed to parse credentials JSON") 94 | } 95 | if id, ok := credsMap["client_id"].(string); ok { 96 | return id, nil 97 | } 98 | return "", errors.New("failed to find service account ID") 99 | } 100 | -------------------------------------------------------------------------------- /cmd/gtoken/internal/gcp/token.go: -------------------------------------------------------------------------------- 1 | package gcp 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | "time" 11 | 12 | "github.com/dgrijalva/jwt-go" 13 | "google.golang.org/api/iamcredentials/v1" 14 | ) 15 | 16 | const ( 17 | // default aud 18 | defaultAud = "gtoken/sts/assume-role-with-web-identity" 19 | ) 20 | 21 | type Token interface { 22 | Generate(context.Context, string) (string, error) 23 | GetDuration(string) (time.Duration, error) 24 | WriteToFile(string, string) error 25 | } 26 | 27 | type IDToken struct{} 28 | 29 | func NewIDToken() Token { 30 | return &IDToken{} 31 | } 32 | 33 | func (IDToken) Generate(ctx context.Context, serviceAccount string) (string, error) { 34 | log.Println("generating a new ID token") 35 | iamCredentialsClient, err := iamcredentials.NewService(ctx) 36 | if err != nil { 37 | return "", fmt.Errorf("failed to get iam credentials client: %s", err.Error()) 38 | } 39 | generateIDTokenResponse, err := iamCredentialsClient.Projects.ServiceAccounts.GenerateIdToken( 40 | fmt.Sprintf("projects/-/serviceAccounts/%s", serviceAccount), 41 | &iamcredentials.GenerateIdTokenRequest{ 42 | Audience: defaultAud, 43 | IncludeEmail: true, 44 | }, 45 | ).Do() 46 | if err != nil { 47 | return "", fmt.Errorf("failed to generate ID token: %s", err.Error()) 48 | } 49 | log.Println("successfully generated ID token") 50 | return generateIDTokenResponse.Token, nil 51 | } 52 | 53 | func (IDToken) GetDuration(jwtToken string) (time.Duration, error) { 54 | // parse JWT token 55 | parser := jwt.Parser{UseJSONNumber: true, SkipClaimsValidation: true} 56 | token, _, err := parser.ParseUnverified(jwtToken, jwt.MapClaims{}) 57 | if err != nil { 58 | return 0, fmt.Errorf("failed to parse jwtToken: %s", err.Error()) 59 | } 60 | if claims, ok := token.Claims.(jwt.MapClaims); ok { 61 | var unixTime int64 62 | unixTime, err = claims["exp"].(json.Number).Int64() 63 | if err != nil { 64 | return 0, fmt.Errorf("failed to convert expire date: %s", err.Error()) 65 | } 66 | return time.Until(time.Unix(unixTime, 0)), nil 67 | } 68 | return 0, fmt.Errorf("failed to get claims from ID token: %s", err.Error()) 69 | } 70 | 71 | func (IDToken) WriteToFile(token, fileName string) error { 72 | // this is a slice of io.Writers we will write the file to 73 | var writers []io.Writer 74 | 75 | // if no file provided 76 | if fileName == "" { 77 | writers = append(writers, os.Stdout) 78 | } 79 | 80 | // if DestFile was provided, lets try to create it and add to the writers 81 | if len(fileName) > 0 { 82 | file, err := os.Create(fileName) 83 | if err != nil { 84 | return fmt.Errorf("failed to create token file: %s; error: %s", fileName, err.Error()) 85 | } 86 | writers = append(writers, file) 87 | defer file.Close() 88 | } 89 | // MultiWriter(io.Writer...) returns a single writer which multiplexes its 90 | // writes across all of the writers we pass in. 91 | dest := io.MultiWriter(writers...) 92 | // write to dest the same way as before, copying from the Body 93 | if _, err := io.WriteString(dest, token); err != nil { 94 | return fmt.Errorf("failed to write token: %s", err.Error()) 95 | } 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /cmd/gtoken/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "runtime" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/doitintl/gtoken/internal/gcp" 14 | 15 | "github.com/urfave/cli/v2" 16 | ) 17 | 18 | var ( 19 | // Version contains the current version. 20 | Version = "dev" 21 | // BuildDate contains a string with the build date. 22 | BuildDate = "unknown" 23 | ) 24 | 25 | func generateIDToken(ctx context.Context, sa gcp.ServiceAccountInfo, idToken gcp.Token, file string, refresh bool) error { 26 | // find out active Service Account, first by ID 27 | serviceAccount, err := sa.GetID(ctx) 28 | if err != nil { 29 | log.Printf("failed to get service account, fallback to metadata email: %s\n", err) 30 | // fallback: try to get Service Account email from metadata server 31 | serviceAccount, err = sa.GetEmail() 32 | } 33 | if err != nil { 34 | return err 35 | } 36 | log.Printf("found service account: %s\n", serviceAccount) 37 | // initial duration to 1ms 38 | duration := time.Millisecond 39 | timer := time.NewTimer(duration).C 40 | for { 41 | // wait for next timer tick or cancel 42 | select { 43 | case <-ctx.Done(): 44 | return nil // avoid goroutine leak 45 | case <-timer: 46 | // generate ID token 47 | token, err := idToken.Generate(ctx, serviceAccount) 48 | if err != nil { 49 | return err 50 | } 51 | // write generated token to file or stdout 52 | err = idToken.WriteToFile(token, file) 53 | if err != nil { 54 | return err 55 | } 56 | // auto-refresh enabled 57 | if refresh { 58 | // get token duration 59 | duration, err = idToken.GetDuration(token) 60 | if err != nil { 61 | return err 62 | } 63 | // reduce duration by 30s 64 | duration -= 30 * time.Second 65 | log.Printf("refreshing token in %s", duration) 66 | // reset timer 67 | timer = time.NewTimer(duration).C 68 | } else { 69 | return nil // avoid goroutine leak 70 | } 71 | } 72 | } 73 | } 74 | 75 | func generateIDTokenCmd(c *cli.Context) error { 76 | return generateIDToken(handleSignals(), gcp.NewSaInfo(), gcp.NewIDToken(), c.String("file"), c.Bool("refresh")) 77 | } 78 | 79 | func handleSignals() context.Context { 80 | // Graceful shut-down on SIGINT/SIGTERM 81 | sig := make(chan os.Signal, 1) 82 | signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) 83 | 84 | // create cancelable context 85 | ctx, cancel := context.WithCancel(context.Background()) 86 | 87 | go func() { 88 | defer cancel() 89 | sid := <-sig 90 | log.Printf("received signal: %d\n", sid) 91 | log.Println("canceling token refresh ...") 92 | }() 93 | 94 | return ctx 95 | } 96 | 97 | func main() { 98 | app := &cli.App{ 99 | Flags: []cli.Flag{ 100 | &cli.BoolFlag{ 101 | Name: "refresh", 102 | Value: false, 103 | Usage: "auto refresh ID token before it expires", 104 | }, 105 | &cli.StringFlag{ 106 | Name: "file", 107 | Usage: "write ID token into file (stdout, if not specified)", 108 | }, 109 | }, 110 | Name: "gtoken", 111 | Usage: "generate ID token with current Google Cloud service account", 112 | Action: generateIDTokenCmd, 113 | Version: Version, 114 | } 115 | cli.VersionPrinter = func(c *cli.Context) { 116 | fmt.Printf("gtoken %s\n", Version) 117 | fmt.Printf(" Build date: %s\n", BuildDate) 118 | fmt.Printf(" Built with: %s\n", runtime.Version()) 119 | } 120 | // print version 121 | log.Printf("running gtoken version: %s\n", app.Version) 122 | 123 | err := app.Run(os.Args) 124 | if err != nil { 125 | log.Fatal(err) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /cmd/gtoken/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | "time" 8 | 9 | "github.com/doitintl/gtoken/internal/gcp" 10 | ) 11 | 12 | //nolint:funlen 13 | func Test_generateIDToken(t *testing.T) { 14 | type args struct { 15 | file string 16 | refresh bool 17 | } 18 | type fields struct { 19 | email string 20 | jwt string 21 | } 22 | tests := []struct { 23 | name string 24 | args args 25 | fields fields 26 | mockInit func(context.Context, *gcp.MockServiceAccountInfo, *gcp.MockToken, args, fields) 27 | wantErr bool 28 | }{ 29 | { 30 | name: "one time token generation", 31 | args: args{ 32 | file: "jwt.token", 33 | }, 34 | fields: fields{ 35 | email: "test@project.iam.gserviceaccount.com", 36 | jwt: "whatever", 37 | }, 38 | mockInit: func(ctx context.Context, sa *gcp.MockServiceAccountInfo, token *gcp.MockToken, args args, fields fields) { 39 | sa.On("GetID", ctx).Return(fields.email, nil) 40 | token.On("Generate", ctx, fields.email).Return(fields.jwt, nil) 41 | token.On("WriteToFile", fields.jwt, args.file).Return(nil) 42 | }, 43 | }, 44 | { 45 | name: "one time token generation from email", 46 | args: args{ 47 | file: "jwt.token", 48 | }, 49 | fields: fields{ 50 | email: "test@project.iam.gserviceaccount.com", 51 | jwt: "whatever", 52 | }, 53 | mockInit: func(ctx context.Context, sa *gcp.MockServiceAccountInfo, token *gcp.MockToken, args args, fields fields) { 54 | sa.On("GetID", ctx).Return("", errors.New("failed to get sa")) 55 | sa.On("GetEmail").Return(fields.email, nil) 56 | token.On("Generate", ctx, fields.email).Return(fields.jwt, nil) 57 | token.On("WriteToFile", fields.jwt, args.file).Return(nil) 58 | }, 59 | }, 60 | { 61 | name: "refresh token generation", 62 | args: args{ 63 | file: "jwt.token", 64 | refresh: true, 65 | }, 66 | fields: fields{ 67 | email: "test@project.iam.gserviceaccount.com", 68 | jwt: "whatever", 69 | }, 70 | mockInit: func(ctx context.Context, sa *gcp.MockServiceAccountInfo, token *gcp.MockToken, args args, fields fields) { 71 | sa.On("GetID", ctx).Return(fields.email, nil) 72 | token.On("Generate", ctx, fields.email).Return(fields.jwt, nil) 73 | token.On("WriteToFile", fields.jwt, args.file).Return(nil) 74 | token.On("GetDuration", fields.jwt).Return(31*time.Second, nil) 75 | token.On("Generate", ctx, fields.email).Return(fields.jwt, nil) 76 | token.On("WriteToFile", fields.jwt, args.file).Return(nil) 77 | }, 78 | }, 79 | { 80 | name: "failed to find sa", 81 | mockInit: func(ctx context.Context, sa *gcp.MockServiceAccountInfo, token *gcp.MockToken, args args, fields fields) { 82 | sa.On("GetID", ctx).Return("", errors.New("failed to get sa")) 83 | sa.On("GetEmail").Return("", errors.New("failed to get sa email")) 84 | }, 85 | wantErr: true, 86 | }, 87 | { 88 | name: "failed to generate token", 89 | args: args{ 90 | file: "jwt.token", 91 | }, 92 | fields: fields{ 93 | email: "test@project.iam.gserviceaccount.com", 94 | jwt: "whatever", 95 | }, 96 | mockInit: func(ctx context.Context, sa *gcp.MockServiceAccountInfo, token *gcp.MockToken, args args, fields fields) { 97 | sa.On("GetID", ctx).Return(fields.email, nil) 98 | token.On("Generate", ctx, fields.email).Return(fields.jwt, nil) 99 | token.On("WriteToFile", fields.jwt, args.file).Return(errors.New("failed to write token to file")) 100 | }, 101 | wantErr: true, 102 | }, 103 | { 104 | name: "failed to write token", 105 | args: args{ 106 | file: "jwt.token", 107 | }, 108 | fields: fields{ 109 | email: "test@project.iam.gserviceaccount.com", 110 | jwt: "whatever", 111 | }, 112 | mockInit: func(ctx context.Context, sa *gcp.MockServiceAccountInfo, token *gcp.MockToken, args args, fields fields) { 113 | sa.On("GetID", ctx).Return(fields.email, nil) 114 | token.On("Generate", ctx, fields.email).Return("", errors.New("failed to generate ID token")) 115 | }, 116 | wantErr: true, 117 | }, 118 | { 119 | name: "failed to get duration from token", 120 | args: args{ 121 | file: "jwt.token", 122 | refresh: true, 123 | }, 124 | fields: fields{ 125 | email: "test@project.iam.gserviceaccount.com", 126 | jwt: "whatever", 127 | }, 128 | mockInit: func(ctx context.Context, sa *gcp.MockServiceAccountInfo, token *gcp.MockToken, args args, fields fields) { 129 | sa.On("GetID", ctx).Return(fields.email, nil) 130 | token.On("Generate", ctx, fields.email).Return(fields.jwt, nil) 131 | token.On("WriteToFile", fields.jwt, args.file).Return(nil) 132 | token.On("GetDuration", fields.jwt).Return(time.Duration(0), errors.New("failed to get duration")) 133 | }, 134 | wantErr: true, 135 | }, 136 | } 137 | for _, tt := range tests { 138 | t.Run(tt.name, func(t *testing.T) { 139 | mockSA := &gcp.MockServiceAccountInfo{} 140 | mockToken := &gcp.MockToken{} 141 | ctx, cancel := context.WithCancel(context.TODO()) 142 | tt.mockInit(ctx, mockSA, mockToken, tt.args, tt.fields) 143 | go func() { 144 | time.Sleep(time.Second) 145 | cancel() 146 | }() 147 | if err := generateIDToken(ctx, mockSA, mockToken, tt.args.file, tt.args.refresh); (err != nil) != tt.wantErr { 148 | t.Errorf("generateIDToken() error = %v, wantErr %v", err, tt.wantErr) 149 | } 150 | mockSA.AssertExpectations(t) 151 | mockToken.AssertExpectations(t) 152 | }) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /deployment/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: gtoken-webhook-cr 5 | labels: 6 | app: gtoken-webhook 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - pods 12 | - events 13 | verbs: 14 | - "*" 15 | - apiGroups: 16 | - apps 17 | resources: 18 | - deployments 19 | - daemonsets 20 | - replicasets 21 | - statefulsets 22 | verbs: 23 | - "*" 24 | - apiGroups: 25 | - autoscaling 26 | resources: 27 | - "*" 28 | verbs: 29 | - "*" 30 | - apiGroups: 31 | - "" 32 | resources: 33 | - serviceaccounts 34 | verbs: 35 | - get -------------------------------------------------------------------------------- /deployment/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | kind: ClusterRoleBinding 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: gtoken-webhook-crb 5 | labels: 6 | app: gtoken-webhook 7 | subjects: 8 | - kind: ServiceAccount 9 | name: gtoken-webhook-sa 10 | namespace: gtoken 11 | roleRef: 12 | apiGroup: rbac.authorization.k8s.io 13 | kind: ClusterRole 14 | name: gtoken-webhook-cr -------------------------------------------------------------------------------- /deployment/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: gtoken-webhook-deployment 5 | namespace: gtoken 6 | labels: 7 | app: gtoken-webhook 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: gtoken-webhook 13 | template: 14 | metadata: 15 | labels: 16 | app: gtoken-webhook 17 | spec: 18 | containers: 19 | - name: gtoken-webhook 20 | image: doitintl/gtoken-webhook 21 | imagePullPolicy: Always 22 | resources: 23 | requests: 24 | cpu: 250m 25 | memory: 512Mi 26 | args: 27 | - --log-level=debug 28 | - server 29 | - --tls-cert-file=/etc/webhook/certs/cert.pem 30 | - --tls-private-key-file=/etc/webhook/certs/key.pem 31 | - --pull-policy=Always 32 | volumeMounts: 33 | - name: webhook-certs 34 | mountPath: /etc/webhook/certs 35 | readOnly: true 36 | serviceAccountName: gtoken-webhook-sa 37 | volumes: 38 | - name: webhook-certs 39 | secret: 40 | secretName: gtoken-webhook-certs 41 | -------------------------------------------------------------------------------- /deployment/mutatingwebhook.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: MutatingWebhookConfiguration 3 | metadata: 4 | name: mutating-gtoken-webhook-cfg 5 | labels: 6 | app: gtoken-webhook 7 | webhooks: 8 | - name: gtoken.doit-intl.com 9 | sideEffects: None 10 | admissionReviewVersions: ["v1", "v1beta1"] 11 | clientConfig: 12 | service: 13 | name: gtoken-webhook-svc 14 | namespace: gtoken 15 | path: "/pods" 16 | caBundle: ${CA_BUNDLE} 17 | # select namespaces without the label "admission.gtoken/ignore" 18 | namespaceSelector: 19 | matchExpressions: 20 | - key: admission.gtoken/ignore 21 | operator: DoesNotExist 22 | rules: 23 | - operations: ["CREATE"] 24 | apiGroups: ["*"] 25 | apiVersions: ["*"] 26 | resources: ["pods"] 27 | scope: "Namespaced" 28 | # ignore failures 29 | failurePolicy: Ignore 30 | 31 | 32 | -------------------------------------------------------------------------------- /deployment/namespace.yaml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: gtoken 5 | labels: 6 | app: gtoken-webhook 7 | admission.gtoken/ignore: "true" -------------------------------------------------------------------------------- /deployment/service-account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: gtoken-webhook-sa 5 | namespace: gtoken 6 | labels: 7 | app: gtoken-webhook -------------------------------------------------------------------------------- /deployment/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: gtoken-webhook-svc 5 | namespace: gtoken 6 | labels: 7 | app: gtoken-webhook 8 | spec: 9 | ports: 10 | - port: 443 11 | targetPort: 8443 12 | selector: 13 | app: gtoken-webhook 14 | -------------------------------------------------------------------------------- /deployment/webhook-create-self-signed-cert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | usage() { 6 | cat <> ${tmpdir}/csr.conf 61 | [req] 62 | req_extensions = v3_req 63 | distinguished_name = req_distinguished_name 64 | [req_distinguished_name] 65 | [ v3_req ] 66 | basicConstraints = CA:FALSE 67 | keyUsage = nonRepudiation, digitalSignature, keyEncipherment 68 | extendedKeyUsage = serverAuth 69 | subjectAltName = @alt_names 70 | [alt_names] 71 | DNS.1 = ${service} 72 | DNS.2 = ${service}.${namespace} 73 | DNS.3 = ${service}.${namespace}.svc 74 | EOF 75 | 76 | # create CA and Server key/certificate 77 | openssl genrsa -out ${tmpdir}/ca.key 2048 78 | openssl req -x509 -newkey rsa:2048 -key ${tmpdir}/ca.key -out ${tmpdir}/ca.crt -days 1825 -nodes -subj "/CN=${service}.${namespace}.svc" 79 | 80 | # create server key/certificate 81 | openssl genrsa -out ${tmpdir}/server.key 2048 82 | openssl req -new -key ${tmpdir}/server.key -subj "/CN=${service}.${namespace}.svc" -out ${tmpdir}/server.csr -config ${tmpdir}/csr.conf 83 | 84 | # Self sign 85 | openssl x509 -extensions v3_req -req -days 1825 -in ${tmpdir}/server.csr -CA ${tmpdir}/ca.crt -CAkey ${tmpdir}/ca.key -CAcreateserial -out ${tmpdir}/server.crt -extfile ${tmpdir}/csr.conf 86 | 87 | # create the secret with CA cert and server cert/key 88 | kubectl create secret generic ${secret} \ 89 | --from-file=key.pem=${tmpdir}/server.key \ 90 | --from-file=cert.pem=${tmpdir}/server.crt \ 91 | --dry-run=client -o yaml | 92 | kubectl -n ${namespace} apply -f - 93 | 94 | # -a means base64 encode 95 | caBundle=$(cat ${tmpdir}/ca.crt | openssl enc -a -A) 96 | 97 | echo "Encoded CA:" 98 | echo -e "${caBundle} \n" -------------------------------------------------------------------------------- /deployment/webhook-create-signed-cert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | usage() { 6 | cat <> ${tmpdir}/csr.conf 63 | [req] 64 | req_extensions = v3_req 65 | distinguished_name = req_distinguished_name 66 | [req_distinguished_name] 67 | [ v3_req ] 68 | basicConstraints = CA:FALSE 69 | keyUsage = nonRepudiation, digitalSignature, keyEncipherment 70 | extendedKeyUsage = serverAuth 71 | subjectAltName = @alt_names 72 | [alt_names] 73 | DNS.1 = ${service} 74 | DNS.2 = ${service}.${namespace} 75 | DNS.3 = ${service}.${namespace}.svc 76 | EOF 77 | 78 | openssl genrsa -out ${tmpdir}/server-key.pem 2048 79 | openssl req -new -key ${tmpdir}/server-key.pem -subj "/CN=${service}.${namespace}.svc" -out ${tmpdir}/server.csr -config ${tmpdir}/csr.conf 80 | 81 | # clean-up any previously created CSR for our service. Ignore errors if not present. 82 | kubectl delete csr ${csrName} 2>/dev/null || true 83 | 84 | # create server cert/key CSR and send to k8s API 85 | cat <&2 121 | exit 1 122 | fi 123 | echo ${serverCert} | openssl base64 -d -A -out ${tmpdir}/server-cert.pem 124 | 125 | 126 | # create the secret with CA cert and server cert/key 127 | kubectl create secret generic ${secret} \ 128 | --from-file=key.pem=${tmpdir}/server-key.pem \ 129 | --from-file=cert.pem=${tmpdir}/server-cert.pem \ 130 | --dry-run=client -o yaml | 131 | kubectl -n ${namespace} apply -f - 132 | 133 | # get CA bundle for use by webhook bootstrap 134 | caBundle=$(kubectl config view --raw --flatten -o json | jq -r '.clusters[] | select(.name == "'$(kubectl config current-context)'") | .cluster."certificate-authority-data"') 135 | echo "Encoded CA:" 136 | echo -e "${caBundle} \n" -------------------------------------------------------------------------------- /deployment/webhook-patch-ca-bundle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ROOT=$(cd $(dirname $0)/../../; pwd) 4 | 5 | set -o errexit 6 | set -o nounset 7 | set -o pipefail 8 | 9 | if [[ -z "${CA_BUNDLE}" ]]; then 10 | echo "CA_BUNDLE not set" 11 | exit 1 12 | fi 13 | 14 | if command -v envsubst >/dev/null 2>&1; then 15 | envsubst 16 | else 17 | sed -e "s|\${CA_BUNDLE}|${CA_BUNDLE}|g" 18 | fi 19 | --------------------------------------------------------------------------------