From c539d16d07372a1e8ba8f099bb570a38f95b7041 Mon Sep 17 00:00:00 2001 From: aalexand Date: Tue, 14 Dec 2021 17:11:29 +0200 Subject: [PATCH 1/2] initial commit with unit tests workflow --- .dockerignore | 5 + .editorconfig | 30 + .env.sample | 14 + .github/CODEOWNERS | 2 + .github/CONTRIBUTING.md | 47 + .github/ISSUE_TEMPLATE.md | 16 + .github/PULL_REQUEST_TEMPLATE.md | 45 + .github/env | 1 + .github/workflows/tests.yml | 21 + .gitignore | 35 + CHANGELOG.md | 31 + CODE_OF_CONDUCT.md | 74 ++ LICENSE | 201 ++++ Makefile | 138 +++ VERSION | 1 + cmd/api/api.go | 57 + cmd/cc/client.go | 166 +++ .../registry.ethos.adobe.com_clusters.yaml | 314 ++++++ config/crd/kustomization.yaml | 21 + config/crd/kustomizeconfig.yaml | 19 + .../patches/cainjection_in_clientconfigs.yaml | 7 + .../crd/patches/cainjection_in_clusters.yaml | 7 + .../crd/patches/webhook_in_clientconfigs.yaml | 14 + config/crd/patches/webhook_in_clusters.yaml | 14 + config/default/kustomization.yaml | 74 ++ config/default/manager_auth_proxy_patch.yaml | 26 + config/default/manager_config_patch.yaml | 20 + config/manager/controller_manager_config.yaml | 21 + config/manager/kustomization.yaml | 10 + config/manager/manager.yaml | 56 + config/prometheus/kustomization.yaml | 2 + config/prometheus/monitor.yaml | 20 + .../rbac/auth_proxy_client_clusterrole.yaml | 9 + config/rbac/auth_proxy_role.yaml | 17 + config/rbac/auth_proxy_role_binding.yaml | 12 + config/rbac/auth_proxy_service.yaml | 14 + config/rbac/clientconfig_editor_role.yaml | 24 + config/rbac/clientconfig_viewer_role.yaml | 20 + config/rbac/kustomization.yaml | 18 + config/rbac/leader_election_role.yaml | 37 + config/rbac/leader_election_role_binding.yaml | 12 + config/rbac/role.yaml | 34 + config/rbac/role_binding.yaml | 12 + config/rbac/service_account.yaml | 5 + go.mod | 39 + go.sum | 982 ++++++++++++++++++ hack/boilerplate.go.txt | 11 + hack/check_license.sh | 17 + pkg/api/api/handler.go | 131 +++ pkg/api/api/handler_test.go | 180 ++++ pkg/api/api/response.go | 41 + pkg/api/api/router.go | 34 + pkg/api/api/validator.go | 32 + pkg/api/authz/jwt.go | 117 +++ pkg/api/authz/jwt_test.go | 271 +++++ pkg/api/database/database.go | 257 +++++ pkg/api/database/database_test.go | 452 ++++++++ pkg/api/docs/docs.go | 527 ++++++++++ pkg/api/docs/docs_test.go | 27 + pkg/api/docs/swagger.json | 469 +++++++++ pkg/api/docs/swagger.yaml | 385 +++++++ pkg/api/monitoring/livez.go | 25 + pkg/api/monitoring/livez_test.go | 38 + pkg/api/monitoring/metrics.go | 280 +++++ pkg/api/monitoring/metrics_test.go | 140 +++ pkg/api/sqs/consumer.go | 191 ++++ pkg/api/sqs/consumer_test.go | 247 +++++ pkg/api/sqs/fakeproducer.go | 34 + pkg/api/sqs/producer.go | 96 ++ pkg/api/sqs/producer_test.go | 72 ++ pkg/api/sqs/sqs.go | 78 ++ pkg/api/sqs/sqs_test.go | 245 +++++ pkg/api/utils/errors.go | 43 + pkg/api/utils/errors_test.go | 66 ++ pkg/cc/api/config/v1/clientconfig_types.go | 54 + pkg/cc/api/config/v1/groupversion_info.go | 32 + pkg/cc/api/config/v1/zz_generated.deepcopy.go | 113 ++ pkg/cc/api/registry/v1/cluster_types.go | 267 +++++ pkg/cc/api/registry/v1/groupversion_info.go | 32 + .../api/registry/v1/zz_generated.deepcopy.go | 336 ++++++ pkg/cc/controllers/cluster_controller.go | 183 ++++ pkg/cc/controllers/suite_test.go | 92 ++ pkg/cc/monitoring/metrics.go | 112 ++ pkg/cc/monitoring/metrics_test.go | 94 ++ pkg/cc/webhook/alert.go | 71 ++ pkg/cc/webhook/server.go | 128 +++ pkg/cc/webhook/server_test.go | 261 +++++ pkg/cc/webhook/suite_test.go | 94 ++ test/e2e_test.go | 258 +++++ test/jwt.go | 41 + test/testdata/alert.json | 18 + .../cluster05-prod-useast1-update.json | 79 ++ test/testdata/cluster05-prod-useast1.json | 79 ++ 93 files changed, 9594 insertions(+) create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .env.sample create mode 100644 .github/CODEOWNERS create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/env create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 VERSION create mode 100644 cmd/api/api.go create mode 100644 cmd/cc/client.go create mode 100644 config/crd/bases/registry.ethos.adobe.com_clusters.yaml create mode 100644 config/crd/kustomization.yaml create mode 100644 config/crd/kustomizeconfig.yaml create mode 100644 config/crd/patches/cainjection_in_clientconfigs.yaml create mode 100644 config/crd/patches/cainjection_in_clusters.yaml create mode 100644 config/crd/patches/webhook_in_clientconfigs.yaml create mode 100644 config/crd/patches/webhook_in_clusters.yaml create mode 100644 config/default/kustomization.yaml create mode 100644 config/default/manager_auth_proxy_patch.yaml create mode 100644 config/default/manager_config_patch.yaml create mode 100644 config/manager/controller_manager_config.yaml create mode 100644 config/manager/kustomization.yaml create mode 100644 config/manager/manager.yaml create mode 100644 config/prometheus/kustomization.yaml create mode 100644 config/prometheus/monitor.yaml create mode 100644 config/rbac/auth_proxy_client_clusterrole.yaml create mode 100644 config/rbac/auth_proxy_role.yaml create mode 100644 config/rbac/auth_proxy_role_binding.yaml create mode 100644 config/rbac/auth_proxy_service.yaml create mode 100644 config/rbac/clientconfig_editor_role.yaml create mode 100644 config/rbac/clientconfig_viewer_role.yaml create mode 100644 config/rbac/kustomization.yaml create mode 100644 config/rbac/leader_election_role.yaml create mode 100644 config/rbac/leader_election_role_binding.yaml create mode 100644 config/rbac/role.yaml create mode 100644 config/rbac/role_binding.yaml create mode 100644 config/rbac/service_account.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 hack/boilerplate.go.txt create mode 100755 hack/check_license.sh create mode 100644 pkg/api/api/handler.go create mode 100644 pkg/api/api/handler_test.go create mode 100644 pkg/api/api/response.go create mode 100644 pkg/api/api/router.go create mode 100644 pkg/api/api/validator.go create mode 100644 pkg/api/authz/jwt.go create mode 100644 pkg/api/authz/jwt_test.go create mode 100644 pkg/api/database/database.go create mode 100644 pkg/api/database/database_test.go create mode 100644 pkg/api/docs/docs.go create mode 100644 pkg/api/docs/docs_test.go create mode 100644 pkg/api/docs/swagger.json create mode 100644 pkg/api/docs/swagger.yaml create mode 100644 pkg/api/monitoring/livez.go create mode 100644 pkg/api/monitoring/livez_test.go create mode 100644 pkg/api/monitoring/metrics.go create mode 100644 pkg/api/monitoring/metrics_test.go create mode 100644 pkg/api/sqs/consumer.go create mode 100644 pkg/api/sqs/consumer_test.go create mode 100644 pkg/api/sqs/fakeproducer.go create mode 100644 pkg/api/sqs/producer.go create mode 100644 pkg/api/sqs/producer_test.go create mode 100644 pkg/api/sqs/sqs.go create mode 100644 pkg/api/sqs/sqs_test.go create mode 100644 pkg/api/utils/errors.go create mode 100644 pkg/api/utils/errors_test.go create mode 100644 pkg/cc/api/config/v1/clientconfig_types.go create mode 100644 pkg/cc/api/config/v1/groupversion_info.go create mode 100644 pkg/cc/api/config/v1/zz_generated.deepcopy.go create mode 100644 pkg/cc/api/registry/v1/cluster_types.go create mode 100644 pkg/cc/api/registry/v1/groupversion_info.go create mode 100644 pkg/cc/api/registry/v1/zz_generated.deepcopy.go create mode 100644 pkg/cc/controllers/cluster_controller.go create mode 100644 pkg/cc/controllers/suite_test.go create mode 100644 pkg/cc/monitoring/metrics.go create mode 100644 pkg/cc/monitoring/metrics_test.go create mode 100644 pkg/cc/webhook/alert.go create mode 100644 pkg/cc/webhook/server.go create mode 100644 pkg/cc/webhook/server_test.go create mode 100644 pkg/cc/webhook/suite_test.go create mode 100644 test/e2e_test.go create mode 100644 test/jwt.go create mode 100644 test/testdata/alert.json create mode 100644 test/testdata/cluster05-prod-useast1-update.json create mode 100644 test/testdata/cluster05-prod-useast1.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..243f81a5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore all files which are not go type +!**/*.go +!**/*.mod +!**/*.sum diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..cbd7a006 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,30 @@ + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +charset = utf-8 +indent_size = 4 + +# Formatting/indentation rules for source files +[*.{js,css,java,json}] +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +# Formatting/indentation rules for markup/configuration files +[*.{html,xml}] +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +# Formatting/indentation rules for miscellaneous files +[*.{eslint,babelrc}] +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true diff --git a/.env.sample b/.env.sample new file mode 100644 index 00000000..05505516 --- /dev/null +++ b/.env.sample @@ -0,0 +1,14 @@ +export AWS_ACCESS_KEY_ID="aws-access-key" +export AWS_SECRET_ACCESS_KEY="aws-secret-access-key" +export AWS_REGION="aws-region" +export DB_AWS_REGION="db-aws-region" +export DB_ENDPOINT="https://hostname:port" +export DB_TABLE_NAME="db-table-name" +export OIDC_ISSUER_URL="https://accounts.google.com" +export OIDC_CLIENT_ID="oidc-client-id" +export OIDC_TENANT_ID="oidc-tenant-id" +export TEST_CLIENT_ID="test-client-id" +export SQS_ENDPOINT="https://hostname:port/queue-name" +export SQS_AWS_REGION="sqs-aws-region" +export SQS_QUEUE_NAME="sqs-queue-name" +export SET_CA_CERT="true" diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..3f3e6339 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Maintainers +* @adobe/ethos diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000..dd00b8fc --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,47 @@ +# Contributing + +Thanks for choosing to contribute! + +The following are a set of guidelines to follow when contributing to this project. + +## Code Of Conduct + +This project adheres to the Adobe [code of conduct](../CODE_OF_CONDUCT.md). By participating, +you are expected to uphold this code. Please report unacceptable behavior to +[Grp-opensourceoffice@adobe.com](mailto:Grp-opensourceoffice@adobe.com). + +## Have A Question? + +Start by filing an issue. The existing committers on this project work to reach +consensus around project direction and issue solutions within issue threads +(when appropriate). + +## Contributor License Agreement + +All third-party contributions to this project must be accompanied by a signed contributor +license agreement. This gives Adobe permission to redistribute your contributions +as part of the project. [Sign our CLA](https://opensource.adobe.com/cla.html). You +only need to submit an Adobe CLA one time, so if you have submitted one previously, +you are good to go! + +## Code Reviews + +All submissions should come in the form of pull requests and need to be reviewed +by project committers. Read [GitHub's pull request documentation](https://help.github.com/articles/about-pull-requests/) +for more information on sending pull requests. + +Lastly, please follow the [pull request template](PULL_REQUEST_TEMPLATE.md) when +submitting a pull request! + +## From Contributor To Committer + +We love contributions from our community! If you'd like to go a step beyond contributor +and become a committer with full write access and a say in the project, you must +be invited to the project. The existing committers employ an internal nomination +process that must reach lazy consensus (silence is approval) before invitations +are issued. If you feel you are qualified and want to get more deeply involved, +feel free to reach out to existing committers to have a conversation about that. + +## Security Issues + +Security issues shouldn't be reported on this issue tracker. Instead, [file an issue to our security experts](https://helpx.adobe.com/security/alertus.html). diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..389679c1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,16 @@ + + + +### Expected Behaviour + +### Actual Behaviour + +### Reproduce Scenario (including but not limited to) + +#### Steps to Reproduce + +#### Platform and Version + +#### Sample Code that illustrates the problem + +#### Logs taken while reproducing problem diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..9efe0d81 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,45 @@ + + +## Description + + + +## Related Issue + + + + + + +## Motivation and Context + + + +## How Has This Been Tested? + + + + + +## Screenshots (if appropriate): + +## Types of changes + + + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) + +## Checklist: + + + + +- [ ] I have signed the [Adobe Open Source CLA](https://opensource.adobe.com/cla.html). +- [ ] My code follows the code style of this project. +- [ ] My change requires a change to the documentation. +- [ ] I have updated the documentation accordingly. +- [ ] I have read the **CONTRIBUTING** document. +- [ ] I have added tests to cover my changes. +- [ ] All new and existing tests passed. diff --git a/.github/env b/.github/env new file mode 100644 index 00000000..5c8b9a85 --- /dev/null +++ b/.github/env @@ -0,0 +1 @@ +golang-version=1.15 \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..1a1a7be1 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,21 @@ +name: tests + +on: + push: + pull_request: + +jobs: + unit-tests: + runs-on: ubuntu-latest + name: Unit tests + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Import environment variables from file + run: cat ".github/env" >> $GITHUB_ENV + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: '${{ env.golang-version }}' + - name: Run tests + run: make test diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3e57913a --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# OSX os files +.DS_Store +.DS_Store? + +# local/env +.env +.envrc +.local +docker/ + +# build artifacts +cluster-registry +kubeconfig +.dynamodb + +# Binaries for programs and plugins +bin +testbin/* +__debug_bin + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +*.swp +*.swo +*~ +.vscode diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..a3de0242 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,31 @@ +# 0.1.3 + +## cluster-registry-api +- Fix metrics registration in Prometheus + +## cluster-registry-client +- Fix metrics registration in Prometheus + +# 0.1.2-2 + +## cluster-registry-api +- N/A + +## cluster-registry-client +- Error handling for loading TLS files + +# 0.1.2-1 + +## cluster-registry-api +- N/A + +## cluster-registry-client +- Fix initializing CAData for in-cluster configuration + +# 0.1.2 + +## cluster-registry-api +- N/A + +## cluster-registry-client +- CertificateAuthorityData field auto-update on CRD create/update diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..cdb381f5 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Adobe Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language. +* Being respectful of differing viewpoints and experiences. +* Gracefully accepting constructive criticism. +* Focusing on what is best for the community. +* Showing empathy towards other community members. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances. +* Trolling, insulting/derogatory comments, and personal or political attacks. +* Public or private harassment. +* Publishing others' private information, such as a physical or electronic + address, without explicit permission. +* Other conduct which could reasonably be considered inappropriate in a + professional setting. + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at Grp-opensourceoffice@adobe.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [https://contributor-covenant.org/version/1/4][version]. + +[homepage]: https://contributor-covenant.org +[version]: https://contributor-covenant.org/version/1/4/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..163d6095 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 Adobe + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..c92224de --- /dev/null +++ b/Makefile @@ -0,0 +1,138 @@ +SHELL=/usr/bin/env bash -o pipefail + +GOOS?=$(shell go env GOOS) +GOARCH?=$(shell go env GOARCH) +ifeq ($(GOARCH),arm) + ARCH=armv7 +else + ARCH=$(GOARCH) +endif + +GO_PKG=github.com/adobe/cluster-registry +TAG?=$(shell git rev-parse --short HEAD) +VERSION?=$(shell cat VERSION | tr -d " \t\n\r") + +# The ldflags for the go build process to set the version related data. +GO_BUILD_LDFLAGS=\ + -s \ + -X $(GO_PKG)/version.Revision=$(BUILD_REVISION) \ + -X $(GO_PKG)/version.BuildUser=$(BUILD_USER) \ + -X $(GO_PKG)/version.BuildDate=$(BUILD_DATE) \ + -X $(GO_PKG)/version.Branch=$(BUILD_BRANCH) \ + -X $(GO_PKG)/version.Version=$(VERSION) + +GO_BUILD_RECIPE=\ + GOOS=$(GOOS) \ + GOARCH=$(GOARCH) \ + CGO_ENABLED=0 \ + go build -ldflags="$(GO_BUILD_LDFLAGS)" + +API_PKGS = $(shell go list ./pkg/api/...) +API_PKGS += $(shell go list ./cmd/api/...) +CC_PKGS = $(shell go list ./pkg/cc/...) +CC_PKGS += $(shell go list ./cmd/cc/...) + +.PHONY: all +all: format generate build test + +.PHONY: clean +clean: + # Remove all files and directories ignored by git. + git clean -Xfd . + + +############ +# Building # +############ + +.PHONY: build +build: # TODO + + +############## +# Formatting # +############## + +.PHONY: format +format: go-fmt jsonnet-fmt check-license shellcheck + +.PHONY: go-fmt +go-fmt: + gofmt -s -w . + +.PHONY: check-license +check-license: + ./hack/check_license.sh + +.PHONY: lint +lint: golangci-lint + $(GOLANGCI_LINT) run + +.PHONY: lint-fix +lint-fix: golangci-lint + $(GOLANGCI_LINT) run --fix + +GOLANGCI_LINT = $(shell pwd)/bin/golangci-lint +golangci-lint: + @[ -f $(GOLANGCI_LINT) ] || { \ + set -e ;\ + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell dirname $(GOLANGCI_LINT)) v1.41.1 ;\ + } + + +########### +# Testing # +########### + +ENVTEST_ASSETS_DIR=$(shell pwd)/testbin + +.PHONY: test +test: test-api test-cc + +.PHONY: test-api +test-api: source-env + source $(shell pwd)/.env.sample; go test -race $(TEST_RUN_ARGS) -short $(API_PKGS) -count=1 -v + +.PHONY: test-cc +test-cc: source-env + mkdir -p ${ENVTEST_ASSETS_DIR} + test -f ${ENVTEST_ASSETS_DIR}/setup-envtest.sh || curl -sSLo ${ENVTEST_ASSETS_DIR}/setup-envtest.sh https://raw.githubusercontent.com/kubernetes-sigs/controller-runtime/v0.7.2/hack/setup-envtest.sh + source ${ENVTEST_ASSETS_DIR}/setup-envtest.sh; fetch_envtest_tools $(ENVTEST_ASSETS_DIR); setup_envtest_env $(ENVTEST_ASSETS_DIR); go test -race $(TEST_RUN_ARGS) -short $(CC_PKGS) -count=1 -v + +source-env: + source $(shell pwd)/.env.sample + + +############### +# Development # +############### + +CRD_OPTIONS ?= "crd:trivialVersions=true,preserveUnknownFields=false" +MANAGER_ROLE ?= "cluster-registry" + +CONTROLLER_GEN = $(shell pwd)/bin/controller-gen +controller-gen: ## Download controller-gen locally if necessary. + $(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.4.1) + +KUSTOMIZE = $(shell pwd)/bin/kustomize +kustomize: ## Download kustomize locally if necessary. + $(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v3@v3.8.7) + +manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=$(MANAGER_ROLE) webhook paths="$(shell pwd)/pkg/cc/..." output:crd:artifacts:config=$(shell pwd)/config/crd/bases output:rbac:artifacts:config=$(shell pwd)/config/rbac + +generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + $(CONTROLLER_GEN) object:headerFile="$(shell pwd)/hack/boilerplate.go.txt" paths="$(shell pwd)/pkg/cc/..." + +# go-get-tool will 'go get' any package $2 and install it to $1. +define go-get-tool +@[ -f $(1) ] || { \ +set -e ;\ +TMP_DIR=$$(mktemp -d) ;\ +cd $$TMP_DIR ;\ +go mod init tmp ;\ +echo "Downloading $(2)" ;\ +GOBIN=$(shell pwd)/bin go get $(2) ;\ +rm -rf $$TMP_DIR ;\ +} +endef \ No newline at end of file diff --git a/VERSION b/VERSION new file mode 100644 index 00000000..446ba66e --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.4 \ No newline at end of file diff --git a/cmd/api/api.go b/cmd/api/api.go new file mode 100644 index 00000000..1c584b11 --- /dev/null +++ b/cmd/api/api.go @@ -0,0 +1,57 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package main + +import ( + "github.com/adobe/cluster-registry/pkg/api/api" + "github.com/adobe/cluster-registry/pkg/api/database" + _ "github.com/adobe/cluster-registry/pkg/api/docs" + "github.com/adobe/cluster-registry/pkg/api/monitoring" + "github.com/adobe/cluster-registry/pkg/api/sqs" + echoSwagger "github.com/swaggo/echo-swagger" +) + +// @title Swagger Example API +// @version 1.0 +// @description Cluster Registry API +// @title Cluster Registry API + +// @host http://cluster-registry.missionctrl.cloud.adobe.io +// @BasePath /api + +// @schemes http https +// @produce application/json +// @consumes application/json + +// @securityDefinitions.apikey bearerAuth +// @in header +// @name Authorization +func main() { + a := api.NewRouter() + + a.GET("/api/swagger/*", echoSwagger.WrapHandler) + a.GET("/livez", monitoring.Livez) + + v1 := a.Group("/api/v1") + m := monitoring.NewMetrics("cluster_registry_api", nil, false) + m.Use(a) + + d := database.NewDb(m) + h := api.NewHandler(d, m) + h.Register(v1) + + c := sqs.NewConsumer(d, m) + go c.Consume() + + a.Logger.Fatal(a.Start(":8080")) +} diff --git a/cmd/cc/client.go b/cmd/cc/client.go new file mode 100644 index 00000000..ea5136b4 --- /dev/null +++ b/cmd/cc/client.go @@ -0,0 +1,166 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package main + +import ( + "flag" + "os" + + "github.com/adobe/cluster-registry/pkg/api/sqs" + "github.com/adobe/cluster-registry/pkg/cc/controllers" + "github.com/adobe/cluster-registry/pkg/cc/monitoring" + + "github.com/prometheus/client_golang/prometheus/promhttp" + + "encoding/base64" + + configv1 "github.com/adobe/cluster-registry/pkg/cc/api/config/v1" + registryv1 "github.com/adobe/cluster-registry/pkg/cc/api/registry/v1" + "github.com/adobe/cluster-registry/pkg/cc/webhook" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + _ "k8s.io/client-go/plugin/pkg/client/auth" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(registryv1.AddToScheme(scheme)) + utilruntime.Must(configv1.AddToScheme(scheme)) +} + +func main() { + var configFile string + var metricsAddr string + var enableLeaderElection bool + var probeAddr string + var alertmanagerWebhookAddr string + var namespace string + flag.StringVar(&configFile, "config", "", + "The controller will load its initial configuration from this file. "+ + "Omit this flag to use the default configuration values. "+ + "Command-line flags override configuration from this file.") + flag.StringVar(&metricsAddr, "metrics-bind-address", ":9090", "The address the metric endpoint binds to.") + flag.StringVar(&probeAddr, "health-probe-bind-address", ":9091", "The address the probe endpoint binds to.") + flag.StringVar(&alertmanagerWebhookAddr, "alertmanager-webhook-bind-address", ":9092", "The address the alertmanager webhook endpoint binds to.") + flag.StringVar(&namespace, "namespace", "cluster-registry", "The namespace where cluster-registry-client will run.") + flag.BoolVar(&enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + opts := zap.Options{ + // TODO: change this to false + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + var err error + clientConfig := configv1.ClientConfig{ + Namespace: namespace, + AlertmanagerWebhook: configv1.AlertmanagerWebhookConfig{ + BindAddress: alertmanagerWebhookAddr, + AlertMap: []configv1.AlertRule{}, + }, + } + options := ctrl.Options{ + Scheme: scheme, + Namespace: namespace, + MetricsBindAddress: metricsAddr, + Port: 9443, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "0c4967d2.registry.ethos.adobe.com", + } + + if configFile != "" { + options, err = options.AndFrom(ctrl.ConfigFile().AtPath(configFile).OfKind(&clientConfig)) + if err != nil { + setupLog.Error(err, "unable to load the config file") + os.Exit(1) + } + } + setupLog.Info("using client configuration", "config", clientConfig) + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), options) + if err != nil { + setupLog.Error(err, "unable to start cluster-registry-client") + os.Exit(1) + } + + m := monitoring.NewMetrics() + m.Init(false) + + // InClusterConfiguration doesn't initialize the CAData field by default + // for some reason, so we're doing this manually by calling LoadTLSFiles + if err = rest.LoadTLSFiles(mgr.GetConfig()); err != nil { + setupLog.Error(err, "failed to load TLS files") + os.Exit(1) + } + + if err = (&controllers.ClusterReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("Cluster"), + Scheme: mgr.GetScheme(), + Queue: sqs.NewProducer(m), + CAData: base64.StdEncoding.EncodeToString(mgr.GetConfig().CAData), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Cluster") + os.Exit(1) + } + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + if err := mgr.AddMetricsExtraHandler("/metrics/extra", promhttp.Handler()); err != nil { + setupLog.Error(err, "unable to set up extra metrics handler") + os.Exit(1) + } + + go func() { + setupLog.Info("starting alertmanager webhook server", "addr", clientConfig.AlertmanagerWebhook.BindAddress) + if err := (&webhook.Server{ + Client: mgr.GetClient(), + Namespace: clientConfig.Namespace, + BindAddress: clientConfig.AlertmanagerWebhook.BindAddress, + Log: ctrl.Log.WithName("webhook"), + Metrics: m, + AlertMap: clientConfig.AlertmanagerWebhook.AlertMap, + }).Start(); err != nil { + setupLog.Error(err, "unable to start alertmanager webhook server") + os.Exit(1) + } + }() + + setupLog.Info("starting cluster-registry-client") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running cluster-registry-client") + os.Exit(1) + } +} diff --git a/config/crd/bases/registry.ethos.adobe.com_clusters.yaml b/config/crd/bases/registry.ethos.adobe.com_clusters.yaml new file mode 100644 index 00000000..559cc046 --- /dev/null +++ b/config/crd/bases/registry.ethos.adobe.com_clusters.yaml @@ -0,0 +1,314 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null + name: clusters.registry.ethos.adobe.com +spec: + group: registry.ethos.adobe.com + names: + kind: Cluster + listKind: ClusterList + plural: clusters + singular: cluster + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: Cluster is the Schema for the clusters API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ClusterSpec defines the desired state of Cluster + properties: + accountId: + description: The cloud account associated with the cluster + type: string + allowedOnboardingTeams: + description: Git teams and/or LDAP groups that are allowed to onboard + and deploy on the cluster + items: + description: AllowedOnboardingTeam represents the Git teams and/or + LDAP groups that are allowed to onboard. + properties: + gitTeams: + description: List of git teams + items: + type: string + type: array + ldapGroups: + description: List of ldap groups + items: + type: string + type: array + name: + description: Name of the team + type: string + required: + - name + type: object + type: array + apiServer: + description: Information about K8s API endpoint and CA cert + properties: + certificateAuthorityData: + description: Information about K8s Api CA Cert + type: string + endpoint: + description: Information about K8s Api Endpoint + type: string + required: + - certificateAuthorityData + - endpoint + type: object + businessUnit: + description: The BU that owns and maintains the cluster + type: string + capabilities: + description: List of cluster capabilities + items: + type: string + type: array + cloudType: + description: The cloud provider. + type: string + environment: + description: Cluster environment. + type: string + extra: + description: Ethos Extra specific information + properties: + domainName: + description: Name of the domain + type: string + ecrIamArns: + additionalProperties: + type: string + description: List of IAM Arns + type: object + egressPorts: + description: Egress ports allowed outside of the namespace + type: string + lbEndpoints: + additionalProperties: + type: string + description: Load balancer endpoints + type: object + loggingEndpoints: + description: Logging endpoints + items: + additionalProperties: + type: string + type: object + type: array + nfsInfo: + additionalProperties: + type: string + description: NFS information + type: object + required: + - domainName + - lbEndpoints + type: object + k8sInfraRelease: + description: K8s Infrastructure release information + properties: + gitSha: + description: GitSha of the release + type: string + lastUpdated: + description: When the release was applied on the cluster + type: string + release: + description: Release name + type: string + required: + - gitSha + - lastUpdated + - release + type: object + lastUpdated: + description: Timestamp when cluster information was updated + type: string + name: + description: Cluster name + maxLength: 64 + minLength: 3 + type: string + offering: + description: The Ethos offering that the cluster is meant for + items: + description: Offering - the Ethos offering that the cluster is meant + for + enum: + - CaaS + - PaaS + type: string + type: array + peerVirtualNetworks: + description: Information about Virtual Networks peered with the cluster + items: + description: PeerVirtualNetwork - peering information done at cluster + onboarding + properties: + cidrs: + description: Remote Virtual Netowrk CIDRs + items: + type: string + type: array + id: + description: Remote Virtual Netowrk ID + type: string + ownerID: + description: Cloud account of the owner + type: string + type: object + type: array + phase: + description: Cluster phase + enum: + - Building + - Testing + - Running + - Upgrading + type: string + region: + description: Cluster standard region name + type: string + registeredAt: + description: Timestamp when cluster was registered in Cluster Registry + type: string + shortName: + description: Cluster name, without dash. + maxLength: 64 + minLength: 3 + type: string + status: + description: Cluster status + enum: + - Inactive + - Active + - Deprecated + - Deleted + type: string + tags: + additionalProperties: + type: string + description: Cluster tags that were applied + type: object + tiers: + description: List of tiers with their associated information + items: + description: Tier details + properties: + containerRuntime: + description: Container runtime + enum: + - docker + - cri-o + type: string + enableKataSupport: + description: EnableKataSupport + type: boolean + instanceType: + description: Type of the instances + type: string + kernelParameters: + additionalProperties: + type: string + description: KernelParameters + type: object + labels: + additionalProperties: + type: string + description: Instance K8s labels + type: object + maxCapacity: + description: Max number of instances + type: integer + minCapacity: + description: Min number of instances + type: integer + name: + description: Name of the tier + type: string + taints: + description: Instance K8s taints + items: + type: string + type: array + required: + - containerRuntime + - instanceType + - maxCapacity + - minCapacity + - name + type: object + type: array + type: + description: The type of the cluster + type: string + virtualNetworks: + description: Virtual Private Networks information + items: + description: VirtualNetwork information. + properties: + cidrs: + description: CIDRs used in this VirtualNetwork + items: + type: string + type: array + id: + description: Virtual private network Id + type: string + required: + - cidrs + - id + type: object + type: array + required: + - accountId + - apiServer + - businessUnit + - cloudType + - environment + - k8sInfraRelease + - name + - offering + - phase + - region + - registeredAt + - shortName + - status + - tiers + - virtualNetworks + type: object + status: + description: ClusterStatus defines the observed state of Cluster + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml new file mode 100644 index 00000000..c9713abe --- /dev/null +++ b/config/crd/kustomization.yaml @@ -0,0 +1,21 @@ +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/registry.ethos.adobe.com_clusters.yaml +#+kubebuilder:scaffold:crdkustomizeresource + +patchesStrategicMerge: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +#- patches/webhook_in_clusters.yaml +#+kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +#- patches/cainjection_in_clusters.yaml +#+kubebuilder:scaffold:crdkustomizecainjectionpatch + +# the following config is for teaching kustomize how to do kustomization for CRDs. +configurations: +- kustomizeconfig.yaml diff --git a/config/crd/kustomizeconfig.yaml b/config/crd/kustomizeconfig.yaml new file mode 100644 index 00000000..ec5c150a --- /dev/null +++ b/config/crd/kustomizeconfig.yaml @@ -0,0 +1,19 @@ +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/config/crd/patches/cainjection_in_clientconfigs.yaml b/config/crd/patches/cainjection_in_clientconfigs.yaml new file mode 100644 index 00000000..e6dd2216 --- /dev/null +++ b/config/crd/patches/cainjection_in_clientconfigs.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: clientconfigs.config.registry.ethos.adobe.com diff --git a/config/crd/patches/cainjection_in_clusters.yaml b/config/crd/patches/cainjection_in_clusters.yaml new file mode 100644 index 00000000..f6262f8d --- /dev/null +++ b/config/crd/patches/cainjection_in_clusters.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: clusters.registry.ethos.adobe.com diff --git a/config/crd/patches/webhook_in_clientconfigs.yaml b/config/crd/patches/webhook_in_clientconfigs.yaml new file mode 100644 index 00000000..70d2dc78 --- /dev/null +++ b/config/crd/patches/webhook_in_clientconfigs.yaml @@ -0,0 +1,14 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clientconfigs.config.registry.ethos.adobe.com +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/crd/patches/webhook_in_clusters.yaml b/config/crd/patches/webhook_in_clusters.yaml new file mode 100644 index 00000000..4bc8b43d --- /dev/null +++ b/config/crd/patches/webhook_in_clusters.yaml @@ -0,0 +1,14 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clusters.registry.ethos.adobe.com +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml new file mode 100644 index 00000000..0e7f9739 --- /dev/null +++ b/config/default/kustomization.yaml @@ -0,0 +1,74 @@ +# Adds namespace to all resources. +namespace: kubebuilder-system + +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +namePrefix: kubebuilder- + +# Labels to add to all resources and selectors. +#commonLabels: +# someName: someValue + +bases: +- ../crd +- ../rbac +- ../manager +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- ../webhook +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. +#- ../certmanager +# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. +#- ../prometheus + +patchesStrategicMerge: +# Protect the /metrics endpoint by putting it behind auth. +# If you want your controller-manager to expose the /metrics +# endpoint w/o any authn/z, please comment the following line. +- manager_auth_proxy_patch.yaml + +# Mount the controller config file for loading manager configurations +# through a ComponentConfig type +#- manager_config_patch.yaml + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- manager_webhook_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. +# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. +# 'CERTMANAGER' needs to be enabled to use ca injection +#- webhookcainjection_patch.yaml + +# the following config is for teaching kustomize how to do var substitution +vars: +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. +#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR +# objref: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldref: +# fieldpath: metadata.namespace +#- name: CERTIFICATE_NAME +# objref: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +#- name: SERVICE_NAMESPACE # namespace of the service +# objref: +# kind: Service +# version: v1 +# name: webhook-service +# fieldref: +# fieldpath: metadata.namespace +#- name: SERVICE_NAME +# objref: +# kind: Service +# version: v1 +# name: webhook-service diff --git a/config/default/manager_auth_proxy_patch.yaml b/config/default/manager_auth_proxy_patch.yaml new file mode 100644 index 00000000..a224be19 --- /dev/null +++ b/config/default/manager_auth_proxy_patch.yaml @@ -0,0 +1,26 @@ +# This patch inject a sidecar container which is a HTTP proxy for the +# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: kube-rbac-proxy + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=10" + ports: + - containerPort: 8443 + name: https + - name: manager + args: + - "--health-probe-bind-address=:8081" + - "--metrics-bind-address=127.0.0.1:8080" + - "--leader-elect" diff --git a/config/default/manager_config_patch.yaml b/config/default/manager_config_patch.yaml new file mode 100644 index 00000000..6c400155 --- /dev/null +++ b/config/default/manager_config_patch.yaml @@ -0,0 +1,20 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + args: + - "--config=controller_manager_config.yaml" + volumeMounts: + - name: manager-config + mountPath: /controller_manager_config.yaml + subPath: controller_manager_config.yaml + volumes: + - name: manager-config + configMap: + name: manager-config diff --git a/config/manager/controller_manager_config.yaml b/config/manager/controller_manager_config.yaml new file mode 100644 index 00000000..c9ec3922 --- /dev/null +++ b/config/manager/controller_manager_config.yaml @@ -0,0 +1,21 @@ +apiVersion: config.registry.ethos.adobe.com/v1 +kind: ClientConfig +health: + healthProbeBindAddress: :8081 +metrics: + bindAddress: 127.0.0.1:8080 +webhook: + port: 9443 +leaderElection: + leaderElect: true + resourceNamespace: cluster-registry + resourceName: 0c4967d2.registry.ethos.adobe.com +namespace: cluster-registry +alertmanagerWebhook: + bindAddress: 127.0.0.1:9092 + alertMap: + - alertName: ClusterCapacityWarning + onFiring: + onboarding: "off" + onResolved: + onboarding: "on" diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml new file mode 100644 index 00000000..2bcd3eea --- /dev/null +++ b/config/manager/kustomization.yaml @@ -0,0 +1,10 @@ +resources: +- manager.yaml + +generatorOptions: + disableNameSuffixHash: true + +configMapGenerator: +- name: manager-config + files: + - controller_manager_config.yaml diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml new file mode 100644 index 00000000..bee8d4aa --- /dev/null +++ b/config/manager/manager.yaml @@ -0,0 +1,56 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + name: cluster-registry +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cluster-registry-client + namespace: cluster-registry + labels: + control-plane: controller-manager +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + labels: + control-plane: controller-manager + spec: + securityContext: + runAsNonRoot: true + containers: + - command: + - /cluster-registry-client + args: + - --leader-elect + image: controller:latest + name: manager + securityContext: + allowPrivilegeEscalation: false + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 100m + memory: 30Mi + requests: + cpu: 100m + memory: 20Mi + serviceAccountName: controller-manager + terminationGracePeriodSeconds: 10 diff --git a/config/prometheus/kustomization.yaml b/config/prometheus/kustomization.yaml new file mode 100644 index 00000000..ed137168 --- /dev/null +++ b/config/prometheus/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- monitor.yaml diff --git a/config/prometheus/monitor.yaml b/config/prometheus/monitor.yaml new file mode 100644 index 00000000..d19136ae --- /dev/null +++ b/config/prometheus/monitor.yaml @@ -0,0 +1,20 @@ + +# Prometheus Monitor Service (Metrics) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: controller-manager + name: controller-manager-metrics-monitor + namespace: system +spec: + endpoints: + - path: /metrics + port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true + selector: + matchLabels: + control-plane: controller-manager diff --git a/config/rbac/auth_proxy_client_clusterrole.yaml b/config/rbac/auth_proxy_client_clusterrole.yaml new file mode 100644 index 00000000..51a75db4 --- /dev/null +++ b/config/rbac/auth_proxy_client_clusterrole.yaml @@ -0,0 +1,9 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: metrics-reader +rules: +- nonResourceURLs: + - "/metrics" + verbs: + - get diff --git a/config/rbac/auth_proxy_role.yaml b/config/rbac/auth_proxy_role.yaml new file mode 100644 index 00000000..80e1857c --- /dev/null +++ b/config/rbac/auth_proxy_role.yaml @@ -0,0 +1,17 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/config/rbac/auth_proxy_role_binding.yaml b/config/rbac/auth_proxy_role_binding.yaml new file mode 100644 index 00000000..ec7acc0a --- /dev/null +++ b/config/rbac/auth_proxy_role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: proxy-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/config/rbac/auth_proxy_service.yaml b/config/rbac/auth_proxy_service.yaml new file mode 100644 index 00000000..6cf656be --- /dev/null +++ b/config/rbac/auth_proxy_service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + targetPort: https + selector: + control-plane: controller-manager diff --git a/config/rbac/clientconfig_editor_role.yaml b/config/rbac/clientconfig_editor_role.yaml new file mode 100644 index 00000000..449f58d9 --- /dev/null +++ b/config/rbac/clientconfig_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit clientconfigs. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: clientconfig-editor-role +rules: +- apiGroups: + - config.registry.ethos.adobe.com + resources: + - clientconfigs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - config.registry.ethos.adobe.com + resources: + - clientconfigs/status + verbs: + - get diff --git a/config/rbac/clientconfig_viewer_role.yaml b/config/rbac/clientconfig_viewer_role.yaml new file mode 100644 index 00000000..fe7e312d --- /dev/null +++ b/config/rbac/clientconfig_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view clientconfigs. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: clientconfig-viewer-role +rules: +- apiGroups: + - config.registry.ethos.adobe.com + resources: + - clientconfigs + verbs: + - get + - list + - watch +- apiGroups: + - config.registry.ethos.adobe.com + resources: + - clientconfigs/status + verbs: + - get diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml new file mode 100644 index 00000000..731832a6 --- /dev/null +++ b/config/rbac/kustomization.yaml @@ -0,0 +1,18 @@ +resources: +# All RBAC will be applied under this service account in +# the deployment namespace. You may comment out this resource +# if your manager will use a service account that exists at +# runtime. Be sure to update RoleBinding and ClusterRoleBinding +# subjects if changing service account names. +- service_account.yaml +- role.yaml +- role_binding.yaml +- leader_election_role.yaml +- leader_election_role_binding.yaml +# Comment the following 4 lines if you want to disable +# the auth proxy (https://github.com/brancz/kube-rbac-proxy) +# which protects your /metrics endpoint. +- auth_proxy_service.yaml +- auth_proxy_role.yaml +- auth_proxy_role_binding.yaml +- auth_proxy_client_clusterrole.yaml diff --git a/config/rbac/leader_election_role.yaml b/config/rbac/leader_election_role.yaml new file mode 100644 index 00000000..4190ec80 --- /dev/null +++ b/config/rbac/leader_election_role.yaml @@ -0,0 +1,37 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: leader-election-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/config/rbac/leader_election_role_binding.yaml b/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 00000000..1d1321ed --- /dev/null +++ b/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: leader-election-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml new file mode 100644 index 00000000..9c5b539c --- /dev/null +++ b/config/rbac/role.yaml @@ -0,0 +1,34 @@ + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: cluster-registry +rules: +- apiGroups: + - registry.ethos.adobe.com + resources: + - clusters + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - registry.ethos.adobe.com + resources: + - clusters/finalizers + verbs: + - update +- apiGroups: + - registry.ethos.adobe.com + resources: + - clusters/status + verbs: + - get + - patch + - update diff --git a/config/rbac/role_binding.yaml b/config/rbac/role_binding.yaml new file mode 100644 index 00000000..0aa84a22 --- /dev/null +++ b/config/rbac/role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: cluster-registry diff --git a/config/rbac/service_account.yaml b/config/rbac/service_account.yaml new file mode 100644 index 00000000..2163f493 --- /dev/null +++ b/config/rbac/service_account.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: controller-manager + namespace: cluster-registry diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..857f9e7c --- /dev/null +++ b/go.mod @@ -0,0 +1,39 @@ +module github.com/adobe/cluster-registry + +go 1.15 + +require ( + cloud.google.com/go v0.60.0 // indirect + github.com/Azure/go-autorest/autorest v0.11.19 // indirect + github.com/Azure/go-autorest/autorest/azure/auth v0.5.7 + github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 + github.com/aws/aws-sdk-go v1.36.30 + github.com/coreos/go-oidc/v3 v3.0.0 + github.com/go-logr/logr v0.4.0 + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.3 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-playground/universal-translator v0.17.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/jinzhu/gorm v1.9.8 + github.com/labstack/echo/v4 v4.3.0 + github.com/labstack/gommon v0.3.0 + github.com/leodido/go-urn v1.2.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/onsi/ginkgo v1.16.5 + github.com/onsi/gomega v1.17.0 + github.com/prometheus/client_golang v1.11.0 + github.com/stretchr/testify v1.7.0 + github.com/swaggo/echo-swagger v1.0.0 + github.com/swaggo/swag v1.7.0 + golang.org/x/crypto v0.0.0-20211202192323-5770296d904e // indirect + golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect + golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 + golang.org/x/tools v0.1.8 // indirect + gopkg.in/go-playground/validator.v9 v9.28.0 + gopkg.in/square/go-jose.v2 v2.5.1 + k8s.io/api v0.21.2 + k8s.io/apimachinery v0.21.2 + k8s.io/client-go v0.21.2 + sigs.k8s.io/controller-runtime v0.9.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..dc50b310 --- /dev/null +++ b/go.sum @@ -0,0 +1,982 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.60.0 h1:R+tDlceO7Ss+zyvtsdhTxacDyZ1k99xwskQ4FT7ruoM= +cloud.google.com/go v0.60.0/go.mod h1:yw2G51M9IfRboUH61Us8GqCeF1PzPblB823Mn2q2eAU= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= +github.com/Azure/go-autorest/autorest v0.11.17/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= +github.com/Azure/go-autorest/autorest v0.11.19 h1:7/IqD2fEYVha1EPeaiytVKhzmPV223pfkRIQUGOK2IE= +github.com/Azure/go-autorest/autorest v0.11.19/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= +github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/adal v0.9.11/go.mod h1:nBKAnTomx8gDtl+3ZCJv2v0KACFHWTB2drffI1B68Pk= +github.com/Azure/go-autorest/autorest/adal v0.9.13 h1:Mp5hbtOePIzM8pJVRa3YLrWWmZtoxRXqUEzCfJt3+/Q= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.7 h1:8DQB8yl7aLQuP+nuR5e2RO6454OvFlSTXXaNHshc16s= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.7/go.mod h1:AkzUsqkrdmNhfP2i54HqINVQopw0CLDnvHpJ88Zz1eI= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 h1:dMOmEJfkLKW/7JsokJqkyoYSgmR08hi9KrhjZb+JALY= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM= +github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-sdk-go v1.36.30 h1:hAwyfe7eZa7sM+S5mIJZFiNFwJMia9Whz6CYblioLoU= +github.com/aws/aws-sdk-go v1.36.30/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-oidc v2.1.0+incompatible h1:sdJrfw8akMnCuUlaZU3tE/uYXFgfqom8DBE9so9EBsM= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-oidc/v3 v3.0.0 h1:/mAA0XMgYJw2Uqm7WKGCsKnjitE/+A0FFbOmiRJm7LQ= +github.com/coreos/go-oidc/v3 v3.0.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.0.0-20190423183735-731ef375ac02/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.11.0+incompatible h1:glyUF9yIYtMHzn8xaKw5rMhdWcwsYV8dZHIq5567/xs= +github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w= +github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= +github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= +github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/zapr v0.4.0 h1:uc1uML3hRYL9/ZZPdgHS/n8Nzo+eaYL/Efxkkamf7OM= +github.com/go-logr/zapr v0.4.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/spec v0.19.14/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeEip0M63doA= +github.com/go-openapi/spec v0.20.3 h1:uH9RQ6vdyPSs2pSy9fL8QPspDF2AMIMPtmK5coSSjtQ= +github.com/go-openapi/spec v0.20.3/go.mod h1:gG4F8wdEDN+YPBMVnzE85Rbhf+Th2DTvA9nFPQ5AYEg= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.11/go.mod h1:Uc0gKkdR+ojzsEpjh39QChyu92vPgIr72POcgHMAgSY= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200507031123-427632fa3b1c/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= +github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jinzhu/gorm v1.9.8 h1:n5uvxqLepIP2R1XF7pudpt9Rv8I3m7G9trGxJVjLZ5k= +github.com/jinzhu/gorm v1.9.8/go.mod h1:bdqTT3q6dhSph2K3pWxrHP6nqxuAp2yQ3KFtc3U3F84= +github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.0.0/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.0.0/go.mod h1:tZv7nai5buKSg5h/8E6zz4LsD/Dqh9/91Mvs7Z5Zyno= +github.com/labstack/echo/v4 v4.3.0 h1:DCP6cbtT+Zu++K6evHOJzSgA2115cPMuCx0xg55q1EQ= +github.com/labstack/echo/v4 v4.3.0/go.mod h1:PvmtTvhVqKDzDQy4d3bWzPjZLzom4iQbAZy2sgZ/qI8= +github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4= +github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= +github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= +github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/swaggo/echo-swagger v1.0.0 h1:ppQFt6Am3/MHIUmTpZOwi4gggMZ/W9zmKP4Z9ahTe5c= +github.com/swaggo/echo-swagger v1.0.0/go.mod h1:Vnz3c2TGeFpoZPSV3CkWCrvyfU0016Gq/S0j4JspQnM= +github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM= +github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E= +github.com/swaggo/gin-swagger v1.2.0 h1:YskZXEiv51fjOMTsXrOetAjrMDfFaXD79PEoQBOe2W0= +github.com/swaggo/gin-swagger v1.2.0/go.mod h1:qlH2+W7zXGZkczuL+r2nEBR2JTT+/lX05Nn6vPhc7OI= +github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y= +github.com/swaggo/swag v1.6.3/go.mod h1:wcc83tB4Mb2aNiL/HP4MFeQdpHUrca+Rp/DRNgWAUio= +github.com/swaggo/swag v1.7.0 h1:5bCA/MTLQoIqDXXyHfOpMeDvL9j68OY/udlK4pQoo4E= +github.com/swaggo/swag v1.7.0/go.mod h1:BdPIL73gvS9NBsdi7M1JOxLvlbfvNRaBP8m6WT6Aajo= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0= +github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20211202192323-5770296d904e h1:MUP6MR3rJ7Gk9LEia0LP2ytiH6MuCfs7qYz+47jGdD8= +golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191204025024-5ee1b9f4859a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 h1:Vv0JUPWTyeqUq42B2WJ1FeIDjjvGKoA2Ss+Ts0lAVbs= +golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191205060818-73c7173a9f7d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w= +golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= +gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200626011028-ee7919e894b5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/go-playground/validator.v9 v9.28.0 h1:6pzvnzx1RWaaQiAmv6e1DvCFULRaz5cKoP5j1VcrLsc= +gopkg.in/go-playground/validator.v9 v9.28.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.21.2 h1:vz7DqmRsXTCSa6pNxXwQ1IYeAZgdIsua+DZU+o+SX3Y= +k8s.io/api v0.21.2/go.mod h1:Lv6UGJZ1rlMI1qusN8ruAp9PUBFyBwpEHAdG24vIsiU= +k8s.io/apiextensions-apiserver v0.21.2 h1:+exKMRep4pDrphEafRvpEi79wTnCFMqKf8LBtlA3yrE= +k8s.io/apiextensions-apiserver v0.21.2/go.mod h1:+Axoz5/l3AYpGLlhJDfcVQzCerVYq3K3CvDMvw6X1RA= +k8s.io/apimachinery v0.21.2 h1:vezUc/BHqWlQDnZ+XkrpXSmnANSLbpnlpwo0Lhk0gpc= +k8s.io/apimachinery v0.21.2/go.mod h1:CdTY8fU/BlvAbJ2z/8kBwimGki5Zp8/fbVuLY8gJumM= +k8s.io/apiserver v0.21.2/go.mod h1:lN4yBoGyiNT7SC1dmNk0ue6a5Wi6O3SWOIw91TsucQw= +k8s.io/client-go v0.21.2 h1:Q1j4L/iMN4pTw6Y4DWppBoUxgKO8LbffEMVEV00MUp0= +k8s.io/client-go v0.21.2/go.mod h1:HdJ9iknWpbl3vMGtib6T2PyI/VYxiZfq936WNVHBRrA= +k8s.io/code-generator v0.21.2/go.mod h1:8mXJDCB7HcRo1xiEQstcguZkbxZaqeUOrO9SsicWs3U= +k8s.io/component-base v0.21.2 h1:EsnmFFoJ86cEywC0DoIkAUiEV6fjgauNugiw1lmIjs4= +k8s.io/component-base v0.21.2/go.mod h1:9lvmIThzdlrJj5Hp8Z/TOgIkdfsNARQ1pT+3PByuiuc= +k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.8.0 h1:Q3gmuM9hKEjefWFFYF0Mat+YyFJvsUyYuwyNNJ5C9Ts= +k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= +k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7 h1:vEx13qjvaZ4yfObSSXW7BrMc/KQBBT/Jyee8XtLf4x0= +k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= +k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210527160623-6fdb442a123b h1:MSqsVQ3pZvPGTqCjptfimO2WjG7A9un2zcpiHkA6M/s= +k8s.io/utils v0.0.0-20210527160623-6fdb442a123b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.19/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/controller-runtime v0.9.2 h1:MnCAsopQno6+hI9SgJHKddzXpmv2wtouZz6931Eax+Q= +sigs.k8s.io/controller-runtime v0.9.2/go.mod h1:TxzMCHyEUpaeuOiZx/bIdc2T81vfs/aKdvJt9wuu0zk= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.1.0 h1:C4r9BgJ98vrKnnVCjwCSXcWjWe0NKcUQkmzDXZXGwH8= +sigs.k8s.io/structured-merge-diff/v4 v4.1.0/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt new file mode 100644 index 00000000..3d984ba6 --- /dev/null +++ b/hack/boilerplate.go.txt @@ -0,0 +1,11 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ diff --git a/hack/check_license.sh b/hack/check_license.sh new file mode 100755 index 00000000..1c234247 --- /dev/null +++ b/hack/check_license.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# exit immediately when a command fails +set -e +# only exit with zero if all commands of the pipeline exit successfully +set -o pipefail +# error on unset variables +set -u + +licRes=$( + find . -type f -iname '*.go' ! -path '*/vendor/*' -exec \ + sh -c 'head -n3 $1 | grep -Eq "(Copyright|generated|GENERATED)" || echo -e $1' {} {} \; +) + +if [ -n "${licRes}" ]; then + echo -e "license header checking failed:\\n${licRes}" + exit 255 +fi \ No newline at end of file diff --git a/pkg/api/api/handler.go b/pkg/api/api/handler.go new file mode 100644 index 00000000..7b6d9f43 --- /dev/null +++ b/pkg/api/api/handler.go @@ -0,0 +1,131 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package api + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/adobe/cluster-registry/pkg/api/authz" + "github.com/adobe/cluster-registry/pkg/api/database" + "github.com/adobe/cluster-registry/pkg/api/monitoring" + "github.com/adobe/cluster-registry/pkg/api/utils" + registryv1 "github.com/adobe/cluster-registry/pkg/cc/api/registry/v1" + "github.com/labstack/echo/v4" + "github.com/labstack/gommon/log" +) + +// Handler interface +type Handler interface { + GetCluster(echo.Context) error + ListClusters(echo.Context) error + Register(*echo.Group) +} + +// handler struct +type handler struct { + db database.Db + met monitoring.MetricsI +} + +// NewHandler func +func NewHandler(d database.Db, m monitoring.MetricsI) Handler { + h := &handler{ + db: d, + met: m, + } + return h +} + +func (h *handler) Register(v1 *echo.Group) { + a, err := authz.NewAuthenticator(h.met) + if err != nil { + log.Fatalf("Failed to initialize authenticator: %v", err) + } + clusters := v1.Group("/clusters", a.VerifyToken()) + clusters.GET("/:name", h.GetCluster) + clusters.GET("", h.ListClusters) +} + +// GetCluster godoc +// @Summary Get an cluster +// @Description Get an cluster. Auth is required +// @ID get-cluster +// @Tags cluster +// @Accept json +// @Produce json +// @Param name path string true "Name of the cluster to get" +// @Success 200 {object} registryv1.ClusterSpec +// @Failure 400 {object} utils.Error +// @Failure 500 {object} utils.Error +// @Security bearerAuth +// @Router /v1/clusters/{name} [get] +func (h *handler) GetCluster(ctx echo.Context) error { + name := ctx.Param("name") + c, err := h.db.GetCluster(name) + + if err != nil { + return ctx.JSON(http.StatusInternalServerError, utils.NewError(err)) + } + + if c == nil { + return ctx.JSON(http.StatusNotFound, utils.NotFound()) + } + + return ctx.JSON(http.StatusOK, newClusterResponse(ctx, c)) +} + +// ListClusters godoc +// @Summary List all clusters +// @Description List all clusters. Use query parameters to filter results. Auth is required +// @ID get-clusters +// @Tags cluster +// @Accept json +// @Produce json +// @Param region query string false "Filter by region" +// @Param environment query string false "Filter by environment" +// @Param businessUnit query string false "Filter by businessUnit" +// @Param status query string false "Filter by status" +// @Param limit query integer false "Limit number of clusters returned (default is 10)" +// @Param offset query integer false "Offset/skip number of clusters (default is 0)" +// @Success 200 {object} clusterList +// @Failure 500 {object} utils.Error +// @Security bearerAuth +// @Router /v1/clusters [get] +func (h *handler) ListClusters(ctx echo.Context) error { + var ( + clusters []registryv1.Cluster + count int + ) + + region := ctx.QueryParam("region") + environment := ctx.QueryParam("environment") + businessUnit := ctx.QueryParam("businessUnit") + status := ctx.QueryParam("status") + + offset, err := strconv.Atoi(ctx.QueryParam("offset")) + if err != nil { + offset = 0 + } + + limit, err := strconv.Atoi(ctx.QueryParam("limit")) + if err != nil { + limit = 20 + } + + fmt.Println(limit, offset) + clusters, count, _ = h.db.ListClusters(region, environment, businessUnit, status) + + return ctx.JSON(http.StatusOK, newClusterListResponse(clusters, count)) +} diff --git a/pkg/api/api/handler_test.go b/pkg/api/api/handler_test.go new file mode 100644 index 00000000..0fbf76c8 --- /dev/null +++ b/pkg/api/api/handler_test.go @@ -0,0 +1,180 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/adobe/cluster-registry/pkg/api/database" + "github.com/adobe/cluster-registry/pkg/api/monitoring" + registryv1 "github.com/adobe/cluster-registry/pkg/cc/api/registry/v1" + _ "github.com/jinzhu/gorm/dialects/sqlite" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" +) + +// mockDatabase database.db +type mockDatabase struct { + database.Db + clusters []registryv1.Cluster +} + +func (m mockDatabase) GetCluster(name string) (*registryv1.Cluster, error) { + for _, c := range m.clusters { + if c.Spec.Name == name { + return &c, nil + } + } + return nil, nil +} + +func (m mockDatabase) ListClusters(region string, environment string, businessUnit string, status string) ([]registryv1.Cluster, int, error) { + return m.clusters, len(m.clusters), nil +} + +func TestNewHandler(t *testing.T) { + test := assert.New(t) + d := mockDatabase{} + m := monitoring.NewMetrics("cluster_registry_api_handler_test", nil, true) + h := NewHandler(d, m) + test.NotNil(h) +} + +func TestGetCluster(t *testing.T) { + test := assert.New(t) + tcs := []struct { + name string + clusterName string + clusters []registryv1.Cluster + expectedResponse string + expectedStatus int + }{ + { + name: "get existing cluster", + clusterName: "cluster1", + clusters: []registryv1.Cluster{{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-14T06:15:32Z", + RegisteredAt: "2019-02-14T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "off"}, + }}}, + expectedStatus: http.StatusOK, + }, + { + name: "get nonexistent cluster", + clusterName: "cluster2", + clusters: []registryv1.Cluster{{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-14T06:15:32Z", + RegisteredAt: "2019-02-14T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "off"}, + }}}, + expectedStatus: http.StatusNotFound, + }, + } + for _, tc := range tcs { + + d := mockDatabase{clusters: tc.clusters} + m := monitoring.NewMetrics("cluster_registry_api_handler_test", nil, true) + h := NewHandler(d, m) + r := NewRouter() + + req := httptest.NewRequest(echo.GET, "/api/v1/clusters/:name", nil) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + + ctx := r.NewContext(req, rec) + ctx.SetPath("/api/articles/:name") + ctx.SetParamNames("name") + ctx.SetParamValues(tc.clusterName) + + err := h.GetCluster(ctx) + test.NoError(err) + + test.Equal(tc.expectedStatus, rec.Code) + + if rec.Code == http.StatusOK { + var c registryv1.ClusterSpec + err := json.Unmarshal(rec.Body.Bytes(), &c) + test.NoError(err) + test.Equal(tc.clusterName, c.Name) + } + } +} + +func TestListClusters(t *testing.T) { + test := assert.New(t) + tcs := []struct { + name string + clusters []registryv1.Cluster + expectedStatus int + expectedItems int + }{ + { + name: "get all clusters", + clusters: []registryv1.Cluster{{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-14T06:15:32Z", + RegisteredAt: "2019-02-14T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "off"}, + }}, { + Spec: registryv1.ClusterSpec{ + Name: "cluster2", + LastUpdated: "2020-02-13T06:15:32Z", + RegisteredAt: "2019-02-13T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "on"}, + }}}, + expectedStatus: http.StatusOK, + expectedItems: 2, + }, + } + for _, tc := range tcs { + + d := mockDatabase{clusters: tc.clusters} + m := monitoring.NewMetrics("cluster_registry_api_handler_test", nil, true) + h := NewHandler(d, m) + r := NewRouter() + + req := httptest.NewRequest(echo.GET, "/api/v1/clusters", nil) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + ctx := r.NewContext(req, rec) + + err := h.ListClusters(ctx) + + test.NoError(err) + test.Equal(tc.expectedStatus, rec.Code) + + if rec.Code == http.StatusOK { + var cl clusterList + err := json.Unmarshal(rec.Body.Bytes(), &cl) + + test.NoError(err) + test.Equal(tc.expectedItems, cl.ItemsCount) + } + } +} diff --git a/pkg/api/api/response.go b/pkg/api/api/response.go new file mode 100644 index 00000000..4875bfd6 --- /dev/null +++ b/pkg/api/api/response.go @@ -0,0 +1,41 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package api + +import ( + registryv1 "github.com/adobe/cluster-registry/pkg/cc/api/registry/v1" + "github.com/labstack/echo/v4" +) + +type clusterList struct { + Items []*registryv1.ClusterSpec `json:"items"` + ItemsCount int `json:"itemsCount"` +} + +func newClusterResponse(ctx echo.Context, c *registryv1.Cluster) *registryv1.ClusterSpec { + cs := &c.Spec + return cs +} + +func newClusterListResponse(clusters []registryv1.Cluster, count int) *clusterList { + r := new(clusterList) + r.Items = make([]*registryv1.ClusterSpec, 0) + + for _, c := range clusters { + cs := c.Spec + r.Items = append(r.Items, &cs) + } + + r.ItemsCount = count + return r +} diff --git a/pkg/api/api/router.go b/pkg/api/api/router.go new file mode 100644 index 00000000..4260c24d --- /dev/null +++ b/pkg/api/api/router.go @@ -0,0 +1,34 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package api + +import ( + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/labstack/gommon/log" +) + +// NewRouter func +func NewRouter() *echo.Echo { + e := echo.New() + e.Logger.SetLevel(log.DEBUG) + e.Pre(middleware.RemoveTrailingSlash()) + e.Use(middleware.Logger()) + e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: []string{"*"}, + AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization}, + AllowMethods: []string{echo.GET, echo.HEAD}, + })) + e.Validator = NewValidator() + return e +} diff --git a/pkg/api/api/validator.go b/pkg/api/api/validator.go new file mode 100644 index 00000000..31f9936a --- /dev/null +++ b/pkg/api/api/validator.go @@ -0,0 +1,32 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package api + +import "gopkg.in/go-playground/validator.v9" + +// NewValidator func +func NewValidator() *Validator { + return &Validator{ + validator: validator.New(), + } +} + +// Validator func +type Validator struct { + validator *validator.Validate +} + +// Validate func +func (v *Validator) Validate(i interface{}) error { + return v.validator.Struct(i) +} diff --git a/pkg/api/authz/jwt.go b/pkg/api/authz/jwt.go new file mode 100644 index 00000000..7fcd67ed --- /dev/null +++ b/pkg/api/authz/jwt.go @@ -0,0 +1,117 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package authz + +import ( + "errors" + "fmt" + "net/http" + "os" + "time" + + "github.com/adobe/cluster-registry/pkg/api/monitoring" + "github.com/adobe/cluster-registry/pkg/api/utils" + "github.com/coreos/go-oidc/v3/oidc" + "github.com/labstack/echo/v4" + "github.com/labstack/gommon/log" + + "golang.org/x/net/context" +) + +const ( + egressTarget = "azure_ad" +) + +var ( + clientID = os.Getenv("OIDC_CLIENT_ID") + issuerURL = os.Getenv("OIDC_ISSUER_URL") + tokenLookup = "Authorization" + authScheme = "Bearer" +) + +// Authenticator implements the OIDC authentication +type Authenticator struct { + verifier *oidc.IDTokenVerifier + ctx context.Context + met monitoring.MetricsI +} + +// NewAuthenticator creates new Authenticator +func NewAuthenticator(m monitoring.MetricsI) (*Authenticator, error) { + ctx := context.Background() + provider, err := oidc.NewProvider(ctx, issuerURL) + + if err != nil { + return nil, fmt.Errorf("init verifier failed: %v", err) + } + + config := &oidc.Config{ + ClientID: clientID, + } + + verifier := provider.Verifier(config) + return &Authenticator{ + verifier: verifier, + ctx: ctx, + met: m, + }, nil +} + +// VerifyToken verifies if the JWT token from request header is valid +func (a *Authenticator) VerifyToken() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + authorization := c.Request().Header.Get(tokenLookup) + + rawToken, err := extractToken(authorization) + if err != nil { + return c.JSON(http.StatusBadRequest, utils.NewError(err)) + } + + start := time.Now() + token, err := a.verifier.Verify(a.ctx, rawToken) + elapsed := float64(time.Since(start)) / float64(time.Second) + + a.met.RecordEgressRequestCnt(egressTarget) + a.met.RecordEgressRequestDur(egressTarget, elapsed) + + if err != nil { + return c.JSON(http.StatusForbidden, utils.NewError(err)) + } + + var claims struct { + Oid string `json:"oid"` + } + if err := token.Claims(&claims); err != nil { + return c.JSON(http.StatusForbidden, utils.NewError(err)) + } + + log.Info("Identity logged in: ", claims.Oid) + return next(c) + } + } +} + +// setVerifier set a custom verifier - used in testing +func (a *Authenticator) setVerifier(v *oidc.IDTokenVerifier) { + a.verifier = v +} + +// extractToken extracts the JWT token from authorization header data +func extractToken(authorization string) (string, error) { + l := len(authScheme) + if len(authorization) > l+1 && authorization[:l] == authScheme { + return authorization[l+1:], nil + } + return "", errors.New("missing or malformed jwt") +} diff --git a/pkg/api/authz/jwt_test.go b/pkg/api/authz/jwt_test.go new file mode 100644 index 00000000..7de6d72a --- /dev/null +++ b/pkg/api/authz/jwt_test.go @@ -0,0 +1,271 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package authz + +import ( + "crypto" + "crypto/x509" + "encoding/hex" + "encoding/json" + "encoding/pem" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/adobe/cluster-registry/pkg/api/monitoring" + "github.com/coreos/go-oidc/v3/oidc" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + "golang.org/x/net/context" + jose "gopkg.in/square/go-jose.v2" +) + +const ( + rsaPrivateKeyType = "RSA PRIVATE KEY" + rsaPublicKeyType = "RSA PUBLIC KEY" + dummyRsaPrivateKey = ` +-----BEGIN RSA PRIVATE KEY----- +MIIBOwIBAAJBAPjSG8Csjiz3in2EAHOXd3Q6tbqmJiQ8hargzGdKJvOwevQzc7i+ +FG0UDrGuRrM1xjgZHuCBHdQbBjq+WUsJsrECAwEAAQJAEyhqRp2CnOe6XAur1TqW +UfarQ2HDkgqu6AdC9bj54s1O68kktbN8/trbA4isTGquG5HQhV5JabprrXFhZNNS +oQIhAP/p6RYhtUnyaYzdrsC3TXYteDC2ddR7aKX0Av+ZHut9AiEA+OeV7tjgRTx5 +gWcJ+6E/jBjWgANYKJJwjfFyQIVWQkUCIHRnf2BTwNR78Urj4wNB3XgtwofV1s7p +u3YRAfQlQA05AiEA6rFbA3qFhWMvYp+onxZ9F/l3j/8XSjJCZOTMCSBwpE0CIQDB +VE2n2slEYlMHln2TVBAInRCL62pwHNQc269J9Y9bSQ== +-----END RSA PRIVATE KEY----- +` + invalidDummyRsaPrivateKey = ` +-----BEGIN RSA PRIVATE KEY----- +MIIBPAIBAAJBANGT0mwSY7+8gntZeUFexC2dpVOG81EBg/WLg1AnRTYTFTaYLFVO +TBxmH3zr/LiyKhcHcUBjhiob4lheJeg3k8sCAwEAAQJBAL2novVv0trRUdckSgmx +I6EQF2u2JPx6fZs4THW9g/GAwCWaT4cWKRzsjWdbHN5iFdLGaJqnu6Jx/Q/wSSh5 +dtkCIQD/dtKLcalDIadFmeg0N1l9AQeKzqsZ8RMwwau04SuoNwIhANIEXAu3llc8 +12/GK2dTQ23munaEOmr6lqUJWdt/+b8NAiAtWXiSzICRrD23e1TfQBwgtrgSChIR +rtwLQbYri/VmDQIhAK9lHq5Wc8OFt3LM8QDJA/5r/HvwcI1ZnKhWR+pOVfidAiEA +yVILFFMdumCAM2se/pV5rZq7e01UnH85py8Ba1Oe838= +-----END RSA PRIVATE KEY----- +` + noSignatureToken = ` +eyJhbGciOiJSUzI1NiIsImtpZCI6ImNkNTVlNTFiODM3YmMxM2Q4NzNjZmYxYTllY2ZmZTIyOTlkMTE1ZTAyOTUwYTM2ZTNiZDY2ZTVmZTBlNzNmNTYifQ.eyJhdWQiOiI2MDMyYjk4My1lZTVhLTQwOTYtOTk1Ny01NTczMmI5MmFiNDQiLCJleHAiOiIxNjE2MDg5NjU1IiwiaWF0IjoiMTYxNjA4NjA1NSIsImlwZCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0L2ZhN2IxYjVhLTdiMzQtNDM4Ny05NGFlLWQyYzE3OGRlY2VlMS8iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9mYTdiMWI1YS03YjM0LTQzODctOTRhZS1kMmMxNzhkZWNlZTEvIiwib2lkIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwIn0 +` + dummyOid = "00000000-0000-0000-0000-000000000000" + expiredDate = "2021-03-11T00:00:00Z" +) + +// getSigningKey converts rsaPrivateKey into a private/public JSONWebKey +func getSigningKey(rsaPrivateKey string, rsaKeyType string) *jose.JSONWebKey { + var key *jose.JSONWebKey + + block, _ := pem.Decode([]byte(rsaPrivateKey)) + if block == nil { + panic("failed to decode pem.") + } + + rsaKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + panic("failed to parse the key: " + err.Error()) + } + + if rsaKeyType == rsaPrivateKeyType { + key = &jose.JSONWebKey{Key: rsaKey, Use: "sig", Algorithm: string(jose.RS256)} + } else { + key = &jose.JSONWebKey{Key: rsaKey.Public(), Use: "sig", Algorithm: string(jose.RS256)} + } + + thumbprint, err := key.Thumbprint(crypto.SHA256) + if err != nil { + panic("failed to compute thumbprint:" + err.Error()) + } + + key.KeyID = hex.EncodeToString(thumbprint) + return key +} + +type claim struct { + key string + value string +} + +// dummyToken represent the token claims +type dummyToken struct { + claims map[string]string +} + +// newDummyToken +func newDummyToken() *dummyToken { + claims := make(map[string]string) + claims["exp"] = fmt.Sprint(time.Now().Add(1 * time.Hour).Unix()) + claims["iat"] = fmt.Sprint(time.Now().Unix()) + claims["iss"] = issuerURL + claims["ipd"] = issuerURL + claims["aud"] = clientID + claims["oid"] = dummyOid + + return &dummyToken{claims: claims} +} + +// setExpiration sets the token expiration +func (t *dummyToken) setExpiration(tm time.Time) { + t.claims["exp"] = fmt.Sprint(tm.Unix()) +} + +// setClaim sets a token claim +func (t *dummyToken) setClaim(c claim) { + t.claims[c.key] = c.value +} + +// signToken +func (t *dummyToken) sign() string { + signingKey := getSigningKey(dummyRsaPrivateKey, rsaPrivateKeyType) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.RS256, + Key: signingKey, + }, nil) + if err != nil { + panic(err.Error()) + } + + claimString, err := json.Marshal(t.claims) + if err != nil { + panic(err.Error()) + } + + signedToken, err := signer.Sign([]byte(claimString)) + if err != nil { + panic(err.Error()) + } + + serializedToken, err := signedToken.CompactSerialize() + if err != nil { + panic(err.Error()) + } + return serializedToken +} + +// staticKeySet implements oidc.KeySet +type staticKeySet struct { + keys []*jose.JSONWebKey +} + +// VerifySignature overwrites oidc.KeySet.VerifySignature +func (s *staticKeySet) VerifySignature(ctx context.Context, jwt string) (payload []byte, err error) { + jws, err := jose.ParseSigned(jwt) + if err != nil { + return nil, err + } + return jws.Verify(s.keys[0]) +} + +// buildAuthHeader builds the authorization header with a JWT bearer token +func buildAuthHeader(expiredToken bool, c claim) string { + dt := newDummyToken() + + if expiredToken == true { + expiration, _ := time.Parse(time.RFC3339Nano, expiredDate) + dt.setExpiration(expiration) + } + + if c.key != "" { + dt.setClaim(c) + } + + signedToken := dt.sign() + return authScheme + " " + signedToken +} + +func TestToken(t *testing.T) { + + test := assert.New(t) + tcs := []struct { + name string + code int + authHeader string + rsaSigningKey string + }{ + { + name: "valid token", + authHeader: buildAuthHeader(false, claim{}), + code: http.StatusOK, + rsaSigningKey: dummyRsaPrivateKey, + }, + { + name: "no authorization header", + authHeader: "", + code: http.StatusBadRequest, + rsaSigningKey: dummyRsaPrivateKey, + }, + { + name: "no bearer token", + authHeader: "test: test", + code: http.StatusBadRequest, + rsaSigningKey: dummyRsaPrivateKey, + }, + { + name: "no signature", + authHeader: authScheme + " " + noSignatureToken, + code: http.StatusForbidden, + rsaSigningKey: dummyRsaPrivateKey, + }, + { + name: "invalid signature", + authHeader: buildAuthHeader(false, claim{}), + code: http.StatusForbidden, + rsaSigningKey: invalidDummyRsaPrivateKey, + }, + { + name: "expired token", + authHeader: buildAuthHeader(true, claim{}), + code: http.StatusForbidden, + rsaSigningKey: dummyRsaPrivateKey, + }, + { + name: "invalid aud", + authHeader: buildAuthHeader(false, claim{key: "aud", value: "test"}), + code: http.StatusForbidden, + rsaSigningKey: dummyRsaPrivateKey, + }, + } + + e := echo.New() + handler := func(c echo.Context) error { + return c.String(http.StatusOK, "test123") + } + + for _, tc := range tcs { + req := httptest.NewRequest(echo.GET, "http://localhost/api/v1/clusters", nil) + if tc.authHeader != "" { + req.Header.Set(echo.HeaderAuthorization, tc.authHeader) + } + res := httptest.NewRecorder() + c := e.NewContext(req, res) + + m := monitoring.NewMetrics("cluster_registry_api_authz_test", nil, true) + auth, err := NewAuthenticator(m) + pubKeys := []*jose.JSONWebKey{getSigningKey(tc.rsaSigningKey, rsaPublicKeyType)} + + if err != nil { + t.Fatalf("Failed to initialize authenticator: %v", err) + } + auth.setVerifier(oidc.NewVerifier( + issuerURL, + &staticKeySet{keys: pubKeys}, + &oidc.Config{ClientID: clientID}, + )) + + h := auth.VerifyToken()(handler) + test.NoError(h(c)) + assert.Equal(t, tc.code, c.Response().Status) + } +} diff --git a/pkg/api/database/database.go b/pkg/api/database/database.go new file mode 100644 index 00000000..f08c0d0c --- /dev/null +++ b/pkg/api/database/database.go @@ -0,0 +1,257 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package database + +import ( + "os" + "reflect" + "time" + + "github.com/adobe/cluster-registry/pkg/api/monitoring" + registryv1 "github.com/adobe/cluster-registry/pkg/cc/api/registry/v1" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" + "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface" + "github.com/aws/aws-sdk-go/service/dynamodb/expression" + "github.com/labstack/gommon/log" +) + +const ( + egressTarget = "database" +) + +// Db provides an interface for interacting with dynamonDb +type Db interface { + GetCluster(name string) (*registryv1.Cluster, error) + ListClusters(region string, environment string, businessUnit string, status string) ([]registryv1.Cluster, int, error) + PutCluster(cluster *registryv1.Cluster) error + DeleteCluster(name string) error +} + +// db struct +type db struct { + dbAPI dynamodbiface.DynamoDBAPI + tableName string + met monitoring.MetricsI +} + +// ClusterDb encapsulates the Cluster CRD +type ClusterDb struct { + Name string `json:"name,hash"` + Cluster *registryv1.Cluster `json:"crd,hash"` +} + +// NewDb func +func NewDb(m monitoring.MetricsI) Db { + dbEndpoint := os.Getenv("DB_ENDPOINT") + awsRegion := os.Getenv("DB_AWS_REGION") + dbTableName := os.Getenv("DB_TABLE_NAME") + + sess := session.Must(session.NewSession(&aws.Config{ + Region: aws.String(awsRegion), + Endpoint: aws.String(dbEndpoint), + })) + + d := dynamodb.New(sess) + dbInst := &db{ + dbAPI: d, + tableName: dbTableName, + met: m, + } + + return dbInst +} + +// GetCluster a single cluster +func (d *db) GetCluster(name string) (*registryv1.Cluster, error) { + params := &dynamodb.GetItemInput{ + TableName: &d.tableName, + Key: map[string]*dynamodb.AttributeValue{ + "name": { + S: aws.String(name), + }, + }, + } + + start := time.Now() + resp, err := d.dbAPI.GetItem(params) + elapsed := float64(time.Since(start)) / float64(time.Second) + + d.met.RecordEgressRequestCnt(egressTarget) + d.met.RecordEgressRequestDur(egressTarget, elapsed) + + if err != nil { + log.Warn(err.Error()) + return nil, err + } + + if resp.Item == nil { + log.Warn("Cluster " + name + " not found") + return nil, nil + } + + var clusterDb *ClusterDb + err = dynamodbattribute.UnmarshalMap(resp.Item, &clusterDb) + if err != nil { + log.Error(err.Error()) + return nil, err + } + + return clusterDb.Cluster, err +} + +// ListClusters all clusters +func (d *db) ListClusters(region string, environment string, businessUnit string, status string) ([]registryv1.Cluster, int, error) { + var clusters []registryv1.Cluster = []registryv1.Cluster{} + var err error + + // add all params to a map + queryParams := make(map[string]string, 4) + var params *dynamodb.ScanInput + var filt expression.ConditionBuilder + + if region != "" { + queryParams["region"] = region + } + if environment != "" { + queryParams["environment"] = environment + } + if businessUnit != "" { + queryParams["businessUnit"] = businessUnit + } + if status != "" { + queryParams["status"] = status + } + + // TODO: use nested filtering + if len(queryParams) > 0 { + for k, v := range queryParams { + if reflect.DeepEqual(filt, expression.ConditionBuilder{}) { + filt = expression.Name(k).Equal(expression.Value(v)) + } else { + filt = filt.And(expression.Name(k).Equal(expression.Value(v))) + } + } + + expr, err := expression.NewBuilder().WithFilter(filt).Build() + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } + params = &dynamodb.ScanInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + FilterExpression: expr.Filter(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(d.tableName), + } + } else { + params = &dynamodb.ScanInput{ + TableName: aws.String(d.tableName), + } + } + + for { + start := time.Now() + result, err := d.dbAPI.Scan(params) + elapsed := float64(time.Since(start)) / float64(time.Second) + + d.met.RecordEgressRequestCnt(egressTarget) + d.met.RecordEgressRequestDur(egressTarget, elapsed) + + if err != nil { + log.Error("Query DynamonDB API call failed: " + err.Error()) + } + + for _, i := range result.Items { + item := ClusterDb{} + + err = dynamodbattribute.UnmarshalMap(i, &item) + + if err != nil { + log.Error("Got error unmarshalling: " + err.Error()) + } + + clusters = append(clusters, *item.Cluster) + } + if result.LastEvaluatedKey == nil { + break + } + params.ExclusiveStartKey = result.LastEvaluatedKey + } + + return clusters, len(clusters), err +} + +// PutCluster (create/update) a cluster in database +func (d *db) PutCluster(cluster *registryv1.Cluster) error { + + clusterDb, err := dynamodbattribute.MarshalMap(ClusterDb{Name: cluster.Spec.Name, Cluster: cluster}) + if err != nil { + log.Error("Cannot marshal cluster into AttributeValue map") + return err + } + + params := &dynamodb.PutItemInput{ + TableName: &d.tableName, + Item: clusterDb, + } + + start := time.Now() + _, err = d.dbAPI.PutItem(params) + elapsed := float64(time.Since(start)) / float64(time.Second) + + d.met.RecordEgressRequestCnt(egressTarget) + d.met.RecordEgressRequestDur(egressTarget, elapsed) + + if err != nil { + log.Error(err.Error()) + return err + } + + log.Info("Cluster " + cluster.Name + " updated successfully") + + return err +} + +// DeleteCluster delete a cluster from database +func (d *db) DeleteCluster(name string) error { + params := &dynamodb.DeleteItemInput{ + TableName: &d.tableName, + Key: map[string]*dynamodb.AttributeValue{ + "name": { + S: aws.String(name), + }, + }, + } + + start := time.Now() + _, err := d.dbAPI.DeleteItem(params) + elapsed := float64(time.Since(start)) / float64(time.Second) + + d.met.RecordEgressRequestCnt(egressTarget) + d.met.RecordEgressRequestDur(egressTarget, elapsed) + + if err != nil { + log.Error(err.Error()) + return err + } + + log.Info("Cluster " + name + " deleted successfully") + + return err +} + +// DeleteAllClusters diff --git a/pkg/api/database/database_test.go b/pkg/api/database/database_test.go new file mode 100644 index 00000000..8e648008 --- /dev/null +++ b/pkg/api/database/database_test.go @@ -0,0 +1,452 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package database + +import ( + "errors" + "fmt" + "os" + "sort" + "testing" + + "github.com/adobe/cluster-registry/pkg/api/monitoring" + registryv1 "github.com/adobe/cluster-registry/pkg/cc/api/registry/v1" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" + "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface" + "github.com/stretchr/testify/assert" +) + +type mockDynamoDBClient struct { + dynamodbiface.DynamoDBAPI + clusters map[string]*ClusterDb +} + +func (m *mockDynamoDBClient) GetItem(input *dynamodb.GetItemInput) (*dynamodb.GetItemOutput, error) { + + resp, err := dynamodbattribute.MarshalMap( + m.clusters[*input.Key["name"].S], + ) + if err != nil { + return nil, err + } + + output := &dynamodb.GetItemOutput{ + Item: resp, + } + return output, nil +} + +func (m *mockDynamoDBClient) DeleteItem(input *dynamodb.DeleteItemInput) (*dynamodb.DeleteItemOutput, error) { + + clusterName := *input.Key["name"].S + + if c, exist := m.clusters[clusterName]; exist { + delete(m.clusters, c.Name) + } else { + return nil, errors.New("cluster not found") + } + + output := &dynamodb.DeleteItemOutput{} + return output, nil +} + +func (m *mockDynamoDBClient) PutItem(input *dynamodb.PutItemInput) (*dynamodb.PutItemOutput, error) { + var cluster ClusterDb + err := dynamodbattribute.UnmarshalMap(input.Item, &cluster) + if err != nil { + return nil, err + } + + m.clusters[cluster.Name] = &cluster + return &dynamodb.PutItemOutput{}, err +} + +func (m *mockDynamoDBClient) Scan(input *dynamodb.ScanInput) (*dynamodb.ScanOutput, error) { + var resp []map[string]*dynamodb.AttributeValue + + for _, c := range m.clusters { + attr, err := dynamodbattribute.MarshalMap( + c, + ) + if err != nil { + return nil, err + } + resp = append(resp, attr) + } + + output := &dynamodb.ScanOutput{ + Items: resp, + } + return output, nil +} + +func TestNewDb(t *testing.T) { + test := assert.New(t) + + os.Setenv("DB_ENDPOINT", "dummy-url") + os.Setenv("DB_AWS_REGION", "dummy-region") + + m := monitoring.NewMetrics("cluster_registry_api_database_test", nil, true) + d := NewDb(m) + test.NotNil(d) +} + +func TestPutCluster(t *testing.T) { + test := assert.New(t) + tcs := []struct { + name string + dbClusters map[string]*ClusterDb + newCluster *registryv1.Cluster + expectedClusters []registryv1.Cluster + expectedError error + }{ + { + name: "new cluster", + dbClusters: map[string]*ClusterDb{}, + newCluster: ®istryv1.Cluster{ + Spec: registryv1.ClusterSpec{ + Name: "cluster2", + LastUpdated: "2020-02-14T06:15:32Z", + RegisteredAt: "2019-02-14T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "off"}, + }}, + expectedClusters: []registryv1.Cluster{{ + Spec: registryv1.ClusterSpec{ + Name: "cluster2", + LastUpdated: "2020-02-14T06:15:32Z", + RegisteredAt: "2019-02-14T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "off"}, + }}, + }, + expectedError: nil, + }, + { + name: "existing cluster", + dbClusters: map[string]*ClusterDb{ + "cluster1": { + Name: "cluster1", + Cluster: ®istryv1.Cluster{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-13T06:15:32Z", + RegisteredAt: "2019-02-13T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "on"}, + }, + }, + }}, + newCluster: ®istryv1.Cluster{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-14T06:15:32Z", + RegisteredAt: "2019-02-13T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "off"}, + }}, + expectedClusters: []registryv1.Cluster{{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-14T06:15:32Z", + RegisteredAt: "2019-02-13T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "off"}, + }}, + }, + expectedError: nil, + }, + } + + for _, tc := range tcs { + db := &db{ + dbAPI: &mockDynamoDBClient{clusters: tc.dbClusters}, + tableName: "cluster-registry", + met: monitoring.NewMetrics("cluster_registry_api_database_test", nil, true), + } + + err := db.PutCluster(tc.newCluster) + + if tc.expectedError != nil { + test.Error(err, "there should be an error processing the message") + test.Contains(fmt.Sprintf("%v", err), fmt.Sprintf("%v", tc.expectedError), "the error message should be as expected") + } else { + test.NoError(err) + } + + clusters, _, err := db.ListClusters("", "", "", "") + test.NoError(err) + + test.Equal(tc.expectedClusters, clusters) + } +} + +func TestDeleteCluster(t *testing.T) { + test := assert.New(t) + tcs := []struct { + name string + clusterName string + dbClusters map[string]*ClusterDb + expectedClusters []registryv1.Cluster + expectedError error + }{ + { + name: "existing cluster", + clusterName: "cluster1", + dbClusters: map[string]*ClusterDb{ + "cluster1": { + Name: "cluster1", + Cluster: ®istryv1.Cluster{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-13T06:15:32Z", + RegisteredAt: "2019-02-13T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "on"}, + }, + }, + }}, + expectedClusters: []registryv1.Cluster{}, + expectedError: nil, + }, + { + name: "non existing cluster", + clusterName: "cluster2", + dbClusters: map[string]*ClusterDb{ + "cluster1": { + Name: "cluster1", + Cluster: ®istryv1.Cluster{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-13T06:15:32Z", + RegisteredAt: "2019-02-13T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "on"}, + }, + }, + }}, + expectedClusters: []registryv1.Cluster{{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-13T06:15:32Z", + RegisteredAt: "2019-02-13T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "on"}, + }}, + }, + expectedError: fmt.Errorf("cluster not found"), + }, + } + + for _, tc := range tcs { + db := &db{ + dbAPI: &mockDynamoDBClient{clusters: tc.dbClusters}, + tableName: "cluster-registry", + met: monitoring.NewMetrics("cluster_registry_api_database_test", nil, true), + } + + err := db.DeleteCluster(tc.clusterName) + + if tc.expectedError != nil { + test.Error(err, "there should be an error processing the message") + test.Contains(fmt.Sprintf("%v", err), fmt.Sprintf("%v", tc.expectedError), "the error message should be as expected") + } else { + test.NoError(err) + } + + c, _, err := db.ListClusters("", "", "", "") + test.NoError(err) + + test.Equal(tc.expectedClusters, c) + } +} + +func TestGetCluster(t *testing.T) { + test := assert.New(t) + tcs := []struct { + name string + clusterName string + dbClusters map[string]*ClusterDb + expectedCluster *registryv1.Cluster + expectedError error + }{ + { + name: "existing cluster", + clusterName: "cluster1", + dbClusters: map[string]*ClusterDb{ + "cluster1": { + Name: "cluster1", + Cluster: ®istryv1.Cluster{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-13T06:15:32Z", + RegisteredAt: "2019-02-13T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "on"}, + }, + }, + }}, + expectedCluster: ®istryv1.Cluster{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-13T06:15:32Z", + RegisteredAt: "2019-02-13T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "on"}, + }}, + expectedError: nil, + }, + { + name: "non existing cluster", + clusterName: "cluster2", + dbClusters: map[string]*ClusterDb{ + "cluster1": { + Name: "cluster1", + Cluster: ®istryv1.Cluster{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-13T06:15:32Z", + RegisteredAt: "2019-02-13T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "on"}, + }, + }, + }}, + expectedCluster: nil, + expectedError: nil, + }, + } + + for _, tc := range tcs { + db := &db{ + dbAPI: &mockDynamoDBClient{clusters: tc.dbClusters}, + tableName: "cluster-registry", + met: monitoring.NewMetrics("cluster_registry_api_database_test", nil, true), + } + + c, err := db.GetCluster(tc.clusterName) + + if tc.expectedError != nil { + test.Error(err, "there should be an error processing the message") + test.Contains(fmt.Sprintf("%v", err), fmt.Sprintf("%v", tc.expectedError), "the error message should be as expected") + } else { + test.NoError(err) + } + test.Equal(tc.expectedCluster, c) + } +} + +// TODO: add tests for filtering +func TestListClusters(t *testing.T) { + test := assert.New(t) + tcs := []struct { + name string + queryParams map[string]string + dbClusters map[string]*ClusterDb + expectedClusters []registryv1.Cluster + expectedError error + }{ + { + name: "all clusters", + queryParams: map[string]string{ + "region": "", + "environment": "", + "businessUnit": "", + "status": "", + }, + dbClusters: map[string]*ClusterDb{ + "cluster1": { + Name: "cluster1", + Cluster: ®istryv1.Cluster{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-13T06:15:32Z", + RegisteredAt: "2019-02-13T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "on"}, + }, + }, + }, + "cluster2": { + Name: "cluster2", + Cluster: ®istryv1.Cluster{ + Spec: registryv1.ClusterSpec{ + Name: "cluster2", + LastUpdated: "2020-02-14T06:15:32Z", + RegisteredAt: "2019-02-14T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "off"}, + }, + }, + }}, + expectedClusters: []registryv1.Cluster{{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-13T06:15:32Z", + RegisteredAt: "2019-02-13T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "on"}, + }}, { + Spec: registryv1.ClusterSpec{ + Name: "cluster2", + LastUpdated: "2020-02-14T06:15:32Z", + RegisteredAt: "2019-02-14T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "off"}, + }}, + }, + expectedError: nil, + }, + } + + for _, tc := range tcs { + db := &db{ + dbAPI: &mockDynamoDBClient{clusters: tc.dbClusters}, + tableName: "cluster-registry", + met: monitoring.NewMetrics("cluster_registry_api_database_test", nil, true), + } + + clusters, _, err := db.ListClusters( + tc.queryParams["region"], tc.queryParams["environments"], + tc.queryParams["businessUnit"], tc.queryParams["status"]) + + if tc.expectedError != nil { + test.Error(err, "there should be an error processing the message") + test.Contains(fmt.Sprintf("%v", err), fmt.Sprintf("%v", tc.expectedError), "the error message should be as expected") + } else { + test.NoError(err) + } + + sort.Slice(clusters, func(i, j int) bool { + return clusters[i].Spec.Name < clusters[j].Spec.Name + }) + + test.Equal(tc.expectedClusters, clusters) + } +} diff --git a/pkg/api/docs/docs.go b/pkg/api/docs/docs.go new file mode 100644 index 00000000..7d0b0b4f --- /dev/null +++ b/pkg/api/docs/docs.go @@ -0,0 +1,527 @@ +// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT +// This file was generated by swaggo/swag + +package docs + +import ( + "bytes" + "encoding/json" + "strings" + + "github.com/alecthomas/template" + "github.com/swaggo/swag" +) + +var doc = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{.Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/v1/clusters": { + "get": { + "security": [ + { + "bearerAuth": [] + } + ], + "description": "List all clusters. Use query parameters to filter results. Auth is required", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "cluster" + ], + "summary": "List all clusters", + "operationId": "get-clusters", + "parameters": [ + { + "type": "string", + "description": "Filter by region", + "name": "region", + "in": "query" + }, + { + "type": "string", + "description": "Filter by environment", + "name": "environment", + "in": "query" + }, + { + "type": "string", + "description": "Filter by businessUnit", + "name": "businessUnit", + "in": "query" + }, + { + "type": "string", + "description": "Filter by status", + "name": "status", + "in": "query" + }, + { + "type": "integer", + "description": "Limit number of clusters returned (default is 10)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset/skip number of clusters (default is 0)", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.clusterList" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.Error" + } + } + } + } + }, + "/v1/clusters/{name}": { + "get": { + "security": [ + { + "bearerAuth": [] + } + ], + "description": "Get an cluster. Auth is required", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "cluster" + ], + "summary": "Get an cluster", + "operationId": "get-cluster", + "parameters": [ + { + "type": "string", + "description": "Name of the cluster to get", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.ClusterSpec" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.Error" + } + } + } + } + } + }, + "definitions": { + "api.clusterList": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ClusterSpec" + } + }, + "itemsCount": { + "type": "integer" + } + } + }, + "utils.Error": { + "type": "object", + "properties": { + "errors": { + "type": "object", + "additionalProperties": true + } + } + }, + "v1.APIServer": { + "type": "object", + "properties": { + "certificateAuthorityData": { + "description": "Information about K8s Api CA Cert", + "type": "string" + }, + "endpoint": { + "description": "Information about K8s Api Endpoint\n+kubebuilder:validation:Required", + "type": "string" + } + } + }, + "v1.AllowedOnboardingTeam": { + "type": "object", + "properties": { + "gitTeams": { + "description": "List of git teams", + "type": "array", + "items": { + "type": "string" + } + }, + "ldapGroups": { + "description": "List of ldap groups", + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "description": "Name of the team\n+kubebuilder:validation:Required", + "type": "string" + } + } + }, + "v1.ClusterSpec": { + "type": "object", + "properties": { + "accountId": { + "description": "The cloud account associated with the cluster\n+kubebuilder:validation:Required", + "type": "string" + }, + "allowedOnboardingTeams": { + "description": "Git teams and/or LDAP groups that are allowed to onboard and deploy on the cluster", + "type": "array", + "items": { + "$ref": "#/definitions/v1.AllowedOnboardingTeam" + } + }, + "apiServer": { + "description": "Information about K8s API endpoint and CA cert\n+kubebuilder:validation:Required", + "$ref": "#/definitions/v1.APIServer" + }, + "businessUnit": { + "description": "The BU that owns and maintains the cluster\n+kubebuilder:validation:Required", + "type": "string" + }, + "capabilities": { + "description": "List of cluster capabilities", + "type": "array", + "items": { + "type": "string" + } + }, + "cloudType": { + "description": "The cloud provider.\n+kubebuilder:validation:Required", + "type": "string" + }, + "environment": { + "description": "Cluster environment.\n+kubebuilder:validation:Required", + "type": "string" + }, + "extra": { + "description": "Ethos Extra specific information", + "$ref": "#/definitions/v1.Extra" + }, + "k8sInfraRelease": { + "description": "K8s Infrastructure release information\n+kubebuilder:validation:Required", + "$ref": "#/definitions/v1.K8sInfraRelease" + }, + "lastUpdated": { + "description": "Timestamp when cluster information was updated", + "type": "string" + }, + "name": { + "description": "Cluster name\n+kubebuilder:validation:Required\n+kubebuilder:validation:MaxLength=64\n+kubebuilder:validation:MinLength=3", + "type": "string" + }, + "offering": { + "description": "The Ethos offering that the cluster is meant for\n+kubebuilder:validation:Required", + "type": "array", + "items": { + "type": "string" + } + }, + "peerVirtualNetworks": { + "description": "Information about Virtual Networks peered with the cluster", + "type": "array", + "items": { + "$ref": "#/definitions/v1.PeerVirtualNetwork" + } + }, + "phase": { + "description": "Cluster phase\n+kubebuilder:validation:Required\n+kubebuilder:validation:Enum=Building;Testing;Running;Upgrading", + "type": "string" + }, + "region": { + "description": "Cluster standard region name\n+kubebuilder:validation:Required", + "type": "string" + }, + "registeredAt": { + "description": "Timestamp when cluster was registered in Cluster Registry\n+kubebuilder:validation:Required", + "type": "string" + }, + "shortName": { + "description": "Cluster name, without dash.\n+kubebuilder:validation:Required\n+kubebuilder:validation:MaxLength=64\n+kubebuilder:validation:MinLength=3", + "type": "string" + }, + "status": { + "description": "Cluster status\n+kubebuilder:validation:Required\n+kubebuilder:validation:Enum=Inactive;Active;Deprecated;Deleted", + "type": "string" + }, + "tags": { + "description": "Cluster tags that were applied", + "type": "array", + "items": { + "type": "string" + } + }, + "tiers": { + "description": "List of tiers with their associated information\n+kubebuilder:validation:Required", + "type": "array", + "items": { + "$ref": "#/definitions/v1.Tier" + } + }, + "type": { + "description": "The type of the cluster", + "type": "string" + }, + "virtualNetworks": { + "description": "Virtual Private Networks information\n+kubebuilder:validation:Required", + "type": "array", + "items": { + "$ref": "#/definitions/v1.VirtualNetwork" + } + } + } + }, + "v1.Extra": { + "type": "object", + "properties": { + "domainName": { + "description": "Name of the domain\n+kubebuilder:validation:Required", + "type": "string" + }, + "ecrIamArns": { + "description": "List of IAM Arns", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "egressPorts": { + "description": "Egress ports allowed outside of the namespace", + "type": "string" + }, + "lbEndpoints": { + "description": "Load balancer endpoints\n+kubebuilder:validation:Required", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "loggingEndpoints": { + "description": "Logging endpoints\n+kubebuilder:validation:Required", + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "nfsInfo": { + "description": "NFS information", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "v1.K8sInfraRelease": { + "type": "object", + "properties": { + "gitSha": { + "description": "GitSha of the release", + "type": "string" + }, + "lastUpdated": { + "description": "When the release was applied on the cluster", + "type": "string" + }, + "release": { + "description": "Release name", + "type": "string" + } + } + }, + "v1.PeerVirtualNetwork": { + "type": "object", + "properties": { + "cidrs": { + "description": "Remote Virtual Netowrk CIDRs", + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "description": "Remote Virtual Netowrk ID", + "type": "string" + }, + "ownerID": { + "description": "Cloud account of the owner", + "type": "string" + } + } + }, + "v1.Tier": { + "type": "object", + "properties": { + "containerRuntime": { + "description": "Container runtime\n+kubebuilder:validation:Required\n+kubebuilder:validation:Enum=docker;cri-o", + "type": "string" + }, + "enableKataSupport": { + "description": "EnableKataSupport", + "type": "boolean" + }, + "instanceType": { + "description": "Type of the instances\n+kubebuilder:validation:Required", + "type": "string" + }, + "kernelParameters": { + "description": "KernelParameters", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "labels": { + "description": "Instance K8s labels", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "maxCapacity": { + "description": "Max number of instances\n+kubebuilder:validation:Required", + "type": "integer" + }, + "minCapacity": { + "description": "Min number of instances\n+kubebuilder:validation:Required", + "type": "integer" + }, + "name": { + "description": "Name of the tier\n+kubebuilder:validation:Required", + "type": "string" + }, + "taints": { + "description": "Instance K8s taints", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "v1.VirtualNetwork": { + "type": "object", + "properties": { + "cidrs": { + "description": "CIDRs used in this VirtualNetwork\n+kubebuilder:validation:Required", + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "description": "Virtual private network Id\n+kubebuilder:validation:Required", + "type": "string" + } + } + } + }, + "securityDefinitions": { + "bearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +}` + +type swaggerInfo struct { + Version string + Host string + BasePath string + Schemes []string + Title string + Description string +} + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = swaggerInfo{ + Version: "1.0", + Host: "http://cluster-registry.missionctrl.cloud.adobe.io", + BasePath: "/api", + Schemes: []string{"http", "https"}, + Title: "Cluster Registry API", + Description: "Cluster Registry API", +} + +type s struct{} + +func (s *s) ReadDoc() string { + sInfo := SwaggerInfo + sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1) + + t, err := template.New("swagger_info").Funcs(template.FuncMap{ + "marshal": func(v interface{}) string { + a, _ := json.Marshal(v) + return string(a) + }, + }).Parse(doc) + if err != nil { + return doc + } + + var tpl bytes.Buffer + if err := t.Execute(&tpl, sInfo); err != nil { + return doc + } + + return tpl.String() +} + +func init() { + swag.Register(swag.Name, &s{}) +} diff --git a/pkg/api/docs/docs_test.go b/pkg/api/docs/docs_test.go new file mode 100644 index 00000000..f8ffe923 --- /dev/null +++ b/pkg/api/docs/docs_test.go @@ -0,0 +1,27 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package docs + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReadDoc(t *testing.T) { + test := assert.New(t) + var s s + resp := s.ReadDoc() + test.Contains(fmt.Sprintf("%v", resp), "schema", "the response should be as expected") +} diff --git a/pkg/api/docs/swagger.json b/pkg/api/docs/swagger.json new file mode 100644 index 00000000..ba93fd8a --- /dev/null +++ b/pkg/api/docs/swagger.json @@ -0,0 +1,469 @@ +{ + "schemes": [ + "http", + "https" + ], + "swagger": "2.0", + "info": { + "description": "Cluster Registry API", + "title": "Cluster Registry API", + "contact": {}, + "version": "1.0" + }, + "host": "http://cluster-registry.missionctrl.cloud.adobe.io", + "basePath": "/api", + "paths": { + "/v1/clusters": { + "get": { + "security": [ + { + "bearerAuth": [] + } + ], + "description": "List all clusters. Use query parameters to filter results. Auth is required", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "cluster" + ], + "summary": "List all clusters", + "operationId": "get-clusters", + "parameters": [ + { + "type": "string", + "description": "Filter by region", + "name": "region", + "in": "query" + }, + { + "type": "string", + "description": "Filter by environment", + "name": "environment", + "in": "query" + }, + { + "type": "string", + "description": "Filter by businessUnit", + "name": "businessUnit", + "in": "query" + }, + { + "type": "string", + "description": "Filter by status", + "name": "status", + "in": "query" + }, + { + "type": "integer", + "description": "Limit number of clusters returned (default is 10)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset/skip number of clusters (default is 0)", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.clusterList" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.Error" + } + } + } + } + }, + "/v1/clusters/{name}": { + "get": { + "security": [ + { + "bearerAuth": [] + } + ], + "description": "Get an cluster. Auth is required", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "cluster" + ], + "summary": "Get an cluster", + "operationId": "get-cluster", + "parameters": [ + { + "type": "string", + "description": "Name of the cluster to get", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.ClusterSpec" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/utils.Error" + } + } + } + } + } + }, + "definitions": { + "api.clusterList": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ClusterSpec" + } + }, + "itemsCount": { + "type": "integer" + } + } + }, + "utils.Error": { + "type": "object", + "properties": { + "errors": { + "type": "object", + "additionalProperties": true + } + } + }, + "v1.APIServer": { + "type": "object", + "properties": { + "certificateAuthorityData": { + "description": "Information about K8s Api CA Cert", + "type": "string" + }, + "endpoint": { + "description": "Information about K8s Api Endpoint\n+kubebuilder:validation:Required", + "type": "string" + } + } + }, + "v1.AllowedOnboardingTeam": { + "type": "object", + "properties": { + "gitTeams": { + "description": "List of git teams", + "type": "array", + "items": { + "type": "string" + } + }, + "ldapGroups": { + "description": "List of ldap groups", + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "description": "Name of the team\n+kubebuilder:validation:Required", + "type": "string" + } + } + }, + "v1.ClusterSpec": { + "type": "object", + "properties": { + "accountId": { + "description": "The cloud account associated with the cluster\n+kubebuilder:validation:Required", + "type": "string" + }, + "allowedOnboardingTeams": { + "description": "Git teams and/or LDAP groups that are allowed to onboard and deploy on the cluster", + "type": "array", + "items": { + "$ref": "#/definitions/v1.AllowedOnboardingTeam" + } + }, + "apiServer": { + "description": "Information about K8s API endpoint and CA cert\n+kubebuilder:validation:Required", + "$ref": "#/definitions/v1.APIServer" + }, + "businessUnit": { + "description": "The BU that owns and maintains the cluster\n+kubebuilder:validation:Required", + "type": "string" + }, + "capabilities": { + "description": "List of cluster capabilities", + "type": "array", + "items": { + "type": "string" + } + }, + "cloudType": { + "description": "The cloud provider.\n+kubebuilder:validation:Required", + "type": "string" + }, + "environment": { + "description": "Cluster environment.\n+kubebuilder:validation:Required", + "type": "string" + }, + "extra": { + "description": "Ethos Extra specific information", + "$ref": "#/definitions/v1.Extra" + }, + "k8sInfraRelease": { + "description": "K8s Infrastructure release information\n+kubebuilder:validation:Required", + "$ref": "#/definitions/v1.K8sInfraRelease" + }, + "lastUpdated": { + "description": "Timestamp when cluster information was updated", + "type": "string" + }, + "name": { + "description": "Cluster name\n+kubebuilder:validation:Required\n+kubebuilder:validation:MaxLength=64\n+kubebuilder:validation:MinLength=3", + "type": "string" + }, + "offering": { + "description": "The Ethos offering that the cluster is meant for\n+kubebuilder:validation:Required", + "type": "array", + "items": { + "type": "string" + } + }, + "peerVirtualNetworks": { + "description": "Information about Virtual Networks peered with the cluster", + "type": "array", + "items": { + "$ref": "#/definitions/v1.PeerVirtualNetwork" + } + }, + "phase": { + "description": "Cluster phase\n+kubebuilder:validation:Required\n+kubebuilder:validation:Enum=Building;Testing;Running;Upgrading", + "type": "string" + }, + "region": { + "description": "Cluster standard region name\n+kubebuilder:validation:Required", + "type": "string" + }, + "registeredAt": { + "description": "Timestamp when cluster was registered in Cluster Registry\n+kubebuilder:validation:Required", + "type": "string" + }, + "shortName": { + "description": "Cluster name, without dash.\n+kubebuilder:validation:Required\n+kubebuilder:validation:MaxLength=64\n+kubebuilder:validation:MinLength=3", + "type": "string" + }, + "status": { + "description": "Cluster status\n+kubebuilder:validation:Required\n+kubebuilder:validation:Enum=Inactive;Active;Deprecated;Deleted", + "type": "string" + }, + "tags": { + "description": "Cluster tags that were applied", + "type": "array", + "items": { + "type": "string" + } + }, + "tiers": { + "description": "List of tiers with their associated information\n+kubebuilder:validation:Required", + "type": "array", + "items": { + "$ref": "#/definitions/v1.Tier" + } + }, + "type": { + "description": "The type of the cluster", + "type": "string" + }, + "virtualNetworks": { + "description": "Virtual Private Networks information\n+kubebuilder:validation:Required", + "type": "array", + "items": { + "$ref": "#/definitions/v1.VirtualNetwork" + } + } + } + }, + "v1.Extra": { + "type": "object", + "properties": { + "domainName": { + "description": "Name of the domain\n+kubebuilder:validation:Required", + "type": "string" + }, + "ecrIamArns": { + "description": "List of IAM Arns", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "egressPorts": { + "description": "Egress ports allowed outside of the namespace", + "type": "string" + }, + "lbEndpoints": { + "description": "Load balancer endpoints\n+kubebuilder:validation:Required", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "loggingEndpoints": { + "description": "Logging endpoints\n+kubebuilder:validation:Required", + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "nfsInfo": { + "description": "NFS information", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "v1.K8sInfraRelease": { + "type": "object", + "properties": { + "gitSha": { + "description": "GitSha of the release", + "type": "string" + }, + "lastUpdated": { + "description": "When the release was applied on the cluster", + "type": "string" + }, + "release": { + "description": "Release name", + "type": "string" + } + } + }, + "v1.PeerVirtualNetwork": { + "type": "object", + "properties": { + "cidrs": { + "description": "Remote Virtual Netowrk CIDRs", + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "description": "Remote Virtual Netowrk ID", + "type": "string" + }, + "ownerID": { + "description": "Cloud account of the owner", + "type": "string" + } + } + }, + "v1.Tier": { + "type": "object", + "properties": { + "containerRuntime": { + "description": "Container runtime\n+kubebuilder:validation:Required\n+kubebuilder:validation:Enum=docker;cri-o", + "type": "string" + }, + "enableKataSupport": { + "description": "EnableKataSupport", + "type": "boolean" + }, + "instanceType": { + "description": "Type of the instances\n+kubebuilder:validation:Required", + "type": "string" + }, + "kernelParameters": { + "description": "KernelParameters", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "labels": { + "description": "Instance K8s labels", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "maxCapacity": { + "description": "Max number of instances\n+kubebuilder:validation:Required", + "type": "integer" + }, + "minCapacity": { + "description": "Min number of instances\n+kubebuilder:validation:Required", + "type": "integer" + }, + "name": { + "description": "Name of the tier\n+kubebuilder:validation:Required", + "type": "string" + }, + "taints": { + "description": "Instance K8s taints", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "v1.VirtualNetwork": { + "type": "object", + "properties": { + "cidrs": { + "description": "CIDRs used in this VirtualNetwork\n+kubebuilder:validation:Required", + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "description": "Virtual private network Id\n+kubebuilder:validation:Required", + "type": "string" + } + } + } + }, + "securityDefinitions": { + "bearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/pkg/api/docs/swagger.yaml b/pkg/api/docs/swagger.yaml new file mode 100644 index 00000000..f4384581 --- /dev/null +++ b/pkg/api/docs/swagger.yaml @@ -0,0 +1,385 @@ +basePath: /api +definitions: + api.clusterList: + properties: + items: + items: + $ref: '#/definitions/v1.ClusterSpec' + type: array + itemsCount: + type: integer + type: object + utils.Error: + properties: + errors: + additionalProperties: true + type: object + type: object + v1.APIServer: + properties: + certificateAuthorityData: + description: Information about K8s Api CA Cert + type: string + endpoint: + description: |- + Information about K8s Api Endpoint + +kubebuilder:validation:Required + type: string + type: object + v1.AllowedOnboardingTeam: + properties: + gitTeams: + description: List of git teams + items: + type: string + type: array + ldapGroups: + description: List of ldap groups + items: + type: string + type: array + name: + description: |- + Name of the team + +kubebuilder:validation:Required + type: string + type: object + v1.ClusterSpec: + properties: + accountId: + description: |- + The cloud account associated with the cluster + +kubebuilder:validation:Required + type: string + allowedOnboardingTeams: + description: Git teams and/or LDAP groups that are allowed to onboard and deploy on the cluster + items: + $ref: '#/definitions/v1.AllowedOnboardingTeam' + type: array + apiServer: + $ref: '#/definitions/v1.APIServer' + description: |- + Information about K8s API endpoint and CA cert + +kubebuilder:validation:Required + businessUnit: + description: |- + The BU that owns and maintains the cluster + +kubebuilder:validation:Required + type: string + capabilities: + description: List of cluster capabilities + items: + type: string + type: array + cloudType: + description: |- + The cloud provider. + +kubebuilder:validation:Required + type: string + environment: + description: |- + Cluster environment. + +kubebuilder:validation:Required + type: string + extra: + $ref: '#/definitions/v1.Extra' + description: Ethos Extra specific information + k8sInfraRelease: + $ref: '#/definitions/v1.K8sInfraRelease' + description: |- + K8s Infrastructure release information + +kubebuilder:validation:Required + lastUpdated: + description: Timestamp when cluster information was updated + type: string + name: + description: |- + Cluster name + +kubebuilder:validation:Required + +kubebuilder:validation:MaxLength=64 + +kubebuilder:validation:MinLength=3 + type: string + offering: + description: |- + The Ethos offering that the cluster is meant for + +kubebuilder:validation:Required + items: + type: string + type: array + peerVirtualNetworks: + description: Information about Virtual Networks peered with the cluster + items: + $ref: '#/definitions/v1.PeerVirtualNetwork' + type: array + phase: + description: |- + Cluster phase + +kubebuilder:validation:Required + +kubebuilder:validation:Enum=Building;Testing;Running;Upgrading + type: string + region: + description: |- + Cluster standard region name + +kubebuilder:validation:Required + type: string + registeredAt: + description: |- + Timestamp when cluster was registered in Cluster Registry + +kubebuilder:validation:Required + type: string + shortName: + description: |- + Cluster name, without dash. + +kubebuilder:validation:Required + +kubebuilder:validation:MaxLength=64 + +kubebuilder:validation:MinLength=3 + type: string + status: + description: |- + Cluster status + +kubebuilder:validation:Required + +kubebuilder:validation:Enum=Inactive;Active;Deprecated;Deleted + type: string + tags: + description: Cluster tags that were applied + items: + type: string + type: array + tiers: + description: |- + List of tiers with their associated information + +kubebuilder:validation:Required + items: + $ref: '#/definitions/v1.Tier' + type: array + type: + description: The type of the cluster + type: string + virtualNetworks: + description: |- + Virtual Private Networks information + +kubebuilder:validation:Required + items: + $ref: '#/definitions/v1.VirtualNetwork' + type: array + type: object + v1.Extra: + properties: + domainName: + description: |- + Name of the domain + +kubebuilder:validation:Required + type: string + ecrIamArns: + additionalProperties: + type: string + description: List of IAM Arns + type: object + egressPorts: + description: Egress ports allowed outside of the namespace + type: string + lbEndpoints: + additionalProperties: + type: string + description: |- + Load balancer endpoints + +kubebuilder:validation:Required + type: object + loggingEndpoints: + description: |- + Logging endpoints + +kubebuilder:validation:Required + items: + additionalProperties: + type: string + type: object + type: array + nfsInfo: + additionalProperties: + type: string + description: NFS information + type: object + type: object + v1.K8sInfraRelease: + properties: + gitSha: + description: GitSha of the release + type: string + lastUpdated: + description: When the release was applied on the cluster + type: string + release: + description: Release name + type: string + type: object + v1.PeerVirtualNetwork: + properties: + cidrs: + description: Remote Virtual Netowrk CIDRs + items: + type: string + type: array + id: + description: Remote Virtual Netowrk ID + type: string + ownerID: + description: Cloud account of the owner + type: string + type: object + v1.Tier: + properties: + containerRuntime: + description: |- + Container runtime + +kubebuilder:validation:Required + +kubebuilder:validation:Enum=docker;cri-o + type: string + enableKataSupport: + description: EnableKataSupport + type: boolean + instanceType: + description: |- + Type of the instances + +kubebuilder:validation:Required + type: string + kernelParameters: + additionalProperties: + type: string + description: KernelParameters + type: object + labels: + additionalProperties: + type: string + description: Instance K8s labels + type: object + maxCapacity: + description: |- + Max number of instances + +kubebuilder:validation:Required + type: integer + minCapacity: + description: |- + Min number of instances + +kubebuilder:validation:Required + type: integer + name: + description: |- + Name of the tier + +kubebuilder:validation:Required + type: string + taints: + description: Instance K8s taints + items: + type: string + type: array + type: object + v1.VirtualNetwork: + properties: + cidrs: + description: |- + CIDRs used in this VirtualNetwork + +kubebuilder:validation:Required + items: + type: string + type: array + id: + description: |- + Virtual private network Id + +kubebuilder:validation:Required + type: string + type: object +host: http://cluster-registry.missionctrl.cloud.adobe.io +info: + contact: {} + description: Cluster Registry API + title: Cluster Registry API + version: "1.0" +paths: + /v1/clusters: + get: + consumes: + - application/json + description: List all clusters. Use query parameters to filter results. Auth is required + operationId: get-clusters + parameters: + - description: Filter by region + in: query + name: region + type: string + - description: Filter by environment + in: query + name: environment + type: string + - description: Filter by businessUnit + in: query + name: businessUnit + type: string + - description: Filter by status + in: query + name: status + type: string + - description: Limit number of clusters returned (default is 10) + in: query + name: limit + type: integer + - description: Offset/skip number of clusters (default is 0) + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.clusterList' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.Error' + security: + - bearerAuth: [] + summary: List all clusters + tags: + - cluster + /v1/clusters/{name}: + get: + consumes: + - application/json + description: Get an cluster. Auth is required + operationId: get-cluster + parameters: + - description: Name of the cluster to get + in: path + name: name + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/v1.ClusterSpec' + "400": + description: Bad Request + schema: + $ref: '#/definitions/utils.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/utils.Error' + security: + - bearerAuth: [] + summary: Get an cluster + tags: + - cluster +schemes: +- http +- https +securityDefinitions: + bearerAuth: + in: header + name: Authorization + type: apiKey +swagger: "2.0" diff --git a/pkg/api/monitoring/livez.go b/pkg/api/monitoring/livez.go new file mode 100644 index 00000000..5c86a142 --- /dev/null +++ b/pkg/api/monitoring/livez.go @@ -0,0 +1,25 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package monitoring + +import ( + "net/http" + + "github.com/labstack/echo/v4" +) + +// Livez checks if the api is healthy +func Livez(c echo.Context) error { + // TODO check to db, sqs and other dependencies + return c.String(http.StatusOK, "OK") +} diff --git a/pkg/api/monitoring/livez_test.go b/pkg/api/monitoring/livez_test.go new file mode 100644 index 00000000..acb4dbe7 --- /dev/null +++ b/pkg/api/monitoring/livez_test.go @@ -0,0 +1,38 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package monitoring + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" +) + +func TestNotFound(t *testing.T) { + test := assert.New(t) + + e := echo.New() + hw := "Hello, World!" + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(hw)) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err := Livez(c) + + test.NoError(err) + test.Equal(c.Response().Status, 200) +} diff --git a/pkg/api/monitoring/metrics.go b/pkg/api/monitoring/metrics.go new file mode 100644 index 00000000..588a2591 --- /dev/null +++ b/pkg/api/monitoring/metrics.go @@ -0,0 +1,280 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package monitoring + +import ( + "errors" + "net/http" + "strconv" + "time" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +const ( + defaultMetricPath = "/metrics" + defaultSubsystem = "echo" +) + +// Ingress metrics +var ingressReqCnt = &Metric{ + ID: "ingresReqCnt", + Name: "ingress_requests_total", + Description: "How many HTTP requests processed, partitioned by status code, HTTP method and url.", + Type: "counter_vec", + Args: []string{"code", "method", "url"}} + +var ingressReqDur = &Metric{ + ID: "ingressReqDur", + Name: "ingress_request_duration_seconds", + Description: "The HTTP request latencies in seconds partitioned by status code, HTTP method and url.", + Args: []string{"code", "method", "url"}, + Type: "histogram_vec"} + +var ingressMetrics = []*Metric{ + ingressReqCnt, + ingressReqDur, +} + +// EgressMetrics +var egressReqCnt = &Metric{ + ID: "egressReqCnt", + Name: "egress_requests_total", + Description: "How many egress requests sent, partitioned by target.", + Type: "counter_vec", + Args: []string{"target"}} + +var egressReqDur = &Metric{ + ID: "egressReqDur", + Name: "egress_request_duration_seconds", + Description: "The Egress HTTP request latencies in seconds partitioned by target.", + Args: []string{"target"}, + Type: "histogram_vec"} + +var egressMetrics = []*Metric{ + egressReqCnt, + egressReqDur, +} + +/* +RequestCounterLabelMappingFunc is a function which can be supplied to the middleware to control +the cardinality of the request counter's "url" label, which might be required in some contexts. +For instance, if for a "/customer/:name" route you don't want to generate a time series for every +possible customer name, you could use this function: + +func(c echo.Context) string { + url := c.Request.URL.Path + for _, p := range c.Params { + if p.Key == "name" { + url = strings.Replace(url, p.Value, ":name", 1) + break + } + } + return url +} + +which would map "/customer/alice" and "/customer/bob" to their template "/customer/:name". +It can also be applied for the "Host" label +*/ +type requestCounterLabelMappingFunc func(c echo.Context) string + +// Metric is a definition for the name, description, type, ID, and +// prometheus.Collector type (i.e. CounterVec, HistrogramVec, etc) of each metric +type Metric struct { + MetricCollector prometheus.Collector + ID string + Name string + Description string + Type string + Args []string +} + +// MetricsI interface +type MetricsI interface { + RecordEgressRequestCnt(target string) + RecordEgressRequestDur(target string, elapsed float64) + RecordIngressRequestCnt(code, method, url string) + RecordIngressRequestDur(code, method, url string, elapsed float64) + Use(e *echo.Echo) +} + +// Metrics contains the metrics gathered by the instance and its path +type Metrics struct { + ingressReqCnt, egressReqCnt *prometheus.CounterVec + ingressReqDur, egressReqDur *prometheus.HistogramVec + + metricsList []*Metric + metricsPath string + subsystem string + skipper middleware.Skipper + isUnitTest bool + + requestCounterURLLabelMappingFunc requestCounterLabelMappingFunc + + // Context string to use as a prometheus URL label + urlLabelFromContext string +} + +// NewMetrics generates a new set of metrics with a certain subsystem name +func NewMetrics(subsystem string, skipper middleware.Skipper, isUnitTest bool) *Metrics { + var metricsList []*Metric + if skipper == nil { + skipper = middleware.DefaultSkipper + } + + metricsList = append(metricsList, ingressMetrics...) + metricsList = append(metricsList, egressMetrics...) + + m := &Metrics{ + metricsList: metricsList, + metricsPath: defaultMetricPath, + subsystem: defaultSubsystem, + skipper: skipper, + isUnitTest: isUnitTest, + requestCounterURLLabelMappingFunc: func(c echo.Context) string { + return c.Path() // i.e. by default do nothing, i.e. return URL as is + }, + } + + m.registerMetrics(subsystem) + + return m +} + +func prometheusHandler() echo.HandlerFunc { + h := promhttp.Handler() + return func(c echo.Context) error { + h.ServeHTTP(c.Response(), c.Request()) + return nil + } +} + +func (m *Metrics) registerMetrics(subsystem string) { + var metric prometheus.Collector + + reg := prometheus.DefaultRegisterer + if m.isUnitTest { + reg = prometheus.NewRegistry() + } + + factory := promauto.With(reg) + + for _, metricDef := range m.metricsList { + switch metricDef.Type { + case "counter_vec": + metric = factory.NewCounterVec( + prometheus.CounterOpts{ + Subsystem: subsystem, + Name: metricDef.Name, + Help: metricDef.Description, + }, + metricDef.Args, + ) + case "histogram_vec": + metric = factory.NewHistogramVec( + prometheus.HistogramOpts{ + Subsystem: subsystem, + Name: metricDef.Name, + Help: metricDef.Description, + }, + metricDef.Args, + ) + } + + switch metricDef { + case ingressReqCnt: + m.ingressReqCnt = metric.(*prometheus.CounterVec) + case ingressReqDur: + m.ingressReqDur = metric.(*prometheus.HistogramVec) + case egressReqCnt: + m.egressReqCnt = metric.(*prometheus.CounterVec) + case egressReqDur: + m.egressReqDur = metric.(*prometheus.HistogramVec) + } + metricDef.MetricCollector = metric + } +} + +// Use adds the middleware to the Echo engine. +func (m *Metrics) Use(e *echo.Echo) { + e.Use(m.handlerFunc) + e.GET(m.metricsPath, prometheusHandler()) +} + +// HandlerFunc defines handler function for middleware +func (m *Metrics) handlerFunc(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if c.Path() == m.metricsPath { + return next(c) + } + if m.skipper(c) { + return next(c) + } + + start := time.Now() + err := next(c) + elapsed := float64(time.Since(start)) / float64(time.Second) + + method := c.Request().Method + status := c.Response().Status + if err != nil { + var httpError *echo.HTTPError + if errors.As(err, &httpError) { + status = httpError.Code + } + if status == 0 || status == http.StatusOK { + status = http.StatusInternalServerError + } + } + + url := m.requestCounterURLLabelMappingFunc(c) + if len(m.urlLabelFromContext) > 0 { + u := c.Get(m.urlLabelFromContext) + if u == nil { + u = "unknown" + } + url = u.(string) + } + + statusStr := strconv.Itoa(status) + m.RecordIngressRequestCnt(statusStr, method, url) + m.RecordIngressRequestDur(statusStr, method, url, elapsed) + + return err + } +} + +// RecordEgressRequestCnt increases the Egress counter for a taget +func (m *Metrics) RecordEgressRequestCnt(target string) { + m.egressReqCnt.WithLabelValues(target).Inc() +} + +// RecordEgressRequestDur registers the Egress duration for a taget +func (m *Metrics) RecordEgressRequestDur(target string, elapsed float64) { + m.egressReqDur.WithLabelValues(target).Observe(elapsed) +} + +// RecordIngressRequestCnt increases the Ingress counter for a taget +func (m *Metrics) RecordIngressRequestCnt(code, method, url string) { + m.ingressReqCnt.WithLabelValues(code, method, url).Inc() +} + +// RecordIngressRequestDur registers the Egress duration for a taget +func (m *Metrics) RecordIngressRequestDur(code, method, url string, elapsed float64) { + m.ingressReqDur.WithLabelValues(code, method, url).Observe(elapsed) +} diff --git a/pkg/api/monitoring/metrics_test.go b/pkg/api/monitoring/metrics_test.go new file mode 100644 index 00000000..22f10600 --- /dev/null +++ b/pkg/api/monitoring/metrics_test.go @@ -0,0 +1,140 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package monitoring + +import ( + "fmt" + "math/rand" + "strings" + "testing" + + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" +) + +const ( + egressTarget = "testing_egress" + expectedMetricsRegistered = 4 + ingressCode = "200" + ingressMethod = "GET" + ingressURL = "/api/v1/clusters/:name" + ingressTarget = "testing_ingress" + minRand = 1 + maxRand = 2.5 + subsystem = "testing" + testingPort = "12345" +) + +func TestNewMetrics(t *testing.T) { + test := assert.New(t) + m := NewMetrics(subsystem, nil, true) + test.NotNil(m) +} + +func TestMetricsRegistered(t *testing.T) { + test := assert.New(t) + m := NewMetrics(subsystem, nil, true) + + test.Equal(len(m.metricsList), expectedMetricsRegistered) +} + +func TestRecordEgressRequestCnt(t *testing.T) { + test := assert.New(t) + m := NewMetrics(subsystem, nil, true) + + m.RecordEgressRequestCnt(egressTarget) + + test.Equal(1, testutil.CollectAndCount(*m.egressReqCnt)) + test.Equal(float64(1), testutil.ToFloat64((*m.egressReqCnt).WithLabelValues(egressTarget))) +} + +// Generate a random float number between min and max +func generateFloatRand(min, max float64) float64 { + return min + rand.Float64()*(max-min) +} + +func TestRecordEgressRequestDur(t *testing.T) { + m := NewMetrics(subsystem, nil, true) + + randomFloat := generateFloatRand(minRand, maxRand) + m.RecordEgressRequestDur(egressTarget, randomFloat) + + expected := fmt.Sprintf(` + # HELP %[1]s_egress_request_duration_seconds The Egress HTTP request latencies in seconds partitioned by target. + # TYPE %[1]s_egress_request_duration_seconds histogram + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="0.005"} 0 + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="0.01"} 0 + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="0.025"} 0 + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="0.05"} 0 + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="0.1"} 0 + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="0.25"} 0 + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="0.5"} 0 + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="1"} 0 + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="2.5"} 1 + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="5"} 1 + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="10"} 1 + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="+Inf"} 1 + %[1]s_egress_request_duration_seconds_sum{target="%[2]s"} %[3]s + %[1]s_egress_request_duration_seconds_count{target="%[2]s"} 1 + `, subsystem, egressTarget, fmt.Sprintf("%.16f", randomFloat)) + + if err := testutil.CollectAndCompare( + *m.egressReqDur, + strings.NewReader(expected), + fmt.Sprintf("%s_egress_request_duration_seconds", subsystem)); err != nil { + t.Errorf("unexpected collecting result:\n%s", err) + } +} + +func TestRecordIngressRequestCnt(t *testing.T) { + test := assert.New(t) + m := NewMetrics(subsystem, nil, true) + + m.RecordIngressRequestCnt(ingressCode, ingressMethod, ingressURL) + + test.Equal(1, testutil.CollectAndCount(*m.ingressReqCnt)) + test.Equal(float64(1), testutil.ToFloat64((*m.ingressReqCnt).WithLabelValues(ingressCode, ingressMethod, ingressURL))) +} + +func TestRecordIngressRequestDur(t *testing.T) { + m := NewMetrics(subsystem, nil, true) + + randomFloat := generateFloatRand(minRand, maxRand) + m.RecordIngressRequestDur(ingressCode, ingressMethod, ingressURL, randomFloat) + + expected := fmt.Sprintf(` + # HELP %[1]s_ingress_request_duration_seconds The HTTP request latencies in seconds partitioned by status code, HTTP method and url. + # TYPE %[1]s_ingress_request_duration_seconds histogram + %[1]s_ingress_request_duration_seconds_bucket{code="%[2]s",method="%[3]s",url="%[4]s",le="0.005"} 0 + %[1]s_ingress_request_duration_seconds_bucket{code="%[2]s",method="%[3]s",url="%[4]s",le="0.01"} 0 + %[1]s_ingress_request_duration_seconds_bucket{code="%[2]s",method="%[3]s",url="%[4]s",le="0.025"} 0 + %[1]s_ingress_request_duration_seconds_bucket{code="%[2]s",method="%[3]s",url="%[4]s",le="0.05"} 0 + %[1]s_ingress_request_duration_seconds_bucket{code="%[2]s",method="%[3]s",url="%[4]s",le="0.1"} 0 + %[1]s_ingress_request_duration_seconds_bucket{code="%[2]s",method="%[3]s",url="%[4]s",le="0.25"} 0 + %[1]s_ingress_request_duration_seconds_bucket{code="%[2]s",method="%[3]s",url="%[4]s",le="0.5"} 0 + %[1]s_ingress_request_duration_seconds_bucket{code="%[2]s",method="%[3]s",url="%[4]s",le="1"} 0 + %[1]s_ingress_request_duration_seconds_bucket{code="%[2]s",method="%[3]s",url="%[4]s",le="2.5"} 1 + %[1]s_ingress_request_duration_seconds_bucket{code="%[2]s",method="%[3]s",url="%[4]s",le="5"} 1 + %[1]s_ingress_request_duration_seconds_bucket{code="%[2]s",method="%[3]s",url="%[4]s",le="10"} 1 + %[1]s_ingress_request_duration_seconds_bucket{code="%[2]s",method="%[3]s",url="%[4]s",le="+Inf"} 1 + %[1]s_ingress_request_duration_seconds_sum{code="%[2]s",method="%[3]s",url="%[4]s"} %[5]s + %[1]s_ingress_request_duration_seconds_count{code="%[2]s",method="%[3]s",url="%[4]s"} 1 + `, subsystem, ingressCode, ingressMethod, ingressURL, fmt.Sprintf("%.16f", randomFloat)) + + if err := testutil.CollectAndCompare( + *m.ingressReqDur, + strings.NewReader(expected), + fmt.Sprintf("%s_ingress_request_duration_seconds", subsystem)); err != nil { + t.Errorf("unexpected collecting result:\n%s", err) + } +} diff --git a/pkg/api/sqs/consumer.go b/pkg/api/sqs/consumer.go new file mode 100644 index 00000000..6eba7024 --- /dev/null +++ b/pkg/api/sqs/consumer.go @@ -0,0 +1,191 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package sqs + +import ( + "encoding/json" + "strconv" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/sqs" + "github.com/aws/aws-sdk-go/service/sqs/sqsiface" + "github.com/labstack/gommon/log" + + "github.com/adobe/cluster-registry/pkg/api/database" + "github.com/adobe/cluster-registry/pkg/api/monitoring" + registryv1 "github.com/adobe/cluster-registry/pkg/cc/api/registry/v1" +) + +const ( + egressTarget = "sqs" +) + +// Consumer interface +type Consumer interface { + Consume() + worker(int) + processMessage(*sqs.Message) error + delete(m *sqs.Message) error +} + +// consumer struct +type consumer struct { + sqs sqsiface.SQSAPI + db database.Db + queueURL string + workerPool int + maxMessages int64 + pollWaitSeconds int64 + retrySeconds int + met monitoring.MetricsI +} + +// NewConsumer - creates new message queue consumer +func NewConsumer(d database.Db, m monitoring.MetricsI) Consumer { + sqsSvc := NewSQS() + sqsQueueName, err := getSqsQueueName(sqsEndpoint) + if err != nil { + log.Fatal(err.Error()) + } + + urlResult, err := sqsSvc.GetQueueUrl(&sqs.GetQueueUrlInput{ + QueueName: &sqsQueueName, + }) + if err != nil { + log.Fatal(err.Error()) + } + + return &consumer{ + sqs: sqsSvc, + db: d, + queueURL: *urlResult.QueueUrl, + workerPool: 10, + maxMessages: 1, + pollWaitSeconds: 1, + retrySeconds: 5, + met: m, + } +} + +// Consume - long pooling +func (c *consumer) Consume() { + for w := 1; w <= c.workerPool; w++ { + go c.worker(w) + } +} + +func (c *consumer) worker(id int) { + for { + start := time.Now() + output, err := c.sqs.ReceiveMessage((&sqs.ReceiveMessageInput{ + QueueUrl: &c.queueURL, + AttributeNames: aws.StringSlice([]string{ + "ClusterName", "SentTimestamp", + }), + MaxNumberOfMessages: aws.Int64(c.maxMessages), + WaitTimeSeconds: aws.Int64(c.pollWaitSeconds), + })) + elapsed := float64(time.Since(start)) / float64(time.Second) + + c.met.RecordEgressRequestCnt(egressTarget) + c.met.RecordEgressRequestDur(egressTarget, elapsed) + + if err != nil { + log.Error(err.Error()) + log.Info("Retrying in", c.retrySeconds, " seconds") + time.Sleep(time.Duration(c.retrySeconds) * time.Second) + continue + } + + for _, m := range output.Messages { + if err := c.processMessage(m); err != nil { + log.Error(err.Error()) + continue + } + err = c.delete(m) + if err != nil { + log.Error(err.Error()) + } + } + } +} + +func (c *consumer) processMessage(m *sqs.Message) error { + var rcvCluster registryv1.Cluster + + err := json.Unmarshal([]byte(*m.Body), &rcvCluster) + if err != nil { + log.Error("Failed to unmarshal message.") + return err + } + + clusterName := rcvCluster.Spec.Name + + msgTimestamp, err := strconv.ParseInt(*m.Attributes["SentTimestamp"], 10, 64) + if err != nil { + log.Error("Wrong time format for sqs message:", m.MessageId) + return err + } + lastUpdated := time.Unix(0, msgTimestamp*int64(time.Millisecond)) + + cluster, err := c.db.GetCluster(clusterName) + if err != nil { + log.Error("Failed to get cluster ", clusterName, " from database.") + return err + } + + if cluster == nil { + rcvCluster.Spec.LastUpdated = lastUpdated.UTC().Format(time.RFC3339Nano) + err = c.db.PutCluster(&rcvCluster) + if err != nil { + log.Error("Cluster ", clusterName, " failed to be created.") + return err + } + log.Info("Cluster ", clusterName, " was created.") + return nil + } + + clusterTime, err := time.Parse(time.RFC3339Nano, cluster.Spec.LastUpdated) + if err != nil { + log.Warn("Wrong time format in database for: ", clusterName) + } else if lastUpdated.Before(clusterTime) { + log.Info("Cluster lastUpdated timestamp is too old. This update will be skip for ", clusterName) + return nil + } + + rcvCluster.Spec.LastUpdated = lastUpdated.UTC().Format(time.RFC3339Nano) + err = c.db.PutCluster(&rcvCluster) + if err != nil { + log.Error("Cluster ", clusterName, " failed to be updated.") + return err + } + + log.Info("Cluster ", clusterName, " was updated.") + return err +} + +func (c *consumer) delete(m *sqs.Message) error { + start := time.Now() + _, err := c.sqs.DeleteMessage( + &sqs.DeleteMessageInput{QueueUrl: &c.queueURL, ReceiptHandle: m.ReceiptHandle}) + elapsed := float64(time.Since(start)) / float64(time.Second) + + c.met.RecordEgressRequestCnt(egressTarget) + c.met.RecordEgressRequestDur(egressTarget, elapsed) + + if err != nil { + log.Error(err.Error()) + } + return err +} diff --git a/pkg/api/sqs/consumer_test.go b/pkg/api/sqs/consumer_test.go new file mode 100644 index 00000000..b0bc2d2f --- /dev/null +++ b/pkg/api/sqs/consumer_test.go @@ -0,0 +1,247 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package sqs + +import ( + "fmt" + "testing" + + "github.com/adobe/cluster-registry/pkg/api/monitoring" + registryv1 "github.com/adobe/cluster-registry/pkg/cc/api/registry/v1" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/sqs" + "github.com/stretchr/testify/assert" +) + +func TestDeleteMessage(t *testing.T) { + test := assert.New(t) + tcs := []struct { + name string + sqsMessages []*sqs.Message + expectedError error + }{ + { + name: "delete message", + sqsMessages: []*sqs.Message{ + {Body: aws.String(`{"apiVersion":"registry.ethos.adobe.com/v1","kind":"Cluster","metadata":{"name":"cluster1","namespace":"cluster-registry"},"spec":{"name":"cluster1","registeredAt":"2019-02-13T06:15:32Z","lastUpdated":"2020-02-13T06:15:32Z","status":"Deprecated","phase":"Running","tags":{"onboarding":"off","scaling":"off"}}}`)}, + {Body: aws.String(`{"apiVersion":"registry.ethos.adobe.com/v1","kind":"Cluster","metadata":{"name":"cluster2","namespace":"cluster-registry"},"spec":{"name":"cluster2","registeredAt":"2019-02-13T06:15:32Z","lastUpdated":"2020-02-13T06:15:32Z","status":"Deprecated","phase":"Running","tags":{"onboarding":"off","scaling":"off"}}}`)}, + }, + expectedError: nil, + }} + + for _, tc := range tcs { + c := &consumer{ + sqs: &mockSQS{messages: tc.sqsMessages}, + db: &mockDatabase{clusters: nil}, + queueURL: "mock-queue", + workerPool: 1, + maxMessages: 1, + pollWaitSeconds: 1, + retrySeconds: 5, + met: monitoring.NewMetrics("cluster_registry_api_sqs_test", nil, true), + } + + err := c.delete(tc.sqsMessages[0]) + if tc.expectedError != nil { + test.Error(err, "there should be an error processing the message") + test.Contains(fmt.Sprintf("%v", err), fmt.Sprintf("%v", tc.expectedError), "the error message should be as expected") + } + + queueDepth, _ := getQueueDepth(c.sqs) + test.Equal(queueDepth, 1) + + err = c.delete(tc.sqsMessages[0]) + if tc.expectedError != nil { + test.Error(err, "there should be an error processing the message") + test.Contains(fmt.Sprintf("%v", err), fmt.Sprintf("%v", tc.expectedError), "the error message should be as expected") + } + + queueDepth, _ = getQueueDepth(c.sqs) + test.Equal(queueDepth, 0) + } +} + +func TestProcessMessage(t *testing.T) { + test := assert.New(t) + tcs := []struct { + name string + dbClusters []registryv1.Cluster + sqsMessages []*sqs.Message + expectedClusters []registryv1.Cluster + expectedError error + }{ + { + name: "new cluster", + dbClusters: []registryv1.Cluster{{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-13T06:15:32Z", + RegisteredAt: "2019-02-13T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "on"}, + }, + }, + }, + sqsMessages: []*sqs.Message{ + { + Body: aws.String(`{"spec":{"name":"cluster2","registeredAt":"2019-02-13T06:15:32Z","status":"Active","phase":"Running","tags":{"onboarding":"on","scaling":"off"}}}`), + Attributes: map[string]*string{ + "SentTimestamp": aws.String("1627321893000"), + }, + }, + }, + expectedClusters: []registryv1.Cluster{{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-13T06:15:32Z", + RegisteredAt: "2019-02-13T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "on"}, + }}, + { + Spec: registryv1.ClusterSpec{ + Name: "cluster2", + LastUpdated: "2021-07-26T17:51:33Z", + RegisteredAt: "2019-02-13T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "off"}, + }}}, + expectedError: nil, + }, + { + name: "older update timestamp", + dbClusters: []registryv1.Cluster{{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-13T06:15:32Z", + RegisteredAt: "2019-02-13T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "on"}, + }, + }, + }, + sqsMessages: []*sqs.Message{ + { + Body: aws.String(`{"apiVersion":"registry.ethos.adobe.com/v1","kind":"Cluster","metadata":{"name":"cluster1","namespace":"cluster-registry"},"spec":{"name":"cluster1","registeredAt":"2019-02-13T06:15:32Z","status":"Deprecated","phase":"Running","tags":{"onboarding":"off","scaling":"off"}}}`), + Attributes: map[string]*string{ + "SentTimestamp": aws.String("1581532782000"), + }, + }, + }, + expectedClusters: []registryv1.Cluster{{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-13T06:15:32Z", + RegisteredAt: "2019-02-13T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "on"}, + }}, + }, + expectedError: nil, + }, + { + name: "wrong sqs message", + dbClusters: []registryv1.Cluster{{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-13T06:15:32Z", + RegisteredAt: "2019-02-13T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "on"}, + }, + }, + }, + sqsMessages: []*sqs.Message{ + {Body: aws.String(`{this is wrong}`)}, + }, + expectedClusters: []registryv1.Cluster{{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-13T06:15:32Z", + RegisteredAt: "2019-02-13T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "on"}, + }}, + }, + expectedError: fmt.Errorf("invalid character 't' looking for beginning of object key string"), + }, + { + name: "wrong timestamp format", + dbClusters: []registryv1.Cluster{{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-13T06:15:32Z", + RegisteredAt: "2019-02-13T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "on"}, + }, + }, + }, + sqsMessages: []*sqs.Message{ + { + Body: aws.String(`{"spec":{"name":"cluster2","registeredAt":"2019-02-13T06:15:32Z","status":"Active","phase":"Running","tags":{"onboarding":"on","scaling":"off"}}}`), + Attributes: map[string]*string{ + "SentTimestamp": aws.String("1234abc"), + }, + }, + }, + expectedClusters: []registryv1.Cluster{{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-13T06:15:32Z", + RegisteredAt: "2019-02-13T06:15:32Z", + Status: "Active", + Phase: "Running", + Tags: map[string]string{"onboarding": "on", "scaling": "on"}, + }}, + }, + expectedError: fmt.Errorf("strconv.ParseInt: parsing \"1234abc\": invalid syntax"), + }, + } + + for _, tc := range tcs { + c := &consumer{ + sqs: &mockSQS{messages: tc.sqsMessages}, + db: &mockDatabase{clusters: tc.dbClusters}, + queueURL: "mock-queue", + workerPool: 1, + maxMessages: 1, + pollWaitSeconds: 1, + retrySeconds: 5, + met: monitoring.NewMetrics("cluster_registry_api_sqs_test", nil, true), + } + + err := c.processMessage(tc.sqsMessages[0]) + if tc.expectedError != nil { + test.Error(err, "there should be an error processing the message") + test.Contains(fmt.Sprintf("%v", err), fmt.Sprintf("%v", tc.expectedError), "the error message should be as expected") + } else { + test.NoError(err) + } + + listClusters, _, err := c.db.ListClusters("", "", "", "") + if err != nil { + test.Error(err, "cannot list cluster from database") + } + + test.Equal(tc.expectedClusters, listClusters) + } +} diff --git a/pkg/api/sqs/fakeproducer.go b/pkg/api/sqs/fakeproducer.go new file mode 100644 index 00000000..b25441d5 --- /dev/null +++ b/pkg/api/sqs/fakeproducer.go @@ -0,0 +1,34 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package sqs + +import ( + "context" + + registryv1 "github.com/adobe/cluster-registry/pkg/cc/api/registry/v1" + "github.com/adobe/cluster-registry/pkg/cc/monitoring" +) + +type fakeproducer struct { + producer +} + +// NewFakeProducer creates a fake producer +func NewFakeProducer(m monitoring.MetricsI) Producer { + return &fakeproducer{} +} + +// Send message in sqs queue +func (p *fakeproducer) Send(ctx context.Context, c *registryv1.Cluster) error { + return nil +} diff --git a/pkg/api/sqs/producer.go b/pkg/api/sqs/producer.go new file mode 100644 index 00000000..8c0a7cdc --- /dev/null +++ b/pkg/api/sqs/producer.go @@ -0,0 +1,96 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package sqs + +import ( + "context" + "encoding/json" + "time" + + registryv1 "github.com/adobe/cluster-registry/pkg/cc/api/registry/v1" + "github.com/adobe/cluster-registry/pkg/cc/monitoring" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/sqs" + "github.com/aws/aws-sdk-go/service/sqs/sqsiface" + "github.com/labstack/gommon/log" +) + +// Producer interface +type Producer interface { + Send(context.Context, *registryv1.Cluster) error +} + +// producer struct +type producer struct { + sqs sqsiface.SQSAPI + queueURL string + met monitoring.MetricsI +} + +// NewProducer - create new message queue producer +func NewProducer(m monitoring.MetricsI) Producer { + + sqsSvc := NewSQS() + sqsQueueName, err := getSqsQueueName(sqsEndpoint) + if err != nil { + log.Fatal(err.Error()) + } + + urlResult, err := sqsSvc.GetQueueUrl(&sqs.GetQueueUrlInput{ + QueueName: &sqsQueueName, + }) + if err != nil { + log.Fatal(err.Error()) + } + + return &producer{ + sqs: sqsSvc, + queueURL: *urlResult.QueueUrl, + met: m, + } +} + +// Send message in sqs queue +func (p *producer) Send(ctx context.Context, c *registryv1.Cluster) error { + + o, err := json.Marshal(c) + if err != nil { + log.Error(err.Error()) + return err + } + + start := time.Now() + result, err := p.sqs.SendMessageWithContext(ctx, &sqs.SendMessageInput{ + DelaySeconds: aws.Int64(10), + MessageAttributes: map[string]*sqs.MessageAttributeValue{ + "ClusterName": { + DataType: aws.String("String"), + StringValue: aws.String(c.Spec.Name), + }, + }, + MessageBody: aws.String(string(o)), + QueueUrl: &p.queueURL, + }) + elapsed := float64(time.Since(start)) / float64(time.Second) + + p.met.RecordEgressRequestCnt(egressTarget) + p.met.RecordEgressRequestDur(egressTarget, elapsed) + + if err != nil { + log.Error(err.Error()) + return err + } + + log.Info("Message ", *result.MessageId, " sent successfully.") + return nil +} diff --git a/pkg/api/sqs/producer_test.go b/pkg/api/sqs/producer_test.go new file mode 100644 index 00000000..f4fcb5b2 --- /dev/null +++ b/pkg/api/sqs/producer_test.go @@ -0,0 +1,72 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package sqs + +import ( + "context" + "encoding/json" + "testing" + + registryv1 "github.com/adobe/cluster-registry/pkg/cc/api/registry/v1" + "github.com/adobe/cluster-registry/pkg/cc/monitoring" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/sqs" + "github.com/stretchr/testify/assert" +) + +func TestSendMessage(t *testing.T) { + test := assert.New(t) + tcs := []struct { + name string + cluster registryv1.Cluster + expectedError error + }{ + { + name: "create cluster", + cluster: registryv1.Cluster{ + Spec: registryv1.ClusterSpec{ + Name: "cluster1", + LastUpdated: "2020-02-13T06:15:32Z", + RegisteredAt: "2019-02-13T06:15:32Z", + Status: "Deprecated", + Phase: "Running", + Tags: map[string]string{"onboarding": "off", "scaling": "off"}, + }}, + expectedError: nil, + }} + + for _, tc := range tcs { + m := monitoring.NewMetrics() + m.Init(true) + p := producer{ + sqs: &mockSQS{}, + queueURL: "mock-queue", + met: m, + } + + p.Send(context.TODO(), &tc.cluster) + + output, err := p.sqs.ReceiveMessage((&sqs.ReceiveMessageInput{ + QueueUrl: &p.queueURL, + MaxNumberOfMessages: aws.Int64(1), + WaitTimeSeconds: aws.Int64(1), + })) + test.NoError(err) + + var rcvCluster registryv1.Cluster + err = json.Unmarshal([]byte(*output.Messages[0].Body), &rcvCluster) + test.NoError(err) + + test.Equal(tc.cluster, rcvCluster) + } +} diff --git a/pkg/api/sqs/sqs.go b/pkg/api/sqs/sqs.go new file mode 100644 index 00000000..f26647f6 --- /dev/null +++ b/pkg/api/sqs/sqs.go @@ -0,0 +1,78 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package sqs + +import ( + "errors" + "log" + "os" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/sqs" + "github.com/aws/aws-sdk-go/service/sqs/sqsiface" +) + +var sqsEndpoint string + +// NewSQS - create new SQS instance +func NewSQS() sqsiface.SQSAPI { + + sqsEndpoint = os.Getenv("SQS_ENDPOINT") + if sqsEndpoint == "" { + log.Fatal("SqS endpoint not set.") + } + + awsRegion, err := getSqsAwsRegion(sqsEndpoint) + if err != nil { + log.Fatal(err.Error()) + } + + sess := session.Must(session.NewSessionWithOptions(session.Options{ + Config: aws.Config{ + Region: aws.String(awsRegion), + Endpoint: aws.String(sqsEndpoint), + }, + })) + + return sqs.New(sess) +} + +func getSqsAwsRegion(sqsEndpoint string) (string, error) { + endpointParts := strings.Split(sqsEndpoint, ".") + var awsRegion string + if len(endpointParts) == 4 { // https://sqs.us-west-2.amazonaws.com/myaccountid/myqueue + awsRegion = endpointParts[1] + } else { + awsRegion = os.Getenv("SQS_AWS_REGION") + } + if awsRegion == "" { + return "", errors.New("cannot get sqs aws region from sqsEndpoint or from environment variables") + } + return awsRegion, nil +} + +func getSqsQueueName(sqsEndpoint string) (string, error) { + endpointParts := strings.Split(sqsEndpoint, "/") + var sqsQueueName string + if len(endpointParts) == 5 { // https://sqs.us-west-2.amazonaws.com/myaccountid/myqueue + sqsQueueName = endpointParts[4] + } else { + sqsQueueName = os.Getenv("SQS_QUEUE_NAME") + } + if sqsQueueName == "" { + return "", errors.New("cannot get sqs queue name from sqsEndpoint or from environment variables") + } + return sqsQueueName, nil +} diff --git a/pkg/api/sqs/sqs_test.go b/pkg/api/sqs/sqs_test.go new file mode 100644 index 00000000..b06fe0bb --- /dev/null +++ b/pkg/api/sqs/sqs_test.go @@ -0,0 +1,245 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package sqs + +import ( + "errors" + "fmt" + "os" + "strconv" + "testing" + + "github.com/adobe/cluster-registry/pkg/api/database" + registryv1 "github.com/adobe/cluster-registry/pkg/cc/api/registry/v1" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/service/sqs" + "github.com/aws/aws-sdk-go/service/sqs/sqsiface" + "github.com/labstack/gommon/log" + "github.com/stretchr/testify/assert" +) + +// mockDatabase database.db +type mockDatabase struct { + database.Db + clusters []registryv1.Cluster +} + +func (m mockDatabase) GetCluster(name string) (*registryv1.Cluster, error) { + for _, c := range m.clusters { + if c.Spec.Name == name { + return &c, nil + } + } + return nil, nil +} + +func (m mockDatabase) ListClusters(region string, environment string, businessUnit string, status string) ([]registryv1.Cluster, int, error) { + return m.clusters, len(m.clusters), nil +} + +func (m *mockDatabase) PutCluster(cluster *registryv1.Cluster) error { + found := false + + for i, c := range m.clusters { + if c.Spec.Name == cluster.Spec.Name { + m.clusters[i] = *cluster + found = true + } + } + + if !found { + m.clusters = append(m.clusters, *cluster) + } + return nil +} + +func (m *mockDatabase) DeleteCluster(name string) error { + for i, c := range m.clusters { + if c.Spec.Name == name { + m.clusters = append(m.clusters[:i], m.clusters[i+1:]...) + } + } + return nil +} + +type mockSQS struct { + sqsiface.SQSAPI + messages []*sqs.Message +} + +func (m *mockSQS) SendMessageWithContext(ctx aws.Context, in *sqs.SendMessageInput, r ...request.Option) (*sqs.SendMessageOutput, error) { + m.messages = append(m.messages, &sqs.Message{ + Body: in.MessageBody, + }) + messageID := "TWVzc2FnZUlkCg==" + + return &sqs.SendMessageOutput{ + MessageId: &messageID, + }, nil +} + +func (m *mockSQS) DeleteMessage(in *sqs.DeleteMessageInput) (*sqs.DeleteMessageOutput, error) { + if len(m.messages) == 0 { + return nil, errors.New("no messages to delete") + } + m.messages = m.messages[1:] + return &sqs.DeleteMessageOutput{}, nil +} + +func (m *mockSQS) ReceiveMessage(in *sqs.ReceiveMessageInput) (*sqs.ReceiveMessageOutput, error) { + if len(m.messages) == 0 { + return &sqs.ReceiveMessageOutput{}, nil + } + response := m.messages[0:1] + m.messages = m.messages[1:] + + return &sqs.ReceiveMessageOutput{ + Messages: response, + }, nil +} + +// Used to get queue length - ApproximateNumberOfMessages +func (m *mockSQS) GetQueueAttributes(in *sqs.GetQueueAttributesInput) (*sqs.GetQueueAttributesOutput, error) { + + response := make(map[string]*string) + value := strconv.Itoa(len(m.messages)) + + response["locationNameKey"] = in.AttributeNames[0] + response["locationNameValue"] = &value + + return &sqs.GetQueueAttributesOutput{ + Attributes: response, + }, nil +} + +func getQueueDepth(s sqsiface.SQSAPI) (int, error) { + attributeName := "ApproximateNumberOfMessages" + params := &sqs.GetQueueAttributesInput{ + QueueUrl: aws.String("mock-queue"), + AttributeNames: []*string{ + &attributeName, + }, + } + + r, err := s.GetQueueAttributes(params) + if err != nil { + log.Error(err.Error()) + return -1, err + } + + result, _ := strconv.Atoi(*r.Attributes["locationNameValue"]) + return result, nil +} + +func TestNewSqs(t *testing.T) { + test := assert.New(t) + + os.Setenv("SQS_ENDPOINT", "dummy-url") + os.Setenv("SQS_AWS_REGION", "dummy-region") + + s := NewSQS() + test.NotNil(s) +} + +func TestGetSqsAwsRegion(t *testing.T) { + test := assert.New(t) + tcs := []struct { + name string + endpointURL string + envSqsAwsRegion string + expectedRegion string + expectedError error + }{ + { + name: "get from endpoint", + endpointURL: "https://sqs.us-west-2.amazonaws.com/myaccountid/myqueue", + envSqsAwsRegion: "", + expectedRegion: "us-west-2", + expectedError: nil, + }, + { + name: "get from env var", + endpointURL: "https://sqs.amazonaws.com/myaccountid/myqueue", + envSqsAwsRegion: "us-west-2", + expectedRegion: "us-west-2", + expectedError: nil, + }, + { + name: "error", + endpointURL: "https://sqs.amazonaws.com/myaccountid/myqueue", + envSqsAwsRegion: "", + expectedRegion: "", + expectedError: fmt.Errorf("cannot get sqs aws region from sqsEndpoint or from environment variables"), + }, + } + + for _, tc := range tcs { + os.Setenv("SQS_AWS_REGION", tc.envSqsAwsRegion) + awsRegion, err := getSqsAwsRegion(tc.endpointURL) + + if tc.expectedError != nil { + test.Error(err, "there should be an error processing the message") + test.Contains(fmt.Sprintf("%v", err), fmt.Sprintf("%v", tc.expectedError), "the error message should be as expected") + } else { + test.NoError(err) + } + test.Equal(tc.expectedRegion, awsRegion) + } +} + +func TestGetSqsQueueName(t *testing.T) { + test := assert.New(t) + tcs := []struct { + name string + endpointURL string + envSqsQueueName string + expectedQueueName string + expectedError error + }{ + { + name: "get from endpoint", + endpointURL: "https://sqs.us-west-2.amazonaws.com/myaccountid/myqueue", + envSqsQueueName: "", + expectedQueueName: "myqueue", + expectedError: nil, + }, + { + name: "get from env var", + endpointURL: "https://sqs.us-west-2.amazonaws.com", + envSqsQueueName: "myqueue", + expectedQueueName: "myqueue", + expectedError: nil, + }, + { + name: "error", + endpointURL: "https://sqs.us-west-2.amazonaws.com", + envSqsQueueName: "", + expectedQueueName: "", + expectedError: fmt.Errorf("cannot get sqs queue name from sqsEndpoint or from environment variables"), + }, + } + + for _, tc := range tcs { + os.Setenv("SQS_QUEUE_NAME", tc.envSqsQueueName) + queueName, err := getSqsQueueName(tc.endpointURL) + + if tc.expectedError != nil { + test.Error(err, "there should be an error processing the message") + test.Contains(fmt.Sprintf("%v", err), fmt.Sprintf("%v", tc.expectedError), "the error message should be as expected") + } else { + test.NoError(err) + } + test.Equal(tc.expectedQueueName, queueName) + } +} diff --git a/pkg/api/utils/errors.go b/pkg/api/utils/errors.go new file mode 100644 index 00000000..85fd8e90 --- /dev/null +++ b/pkg/api/utils/errors.go @@ -0,0 +1,43 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package utils + +import ( + "github.com/labstack/echo/v4" +) + +// Error struct +type Error struct { + Errors map[string]interface{} `json:"errors"` +} + +// NewError constructor +func NewError(err error) Error { + e := Error{} + e.Errors = make(map[string]interface{}) + switch v := err.(type) { + case *echo.HTTPError: + e.Errors["body"] = v.Message + default: + e.Errors["body"] = v.Error() + } + return e +} + +// NotFound returns an error in case a resource was not found +func NotFound() Error { + e := Error{} + e.Errors = make(map[string]interface{}) + e.Errors["body"] = "resource not found" + return e +} diff --git a/pkg/api/utils/errors_test.go b/pkg/api/utils/errors_test.go new file mode 100644 index 00000000..d0799939 --- /dev/null +++ b/pkg/api/utils/errors_test.go @@ -0,0 +1,66 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package utils + +import ( + "fmt" + "net/http" + "testing" + + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" +) + +func TestNewError(t *testing.T) { + test := assert.New(t) + tcs := []struct { + name string + inputError error + expectedError Error + }{ + { + name: "simple error", + inputError: fmt.Errorf("Validation failed"), + expectedError: Error{Errors: map[string]interface{}{"body": "Validation failed"}}, + }, + { + name: "http error", + inputError: echo.NewHTTPError(http.StatusBadGateway, "Bad gateway"), + expectedError: Error{Errors: map[string]interface{}{"body": "Bad gateway"}}, + }, + } + + for _, tc := range tcs { + err := NewError(tc.inputError) + test.Contains(fmt.Sprintf("%v", err), fmt.Sprintf("%v", tc.expectedError), "the error message should be as expected") + } +} + +func TestNotFound(t *testing.T) { + test := assert.New(t) + tcs := []struct { + name string + inputError Error + expectedError Error + }{ + { + name: "simple error", + inputError: NotFound(), + expectedError: Error{Errors: map[string]interface{}{"body": "resource not found"}}, + }, + } + + for _, tc := range tcs { + test.Contains(fmt.Sprintf("%v", tc.inputError), fmt.Sprintf("%v", tc.expectedError), "the error message should be as expected") + } +} diff --git a/pkg/cc/api/config/v1/clientconfig_types.go b/pkg/cc/api/config/v1/clientconfig_types.go new file mode 100644 index 00000000..dfb8e149 --- /dev/null +++ b/pkg/cc/api/config/v1/clientconfig_types.go @@ -0,0 +1,54 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + cfg "sigs.k8s.io/controller-runtime/pkg/config/v1alpha1" +) + +// ClientConfigStatus defines the observed state of ClientConfig +type ClientConfigStatus struct { +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// ClientConfig is the Schema for the clientconfigs API +type ClientConfig struct { + metav1.TypeMeta `json:",inline"` + + // ControllerManagerConfigurationSpec returns the contfigurations for controllers + cfg.ControllerManagerConfigurationSpec `json:",inline"` + + Namespace string `json:"namespace,omitempty"` + + AlertmanagerWebhook AlertmanagerWebhookConfig `json:"alertmanagerWebhook"` +} + +// AlertmanagerWebhookConfig ... +type AlertmanagerWebhookConfig struct { + BindAddress string `json:"bindAddress"` + AlertMap []AlertRule `json:"alertMap"` +} + +// AlertRule ... +type AlertRule struct { + AlertName string `json:"alertName"` + OnFiring map[string]string `json:"onFiring"` + OnResolved map[string]string `json:"onResolved"` +} + +func init() { + SchemeBuilder.Register(&ClientConfig{}) +} diff --git a/pkg/cc/api/config/v1/groupversion_info.go b/pkg/cc/api/config/v1/groupversion_info.go new file mode 100644 index 00000000..8b3fbdef --- /dev/null +++ b/pkg/cc/api/config/v1/groupversion_info.go @@ -0,0 +1,32 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +// Package v1 contains API Schema definitions for the config v1 API group +//+kubebuilder:object:generate=true +//+groupName=config.registry.ethos.adobe.com +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "config.registry.ethos.adobe.com", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/pkg/cc/api/config/v1/zz_generated.deepcopy.go b/pkg/cc/api/config/v1/zz_generated.deepcopy.go new file mode 100644 index 00000000..f1decbc0 --- /dev/null +++ b/pkg/cc/api/config/v1/zz_generated.deepcopy.go @@ -0,0 +1,113 @@ +// +build !ignore_autogenerated + +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AlertRule) DeepCopyInto(out *AlertRule) { + *out = *in + if in.OnFiring != nil { + in, out := &in.OnFiring, &out.OnFiring + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.OnResolved != nil { + in, out := &in.OnResolved, &out.OnResolved + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertRule. +func (in *AlertRule) DeepCopy() *AlertRule { + if in == nil { + return nil + } + out := new(AlertRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AlertmanagerWebhookConfig) DeepCopyInto(out *AlertmanagerWebhookConfig) { + *out = *in + if in.AlertMap != nil { + in, out := &in.AlertMap, &out.AlertMap + *out = make([]AlertRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertmanagerWebhookConfig. +func (in *AlertmanagerWebhookConfig) DeepCopy() *AlertmanagerWebhookConfig { + if in == nil { + return nil + } + out := new(AlertmanagerWebhookConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClientConfig) DeepCopyInto(out *ClientConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ControllerManagerConfigurationSpec.DeepCopyInto(&out.ControllerManagerConfigurationSpec) + in.AlertmanagerWebhook.DeepCopyInto(&out.AlertmanagerWebhook) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientConfig. +func (in *ClientConfig) DeepCopy() *ClientConfig { + if in == nil { + return nil + } + out := new(ClientConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClientConfig) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClientConfigStatus) DeepCopyInto(out *ClientConfigStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientConfigStatus. +func (in *ClientConfigStatus) DeepCopy() *ClientConfigStatus { + if in == nil { + return nil + } + out := new(ClientConfigStatus) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/cc/api/registry/v1/cluster_types.go b/pkg/cc/api/registry/v1/cluster_types.go new file mode 100644 index 00000000..e0538426 --- /dev/null +++ b/pkg/cc/api/registry/v1/cluster_types.go @@ -0,0 +1,267 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ClusterSpec defines the desired state of Cluster +type ClusterSpec struct { + + // Cluster name + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=64 + // +kubebuilder:validation:MinLength=3 + Name string `json:"name,hash"` + + // Cluster name, without dash. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=64 + // +kubebuilder:validation:MinLength=3 + ShortName string `json:"shortName"` + + // Information about K8s API endpoint and CA cert + // +kubebuilder:validation:Required + APIServer APIServer `json:"apiServer"` + + // Cluster standard region name + // +kubebuilder:validation:Required + Region string `json:"region"` + + // The cloud provider. + // +kubebuilder:validation:Required + CloudType string `json:"cloudType"` + + // Cluster environment. + // +kubebuilder:validation:Required + Environment string `json:"environment"` + + // The BU that owns and maintains the cluster + // +kubebuilder:validation:Required + BusinessUnit string `json:"businessUnit"` + + // The Ethos offering that the cluster is meant for + // +kubebuilder:validation:Required + Offering []Offering `json:"offering"` + + // The cloud account associated with the cluster + // +kubebuilder:validation:Required + AccountID string `json:"accountId"` + + // List of tiers with their associated information + // +kubebuilder:validation:Required + Tiers []Tier `json:"tiers"` + + // Virtual Private Networks information + // +kubebuilder:validation:Required + VirtualNetworks []VirtualNetwork `json:"virtualNetworks"` + + // K8s Infrastructure release information + // +kubebuilder:validation:Required + K8sInfraRelease K8sInfraRelease `json:"k8sInfraRelease"` + + // Timestamp when cluster was registered in Cluster Registry + // +kubebuilder:validation:Required + RegisteredAt string `json:"registeredAt"` + + // Cluster status + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=Inactive;Active;Deprecated;Deleted + Status string `json:"status"` + + // Cluster phase + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=Building;Testing;Running;Upgrading + Phase string `json:"phase"` + + // The type of the cluster + Type string `json:"type,omitempty"` + + // Ethos Extra specific information + Extra Extra `json:"extra,omitempty"` + + // Git teams and/or LDAP groups that are allowed to onboard and deploy on the cluster + AllowedOnboardingTeams []AllowedOnboardingTeam `json:"allowedOnboardingTeams,omitempty"` + + // List of cluster capabilities + Capabilities []string `json:"capabilities,omitempty"` + + // Information about Virtual Networks peered with the cluster + PeerVirtualNetworks []PeerVirtualNetwork `json:"peerVirtualNetworks,omitempty"` + + // Timestamp when cluster information was updated + LastUpdated string `json:"lastUpdated,omitempty"` + + // Cluster tags that were applied + Tags map[string]string `json:"tags,omitempty"` +} + +// Offering - the Ethos offering that the cluster is meant for +// +kubebuilder:validation:Enum=CaaS;PaaS +type Offering string + +// APIServer - information about K8s API server +type APIServer struct { + + // Information about K8s Api Endpoint + // +kubebuilder:validation:Required + Endpoint string `json:"endpoint"` + + // Information about K8s Api CA Cert + CertificateAuthorityData string `json:"certificateAuthorityData"` +} + +// AllowedOnboardingTeam represents the Git teams and/or LDAP groups that are allowed to onboard. +type AllowedOnboardingTeam struct { + + // Name of the team + // +kubebuilder:validation:Required + Name string `json:"name"` + + // List of git teams + GitTeams []string `json:"gitTeams,omitempty"` + + // List of ldap groups + LdapGroups []string `json:"ldapGroups,omitempty"` +} + +// Extra information. +type Extra struct { + // Name of the domain + // +kubebuilder:validation:Required + DomainName string `json:"domainName"` + + // Load balancer endpoints + // +kubebuilder:validation:Required + LbEndpoints map[string]string `json:"lbEndpoints"` + + // Logging endpoints + // +kubebuilder:validation:Required + LoggingEndpoints []map[string]string `json:"loggingEndpoints,omitempty"` + + // List of IAM Arns + EcrIamArns map[string]string `json:"ecrIamArns,omitempty"` + + // Egress ports allowed outside of the namespace + EgressPorts string `json:"egressPorts,omitempty"` + + // NFS information + NFSInfo map[string]string `json:"nfsInfo,omitempty"` +} + +// Tier details +type Tier struct { + + // Name of the tier + // +kubebuilder:validation:Required + Name string `json:"name"` + + // Type of the instances + // +kubebuilder:validation:Required + InstanceType string `json:"instanceType"` + + // Container runtime + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=docker;cri-o + ContainerRuntime string `json:"containerRuntime"` + + // Min number of instances + // +kubebuilder:validation:Required + MinCapacity int `json:"minCapacity"` + + // Max number of instances + // +kubebuilder:validation:Required + MaxCapacity int `json:"maxCapacity"` + + // Instance K8s labels + Labels map[string]string `json:"labels,omitempty"` + + // Instance K8s taints + Taints []string `json:"taints,omitempty"` + + // EnableKataSupport + EnableKataSupport bool `json:"enableKataSupport,omitempty"` + + // KernelParameters + KernelParameters map[string]string `json:"kernelParameters,omitempty"` +} + +// VirtualNetwork information. +type VirtualNetwork struct { + + // Virtual private network Id + // +kubebuilder:validation:Required + ID string `json:"id"` + + // CIDRs used in this VirtualNetwork + // +kubebuilder:validation:Required + Cidrs []string `json:"cidrs"` +} + +// PeerVirtualNetwork - peering information done at cluster onboarding +type PeerVirtualNetwork struct { + + // Remote Virtual Netowrk ID + ID string `json:"id,omitempty"` + + // Remote Virtual Netowrk CIDRs + Cidrs []string `json:"cidrs,omitempty"` + + // Cloud account of the owner + OwnerID string `json:"ownerID,omitempty"` +} + +// K8sInfraRelease information +type K8sInfraRelease struct { + + // GitSha of the release + GitSha string `json:"gitSha"` + + // When the release was applied on the cluster + LastUpdated string `json:"lastUpdated"` + + // Release name + Release string `json:"release"` +} + +// ClusterStatus defines the observed state of Cluster +type ClusterStatus struct { + // Send/Receive Errors + // Last Update Timestamp? +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Cluster is the Schema for the clusters API +type Cluster struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ClusterSpec `json:"spec,omitempty"` + Status ClusterStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// ClusterList contains a list of Cluster +type ClusterList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Cluster `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Cluster{}, &ClusterList{}) +} diff --git a/pkg/cc/api/registry/v1/groupversion_info.go b/pkg/cc/api/registry/v1/groupversion_info.go new file mode 100644 index 00000000..e15ec754 --- /dev/null +++ b/pkg/cc/api/registry/v1/groupversion_info.go @@ -0,0 +1,32 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +// Package v1 contains API Schema definitions for the registry v1 API group +//+kubebuilder:object:generate=true +//+groupName=registry.ethos.adobe.com +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "registry.ethos.adobe.com", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/pkg/cc/api/registry/v1/zz_generated.deepcopy.go b/pkg/cc/api/registry/v1/zz_generated.deepcopy.go new file mode 100644 index 00000000..e435d0cc --- /dev/null +++ b/pkg/cc/api/registry/v1/zz_generated.deepcopy.go @@ -0,0 +1,336 @@ +// +build !ignore_autogenerated + +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIServer) DeepCopyInto(out *APIServer) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServer. +func (in *APIServer) DeepCopy() *APIServer { + if in == nil { + return nil + } + out := new(APIServer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AllowedOnboardingTeam) DeepCopyInto(out *AllowedOnboardingTeam) { + *out = *in + if in.GitTeams != nil { + in, out := &in.GitTeams, &out.GitTeams + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.LdapGroups != nil { + in, out := &in.LdapGroups, &out.LdapGroups + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AllowedOnboardingTeam. +func (in *AllowedOnboardingTeam) DeepCopy() *AllowedOnboardingTeam { + if in == nil { + return nil + } + out := new(AllowedOnboardingTeam) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Cluster) DeepCopyInto(out *Cluster) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Cluster. +func (in *Cluster) DeepCopy() *Cluster { + if in == nil { + return nil + } + out := new(Cluster) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Cluster) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterList) DeepCopyInto(out *ClusterList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Cluster, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterList. +func (in *ClusterList) DeepCopy() *ClusterList { + if in == nil { + return nil + } + out := new(ClusterList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) { + *out = *in + out.APIServer = in.APIServer + if in.Offering != nil { + in, out := &in.Offering, &out.Offering + *out = make([]Offering, len(*in)) + copy(*out, *in) + } + if in.Tiers != nil { + in, out := &in.Tiers, &out.Tiers + *out = make([]Tier, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.VirtualNetworks != nil { + in, out := &in.VirtualNetworks, &out.VirtualNetworks + *out = make([]VirtualNetwork, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + out.K8sInfraRelease = in.K8sInfraRelease + in.Extra.DeepCopyInto(&out.Extra) + if in.AllowedOnboardingTeams != nil { + in, out := &in.AllowedOnboardingTeams, &out.AllowedOnboardingTeams + *out = make([]AllowedOnboardingTeam, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Capabilities != nil { + in, out := &in.Capabilities, &out.Capabilities + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.PeerVirtualNetworks != nil { + in, out := &in.PeerVirtualNetworks, &out.PeerVirtualNetworks + *out = make([]PeerVirtualNetwork, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterSpec. +func (in *ClusterSpec) DeepCopy() *ClusterSpec { + if in == nil { + return nil + } + out := new(ClusterSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterStatus) DeepCopyInto(out *ClusterStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterStatus. +func (in *ClusterStatus) DeepCopy() *ClusterStatus { + if in == nil { + return nil + } + out := new(ClusterStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Extra) DeepCopyInto(out *Extra) { + *out = *in + if in.LbEndpoints != nil { + in, out := &in.LbEndpoints, &out.LbEndpoints + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.LoggingEndpoints != nil { + in, out := &in.LoggingEndpoints, &out.LoggingEndpoints + *out = make([]map[string]string, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + } + } + if in.EcrIamArns != nil { + in, out := &in.EcrIamArns, &out.EcrIamArns + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.NFSInfo != nil { + in, out := &in.NFSInfo, &out.NFSInfo + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Extra. +func (in *Extra) DeepCopy() *Extra { + if in == nil { + return nil + } + out := new(Extra) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *K8sInfraRelease) DeepCopyInto(out *K8sInfraRelease) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new K8sInfraRelease. +func (in *K8sInfraRelease) DeepCopy() *K8sInfraRelease { + if in == nil { + return nil + } + out := new(K8sInfraRelease) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PeerVirtualNetwork) DeepCopyInto(out *PeerVirtualNetwork) { + *out = *in + if in.Cidrs != nil { + in, out := &in.Cidrs, &out.Cidrs + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PeerVirtualNetwork. +func (in *PeerVirtualNetwork) DeepCopy() *PeerVirtualNetwork { + if in == nil { + return nil + } + out := new(PeerVirtualNetwork) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Tier) DeepCopyInto(out *Tier) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Taints != nil { + in, out := &in.Taints, &out.Taints + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.KernelParameters != nil { + in, out := &in.KernelParameters, &out.KernelParameters + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Tier. +func (in *Tier) DeepCopy() *Tier { + if in == nil { + return nil + } + out := new(Tier) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualNetwork) DeepCopyInto(out *VirtualNetwork) { + *out = *in + if in.Cidrs != nil { + in, out := &in.Cidrs, &out.Cidrs + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualNetwork. +func (in *VirtualNetwork) DeepCopy() *VirtualNetwork { + if in == nil { + return nil + } + out := new(VirtualNetwork) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/cc/controllers/cluster_controller.go b/pkg/cc/controllers/cluster_controller.go new file mode 100644 index 00000000..1246a2ad --- /dev/null +++ b/pkg/cc/controllers/cluster_controller.go @@ -0,0 +1,183 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package controllers + +import ( + "context" + "crypto/sha256" + "encoding/json" + "fmt" + "os" + "strconv" + "time" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + "github.com/adobe/cluster-registry/pkg/api/sqs" + registryv1 "github.com/adobe/cluster-registry/pkg/cc/api/registry/v1" +) + +// ClusterReconciler reconciles a Cluster object +type ClusterReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + Queue sqs.Producer + CAData string +} + +const ( + // HashAnnotation ... + HashAnnotation = "registry.ethos.adobe.com/hash" +) + +//+kubebuilder:rbac:groups=registry.ethos.adobe.com,resources=clusters,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=registry.ethos.adobe.com,resources=clusters/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=registry.ethos.adobe.com,resources=clusters/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := r.Log.WithValues("name", req.NamespacedName) + + instance := new(registryv1.Cluster) + if err := r.Get(ctx, req.NamespacedName, instance); err != nil { + log.Error(err, "unable to fetch object") + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + setCACert := true + + if envString, ok := os.LookupEnv("SET_CA_CERT"); !ok { + // do nothing + } else if envBool, err := strconv.ParseBool(envString); err != nil { + log.Info("SET_CA_CERT environment variable must be boolean") + } else { + setCACert = envBool + } + + if setCACert { + if r.CAData != "" { + instance.Spec.APIServer.CertificateAuthorityData = r.CAData + } else { + log.Info("Certificate Authority data is empty") + } + } else { + instance.Spec.APIServer.CertificateAuthorityData = "" + } + + return r.ReconcileCreateUpdate(instance, log) +} + +// ReconcileCreateUpdate ... +func (r *ClusterReconciler) ReconcileCreateUpdate(instance *registryv1.Cluster, log logr.Logger) (ctrl.Result, error) { + hash := Hash(instance) + + annotations := instance.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string, 1) + } + annotations[HashAnnotation] = hash + instance.SetAnnotations(annotations) + + err := r.enqueue(instance, log) + if err != nil { + log.Error(err, "Error enqueuing message") + return ctrl.Result{}, err + } + + if err := r.Client.Update(context.TODO(), instance); err != nil { + log.Error(err, "Cannot update the object") + return ctrl.Result{}, err + } + log.V(1).Info("Object updated") + + return ctrl.Result{}, nil +} + +func (r *ClusterReconciler) hasDifferentHash(object runtime.Object) bool { + instance := object.(*registryv1.Cluster) + oldHash := instance.GetAnnotations()[HashAnnotation] + newHash := Hash(instance) + + if oldHash != newHash { + r.Log.Info("Different hash found", "old", oldHash, "new", newHash, + "name", fmt.Sprintf("%s/%s", instance.GetNamespace(), instance.GetName())) + return true + } + r.Log.Info("Same hash found", "name", fmt.Sprintf("%s/%s", instance.GetNamespace(), instance.GetName())) + return false +} + +func (r *ClusterReconciler) eventFilters() predicate.Predicate { + return predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + r.Log.Info("CreateEvent", "event", e.Object) + return r.hasDifferentHash(e.Object) + }, + UpdateFunc: func(e event.UpdateEvent) bool { + r.Log.Info("UpdateEvent", "event", e.ObjectNew) + return r.hasDifferentHash(e.ObjectNew) + }, + DeleteFunc: func(e event.DeleteEvent) bool { + r.Log.Info("DeleteEvent", "event", e.Object) + return !e.DeleteStateUnknown + }, + } +} + +func (r *ClusterReconciler) enqueue(instance *registryv1.Cluster, log logr.Logger) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + err := r.Queue.Send(ctx, instance) + + if err != nil { + return err + } + + log.Info("Successfully enqueued cluster " + instance.Spec.Name) + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(®istryv1.Cluster{}, builder.WithPredicates(r.eventFilters())). + Complete(r) +} + +// Hash returns a SHA256 hash of the Cluster object, after removing the ResourceVersion, +// ManagedFields and Hash annotation +func Hash(instance *registryv1.Cluster) string { + clone := instance.DeepCopyObject().(*registryv1.Cluster) + + annotations := clone.GetAnnotations() + delete(annotations, HashAnnotation) + clone.SetAnnotations(annotations) + + clone.SetResourceVersion("") + clone.SetManagedFields(nil) + + b, _ := json.Marshal(clone) + h := sha256.New() + h.Write([]byte(fmt.Sprintf("%v", b))) + + return fmt.Sprintf("%x", h.Sum(nil)) +} diff --git a/pkg/cc/controllers/suite_test.go b/pkg/cc/controllers/suite_test.go new file mode 100644 index 00000000..cf73ab0d --- /dev/null +++ b/pkg/cc/controllers/suite_test.go @@ -0,0 +1,92 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package controllers + +import ( + "path/filepath" + "testing" + "time" + + "github.com/adobe/cluster-registry/pkg/api/sqs" + registryv1 "github.com/adobe/cluster-registry/pkg/cc/api/registry/v1" + "github.com/adobe/cluster-registry/pkg/cc/monitoring" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" + "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +var ( + k8sClient client.Client + k8sManager ctrl.Manager + testEnv *envtest.Environment +) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Webhook Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + } + + cfg, err := testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = registryv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + k8sManager, err = ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + }) + Expect(err).ToNot(HaveOccurred()) + + metrics := monitoring.NewMetrics() + metrics.Init(true) + + err = (&ClusterReconciler{ + Client: k8sManager.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("Cluster"), + Scheme: k8sManager.GetScheme(), + Queue: sqs.NewFakeProducer(metrics), + CAData: "_cert_data_", + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + go func() { + err = k8sManager.Start(ctrl.SetupSignalHandler()) + Expect(err).ToNot(HaveOccurred()) + }() + + k8sClient = k8sManager.GetClient() + Expect(k8sClient).ToNot(BeNil()) +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + gexec.KillAndWait(5 * time.Second) + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/pkg/cc/monitoring/metrics.go b/pkg/cc/monitoring/metrics.go new file mode 100644 index 00000000..853c9c43 --- /dev/null +++ b/pkg/cc/monitoring/metrics.go @@ -0,0 +1,112 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package monitoring + +import ( + "regexp" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +// MetricsI interface +type MetricsI interface { + RecordEgressRequestCnt(target string) + RecordEgressRequestDur(target string, elapsed float64) + RecordDMSLastTimestamp() + GetMetricByName(name string) prometheus.Collector +} + +// Metrics contains the metrics gathered by the instance and its path +type Metrics struct { + egressReqCnt *prometheus.CounterVec + egressReqDur *prometheus.HistogramVec + dmsLastTimestamp prometheus.Gauge + metrics []prometheus.Collector +} + +// NewMetrics func +func NewMetrics() *Metrics { + return &Metrics{} +} + +// Init func +func (m *Metrics) Init(isUnitTest bool) { + reg := prometheus.DefaultRegisterer + if isUnitTest { + reg = prometheus.NewRegistry() + } + + var egressReqCnt prometheus.Collector = promauto.With(reg).NewCounterVec( + prometheus.CounterOpts{ + Name: "cluster_registry_cc_egress_requests_total", + Help: "How many egress requests sent, partitioned by target.", + }, + []string{"target"}, + ) + m.egressReqCnt = egressReqCnt.(*prometheus.CounterVec) + m.metrics = append(m.metrics, m.egressReqCnt) + + var egressReqDur prometheus.Collector = promauto.With(reg).NewHistogramVec( + prometheus.HistogramOpts{ + Name: "cluster_registry_cc_egress_request_duration_seconds", + Help: "The Egress HTTP request latencies in seconds partitioned by target.", + }, + []string{"target"}, + ) + m.egressReqDur = egressReqDur.(*prometheus.HistogramVec) + m.metrics = append(m.metrics, m.egressReqDur) + + var dmsLastTimestamp prometheus.Collector = promauto.With(reg).NewGauge( + prometheus.GaugeOpts{ + Name: "cluster_registry_cc_deadmansswitch_last_timestamp_seconds", + Help: "Last timestamp when a DeadMansSwitch alert was received.", + }, + ) + m.dmsLastTimestamp = dmsLastTimestamp.(prometheus.Gauge) + m.metrics = append(m.metrics, m.dmsLastTimestamp) +} + +// RecordEgressRequestCnt increases the Egress counter for a taget +func (m *Metrics) RecordEgressRequestCnt(target string) { + m.egressReqCnt.WithLabelValues(target).Inc() +} + +// RecordEgressRequestDur registers the Egress duration for a taget +func (m *Metrics) RecordEgressRequestDur(target string, elapsed float64) { + m.egressReqDur.WithLabelValues(target).Observe(elapsed) +} + +// RecordDMSLastTimestamp records the current timestamp when a DMS is received +func (m *Metrics) RecordDMSLastTimestamp() { + m.dmsLastTimestamp.SetToCurrentTime() +} + +// GetMetricByName returns a metric by it's fully qualified name (used for testing purposes) +func (m *Metrics) GetMetricByName(name string) prometheus.Collector { + // don't judge me :( + desc := make(chan *prometheus.Desc, 1) + for _, metric := range m.metrics { + metric.Describe(desc) + re, err := regexp.Compile(`Desc{fqName: "([a-z_]+)"`) + if err != nil { + continue + } + fqName := re.FindStringSubmatch((*<-desc).String())[1] + if fqName == name { + return metric + } + } + + return nil +} diff --git a/pkg/cc/monitoring/metrics_test.go b/pkg/cc/monitoring/metrics_test.go new file mode 100644 index 00000000..43261342 --- /dev/null +++ b/pkg/cc/monitoring/metrics_test.go @@ -0,0 +1,94 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package monitoring + +import ( + "fmt" + "math/rand" + "strings" + "testing" + + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" +) + +const ( + egressTarget = "testing_egress" + minRand = 1 + maxRand = 2.5 + subsystem = "cluster_registry_cc" +) + +func TestNewMetrics(t *testing.T) { + test := assert.New(t) + m := NewMetrics() + test.NotNil(m) +} + +func TestInit(t *testing.T) { + test := assert.New(t) + m := NewMetrics() + m.Init(true) + test.NotNil(m.egressReqCnt) + test.NotNil(m.egressReqDur) +} + +func TestRecordEgressRequestCnt(t *testing.T) { + test := assert.New(t) + m := NewMetrics() + m.Init(true) + + m.RecordEgressRequestCnt(egressTarget) + + test.Equal(1, testutil.CollectAndCount(*m.egressReqCnt)) + test.Equal(float64(1), testutil.ToFloat64((*m.egressReqCnt).WithLabelValues(egressTarget))) +} + +// Generate a random float number between min and max +func generateFloatRand(min, max float64) float64 { + return min + rand.Float64()*(max-min) +} + +func TestRecordEgressRequestDur(t *testing.T) { + m := NewMetrics() + m.Init(true) + + randomFloat := generateFloatRand(minRand, maxRand) + m.RecordEgressRequestDur(egressTarget, randomFloat) + + expected := fmt.Sprintf(` + # HELP %[1]s_egress_request_duration_seconds The Egress HTTP request latencies in seconds partitioned by target. + # TYPE %[1]s_egress_request_duration_seconds histogram + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="0.005"} 0 + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="0.01"} 0 + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="0.025"} 0 + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="0.05"} 0 + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="0.1"} 0 + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="0.25"} 0 + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="0.5"} 0 + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="1"} 0 + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="2.5"} 1 + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="5"} 1 + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="10"} 1 + %[1]s_egress_request_duration_seconds_bucket{target="%[2]s",le="+Inf"} 1 + %[1]s_egress_request_duration_seconds_sum{target="%[2]s"} %[3]s + %[1]s_egress_request_duration_seconds_count{target="%[2]s"} 1 + `, subsystem, egressTarget, fmt.Sprintf("%.16f", randomFloat)) + + if err := testutil.CollectAndCompare( + *m.egressReqDur, + strings.NewReader(expected), + fmt.Sprintf("%s_egress_request_duration_seconds", subsystem)); err != nil { + t.Errorf("unexpected collecting result:\n%s", err) + } +} diff --git a/pkg/cc/webhook/alert.go b/pkg/cc/webhook/alert.go new file mode 100644 index 00000000..2ec3ce78 --- /dev/null +++ b/pkg/cc/webhook/alert.go @@ -0,0 +1,71 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package webhook + +import "time" + +const ( + // AlertStatusFiring alert status when it is firing + AlertStatusFiring string = "firing" + + // AlertStatusResolved alert status when it is resolved + AlertStatusResolved string = "resolved" +) + +// Alert represents an Alertmanager alert +type Alert struct { + Receiver string `json:"receiver"` + Status string `json:"status"` + Alerts []AlertItem `json:"alerts"` + GroupLabels GroupLabels `json:"groupLabels"` + CommonLabels CommonLabels `json:"commonLabels"` + CommonAnnotations CommonAnnotations `json:"commonAnnotations"` + ExternalURL string `json:"externalURL"` + Version string `json:"version"` + GroupKey string `json:"groupKey"` +} + +// AlertItem ... +type AlertItem struct { + Status string `json:"status"` + Labels AlertLabels `json:"labels"` + Annotations CommonAnnotations `json:"annotations"` + StartsAt time.Time `json:"startsAt"` + EndsAt time.Time `json:"endsAt"` + GeneratorURL string `json:"generatorURL"` + Fingerprint string `json:"fingerprint"` +} + +// AlertLabels ... +type AlertLabels struct { + Alertname string `json:"alertname"` + Service string `json:"service"` + Severity string `json:"severity"` +} + +// GroupLabels ... +type GroupLabels struct { + Alertname string `json:"alertname"` +} + +// CommonLabels ... +type CommonLabels struct { + Alertname string `json:"alertname"` + Service string `json:"service"` + Severity string `json:"severity"` +} + +// CommonAnnotations ... +type CommonAnnotations struct { + Summary string `json:"summary"` +} diff --git a/pkg/cc/webhook/server.go b/pkg/cc/webhook/server.go new file mode 100644 index 00000000..0a8ff7f7 --- /dev/null +++ b/pkg/cc/webhook/server.go @@ -0,0 +1,128 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package webhook + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + configv1 "github.com/adobe/cluster-registry/pkg/cc/api/config/v1" + registryv1 "github.com/adobe/cluster-registry/pkg/cc/api/registry/v1" + "github.com/adobe/cluster-registry/pkg/cc/monitoring" + "github.com/go-logr/logr" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Server ... +type Server struct { + Client client.Client + Namespace string + Log logr.Logger + BindAddress string + Metrics monitoring.MetricsI + AlertMap []configv1.AlertRule +} + +const ( + // DeadMansSwitchAlertName is the name of the DMS alert + DeadMansSwitchAlertName = "CRCDeadMansSwitch" +) + +func (s *Server) webhookHandler(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + var alert Alert + + body, err := ioutil.ReadAll(r.Body) + if err != nil { + s.Log.Error(err, "unable to read response body") + w.WriteHeader(http.StatusInternalServerError) + return + } + + err = json.Unmarshal(body, &alert) + if err != nil { + s.Log.Error(err, "unable to unmarshal response body") + w.WriteHeader(http.StatusBadRequest) + return + } + + s.Log.Info("got alert", "alert", alert) + err = s.process(alert) + if err != nil { + s.Log.Error(err, "unable to handle alert", "alert", alert) + w.WriteHeader(http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusOK) +} + +func (s *Server) process(alert Alert) error { + if alert.CommonLabels.Alertname == DeadMansSwitchAlertName && alert.Status == AlertStatusFiring { + s.Metrics.RecordDMSLastTimestamp() + s.Log.Info("received deadmansswitch", "alertname", DeadMansSwitchAlertName) + return nil + } + + for _, i := range s.AlertMap { + if i.AlertName != alert.CommonLabels.Alertname { + continue + } + + var tag map[string]string + + if alert.Status == AlertStatusFiring { + s.Log.Info("OnFiring", "alert", alert.CommonLabels.Alertname, "tag", i.OnFiring) + tag = i.OnFiring + } else if alert.Status == AlertStatusResolved { + s.Log.Info("OnResolved", "alert", alert.CommonLabels.Alertname, "tag", i.OnResolved) + tag = i.OnResolved + } else { + return fmt.Errorf("invalid alert status") + } + + clusterList := ®istryv1.ClusterList{} + err := s.Client.List(context.TODO(), clusterList, &client.ListOptions{Namespace: s.Namespace}) + if err != nil { + return err + } + for i := range clusterList.Items { + cluster := &clusterList.Items[i] + if cluster.Spec.Tags == nil { + cluster.Spec.Tags = make(map[string]string) + } + for key, value := range tag { + cluster.Spec.Tags[key] = value + } + if err := s.Client.Update(context.TODO(), &clusterList.Items[i]); err != nil { + return err + } + } + return nil + } + return fmt.Errorf("unmapped alert") +} + +// Start starts the webhook server +func (s *Server) Start() error { + http.HandleFunc("/webhook", s.webhookHandler) + if err := http.ListenAndServe(s.BindAddress, nil); err != nil { + return err + } + + return nil +} diff --git a/pkg/cc/webhook/server_test.go b/pkg/cc/webhook/server_test.go new file mode 100644 index 00000000..756c5aff --- /dev/null +++ b/pkg/cc/webhook/server_test.go @@ -0,0 +1,261 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package webhook + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "time" + + configv1 "github.com/adobe/cluster-registry/pkg/cc/api/config/v1" + registryv1 "github.com/adobe/cluster-registry/pkg/cc/api/registry/v1" + "github.com/adobe/cluster-registry/pkg/cc/monitoring" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/prometheus/client_golang/prometheus/testutil" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" +) + +func newTestServer() *Server { + metrics := monitoring.NewMetrics() + metrics.Init(true) + + return &Server{ + Client: k8sClient, + Namespace: "cluster-registry", + BindAddress: "localhost:9999", + Log: ctrl.Log.WithName("webhook").WithName("Server"), + Metrics: metrics, + } +} + +func newTestAlert(name string, status string) *Alert { + return &Alert{ + Receiver: "cluster-registry-webhook-testing", + Status: status, + Alerts: []AlertItem{ + { + Status: status, + Labels: AlertLabels{ + Alertname: name, + }, + StartsAt: time.Now(), + EndsAt: time.Now().Add(time.Minute), + }, + }, + GroupLabels: GroupLabels{ + Alertname: name, + }, + CommonLabels: CommonLabels{ + Alertname: name, + }, + } +} + +func newTestAlertAsJSON(name string, status string) string { + alert := newTestAlert(name, status) + b, err := json.Marshal(alert) + if err != nil { + return "" + } + return string(b) +} + +var _ = Describe("Webhook Server", func() { + var server *Server + + const ( + timeout = time.Second * 10 + duration = time.Second * 10 + interval = time.Millisecond * 250 + ) + + BeforeEach(func() { + server = newTestServer() + }) + + AfterEach(func() { + // + }) + + Context("Webhook Handler", func() { + It("Should handle an empty/invalid request", func() { + req := httptest.NewRequest(http.MethodGet, "/webhook", strings.NewReader("")) + w := httptest.NewRecorder() + server.webhookHandler(w, req) + Expect(w.Result().StatusCode).To(Equal(http.StatusBadRequest)) + }) + + It("Should handle a valid request", func() { + req := httptest.NewRequest( + http.MethodGet, + "/webhook", + strings.NewReader(newTestAlertAsJSON("CRCDeadMansSwitch", AlertStatusFiring)), + ) + w := httptest.NewRecorder() + server.webhookHandler(w, req) + Expect(w.Result().StatusCode).To(Equal(http.StatusOK)) + }) + + It("Should handle DeadMansSwitch alert and record the metric", func() { + // TODO: improve this test, at the moment it checks if the output has changed from the + // initial state since we cannot reliable test for a timestamp value, and testutil does + // not provide a regex/custom comparison + + By("Checking that the metric was initialized successfully") + initial := strings.NewReader(` + # HELP cluster_registry_cc_deadmansswitch_last_timestamp_seconds Last timestamp when a DeadMansSwitch alert was received. + # TYPE cluster_registry_cc_deadmansswitch_last_timestamp_seconds gauge + cluster_registry_cc_deadmansswitch_last_timestamp_seconds 0 + `) + metric := server.Metrics.GetMetricByName("cluster_registry_cc_deadmansswitch_last_timestamp_seconds") + Expect(metric).NotTo(BeNil()) + Expect(testutil.CollectAndCompare(metric, initial)).To(Succeed()) + + By("Firing a DMS alert and expecting the metric have changed from its initial state") + req := httptest.NewRequest( + http.MethodGet, + "/webhook", + strings.NewReader(newTestAlertAsJSON("CRCDeadMansSwitch", AlertStatusFiring)), + ) + w := httptest.NewRecorder() + server.webhookHandler(w, req) + Expect(w.Result().StatusCode).To(Equal(http.StatusOK)) + Expect(testutil.CollectAndCompare(metric, initial)).ToNot(Succeed()) + }) + }) + + Context("Alert Mapping", func() { + const ( + clusterName = "test-cluster" + ) + + It("Should handle an alert based on existing alert mapping", func() { + ctx := context.Background() + + By("Defining the alert mapping") + server.AlertMap = []configv1.AlertRule{ + { + AlertName: "TestAlertMapping", + OnFiring: map[string]string{ + "my-tag": "on", + }, + OnResolved: map[string]string{ + "my-tag": "off", + }, + }, + } + + By("Creating cluster-registry namespace") + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: server.Namespace, + }, + } + Expect(k8sClient.Create(ctx, namespace)).Should(Succeed()) + + By("Creating a new Cluster CRD") + cluster := ®istryv1.Cluster{ + TypeMeta: metav1.TypeMeta{ + Kind: "Cluster", + APIVersion: registryv1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: clusterName, + Namespace: server.Namespace, + }, + Spec: registryv1.ClusterSpec{ + Name: "cluster01-prod-useast1", + ShortName: "cluster01produseast1", + APIServer: registryv1.APIServer{Endpoint: "", CertificateAuthorityData: ""}, + Region: "useast1", + CloudType: "Azure", + Environment: "Prod", + BusinessUnit: "BU1", + Offering: []registryv1.Offering{}, + AccountID: "", + Tiers: []registryv1.Tier{}, + VirtualNetworks: []registryv1.VirtualNetwork{}, + K8sInfraRelease: registryv1.K8sInfraRelease{}, + RegisteredAt: "", + Status: "Active", + Phase: "Running", + Type: "Shared", + Extra: registryv1.Extra{ + DomainName: "", + LbEndpoints: map[string]string{}, + LoggingEndpoints: []map[string]string{}, + EcrIamArns: map[string]string{}, + EgressPorts: "", + NFSInfo: map[string]string{}, + }, + AllowedOnboardingTeams: []registryv1.AllowedOnboardingTeam{}, + Capabilities: []string{}, + PeerVirtualNetworks: []registryv1.PeerVirtualNetwork{}, + LastUpdated: "", + }, + } + Expect(k8sClient.Create(ctx, cluster)).Should(Succeed()) + + clusterLookupKey := types.NamespacedName{Name: clusterName, Namespace: server.Namespace} + createdCluster := ®istryv1.Cluster{} + Eventually(func() bool { + err := k8sClient.Get(ctx, clusterLookupKey, createdCluster) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("Firing an alert and having it be mapped correctly") + req := httptest.NewRequest( + http.MethodGet, + "/webhook", + strings.NewReader(newTestAlertAsJSON("TestAlertMapping", AlertStatusFiring)), + ) + w := httptest.NewRecorder() + server.webhookHandler(w, req) + Expect(w.Result().StatusCode).To(Equal(http.StatusOK)) + + updatedCluster := ®istryv1.Cluster{} + Eventually(func() bool { + err := k8sClient.Get(ctx, clusterLookupKey, updatedCluster) + if err != nil { + return false + } + return updatedCluster.Spec.Tags["my-tag"] == "on" + }, timeout, interval).Should(BeTrue()) + + By("Resolving the alert and having it be mapped correctly") + req = httptest.NewRequest( + http.MethodGet, + "/webhook", + strings.NewReader(newTestAlertAsJSON("TestAlertMapping", AlertStatusResolved)), + ) + server.webhookHandler(w, req) + Expect(w.Result().StatusCode).To(Equal(http.StatusOK)) + + updatedCluster = ®istryv1.Cluster{} + Eventually(func() bool { + err := k8sClient.Get(ctx, clusterLookupKey, updatedCluster) + if err != nil { + return false + } + return updatedCluster.Spec.Tags["my-tag"] == "off" + }, timeout, interval).Should(BeTrue()) + }) + }) +}) diff --git a/pkg/cc/webhook/suite_test.go b/pkg/cc/webhook/suite_test.go new file mode 100644 index 00000000..e3e130d8 --- /dev/null +++ b/pkg/cc/webhook/suite_test.go @@ -0,0 +1,94 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package webhook + +import ( + "path/filepath" + "testing" + "time" + + "github.com/adobe/cluster-registry/pkg/api/sqs" + registryv1 "github.com/adobe/cluster-registry/pkg/cc/api/registry/v1" + "github.com/adobe/cluster-registry/pkg/cc/controllers" + "github.com/adobe/cluster-registry/pkg/cc/monitoring" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" + "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +var ( + k8sClient client.Client + k8sManager ctrl.Manager + testEnv *envtest.Environment +) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Webhook Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + } + + cfg, err := testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = registryv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + k8sManager, err = ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + MetricsBindAddress: ":8180", + }) + Expect(err).ToNot(HaveOccurred()) + + metrics := monitoring.NewMetrics() + metrics.Init(true) + + err = (&controllers.ClusterReconciler{ + Client: k8sManager.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("Cluster"), + Scheme: k8sManager.GetScheme(), + Queue: sqs.NewFakeProducer(metrics), + CAData: "_cert_data_", + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + go func() { + err = k8sManager.Start(ctrl.SetupSignalHandler()) + Expect(err).ToNot(HaveOccurred()) + }() + + k8sClient = k8sManager.GetClient() + Expect(k8sClient).ToNot(BeNil()) +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + gexec.KillAndWait(5 * time.Second) + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/test/e2e_test.go b/test/e2e_test.go new file mode 100644 index 00000000..c4d46a08 --- /dev/null +++ b/test/e2e_test.go @@ -0,0 +1,258 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package e2e + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "testing" + "time" + + "github.com/adobe/cluster-registry/pkg/api/database" + "github.com/adobe/cluster-registry/pkg/api/monitoring" + registryv1 "github.com/adobe/cluster-registry/pkg/cc/api/registry/v1" + "github.com/stretchr/testify/suite" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" +) + +type e2eTestSuite struct { + suite.Suite + apiPort int +} + +type clusterList struct { + Items []*registryv1.ClusterSpec `json:"items"` + ItemsCount int `json:"itemsCount"` +} + +func TestE2ETestSuite(t *testing.T) { + suite.Run(t, &e2eTestSuite{}) +} + +func (s *e2eTestSuite) SetupSuite() { + s.apiPort = 8080 +} + +func (s *e2eTestSuite) TearDownSuite() { +} + +func (s *e2eTestSuite) SetupTest() { +} + +func (s *e2eTestSuite) TearDownTest() { + +} + +func (s *e2eTestSuite) Test_EndToEnd_GetClusters() { + + var clusters clusterList + jwt_token := getToken() + bearer := "Bearer " + jwt_token + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:%d/api/v1/clusters", s.apiPort), nil) + if err != nil { + log.Fatal(err) + } + + req.Header.Add("Authorization", bearer) + // Send req using http Client + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + + err = json.Unmarshal([]byte(body), &clusters) + if err != nil { + log.Fatal(err) + } + + s.Assert().Equal(3, clusters.ItemsCount) +} + +func (s *e2eTestSuite) Test_EndToEnd_CreateCluster() { + + var inputCluster registryv1.Cluster + var outputCluster registryv1.ClusterSpec + + input_file := "./testdata/cluster05-prod-useast1.json" + data, err := ioutil.ReadFile(input_file) + if err != nil { + log.Fatal(err.Error()) + } + + err = json.Unmarshal([]byte(data), &inputCluster) + if err != nil { + log.Fatal(err.Error()) + } + + config, err := clientcmd.BuildConfigFromFlags("", "../kubeconfig") + if err != nil { + log.Fatal(err.Error()) + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + log.Fatal(err.Error()) + } + + _, err = clientset.CoreV1().RESTClient(). + Post(). + AbsPath("/apis/registry.ethos.adobe.com/v1/namespaces/cluster-registry/clusters"). + Resource("clusters"). + Body(data). + DoRaw(context.TODO()) + + if err != nil { + log.Fatal(err) + } + fmt.Printf("Successfully created Cluster %s\n", inputCluster.Spec.Name) + + time.Sleep(10 * time.Second) + + jwt_token := getToken() + bearer := "Bearer " + jwt_token + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:%d/api/v1/clusters/%s", s.apiPort, inputCluster.Spec.Name), nil) + if err != nil { + log.Fatal(err) + } + + req.Header.Add("Authorization", bearer) + client := &http.Client{} + + resp, err := client.Do(req) + + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + + err = json.Unmarshal([]byte(body), &outputCluster) + + s.Assert().Equal(http.StatusOK, resp.StatusCode) + + inputCluster.Spec.APIServer.CertificateAuthorityData = base64.StdEncoding.EncodeToString(config.CAData) + inputCluster.Spec.LastUpdated = outputCluster.LastUpdated + s.Assert().Equal(inputCluster.Spec, outputCluster) + + s.T().Cleanup( + func() { + m := monitoring.NewMetrics("cluster_registry_api_e2e_test", nil, true) + d := database.NewDb(m) + d.DeleteCluster(inputCluster.Spec.Name) + + _, err = clientset.CoreV1().RESTClient(). + Delete(). + AbsPath("/apis/registry.ethos.adobe.com/v1"). + Namespace("cluster-registry"). + Resource("clusters"). + Name(inputCluster.Spec.Name). + DoRaw(context.TODO()) + + if err != nil { + fmt.Printf("Cannot delete Cluster %s\nErr:\n%s", inputCluster.Spec.Name, err.Error()) + } + }) +} + +//fix patch to k8s api +func (s *e2eTestSuite) TBD_Test_EndToEnd_UpdateCluster() { + + var inputCluster registryv1.Cluster + var outputCluster registryv1.ClusterSpec + + input_file := "./testdata/cluster05-prod-useast1-update.json" + data, err := ioutil.ReadFile(input_file) + if err != nil { + log.Fatal(err.Error()) + } + + err = json.Unmarshal([]byte(data), &inputCluster) + if err != nil { + log.Fatal(err.Error()) + } + + config, err := clientcmd.BuildConfigFromFlags("", "../kubeconfig") + if err != nil { + log.Fatal(err.Error()) + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + log.Fatal(err.Error()) + } + + _, err = clientset.CoreV1().RESTClient(). + Patch(types.JSONPatchType). + AbsPath("/apis/registry.ethos.adobe.com/v1"). + Resource("clusters"). + Namespace("cluster-registry"). + Name(inputCluster.Spec.Name). + Body(data). + DoRaw(context.TODO()) + + if err != nil { + fmt.Println(err.Error()) + log.Fatal(err) + } + fmt.Printf("Successfully created Cluster %s\n", inputCluster.ClusterName) + + time.Sleep(10 * time.Second) + + jwt_token := getToken() + bearer := "Bearer " + jwt_token + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:%d/api/v1/clusters/%s", s.apiPort, inputCluster.Spec.Name), nil) + if err != nil { + log.Fatal(err) + } + + req.Header.Add("Authorization", bearer) + client := &http.Client{} + + resp, err := client.Do(req) + + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + + err = json.Unmarshal([]byte(body), &outputCluster) + if err != nil { + log.Fatal(err) + } + + s.Assert().Equal(resp.StatusCode, http.StatusOK) + s.Assert().Equal(inputCluster.Spec.Status, outputCluster.Status) +} diff --git a/test/jwt.go b/test/jwt.go new file mode 100644 index 00000000..dd40f5f2 --- /dev/null +++ b/test/jwt.go @@ -0,0 +1,41 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package e2e + +import ( + "fmt" + "os" + + "github.com/Azure/go-autorest/autorest/azure/auth" +) + +func getToken() string { + resourceID := os.Getenv("OIDC_CLIENT_ID") // Cluster Registry App ID + tenantID := os.Getenv("OIDC_TENANT_ID") // Tenant ID + clientID := os.Getenv("TEST_CLIENT_ID") // Test App ID + clientSecret := os.Getenv("TEST_CLIENT_SECRET") // Test App Secret + + clientCredentials := auth.NewClientCredentialsConfig(clientID, clientSecret, tenantID) + + token, err := clientCredentials.ServicePrincipalToken() + if err != nil { + fmt.Println(err) + } + + err = token.RefreshExchange(resourceID) + if err != nil { + fmt.Println(err) + } + + return token.Token().AccessToken +} diff --git a/test/testdata/alert.json b/test/testdata/alert.json new file mode 100644 index 00000000..94cc12d3 --- /dev/null +++ b/test/testdata/alert.json @@ -0,0 +1,18 @@ +{ + "Receiver": "cluster-registry-webhook-testing", + "Status": "firing", + "Alerts": [{ + "Status": "firing", + "Labels": { + "Alertname": "TestAlertMapping" + }, + "StartsAt": "2019-02-13T06:15:32Z", + "EndsAt": "2100-02-13T06:15:32Z" + }], + "GroupLabels": { + "Alertname": "TestAlertMapping" + }, + "CommonLabels": { + "Alertname": "TestAlertMapping" + } +} diff --git a/test/testdata/cluster05-prod-useast1-update.json b/test/testdata/cluster05-prod-useast1-update.json new file mode 100644 index 00000000..17009e36 --- /dev/null +++ b/test/testdata/cluster05-prod-useast1-update.json @@ -0,0 +1,79 @@ +{ + "apiVersion": "registry.ethos.adobe.com/v1", + "kind": "Cluster", + "metadata": { + "name": "cluster05-prod-useast1", + "namespace": "cluster-registry" + }, + "spec": { + "accountId": "11111-2222-3333-4444-55555555", + "apiServer": { + "endpoint": "https://cluster05-prod-useast1.example.com", + "certificateAuthorityData": "" + }, + "businessUnit": "BU1", + "cloudType": "azure", + "environment": "prod", + "extra": { + "domainName": "example.com", + "egressPorts": "80,443", + "lbEndpoints": { + "internal": "internal.cluster05-prod-useast1.example.com", + "public": "cluster05-prod-useast1.example.com" + } + }, + "k8sInfraRelease": { + "gitSha": "1e8cbd109d7a77909f627ec5247520b70cc209e9", + "lastUpdated": "2021-03-22T11:55:41Z", + "release": "2021-W06-1234" + }, + "name": "cluster05-prod-useast1", + "offering": [ + "PaaS", + "CaaS" + ], + "phase": "Upgrading", + "region": "useast1", + "registeredAt": "2019-02-13T06:15:32Z", + "shortName": "cluster05produseast1", + "status": "Active", + "tiers": [ + { + "containerRuntime": "cri-o", + "enableKataSupport": true, + "instanceType": "Standard_F16s_v2", + "labels": { + "node.kubernetes.io/instance-family": "Fs_v2" + }, + "maxCapacity": 1000, + "minCapacity": 1, + "name": "kata0", + "taints": [ + "node.kubernetes.io/kata-containers=true:NoSchedule" + ] + }, + { + "containerRuntime": "docker", + "instanceType": "Standard_E16s_v3", + "labels": { + "node.kubernetes.io/role": "proxy" + }, + "maxCapacity": 1000, + "minCapacity": 1, + "name": "proxy", + "taints": [ + "node.kubernetes.io/type=proxy:NoSchedule" + ] + } + ], + "type": "Shared", + "virtualNetworks": [ + { + "cidrs": [ + "10.0.0.0/24" + ], + "id": "vnet-1234" + } + ] + } +} diff --git a/test/testdata/cluster05-prod-useast1.json b/test/testdata/cluster05-prod-useast1.json new file mode 100644 index 00000000..d7030568 --- /dev/null +++ b/test/testdata/cluster05-prod-useast1.json @@ -0,0 +1,79 @@ +{ + "apiVersion": "registry.ethos.adobe.com/v1", + "kind": "Cluster", + "metadata": { + "name": "cluster05-prod-useast1", + "namespace": "cluster-registry" + }, + "spec": { + "accountId": "11111-2222-3333-4444-55555555", + "apiServer": { + "endpoint": "https://cluster05-prod-useast1.example.com", + "certificateAuthorityData": "" + }, + "businessUnit": "BU1", + "cloudType": "azure", + "environment": "prod", + "extra": { + "domainName": "example.com", + "egressPorts": "80,443", + "lbEndpoints": { + "internal": "internal.cluster05-prod-useast1.example.com", + "public": "cluster05-prod-useast1.example.com" + } + }, + "k8sInfraRelease": { + "gitSha": "1e8cbd109d7a77909f627ec5247520b70cc209e9", + "lastUpdated": "2021-03-22T11:55:41Z", + "release": "2021-W06-1234" + }, + "name": "cluster05-prod-useast1", + "offering": [ + "PaaS", + "CaaS" + ], + "phase": "Upgrading", + "region": "useast1", + "registeredAt": "2019-02-13T06:15:32Z", + "shortName": "cluster05produseast1", + "status": "Deprecated", + "tiers": [ + { + "containerRuntime": "cri-o", + "enableKataSupport": true, + "instanceType": "Standard_F16s_v2", + "labels": { + "node.kubernetes.io/instance-family": "Fs_v2" + }, + "maxCapacity": 1000, + "minCapacity": 1, + "name": "kata0", + "taints": [ + "node.kubernetes.io/kata-containers=true:NoSchedule" + ] + }, + { + "containerRuntime": "docker", + "instanceType": "Standard_E16s_v3", + "labels": { + "node.kubernetes.io/role": "proxy" + }, + "maxCapacity": 1000, + "minCapacity": 1, + "name": "proxy", + "taints": [ + "node.kubernetes.io/type=proxy:NoSchedule" + ] + } + ], + "type": "Shared", + "virtualNetworks": [ + { + "cidrs": [ + "10.0.0.0/24" + ], + "id": "vnet-1234" + } + ] + } +} From 802bf5192fc0e2b6ce0e6a087f120381f4e6d0d5 Mon Sep 17 00:00:00 2001 From: aalexand Date: Tue, 14 Dec 2021 17:26:30 +0200 Subject: [PATCH 2/2] bumped github workflows golang-version to 1.16 --- .github/env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/env b/.github/env index 5c8b9a85..6046a169 100644 --- a/.github/env +++ b/.github/env @@ -1 +1 @@ -golang-version=1.15 \ No newline at end of file +golang-version=1.16 \ No newline at end of file