From 308fa19b68670756957a3514e3101e560184a206 Mon Sep 17 00:00:00 2001 From: Ageev Pavel Date: Fri, 18 Dec 2020 01:28:41 +0300 Subject: [PATCH] Initial commit --- .github/CODEOWNERS | 2 + .github/ISSUE_TEMPLATE/bug_report.md | 35 ++ .github/ISSUE_TEMPLATE/feature_request.md | 24 + .github/pull_request_template.md | 20 + .github/workflows/main.yml | 181 ++++++++ .gitignore | 5 + .golangci.yml | 6 + AUTHORS | 11 + CONTRIBUTORS | 9 + LICENSE | 202 +++++++++ Makefile | 123 ++++++ README.md | 40 ++ bin/.gitignore | 2 + build/docker/cmd/pinchy/Dockerfile | 7 + build/docker/utils/wire/Dockerfile | 6 + cmd/pinchy/internal/command.go | 76 ++++ cmd/pinchy/internal/modules.go | 8 + cmd/pinchy/internal/provider.go | 77 ++++ cmd/pinchy/internal/wire.go | 42 ++ cmd/pinchy/main.go | 23 + configs/source/file/example.yaml | 9 + .../docker-compose/consul/docker-compose.yml | 29 ++ docs/README.md | 7 + docs/contributing-guide.md | 22 + docs/registry/consul.md | 7 + docs/source/file.md | 20 + docs/user-guide.md | 64 +++ go.mod | 25 ++ go.sum | 414 ++++++++++++++++++ internal/extension/extension.go | 21 + internal/extension/extension_test.go | 58 +++ .../extension/registry/consul/provider.go | 63 +++ internal/extension/registry/consul/wire.go | 23 + internal/extension/registry/extension.go | 97 ++++ internal/extension/registry/extension_test.go | 332 ++++++++++++++ internal/extension/source/extension.go | 97 ++++ internal/extension/source/extension_test.go | 332 ++++++++++++++ internal/extension/source/file/provider.go | 41 ++ internal/extension/source/file/wire.go | 22 + pkg/core/logger.go | 35 ++ pkg/core/manager.go | 123 ++++++ pkg/core/manager_test.go | 282 ++++++++++++ pkg/core/registry.go | 15 + pkg/core/registry/consul/registry.go | 87 ++++ pkg/core/registry/consul/registry_test.go | 221 ++++++++++ pkg/core/scheduler.go | 37 ++ pkg/core/scheduler_test.go | 98 +++++ pkg/core/service.go | 53 +++ pkg/core/service_test.go | 128 ++++++ pkg/core/source.go | 13 + pkg/core/source/file/source.go | 55 +++ pkg/core/source/file/source_test.go | 126 ++++++ test/.gitignore | 2 + 53 files changed, 3857 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/main.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 AUTHORS create mode 100644 CONTRIBUTORS create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 bin/.gitignore create mode 100644 build/docker/cmd/pinchy/Dockerfile create mode 100644 build/docker/utils/wire/Dockerfile create mode 100644 cmd/pinchy/internal/command.go create mode 100644 cmd/pinchy/internal/modules.go create mode 100644 cmd/pinchy/internal/provider.go create mode 100644 cmd/pinchy/internal/wire.go create mode 100644 cmd/pinchy/main.go create mode 100644 configs/source/file/example.yaml create mode 100644 deploy/docker-compose/consul/docker-compose.yml create mode 100644 docs/README.md create mode 100644 docs/contributing-guide.md create mode 100644 docs/registry/consul.md create mode 100644 docs/source/file.md create mode 100644 docs/user-guide.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/extension/extension.go create mode 100644 internal/extension/extension_test.go create mode 100644 internal/extension/registry/consul/provider.go create mode 100644 internal/extension/registry/consul/wire.go create mode 100644 internal/extension/registry/extension.go create mode 100644 internal/extension/registry/extension_test.go create mode 100644 internal/extension/source/extension.go create mode 100644 internal/extension/source/extension_test.go create mode 100644 internal/extension/source/file/provider.go create mode 100644 internal/extension/source/file/wire.go create mode 100644 pkg/core/logger.go create mode 100644 pkg/core/manager.go create mode 100644 pkg/core/manager_test.go create mode 100644 pkg/core/registry.go create mode 100644 pkg/core/registry/consul/registry.go create mode 100644 pkg/core/registry/consul/registry_test.go create mode 100644 pkg/core/scheduler.go create mode 100644 pkg/core/scheduler_test.go create mode 100644 pkg/core/service.go create mode 100644 pkg/core/service_test.go create mode 100644 pkg/core/source.go create mode 100644 pkg/core/source/file/source.go create mode 100644 pkg/core/source/file/source_test.go create mode 100644 test/.gitignore diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..89883f5 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +* @insidieux +* @Sigthror diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..fcc7252 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: insidieux, Sigthror + +--- + +**Describe the bug** + +A clear and concise description of what the bug is. + +**To Reproduce** + +Steps to reproduce the behavior: + - Run binary with flags '...' + - ... + - See error + +**Expected behavior** + +A clear and concise description of what you expected to happen. + +**Environment:** + + - Version: [e.g. v1.0.0] + - GOOS: [e.g. linux, darwin] + +**Additional context** + + - Logs + - Stacktrace + +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..73db2e3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,24 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: insidieux, Sigthror + +--- + +**Is your feature request related to a problem? Please describe.** + +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** + +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** + +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** + +Add any other context or information about the feature request here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..7285bc4 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,20 @@ +**Description** + +Please include a summary of the change and which issue was fixed. +Please also include relevant motivation and context. + +Fixes # (issue) + +**Type of change** + +Please delete options that are not relevant. + +- [ ] 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 not work as expected) +- [ ] This change requires a documentation update + +**Checklist** +- [ ] This PR contains documentation +- [ ] This PR contains tests +- [ ] This PR has been tested for backwards compatibility diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..c528a19 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,181 @@ +name: CI + +on: + push: + branches: + - '*' + tags: + - 'v*' + paths-ignore: + - '**.md' + pull_request: + paths-ignore: + - '**.md' + +jobs: + test: + name: Test + runs-on: ubuntu-18.04 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Cache vendor + id: cache-vendor + uses: actions/cache@v2 + with: + path: vendor + key: ${{ github.sha }} + - name: Vendoring + if: steps.cache-vendor.outputs.cache-hit != 'true' + run: make vendor + - name: Generate wire injectors + run: make wire + - name: Lint + run: make lint + - name: Run tests + run: make test + - name: Send code coverage report + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: test/coverage.out + + binary: + name: Build binary + runs-on: ubuntu-18.04 + needs: test + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + strategy: + matrix: + goos: [ darwin, linux, windows ] + goarch: [ amd64 ] + max-parallel: 3 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Cache vendor + id: cache-vendor + uses: actions/cache@v2 + with: + path: vendor + key: ${{ github.sha }} + - name: Vendoring + if: steps.cache-vendor.outputs.cache-hit != 'true' + run: make vendor + - name: Generate wire injectors + run: make wire + - name: Get tag reference + id: get-tag-reference + uses: ankitvgupta/ref-to-tag-action@master + with: + ref: ${{ github.ref }} + head_ref: ${{ github.head_ref }} + - name: Build + run: make build GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} BUIlD_VERSION="${{ steps.get-tag-reference.outputs.tag }}" + - name: Upload compiled binary + uses: actions/upload-artifact@v2 + with: + name: pinchy-${{ matrix.goos }}-${{ matrix.goarch }} + path: bin/pinchy + + image: + name: Push docker image + runs-on: ubuntu-18.04 + needs: binary + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + strategy: + matrix: + goos: [ linux ] + goarch: [ amd64 ] + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Download compiled binary + uses: actions/download-artifact@v2 + with: + name: pinchy-${{ matrix.goos }}-${{ matrix.goarch }} + path: bin + - name: Get tag reference + id: get-tag-reference + uses: ankitvgupta/ref-to-tag-action@master + with: + ref: ${{ github.ref }} + head_ref: ${{ github.head_ref }} + - name: Build docker image for github registry + run: | + make docker-image-build DOCKER_TAG=${{ steps.get-tag-reference.outputs.tag }} + - name: Push docker image to github registry + run: | + make docker-image-push \ + DOCKER_USER=${{ github.actor }} \ + DOCKER_PASSWORD=${{ secrets.GITHUB_TOKEN }} \ + DOCKER_TAG=${{ steps.get-tag-reference.outputs.tag }} + - name: Build docker image for github registry as latest + run: | + make docker-image-build DOCKER_TAG="latest" + - name: Push docker image to github registry as latest + run: | + make docker-image-push \ + DOCKER_USER=${{ github.actor }} \ + DOCKER_PASSWORD=${{ secrets.GITHUB_TOKEN }} \ + DOCKER_TAG="latest" + - name: Build docker image for docker hub registry + run: | + make docker-image-build \ + DOCKER_IMAGE=${{ github.repository }} \ + DOCKER_TAG=${{ steps.get-tag-reference.outputs.tag }} + - name: Push docker image to docker hub registry + run: | + make docker-image-push \ + DOCKER_REGISTRY=docker.io \ + DOCKER_IMAGE=${{ github.repository }} \ + DOCKER_USER=${{ secrets.DOCKER_HUB_USERNAME }} \ + DOCKER_PASSWORD=${{ secrets.DOCKER_HUB_PASSWORD }} \ + DOCKER_TAG=${{ steps.get-tag-reference.outputs.tag }} + - name: Build docker image for docker hub registry as latest + run: | + make docker-image-build \ + DOCKER_IMAGE=${{ github.repository }} \ + DOCKER_TAG="latest" + - name: Push docker image to docker hub registry as latest + run: | + make docker-image-push \ + DOCKER_REGISTRY=docker.io \ + DOCKER_IMAGE=${{ github.repository }} \ + DOCKER_USER=${{ secrets.DOCKER_HUB_USERNAME }} \ + DOCKER_PASSWORD=${{ secrets.DOCKER_HUB_PASSWORD }} \ + DOCKER_TAG="latest" + release: + name: Upload release asset + runs-on: ubuntu-18.04 + needs: binary + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + strategy: + matrix: + goos: [ darwin, linux, windows ] + goarch: [ amd64 ] + max-parallel: 3 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Download compiled binary + uses: actions/download-artifact@v2 + with: + name: pinchy-${{ matrix.goos }}-${{ matrix.goarch }} + path: bin + - name: Tar artifact + run: tar -zcvf pinchy-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz -C bin pinchy + - name: Get tag reference + id: get-tag-reference + uses: ankitvgupta/ref-to-tag-action@master + with: + ref: ${{ github.ref }} + head_ref: ${{ github.head_ref }} + - name: Upload binaries to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: pinchy-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz + asset_name: pinchy-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz + tag: ${{ steps.get-tag-reference.outputs.tag }} + release_name: Pinchy ${{ steps.get-tag-reference.outputs.tag }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b22e762 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.idea +/vendor + +mock_*_test.go +wire_gen.go diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..171816b --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,6 @@ +run: + timeout: 5m + tests: false + skip-dirs: + - vendor + modules-download-mode: vendor diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..d76c613 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,11 @@ +# This is the official list of Pinchy authors for copyright purposes. +# This file is distinct from the CONTRIBUTORS files. +# See the latter for an explanation. + +# Names should be added to this file as +# Individual's name + +# Please keep the list sorted. +Ageev Pavel +Artem Mezentsev + diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000..f1b2779 --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,9 @@ +# This is the official list of people who can contribute +# (and typically have contributed) code to the Pinchy repository. +# The AUTHORS file lists the copyright holders; this file +# lists people. + +# Names should be added to this file like so: +# Individual's name + +# Please keep the list sorted. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..04bf72d --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + 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 2020 Pavel Ageev + + 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1f05388 --- /dev/null +++ b/Makefile @@ -0,0 +1,123 @@ +override APP_NAME=pinchy +override GO_VERSION=1.15 +override DOCKER_BUILDKIT=1 + +GOOS?=$(shell go env GOOS || echo linux) +GOARCH?=$(shell go env GOARCH || echo amd64) +CGO_ENABLED?=0 + +BUIlD_VERSION?=latest + +DOCKER_REGISTRY?=docker.pkg.github.com +DOCKER_IMAGE?=${DOCKER_REGISTRY}/insidieux/pinchy/${APP_NAME} +DOCKER_USER= +DOCKER_PASSWORD= +DOCKER_TAG?=latest + +ifeq (, $(shell which docker)) +$(error "Binary docker not found in $(PATH)") +endif + +.PHONY: all +all: cleanup vendor wire lint test build + +.PHONY: cleanup +cleanup: + @rm ${PWD}/bin/${APP_NAME}* || true + @rm ${PWD}/tests/coverage.out || true + @find ${PWD} -type f -name "wire_gen.go" -delete + @find ${PWD} -type f -name "mock_*_test.go" -delete + @rm -r ${PWD}/vendor || true + +.PHONY: vendor +vendor: + @rm -r ${PWD}/vendor || true + @docker run --rm -v ${PWD}:/project -w /project golang:${GO_VERSION} go mod tidy + @docker run --rm -v ${PWD}:/project -w /project golang:${GO_VERSION} go mod vendor + +.PHONY: wire +wire: + @docker build \ + --build-arg GO_VERSION=${GO_VERSION} \ + -f ${PWD}/build/docker/utils/wire/Dockerfile \ + -t wire:custom \ + build/docker/utils/wire + @find ${PWD} -type f -name "wire_gen.go" -delete + @docker run --rm \ + -v ${PWD}:/project \ + -w /project \ + wire:custom \ + /project/... + +.PHONY: lint +lint: + @docker run --rm \ + -v ${PWD}:/project \ + -w /project \ + golangci/golangci-lint:v1.33.0 \ + golangci-lint run -v + +.PHONY: test +test: + @rm -r ${PWD}/test/coverage.out || true + @docker run --rm \ + -v ${PWD}:/project \ + -w /project \ + golang:${GO_VERSION} \ + go test \ + -race \ + -mod vendor \ + -covermode=atomic \ + -coverprofile=/project/test/coverage.out \ + /project/... + +.PHONY: build +build: + @rm ${PWD}/bin/${APP_NAME} || true + @docker run --rm \ + -v ${PWD}:/project \ + -w /project \ + -e GOOS=${GOOS} \ + -e GOARCH=${GOARCH} \ + -e CGO_ENABLED=${CGO_ENABLED} \ + -e GO111MODULE=on \ + golang:${GO_VERSION} \ + go build \ + -mod vendor \ + -ldflags "-X main.version=${BUIlD_VERSION}" \ + -o /project/bin/${APP_NAME} \ + -v /project/cmd/${APP_NAME} + +.PHONY: docker-image-build +docker-image-build: + @docker rmi ${DOCKER_IMAGE}:${DOCKER_TAG} || true + @docker build \ + -f ${PWD}/build/docker/cmd/pinchy/Dockerfile \ + -t ${DOCKER_IMAGE}:${DOCKER_TAG} \ + . + +.PHONY: docker-image-push +docker-image-push: + @docker login -u ${DOCKER_USER} -p ${DOCKER_PASSWORD} ${DOCKER_REGISTRY} + @docker push ${DOCKER_IMAGE}:${DOCKER_TAG} + + +.PHONY: mockery +mockery: +ifndef MOCKERY_SOURCE_DIR + $(error MOCKERY_SOURCE_DIR is not set) +endif + @docker pull vektra/mockery:v2.4.0 + @find ${PWD} -type f -name "mock_*_test.go" -delete + @docker run \ + --rm \ + -v ${PWD}:/project \ + -w /project \ + vektra/mockery:v2.4.0 \ + --testonly \ + --inpackage \ + --all \ + --dir /project/${MOCKERY_SOURCE_DIR} \ + --output /project/${MOCKERY_SOURCE_DIR} \ + --case snake \ + --log-level trace diff --git a/README.md b/README.md new file mode 100644 index 0000000..98f61fc --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# Pinchy + +Service discovering and registry bridge. + +[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/insidieux/pinchy/CI?style=flat-square)](https://github.com/insidieux/pinchy/actions?query=workflow%3ACI) +[![Go Report Card](https://goreportcard.com/badge/github.com/insidieux/pinchy?style=flat-square)](https://goreportcard.com/report/github.com/insidieux/pinchy) +[![codecov](https://codecov.io/gh/insidieux/pinchy/branch/master/graph/badge.svg?token=BI6HEMPLB1)](https://codecov.io/gh/insidieux/pinchy/branch/master) +![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/insidieux/pinchy) + +Pinchy is a simple binary, which allows to automatically fetch services info from `Source` and register/remove them to/from `Registry`. + +Supported pluggable service sources: +- [YAML File](https://ru.wikipedia.org/wiki/YAML) + +Supported pluggable service registries: +- [Consul](http://www.consul.io/) + +## Installing + +Install Pinchy by running: + +```shell +go get github.com/insidiuex/pinchy/cmd/pinchy +``` + +Ensure that `$GOPATH/bin` is added to your `$PATH`. + +## Documentation + +- [User guide][] +- [Contributing guide][] + +[User guide]: ./docs/user-guide.md +[Contributing guide]: ./docs/contributing-guide.md + +## License + +[Apache][] + +[Apache]: ./LICENSE \ No newline at end of file diff --git a/bin/.gitignore b/bin/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/bin/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/build/docker/cmd/pinchy/Dockerfile b/build/docker/cmd/pinchy/Dockerfile new file mode 100644 index 0000000..ae8eb1b --- /dev/null +++ b/build/docker/cmd/pinchy/Dockerfile @@ -0,0 +1,7 @@ +FROM alpine:3.12 + +COPY bin/pinchy /usr/local/bin/pinchy + +RUN chmod +x /usr/local/bin/pinchy + +ENTRYPOINT ["pinchy"] diff --git a/build/docker/utils/wire/Dockerfile b/build/docker/utils/wire/Dockerfile new file mode 100644 index 0000000..f0488ca --- /dev/null +++ b/build/docker/utils/wire/Dockerfile @@ -0,0 +1,6 @@ +ARG GO_VERSION=1.15 +FROM golang:${GO_VERSION} + +RUN go get -u -t github.com/google/wire/cmd/wire + +ENTRYPOINT ["/go/bin/wire"] diff --git a/cmd/pinchy/internal/command.go b/cmd/pinchy/internal/command.go new file mode 100644 index 0000000..7d35a55 --- /dev/null +++ b/cmd/pinchy/internal/command.go @@ -0,0 +1,76 @@ +package internal + +import ( + "fmt" + "time" + + "github.com/insidieux/pinchy/internal/extension/registry" + "github.com/insidieux/pinchy/internal/extension/source" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +const ( + name = `pinchy` +) + +func NewCommand(version string) *cobra.Command { + rootCommand := &cobra.Command{ + Use: name, + Version: version, + } + rootCommand.SetOut(logrus.New().Out) + + for _, sourceProvider := range source.GetProviderList() { + sourceCmd := &cobra.Command{ + Use: sourceProvider.Name(), + Short: fmt.Sprintf(`Fetch data from source "%s"`, sourceProvider.Name()), + } + for _, registryProvider := range registry.GetProviderList() { + registryCmd := &cobra.Command{ + Use: registryProvider.Name(), + Short: fmt.Sprintf(`Save data in registry "%s"`, registryProvider.Name()), + } + onceCommand := &cobra.Command{ + Use: `once`, + Short: `Run main process only once: sync and return result`, + RunE: func(cmd *cobra.Command, args []string) error { + manager, cleanup, err := newManager(cmd.Flags(), sourceProvider.Factory(), registryProvider.Factory()) + if cleanup != nil { + cleanup() + } + if err != nil { + return errors.Wrap(err, `failed to bootstrap manager`) + } + return manager.Run(cmd.Context()) + }, + } + watchCommand := &cobra.Command{ + Use: `watch`, + Short: `Run main process as daemon: sync repeatedly with constant interval`, + RunE: func(cmd *cobra.Command, args []string) error { + sc, cleanup, err := newScheduler(cmd.Flags(), sourceProvider.Factory(), registryProvider.Factory()) + if cleanup != nil { + cleanup() + } + if err != nil { + return errors.Wrap(err, `failed to bootstrap scheduler`) + } + sc.Run(cmd.Context()) + return nil + }, + } + watchCommand.Flags().Duration(`scheduler.interval`, time.Minute, `Interval between manager runs (1s, 1m, 5m, 1h and others)`) + registryCmd.PersistentFlags().Bool(`manager.continue-on-error`, false, `Omit errors during process manager`) + registryCmd.PersistentFlags().AddFlagSet(registryProvider.Flags()) + registryCmd.AddCommand(onceCommand) + registryCmd.AddCommand(watchCommand) + sourceCmd.AddCommand(registryCmd) + } + sourceCmd.PersistentFlags().AddFlagSet(sourceProvider.Flags()) + rootCommand.AddCommand(sourceCmd) + } + rootCommand.PersistentFlags().String(`logger.level`, logrus.InfoLevel.String(), `Log level`) + return rootCommand +} diff --git a/cmd/pinchy/internal/modules.go b/cmd/pinchy/internal/modules.go new file mode 100644 index 0000000..2926435 --- /dev/null +++ b/cmd/pinchy/internal/modules.go @@ -0,0 +1,8 @@ +package internal + +import ( + // List of imports for registry extensions + _ "github.com/insidieux/pinchy/internal/extension/registry/consul" + // List of imports for source extensions + _ "github.com/insidieux/pinchy/internal/extension/source/file" +) diff --git a/cmd/pinchy/internal/provider.go b/cmd/pinchy/internal/provider.go new file mode 100644 index 0000000..e9e22e7 --- /dev/null +++ b/cmd/pinchy/internal/provider.go @@ -0,0 +1,77 @@ +package internal + +import ( + "strings" + "time" + + "github.com/insidieux/pinchy/internal/extension/registry" + "github.com/insidieux/pinchy/internal/extension/source" + "github.com/insidieux/pinchy/pkg/core" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +// Provider for viper.Viper bound to current command pflag.FlagSet +func provideViper(set *pflag.FlagSet) (*viper.Viper, error) { + v := viper.New() + v.SetEnvPrefix(strings.ToUpper(name)) + v.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_")) + v.AutomaticEnv() + if err := v.BindPFlags(set); err != nil { + return nil, errors.Wrap(err, `failed to bind command line arguments`) + } + return v, nil +} + +// Provider for logrus.Level +func provideLoggerLevel(commandViper *viper.Viper) (logrus.Level, error) { + level := commandViper.GetString(`logger.level`) + if level == `` { + level = logrus.InfoLevel.String() + } + return logrus.ParseLevel(level) +} + +// Provider for logrus.FieldLogger +func provideLogger(level logrus.Level) core.LoggerInterface { + logger := logrus.New() + logger.SetFormatter(&logrus.TextFormatter{}) + logger.SetLevel(level) + return logger +} + +// Provider for core.Registry +func provideRegistry(commandViper *viper.Viper, factory registry.Factory, logger core.LoggerInterface) (core.Registry, func(), error) { + r, cleanup, err := factory(commandViper) + if r != nil { + lr, ok := r.(core.Loggable) + if ok { + lr.WithLogger(logger) + } + } + return r, cleanup, err +} + +// Provider for core.Source +func provideSource(commandViper *viper.Viper, factory source.Factory, logger core.LoggerInterface) (core.Source, func(), error) { + s, cleanup, err := factory(commandViper) + if s != nil { + ls, ok := s.(core.Loggable) + if ok { + ls.WithLogger(logger) + } + } + return s, cleanup, err +} + +// Provider for core.Source +func provideManagerExitOnError(commandViper *viper.Viper) core.ManagerExitOnError { + return core.ManagerExitOnError(commandViper.GetBool(`manager.continue-on-error`)) +} + +// Provider for time.Ticker +func provideTicker(commandViper *viper.Viper) *time.Ticker { + return time.NewTicker(commandViper.GetDuration(`scheduler.interval`)) +} diff --git a/cmd/pinchy/internal/wire.go b/cmd/pinchy/internal/wire.go new file mode 100644 index 0000000..a4aeb24 --- /dev/null +++ b/cmd/pinchy/internal/wire.go @@ -0,0 +1,42 @@ +// +build wireinject + +package internal + +import ( + "github.com/google/wire" + "github.com/insidieux/pinchy/internal/extension/registry" + "github.com/insidieux/pinchy/internal/extension/source" + "github.com/insidieux/pinchy/pkg/core" + "github.com/spf13/pflag" +) + +var ( + managerWireSet = wire.NewSet( + provideViper, + wire.NewSet( + provideLoggerLevel, + provideLogger, + ), + provideRegistry, + provideSource, + provideManagerExitOnError, + core.NewManager, + ) + schedulerWireSet = wire.NewSet( + provideTicker, + managerWireSet, + core.NewScheduler, + ) +) + +func newManager(_ *pflag.FlagSet, _ source.Factory, _ registry.Factory) (core.ManagerInterface, func(), error) { + panic(wire.Build( + managerWireSet, + )) +} + +func newScheduler(_ *pflag.FlagSet, _ source.Factory, _ registry.Factory) (*core.Scheduler, func(), error) { + panic(wire.Build( + schedulerWireSet, + )) +} diff --git a/cmd/pinchy/main.go b/cmd/pinchy/main.go new file mode 100644 index 0000000..6d8a53e --- /dev/null +++ b/cmd/pinchy/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "log" + "syscall" + + "github.com/insidieux/pinchy/cmd/pinchy/internal" + "github.com/sethvargo/go-signalcontext" +) + +var ( + version string +) + +func main() { + ctx, cancel := signalcontext.On(syscall.SIGINT, syscall.SIGTERM) + if cancel != nil { + defer cancel() + } + if err := internal.NewCommand(version).ExecuteContext(ctx); err != nil { + log.Fatalf(`Failed to execute command: %s`, err.Error()) + } +} diff --git a/configs/source/file/example.yaml b/configs/source/file/example.yaml new file mode 100644 index 0000000..04dbd31 --- /dev/null +++ b/configs/source/file/example.yaml @@ -0,0 +1,9 @@ +- name: service-name + address: 127.0.0.1 + id: service-id + port: 80 + tags: + - tag-1 + - tag-2 + meta: + key: value diff --git a/deploy/docker-compose/consul/docker-compose.yml b/deploy/docker-compose/consul/docker-compose.yml new file mode 100644 index 0000000..6dec514 --- /dev/null +++ b/deploy/docker-compose/consul/docker-compose.yml @@ -0,0 +1,29 @@ +version: '3' + +services: + + consul-agent-1: &consul-agent + image: consul:latest + command: "agent -retry-join consul-server-bootstrap -client 0.0.0.0" + + consul-agent-2: + <<: *consul-agent + + consul-agent-3: + <<: *consul-agent + + consul-server-1: &consul-server + <<: *consul-agent + command: "agent -server -retry-join consul-server-bootstrap -client 0.0.0.0" + + consul-server-2: + <<: *consul-server + + consul-server-bootstrap: + <<: *consul-agent + ports: + - "8400:8400" + - "8500:8500" + - "8600:8600" + - "8600:8600/udp" + command: "agent -server -bootstrap-expect 3 -ui -client 0.0.0.0" diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..d51f878 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,7 @@ +# Pinchy documentation + +- [User guide][] +- [Contributing guide][] + +[User guide]: ./user-guide.md +[Contributing guide]: ./contributing-guide.md diff --git a/docs/contributing-guide.md b/docs/contributing-guide.md new file mode 100644 index 0000000..1dd79af --- /dev/null +++ b/docs/contributing-guide.md @@ -0,0 +1,22 @@ +# Contributing guide + +- Create an issue +- Fork project +- Create your feature branch (git checkout -b issue-id) +- Commit your changes (git commit -am 'Add some feature') +- Push to the branch (git push origin issue-id) +- Create new Pull Request + +## Requirements + +- Every pull request code **must be** covered by tests +- Every new source/registry **must be** documented at /docs + +## Adding new source/provider + +- Add new sub directory to `/pkg/core/{source,registry}` +- Put your implementation of interface +- Add new sub directory to `/internal/extension/{source,registry}` +- Put wiring code for dynamic load implementations: + - Register implementation of `ProviderInterface` at `init` function + - Add anonymous import to `/cmd/pinchy/modules.go` diff --git a/docs/registry/consul.md b/docs/registry/consul.md new file mode 100644 index 0000000..070c159 --- /dev/null +++ b/docs/registry/consul.md @@ -0,0 +1,7 @@ +# Pinchy registry "Consul" + +## Available flags + +``` +--registry.address %full-http-url-to-consul-api% +``` diff --git a/docs/source/file.md b/docs/source/file.md new file mode 100644 index 0000000..21c48b8 --- /dev/null +++ b/docs/source/file.md @@ -0,0 +1,20 @@ +# Pinchy source "File" + +## Available flags + +``` +--source.path %full-path-to-yml-file% +``` + +## config.yml example + +```yaml +- name: service-name + id: service-id + port: 80 + tags: + - tag-1 + - tag-2 + meta: + key: value +``` diff --git a/docs/user-guide.md b/docs/user-guide.md new file mode 100644 index 0000000..a3750f1 --- /dev/null +++ b/docs/user-guide.md @@ -0,0 +1,64 @@ +# User guide + +## Installing + +### Github Release + +Visit the [releases page](https://github.com/insidieux/pinchy/releases/latest) to download one of the pre-built binaries for your platform. + +### Docker + +Use the [Docker image](https://hub.docker.com/repository/docker/insidieux/pinchy) + +```shell +docker pull insidieux/pinchy +``` + +or + +```shell +echo PASSWORD_FILE | docker login docker.pkg.github.com --username USERNAME --password-stdin +docker pull docker.pkg.github.com/insidieux/pinchy +``` + +### go get + +Alternatively, you can use the go get method: + +```shell +go get github.com/insidiuex/pinchy/cmd/pinchy +``` + +Ensure that `$GOPATH/bin` is added to your `$PATH`. + +## Usage + +### Binary + +```shell +pinchy ... +``` + +### Docker + +```shell +docker run insidieux/pinchy +``` + +or + +```shell +docker run docker.pkg.github.com/insidieux/pinchy/pinchy +``` + +### Available source types + +- [file] + +[file]: ./source/file.md + +### Available registry types + +- [consul] + +[consul]: ./registry/consul.md diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..924e056 --- /dev/null +++ b/go.mod @@ -0,0 +1,25 @@ +module github.com/insidieux/pinchy + +go 1.15 + +require ( + github.com/agrea/ptr v0.0.0-20180711073057-77a518d99b7b + github.com/go-playground/validator/v10 v10.4.0 + github.com/google/wire v0.4.0 + github.com/hashicorp/consul/api v1.7.0 + github.com/hashicorp/go-cleanhttp v0.5.1 + github.com/pkg/errors v0.9.1 + github.com/sethvargo/go-signalcontext v0.1.0 + github.com/sirupsen/logrus v1.2.0 + github.com/spf13/afero v1.2.2 + github.com/spf13/cast v1.3.0 + github.com/spf13/cobra v1.1.1 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.7.1 + github.com/stretchr/testify v1.6.1 + github.com/thoas/go-funk v0.7.0 + golang.org/x/net v0.0.0-20200519113804-d87ec0cfa476 // indirect + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect + gopkg.in/yaml.v2 v2.3.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..542338d --- /dev/null +++ b/go.sum @@ -0,0 +1,414 @@ +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.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/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +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/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +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/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/agrea/ptr v0.0.0-20180711073057-77a518d99b7b h1:WMhlIaJkDgEQSVJQM06YV+cYUl1r5OY5//ijMXJNqtA= +github.com/agrea/ptr v0.0.0-20180711073057-77a518d99b7b/go.mod h1:Tie46d3UWzXpj+Fh9+DQTyaUxEpFBPOLXrnx7nxlKRo= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +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 h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= +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/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +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/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/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +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-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +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/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/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +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-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +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-playground/validator/v10 v10.4.0 h1:72qIR/m8ybvL8L5TIyfgrigqkrw7kVYAvjEvpT85l70= +github.com/go-playground/validator/v10 v10.4.0/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/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/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 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= +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/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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TUH7k= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/wire v0.4.0 h1:kXcsA/rIGzJImVqPdhfnr6q0xsS9gU0515q1EPpJ9fE= +github.com/google/wire v0.4.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= +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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/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/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/api v1.7.0 h1:tGs8Oep67r8CcA2Ycmb/8BLBcJ70St44mF2X10a/qPg= +github.com/hashicorp/consul/api v1.7.0/go.mod h1:1NSuaUUkFaJzMasbfq/11wKYWSR67Xn6r2DXKhuDNFg= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/sdk v0.6.0 h1:FfhMEkwvQl57CildXJyGHnwGGM4HMODGyfjGwNM1Vdw= +github.com/hashicorp/consul/sdk v0.6.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.12.0 h1:d4QkX8FRTYaKaCZBoXYY8zJX2BXjWxurN/GA2tkrmZM= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= +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-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs= +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 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= +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 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +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/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/memberlist v0.2.2 h1:5+RffWKwqJ71YPu9mWsF7ZOscZmwfasdA8kbdC7AO2g= +github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/serf v0.9.3 h1:AVF6JDQQens6nMHT9OGERBvK0f8rPrAGILnsKLr6lzM= +github.com/hashicorp/serf v0.9.3/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +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/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/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 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +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/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +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/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= +github.com/mattn/go-colorable v0.1.6/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.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +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 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= +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 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +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/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +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 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sethvargo/go-signalcontext v0.1.0 h1:3IU7HOlmRXF0PSDf85C4nJ/zjYDjF+DS+LufcKfLvyk= +github.com/sethvargo/go-signalcontext v0.1.0/go.mod h1:PXu9UmR2f7mmp8kEwgkKmaDbxq/PbqixkiC66WIkkWE= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +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 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +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/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= +github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/thoas/go-funk v0.7.0 h1:GmirKrs6j6zJbhJIficOsz2aAI7700KsU/5YrdHRM1Y= +github.com/thoas/go-funk v0.7.0/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +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-20190308221718-c2843e01d9a2/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-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +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/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/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/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-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-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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200519113804-d87ec0cfa476 h1:E7ct1C6/33eOdrGZKMoyntcEvs2dwZnDe30crG5vpYU= +golang.org/x/net v0.0.0-20200519113804-d87ec0cfa476/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +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/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 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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-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-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-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-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +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/tools v0.0.0-20180221164845-07fd8470d635/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-20190422233926-fe54fb35175b/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-20190606124116-d0a3d012864b/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-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-20190907020128-2ca718005c18/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-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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/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/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-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/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= +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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +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.4/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 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/internal/extension/extension.go b/internal/extension/extension.go new file mode 100644 index 0000000..18e7a55 --- /dev/null +++ b/internal/extension/extension.go @@ -0,0 +1,21 @@ +package extension + +import ( + "strings" +) + +type ( + RegisterError []error +) + +func (re RegisterError) String() string { + var slice []string + for _, err := range re { + slice = append(slice, err.Error()) + } + return strings.Join(slice, `; `) +} + +func (re RegisterError) Error() string { + return re.String() +} diff --git a/internal/extension/extension_test.go b/internal/extension/extension_test.go new file mode 100644 index 0000000..5b01233 --- /dev/null +++ b/internal/extension/extension_test.go @@ -0,0 +1,58 @@ +package extension + +import ( + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/suite" +) + +// --- Tests --- + +func TestRegisterError_String(t *testing.T) { + suite.Run(t, new(registerErrorStringTestSuite)) +} + +func TestRegisterError_Error(t *testing.T) { + suite.Run(t, new(registerErrorErrorTestSuite)) +} + +// --- Suites --- + +type registerErrorStringTestSuite struct { + suite.Suite + err RegisterError +} + +func (s *registerErrorStringTestSuite) SetupTest() { + s.err = RegisterError{} +} + +func (s *registerErrorStringTestSuite) TestEmptyError() { + s.Equal(``, s.err.String()) +} + +func (s *registerErrorStringTestSuite) TestNonEmptyError() { + s.err = append(s.err, errors.New(`expected error 1`)) + s.err = append(s.err, errors.New(`expected error 2`)) + s.Equal(`expected error 1; expected error 2`, s.err.String()) +} + +type registerErrorErrorTestSuite struct { + suite.Suite + err RegisterError +} + +func (s *registerErrorErrorTestSuite) SetupTest() { + s.err = RegisterError{} +} + +func (s *registerErrorErrorTestSuite) TestEmptyError() { + s.Equal(``, s.err.String()) +} + +func (s *registerErrorErrorTestSuite) TestNonEmptyError() { + s.err = append(s.err, errors.New(`expected error 1`)) + s.err = append(s.err, errors.New(`expected error 2`)) + s.Equal(`expected error 1; expected error 2`, s.err.Error()) +} diff --git a/internal/extension/registry/consul/provider.go b/internal/extension/registry/consul/provider.go new file mode 100644 index 0000000..df6f6b0 --- /dev/null +++ b/internal/extension/registry/consul/provider.go @@ -0,0 +1,63 @@ +package consul + +import ( + "net/http" + + pkgConsul "github.com/insidieux/pinchy/pkg/core/registry/consul" + + "github.com/hashicorp/consul/api" + "github.com/insidieux/pinchy/internal/extension/registry" + "github.com/pkg/errors" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +const ( + registryName = `consul` + + flagConsulAddress = `address` +) + +type ( + client interface { + Agent() *api.Agent + } + factory func(*api.Config) (*api.Client, error) +) + +func init() { + set := pflag.NewFlagSet(registryName, pflag.ExitOnError) + set.String(registry.MakeFlagName(flagConsulAddress), `127.0.0.1:8500`, `Consul http api address`) + if err := registry.Register(registryName, set, NewRegistry); err != nil { + panic(err) + } +} + +func newClientConfig(v *viper.Viper, transport *http.Transport) (*api.Config, error) { + flag := registry.MakeFlagName(flagConsulAddress) + address := v.GetString(flag) + if address == `` { + return nil, errors.Errorf(`Flag "%s" is required`, flag) + } + + cfg := api.DefaultConfig() + cfg.Address = address + cfg.Transport = transport + return cfg, nil +} + +func provideConsulClientFactory() factory { + return api.NewClient +} + +func newClient(cfg *api.Config, factory factory) (client, error) { + c, err := factory(cfg) + if err != nil { + return nil, errors.Wrap(err, `failed to create consul client`) + } + return c, nil +} + +func newAgent(c client) pkgConsul.Agent { + return c.Agent() +} diff --git a/internal/extension/registry/consul/wire.go b/internal/extension/registry/consul/wire.go new file mode 100644 index 0000000..ba2b6b8 --- /dev/null +++ b/internal/extension/registry/consul/wire.go @@ -0,0 +1,23 @@ +// +build wireinject + +package consul + +import ( + "github.com/google/wire" + "github.com/hashicorp/go-cleanhttp" + "github.com/insidieux/pinchy/pkg/core" + "github.com/insidieux/pinchy/pkg/core/registry/consul" + "github.com/spf13/viper" +) + +func NewRegistry(*viper.Viper) (core.Registry, func(), error) { + panic(wire.Build( + cleanhttp.DefaultPooledTransport, + newClientConfig, + provideConsulClientFactory, + newClient, + newAgent, + consul.NewRegistry, + wire.Bind(new(core.Registry), new(*consul.Registry)), + )) +} diff --git a/internal/extension/registry/extension.go b/internal/extension/registry/extension.go new file mode 100644 index 0000000..5705e88 --- /dev/null +++ b/internal/extension/registry/extension.go @@ -0,0 +1,97 @@ +package registry + +import ( + "fmt" + "strings" + + "github.com/insidieux/pinchy/internal/extension" + "github.com/insidieux/pinchy/pkg/core" + "github.com/pkg/errors" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +const ( + flagPrefix = `registry` +) + +type ( + Factory func(*viper.Viper) (core.Registry, func(), error) + ProviderInterface interface { + Name() string + Flags() *pflag.FlagSet + Factory() Factory + } + ProviderList []ProviderInterface + provider struct { + name string + flags *pflag.FlagSet + factory Factory + } +) + +var ( + providerList = newProviderList() +) + +func newProviderList() *ProviderList { + return new(ProviderList) +} + +func GetProviderList() ProviderList { + return *providerList +} + +func MakeFlagName(name string) string { + return fmt.Sprintf(`%s.%s`, flagPrefix, name) +} + +func Register(name string, flags *pflag.FlagSet, factory Factory) error { + return providerList.register(&provider{ + name: name, + flags: flags, + factory: factory, + }) +} + +func (p *provider) Name() string { + return p.name +} + +func (p *provider) Flags() *pflag.FlagSet { + return p.flags +} + +func (p *provider) Factory() Factory { + return p.factory +} + +func (pl *ProviderList) register(p ProviderInterface) error { + if ep, _ := pl.Lookup(p.Name()); ep != nil { + return errors.Errorf(`registry provider with name "%s" has been already registered`, p.Name()) + } + var err extension.RegisterError + p.Flags().VisitAll(func(flag *pflag.Flag) { + if !strings.HasPrefix(flag.Name, flagPrefix) { + err = append(err, errors.Errorf(`flag "%s" does not contain required prefix "%s"`, flag.Name, flagPrefix)) + } + }) + if err != nil { + return errors.Wrapf(err, `registry "%s" flags validation error`, p.Name()) + } + *pl = append(*pl, p) + return nil +} + +func (pl ProviderList) Lookup(name string) (ProviderInterface, error) { + for _, p := range pl { + if p.Name() == name { + return p, nil + } + } + return nil, errors.Errorf(`registry provider with name "%s" was not registered`, name) +} + +func (pl ProviderList) Get() []ProviderInterface { + return pl +} diff --git a/internal/extension/registry/extension_test.go b/internal/extension/registry/extension_test.go new file mode 100644 index 0000000..338e671 --- /dev/null +++ b/internal/extension/registry/extension_test.go @@ -0,0 +1,332 @@ +package registry + +import ( + "fmt" + "testing" + + "github.com/insidieux/pinchy/pkg/core" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +// --- Tests --- + +func Test_newProviderList(t *testing.T) { + suite.Run(t, new(newProviderListTestSuite)) +} + +func TestGetProviderList(t *testing.T) { + suite.Run(t, new(getProviderListTestSuite)) +} + +func TestMakeFlagName(t *testing.T) { + suite.Run(t, new(makeFlagNameTestSuite)) +} + +func TestProviderList_Get(t *testing.T) { + suite.Run(t, new(providerListGetTestSuite)) +} + +func TestProviderList_Lookup(t *testing.T) { + suite.Run(t, new(providerListLookupTestSuite)) +} + +func TestProviderList_register(t *testing.T) { + suite.Run(t, new(providerListRegisterTestSuite)) +} + +func TestProvider_Factory(t *testing.T) { + suite.Run(t, new(providerFactoryTestSuite)) +} + +func TestProvider_Flags(t *testing.T) { + suite.Run(t, new(providerFlagsTestSuite)) +} + +func TestProvider_Name(t *testing.T) { + suite.Run(t, new(providerNameTestSuite)) +} + +func TestRegister(t *testing.T) { + suite.Run(t, new(registerTestSuite)) +} + +// --- Suites --- + +type newProviderListTestSuite struct { + suite.Suite +} + +func (s *newProviderListTestSuite) TestNewProviderList() { + got := newProviderList() + s.Equal(new(ProviderList), got) +} + +type getProviderListTestSuite struct { + suite.Suite +} + +func (s *newProviderListTestSuite) TestGetProviderList() { + got := GetProviderList() + s.Equal(*newProviderList(), got) +} + +type makeFlagNameTestSuite struct { + suite.Suite +} + +func (s *makeFlagNameTestSuite) TestMakeFlagName() { + s.Equal(fmt.Sprintf(`%s.%s`, flagPrefix, `flag`), MakeFlagName(`flag`)) +} + +type providerListGetTestSuite struct { + suite.Suite +} + +func (s *providerListGetTestSuite) TestGet() { + s.Equal(*new([]ProviderInterface), newProviderList().Get()) +} + +type providerListLookupTestSuite struct { + suite.Suite + list *ProviderList +} + +func (s *providerListLookupTestSuite) SetupTest() { + s.list = newProviderList() +} + +func (s *providerListLookupTestSuite) TestErrorProviderWasNotRegistered() { + provider, err := s.list.Lookup(`provider`) + s.Nil(provider) + s.Error(err) + s.EqualError(err, `registry provider with name "provider" was not registered`) +} + +func (s *providerListLookupTestSuite) TestSuccess() { + registeredProvider := new(MockProviderInterface) + registeredProvider.On(`Name`).Return(`provider`) + *s.list = append(*s.list, registeredProvider) + + provider, err := s.list.Lookup(`provider`) + s.NoError(err) + s.NotNil(provider) + s.Equal(registeredProvider, provider) +} + +type providerListRegisterTestSuite struct { + suite.Suite + list *ProviderList + provider *MockProviderInterface +} + +func (s *providerListRegisterTestSuite) SetupTest() { + s.list = newProviderList() + s.provider = new(MockProviderInterface) + s.provider.On(`Name`).Return(`provider`) +} + +func (s *providerListRegisterTestSuite) TestErrorProviderHasBeenRegistered() { + beforeRegisterProvider := new(MockProviderInterface) + beforeRegisterProvider.On(`Name`).Return(`provider`) + *s.list = append(*s.list, beforeRegisterProvider) + + err := s.list.register(s.provider) + s.Error(err) + s.EqualError(err, `registry provider with name "provider" has been already registered`) +} + +func (s *providerListRegisterTestSuite) TestErrorProviderHasFlagsWithoutRequiredPrefix() { + flags := pflag.NewFlagSet(`provider`, pflag.ExitOnError) + flags.String(`wrong.flag`, `value`, `usage`) + s.provider.On(`Flags`).Return(flags) + + err := s.list.register(s.provider) + s.Error(err) + s.EqualError( + err, + fmt.Sprintf( + `registry "%s" flags validation error: flag "%s" does not contain required prefix "%s"`, + `provider`, + `wrong.flag`, + flagPrefix, + ), + ) +} + +func (s *providerListRegisterTestSuite) TestSuccess() { + flags := pflag.NewFlagSet(`provider`, pflag.ExitOnError) + flags.String(`registry.flag`, `value`, `usage`) + s.provider.On(`Flags`).Return(flags) + + err := s.list.register(s.provider) + s.NoError(err) +} + +type providerFactoryTestSuite struct { + suite.Suite +} + +func (s *providerFactoryTestSuite) TestSuccess() { + factory := new(MockFactory) + p := &provider{ + factory: factory.Execute, + } + s.IsType(*new(Factory), p.Factory()) +} + +type providerFlagsTestSuite struct { + suite.Suite +} + +func (s *providerFlagsTestSuite) TestSuccess() { + p := &provider{ + flags: pflag.CommandLine, + } + s.Equal(pflag.CommandLine, p.Flags()) +} + +type providerNameTestSuite struct { + suite.Suite +} + +func (s *providerNameTestSuite) TestSuccess() { + p := &provider{ + name: `name`, + } + s.Equal(`name`, p.Name()) +} + +type registerTestSuite struct { + suite.Suite + provider *MockProviderInterface +} + +func (s *registerTestSuite) SetupTest() { + providerList = newProviderList() +} + +func (s *registerTestSuite) TestErrorProviderHasBeenRegistered() { + *providerList = append(*providerList, &provider{name: `provider`}) + + err := Register(`provider`, nil, nil) + s.Error(err) + s.EqualError(err, `registry provider with name "provider" has been already registered`) +} + +func (s *registerTestSuite) TestErrorProviderHasFlagsWithoutRequiredPrefix() { + flags := pflag.NewFlagSet(`provider`, pflag.ExitOnError) + flags.String(`wrong.flag`, `value`, `usage`) + + err := Register(`provider`, flags, nil) + s.Error(err) + s.EqualError( + err, + fmt.Sprintf( + `registry "%s" flags validation error: flag "%s" does not contain required prefix "%s"`, + `provider`, + `wrong.flag`, + flagPrefix, + ), + ) +} + +func (s *registerTestSuite) TestSuccess() { + flags := pflag.NewFlagSet(`provider`, pflag.ExitOnError) + flags.String(`registry.flag`, `value`, `usage`) + + err := Register(`provider`, flags, nil) + s.NoError(err) +} + +// --- Mocks --- + +// MockProviderInterface is an autogenerated mock type for the ProviderInterface type +type MockProviderInterface struct { + mock.Mock +} + +// Factory provides a mock function with given fields: +func (_m *MockProviderInterface) Factory() Factory { + ret := _m.Called() + + var r0 Factory + if rf, ok := ret.Get(0).(func() Factory); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(Factory) + } + } + + return r0 +} + +// Flags provides a mock function with given fields: +func (_m *MockProviderInterface) Flags() *pflag.FlagSet { + ret := _m.Called() + + var r0 *pflag.FlagSet + if rf, ok := ret.Get(0).(func() *pflag.FlagSet); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*pflag.FlagSet) + } + } + + return r0 +} + +// Name provides a mock function with given fields: +func (_m *MockProviderInterface) Name() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// MockFactory is an autogenerated mock type for the Factory type +type MockFactory struct { + mock.Mock +} + +// Execute provides a mock function with given fields: _a0 +func (_m *MockFactory) Execute(_a0 *viper.Viper) (core.Registry, func(), error) { + ret := _m.Called(_a0) + + var r0 core.Registry + if rf, ok := ret.Get(0).(func(*viper.Viper) core.Registry); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(core.Registry) + } + } + + var r1 func() + if rf, ok := ret.Get(1).(func(*viper.Viper) func()); ok { + r1 = rf(_a0) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(func()) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(*viper.Viper) error); ok { + r2 = rf(_a0) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} diff --git a/internal/extension/source/extension.go b/internal/extension/source/extension.go new file mode 100644 index 0000000..0c6224f --- /dev/null +++ b/internal/extension/source/extension.go @@ -0,0 +1,97 @@ +package source + +import ( + "fmt" + "strings" + + "github.com/insidieux/pinchy/internal/extension" + "github.com/insidieux/pinchy/pkg/core" + "github.com/pkg/errors" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +const ( + flagPrefix = `source` +) + +type ( + Factory func(*viper.Viper) (core.Source, func(), error) + ProviderInterface interface { + Name() string + Flags() *pflag.FlagSet + Factory() Factory + } + ProviderList []ProviderInterface + provider struct { + name string + flags *pflag.FlagSet + factory Factory + } +) + +var ( + providerList = newProviderList() +) + +func newProviderList() *ProviderList { + return new(ProviderList) +} + +func GetProviderList() ProviderList { + return *providerList +} + +func MakeFlagName(name string) string { + return fmt.Sprintf(`%s.%s`, flagPrefix, name) +} + +func Register(name string, flags *pflag.FlagSet, factory Factory) error { + return providerList.register(&provider{ + name: name, + flags: flags, + factory: factory, + }) +} + +func (p *provider) Name() string { + return p.name +} + +func (p *provider) Flags() *pflag.FlagSet { + return p.flags +} + +func (p *provider) Factory() Factory { + return p.factory +} + +func (pl *ProviderList) register(p ProviderInterface) error { + if ep, _ := pl.Lookup(p.Name()); ep != nil { + return errors.Errorf(`source provider with name "%s" has been already registered`, p.Name()) + } + var err extension.RegisterError + p.Flags().VisitAll(func(flag *pflag.Flag) { + if !strings.HasPrefix(flag.Name, flagPrefix) { + err = append(err, errors.Errorf(`flag "%s" does not contain required prefix "%s"`, flag.Name, flagPrefix)) + } + }) + if err != nil { + return errors.Wrapf(err, `source "%s" flags validation error`, p.Name()) + } + *pl = append(*pl, p) + return nil +} + +func (pl ProviderList) Lookup(name string) (ProviderInterface, error) { + for _, p := range pl { + if p.Name() == name { + return p, nil + } + } + return nil, errors.Errorf(`source provider with name "%s" was not registered`, name) +} + +func (pl ProviderList) Get() []ProviderInterface { + return pl +} diff --git a/internal/extension/source/extension_test.go b/internal/extension/source/extension_test.go new file mode 100644 index 0000000..1140ec4 --- /dev/null +++ b/internal/extension/source/extension_test.go @@ -0,0 +1,332 @@ +package source + +import ( + "fmt" + "testing" + + "github.com/insidieux/pinchy/pkg/core" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +// --- Tests --- + +func Test_newProviderList(t *testing.T) { + suite.Run(t, new(newProviderListTestSuite)) +} + +func TestGetProviderList(t *testing.T) { + suite.Run(t, new(getProviderListTestSuite)) +} + +func TestMakeFlagName(t *testing.T) { + suite.Run(t, new(makeFlagNameTestSuite)) +} + +func TestProviderList_Get(t *testing.T) { + suite.Run(t, new(providerListGetTestSuite)) +} + +func TestProviderList_Lookup(t *testing.T) { + suite.Run(t, new(providerListLookupTestSuite)) +} + +func TestProviderList_register(t *testing.T) { + suite.Run(t, new(providerListRegisterTestSuite)) +} + +func TestProvider_Factory(t *testing.T) { + suite.Run(t, new(providerFactoryTestSuite)) +} + +func TestProvider_Flags(t *testing.T) { + suite.Run(t, new(providerFlagsTestSuite)) +} + +func TestProvider_Name(t *testing.T) { + suite.Run(t, new(providerNameTestSuite)) +} + +func TestRegister(t *testing.T) { + suite.Run(t, new(registerTestSuite)) +} + +// --- Suites --- + +type newProviderListTestSuite struct { + suite.Suite +} + +func (s *newProviderListTestSuite) TestNewProviderList() { + got := newProviderList() + s.Equal(new(ProviderList), got) +} + +type getProviderListTestSuite struct { + suite.Suite +} + +func (s *newProviderListTestSuite) TestGetProviderList() { + got := GetProviderList() + s.Equal(*newProviderList(), got) +} + +type makeFlagNameTestSuite struct { + suite.Suite +} + +func (s *makeFlagNameTestSuite) TestMakeFlagName() { + s.Equal(fmt.Sprintf(`%s.%s`, flagPrefix, `flag`), MakeFlagName(`flag`)) +} + +type providerListGetTestSuite struct { + suite.Suite +} + +func (s *providerListGetTestSuite) TestGet() { + s.Equal(*new([]ProviderInterface), newProviderList().Get()) +} + +type providerListLookupTestSuite struct { + suite.Suite + list *ProviderList +} + +func (s *providerListLookupTestSuite) SetupTest() { + s.list = newProviderList() +} + +func (s *providerListLookupTestSuite) TestErrorProviderWasNotRegistered() { + provider, err := s.list.Lookup(`provider`) + s.Nil(provider) + s.Error(err) + s.EqualError(err, `source provider with name "provider" was not registered`) +} + +func (s *providerListLookupTestSuite) TestSuccess() { + registeredProvider := new(MockProviderInterface) + registeredProvider.On(`Name`).Return(`provider`) + *s.list = append(*s.list, registeredProvider) + + provider, err := s.list.Lookup(`provider`) + s.NoError(err) + s.NotNil(provider) + s.Equal(registeredProvider, provider) +} + +type providerListRegisterTestSuite struct { + suite.Suite + list *ProviderList + provider *MockProviderInterface +} + +func (s *providerListRegisterTestSuite) SetupTest() { + s.list = newProviderList() + s.provider = new(MockProviderInterface) + s.provider.On(`Name`).Return(`provider`) +} + +func (s *providerListRegisterTestSuite) TestErrorProviderHasBeenRegistered() { + beforeRegisterProvider := new(MockProviderInterface) + beforeRegisterProvider.On(`Name`).Return(`provider`) + *s.list = append(*s.list, beforeRegisterProvider) + + err := s.list.register(s.provider) + s.Error(err) + s.EqualError(err, `source provider with name "provider" has been already registered`) +} + +func (s *providerListRegisterTestSuite) TestErrorProviderHasFlagsWithoutRequiredPrefix() { + flags := pflag.NewFlagSet(`provider`, pflag.ExitOnError) + flags.String(`wrong.flag`, `value`, `usage`) + s.provider.On(`Flags`).Return(flags) + + err := s.list.register(s.provider) + s.Error(err) + s.EqualError( + err, + fmt.Sprintf( + `source "%s" flags validation error: flag "%s" does not contain required prefix "%s"`, + `provider`, + `wrong.flag`, + flagPrefix, + ), + ) +} + +func (s *providerListRegisterTestSuite) TestSuccess() { + flags := pflag.NewFlagSet(`provider`, pflag.ExitOnError) + flags.String(`source.flag`, `value`, `usage`) + s.provider.On(`Flags`).Return(flags) + + err := s.list.register(s.provider) + s.NoError(err) +} + +type providerFactoryTestSuite struct { + suite.Suite +} + +func (s *providerFactoryTestSuite) TestSuccess() { + factory := new(MockFactory) + p := &provider{ + factory: factory.Execute, + } + s.IsType(*new(Factory), p.Factory()) +} + +type providerFlagsTestSuite struct { + suite.Suite +} + +func (s *providerFlagsTestSuite) TestSuccess() { + p := &provider{ + flags: pflag.CommandLine, + } + s.Equal(pflag.CommandLine, p.Flags()) +} + +type providerNameTestSuite struct { + suite.Suite +} + +func (s *providerNameTestSuite) TestSuccess() { + p := &provider{ + name: `name`, + } + s.Equal(`name`, p.Name()) +} + +type registerTestSuite struct { + suite.Suite + provider *MockProviderInterface +} + +func (s *registerTestSuite) SetupTest() { + providerList = newProviderList() +} + +func (s *registerTestSuite) TestErrorProviderHasBeenRegistered() { + *providerList = append(*providerList, &provider{name: `provider`}) + + err := Register(`provider`, nil, nil) + s.Error(err) + s.EqualError(err, `source provider with name "provider" has been already registered`) +} + +func (s *registerTestSuite) TestErrorProviderHasFlagsWithoutRequiredPrefix() { + flags := pflag.NewFlagSet(`provider`, pflag.ExitOnError) + flags.String(`wrong.flag`, `value`, `usage`) + + err := Register(`provider`, flags, nil) + s.Error(err) + s.EqualError( + err, + fmt.Sprintf( + `source "%s" flags validation error: flag "%s" does not contain required prefix "%s"`, + `provider`, + `wrong.flag`, + flagPrefix, + ), + ) +} + +func (s *registerTestSuite) TestSuccess() { + flags := pflag.NewFlagSet(`provider`, pflag.ExitOnError) + flags.String(`source.flag`, `value`, `usage`) + + err := Register(`provider`, flags, nil) + s.NoError(err) +} + +// --- Mocks --- + +// MockProviderInterface is an autogenerated mock type for the ProviderInterface type +type MockProviderInterface struct { + mock.Mock +} + +// Factory provides a mock function with given fields: +func (_m *MockProviderInterface) Factory() Factory { + ret := _m.Called() + + var r0 Factory + if rf, ok := ret.Get(0).(func() Factory); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(Factory) + } + } + + return r0 +} + +// Flags provides a mock function with given fields: +func (_m *MockProviderInterface) Flags() *pflag.FlagSet { + ret := _m.Called() + + var r0 *pflag.FlagSet + if rf, ok := ret.Get(0).(func() *pflag.FlagSet); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*pflag.FlagSet) + } + } + + return r0 +} + +// Name provides a mock function with given fields: +func (_m *MockProviderInterface) Name() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// MockFactory is an autogenerated mock type for the Factory type +type MockFactory struct { + mock.Mock +} + +// Execute provides a mock function with given fields: _a0 +func (_m *MockFactory) Execute(_a0 *viper.Viper) (core.Source, func(), error) { + ret := _m.Called(_a0) + + var r0 core.Source + if rf, ok := ret.Get(0).(func(*viper.Viper) core.Source); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(core.Source) + } + } + + var r1 func() + if rf, ok := ret.Get(1).(func(*viper.Viper) func()); ok { + r1 = rf(_a0) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(func()) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(*viper.Viper) error); ok { + r2 = rf(_a0) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} diff --git a/internal/extension/source/file/provider.go b/internal/extension/source/file/provider.go new file mode 100644 index 0000000..f78e05d --- /dev/null +++ b/internal/extension/source/file/provider.go @@ -0,0 +1,41 @@ +package file + +import ( + pkgFile "github.com/insidieux/pinchy/pkg/core/source/file" + + "github.com/insidieux/pinchy/internal/extension/source" + "github.com/pkg/errors" + "github.com/spf13/afero" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +const ( + sourceName = `file` + + flagFilePath = `path` +) + +func init() { + set := pflag.NewFlagSet(sourceName, pflag.ExitOnError) + set.String(source.MakeFlagName(flagFilePath), `$HOME/services.yml`, `services.yml config path`) + + if err := source.Register(sourceName, set, NewSource); err != nil { + panic(err) + } +} + +func newReader() afero.Afero { + return afero.Afero{ + Fs: afero.NewReadOnlyFs(afero.NewOsFs()), + } +} + +func newPath(v *viper.Viper) (pkgFile.Path, error) { + flag := source.MakeFlagName(flagFilePath) + path := v.GetString(flag) + if path == `` { + return ``, errors.Errorf(`flag "%s" is required`, flag) + } + return pkgFile.Path(path), nil +} diff --git a/internal/extension/source/file/wire.go b/internal/extension/source/file/wire.go new file mode 100644 index 0000000..b05322f --- /dev/null +++ b/internal/extension/source/file/wire.go @@ -0,0 +1,22 @@ +// +build wireinject + +package file + +import ( + pkgFile "github.com/insidieux/pinchy/pkg/core/source/file" + + "github.com/google/wire" + "github.com/insidieux/pinchy/pkg/core" + "github.com/spf13/afero" + "github.com/spf13/viper" +) + +func NewSource(*viper.Viper) (core.Source, func(), error) { + panic(wire.Build( + newReader, + wire.Bind(new(pkgFile.Reader), new(afero.Afero)), + newPath, + pkgFile.NewSource, + wire.Bind(new(core.Source), new(*pkgFile.Source)), + )) +} diff --git a/pkg/core/logger.go b/pkg/core/logger.go new file mode 100644 index 0000000..064f925 --- /dev/null +++ b/pkg/core/logger.go @@ -0,0 +1,35 @@ +package core + +type ( + LoggerInterface interface { + Debugf(format string, args ...interface{}) + Infof(format string, args ...interface{}) + Printf(format string, args ...interface{}) + Warnf(format string, args ...interface{}) + Warningf(format string, args ...interface{}) + Errorf(format string, args ...interface{}) + Fatalf(format string, args ...interface{}) + Panicf(format string, args ...interface{}) + + Debug(args ...interface{}) + Info(args ...interface{}) + Print(args ...interface{}) + Warn(args ...interface{}) + Warning(args ...interface{}) + Error(args ...interface{}) + Fatal(args ...interface{}) + Panic(args ...interface{}) + + Debugln(args ...interface{}) + Infoln(args ...interface{}) + Println(args ...interface{}) + Warnln(args ...interface{}) + Warningln(args ...interface{}) + Errorln(args ...interface{}) + Fatalln(args ...interface{}) + Panicln(args ...interface{}) + } + Loggable interface { + WithLogger(LoggerInterface) + } +) diff --git a/pkg/core/manager.go b/pkg/core/manager.go new file mode 100644 index 0000000..f35cec4 --- /dev/null +++ b/pkg/core/manager.go @@ -0,0 +1,123 @@ +package core + +import ( + "context" + "strings" + + "github.com/pkg/errors" + "github.com/spf13/cast" + "github.com/thoas/go-funk" +) + +type ( + ManagerInterface interface { + Run(ctx context.Context) error + } + Manager struct { + source Source + registry Registry + logger LoggerInterface + exitOnError ManagerExitOnError + } + ManagerExitOnError bool + managerError []error +) + +func NewManager(source Source, registry Registry, logger LoggerInterface, exitOnError ManagerExitOnError) ManagerInterface { + return &Manager{ + source: source, + registry: registry, + logger: logger, + exitOnError: exitOnError, + } +} + +func (m *Manager) Run(ctx context.Context) error { + incoming, err := m.source.Fetch(ctx) + if err != nil { + return errors.Wrap(err, `failed to fetch services from source`) + } + + registered, err := m.registry.Fetch(ctx) + if err != nil { + return errors.Wrap(err, `failed to fetch services from registry`) + } + + orphan := m.findOrphan(incoming, registered) + if len(orphan) > 0 { + if err := m.deregisterServices(ctx, orphan); err != nil { + err := errors.Wrap(err, `failed to deregister services`) + m.logger.Error(err.Error()) + if m.exitOnError { + return err + } + } + } + if err := m.registerServices(ctx, incoming); err != nil { + err := errors.Wrap(err, `failed to register services`) + m.logger.Error(err.Error()) + if m.exitOnError { + return err + } + } + return nil +} + +func (m *Manager) findOrphan(incoming Services, registered Services) Services { + _, right := funk.Difference(incoming.IDs(), registered.IDs()) + orphan := make(Services, 0) + for _, id := range cast.ToStringSlice(right) { + if service := registered.Lookup(id); service != nil { + orphan = append(orphan, service) + } + } + return orphan +} + +func (m *Manager) deregisterServices(ctx context.Context, services Services) error { + me := new(managerError) + for _, service := range services { + if err := m.registry.Deregister(ctx, service.RegistrationID()); err != nil { + me.Add(errors.Wrapf(err, `failed to deregister service "%s" from registry`, service.RegistrationID())) + continue + } + } + if me.HasErrors() { + return me + } + return nil +} + +func (m *Manager) registerServices(ctx context.Context, services Services) error { + me := new(managerError) + for _, service := range services { + if err := m.registry.Register(ctx, service); err != nil { + me.Add(errors.Wrapf(err, `failed to register service "%s" in registry`, service.RegistrationID())) + continue + } + } + if me.HasErrors() { + return me + } + return nil +} + +func (e *managerError) Add(err error) { + *e = append(*e, err) +} + +func (e *managerError) HasErrors() bool { + return len(*e) > 0 +} + +func (e *managerError) String() string { + var slice []string + for _, err := range *e { + slice = append(slice, err.Error()) + } + return strings.Join(slice, `; `) +} + +func (e *managerError) Error() string { + return e.String() +} diff --git a/pkg/core/manager_test.go b/pkg/core/manager_test.go new file mode 100644 index 0000000..7e149dc --- /dev/null +++ b/pkg/core/manager_test.go @@ -0,0 +1,282 @@ +package core + +import ( + "context" + "testing" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +// --- Tests --- + +func TestNewManager(t *testing.T) { + suite.Run(t, new(newManagerTestSuite)) +} + +func TestManager_Run(t *testing.T) { + suite.Run(t, new(managerRunTestSuite)) +} + +func TestManagerError_Add(t *testing.T) { + suite.Run(t, new(managerErrorAddTestSuite)) +} + +func TestManagerError_String(t *testing.T) { + suite.Run(t, new(managerErrorStringTestSuite)) +} + +func TestManagerError_Error(t *testing.T) { + suite.Run(t, new(managerErrorErrorTestSuite)) +} + +func TestManagerError_HasErrors(t *testing.T) { + suite.Run(t, new(managerErrorHasErrorsTestSuite)) +} + +// --- Suites --- + +type newManagerTestSuite struct { + suite.Suite +} + +func (s *newManagerTestSuite) TestNewManager() { + s.Equal( + &Manager{nil, nil, nil, true}, + NewManager(nil, nil, nil, true), + ) +} + +type managerRunTestSuite struct { + suite.Suite + manager *Manager +} + +func (s *managerRunTestSuite) SetupTest() { + s.manager = new(Manager) +} + +func (s *managerRunTestSuite) TestErrorFetchFromSource() { + ctx := context.Background() + sourceMock := new(MockSource) + sourceMock.On(`Fetch`, ctx).Return(nil, errors.New(`expected error`)) + + s.manager.source = sourceMock + err := s.manager.Run(ctx) + s.Error(err) + s.EqualError(err, `failed to fetch services from source: expected error`) +} + +func (s *managerRunTestSuite) TestErrorFetchFromRegistry() { + ctx := context.Background() + sourceMock := new(MockSource) + sourceMock.On(`Fetch`, ctx).Return(Services{}, nil) + registryMock := new(MockRegistry) + registryMock.On(`Fetch`, ctx).Return(nil, errors.New(`expected error`)) + + s.manager.source = sourceMock + s.manager.registry = registryMock + + err := s.manager.Run(ctx) + s.Error(err) + s.EqualError(err, `failed to fetch services from registry: expected error`) +} + +func (s *managerRunTestSuite) TestErrorDeregisterOrphan() { + ctx := context.Background() + sourceMock := new(MockSource) + sourceMock.On(`Fetch`, ctx).Return(Services{{Name: `service-1`}}, nil) + registryMock := new(MockRegistry) + registryMock.On(`Fetch`, ctx).Return(Services{{Name: `service-2`}}, nil) + registryMock.On(`Deregister`, ctx, `service-2`).Return(errors.New(`expected error`)) + logger, _ := test.NewNullLogger() + + s.manager.source = sourceMock + s.manager.registry = registryMock + s.manager.logger = logger + s.manager.exitOnError = true + + err := s.manager.Run(ctx) + s.Error(err) + s.EqualError(err, `failed to deregister services: failed to deregister service "service-2" from registry: expected error`) +} + +func (s *managerRunTestSuite) TestErrorRegister() { + ctx := context.Background() + service := &Service{Name: `service-1`} + sourceMock := new(MockSource) + sourceMock.On(`Fetch`, ctx).Return(Services{service}, nil) + registryMock := new(MockRegistry) + registryMock.On(`Fetch`, ctx).Return(Services{}, nil) + registryMock.On(`Register`, ctx, service).Return(errors.New(`expected error`)) + logger, _ := test.NewNullLogger() + + s.manager.source = sourceMock + s.manager.registry = registryMock + s.manager.logger = logger + s.manager.exitOnError = true + + err := s.manager.Run(ctx) + s.Error(err) + s.EqualError(err, `failed to register services: failed to register service "service-1" in registry: expected error`) +} + +func (s *managerRunTestSuite) TestSuccess() { + ctx := context.Background() + sourceMock := new(MockSource) + sourceMock.On(`Fetch`, ctx).Return(Services{{Name: `service-1`}}, nil) + registryMock := new(MockRegistry) + registryMock.On(`Fetch`, ctx).Return(Services{{Name: `service-2`}}, nil) + registryMock.On(`Deregister`, ctx, `service-2`).Return(nil) + registryMock.On(`Register`, ctx, mock.Anything).Return(nil) + + s.manager.source = sourceMock + s.manager.registry = registryMock + s.NoError(s.manager.Run(ctx)) +} + +type managerErrorAddTestSuite struct { + suite.Suite + err managerError +} + +func (s *managerErrorAddTestSuite) TestAdd() { + s.err = append(s.err, errors.New(`expected error`)) + s.Len(s.err, 1) + s.Equal(`expected error`, s.err[0].Error()) +} + +type managerErrorStringTestSuite struct { + suite.Suite + err managerError +} + +func (s *managerErrorStringTestSuite) TestEmptyString() { + s.Empty(s.err.String()) +} + +func (s *managerErrorStringTestSuite) TestNonEmptyString() { + s.err = append(s.err, errors.New(`expected error 1`)) + s.err = append(s.err, errors.New(`expected error 2`)) + s.NotEmpty(s.err.String()) + s.Equal(`expected error 1; expected error 2`, s.err.String()) +} + +type managerErrorErrorTestSuite struct { + suite.Suite + err managerError +} + +func (s *managerErrorErrorTestSuite) TestEmptyError() { + s.Empty(s.err.Error()) +} + +func (s *managerErrorErrorTestSuite) TestNonEmptyError() { + s.err = append(s.err, errors.New(`expected error 1`)) + s.err = append(s.err, errors.New(`expected error 2`)) + s.NotEmpty(s.err.Error()) + s.Equal(`expected error 1; expected error 2`, s.err.Error()) +} + +type managerErrorHasErrorsTestSuite struct { + suite.Suite + err managerError +} + +func (s *managerErrorHasErrorsTestSuite) TestEmptyError() { + s.False(s.err.HasErrors()) +} + +func (s *managerErrorHasErrorsTestSuite) TestNonEmptyError() { + s.err = append(s.err, errors.New(`expected error 1`)) + s.err = append(s.err, errors.New(`expected error 2`)) + s.True(s.err.HasErrors()) +} + +// --- Mocks --- + +// MockRegistry is an autogenerated mock type for the Registry type +type MockRegistry struct { + mock.Mock +} + +// Deregister provides a mock function with given fields: ctx, serviceID +func (_m *MockRegistry) Deregister(ctx context.Context, serviceID string) error { + ret := _m.Called(ctx, serviceID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, serviceID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Fetch provides a mock function with given fields: ctx +func (_m *MockRegistry) Fetch(ctx context.Context) (Services, error) { + ret := _m.Called(ctx) + + var r0 Services + if rf, ok := ret.Get(0).(func(context.Context) Services); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(Services) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Register provides a mock function with given fields: ctx, service +func (_m *MockRegistry) Register(ctx context.Context, service *Service) error { + ret := _m.Called(ctx, service) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *Service) error); ok { + r0 = rf(ctx, service) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockSource is an autogenerated mock type for the Source type +type MockSource struct { + mock.Mock +} + +// Fetch provides a mock function with given fields: ctx +func (_m *MockSource) Fetch(ctx context.Context) (Services, error) { + ret := _m.Called(ctx) + + var r0 Services + if rf, ok := ret.Get(0).(func(context.Context) Services); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(Services) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/pkg/core/registry.go b/pkg/core/registry.go new file mode 100644 index 0000000..1c4dd45 --- /dev/null +++ b/pkg/core/registry.go @@ -0,0 +1,15 @@ +package core + +import ( + "context" +) + +type ( + // Registry is endpoint/upstream for storing information about Services fetched from Source. + // Registry must implements 3 methods to make store clean: fetch current state, deregister orphans, register/update incoming + Registry interface { + Fetch(ctx context.Context) (Services, error) + Register(ctx context.Context, service *Service) error + Deregister(ctx context.Context, serviceID string) error + } +) diff --git a/pkg/core/registry/consul/registry.go b/pkg/core/registry/consul/registry.go new file mode 100644 index 0000000..744bd5c --- /dev/null +++ b/pkg/core/registry/consul/registry.go @@ -0,0 +1,87 @@ +package consul + +import ( + "context" + + "github.com/agrea/ptr" + "github.com/hashicorp/consul/api" + "github.com/insidieux/pinchy/pkg/core" + "github.com/pkg/errors" +) + +type ( + Agent interface { + Services() (map[string]*api.AgentService, error) + ServiceRegister(service *api.AgentServiceRegistration) error + ServiceDeregister(serviceID string) error + } + Registry struct { + agent Agent + } +) + +func NewRegistry(agent Agent) *Registry { + return &Registry{ + agent: agent, + } +} + +func (r *Registry) Fetch(_ context.Context) (core.Services, error) { + registered, err := r.agent.Services() + if err != nil { + return nil, errors.Wrap(err, `failed to fetch registered services info`) + } + result := make([]*core.Service, 0) + for _, item := range registered { + service := &core.Service{ + Name: item.Service, + Address: item.Address, + ID: ptr.String(item.ID), + } + if item.Port != 0 { + service.Port = ptr.Int(item.Port) + } + if len(item.Tags) > 0 { + service.Tags = &item.Tags + } + if len(item.Meta) > 0 { + service.Meta = &item.Meta + } + result = append(result, service) + } + return result, nil +} + +func (r *Registry) Deregister(_ context.Context, serviceID string) error { + if err := r.agent.ServiceDeregister(serviceID); err != nil { + return errors.Wrapf(err, `failed deregister service by service id "%s"`, serviceID) + } + return nil +} + +func (r *Registry) Register(ctx context.Context, service *core.Service) error { + if err := service.Validate(ctx); err != nil { + return errors.Wrap(err, `service has validation error before registration`) + } + asr := &api.AgentServiceRegistration{ + Kind: api.ServiceKindTypical, + Name: service.Name, + Address: service.Address, + } + if service.ID != nil { + asr.ID = *service.ID + } + if service.Port != nil { + asr.Port = *service.Port + } + if service.Tags != nil { + asr.Tags = *service.Tags + } + if service.Meta != nil { + asr.Meta = *service.Meta + } + if err := r.agent.ServiceRegister(asr); err != nil { + return errors.Wrapf(err, `failed register service by service id "%s"`, service.RegistrationID()) + } + return nil +} diff --git a/pkg/core/registry/consul/registry_test.go b/pkg/core/registry/consul/registry_test.go new file mode 100644 index 0000000..80b2b58 --- /dev/null +++ b/pkg/core/registry/consul/registry_test.go @@ -0,0 +1,221 @@ +package consul + +import ( + "context" + "testing" + + "github.com/agrea/ptr" + "github.com/hashicorp/consul/api" + "github.com/insidieux/pinchy/pkg/core" + "github.com/pkg/errors" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +// --- Tests --- + +func TestNewRegistry(t *testing.T) { + suite.Run(t, new(newRegistryTestSuite)) +} + +func TestRegistry_Fetch(t *testing.T) { + suite.Run(t, new(registryFetchTestSuite)) +} + +func TestRegistry_Deregister(t *testing.T) { + suite.Run(t, new(registryDeregisterTestSuite)) +} + +func TestRegistry_Register(t *testing.T) { + suite.Run(t, new(registryRegisterTestSuite)) +} + +// --- Suites --- + +type newRegistryTestSuite struct { + suite.Suite +} + +func (s *newRegistryTestSuite) TestNewSource() { + got := NewRegistry(nil) + s.Implements((*core.Registry)(nil), got) + s.Equal(&Registry{nil}, got) +} + +type registryFetchTestSuite struct { + suite.Suite + agent *MockAgent + registry *Registry +} + +func (s *registryFetchTestSuite) SetupTest() { + s.agent = new(MockAgent) + s.registry = NewRegistry(s.agent) +} + +func (s *registryFetchTestSuite) TestErrorAgentFetch() { + s.agent.On(`Services`).Return(nil, errors.New(`expected error`)) + + s.registry.agent = s.agent + services, err := s.registry.Fetch(context.Background()) + s.Nil(services) + s.EqualError(err, `failed to fetch registered services info: expected error`) +} + +func (s *registryFetchTestSuite) TestSuccess() { + s.agent.On(`Services`).Return(map[string]*api.AgentService{ + `name`: { + ID: `id`, + Service: `name`, + Tags: []string{`tags`}, + Meta: map[string]string{`key`: `value`}, + Port: 80, + Address: `127.0.0.1`, + }, + }, nil) + + fetchedServices, err := s.registry.Fetch(context.Background()) + + expectedTags := []string{`tags`} + expectedMeta := map[string]string{`key`: `value`} + s.NoError(err) + s.Equal(core.Services{ + &core.Service{ + Name: `name`, + Address: `127.0.0.1`, + ID: ptr.String(`id`), + Tags: &expectedTags, + Meta: &expectedMeta, + Port: ptr.Int(80), + }, + }, fetchedServices) + +} + +type registryDeregisterTestSuite struct { + suite.Suite + agent *MockAgent + registry *Registry +} + +func (s *registryDeregisterTestSuite) SetupTest() { + s.agent = new(MockAgent) + s.registry = NewRegistry(s.agent) +} + +func (s *registryDeregisterTestSuite) TestErrorAgentDeregister() { + s.agent.On(`ServiceDeregister`, `service`).Return(errors.New(`expected error`)) + + err := s.registry.Deregister(context.Background(), `service`) + s.EqualError(err, `failed deregister service by service id "service": expected error`) +} + +func (s *registryDeregisterTestSuite) TestSuccess() { + s.agent.On(`ServiceDeregister`, `service`).Return(nil) + + err := s.registry.Deregister(context.Background(), `service`) + s.NoError(err) +} + +type registryRegisterTestSuite struct { + suite.Suite + agent *MockAgent + registry *Registry +} + +func (s *registryRegisterTestSuite) SetupTest() { + s.agent = new(MockAgent) + s.registry = NewRegistry(s.agent) +} + +func (s *registryRegisterTestSuite) TestErrorServiceValidation() { + err := s.registry.Register(context.Background(), &core.Service{ + Name: `name`, + }) + s.Error(err) + s.Contains(err.Error(), `service has validation error before registration`) +} + +func (s *registryRegisterTestSuite) TestErrorAgentRegister() { + s.agent.On(`ServiceRegister`, mock.Anything).Return(errors.New(`expected error`)) + + err := s.registry.Register(context.Background(), &core.Service{ + Name: `name`, + Address: `127.0.0.1`, + }) + s.EqualError(err, `failed register service by service id "name": expected error`) +} + +func (s *registryRegisterTestSuite) TestSuccess() { + s.agent.On(`ServiceRegister`, mock.Anything).Return(nil) + + expectedTags := []string{`tags`} + expectedMeta := map[string]string{`key`: `value`} + err := s.registry.Register(context.Background(), &core.Service{ + Name: `name`, + Address: `127.0.0.1`, + ID: ptr.String(`id`), + Tags: &expectedTags, + Meta: &expectedMeta, + Port: ptr.Int(80), + }) + s.NoError(err) +} + +// --- Mocks --- + +// MockAgent is an autogenerated mock type for the Agent type +type MockAgent struct { + mock.Mock +} + +// ServiceDeregister provides a mock function with given fields: serviceID +func (_m *MockAgent) ServiceDeregister(serviceID string) error { + ret := _m.Called(serviceID) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(serviceID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ServiceRegister provides a mock function with given fields: service +func (_m *MockAgent) ServiceRegister(service *api.AgentServiceRegistration) error { + ret := _m.Called(service) + + var r0 error + if rf, ok := ret.Get(0).(func(*api.AgentServiceRegistration) error); ok { + r0 = rf(service) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Services provides a mock function with given fields: +func (_m *MockAgent) Services() (map[string]*api.AgentService, error) { + ret := _m.Called() + + var r0 map[string]*api.AgentService + if rf, ok := ret.Get(0).(func() map[string]*api.AgentService); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]*api.AgentService) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/pkg/core/scheduler.go b/pkg/core/scheduler.go new file mode 100644 index 0000000..6bc4928 --- /dev/null +++ b/pkg/core/scheduler.go @@ -0,0 +1,37 @@ +package core + +import ( + "context" + "time" + + "github.com/pkg/errors" +) + +type ( + Scheduler struct { + ticker *time.Ticker + manager ManagerInterface + logger LoggerInterface + } +) + +func NewScheduler(ticker *time.Ticker, manager ManagerInterface, logger LoggerInterface) *Scheduler { + return &Scheduler{ + ticker: ticker, + manager: manager, + logger: logger, + } +} + +func (s *Scheduler) Run(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + case <-s.ticker.C: + if err := s.manager.Run(ctx); err != nil { + s.logger.Errorln(errors.Wrap(err, `failed to process manager run`).Error()) + } + } + } +} diff --git a/pkg/core/scheduler_test.go b/pkg/core/scheduler_test.go new file mode 100644 index 0000000..6998d64 --- /dev/null +++ b/pkg/core/scheduler_test.go @@ -0,0 +1,98 @@ +package core + +import ( + "context" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +// --- Tests --- + +func TestNewScheduler(t *testing.T) { + suite.Run(t, new(newSchedulerTestSuite)) +} + +func TestScheduler_Run(t *testing.T) { + suite.Run(t, new(schedulerRunTestSuite)) +} + +// --- Suites --- + +type newSchedulerTestSuite struct { + suite.Suite +} + +func (s *newSchedulerTestSuite) TestNewManager() { + s.Equal( + &Scheduler{nil, nil, nil}, + NewScheduler(nil, nil, nil), + ) +} + +type schedulerRunTestSuite struct { + suite.Suite + manager *MockManagerInterface + scheduler *Scheduler + hook *test.Hook +} + +func (s *schedulerRunTestSuite) SetupTest() { + logger, hook := test.NewNullLogger() + s.manager = new(MockManagerInterface) + s.hook = hook + s.scheduler = &Scheduler{ + ticker: time.NewTicker(time.Microsecond * 100), + manager: s.manager, + logger: logger, + } +} + +func (s *schedulerRunTestSuite) TestWithManagerError() { + s.manager.On(`Run`, mock.Anything).Return(errors.New(`expected error`)) + ctx, cancel := context.WithCancel(context.Background()) + go func() { + s.scheduler.Run(ctx) + }() + + <-time.Tick(time.Microsecond * 1000) + cancel() + s.Equal(s.hook.LastEntry().Message, `failed to process manager run: expected error`) +} + +func (s *schedulerRunTestSuite) TestWithoutManagerError() { + s.manager.On(`Run`, mock.Anything).Return(nil) + ctx, cancel := context.WithCancel(context.Background()) + go func() { + s.scheduler.Run(ctx) + }() + + <-time.Tick(time.Microsecond * 100) + cancel() + s.Len(s.hook.AllEntries(), 0) +} + +// --- Mocks --- + +// MockManagerInterface is an autogenerated mock type for the ManagerInterface type +type MockManagerInterface struct { + mock.Mock +} + +// Run provides a mock function with given fields: ctx +func (_m *MockManagerInterface) Run(ctx context.Context) error { + ret := _m.Called(ctx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/pkg/core/service.go b/pkg/core/service.go new file mode 100644 index 0000000..d49c1bb --- /dev/null +++ b/pkg/core/service.go @@ -0,0 +1,53 @@ +package core + +import ( + "context" + + "github.com/go-playground/validator/v10" + "github.com/spf13/cast" + "github.com/thoas/go-funk" +) + +var ( + validation = validator.New() +) + +type ( + Service struct { + Name string `json:"," validate:"required"` + Address string `json:"," validate:"required"` + ID *string `json:",omitempty"` + Port *int `json:",omitempty"` + Tags *[]string `json:",omitempty"` + Meta *map[string]string `json:",omitempty"` + } + Services []*Service +) + +func (s *Service) Validate(ctx context.Context) error { + return validation.StructCtx(ctx, s) +} + +func (s *Service) RegistrationID() string { + id := s.Name + if s.ID != nil { + id = *s.ID + } + return id +} + +func (s Services) IDs() []string { + ids := funk.Map(s, func(service *Service) string { + return service.RegistrationID() + }) + return cast.ToStringSlice(ids) +} + +func (s Services) Lookup(id string) *Service { + for _, service := range s { + if service.RegistrationID() == id { + return service + } + } + return nil +} diff --git a/pkg/core/service_test.go b/pkg/core/service_test.go new file mode 100644 index 0000000..698ad6d --- /dev/null +++ b/pkg/core/service_test.go @@ -0,0 +1,128 @@ +package core + +import ( + "context" + "testing" + + "github.com/agrea/ptr" + "github.com/stretchr/testify/suite" +) + +// --- Tests --- + +func TestService_Validate(t *testing.T) { + suite.Run(t, new(serviceValidateTestSuite)) +} + +func TestService_RegistrationID(t *testing.T) { + suite.Run(t, new(serviceRegistrationIDTestSuite)) +} + +func TestServices_IDs(t *testing.T) { + suite.Run(t, new(servicesIDsTestSuite)) +} + +func TestServices_Lookup(t *testing.T) { + suite.Run(t, new(servicesLookupSuite)) +} + +// --- Suites --- + +type serviceValidateTestSuite struct { + suite.Suite + service *Service +} + +func (s *serviceValidateTestSuite) SetupTest() { + s.service = new(Service) +} + +func (s *serviceValidateTestSuite) TestEmptyName() { + s.service.Address = `127.0.0.1` + s.Error(s.service.Validate(context.Background())) +} + +func (s *serviceValidateTestSuite) TestEmptyAddress() { + s.service.Name = `service` + s.Error(s.service.Validate(context.Background())) +} + +func (s *serviceValidateTestSuite) TestValidationPassed() { + s.service.Name = `service` + s.service.Address = `127.0.0.1` + s.NoError(s.service.Validate(context.Background())) +} + +type serviceRegistrationIDTestSuite struct { + suite.Suite + service *Service +} + +func (s *serviceRegistrationIDTestSuite) SetupTest() { + s.service = new(Service) +} + +func (s serviceRegistrationIDTestSuite) TestIDFromName() { + s.service.Name = `service` + s.Equal(s.service.Name, s.service.RegistrationID()) +} + +func (s serviceRegistrationIDTestSuite) TestIDFromID() { + s.service.Name = `service` + s.service.ID = ptr.String(`id`) + s.Equal(*s.service.ID, s.service.RegistrationID()) +} + +type servicesIDsTestSuite struct { + suite.Suite + services Services +} + +func (s *servicesIDsTestSuite) SetupTest() { + s.services = make(Services, 0) +} + +func (s *servicesIDsTestSuite) TestEmptyList() { + s.Empty(s.services.IDs()) +} + +func (s *servicesIDsTestSuite) TestNonEmptyList() { + s.services = append(s.services, &Service{Name: `name-1`}, &Service{ID: ptr.String(`id-1`)}) + ids := s.services.IDs() + s.Len(ids, 2) + s.Contains(ids, `name-1`) + s.Contains(ids, `id-1`) +} + +type servicesLookupSuite struct { + suite.Suite + services Services +} + +func (s *servicesLookupSuite) SetupTest() { + s.services = make(Services, 0) +} + +func (s *servicesLookupSuite) TestNilLookup() { + s.Nil(s.services.Lookup(`id`)) +} + +func (s *servicesLookupSuite) TestNotNilLookupByName() { + expected := &Service{ + Name: `name`, + } + s.services = append(s.services, expected) + got := s.services.Lookup(`name`) + s.NotNil(got) + s.Equal(expected, got) +} + +func (s *servicesLookupSuite) TestNotNilLookupByID() { + expected := &Service{ + ID: ptr.String(`id`), + } + s.services = append(s.services, expected) + got := s.services.Lookup(`id`) + s.NotNil(got) + s.Equal(expected, got) +} diff --git a/pkg/core/source.go b/pkg/core/source.go new file mode 100644 index 0000000..49200f2 --- /dev/null +++ b/pkg/core/source.go @@ -0,0 +1,13 @@ +package core + +import ( + "context" +) + +type ( + // Source provides information about Services to be registered in Registry. + // Source does not know about changes between Fetch calls, it must just return actual state of source. + Source interface { + Fetch(ctx context.Context) (Services, error) + } +) diff --git a/pkg/core/source/file/source.go b/pkg/core/source/file/source.go new file mode 100644 index 0000000..84a123c --- /dev/null +++ b/pkg/core/source/file/source.go @@ -0,0 +1,55 @@ +package file + +import ( + "context" + + "github.com/insidieux/pinchy/pkg/core" + "github.com/pkg/errors" + "gopkg.in/yaml.v3" +) + +type ( + Reader interface { + ReadFile(name string) ([]byte, error) + } + Path string + Source struct { + reader Reader + filename Path + logger core.LoggerInterface + } +) + +func NewSource(reader Reader, filename Path) *Source { + return &Source{ + reader: reader, + filename: filename, + } +} + +func (s *Source) Fetch(ctx context.Context) (core.Services, error) { + contents, err := s.reader.ReadFile(string(s.filename)) + if err != nil { + return nil, errors.Wrap(err, `failed read content from config file`) + } + + items := make([]*core.Service, 0) + if err := yaml.Unmarshal(contents, &items); err != nil { + return nil, errors.Wrap(err, `failed unmarshal content from config file`) + } + + result := make([]*core.Service, 0) + for _, item := range items { + if err := item.Validate(ctx); err != nil { + s.logger.Warningln(errors.Wrap(err, `failed to validate service`).Error()) + continue + } + result = append(result, item) + } + + return result, nil +} + +func (s *Source) WithLogger(logger core.LoggerInterface) { + s.logger = logger +} diff --git a/pkg/core/source/file/source_test.go b/pkg/core/source/file/source_test.go new file mode 100644 index 0000000..8817247 --- /dev/null +++ b/pkg/core/source/file/source_test.go @@ -0,0 +1,126 @@ +package file + +import ( + "context" + "testing" + + "github.com/insidieux/pinchy/pkg/core" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus/hooks/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/suite" + "gopkg.in/yaml.v3" +) + +// --- Tests --- + +func TestNewSource(t *testing.T) { + suite.Run(t, new(newSourceTestSuite)) +} + +func TestSource_Fetch(t *testing.T) { + suite.Run(t, new(sourceFetchTestSuite)) +} + +// --- Suites --- + +type newSourceTestSuite struct { + suite.Suite +} + +func (s *newSourceTestSuite) TestNewSource() { + got := NewSource(nil, `filename`) + s.Implements((*core.Source)(nil), got) + s.Equal(&Source{nil, `filename`, nil}, got) +} + +type sourceFetchTestSuite struct { + suite.Suite + source *Source + reader afero.Afero +} + +func (s *sourceFetchTestSuite) SetupTest() { + s.reader = afero.Afero{Fs: afero.NewMemMapFs()} + s.source = NewSource(s.reader, `filename`) +} + +func (s *sourceFetchTestSuite) TestErrorRead() { + services, err := s.source.Fetch(context.Background()) + s.Nil(services) + s.Error(err) + s.EqualError(err, `failed read content from config file: open filename: file does not exist`) +} + +func (s *sourceFetchTestSuite) TestErrorUnmarshal() { + inMemoryFile, err := s.reader.Create(string(s.source.filename)) + if err != nil { + panic(errors.Wrap(err, `failed to create in-memory file`)) + } + if _, err := inMemoryFile.WriteString(`{"key": "value"}`); err != nil { + panic(errors.Wrap(err, `failed to write to in-memory file`)) + } + + services, err := s.source.Fetch(context.Background()) + s.Nil(services) + s.Error(err) + s.Contains(err.Error(), `failed unmarshal content from config file`) +} + +func (s *sourceFetchTestSuite) TestSkipServiceValidationCase() { + inMemoryFile, err := s.reader.Create(string(s.source.filename)) + if err != nil { + panic(errors.Wrap(err, `failed to create in-memory file`)) + } + serviceBytes, err := yaml.Marshal(core.Services{ + { + Name: `service`, + }, + }) + if err != nil { + panic(errors.Wrap(err, `failed to marshal service`)) + } + if _, err := inMemoryFile.Write(serviceBytes); err != nil { + panic(errors.Wrap(err, `failed to write to in-memory file`)) + } + + logger, hook := test.NewNullLogger() + s.source.WithLogger(logger) + + services, err := s.source.Fetch(context.Background()) + s.NotNil(services) + s.NoError(err) + s.Equal(core.Services{}, services) + s.Equal(hook.LastEntry().Level, logrus.WarnLevel) + s.Contains(hook.LastEntry().Message, `failed to validate service`) +} + +func (s *sourceFetchTestSuite) TestSuccess() { + inMemoryFile, err := s.reader.Create(string(s.source.filename)) + if err != nil { + panic(errors.Wrap(err, `failed to create in-memory file`)) + } + expected := core.Services{ + { + Name: `service-1`, + Address: `127.0.0.1`, + }, + { + Name: `service-2`, + Address: `127.0.0.2`, + }, + } + servicesBytes, err := yaml.Marshal(expected) + if err != nil { + panic(errors.Wrap(err, `failed to marshal services`)) + } + if _, err := inMemoryFile.Write(servicesBytes); err != nil { + panic(errors.Wrap(err, `failed to write to in-memory file`)) + } + + services, err := s.source.Fetch(context.Background()) + s.NotNil(services) + s.NoError(err) + s.Equal(expected, services) +} diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/test/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore