├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── ci.yaml │ ├── jira-issues.yaml │ └── jira-pr.yaml ├── .gitignore ├── .go-version ├── .golangci.yml ├── LICENSE ├── README.md ├── discovery ├── acl.go ├── acl_test.go ├── addr.go ├── addr_test.go ├── config.go ├── config_test.go ├── discoverer.go ├── event.go ├── event_test.go ├── interceptor.go ├── interceptor_test.go ├── resolver.go ├── resolver_test.go ├── stats.go ├── time.go ├── time_test.go ├── watcher.go └── watcher_test.go ├── docs ├── conn-state-example.excalidraw.svg └── grpc-integration.md ├── go.mod ├── go.sum └── mocks ├── ACLServiceClient.go └── generate.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hashicorp/consul-core 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: gomod 7 | open-pull-requests-limit: 10 8 | directory: "/" 9 | labels: 10 | - "dependencies" 11 | schedule: 12 | interval: weekly 13 | - package-ecosystem: github-actions 14 | open-pull-requests-limit: 5 15 | directory: / 16 | labels: 17 | - "dependencies" 18 | schedule: 19 | interval: weekly 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | get-go-version: 8 | name: "Determine Go toolchain version" 9 | runs-on: ubuntu-latest 10 | outputs: 11 | go-version: ${{ steps.get-go-version.outputs.go-version }} 12 | steps: 13 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 14 | - name: Determine Go version 15 | id: get-go-version 16 | # We use .go-version as our source of truth for current Go 17 | # version, because "goenv" can react to it automatically. 18 | run: | 19 | echo "Building with Go $(cat .go-version)" 20 | echo "go-version=$(cat .go-version)" >> "${GITHUB_OUTPUT}" 21 | 22 | unit-tests: 23 | name: unit-tests (consul-version=${{ matrix.consul-version}}) 24 | needs: [get-go-version] 25 | runs-on: ubuntu-latest 26 | strategy: 27 | matrix: 28 | #TODO We should also be testing w/ latest dev here, and ensure 29 | # these versions are automatically updated as new releases become 30 | # available. 31 | consul-version: 32 | - 1.15.10 33 | - 1.16.6 34 | - 1.17.3 35 | - 1.18.1 36 | steps: 37 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 38 | - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 39 | - name: Install Consul 40 | run: | 41 | CONSUL_VERSION="${{ matrix.consul-version }}" 42 | FILENAME="consul_${CONSUL_VERSION}_linux_amd64.zip" 43 | curl -sSLO "https://releases.hashicorp.com/consul/${CONSUL_VERSION}/${FILENAME}" && \ 44 | unzip "${FILENAME}" -d /usr/local/bin && \ 45 | rm "${FILENAME}" 46 | consul version 47 | - name: Test 48 | run: go test ./... -race 49 | 50 | lint: 51 | name: lint 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 55 | - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 56 | - name: golangci-lint 57 | uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0 58 | with: 59 | version: v1.63.4 60 | 61 | # This is job is required for branch protection as a required GitHub check 62 | # because GitHub actions show up as checks at the job level and not the 63 | # workflow level. This is currently a feature request: 64 | # https://github.com/orgs/community/discussions/12395 65 | # 66 | # This job must: 67 | # - be placed after the fanout of a workflow so that everything fans back in 68 | # to this job. 69 | # - "need" any job that is part of the fan out / fan in 70 | # - include if: always() logic because we may have conditional jobs that this job 71 | # needs, and this would potentially get skipped if a previous job got skipped. 72 | # The if clause ensures it does not get skipped. 73 | test-success: 74 | needs: 75 | - lint 76 | - unit-tests 77 | runs-on: ubuntu-latest 78 | if: always() 79 | steps: 80 | - name: evaluate upstream job results 81 | run: | 82 | # exit 1 if failure or cancelled result for any upstream job 83 | # this ensures that we fail the PR check regardless of cancellation, rather than skip-passing it 84 | # see https://docs.github.com/en/actions/using-jobs/using-conditions-to-control-job-execution#overview 85 | if printf '${{ toJSON(needs) }}' | grep -E -i '\"result\": \"(failure|cancelled)\"'; then 86 | printf "Tests failed or workflow cancelled:\n\n${{ toJSON(needs) }}" 87 | exit 1 88 | fi 89 | -------------------------------------------------------------------------------- /.github/workflows/jira-issues.yaml: -------------------------------------------------------------------------------- 1 | # NET-9237 : these need to be re-written with maintained GitHub Actions. 2 | # Currently all of the atlassian/gajira-* actions are unmaintained for some years. 3 | 4 | # on: 5 | # issues: 6 | # types: [opened, closed, deleted, reopened] 7 | # issue_comment: 8 | # types: [created] 9 | # workflow_dispatch: 10 | 11 | # name: Jira Community Issue Sync 12 | 13 | # jobs: 14 | # sync: 15 | # runs-on: ubuntu-latest 16 | # name: Jira Community Issue sync 17 | # steps: 18 | # - name: Login 19 | # uses: atlassian/gajira-login@45fd029b9f1d6d8926c6f04175aa80c0e42c9026 # v3.0.1 20 | # env: 21 | # JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} 22 | # JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} 23 | # JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} 24 | 25 | # - name: Set ticket type 26 | # id: set-ticket-type 27 | # run: | 28 | # echo "TYPE=GH Issue" >> $GITHUB_OUTPUT 29 | 30 | # - name: Set ticket labels 31 | # if: github.event.action == 'opened' 32 | # id: set-ticket-labels 33 | # run: | 34 | # LABELS="[" 35 | # if [[ "${{ contains(github.event.issue.labels.*.name, 'type/bug') }}" == "true" ]]; then LABELS+="\"type/bug\", "; fi 36 | # if [[ "${{ contains(github.event.issue.labels.*.name, 'type/enhancement') }}" == "true" ]]; then LABELS+="\"type/enhancement\", "; fi 37 | # if [[ ${#LABELS} != 1 ]]; then LABELS=${LABELS::-2}"]"; else LABELS+="]"; fi 38 | # echo "LABELS=${LABELS}" >> $GITHUB_OUTPUT 39 | 40 | # - name: Create ticket if an issue is filed, or if PR not by a team member is opened 41 | # if: github.event.action == 'opened' 42 | # uses: tomhjp/gh-action-jira-create@3ed1789cad3521292e591a7cfa703215ec1348bf # v0.2.1 43 | # with: 44 | # project: NET 45 | # issuetype: "${{ steps.set-ticket-type.outputs.TYPE }}" 46 | # summary: "${{ github.event.repository.name }} [${{ steps.set-ticket-type.outputs.TYPE }} #${{ github.event.issue.number }}]: ${{ github.event.issue.title }}" 47 | # description: "${{ github.event.issue.body || github.event.pull_request.body }}\n\n_Created in GitHub by ${{ github.actor }}._" 48 | # # customfield_10089 is "Issue Link", customfield_10371 is "Source" (use JIRA API to retrieve) 49 | # extraFields: '{ "customfield_10089": "${{ github.event.issue.html_url || github.event.pull_request.html_url }}", 50 | # "customfield_10371": { "value": "GitHub" }, 51 | # "labels": ${{ steps.set-ticket-labels.outputs.LABELS }} }' 52 | # env: 53 | # JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} 54 | # JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} 55 | # JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} 56 | 57 | # - name: Search 58 | # if: github.event.action != 'opened' 59 | # id: search 60 | # uses: tomhjp/gh-action-jira-search@04700b457f317c3e341ce90da5a3ff4ce058f2fa # v0.2.2 61 | # with: 62 | # # cf[10089] is Issue Link (use JIRA API to retrieve) 63 | # jql: 'issuetype = "${{ steps.set-ticket-type.outputs.TYPE }}" and cf[10089] = "${{ github.event.issue.html_url || github.event.pull_request.html_url }}"' 64 | 65 | # - name: Sync comment 66 | # if: github.event.action == 'created' && steps.search.outputs.issue 67 | # uses: tomhjp/gh-action-jira-comment@6eb6b9ead70221916b6badd118c24535ed220bd9 # v0.2.0 68 | # with: 69 | # issue: ${{ steps.search.outputs.issue }} 70 | # comment: "${{ github.actor }} ${{ github.event.review.state || 'commented' }}:\n\n${{ github.event.comment.body || github.event.review.body }}\n\n${{ github.event.comment.html_url || github.event.review.html_url }}" 71 | 72 | # - name: Close ticket 73 | # if: ( github.event.action == 'closed' || github.event.action == 'deleted' ) && steps.search.outputs.issue 74 | # uses: atlassian/gajira-transition@38fc9cd61b03d6a53dd35fcccda172fe04b36de3 # v3.0.1 75 | # with: 76 | # issue: ${{ steps.search.outputs.issue }} 77 | # transition: "Closed" 78 | 79 | # - name: Reopen ticket 80 | # if: github.event.action == 'reopened' && steps.search.outputs.issue 81 | # uses: atlassian/gajira-transition@38fc9cd61b03d6a53dd35fcccda172fe04b36de3 # v3.0.1 82 | # with: 83 | # issue: ${{ steps.search.outputs.issue }} 84 | # transition: "To Do" 85 | -------------------------------------------------------------------------------- /.github/workflows/jira-pr.yaml: -------------------------------------------------------------------------------- 1 | # NET-9237 : these need to be re-written with maintained GitHub Actions. 2 | # Currently all of the atlassian/gajira-* actions are unmaintained for some years. 3 | 4 | # on: 5 | # pull_request_target: 6 | # types: [opened, closed, reopened] 7 | # workflow_dispatch: 8 | 9 | # name: Jira Community PR Sync 10 | 11 | # jobs: 12 | # sync: 13 | # runs-on: ubuntu-latest 14 | # name: Jira sync 15 | # steps: 16 | # - name: Login 17 | # uses: atlassian/gajira-login@45fd029b9f1d6d8926c6f04175aa80c0e42c9026 # v3.0.1 18 | # env: 19 | # JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} 20 | # JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} 21 | # JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} 22 | 23 | # - name: Set ticket type 24 | # id: set-ticket-type 25 | # run: | 26 | # echo "TYPE=GH Issue" >> $GITHUB_OUTPUT 27 | 28 | # - name: Set ticket labels 29 | # if: github.event.action == 'opened' 30 | # id: set-ticket-labels 31 | # run: | 32 | # LABELS="[" 33 | # if [[ "${{ contains(github.event.issue.labels.*.name, 'type/bug') }}" == "true" ]]; then LABELS+="\"type/bug\", "; fi 34 | # if [[ "${{ contains(github.event.issue.labels.*.name, 'type/enhancement') }}" == "true" ]]; then LABELS+="\"type/enhancement\", "; fi 35 | # if [[ ${#LABELS} != 1 ]]; then LABELS=${LABELS::-2}"]"; else LABELS+="]"; fi 36 | # echo "LABELS=${LABELS}" >> $GITHUB_OUTPUT 37 | 38 | # - name: Check if team member 39 | # if: github.event.action == 'opened' 40 | # id: is-team-member 41 | # run: | 42 | # TEAM=consul 43 | # ROLE="$(gh api orgs/hashicorp/teams/${TEAM}/memberships/${{ github.actor }} | jq -r '.role | select(.!=null)')" 44 | # if [[ -n ${ROLE} ]]; then 45 | # echo "Actor ${{ github.actor }} is a ${TEAM} team member" 46 | # echo "MESSAGE=true" >> $GITHUB_OUTPUT 47 | # else 48 | # echo "Actor ${{ github.actor }} is NOT a ${TEAM} team member" 49 | # echo "MESSAGE=false" >> $GITHUB_OUTPUT 50 | # fi 51 | # env: 52 | # GITHUB_TOKEN: ${{ secrets.JIRA_SYNC_GITHUB_TOKEN }} 53 | 54 | # - name: Create ticket if an issue is filed, or if PR not by a team member is opened 55 | # if: ( github.event.action == 'opened' && steps.is-team-member.outputs.MESSAGE == 'false' ) 56 | # uses: tomhjp/gh-action-jira-create@3ed1789cad3521292e591a7cfa703215ec1348bf # v0.2.1 57 | # with: 58 | # project: NET 59 | # issuetype: "${{ steps.set-ticket-type.outputs.TYPE }}" 60 | # summary: "${{ github.event.repository.name }} [${{ steps.set-ticket-type.outputs.TYPE }} #${{ github.event.pull_request.number }}]: ${{ github.event.pull_request.title }}" 61 | # description: "${{ github.event.issue.body || github.event.pull_request.body }}\n\n_Created in GitHub by ${{ github.actor }}._" 62 | # # customfield_10089 is "Issue Link", customfield_10371 is "Source" (use JIRA API to retrieve) 63 | # extraFields: '{ "customfield_10089": "${{ github.event.pull_request.html_url }}", 64 | # "customfield_10371": { "value": "GitHub" }, 65 | # "labels": ${{ steps.set-ticket-labels.outputs.LABELS }} }' 66 | # env: 67 | # JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} 68 | # JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} 69 | # JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} 70 | 71 | # - name: Search 72 | # if: github.event.action != 'opened' 73 | # id: search 74 | # uses: tomhjp/gh-action-jira-search@04700b457f317c3e341ce90da5a3ff4ce058f2fa # v0.2.2 75 | # with: 76 | # # cf[10089] is Issue Link (use JIRA API to retrieve) 77 | # jql: 'issuetype = "${{ steps.set-ticket-type.outputs.TYPE }}" and cf[10089] = "${{ github.event.issue.html_url || github.event.pull_request.html_url }}"' 78 | 79 | # - name: Sync comment 80 | # if: github.event.action == 'created' && steps.search.outputs.issue 81 | # uses: tomhjp/gh-action-jira-comment@6eb6b9ead70221916b6badd118c24535ed220bd9 # v0.2.0 82 | # with: 83 | # issue: ${{ steps.search.outputs.issue }} 84 | # comment: "${{ github.actor }} ${{ github.event.review.state || 'commented' }}:\n\n${{ github.event.comment.body || github.event.review.body }}\n\n${{ github.event.comment.html_url || github.event.review.html_url }}" 85 | 86 | # - name: Close ticket 87 | # if: ( github.event.action == 'closed' || github.event.action == 'deleted' ) && steps.search.outputs.issue 88 | # uses: atlassian/gajira-transition@38fc9cd61b03d6a53dd35fcccda172fe04b36de3 # v3.0.1 89 | # with: 90 | # issue: ${{ steps.search.outputs.issue }} 91 | # transition: "Closed" 92 | 93 | # - name: Reopen ticket 94 | # if: github.event.action == 'reopened' && steps.search.outputs.issue 95 | # uses: atlassian/gajira-transition@38fc9cd61b03d6a53dd35fcccda172fe04b36de3 # v3.0.1 96 | # with: 97 | # issue: ${{ steps.search.outputs.issue }} 98 | # transition: "To Do" 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .terraform/ 3 | .terraform.tfstate* 4 | .terraform.lock.hcl 5 | terraform.tfstate* 6 | terraform.tfvars 7 | values.dev.yaml 8 | bin/ 9 | pkg/ 10 | .idea/ 11 | .vscode 12 | .bob/ 13 | -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- 1 | 1.21.9 2 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | run: 5 | deadline: 5m 6 | 7 | linters-settings: 8 | depguard: 9 | rules: 10 | main: 11 | list-mode: lax 12 | allow: 13 | - "github.com/hashicorp/go-metrics/compat" 14 | deny: 15 | - pkg: "github.com/hashicorp/go-metrics" 16 | desc: not allowed, use github.com/hashicorp/go-metrics/compat instead 17 | - pkg: "github.com/armon/go-metrics" 18 | desc: not allowed, use github.com/hashicorp/go-metrics/compat instead 19 | 20 | linters: 21 | enable: 22 | - depguard 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 HashiCorp, Inc. 2 | 3 | Mozilla Public License Version 2.0 4 | ================================== 5 | 6 | 1. Definitions 7 | -------------- 8 | 9 | 1.1. "Contributor" 10 | means each individual or legal entity that creates, contributes to 11 | the creation of, or owns Covered Software. 12 | 13 | 1.2. "Contributor Version" 14 | means the combination of the Contributions of others (if any) used 15 | by a Contributor and that particular Contributor's Contribution. 16 | 17 | 1.3. "Contribution" 18 | means Covered Software of a particular Contributor. 19 | 20 | 1.4. "Covered Software" 21 | means Source Code Form to which the initial Contributor has attached 22 | the notice in Exhibit A, the Executable Form of such Source Code 23 | Form, and Modifications of such Source Code Form, in each case 24 | including portions thereof. 25 | 26 | 1.5. "Incompatible With Secondary Licenses" 27 | means 28 | 29 | (a) that the initial Contributor has attached the notice described 30 | in Exhibit B to the Covered Software; or 31 | 32 | (b) that the Covered Software was made available under the terms of 33 | version 1.1 or earlier of the License, but not also under the 34 | terms of a Secondary License. 35 | 36 | 1.6. "Executable Form" 37 | means any form of the work other than Source Code Form. 38 | 39 | 1.7. "Larger Work" 40 | means a work that combines Covered Software with other material, in 41 | a separate file or files, that is not Covered Software. 42 | 43 | 1.8. "License" 44 | means this document. 45 | 46 | 1.9. "Licensable" 47 | means having the right to grant, to the maximum extent possible, 48 | whether at the time of the initial grant or subsequently, any and 49 | all of the rights conveyed by this License. 50 | 51 | 1.10. "Modifications" 52 | means any of the following: 53 | 54 | (a) any file in Source Code Form that results from an addition to, 55 | deletion from, or modification of the contents of Covered 56 | Software; or 57 | 58 | (b) any new file in Source Code Form that contains any Covered 59 | Software. 60 | 61 | 1.11. "Patent Claims" of a Contributor 62 | means any patent claim(s), including without limitation, method, 63 | process, and apparatus claims, in any patent Licensable by such 64 | Contributor that would be infringed, but for the grant of the 65 | License, by the making, using, selling, offering for sale, having 66 | made, import, or transfer of either its Contributions or its 67 | Contributor Version. 68 | 69 | 1.12. "Secondary License" 70 | means either the GNU General Public License, Version 2.0, the GNU 71 | Lesser General Public License, Version 2.1, the GNU Affero General 72 | Public License, Version 3.0, or any later versions of those 73 | licenses. 74 | 75 | 1.13. "Source Code Form" 76 | means the form of the work preferred for making modifications. 77 | 78 | 1.14. "You" (or "Your") 79 | means an individual or a legal entity exercising rights under this 80 | License. For legal entities, "You" includes any entity that 81 | controls, is controlled by, or is under common control with You. For 82 | purposes of this definition, "control" means (a) the power, direct 83 | or indirect, to cause the direction or management of such entity, 84 | whether by contract or otherwise, or (b) ownership of more than 85 | fifty percent (50%) of the outstanding shares or beneficial 86 | ownership of such entity. 87 | 88 | 2. License Grants and Conditions 89 | -------------------------------- 90 | 91 | 2.1. Grants 92 | 93 | Each Contributor hereby grants You a world-wide, royalty-free, 94 | non-exclusive license: 95 | 96 | (a) under intellectual property rights (other than patent or trademark) 97 | Licensable by such Contributor to use, reproduce, make available, 98 | modify, display, perform, distribute, and otherwise exploit its 99 | Contributions, either on an unmodified basis, with Modifications, or 100 | as part of a Larger Work; and 101 | 102 | (b) under Patent Claims of such Contributor to make, use, sell, offer 103 | for sale, have made, import, and otherwise transfer either its 104 | Contributions or its Contributor Version. 105 | 106 | 2.2. Effective Date 107 | 108 | The licenses granted in Section 2.1 with respect to any Contribution 109 | become effective for each Contribution on the date the Contributor first 110 | distributes such Contribution. 111 | 112 | 2.3. Limitations on Grant Scope 113 | 114 | The licenses granted in this Section 2 are the only rights granted under 115 | this License. No additional rights or licenses will be implied from the 116 | distribution or licensing of Covered Software under this License. 117 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 118 | Contributor: 119 | 120 | (a) for any code that a Contributor has removed from Covered Software; 121 | or 122 | 123 | (b) for infringements caused by: (i) Your and any other third party's 124 | modifications of Covered Software, or (ii) the combination of its 125 | Contributions with other software (except as part of its Contributor 126 | Version); or 127 | 128 | (c) under Patent Claims infringed by Covered Software in the absence of 129 | its Contributions. 130 | 131 | This License does not grant any rights in the trademarks, service marks, 132 | or logos of any Contributor (except as may be necessary to comply with 133 | the notice requirements in Section 3.4). 134 | 135 | 2.4. Subsequent Licenses 136 | 137 | No Contributor makes additional grants as a result of Your choice to 138 | distribute the Covered Software under a subsequent version of this 139 | License (see Section 10.2) or under the terms of a Secondary License (if 140 | permitted under the terms of Section 3.3). 141 | 142 | 2.5. Representation 143 | 144 | Each Contributor represents that the Contributor believes its 145 | Contributions are its original creation(s) or it has sufficient rights 146 | to grant the rights to its Contributions conveyed by this License. 147 | 148 | 2.6. Fair Use 149 | 150 | This License is not intended to limit any rights You have under 151 | applicable copyright doctrines of fair use, fair dealing, or other 152 | equivalents. 153 | 154 | 2.7. Conditions 155 | 156 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 157 | in Section 2.1. 158 | 159 | 3. Responsibilities 160 | ------------------- 161 | 162 | 3.1. Distribution of Source Form 163 | 164 | All distribution of Covered Software in Source Code Form, including any 165 | Modifications that You create or to which You contribute, must be under 166 | the terms of this License. You must inform recipients that the Source 167 | Code Form of the Covered Software is governed by the terms of this 168 | License, and how they can obtain a copy of this License. You may not 169 | attempt to alter or restrict the recipients' rights in the Source Code 170 | Form. 171 | 172 | 3.2. Distribution of Executable Form 173 | 174 | If You distribute Covered Software in Executable Form then: 175 | 176 | (a) such Covered Software must also be made available in Source Code 177 | Form, as described in Section 3.1, and You must inform recipients of 178 | the Executable Form how they can obtain a copy of such Source Code 179 | Form by reasonable means in a timely manner, at a charge no more 180 | than the cost of distribution to the recipient; and 181 | 182 | (b) You may distribute such Executable Form under the terms of this 183 | License, or sublicense it under different terms, provided that the 184 | license for the Executable Form does not attempt to limit or alter 185 | the recipients' rights in the Source Code Form under this License. 186 | 187 | 3.3. Distribution of a Larger Work 188 | 189 | You may create and distribute a Larger Work under terms of Your choice, 190 | provided that You also comply with the requirements of this License for 191 | the Covered Software. If the Larger Work is a combination of Covered 192 | Software with a work governed by one or more Secondary Licenses, and the 193 | Covered Software is not Incompatible With Secondary Licenses, this 194 | License permits You to additionally distribute such Covered Software 195 | under the terms of such Secondary License(s), so that the recipient of 196 | the Larger Work may, at their option, further distribute the Covered 197 | Software under the terms of either this License or such Secondary 198 | License(s). 199 | 200 | 3.4. Notices 201 | 202 | You may not remove or alter the substance of any license notices 203 | (including copyright notices, patent notices, disclaimers of warranty, 204 | or limitations of liability) contained within the Source Code Form of 205 | the Covered Software, except that You may alter any license notices to 206 | the extent required to remedy known factual inaccuracies. 207 | 208 | 3.5. Application of Additional Terms 209 | 210 | You may choose to offer, and to charge a fee for, warranty, support, 211 | indemnity or liability obligations to one or more recipients of Covered 212 | Software. However, You may do so only on Your own behalf, and not on 213 | behalf of any Contributor. You must make it absolutely clear that any 214 | such warranty, support, indemnity, or liability obligation is offered by 215 | You alone, and You hereby agree to indemnify every Contributor for any 216 | liability incurred by such Contributor as a result of warranty, support, 217 | indemnity or liability terms You offer. You may include additional 218 | disclaimers of warranty and limitations of liability specific to any 219 | jurisdiction. 220 | 221 | 4. Inability to Comply Due to Statute or Regulation 222 | --------------------------------------------------- 223 | 224 | If it is impossible for You to comply with any of the terms of this 225 | License with respect to some or all of the Covered Software due to 226 | statute, judicial order, or regulation then You must: (a) comply with 227 | the terms of this License to the maximum extent possible; and (b) 228 | describe the limitations and the code they affect. Such description must 229 | be placed in a text file included with all distributions of the Covered 230 | Software under this License. Except to the extent prohibited by statute 231 | or regulation, such description must be sufficiently detailed for a 232 | recipient of ordinary skill to be able to understand it. 233 | 234 | 5. Termination 235 | -------------- 236 | 237 | 5.1. The rights granted under this License will terminate automatically 238 | if You fail to comply with any of its terms. However, if You become 239 | compliant, then the rights granted under this License from a particular 240 | Contributor are reinstated (a) provisionally, unless and until such 241 | Contributor explicitly and finally terminates Your grants, and (b) on an 242 | ongoing basis, if such Contributor fails to notify You of the 243 | non-compliance by some reasonable means prior to 60 days after You have 244 | come back into compliance. Moreover, Your grants from a particular 245 | Contributor are reinstated on an ongoing basis if such Contributor 246 | notifies You of the non-compliance by some reasonable means, this is the 247 | first time You have received notice of non-compliance with this License 248 | from such Contributor, and You become compliant prior to 30 days after 249 | Your receipt of the notice. 250 | 251 | 5.2. If You initiate litigation against any entity by asserting a patent 252 | infringement claim (excluding declaratory judgment actions, 253 | counter-claims, and cross-claims) alleging that a Contributor Version 254 | directly or indirectly infringes any patent, then the rights granted to 255 | You by any and all Contributors for the Covered Software under Section 256 | 2.1 of this License shall terminate. 257 | 258 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 259 | end user license agreements (excluding distributors and resellers) which 260 | have been validly granted by You or Your distributors under this License 261 | prior to termination shall survive termination. 262 | 263 | ************************************************************************ 264 | * * 265 | * 6. Disclaimer of Warranty * 266 | * ------------------------- * 267 | * * 268 | * Covered Software is provided under this License on an "as is" * 269 | * basis, without warranty of any kind, either expressed, implied, or * 270 | * statutory, including, without limitation, warranties that the * 271 | * Covered Software is free of defects, merchantable, fit for a * 272 | * particular purpose or non-infringing. The entire risk as to the * 273 | * quality and performance of the Covered Software is with You. * 274 | * Should any Covered Software prove defective in any respect, You * 275 | * (not any Contributor) assume the cost of any necessary servicing, * 276 | * repair, or correction. This disclaimer of warranty constitutes an * 277 | * essential part of this License. No use of any Covered Software is * 278 | * authorized under this License except under this disclaimer. * 279 | * * 280 | ************************************************************************ 281 | 282 | ************************************************************************ 283 | * * 284 | * 7. Limitation of Liability * 285 | * -------------------------- * 286 | * * 287 | * Under no circumstances and under no legal theory, whether tort * 288 | * (including negligence), contract, or otherwise, shall any * 289 | * Contributor, or anyone who distributes Covered Software as * 290 | * permitted above, be liable to You for any direct, indirect, * 291 | * special, incidental, or consequential damages of any character * 292 | * including, without limitation, damages for lost profits, loss of * 293 | * goodwill, work stoppage, computer failure or malfunction, or any * 294 | * and all other commercial damages or losses, even if such party * 295 | * shall have been informed of the possibility of such damages. This * 296 | * limitation of liability shall not apply to liability for death or * 297 | * personal injury resulting from such party's negligence to the * 298 | * extent applicable law prohibits such limitation. Some * 299 | * jurisdictions do not allow the exclusion or limitation of * 300 | * incidental or consequential damages, so this exclusion and * 301 | * limitation may not apply to You. * 302 | * * 303 | ************************************************************************ 304 | 305 | 8. Litigation 306 | ------------- 307 | 308 | Any litigation relating to this License may be brought only in the 309 | courts of a jurisdiction where the defendant maintains its principal 310 | place of business and such litigation shall be governed by laws of that 311 | jurisdiction, without reference to its conflict-of-law provisions. 312 | Nothing in this Section shall prevent a party's ability to bring 313 | cross-claims or counter-claims. 314 | 315 | 9. Miscellaneous 316 | ---------------- 317 | 318 | This License represents the complete agreement concerning the subject 319 | matter hereof. If any provision of this License is held to be 320 | unenforceable, such provision shall be reformed only to the extent 321 | necessary to make it enforceable. Any law or regulation which provides 322 | that the language of a contract shall be construed against the drafter 323 | shall not be used to construe this License against a Contributor. 324 | 325 | 10. Versions of the License 326 | --------------------------- 327 | 328 | 10.1. New Versions 329 | 330 | Mozilla Foundation is the license steward. Except as provided in Section 331 | 10.3, no one other than the license steward has the right to modify or 332 | publish new versions of this License. Each version will be given a 333 | distinguishing version number. 334 | 335 | 10.2. Effect of New Versions 336 | 337 | You may distribute the Covered Software under the terms of the version 338 | of the License under which You originally received the Covered Software, 339 | or under the terms of any subsequent version published by the license 340 | steward. 341 | 342 | 10.3. Modified Versions 343 | 344 | If you create software not governed by this License, and you want to 345 | create a new license for such software, you may create and use a 346 | modified version of this License if you rename the license and remove 347 | any references to the name of the license steward (except to note that 348 | such modified license differs from this License). 349 | 350 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 351 | Licenses 352 | 353 | If You choose to distribute Source Code Form that is Incompatible With 354 | Secondary Licenses under the terms of this version of the License, the 355 | notice described in Exhibit B of this License must be attached. 356 | 357 | Exhibit A - Source Code Form License Notice 358 | ------------------------------------------- 359 | 360 | This Source Code Form is subject to the terms of the Mozilla Public 361 | License, v. 2.0. If a copy of the MPL was not distributed with this 362 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 363 | 364 | If it is not possible or desirable to put the notice in a particular 365 | file, then You may include the notice in a location (such as a LICENSE 366 | file in a relevant directory) where a recipient would be likely to look 367 | for such a notice. 368 | 369 | You may add additional accurate notices of copyright ownership. 370 | 371 | Exhibit B - "Incompatible With Secondary Licenses" Notice 372 | --------------------------------------------------------- 373 | 374 | This Source Code Form is "Incompatible With Secondary Licenses", as 375 | defined by the Mozilla Public License, v. 2.0. 376 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://pkg.go.dev/badge/github.com/hashicorp/consul-server-connection-manager)](https://pkg.go.dev/github.com/hashicorp/consul-server-connection-manager) 2 | 3 | ## Summary 4 | 5 | This is a Go library used to connect to a [HashiCorp Consul](https://www.consul.io/) server. It 6 | implements server discovery and provides a gRPC connection to a Consul server. 7 | 8 | It was designed to maintain a current connection to a Consul server in absence of a Consul client 9 | agent. 10 | 11 | It supports the following: 12 | 13 | * Discovering Consul server addresses using 14 | [go-netaddrs](https://github.com/hashicorp/go-netaddrs) and Consul's [ServerWatch gRPC 15 | stream](https://github.com/hashicorp/consul/blob/main/proto-public/pbserverdiscovery/serverdiscovery.proto) 16 | * Connecting to a Consul server over gRPC 17 | * Automatic rediscovery and reconnection to another Consul server 18 | * Consul ACL token authentication 19 | * Compatibility with Consul server xDS load balancing 20 | * Optional custom server filtering 21 | 22 | ## Usage 23 | 24 | First, import the library: 25 | 26 | ```go 27 | import "github.com/hashicorp/consul-server-connection-manager/discovery" 28 | ``` 29 | 30 | The following shows how to configure and start a `Watcher`. The 31 | `Watcher` runs continually to discover Consul server addresses 32 | and to maintain a current gRPC connection to a Consul server. 33 | 34 | ```go 35 | watcher, err := discovery.NewWatcher( 36 | ctx, 37 | discovery.Config{ 38 | Addresses: "exec=./discover-ips.sh", 39 | GRPCPort: 8502, 40 | TLS: tlsCfg, 41 | Credentials: discovery.Credentials{ 42 | Static: discovery.StaticTokenCredential{ 43 | Token: testToken, 44 | }, 45 | }, 46 | }, 47 | hclog.New(&hclog.LoggerOptions{ 48 | Name: "server-connection-manager", 49 | }), 50 | ) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | 55 | // Start the Watcher. It runs continually to maintain a current gRPC connection 56 | // to one of the Consul servers. 57 | go watcher.Run() 58 | 59 | // Stop the Watcher when we are done. 60 | // This will close the gRPC connection for you. 61 | defer watcher.Stop() 62 | 63 | // Get initial state. This blocks until a Consul server is discovered and until 64 | // the Watcher has a successful connection to a Consul server. 65 | state, err := watcher.State() 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | 70 | // Now use the gRPC connection to initalize your gRPC clients. 71 | // This gRPC connection is valid as long as the Watcher is running. 72 | // The connection automatically switches to another Consul as needed. 73 | // If applicable, the ACL token is automatically injected into requests on the 74 | // connection. 75 | client := myclient.NewSampleClient(state.GRPCConn) 76 | ``` 77 | 78 | ### Usage for HTTP Clients 79 | 80 | The library does not currently integrate directly with HTTP clients, so 81 | you must rebuild or update your HTTP client when the Watcher switches to 82 | a new Consul server. 83 | 84 | The following shows how to subscribe to receive an update each time the Watcher 85 | switches to another Consul server. 86 | 87 | ```go 88 | // Configure and start the Watcher. 89 | watcher, err := discovery.NewWatcher(...) 90 | if err != nil { 91 | log.Fatal(err) 92 | } 93 | go watcher.Run() 94 | defer watcher.Stop() 95 | 96 | // Wait for initial state. 97 | state, err := watcher.State() 98 | if err != nil { 99 | log.Fatal(err) 100 | } 101 | 102 | // Sample function to create a Consul HTTP API client, 103 | // when using "github.com/hashicorp/consul/api". 104 | // 105 | // The state contains the server address and ACL token (if applicable). 106 | makeClient := func(s discovery.State) *api.Client { 107 | cfg := api.DefaultConfig() 108 | // Append the Consul HTTP(S) port (8500 or 8501) 109 | cfg.Address = fmt.Sprintf("%s:%d", s.Address, 8501) 110 | cfg.Token = s.Token 111 | return api.NewClient(cfg) 112 | } 113 | 114 | // Create a client the first time. 115 | client := makeClient(state) 116 | 117 | // Subscribe to the Watcher. This returns a channel that receives 118 | // a new discovery.State whenever the Watches connects to another 119 | // Consul server 120 | ch := watcher.Subscribe() 121 | 122 | // Monitor the channel and rebuild the client when needed 123 | for { 124 | select { 125 | case state := <-ch: 126 | client = makeClient(state) 127 | case <-ctx.Done(): 128 | log.Fatal(ctx.Err()) 129 | } 130 | } 131 | ``` 132 | 133 | ### Servers Behind a Load Balancer 134 | 135 | By default, the Watcher opens a 136 | [`WatchServers`](https://github.com/hashicorp/consul/blob/main/proto-public/pbserverdiscovery/serverdiscovery.proto) 137 | gRPC stream after connecting to a Consul server. This stream notifies us of new or changed Consul 138 | server addresses. 139 | 140 | The server watch stream should be disabled for cases where `WatchServers` returns different 141 | addresses than those we should connect to. For example, if your Consul servers are behind a load 142 | balancer, the library should connect to the load balancer address rather than directly to one of the 143 | Consul server addresses. 144 | 145 | When the server watch stream is disabled, the Watcher periodically makes a gRPC request to its 146 | current server to check if it can still connect to that server. By default, it makes a gRPC request 147 | once per minute, which can be changed by setting the `ServerWatchDisabledInterval`. 148 | 149 | ```go 150 | cfg := discovery.Config{ 151 | Addresses: "my.loadbalancer.example.com", 152 | // This disables use of the WatchServers stream. 153 | ServerWatchDisabled: true, 154 | // The following is the default value for a periodic connect to the server. 155 | ServerWatchDisabledInterval: 1 * time.Minute, 156 | } 157 | ``` 158 | 159 | #### Server Watch Unsupported 160 | 161 | Additionally, the `WatchServers` stream is not used if a particular Consul server does not support 162 | `WatchServers`. The Watcher determines feature support by fetching the [supported dataplane 163 | features](https://github.com/hashicorp/consul/blob/main/proto-public/pbdataplane/dataplane.proto) 164 | and checking for the `DATAPLANE_FEATURES_WATCH_SERVERS` feature in the response. 165 | 166 | When `WatchServers` is unsupported, the Watcher works the same as when `ServerWatchDisabled = true`. 167 | It periodically makes a gRPC request to check if it can still conect to the current server, at an 168 | interval controlled by the `ServerWatchDisabledInterval`. 169 | 170 | ### Server Filtering 171 | 172 | By default, the Watcher will choose any server at random. You can pass `ServerEvalFn` to have the 173 | Watcher ignore certain servers. This function is called each time the Watcher selects a new server. 174 | When the function returns false, the Watcher will ignore that server. 175 | 176 | A common reason to set `ServerEvalFn` is to filter servers based on supported dataplane features, 177 | for which you can use the `discovery.SupportsDataplaneFeatures` helper: 178 | 179 | ```go 180 | cfg := discovery.Config{ 181 | ServerEvalFn: discovery.SupportsDataplaneFeatures( 182 | pbdataplane.DataplaneFeatures_DATAPLANE_FEATURES_ENVOY_BOOTSTRAP_CONFIGURATION.String(), 183 | ), 184 | } 185 | ``` 186 | -------------------------------------------------------------------------------- /discovery/acl.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package discovery 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "sync" 11 | "time" 12 | 13 | "github.com/hashicorp/consul/proto-public/pbacl" 14 | "github.com/hashicorp/go-metrics/compat" 15 | "google.golang.org/grpc" 16 | ) 17 | 18 | var ( 19 | ErrAlreadyLoggedOut = errors.New("already logged out") 20 | ErrAlreadyLoggedIn = errors.New("already logged in") 21 | ) 22 | 23 | type ACLs struct { 24 | client pbacl.ACLServiceClient 25 | cfg LoginCredential 26 | 27 | // remember this for logout. 28 | token *pbacl.LoginToken 29 | clock Clock 30 | mu sync.Mutex 31 | } 32 | 33 | func newACLs(conn grpc.ClientConnInterface, config Config) *ACLs { 34 | return &ACLs{ 35 | client: pbacl.NewACLServiceClient(conn), 36 | cfg: config.Credentials.Login, 37 | clock: &SystemClock{}, 38 | } 39 | } 40 | 41 | func (a *ACLs) Login(ctx context.Context) (string, string, error) { 42 | a.mu.Lock() 43 | defer a.mu.Unlock() 44 | 45 | if a.token != nil { 46 | return "", "", ErrAlreadyLoggedIn 47 | } 48 | 49 | req := &pbacl.LoginRequest{ 50 | AuthMethod: a.cfg.AuthMethod, 51 | BearerToken: a.cfg.BearerToken, 52 | Meta: a.cfg.Meta, 53 | Namespace: a.cfg.Namespace, 54 | Partition: a.cfg.Partition, 55 | Datacenter: a.cfg.Datacenter, 56 | } 57 | start := a.clock.Now() 58 | resp, err := a.client.Login(ctx, req) 59 | if err != nil { 60 | return "", "", err 61 | } 62 | metrics.MeasureSince([]string{"login_duration"}, start) 63 | 64 | a.token = resp.GetToken() 65 | if a.token == nil || a.token.SecretId == "" { 66 | return "", "", fmt.Errorf("no secret id in response") 67 | } 68 | // TODO: We are prone to a negative caching problem that might cause "ACL not found" on 69 | // subsequent requests that use the token for the caching period (default 30s). See: 70 | // https://github.com/hashicorp/consul-k8s/pull/887 71 | // 72 | // A short sleep should mitigate some cases of the problem until we address this properly. 73 | a.clock.Sleep(100 * time.Millisecond) 74 | return a.token.AccessorId, a.token.SecretId, nil 75 | } 76 | 77 | func (a *ACLs) Logout(ctx context.Context) error { 78 | a.mu.Lock() 79 | defer a.mu.Unlock() 80 | 81 | if a.token == nil || a.token.SecretId == "" { 82 | return ErrAlreadyLoggedOut 83 | } 84 | _, err := a.client.Logout(ctx, &pbacl.LogoutRequest{ 85 | Token: a.token.SecretId, 86 | Datacenter: a.cfg.Datacenter, 87 | }) 88 | if err == nil { 89 | a.token = nil 90 | } 91 | return err 92 | } 93 | -------------------------------------------------------------------------------- /discovery/acl_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package discovery 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "testing" 10 | 11 | "github.com/hashicorp/consul-server-connection-manager/mocks" 12 | "github.com/hashicorp/consul/proto-public/pbacl" 13 | "github.com/stretchr/testify/mock" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestACLLoginLogout(t *testing.T) { 18 | cases := map[string]func(ACLMockFixture) ACLMockFixture{ 19 | "login and logout success": func(a ACLMockFixture) ACLMockFixture { 20 | return a 21 | }, 22 | "login error": func(a ACLMockFixture) ACLMockFixture { 23 | a.LoginErr = fmt.Errorf("mock login error") 24 | a.LogoutErr = ErrAlreadyLoggedOut 25 | // no logout request is be made if login failed (no token) 26 | a.ExpLogoutRequest = nil 27 | return a 28 | }, 29 | "logout error": func(a ACLMockFixture) ACLMockFixture { 30 | a.LogoutErr = fmt.Errorf("mock logout error") 31 | return a 32 | }, 33 | } 34 | for name, fn := range cases { 35 | fn := fn 36 | t.Run(name, func(t *testing.T) { 37 | fixture := fn(makeACLMockFixture("test-secret")) 38 | acls := fixture.SetupClientMock(t) 39 | ctx := context.Background() 40 | 41 | // Test Login. 42 | accessor, secret, err := acls.Login(ctx) 43 | if fixture.LoginErr != nil { 44 | require.Equal(t, fixture.LoginErr, err) 45 | require.Equal(t, accessor, "") 46 | require.Equal(t, secret, "") 47 | } else { 48 | require.NoError(t, err) 49 | require.Equal(t, "test-accessor", accessor) 50 | require.Equal(t, "test-secret", secret) 51 | require.NotNil(t, acls.token) 52 | } 53 | 54 | // Test Logout. 55 | err = acls.Logout(ctx) 56 | if fixture.LogoutErr != nil { 57 | require.Equal(t, fixture.LogoutErr, err) 58 | } else { 59 | require.NoError(t, err) 60 | require.Nil(t, acls.token) 61 | } 62 | }) 63 | } 64 | } 65 | 66 | // This holds the many values for mocking the ACL Login / Logout gRPC 67 | // requests. 68 | type ACLMockFixture struct { 69 | Config LoginCredential 70 | 71 | // Expected args and mocked return values for Login(ctx, req) gRPC request. 72 | // If ExpLoginRequest is nil, a Login call is not expected. 73 | ExpLoginRequest *pbacl.LoginRequest 74 | LoginResponse *pbacl.LoginResponse 75 | LoginErr error 76 | 77 | // Expected args and mocked return values for Logout(ctx, req) gRPC request. 78 | // If ExpLogoutRequest is nil, a Logout call is not expected. 79 | ExpLogoutRequest *pbacl.LogoutRequest 80 | LogoutResponse *pbacl.LogoutResponse 81 | LogoutErr error 82 | } 83 | 84 | // makeACLMockFixture mocks a successful login/logout request. You can modify 85 | // the returned value to create an unsuccessful request. 86 | func makeACLMockFixture(token string) ACLMockFixture { 87 | return ACLMockFixture{ 88 | Config: LoginCredential{ 89 | AuthMethod: "test-auth-method", 90 | BearerToken: "test-token", 91 | Namespace: "test-ns", 92 | Partition: "test-ptn", 93 | Datacenter: "test-dc", 94 | Meta: map[string]string{ 95 | "test": "meta", 96 | }, 97 | }, 98 | ExpLoginRequest: &pbacl.LoginRequest{ 99 | AuthMethod: "test-auth-method", 100 | BearerToken: "test-token", 101 | Meta: map[string]string{ 102 | "test": "meta", 103 | }, 104 | Namespace: "test-ns", 105 | Partition: "test-ptn", 106 | Datacenter: "test-dc", 107 | }, 108 | LoginResponse: &pbacl.LoginResponse{ 109 | Token: &pbacl.LoginToken{ 110 | AccessorId: "test-accessor", 111 | SecretId: token, 112 | }, 113 | }, 114 | 115 | ExpLogoutRequest: &pbacl.LogoutRequest{ 116 | Token: token, 117 | Datacenter: "test-dc", 118 | }, 119 | LogoutResponse: &pbacl.LogoutResponse{}, 120 | } 121 | } 122 | 123 | func (a ACLMockFixture) SetupClientMock(t *testing.T) *ACLs { 124 | ctx := mock.Anything 125 | client := mocks.NewACLServiceClient(t) 126 | if a.ExpLoginRequest != nil { 127 | client.Mock.On("Login", ctx, a.ExpLoginRequest).Return(a.LoginResponse, a.LoginErr) 128 | } 129 | if a.ExpLogoutRequest != nil { 130 | client.Mock.On("Logout", ctx, a.ExpLogoutRequest).Return(a.LogoutResponse, a.LogoutErr) 131 | } 132 | return &ACLs{client: client, cfg: a.Config, clock: &mockClock{}} 133 | } 134 | -------------------------------------------------------------------------------- /discovery/addr.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package discovery 5 | 6 | import ( 7 | "fmt" 8 | "net" 9 | "sort" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | type Addr struct { 15 | // Something with IP + Port 16 | net.TCPAddr 17 | } 18 | 19 | func MakeAddr(ipStr string, port int) (Addr, error) { 20 | ip := net.ParseIP(ipStr) 21 | if ip == nil { 22 | return Addr{}, fmt.Errorf("unable to parse ip: %q", ipStr) 23 | } 24 | return Addr{ 25 | TCPAddr: net.TCPAddr{ 26 | IP: ip, 27 | Port: port, 28 | }, 29 | }, nil 30 | } 31 | 32 | func (a Addr) String() string { 33 | return a.TCPAddr.String() 34 | } 35 | 36 | func (a Addr) Empty() bool { 37 | return len(a.TCPAddr.IP) == 0 38 | } 39 | 40 | // addrSet represents a set of addresses. For each address it can track the 41 | // last time the address was attempted and the last time the address was 42 | // "updated" (inserted into the set). 43 | type addrSet struct { 44 | sync.Mutex 45 | data map[string]*addrWithStatus 46 | clock Clock 47 | } 48 | 49 | type addrWithStatus struct { 50 | Addr 51 | // lastAttempt is the last time the address was ever attempted. This field 52 | // is generally preserved by the addrSet. 53 | lastAttempt time.Time 54 | // lastUpdate is the last time the address was inserted to the addrSet. This 55 | // is set when the addrSet is first constructed, and when calling SetAddrs. 56 | lastUpdate time.Time 57 | } 58 | 59 | // unattempted returns whether the address has been attempted since the last update. 60 | func (a *addrWithStatus) unattempted() bool { 61 | return a.lastAttempt.IsZero() || a.lastAttempt.Before(a.lastUpdate) 62 | } 63 | 64 | func newAddrSet(addrs ...Addr) *addrSet { 65 | a := &addrSet{ 66 | data: map[string]*addrWithStatus{}, 67 | clock: &SystemClock{}, 68 | } 69 | a.putNoLock(addrs...) 70 | return a 71 | } 72 | 73 | func (s *addrSet) putNoLock(addrs ...Addr) { 74 | for _, addr := range addrs { 75 | val, ok := s.data[addr.String()] 76 | if !ok { 77 | val = &addrWithStatus{Addr: addr} 78 | } 79 | // preserve lastAttempt 80 | val.lastUpdate = s.clock.Now() 81 | s.data[addr.String()] = val 82 | } 83 | } 84 | 85 | // SetAddrs replaces the existing addresses in this set with the given 86 | // addresses. This preserves the lastAttempt timestamp for any of the given 87 | // addresses that are already in the set. It sets lastUpdate to the current 88 | // time for each address. 89 | // 90 | // After calling this function, each address is considered unattempted until 91 | // the next call to SetAttemptTime. 92 | func (s *addrSet) SetAddrs(addrs ...Addr) { 93 | s.Lock() 94 | defer s.Unlock() 95 | 96 | old := s.data 97 | 98 | s.data = map[string]*addrWithStatus{} 99 | s.putNoLock(addrs...) 100 | 101 | // preserve lastAttempt timestamps 102 | for _, oldVal := range old { 103 | if newVal, ok := s.data[oldVal.String()]; ok { 104 | newVal.lastAttempt = oldVal.lastAttempt 105 | } 106 | } 107 | } 108 | 109 | func (s *addrSet) AllAttempted() bool { 110 | if s == nil { 111 | // no addrs, so all attempted 112 | return true 113 | } 114 | s.Lock() 115 | defer s.Unlock() 116 | 117 | for _, a := range s.data { 118 | if a.unattempted() { 119 | return false 120 | } 121 | } 122 | return true 123 | } 124 | 125 | // Sorted returns an ordered list of addresses. This sorts first by moving 126 | // unattempted addresses to the front, and then by the lastAttempt timestamp. 127 | func (s *addrSet) Sorted() []Addr { 128 | if s == nil { 129 | return nil 130 | } 131 | s.Lock() 132 | defer s.Unlock() 133 | 134 | return s.sortNoLock() 135 | } 136 | 137 | func (s *addrSet) sortNoLock() []Addr { 138 | result := make([]Addr, 0, len(s.data)) 139 | for _, a := range s.data { 140 | result = append(result, a.Addr) 141 | } 142 | 143 | sort.Slice(result, func(i, j int) bool { 144 | a := s.data[result[i].String()] 145 | b := s.data[result[j].String()] 146 | 147 | // Sort unattempted addresses to the front. 148 | if a.unattempted() != b.unattempted() { 149 | // If a != b then there are two cases: 150 | // a=true b=false --> return true 151 | // a=false b=true --> return false 152 | // Which simplifies to "return a" 153 | return a.unattempted() 154 | } 155 | // Otherwise, sort by lastAttempt. 156 | return a.lastAttempt.Before(b.lastAttempt) 157 | 158 | }) 159 | return result 160 | } 161 | 162 | func (s *addrSet) SetAttemptTime(addr Addr) { 163 | s.Lock() 164 | defer s.Unlock() 165 | 166 | s.data[addr.String()].lastAttempt = s.clock.Now() 167 | } 168 | 169 | func (s *addrSet) String() string { 170 | if s == nil { 171 | return "" 172 | } 173 | s.Lock() 174 | defer s.Unlock() 175 | 176 | return fmt.Sprint(s.sortNoLock()) 177 | } 178 | -------------------------------------------------------------------------------- /discovery/addr_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package discovery 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestMakeAddr(t *testing.T) { 13 | _, err := MakeAddr("", 0) 14 | require.Error(t, err) 15 | require.Equal(t, `unable to parse ip: ""`, err.Error()) 16 | 17 | _, err = MakeAddr("asdf", 0) 18 | require.Error(t, err) 19 | require.Equal(t, `unable to parse ip: "asdf"`, err.Error()) 20 | 21 | a, err := MakeAddr("127.0.0.1", 0) 22 | require.NoError(t, err) 23 | require.Equal(t, a.String(), "127.0.0.1:0") 24 | 25 | a, err = MakeAddr("127.0.0.1", 1234) 26 | require.NoError(t, err) 27 | require.Equal(t, a.String(), "127.0.0.1:1234") 28 | } 29 | 30 | func TestAddrEmpty(t *testing.T) { 31 | a := Addr{} 32 | require.True(t, a.Empty()) 33 | 34 | a, err := MakeAddr("127.0.0.1", 0) 35 | require.NoError(t, err) 36 | require.False(t, a.Empty()) 37 | } 38 | 39 | func TestAddrSetSort(t *testing.T) { 40 | addrs := []Addr{ 41 | mustMakeAddr(t, "127.0.0.1", 1), 42 | mustMakeAddr(t, "127.0.0.1", 2), 43 | mustMakeAddr(t, "127.0.0.1", 3), 44 | } 45 | 46 | set := newAddrSet(addrs...) 47 | 48 | // match in any order. 49 | require.ElementsMatch(t, addrs, set.Sorted()) 50 | 51 | // record the last attempt time. 52 | set.SetAttemptTime(addrs[1]) 53 | sorted := set.Sorted() 54 | require.ElementsMatch(t, addrs, sorted) 55 | require.Equal(t, addrs[1], sorted[2]) // last attempted address at the end 56 | 57 | set.SetAttemptTime(addrs[0]) 58 | require.Equal(t, []Addr{addrs[2], addrs[1], addrs[0]}, set.Sorted()) 59 | 60 | set.SetAttemptTime(addrs[2]) 61 | require.Equal(t, []Addr{addrs[1], addrs[0], addrs[2]}, set.Sorted()) 62 | 63 | // Try some other order 64 | set.SetAttemptTime(addrs[0]) 65 | set.SetAttemptTime(addrs[2]) 66 | set.SetAttemptTime(addrs[1]) 67 | require.Equal(t, []Addr{addrs[0], addrs[2], addrs[1]}, set.Sorted()) 68 | 69 | // Set the lastUpdate time. The lastAttempt timestamp should be preserved, 70 | // and the should sort in the same order as prior to Update. 71 | set.SetAddrs(addrs...) 72 | require.Equal(t, []Addr{addrs[0], addrs[2], addrs[1]}, set.Sorted()) 73 | } 74 | 75 | func TestAddrSetAllAttempted(t *testing.T) { 76 | var set *addrSet 77 | require.True(t, set.AllAttempted()) 78 | 79 | addrs := []Addr{ 80 | mustMakeAddr(t, "127.0.0.1", 1), 81 | mustMakeAddr(t, "127.0.0.1", 2), 82 | mustMakeAddr(t, "127.0.0.1", 3), 83 | } 84 | 85 | set = newAddrSet(addrs...) 86 | require.False(t, set.AllAttempted()) 87 | set.SetAttemptTime(addrs[2]) 88 | require.False(t, set.AllAttempted()) 89 | set.SetAttemptTime(addrs[1]) 90 | require.False(t, set.AllAttempted()) 91 | set.SetAttemptTime(addrs[0]) 92 | require.True(t, set.AllAttempted()) 93 | } 94 | -------------------------------------------------------------------------------- /discovery/config.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package discovery 5 | 6 | import ( 7 | "crypto/tls" 8 | "strings" 9 | "time" 10 | 11 | "github.com/cenkalti/backoff/v4" 12 | ) 13 | 14 | type ServerEvalFn func(State) bool 15 | 16 | type CredentialsType string 17 | 18 | const ( 19 | CredentialsTypeStatic CredentialsType = "static" 20 | CredentialsTypeLogin CredentialsType = "login" 21 | ) 22 | 23 | const ( 24 | DefaultServerWatchDisabledInterval = 1 * time.Minute 25 | 26 | DefaultBackOffInitialInterval = 500 * time.Millisecond 27 | DefaultBackOffMaxInterval = 60 * time.Second 28 | DefaultBackOffMultiplier = 1.5 29 | DefaultBackOffRandomizationFactor = 0.5 30 | DefaultBackOffResetInterval = 3 * DefaultBackOffMaxInterval 31 | ) 32 | 33 | type Config struct { 34 | // Addresses is a DNS name or exec command for go-netaddrs. 35 | Addresses string 36 | 37 | // GRPCPort is the gRPC port to connect to. This must be the 38 | // same for all Consul servers for now. Defaults to 8502. 39 | GRPCPort int 40 | 41 | // ServerWatchDisabled disables opening the ServerWatch gRPC stream. This 42 | // should be used when your Consul servers are behind a load balancer, for 43 | // example, since the server addresses returned in the ServerWatch stream 44 | // will differ from the load balancer address. 45 | ServerWatchDisabled bool 46 | 47 | // ServerWatchDisabledInterval is the amount of time to sleep if 48 | // ServerWatchDisabled=true or when connecting to a server that does not 49 | // support the server watch stream. When the Watcher wakes up, it will 50 | // check that the current server is still OK and then continue sleeping. If 51 | // the current server is not OK, then it will switch to another server. 52 | // 53 | // This defaults to 1 minute. 54 | ServerWatchDisabledInterval time.Duration 55 | 56 | // ServerEvalFn is optional. It can be used to exclude servers based on 57 | // custom criteria. If not nil, it is called after connecting to a server 58 | // but prior to marking the server "current". When this returns false, 59 | // the Watcher will skip the server. 60 | // 61 | // The State passed to this function will be valid. The GRPCConn will be 62 | // valid to use and DataplaneFeatures will be populated and the Address 63 | // and Token (if applicable) will be set. 64 | // 65 | // This is called synchronously in the same goroutine as Watcher.Run(), 66 | // so it should not block, or at least not for too long. 67 | // 68 | // To filter dataplane features, you can use the SupportsDataplaneFeatures 69 | // helper, `cfg.ServerEvalFn = SupportsDataplaneFeatures("")`. 70 | ServerEvalFn ServerEvalFn 71 | 72 | // TLS contains the TLS settings to use for the gRPC connections to the 73 | // Consul servers. By default this is nil, indicating that TLS is disabled. 74 | // 75 | // If unset, the ServerName field is automatically set if Addresses 76 | // contains a DNS hostname. The ServerName field is only set if TLS and TLS 77 | // verification are enabled. 78 | TLS *tls.Config 79 | Credentials Credentials 80 | 81 | BackOff BackOffConfig 82 | } 83 | 84 | func (c Config) withDefaults() Config { 85 | if c.ServerWatchDisabledInterval == 0 { 86 | c.ServerWatchDisabledInterval = DefaultServerWatchDisabledInterval 87 | } 88 | 89 | // Infer the ServerName field if a hostname is used in Addresses. 90 | if c.TLS != nil && !c.TLS.InsecureSkipVerify && c.TLS.ServerName == "" && !strings.HasPrefix(c.Addresses, "exec=") { 91 | c.TLS = c.TLS.Clone() 92 | c.TLS.ServerName = c.Addresses 93 | } 94 | 95 | c.BackOff = c.BackOff.withDefaults() 96 | 97 | return c 98 | } 99 | 100 | type Credentials struct { 101 | // Type is either "static" for a statically-configured ACL 102 | // token, or "login" to obtain an ACL token by logging into a 103 | // Consul auth method. 104 | Type CredentialsType 105 | 106 | // Static is used if Type is "static". 107 | Static StaticTokenCredential 108 | 109 | // Login is used if Type is "login". 110 | Login LoginCredential 111 | } 112 | 113 | type StaticTokenCredential struct { 114 | // Token is a static ACL token used for gRPC requests to the 115 | // Consul servers. 116 | Token string 117 | } 118 | 119 | type LoginCredential struct { 120 | // AuthMethod is the name of the Consul auth method. 121 | AuthMethod string 122 | // Namespace is the namespace containing the auth method. 123 | Namespace string 124 | // Partition is the partition containing the auth method. 125 | Partition string 126 | // Datacenter is the datacenter containing the auth method. 127 | Datacenter string 128 | // BearerToken is the bearer token presented to the auth method. 129 | BearerToken string 130 | // Meta is the arbitrary set of key-value pairs to attach to the 131 | // token. These are included in the Description field of the token. 132 | Meta map[string]string 133 | } 134 | 135 | type BackOffConfig struct { 136 | // InitialInterval is initial backoff retry interval for exponential backoff. Default: 500ms. 137 | InitialInterval time.Duration 138 | // Multiplier is the factor by which the backoff retry interval increases on each subsequent 139 | // retry. Default: 1.5. 140 | Multiplier float64 141 | // MaxInterval is the maximum backoff interval for exponential backoff. Default: 1m. 142 | MaxInterval time.Duration 143 | // RandomizationFactor randomizes the backoff retry interval using the formula: 144 | // RetryInterval * (random value in range [1-RandomizationFactor, 1+RandomizationFactor]) 145 | // Default: 0.5. 146 | RandomizationFactor float64 147 | // ResetInterval determines how long before resetting the backoff policy, If we are in a good 148 | // state for at least this interval, then exponential backoff is reset. Default: 3m. 149 | ResetInterval time.Duration 150 | } 151 | 152 | func (b BackOffConfig) withDefaults() BackOffConfig { 153 | if b.InitialInterval == 0 { 154 | b.InitialInterval = DefaultBackOffInitialInterval 155 | } 156 | if b.Multiplier == 0 { 157 | b.Multiplier = DefaultBackOffMultiplier 158 | } 159 | if b.MaxInterval == 0 { 160 | b.MaxInterval = DefaultBackOffMaxInterval 161 | } 162 | if b.RandomizationFactor == 0 { 163 | b.RandomizationFactor = DefaultBackOffRandomizationFactor 164 | } 165 | if b.ResetInterval == 0 { 166 | b.ResetInterval = DefaultBackOffResetInterval 167 | } 168 | return b 169 | } 170 | 171 | func (b BackOffConfig) getPolicy() backoff.BackOff { 172 | result := backoff.NewExponentialBackOff() 173 | result.InitialInterval = b.InitialInterval 174 | result.MaxInterval = b.MaxInterval 175 | result.Multiplier = b.Multiplier 176 | result.RandomizationFactor = b.RandomizationFactor 177 | // Backoff forever. 178 | result.MaxElapsedTime = 0 179 | return result 180 | } 181 | 182 | // SupportsDataplaneFeatures returns a ServerEvalFn that selects Consul servers 183 | // that support a list of given dataplane features. 184 | // 185 | // The following are dataplane feature name strings: 186 | // 187 | // "DATAPLANE_FEATURES_WATCH_SERVERS" 188 | // "DATAPLANE_FEATURES_EDGE_CERTIFICATE_MANAGEMENT" 189 | // "DATAPLANE_FEATURES_ENVOY_BOOTSTRAP_CONFIGURATION" 190 | // 191 | // See the hashicorp/consul/proto-public package for a up-to-date list. 192 | func SupportsDataplaneFeatures(names ...string) ServerEvalFn { 193 | return func(s State) bool { 194 | for _, name := range names { 195 | if !s.DataplaneFeatures[name] { 196 | return false 197 | } 198 | } 199 | return true 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /discovery/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package discovery 5 | 6 | import ( 7 | "crypto/tls" 8 | "testing" 9 | 10 | "github.com/cenkalti/backoff/v4" 11 | "github.com/google/go-cmp/cmp" 12 | "github.com/google/go-cmp/cmp/cmpopts" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestConfigDefaults(t *testing.T) { 17 | makeDefaultConfig := func() Config { 18 | return Config{ 19 | ServerWatchDisabledInterval: DefaultServerWatchDisabledInterval, 20 | BackOff: BackOffConfig{ 21 | InitialInterval: DefaultBackOffInitialInterval, 22 | Multiplier: DefaultBackOffMultiplier, 23 | MaxInterval: DefaultBackOffMaxInterval, 24 | RandomizationFactor: DefaultBackOffRandomizationFactor, 25 | ResetInterval: DefaultBackOffResetInterval, 26 | }, 27 | } 28 | } 29 | 30 | cases := map[string]struct { 31 | cfg Config 32 | // this modifies the "base" default config from makeDefaults() 33 | expCfgFn func(Config) Config 34 | }{ 35 | "default server watch disabled interval": { 36 | cfg: Config{}, 37 | expCfgFn: func(c Config) Config { return c }, 38 | }, 39 | "custom server watch disabled interval": { 40 | cfg: Config{ 41 | ServerWatchDisabledInterval: 1234, 42 | }, 43 | expCfgFn: func(c Config) Config { 44 | c.ServerWatchDisabledInterval = 1234 45 | return c 46 | }, 47 | }, 48 | "custom backoff config": { 49 | cfg: Config{ 50 | BackOff: BackOffConfig{ 51 | InitialInterval: 1, 52 | Multiplier: 2, 53 | MaxInterval: 3, 54 | RandomizationFactor: 4, 55 | ResetInterval: 5, 56 | }, 57 | }, 58 | expCfgFn: func(c Config) Config { 59 | c.BackOff.InitialInterval = 1 60 | c.BackOff.Multiplier = 2 61 | c.BackOff.MaxInterval = 3 62 | c.BackOff.RandomizationFactor = 4 63 | c.BackOff.ResetInterval = 5 64 | return c 65 | }, 66 | }, 67 | "infer tls server name": { 68 | cfg: Config{ 69 | Addresses: "my.host.name", 70 | TLS: &tls.Config{}, 71 | }, 72 | expCfgFn: func(c Config) Config { 73 | c.Addresses = "my.host.name" 74 | c.TLS = &tls.Config{ServerName: "my.host.name"} 75 | return c 76 | }, 77 | }, 78 | "infer tls server name when address is ip": { 79 | cfg: Config{ 80 | Addresses: "1.2.3.4", 81 | TLS: &tls.Config{}, 82 | }, 83 | expCfgFn: func(c Config) Config { 84 | c.Addresses = "1.2.3.4" 85 | c.TLS = &tls.Config{ServerName: "1.2.3.4"} 86 | return c 87 | }, 88 | }, 89 | "do not infer tls server name when address is exec command": { 90 | cfg: Config{ 91 | Addresses: "exec=./script.sh", 92 | TLS: &tls.Config{}, 93 | }, 94 | expCfgFn: func(c Config) Config { 95 | c.Addresses = "exec=./script.sh" 96 | c.TLS = &tls.Config{} 97 | return c 98 | }, 99 | }, 100 | "do not infer tls server name when TLS is disabled": { 101 | cfg: Config{ 102 | Addresses: "my.host.name", 103 | TLS: nil, 104 | }, 105 | expCfgFn: func(c Config) Config { 106 | c.Addresses = "my.host.name" 107 | return c 108 | }, 109 | }, 110 | "do not infer tls server name when TLS verification is disabled": { 111 | cfg: Config{ 112 | Addresses: "my.host.name", 113 | TLS: &tls.Config{InsecureSkipVerify: true}, 114 | }, 115 | expCfgFn: func(c Config) Config { 116 | c.Addresses = "my.host.name" 117 | c.TLS = &tls.Config{InsecureSkipVerify: true} 118 | return c 119 | }, 120 | }, 121 | "do not infer tls server name when already set": { 122 | cfg: Config{ 123 | Addresses: "my.host.name", 124 | TLS: &tls.Config{ServerName: "other.host"}, 125 | }, 126 | expCfgFn: func(c Config) Config { 127 | c.Addresses = "my.host.name" 128 | c.TLS = &tls.Config{ServerName: "other.host"} 129 | return c 130 | }, 131 | }, 132 | } 133 | for name, c := range cases { 134 | c := c 135 | t.Run(name, func(t *testing.T) { 136 | expCfg := c.expCfgFn(makeDefaultConfig()) 137 | require.Equal(t, expCfg, c.cfg.withDefaults()) 138 | }) 139 | } 140 | } 141 | 142 | func TestSupportsDataplaneFeatures(t *testing.T) { 143 | matchNone := SupportsDataplaneFeatures() 144 | match1And2 := SupportsDataplaneFeatures("feat1", "feat2") 145 | 146 | // No supported features. 147 | { 148 | state := State{} 149 | require.True(t, matchNone(state)) 150 | require.False(t, match1And2(state)) 151 | } 152 | 153 | // feat2 not supported. 154 | { 155 | state := State{ 156 | DataplaneFeatures: map[string]bool{ 157 | "feat1": true, 158 | "feat2": false, 159 | }, 160 | } 161 | require.True(t, matchNone(state)) 162 | require.False(t, match1And2(state)) 163 | } 164 | 165 | // Both feat1 and feat2 supported. 166 | { 167 | state := State{ 168 | DataplaneFeatures: map[string]bool{ 169 | "feat1": true, 170 | "feat2": true, 171 | }, 172 | } 173 | require.True(t, matchNone(state)) 174 | require.True(t, match1And2(state)) 175 | } 176 | } 177 | 178 | func TestGetBackoffPolicy(t *testing.T) { 179 | cfg := BackOffConfig{}.withDefaults() 180 | bo := cfg.getPolicy() 181 | 182 | exp := &backoff.ExponentialBackOff{ 183 | InitialInterval: DefaultBackOffInitialInterval, 184 | RandomizationFactor: DefaultBackOffRandomizationFactor, 185 | Multiplier: DefaultBackOffMultiplier, 186 | MaxInterval: DefaultBackOffMaxInterval, 187 | MaxElapsedTime: 0, 188 | Stop: backoff.Stop, 189 | Clock: backoff.SystemClock, 190 | } 191 | require.Empty(t, cmp.Diff(bo, exp, cmpopts.IgnoreUnexported(*exp))) 192 | } 193 | -------------------------------------------------------------------------------- /discovery/discoverer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package discovery 5 | 6 | import ( 7 | "context" 8 | "net" 9 | 10 | "github.com/hashicorp/go-hclog" 11 | "github.com/hashicorp/go-metrics/compat" 12 | "github.com/hashicorp/go-netaddrs" 13 | ) 14 | 15 | type Discoverer interface { 16 | Discover(ctx context.Context) ([]Addr, error) 17 | } 18 | 19 | type NetaddrsDiscoverer struct { 20 | config Config 21 | log hclog.Logger 22 | clock Clock 23 | } 24 | 25 | func NewNetaddrsDiscoverer(config Config, log hclog.Logger) *NetaddrsDiscoverer { 26 | return &NetaddrsDiscoverer{ 27 | config: config, 28 | log: log, 29 | clock: &SystemClock{}, 30 | } 31 | } 32 | 33 | func (n *NetaddrsDiscoverer) Discover(ctx context.Context) ([]Addr, error) { 34 | start := n.clock.Now() 35 | addrs, err := netaddrs.IPAddrs(ctx, n.config.Addresses, n.log) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | metrics.MeasureSince([]string{"discover_servers_duration"}, start) 41 | 42 | var result []Addr 43 | for _, addr := range addrs { 44 | result = append(result, Addr{ 45 | TCPAddr: net.TCPAddr{ 46 | IP: addr.IP, 47 | Port: n.config.GRPCPort, 48 | Zone: addr.Zone, 49 | }, 50 | }) 51 | } 52 | return result, nil 53 | } 54 | -------------------------------------------------------------------------------- /discovery/event.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package discovery 5 | 6 | import ( 7 | "context" 8 | "sync" 9 | ) 10 | 11 | // event is a one time event that can be marked done. 12 | type event struct { 13 | done chan struct{} 14 | once sync.Once 15 | } 16 | 17 | func newEvent() *event { 18 | return &event{ 19 | done: make(chan struct{}), 20 | } 21 | } 22 | 23 | // SetDone marks the event as done. 24 | // After this, Done() returns a closed channel. 25 | // This is safe to call multiple times, concurrently. 26 | func (e *event) SetDone() { 27 | e.once.Do(func() { 28 | close(e.done) 29 | }) 30 | } 31 | 32 | // Done returns a channel that is closed once this event is done. 33 | func (e *event) Done() <-chan struct{} { 34 | return e.done 35 | } 36 | 37 | // IsDone returns true if the event is done. 38 | func (e *event) IsDone() bool { 39 | select { 40 | case <-e.Done(): 41 | return true 42 | default: 43 | return false 44 | } 45 | } 46 | 47 | // Wait waits until either the event or context is done. 48 | func (e *event) Wait(ctx context.Context) error { 49 | select { 50 | case <-e.Done(): 51 | return nil 52 | case <-ctx.Done(): 53 | return ctx.Err() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /discovery/event_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package discovery 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestEvent(t *testing.T) { 15 | // check we can call these multiple times safely. 16 | e := newEvent() 17 | require.False(t, e.IsDone()) 18 | require.False(t, e.IsDone()) 19 | 20 | e.SetDone() 21 | require.True(t, e.IsDone()) 22 | 23 | e.SetDone() 24 | require.True(t, e.IsDone()) 25 | } 26 | 27 | func TestEventWait(t *testing.T) { 28 | e := newEvent() 29 | 30 | time.AfterFunc(50*time.Millisecond, e.SetDone) 31 | 32 | ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) 33 | t.Cleanup(cancel) 34 | require.NoError(t, e.Wait(ctx)) 35 | 36 | e = newEvent() 37 | ctx, cancel = context.WithTimeout(context.Background(), 50*time.Millisecond) 38 | t.Cleanup(cancel) 39 | 40 | require.Error(t, e.Wait(ctx)) 41 | } 42 | -------------------------------------------------------------------------------- /discovery/interceptor.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package discovery 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/go-metrics/compat" 10 | "google.golang.org/grpc" 11 | "google.golang.org/grpc/codes" 12 | "google.golang.org/grpc/metadata" 13 | "google.golang.org/grpc/status" 14 | ) 15 | 16 | const tokenField = "x-consul-token" 17 | 18 | func makeUnaryInterceptor(watcher *Watcher) grpc.UnaryClientInterceptor { 19 | return func( 20 | ctx context.Context, 21 | method string, 22 | req, reply interface{}, 23 | cc *grpc.ClientConn, 24 | invoker grpc.UnaryInvoker, 25 | opts ...grpc.CallOption, 26 | ) error { 27 | ctx = interceptContext(watcher, ctx) 28 | err := invoker(ctx, method, req, reply, cc, opts...) 29 | interceptError(watcher, err) 30 | return err 31 | } 32 | } 33 | 34 | func makeStreamInterceptor(watcher *Watcher) grpc.StreamClientInterceptor { 35 | return func( 36 | ctx context.Context, 37 | desc *grpc.StreamDesc, 38 | cc *grpc.ClientConn, 39 | method string, 40 | streamer grpc.Streamer, 41 | opts ...grpc.CallOption, 42 | ) (grpc.ClientStream, error) { 43 | ctx = interceptContext(watcher, ctx) 44 | stream, err := streamer(ctx, desc, cc, method, opts...) 45 | interceptError(watcher, err) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return &streamWrapper{ClientStream: stream, watcher: watcher}, nil 50 | } 51 | } 52 | 53 | type streamWrapper struct { 54 | grpc.ClientStream 55 | watcher *Watcher 56 | } 57 | 58 | func (s *streamWrapper) SendMsg(m interface{}) error { 59 | err := s.ClientStream.SendMsg(m) 60 | interceptError(s.watcher, err) 61 | return err 62 | } 63 | 64 | func (s *streamWrapper) RecvMsg(m interface{}) error { 65 | err := s.ClientStream.RecvMsg(m) 66 | interceptError(s.watcher, err) 67 | return err 68 | } 69 | 70 | // interceptContext will automatically include the ACL token on the context. 71 | func interceptContext(watcher *Watcher, ctx context.Context) context.Context { 72 | token, ok := watcher.token.Load().(string) 73 | if !ok || token == "" { 74 | // no token to add to context 75 | return ctx 76 | } 77 | 78 | // skip if there is already a token in the context 79 | if md, ok := metadata.FromOutgoingContext(ctx); ok { 80 | if tok := md.Get(tokenField); len(tok) > 0 { 81 | return ctx 82 | } 83 | } 84 | 85 | return metadata.AppendToOutgoingContext(ctx, tokenField, token) 86 | } 87 | 88 | // interceptError can automatically react to errors. 89 | func interceptError(watcher *Watcher, err error) { 90 | if err == nil { 91 | return 92 | } 93 | metrics.SetGauge([]string{"connection_errors"}, float32(1)) 94 | 95 | s, ok := status.FromError(err) 96 | if !ok { 97 | return 98 | } 99 | metrics.SetGaugeWithLabels([]string{"connection_errors"}, float32(1), []metrics.Label{{Name: "error_code", Value: s.Code().String()}}) 100 | 101 | if s.Code() == codes.ResourceExhausted { 102 | watcher.log.Debug("saw gRPC ResourceExhausted status code") 103 | watcher.requestServerSwitch() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /discovery/interceptor_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package discovery 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | "google.golang.org/grpc/metadata" 12 | ) 13 | 14 | func TestInterceptContext(t *testing.T) { 15 | makeMD := func() metadata.MD { 16 | return metadata.MD{tokenField: []string{"test-token"}} 17 | } 18 | 19 | cases := map[string]struct { 20 | // configure the watcher with this token 21 | watcherToken string 22 | // call interceptContext with this context 23 | context context.Context 24 | // expect a context back with this metadata 25 | expMD metadata.MD 26 | }{ 27 | "no watcher token": { 28 | context: metadata.NewOutgoingContext(context.Background(), metadata.MD{}), 29 | expMD: metadata.MD{}, 30 | }, 31 | "watcher has token": { 32 | watcherToken: "test-token", 33 | context: metadata.NewOutgoingContext(context.Background(), metadata.MD{}), 34 | expMD: makeMD(), 35 | }, 36 | "context already has token": { 37 | watcherToken: "other-token", 38 | context: metadata.NewOutgoingContext(context.Background(), makeMD()), 39 | expMD: makeMD(), 40 | }, 41 | } 42 | for name, c := range cases { 43 | c := c 44 | t.Run(name, func(t *testing.T) { 45 | watcher := &Watcher{} 46 | watcher.token.Store(c.watcherToken) 47 | 48 | ctx := interceptContext(watcher, c.context) 49 | md, ok := metadata.FromOutgoingContext(ctx) 50 | require.True(t, ok) 51 | require.Equal(t, c.expMD, md) 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /discovery/resolver.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package discovery 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/hashicorp/go-hclog" 10 | "google.golang.org/grpc/resolver" 11 | ) 12 | 13 | type watcherResolver struct { 14 | cc resolver.ClientConn 15 | log hclog.Logger 16 | } 17 | 18 | var _ resolver.Builder = (*watcherResolver)(nil) 19 | var _ resolver.Resolver = (*watcherResolver)(nil) 20 | 21 | func newResolver(log hclog.Logger) *watcherResolver { 22 | return &watcherResolver{log: log} 23 | } 24 | 25 | // Build implements resolver.Builder 26 | // 27 | // This is called by gRPC synchronously when we grpc.Dial. 28 | func (r *watcherResolver) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) { 29 | if r.cc != nil { 30 | return nil, fmt.Errorf("watcher does not support redialing") 31 | } 32 | r.cc = cc 33 | return r, nil 34 | } 35 | 36 | // Scheme implements resolver.Builder 37 | // 38 | // This enables us to dial grpc.Dial("consul://") when using this resolver. 39 | func (r *watcherResolver) Scheme() string { 40 | return "consul" 41 | } 42 | 43 | // SetAddress updates the connection to use the given address. 44 | // After this is called, the connection will eventually complete a graceful 45 | // switchover to the new address. 46 | func (r *watcherResolver) SetAddress(addr Addr) error { 47 | if r.cc == nil { 48 | // We shouldn't run into this, as long as we Dial prior to calling SetAddress. 49 | return fmt.Errorf("resolver missing ClientConn") 50 | } 51 | 52 | var addrs []resolver.Address 53 | if !addr.Empty() { 54 | addrs = append(addrs, resolver.Address{Addr: addr.String()}) 55 | } 56 | // In case we connect to a server, and then all servers go away, 57 | // support updating this to an empty list of addresses. 58 | err := r.cc.UpdateState(resolver.State{Addresses: addrs}) 59 | if err != nil { 60 | r.log.Debug("gRPC resolver failed to update connection address", "error", err) 61 | return err 62 | } 63 | return nil 64 | 65 | } 66 | 67 | // Close implements resolver.Resolver 68 | func (r *watcherResolver) Close() {} 69 | 70 | // ResolveNow implements resolver.Resolver 71 | // 72 | // "ResolveNow will be called by gRPC to try to resolve the target name 73 | // again. It's just a hint, resolver can ignore this if it's not necessary. 74 | // It could be called multiple times concurrently." 75 | func (r *watcherResolver) ResolveNow(_ resolver.ResolveNowOptions) {} 76 | -------------------------------------------------------------------------------- /discovery/resolver_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package discovery 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/hashicorp/go-hclog" 10 | "github.com/stretchr/testify/require" 11 | "google.golang.org/grpc/resolver" 12 | "google.golang.org/grpc/serviceconfig" 13 | ) 14 | 15 | func TestResolver(t *testing.T) { 16 | cc := &fakeClientConn{} 17 | 18 | rs := newResolver(hclog.NewNullLogger()) 19 | 20 | rs2, err := rs.Build(resolver.Target{}, cc, resolver.BuildOptions{}) 21 | require.NoError(t, err) 22 | 23 | // builder and resolver are the same instance 24 | require.Equal(t, rs, rs2) 25 | require.Equal(t, cc, rs.cc) 26 | 27 | addr, err := MakeAddr("127.0.0.1", 1234) 28 | require.NoError(t, err) 29 | 30 | // check that SetAddress passes addresses into the resolver's ClientConn 31 | err = rs.SetAddress(addr) 32 | require.NoError(t, err) 33 | require.Equal(t, []resolver.Address{{Addr: "127.0.0.1:1234"}}, cc.state.Addresses) 34 | 35 | addr.Port = 2345 36 | err = rs.SetAddress(addr) 37 | require.NoError(t, err) 38 | require.Equal(t, []resolver.Address{{Addr: "127.0.0.1:2345"}}, cc.state.Addresses) 39 | 40 | err = rs.SetAddress(Addr{}) 41 | require.NoError(t, err) 42 | require.Len(t, cc.state.Addresses, 0) 43 | } 44 | 45 | // fakeClientConn implements resolver.ClientConn for tests 46 | type fakeClientConn struct { 47 | state resolver.State 48 | } 49 | 50 | var _ resolver.ClientConn = (*fakeClientConn)(nil) 51 | 52 | func (f *fakeClientConn) UpdateState(state resolver.State) error { 53 | f.state = state 54 | return nil 55 | } 56 | 57 | func (*fakeClientConn) ReportError(error) {} 58 | func (*fakeClientConn) NewAddress(addresses []resolver.Address) {} 59 | func (*fakeClientConn) NewServiceConfig(serviceConfig string) {} 60 | func (*fakeClientConn) ParseServiceConfig(serviceConfigJSON string) *serviceconfig.ParseResult { 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /discovery/stats.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package discovery 5 | 6 | import "github.com/hashicorp/go-metrics/compat/prometheus" 7 | 8 | var Summaries = []prometheus.SummaryDefinition{ 9 | { 10 | Name: []string{"connect_duration"}, 11 | Help: "This will be a sample of the time it takes to get connected to a server. This duration will cover everything from making the server features request all the way through to opening an xDS session with a server", 12 | }, 13 | { 14 | Name: []string{"discover_servers_duration"}, 15 | Help: "This will be a sample of the time it takes to discover Consul server IPs.", 16 | }, 17 | { 18 | Name: []string{"login_duration"}, 19 | Help: "This will be a sample of the time it takes to login to Consul.", 20 | }, 21 | } 22 | 23 | var Gauges = []prometheus.GaugeDefinition{ 24 | { 25 | Name: []string{"consul_connected"}, 26 | Help: "This will either be 0 or 1 depending on whether the dataplane is currently connected to a Consul server.", 27 | }, 28 | { 29 | Name: []string{"connection_errors"}, 30 | Help: "This will track the number of errors encountered during the stream connection", 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /discovery/time.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package discovery 5 | 6 | import "time" 7 | 8 | type Clock interface { 9 | Now() time.Time 10 | After(d time.Duration) <-chan time.Time 11 | Sleep(d time.Duration) 12 | } 13 | 14 | type SystemClock struct{} 15 | 16 | var _ Clock = (*SystemClock)(nil) 17 | 18 | func (*SystemClock) After(d time.Duration) <-chan time.Time { 19 | return time.After(d) 20 | } 21 | 22 | func (*SystemClock) Now() time.Time { 23 | return time.Now() 24 | } 25 | 26 | func (*SystemClock) Sleep(d time.Duration) { 27 | time.Sleep(d) 28 | } 29 | -------------------------------------------------------------------------------- /discovery/time_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package discovery 5 | 6 | import ( 7 | "sync" 8 | "time" 9 | ) 10 | 11 | type mockClock struct { 12 | sync.Mutex 13 | 14 | currentTime time.Time 15 | 16 | // afterChans stores channels in order to mimic time.After. Each channnel 17 | // is stores with the timestamp when the channel should return. 18 | afterChans map[chan time.Time]time.Time 19 | } 20 | 21 | var _ Clock = (*mockClock)(nil) 22 | 23 | // You must call Sleep to advance the mock clock. 24 | func (f *mockClock) After(d time.Duration) <-chan time.Time { 25 | f.Lock() 26 | defer f.Unlock() 27 | 28 | ch := make(chan time.Time, 1) 29 | if d == 0 { 30 | // Channel will immediately return on 0 duration. 31 | ch <- f.currentTime 32 | close(ch) 33 | return ch 34 | } else { 35 | // Otherwise, handle the send at the next Tick 36 | f.afterChans[ch] = f.currentTime.Add(d) 37 | } 38 | return ch 39 | } 40 | 41 | func (f *mockClock) Now() time.Time { 42 | f.Lock() 43 | defer f.Unlock() 44 | 45 | return f.currentTime 46 | } 47 | 48 | func (f *mockClock) Sleep(d time.Duration) { 49 | f.Lock() 50 | defer f.Unlock() 51 | 52 | f.currentTime = f.currentTime.Add(d) 53 | 54 | // Send to channels returned by After() 55 | for ch, end := range f.afterChans { 56 | if f.currentTime.After(end) { 57 | // non-blocking send 58 | select { 59 | case ch <- f.currentTime: 60 | default: 61 | } 62 | 63 | close(ch) 64 | delete(f.afterChans, ch) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /discovery/watcher.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package discovery 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "sync" 11 | "sync/atomic" 12 | "time" 13 | 14 | "github.com/cenkalti/backoff/v4" 15 | "github.com/hashicorp/consul/proto-public/pbdataplane" 16 | "github.com/hashicorp/consul/proto-public/pbserverdiscovery" 17 | "github.com/hashicorp/go-hclog" 18 | "github.com/hashicorp/go-metrics/compat" 19 | "google.golang.org/grpc" 20 | backoff2 "google.golang.org/grpc/backoff" 21 | "google.golang.org/grpc/codes" 22 | "google.golang.org/grpc/credentials" 23 | "google.golang.org/grpc/credentials/insecure" 24 | "google.golang.org/grpc/keepalive" 25 | "google.golang.org/grpc/status" 26 | ) 27 | 28 | // State is the info a caller wants to know after initialization. 29 | type State struct { 30 | // GRPCConn is the gRPC connection shared with this library. Use 31 | // this to create your gRPC clients. The gRPC connection is 32 | // automatically updated to switch to a new server, so you can 33 | // use this connection for the lifetime of the associated 34 | // Watcher. 35 | GRPCConn *grpc.ClientConn 36 | 37 | // Token is the ACL token obtain from logging in (if applicable). 38 | // If login is not supported this will be set to the static token 39 | // from the Config object. 40 | Token string 41 | 42 | // Address is the address of current the Consul server the Watcher is using. 43 | Address Addr 44 | 45 | // DataplaneFeatures contains the dataplane features supported by the 46 | // current Consul server. 47 | DataplaneFeatures map[string]bool 48 | } 49 | 50 | type Watcher struct { 51 | // This is the "top-level" internal context. This is used to cancel the 52 | // Watcher's Run method, including the gRPC connection. 53 | ctx context.Context 54 | ctxCancel context.CancelFunc 55 | 56 | // This is an internal sub-context used to cancel operations in order to 57 | // switch servers. 58 | ctxForSwitch context.Context 59 | cancelForSwitch context.CancelFunc 60 | switchLock sync.Mutex 61 | 62 | config Config 63 | log hclog.Logger 64 | 65 | currentServer atomic.Value 66 | 67 | initComplete *event 68 | runComplete *event 69 | runOnce sync.Once 70 | 71 | conn *grpc.ClientConn 72 | token atomic.Value 73 | 74 | resolver *watcherResolver 75 | 76 | acls *ACLs 77 | 78 | subscribers []chan State 79 | subscriptionMutex sync.RWMutex 80 | 81 | // discoverer discovers IP addresses. In tests, we use mock this interface 82 | // to inject custom server ports. 83 | discoverer Discoverer 84 | // nodeToAddrFn parses and returns the address for the given Consul node 85 | // ID. In tets, we use this to inject custom server ports. 86 | nodeToAddrFn func(nodeID, addr string) (Addr, error) 87 | // clock provides time functions. Mocking this enables us to control time 88 | // for unit tests. 89 | clock Clock 90 | } 91 | 92 | type serverState struct { 93 | addr Addr 94 | dataplaneFeatures map[string]bool 95 | } 96 | 97 | func NewWatcher(ctx context.Context, config Config, log hclog.Logger) (*Watcher, error) { 98 | if log == nil { 99 | log = hclog.NewNullLogger() 100 | } 101 | 102 | config = config.withDefaults() 103 | 104 | w := &Watcher{ 105 | config: config, 106 | log: log, 107 | resolver: newResolver(log), 108 | discoverer: NewNetaddrsDiscoverer(config, log), 109 | initComplete: newEvent(), 110 | runComplete: newEvent(), 111 | nodeToAddrFn: func(_, addr string) (Addr, error) { 112 | return MakeAddr(addr, config.GRPCPort) 113 | }, 114 | clock: &SystemClock{}, 115 | } 116 | w.ctx, w.ctxCancel = context.WithCancel(ctx) 117 | w.currentServer.Store(serverState{}) 118 | w.token.Store("") 119 | 120 | var cred credentials.TransportCredentials 121 | if tls := w.config.TLS; tls != nil { 122 | cred = credentials.NewTLS(tls) 123 | } else { 124 | cred = insecure.NewCredentials() 125 | } 126 | dialOpts := []grpc.DialOption{ 127 | grpc.WithTransportCredentials(cred), 128 | grpc.WithUnaryInterceptor(makeUnaryInterceptor(w)), 129 | grpc.WithStreamInterceptor(makeStreamInterceptor(w)), 130 | // These keepalive parameters were chosen to match the existing behavior of 131 | // Consul agents [1]. 132 | // 133 | // Consul servers have a policy to terminate connections that send keepalive 134 | // pings more frequently than every 15 seconds [2] so we need to choose an 135 | // interval larger than that. 136 | // 137 | // Some users choose to front their Consul servers with an AWS Network Load 138 | // Balancer, which has a hard idle timeout of 350 seconds [3] so we need to 139 | // choose an interval smaller than that. 140 | // 141 | // 1. https://github.com/hashicorp/consul/blob/e6b55d1d81c6e90dd5d09e7dfb24d1db7604b7b5/agent/grpc-internal/client.go#L137-L151 142 | // 2. https://github.com/hashicorp/consul/blob/e6b55d1d81c6e90dd5d09e7dfb24d1db7604b7b5/agent/grpc-external/server.go#L44-L47 143 | // 3. https://docs.aws.amazon.com/elasticloadbalancing/latest/network/network-load-balancers.html#connection-idle-timeout 144 | grpc.WithKeepaliveParams(keepalive.ClientParameters{ 145 | Time: 30 * time.Second, 146 | Timeout: 30 * time.Second, 147 | }), 148 | // note: experimental apis 149 | grpc.WithResolvers(w.resolver), 150 | grpc.WithConnectParams(grpc.ConnectParams{ 151 | Backoff: backoff2.DefaultConfig, 152 | MinConnectTimeout: 10 * time.Second, 153 | }), 154 | // Note: We rely on the behavior of the default pick_first balancer [1] 155 | // to track the currently active address. Update loadBalancing policy with caution. 156 | // 157 | // [1]: https://github.com/grpc/grpc/blob/master/doc/load-balancing.md#pick_first 158 | } 159 | 160 | // Dial with "consul://" to trigger our custom resolver. We don't 161 | // provide a server address. The connection will be updated by the 162 | // Watcher via the custom resolver once an address is known. 163 | conn, err := grpc.DialContext(w.ctx, "consul://", dialOpts...) 164 | if err != nil { 165 | return nil, err 166 | } 167 | w.conn = conn 168 | 169 | return w, nil 170 | } 171 | 172 | func (w *Watcher) Subscribe() <-chan State { 173 | w.subscriptionMutex.Lock() 174 | defer w.subscriptionMutex.Unlock() 175 | 176 | ch := make(chan State, 1) 177 | w.subscribers = append(w.subscribers, ch) 178 | return ch 179 | } 180 | 181 | func (w *Watcher) notifySubscribers() { 182 | w.subscriptionMutex.RLock() 183 | defer w.subscriptionMutex.RUnlock() 184 | 185 | state := w.currentState() 186 | for _, ch := range w.subscribers { 187 | select { 188 | case ch <- state: 189 | // success 190 | default: 191 | // could not send; updated dropped! 192 | } 193 | } 194 | } 195 | 196 | // Run watches for Consul server set changes forever. Run should be called in a 197 | // goroutine. Run can be aborted by cancelling the context passed to NewWatcher 198 | // or by calling Stop. Call State after Run in order to wait for initialization 199 | // to complete. 200 | // 201 | // w, _ := NewWatcher(ctx, ...) 202 | // go w.Run() 203 | // state, err := w.State() 204 | func (w *Watcher) Run() { 205 | w.runOnce.Do(func() { 206 | defer w.runComplete.SetDone() 207 | w.run(w.config.BackOff.getPolicy(), w.nextServer) 208 | }) 209 | } 210 | 211 | // State returns the current state. This blocks for initialization to complete, 212 | // after which it will have found a Consul server, completed ACL token login 213 | // (if applicable), and retrieved supported dataplane features. 214 | // 215 | // Run must be called or State will never return. State can be aborted by 216 | // cancelling the context passed to NewWatcher or by calling Stop. 217 | func (w *Watcher) State() (State, error) { 218 | err := w.initComplete.Wait(w.ctx) 219 | if err != nil { 220 | return State{}, err 221 | } 222 | return w.currentState(), nil 223 | } 224 | 225 | func (w *Watcher) currentState() State { 226 | current := w.currentServer.Load().(serverState) 227 | return State{ 228 | GRPCConn: w.conn, 229 | Token: w.token.Load().(string), 230 | Address: current.addr, 231 | DataplaneFeatures: current.dataplaneFeatures, 232 | } 233 | } 234 | 235 | // Stop stops the Watcher after Run is called. This logs out of the auth method, 236 | // if applicable, waits for the Watcher's Run method to return, and closes the 237 | // gRPC connection. After calling Stop, the Watcher and the gRPC connection are 238 | // no longer valid to use. 239 | func (w *Watcher) Stop() { 240 | w.log.Info("stopping") 241 | 242 | // If applicable, attempt to log out. This must be done prior to the 243 | // connection being closed. We ignore errors since we must continue to shut 244 | // down. 245 | // 246 | // Do not use w.ctx which is likely already cancelled at this point. 247 | if w.acls != nil { 248 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 249 | defer cancel() 250 | 251 | err := w.acls.Logout(ctx) 252 | if err != nil { 253 | if err != ErrAlreadyLoggedOut { 254 | w.log.Error("ACL auth method logout failed", "error", err) 255 | } 256 | } else { 257 | w.log.Info("ACL auth method logout succeeded") 258 | } 259 | } 260 | 261 | // canceling the context will abort w.run() 262 | w.ctxCancel() 263 | // w.run() sets runComplete when it returns 264 | <-w.runComplete.Done() 265 | 266 | w.conn.Close() 267 | } 268 | 269 | func (w *Watcher) run(bo backoff.BackOff, nextServer func(*addrSet) error) { 270 | // addrs is the current set of servers we know about. 271 | addrs := newAddrSet() 272 | 273 | for { 274 | resetTime := w.clock.Now().Add(w.config.BackOff.ResetInterval) 275 | 276 | // Find and connect to a server. 277 | // 278 | // nextServer picks a server and tries to connect to it. It blocks 279 | // while connected to the server. It always returns an error on failure 280 | // or when disconnected. 281 | metrics.SetGauge([]string{"consul_connected"}, 0) 282 | w.log.Info("trying to connect to a Consul server") 283 | err := nextServer(addrs) 284 | if err != nil { 285 | logNonContextError(w.log, "connection error", err) 286 | } 287 | metrics.SetGauge([]string{"consul_connected"}, 0) 288 | 289 | // Reset the backoff if nextServer took longer than the reset interval. 290 | if w.clock.Now().After(resetTime) { 291 | bo.Reset() 292 | } 293 | 294 | // Retry with backoff. 295 | duration := bo.NextBackOff() 296 | if duration == backoff.Stop { 297 | // We should not hit this since we set MaxElapsedTime = 0. 298 | w.log.Warn("backoff stopped; aborting") 299 | return 300 | } 301 | 302 | w.log.Debug("backoff", "retry after", duration) 303 | select { 304 | case <-w.ctx.Done(): 305 | w.log.Debug("aborting", "error", w.ctx.Err()) 306 | return 307 | case <-w.clock.After(duration): 308 | } 309 | } 310 | } 311 | 312 | // nextServer does everything necessary to find and connect to a server. 313 | // It runs discovery, selects a server, connects to it, and then blocks 314 | // while it is connected, watching for server set changes. 315 | // 316 | // nextServer returns on any error, such as failure to connect or upon being 317 | // disconnected for any reason. It should always return with a non-nil erorr. 318 | func (w *Watcher) nextServer(addrs *addrSet) error { 319 | w.log.Trace("Watcher.nextServer", "addrs", addrs.String()) 320 | 321 | w.switchLock.Lock() 322 | w.ctxForSwitch, w.cancelForSwitch = context.WithCancel(w.ctx) 323 | w.switchLock.Unlock() 324 | start := w.clock.Now() 325 | 326 | defer func() { 327 | // If we return without picking a server, then clear the gRPC connection's 328 | // address list. This prevents gRPC from retrying the connection to the server 329 | // faster than our own exponential backoff. While the gRPC connection has an 330 | // empty list of addresses, callers will see an error like "resolver error: 331 | // produced zero addresses" from their gRPC clients. 332 | current := w.currentServer.Load().(serverState) 333 | if current.addr.Empty() { 334 | _ = w.resolver.SetAddress(Addr{}) 335 | } 336 | }() 337 | 338 | w.currentServer.Store(serverState{}) 339 | 340 | if addrs.AllAttempted() { 341 | // Re-run discovery. We've attempted all servers since the last time 342 | // addresses were updated (by discovery or by the server watch stream, 343 | // if enabled). Or, we have no addresses. 344 | // 345 | // We run discovery at these times: 346 | // - At startup: when we have no addresses. 347 | // - If the server watch is disabled: After attempting each address once. 348 | // - If the server watch is enabled: Only after attempting each address 349 | // once since the last successful update from the server watch stream. 350 | // If the server watch stream is enabled and generally healthy 351 | // then we will rarely ever re-run discovery. 352 | found, err := w.discoverer.Discover(w.ctx) 353 | if err != nil { 354 | return fmt.Errorf("failed to discover Consul server addresses: %w", err) 355 | } 356 | w.log.Info("discovered Consul servers", "addresses", found) 357 | 358 | // This preserves lastAttempt timestamps for addresses currently in 359 | // addrs, so that we try the least-recently attempted server. This 360 | // ensures we try all servers once before trying the same server twice. 361 | addrs.SetAddrs(found...) 362 | } 363 | 364 | // Choose a server as "current" and connect to it. 365 | // 366 | // Addresses are sorted first by unattempted, and then by lastAttempt timestamp 367 | // so that we always try the least-recently attempted address. 368 | sortedAddrs := addrs.Sorted() 369 | w.log.Info("current prioritized list of known Consul servers", "addresses", sortedAddrs) 370 | 371 | if !addrs.AllAttempted() { 372 | addr := sortedAddrs[0] 373 | addrs.SetAttemptTime(addr) 374 | 375 | server, err := w.connect(addr) 376 | if err != nil { 377 | // Return here in order to backoff between attempts to each server. 378 | return err 379 | } 380 | w.currentServer.Store(server) 381 | } 382 | 383 | current := w.currentServer.Load().(serverState) 384 | if current.addr.Empty() { 385 | return fmt.Errorf("unable to connect to a Consul server") 386 | } 387 | 388 | if eval := w.config.ServerEvalFn; eval != nil { 389 | state := w.currentState() 390 | if !eval(state) { 391 | w.currentServer.Store(serverState{}) 392 | return fmt.Errorf("skipping Consul server %q because ServerEvalFn returned false", state.Address.String()) 393 | } 394 | } 395 | metrics.MeasureSince([]string{"connect_duration"}, start) 396 | metrics.SetGauge([]string{"consul_connected"}, 1) 397 | 398 | w.log.Info("connected to Consul server", "address", current.addr) 399 | 400 | // Set init complete here. This indicates to Run() that initialization 401 | // completed: we found a server, have a token (if any), fetched dataplane 402 | // features, and the ServerEvalFn (if any) did not reject the server. 403 | w.initComplete.SetDone() 404 | 405 | w.notifySubscribers() 406 | 407 | // Wait forever while connected to this server, until an error or 408 | // disconnect for any reason. 409 | return w.watch(addrs) 410 | } 411 | 412 | // connect does initialization for the given address. This includes updating the 413 | // gRPC connection to use that address, doing the ACL token login (one time 414 | // only) and grabbing dataplane features for this server. 415 | func (w *Watcher) connect(addr Addr) (serverState, error) { 416 | w.log.Trace("Watcher.connect", "addr", addr) 417 | 418 | // Tell the gRPC connection to switch to the selected server. 419 | w.log.Debug("switching to Consul server", "address", addr) 420 | err := w.switchServer(addr) 421 | if err != nil { 422 | return serverState{}, fmt.Errorf("failed to switch to Consul server %q: %w", addr, err) 423 | } 424 | 425 | // One time, do the ACL token login. 426 | select { 427 | case <-w.initComplete.Done(): 428 | // already done 429 | default: 430 | if w.token.Load().(string) == "" { 431 | switch w.config.Credentials.Type { 432 | case CredentialsTypeStatic: 433 | w.token.Store(w.config.Credentials.Static.Token) 434 | case CredentialsTypeLogin: 435 | if w.acls == nil { 436 | w.acls = newACLs(w.conn, w.config) 437 | } 438 | accessorId, secretId, err := w.acls.Login(w.ctx) 439 | if err != nil { 440 | if err != ErrAlreadyLoggedIn { 441 | w.log.Error("ACL auth method login failed", "error", err) 442 | return serverState{}, err 443 | } 444 | } else { 445 | w.log.Info("ACL auth method login succeeded", "accessorID", accessorId) 446 | } 447 | w.token.Store(secretId) 448 | } 449 | } 450 | } 451 | 452 | // Fetch dataplane features for this server. 453 | features, err := w.getDataplaneFeatures() 454 | if err != nil { 455 | return serverState{}, err 456 | } 457 | 458 | for name, supported := range features { 459 | w.log.Debug("feature", "supported", supported, "name", name) 460 | } 461 | 462 | return serverState{addr: addr, dataplaneFeatures: features}, nil 463 | } 464 | 465 | // switchServer updates the gRPC connection to use the given server. 466 | // If switchServer returns without error, we are guaranteed that 467 | // subsequent gRPC requests will not use the old subconnection because 468 | // the [pick_first] balancer synchronously removes the subconnection 469 | // before establishing a new one. The subsequent request will block until 470 | // the new subconnection is ready. 471 | // 472 | // [pick_first]: https://github.com/grpc/grpc/blob/master/doc/load-balancing.md#pick_first 473 | func (w *Watcher) switchServer(to Addr) error { 474 | w.log.Trace("Watcher.switchServer", "to", to) 475 | w.switchLock.Lock() 476 | defer w.switchLock.Unlock() 477 | 478 | return w.resolver.SetAddress(to) 479 | } 480 | 481 | // requestServerSwitch requests a switch to some other server. This is safe to 482 | // call from other goroutines. It does not block to wait for the server switch. 483 | // 484 | // This works by canceling a context (w.ctxForSwitch) to abort certain types of 485 | // Watcher operations. This induces an error that causes the Watcher to 486 | // disconnect from it's current server and pick a new one, which is the same 487 | // logic as for any other type of error. This is kind of indirect, but also 488 | // nice in that we don't have to special case much here. 489 | func (w *Watcher) requestServerSwitch() { 490 | w.log.Trace("Watcher.requestServerSwitch") 491 | if !w.switchLock.TryLock() { 492 | // switch currently in progress. 493 | return 494 | } 495 | defer w.switchLock.Unlock() 496 | 497 | // interrupt the Watcher. 498 | if w.cancelForSwitch != nil { 499 | w.cancelForSwitch() 500 | } 501 | } 502 | 503 | func (w *Watcher) getDataplaneFeatures() (map[string]bool, error) { 504 | client := pbdataplane.NewDataplaneServiceClient(w.conn) 505 | resp, err := client.GetSupportedDataplaneFeatures(w.ctxForSwitch, &pbdataplane.GetSupportedDataplaneFeaturesRequest{}) 506 | if err != nil { 507 | return nil, fmt.Errorf("fetching supported dataplane features: %w", err) 508 | } 509 | 510 | // Translate features to a map, so that we don't have to pass gRPC 511 | // types back to users. 512 | features := map[string]bool{} 513 | for _, feat := range resp.SupportedDataplaneFeatures { 514 | nameStr := pbdataplane.DataplaneFeatures_name[int32(feat.FeatureName)] 515 | features[nameStr] = feat.GetSupported() 516 | } 517 | 518 | return features, nil 519 | } 520 | 521 | // watch blocks to wait for server set changes. It aborts on receiving an error 522 | // from the server, including when the Watcher's context is cancelled, and 523 | // including when the Watcher is told to switch servers. 524 | // 525 | // This updates addrs in place to add or remove servers found from the server 526 | // watch stream, if applicable. 527 | func (w *Watcher) watch(addrs *addrSet) error { 528 | current := w.currentServer.Load().(serverState) 529 | if current.dataplaneFeatures["DATAPLANE_FEATURES_WATCH_SERVERS"] && !w.config.ServerWatchDisabled { 530 | return w.watchStream(addrs) 531 | } else { 532 | return w.watchSleep() 533 | } 534 | } 535 | 536 | // watchStream opens a gRPC stream to receive server set changes. This blocks 537 | // potentially forever. It updates addrs in place, adding or removing servers 538 | // to match the response from the server watch stream. 539 | // 540 | // This may be aborted when the gRPC stream receives some error, when the 541 | // Watcher's context is cancelled, or when the Watcher is told to switch 542 | // servers. 543 | func (w *Watcher) watchStream(addrs *addrSet) error { 544 | w.log.Trace("Watcher.watchStream") 545 | client := pbserverdiscovery.NewServerDiscoveryServiceClient(w.conn) 546 | serverStream, err := client.WatchServers(w.ctxForSwitch, &pbserverdiscovery.WatchServersRequest{}) 547 | if err != nil { 548 | logNonContextError(w.log, "failed to open server watch stream", err) 549 | return err 550 | } 551 | 552 | for { 553 | // This blocks until there is a change from the server. 554 | resp, err := serverStream.Recv() 555 | if err != nil { 556 | return err 557 | } 558 | 559 | // Collect addresses from the stream. 560 | streamAddrs := []Addr{} 561 | for _, srv := range resp.Servers { 562 | addr, err := w.nodeToAddrFn(srv.Id, srv.Address) 563 | if err != nil { 564 | // ignore the server on failure. 565 | w.log.Warn("failed to parse server address from server watch stream; ignoring address", "error", err) 566 | continue 567 | } 568 | streamAddrs = append(streamAddrs, addr) 569 | } 570 | 571 | // Update the addrSet. This sets the lastUpdated timestamp for each address, 572 | // and removes servers not present in the server watch stream. 573 | addrs.SetAddrs(streamAddrs...) 574 | w.log.Info("updated known Consul servers from watch stream", "addresses", addrs.Sorted()) 575 | } 576 | } 577 | 578 | // watchSleep is used when the server watch stream is not supported. 579 | // It may be interrupted if we are told to switch servers. 580 | func (w *Watcher) watchSleep() error { 581 | w.log.Trace("Watcher.watchSleep", "interval", w.config.ServerWatchDisabledInterval) 582 | 583 | for { 584 | select { 585 | case <-w.ctxForSwitch.Done(): 586 | return w.ctxForSwitch.Err() 587 | case <-w.clock.After(w.config.ServerWatchDisabledInterval): 588 | } 589 | 590 | // is the server still OK? 591 | _, err := w.getDataplaneFeatures() 592 | if err != nil { 593 | return fmt.Errorf("failed to reach Consul server with server watch stream disabled: %w", err) 594 | } 595 | } 596 | } 597 | 598 | func logNonContextError(log hclog.Logger, msg string, err error, args ...interface{}) { 599 | args = append(args, "error", err) 600 | 601 | s, ok := status.FromError(err) 602 | if errors.Is(err, context.Canceled) || (ok && s.Code() == codes.Canceled) { 603 | log.Trace("context error: "+msg, args...) 604 | } else { 605 | log.Error(msg, args...) 606 | } 607 | } 608 | -------------------------------------------------------------------------------- /discovery/watcher_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package discovery 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "sync" 11 | "testing" 12 | "time" 13 | 14 | "github.com/cenkalti/backoff/v4" 15 | "github.com/hashicorp/consul/proto-public/pbdataplane" 16 | "github.com/hashicorp/consul/proto-public/pbserverdiscovery" 17 | "github.com/hashicorp/consul/sdk/testutil" 18 | "github.com/hashicorp/go-hclog" 19 | "github.com/stretchr/testify/require" 20 | ) 21 | 22 | const testServerManagementToken = "12345678-90ab-cdef-0000-12345678abcd" 23 | 24 | // TestRun starts a Consul server cluster and starts a Watcher. 25 | func TestRun(t *testing.T) { 26 | cases := map[string]struct { 27 | config Config 28 | serverConfigFn testutil.ServerConfigCallback 29 | testWithServerEvalFn bool 30 | }{ 31 | "no acls": { 32 | config: Config{}, 33 | testWithServerEvalFn: true, 34 | }, 35 | "static token": { 36 | config: Config{ 37 | Credentials: Credentials{ 38 | Type: CredentialsTypeStatic, 39 | Static: StaticTokenCredential{ 40 | Token: testServerManagementToken, 41 | }, 42 | }, 43 | }, 44 | serverConfigFn: enableACLsConfigFn, 45 | }, 46 | "auth method login": { 47 | config: Config{ 48 | Credentials: Credentials{ 49 | Type: CredentialsTypeLogin, 50 | // We mock the login/logout calls. 51 | Login: LoginCredential{ 52 | AuthMethod: "kubernetes", 53 | BearerToken: "fake-token", 54 | }, 55 | }, 56 | }, 57 | }, 58 | "server watch disabled": { 59 | config: Config{ 60 | ServerWatchDisabled: true, 61 | ServerWatchDisabledInterval: 1 * time.Second, 62 | }, 63 | testWithServerEvalFn: true, 64 | }, 65 | } 66 | for name, c := range cases { 67 | c := c 68 | 69 | ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) 70 | t.Cleanup(cancel) 71 | 72 | wasServerEvalFnCalled := false 73 | if c.testWithServerEvalFn { 74 | c.config.ServerEvalFn = func(state State) bool { 75 | require.NotNil(t, state.GRPCConn) 76 | require.NotEmpty(t, state.Address.String()) 77 | require.NotEmpty(t, state.DataplaneFeatures) 78 | wasServerEvalFnCalled = true 79 | return true 80 | } 81 | } 82 | 83 | n := name 84 | t.Run(name, func(t *testing.T) { 85 | t.Parallel() 86 | 87 | servers := startConsulServers(t, 3, c.serverConfigFn) 88 | 89 | w, err := NewWatcher(ctx, c.config, hclog.New(&hclog.LoggerOptions{ 90 | Name: fmt.Sprintf("watcher/%s", n), 91 | Level: hclog.Debug, 92 | })) 93 | require.NoError(t, err) 94 | 95 | // To test with local Consul servers, we inject custom server ports. The Config struct, 96 | // go-netaddrs, and the server watch stream do not support per-server ports. 97 | w.discoverer = servers 98 | w.nodeToAddrFn = servers.nodeToAddrFn 99 | 100 | // Mock the ACL Login / Logout gRPC calls to return this token. 101 | // This must be a real token since we have real (test) Consul servers. 102 | if c.config.Credentials.Type == CredentialsTypeLogin { 103 | aclFixture := makeACLMockFixture(testServerManagementToken) 104 | w.acls = aclFixture.SetupClientMock(t) 105 | } 106 | 107 | // Start the Watcher. 108 | subscribeChan := w.Subscribe() 109 | go w.Run() 110 | t.Cleanup(w.Stop) 111 | 112 | // Get initial state. This blocks until initialization is complete. 113 | initialState, err := w.State() 114 | require.NoError(t, err) 115 | 116 | // subscribe again for race detection 117 | _ = w.Subscribe() 118 | 119 | require.NotNil(t, initialState, initialState.GRPCConn) 120 | require.Contains(t, servers.servers, initialState.Address.String()) 121 | 122 | // Make sure the ServerEvalFn is called (or not). 123 | require.Equal(t, c.testWithServerEvalFn, wasServerEvalFnCalled) 124 | 125 | // Check we can also get state this way. 126 | require.Equal(t, initialState, receiveSubscribeState(t, ctx, subscribeChan)) 127 | 128 | // Check the token we get back. 129 | if c.config.Credentials.Type != "" { 130 | require.Equal(t, initialState.Token, testServerManagementToken) 131 | } else { 132 | require.Equal(t, initialState.Token, "") 133 | } 134 | 135 | unaryClient := pbdataplane.NewDataplaneServiceClient(initialState.GRPCConn) 136 | unaryRequest := func(t require.TestingT) { 137 | req := &pbdataplane.GetSupportedDataplaneFeaturesRequest{} 138 | resp, err := unaryClient.GetSupportedDataplaneFeatures(ctx, req) 139 | require.NoError(t, err, "error from unary request") 140 | require.NotNil(t, resp) 141 | } 142 | 143 | streamClient := pbserverdiscovery.NewServerDiscoveryServiceClient(initialState.GRPCConn) 144 | streamRequest := func(t require.TestingT) { 145 | // It seems like the stream will not automatically switch servers via the resolver. 146 | // It gets an address once when the stream is created. 147 | stream, err := streamClient.WatchServers(ctx, &pbserverdiscovery.WatchServersRequest{}) 148 | require.NoError(t, err, "opening stream") 149 | _, err = stream.Recv() 150 | require.NoError(t, err, "error from stream") 151 | } 152 | 153 | // Make a gRPC request to check that the gRPC connection is working. 154 | // This validates that the custom interceptor is injecting the ACL token. 155 | unaryRequest(t) 156 | streamRequest(t) 157 | 158 | servers.stopServer(t, initialState.Address) 159 | 160 | // Wait for the server switch. 161 | stateAfterStop := receiveSubscribeState(t, ctx, subscribeChan) 162 | require.NotEmpty(t, stateAfterStop.Address.String()) 163 | require.NotEqual(t, stateAfterStop.Address, initialState.Address) 164 | 165 | // Check we can also get state this way. 166 | state, err := w.State() 167 | require.Equal(t, stateAfterStop, state) 168 | 169 | // Check requests work. 170 | unaryRequest(t) 171 | streamRequest(t) 172 | 173 | // Tell the Watcher to switch servers. 174 | w.requestServerSwitch() 175 | stateAfterSwitch := receiveSubscribeState(t, ctx, subscribeChan) 176 | 177 | // Check the server changed. 178 | require.NoError(t, err) 179 | require.NotEmpty(t, stateAfterStop.Address.String()) 180 | require.NotEqual(t, stateAfterStop.Address, initialState.Address) 181 | 182 | // Check we can also get state this way. 183 | state, err = w.State() 184 | require.NoError(t, err) 185 | require.Equal(t, stateAfterSwitch, state) 186 | 187 | unaryRequest(t) 188 | streamRequest(t) 189 | 190 | t.Logf("test successful") 191 | }) 192 | } 193 | } 194 | 195 | func TestWatcherBackoffReset(t *testing.T) { 196 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 197 | t.Cleanup(cancel) 198 | 199 | clock := &mockClock{} 200 | bo := &fakeBackOff{} 201 | 202 | watcher := &Watcher{ 203 | ctx: ctx, 204 | ctxCancel: cancel, 205 | config: Config{ 206 | BackOff: BackOffConfig{ 207 | ResetInterval: 10 * time.Second, 208 | }, 209 | }, 210 | log: hclog.NewNullLogger(), 211 | clock: clock, 212 | } 213 | 214 | testEvents := []struct { 215 | tick time.Duration 216 | // note that backoff is reset following the call to nextServer 217 | expBackOffResets int 218 | }{ 219 | // ResetInterval is 10s. 220 | {1 * time.Second, 0}, // 1s <= 10s: BackOff not reset 221 | {9999 * time.Millisecond, 0}, // 9999ms <= 10s: BackOff not reset 222 | {10001 * time.Millisecond, 0}, // 10001ms > 10s: BackOff will be reset 223 | {1 * time.Second, 1}, // 1s <= 10s: BackOff not reset 224 | {10 * time.Second, 1}, // 10s <= 10s: BackOff not reset 225 | {11 * time.Second, 1}, // 11s > 10s: BackOff will be reset 226 | {0 * time.Second, 2}, 227 | } 228 | 229 | nextServerCount := 0 230 | fakeNextServer := func(*addrSet) error { 231 | if nextServerCount >= len(testEvents) { 232 | cancel() 233 | return fmt.Errorf("test over") 234 | } 235 | 236 | ev := testEvents[nextServerCount] 237 | require.Equal(t, ev.expBackOffResets, bo.resetCount) 238 | clock.Sleep(ev.tick) 239 | t.Logf("t=%v : tick %s, resets %d", clock.Now(), ev.tick, bo.resetCount) 240 | 241 | nextServerCount++ 242 | 243 | // we don't condition backoff reset on errors. 244 | return errors.New("next server error") 245 | } 246 | 247 | watcher.run(bo, fakeNextServer) 248 | // make sure fakeNextServer was called the correct number of times. 249 | require.Equal(t, nextServerCount, len(testEvents)) 250 | } 251 | 252 | type fakeBackOff struct { 253 | resetCount int 254 | } 255 | 256 | var _ backoff.BackOff = (*fakeBackOff)(nil) 257 | 258 | func (f *fakeBackOff) NextBackOff() time.Duration { 259 | return 0 260 | } 261 | 262 | func (f *fakeBackOff) Reset() { 263 | f.resetCount++ 264 | } 265 | 266 | func receiveSubscribeState(t *testing.T, ctx context.Context, ch <-chan State) State { 267 | select { 268 | case val := <-ch: 269 | return val 270 | case <-ctx.Done(): 271 | require.Failf(t, "failed to receive from channel", "error=%s", ctx.Err()) 272 | } 273 | return State{} 274 | } 275 | 276 | type consulServers struct { 277 | servers map[string]*testutil.TestServer 278 | sync.Mutex 279 | } 280 | 281 | // Implement a custom Discoverer to inject addresses with custom ports, so that 282 | // we can use multiple local Consul test servers. go-netaddrs doesn't support 283 | // per-server ports. 284 | var _ Discoverer = (*consulServers)(nil) 285 | 286 | func (c *consulServers) Discover(ctx context.Context) ([]Addr, error) { 287 | return c.grpcAddrs() 288 | } 289 | 290 | func (c *consulServers) grpcAddrs() ([]Addr, error) { 291 | c.Lock() 292 | defer c.Unlock() 293 | 294 | var addrs []Addr 295 | for _, srv := range c.servers { 296 | addr, err := MakeAddr(srv.Config.Bind, srv.Config.Ports.GRPC) 297 | if err != nil { 298 | return nil, err 299 | } 300 | addrs = append(addrs, addr) 301 | } 302 | return addrs, nil 303 | } 304 | 305 | func (c *consulServers) nodeToAddrFn(nodeID, addr string) (Addr, error) { 306 | c.Lock() 307 | defer c.Unlock() 308 | 309 | for _, srv := range c.servers { 310 | if srv.Config.NodeID == nodeID { 311 | return MakeAddr(addr, srv.Config.Ports.GRPC) 312 | } 313 | } 314 | return Addr{}, fmt.Errorf("no test server with node id: %q", nodeID) 315 | 316 | } 317 | 318 | func (c *consulServers) stopServer(t *testing.T, addr Addr) { 319 | c.Lock() 320 | defer c.Unlock() 321 | 322 | srv := c.servers[addr.String()] 323 | require.NotNil(t, srv, "no test server for address %s", addr) 324 | 325 | // remove the server so that we don't return it in subsequent discover/watch requests. 326 | delete(c.servers, addr.String()) 327 | 328 | _ = srv.Stop() 329 | } 330 | 331 | // startConsulServers starts a multi-server Consul test cluster. It returns a consulServers 332 | // struct containing the servers. 333 | func startConsulServers(t *testing.T, n int, cb testutil.ServerConfigCallback) *consulServers { 334 | require.Greater(t, n, 0) 335 | 336 | servers := map[string]*testutil.TestServer{} 337 | for i := 0; i < n; i++ { 338 | server, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) { 339 | c.Bootstrap = len(servers) == 0 340 | for _, srv := range servers { 341 | addr := fmt.Sprintf("%s:%d", srv.Config.Bind, srv.Config.Ports.SerfLan) 342 | c.RetryJoin = append(c.RetryJoin, addr) 343 | } 344 | c.LogLevel = "warn" 345 | if cb != nil { 346 | cb(c) 347 | } 348 | }) 349 | require.NoError(t, err) 350 | t.Cleanup(func() { 351 | _ = server.Stop() 352 | }) 353 | 354 | addr := mustMakeAddr(t, server.Config.Bind, server.Config.Ports.GRPC) 355 | servers[addr.String()] = server 356 | } 357 | 358 | for _, server := range servers { 359 | server.WaitForLeader(t) 360 | } 361 | return &consulServers{servers: servers} 362 | } 363 | 364 | func enableACLsConfigFn(c *testutil.TestServerConfig) { 365 | c.ACL.Enabled = true 366 | c.ACL.Tokens.InitialManagement = testServerManagementToken 367 | c.ACL.DefaultPolicy = "deny" 368 | } 369 | 370 | func mustMakeAddr(t *testing.T, addr string, port int) Addr { 371 | a, err := MakeAddr(addr, port) 372 | require.NoError(t, err) 373 | return a 374 | } 375 | -------------------------------------------------------------------------------- /docs/grpc-integration.md: -------------------------------------------------------------------------------- 1 | # Integration with `grpc-go` 2 | 3 | The explains some of how and why this library integrates closely with gRPC. 4 | 5 | The Watcher will sometimes need to switch from one Consul server to another. For example, it will 6 | switch to another Consul server if the current server is unreachable, or it is receiving errors 7 | from this Consul server, or if this Consul server told it to switch for xDS load balancing. 8 | 9 | The UX goal here is for the gRPC connection object to automatically and transparently update its 10 | address, so that callers of the library do not need to rebuild their gRPC clients. 11 | 12 | ## Custom gRPC resolver 13 | 14 | We use a custom gRPC resolver to update the address the gRPC connection is using. 15 | The Watcher tracks known addresses and uses the resolver to set a single address on the gRPC connection. 16 | When the gRPC connection is first created using `grpc.Dial`, it initially has no address: 17 | 18 | ```go 19 | conn, err := grpc.DialContext(w.ctx, "consul://", dialOpts...) 20 | ``` 21 | 22 | Having no address is a "valid" state for the connection. The Watcher also clears the connection's 23 | address list as part of the process of switching servers. Whenever the Watcher next discovers Consul 24 | server addresses and selects an address to connect to, it will update the gRPC connection with an 25 | address via the custom resolver. 26 | 27 | ## Connection switching 28 | 29 | When using the custom gRPC resolver, we set a new address for the connection to use. GRPC then 30 | handles the process of switching to the new address. After calling `resolver.ClientConn.UpdateState` 31 | to switch addresses the gRPC connection will gracefully close the existing sub-connection and open 32 | a new sub-connection for the new address. 33 | 34 | In order to track which address we are connected to, we pass at most one "intended" address to the resolver, 35 | so there is at most one active sub-connection at any time. 36 | 37 | The following example diagram shows the connection state transitions: 38 | 39 | ![Connection Flow Diagram](conn-state-example.excalidraw.svg) 40 | 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/consul-server-connection-manager 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/cenkalti/backoff/v4 v4.1.3 7 | github.com/google/go-cmp v0.5.9 8 | github.com/hashicorp/consul/proto-public v0.6.2 9 | github.com/hashicorp/consul/sdk v0.16.1 10 | github.com/hashicorp/go-hclog v1.5.0 11 | github.com/hashicorp/go-metrics v0.5.4 12 | github.com/hashicorp/go-netaddrs v0.1.0 13 | github.com/stretchr/testify v1.9.0 14 | google.golang.org/grpc v1.56.3 15 | ) 16 | 17 | require ( 18 | github.com/armon/go-metrics v0.4.1 // indirect 19 | github.com/beorn7/perks v1.0.1 // indirect 20 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 21 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 22 | github.com/fatih/color v1.16.0 // indirect 23 | github.com/golang/protobuf v1.5.4 // indirect 24 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 25 | github.com/hashicorp/go-immutable-radix v1.0.0 // indirect 26 | github.com/hashicorp/go-uuid v1.0.3 // indirect 27 | github.com/hashicorp/go-version v1.2.1 // indirect 28 | github.com/hashicorp/golang-lru v0.5.0 // indirect 29 | github.com/mattn/go-colorable v0.1.13 // indirect 30 | github.com/mattn/go-isatty v0.0.20 // indirect 31 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 32 | github.com/pkg/errors v0.9.1 // indirect 33 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 34 | github.com/prometheus/client_golang v1.11.1 // indirect 35 | github.com/prometheus/client_model v0.2.0 // indirect 36 | github.com/prometheus/common v0.26.0 // indirect 37 | github.com/prometheus/procfs v0.6.0 // indirect 38 | github.com/stretchr/objx v0.5.2 // indirect 39 | golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect 40 | golang.org/x/net v0.24.0 // indirect 41 | golang.org/x/sys v0.19.0 // indirect 42 | golang.org/x/text v0.14.0 // indirect 43 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect 44 | google.golang.org/protobuf v1.33.0 // indirect 45 | gopkg.in/yaml.v3 v3.0.1 // indirect 46 | ) 47 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= 3 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 4 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 5 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 6 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 7 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 8 | github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= 9 | github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= 10 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 11 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 12 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 13 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 14 | github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= 15 | github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= 16 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 17 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 18 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 19 | github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= 20 | github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= 21 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 24 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 26 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 27 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 28 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 29 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 30 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 31 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 32 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 33 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 34 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 35 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 36 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 37 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 38 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 39 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 40 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 41 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 42 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 43 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 44 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 45 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 46 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 47 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 48 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 49 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 50 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 51 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 52 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 53 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 54 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 55 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 56 | github.com/hashicorp/consul/proto-public v0.6.2 h1:+DA/3g/IiKlJZb88NBn0ZgXrxJp2NlvCZdEyl+qxvL0= 57 | github.com/hashicorp/consul/proto-public v0.6.2/go.mod h1:cXXbOg74KBNGajC+o8RlA502Esf0R9prcoJgiOX/2Tg= 58 | github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg= 59 | github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s= 60 | github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 61 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 62 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 63 | github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= 64 | github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 65 | github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= 66 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 67 | github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY= 68 | github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI= 69 | github.com/hashicorp/go-netaddrs v0.1.0 h1:TnlYvODD4C/wO+j7cX1z69kV5gOzI87u3OcUinANaW8= 70 | github.com/hashicorp/go-netaddrs v0.1.0/go.mod h1:33+a/emi5R5dqRspOuZKO0E+Tuz5WV1F84eRWALkedA= 71 | github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= 72 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 73 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 74 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 75 | github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= 76 | github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 77 | github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= 78 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 79 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 80 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 81 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 82 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 83 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 84 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 85 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 86 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 87 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 88 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 89 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 90 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 91 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 92 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 93 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 94 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 95 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 96 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 97 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 98 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 99 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 100 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 101 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 102 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 103 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 104 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 105 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 106 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 107 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 108 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 109 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 110 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 111 | github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= 112 | github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 113 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 114 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 115 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 116 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 117 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 118 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 119 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 120 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 121 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 122 | github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= 123 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 124 | github.com/prometheus/client_golang v1.11.1 h1:+4eQaD7vAZ6DsfsxB15hbE0odUjGI5ARs9yskGu1v4s= 125 | github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 126 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 127 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 128 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 129 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 130 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 131 | github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= 132 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 133 | github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= 134 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 135 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 136 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 137 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 138 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 139 | github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= 140 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 141 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 142 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 143 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 144 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 145 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 146 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 147 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 148 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 149 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 150 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 151 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 152 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 153 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 154 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 155 | github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= 156 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 157 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 158 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 159 | golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= 160 | golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= 161 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 162 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 163 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 164 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 165 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 166 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 167 | golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= 168 | golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= 169 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 170 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 171 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 172 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 173 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 174 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 175 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 176 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 177 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 178 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 179 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 180 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 181 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 182 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 183 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 184 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 185 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 186 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 187 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 188 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 189 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 190 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 191 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 192 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 193 | golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= 194 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 195 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 196 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 197 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 198 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 199 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 200 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 201 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 202 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= 203 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= 204 | google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= 205 | google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= 206 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 207 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 208 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 209 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 210 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 211 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 212 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 213 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 214 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 215 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 216 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 217 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 218 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 219 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 220 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 221 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 222 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 223 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 224 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 225 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 226 | -------------------------------------------------------------------------------- /mocks/ACLServiceClient.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.37.1. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | grpc "google.golang.org/grpc" 9 | 10 | mock "github.com/stretchr/testify/mock" 11 | 12 | pbacl "github.com/hashicorp/consul/proto-public/pbacl" 13 | ) 14 | 15 | // ACLServiceClient is an autogenerated mock type for the ACLServiceClient type 16 | type ACLServiceClient struct { 17 | mock.Mock 18 | } 19 | 20 | // Login provides a mock function with given fields: ctx, in, opts 21 | func (_m *ACLServiceClient) Login(ctx context.Context, in *pbacl.LoginRequest, opts ...grpc.CallOption) (*pbacl.LoginResponse, error) { 22 | _va := make([]interface{}, len(opts)) 23 | for _i := range opts { 24 | _va[_i] = opts[_i] 25 | } 26 | var _ca []interface{} 27 | _ca = append(_ca, ctx, in) 28 | _ca = append(_ca, _va...) 29 | ret := _m.Called(_ca...) 30 | 31 | var r0 *pbacl.LoginResponse 32 | var r1 error 33 | if rf, ok := ret.Get(0).(func(context.Context, *pbacl.LoginRequest, ...grpc.CallOption) (*pbacl.LoginResponse, error)); ok { 34 | return rf(ctx, in, opts...) 35 | } 36 | if rf, ok := ret.Get(0).(func(context.Context, *pbacl.LoginRequest, ...grpc.CallOption) *pbacl.LoginResponse); ok { 37 | r0 = rf(ctx, in, opts...) 38 | } else { 39 | if ret.Get(0) != nil { 40 | r0 = ret.Get(0).(*pbacl.LoginResponse) 41 | } 42 | } 43 | 44 | if rf, ok := ret.Get(1).(func(context.Context, *pbacl.LoginRequest, ...grpc.CallOption) error); ok { 45 | r1 = rf(ctx, in, opts...) 46 | } else { 47 | r1 = ret.Error(1) 48 | } 49 | 50 | return r0, r1 51 | } 52 | 53 | // Logout provides a mock function with given fields: ctx, in, opts 54 | func (_m *ACLServiceClient) Logout(ctx context.Context, in *pbacl.LogoutRequest, opts ...grpc.CallOption) (*pbacl.LogoutResponse, error) { 55 | _va := make([]interface{}, len(opts)) 56 | for _i := range opts { 57 | _va[_i] = opts[_i] 58 | } 59 | var _ca []interface{} 60 | _ca = append(_ca, ctx, in) 61 | _ca = append(_ca, _va...) 62 | ret := _m.Called(_ca...) 63 | 64 | var r0 *pbacl.LogoutResponse 65 | var r1 error 66 | if rf, ok := ret.Get(0).(func(context.Context, *pbacl.LogoutRequest, ...grpc.CallOption) (*pbacl.LogoutResponse, error)); ok { 67 | return rf(ctx, in, opts...) 68 | } 69 | if rf, ok := ret.Get(0).(func(context.Context, *pbacl.LogoutRequest, ...grpc.CallOption) *pbacl.LogoutResponse); ok { 70 | r0 = rf(ctx, in, opts...) 71 | } else { 72 | if ret.Get(0) != nil { 73 | r0 = ret.Get(0).(*pbacl.LogoutResponse) 74 | } 75 | } 76 | 77 | if rf, ok := ret.Get(1).(func(context.Context, *pbacl.LogoutRequest, ...grpc.CallOption) error); ok { 78 | r1 = rf(ctx, in, opts...) 79 | } else { 80 | r1 = ret.Error(1) 81 | } 82 | 83 | return r0, r1 84 | } 85 | 86 | // NewACLServiceClient creates a new instance of ACLServiceClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 87 | // The first argument is typically a *testing.T value. 88 | func NewACLServiceClient(t interface { 89 | mock.TestingT 90 | Cleanup(func()) 91 | }) *ACLServiceClient { 92 | mock := &ACLServiceClient{} 93 | mock.Mock.Test(t) 94 | 95 | t.Cleanup(func() { mock.AssertExpectations(t) }) 96 | 97 | return mock 98 | } 99 | -------------------------------------------------------------------------------- /mocks/generate.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package mocks 5 | 6 | import "github.com/hashicorp/consul/proto-public/pbacl" 7 | 8 | //go:generate mockery --srcpkg "github.com/hashicorp/consul/proto-public/pbacl" --name ACLServiceClient --output . 9 | 10 | var _ pbacl.ACLServiceClient = &ACLServiceClient{} 11 | --------------------------------------------------------------------------------