├── .cobra.yaml ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .tool-versions ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── cmd ├── diff.go ├── result.go ├── result_test.go ├── root.go ├── status.go ├── sync.go └── template.go ├── config ├── chart.go ├── chart_test.go ├── config.go ├── config_test.go ├── repository.go └── repository_test.go ├── go.mod ├── go.sum ├── main.go ├── scripts └── build.sh └── testdata ├── absent.yml ├── default-state.yml ├── demo.yml ├── invalid.yml ├── unmarshallable.yml └── without-repo.yml /.cobra.yaml: -------------------------------------------------------------------------------- 1 | author: Anthony Spring 2 | license: MIT 3 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | # Trigger only when a tag is pushed 4 | on: 5 | push: 6 | tags: 7 | - '*' 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | 16 | - name: Cache Go Dependencies 17 | uses: actions/cache@v2 18 | with: 19 | path: | 20 | ~/go/pkg/mod 21 | ~/.cache/go-build 22 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 23 | restore-keys: | 24 | ${{ runner.os }}-go- 25 | 26 | - name: Install Go 27 | uses: actions/setup-go@v2 28 | with: 29 | go-version: 1.18.2 30 | 31 | - name: Test 32 | run: go test -v ./... 33 | 34 | - name: Build 35 | run: make build 36 | env: 37 | TARGETS: release 38 | GENERATE_PACKAGES: true 39 | 40 | - name: Release 41 | uses: softprops/action-gh-release@35d938cf01f60fbe522917c81be1e892074f6ad6 42 | with: 43 | files: | 44 | pkg/binnacle-*.tar.gz 45 | pkg/SHA256SUM.txt 46 | fail_on_unmatched_files: true 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # Adapted from https://github.com/mvdan/github-actions-golang 2 | on: [push, pull_request] 3 | name: Test 4 | jobs: 5 | test: 6 | strategy: 7 | matrix: 8 | go-version: [1.18.2] 9 | os: [ubuntu-20.04, macos-12] 10 | runs-on: ${{ matrix.os }} 11 | 12 | steps: 13 | - name: Install Go 14 | uses: actions/setup-go@v2 15 | with: 16 | go-version: ${{ matrix.go-version }} 17 | 18 | - name: Checkout code 19 | uses: actions/checkout@v2 20 | 21 | - name: Cache Go Dependencies 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/go/pkg/mod 26 | ~/.cache/go-build 27 | ~/Library/Caches/go-build 28 | %LocalAppData%\go-build 29 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 30 | restore-keys: | 31 | ${{ runner.os }}-go- 32 | 33 | - name: Test 34 | run: go test -v ./... 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | bin/* 27 | docs/coverage/* 28 | docs/unit/* 29 | pkg/* -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | golang 1.18.2 2 | helm 3.8.2 3 | kustomize 5.4.2 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [Unreleased] 7 | 8 | - None at this time 9 | 10 | ## [0.8.0] - 2022-05-12 11 | 12 | - Remove support for Helm 2 13 | - Fix build to properly include VERSION 14 | - Standardize error handling 15 | - Add support for running Kustomize as a post-renderer 16 | - Upgrade cobra to 1.4.0 17 | - Upgrade viper to 1.11.0 18 | - Upgrade to go 1.18.2 19 | - Upgrade logrus to 1.8.1 20 | - Replace use of ghodss/yaml library with go-yaml.v3 21 | 22 | ## [0.7.1] - 2022-05-05 23 | 24 | Changes: 25 | 26 | - Pass chart namespace to helm in `binnacle diff` and `binnacle status` commands 27 | 28 | ## [0.7.0] - 2021-11-22 29 | 30 | Changes: 31 | 32 | - Upgrade to go 1.17.3 33 | - Use go modules instead of govendor 34 | - Add Darwin ARM64 as a build target to generate an artifact for M1 Macs 35 | - Remove code for building in Docker and generating docs 36 | 37 | ## [0.6.0] - 2021-09-14 38 | 39 | Changes: 40 | 41 | - Forward args to the ReleaseExists function and to Helm 42 | 43 | ## [0.5.1] - 2021-04-19 44 | 45 | Changes: 46 | 47 | - Use Github Actions for releases 48 | 49 | ## [0.5.0] - 2021-04-19 50 | 51 | Changes: 52 | 53 | - Repositories listed under the `repositories` key are now added/updated for the `template` and `diff` commands. Previously, these repositories were only used when running `sync`. 54 | - Due to build issues, this release was never created. 0.5.1 supersedes it 55 | 56 | 57 | ## [0.4.0] - 2020-11-12 58 | 59 | Changes: 60 | 61 | - Previously when syncing a configuration file containing repositories and charts, there was no way to utilize newly added charts in a single run because a `helm repo update` needed to be executed after the repositories were added. A call to `helm repo update` has been added whenever a new repository is added. 62 | 63 | - Binnacles whose state is set to absent will no longer be rendered via the `template` command. 64 | 65 | - Introduced logic that checks to see if a release exists prior to attempting to remove the release. 66 | 67 | Fixes: 68 | 69 | - Invalid configuration files were not properly reported as errors to the user. This has been corrected. 70 | 71 | ## [0.3.1] - 2020-04-01 72 | 73 | Fixes: 74 | 75 | - When generating a template the chart version was not properly provided for Helm3. 76 | 77 | ## [0.3.0] - 2020-02-04 78 | 79 | Breaking Changes: 80 | 81 | - Prior to this release if the `repo` for a chart was not specified it defaulted to `local`. This default has been changed to an empty string. 82 | 83 | Changes: 84 | 85 | - Added the ability to reference a chart on the local file system or URL. To utilize this functionality leave the repo empty for a chart and pass the necessary path/URL as the `name` of the chart. 86 | 87 | ```yaml 88 | charts: 89 | - name: https://github.com/pantsel/konga/blob/master/charts/konga/konga-1.0.0.tgz?raw=true 90 | namespace: kube-system 91 | release: konga 92 | state: present 93 | ``` 94 | 95 | This example shows the `repo` has been omitted and the name pointing to a URL used to access the desired version of the chart. 96 | 97 | ## [0.2.1] - 2020-01-129 98 | 99 | - Remove the explicit '--force' from the command passed to helm3 upgrade during a `binnacle sync`. 100 | 101 | ## [0.2.0] - 2020-01-13 102 | 103 | - This release introduces Helm 3 support by adding a lightweight touchpoint to detect if helm2 or helm3 is getting targetted and treating helm2 as the exception case for processing. This will allow helm2 support to be easily removed upon its EOL. To facilitate this detected binnacle will run `helm version` during certain commands to help determine the target version and change the underlying helm commands accordingly. 104 | 105 | ## [0.1.1] - 2018-11-09 106 | 107 | - The 0.1.0 release improperly used the 0.0.5 version. This change is the exact functionality as 0.1.0 but with the version correctly updated. 108 | 109 | ## [0.1.0] - 2018-11-09 110 | 111 | - maps read from YAML values were being transformed into `map[string]string`, but will now be `map[string]interface{}` to maintain the values' types 112 | 113 | ## [0.0.5] - 2018-07-20 114 | 115 | ### Notes 116 | 117 | - The 0.0.4 release improperly used the 0.0.3 version. This change is the exact functionality as 0.0.4 but with the version correctly updated. 118 | 119 | ## [0.0.4] - 2018-07-20 120 | 121 | ### Notes 122 | 123 | - The `binnacle` binaries were improperly build as non-static binaries. They have been converted to static binaries. 124 | - The Darwin build of `binnacle` was not working properly on Travis. This has been resolved. 125 | 126 | ## [0.0.3] - 2018-07-12 127 | 128 | ### Notes 129 | 130 | - The 0.0.2 release improperly used the 0.0.1 version. This change is the exact functionality as 0.0.2 but with the version correctly updated. 131 | 132 | ## [0.0.2] - 2018-05-11 133 | 134 | ### Notes 135 | 136 | - Added support for the `helm-diff` plugin via the `diff` subcommand. 137 | 138 | ## [0.0.1] - 2018-04-27 139 | 140 | ### Notes 141 | 142 | - Initial release 143 | 144 | [Unreleased]: https://github.com/traackr/binnacle/compare/v0.8.0...HEAD 145 | [0.8.0]: https://github.com/traackr/binnacle/tree/0.8.0 146 | [0.7.1]: https://github.com/traackr/binnacle/tree/0.7.1 147 | [0.7.0]: https://github.com/traackr/binnacle/tree/0.7.0 148 | [0.6.0]: https://github.com/traackr/binnacle/tree/0.6.0 149 | [0.5.1]: https://github.com/traackr/binnacle/tree/0.5.1 150 | [0.5.0]: https://github.com/traackr/binnacle/tree/0.5.0 151 | [0.4.0]: https://github.com/traackr/binnacle/tree/0.4.0 152 | [0.3.1]: https://github.com/traackr/binnacle/tree/0.3.1 153 | [0.3.0]: https://github.com/traackr/binnacle/tree/0.3.0 154 | [0.2.1]: https://github.com/traackr/binnacle/tree/0.2.1 155 | [0.2.0]: https://github.com/traackr/binnacle/tree/0.2.0 156 | [0.1.0]: https://github.com/traackr/binnacle/tree/0.1.0 157 | [0.0.5]: https://github.com/traackr/binnacle/tree/0.0.5 158 | [0.0.4]: https://github.com/traackr/binnacle/tree/0.0.4 159 | [0.0.3]: https://github.com/traackr/binnacle/tree/0.0.3 160 | [0.0.2]: https://github.com/traackr/binnacle/tree/0.0.2 161 | [0.0.1]: https://github.com/traackr/binnacle/tree/0.0.1 162 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Anthony Spring 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build clean test 2 | 3 | build: 4 | @scripts/build.sh 5 | 6 | clean: 7 | @rm -rf bin pkg 8 | 9 | test: 10 | @go test -v ./... -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # binnacle [![Release][release-image]][release-url] [![Test](https://github.com/Traackr/binnacle/actions/workflows/test.yml/badge.svg)](https://github.com/Traackr/binnacle/actions/workflows/test.yml) 2 | 3 | `binnacle` is an opinionated automation tool used to interact with Kubernetes' [Helm][helm]. By offering a single file to manage one or many charts, you can easily control all aspects of your Helm managed applications. 4 | 5 | `binnacle` is similar in nature to [Helmfile][helmfile] with a slightly different approach to managing Helm Charts. 6 | 7 | ## Installation 8 | 9 | A binary for various operating systems is available through [Github Releases][github-releases]. Download the appropriate archive, and extract into a directory within your PATH. 10 | 11 | ## Usage 12 | 13 | For the full list of options: 14 | 15 | ```shell 16 | binnacle --help 17 | ``` 18 | 19 | To see the version of `binnacle` you can use the following: 20 | 21 | ```shell 22 | binnacle --version 23 | ``` 24 | 25 | ## Getting Started 26 | 27 | ### Configuration File Format 28 | 29 | Configuration files can be written in YAML, TOML or JSON. 30 | 31 | ```yaml 32 | --- 33 | # charts takes a list of chart configurations 34 | charts: 35 | # This is the name of the chart 36 | - name: concourse 37 | # This is the namespace into which the chart is launched 38 | namespace: apps 39 | # This is the name for the release of this chart 40 | release: apps-concourse 41 | # This is the name of the repository within which the helm chart is located 42 | repo: stable 43 | # This determines if the release is created or removed. Default: present Options: absent, present 44 | state: present 45 | # Any data under values are passed to Helm to configure the given chart 46 | values: 47 | image: concourse/concourse 48 | imageTag: "3.10.0" 49 | # This is the version of the Helm chart. If this is omitted, the latest is used. 50 | version: 1.3.1 51 | 52 | # repositories takes a list of repository configurations 53 | repositories: 54 | # This is the name of the repository 55 | - name: stable 56 | # This is the URL of the repository 57 | url: https://kubernetes-charts.storage.googleapis.com 58 | # This determines if the repository is created or removed. Default: present Options: absent, present 59 | state: present 60 | ``` 61 | 62 | ### Using Binnacle 63 | 64 | The standard workflow when using binnacle is to use the `template` command to verify the desired configuration files are generated, use the `sync` command to create/update the existing release configuration within Helm, and `status` to get the status of a release. 65 | 66 | Using the configuration available at `test-data/demo.yml` you can run the template command: 67 | 68 | ```bash 69 | $ binnacle template -c ./test-data/demo.yml 70 | Loading config file: ./test-data/demo.yml 71 | --- 72 | # Source: concourse/templates/namespace.yaml 73 | 74 | --- 75 | apiVersion: v1 76 | kind: Namespace 77 | metadata: 78 | annotations: 79 | "helm.sh/resource-policy": keep 80 | name: apps-concourse-main 81 | labels: 82 | ... 83 | ``` 84 | 85 | By reviewing the output you are able to verify that you have specificied all of the necessary configuration aspects of the chart. Once you are happy with how the chart is configured you can `sync` the charts to Helm: 86 | 87 | ```bash 88 | 89 | $ binnacle sync -c ./test-data/demo.yml 90 | Loading config file: ./test-data/demo.yml 91 | "stable" has been added to your repositories 92 | Release "apps-concourse" does not exist. Installing it now. 93 | NAME: apps-concourse 94 | LAST DEPLOYED: Fri Apr 27 14:24:19 2018 95 | NAMESPACE: apps 96 | STATUS: DEPLOYED 97 | 98 | RESOURCES: 99 | ==> v1beta1/StatefulSet 100 | NAME DESIRED CURRENT AGE 101 | apps-concourse-worker 2 0 1s 102 | 103 | ==> v1/Pod(related) 104 | NAME READY STATUS RESTARTS AGE 105 | apps-concourse-postgresql-5f964dd587-6ng8f 0/1 Pending 0 1s 106 | apps-concourse-web-5dd649b7f6-7v78w 0/1 ContainerCreating 0 1s 107 | 108 | ==> v1/Namespace 109 | NAME STATUS AGE 110 | apps-concourse-main Active 1s 111 | 112 | ==> v1/Secret 113 | NAME TYPE DATA AGE 114 | apps-concourse-postgresql Opaque 1 1s 115 | apps-concourse-concourse Opaque 7 1s 116 | 117 | ==> v1/PersistentVolumeClaim 118 | NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE 119 | apps-concourse-postgresql Pending 1s 120 | 121 | ==> v1beta1/ClusterRole 122 | NAME AGE 123 | apps-concourse-web 1s 124 | 125 | ==> v1/Service 126 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 127 | apps-concourse-postgresql ClusterIP 10.233.89.90 5432/TCP 1s 128 | apps-concourse-web ClusterIP 10.233.57.192 8080/TCP,2222/TCP 1s 129 | apps-concourse-worker ClusterIP None 1s 130 | 131 | ==> v1/ServiceAccount 132 | NAME SECRETS AGE 133 | apps-concourse-web 1 1s 134 | apps-concourse-worker 1 1s 135 | 136 | ==> v1beta1/Role 137 | NAME AGE 138 | apps-concourse-worker 1s 139 | 140 | ==> v1beta1/RoleBinding 141 | NAME AGE 142 | apps-concourse-web-main 1s 143 | apps-concourse-worker 1s 144 | 145 | ==> v1beta1/Deployment 146 | NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE 147 | apps-concourse-postgresql 1 1 1 0 1s 148 | apps-concourse-web 1 1 1 0 1s 149 | 150 | ==> v1beta1/PodDisruptionBudget 151 | NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE 152 | apps-concourse-worker 1 N/A 0 1s 153 | 154 | 155 | NOTES: 156 | 157 | * Concourse can be accessed: 158 | 159 | * Within your cluster, at the following DNS name at port 8080: 160 | 161 | apps-concourse-web.apps.svc.cluster.local 162 | 163 | * From outside the cluster, run these commands in the same shell: 164 | 165 | export POD_NAME=$(kubectl get pods --namespace apps -l "app=apps-concourse-web" -o jsonpath="{.items[0].metadata.name}") 166 | echo "Visit http://127.0.0.1:8080 to use Concourse" 167 | kubectl port-forward --namespace apps $POD_NAME 8080:8080 168 | 169 | * Login with the following credentials 170 | 171 | Username: concourse 172 | Password: concourse 173 | 174 | * If this is your first time using Concourse, follow the tutorial at https://concourse-ci.org/hello-world.html 175 | 176 | ******************* 177 | ******WARNING****** 178 | ******************* 179 | 180 | You are using the "naive" baggage claim driver, which is also the default value for this chart. This is the default for compatibility reasons, but is very space inefficient, and should be changed to either "btrfs" (recommended) or "overlay" depending on that filesystem's support in the Linux kernel your cluster is using. Please see https://github.com/concourse/concourse/issues/1230 and https://github.com/concourse/concourse/issues/1966 for background. 181 | ``` 182 | 183 | From the output you can see that the release did not exist "apps-concourse" so Helm created it. To get updates on the status of the deployment you can use the `status` command: 184 | 185 | ```bash 186 | 187 | $ binnacle status -c ./test-data/demo.yml 188 | Loading config file: ./test-data/demo.yml 189 | LAST DEPLOYED: Fri Apr 27 14:24:19 2018 190 | NAMESPACE: apps 191 | STATUS: DEPLOYED 192 | 193 | RESOURCES: 194 | ==> v1/Pod(related) 195 | NAME READY STATUS RESTARTS AGE 196 | apps-concourse-postgresql-5f964dd587-6ng8f 0/1 Pending 0 1m 197 | apps-concourse-web-5dd649b7f6-7v78w 0/1 Running 0 1m 198 | 199 | ==> v1/Namespace 200 | NAME STATUS AGE 201 | apps-concourse-main Active 1m 202 | 203 | ==> v1/Secret 204 | NAME TYPE DATA AGE 205 | apps-concourse-postgresql Opaque 1 1m 206 | apps-concourse-concourse Opaque 7 1m 207 | 208 | ==> v1beta1/Role 209 | NAME AGE 210 | apps-concourse-worker 1m 211 | 212 | ==> v1/Service 213 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 214 | apps-concourse-postgresql ClusterIP 10.233.89.90 5432/TCP 1m 215 | apps-concourse-web ClusterIP 10.233.57.192 8080/TCP,2222/TCP 1m 216 | apps-concourse-worker ClusterIP None 1m 217 | 218 | ==> v1beta1/PodDisruptionBudget 219 | NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE 220 | apps-concourse-worker 1 N/A 0 1m 221 | 222 | ==> v1beta1/StatefulSet 223 | NAME DESIRED CURRENT AGE 224 | apps-concourse-worker 2 2 1m 225 | 226 | ==> v1/PersistentVolumeClaim 227 | NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE 228 | apps-concourse-postgresql Pending 1m 229 | 230 | ==> v1/ServiceAccount 231 | NAME SECRETS AGE 232 | apps-concourse-web 1 1m 233 | apps-concourse-worker 1 1m 234 | 235 | ==> v1beta1/ClusterRole 236 | NAME AGE 237 | apps-concourse-web 1m 238 | 239 | ==> v1beta1/RoleBinding 240 | NAME AGE 241 | apps-concourse-web-main 1m 242 | apps-concourse-worker 1m 243 | 244 | ==> v1beta1/Deployment 245 | NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE 246 | apps-concourse-postgresql 1 1 1 0 1m 247 | apps-concourse-web 1 1 1 0 1m 248 | 249 | 250 | NOTES: 251 | 252 | * Concourse can be accessed: 253 | 254 | * Within your cluster, at the following DNS name at port 8080: 255 | 256 | apps-concourse-web.apps.svc.cluster.local 257 | 258 | * From outside the cluster, run these commands in the same shell: 259 | 260 | export POD_NAME=$(kubectl get pods --namespace apps -l "app=apps-concourse-web" -o jsonpath="{.items[0].metadata.name}") 261 | echo "Visit http://127.0.0.1:8080 to use Concourse" 262 | kubectl port-forward --namespace apps $POD_NAME 8080:8080 263 | 264 | * Login with the following credentials 265 | 266 | Username: concourse 267 | Password: concourse 268 | 269 | * If this is your first time using Concourse, follow the tutorial at https://concourse-ci.org/hello-world.html 270 | 271 | ******************* 272 | ******WARNING****** 273 | ******************* 274 | 275 | You are using the "naive" baggage claim driver, which is also the default value for this chart. This is the default for compatibility reasons, but is very space inefficient, and should be changed to either "btrfs" (recommended) or "overlay" depending on that filesystem's support in the Linux kernel your cluster is using. Please see https://github.com/concourse/concourse/issues/1230 and https://github.com/concourse/concourse/issues/1966 for background. 276 | ``` 277 | 278 | If you want to remove a release, you can change the `state` from `present` to `absent` and run the `sync` command: 279 | 280 | ```bash 281 | $ binnacle sync -c ./test-data/demo.yml 282 | Loading config file: ./test-data/demo.yml 283 | "stable" has been added to your repositories 284 | These resources were kept due to the resource policy: 285 | [Namespace] apps-concourse-main 286 | 287 | release "apps-concourse" deleted 288 | ``` 289 | 290 | ## Development 291 | 292 | Prerequisites: 293 | 294 | - go 1.17 295 | 296 | ### Local Go Environment 297 | 298 | First, check out the repository: 299 | 300 | ```script 301 | git clone https://github.com/Traackr/binnacle.git 302 | cd binnacle 303 | ``` 304 | 305 | To compile a version of binnacle for your local machine you can run: 306 | 307 | ```script 308 | make build 309 | ``` 310 | 311 | This will generate a binary within the ./bin directory of the project. 312 | 313 | To run the unit tests: 314 | 315 | ```script 316 | make test 317 | ``` 318 | 319 | [github-releases]: https://github.com/Traackr/binnacle/releases 320 | [helm]: https://helm.sh/ 321 | [helmfile]: https://github.com/roboll/helmfile 322 | [release-url]: https://github.com/Traackr/binnacle/releases/latest 323 | [release-image]: https://img.shields.io/github/release/Traackr/binnacle.svg -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.8.1 -------------------------------------------------------------------------------- /cmd/diff.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Anthony Spring 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "os" 26 | "strings" 27 | 28 | "github.com/Traackr/binnacle/config" 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | // diffCmd represents the diff command 33 | var diffCmd = &cobra.Command{ 34 | Use: "diff", 35 | Short: "Displays a diff between the current release and new release of a Helm chart. (Requires helm-diff plugin)", 36 | Long: ``, 37 | PreRun: func(cmd *cobra.Command, args []string) { 38 | diffCmdPreRun() 39 | }, 40 | RunE: func(cmd *cobra.Command, args []string) error { 41 | return diffCmdRun(args...) 42 | }, 43 | PostRun: func(cmd *cobra.Command, args []string) { 44 | diffCmdPostRun() 45 | }, 46 | } 47 | 48 | func init() { 49 | RootCmd.AddCommand(diffCmd) 50 | } 51 | 52 | func diffCmdPreRun() { 53 | log.Debug("Executing `diff` command.") 54 | } 55 | 56 | func diffCmdRun(args ...string) error { 57 | var err error 58 | 59 | // Detect if the diff plugin is installed. 60 | pluginInstalled, err := PluginInstalled("diff") 61 | if err != nil { 62 | return fmt.Errorf("detecting if helm-diff plugin is installed: %w", err) 63 | } 64 | 65 | if !pluginInstalled { 66 | return fmt.Errorf("checking for helm-diff plugin: helm-diff plugin is required, Please see: https://github.com/databus23/helm-diff") 67 | } 68 | 69 | // Load our configuration 70 | c, err := config.LoadAndValidateFromViper() 71 | if err != nil { 72 | return err 73 | } 74 | 75 | // Sync repositories 76 | if err := syncRepositories(c.Repositories, args...); err != nil { 77 | return err 78 | } 79 | 80 | var charts = c.Charts 81 | 82 | log.Debugf("Loaded %d charts.", len(charts)) 83 | 84 | // Iterate the charts in the config 85 | for _, chart := range charts { 86 | var cmdArgs []string 87 | var res Result 88 | 89 | log.Debugf("Processing chart: %s", chart.ChartURL()) 90 | 91 | // Create a temp working directory 92 | dir, err := SetupBinnacleWorkingDir() 93 | if err != nil { 94 | return err 95 | } 96 | defer os.RemoveAll(dir) 97 | 98 | // 99 | // Template out the charts values 100 | // 101 | valuesFile, err := chart.WriteValueFile(dir) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | cmdArgs = append(cmdArgs, "diff") 107 | cmdArgs = append(cmdArgs, "upgrade") 108 | cmdArgs = append(cmdArgs, chart.Release) 109 | cmdArgs = append(cmdArgs, chart.ChartURL()) 110 | cmdArgs = append(cmdArgs, "--color") 111 | cmdArgs = append(cmdArgs, "--normalize-manifests") 112 | cmdArgs = append(cmdArgs, "--install") 113 | cmdArgs = append(cmdArgs, "--three-way-merge") 114 | cmdArgs = append(cmdArgs, "--values") 115 | cmdArgs = append(cmdArgs, valuesFile) 116 | 117 | if len(chart.Namespace) > 0 { 118 | cmdArgs = append(cmdArgs, "--namespace") 119 | cmdArgs = append(cmdArgs, chart.Namespace) 120 | } 121 | 122 | if len(chart.Version) > 0 { 123 | cmdArgs = append(cmdArgs, "--version") 124 | cmdArgs = append(cmdArgs, chart.Version) 125 | } 126 | 127 | if !chart.Kustomize.Empty() { 128 | postRenderExecutable, err := SetupKustomize(dir, c.ConfigFile, chart) 129 | if err != nil { 130 | return err 131 | } 132 | cmdArgs = append(cmdArgs, "--post-renderer") 133 | cmdArgs = append(cmdArgs, postRenderExecutable) 134 | } 135 | 136 | cmdArgs = append(cmdArgs, args...) 137 | res, err = RunHelmCommand(cmdArgs...) 138 | if err != nil { 139 | return fmt.Errorf("running helm diff for release %s: %s: %w", chart.Release, res.Stderr, err) 140 | } 141 | 142 | fmt.Println(strings.TrimSpace(res.Stdout)) 143 | } 144 | 145 | return nil 146 | } 147 | 148 | func diffCmdPostRun() { 149 | log.Debug("Execution of the `diff` command has completed.") 150 | } 151 | -------------------------------------------------------------------------------- /cmd/result.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Anthony Spring 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | // Result definition 24 | type Result struct { 25 | Stdout string 26 | Stderr string 27 | ExitCode int 28 | } 29 | 30 | func (r *Result) Error() string { 31 | return r.Stderr 32 | } 33 | 34 | // NewResult definition 35 | func NewResult() *Result { 36 | return &Result{ExitCode: 1} 37 | } 38 | -------------------------------------------------------------------------------- /cmd/result_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Anthony Spring 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "testing" 25 | ) 26 | 27 | func TestNewResult(t *testing.T) { 28 | got := NewResult().ExitCode 29 | want := 1 30 | if got != want { 31 | t.Errorf("want NewResult exit code to be %d, got %d", want, got) 32 | } 33 | } 34 | func TestGetError(t *testing.T) { 35 | var result Result 36 | var testData = "foo" 37 | 38 | result.Stderr = testData 39 | 40 | got := result.Error() 41 | want := testData 42 | if got != want { 43 | t.Errorf("want result.Error to be %s, got %s", want, got) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Anthony Spring 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "bytes" 25 | "fmt" 26 | "os" 27 | "os/exec" 28 | "path/filepath" 29 | "strings" 30 | 31 | "github.com/Traackr/binnacle/config" 32 | "github.com/google/uuid" 33 | "github.com/sirupsen/logrus" 34 | "github.com/spf13/cobra" 35 | "github.com/spf13/viper" 36 | "gopkg.in/yaml.v3" 37 | ) 38 | 39 | var cfgFile string 40 | 41 | // log The general purpose logging interface available to all commands 42 | var log = logrus.New() 43 | 44 | // GITCOMMIT The gitcommit the application was built from 45 | var GITCOMMIT string 46 | 47 | // VERSION The version of the application 48 | var VERSION string 49 | 50 | // RootCmd represents the base command when called without any subcommands 51 | var RootCmd = &cobra.Command{ 52 | Use: "binnacle", 53 | Short: "An opinionated automation tool for Kubernetes' Helm.", 54 | Long: ``, 55 | SilenceUsage: true, 56 | Version: fmt.Sprintf("%s-%s", VERSION, GITCOMMIT), 57 | } 58 | 59 | // Execute adds all child commands to the root command sets flags appropriately. 60 | // This is called by main.main(). It only needs to happen once to the rootCmd. 61 | func Execute() { 62 | if err := RootCmd.Execute(); err != nil { 63 | os.Exit(1) 64 | } 65 | } 66 | 67 | func init() { 68 | cobra.OnInitialize(initConfig) 69 | // General Flags 70 | RootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "The Binnacle config file (required)") 71 | RootCmd.MarkFlagRequired("config") 72 | 73 | // Logging Flags 74 | RootCmd.PersistentFlags().String("loglevel", "info", "The level of logging. Acceptable values: debug, info, warn, error, fatal, panic.") 75 | viper.BindPFlag("loglevel", RootCmd.PersistentFlags().Lookup("loglevel")) 76 | } 77 | 78 | func initConfig() { 79 | if cfgFile == "" { 80 | log.Fatal("no configuration file specified") 81 | } 82 | 83 | viper.SetConfigFile(cfgFile) 84 | viper.AddConfigPath(".") // check current dir 85 | viper.AutomaticEnv() // read in environment variables that match 86 | 87 | // If a config file is found, read it in. 88 | if err := viper.ReadInConfig(); err != nil { 89 | log.Fatalf("Failed to load configuration file '%s': %v", viper.ConfigFileUsed(), err) 90 | } 91 | 92 | fmt.Println("Loaded config file:", viper.ConfigFileUsed()) 93 | 94 | // Initialize the logger for all commands to use 95 | logLevel, _ := logrus.ParseLevel(viper.GetString("loglevel")) 96 | log.Level = logLevel 97 | log.Debug("Logger initialized.") 98 | 99 | } 100 | 101 | // PluginInstalled returns if the given plugin is installed 102 | func PluginInstalled(plugin string) (bool, error) { 103 | var err error 104 | var output []string 105 | var res Result 106 | 107 | // Get a list of currently installed plugins 108 | res, err = RunHelmCommand("plugin", "list") 109 | if err != nil { 110 | return false, fmt.Errorf("running helm plugin list: %s: %w", res.Stderr, err) 111 | } 112 | 113 | // Split the output on the new line 114 | output = strings.Split(res.Stdout, "\n") 115 | 116 | // Remove the column titles 117 | if len(output) > 0 { 118 | output = output[1:] 119 | } 120 | 121 | // Iterate the plugins 122 | for _, line := range output { 123 | if len(line) == 0 { 124 | continue 125 | } 126 | 127 | // Split the string by a space 128 | split := strings.Fields(line) 129 | 130 | if plugin == split[0] { 131 | return true, nil 132 | } 133 | } 134 | 135 | return false, nil 136 | } 137 | 138 | func ReleaseExists(namespace string, release string, args ...string) bool { 139 | var exists = true 140 | var err error 141 | var res Result 142 | var cmdArgs []string 143 | 144 | cmdArgs = append(cmdArgs, "status") 145 | cmdArgs = append(cmdArgs, release) 146 | cmdArgs = append(cmdArgs, "--namespace") 147 | cmdArgs = append(cmdArgs, namespace) 148 | cmdArgs = append(cmdArgs, args...) 149 | 150 | // Get the status of the release for the namespace 151 | res, err = RunHelmCommand(cmdArgs...) 152 | if err != nil { 153 | if res.Stderr != "Error: release: not found" { 154 | exists = false 155 | } 156 | } 157 | 158 | return exists 159 | } 160 | 161 | // RunHelmCommand runs the given command against helm 162 | func RunHelmCommand(args ...string) (Result, error) { 163 | var result Result 164 | var outbuf, errbuf bytes.Buffer 165 | 166 | helm, err := exec.LookPath("helm") 167 | if err != nil { 168 | return result, fmt.Errorf("searching for helm on PATH: %w", err) 169 | } 170 | cmd := exec.Command(helm, args...) 171 | 172 | log.Debugf("Executing command: %v", cmd.Args) 173 | 174 | cmd.Stdout = &outbuf 175 | cmd.Stderr = &errbuf 176 | 177 | err = cmd.Run() 178 | 179 | log.Debugf("Execution complete.") 180 | 181 | result.Stdout = strings.Trim(outbuf.String(), " ") 182 | result.Stderr = strings.Trim(errbuf.String(), " ") 183 | 184 | if err != nil { 185 | // 186 | // This crafty snippet is from https://stackoverflow.com/a/55055100 187 | // 188 | if exitError, ok := err.(*exec.ExitError); ok { 189 | result.ExitCode = exitError.ExitCode() 190 | } else { 191 | if result.Stderr == "" { 192 | result.Stderr = err.Error() 193 | } 194 | } 195 | } 196 | 197 | return result, err 198 | } 199 | 200 | func syncRepositories(repos []config.RepositoryConfig, args ...string) error { 201 | var reposModified = false 202 | 203 | for _, repo := range repos { 204 | var cmdArgs []string 205 | var err error 206 | var res Result 207 | var currentRepos []config.RepositoryConfig 208 | 209 | log.Debugf("Processing repo: %s", repo.Name) 210 | 211 | currentRepos, err = getCurrentRepositories() 212 | if err != nil { 213 | return err 214 | } 215 | 216 | repoExists, repoFullMatch := repoExists(repo, currentRepos) 217 | 218 | // If the repo exists and is not a full match, or we are deleting the repo - we need to delete the repo 219 | if repoExists && (!repoFullMatch || repo.State != config.StatePresent) { 220 | cmdArgs = append(cmdArgs, "repo") 221 | cmdArgs = append(cmdArgs, "remove") 222 | cmdArgs = append(cmdArgs, args...) 223 | cmdArgs = append(cmdArgs, repo.Name) 224 | 225 | res, err = RunHelmCommand(cmdArgs...) 226 | if err != nil { 227 | return fmt.Errorf("running helm repo remove: %s: %w", res.Stderr, err) 228 | } 229 | } 230 | cmdArgs = nil 231 | 232 | if repo.State == config.StatePresent { 233 | cmdArgs = append(cmdArgs, "repo") 234 | cmdArgs = append(cmdArgs, "add") 235 | cmdArgs = append(cmdArgs, args...) 236 | cmdArgs = append(cmdArgs, repo.Name) 237 | cmdArgs = append(cmdArgs, repo.URL) 238 | 239 | res, err = RunHelmCommand(cmdArgs...) 240 | if err != nil { 241 | return fmt.Errorf("running helm repo add: %s: %w", res.Stderr, err) 242 | } 243 | reposModified = true 244 | 245 | fmt.Println(strings.TrimSpace(res.Stdout)) 246 | } 247 | } 248 | 249 | // If any repos have been added during this sync execute a helm repos update to update the cache. 250 | if reposModified { 251 | var cmdArgs []string 252 | var err error 253 | var res Result 254 | 255 | cmdArgs = append(cmdArgs, "repo") 256 | cmdArgs = append(cmdArgs, "update") 257 | res, err = RunHelmCommand(cmdArgs...) 258 | if err != nil { 259 | return fmt.Errorf("running helm repo update: %s: %w", res.Stderr, err) 260 | } 261 | fmt.Println(strings.TrimSpace(res.Stdout)) 262 | } 263 | 264 | return nil 265 | } 266 | 267 | func getCurrentRepositories() ([]config.RepositoryConfig, error) { 268 | var err error 269 | var output []string 270 | var repos []config.RepositoryConfig 271 | var res Result 272 | 273 | // Get a list of currently configured repositories 274 | res, err = RunHelmCommand("repo", "list") 275 | if err != nil { 276 | return nil, fmt.Errorf("running helm repo list: %s: %w", res.Stderr, err) 277 | } 278 | 279 | // Split the output on the new line 280 | output = strings.Split(res.Stdout, "\n") 281 | 282 | // Remove the column titles 283 | if len(output) > 0 { 284 | output = output[1:] 285 | } 286 | 287 | // Populate the repos 288 | for _, line := range output { 289 | var repo config.RepositoryConfig 290 | 291 | if len(line) == 0 { 292 | continue 293 | } 294 | 295 | // Split the string by a space 296 | split := strings.Fields(line) 297 | 298 | // Build the repository config 299 | repo.Name = split[0] 300 | repo.URL = split[1] 301 | 302 | repos = append(repos, repo) 303 | } 304 | 305 | return repos, nil 306 | } 307 | 308 | func repoExists(repo config.RepositoryConfig, repos []config.RepositoryConfig) (bool, bool) { 309 | var exists = false 310 | var fullMatch = false 311 | 312 | // Check if this repo already exists 313 | for _, checkRepo := range repos { 314 | // Check if the repos are equal 315 | if repo.Equal(checkRepo) { 316 | exists = true 317 | fullMatch = true 318 | break 319 | } else { 320 | // Check if their names match but URLs are different 321 | if repo.Name == checkRepo.Name { 322 | exists = true 323 | if repo.URL != checkRepo.URL { 324 | fullMatch = true 325 | } 326 | break 327 | } 328 | } 329 | 330 | } 331 | 332 | return exists, fullMatch 333 | } 334 | 335 | func SetupBinnacleWorkingDir() (string, error) { 336 | dir, err := os.MkdirTemp("", "binnacle-exec") 337 | if err != nil { 338 | return "", fmt.Errorf("creating temporary working directory: %w", err) 339 | } 340 | 341 | return dir, nil 342 | } 343 | 344 | // Set up the kustomize post-renderer script and kustomization.yml 345 | func SetupKustomize(tmpDir string, configPath string, chart config.ChartConfig) (string, error) { 346 | kustomize, err := exec.LookPath("kustomize") 347 | if err != nil { 348 | return "", fmt.Errorf("configuring kustomize: kustomize was not installed") 349 | } 350 | 351 | // Use a random filename to prevent conflicts with any actual filenames 352 | helmTemplateFilename := fmt.Sprintf("%s.yml", uuid.New()) 353 | // Script that reads stdin (result of helm template) and runs kustomize, 354 | // which will write the result to stdout, returning it to Helm 355 | // NOTE: The script will be executed by Helm, using the current PATH and current working directory 356 | script := fmt.Sprintf(`#!/bin/sh 357 | cat > %s 358 | exec %s build %s 359 | `, filepath.Join(tmpDir, helmTemplateFilename), kustomize, tmpDir) 360 | scriptPath := filepath.Join(tmpDir, "exec-kustomize.sh") 361 | err = os.WriteFile(scriptPath, []byte(script), 0755) 362 | if err != nil { 363 | return "", fmt.Errorf("writing exec-kustomize script: %w", err) 364 | } 365 | 366 | binnacleFilesDir := filepath.Dir(configPath) 367 | 368 | // Fix relative paths (relative to binnacle dir) to be accessible from the tmp dir 369 | resources := chart.Kustomize.Resources 370 | for i, r := range resources { 371 | resourcePath := filepath.Join(binnacleFilesDir, r) 372 | data, err := os.ReadFile(resourcePath) 373 | if err != nil { 374 | return "", fmt.Errorf("reading kustomize resource file: %w", err) 375 | } 376 | tmpResourcePath := filepath.Join(tmpDir, filepath.Base(r)) 377 | err = os.WriteFile(tmpResourcePath, data, 0644) 378 | if err != nil { 379 | return "", fmt.Errorf("writing temporary kustomize resource file: %w", err) 380 | } 381 | resources[i] = filepath.Base(r) 382 | } 383 | // Add in the file with the helm-templated contents 384 | resources = append(resources, helmTemplateFilename) 385 | 386 | patches := chart.Kustomize.Patches 387 | if len(patches) == 0 { 388 | patches = make([]config.Patch, 0) 389 | } 390 | for i, p := range patches { 391 | if len(p.Path) == 0 { 392 | continue 393 | } 394 | 395 | patchPath := filepath.Join(binnacleFilesDir, p.Path) 396 | data, err := os.ReadFile(patchPath) 397 | if err != nil { 398 | return "", fmt.Errorf("reading kustomize patch file: %w", err) 399 | } 400 | tmpPatchPath := filepath.Join(tmpDir, filepath.Base(p.Path)) 401 | err = os.WriteFile(tmpPatchPath, data, 0644) 402 | if err != nil { 403 | return "", fmt.Errorf("writing temporary kustomize patch file: %w", err) 404 | } 405 | patches[i].Path = filepath.Base(p.Path) 406 | } 407 | 408 | kustomizationData, err := yaml.Marshal(config.BinnacleKustomization{ 409 | Resources: resources, 410 | Patches: patches, 411 | }) 412 | if err != nil { 413 | return "", fmt.Errorf("marshalling generated kustomization.yml to yaml: %w", err) 414 | } 415 | 416 | kustomizationYmlPath := filepath.Join(tmpDir, "kustomization.yml") 417 | log.Debugf("kustomization.yml: \n%s", string(kustomizationData)) 418 | err = os.WriteFile(kustomizationYmlPath, kustomizationData, 0644) 419 | if err != nil { 420 | return "", fmt.Errorf("writing generated kustomization.yml: %w", err) 421 | } 422 | 423 | return scriptPath, nil 424 | } 425 | -------------------------------------------------------------------------------- /cmd/status.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Anthony Spring 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "strings" 26 | 27 | "github.com/Traackr/binnacle/config" 28 | "github.com/spf13/cobra" 29 | ) 30 | 31 | // statusCmd represents the status command 32 | var statusCmd = &cobra.Command{ 33 | Use: "status", 34 | Short: "Displays the `helm` status for each release within the given Binnacle configuration", 35 | Long: ``, 36 | PreRun: func(cmd *cobra.Command, args []string) { 37 | statusCmdPreRun() 38 | }, 39 | RunE: func(cmd *cobra.Command, args []string) error { 40 | return statusCmdRun(args...) 41 | }, 42 | PostRun: func(cmd *cobra.Command, args []string) { 43 | statusCmdPostRun() 44 | }, 45 | } 46 | 47 | func init() { 48 | RootCmd.AddCommand(statusCmd) 49 | } 50 | 51 | func statusCmdPreRun() { 52 | log.Debug("Executing `status` command.") 53 | } 54 | 55 | func statusCmdRun(args ...string) error { 56 | 57 | // Load our configuration 58 | c, err := config.LoadAndValidateFromViper() 59 | if err != nil { 60 | return err 61 | } 62 | 63 | var charts = c.Charts 64 | 65 | log.Debugf("Loaded %d charts.", len(charts)) 66 | 67 | // Iterate the charts in the config 68 | for _, chart := range charts { 69 | var cmdArgs []string 70 | var res Result 71 | 72 | log.Debugf("Processing chart: %s", chart.ChartURL()) 73 | 74 | cmdArgs = append(cmdArgs, "status") 75 | cmdArgs = append(cmdArgs, chart.Release) 76 | cmdArgs = append(cmdArgs, args...) 77 | 78 | if len(chart.Namespace) > 0 { 79 | cmdArgs = append(cmdArgs, "--namespace") 80 | cmdArgs = append(cmdArgs, chart.Namespace) 81 | } 82 | 83 | res, err = RunHelmCommand(cmdArgs...) 84 | if err != nil { 85 | return fmt.Errorf("running helm status for release %s: %w", chart.Release, err) 86 | } 87 | 88 | fmt.Println(strings.TrimSpace(res.Stdout)) 89 | } 90 | 91 | return nil 92 | } 93 | 94 | func statusCmdPostRun() { 95 | log.Debug("Execution of the `status` command has completed.") 96 | } 97 | -------------------------------------------------------------------------------- /cmd/sync.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Anthony Spring 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "os" 26 | "strings" 27 | 28 | "github.com/Traackr/binnacle/config" 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | // syncCmd represents the status command 33 | var syncCmd = &cobra.Command{ 34 | Use: "sync", 35 | Short: "Syncs each release within the given Binnacle configuration with `helm`", 36 | Long: ``, 37 | PreRun: func(cmd *cobra.Command, args []string) { 38 | syncCmdPreRun() 39 | }, 40 | RunE: func(cmd *cobra.Command, args []string) error { 41 | return syncCmdRun(args...) 42 | }, 43 | PostRun: func(cmd *cobra.Command, args []string) { 44 | syncCmdPostRun() 45 | }, 46 | } 47 | 48 | func init() { 49 | RootCmd.AddCommand(syncCmd) 50 | } 51 | 52 | func syncCmdPreRun() { 53 | log.Debug("Executing `sync` command.") 54 | } 55 | 56 | func syncCmdRun(args ...string) error { 57 | // Load our configuration 58 | c, err := config.LoadAndValidateFromViper() 59 | if err != nil { 60 | return err 61 | } 62 | 63 | // Sync repositories 64 | if err := syncRepositories(c.Repositories, args...); err != nil { 65 | return err 66 | } 67 | 68 | // Sync charts 69 | if err := syncCharts(c.Charts, c.ConfigFile, args...); err != nil { 70 | return err 71 | } 72 | 73 | return nil 74 | } 75 | 76 | func syncCmdPostRun() { 77 | log.Debug("Execution of the `sync` command has completed.") 78 | } 79 | 80 | func syncCharts(charts []config.ChartConfig, configFile string, args ...string) error { 81 | for _, chart := range charts { 82 | var cmdArgs []string 83 | var res Result 84 | 85 | log.Debugf("Processing chart: %s", chart.ChartURL()) 86 | 87 | if chart.State == config.StatePresent { 88 | // Create a temp working directory 89 | dir, err := SetupBinnacleWorkingDir() 90 | if err != nil { 91 | return err 92 | } 93 | defer os.RemoveAll(dir) 94 | 95 | // 96 | // Template out the charts values 97 | // 98 | valuesFile, err := chart.WriteValueFile(dir) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | cmdArgs = append(cmdArgs, "upgrade") 104 | cmdArgs = append(cmdArgs, chart.Release) 105 | cmdArgs = append(cmdArgs, chart.ChartURL()) 106 | cmdArgs = append(cmdArgs, "-i") 107 | 108 | if len(chart.Namespace) > 0 { 109 | cmdArgs = append(cmdArgs, "--namespace") 110 | cmdArgs = append(cmdArgs, chart.Namespace) 111 | } 112 | 113 | cmdArgs = append(cmdArgs, "--values") 114 | cmdArgs = append(cmdArgs, valuesFile) 115 | if len(chart.Version) > 0 { 116 | cmdArgs = append(cmdArgs, "--version") 117 | cmdArgs = append(cmdArgs, chart.Version) 118 | } 119 | 120 | if !chart.Kustomize.Empty() { 121 | postRenderExecutable, err := SetupKustomize(dir, configFile, chart) 122 | if err != nil { 123 | return err 124 | } 125 | cmdArgs = append(cmdArgs, "--post-renderer") 126 | cmdArgs = append(cmdArgs, postRenderExecutable) 127 | } 128 | } else { 129 | 130 | // If the release does not exist do not attempt to delete the release 131 | exists := ReleaseExists(chart.Namespace, chart.Release, args...) 132 | if !exists { 133 | log.Infof("Skipping '%s/%s' as the release does not exist.", chart.Namespace, chart.Release) 134 | continue 135 | } 136 | 137 | cmdArgs = append(cmdArgs, "uninstall") 138 | cmdArgs = append(cmdArgs, chart.Release) 139 | cmdArgs = append(cmdArgs, "--namespace") 140 | cmdArgs = append(cmdArgs, chart.Namespace) 141 | } 142 | 143 | cmdArgs = append(cmdArgs, args...) 144 | 145 | res, err := RunHelmCommand(cmdArgs...) 146 | if err != nil { 147 | return fmt.Errorf("running helm sync for release %s: %s: %w", chart.Release, res.Stderr, err) 148 | } 149 | 150 | fmt.Println(strings.TrimSpace(res.Stdout)) 151 | } 152 | 153 | return nil 154 | } 155 | -------------------------------------------------------------------------------- /cmd/template.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Anthony Spring 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "os" 26 | "strings" 27 | 28 | "github.com/Traackr/binnacle/config" 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | // templateCmd represents the template command 33 | var templateCmd = &cobra.Command{ 34 | Use: "template", 35 | Short: "Templates out each release, with values, from a given Binnacle configuration.", 36 | Long: ``, 37 | PreRun: func(cmd *cobra.Command, args []string) { 38 | templateCmdPreRun() 39 | }, 40 | RunE: func(cmd *cobra.Command, args []string) error { 41 | return templateCmdRun(args...) 42 | }, 43 | PostRun: func(cmd *cobra.Command, args []string) { 44 | templateCmdPostRun() 45 | }, 46 | } 47 | 48 | func init() { 49 | RootCmd.AddCommand(templateCmd) 50 | } 51 | 52 | func templateCmdPreRun() { 53 | log.Debug("Executing `template` command.") 54 | } 55 | 56 | func templateCmdRun(args ...string) error { 57 | // Load our configuration 58 | c, err := config.LoadAndValidateFromViper() 59 | 60 | if err != nil { 61 | return err 62 | } 63 | 64 | // Sync repositories 65 | if err := syncRepositories(c.Repositories, args...); err != nil { 66 | return err 67 | } 68 | 69 | var charts = c.Charts 70 | 71 | var absentCharts []string 72 | 73 | log.Debugf("Loaded %d charts.", len(charts)) 74 | 75 | // Iterate the charts in the config 76 | for _, chart := range charts { 77 | var cmdArgs []string 78 | var res Result 79 | 80 | log.Debugf("Processing chart: %s", chart.ChartURL()) 81 | 82 | // If the state is not set to present add the namespace/release to the not rendered list 83 | if chart.State != config.StatePresent { 84 | absentCharts = append(absentCharts, chart.Namespace+"/"+chart.Release) 85 | continue 86 | } 87 | 88 | // Create a temp working directory 89 | dir, err := SetupBinnacleWorkingDir() 90 | if err != nil { 91 | return err 92 | } 93 | defer os.RemoveAll(dir) 94 | 95 | // 96 | // Template out the charts values 97 | // 98 | valuesFile, err := chart.WriteValueFile(dir) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | // 104 | // Template against the chart 105 | // 106 | cmdArgs = nil 107 | 108 | cmdArgs = append(cmdArgs, "template") 109 | 110 | // NAME 111 | cmdArgs = append(cmdArgs, chart.Release) 112 | 113 | // CHART 114 | cmdArgs = append(cmdArgs, chart.ChartURL()) 115 | 116 | // Add the namespace if given 117 | if len(chart.Namespace) > 0 { 118 | cmdArgs = append(cmdArgs, "--namespace") 119 | cmdArgs = append(cmdArgs, chart.Namespace) 120 | } 121 | 122 | cmdArgs = append(cmdArgs, "--values") 123 | cmdArgs = append(cmdArgs, valuesFile) 124 | 125 | if len(chart.Version) > 0 { 126 | cmdArgs = append(cmdArgs, "--version") 127 | cmdArgs = append(cmdArgs, chart.Version) 128 | } 129 | 130 | if !chart.Kustomize.Empty() { 131 | postRenderExecutable, err := SetupKustomize(dir, c.ConfigFile, chart) 132 | if err != nil { 133 | return err 134 | } 135 | cmdArgs = append(cmdArgs, "--post-renderer") 136 | cmdArgs = append(cmdArgs, postRenderExecutable) 137 | } 138 | 139 | cmdArgs = append(cmdArgs, args...) 140 | 141 | res, err = RunHelmCommand(cmdArgs...) 142 | if err != nil { 143 | return fmt.Errorf("running helm template for release %s: %s: %w", chart.Release, res.Stderr, err) 144 | } 145 | 146 | fmt.Println(strings.TrimSpace(res.Stdout)) 147 | } 148 | 149 | // Display output about the released that were not rendered 150 | if len(absentCharts) > 0 { 151 | log.Info("The following releases were set to absent and were not rendered.") 152 | for _, chart := range absentCharts { 153 | log.Infof(" %s", chart) 154 | } 155 | } 156 | 157 | return nil 158 | } 159 | 160 | func templateCmdPostRun() { 161 | log.Debug("Execution of the `template` command has completed.") 162 | } 163 | -------------------------------------------------------------------------------- /config/chart.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Anthony Spring 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package config 22 | 23 | import ( 24 | "fmt" 25 | "os" 26 | "path/filepath" 27 | 28 | "gopkg.in/yaml.v3" 29 | ) 30 | 31 | // ChartConfig definition 32 | type ChartConfig struct { 33 | Kustomize BinnacleKustomization `mapstructure:"kustomize"` 34 | Name string `mapstructure:"name"` 35 | Namespace string `mapstructure:"namespace"` 36 | Release string `mapstructure:"release"` 37 | Repo string `mapstructure:"repo"` 38 | State string `mapstructure:"state"` 39 | URL string `mapstructure:"url"` 40 | Values map[string]any `mapstructure:"values"` 41 | Version string `mapstructure:"version"` 42 | } 43 | 44 | // Adapted from https://github.com/kubernetes-sigs/kustomize/blob/master/api/types/kustomization.go 45 | type BinnacleKustomization struct { 46 | // https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/resource/ 47 | Resources []string `mapstructure:"resources,omitempty" yaml:"resources,omitempty"` 48 | 49 | // https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/patches/ 50 | Patches []Patch `mapstructure:"patches,omitempty" yaml:"patches,omitempty"` 51 | } 52 | 53 | type Patch struct { 54 | Path string `mapstructure:"path,omitempty" yaml:"path,omitempty"` 55 | Patch string `mapstructure:"patch,omitempty" yaml:"patch,omitempty"` 56 | Target *Selector `mapstructure:"target,omitempty" yaml:"target,omitempty"` 57 | Options map[string]bool `mapstructure:"options,omitempty" yaml:"options,omitempty"` 58 | } 59 | 60 | type Selector struct { 61 | ResId `mapstructure:",squash,omitempty" yaml:",inline,omitempty"` 62 | AnnotationSelector string `mapstructure:"annotationSelector,omitempty" yaml:"annotationSelector,omitempty"` 63 | LabelSelector string `mapstructure:"labelSelector,omitempty" yaml:"labelSelector,omitempty"` 64 | } 65 | 66 | type ResId struct { 67 | Gvk `mapstructure:",squash,omitempty" yaml:",inline,omitempty"` 68 | Name string `mapstructure:"name,omitempty" yaml:"name,omitempty"` 69 | Namespace string `mapstructure:"namespace,omitempty" yaml:"namespace,omitempty"` 70 | } 71 | 72 | type Gvk struct { 73 | Group string `mapstructure:"group,omitempty" yaml:"group,omitempty"` 74 | Version string `mapstructure:"version,omitempty" yaml:"version,omitempty"` 75 | Kind string `mapstructure:"kind,omitempty" yaml:"kind,omitempty"` 76 | } 77 | 78 | func (k BinnacleKustomization) Empty() bool { 79 | return len(k.Resources) == 0 && len(k.Patches) == 0 80 | } 81 | 82 | // ChartURL returns a URL related to the given repo and name of the chart based off of 83 | // criteria 1 through 4 of the following documentation on how to specify local and remote charts 84 | // 85 | // 1. By chart reference: helm install mymaria example/mariadb 86 | // 2. By path to a packaged chart: helm install mynginx ./nginx-1.2.3.tgz 87 | // 3. By path to an unpacked chart directory: helm install mynginx ./nginx 88 | // 4. By absolute URL: helm install mynginx https://example.com/charts/nginx-1.2.3.tgz 89 | // 90 | func (c ChartConfig) ChartURL() string { 91 | // If a repository is given return the c 92 | if len(c.Repo) > 0 { 93 | return c.Repo + "/" + c.Name 94 | } 95 | return c.Name 96 | } 97 | 98 | // WriteValueFile writes the given file containing the Chart's Values 99 | func (c ChartConfig) WriteValueFile(dir string) (string, error) { 100 | // Marshall the values into a string 101 | y, err := yaml.Marshal(c.Values) 102 | if err != nil { 103 | return "", fmt.Errorf("marshalling chart values: %w", err) 104 | } 105 | 106 | valuesYml := filepath.Join(dir, "values.yml") 107 | err = os.WriteFile(valuesYml, y, 0644) 108 | if err != nil { 109 | return "", fmt.Errorf("writing temporary values.yml file: %w", err) 110 | } 111 | 112 | return valuesYml, nil 113 | } 114 | -------------------------------------------------------------------------------- /config/chart_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Anthony Spring 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package config 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/spf13/viper" 27 | ) 28 | 29 | func TestChartURL_WithRepo(t *testing.T) { 30 | viper.SetConfigFile("../testdata/demo.yml") 31 | viper.ReadInConfig() 32 | c, _ := LoadAndValidateFromViper() 33 | 34 | got := c.Charts[0].ChartURL() 35 | want := "stable/concourse" 36 | if got != want { 37 | t.Errorf("want chart URL %s, but got %s", want, got) 38 | } 39 | } 40 | 41 | func TestChartURL_WithoutRepo(t *testing.T) { 42 | viper.SetConfigFile("../testdata/without-repo.yml") 43 | viper.ReadInConfig() 44 | c, _ := LoadAndValidateFromViper() 45 | 46 | got := c.Charts[0].ChartURL() 47 | want := "https://github.com/pantsel/konga/blob/master/charts/konga/konga-1.0.0.tgz?raw=true" 48 | if got != want { 49 | t.Errorf("want chart URL %s, but got %s", want, got) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Anthony Spring 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package config 22 | 23 | import ( 24 | "fmt" 25 | 26 | "github.com/spf13/viper" 27 | ) 28 | 29 | // StatePresent represents the present state 30 | const StatePresent = "present" 31 | 32 | // BinnacleConfig definition 33 | type BinnacleConfig struct { 34 | Charts []ChartConfig `mapstructure:"charts"` 35 | ConfigFile string 36 | Context string `mapstructure:"kube-context"` 37 | LogLevel string `mapstructure:"loglevel"` 38 | Release string `mapstructure:"release"` 39 | Repositories []RepositoryConfig `mapstructure:"repositories"` 40 | } 41 | 42 | // LoadAndValidateFromViper creates a BinnacleConfig object from Viper 43 | func LoadAndValidateFromViper() (*BinnacleConfig, error) { 44 | var config BinnacleConfig 45 | 46 | if err := viper.UnmarshalExact(&config); err != nil { 47 | return nil, fmt.Errorf("reading config file: %w", err) 48 | } 49 | 50 | config.ConfigFile = viper.ConfigFileUsed() 51 | 52 | // Set general defaults 53 | if len(config.Context) == 0 { 54 | config.Context = "default" 55 | } 56 | 57 | // Set defaults for charts 58 | for idx := range config.Charts { 59 | chart := &config.Charts[idx] 60 | 61 | if len(chart.Repo) == 0 { 62 | chart.Repo = "" 63 | } 64 | 65 | if len(chart.State) == 0 { 66 | chart.State = StatePresent 67 | } 68 | 69 | for k, v := range chart.Values { 70 | chart.Values[k] = cleanupMapValue(v) 71 | } 72 | } 73 | 74 | // Set defaults for repos 75 | for idx, repo := range config.Repositories { 76 | if len(repo.State) == 0 { 77 | config.Repositories[idx].State = StatePresent 78 | } 79 | } 80 | 81 | if err := validateConfig(&config); err != nil { 82 | return nil, err 83 | } 84 | 85 | return &config, nil 86 | } 87 | 88 | func cleanupInterfaceArray(in []interface{}) []interface{} { 89 | res := make([]interface{}, len(in)) 90 | for i, v := range in { 91 | res[i] = cleanupMapValue(v) 92 | } 93 | return res 94 | } 95 | 96 | func cleanupInterfaceMap(in map[interface{}]interface{}) map[string]interface{} { 97 | res := make(map[string]interface{}) 98 | for k, v := range in { 99 | res[fmt.Sprintf("%v", k)] = cleanupMapValue(v) 100 | } 101 | return res 102 | } 103 | 104 | // In order to marshal to JSON, map keys must be Strings even though YAML allows other types 105 | // Recursively walk through this value to transform map[interface{}]interface{} into map[string]interface{} 106 | // 107 | // See: https://github.com/go-yaml/yaml/issues/139 (supposedly a fix will be available in v3 of go-yaml) 108 | func cleanupMapValue(v interface{}) interface{} { 109 | switch v := v.(type) { 110 | case []interface{}: 111 | return cleanupInterfaceArray(v) 112 | case map[interface{}]interface{}: 113 | return cleanupInterfaceMap(v) 114 | default: 115 | return v 116 | } 117 | } 118 | 119 | func validateConfig(c *BinnacleConfig) error { 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Anthony Spring 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package config 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/spf13/viper" 27 | ) 28 | 29 | func TestBooleanIsNotCoerced(t *testing.T) { 30 | viper.SetConfigFile("../testdata/demo.yml") 31 | viper.ReadInConfig() 32 | c, _ := LoadAndValidateFromViper() 33 | 34 | ingressConfig := c.Charts[0].Values["ingress"].(map[string]interface{}) 35 | want := true 36 | got := ingressConfig["enabled"] 37 | if want != got { 38 | t.Errorf("want `ingress.enabled` to be type=%T value=%v, but got type=%T value=%v", want, want, got, got) 39 | } 40 | } 41 | 42 | func TestLoadAndValidateFromViper_Unmarshallable(t *testing.T) { 43 | viper.SetConfigFile("../testdata/unmarshallable.yml") 44 | viper.ReadInConfig() 45 | 46 | _, err := LoadAndValidateFromViper() 47 | if err == nil { 48 | t.Errorf("want an error for unmarshallable data, but was nil") 49 | } 50 | } 51 | 52 | func TestLoadAndValidateFromViper_DefaultChartState(t *testing.T) { 53 | viper.SetConfigFile("../testdata/default-state.yml") 54 | viper.ReadInConfig() 55 | 56 | c, _ := LoadAndValidateFromViper() 57 | got := c.Charts[0].State 58 | want := "present" 59 | if got != want { 60 | t.Errorf("want state to be %s, but got %s", want, got) 61 | } 62 | } 63 | 64 | func TestLoadAndValidateFromViper_DefaultRepoState(t *testing.T) { 65 | viper.SetConfigFile("../testdata/default-state.yml") 66 | viper.ReadInConfig() 67 | 68 | c, _ := LoadAndValidateFromViper() 69 | got := c.Repositories[0].State 70 | want := "present" 71 | if got != want { 72 | t.Errorf("want state to be %s, but got %s", want, got) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /config/repository.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Anthony Spring 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package config 22 | 23 | // RepositoryConfig definition 24 | type RepositoryConfig struct { 25 | Name string `mapstructure:"name"` 26 | State string `mapstructure:"state"` 27 | URL string `mapstructure:"url"` 28 | } 29 | 30 | // Equal comparison operator 31 | func (c RepositoryConfig) Equal(x RepositoryConfig) bool { 32 | if c.Name != x.Name { 33 | return false 34 | } 35 | 36 | if c.URL != x.URL { 37 | return false 38 | } 39 | 40 | return true 41 | } 42 | -------------------------------------------------------------------------------- /config/repository_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Anthony Spring 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package config 22 | 23 | import ( 24 | "testing" 25 | ) 26 | 27 | func TestRepositoryEquals_NamesDoNotMatch(t *testing.T) { 28 | rep1 := RepositoryConfig{Name: "foo1", URL: ""} 29 | rep2 := RepositoryConfig{Name: "foo2", URL: ""} 30 | 31 | if rep1.Equal(rep2) { 32 | t.Errorf("want %#v to NOT equal %#v", rep1, rep2) 33 | } 34 | } 35 | 36 | func TestRepositoryEquals_URLsDoNotMatch(t *testing.T) { 37 | rep1 := RepositoryConfig{Name: "foo", URL: "url1"} 38 | rep2 := RepositoryConfig{Name: "foo", URL: "url2"} 39 | 40 | if rep1.Equal(rep2) { 41 | t.Errorf("want %#v to NOT equal %#v", rep1, rep2) 42 | } 43 | } 44 | 45 | func TestRepositoryEquals_IgnoresState(t *testing.T) { 46 | rep1 := RepositoryConfig{Name: "foo", URL: "url", State: "present"} 47 | rep2 := RepositoryConfig{Name: "foo", URL: "url", State: "absent"} 48 | 49 | if !rep1.Equal(rep2) { 50 | t.Errorf("want %#v to equal %#v", rep1, rep2) 51 | } 52 | } 53 | 54 | func TestRepositoryEquals(t *testing.T) { 55 | rep1 := RepositoryConfig{Name: "foo", URL: "url"} 56 | rep2 := RepositoryConfig{Name: "foo", URL: "url"} 57 | 58 | if !rep1.Equal(rep2) { 59 | t.Errorf("want %#v to equal %#v", rep1, rep2) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Traackr/binnacle 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/google/uuid v1.3.0 7 | github.com/sirupsen/logrus v1.8.1 8 | github.com/spf13/cobra v1.4.0 9 | github.com/spf13/viper v1.11.0 10 | gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99 11 | ) 12 | 13 | require ( 14 | github.com/fsnotify/fsnotify v1.5.4 // indirect 15 | github.com/hashicorp/hcl v1.0.0 // indirect 16 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 17 | github.com/magiconair/properties v1.8.6 // indirect 18 | github.com/mitchellh/mapstructure v1.5.0 // indirect 19 | github.com/pelletier/go-toml v1.9.5 // indirect 20 | github.com/pelletier/go-toml/v2 v2.0.1 // indirect 21 | github.com/spf13/afero v1.8.2 // indirect 22 | github.com/spf13/cast v1.5.0 // indirect 23 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 24 | github.com/spf13/pflag v1.0.5 // indirect 25 | github.com/subosito/gotenv v1.2.0 // indirect 26 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect 27 | golang.org/x/text v0.3.7 // indirect 28 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 29 | gopkg.in/ini.v1 v1.66.4 // indirect 30 | gopkg.in/yaml.v2 v2.4.0 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /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.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 9 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 10 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 11 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 12 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 13 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 14 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 15 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 16 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 17 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 18 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 19 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= 20 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 21 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 22 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 23 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 24 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 25 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 26 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 27 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 28 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 29 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 30 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 31 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 32 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 33 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 34 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 35 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 36 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 37 | cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= 38 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 39 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 40 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 41 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 42 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 43 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 44 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 45 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 46 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 47 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 48 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 49 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 50 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 51 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 52 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 53 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 54 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 55 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 56 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 57 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 58 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 59 | github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= 60 | github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= 61 | github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= 62 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 63 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 64 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 65 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 66 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 67 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 68 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 69 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 70 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 71 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 72 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 73 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 74 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 75 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 76 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 77 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 78 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 79 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 80 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 81 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 82 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 83 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 84 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 85 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 86 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 87 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 88 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 89 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 90 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 91 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 92 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 93 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 94 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 95 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 96 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 97 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 98 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 99 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 100 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 101 | github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= 102 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 103 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 104 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 105 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 106 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 107 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 108 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 109 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 110 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 111 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 112 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 113 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 114 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 115 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 116 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 117 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 118 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 119 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 120 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 121 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= 122 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 123 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 124 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 125 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 126 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 127 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 128 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 129 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 130 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 131 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 132 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 133 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 134 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 135 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 136 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 137 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 138 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 139 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 140 | github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= 141 | github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= 142 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 143 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 144 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 145 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 146 | github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= 147 | github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= 148 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 149 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 150 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 151 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 152 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 153 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 154 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 155 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 156 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 157 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 158 | github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= 159 | github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= 160 | github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= 161 | github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= 162 | github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= 163 | github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= 164 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 165 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 166 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 167 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 168 | github.com/spf13/viper v1.11.0 h1:7OX/1FS6n7jHD1zGrZTM7WtY13ZELRyosK4k93oPr44= 169 | github.com/spf13/viper v1.11.0/go.mod h1:djo0X/bA5+tYVoCn+C7cAYJGcVn/qYLFTG8gdUsX7Zk= 170 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 171 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 172 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 173 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 174 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 175 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 176 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 177 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 178 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 179 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 180 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 181 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 182 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 183 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 184 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 185 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 186 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 187 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 188 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 189 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 190 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 191 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 192 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 193 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 194 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 195 | golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 196 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 197 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 198 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 199 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 200 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 201 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 202 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 203 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 204 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 205 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 206 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 207 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 208 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 209 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 210 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 211 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 212 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 213 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 214 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 215 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 216 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 217 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 218 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 219 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 220 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 221 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 222 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 223 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 224 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 225 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 226 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 227 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 228 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 229 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 230 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 231 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 232 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 233 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 234 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 235 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 236 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 237 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 238 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 239 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 240 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 241 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 242 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 243 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 244 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 245 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 246 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 247 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 248 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 249 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 250 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 251 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 252 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 253 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 254 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 255 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 256 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 257 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 258 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 259 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 260 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 261 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 262 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 263 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 264 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 265 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 266 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 267 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 268 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 269 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 270 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 271 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 272 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 273 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 274 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 275 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 276 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 277 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 278 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 279 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 280 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 281 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 282 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 283 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 284 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 285 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 286 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 287 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 288 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 289 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 290 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 291 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 292 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 293 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 294 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 295 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 296 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 297 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 298 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 299 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 300 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 301 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 302 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 303 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 304 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 305 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 306 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 307 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 308 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 309 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 310 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 311 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 312 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 313 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 314 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 315 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60= 316 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 317 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 318 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 319 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 320 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 321 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 322 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 323 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 324 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 325 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 326 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 327 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 328 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 329 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 330 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 331 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 332 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 333 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 334 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 335 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 336 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 337 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 338 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 339 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 340 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 341 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 342 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 343 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 344 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 345 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 346 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 347 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 348 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 349 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 350 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 351 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 352 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 353 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 354 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 355 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 356 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 357 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 358 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 359 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 360 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 361 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 362 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 363 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 364 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 365 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 366 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 367 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 368 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 369 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 370 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 371 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 372 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 373 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 374 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 375 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 376 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 377 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 378 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 379 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 380 | golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U= 381 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 382 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 383 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 384 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 385 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 386 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 387 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 388 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 389 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 390 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 391 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 392 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 393 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 394 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 395 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 396 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 397 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 398 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 399 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 400 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 401 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 402 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 403 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 404 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 405 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 406 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 407 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 408 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 409 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 410 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 411 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 412 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 413 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 414 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 415 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 416 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 417 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 418 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 419 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 420 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 421 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 422 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 423 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 424 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 425 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 426 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 427 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 428 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 429 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 430 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 431 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 432 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 433 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 434 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 435 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 436 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 437 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 438 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 439 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 440 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 441 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 442 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 443 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 444 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 445 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 446 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 447 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 448 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 449 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 450 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 451 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 452 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 453 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 454 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 455 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 456 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 457 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 458 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 459 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 460 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 461 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 462 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 463 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 464 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 465 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 466 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 467 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 468 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 469 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 470 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 471 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 472 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 473 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 474 | gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= 475 | gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 476 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 477 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 478 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 479 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 480 | gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99 h1:dbuHpmKjkDzSOMKAWl10QNlgaZUd3V1q99xc81tt2Kc= 481 | gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 482 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 483 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 484 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 485 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 486 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 487 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 488 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 489 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 490 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 491 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 492 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Anthony Spring 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import "github.com/Traackr/binnacle/cmd" 18 | 19 | func main() { 20 | cmd.Execute() 21 | } 22 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # Get the base repository path 5 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" 6 | # Get the name of the package we are building 7 | PACKAGE="$(basename "$DIR")" 8 | 9 | : ${LOCAL_TARGET="$(go env GOOS)_$(go env GOARCH)"} 10 | 11 | # Move into our base repository path 12 | cd "$DIR" 13 | 14 | # Get the version of the app 15 | VERSION="$(cat VERSION)" 16 | 17 | # Clean up old binaries and packages 18 | echo "==> Cleaning up build environment..." 19 | rm -rf pkg/* 20 | rm -rf bin/* 21 | mkdir -p bin 22 | mkdir -p pkg 23 | 24 | shasum256() { 25 | if hash sha256sum 2>/dev/null; then 26 | sha256sum "$@" 27 | else 28 | shasum -a 256 "$@" 29 | fi 30 | } 31 | 32 | # 33 | # Compile Configuration 34 | # 35 | 36 | GIT_COMMIT="$(git rev-parse --short HEAD)" 37 | GIT_DIRTY="$(test -n "`git status --porcelain`" && echo "+CHANGES" || true)" 38 | EXTLDFLAGS="-X github.com/Traackr/binnacle/cmd.GITCOMMIT=${GIT_COMMIT}${GIT_DIRTY} -X github.com/Traackr/binnacle/cmd.VERSION=$VERSION" 39 | STATIC="-extldflags '-static'" 40 | 41 | # 42 | # Determine build targets 43 | # 44 | 45 | # Default to local os/arch 46 | targets="$LOCAL_TARGET" 47 | 48 | # If we are building for release change targets based off of environment 49 | if [[ "$TARGETS" == "release" ]]; then 50 | targets="darwin_amd64 darwin_arm64 linux_amd64 linux_amd64-lxc windows_amd64" 51 | elif [[ "$TARGETS" != "" ]]; then 52 | targets="$TARGETS" 53 | fi 54 | 55 | for target in $targets; do 56 | case $target in 57 | "darwin_amd64") 58 | echo "==> Building darwin amd64..." 59 | CGO_ENABLED=$DARWIN_CGO_ENABLED GOARCH="amd64" GOOS="darwin" \ 60 | go build -ldflags "$EXTLDFLAGS" -o "pkg/darwin_amd64/$PACKAGE" 61 | ;; 62 | "darwin_arm64") 63 | echo "==> Building darwin arm64..." 64 | CGO_ENABLED=$DARWIN_CGO_ENABLED GOARCH="arm64" GOOS="darwin" \ 65 | go build -ldflags "$EXTLDFLAGS" -o "pkg/darwin_arm64/$PACKAGE" 66 | ;; 67 | "linux_amd64") 68 | echo "==> Building linux amd64..." 69 | CGO_ENABLED=0 GOOS="linux" GOARCH="amd64" \ 70 | go build -ldflags "$STATIC $EXTLDFLAGS" -o "pkg/linux_amd64/$PACKAGE" 71 | ;; 72 | "linux_amd64-lxc") 73 | echo "==> Building linux amd64 with lxc..." 74 | CGO_ENABLED=0 GOOS="linux" GOARCH="amd64" \ 75 | go build -ldflags "$STATIC $EXTLDFLAGS" -o "pkg/linux_amd64-lxc/$PACKAGE" -tags "lxc" 76 | ;; 77 | "windows_amd64") 78 | echo "==> Building windows amd64..." 79 | CGO_ENABLED=0 GOOS="windows" GOARCH="amd64" CXX="x86_64-w64-mingw32-g++" CC="x86_64-w64-mingw32-gcc" \ 80 | go build -ldflags "$STATIC $EXTLDFLAGS" -o "pkg/windows_amd64/$PACKAGE.exe" 81 | ;; 82 | *) 83 | echo "--> Invalid target: $target" 84 | ;; 85 | esac 86 | done 87 | 88 | set -e 89 | 90 | # Copy our local OS/Arch to the bin/ directory 91 | for F in $(find ./pkg/${LOCAL_TARGET} -mindepth 1 -maxdepth 1 -type f); do 92 | echo "==> Copying ${LOCAL_TARGET} to ./bin" 93 | cp ${F} bin/ 94 | chmod 755 bin/* 95 | done 96 | 97 | # Package up the artifacts 98 | if [[ "$GENERATE_PACKAGES" != "" ]]; then 99 | for PLATFORM in $(find ./pkg -mindepth 1 -maxdepth 1 -type d); do 100 | OSARCH=$(basename ${PLATFORM}) 101 | echo "==> Packaging ${OSARCH}" 102 | pushd $PLATFORM >/dev/null 2>&1 103 | tar czvf ../${PACKAGE}-${OSARCH}.tar.gz ./* >/dev/null 104 | popd >/dev/null 2>&1 105 | #rm -rf $PLATFORM >/dev/null 106 | done 107 | 108 | echo "==> Generating SHA256..." 109 | for F in $(find ./pkg -mindepth 1 -maxdepth 1 -type f); do 110 | FILENAME=$(basename ${F}) 111 | shasum256 "./pkg/${FILENAME}" >> ./pkg/SHA256SUM.txt 112 | done 113 | echo 114 | cat ./pkg/SHA256SUM.txt 115 | fi 116 | -------------------------------------------------------------------------------- /testdata/absent.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # charts takes a list of chart configurations 3 | charts: 4 | # This is the name of the chart 5 | - name: concourse 6 | # This is the namespace into which the chart is launched 7 | namespace: apps 8 | # This is the name for the release of this chart 9 | release: apps-concourse 10 | # This is the name of the repository within which the helm chart is located 11 | repo: stable 12 | # This determines if the release is created or removed. Default: present Options: absent, present 13 | state: absent 14 | # Any data under values are passed to Helm to configure the given chart 15 | values: 16 | image: concourse/concourse 17 | imageTag: "3.10.0" 18 | ingress: 19 | enabled: true 20 | # This is the version of the Helm chart. If this is omitted, the latest is used. 21 | version: 1.3.1 -------------------------------------------------------------------------------- /testdata/default-state.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # charts takes a list of chart configurations 3 | charts: 4 | # This is the name of the chart 5 | - name: concourse 6 | # This is the namespace into which the chart is launched 7 | namespace: apps 8 | # This is the name for the release of this chart 9 | release: apps-concourse 10 | # This is the name of the repository within which the helm chart is located 11 | repo: stable 12 | # Any data under values are passed to Helm to configure the given chart 13 | values: 14 | image: concourse/concourse 15 | imageTag: "3.10.0" 16 | ingress: 17 | enabled: true 18 | # This is the version of the Helm chart. If this is omitted, the latest is used. 19 | version: 1.3.1 20 | 21 | 22 | # repositories takes a list of repository configurations 23 | repositories: 24 | # This is the name of the repository 25 | - name: stable 26 | # This is the URL of the repository 27 | url: https://kubernetes-charts.storage.googleapis.com 28 | -------------------------------------------------------------------------------- /testdata/demo.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # charts takes a list of chart configurations 3 | charts: 4 | # This is the name of the chart 5 | - name: concourse 6 | # This is the namespace into which the chart is launched 7 | namespace: apps 8 | # This is the name for the release of this chart 9 | release: apps-concourse 10 | # This is the name of the repository within which the helm chart is located 11 | repo: stable 12 | # This determines if the release is created or removed. Default: present Options: absent, present 13 | state: present 14 | # Any data under values are passed to Helm to configure the given chart 15 | values: 16 | image: concourse/concourse 17 | imageTag: "3.10.0" 18 | ingress: 19 | enabled: true 20 | # This is the version of the Helm chart. If this is omitted, the latest is used. 21 | version: 1.3.1 22 | 23 | # repositories takes a list of repository configurations 24 | repositories: 25 | # This is the name of the repository 26 | - name: stable 27 | # This is the URL of the repository 28 | url: https://kubernetes-charts.storage.googleapis.com 29 | # This determines if the repository is created or removed. Default: present Options: absent, present 30 | state: present 31 | -------------------------------------------------------------------------------- /testdata/invalid.yml: -------------------------------------------------------------------------------- 1 | @invalid -------------------------------------------------------------------------------- /testdata/unmarshallable.yml: -------------------------------------------------------------------------------- 1 | --- 2 | charts: 3 | foo: bar 4 | biz: baz -------------------------------------------------------------------------------- /testdata/without-repo.yml: -------------------------------------------------------------------------------- 1 | --- 2 | charts: 3 | - name: https://github.com/pantsel/konga/blob/master/charts/konga/konga-1.0.0.tgz?raw=true 4 | namespace: kube-system 5 | release: konga 6 | state: present 7 | --------------------------------------------------------------------------------