├── .changelog ├── 44.txt ├── 46.txt ├── 51.txt ├── 52.txt ├── 59.txt ├── changelog.tmpl └── note.tmpl ├── .github ├── CODEOWNERS └── workflows │ ├── build.yml │ ├── changelog-checker.yml │ ├── reusable-get-go-version.yml │ └── security-scan.yml ├── .gitignore ├── .go-version ├── .release ├── ci.hcl ├── release-metadata.hcl └── security-scan.hcl ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── catalog ├── aws.go ├── aws_test.go ├── consul.go ├── consul_test.go ├── service.go ├── service_test.go ├── sync.go └── sync_test.go ├── commands.go ├── go.mod ├── go.sum ├── internal └── flags │ ├── config.go │ ├── http.go │ ├── http_test.go │ ├── merge.go │ └── usage.go ├── main.go ├── scan.hcl ├── subcommand ├── auth.go ├── sync-catalog │ └── command.go └── version │ └── command.go └── version ├── VERSION └── version.go /.changelog/44.txt: -------------------------------------------------------------------------------- 1 | ```release-note:security 2 | Update dependencies to fix security vulnerabilities. 3 | Due to the large number of dependencies, the list of CVEs is not included in this release note. 4 | ``` -------------------------------------------------------------------------------- /.changelog/46.txt: -------------------------------------------------------------------------------- 1 | ```release-note:bug 2 | Fix a bug that would cause AWS instance discovery queries to fail if the CloudMap Namespace name was different from it's HTTP Name. 3 | This can happen when a CloudMap Namespace is re-created with the same Name. 4 | ``` 5 | -------------------------------------------------------------------------------- /.changelog/51.txt: -------------------------------------------------------------------------------- 1 | ```release-note:enhancement 2 | bump go version to 1.22.3. 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/52.txt: -------------------------------------------------------------------------------- 1 | ```release-note:enhancement 2 | bump go version to 1.22.4. 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/59.txt: -------------------------------------------------------------------------------- 1 | ```release-note:enhancement 2 | bump go version to 1.24.2. 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/changelog.tmpl: -------------------------------------------------------------------------------- 1 | {{- if index .NotesByType "breaking-change" -}} 2 | BREAKING CHANGES: 3 | 4 | {{range index .NotesByType "breaking-change" -}} 5 | * {{ template "note" . }} 6 | {{ end -}} 7 | {{- end -}} 8 | 9 | {{- if .NotesByType.security }} 10 | SECURITY: 11 | 12 | {{range .NotesByType.security -}} 13 | * {{ template "note" . }} 14 | {{ end -}} 15 | {{- end -}} 16 | 17 | {{- if .NotesByType.feature }} 18 | FEATURES: 19 | 20 | {{range .NotesByType.feature -}} 21 | * {{ template "note" . }} 22 | {{ end -}} 23 | {{- end -}} 24 | 25 | {{- $improvements := combineTypes .NotesByType.improvement .NotesByType.enhancement -}} 26 | {{- if $improvements }} 27 | IMPROVEMENTS: 28 | 29 | {{range $improvements | sort -}} 30 | * {{ template "note" . }} 31 | {{ end -}} 32 | {{- end -}} 33 | 34 | {{- if .NotesByType.deprecation }} 35 | DEPRECATIONS: 36 | 37 | {{range .NotesByType.deprecation -}} 38 | * {{ template "note" . }} 39 | {{ end -}} 40 | {{- end -}} 41 | 42 | {{- if .NotesByType.bug }} 43 | BUG FIXES: 44 | 45 | {{range .NotesByType.bug -}} 46 | * {{ template "note" . }} 47 | {{ end -}} 48 | {{- end -}} 49 | 50 | {{- if .NotesByType.note }} 51 | NOTES: 52 | 53 | {{range .NotesByType.note -}} 54 | * {{ template "note" . }} 55 | {{ end -}} 56 | {{- end -}} 57 | -------------------------------------------------------------------------------- /.changelog/note.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "note" -}} 2 | {{.Body}}{{if not (stringHasPrefix .Issue "_")}} [[GH-{{- .Issue -}}](https://github.com/hashicorp/consul/issues/{{- .Issue -}})]{{end}} 3 | {{- end -}} 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hashicorp/consul-selfmanage-maintainers 2 | 3 | # release configuration 4 | /.release/ @hashicorp/team-selfmanaged-releng 5 | /.github/workflows/build.yml @hashicorp/team-selfmanaged-releng 6 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | # We now default to running this workflow on every push to every branch. 4 | # This provides fast feedback when build issues occur, so they can be 5 | # fixed prior to being merged to the main branch. 6 | # 7 | # If you want to opt out of this, and only run the build on certain branches 8 | # please refer to the documentation on branch filtering here: 9 | # 10 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onpushbranchestagsbranches-ignoretags-ignore 11 | # 12 | on: [workflow_dispatch, push] 13 | 14 | env: 15 | PKG_NAME: "consul-aws" 16 | 17 | jobs: 18 | get-go-version: 19 | uses: ./.github/workflows/reusable-get-go-version.yml 20 | 21 | set-product-version: 22 | runs-on: ubuntu-latest 23 | outputs: 24 | product-version: ${{ steps.set-product-version.outputs.product-version }} 25 | product-base-version: ${{ steps.set-product-version.outputs.base-product-version }} 26 | product-prerelease-version: ${{ steps.set-product-version.outputs.prerelease-product-version }} 27 | product-minor-version: ${{ steps.set-product-version.outputs.minor-product-version }} 28 | steps: 29 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 30 | - name: Set Product version 31 | id: set-product-version 32 | uses: hashicorp/actions-set-product-version@v2 33 | 34 | generate-metadata-file: 35 | needs: set-product-version 36 | runs-on: ubuntu-latest 37 | outputs: 38 | filepath: ${{ steps.generate-metadata-file.outputs.filepath }} 39 | steps: 40 | - name: "Checkout directory" 41 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 42 | - name: Generate metadata file 43 | id: generate-metadata-file 44 | uses: hashicorp/actions-generate-metadata@v1 45 | with: 46 | version: ${{ needs.set-product-version.outputs.product-version }} 47 | product: ${{ env.PKG_NAME }} 48 | repositoryOwner: "hashicorp" 49 | - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 50 | with: 51 | name: metadata.json 52 | path: ${{ steps.generate-metadata-file.outputs.filepath }} 53 | 54 | build-other: 55 | needs: 56 | - get-go-version 57 | - set-product-version 58 | runs-on: ubuntu-latest 59 | strategy: 60 | fail-fast: false # recommended during development 61 | matrix: 62 | goos: [freebsd, windows, netbsd, openbsd, solaris] 63 | goarch: ["386", "amd64", "arm"] 64 | exclude: 65 | - goos: solaris 66 | goarch: 386 67 | - goos: solaris 68 | goarch: arm 69 | - goos: windows 70 | goarch: arm 71 | 72 | name: Go ${{ needs.get-go-version.outputs.go-version }} ${{ matrix.goos }} ${{ matrix.goarch }} build 73 | 74 | steps: 75 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 76 | 77 | - uses: hashicorp/actions-go-build@v1 78 | env: 79 | BASE_VERSION: ${{ needs.set-product-version.outputs.product-base-version }} 80 | PRERELEASE_VERSION: ${{ needs.set-product-version.outputs.product-prerelease-version}} 81 | METADATA_VERSION: ${{ env.METADATA }} 82 | with: 83 | product_name: ${{ env.PKG_NAME }} 84 | product_version: ${{ needs.set-product-version.outputs.product-version }} 85 | go_version: ${{ needs.get-go-version.outputs.go-version }} 86 | os: ${{ matrix.goos }} 87 | arch: ${{ matrix.goarch }} 88 | reproducible: assert 89 | instructions: | 90 | cp LICENSE $TARGET_DIR/LICENSE.txt 91 | make build 92 | 93 | build-linux: 94 | needs: 95 | - get-go-version 96 | - set-product-version 97 | runs-on: ubuntu-latest 98 | strategy: 99 | matrix: 100 | include: 101 | - {goos: "linux", goarch: "arm"} 102 | - {goos: "linux", goarch: "arm64"} 103 | - {goos: "linux", goarch: "386"} 104 | - {goos: "linux", goarch: "amd64"} 105 | - {goos: "darwin", goarch: "amd64"} 106 | - {goos: "darwin", goarch: "arm64"} 107 | fail-fast: true 108 | 109 | name: Go ${{ needs.get-go-version.outputs.go-version }} ${{ matrix.goos }} ${{ matrix.goarch }} build 110 | 111 | steps: 112 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 113 | 114 | - uses: hashicorp/actions-go-build@v1 115 | with: 116 | product_name: ${{ env.PKG_NAME }} 117 | product_version: ${{ needs.set-product-version.outputs.product-version }} 118 | go_version: ${{ needs.get-go-version.outputs.go-version }} 119 | os: ${{ matrix.goos }} 120 | arch: ${{ matrix.goarch }} 121 | reproducible: assert 122 | instructions: | 123 | cp LICENSE $TARGET_DIR/LICENSE.txt 124 | make build 125 | 126 | build-docker-default: 127 | name: Docker ${{ matrix.arch }} ${{ matrix.fips }} default release build 128 | needs: 129 | - get-go-version 130 | - set-product-version 131 | - build-linux 132 | runs-on: ubuntu-latest 133 | strategy: 134 | matrix: 135 | include: 136 | - { arch: "amd64" } 137 | - { arch: "arm64" } 138 | env: 139 | repo: ${{ github.event.repository.name }} 140 | version: ${{ needs.set-product-version.outputs.product-version }} 141 | steps: 142 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 143 | 144 | # This naming convention will be used ONLY for per-commit dev images 145 | - name: Set docker dev tag 146 | run: | 147 | echo "full_dev_tag=${{ env.version }}" 148 | echo "full_dev_tag=${{ env.version }}" >> $GITHUB_ENV 149 | echo "minor_dev_tag=$(echo ${{ env.version }}| sed -E 's/([0-9]+\.[0-9]+)\.[0-9]+(-[0-9a-zA-Z\+\.]+)?$/\1\2/')" 150 | echo "minor_dev_tag=$(echo ${{ env.version }}| sed -E 's/([0-9]+\.[0-9]+)\.[0-9]+(-[0-9a-zA-Z\+\.]+)?$/\1\2/')" >> $GITHUB_ENV 151 | 152 | - name: Docker Build (Action) 153 | uses: hashicorp/actions-docker-build@v2 154 | with: 155 | smoke_test: | 156 | TEST_VERSION="$(docker run "${IMAGE_NAME}" version | head -n1 | cut -d' ' -f2 | sed 's/^v//')" 157 | if [ "${TEST_VERSION}" != "${version}" ]; then 158 | echo "Test FAILED: Got ${TEST_VERSION}, want ${version}." 159 | exit 1 160 | fi 161 | echo "Test PASSED" 162 | version: ${{ env.version }} 163 | target: release-default 164 | arch: ${{ matrix.arch }} 165 | tags: | 166 | docker.io/hashicorp/${{env.repo}}:${{env.version}} 167 | public.ecr.aws/hashicorp/${{env.repo}}:${{env.version}} 168 | dev_tags: | 169 | docker.io/hashicorppreview/${{ env.repo }}:${{ env.full_dev_tag }} 170 | docker.io/hashicorppreview/${{ env.repo }}:${{ env.full_dev_tag }}-${{ github.sha }} 171 | docker.io/hashicorppreview/${{ env.repo }}:${{ env.minor_dev_tag }} 172 | docker.io/hashicorppreview/${{ env.repo }}:${{ env.minor_dev_tag }}-${{ github.sha }} 173 | extra_build_args: | 174 | GOLANG_VERSION=${{ needs.get-go-version.outputs.go-version }} 175 | 176 | # build-docker-redhat: 177 | # name: Docker UBI Image Build (for Red Hat Certified Container Registry) 178 | # needs: 179 | # - get-go-version 180 | # - set-product-version 181 | # - build-linux 182 | # runs-on: ubuntu-latest 183 | # strategy: 184 | # matrix: 185 | # include: 186 | # - { target-name: "release-ubi", registry-id: "631f805e0d15f623c5996c2e" } 187 | # env: 188 | # repo: ${{github.event.repository.name}} 189 | # version: ${{needs.set-product-version.outputs.product-version}} 190 | # steps: 191 | # - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 192 | # - uses: hashicorp/actions-docker-build@v2 193 | # with: 194 | # version: ${{env.version}} 195 | # target: ${{ matrix.target-name }} 196 | # arch: amd64 197 | # redhat_tag: quay.io/redhat-isv-containers/${{matrix.registry-id}}:${{env.version}}-ubi 198 | 199 | # build-docker-ubi-dockerhub: 200 | # name: Docker ${{ matrix.arch }} UBI build for DockerHub 201 | # needs: 202 | # - get-go-version 203 | # - set-product-version 204 | # - build-linux 205 | # runs-on: ubuntu-latest 206 | # strategy: 207 | # matrix: 208 | # include: 209 | # - { arch: "amd64" } 210 | # env: 211 | # repo: ${{ github.event.repository.name }} 212 | # version: ${{ needs.set-product-version.outputs.product-version }} 213 | # steps: 214 | # - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 215 | # # This naming convention will be used ONLY for per-commit dev images 216 | # - name: Set docker dev tag 217 | # run: | 218 | # echo "full_dev_tag=${{ env.version }}" 219 | # echo "full_dev_tag=${{ env.version }}" >> $GITHUB_ENV 220 | # echo "minor_dev_tag=$(echo ${{ env.version }}| sed -E 's/([0-9]+\.[0-9]+)\.[0-9]+(-[0-9a-zA-Z\+\.]+)?$/\1\2/')" 221 | # echo "minor_dev_tag=$(echo ${{ env.version }}| sed -E 's/([0-9]+\.[0-9]+)\.[0-9]+(-[0-9a-zA-Z\+\.]+)?$/\1\2/')" >> $GITHUB_ENV 222 | 223 | # - name: Docker Build (Action) 224 | # uses: hashicorp/actions-docker-build@v2 225 | # with: 226 | # smoke_test: | 227 | # TEST_VERSION="$(docker run "${IMAGE_NAME}" version | head -n1 | cut -d' ' -f2 | sed 's/^v//')" 228 | # if [ "${TEST_VERSION}" != "${version}" ]; then 229 | # echo "Test FAILED: Got ${TEST_VERSION}, want ${version}}." 230 | # exit 1 231 | # fi 232 | # echo "Test PASSED" 233 | # version: ${{ env.version }} 234 | # target: release-ubi 235 | # arch: ${{ matrix.arch }} 236 | # tags: | 237 | # docker.io/hashicorp/${{env.repo}}:${{env.version}}-ubi 238 | # public.ecr.aws/hashicorp/${{env.repo}}:${{env.version}}-ubi 239 | # dev_tags: | 240 | # docker.io/hashicorppreview/${{ env.repo }}:${{ env.full_dev_tag }}-ubi 241 | # docker.io/hashicorppreview/${{ env.repo }}:${{ env.full_dev_tag }}-ubi-${{ github.sha }} 242 | # docker.io/hashicorppreview/${{ env.repo }}:${{ env.minor_dev_tag }}-ubi 243 | # docker.io/hashicorppreview/${{ env.repo }}:${{ env.minor_dev_tag }}-ubi-${{ github.sha }} 244 | 245 | # TODO: Integration Test Here 246 | -------------------------------------------------------------------------------- /.github/workflows/changelog-checker.yml: -------------------------------------------------------------------------------- 1 | # This workflow checks that there is either a 'pr/no-changelog' label applied to a PR 2 | # or there is a .changelog/.txt file associated with a PR for a changelog entry 3 | 4 | name: Changelog Checker 5 | 6 | on: 7 | pull_request: 8 | types: [opened, synchronize, labeled] 9 | # Runs on PRs to main and all release branches 10 | branches: 11 | - main 12 | - release/* 13 | 14 | jobs: 15 | # checks that a .changelog entry is present for a PR 16 | changelog-check: 17 | # If there a `pr/no-changelog` label we ignore this check. 18 | if: "! contains(github.event.pull_request.labels.*.name, 'pr/no-changelog')" 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 23 | with: 24 | ref: ${{ github.event.pull_request.head.sha }} 25 | fetch-depth: 0 # by default the checkout action doesn't checkout all branches 26 | - name: Check for changelog entry in diff 27 | run: | 28 | # check if there is a diff in the .changelog directory 29 | # for PRs against the main branch, the changelog file name should match the PR number 30 | if [ "${{ github.event.pull_request.base.ref }}" = "${{ github.event.repository.default_branch }}" ]; then 31 | enforce_matching_pull_request_number="matching this PR number " 32 | changelog_file_path=".changelog/${{ github.event.pull_request.number }}.txt" 33 | else 34 | changelog_file_path=".changelog/*.txt" 35 | fi 36 | 37 | changelog_files=$(git --no-pager diff --name-only HEAD "$(git merge-base HEAD "origin/main")" -- ${changelog_file_path}) 38 | 39 | # If we do not find a file in .changelog/, we fail the check 40 | if [ -z "$changelog_files" ]; then 41 | # Fail status check when no .changelog entry was found on the PR 42 | echo "Did not find a .changelog entry ${enforce_matching_pull_request_number}and the 'pr/no-changelog' label was not applied." 43 | exit 1 44 | else 45 | echo "Found .changelog entry in PR!" 46 | fi 47 | -------------------------------------------------------------------------------- /.github/workflows/reusable-get-go-version.yml: -------------------------------------------------------------------------------- 1 | name: get-go-version 2 | 3 | on: 4 | workflow_call: 5 | outputs: 6 | go-version: 7 | description: "The Go version detected by this workflow" 8 | value: ${{ jobs.get-go-version.outputs.go-version }} 9 | go-version-previous: 10 | description: "The Go version (MAJOR.MINOR) prior to the current one, used for backwards compatibility testing" 11 | value: ${{ jobs.get-go-version.outputs.go-version-previous }} 12 | 13 | jobs: 14 | get-go-version: 15 | name: "Determine Go toolchain version" 16 | runs-on: ubuntu-latest 17 | outputs: 18 | go-version: ${{ steps.get-go-version.outputs.go-version }} 19 | go-version-previous: ${{ steps.get-go-version.outputs.go-version-previous }} 20 | steps: 21 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 22 | - name: Determine Go version 23 | id: get-go-version 24 | # We use .go-version as our source of truth for current Go 25 | # version, because "goenv" can react to it automatically. 26 | # 27 | # In the future, we can transition from .go-version and goenv to 28 | # Go 1.21 `toolchain` directives by updating this workflow rather 29 | # than individually setting `go-version-file` in each `setup-go` 30 | # job (as of 2024-01-03, `setup-go` does not support `toolchain`). 31 | # 32 | # When changing the method of Go version detection, also update 33 | # GOLANG_VERSION detection in the root Makefile; this is used for 34 | # setting the Dockerfile Go version. 35 | run: | 36 | GO_VERSION=$(head -n 1 .go-version) 37 | echo "Building with Go ${GO_VERSION}" 38 | echo "go-version=${GO_VERSION}" >> $GITHUB_OUTPUT 39 | GO_MINOR_VERSION=${GO_VERSION%.*} 40 | GO_VERSION_PREVIOUS="${GO_MINOR_VERSION%.*}.$((${GO_MINOR_VERSION#*.}-1))" 41 | echo "Previous version ${GO_VERSION_PREVIOUS}" 42 | echo "go-version-previous=${GO_VERSION_PREVIOUS}" >> $GITHUB_OUTPUT 43 | -------------------------------------------------------------------------------- /.github/workflows/security-scan.yml: -------------------------------------------------------------------------------- 1 | name: Security Scan 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - release/** 8 | pull_request: 9 | branches: 10 | - main 11 | - release/** 12 | 13 | # cancel existing runs of the same workflow on the same ref 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | get-go-version: 20 | uses: ./.github/workflows/reusable-get-go-version.yml 21 | 22 | scan: 23 | needs: 24 | - get-go-version 25 | runs-on: ubuntu-latest 26 | # The first check ensures this doesn't run on community-contributed PRs, who 27 | # won't have the permissions to run this job. 28 | if: ${{ (github.repository != 'hashicorp/consul-aws' || (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name)) 29 | && (github.actor != 'dependabot[bot]') && (github.actor != 'hc-github-team-consul-core') }} 30 | 31 | steps: 32 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 33 | 34 | - name: Set up Go 35 | uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 36 | with: 37 | go-version: ${{ needs.get-go-version.outputs.go-version }} 38 | 39 | - name: Clone Security Scanner repo 40 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 41 | with: 42 | repository: hashicorp/security-scanner 43 | token: ${{ secrets.PRODSEC_SCANNER_READ_ONLY }} 44 | path: security-scanner 45 | ref: main 46 | 47 | - name: Scan 48 | id: scan 49 | uses: ./security-scanner 50 | with: 51 | repository: "$PWD" 52 | # See scan.hcl at repository root for config. 53 | 54 | - name: SARIF Output 55 | shell: bash 56 | run: | 57 | cat results.sarif | jq 58 | 59 | - name: Upload SARIF file 60 | uses: github/codeql-action/upload-sarif@c4fb451437765abf5018c6fbf22cce1a7da1e5cc # codeql-bundle-v2.17.1 61 | with: 62 | sarif_file: results.sarif 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /bin 3 | /pkg 4 | 5 | .vscode 6 | .idea 7 | .zed -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- 1 | 1.24.2 2 | -------------------------------------------------------------------------------- /.release/ci.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | schema = "1" 5 | 6 | project "consul-aws" { 7 | team = "consul" 8 | slack { 9 | notification_channel = "C9KPKPKRN" # feed-consul-ci 10 | } 11 | github { 12 | organization = "hashicorp" 13 | repository = "consul-aws" 14 | release_branches = [ 15 | "main", 16 | "release/**", 17 | ] 18 | } 19 | } 20 | 21 | event "merge" { 22 | // "entrypoint" to use if build is not run automatically 23 | // i.e. send "merge" complete signal to orchestrator to trigger build 24 | } 25 | 26 | event "build" { 27 | depends = ["merge"] 28 | action "build" { 29 | organization = "hashicorp" 30 | repository = "consul-aws" 31 | workflow = "build" 32 | } 33 | } 34 | 35 | event "prepare" { 36 | depends = ["build"] 37 | action "prepare" { 38 | organization = "hashicorp" 39 | repository = "crt-workflows-common" 40 | workflow = "prepare" 41 | depends = ["build"] 42 | } 43 | 44 | notification { 45 | on = "fail" 46 | } 47 | } 48 | 49 | ## These are promotion and post-publish events 50 | ## they should be added to the end of the file after the verify event stanza. 51 | 52 | event "trigger-staging" { 53 | // This event is dispatched by the bob trigger-promotion command 54 | // and is required - do not delete. 55 | } 56 | 57 | event "promote-staging" { 58 | depends = ["trigger-staging"] 59 | action "promote-staging" { 60 | organization = "hashicorp" 61 | repository = "crt-workflows-common" 62 | workflow = "promote-staging" 63 | config = "release-metadata.hcl" 64 | } 65 | 66 | notification { 67 | on = "always" 68 | } 69 | } 70 | 71 | event "promote-staging-docker" { 72 | depends = ["promote-staging"] 73 | action "promote-staging-docker" { 74 | organization = "hashicorp" 75 | repository = "crt-workflows-common" 76 | workflow = "promote-staging-docker" 77 | } 78 | 79 | notification { 80 | on = "always" 81 | } 82 | } 83 | 84 | event "trigger-production" { 85 | // This event is dispatched by the bob trigger-promotion command 86 | // and is required - do not delete. 87 | } 88 | 89 | event "promote-production" { 90 | depends = ["trigger-production"] 91 | action "promote-production" { 92 | organization = "hashicorp" 93 | repository = "crt-workflows-common" 94 | workflow = "promote-production" 95 | } 96 | 97 | notification { 98 | on = "always" 99 | } 100 | } 101 | 102 | event "promote-production-docker" { 103 | depends = ["promote-production"] 104 | action "promote-production-docker" { 105 | organization = "hashicorp" 106 | repository = "crt-workflows-common" 107 | workflow = "promote-production-docker" 108 | } 109 | 110 | notification { 111 | on = "always" 112 | } 113 | } 114 | 115 | -------------------------------------------------------------------------------- /.release/release-metadata.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | url_docker_registry_dockerhub = "https://hub.docker.com/r/hashicorp/consul-aws" 5 | url_docker_registry_ecr = "https://gallery.ecr.aws/hashicorp/consul-aws" 6 | url_license = "https://github.com/hashicorp/consul-aws/blob/main/LICENSE" 7 | url_source_repository = "https://github.com/hashicorp/consul-aws" 8 | url_project_website = "https://www.consul.io" 9 | url_release_notes = "https://developer.hashicorp.com/consul/docs/release-notes" -------------------------------------------------------------------------------- /.release/security-scan.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # These scan results are run as part of CRT workflows. 5 | 6 | # Un-triaged results will block release. See `security-scanner` docs for more 7 | # information on how to add `triage` config to unblock releases for specific results. 8 | # In most cases, we should not need to disable the entire scanner to unblock a release. 9 | 10 | # To run manually, install scanner and then from the repository root run 11 | # `SECURITY_SCANNER_CONFIG_FILE=.release/security-scan.hcl scan ...` 12 | # To scan a local container, add `local_daemon = true` to the `container` block below. 13 | # See `security-scanner` docs or run with `--help` for scan target syntax. 14 | 15 | container { 16 | dependencies = true 17 | alpine_secdb = true 18 | 19 | secrets { 20 | all = true 21 | } 22 | } 23 | 24 | binary { 25 | go_modules = true 26 | osv = true 27 | 28 | secrets { 29 | all = true 30 | } 31 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.3 (April, 23, 2024) 2 | 3 | SECURITY: 4 | 5 | * Update dependencies to fix security vulnerabilities. 6 | Due to the large number of dependencies, the list of CVEs is not included in this release note. [[GH-44](https://github.com/hashicorp/consul/issues/44)] 7 | 8 | BUG FIXES: 9 | 10 | * Fix a bug that would cause AWS instance discovery queries to fail if the CloudMap Namespace name was different from it's HTTP Name. 11 | This can happen when a CloudMap Namespace is re-created with the same Name. [[GH-46](https://github.com/hashicorp/consul/issues/46)] 12 | 13 | ## 0.1.2 (April 7, 2020) 14 | 15 | IMPROVEMENTS: 16 | 17 | * Do not create Cloud Map services with A+SRV DNS records [[GH-20](https://github.com/hashicorp/consul-aws/pull/20)] 18 | 19 | ## 0.1.1 (December 20, 2018) 20 | 21 | IMPROVEMENTS: 22 | 23 | * Use the new DiscoverInstances API [[GH-2](https://github.com/hashicorp/consul-aws/pull/2)] 24 | * Introduce `-aws-poll-interval` and deprecate `-aws-pull-interval` [[GH-4](https://github.com/hashicorp/consul-aws/pull/4)] 25 | 26 | BUG FIXES: 27 | 28 | * Fix an issue where prefixes are not handled properly [[GH-2](https://github.com/hashicorp/consul-aws/pull/2)] 29 | 30 | ## 0.1.0 (November 29, 2018) 31 | 32 | * Initial release 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # This Dockerfile contains multiple targets. 5 | # Use 'docker build --target= .' to build one. 6 | # 7 | # Every target has a BIN_NAME argument that must be provided via --build-arg=BIN_NAME= 8 | # when building. 9 | 10 | # Pull in dumb-init from alpine, as our distroless release image doesn't have a 11 | # package manager and there's no RPM package for UBI. 12 | FROM alpine:latest AS dumb-init 13 | RUN apk add dumb-init 14 | 15 | # release-default release image 16 | # ----------------------------------- 17 | FROM gcr.io/distroless/base-debian11 AS release-default 18 | 19 | ARG BIN_NAME=consul-aws 20 | ENV BIN_NAME=$BIN_NAME 21 | # PRODUCT_* variables are set by the hashicorp/actions-build-docker action. 22 | ARG PRODUCT_VERSION 23 | ARG PRODUCT_REVISION 24 | ARG PRODUCT_NAME=$BIN_NAME 25 | ENV PRODUCT_NAME=$PRODUCT_NAME 26 | 27 | # TARGETARCH and TARGETOS are set automatically when --platform is provided. 28 | ARG TARGETOS 29 | ARG TARGETARCH 30 | 31 | LABEL name=${BIN_NAME}\ 32 | maintainer="Consul Team " \ 33 | vendor="HashiCorp" \ 34 | version=${PRODUCT_VERSION} \ 35 | release=${PRODUCT_REVISION} \ 36 | revision=${PRODUCT_REVISION} \ 37 | summary="Consul AWS is a tool for bi-directional sync between AWS CloudMap and Consul." \ 38 | licenses="MPL-2.0" \ 39 | description="Consul AWS is a tool for bi-directional sync between AWS CloudMap and Consul." 40 | 41 | COPY --from=dumb-init /usr/bin/dumb-init /usr/local/bin/ 42 | COPY dist/$TARGETOS/$TARGETARCH/$BIN_NAME /usr/local/bin/ 43 | COPY LICENSE /licenses/copyright.txt 44 | COPY LICENSE /usr/share/doc/$PRODUCT_NAME/LICENSE.txt 45 | 46 | USER 100 47 | 48 | ENTRYPOINT ["/usr/local/bin/dumb-init", "/usr/local/bin/consul-aws"] 49 | 50 | # Red Hat UBI-based image 51 | # This image is based on the Red Hat UBI base image, and has the necessary 52 | # labels, license file, and non-root user. 53 | # ----------------------------------- 54 | FROM registry.access.redhat.com/ubi9-minimal:9.3 as release-ubi 55 | 56 | ARG BIN_NAME=consul-aws 57 | ENV BIN_NAME=$BIN_NAME 58 | # PRODUCT_* variables are set by the hashicorp/actions-build-docker action. 59 | ARG PRODUCT_VERSION 60 | ARG PRODUCT_REVISION 61 | ARG PRODUCT_NAME=$BIN_NAME 62 | ENV PRODUCT_NAME=$PRODUCT_NAME 63 | # TARGETARCH and TARGETOS are set automatically when --platform is provided. 64 | ARG TARGETOS 65 | ARG TARGETARCH 66 | 67 | LABEL name=${BIN_NAME}\ 68 | maintainer="Consul Team " \ 69 | vendor="HashiCorp" \ 70 | version=${PRODUCT_VERSION} \ 71 | release=${PRODUCT_REVISION} \ 72 | revision=${PRODUCT_REVISION} \ 73 | summary="Consul AWS is a tool for bi-directional sync between AWS CloudMap and Consul." \ 74 | licenses="MPL-2.0" \ 75 | description="Consul AWS is a tool for bi-directional sync between AWS CloudMap and Consul." 76 | 77 | RUN microdnf install -y shadow-utils 78 | 79 | # Create a non-root user to run the software. 80 | RUN groupadd --gid 1000 $PRODUCT_NAME && \ 81 | adduser --uid 100 --system -g $PRODUCT_NAME $PRODUCT_NAME && \ 82 | usermod -a -G root $PRODUCT_NAME 83 | 84 | COPY --from=dumb-init /usr/bin/dumb-init /usr/local/bin/ 85 | COPY dist/$TARGETOS/$TARGETARCH/$BIN_NAME /usr/local/bin/ 86 | COPY LICENSE /licenses/copyright.txt 87 | COPY LICENSE /usr/share/doc/$PRODUCT_NAME/LICENSE.txt 88 | 89 | USER 100 90 | ENTRYPOINT ["/usr/local/bin/dumb-init", "/usr/local/bin/consul-aws"] 91 | 92 | # =================================== 93 | # 94 | # Set default target to 'release-default'. 95 | # 96 | # =================================== 97 | FROM release-default 98 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 HashiCorp, Inc. 2 | 3 | Mozilla Public License, version 2.0 4 | 5 | 1. Definitions 6 | 7 | 1.1. “Contributor” 8 | 9 | means each individual or legal entity that creates, contributes to the 10 | creation of, or owns Covered Software. 11 | 12 | 1.2. “Contributor Version” 13 | 14 | means the combination of the Contributions of others (if any) used by a 15 | Contributor and that particular Contributor’s Contribution. 16 | 17 | 1.3. “Contribution” 18 | 19 | means Covered Software of a particular Contributor. 20 | 21 | 1.4. “Covered Software” 22 | 23 | means Source Code Form to which the initial Contributor has attached the 24 | notice in Exhibit A, the Executable Form of such Source Code Form, and 25 | Modifications of such Source Code Form, in each case including portions 26 | thereof. 27 | 28 | 1.5. “Incompatible With Secondary Licenses” 29 | means 30 | 31 | a. that the initial Contributor has attached the notice described in 32 | Exhibit B to the Covered Software; or 33 | 34 | b. that the Covered Software was made available under the terms of version 35 | 1.1 or earlier of the License, but not also under the terms of a 36 | Secondary License. 37 | 38 | 1.6. “Executable Form” 39 | 40 | means any form of the work other than Source Code Form. 41 | 42 | 1.7. “Larger Work” 43 | 44 | means a work that combines Covered Software with other material, in a separate 45 | file or files, that is not Covered Software. 46 | 47 | 1.8. “License” 48 | 49 | means this document. 50 | 51 | 1.9. “Licensable” 52 | 53 | means having the right to grant, to the maximum extent possible, whether at the 54 | time of the initial grant or subsequently, any and all of the rights conveyed by 55 | this License. 56 | 57 | 1.10. “Modifications” 58 | 59 | means any of the following: 60 | 61 | a. any file in Source Code Form that results from an addition to, deletion 62 | from, or modification of the contents of Covered Software; or 63 | 64 | b. any new file in Source Code Form that contains any Covered Software. 65 | 66 | 1.11. “Patent Claims” of a Contributor 67 | 68 | means any patent claim(s), including without limitation, method, process, 69 | and apparatus claims, in any patent Licensable by such Contributor that 70 | would be infringed, but for the grant of the License, by the making, 71 | using, selling, offering for sale, having made, import, or transfer of 72 | either its Contributions or its Contributor Version. 73 | 74 | 1.12. “Secondary License” 75 | 76 | means either the GNU General Public License, Version 2.0, the GNU Lesser 77 | General Public License, Version 2.1, the GNU Affero General Public 78 | License, Version 3.0, or any later versions of those licenses. 79 | 80 | 1.13. “Source Code Form” 81 | 82 | means the form of the work preferred for making modifications. 83 | 84 | 1.14. “You” (or “Your”) 85 | 86 | means an individual or a legal entity exercising rights under this 87 | License. For legal entities, “You” includes any entity that controls, is 88 | controlled by, or is under common control with You. For purposes of this 89 | definition, “control” means (a) the power, direct or indirect, to cause 90 | the direction or management of such entity, whether by contract or 91 | otherwise, or (b) ownership of more than fifty percent (50%) of the 92 | outstanding shares or beneficial ownership of such entity. 93 | 94 | 95 | 2. License Grants and Conditions 96 | 97 | 2.1. Grants 98 | 99 | Each Contributor hereby grants You a world-wide, royalty-free, 100 | non-exclusive license: 101 | 102 | a. under intellectual property rights (other than patent or trademark) 103 | Licensable by such Contributor to use, reproduce, make available, 104 | modify, display, perform, distribute, and otherwise exploit its 105 | Contributions, either on an unmodified basis, with Modifications, or as 106 | part of a Larger Work; and 107 | 108 | b. under Patent Claims of such Contributor to make, use, sell, offer for 109 | sale, have made, import, and otherwise transfer either its Contributions 110 | or its Contributor Version. 111 | 112 | 2.2. Effective Date 113 | 114 | The licenses granted in Section 2.1 with respect to any Contribution become 115 | effective for each Contribution on the date the Contributor first distributes 116 | such Contribution. 117 | 118 | 2.3. Limitations on Grant Scope 119 | 120 | The licenses granted in this Section 2 are the only rights granted under this 121 | License. No additional rights or licenses will be implied from the distribution 122 | or licensing of Covered Software under this License. Notwithstanding Section 123 | 2.1(b) above, no patent license is granted by a Contributor: 124 | 125 | a. for any code that a Contributor has removed from Covered Software; or 126 | 127 | b. for infringements caused by: (i) Your and any other third party’s 128 | modifications of Covered Software, or (ii) the combination of its 129 | Contributions with other software (except as part of its Contributor 130 | Version); or 131 | 132 | c. under Patent Claims infringed by Covered Software in the absence of its 133 | Contributions. 134 | 135 | This License does not grant any rights in the trademarks, service marks, or 136 | logos of any Contributor (except as may be necessary to comply with the 137 | notice requirements in Section 3.4). 138 | 139 | 2.4. Subsequent Licenses 140 | 141 | No Contributor makes additional grants as a result of Your choice to 142 | distribute the Covered Software under a subsequent version of this License 143 | (see Section 10.2) or under the terms of a Secondary License (if permitted 144 | under the terms of Section 3.3). 145 | 146 | 2.5. Representation 147 | 148 | Each Contributor represents that the Contributor believes its Contributions 149 | are its original creation(s) or it has sufficient rights to grant the 150 | rights to its Contributions conveyed by this License. 151 | 152 | 2.6. Fair Use 153 | 154 | This License is not intended to limit any rights You have under applicable 155 | copyright doctrines of fair use, fair dealing, or other equivalents. 156 | 157 | 2.7. Conditions 158 | 159 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 160 | Section 2.1. 161 | 162 | 163 | 3. Responsibilities 164 | 165 | 3.1. Distribution of Source Form 166 | 167 | All distribution of Covered Software in Source Code Form, including any 168 | Modifications that You create or to which You contribute, must be under the 169 | terms of this License. You must inform recipients that the Source Code Form 170 | of the Covered Software is governed by the terms of this License, and how 171 | they can obtain a copy of this License. You may not attempt to alter or 172 | restrict the recipients’ rights in the Source Code Form. 173 | 174 | 3.2. Distribution of Executable Form 175 | 176 | If You distribute Covered Software in Executable Form then: 177 | 178 | a. such Covered Software must also be made available in Source Code Form, 179 | as described in Section 3.1, and You must inform recipients of the 180 | Executable Form how they can obtain a copy of such Source Code Form by 181 | reasonable means in a timely manner, at a charge no more than the cost 182 | of distribution to the recipient; and 183 | 184 | b. You may distribute such Executable Form under the terms of this License, 185 | or sublicense it under different terms, provided that the license for 186 | the Executable Form does not attempt to limit or alter the recipients’ 187 | rights in the Source Code Form under this License. 188 | 189 | 3.3. Distribution of a Larger Work 190 | 191 | You may create and distribute a Larger Work under terms of Your choice, 192 | provided that You also comply with the requirements of this License for the 193 | Covered Software. If the Larger Work is a combination of Covered Software 194 | with a work governed by one or more Secondary Licenses, and the Covered 195 | Software is not Incompatible With Secondary Licenses, this License permits 196 | You to additionally distribute such Covered Software under the terms of 197 | such Secondary License(s), so that the recipient of the Larger Work may, at 198 | their option, further distribute the Covered Software under the terms of 199 | either this License or such Secondary License(s). 200 | 201 | 3.4. Notices 202 | 203 | You may not remove or alter the substance of any license notices (including 204 | copyright notices, patent notices, disclaimers of warranty, or limitations 205 | of liability) contained within the Source Code Form of the Covered 206 | Software, except that You may alter any license notices to the extent 207 | required to remedy known factual inaccuracies. 208 | 209 | 3.5. Application of Additional Terms 210 | 211 | You may choose to offer, and to charge a fee for, warranty, support, 212 | indemnity or liability obligations to one or more recipients of Covered 213 | Software. However, You may do so only on Your own behalf, and not on behalf 214 | of any Contributor. You must make it absolutely clear that any such 215 | warranty, support, indemnity, or liability obligation is offered by You 216 | alone, and You hereby agree to indemnify every Contributor for any 217 | liability incurred by such Contributor as a result of warranty, support, 218 | indemnity or liability terms You offer. You may include additional 219 | disclaimers of warranty and limitations of liability specific to any 220 | jurisdiction. 221 | 222 | 4. Inability to Comply Due to Statute or Regulation 223 | 224 | If it is impossible for You to comply with any of the terms of this License 225 | with respect to some or all of the Covered Software due to statute, judicial 226 | order, or regulation then You must: (a) comply with the terms of this License 227 | to the maximum extent possible; and (b) describe the limitations and the code 228 | they affect. Such description must be placed in a text file included with all 229 | distributions of the Covered Software under this License. Except to the 230 | extent prohibited by statute or regulation, such description must be 231 | sufficiently detailed for a recipient of ordinary skill to be able to 232 | understand it. 233 | 234 | 5. Termination 235 | 236 | 5.1. The rights granted under this License will terminate automatically if You 237 | fail to comply with any of its terms. However, if You become compliant, 238 | then the rights granted under this License from a particular Contributor 239 | are reinstated (a) provisionally, unless and until such Contributor 240 | explicitly and finally terminates Your grants, and (b) on an ongoing basis, 241 | if such Contributor fails to notify You of the non-compliance by some 242 | reasonable means prior to 60 days after You have come back into compliance. 243 | Moreover, Your grants from a particular Contributor are reinstated on an 244 | ongoing basis if such Contributor notifies You of the non-compliance by 245 | some reasonable means, this is the first time You have received notice of 246 | non-compliance with this License from such Contributor, and You become 247 | compliant prior to 30 days after Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, counter-claims, 251 | and cross-claims) alleging that a Contributor Version directly or 252 | indirectly infringes any patent, then the rights granted to You by any and 253 | all Contributors for the Covered Software under Section 2.1 of this License 254 | shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 257 | license agreements (excluding distributors and resellers) which have been 258 | validly granted by You or Your distributors under this License prior to 259 | termination shall survive termination. 260 | 261 | 6. Disclaimer of Warranty 262 | 263 | Covered Software is provided under this License on an “as is” basis, without 264 | warranty of any kind, either expressed, implied, or statutory, including, 265 | without limitation, warranties that the Covered Software is free of defects, 266 | merchantable, fit for a particular purpose or non-infringing. The entire 267 | risk as to the quality and performance of the Covered Software is with You. 268 | Should any Covered Software prove defective in any respect, You (not any 269 | Contributor) assume the cost of any necessary servicing, repair, or 270 | correction. This disclaimer of warranty constitutes an essential part of this 271 | License. No use of any Covered Software is authorized under this License 272 | except under this disclaimer. 273 | 274 | 7. Limitation of Liability 275 | 276 | Under no circumstances and under no legal theory, whether tort (including 277 | negligence), contract, or otherwise, shall any Contributor, or anyone who 278 | distributes Covered Software as permitted above, be liable to You for any 279 | direct, indirect, special, incidental, or consequential damages of any 280 | character including, without limitation, damages for lost profits, loss of 281 | goodwill, work stoppage, computer failure or malfunction, or any and all 282 | other commercial damages or losses, even if such party shall have been 283 | informed of the possibility of such damages. This limitation of liability 284 | shall not apply to liability for death or personal injury resulting from such 285 | party’s negligence to the extent applicable law prohibits such limitation. 286 | Some jurisdictions do not allow the exclusion or limitation of incidental or 287 | consequential damages, so this exclusion and limitation may not apply to You. 288 | 289 | 8. Litigation 290 | 291 | Any litigation relating to this License may be brought only in the courts of 292 | a jurisdiction where the defendant maintains its principal place of business 293 | and such litigation shall be governed by laws of that jurisdiction, without 294 | reference to its conflict-of-law provisions. Nothing in this Section shall 295 | prevent a party’s ability to bring cross-claims or counter-claims. 296 | 297 | 9. Miscellaneous 298 | 299 | This License represents the complete agreement concerning the subject matter 300 | hereof. If any provision of this License is held to be unenforceable, such 301 | provision shall be reformed only to the extent necessary to make it 302 | enforceable. Any law or regulation which provides that the language of a 303 | contract shall be construed against the drafter shall not be used to construe 304 | this License against a Contributor. 305 | 306 | 307 | 10. Versions of the License 308 | 309 | 10.1. New Versions 310 | 311 | Mozilla Foundation is the license steward. Except as provided in Section 312 | 10.3, no one other than the license steward has the right to modify or 313 | publish new versions of this License. Each version will be given a 314 | distinguishing version number. 315 | 316 | 10.2. Effect of New Versions 317 | 318 | You may distribute the Covered Software under the terms of the version of 319 | the License under which You originally received the Covered Software, or 320 | under the terms of any subsequent version published by the license 321 | steward. 322 | 323 | 10.3. Modified Versions 324 | 325 | If you create software not governed by this License, and you want to 326 | create a new license for such software, you may create and use a modified 327 | version of this License if you rename the license and remove any 328 | references to the name of the license steward (except to note that such 329 | modified license differs from this License). 330 | 331 | 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses 332 | If You choose to distribute Source Code Form that is Incompatible With 333 | Secondary Licenses under the terms of this version of the License, the 334 | notice described in Exhibit B of this License must be attached. 335 | 336 | Exhibit A - Source Code Form License Notice 337 | 338 | This Source Code Form is subject to the 339 | terms of the Mozilla Public License, v. 340 | 2.0. If a copy of the MPL was not 341 | distributed with this file, You can 342 | obtain one at 343 | http://mozilla.org/MPL/2.0/. 344 | 345 | If it is not possible or desirable to put the notice in a particular file, then 346 | You may include the notice in a location (such as a LICENSE file in a relevant 347 | directory) where a recipient would be likely to look for such a notice. 348 | 349 | You may add additional accurate notices of copyright ownership. 350 | 351 | Exhibit B - “Incompatible With Secondary Licenses” Notice 352 | 353 | This Source Code Form is “Incompatible 354 | With Secondary Licenses”, as defined by 355 | the Mozilla Public License, v. 2.0. 356 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /usr/bin/env bash -euo pipefail -c 2 | 3 | PRODUCT_NAME ?= consul-aws 4 | BIN_NAME ?= $(PRODUCT_NAME) 5 | GOPATH ?= $(shell go env GOPATH) 6 | GOBIN ?= $(GOPATH)/bin 7 | 8 | VERSION = $(shell head -n 1 version/VERSION) 9 | GOLANG_VERSION ?= $(shell head -n 1 .go-version) 10 | 11 | DEV_IMAGE?=consul-aws-dev 12 | GO_BUILD_TAG?=consul-aws-build-go 13 | GIT_COMMIT?=$(shell git rev-parse --short HEAD) 14 | GIT_DIRTY?=$(shell test -n "`git status --porcelain`" && echo "+CHANGES" || true) 15 | GIT_DESCRIBE?=$(shell git describe --tags --always) 16 | GIT_IMPORT=github.com/hashicorp/consul-aws/version 17 | GOLDFLAGS=-X $(GIT_IMPORT).GitCommit=$(GIT_COMMIT)$(GIT_DIRTY) 18 | 19 | # Get local ARCH; on Intel Mac, 'uname -m' returns x86_64 which we turn into amd64. 20 | # Not using 'go env GOOS/GOARCH' here so 'make docker' will work without local Go install. 21 | ARCH ?= $(shell A=$$(uname -m); [ $$A = x86_64 ] && A=amd64; echo $$A) 22 | OS ?= $(shell go env GOOS) 23 | PLATFORM = $(OS)/$(ARCH) 24 | DIST = dist/$(PLATFORM) 25 | BIN = $(DIST)/$(BIN_NAME) 26 | 27 | # Docker Stuff. 28 | export DOCKER_BUILDKIT=1 29 | BUILD_ARGS = BIN_NAME=$(BIN_NAME) PRODUCT_VERSION=$(VERSION) PRODUCT_REVISION=$(REVISION) 30 | TAG = $(PRODUCT_NAME):$(VERSION) 31 | BA_FLAGS = $(addprefix --build-arg=,$(BUILD_ARGS)) 32 | FLAGS = --target $(TARGET) --platform linux/$(ARCH) --tag $(TAG) $(BA_FLAGS) 33 | 34 | HTTP_FLAGS_PACKAGE_DIR=internal/flags 35 | 36 | dist: ## make dist directory and ignore everything 37 | mkdir -p $(DIST) 38 | echo '*' > dist/.gitignore 39 | 40 | .PHONY: bin 41 | bin: dist ## Build the binary 42 | GOARCH=$(ARCH) GOOS=$(OS) CGO_ENABLED=0 go build -trimpath -buildvcs=false -ldflags="$(GOLDFLAGS)" -o $(BIN) . 43 | 44 | linux: ## Linux builds a linux binary compatible with the source platform 45 | @mkdir -p ./dist/linux/$(ARCH)/$(BIN_NAME) 46 | CGO_ENABLED=0 GOOS=linux GOARCH=$(ARCH) go build -o ./dist/linux/$(ARCH)/$(BIN_NAME) -ldflags "$(GOLDFLAGS)" -tags "$(GOTAGS)" 47 | 48 | .PHONY: dev 49 | dev: bin ## Build binary and copy to the destination 50 | cp $(BIN) $(GOBIN)/$(BIN_NAME) 51 | 52 | # build is used for the CRT build.yml workflow. 53 | # Environment variables are populated by hashicorp/actions-go-build, not the makefile. 54 | # https://github.com/hashicorp/actions-go-build 55 | .PHONY: build 56 | build: 57 | CGO_ENABLED=0 go build \ 58 | -a \ 59 | -o="${BIN_PATH}" \ 60 | -ldflags " \ 61 | -X 'github.com/hashicorp/http-echo/version.GitCommit=${PRODUCT_REVISION}' \ 62 | " \ 63 | -tags "${GOTAGS}" \ 64 | -trimpath \ 65 | -buildvcs=false 66 | 67 | .PHONY: docker 68 | docker: linux ## build the release-target docker image 69 | $(eval TARGET := release-default) # there are many targets in the Dockerfile, add more build if you need to customize the target 70 | docker build $(FLAGS) . 71 | @echo 'Image built; run "docker run --rm $(TAG)" to try it out.' 72 | 73 | docker-run: docker ## run the image of $(TAG) 74 | docker run --rm $(TAG) 75 | 76 | .PHONY: dev-docker 77 | dev-docker: docker ## build docker image and tag the image to local 78 | docker tag '$(PRODUCT_NAME):$(VERSION)' '$(PRODUCT_NAME):local' 79 | 80 | test: 81 | go test ./... 82 | 83 | tools: 84 | go get -u -v $(GOTOOLS) 85 | 86 | clean: 87 | @rm -rf \ 88 | $(CURDIR)/bin \ 89 | $(CURDIR)/pkg 90 | 91 | # We support all the same CLI flags for connecting to Consul as the main CLI. 92 | # Rather than import the root Consul module, which is not supported, we copy the 93 | # relevant module. This is similar to what we do in consul-dataplane. 94 | # We don't include config_test.go because it pulls in a directory with test data. 95 | .PHONY: copy-http-flags 96 | copy-http-flags: ## copy http flags modules from consul 97 | for file in config.go http.go http_test.go merge.go usage.go; do \ 98 | curl --fail https://raw.githubusercontent.com/hashicorp/consul/main/command/flags/$$file | \ 99 | sed 's/BUSL-1.1/MPL-2.0/' | \ 100 | sed '1s:^:// Code generated by make copy-http-flags. DO NOT EDIT.\n:' | \ 101 | gofmt \ 102 | > $(HTTP_FLAGS_PACKAGE_DIR)/$$file; \ 103 | done 104 | 105 | .PHONY: all bin clean dev dist docker-images go-build-image test tools docker-publish 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Consul-AWS 2 | 3 | `consul-aws` syncs the services in an [AWS CloudMap](https://docs.aws.amazon.com/cloud-map/latest/dg/what-is-cloud-map.html) namespace to a Consul datacenter. 4 | Consul services will be created in AWS CloudMap and the other way around. 5 | This enables native service discovery across Consul and AWS CloudMap. 6 | 7 | This project is versioned separately from Consul. Supported Consul versions for each feature will be noted below. By versioning this project separately, we can iterate on AWS integrations more quickly and release new versions without forcing Consul users to do a full Consul upgrade. 8 | 9 | ## Installation 10 | 11 | 1. Download a pre-compiled, released version from the [Consul-AWS releases page][releases]. 12 | 13 | 1. Extract the binary using `unzip` or `tar`. 14 | 15 | 1. Move the binary into `$PATH`. 16 | 17 | To compile from source, please see the instructions in the [contributing section](#contributing). 18 | 19 | ## Usage 20 | 21 | `consul-aws` can sync from Consul to AWS CloudMap (`-to-aws`), from AWS CloudMap to Consul (`-to-consul`) and both at the same time. 22 | No matter which direction is being used `consul-aws` needs to be connected to Consul and AWS CloudMap. 23 | 24 | In order to help with connecting to a Consul cluster, `consul-aws` provides all the flags you might need including the possibility to set an ACL token. 25 | `consul-aws` loads your AWS configuration from `.aws`, from the instance profile and ENV variables - it supports everything provided by the AWS golang sdk. 26 | A default AWS region is not assumed. 27 | You can specify this with the standard AWS environment variables or as part of your static credentials. 28 | 29 | Apart from that a AWS CloudMap namespace id has to be provided. This is how `consul-aws` could be invoked to sync both directions: 30 | 31 | ```shell 32 | $ ./consul-aws sync-catalog -aws-namespace-id ns-hjrgt3bapp7phzff -to-aws -to-consul 33 | ``` 34 | 35 | ## Contributing 36 | 37 | To build and install `consul-aws` locally, Go version 1.21+ is required. 38 | You will also need to install the Docker engine: 39 | 40 | - [Docker for Mac](https://docs.docker.com/engine/installation/mac/) 41 | - [Docker for Windows](https://docs.docker.com/engine/installation/windows/) 42 | - [Docker for Linux](https://docs.docker.com/engine/installation/linux/ubuntulinux/) 43 | 44 | Clone the repository: 45 | 46 | ```shell 47 | $ git clone https://github.com/hashicorp/consul-aws.git 48 | ``` 49 | 50 | To compile the `consul-aws` binary for your local machine: 51 | 52 | ```shell 53 | $ make dev 54 | ``` 55 | 56 | This will compile the `consul-aws` binary into `dist/$OS/$ARCH/consul-aws` as well as your `$GOPATH`. 57 | 58 | To create a docker image with your local changes: 59 | 60 | ```shell 61 | $ make dev-docker 62 | ``` 63 | ## Testing 64 | 65 | If you just want to run the tests: 66 | 67 | ```shell 68 | $ make test 69 | ``` 70 | 71 | Or to run a specific test in the suite: 72 | 73 | ```shell 74 | go test ./... -run SomeTestFunction_name 75 | ``` 76 | 77 | **Note:** To run the sync integration tests, you must specify `INTTEST=1` in your environment and [AWS credentials](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials). 78 | You must also have a Consul server running locally. 79 | 80 | ## Compatibility with Consul 81 | 82 | `consul-aws` is compatible with supported versions of Consul. 83 | See [long-term support docs](https://developer.hashicorp.com/consul/docs/enterprise/long-term-support#long-term-support-lifecycle) for more information. 84 | 85 | [releases]: https://releases.hashicorp.com/consul-aws "Consul-AWS Releases" 86 | -------------------------------------------------------------------------------- /catalog/aws.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package catalog 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "github.com/aws/aws-sdk-go-v2/aws" 16 | awssd "github.com/aws/aws-sdk-go-v2/service/servicediscovery" 17 | awssdtypes "github.com/aws/aws-sdk-go-v2/service/servicediscovery/types" 18 | "github.com/hashicorp/go-hclog" 19 | ) 20 | 21 | const ( 22 | // ConsulAWSTag is used for imported services from AWS 23 | ConsulAWSTag = "aws" 24 | ConsulSourceKey = "external-source" 25 | ConsulAWSNS = "external-aws-ns" 26 | ConsulAWSID = "external-aws-id" 27 | ) 28 | 29 | type awsSyncer struct { 30 | lock sync.RWMutex 31 | client *awssd.Client 32 | log hclog.Logger 33 | namespace *awssdtypes.Namespace 34 | services map[string]service 35 | trigger chan bool 36 | consulPrefix string 37 | awsPrefix string 38 | toConsul bool 39 | pullInterval time.Duration 40 | dnsTTL int64 41 | } 42 | 43 | var awsServiceDescription = "Imported from Consul" 44 | 45 | func (a *awsSyncer) sync(consul *consul, stop, stopped chan struct{}) { 46 | defer close(stopped) 47 | for { 48 | select { 49 | case <-a.trigger: 50 | if !a.toConsul { 51 | continue 52 | } 53 | create := onlyInFirst(a.getServices(), consul.getServices()) 54 | count := consul.create(create) 55 | if count > 0 { 56 | consul.log.Info("created", "count", fmt.Sprintf("%d", count)) 57 | } 58 | 59 | remove := onlyInFirst(consul.getServices(), a.getServices()) 60 | count = consul.remove(remove) 61 | if count > 0 { 62 | consul.log.Info("removed", "count", fmt.Sprintf("%d", count)) 63 | } 64 | case <-stop: 65 | return 66 | } 67 | } 68 | } 69 | 70 | func (a *awsSyncer) fetchNamespace(id string) (*awssdtypes.Namespace, error) { 71 | resp, err := a.client.GetNamespace(context.Background(), &awssd.GetNamespaceInput{Id: aws.String(id)}) 72 | if err != nil { 73 | return nil, err 74 | } 75 | return resp.Namespace, nil 76 | } 77 | 78 | func (a *awsSyncer) fetchServices() ([]awssdtypes.ServiceSummary, error) { 79 | paginator := awssd.NewListServicesPaginator(a.client, &awssd.ListServicesInput{ 80 | Filters: []awssdtypes.ServiceFilter{{ 81 | Name: awssdtypes.ServiceFilterNameNamespaceId, 82 | Condition: awssdtypes.FilterConditionEq, 83 | Values: []string{*a.namespace.Id}, 84 | }}, 85 | }) 86 | 87 | services := []awssdtypes.ServiceSummary{} 88 | for paginator.HasMorePages() { 89 | p, err := paginator.NextPage(context.TODO()) 90 | if err != nil { 91 | return nil, fmt.Errorf("error paging through services: %s", err) 92 | } 93 | services = append(services, p.Services...) 94 | } 95 | return services, nil 96 | } 97 | 98 | func (a *awsSyncer) transformServices(awsServices []awssdtypes.ServiceSummary) map[string]service { 99 | services := map[string]service{} 100 | for _, as := range awsServices { 101 | s := service{ 102 | id: *as.Id, 103 | name: *as.Name, 104 | awsID: *as.Id, 105 | awsNamespace: *a.namespace.Id, 106 | } 107 | if as.Description != nil && *as.Description == awsServiceDescription { 108 | s.fromConsul = true 109 | s.name = strings.TrimPrefix(s.name, a.consulPrefix) 110 | } 111 | 112 | services[s.name] = s 113 | } 114 | return services 115 | } 116 | 117 | func (a *awsSyncer) setupNamespace(id string) error { 118 | namespace, err := a.fetchNamespace(id) 119 | if err != nil { 120 | return err 121 | } 122 | a.namespace = namespace 123 | return nil 124 | } 125 | 126 | func (a *awsSyncer) fetch() error { 127 | awsService, err := a.fetchServices() 128 | if err != nil { 129 | return err 130 | } 131 | services := a.transformServices(awsService) 132 | for h, s := range services { 133 | var awsNodes []awssdtypes.InstanceSummary 134 | var err error 135 | name := s.name 136 | if s.fromConsul { 137 | name = a.consulPrefix + name 138 | } 139 | awsNodes, err = a.discoverNodes(name) 140 | if err != nil { 141 | a.log.Error("cannot discover nodes", "error", err) 142 | continue 143 | } 144 | 145 | nodes := a.transformNodes(awsNodes) 146 | if len(nodes) == 0 { 147 | continue 148 | } 149 | s.nodes = nodes 150 | 151 | healths, err := a.fetchHealths(s.awsID) 152 | if err != nil { 153 | a.log.Error("cannot fetch healths", "error", err) 154 | } else { 155 | if s.fromConsul { 156 | healths = a.rekeyHealths(s.name, healths) 157 | } 158 | s.healths = healths 159 | } 160 | 161 | services[h] = s 162 | } 163 | a.setServices(services) 164 | return nil 165 | } 166 | 167 | func (a *awsSyncer) getNodeForConsulID(name, id string) (node, bool) { 168 | a.lock.RLock() 169 | copy, ok := a.services[name] 170 | a.lock.RUnlock() 171 | if !ok { 172 | return node{}, ok 173 | } 174 | for _, nodes := range copy.nodes { 175 | for _, n := range nodes { 176 | if n.consulID == id { 177 | return n, true 178 | } 179 | } 180 | } 181 | return node{}, false 182 | } 183 | 184 | func (a *awsSyncer) rekeyHealths(name string, healths map[string]health) map[string]health { 185 | rekeyed := map[string]health{} 186 | s, ok := a.getService(name) 187 | if !ok { 188 | return nil 189 | } 190 | for k, h := range s.healths { 191 | if n, ok := a.getNodeForConsulID(name, k); ok { 192 | rekeyed[id(k, n.host, n.port)] = h 193 | } 194 | } 195 | return rekeyed 196 | } 197 | 198 | func statusFromAWS(aws awssdtypes.HealthStatus) health { 199 | var result health 200 | switch aws { 201 | case awssdtypes.HealthStatusHealthy: 202 | result = passing 203 | case awssdtypes.HealthStatusUnhealthy: 204 | result = critical 205 | case awssdtypes.HealthStatusUnknown: 206 | result = unknown 207 | } 208 | return result 209 | } 210 | 211 | func (a *awsSyncer) fetchHealths(id string) (map[string]health, error) { 212 | paginator := awssd.NewGetInstancesHealthStatusPaginator(a.client, &awssd.GetInstancesHealthStatusInput{ 213 | ServiceId: &id, 214 | }) 215 | 216 | result := map[string]health{} 217 | for paginator.HasMorePages() { 218 | p, err := paginator.NextPage(context.TODO()) 219 | 220 | var notFound *awssdtypes.InstanceNotFound 221 | if errors.As(err, ¬Found) { 222 | // Note (dans): I think this is the case when all the instances have an unknown health status for a service, 223 | // which is fairly common because we don't sync the health status from Consul to AWS (yet). 224 | a.log.Trace("instance not found", "service-id", id) 225 | return result, nil 226 | } 227 | if err != nil { 228 | return nil, fmt.Errorf("error paging through healths: %s", err) 229 | } 230 | 231 | for id, health := range p.Status { 232 | result[id] = statusFromAWS(health) 233 | } 234 | } 235 | 236 | return result, nil 237 | } 238 | 239 | func (a *awsSyncer) transformNodes(awsNodes []awssdtypes.InstanceSummary) map[string]map[int]node { 240 | nodes := map[string]map[int]node{} 241 | for _, an := range awsNodes { 242 | h := an.Attributes["AWS_INSTANCE_IPV4"] 243 | p := 0 244 | if an.Attributes["AWS_INSTANCE_PORT"] != "" { 245 | p, _ = strconv.Atoi(an.Attributes["AWS_INSTANCE_PORT"]) 246 | } 247 | if nodes[h] == nil { 248 | nodes[h] = map[int]node{} 249 | } 250 | n := nodes[h] 251 | n[p] = node{port: p, host: h, awsID: *an.Id, attributes: an.Attributes} 252 | nodes[h] = n 253 | } 254 | return nodes 255 | } 256 | 257 | func (a *awsSyncer) fetchNodes(id string) ([]awssdtypes.InstanceSummary, error) { 258 | paginator := awssd.NewListInstancesPaginator(a.client, &awssd.ListInstancesInput{ 259 | ServiceId: &id, 260 | }) 261 | 262 | nodes := []awssdtypes.InstanceSummary{} 263 | for paginator.HasMorePages() { 264 | p, err := paginator.NextPage(context.TODO()) 265 | if err != nil { 266 | return nil, fmt.Errorf("error paging through instances: %s", err) 267 | } 268 | nodes = append(nodes, p.Instances...) 269 | } 270 | return nodes, nil 271 | } 272 | 273 | func (a *awsSyncer) discoverNodes(name string) ([]awssdtypes.InstanceSummary, error) { 274 | if a.namespace.Properties == nil || 275 | a.namespace.Properties.HttpProperties == nil || 276 | a.namespace.Properties.HttpProperties.HttpName == nil { 277 | return nil, fmt.Errorf("namespace properties are nil") 278 | } 279 | 280 | resp, err := a.client.DiscoverInstances(context.TODO(), &awssd.DiscoverInstancesInput{ 281 | HealthStatus: awssdtypes.HealthStatusFilterHealthy, 282 | // This is the http name, which can be different from the display name in the event there have been 283 | // multiple versions of the namespace (i.e. it has been recreated). 284 | NamespaceName: a.namespace.Properties.HttpProperties.HttpName, 285 | ServiceName: aws.String(name), 286 | }) 287 | if err != nil { 288 | return nil, err 289 | } 290 | nodes := []awssdtypes.InstanceSummary{} 291 | for _, i := range resp.Instances { 292 | nodes = append(nodes, awssdtypes.InstanceSummary{Id: i.InstanceId, Attributes: i.Attributes}) 293 | } 294 | return nodes, nil 295 | } 296 | 297 | func (a *awsSyncer) getServices() map[string]service { 298 | a.lock.RLock() 299 | copy := a.services 300 | a.lock.RUnlock() 301 | return copy 302 | } 303 | 304 | func (a *awsSyncer) getService(name string) (service, bool) { 305 | a.lock.RLock() 306 | copy, ok := a.services[name] 307 | a.lock.RUnlock() 308 | return copy, ok 309 | } 310 | 311 | func (a *awsSyncer) setServices(services map[string]service) { 312 | a.lock.Lock() 313 | a.services = services 314 | a.lock.Unlock() 315 | } 316 | 317 | func (a *awsSyncer) create(services map[string]service) int { 318 | wg := sync.WaitGroup{} 319 | count := 0 320 | for k, s := range services { 321 | if s.fromAWS { 322 | continue 323 | } 324 | name := a.consulPrefix + k 325 | if len(s.awsID) == 0 { 326 | input := awssd.CreateServiceInput{ 327 | Description: &awsServiceDescription, 328 | Name: &name, 329 | NamespaceId: a.namespace.Id, 330 | } 331 | if a.namespace.Type != awssdtypes.NamespaceTypeHttp { 332 | input.DnsConfig = &awssdtypes.DnsConfig{ 333 | DnsRecords: []awssdtypes.DnsRecord{ 334 | {TTL: &a.dnsTTL, Type: awssdtypes.RecordTypeSrv}, 335 | }, 336 | } 337 | } 338 | resp, err := a.client.CreateService(context.TODO(), &input) 339 | if err != nil { 340 | var alreadyExists *awssdtypes.ServiceAlreadyExists 341 | if !errors.As(err, &alreadyExists) { 342 | a.log.Info("service already exists", "name", name) 343 | } else { 344 | a.log.Error("cannot create services in AWS", "error", err.Error()) 345 | } 346 | continue 347 | } 348 | s.awsID = *resp.Service.Id 349 | count++ 350 | } 351 | for h, nodes := range s.nodes { 352 | for _, n := range nodes { 353 | wg.Add(1) 354 | go func(serviceID, name, h string, n node) { 355 | wg.Done() 356 | instanceID := id(serviceID, h, n.port) 357 | attributes := n.attributes 358 | attributes["AWS_INSTANCE_IPV4"] = h 359 | attributes["AWS_INSTANCE_PORT"] = fmt.Sprintf("%d", n.port) 360 | _, err := a.client.RegisterInstance(context.TODO(), &awssd.RegisterInstanceInput{ 361 | ServiceId: &serviceID, 362 | Attributes: attributes, 363 | InstanceId: &instanceID, 364 | }) 365 | if err != nil { 366 | a.log.Error("cannot create nodes", "error", err.Error()) 367 | } 368 | }(s.awsID, name, h, n) 369 | } 370 | } 371 | // for instanceID, h := range s.healths { 372 | // wg.Add(1) 373 | // go func(serviceID, instanceID string, h health) { 374 | // defer wg.Done() 375 | // req := a.client.UpdateInstanceCustomHealthStatusRequest(&awssd.UpdateInstanceCustomHealthStatusInput{ 376 | // ServiceId: &serviceID, 377 | // InstanceId: &instanceID, 378 | // Status: statusToCustomHealth(h), 379 | // }) 380 | // _, err := req.Send() 381 | // if err != nil { 382 | // a.log.Error("cannot create custom health", "error", err.Error()) 383 | // } 384 | // }(s.awsID, instanceID, h) 385 | // } 386 | } 387 | wg.Wait() 388 | return count 389 | } 390 | 391 | func (a *awsSyncer) remove(services map[string]service) int { 392 | wg := sync.WaitGroup{} 393 | for _, s := range services { 394 | if !s.fromConsul || len(s.awsID) == 0 { 395 | continue 396 | } 397 | for h, nodes := range s.nodes { 398 | for _, n := range nodes { 399 | wg.Add(1) 400 | go func(serviceID, id string) { 401 | defer wg.Done() 402 | _, err := a.client.DeregisterInstance(context.TODO(), &awssd.DeregisterInstanceInput{ 403 | ServiceId: &serviceID, 404 | InstanceId: &id, 405 | }) 406 | if err != nil { 407 | a.log.Error("cannot remove instance", "error", err.Error()) 408 | } 409 | }(s.awsID, id(s.awsID, h, n.port)) 410 | } 411 | } 412 | } 413 | wg.Wait() 414 | 415 | count := 0 416 | for k, s := range services { 417 | if !s.fromConsul || len(s.awsID) == 0 { 418 | continue 419 | } 420 | origService, _ := a.getService(k) 421 | if len(s.nodes) < len(origService.nodes) { 422 | continue 423 | } 424 | _, err := a.client.DeleteService(context.TODO(), &awssd.DeleteServiceInput{ 425 | Id: &s.awsID, 426 | }) 427 | if err != nil { 428 | a.log.Error("cannot remove services", "name", k, "id", s.awsID, "error", err.Error()) 429 | } else { 430 | count++ 431 | } 432 | } 433 | return count 434 | } 435 | 436 | func (a *awsSyncer) fetchIndefinetely(stop, stopped chan struct{}) { 437 | defer close(stopped) 438 | for { 439 | err := a.fetch() 440 | if err != nil { 441 | a.log.Error("error fetching", "error", err.Error()) 442 | } else { 443 | a.trigger <- true 444 | } 445 | select { 446 | case <-stop: 447 | return 448 | case <-time.After(a.pullInterval): 449 | continue 450 | } 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /catalog/aws_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package catalog 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/aws/aws-sdk-go-v2/aws" 10 | awssdtypes "github.com/aws/aws-sdk-go-v2/service/servicediscovery/types" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestAWSTransformNodes(t *testing.T) { 15 | a := awsSyncer{} 16 | nodes := []awssdtypes.InstanceSummary{ 17 | {Id: aws.String("one"), Attributes: map[string]string{"AWS_INSTANCE_IPV4": "1.1.1.1", "AWS_INSTANCE_PORT": "1"}}, 18 | {Id: aws.String("two"), Attributes: map[string]string{"AWS_INSTANCE_IPV4": "1.1.1.2", "AWS_INSTANCE_PORT": "A"}}, 19 | {Id: aws.String("three"), Attributes: map[string]string{"AWS_INSTANCE_IPV4": "1.1.1.3"}}, 20 | {Id: aws.String("four"), Attributes: map[string]string{"AWS_INSTANCE_IPV4": "1.1.1.1", "AWS_INSTANCE_PORT": "2"}}, 21 | {Id: aws.String("five"), Attributes: map[string]string{"AWS_INSTANCE_IPV4": "1.1.1.4", "AWS_INSTANCE_PORT": "4", "custom": "aha"}}, 22 | } 23 | expected := map[string]map[int]node{ 24 | "1.1.1.1": { 25 | 1: {port: 1, host: "1.1.1.1", awsID: "one", attributes: map[string]string{"AWS_INSTANCE_IPV4": "1.1.1.1", "AWS_INSTANCE_PORT": "1"}}, 26 | 2: {port: 2, host: "1.1.1.1", awsID: "four", attributes: map[string]string{"AWS_INSTANCE_IPV4": "1.1.1.1", "AWS_INSTANCE_PORT": "2"}}, 27 | }, 28 | "1.1.1.2": { 29 | 0: {port: 0, host: "1.1.1.2", awsID: "two", attributes: map[string]string{"AWS_INSTANCE_IPV4": "1.1.1.2", "AWS_INSTANCE_PORT": "A"}}, 30 | }, 31 | "1.1.1.3": { 32 | 0: {port: 0, host: "1.1.1.3", awsID: "three", attributes: map[string]string{"AWS_INSTANCE_IPV4": "1.1.1.3"}}, 33 | }, 34 | "1.1.1.4": { 35 | 4: {port: 4, host: "1.1.1.4", awsID: "five", attributes: map[string]string{"AWS_INSTANCE_IPV4": "1.1.1.4", "AWS_INSTANCE_PORT": "4", "custom": "aha"}}, 36 | }, 37 | } 38 | require.Equal(t, expected, a.transformNodes(nodes)) 39 | } 40 | 41 | func TestAWSTransformServices(t *testing.T) { 42 | a := awsSyncer{} 43 | services := []awssdtypes.ServiceSummary{ 44 | {Id: aws.String("one"), Name: aws.String("web"), Description: &awsServiceDescription}, 45 | {Id: aws.String("two"), Name: aws.String("redis")}, 46 | } 47 | expected := map[string]service{ 48 | "web": {id: "one", name: "web", awsID: "one", fromConsul: true}, 49 | "redis": {id: "two", name: "redis", awsID: "two", fromConsul: false}, 50 | } 51 | require.Equal(t, expected, a.transformServices(services)) 52 | } 53 | -------------------------------------------------------------------------------- /catalog/consul.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package catalog 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/hashicorp/consul/api" 13 | "github.com/hashicorp/go-hclog" 14 | ) 15 | 16 | const ( 17 | ConsulAWSNodeName = "consul-aws" 18 | WaitTime = 10 19 | ) 20 | 21 | type consul struct { 22 | client *api.Client 23 | log hclog.Logger 24 | consulPrefix string 25 | awsPrefix string 26 | services map[string]service 27 | trigger chan bool 28 | lock sync.RWMutex 29 | toAWS bool 30 | stale bool 31 | } 32 | 33 | func (c *consul) getServices() map[string]service { 34 | c.lock.RLock() 35 | copy := c.services 36 | c.lock.RUnlock() 37 | return copy 38 | } 39 | 40 | func (c *consul) getService(name string) (service, bool) { 41 | c.lock.RLock() 42 | copy, ok := c.services[name] 43 | c.lock.RUnlock() 44 | return copy, ok 45 | } 46 | 47 | func (c *consul) getAWSID(name, host string, port int) (string, bool) { 48 | if n, ok := c.getNode(name, host, port); ok { 49 | return n.awsID, true 50 | } 51 | return "", false 52 | } 53 | 54 | func (c *consul) getNode(name, host string, port int) (node, bool) { 55 | c.lock.RLock() 56 | copy, ok := c.services[name] 57 | c.lock.RUnlock() 58 | if ok { 59 | if nodes, ok := copy.nodes[host]; ok { 60 | for _, n := range nodes { 61 | if n.port == port { 62 | return n, true 63 | } 64 | } 65 | } 66 | } 67 | return node{}, false 68 | } 69 | 70 | func (c *consul) getNodeForAWSID(name, id string) (node, bool) { 71 | c.lock.RLock() 72 | copy, ok := c.services[name] 73 | c.lock.RUnlock() 74 | if !ok { 75 | return node{}, ok 76 | } 77 | 78 | for _, nodes := range copy.nodes { 79 | for _, n := range nodes { 80 | if n.awsID == id { 81 | return n, true 82 | } 83 | } 84 | } 85 | return node{}, false 86 | } 87 | 88 | func (c *consul) setServices(services map[string]service) { 89 | c.lock.Lock() 90 | c.services = services 91 | c.lock.Unlock() 92 | } 93 | 94 | func (c *consul) setNode(k, h string, p int, n node) { 95 | c.lock.Lock() 96 | if s, ok := c.services[k]; ok { 97 | nodes := s.nodes 98 | if nodes == nil { 99 | nodes = map[string]map[int]node{} 100 | } 101 | ports := nodes[h] 102 | if ports == nil { 103 | ports = map[int]node{} 104 | } 105 | ports[p] = n 106 | nodes[h] = ports 107 | s.nodes = nodes 108 | c.services[k] = s 109 | } 110 | c.lock.Unlock() 111 | } 112 | 113 | func (c *consul) sync(aws *awsSyncer, stop, stopped chan struct{}) { 114 | defer close(stopped) 115 | for { 116 | select { 117 | case <-c.trigger: 118 | if !c.toAWS { 119 | continue 120 | } 121 | create := onlyInFirst(c.getServices(), aws.getServices()) 122 | count := aws.create(create) 123 | if count > 0 { 124 | aws.log.Info("created", "count", fmt.Sprintf("%d", count)) 125 | } 126 | 127 | remove := onlyInFirst(aws.getServices(), c.getServices()) 128 | count = aws.remove(remove) 129 | if count > 0 { 130 | aws.log.Info("removed", "count", fmt.Sprintf("%d", count)) 131 | } 132 | case <-stop: 133 | return 134 | } 135 | } 136 | } 137 | 138 | func (c *consul) transformNodes(cnodes []*api.CatalogService) map[string]map[int]node { 139 | nodes := map[string]map[int]node{} 140 | for _, n := range cnodes { 141 | address := n.ServiceAddress 142 | if len(address) == 0 { 143 | address = n.Address 144 | } 145 | if nodes[address] == nil { 146 | nodes[address] = map[int]node{} 147 | } 148 | ports := nodes[address] 149 | ports[n.ServicePort] = node{port: n.ServicePort, host: address, consulID: n.ServiceID, awsID: n.ServiceMeta[ConsulAWSID], attributes: n.ServiceMeta} 150 | nodes[address] = ports 151 | } 152 | return nodes 153 | } 154 | 155 | func (c *consul) fetchNodes(service string) ([]*api.CatalogService, error) { 156 | opts := &api.QueryOptions{AllowStale: c.stale} 157 | nodes, _, err := c.client.Catalog().Service(service, "", opts) 158 | if err != nil { 159 | return nil, fmt.Errorf("error querying services, will retry: %s", err) 160 | } 161 | return nodes, err 162 | } 163 | 164 | func (c *consul) transformHealth(chealths api.HealthChecks) map[string]health { 165 | healths := map[string]health{} 166 | for _, h := range chealths { 167 | switch h.Status { 168 | case "passing": 169 | healths[h.ServiceID] = passing 170 | case "critical": 171 | healths[h.ServiceID] = critical 172 | default: 173 | healths[h.ServiceID] = unknown 174 | } 175 | } 176 | return healths 177 | } 178 | 179 | func (c *consul) fetchHealth(name string) (api.HealthChecks, error) { 180 | opts := &api.QueryOptions{AllowStale: c.stale} 181 | status, _, err := c.client.Health().Checks(name, opts) 182 | if err != nil { 183 | return nil, fmt.Errorf("error querying health, will retry: %s", err) 184 | } 185 | return status, nil 186 | } 187 | 188 | func (c *consul) fetchServices(waitIndex uint64) (map[string][]string, uint64, error) { 189 | opts := &api.QueryOptions{ 190 | AllowStale: c.stale, 191 | WaitIndex: waitIndex, 192 | WaitTime: WaitTime * time.Second, 193 | } 194 | services, meta, err := c.client.Catalog().Services(opts) 195 | if err != nil { 196 | return services, 0, err 197 | } 198 | return services, meta.LastIndex, nil 199 | } 200 | 201 | func (c *consul) fetch(waitIndex uint64) (uint64, error) { 202 | cservices, waitIndex, err := c.fetchServices(waitIndex) 203 | if err != nil { 204 | return waitIndex, fmt.Errorf("error fetching services: %s", err) 205 | } 206 | services := c.transformServices(cservices) 207 | for id, s := range c.transformServices(cservices) { 208 | if s.fromAWS { 209 | id = c.awsPrefix + id 210 | } 211 | if cnodes, err := c.fetchNodes(id); err == nil { 212 | s.nodes = c.transformNodes(cnodes) 213 | } else { 214 | c.log.Error("error fetching nodes", "error", err) 215 | continue 216 | } 217 | if chealths, err := c.fetchHealth(id); err == nil { 218 | s.healths = c.transformHealth(chealths) 219 | } else { 220 | // TODO (hans): decide what to do when health errors 221 | c.log.Error("error fetching health", "error", err) 222 | } 223 | if s.fromAWS { 224 | s.healths = c.rekeyHealths(s.name, s.healths) 225 | } 226 | services[id] = s 227 | } 228 | c.setServices(services) 229 | return waitIndex, nil 230 | } 231 | 232 | func (c *consul) transformServices(cservices map[string][]string) map[string]service { 233 | services := make(map[string]service, len(cservices)) 234 | for k, tags := range cservices { 235 | s := service{id: k, name: k, consulID: k} 236 | for _, t := range tags { 237 | if t == ConsulAWSTag { 238 | s.fromAWS = true 239 | break 240 | } 241 | } 242 | if s.fromAWS { 243 | s.name = strings.TrimPrefix(k, c.awsPrefix) 244 | } 245 | services[s.name] = s 246 | } 247 | return services 248 | } 249 | 250 | func (c *consul) rekeyHealths(name string, healths map[string]health) map[string]health { 251 | rekeyed := map[string]health{} 252 | for id, h := range healths { 253 | host, port := hostPortFromID(id) 254 | if awsID, ok := c.getAWSID(name, host, port); ok { 255 | rekeyed[awsID] = h 256 | } 257 | } 258 | return rekeyed 259 | } 260 | 261 | func (c *consul) fetchIndefinetely(stop, stopped chan struct{}) { 262 | defer close(stopped) 263 | waitIndex := uint64(1) 264 | subsequentErrors := 0 265 | for { 266 | newIndex, err := c.fetch(waitIndex) 267 | if err != nil { 268 | c.log.Error("error fetching", "error", err.Error()) 269 | subsequentErrors++ 270 | if subsequentErrors > 10 { 271 | return 272 | } 273 | time.Sleep(500 * time.Millisecond) 274 | } else { 275 | subsequentErrors = 0 276 | waitIndex = newIndex 277 | c.trigger <- true 278 | } 279 | select { 280 | case <-stop: 281 | return 282 | default: 283 | } 284 | } 285 | } 286 | 287 | func (c *consul) create(services map[string]service) int { 288 | wg := sync.WaitGroup{} 289 | count := 0 290 | for k, s := range services { 291 | if s.fromConsul { 292 | continue 293 | } 294 | name := c.awsPrefix + k 295 | for h, nodes := range s.nodes { 296 | for _, n := range nodes { 297 | wg.Add(1) 298 | go func(ns, k, name, h string, n node) { 299 | defer wg.Done() 300 | id := id(k, h, n.port) 301 | meta := map[string]string{} 302 | for k, v := range n.attributes { 303 | meta[k] = v 304 | } 305 | meta[ConsulSourceKey] = ConsulAWSTag 306 | meta[ConsulAWSNS] = ns 307 | meta[ConsulAWSID] = n.awsID 308 | service := api.AgentService{ 309 | ID: id, 310 | Service: name, 311 | Tags: []string{ConsulAWSTag}, 312 | Address: h, 313 | Meta: meta, 314 | } 315 | if n.port != 0 { 316 | service.Port = n.port 317 | } 318 | reg := api.CatalogRegistration{ 319 | Node: ConsulAWSNodeName, 320 | Address: h, 321 | NodeMeta: map[string]string{ConsulSourceKey: ConsulAWSTag}, 322 | SkipNodeUpdate: true, 323 | Service: &service, 324 | } 325 | _, err := c.client.Catalog().Register(®, nil) 326 | if err != nil { 327 | c.log.Error("cannot create service", "error", err.Error()) 328 | } else { 329 | c.setNode(k, h, n.port, n) 330 | count++ 331 | } 332 | }(s.awsNamespace, k, name, h, n) 333 | } 334 | } 335 | for awsID, h := range s.healths { 336 | n, ok := c.getNodeForAWSID(k, awsID) 337 | if !ok { 338 | continue 339 | } 340 | wg.Add(1) 341 | go func(serviceID string, n node, h health) { 342 | defer wg.Done() 343 | reg := api.CatalogRegistration{ 344 | Node: ConsulAWSNodeName, 345 | SkipNodeUpdate: true, 346 | Check: &api.AgentCheck{ 347 | CheckID: "check" + id(serviceID, n.host, n.port), 348 | ServiceID: id(serviceID, n.host, n.port), 349 | Node: "consul-aws", 350 | Name: "AWS Route53 Health Check", 351 | Status: string(h), 352 | }, 353 | } 354 | _, err := c.client.Catalog().Register(®, nil) 355 | if err != nil { 356 | c.log.Error("cannot create healthcheck", "id", id(k, n.host, n.port), "error", err.Error()) 357 | } else { 358 | count++ 359 | } 360 | }(k, n, h) 361 | } 362 | } 363 | wg.Wait() 364 | return count 365 | } 366 | 367 | func (c *consul) remove(services map[string]service) int { 368 | wg := sync.WaitGroup{} 369 | count := 0 370 | for k, s := range services { 371 | if !s.fromAWS { 372 | continue 373 | } 374 | for h, nodes := range s.nodes { 375 | for p := range nodes { 376 | wg.Add(1) 377 | go func(id string) { 378 | defer wg.Done() 379 | _, err := c.client.Catalog().Deregister(&api.CatalogDeregistration{Node: ConsulAWSNodeName, ServiceID: id}, nil) 380 | if err != nil { 381 | c.log.Error("cannot remove service", "error", err.Error()) 382 | } else { 383 | count++ 384 | } 385 | }(id(k, h, p)) 386 | } 387 | } 388 | } 389 | wg.Wait() 390 | return count 391 | } 392 | -------------------------------------------------------------------------------- /catalog/consul_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package catalog 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/hashicorp/consul/api" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestConsulRekeyHealths(t *testing.T) { 14 | type variant struct { 15 | nodes map[string]map[int]node 16 | healths map[string]health 17 | expected map[string]health 18 | } 19 | variants := []variant{ 20 | { 21 | nodes: map[string]map[int]node{}, 22 | healths: map[string]health{}, 23 | expected: map[string]health{}, 24 | }, 25 | { 26 | nodes: map[string]map[int]node{ 27 | "1.1.1.1": {8000: {port: 8000, awsID: "X1"}}, 28 | }, 29 | healths: map[string]health{ 30 | "web_1.1.1.1_8000": passing, 31 | }, 32 | expected: map[string]health{ 33 | "X1": passing, 34 | }, 35 | }, 36 | } 37 | 38 | for _, v := range variants { 39 | c := consul{ 40 | services: map[string]service{ 41 | "s1": { 42 | nodes: v.nodes, 43 | healths: v.healths, 44 | }, 45 | }, 46 | } 47 | require.Equal(t, v.expected, c.rekeyHealths("s1", c.services["s1"].healths)) 48 | } 49 | } 50 | 51 | func TestConsulTransformServices(t *testing.T) { 52 | c := consul{awsPrefix: "aws_"} 53 | services := map[string][]string{"s1": {"abc"}, "aws_s2": {ConsulAWSTag}} 54 | expected := map[string]service{"s1": {id: "s1", name: "s1", consulID: "s1"}, "s2": {id: "aws_s2", name: "s2", consulID: "aws_s2", fromAWS: true}} 55 | 56 | require.Equal(t, expected, c.transformServices(services)) 57 | } 58 | 59 | func TestConsulTransformNodes(t *testing.T) { 60 | c := consul{} 61 | nodes := []*api.CatalogService{ 62 | { 63 | ServiceAddress: "1.1.1.1", 64 | ServicePort: 1, 65 | ServiceID: "s1", 66 | ServiceMeta: map[string]string{ConsulAWSID: "aws1"}, 67 | }, 68 | { 69 | Address: "1.1.1.2", 70 | ServicePort: 1, 71 | ServiceID: "s1", 72 | ServiceMeta: map[string]string{ConsulAWSID: "aws1"}, 73 | }, 74 | { 75 | Address: "1.1.1.3", 76 | ServicePort: 3, 77 | ServiceID: "s2", 78 | ServiceMeta: map[string]string{"A": "B"}, 79 | }, 80 | } 81 | expected := map[string]map[int]node{ 82 | "1.1.1.1": {1: {port: 1, host: "1.1.1.1", awsID: "aws1", consulID: "s1", attributes: map[string]string{ConsulAWSID: "aws1"}}}, 83 | "1.1.1.2": {1: {port: 1, host: "1.1.1.2", awsID: "aws1", consulID: "s1", attributes: map[string]string{ConsulAWSID: "aws1"}}}, 84 | "1.1.1.3": {3: {port: 3, host: "1.1.1.3", consulID: "s2", attributes: map[string]string{"A": "B"}}}, 85 | } 86 | require.Equal(t, expected, c.transformNodes(nodes)) 87 | } 88 | 89 | func TestConsulTransformHeath(t *testing.T) { 90 | c := consul{} 91 | healths := api.HealthChecks{ 92 | &api.HealthCheck{Status: "passing", ServiceID: "s1"}, 93 | &api.HealthCheck{Status: "critical", ServiceID: "s2"}, 94 | &api.HealthCheck{Status: "warning", ServiceID: "s3"}, 95 | } 96 | expected := map[string]health{ 97 | "s1": passing, 98 | "s2": critical, 99 | "s3": unknown, 100 | } 101 | require.Equal(t, expected, c.transformHealth(healths)) 102 | } 103 | -------------------------------------------------------------------------------- /catalog/service.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package catalog 5 | 6 | import ( 7 | "fmt" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | type health string 13 | 14 | const ( 15 | passing health = "passing" 16 | critical health = "critical" 17 | unknown health = "" 18 | ) 19 | 20 | type service struct { 21 | id string 22 | name string 23 | nodes map[string]map[int]node 24 | healths map[string]health 25 | fromConsul bool 26 | fromAWS bool 27 | awsID string 28 | consulID string 29 | awsNamespace string 30 | } 31 | 32 | type node struct { 33 | port int 34 | host string 35 | awsID string 36 | consulID string 37 | attributes map[string]string 38 | } 39 | 40 | func hostPortFromID(checkid string) (string, int) { 41 | parts := strings.Split(checkid, "_") 42 | l := len(parts) 43 | if l >= 2 { 44 | host := parts[l-2] 45 | portString := parts[l-1] 46 | port, _ := strconv.Atoi(portString) 47 | return host, port 48 | } 49 | return "", 0 50 | } 51 | 52 | func id(id, host string, port int) string { 53 | return fmt.Sprintf("%s_%s_%d", id, host, port) 54 | } 55 | 56 | func onlyInFirst(servicesA, servicesB map[string]service) map[string]service { 57 | result := map[string]service{} 58 | for k, sa := range servicesA { 59 | if sb, ok := servicesB[k]; !ok { 60 | result[k] = sa 61 | } else { 62 | nodes := map[string]map[int]node{} 63 | for h, nodesa := range sa.nodes { 64 | nodesb, ok := sb.nodes[h] 65 | if !ok { 66 | nodes[h] = nodesa 67 | continue 68 | } 69 | ports := map[int]node{} 70 | for p, na := range nodesa { 71 | if _, ok = nodesb[p]; !ok { 72 | ports[p] = na 73 | } 74 | } 75 | if len(ports) > 0 { 76 | nodes[h] = ports 77 | } 78 | } 79 | healths := map[string]health{} 80 | for k, ha := range sa.healths { 81 | if hb, ok := sb.healths[k]; !ok { 82 | healths[k] = ha 83 | } else { 84 | if ha != hb { 85 | healths[k] = ha 86 | } 87 | } 88 | } 89 | if len(nodes) == 0 && len(healths) == 0 { 90 | continue 91 | } 92 | id := sa.id 93 | if len(id) == 0 { 94 | id = sb.id 95 | } 96 | name := sa.name 97 | if len(name) == 0 { 98 | name = sb.name 99 | } 100 | aid := sa.awsID 101 | if len(aid) == 0 { 102 | aid = sb.awsID 103 | } 104 | cid := sa.consulID 105 | if len(cid) == 0 { 106 | cid = sb.consulID 107 | } 108 | ns := sa.awsNamespace 109 | if len(ns) == 0 { 110 | ns = sb.awsNamespace 111 | } 112 | s := service{ 113 | id: id, 114 | name: name, 115 | awsID: aid, 116 | consulID: cid, 117 | awsNamespace: ns, 118 | fromConsul: sa.fromConsul || sb.fromConsul, 119 | fromAWS: sa.fromAWS || sb.fromAWS, 120 | } 121 | if len(nodes) > 0 { 122 | s.nodes = nodes 123 | } 124 | if len(healths) > 0 { 125 | s.healths = healths 126 | } 127 | result[k] = s 128 | } 129 | } 130 | return result 131 | } 132 | -------------------------------------------------------------------------------- /catalog/service_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package catalog 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestOnlyInFirst(t *testing.T) { 13 | type variant struct { 14 | a map[string]service 15 | b map[string]service 16 | expected map[string]service 17 | } 18 | 19 | table := []variant{ 20 | { 21 | a: map[string]service{}, 22 | b: map[string]service{}, 23 | expected: map[string]service{}, 24 | }, 25 | { 26 | a: map[string]service{ 27 | "s1": {fromConsul: true}, 28 | }, 29 | b: map[string]service{}, 30 | expected: map[string]service{ 31 | "s1": {fromConsul: true}, 32 | }, 33 | }, 34 | { 35 | a: map[string]service{ 36 | "s2": {fromConsul: true, nodes: map[string]map[int]node{"h1": {1: {}}}}, 37 | }, 38 | b: map[string]service{ 39 | "s2": {nodes: map[string]map[int]node{"h2": {2: {}}}}, 40 | }, 41 | expected: map[string]service{ 42 | "s2": {fromConsul: true, nodes: map[string]map[int]node{"h1": {1: {}}}}, 43 | }, 44 | }, 45 | { 46 | a: map[string]service{ 47 | "s3": {fromConsul: false, nodes: map[string]map[int]node{"h1": {1: {port: 1}}}}, 48 | }, 49 | b: map[string]service{ 50 | "s3": {fromConsul: true, nodes: map[string]map[int]node{"h2": {2: {port: 2}}}}, 51 | }, 52 | expected: map[string]service{ 53 | "s3": {fromConsul: true, nodes: map[string]map[int]node{"h1": {1: {port: 1}}}}, 54 | }, 55 | }, 56 | { 57 | a: map[string]service{ 58 | "s4": {fromAWS: true}, 59 | }, 60 | b: map[string]service{}, 61 | expected: map[string]service{ 62 | "s4": {fromAWS: true}, 63 | }, 64 | }, 65 | { 66 | a: map[string]service{ 67 | "s5": {fromAWS: true, nodes: map[string]map[int]node{"h1": {1: {port: 1}}}}, 68 | }, 69 | b: map[string]service{ 70 | "s5": {nodes: map[string]map[int]node{"h2": {2: {port: 2}}}}, 71 | }, 72 | expected: map[string]service{ 73 | "s5": {fromAWS: true, nodes: map[string]map[int]node{"h1": {1: {port: 1}}}}, 74 | }, 75 | }, 76 | { 77 | a: map[string]service{ 78 | "s6": {fromAWS: false, nodes: map[string]map[int]node{"h1": {1: {port: 1}}}}, 79 | }, 80 | b: map[string]service{ 81 | "s6": {fromAWS: true, nodes: map[string]map[int]node{"h2": {2: {port: 2}}}}, 82 | }, 83 | expected: map[string]service{ 84 | "s6": {fromAWS: true, nodes: map[string]map[int]node{"h1": {1: {port: 1}}}}, 85 | }, 86 | }, 87 | { 88 | a: map[string]service{"s7": {}}, 89 | b: map[string]service{}, 90 | expected: map[string]service{"s7": {}}, 91 | }, 92 | { 93 | a: map[string]service{"s8": {}}, 94 | b: map[string]service{"s8": {}}, 95 | expected: map[string]service{}, 96 | }, 97 | { 98 | a: map[string]service{"s9": {}, "s10": {}}, 99 | b: map[string]service{"s9": {}}, 100 | expected: map[string]service{"s10": {}}, 101 | }, 102 | { 103 | a: map[string]service{ 104 | "s11": {nodes: map[string]map[int]node{"h1": {1: {port: 1}}, "h2": {2: {port: 2}}}}, 105 | }, 106 | b: map[string]service{ 107 | "s11": {nodes: map[string]map[int]node{"h1": {1: {port: 1}}, "h2": {2: {port: 2}}}}, 108 | }, 109 | expected: map[string]service{}, 110 | }, 111 | { 112 | a: map[string]service{ 113 | "s12": {nodes: map[string]map[int]node{"h1": {1: {port: 1}}, "h2": {2: {port: 2}}}}, 114 | }, 115 | b: map[string]service{ 116 | "s12": {nodes: map[string]map[int]node{"h2": {2: {port: 2}}}}, 117 | }, 118 | expected: map[string]service{ 119 | "s12": {nodes: map[string]map[int]node{"h1": {1: {port: 1}}}}, 120 | }, 121 | }, 122 | { 123 | a: map[string]service{ 124 | "s13": {nodes: map[string]map[int]node{"h1": {1: {port: 1}}, "h2": {2: {port: 2}}}}, 125 | }, 126 | b: map[string]service{ 127 | "s13": {awsID: "id", nodes: map[string]map[int]node{"h2": {2: {port: 2}}}}, 128 | }, 129 | expected: map[string]service{ 130 | "s13": {awsID: "id", nodes: map[string]map[int]node{"h1": {1: {port: 1}}}}, 131 | }, 132 | }, 133 | { 134 | a: map[string]service{ 135 | "s14": {nodes: map[string]map[int]node{"h1": {1: {port: 1}}, "h2": {2: {port: 2}}}}, 136 | }, 137 | b: map[string]service{ 138 | "s14": {awsNamespace: "ns1", nodes: map[string]map[int]node{"h2": {2: {port: 2}}}}, 139 | }, 140 | expected: map[string]service{ 141 | "s14": {awsNamespace: "ns1", nodes: map[string]map[int]node{"h1": {1: {port: 1}}}}, 142 | }, 143 | }, 144 | { 145 | a: map[string]service{ 146 | "s15": {nodes: map[string]map[int]node{"h1": {1: {awsID: "a1"}}}}, 147 | }, 148 | b: map[string]service{ 149 | "s15": {nodes: map[string]map[int]node{"h2": {}}}, 150 | }, 151 | expected: map[string]service{ 152 | "s15": {nodes: map[string]map[int]node{"h1": {1: {awsID: "a1"}}}}, 153 | }, 154 | }, 155 | { 156 | a: map[string]service{ 157 | "s16": {healths: map[string]health{"h1": passing, "h2": critical}}, 158 | }, 159 | b: map[string]service{ 160 | "s16": {healths: map[string]health{"h1": passing}}, 161 | }, 162 | expected: map[string]service{ 163 | "s16": {healths: map[string]health{"h2": critical}}, 164 | }, 165 | }, 166 | { 167 | a: map[string]service{ 168 | "s17": {healths: map[string]health{"h1": passing}}, 169 | }, 170 | b: map[string]service{ 171 | "s17": {healths: map[string]health{"h1": critical}}, 172 | }, 173 | expected: map[string]service{ 174 | "s17": {healths: map[string]health{"h1": passing}}, 175 | }, 176 | }, 177 | { 178 | a: map[string]service{ 179 | "s18": {healths: map[string]health{"h1": passing, "h2": critical}}, 180 | }, 181 | b: map[string]service{ 182 | "s18": {healths: map[string]health{"h2": critical, "h1": passing}}, 183 | }, 184 | expected: map[string]service{}, 185 | }, 186 | { 187 | a: map[string]service{ 188 | "s19": {nodes: map[string]map[int]node{"h1": {1: {port: 1}}, "h2": {2: {port: 2}}}}, 189 | }, 190 | b: map[string]service{ 191 | "s19": {consulID: "id", nodes: map[string]map[int]node{"h2": {2: {port: 2}}}}, 192 | }, 193 | expected: map[string]service{ 194 | "s19": {consulID: "id", nodes: map[string]map[int]node{"h1": {1: {port: 1}}}}, 195 | }, 196 | }, 197 | { 198 | a: map[string]service{ 199 | "s20": {nodes: map[string]map[int]node{"h1": {1: {port: 1}}, "h2": {2: {port: 2}}}}, 200 | }, 201 | b: map[string]service{ 202 | "s20": {id: "id", name: "name", nodes: map[string]map[int]node{"h2": {2: {port: 2}}}}, 203 | }, 204 | expected: map[string]service{ 205 | "s20": {id: "id", name: "name", nodes: map[string]map[int]node{"h1": {1: {port: 1}}}}, 206 | }, 207 | }, 208 | } 209 | 210 | for _, v := range table { 211 | require.Equal(t, v.expected, onlyInFirst(v.a, v.b)) 212 | } 213 | } 214 | 215 | func TestHostPortFromCheckID(t *testing.T) { 216 | host, port := hostPortFromID("service_abc_1.9.9.9_3333") 217 | require.Equal(t, "1.9.9.9", host) 218 | require.Equal(t, 3333, port) 219 | } 220 | -------------------------------------------------------------------------------- /catalog/sync.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package catalog 5 | 6 | import ( 7 | "time" 8 | 9 | awssd "github.com/aws/aws-sdk-go-v2/service/servicediscovery" 10 | "github.com/hashicorp/consul/api" 11 | "github.com/hashicorp/go-hclog" 12 | ) 13 | 14 | // Sync aws->consul and vice versa. 15 | func Sync(toAWS, toConsul bool, namespaceID, consulPrefix, awsPrefix, awsPullInterval string, awsDNSTTL int64, stale bool, awsClient *awssd.Client, consulClient *api.Client, stop, stopped chan struct{}) { 16 | defer close(stopped) 17 | log := hclog.Default().Named("sync") 18 | consul := consul{ 19 | client: consulClient, 20 | log: hclog.Default().Named("consul"), 21 | trigger: make(chan bool, 1), 22 | consulPrefix: consulPrefix, 23 | awsPrefix: awsPrefix, 24 | toAWS: toAWS, 25 | stale: stale, 26 | } 27 | pullInterval, err := time.ParseDuration(awsPullInterval) 28 | if err != nil { 29 | log.Error("cannot parse aws pull interval", "error", err) 30 | return 31 | } 32 | aws := awsSyncer{ 33 | client: awsClient, 34 | log: hclog.Default().Named("awsSyncer"), 35 | trigger: make(chan bool, 1), 36 | consulPrefix: consulPrefix, 37 | awsPrefix: awsPrefix, 38 | toConsul: toConsul, 39 | pullInterval: pullInterval, 40 | dnsTTL: awsDNSTTL, 41 | } 42 | 43 | err = aws.setupNamespace(namespaceID) 44 | if err != nil { 45 | log.Error("cannot setup namespace", "error", err) 46 | return 47 | } 48 | 49 | fetchConsulStop := make(chan struct{}) 50 | fetchConsulStopped := make(chan struct{}) 51 | go consul.fetchIndefinetely(fetchConsulStop, fetchConsulStopped) 52 | fetchAWSStop := make(chan struct{}) 53 | fetchAWSStopped := make(chan struct{}) 54 | go aws.fetchIndefinetely(fetchAWSStop, fetchAWSStopped) 55 | 56 | toConsulStop := make(chan struct{}) 57 | toConsulStopped := make(chan struct{}) 58 | toAWSStop := make(chan struct{}) 59 | toAWSStopped := make(chan struct{}) 60 | 61 | go aws.sync(&consul, toConsulStop, toConsulStopped) 62 | go consul.sync(&aws, toAWSStop, toAWSStopped) 63 | 64 | select { 65 | case <-stop: 66 | close(toConsulStop) 67 | close(toAWSStop) 68 | close(fetchConsulStop) 69 | close(fetchAWSStop) 70 | <-toConsulStopped 71 | <-toAWSStopped 72 | <-fetchAWSStopped 73 | <-fetchConsulStopped 74 | case <-fetchAWSStopped: 75 | log.Info("problem wit awsSyncer fetch. shutting down...") 76 | close(toConsulStop) 77 | close(toAWSStop) 78 | close(fetchConsulStop) 79 | <-toConsulStopped 80 | <-toAWSStopped 81 | <-fetchConsulStopped 82 | case <-fetchConsulStopped: 83 | log.Info("problem with consul fetch. shutting down...") 84 | close(toConsulStop) 85 | close(fetchAWSStop) 86 | close(toAWSStop) 87 | <-toConsulStopped 88 | <-toAWSStopped 89 | <-fetchAWSStopped 90 | case <-toConsulStopped: 91 | log.Info("problem with consul sync. shutting down...") 92 | close(fetchConsulStop) 93 | close(toAWSStop) 94 | close(fetchAWSStop) 95 | <-toAWSStopped 96 | <-fetchAWSStopped 97 | <-fetchConsulStopped 98 | case <-toAWSStopped: 99 | log.Info("problem with awsSyncer sync. shutting down...") 100 | close(toConsulStop) 101 | close(fetchConsulStop) 102 | close(fetchAWSStop) 103 | <-toConsulStopped 104 | <-fetchConsulStopped 105 | <-fetchAWSStopped 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /catalog/sync_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package catalog 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "os" 10 | "testing" 11 | "time" 12 | 13 | awsconfig "github.com/aws/aws-sdk-go-v2/config" 14 | awssd "github.com/aws/aws-sdk-go-v2/service/servicediscovery" 15 | awssdtypes "github.com/aws/aws-sdk-go-v2/service/servicediscovery/types" 16 | 17 | "github.com/hashicorp/consul/api" 18 | 19 | "github.com/hashicorp/consul-aws/internal/flags" 20 | ) 21 | 22 | // TestSync is an integration test that creates a service in Consul and AWS. 23 | // Documentation on setup can be found in the engineering docs for `consul-aws`. 24 | func TestSync(t *testing.T) { 25 | if len(os.Getenv("INTTEST")) == 0 { 26 | t.Skip("Set INTTEST=1 to enable integration tests") 27 | } 28 | namespaceID := os.Getenv("NAMESPACEID") 29 | if namespaceID == "" { 30 | t.Fatalf("The NAMESPACEID variable must be set.") 31 | } 32 | runSyncTest(t, namespaceID) 33 | } 34 | 35 | func runSyncTest(t *testing.T, namespaceID string) { 36 | // Test Setup 37 | config, err := awsconfig.LoadDefaultConfig(context.TODO()) 38 | if err != nil { 39 | t.Fatalf("Error retrieving AWS session: %s", err) 40 | } 41 | awssdClient := awssd.NewFromConfig(config) 42 | 43 | f := flags.HTTPFlags{} 44 | consulClient, err := f.APIClient() 45 | if err != nil { 46 | t.Fatalf("Error connecting to Consul agent: %s", err) 47 | } 48 | 49 | consulServiceID := "r1" 50 | consulServiceName := "redis" 51 | awsServiceName := "web" 52 | 53 | err = createServiceInConsul(consulClient, consulServiceID, consulServiceName) 54 | if err != nil { 55 | t.Fatalf("error creating service in Consul: %s", err) 56 | } 57 | 58 | awsServiceID, err := createServiceInAWS(awssdClient, namespaceID, awsServiceName) 59 | if err != nil { 60 | t.Fatalf("error creating service %s in aws: %s", awsServiceName, err) 61 | } 62 | err = createInstanceInAWS(awssdClient, awsServiceID) 63 | if err != nil { 64 | t.Fatalf("error creating instance in aws: %s", err) 65 | } 66 | 67 | stop := make(chan struct{}) 68 | stopped := make(chan struct{}) 69 | go Sync( 70 | true, true, namespaceID, 71 | "consul_", "aws_", 72 | "1s", 0, true, 73 | awssdClient, consulClient, 74 | stop, stopped, 75 | ) 76 | 77 | doneC := make(chan struct{}) 78 | doneA := make(chan struct{}) 79 | go func() { 80 | if err := checkForImportedAWSService(consulClient, "aws_"+awsServiceName, namespaceID, awsServiceID, 100); err != nil { 81 | t.Error(err) 82 | } else { 83 | close(doneA) 84 | } 85 | }() 86 | go func() { 87 | if err := checkForImportedConsulService(awssdClient, namespaceID, "consul_"+consulServiceName, 100); err != nil { 88 | t.Error(err) 89 | } else { 90 | close(doneC) 91 | } 92 | }() 93 | select { 94 | case <-time.After(20 * time.Second): 95 | } 96 | 97 | select { 98 | case <-doneC: 99 | default: 100 | t.Error("service was not imported in consul") 101 | } 102 | select { 103 | case <-doneA: 104 | default: 105 | t.Error("service was not imported in aws") 106 | } 107 | 108 | err = deleteInstanceInAWS(awssdClient, awsServiceID) 109 | if err != nil { 110 | t.Logf("error deregistering instance in AWS: %s", err) 111 | } 112 | err = deleteServiceInAWS(awssdClient, awsServiceID) 113 | if err != nil { 114 | t.Logf("error deleting service in AWS: %s", err) 115 | } 116 | err = deleteServiceInConsul(consulClient, consulServiceID) 117 | if err != nil { 118 | t.Logf("error deleting service in Consul: %s", err) 119 | } 120 | 121 | select { 122 | case <-time.After((WaitTime * 5) * time.Second): 123 | } 124 | if err = checkForImportedAWSService(consulClient, "aws_"+awsServiceName, namespaceID, awsServiceID, 1); err == nil { 125 | t.Error("Expected that the imported aws services is deleted") 126 | } 127 | if err = checkForImportedConsulService(awssdClient, namespaceID, "consul_"+consulServiceName, 1); err == nil { 128 | t.Error("Expected that the imported consul services is deleted") 129 | } 130 | 131 | close(stop) 132 | <-stopped 133 | } 134 | func createServiceInConsul(c *api.Client, id, name string) error { 135 | reg := api.CatalogRegistration{ 136 | Node: ConsulAWSNodeName, 137 | Address: "127.0.0.1", 138 | SkipNodeUpdate: true, 139 | Service: &api.AgentService{ 140 | ID: id, 141 | Service: name, 142 | Address: "127.0.0.1", 143 | Port: 6379, 144 | Meta: map[string]string{ 145 | "BARFU": "FUBAR", 146 | }, 147 | }, 148 | } 149 | _, err := c.Catalog().Register(®, nil) 150 | return err 151 | } 152 | 153 | func deleteServiceInConsul(c *api.Client, id string) error { 154 | _, err := c.Catalog().Deregister(&api.CatalogDeregistration{Node: ConsulAWSNodeName, ServiceID: id}, nil) 155 | return err 156 | } 157 | 158 | func createServiceInAWS(a *awssd.Client, namespaceID, name string) (string, error) { 159 | ttl := int64(60) 160 | input := awssd.CreateServiceInput{ 161 | Name: &name, 162 | NamespaceId: &namespaceID, 163 | DnsConfig: &awssdtypes.DnsConfig{ 164 | DnsRecords: []awssdtypes.DnsRecord{ 165 | {TTL: &ttl, Type: awssdtypes.RecordTypeSrv}, 166 | }, 167 | RoutingPolicy: awssdtypes.RoutingPolicyMultivalue, 168 | }, 169 | HealthCheckCustomConfig: &awssdtypes.HealthCheckCustomConfig{}, 170 | } 171 | resp, err := a.CreateService(context.TODO(), &input) 172 | if err != nil { 173 | return "", err 174 | } 175 | return *resp.Service.Id, nil 176 | } 177 | 178 | func createInstanceInAWS(a *awssd.Client, serviceID string) error { 179 | _, err := a.RegisterInstance(context.TODO(), &awssd.RegisterInstanceInput{ 180 | ServiceId: &serviceID, 181 | InstanceId: &serviceID, 182 | Attributes: map[string]string{ 183 | "AWS_INSTANCE_IPV4": "127.0.0.1", 184 | "AWS_INSTANCE_PORT": "8000", 185 | "FUBAR": "BARFU", 186 | }, 187 | }) 188 | //if err != nil { 189 | // return err 190 | //} 191 | // 192 | //time.Sleep(30 * time.Second) // This is a hack to wait for the instance to be created 193 | //_, err = a.UpdateInstanceCustomHealthStatus(context.TODO(), &awssd.UpdateInstanceCustomHealthStatusInput{ 194 | // InstanceId: &serviceID, 195 | // ServiceId: &serviceID, 196 | // Status: awssdtypes.CustomHealthStatusHealthy, 197 | //}) 198 | return err 199 | } 200 | 201 | func deleteInstanceInAWS(a *awssd.Client, id string) error { 202 | _, err := a.DeregisterInstance(context.TODO(), &awssd.DeregisterInstanceInput{ServiceId: &id, InstanceId: &id}) 203 | return err 204 | } 205 | 206 | func deleteServiceInAWS(a *awssd.Client, id string) error { 207 | var err error 208 | for i := 0; i < 50; i++ { 209 | _, err = a.DeleteService(context.TODO(), &awssd.DeleteServiceInput{Id: &id}) 210 | if err != nil { 211 | time.Sleep(100 * time.Millisecond) 212 | } else { 213 | break 214 | } 215 | } 216 | return err 217 | } 218 | 219 | func checkForImportedAWSService(c *api.Client, name, namespaceID, serviceID string, repeat int) error { 220 | for i := 0; i < repeat; i++ { 221 | services, _, err := c.Catalog().Services(nil) 222 | if err == nil { 223 | if tags, ok := services[name]; ok { 224 | found := false 225 | for _, t := range tags { 226 | if t == ConsulAWSTag { 227 | found = true 228 | } 229 | } 230 | if !found { 231 | return fmt.Errorf("aws tag is missing on consul service") 232 | } 233 | cservices, _, err := c.Catalog().Service(name, ConsulAWSTag, nil) 234 | if err != nil { 235 | return err 236 | } 237 | if len(cservices) != 1 { 238 | return fmt.Errorf("not 1 services") 239 | } 240 | m := cservices[0].ServiceMeta 241 | if m["FUBAR"] != "BARFU" { 242 | return fmt.Errorf("custom meta doesn't match: %s", m["FUBAR"]) 243 | } 244 | if m[ConsulSourceKey] != ConsulAWSTag { 245 | return fmt.Errorf("%s meta doesn't match: %s", ConsulSourceKey, m[ConsulSourceKey]) 246 | } 247 | if m[ConsulAWSNS] != namespaceID { 248 | return fmt.Errorf("%s meta doesn't match: expected: %s actual: %s", ConsulAWSNS, namespaceID, m[ConsulAWSNS]) 249 | } 250 | if m[ConsulAWSID] != serviceID { 251 | return fmt.Errorf("%s meta doesn't match: expected: %s, actual: %s", ConsulAWSID, serviceID, m[ConsulAWSID]) 252 | } 253 | return nil 254 | } 255 | } 256 | time.Sleep(100 * time.Millisecond) 257 | } 258 | return fmt.Errorf("shrug") 259 | } 260 | 261 | func checkForImportedConsulService(a *awssd.Client, namespaceID, name string, repeat int) error { 262 | for i := 0; i < repeat; i++ { 263 | paginator := awssd.NewListServicesPaginator(a, &awssd.ListServicesInput{ 264 | Filters: []awssdtypes.ServiceFilter{ 265 | { 266 | Name: awssdtypes.ServiceFilterNameNamespaceId, 267 | Condition: awssdtypes.FilterConditionEq, 268 | Values: []string{namespaceID}, 269 | }, 270 | }, 271 | }) 272 | 273 | for paginator.HasMorePages() { 274 | p, err := paginator.NextPage(context.TODO()) 275 | if err != nil { 276 | return fmt.Errorf("error paging through services: %s", err) 277 | } 278 | 279 | for _, s := range p.Services { 280 | if *s.Name == name { 281 | if !(s.Description != nil || *s.Description == awsServiceDescription) { 282 | return fmt.Errorf("consul description is missing on aws service") 283 | } 284 | var instance *awssdtypes.InstanceSummary 285 | for i := 0; i < 20; i++ { 286 | out, err := a.ListInstances(context.TODO(), &awssd.ListInstancesInput{ 287 | ServiceId: s.Id, 288 | }) 289 | if err != nil { 290 | continue 291 | } 292 | if len(out.Instances) != 1 { 293 | time.Sleep(200 * time.Millisecond) 294 | continue 295 | } 296 | instance = &out.Instances[0] 297 | } 298 | if instance == nil { 299 | return fmt.Errorf("couldn't get instance") 300 | } 301 | m := instance.Attributes 302 | 303 | if m["AWS_INSTANCE_IPV4"] != "127.0.0.1" { 304 | return fmt.Errorf("AWS_INSTANCE_IPV4 not correct") 305 | } 306 | if m["AWS_INSTANCE_PORT"] != "6379" { 307 | return fmt.Errorf("AWS_INSTANCE_PORT not correct") 308 | } 309 | if m["BARFU"] != "FUBAR" { 310 | return fmt.Errorf("custom meta not correct") 311 | } 312 | return nil 313 | } 314 | } 315 | } 316 | time.Sleep(100 * time.Millisecond) 317 | } 318 | return fmt.Errorf("shrug") 319 | } 320 | -------------------------------------------------------------------------------- /commands.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "os" 8 | 9 | "github.com/mitchellh/cli" 10 | 11 | cmdSyncCatalog "github.com/hashicorp/consul-aws/subcommand/sync-catalog" 12 | cmdVersion "github.com/hashicorp/consul-aws/subcommand/version" 13 | "github.com/hashicorp/consul-aws/version" 14 | ) 15 | 16 | // Commands is the mapping of all available consul-aws commands. 17 | var Commands map[string]cli.CommandFactory 18 | 19 | func init() { 20 | ui := &cli.BasicUi{Writer: os.Stdout, ErrorWriter: os.Stderr} 21 | 22 | Commands = map[string]cli.CommandFactory{ 23 | "sync-catalog": func() (cli.Command, error) { 24 | return &cmdSyncCatalog.Command{UI: ui}, nil 25 | }, 26 | 27 | "version": func() (cli.Command, error) { 28 | return &cmdVersion.Command{UI: ui, Version: version.GetHumanVersion(), GitCommit: version.GitCommit}, nil 29 | }, 30 | } 31 | } 32 | 33 | func helpFunc() cli.HelpFunc { 34 | // This should be updated for any commands we want to hide for any reason. 35 | // Hidden commands can still be executed if you know the command, but 36 | // aren't shown in any help output. We use this for prerelease functionality 37 | // or advanced features. 38 | hidden := map[string]struct{}{ 39 | "inject-connect": {}, 40 | } 41 | 42 | var include []string 43 | for k := range Commands { 44 | if _, ok := hidden[k]; !ok { 45 | include = append(include, k) 46 | } 47 | } 48 | 49 | return cli.FilteredHelpFunc(include, cli.BasicHelpFunc("consul-aws")) 50 | } 51 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/consul-aws 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go-v2 v1.26.1 7 | github.com/aws/aws-sdk-go-v2/config v1.27.10 8 | github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.29.4 9 | github.com/hashicorp/consul/api v1.28.2 10 | github.com/hashicorp/go-hclog v1.6.3 11 | github.com/kr/text v0.2.0 12 | github.com/mitchellh/cli v1.1.5 13 | github.com/mitchellh/mapstructure v1.5.0 14 | github.com/stretchr/testify v1.9.0 15 | ) 16 | 17 | require ( 18 | github.com/Masterminds/goutils v1.1.1 // indirect 19 | github.com/Masterminds/semver/v3 v3.2.1 // indirect 20 | github.com/Masterminds/sprig/v3 v3.2.3 // indirect 21 | github.com/armon/go-metrics v0.4.1 // indirect 22 | github.com/armon/go-radix v1.0.0 // indirect 23 | github.com/aws/aws-sdk-go-v2/credentials v1.17.10 // indirect 24 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 // indirect 25 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect 26 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect 27 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect 28 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect 29 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect 30 | github.com/aws/aws-sdk-go-v2/service/sso v1.20.4 // indirect 31 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 // indirect 32 | github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 // indirect 33 | github.com/aws/smithy-go v1.20.2 // indirect 34 | github.com/bgentry/speakeasy v0.1.0 // indirect 35 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 36 | github.com/fatih/color v1.16.0 // indirect 37 | github.com/google/uuid v1.6.0 // indirect 38 | github.com/hashicorp/errwrap v1.1.0 // indirect 39 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 40 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 41 | github.com/hashicorp/go-multierror v1.1.1 // indirect 42 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 43 | github.com/hashicorp/golang-lru v1.0.2 // indirect 44 | github.com/hashicorp/serf v0.10.1 // indirect 45 | github.com/huandu/xstrings v1.4.0 // indirect 46 | github.com/imdario/mergo v0.3.16 // indirect 47 | github.com/mattn/go-colorable v0.1.13 // indirect 48 | github.com/mattn/go-isatty v0.0.20 // indirect 49 | github.com/mitchellh/copystructure v1.2.0 // indirect 50 | github.com/mitchellh/go-homedir v1.1.0 // indirect 51 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 52 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 53 | github.com/posener/complete v1.2.3 // indirect 54 | github.com/shopspring/decimal v1.3.1 // indirect 55 | github.com/spf13/cast v1.6.0 // indirect 56 | golang.org/x/crypto v0.37.0 // indirect 57 | golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect 58 | golang.org/x/sys v0.32.0 // indirect 59 | gopkg.in/yaml.v3 v3.0.1 // indirect 60 | ) 61 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= 2 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 3 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 4 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 5 | github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= 6 | github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= 7 | github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= 8 | github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= 9 | github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= 10 | github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= 11 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 12 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 13 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 14 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 15 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 16 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 17 | github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= 18 | github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= 19 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 20 | github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= 21 | github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 22 | github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= 23 | github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= 24 | github.com/aws/aws-sdk-go-v2/config v1.27.10 h1:PS+65jThT0T/snC5WjyfHHyUgG+eBoupSDV+f838cro= 25 | github.com/aws/aws-sdk-go-v2/config v1.27.10/go.mod h1:BePM7Vo4OBpHreKRUMuDXX+/+JWP38FLkzl5m27/Jjs= 26 | github.com/aws/aws-sdk-go-v2/credentials v1.17.10 h1:qDZ3EA2lv1KangvQB6y258OssCHD0xvaGiEDkG4X/10= 27 | github.com/aws/aws-sdk-go-v2/credentials v1.17.10/go.mod h1:6t3sucOaYDwDssHQa0ojH1RpmVmF5/jArkye1b2FKMI= 28 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4= 29 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg= 30 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg= 31 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I= 32 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0= 33 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc= 34 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= 35 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= 36 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= 37 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= 38 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo= 39 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk= 40 | github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.29.4 h1:NkeK09CJZcPwQZicMNObg+/DgZW9h/ib6I9VDETfSiQ= 41 | github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.29.4/go.mod h1:3pzLFJnbjkymz6RdZ963DuvMR9rzrKMXrlbteSk4Sxc= 42 | github.com/aws/aws-sdk-go-v2/service/sso v1.20.4 h1:WzFol5Cd+yDxPAdnzTA5LmpHYSWinhmSj4rQChV0ee8= 43 | github.com/aws/aws-sdk-go-v2/service/sso v1.20.4/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM= 44 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 h1:Jux+gDDyi1Lruk+KHF91tK2KCuY61kzoCpvtvJJBtOE= 45 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak= 46 | github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 h1:cwIxeBttqPN3qkaAjcEcsh8NYr8n2HZPkcKgPAi1phU= 47 | github.com/aws/aws-sdk-go-v2/service/sts v1.28.6/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw= 48 | github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= 49 | github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= 50 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 51 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 52 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 53 | github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= 54 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 55 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 56 | github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= 57 | github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= 58 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 59 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 60 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 61 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 62 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 63 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 64 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 65 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 66 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 67 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 68 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 69 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 70 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 71 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 72 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 73 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 74 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 75 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 76 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 77 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 78 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 79 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 80 | github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= 81 | github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= 82 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 83 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 84 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 85 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 86 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 87 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 88 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 89 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 90 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 91 | github.com/hashicorp/consul/api v1.28.2 h1:mXfkRHrpHN4YY3RqL09nXU1eHKLNiuAN4kHvDQ16k/8= 92 | github.com/hashicorp/consul/api v1.28.2/go.mod h1:KyzqzgMEya+IZPcD65YFoOVAgPpbfERu4I/tzG6/ueE= 93 | github.com/hashicorp/consul/sdk v0.16.0 h1:SE9m0W6DEfgIVCJX7xU+iv/hUl4m/nxqMTnCdMxDpJ8= 94 | github.com/hashicorp/consul/sdk v0.16.0/go.mod h1:7pxqqhqoaPqnBnzXD1StKed62LqJeClzVsUEy85Zr0A= 95 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 96 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 97 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 98 | github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 99 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 100 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 101 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 102 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 103 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 104 | github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= 105 | github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 106 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 107 | github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= 108 | github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 109 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 110 | github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= 111 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 112 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 113 | github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= 114 | github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= 115 | github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= 116 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 117 | github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= 118 | github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= 119 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 120 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 121 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 122 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 123 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 124 | github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= 125 | github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 126 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 127 | github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 128 | github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 129 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 130 | github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= 131 | github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM= 132 | github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= 133 | github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= 134 | github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= 135 | github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 136 | github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 137 | github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 138 | github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= 139 | github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 140 | github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 141 | github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= 142 | github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= 143 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 144 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 145 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 146 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 147 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 148 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 149 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 150 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 151 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 152 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 153 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 154 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 155 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 156 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 157 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 158 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 159 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 160 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 161 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 162 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 163 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 164 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 165 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 166 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 167 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 168 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 169 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 170 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 171 | github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= 172 | github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= 173 | github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= 174 | github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= 175 | github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= 176 | github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4= 177 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 178 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 179 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 180 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 181 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 182 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 183 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 184 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 185 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 186 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 187 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 188 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 189 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 190 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 191 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 192 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 193 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 194 | github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= 195 | github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 196 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 197 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 198 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 199 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 200 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 201 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 202 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 203 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 204 | github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= 205 | github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= 206 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 207 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 208 | github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= 209 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 210 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 211 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 212 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 213 | github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= 214 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 215 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 216 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 217 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 218 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 219 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 220 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= 221 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 222 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 223 | github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= 224 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 225 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 226 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 227 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 228 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 229 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 230 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 231 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 232 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 233 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 234 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 235 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 236 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 237 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 238 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 239 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 240 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 241 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 242 | github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= 243 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 244 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 245 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 246 | golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= 247 | golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 248 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 249 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 250 | golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 251 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 252 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 253 | golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= 254 | golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= 255 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 256 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 257 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 258 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 259 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 260 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 261 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 262 | golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= 263 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 264 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 265 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= 266 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 267 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 268 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 269 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 270 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 271 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 272 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 273 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 274 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 275 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 276 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 277 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 278 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 279 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 280 | golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 281 | golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 282 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 283 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 284 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 285 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 286 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 287 | golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 288 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 289 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 290 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 291 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 292 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 293 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 294 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 295 | golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 296 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 297 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 298 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 299 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 300 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 301 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 302 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 303 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 304 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 305 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 306 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 307 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 308 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 309 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 310 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 311 | golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 312 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 313 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 314 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 315 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 316 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 317 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 318 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 319 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 320 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 321 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 322 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 323 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 324 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 325 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 326 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 327 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 328 | -------------------------------------------------------------------------------- /internal/flags/config.go: -------------------------------------------------------------------------------- 1 | // Code generated by make copy-http-flags. DO NOT EDIT. 2 | // Copyright (c) HashiCorp, Inc. 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | package flags 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | "path/filepath" 11 | "reflect" 12 | "sort" 13 | "strconv" 14 | "time" 15 | 16 | "github.com/mitchellh/mapstructure" 17 | ) 18 | 19 | // TODO (slackpad) - Trying out a different pattern here for config handling. 20 | // These classes support the flag.Value interface but work in a manner where 21 | // we can tell if they have been set. This lets us work with an all-pointer 22 | // config structure and merge it in a clean-ish way. If this ends up being a 23 | // good pattern we should pull this out into a reusable library. 24 | 25 | // ConfigDecodeHook should be passed to mapstructure in order to decode into 26 | // the *Value objects here. 27 | var ConfigDecodeHook = mapstructure.ComposeDecodeHookFunc( 28 | BoolToBoolValueFunc(), 29 | StringToDurationValueFunc(), 30 | StringToStringValueFunc(), 31 | Float64ToUintValueFunc(), 32 | ) 33 | 34 | // BoolValue provides a flag value that's aware if it has been set. 35 | type BoolValue struct { 36 | v *bool 37 | } 38 | 39 | // IsBoolFlag is an optional method of the flag.Value 40 | // interface which marks this value as boolean when 41 | // the return value is true. See flag.Value for details. 42 | func (b *BoolValue) IsBoolFlag() bool { 43 | return true 44 | } 45 | 46 | // Merge will overlay this value if it has been set. 47 | func (b *BoolValue) Merge(onto *bool) { 48 | if b.v != nil { 49 | *onto = *(b.v) 50 | } 51 | } 52 | 53 | // Set implements the flag.Value interface. 54 | func (b *BoolValue) Set(v string) error { 55 | if b.v == nil { 56 | b.v = new(bool) 57 | } 58 | var err error 59 | *(b.v), err = strconv.ParseBool(v) 60 | return err 61 | } 62 | 63 | // String implements the flag.Value interface. 64 | func (b *BoolValue) String() string { 65 | var current bool 66 | if b.v != nil { 67 | current = *(b.v) 68 | } 69 | return fmt.Sprintf("%v", current) 70 | } 71 | 72 | // BoolToBoolValueFunc is a mapstructure hook that looks for an incoming bool 73 | // mapped to a BoolValue and does the translation. 74 | func BoolToBoolValueFunc() mapstructure.DecodeHookFunc { 75 | return func( 76 | f reflect.Type, 77 | t reflect.Type, 78 | data interface{}) (interface{}, error) { 79 | if f.Kind() != reflect.Bool { 80 | return data, nil 81 | } 82 | 83 | val := BoolValue{} 84 | if t != reflect.TypeOf(val) { 85 | return data, nil 86 | } 87 | 88 | val.v = new(bool) 89 | *(val.v) = data.(bool) 90 | return val, nil 91 | } 92 | } 93 | 94 | // DurationValue provides a flag value that's aware if it has been set. 95 | type DurationValue struct { 96 | v *time.Duration 97 | } 98 | 99 | // Merge will overlay this value if it has been set. 100 | func (d *DurationValue) Merge(onto *time.Duration) { 101 | if d.v != nil { 102 | *onto = *(d.v) 103 | } 104 | } 105 | 106 | // Set implements the flag.Value interface. 107 | func (d *DurationValue) Set(v string) error { 108 | if d.v == nil { 109 | d.v = new(time.Duration) 110 | } 111 | var err error 112 | *(d.v), err = time.ParseDuration(v) 113 | return err 114 | } 115 | 116 | // String implements the flag.Value interface. 117 | func (d *DurationValue) String() string { 118 | var current time.Duration 119 | if d.v != nil { 120 | current = *(d.v) 121 | } 122 | return current.String() 123 | } 124 | 125 | // StringToDurationValueFunc is a mapstructure hook that looks for an incoming 126 | // string mapped to a DurationValue and does the translation. 127 | func StringToDurationValueFunc() mapstructure.DecodeHookFunc { 128 | return func( 129 | f reflect.Type, 130 | t reflect.Type, 131 | data interface{}) (interface{}, error) { 132 | if f.Kind() != reflect.String { 133 | return data, nil 134 | } 135 | 136 | val := DurationValue{} 137 | if t != reflect.TypeOf(val) { 138 | return data, nil 139 | } 140 | if err := val.Set(data.(string)); err != nil { 141 | return nil, err 142 | } 143 | return val, nil 144 | } 145 | } 146 | 147 | // StringValue provides a flag value that's aware if it has been set. 148 | type StringValue struct { 149 | v *string 150 | } 151 | 152 | // Merge will overlay this value if it has been set. 153 | func (s *StringValue) Merge(onto *string) { 154 | if s.v != nil { 155 | *onto = *(s.v) 156 | } 157 | } 158 | 159 | // Set implements the flag.Value interface. 160 | func (s *StringValue) Set(v string) error { 161 | if s.v == nil { 162 | s.v = new(string) 163 | } 164 | *(s.v) = v 165 | return nil 166 | } 167 | 168 | // String implements the flag.Value interface. 169 | func (s *StringValue) String() string { 170 | var current string 171 | if s.v != nil { 172 | current = *(s.v) 173 | } 174 | return current 175 | } 176 | 177 | // StringToStringValueFunc is a mapstructure hook that looks for an incoming 178 | // string mapped to a StringValue and does the translation. 179 | func StringToStringValueFunc() mapstructure.DecodeHookFunc { 180 | return func( 181 | f reflect.Type, 182 | t reflect.Type, 183 | data interface{}) (interface{}, error) { 184 | if f.Kind() != reflect.String { 185 | return data, nil 186 | } 187 | 188 | val := StringValue{} 189 | if t != reflect.TypeOf(val) { 190 | return data, nil 191 | } 192 | val.v = new(string) 193 | *(val.v) = data.(string) 194 | return val, nil 195 | } 196 | } 197 | 198 | // UintValue provides a flag value that's aware if it has been set. 199 | type UintValue struct { 200 | v *uint 201 | } 202 | 203 | // Merge will overlay this value if it has been set. 204 | func (u *UintValue) Merge(onto *uint) { 205 | if u.v != nil { 206 | *onto = *(u.v) 207 | } 208 | } 209 | 210 | // Set implements the flag.Value interface. 211 | func (u *UintValue) Set(v string) error { 212 | if u.v == nil { 213 | u.v = new(uint) 214 | } 215 | parsed, err := strconv.ParseUint(v, 0, 64) 216 | *(u.v) = (uint)(parsed) 217 | return err 218 | } 219 | 220 | // String implements the flag.Value interface. 221 | func (u *UintValue) String() string { 222 | var current uint 223 | if u.v != nil { 224 | current = *(u.v) 225 | } 226 | return fmt.Sprintf("%v", current) 227 | } 228 | 229 | // Float64ToUintValueFunc is a mapstructure hook that looks for an incoming 230 | // float64 mapped to a UintValue and does the translation. 231 | func Float64ToUintValueFunc() mapstructure.DecodeHookFunc { 232 | return func( 233 | f reflect.Type, 234 | t reflect.Type, 235 | data interface{}) (interface{}, error) { 236 | if f.Kind() != reflect.Float64 { 237 | return data, nil 238 | } 239 | 240 | val := UintValue{} 241 | if t != reflect.TypeOf(val) { 242 | return data, nil 243 | } 244 | 245 | fv := data.(float64) 246 | if fv < 0 { 247 | return nil, fmt.Errorf("value cannot be negative") 248 | } 249 | 250 | // The standard guarantees at least this, and this is fine for 251 | // values we expect to use in configs vs. being fancy with the 252 | // machine's size for uint. 253 | if fv > (1<<32 - 1) { 254 | return nil, fmt.Errorf("value is too large") 255 | } 256 | 257 | val.v = new(uint) 258 | *(val.v) = (uint)(fv) 259 | return val, nil 260 | } 261 | } 262 | 263 | // VisitFn is a callback that gets a chance to visit each file found during a 264 | // traversal with visit(). 265 | type VisitFn func(path string) error 266 | 267 | // Visit will call the visitor function on the path if it's a file, or for each 268 | // file in the path if it's a directory. Directories will not be recursed into, 269 | // and files in the directory will be visited in alphabetical order. 270 | func Visit(path string, visitor VisitFn) error { 271 | f, err := os.Open(path) 272 | if err != nil { 273 | return fmt.Errorf("error reading %q: %v", path, err) 274 | } 275 | defer f.Close() 276 | 277 | fi, err := f.Stat() 278 | if err != nil { 279 | return fmt.Errorf("error checking %q: %v", path, err) 280 | } 281 | 282 | if !fi.IsDir() { 283 | if err := visitor(path); err != nil { 284 | return fmt.Errorf("error in %q: %v", path, err) 285 | } 286 | return nil 287 | } 288 | 289 | contents, err := f.Readdir(-1) 290 | if err != nil { 291 | return fmt.Errorf("error listing %q: %v", path, err) 292 | } 293 | 294 | sort.Sort(dirEnts(contents)) 295 | for _, fi := range contents { 296 | if fi.IsDir() { 297 | continue 298 | } 299 | 300 | fullPath := filepath.Join(path, fi.Name()) 301 | if err := visitor(fullPath); err != nil { 302 | return fmt.Errorf("error in %q: %v", fullPath, err) 303 | } 304 | } 305 | 306 | return nil 307 | } 308 | 309 | // dirEnts applies sort.Interface to directory entries for sorting by name. 310 | type dirEnts []os.FileInfo 311 | 312 | func (d dirEnts) Len() int { return len(d) } 313 | func (d dirEnts) Less(i, j int) bool { return d[i].Name() < d[j].Name() } 314 | func (d dirEnts) Swap(i, j int) { d[i], d[j] = d[j], d[i] } 315 | -------------------------------------------------------------------------------- /internal/flags/http.go: -------------------------------------------------------------------------------- 1 | // Code generated by make copy-http-flags. DO NOT EDIT. 2 | // Copyright (c) HashiCorp, Inc. 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | package flags 6 | 7 | import ( 8 | "flag" 9 | "os" 10 | "strings" 11 | 12 | "github.com/hashicorp/consul/api" 13 | ) 14 | 15 | type HTTPFlags struct { 16 | // client api flags 17 | address StringValue 18 | token StringValue 19 | tokenFile StringValue 20 | caFile StringValue 21 | caPath StringValue 22 | certFile StringValue 23 | keyFile StringValue 24 | tlsServerName StringValue 25 | 26 | // server flags 27 | datacenter StringValue 28 | stale BoolValue 29 | 30 | // multi-tenancy flags 31 | namespace StringValue 32 | partition StringValue 33 | peer StringValue 34 | } 35 | 36 | func (f *HTTPFlags) ClientFlags() *flag.FlagSet { 37 | fs := flag.NewFlagSet("", flag.ContinueOnError) 38 | fs.Var(&f.address, "http-addr", 39 | "The `address` and port of the Consul HTTP agent. The value can be an IP "+ 40 | "address or DNS address, but it must also include the port. This can "+ 41 | "also be specified via the CONSUL_HTTP_ADDR environment variable. The "+ 42 | "default value is http://127.0.0.1:8500. The scheme can also be set to "+ 43 | "HTTPS by setting the environment variable CONSUL_HTTP_SSL=true.") 44 | fs.Var(&f.token, "token", 45 | "ACL token to use in the request. This can also be specified via the "+ 46 | "CONSUL_HTTP_TOKEN environment variable. If unspecified, the query will "+ 47 | "default to the token of the Consul agent at the HTTP address.") 48 | fs.Var(&f.tokenFile, "token-file", 49 | "File containing the ACL token to use in the request instead of one specified "+ 50 | "via the -token argument or CONSUL_HTTP_TOKEN environment variable. "+ 51 | "This can also be specified via the CONSUL_HTTP_TOKEN_FILE environment variable.") 52 | fs.Var(&f.caFile, "ca-file", 53 | "Path to a CA file to use for TLS when communicating with Consul. This "+ 54 | "can also be specified via the CONSUL_CACERT environment variable.") 55 | fs.Var(&f.caPath, "ca-path", 56 | "Path to a directory of CA certificates to use for TLS when communicating "+ 57 | "with Consul. This can also be specified via the CONSUL_CAPATH environment variable.") 58 | fs.Var(&f.certFile, "client-cert", 59 | "Path to a client cert file to use for TLS when 'verify_incoming' is enabled. This "+ 60 | "can also be specified via the CONSUL_CLIENT_CERT environment variable.") 61 | fs.Var(&f.keyFile, "client-key", 62 | "Path to a client key file to use for TLS when 'verify_incoming' is enabled. This "+ 63 | "can also be specified via the CONSUL_CLIENT_KEY environment variable.") 64 | fs.Var(&f.tlsServerName, "tls-server-name", 65 | "The server name to use as the SNI host when connecting via TLS. This "+ 66 | "can also be specified via the CONSUL_TLS_SERVER_NAME environment variable.") 67 | return fs 68 | } 69 | 70 | func (f *HTTPFlags) ServerFlags() *flag.FlagSet { 71 | fs := flag.NewFlagSet("", flag.ContinueOnError) 72 | fs.Var(&f.datacenter, "datacenter", 73 | "Name of the datacenter to query. If unspecified, this will default to "+ 74 | "the datacenter of the queried agent.") 75 | fs.Var(&f.stale, "stale", 76 | "Permit any Consul server (non-leader) to respond to this request. This "+ 77 | "allows for lower latency and higher throughput, but can result in "+ 78 | "stale data. This option has no effect on non-read operations. The "+ 79 | "default value is false.") 80 | return fs 81 | } 82 | 83 | func (f *HTTPFlags) MultiTenancyFlags() *flag.FlagSet { 84 | fs := flag.NewFlagSet("", flag.ContinueOnError) 85 | fs.Var(&f.namespace, "namespace", 86 | "Specifies the namespace to query. If not provided, the namespace will be inferred "+ 87 | "from the request's ACL token, or will default to the `default` namespace. "+ 88 | "Namespaces are a Consul Enterprise feature.") 89 | f.AddPartitionFlag(fs) 90 | return fs 91 | } 92 | 93 | func (f *HTTPFlags) PartitionFlag() *flag.FlagSet { 94 | fs := flag.NewFlagSet("", flag.ContinueOnError) 95 | f.AddPartitionFlag(fs) 96 | return fs 97 | } 98 | func (f *HTTPFlags) Addr() string { 99 | return f.address.String() 100 | } 101 | 102 | func (f *HTTPFlags) Datacenter() string { 103 | return f.datacenter.String() 104 | } 105 | 106 | func (f *HTTPFlags) Namespace() string { 107 | return f.namespace.String() 108 | } 109 | 110 | func (f *HTTPFlags) Partition() string { 111 | return f.partition.String() 112 | } 113 | 114 | func (f *HTTPFlags) PeerName() string { 115 | return f.peer.String() 116 | } 117 | 118 | func (f *HTTPFlags) Stale() bool { 119 | if f.stale.v == nil { 120 | return false 121 | } 122 | return *f.stale.v 123 | } 124 | 125 | func (f *HTTPFlags) Token() string { 126 | return f.token.String() 127 | } 128 | 129 | func (f *HTTPFlags) SetToken(v string) error { 130 | return f.token.Set(v) 131 | } 132 | 133 | func (f *HTTPFlags) TokenFile() string { 134 | return f.tokenFile.String() 135 | } 136 | 137 | func (f *HTTPFlags) SetTokenFile(v string) error { 138 | return f.tokenFile.Set(v) 139 | } 140 | 141 | func (f *HTTPFlags) ReadTokenFile() (string, error) { 142 | tokenFile := f.tokenFile.String() 143 | if tokenFile == "" { 144 | return "", nil 145 | } 146 | 147 | data, err := os.ReadFile(tokenFile) 148 | if err != nil { 149 | return "", err 150 | } 151 | 152 | return strings.TrimSpace(string(data)), nil 153 | } 154 | 155 | func (f *HTTPFlags) APIClient() (*api.Client, error) { 156 | c := api.DefaultConfig() 157 | 158 | f.MergeOntoConfig(c) 159 | 160 | return api.NewClient(c) 161 | } 162 | 163 | func (f *HTTPFlags) MergeOntoConfig(c *api.Config) { 164 | f.address.Merge(&c.Address) 165 | f.token.Merge(&c.Token) 166 | f.tokenFile.Merge(&c.TokenFile) 167 | f.caFile.Merge(&c.TLSConfig.CAFile) 168 | f.caPath.Merge(&c.TLSConfig.CAPath) 169 | f.certFile.Merge(&c.TLSConfig.CertFile) 170 | f.keyFile.Merge(&c.TLSConfig.KeyFile) 171 | f.tlsServerName.Merge(&c.TLSConfig.Address) 172 | f.datacenter.Merge(&c.Datacenter) 173 | f.namespace.Merge(&c.Namespace) 174 | f.partition.Merge(&c.Partition) 175 | } 176 | 177 | func (f *HTTPFlags) AddPartitionFlag(fs *flag.FlagSet) { 178 | fs.Var(&f.partition, "partition", 179 | "Specifies the admin partition to query. If not provided, the admin partition will be inferred "+ 180 | "from the request's ACL token, or will default to the `default` admin partition. "+ 181 | "Admin Partitions are a Consul Enterprise feature.") 182 | } 183 | 184 | func (f *HTTPFlags) AddPeerName() *flag.FlagSet { 185 | fs := flag.NewFlagSet("", flag.ContinueOnError) 186 | fs.Var(&f.peer, "peer", "Specifies the name of peer to query. By default, it is `local`.") 187 | return fs 188 | } 189 | -------------------------------------------------------------------------------- /internal/flags/http_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by make copy-http-flags. DO NOT EDIT. 2 | // Copyright (c) HashiCorp, Inc. 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | package flags 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestHTTPFlagsSetToken(t *testing.T) { 14 | var f HTTPFlags 15 | require.Empty(t, f.Token()) 16 | require.NoError(t, f.SetToken("foo")) 17 | require.Equal(t, "foo", f.Token()) 18 | } 19 | -------------------------------------------------------------------------------- /internal/flags/merge.go: -------------------------------------------------------------------------------- 1 | // Code generated by make copy-http-flags. DO NOT EDIT. 2 | // Copyright (c) HashiCorp, Inc. 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | package flags 6 | 7 | import "flag" 8 | 9 | func Merge(dst, src *flag.FlagSet) { 10 | if dst == nil { 11 | panic("dst cannot be nil") 12 | } 13 | if src == nil { 14 | return 15 | } 16 | src.VisitAll(func(f *flag.Flag) { 17 | dst.Var(f.Value, f.Name, f.Usage) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /internal/flags/usage.go: -------------------------------------------------------------------------------- 1 | // Code generated by make copy-http-flags. DO NOT EDIT. 2 | // Copyright (c) HashiCorp, Inc. 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | package flags 6 | 7 | import ( 8 | "bytes" 9 | "flag" 10 | "fmt" 11 | "io" 12 | "strings" 13 | 14 | text "github.com/kr/text" 15 | ) 16 | 17 | func Usage(txt string, flags *flag.FlagSet) string { 18 | u := &Usager{ 19 | Usage: txt, 20 | Flags: flags, 21 | } 22 | return u.String() 23 | } 24 | 25 | type Usager struct { 26 | Usage string 27 | Flags *flag.FlagSet 28 | } 29 | 30 | func (u *Usager) String() string { 31 | out := new(bytes.Buffer) 32 | out.WriteString(strings.TrimSpace(u.Usage)) 33 | out.WriteString("\n") 34 | out.WriteString("\n") 35 | 36 | if u.Flags != nil { 37 | f := &HTTPFlags{} 38 | clientFlags := f.ClientFlags() 39 | serverFlags := f.ServerFlags() 40 | 41 | var httpFlags, cmdFlags *flag.FlagSet 42 | u.Flags.VisitAll(func(f *flag.Flag) { 43 | if contains(clientFlags, f) || contains(serverFlags, f) { 44 | if httpFlags == nil { 45 | httpFlags = flag.NewFlagSet("", flag.ContinueOnError) 46 | } 47 | httpFlags.Var(f.Value, f.Name, f.Usage) 48 | } else { 49 | if cmdFlags == nil { 50 | cmdFlags = flag.NewFlagSet("", flag.ContinueOnError) 51 | } 52 | cmdFlags.Var(f.Value, f.Name, f.Usage) 53 | } 54 | }) 55 | 56 | if httpFlags != nil { 57 | printTitle(out, "HTTP API Options") 58 | httpFlags.VisitAll(func(f *flag.Flag) { 59 | printFlag(out, f) 60 | }) 61 | } 62 | 63 | if cmdFlags != nil { 64 | printTitle(out, "Command Options") 65 | cmdFlags.VisitAll(func(f *flag.Flag) { 66 | printFlag(out, f) 67 | }) 68 | } 69 | } 70 | 71 | return strings.TrimRight(out.String(), "\n") 72 | } 73 | 74 | // printTitle prints a consistently-formatted title to the given writer. 75 | func printTitle(w io.Writer, s string) { 76 | fmt.Fprintf(w, "%s\n\n", s) 77 | } 78 | 79 | // printFlag prints a single flag to the given writer. 80 | func printFlag(w io.Writer, f *flag.Flag) { 81 | example, _ := flag.UnquoteUsage(f) 82 | if example != "" { 83 | fmt.Fprintf(w, " -%s=<%s>\n", f.Name, example) 84 | } else { 85 | fmt.Fprintf(w, " -%s\n", f.Name) 86 | } 87 | 88 | indented := wrapAtLength(f.Usage, 5) 89 | fmt.Fprintf(w, "%s\n\n", indented) 90 | } 91 | 92 | // contains returns true if the given flag is contained in the given flag 93 | // set or false otherwise. 94 | func contains(fs *flag.FlagSet, f *flag.Flag) bool { 95 | if fs == nil { 96 | return false 97 | } 98 | 99 | var in bool 100 | fs.VisitAll(func(hf *flag.Flag) { 101 | in = in || f.Name == hf.Name 102 | }) 103 | return in 104 | } 105 | 106 | // maxLineLength is the maximum width of any line. 107 | const maxLineLength int = 72 108 | 109 | // wrapAtLength wraps the given text at the maxLineLength, taking into account 110 | // any provided left padding. 111 | func wrapAtLength(s string, pad int) string { 112 | wrapped := text.Wrap(s, maxLineLength-pad) 113 | lines := strings.Split(wrapped, "\n") 114 | for i, line := range lines { 115 | lines[i] = strings.Repeat(" ", pad) + line 116 | } 117 | return strings.Join(lines, "\n") 118 | } 119 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "log" 8 | "os" 9 | 10 | "github.com/mitchellh/cli" 11 | 12 | "github.com/hashicorp/consul-aws/version" 13 | ) 14 | 15 | func main() { 16 | c := cli.NewCLI("consul-aws", version.GetHumanVersion()) 17 | c.Args = os.Args[1:] 18 | c.Commands = Commands 19 | c.HelpFunc = helpFunc() 20 | 21 | exitStatus, err := c.Run() 22 | if err != nil { 23 | log.Println(err) 24 | } 25 | os.Exit(exitStatus) 26 | } 27 | -------------------------------------------------------------------------------- /scan.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # Configuration for security scanner. 5 | # Run on PRs and pushes to `main` and `release/**` branches. 6 | # See .github/workflows/security-scan.yml for CI config. 7 | 8 | # To run manually, install scanner and then run `scan repository .` 9 | 10 | # Scan results are triaged via the GitHub Security tab for this repo. 11 | # See `security-scanner` docs for more information on how to add `triage` config 12 | # for specific results or to exclude paths. 13 | 14 | # .release/security-scan.hcl controls scanner config for release artifacts, which 15 | # unlike the scans configured here, will block releases in CRT. 16 | 17 | repository { 18 | go_modules = true 19 | osv = true 20 | 21 | secrets { 22 | all = true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /subcommand/auth.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package subcommand 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/aws/aws-sdk-go-v2/aws" 10 | awsconfig "github.com/aws/aws-sdk-go-v2/config" 11 | ) 12 | 13 | func AWSConfig() (aws.Config, error) { 14 | return awsconfig.LoadDefaultConfig(context.TODO()) 15 | } 16 | -------------------------------------------------------------------------------- /subcommand/sync-catalog/command.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package synccatalog 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "os" 10 | "os/signal" 11 | "sync" 12 | 13 | sd "github.com/aws/aws-sdk-go-v2/service/servicediscovery" 14 | "github.com/mitchellh/cli" 15 | 16 | "github.com/hashicorp/consul-aws/internal/flags" 17 | 18 | "github.com/hashicorp/consul-aws/catalog" 19 | "github.com/hashicorp/consul-aws/subcommand" 20 | ) 21 | 22 | const DefaultPollInterval = "30s" 23 | 24 | // Command is the command for syncing the A 25 | type Command struct { 26 | UI cli.Ui 27 | 28 | flags *flag.FlagSet 29 | http *flags.HTTPFlags 30 | flagToConsul bool 31 | flagToAWS bool 32 | flagAWSNamespaceID string 33 | flagAWSServicePrefix string 34 | flagAWSDeprecatedPullInterval string 35 | flagAWSPollInterval string 36 | flagAWSDNSTTL int64 37 | flagConsulServicePrefix string 38 | flagConsulDomain string 39 | 40 | once sync.Once 41 | help string 42 | } 43 | 44 | func (c *Command) init() { 45 | c.flags = flag.NewFlagSet("", flag.ContinueOnError) 46 | c.flags.BoolVar(&c.flagToConsul, "to-consul", false, 47 | "If true, AWS services will be synced to Consul. (Defaults to false)") 48 | c.flags.BoolVar(&c.flagToAWS, "to-aws", false, 49 | "If true, Consul services will be synced to AWS. (Defaults to false)") 50 | c.flags.StringVar(&c.flagAWSNamespaceID, "aws-namespace-id", 51 | "", "The AWS namespace to sync with Consul services.") 52 | c.flags.StringVar(&c.flagAWSServicePrefix, "aws-service-prefix", 53 | "", "A prefix to prepend to all services written to AWS from Consul. "+ 54 | "If this is not set then services will have no prefix.") 55 | c.flags.StringVar(&c.flagConsulServicePrefix, "consul-service-prefix", 56 | "", "A prefix to prepend to all services written to Consul from AWS. "+ 57 | "If this is not set then services will have no prefix.") 58 | c.flags.StringVar(&c.flagAWSDeprecatedPullInterval, "aws-pull-interval", 59 | DefaultPollInterval, "[DEPRECATED] The interval between fetching from AWS CloudMap. "+ 60 | "Accepts a sequence of decimal numbers, each with optional "+ 61 | "fraction and a unit suffix, such as \"300ms\", \"10s\", \"1.5m\". "+ 62 | "Defaults to 30s)") 63 | c.flags.StringVar(&c.flagAWSPollInterval, "aws-poll-interval", 64 | DefaultPollInterval, "The interval between fetching from AWS CloudMap. "+ 65 | "Accepts a sequence of decimal numbers, each with optional "+ 66 | "fraction and a unit suffix, such as \"300ms\", \"10s\", \"1.5m\". "+ 67 | "Defaults to 30s)") 68 | c.flags.Int64Var(&c.flagAWSDNSTTL, "aws-dns-ttl", 69 | 60, "DNS TTL for services created in AWS CloudMap in seconds. (Defaults to 60)") 70 | 71 | c.http = &flags.HTTPFlags{} 72 | flags.Merge(c.flags, c.http.ClientFlags()) 73 | flags.Merge(c.flags, c.http.ServerFlags()) 74 | c.help = flags.Usage(help, c.flags) 75 | } 76 | 77 | func (c *Command) Run(args []string) int { 78 | c.once.Do(c.init) 79 | if err := c.flags.Parse(args); err != nil { 80 | return 1 81 | } 82 | if len(c.flags.Args()) > 0 { 83 | c.UI.Error("Should have no non-flag arguments.") 84 | return 1 85 | } 86 | if len(c.flagAWSNamespaceID) == 0 { 87 | c.UI.Error("Please provide -aws-namespace-id.") 88 | return 1 89 | } 90 | config, err := subcommand.AWSConfig() 91 | if err != nil { 92 | c.UI.Error(fmt.Sprintf("Error retrieving AWS session: %s", err)) 93 | return 1 94 | } 95 | awsClient := sd.NewFromConfig(config) 96 | 97 | consulClient, err := c.http.APIClient() 98 | if err != nil { 99 | c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) 100 | return 1 101 | } 102 | 103 | pollInterval := c.flagAWSPollInterval 104 | if pollInterval == DefaultPollInterval && c.flagAWSDeprecatedPullInterval != DefaultPollInterval { 105 | c.UI.Info("Please use -aws-poll-interval instead of the deprecated -aws-pull-interval") 106 | pollInterval = c.flagAWSDeprecatedPullInterval 107 | } 108 | 109 | stop := make(chan struct{}) 110 | stopped := make(chan struct{}) 111 | go catalog.Sync( 112 | c.flagToAWS, c.flagToConsul, c.flagAWSNamespaceID, 113 | c.flagConsulServicePrefix, c.flagAWSServicePrefix, 114 | pollInterval, c.flagAWSDNSTTL, c.getStaleWithDefaultTrue(), 115 | awsClient, consulClient, 116 | stop, stopped, 117 | ) 118 | 119 | sigCh := make(chan os.Signal, 1) 120 | signal.Notify(sigCh, os.Interrupt) 121 | select { 122 | // Unexpected failure 123 | case <-stopped: 124 | return 1 125 | case <-sigCh: 126 | c.UI.Info("shutting down...") 127 | close(stop) 128 | <-stopped 129 | } 130 | return 0 131 | } 132 | 133 | func (c *Command) getStaleWithDefaultTrue() bool { 134 | stale := true 135 | c.flags.Visit(func(f *flag.Flag) { 136 | if f.Name == "stale" { 137 | stale = c.http.Stale() 138 | return 139 | } 140 | }) 141 | return stale 142 | } 143 | 144 | func (c *Command) Synopsis() string { return synopsis } 145 | func (c *Command) Help() string { 146 | c.once.Do(c.init) 147 | return c.help 148 | } 149 | 150 | const synopsis = "Sync AWS services and Consul services." 151 | const help = ` 152 | Usage: consul-aws sync-catalog [options] 153 | 154 | Sync AWS services, and more with the Consul service catalog. 155 | This enables AWS services to discover and communicate with external 156 | services, and allows external services to discover and communicate with 157 | AWS services. 158 | 159 | ` 160 | -------------------------------------------------------------------------------- /subcommand/version/command.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package version 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/mitchellh/cli" 10 | ) 11 | 12 | type Command struct { 13 | UI cli.Ui 14 | Version string 15 | GitCommit string 16 | } 17 | 18 | func (c *Command) Run(_ []string) int { 19 | c.UI.Output(fmt.Sprintf("consul-aws %s", c.Version)) 20 | c.UI.Output(fmt.Sprintf("Git Commit: %s", c.GitCommit)) 21 | return 0 22 | } 23 | 24 | func (c *Command) Synopsis() string { 25 | return "Prints the version" 26 | } 27 | 28 | func (c *Command) Help() string { 29 | return "" 30 | } 31 | -------------------------------------------------------------------------------- /version/VERSION: -------------------------------------------------------------------------------- 1 | 0.1.4-dev 2 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package version 5 | 6 | import ( 7 | _ "embed" 8 | "fmt" 9 | "strings" 10 | ) 11 | 12 | var ( 13 | // The git commit that was compiled. These will be filled in by the 14 | // compiler. 15 | GitCommit string 16 | 17 | // The next version number that will be released. This will be updated after every release 18 | // Version must conform to the format expected by github.com/hashicorp/go-version 19 | // for tests to work. 20 | // A pre-release marker for the version can also be specified (e.g -dev). If this is omitted 21 | // then it means that it is a final release. Otherwise, this is a pre-release 22 | // such as "dev" (in development), "beta", "rc1", etc. 23 | //go:embed VERSION 24 | fullVersion string 25 | 26 | Version, VersionPrerelease, _ = strings.Cut(strings.TrimSpace(fullVersion), "-") 27 | 28 | // https://semver.org/#spec-item-10 29 | VersionMetadata = "" 30 | ) 31 | 32 | // GetHumanVersion composes the parts of the version in a way that's suitable 33 | // for displaying to humans. 34 | func GetHumanVersion() string { 35 | version := Version 36 | release := VersionPrerelease 37 | metadata := VersionMetadata 38 | 39 | if release != "" { 40 | version += fmt.Sprintf("-%s", release) 41 | } 42 | 43 | if metadata != "" { 44 | version += fmt.Sprintf("+%s", metadata) 45 | } 46 | 47 | // Strip off any single quotes added by the git information. 48 | return strings.ReplaceAll(version, "'", "") 49 | } 50 | --------------------------------------------------------------------------------