Repository: hashicorp/levant Branch: main Commit: 5492e67eeea9 Files: 90 Total size: 304.0 KB Directory structure: gitextract_ffa58g6j/ ├── .dockerignore ├── .github/ │ ├── CODEOWNERS │ ├── CONTRIBUTING.md │ ├── ISSUE_TEMPLATE.md │ ├── dependabot.yml │ └── workflows/ │ ├── actionlint.yml │ ├── build.yml │ ├── ci.yml │ ├── nightly-release-readme.md │ └── nightly-release.yml ├── .gitignore ├── .go-version ├── .golangci.yml ├── .release/ │ ├── ci.hcl │ ├── levant-artifacts.hcl │ ├── release-metadata.hcl │ └── security-scan.hcl ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── GNUmakefile ├── LICENSE ├── README.md ├── client/ │ ├── consul.go │ └── nomad.go ├── command/ │ ├── deploy.go │ ├── deploy_test.go │ ├── dispatch.go │ ├── meta.go │ ├── plan.go │ ├── render.go │ ├── scale_in.go │ ├── scale_out.go │ ├── test-fixtures/ │ │ ├── group_canary.nomad │ │ ├── job_canary.nomad │ │ └── periodic_batch.nomad │ └── version.go ├── commands.go ├── docs/ │ ├── README.md │ ├── clients.md │ ├── commands.md │ └── templates.md ├── go.mod ├── go.sum ├── helper/ │ ├── files.go │ ├── files_test.go │ ├── kvflag.go │ ├── kvflag_test.go │ ├── nomad/ │ │ ├── opts.go │ │ └── opts_test.go │ ├── variable.go │ └── variable_test.go ├── levant/ │ ├── auto_revert.go │ ├── deploy.go │ ├── dispatch.go │ ├── failure_inspector.go │ ├── job_status_checker.go │ ├── job_status_checker_test.go │ ├── plan.go │ └── structs/ │ └── config.go ├── logging/ │ └── logging.go ├── main.go ├── scale/ │ ├── scale.go │ └── scale_test.go ├── scripts/ │ ├── docker-entrypoint.sh │ └── version.sh ├── template/ │ ├── funcs.go │ ├── render.go │ ├── render_test.go │ ├── template.go │ └── test-fixtures/ │ ├── missing_var.nomad │ ├── multi_templated.nomad │ ├── none_templated.nomad │ ├── single_templated.nomad │ ├── test-overwrite.yaml │ ├── test.tf │ └── test.yaml ├── test/ │ ├── acctest/ │ │ ├── acctest.go │ │ └── deploy.go │ ├── deploy_test.go │ └── fixtures/ │ ├── deploy_alloc_error.nomad │ ├── deploy_basic.nomad │ ├── deploy_canary.nomad │ ├── deploy_count.nomad │ ├── deploy_driver_error.nomad │ ├── deploy_lifecycle.nomad │ ├── deploy_namespace.nomad │ └── deploy_task_scaling.nomad └── version/ ├── version.go └── version_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ ./bin/ ./pkg/ .git ================================================ FILE: .github/CODEOWNERS ================================================ # release configuration /.release/ @hashicorp/release-engineering /.github/workflows/build.yml @hashicorp/release-engineering ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing to Levant **First** of all welcome and thank you for even considering to contribute to the Levant project. If you're unsure or afraid of anything, just ask or submit the issue or pull request anyways. You won't be yelled at for giving your best effort. If you wish to work on Levant itself or any of its built-in components, you will first need [Go](https://golang.org/) installed on your machine (version 1.9+ is required) and ensure your [GOPATH](https://golang.org/doc/code.html#GOPATH) is correctly configured. ## Issues Remember to craft your issues in such a way as to help others who might be facing similar challenges. Give your issues meaningful titles, that offer context. Please try to use complete sentences in your issue. Everything is okay to submit as an issue, even questions or ideas. Not every contribution requires an issue, but all bugs and significant changes to core functionality do. A pull request to fix a bug or implement a core change will not be accepted without a corresponding bug report. ## What Good Issues Look Like Levant includes a default issue template, please endeavor to provide as much information as possible. 1. **Avoid raising duplicate issues.** Please use the GitHub issue search feature to check whether your bug report or feature request has been mentioned in the past. 1. **Provide Bug Details.** When filing a bug report, include debug logs, version details and stack traces. Your issue should provide: 1. Guidance on **how to reproduce the issue.** 1. Tell us **what you expected to happen.** 1. Tell us **what actually happened.** 1. Tell us **what version of Levant and Nomad you're using.** 1. **Provide Feature Details.** When filing a feature request, include background on why you're requesting the feature and how it will be useful to others. If you have a design proposal to implement the feature, please include these details so the maintainers can provide feedback on the proposed approach. ## Pull Requests **All pull requests must include a description.** The description should at a minimum, provide background on the purpose of the pull request. Consider providing an overview of why the work is taking place; don’t assume familiarity with the history. If the pull request is related to an issue, make sure to mention the issue number(s). Try to keep pull requests tidy, and be prepared for feedback. Everyone is welcome to contribute to Levant but we do try to keep a high quality of code standard. Be ready to face this. Feel free to open a pull request for anything, about anything. **Everyone** is welcome. ## Get Early Feedback If you are contributing, do not feel the need to sit on your contribution until it is perfectly polished and complete. It helps everyone involved for you to seek feedback as early as you possibly can. Submitting an early, unfinished version of your contribution for feedback in no way prejudices your chances of getting that contribution accepted, and can save you from putting a lot of work into a contribution that is not suitable for the project. ## Code Review Pull requests will not be merged until they've been code reviewed by at least one maintainer. You should implement any code review feedback unless you strongly object to it. In the event that you object to the code review feedback, you should make your case clearly and calmly. If, after doing so, the feedback is judged to still apply, you must either apply the feedback or withdraw your contribution. # Tests and Checks As Levant continues to mature, additional test harnesses will be implemented. Once these harnesses are in place, tests will be required for all bugs fixes and features. No exception. ## Linting All Go code in your pull request must pass `lint` checks. You can run lint on all Golang files using the `make lint` target. All lint checks are automatically enforced by CI tests. ## Formatting **Do your best to follow existing conventions you see in the codebase**, and ensure your code is formatted with `go fmt`. You can run `fmt` on all Golang files using the `make fmt` target. All format checks are automatically enforced by CI tests. ## Testing Tests are required for all new functionality where practical; certain portions of Levant have no tests but this should be the exception and not the rule. # Building Levant is linted, tested and built using make: ``` make ``` The resulting binary file will be stored in the project root directory and is named `levant-local` which can be invoked as required. The binary is built by default for the host system only. If you wish to build for different architectures and operating systems please see the [golang docs](https://golang.org/doc/install/source) for the whole list of available `GOOS` and `GOARCH` values. ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ **Description** **Relevant Nomad job specification file** ``` (paste your output here) ``` **Output of `levant version`:** ``` (paste your output here) ``` **Output of `consul version`:** ``` (paste your output here) ``` **Output of `nomad version`:** ``` (paste your output here) ``` **Additional environment details:** **Debug log outputs from Levant:** ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" ================================================ FILE: .github/workflows/actionlint.yml ================================================ # If the repository is public, be sure to change to GitHub hosted runners name: Lint GitHub Actions Workflows on: pull_request: permissions: contents: read jobs: actionlint: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: "Check workflow files" uses: docker://docker.mirror.hashicorp.services/rhysd/actionlint:latest ================================================ FILE: .github/workflows/build.yml ================================================ name: build on: push: workflow_dispatch: workflow_call: env: PKG_NAME: "levant" jobs: get-go-version: name: "Determine Go toolchain version" runs-on: ubuntu-22.04 outputs: go-version: ${{ steps.get-go-version.outputs.go-version }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Determine Go version id: get-go-version run: | echo "Building with Go $(cat .go-version)" echo "go-version=$(cat .go-version)" >> "$GITHUB_OUTPUT" get-product-version: runs-on: ubuntu-22.04 outputs: product-version: ${{ steps.get-product-version.outputs.product-version }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: get product version id: get-product-version run: | make version echo "product-version=$(make version)" >> "$GITHUB_OUTPUT" generate-metadata-file: needs: get-product-version runs-on: ubuntu-22.04 outputs: filepath: ${{ steps.generate-metadata-file.outputs.filepath }} steps: - name: "Checkout directory" uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Generate metadata file id: generate-metadata-file uses: hashicorp/actions-generate-metadata@fdbc8803a0e53bcbb912ddeee3808329033d6357 # v1.1.1 with: version: ${{ needs.get-product-version.outputs.product-version }} product: ${{ env.PKG_NAME }} repository: ${{ env.PKG_NAME }} repositoryOwner: "hashicorp" - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 if: ${{ !env.ACT }} with: name: metadata.json path: ${{ steps.generate-metadata-file.outputs.filepath }} generate-ldflags: needs: get-product-version runs-on: ubuntu-22.04 outputs: ldflags: ${{ steps.generate-ldflags.outputs.ldflags }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Generate ld flags id: generate-ldflags run: | echo "ldflags=-X 'github.com/hashicorp/levant/version.GitDescribe=v${{ needs.get-product-version.outputs.product-version }}'" >> "$GITHUB_OUTPUT" build: needs: - get-go-version - get-product-version - generate-ldflags runs-on: ubuntu-22.04 strategy: matrix: goos: ["linux", "darwin", "windows", "freebsd"] goarch: [ "amd64", "arm64"] fail-fast: true name: Go ${{ needs.get-go-version.outputs.go-version }} ${{ matrix.goos }} ${{ matrix.goarch }} build steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup go uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version-file: ".go-version" - name: Build Levant env: GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} GO_LDFLAGS: ${{ needs.generate-ldflags.outputs.ldflags }} CGO_ENABLED: "0" BIN_PATH: dist/levant uses: hashicorp/actions-go-build@37358f6098ef21b09542d84a9814ebb843aa4e3e # v1.0.0 with: product_name: ${{ env.PKG_NAME }} product_version: ${{ needs.get-product-version.outputs.product-version }} go_version: ${{ needs.get-go-version.outputs.go-version }} os: ${{ matrix.goos }} arch: ${{ matrix.goarch }} reproducible: nope instructions: |- make crt - if: ${{ matrix.goos == 'linux' }} uses: hashicorp/actions-packaging-linux@8d55a640bb30b5508f16757ea908b274564792d4 # v1.7.0 with: name: "levant" description: "Levant is a templating and deployment tool for HashiCorp Nomad" arch: ${{ matrix.goarch }} version: ${{ needs.get-product-version.outputs.product-version }} maintainer: "HashiCorp" homepage: "https://github.com/hashicorp/levant" license: "MPL-2.0" binary: "dist/levant" deb_depends: "openssl" rpm_depends: "openssl" - if: ${{ matrix.goos == 'linux' }} name: Determine package file names run: | echo "RPM_PACKAGE=$(basename out/*.rpm)" >> "$GITHUB_ENV" echo "DEB_PACKAGE=$(basename out/*.deb)" >> "$GITHUB_ENV" - if: ${{ matrix.goos == 'linux' }} uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: ${{ env.RPM_PACKAGE }} path: out/${{ env.RPM_PACKAGE }} if-no-files-found: error - if: ${{ matrix.goos == 'linux' }} uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: ${{ env.DEB_PACKAGE }} path: out/${{ env.DEB_PACKAGE }} if-no-files-found: error build-docker-default: needs: - get-product-version - build runs-on: ubuntu-22.04 strategy: matrix: arch: ["arm64", "amd64"] fail-fast: true env: version: ${{ needs.get-product-version.outputs.product-version }} name: Docker ${{ matrix.arch }} default release build steps: - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Docker Build (Action) uses: hashicorp/actions-docker-build@11d43ef520c65f58683d048ce9b47d6617893c9a # v2.0.0 with: smoke_test: | TEST_VERSION="$(docker run "${IMAGE_NAME}" version | awk '/Levant/{print $2}')" if [ "${TEST_VERSION}" != "v${version}" ]; then echo "Test FAILED" exit 1 fi echo "Test PASSED" version: ${{ needs.get-product-version.outputs.product-version }} target: release arch: ${{ matrix.arch }} tags: | docker.io/hashicorp/${{ env.PKG_NAME }}:${{ env.version }} dev_tags: | docker.io/hashicorppreview/${{ env.PKG_NAME }}:${{ env.version }}-dev docker.io/hashicorppreview/${{ env.PKG_NAME }}:${{ env.version }}-${{ github.sha }} permissions: contents: read ================================================ FILE: .github/workflows/ci.yml ================================================ name: ci on: push: jobs: lint-go: runs-on: ubuntu-latest env: GO_TAGS: '' steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version-file: ".go-version" - name: Setup golangci-lint run: |- download=https://raw.githubusercontent.com/golangci/golangci-lint/9a8a056e9fe49c0e9ed2287aedce1022c79a115b/install.sh # v1.52.2 curl -sSf "$download" | sh -s v1.51.2 ./bin/golangci-lint version - run: make check check-deps-go: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version-file: ".go-version" - run: make check-mod test-go: runs-on: ubuntu-latest env: GO_TAGS: '' steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version-file: ".go-version" - run: make test build-go: runs-on: ubuntu-latest env: GO_TAGS: '' steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version-file: ".go-version" - run: make dev permissions: contents: read ================================================ FILE: .github/workflows/nightly-release-readme.md ================================================ Nightly releases are snapshots of the development activity on the Levant project that may include new features and bug fixes scheduled for upcoming releases. These releases are made available to make it easier for users to test their existing build configurations against the latest Levant code base for potential issues or to experiment with new features, with a chance to provide feedback on ways to improve the changes before being released. As these releases are snapshots of the latest code, you may encounter an issue compared to the latest stable release. Users are encouraged to run nightly releases in a non production environment. If you encounter an issue, please check our [issue tracker](https://github.com/hashicorp/levant/issues) to see if the issue has already been reported; if a report hasn't been made, please report it so we can review the issue and make any needed fixes. **Note**: Nightly releases are only available via GitHub Releases, and artifacts are not codesigned or notarized. Distribution via other [Release Channels](https://www.hashicorp.com/official-release-channels) such as the Releases Site or Homebrew is not yet supported. ================================================ FILE: .github/workflows/nightly-release.yml ================================================ # This GitHub action triggers a fresh set of Levant builds and publishes them # to GitHub Releases under the `nightly` tag. # Note that artifacts available via GitHub Releases are not codesigned or # notarized. # Failures are reported to slack. name: Nightly Release on: schedule: # Runs against the default branch every day overnight - cron: "18 3 * * *" workflow_dispatch: jobs: # Build a fresh set of artifacts build-artifacts: uses: ./.github/workflows/build.yml github-release: needs: build-artifacts runs-on: ubuntu-22.04 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 - name: Download built artifacts uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: path: out/ # Set BUILD_OUTPUT_LIST to out\-.\*,out\... # This is needed to attach the build artifacts to the GitHub Release - name: Set BUILD_OUTPUT_LIST run: | (ls -xm1 out/) > tmp.txt sed 's:.*:out/&/*:' < tmp.txt > tmp2.txt echo "BUILD_OUTPUT_LIST=$(tr '\n' ',' < tmp2.txt | perl -ple 'chop')" >> "$GITHUB_ENV" rm -rf tmp.txt && rm -rf tmp2.txt - name: Advance nightly tag uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | try { await github.rest.git.deleteRef({ owner: context.repo.owner, repo: context.repo.repo, ref: "tags/nightly" }) } catch (e) { console.log("Warning: The nightly tag doesn't exist yet, so there's nothing to do. Trace: " + e) } await github.rest.git.createRef({ owner: context.repo.owner, repo: context.repo.repo, ref: "refs/tags/nightly", sha: context.sha }) # This will create a new GitHub Release called `nightly` # If a release with this name already exists, it will overwrite the existing data - name: Create a nightly GitHub prerelease id: create_prerelease uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 # v1.16.0 with: name: nightly artifacts: "${{ env.BUILD_OUTPUT_LIST }}" tag: nightly bodyFile: ".github/workflows/nightly-release-readme.md" prerelease: true allowUpdates: true removeArtifacts: true draft: false token: ${{ secrets.GITHUB_TOKEN }} - name: Publish nightly GitHub prerelease uses: eregon/publish-release@01df127f5e9a3c26935118e22e738d95b59d10ce # v1.0.6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: release_id: ${{ steps.create_prerelease.outputs.id }} # Send a slack notification if either job defined above fails slack-notify: needs: - build-artifacts - github-release if: always() && (needs.build-artifacts.result == 'failure' || needs.github-release.result == 'failure') runs-on: ubuntu-latest steps: - name: Notify Slack on Nightly Release Failure uses: hashicorp/actions-slack-status@1a3f63b30bd476aee1f3bd6f9d8f2aacc4f14d81 # v2.0.1 with: failure-message: |- :x::moon::nomad-sob: Levant Nightly Release *FAILED* on status: failure slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} permissions: contents: write ================================================ FILE: .gitignore ================================================ .DS_Store # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test dist/ # Build output directory. /bin /pkg # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof /pkg levant-local .idea vendor/*/.hg/* # assets download path when using bob CLI .bob ================================================ FILE: .go-version ================================================ 1.24.4 ================================================ FILE: .golangci.yml ================================================ # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: MPL-2.0 run: deadline: 5m issues-exit-code: 1 tests: true output: formats: colored-line-number print-issued-lines: true print-linter-name: true linters: enable: - errcheck - goconst - gocyclo - gofmt - goimports - gosec - govet - ineffassign - misspell - prealloc - revive - unconvert - unparam - unused enable-all: false disable-all: true ================================================ FILE: .release/ci.hcl ================================================ # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: MPL-2.0 schema = "1" project "levant" { team = "nomad" slack { notification_channel = "C03B5EWFW01" } github { organization = "hashicorp" repository = "levant" release_branches = [ "main", "release/**", ] } } event "build" { action "build" { organization = "hashicorp" repository = "levant" workflow = "build" } } event "prepare" { depends = ["build"] action "prepare" { organization = "hashicorp" repository = "crt-workflows-common" workflow = "prepare" depends = ["build"] } notification { on = "always" } } ## These are promotion and post-publish events ## they should be added to the end of the file after the verify event stanza. event "trigger-staging" { // This event is dispatched by the bob trigger-promotion command // and is required - do not delete. } event "promote-staging" { depends = ["trigger-staging"] action "promote-staging" { organization = "hashicorp" repository = "crt-workflows-common" workflow = "promote-staging" config = "release-metadata.hcl" } notification { on = "always" } } event "promote-staging-docker" { depends = ["promote-staging"] action "promote-staging-docker" { organization = "hashicorp" repository = "crt-workflows-common" workflow = "promote-staging-docker" } notification { on = "always" } } event "trigger-production" { // This event is dispatched by the bob trigger-promotion command // and is required - do not delete. } event "promote-production" { depends = ["trigger-production"] action "promote-production" { organization = "hashicorp" repository = "crt-workflows-common" workflow = "promote-production" } notification { on = "always" } } event "promote-production-docker" { depends = ["promote-production"] action "promote-production-docker" { organization = "hashicorp" repository = "crt-workflows-common" workflow = "promote-production-docker" } notification { on = "always" } } event "promote-production-packaging" { depends = ["promote-production-docker"] action "promote-production-packaging" { organization = "hashicorp" repository = "crt-workflows-common" workflow = "promote-production-packaging" } notification { on = "always" } } ================================================ FILE: .release/levant-artifacts.hcl ================================================ # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: MPL-2.0 schema = 1 artifacts { zip = [ "levant_${version}_darwin_amd64.zip", "levant_${version}_darwin_arm64.zip", "levant_${version}_freebsd_amd64.zip", "levant_${version}_freebsd_arm64.zip", "levant_${version}_linux_amd64.zip", "levant_${version}_linux_arm64.zip", "levant_${version}_windows_amd64.zip", "levant_${version}_windows_arm64.zip", ] rpm = [ "levant-${version_linux}-1.aarch64.rpm", "levant-${version_linux}-1.x86_64.rpm", ] deb = [ "levant_${version_linux}-1_amd64.deb", "levant_${version_linux}-1_arm64.deb", ] container = [ "levant_release_linux_amd64_${version}_${commit_sha}.docker.dev.tar", "levant_release_linux_amd64_${version}_${commit_sha}.docker.tar", "levant_release_linux_arm64_${version}_${commit_sha}.docker.dev.tar", "levant_release_linux_arm64_${version}_${commit_sha}.docker.tar", ] } ================================================ FILE: .release/release-metadata.hcl ================================================ # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: MPL-2.0 url_docker_registry_dockerhub = "https://hub.docker.com/r/hashicorp/levant" url_license = "https://github.com/hashicorp/levant/blob/main/LICENSE" url_source_repository = "https://github.com/hashicorp/levant" ================================================ FILE: .release/security-scan.hcl ================================================ # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: MPL-2.0 container { dependencies = true alpine_security = true secrets { all = true } # Triage items that are _safe_ to ignore here. Note that this list should be # periodically cleaned up to remove items that are no longer found by the scanner. triage { suppress { vulnerabilities = [ "GHSA-rx97-6c62-55mf", // https://github.com/github/advisory-database/pull/5759 TODO(dduzgun): remove when dep updated. "CVE-2025-46394", // busybox@1.37.0-r18 TODO(dduzgun): remove when dep updated. "CVE-2024-58251", // busybox@1.37.0-r18 TODO(dduzgun): remove when dep updated. ] } } } binary { go_modules = true osv = true go_stdlib = true oss_index = false nvd = false secrets { all = true } # Triage items that are _safe_ to ignore here. Note that this list should be # periodically cleaned up to remove items that are no longer found by the scanner. triage { suppress { vulnerabilities = [ "GHSA-rx97-6c62-55mf", // https://github.com/github/advisory-database/pull/5759 TODO(dduzgun): remove when dep updated. "CVE-2025-46394", // busybox@1.37.0-r18 TODO(dduzgun): remove when dep updated. "CVE-2024-58251", // busybox@1.37.0-r18 TODO(dduzgun): remove when dep updated. ] } } } ================================================ FILE: CHANGELOG.md ================================================ ## 0.4.0 (June 26, 2025) __BACKWARDS INCOMPATIBILITIES:__ * cli: Levant no longer supports the deprecated Vault token workflow. IMPROVEMENT: * build: Now builds with Go v1.24 * deps: Updated Nomad API to v1.10.2 ## 0.3.3 (October 5, 2023) IMPROVEMENTS: * build: Now builds with Go v1.21.1 [[GH-507](https://github.com/hashicorp/levant/pull/507)] * deps: Updated Nomad dependency to v1.6.2 [[GH-507](https://github.com/hashicorp/levant/pull/507)] ## 0.3.2 (October 20, 2022) IMPROVEMENTS: * build: Now builds with go v1.19.1 [[GH-464](https://github.com/hashicorp/levant/pull/464)] * deps: Updated Nomad dependency to v1.4.1. [[GH-464](https://github.com/hashicorp/levant/pull/464)] * deps: Updated golang.org/x/text to v0.4.0 [[GH-465](https://github.com/hashicorp/levant/pull/465)] ## 0.3.1 (February 14, 2022) IMPROVEMENTS: * build: Updated Nomad dependency to 1.2.4. [[GH-438](https://github.com/hashicorp/levant/pull/438)] ## 0.3.0 (March 09, 2021) __BACKWARDS INCOMPATIBILITIES:__ * template: existing Levant functions that share a name with [sprig](https://github.com/Masterminds/sprig) functions have been renamed to include the prefix `levant` such as `levantEnv`. BUG FIXES: * cli: Fixed panic when dispatching a job. [[GH-348](https://github.com/hashicorp/levant/pull/348)] * status-checker: Pass the namespace to the query options when calling the Nomad API [[GH-356](https://github.com/hashicorp/levant/pull/356)] * template: Fixed issue with default variables file not being used. [[GH-353](https://github.com/hashicorp/levant/pull/353)] IMPROVEMENTS: * build: Updated Nomad dependency to 1.0.4. [[GH-399](https://github.com/hashicorp/levant/pull/399)] * cli: Added `log-level` and `log-format` flags to render command. [[GH-346](https://github.com/hashicorp/levant/pull/346)] * render: when rendering, send logging to stderr if stdout is not a terminal [[GH-386](https://github.com/hashicorp/levant/pull/386)] * template: Added [sprig](https://github.com/Masterminds/sprig) template functions. [[GH-347](https://github.com/hashicorp/levant/pull/347)] * template: Added `spewDump` and `spewPrintf` functions for easier debugging. [[GH-344](https://github.com/hashicorp/levant/pull/344)] ## 0.2.9 (27 December 2019) IMPROVEMENTS: * Update vendoered version of Nomad to 0.9.6 [GH-313](https://github.com/jrasell/levant/pull/313) * Update to go 1.13 and use modules rather than dep [GH-319](https://github.com/jrasell/levant/pull/319) * Remove use of vendor nomad/structs import to allow easier vendor [GH-320](https://github.com/jrasell/levant/pull/320) * Add template replace function [GH-291](https://github.com/jrasell/levant/pull/291) BUG FIXES: * Use info level logs when no changes are detected [GH-303](https://github.com/jrasell/levant/pull/303) ## 0.2.8 (14 September 2019) IMPROVEMENTS: * Add `-force` flag to deploy CLI command which allows for forcing a deployment even if Levant detects 0 changes on plan [GH-296](https://github.com/jrasell/levant/pull/296) BUG FIXES: * Fix segfault when logging deployID details [GH-286](https://github.com/jrasell/levant/pull/286) * Fix error message within scale-in which incorrectly referenced scale-out [GH-285](https://github.com/jrasell/levant/pull/285/files) ## 0.2.7 (19 March 2019) IMPROVEMENTS: * Use `missingkey=zero` rather than error which allows better use of standard go templating, particulary conditionals [GH-275](https://github.com/jrasell/levant/pull/275) * Added maths functions add, subtract, multiply, divide and modulo to the template rendering process [GH-277](https://github.com/jrasell/levant/pull/277) ## 0.2.6 (25 February 2019) IMPROVEMENTS: * Add the ability to supply a Vault token to a job during deployment via either a `vault` or `vault-token` flag [GH-258](https://github.com/jrasell/levant/pull/258) * New `fileContents` template function which allows the entire contents of a file to be read into the template [GH-261](https://github.com/jrasell/levant/pull/261) BUG FIXES: * Fix a panic when running scale* deployment watcher due to incorrectly initialized client config [GH-253](https://github.com/jrasell/levant/pull/253) * Fix incorrect behavior when flag `ignore-no-changes` was set [GH-264](https://github.com/jrasell/levant/pull/264) * Fix endless deployment loop when Nomad doesn't return a deployment ID [GH-268](https://github.com/jrasell/levant/pull/268) ## 0.2.5 (25 October 2018) BUG FIXES: * Fix panic in deployment where count is not specified due to unsafe count checking on task groups [GH-249](https://github.com/jrasell/levant/pull/249) ## 0.2.4 (24 October 2018) BUG FIXES: * Fix panic in scale commands due to an incorrectly initialized configuration struct [GH-244](https://github.com/jrasell/levant/pull/244) * Fix bug where job deploys with taskgroup counts of 0 would hang for 1 hour [GH-246](https://github.com/jrasell/levant/pull/246) ## 0.2.3 (2 October 2018) IMPROVEMENTS: * New `env` template function allows the lookup and substitution of variables by environemnt variables [GH-225](https://github.com/jrasell/levant/pull/225) * Add plan command to allow running a plan whilst using templating [GH-234](https://github.com/jrasell/levant/pull/234) * Add `toUpper` and `toLower` template funcs [GH-237](https://github.com/jrasell/levant/pull/237) ## 0.2.2 (6 August 2018) BUG FIXES: * Fix an issue where if an evaluation had filtered nodes Levant would exit immediately rather than tracking the deployment which could still succeed [GH-221](https://github.com/jrasell/levant/pull/221) * Fixed failure inspector to report on tasks that are restarting [GH-82](https://github.com/jrasell/levant/pull/82) ## 0.2.1 (20 July 2018) IMPROVEMENTS: * JSON can now be used as a variable file format [GH-210](https://github.com/jrasell/levant/pull/210) * The template funcs now include numerous parse functions to provide greater flexibility [GH-212](https://github.com/jrasell/levant/pull/212) * Ability to configure allow-stale Nomad setting when performing calls to help in environments with high network latency [GH-185](https://github.com/jrasell/levant/pull/185) BUG FIXES: * Update vendored package of Nomad to fix failures when interacting with jobs configured with update progress_deadline params [GH-216](https://github.com/jrasell/levant/pull/216) ## 0.2.0 (4 July 2018) IMPROVEMENTS: * New `scale-in` and `scale-out` commands allow an operator to manually scale jobs and task groups based on counts or percentages [GH-172](https://github.com/jrasell/levant/pull/172) * New template functions allowing the lookup of variables from Consul KVs, ISO-8601 timestamp generation and loops [GH-175](https://github.com/jrasell/levant/pull/175), [GH-202](https://github.com/jrasell/levant/pull/202) * Multiple variable files can be passed on each run, allowing for common configuration to be shared across jobs [GH-180](https://github.com/jrasell/levant/pull/180) * Provide better command help for deploy and render commands [GH-183](https://github.com/jrasell/levant/pull/184) * Add `-ignore-no-changes` flag to deploy CLI command which allows the changing on behaviour to exit 0 even if Levant detects 0 changes on plan [GH-196](https://github.com/jrasell/levant/pull/196) BUG FIXES: * Fix formatting with version summary output which had erronous quote [GH-170](https://github.com/jrasell/levant/pull/170) ## 0.1.1 (13 May 2018) IMPROVEMENTS: * Use govvv for builds and to supply additional version information in the version command output [GH-151](https://github.com/jrasell/levant/pull/151) * Levant will now run Nomad plan before deployments to log the plan diff [GH-153](https://github.com/jrasell/levant/pull/153) * Logging can now be output in JSON format and uses contextual data for better processing ability [GH-157](https://github.com/jrasell/levant/pull/157) BUG FIXES: * Fix occasional panic when performing deployment check of a batch job deployment [GH-150](https://github.com/jrasell/levant/pull/150) ## 0.1.0 (18 April 2018) IMPROVEMENTS: * New 'dispatch' command which allows Levant to dispatch Nomad jobs which will go through Levants additional job checking [GH-128](https://github.com/jrasell/levant/pull/128) * New 'force-batch' deploy flag which allows users to trigger a periodic run on deployment independent of the schedule [GH-110](https://github.com/jrasell/levant/pull/110) * Enhanced job status checking for non-service type jobs [GH-96](https://github.com/jrasell/levant/pull/96), [GH-109](https://github.com/jrasell/levant/pull/109) * Implement config struct for Levant to track config during run [GH-102](https://github.com/jrasell/levant/pull/102) * Test and build Levant with Go version 1.10 [GH-119](https://github.com/jrasell/levant/pull/119), [GH-116](https://github.com/jrasell/levant/pull/116) * Add a catchall for unhandled failure cases to log more useful information for the operator [GH-138](https://github.com/jrasell/levant/pull/138) * Updated vendored dependancy of Nomad to 0.8.0 [GH-137](https://github.com/jrasell/levant/pull/137) BUG FIXES: * Service jobs that don't have an update stanza do not produce deployments and should skip the deployment watcher [GH-99](https://github.com/jrasell/levant/pull/99) * Ensure the count updater ignores jobs that are in stopped state [GH-106](https://github.com/jrasell/levant/pull/106) * Fix a small formatting issue with the deploy command arg help [GH-111](https://github.com/jrasell/levant/pull/111) * Do not run the auto-revert inspector if auto-promote fails [GH-122](https://github.com/jrasell/levant/pull/122) * Fix issue where allocationStatusChecker logged incorrectly [GH-131](https://github.com/jrasell/levant/pull/131) * Add retry to auto-revert checker to ensure the correct deployment is monitored, and not the original [GH-134](https://github.com/jrasell/levant/pull/134) ## 0.0.4 (25 January 2018) IMPROVEMENTS: * Job types of `batch` now undergo checking to confirm the job reaches status of `running` [GH-73](https://github.com/jrasell/levant/pull/73) * Vendored Nomad version has been increased to 0.7.1 allowing use of Nomad ACL tokens [GH-76](https://github.com/jrasell/levant/pull/76) * Log messages now includes the date, time and timezone [GH-80](https://github.com/jrasell/levant/pull/80) BUG FIXES: * Skip health checks for task groups without canaries when performing canary auto-promote health checking [GH-83](https://github.com/jrasell/levant/pull/83) * Fix issue where jobs without specified count caused panic [GH-89](https://github.com/jrasell/levant/pull/89) ## 0.0.3 (23 December 2017) IMPROVEMENTS: * Levant can now track Nomad auto-revert of a failed deployment [GH-55](https://github.com/jrasell/levant/pull/55) * Provide greater feedback around variables file passed, CLI variables passed and which variables are being used by Levant.[GH-62](https://github.com/jrasell/levant/pull/62) * Levant supports autoloading of default files when running `levant deploy` [GH-37](https://github.com/jrasell/levant/pull/37) BUG FIXES: * Fix issue where Levant did not correctly handle deploying jobs of type `batch` [GH-52](https://github.com/jrasell/levant/pull/52) * Fix issue where evaluations errors were not being fully checked [GH-66](https://github.com/jrasell/levant/pull/66) * Fix issue in failure_inspector incorrectly handling multi-groups [GH-69](https://github.com/jrasell/levant/pull/69) ## 0.0.2 (29 November 2017) IMPROVEMENTS: * Introduce `-force-count` flag into deploy command which disables dynamic count updating; meaning Levant will explicity use counts defined in the job specification template [GH-33](https://github.com/jrasell/levant/pull/33) * Levant deployments now inspect the evaluation results and log any error messages [GH-40](https://github.com/jrasell/levant/pull/40) BUG FIXES: * Fix formatting issue in render command help [GH-28](https://github.com/jrasell/levant/pull/28) * Update failure_inspector to cover more failure use cases [GH-27](https://github.com/jrasell/levant/pull/27) * Fix a bug in handling Nomad job types incorrectly [GH-32](https://github.com/jrasell/levant/pull/32) * Fix issue where jobs deployed with all task group counts at 0 would cause a failure as no deployment ID is returned [GH-36](https://github.com/jrasell/levant/pull/36) ## 0.0.1 (30 October 2017) - Initial release. ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Code of Conduct ## 1. Purpose A primary goal of Levant is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof). This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior. We invite all those who participate in Levant to help us create safe and positive experiences for everyone. ## 2. Open Source Citizenship A supplemental goal of this Code of Conduct is to increase open source citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community. Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society. If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know. ## 3. Expected Behavior The following behaviors are expected and requested of all community members: * Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community. * Exercise consideration and respect in your speech and actions. * Attempt collaboration before conflict. * Refrain from demeaning, discriminatory, or harassing behavior and speech. * Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential. * Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations. ## 4. Unacceptable Behavior The following behaviors are considered harassment and are unacceptable within our community: * Violence, threats of violence or violent language directed against another person. * Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language. * Posting or displaying sexually explicit or violent material. * Posting or threatening to post other people’s personally identifying information ("doxing"). * Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability. * Inappropriate photography or recording. * Inappropriate physical contact. You should have someone’s consent before touching them. * Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances. * Deliberate intimidation, stalking or following (online or in person). * Advocating for, or encouraging, any of the above behavior. * Sustained disruption of community events, including talks and presentations. ## 5. Consequences of Unacceptable Behavior Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated. Anyone asked to stop unacceptable behavior is expected to comply immediately. If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event). ## 6. Reporting Guidelines If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. jamesrasell@gmail.com. Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress. ## 7. Addressing Grievances If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify Jrasell with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies. ## 8. Scope We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues–online and in-person–as well as in all one-on-one communications pertaining to community business. This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members. ## 9. Contact info jamesrasell@gmail.com ## 10. License and attribution This Code of Conduct is distributed under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/). Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy). Retrieved on November 22, 2016 from [http://citizencodeofconduct.org/](http://citizencodeofconduct.org/) ================================================ FILE: Dockerfile ================================================ # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: MPL-2.0 # This Dockerfile contains multiple targets. # Use 'docker build --target= .' to build one. # =================================== # Non-release images. # =================================== # devbuild compiles the binary # ----------------------------------- FROM golang:1.24 AS devbuild # Disable CGO to make sure we build static binaries ENV CGO_ENABLED=0 # Escape the GOPATH WORKDIR /build COPY . ./ RUN go build -o levant . # dev runs the binary from devbuild # ----------------------------------- FROM alpine:3.22 AS dev COPY --from=devbuild /build/levant /bin/ COPY ./scripts/docker-entrypoint.sh / ENTRYPOINT ["/docker-entrypoint.sh"] CMD ["help"] # =================================== # Release images. # =================================== FROM alpine:3.22 AS release ARG PRODUCT_NAME=levant ARG PRODUCT_VERSION ARG PRODUCT_REVISION # TARGETARCH and TARGETOS are set automatically when --platform is provided. ARG TARGETOS TARGETARCH LABEL maintainer="Nomad Team " LABEL version=${PRODUCT_VERSION} LABEL revision=${PRODUCT_REVISION} COPY dist/$TARGETOS/$TARGETARCH/levant /bin/ COPY ./scripts/docker-entrypoint.sh / # Create a non-root user to run the software. RUN addgroup $PRODUCT_NAME && \ adduser -S -G $PRODUCT_NAME $PRODUCT_NAME USER $PRODUCT_NAME ENTRYPOINT ["/docker-entrypoint.sh"] CMD ["help"] # =================================== # Set default target to 'dev'. # =================================== FROM dev ================================================ FILE: GNUmakefile ================================================ SHELL = bash default: lint test check-mod dev GIT_COMMIT := $(shell git rev-parse --short HEAD) GIT_DIRTY := $(if $(shell git status --porcelain),+CHANGES) GO_LDFLAGS := "$(GO_LDFLAGS) -X github.com/hashicorp/levant/version.GitCommit=$(GIT_COMMIT)$(GIT_DIRTY)" .PHONY: tools tools: ## Install the tools used to test and build @echo "==> Installing tools..." go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.5 go install github.com/hashicorp/hcl/v2/cmd/hclfmt@d0c4fa8b0bbc2e4eeccd1ed2a32c2089ed8c5cf1 @echo "==> Done" pkg/%/levant: GO_OUT ?= $@ pkg/windows_%/levant: GO_OUT = $@.exe pkg/%/levant: ## Build Levant for GOOS_GOARCH, e.g. pkg/linux_amd64/levant @echo "==> Building $@ with tags $(GO_TAGS)..." @CGO_ENABLED=0 \ GOOS=$(firstword $(subst _, ,$*)) \ GOARCH=$(lastword $(subst _, ,$*)) \ go build -trimpath -ldflags $(GO_LDFLAGS) -tags "$(GO_TAGS)" -o "$(GO_OUT)" .PRECIOUS: pkg/%/levant pkg/%.zip: pkg/%/levant ## Build and zip Levant for GOOS_GOARCH, e.g. pkg/linux_amd64.zip @echo "==> Packaging for $@..." zip -j $@ $(dir $<)* .PHONY: crt crt: @CGO_ENABLED=0 go build -trimpath -ldflags $(GO_LDFLAGS) -tags "$(GO_TAGS)" -o "$(BIN_PATH)" .PHONY: dev dev: #check ## Build for the current development version @echo "==> Building Levant..." @CGO_ENABLED=0 GO111MODULE=on \ go build \ -ldflags $(GO_LDFLAGS) \ -o ./bin/levant @echo "==> Done" .PHONY: test test: ## Test the source code @echo "==> Testing source code..." @go test -cover -v -tags -race \ "$(BUILDTAGS)" $(shell go list ./... |grep -v vendor |grep -v test) .PHONY: acceptance-test acceptance-test: ## Run the Levant acceptance tests @echo "==> Running $@..." go test -timeout 300s github.com/hashicorp/levant/test -v .PHONY: check check: tools lint check-mod ## Lint the source code and check other properties .PHONY: lint lint: hclfmt ## Lint the source code @echo "==> Linting source code..." @golangci-lint run -j 1 @echo "==> Done" .PHONY: hclfmt hclfmt: ## Format HCL files with hclfmt @echo "--> Formatting HCL" @find . -name '.git' -prune \ -o -name '*fixtures*' -prune \ -o \( -name '*.nomad' -o -name '*.hcl' -o -name '*.tf' \) \ -print0 | xargs -0 hclfmt -w @if (git status -s | grep -q -e '\.hcl$$' -e '\.nomad$$' -e '\.tf$$'); then echo The following HCL files are out of sync; git status -s | grep -e '\.hcl$$' -e '\.nomad$$' -e '\.tf$$'; exit 1; fi .PHONY: check-mod check-mod: ## Checks the Go mod is tidy @echo "==> Checking Go mod..." @GO111MODULE=on go mod tidy @if (git status --porcelain | grep -q go.mod); then \ echo go.mod needs updating; \ git --no-pager diff go.mod; \ exit 1; fi @echo "==> Done" HELP_FORMAT=" \033[36m%-25s\033[0m %s\n" .PHONY: help help: ## Display this usage information @echo "Levant make commands:" @grep -E '^[^ ]+:.*?## .*$$' $(MAKEFILE_LIST) | \ sort | \ awk 'BEGIN {FS = ":.*?## "}; \ {printf $(HELP_FORMAT), $$1, $$2}' .PHONY: version version: ifneq (,$(wildcard version/version_ent.go)) @$(CURDIR)/scripts/version.sh version/version.go version/version_ent.go else @$(CURDIR)/scripts/version.sh version/version.go version/version.go endif ================================================ FILE: LICENSE ================================================ Copyright (c) 2017 HashiCorp, Inc. Mozilla Public License, version 2.0 1. Definitions 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means a. that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or b. that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: a. any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or b. any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: a. under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and b. under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: a. for any code that a Contributor has removed from Covered Software; or b. for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or c. under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: a. such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and b. You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. 6. Disclaimer of Warranty Covered Software is provided under this License on an "as is" basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer. 7. Limitation of Liability Under no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party's negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You. 8. Litigation Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. ================================================ FILE: README.md ================================================ # Levant _Levant v0.4.0 is the final release of Levant. All users are encouraged to migrate to [Nomad Pack](https://github.com/hashicorp/nomad-pack)._ [![Build Status](https://circleci.com/gh/hashicorp/levant.svg?style=svg)](https://circleci.com/gh/hashicorp/levant) [![Discuss](https://img.shields.io/badge/discuss-nomad-00BC7F?style=flat)](https://discuss.hashicorp.com/c/nomad) Levant is an open source templating and deployment tool for [HashiCorp Nomad][] jobs that provides realtime feedback and detailed failure messages upon deployment issues. ## Features - **Realtime Feedback**: Using watchers, Levant provides realtime feedback on Nomad job deployments allowing for greater insight and knowledge about application deployments. - **Advanced Job Status Checking**: Particularly for system and batch jobs, Levant ensures the job, evaluations and allocations all reach the desired state providing feedback at every stage. - **Dynamic Job Group Counts**: If the Nomad job is currently running on the cluster, Levant dynamically updates the rendered template with the relevant job group counts before deployment. - **Failure Inspection**: Upon a deployment failure, Levant inspects each allocation and logs information about each event, providing useful information for debugging without the need for querying the cluster retrospectively. - **Canary Auto Promotion**: In environments with advanced automation and alerting, automatic promotion of canary deployments may be desirable after a certain time threshold. Levant allows the user to specify a `canary-auto-promote` time period, which if reached with a healthy set of canaries, automatically promotes the deployment. - **Multiple Variable File Formats**: Currently Levant supports `.json`, `.tf`, `.yaml`, and `.yml` file extensions for the declaration of template variables. - **Auto Revert Checking**: In the event that a job deployment does not pass its healthy threshold and the job has auto-revert enabled; Levant tracks the resulting rollback deployment so you can see the exact outcome of the deployment process. ## Download & Install - Official Levant binaries can be downloaded from the [HashiCorp releases site][releases-hashicorp]. - Levant can be installed via the go toolkit using `go get github.com/hashicorp/levant && go install github.com/hashicorp/levant` - A docker image can be found on [Docker Hub][levant-docker]. The latest version can be downloaded using `docker pull hashicorp/levant`. - Levant can be built from source by firstly cloning the repository `git clone git://github.com/hashicorp/levant.git`. Once cloned, a binary can be built using the `make dev` command which will be available at `./bin/levant`. - There is a [Levant Ansible role][levant-ansible] available to help installation on machines. Thanks to @stevenscg for this. - Pre-built binaries of Levant from versions 0.2.9 and earlier can be downloaded from the [GitHub releases page][releases] page. These binaries were released prior to the migration to the HashiCorp organization. For example: `curl -L https://github.com/hashicorp/levant/releases/download/0.2.9/linux-amd64-levant -o levant` ## Templating Levant includes functionality to perform template variables substitution as well as trigger built-in template functions to add timestamps or retrieve information from Consul. For full details please consult the [templates][] documentation page. ## Commands Levant supports a number of command line arguments which provide control over the Levant binary. For detail about each command and its supported flags, please consult the [commands][] documentation page. ## Clients Levant utilizes the Nomad and Consul official clients and configuration can be done via a number of environment variables. For detail about these please read through the [clients][] documentation page. ## Contributing Community contributions to Levant are encouraged. Please refer to the [contribution guide][] for details about hacking on Levant. [clients]: ./docs/clients.md [commands]: ./docs/commands.md [templates]: ./docs/templates.md [contribution guide]: https://github.com/hashicorp/levant/blob/master/.github/CONTRIBUTING.md [hashicorp nomad]: https://www.nomadproject.io/ [releases]: https://github.com/hashicorp/levant/releases [levant-docker]: https://hub.docker.com/r/hashicorp/levant/ [levant-ansible]: https://github.com/stevenscg/ansible-role-levant [releases-hashicorp]: https://releases.hashicorp.com/levant/ ================================================ FILE: client/consul.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package client import ( consul "github.com/hashicorp/consul/api" ) // NewConsulClient is used to create a new client to interact with Consul. func NewConsulClient(addr string) (*consul.Client, error) { config := consul.DefaultConfig() if addr != "" { config.Address = addr } c, err := consul.NewClient(config) if err != nil { return nil, err } return c, nil } ================================================ FILE: client/nomad.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package client import ( nomad "github.com/hashicorp/nomad/api" ) // NewNomadClient is used to create a new client to interact with Nomad. func NewNomadClient(addr string) (*nomad.Client, error) { config := nomad.DefaultConfig() if addr != "" { config.Address = addr } c, err := nomad.NewClient(config) if err != nil { return nil, err } return c, nil } ================================================ FILE: command/deploy.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package command import ( "fmt" "strings" "github.com/hashicorp/levant/helper" "github.com/hashicorp/levant/levant" "github.com/hashicorp/levant/levant/structs" "github.com/hashicorp/levant/logging" "github.com/hashicorp/levant/template" nomad "github.com/hashicorp/nomad/api" ) // DeployCommand is the command implementation that allows users to deploy a // Nomad job based on passed templates and variables. type DeployCommand struct { Meta } // Help provides the help information for the deploy command. func (c *DeployCommand) Help() string { helpText := ` Usage: levant deploy [options] [TEMPLATE] Deploy a Nomad job based on input templates and variable files. The deploy command supports passing variables individually on the command line. Multiple commands can be passed in the format of -var 'key=value'. Variables passed via the command line take precedence over the same variable declared within a passed variable file. Arguments: TEMPLATE nomad job template If no argument is given we look for a single *.nomad file General Options: -address= The Nomad HTTP API address including port which Levant will use to make calls. -allow-stale Allow stale consistency mode for requests into nomad. -canary-auto-promote= The time in seconds, after which Levant will auto-promote a canary job if all canaries within the deployment are healthy. -consul-address= The Consul host and port to use when making Consul KeyValue lookups for template rendering. -force Execute deployment even though there were no changes. -force-batch Forces a new instance of the periodic job. A new instance will be created even if it violates the job's prohibit_overlap settings. -force-count Use the taskgroup count from the Nomad jobfile instead of the count that is currently set in a running job. -ignore-no-changes By default if no changes are detected when running a deployment Levant will exit with a status 1 to indicate a deployment didn't happen. This behaviour can be changed using this flag so that Levant will exit cleanly ensuring CD pipelines don't fail when no changes are detected. -log-level= Specify the verbosity level of Levant's logs. Valid values include DEBUG, INFO, and WARN, in decreasing order of verbosity. The default is INFO. -log-format= Specify the format of Levant's logs. Valid values are HUMAN or JSON. The default is HUMAN. -var-file= Path to a file containing user variables used when rendering the job template. You can repeat this flag multiple times to supply multiple var-files. Defaults to levant.(json|yaml|yml|tf). [default: levant.(json|yaml|yml|tf)] ` return strings.TrimSpace(helpText) } // Synopsis is provides a brief summary of the deploy command. func (c *DeployCommand) Synopsis() string { return "Render and deploy a Nomad job from a template" } // Run triggers a run of the Levant template and deploy functions. func (c *DeployCommand) Run(args []string) int { var err error var level, format string config := &levant.DeployConfig{ Client: &structs.ClientConfig{}, Deploy: &structs.DeployConfig{}, Plan: &structs.PlanConfig{}, Template: &structs.TemplateConfig{}, } flags := c.Meta.FlagSet("deploy", FlagSetVars) flags.Usage = func() { c.UI.Output(c.Help()) } flags.StringVar(&config.Client.Addr, "address", "", "") flags.BoolVar(&config.Client.AllowStale, "allow-stale", false, "") flags.IntVar(&config.Deploy.Canary, "canary-auto-promote", 0, "") flags.StringVar(&config.Client.ConsulAddr, "consul-address", "", "") flags.BoolVar(&config.Deploy.Force, "force", false, "") flags.BoolVar(&config.Deploy.ForceBatch, "force-batch", false, "") flags.BoolVar(&config.Deploy.ForceCount, "force-count", false, "") flags.BoolVar(&config.Plan.IgnoreNoChanges, "ignore-no-changes", false, "") flags.StringVar(&level, "log-level", "INFO", "") flags.StringVar(&format, "log-format", "HUMAN", "") flags.Var((*helper.FlagStringSlice)(&config.Template.VariableFiles), "var-file", "") if err = flags.Parse(args); err != nil { return 1 } args = flags.Args() if err = logging.SetupLogger(level, format); err != nil { c.UI.Error(err.Error()) return 1 } if len(args) == 1 { config.Template.TemplateFile = args[0] } else if len(args) == 0 { if config.Template.TemplateFile = helper.GetDefaultTmplFile(); config.Template.TemplateFile == "" { c.UI.Error(c.Help()) c.UI.Error("\nERROR: Template arg missing and no default template found") return 1 } } else { c.UI.Error(c.Help()) return 1 } config.Template.Job, err = template.RenderJob(config.Template.TemplateFile, config.Template.VariableFiles, config.Client.ConsulAddr, &c.Meta.flagVars) if err != nil { c.UI.Error(fmt.Sprintf("[ERROR] levant/command: %v", err)) return 1 } if config.Deploy.Canary > 0 { if err = c.checkCanaryAutoPromote(config.Template.Job, config.Deploy.Canary); err != nil { c.UI.Error(fmt.Sprintf("[ERROR] levant/command: %v", err)) return 1 } } if config.Deploy.ForceBatch { if err = c.checkForceBatch(config.Template.Job, config.Deploy.ForceBatch); err != nil { c.UI.Error(fmt.Sprintf("[ERROR] levant/command: %v", err)) return 1 } } if !config.Deploy.Force { p := levant.PlanConfig{ Client: config.Client, Plan: config.Plan, Template: config.Template, } planSuccess, changes := levant.TriggerPlan(&p) if !planSuccess { return 1 } else if !changes && p.Plan.IgnoreNoChanges { return 0 } } success := levant.TriggerDeployment(config, nil) if !success { return 1 } return 0 } func (c *DeployCommand) checkCanaryAutoPromote(job *nomad.Job, canaryAutoPromote int) error { if canaryAutoPromote == 0 { return nil } if job.Update != nil && job.Update.Canary != nil && *job.Update.Canary > 0 { return nil } for _, group := range job.TaskGroups { if group.Update != nil && group.Update.Canary != nil && *group.Update.Canary > 0 { return nil } } return fmt.Errorf("canary-auto-update of %v passed but job is not canary enabled", canaryAutoPromote) } // checkForceBatch ensures that if the force-batch flag is passed, the job is // periodic. func (c *DeployCommand) checkForceBatch(job *nomad.Job, forceBatch bool) error { if forceBatch && job.IsPeriodic() { return nil } return fmt.Errorf("force-batch passed but job is not periodic") } ================================================ FILE: command/deploy_test.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package command import ( "testing" "github.com/hashicorp/levant/template" ) func TestDeploy_checkCanaryAutoPromote(t *testing.T) { fVars := make(map[string]interface{}) depCommand := &DeployCommand{} canaryPromote := 30 cases := []struct { File string CanaryPromote int Output error }{ { File: "test-fixtures/job_canary.nomad", CanaryPromote: canaryPromote, Output: nil, }, { File: "test-fixtures/group_canary.nomad", CanaryPromote: canaryPromote, Output: nil, }, } for i, c := range cases { job, err := template.RenderJob(c.File, []string{}, "", &fVars) if err != nil { t.Fatalf("case %d failed: %v", i, err) } out := depCommand.checkCanaryAutoPromote(job, c.CanaryPromote) if out != c.Output { t.Fatalf("case %d: got \"%v\"; want %v", i, out, c.Output) } } } func TestDeploy_checkForceBatch(t *testing.T) { fVars := make(map[string]interface{}) depCommand := &DeployCommand{} forceBatch := true cases := []struct { File string ForceBatch bool Output error }{ { File: "test-fixtures/periodic_batch.nomad", ForceBatch: forceBatch, Output: nil, }, } for i, c := range cases { job, err := template.RenderJob(c.File, []string{}, "", &fVars) if err != nil { t.Fatalf("case %d failed: %v", i, err) } out := depCommand.checkForceBatch(job, c.ForceBatch) if out != c.Output { t.Fatalf("case %d: got \"%v\"; want %v", i, out, c.Output) } } } ================================================ FILE: command/dispatch.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package command import ( "fmt" "io" "os" "strings" "github.com/hashicorp/levant/levant" "github.com/hashicorp/levant/logging" flaghelper "github.com/hashicorp/nomad/helper/flags" ) // DispatchCommand is the command implementation that allows users to // dispatch a Nomad job. type DispatchCommand struct { Meta } // Help provides the help information for the dispatch command. func (c *DispatchCommand) Help() string { helpText := ` Usage: levant dispatch [options] [input source] Dispatch creates an instance of a parameterized job. A data payload to the dispatched instance can be provided via stdin by using "-" or by specifying a path to a file. Metadata can be supplied by using the meta flag one or more times. General Options: -address= The Nomad HTTP API address including port which Levant will use to make calls. -log-level= Specify the verbosity level of Levant's logs. Valid values include DEBUG, INFO, and WARN, in decreasing order of verbosity. The default is INFO. -log-format= Specify the format of Levant's logs. Valid values are HUMAN or JSON. The default is HUMAN. Dispatch Options: -meta = Meta takes a key/value pair separated by "=". The metadata key will be merged into the job's metadata. The job may define a default value for the key which is overridden when dispatching. The flag can be provided more than once to inject multiple metadata key/value pairs. Arbitrary keys are not allowed. The parameterized job must allow the key to be merged. ` return strings.TrimSpace(helpText) } // Synopsis is provides a brief summary of the dispatch command. func (c *DispatchCommand) Synopsis() string { return "Dispatch an instance of a parameterized job" } // Run triggers a run of the Levant dispatch functions. func (c *DispatchCommand) Run(args []string) int { var meta []string var addr, logLevel, logFormat string flags := c.Meta.FlagSet("dispatch", FlagSetVars) flags.Usage = func() { c.UI.Output(c.Help()) } flags.Var((*flaghelper.StringFlag)(&meta), "meta", "") flags.StringVar(&addr, "address", "", "") flags.StringVar(&logLevel, "log-level", "INFO", "") flags.StringVar(&logFormat, "log-format", "human", "") if err := flags.Parse(args); err != nil { return 1 } args = flags.Args() if l := len(args); l < 1 || l > 2 { c.UI.Error(c.Help()) return 1 } err := logging.SetupLogger(logLevel, logFormat) if err != nil { c.UI.Error(fmt.Sprintf("Error setting up logging: %v", err)) } job := args[0] var payload []byte var readErr error if len(args) == 2 { switch args[1] { case "-": payload, readErr = io.ReadAll(os.Stdin) default: payload, readErr = os.ReadFile(args[1]) } if readErr != nil { c.UI.Error(fmt.Sprintf("Error reading input data: %v", readErr)) return 1 } } metaMap := make(map[string]string, len(meta)) for _, m := range meta { split := strings.SplitN(m, "=", 2) if len(split) != 2 { c.UI.Error(fmt.Sprintf("Error parsing meta value: %v", m)) return 1 } metaMap[split[0]] = split[1] } success := levant.TriggerDispatch(job, metaMap, payload, addr) if !success { return 1 } return 0 } ================================================ FILE: command/meta.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package command import ( "bufio" "flag" "io" "github.com/hashicorp/levant/helper" "github.com/mitchellh/cli" ) // FlagSetFlags is an enum to define what flags are present in the // default FlagSet returned by Meta.FlagSet type FlagSetFlags uint // Consts which helps us track meta CLI falgs. const ( FlagSetNone FlagSetFlags = 0 FlagSetBuildFilter FlagSetFlags = 1 << iota FlagSetVars ) // Meta contains the meta-options and functionality that nearly every // Levant command inherits. type Meta struct { UI cli.Ui // These are set by command-line flags flagVars map[string]interface{} } // FlagSet returns a FlagSet with the common flags that every // command implements. func (m *Meta) FlagSet(n string, fs FlagSetFlags) *flag.FlagSet { f := flag.NewFlagSet(n, flag.ContinueOnError) // FlagSetVars tells us what variables to use if fs&FlagSetVars != 0 { f.Var((*helper.Flag)(&m.flagVars), "var", "") } // Create an io.Writer that writes to our Ui properly for errors. errR, errW := io.Pipe() errScanner := bufio.NewScanner(errR) go func() { for errScanner.Scan() { m.UI.Error(errScanner.Text()) } }() f.SetOutput(errW) return f } ================================================ FILE: command/plan.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package command import ( "fmt" "strings" "github.com/hashicorp/levant/helper" "github.com/hashicorp/levant/levant" "github.com/hashicorp/levant/levant/structs" "github.com/hashicorp/levant/logging" "github.com/hashicorp/levant/template" ) // PlanCommand is the command implementation that allows users to plan a // Nomad job based on passed templates and variables. type PlanCommand struct { Meta } // Help provides the help information for the plan command. func (c *PlanCommand) Help() string { helpText := ` Usage: levant plan [options] [TEMPLATE] Perform a Nomad plan based on input templates and variable files. The plan command supports passing variables individually on the command line. Multiple commands can be passed in the format of -var 'key=value'. Variables passed via the command line take precedence over the same variable declared within a passed variable file. Arguments: TEMPLATE nomad job template If no argument is given we look for a single *.nomad file General Options: -address= The Nomad HTTP API address including port which Levant will use to make calls. -allow-stale Allow stale consistency mode for requests into nomad. -consul-address= The Consul host and port to use when making Consul KeyValue lookups for template rendering. -force-count Use the taskgroup count from the Nomad jobfile instead of the count that is currently set in a running job. -ignore-no-changes By default if no changes are detected when running a plan Levant will exit with a status 1 to indicate there are no changes. This behaviour can be changed using this flag so that Levant will exit cleanly ensuring CD pipelines don't fail when no changes are detected. -log-level= Specify the verbosity level of Levant's logs. Valid values include DEBUG, INFO, and WARN, in decreasing order of verbosity. The default is INFO. -log-format= Specify the format of Levant's logs. Valid values are HUMAN or JSON. The default is HUMAN. -var-file= Path to a file containing user variables used when rendering the job template. You can repeat this flag multiple times to supply multiple var-files. Defaults to levant.(json|yaml|yml|tf). [default: levant.(json|yaml|yml|tf)] ` return strings.TrimSpace(helpText) } // Synopsis is provides a brief summary of the plan command. func (c *PlanCommand) Synopsis() string { return "Render and perform a Nomad job plan from a template" } // Run triggers a run of the Levant template and plan functions. func (c *PlanCommand) Run(args []string) int { var err error var level, format string config := &levant.PlanConfig{ Client: &structs.ClientConfig{}, Plan: &structs.PlanConfig{}, Template: &structs.TemplateConfig{}, } flags := c.Meta.FlagSet("plan", FlagSetVars) flags.Usage = func() { c.UI.Output(c.Help()) } flags.StringVar(&config.Client.Addr, "address", "", "") flags.BoolVar(&config.Client.AllowStale, "allow-stale", false, "") flags.StringVar(&config.Client.ConsulAddr, "consul-address", "", "") flags.BoolVar(&config.Plan.IgnoreNoChanges, "ignore-no-changes", false, "") flags.StringVar(&level, "log-level", "INFO", "") flags.StringVar(&format, "log-format", "HUMAN", "") flags.Var((*helper.FlagStringSlice)(&config.Template.VariableFiles), "var-file", "") if err = flags.Parse(args); err != nil { return 1 } args = flags.Args() if err = logging.SetupLogger(level, format); err != nil { c.UI.Error(err.Error()) return 1 } if len(args) == 1 { config.Template.TemplateFile = args[0] } else if len(args) == 0 { if config.Template.TemplateFile = helper.GetDefaultTmplFile(); config.Template.TemplateFile == "" { c.UI.Error(c.Help()) c.UI.Error("\nERROR: Template arg missing and no default template found") return 1 } } else { c.UI.Error(c.Help()) return 1 } config.Template.Job, err = template.RenderJob(config.Template.TemplateFile, config.Template.VariableFiles, config.Client.ConsulAddr, &c.Meta.flagVars) if err != nil { c.UI.Error(fmt.Sprintf("[ERROR] levant/command: %v", err)) return 1 } success, changes := levant.TriggerPlan(config) if !success { return 1 } else if !changes && config.Plan.IgnoreNoChanges { return 0 } return 0 } ================================================ FILE: command/render.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package command import ( "bytes" "fmt" "os" "strings" "github.com/hashicorp/levant/helper" "github.com/hashicorp/levant/logging" "github.com/hashicorp/levant/template" ) // RenderCommand is the command implementation that allows users to render a // Nomad job template based on passed templates and variables. type RenderCommand struct { Meta } // Help provides the help information for the template command. func (c *RenderCommand) Help() string { helpText := ` Usage: levant render [options] [TEMPLATE] Render a Nomad job template, useful for debugging. Like deploy, the render command also supports passing variables individually on the command line. Multiple vars can be passed in the format of -var 'key=value'. Variables passed via the command line take precedence over the same variable declared within a passed variable file. Arguments: TEMPLATE nomad job template If no argument is given we look for a single *.nomad file General Options: -consul-address= The Consul host and port to use when making Consul KeyValue lookups for template rendering. -log-level= Specify the verbosity level of Levant's logs. Valid values include DEBUG, INFO, and WARN, in decreasing order of verbosity. The default is INFO. -log-format= Specify the format of Levant's logs. Valid values are HUMAN or JSON. The default is HUMAN. -out= Specify the path to write the rendered template out to, if a file exists at the specified path it will be truncated before rendering. The template will be rendered to stdout if this is not set. -var-file= The variables file to render the template with. You can repeat this flag multiple times to supply multiple var-files. [default: levant.(json|yaml|yml|tf)] ` return strings.TrimSpace(helpText) } // Synopsis is provides a brief summary of the template command. func (c *RenderCommand) Synopsis() string { return "Render a Nomad job from a template" } // Run triggers a run of the Levant template functions. func (c *RenderCommand) Run(args []string) int { var addr, outPath, templateFile string var variables []string var err error var tpl *bytes.Buffer var level, format string flags := c.Meta.FlagSet("render", FlagSetVars) flags.Usage = func() { c.UI.Output(c.Help()) } flags.StringVar(&addr, "consul-address", "", "") flags.StringVar(&level, "log-level", "INFO", "") flags.StringVar(&format, "log-format", "HUMAN", "") flags.Var((*helper.FlagStringSlice)(&variables), "var-file", "") flags.StringVar(&outPath, "out", "", "") if err = flags.Parse(args); err != nil { return 1 } args = flags.Args() if err = logging.SetupLogger(level, format); err != nil { c.UI.Error(err.Error()) return 1 } if len(args) == 1 { templateFile = args[0] } else if len(args) == 0 { if templateFile = helper.GetDefaultTmplFile(); templateFile == "" { c.UI.Error(c.Help()) c.UI.Error("\nERROR: Template arg missing and no default template found") return 1 } } else { c.UI.Error(c.Help()) return 1 } tpl, err = template.RenderTemplate(templateFile, variables, addr, &c.Meta.flagVars) if err != nil { c.UI.Error(fmt.Sprintf("[ERROR] levant/command: %v", err)) return 1 } out := os.Stdout if outPath != "" { out, err = os.Create(outPath) if err != nil { c.UI.Error(fmt.Sprintf("[ERROR] levant/command: %v", err)) return 1 } } _, err = tpl.WriteTo(out) if err != nil { c.UI.Error(fmt.Sprintf("[ERROR] levant/command: %v", err)) return 1 } return 0 } ================================================ FILE: command/scale_in.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package command import ( "strings" "github.com/hashicorp/levant/levant/structs" "github.com/hashicorp/levant/logging" "github.com/hashicorp/levant/scale" ) // ScaleInCommand is the command implementation that allows users to scale a // Nomad job out. type ScaleInCommand struct { Meta } // Help provides the help information for the scale-in command. func (c *ScaleInCommand) Help() string { helpText := ` Usage: levant scale-in [options] Scale a Nomad job and optional task group out. General Options: -address= The Nomad HTTP API address including port which Levant will use to make calls. -allow-stale Allow stale consistency mode for requests into nomad. -log-level= Specify the verbosity level of Levant's logs. Valid values include DEBUG, INFO, and WARN, in decreasing order of verbosity. The default is INFO. -log-format= Specify the format of Levant's logs. Valid values are HUMAN or JSON. The default is HUMAN. Scale In Options: -count= The count by which the job and task groups should be scaled in by. Only one of count or percent can be passed. -percent= A percentage value by which the job and task groups should be scaled in by. Counts will be rounded up, to ensure required capacity is met. Only one of count or percent can be passed. -task-group= The name of the task group you wish to target for scaling. If this is not specified, all task groups within the job will be scaled. ` return strings.TrimSpace(helpText) } // Synopsis is provides a brief summary of the scale-in command. func (c *ScaleInCommand) Synopsis() string { return "Scale in a Nomad job" } // Run triggers a run of the Levant scale-in functions. func (c *ScaleInCommand) Run(args []string) int { var err error var logL, logF string config := &scale.Config{ Client: &structs.ClientConfig{}, Scale: &structs.ScaleConfig{ Direction: structs.ScalingDirectionIn, }, } flags := c.Meta.FlagSet("scale-in", FlagSetVars) flags.Usage = func() { c.UI.Output(c.Help()) } flags.StringVar(&config.Client.Addr, "address", "", "") flags.BoolVar(&config.Client.AllowStale, "allow-stale", false, "") flags.StringVar(&logL, "log-level", "INFO", "") flags.StringVar(&logF, "log-format", "HUMAN", "") flags.IntVar(&config.Scale.Count, "count", 0, "") flags.IntVar(&config.Scale.Percent, "percent", 0, "") flags.StringVar(&config.Scale.TaskGroup, "task-group", "", "") if err = flags.Parse(args); err != nil { return 1 } args = flags.Args() if len(args) != 1 { c.UI.Error("This command takes one argument: ") return 1 } config.Scale.JobID = args[0] if config.Scale.Count == 0 && config.Scale.Percent == 0 || config.Scale.Count > 0 && config.Scale.Percent > 0 { c.UI.Error("You must set either -count or -percent flag to scale-in") return 1 } if config.Scale.Count > 0 { config.Scale.DirectionType = structs.ScalingDirectionTypeCount } if config.Scale.Percent > 0 { config.Scale.DirectionType = structs.ScalingDirectionTypePercent } if err = logging.SetupLogger(logL, logF); err != nil { c.UI.Error(err.Error()) return 1 } success := scale.TriggerScalingEvent(config) if !success { return 1 } return 0 } ================================================ FILE: command/scale_out.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package command import ( "strings" "github.com/hashicorp/levant/levant/structs" "github.com/hashicorp/levant/logging" "github.com/hashicorp/levant/scale" ) // ScaleOutCommand is the command implementation that allows users to scale a // Nomad job out. type ScaleOutCommand struct { Meta } // Help provides the help information for the scale-out command. func (c *ScaleOutCommand) Help() string { helpText := ` Usage: levant scale-out [options] Scale a Nomad job and optional task group out. General Options: -address= The Nomad HTTP API address including port which Levant will use to make calls. -allow-stale Allow stale consistency mode for requests into nomad. -log-level= Specify the verbosity level of Levant's logs. Valid values include DEBUG, INFO, and WARN, in decreasing order of verbosity. The default is INFO. -log-format= Specify the format of Levant's logs. Valid values are HUMAN or JSON. The default is HUMAN. Scale Out Options: -count= The count by which the job and task groups should be scaled out by. Only one of count or percent can be passed. -percent= A percentage value by which the job and task groups should be scaled out by. Counts will be rounded up, to ensure required capacity is met. Only one of count or percent can be passed. -task-group= The name of the task group you wish to target for scaling. Is this is not specified all task groups within the job will be scaled. ` return strings.TrimSpace(helpText) } // Synopsis is provides a brief summary of the scale-out command. func (c *ScaleOutCommand) Synopsis() string { return "Scale out a Nomad job" } // Run triggers a run of the Levant scale-out functions. func (c *ScaleOutCommand) Run(args []string) int { var err error var logL, logF string config := &scale.Config{ Client: &structs.ClientConfig{}, Scale: &structs.ScaleConfig{ Direction: structs.ScalingDirectionOut, }, } flags := c.Meta.FlagSet("scale-out", FlagSetVars) flags.Usage = func() { c.UI.Output(c.Help()) } flags.StringVar(&config.Client.Addr, "address", "", "") flags.BoolVar(&config.Client.AllowStale, "allow-stale", false, "") flags.StringVar(&logL, "log-level", "INFO", "") flags.StringVar(&logF, "log-format", "HUMAN", "") flags.IntVar(&config.Scale.Count, "count", 0, "") flags.IntVar(&config.Scale.Percent, "percent", 0, "") flags.StringVar(&config.Scale.TaskGroup, "task-group", "", "") if err = flags.Parse(args); err != nil { return 1 } args = flags.Args() if len(args) != 1 { c.UI.Error("This command takes one argument: ") return 1 } config.Scale.JobID = args[0] if config.Scale.Count == 0 && config.Scale.Percent == 0 || config.Scale.Count > 0 && config.Scale.Percent > 0 { c.UI.Error("You must set either -count or -percent flag to scale-out") return 1 } if config.Scale.Count > 0 { config.Scale.DirectionType = structs.ScalingDirectionTypeCount } if config.Scale.Percent > 0 { config.Scale.DirectionType = structs.ScalingDirectionTypePercent } if err = logging.SetupLogger(logL, logF); err != nil { c.UI.Error(err.Error()) return 1 } success := scale.TriggerScalingEvent(config) if !success { return 1 } return 0 } ================================================ FILE: command/test-fixtures/group_canary.nomad ================================================ # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: MPL-2.0 job "example" { datacenters = ["dc1"] type = "service" group "cache" { update { max_parallel = 1 min_healthy_time = "10s" healthy_deadline = "1m" auto_revert = true canary = 1 } count = 1 restart { attempts = 10 interval = "5m" delay = "25s" mode = "delay" } ephemeral_disk { size = 300 } task "redis" { artifact { source = "google.com" } driver = "docker" config { image = "redis:3.2" port_map { db = 6379 } } resources { cpu = 500 memory = 256 network { mbits = 10 port "db" {} } } service { name = "global-redis-check" tags = ["global", "cache"] port = "db" check { name = "alive" type = "tcp" interval = "10s" timeout = "2s" } } } } } ================================================ FILE: command/test-fixtures/job_canary.nomad ================================================ # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: MPL-2.0 job "example" { datacenters = ["dc1"] type = "service" update { max_parallel = 1 min_healthy_time = "10s" healthy_deadline = "1m" auto_revert = true canary = 1 } group "cache" { count = 1 restart { attempts = 10 interval = "5m" delay = "25s" mode = "delay" } ephemeral_disk { size = 300 } task "redis" { artifact { source = "google.com" } driver = "docker" config { image = "redis:3.2" port_map { db = 6379 } } resources { cpu = 500 memory = 256 network { mbits = 10 port "db" {} } } service { name = "global-redis-check" tags = ["global", "cache"] port = "db" check { name = "alive" type = "tcp" interval = "10s" timeout = "2s" } } } } } ================================================ FILE: command/test-fixtures/periodic_batch.nomad ================================================ # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: MPL-2.0 job "periodic_batch_test" { datacenters = ["dc1"] region = "global" type = "batch" priority = 75 periodic { cron = "* 1 * * * *" prohibit_overlap = true } group "periodic_batch" { task "periodic_batch" { driver = "docker" config { image = "cogniteev/echo" } resources { cpu = 100 memory = 128 network { mbits = 1 } } } } } ================================================ FILE: command/version.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package command import ( "github.com/mitchellh/cli" ) var _ cli.Command = &VersionCommand{} // VersionCommand is a Command implementation that prints the version. type VersionCommand struct { Version string UI cli.Ui } // Help provides the help information for the version command. func (c *VersionCommand) Help() string { return "" } // Synopsis is provides a brief summary of the version command. func (c *VersionCommand) Synopsis() string { return "Prints the Levant version" } // Run executes the version command. func (c *VersionCommand) Run(_ []string) int { c.UI.Info(c.Version) return 0 } ================================================ FILE: commands.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package main import ( "fmt" "os" "github.com/hashicorp/levant/command" "github.com/hashicorp/levant/version" "github.com/mitchellh/cli" ) // Commands returns the mapping of CLI commands for Levant. The meta parameter // lets you set meta options for all commands. func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory { if metaPtr == nil { metaPtr = new(command.Meta) } meta := *metaPtr if meta.UI == nil { meta.UI = &cli.BasicUi{ Reader: os.Stdin, Writer: os.Stdout, ErrorWriter: os.Stderr, } } return map[string]cli.CommandFactory{ "deploy": func() (cli.Command, error) { return &command.DeployCommand{ Meta: meta, }, nil }, "dispatch": func() (cli.Command, error) { return &command.DispatchCommand{ Meta: meta, }, nil }, "plan": func() (cli.Command, error) { return &command.PlanCommand{ Meta: meta, }, nil }, "render": func() (cli.Command, error) { return &command.RenderCommand{ Meta: meta, }, nil }, "scale-in": func() (cli.Command, error) { return &command.ScaleInCommand{ Meta: meta, }, nil }, "scale-out": func() (cli.Command, error) { return &command.ScaleOutCommand{ Meta: meta, }, nil }, "version": func() (cli.Command, error) { return &command.VersionCommand{ Version: fmt.Sprintf("Levant %s", version.GetHumanVersion()), UI: meta.UI, }, nil }, } } ================================================ FILE: docs/README.md ================================================ # Levant Documentation Welcome to the Levant documentation. The `docs` directory aims to host detailed and thorough documentation about Levant including design rational. Levant is an open source templating and deployment tool for [HashiCorp Nomad](https://www.nomadproject.io/) jobs that provides realtime feedback and detailed failure messages upon deployment issues. ## Documentation Pages * [Commands](./commands.md) - detail about the Levant CLI and flags associated with each command. * [Templates](./templates.md) - detail about Levant's templating engine and functions. * [Clients](./clients.md) - information on Nomad and Consul client configuration options. ## Contributing If there is something missing or wrong, contributions to the Levant docs are very welcome and greatly appreciated. ================================================ FILE: docs/clients.md ================================================ ## Clients Levant uses Nomad and Consul clients in order to perform its work. Currently only the HTTP address client parameter can be configured for each client via CLI flags; a choice made to keep the number of flags low. In order to further configure the clients you can use environment variables as detailed below. ### Nomad Client The project uses the Nomad [Default API Client](https://github.com/hashicorp/nomad/blob/master/api/api.go#L201) which means the following Nomad client parameters used by Levant are configurable via environment variables: * **NOMAD_ADDR** - The address of the Nomad server. * **NOMAD_REGION** - The region of the Nomad servers to forward commands to. * **NOMAD_NAMESPACE** - The target namespace for queries and actions bound to a namespace. * **NOMAD_CACERT** - Path to a PEM encoded CA cert file to use to verify the Nomad server SSL certificate. * **NOMAD_CAPATH** - Path to a directory of PEM encoded CA cert files to verify the Nomad server SSL certificate. * **NOMAD_CLIENT_CERT** - Path to a PEM encoded client certificate for TLS authentication to the Nomad server. * **NOMAD_CLIENT_KEY** - Path to an unencrypted PEM encoded private key matching the client certificate from `NOMAD_CLIENT_CERT`. * **NOMAD_SKIP_VERIFY** - Do not verify TLS certificate. * **NOMAD_TOKEN** - The SecretID of an ACL token to use to authenticate API requests with. ### Consul Client The project also uses the Consul [Default API Client](https://github.com/hashicorp/consul/blob/master/api/api.go#L282) which means the following Consul client parameters used by Levant are configurable via environment variables: * **CONSUL_CACERT** - Path to a CA file to use for TLS when communicating with Consul. * **CONSUL_CAPATH** - Path to a directory of CA certificates to use for TLS when communicating with Consul. * **CONSUL_CLIENT_CERT** - Path to a client cert file to use for TLS when 'verify_incoming' is enabled. * **CONSUL_CLIENT_KEY** - Path to a client key file to use for TLS when 'verify_incoming' is enabled. * **CONSUL_HTTP_ADDR** - The `address` and port of the Consul HTTP agent. The value can be an IP address or DNS address, but it must also include the port. * **CONSUL_TLS_SERVER_NAME** - The server name to use as the SNI host when connecting via TLS. * **CONSUL_HTTP_TOKEN** - ACL token to use in the request. If unspecified, the query will default to the token of the Consul agent at the HTTP address. ================================================ FILE: docs/commands.md ================================================ ## Commands Levant supports a number of command line arguments which provide control over the Levant binary. Each command supports the `--help` flag to provide usage assistance. ### Command: `deploy` `deploy` is the main entry point into Levant for deploying a Nomad job and supports the following flags which should then be proceeded by the Nomad job template you whish to deploy. Levant also supports autoloading files by which Levant will look in the current working directory for a `levant.[yaml,yml,tf]` file and a single `*.nomad` file to use for the command actions. * **-address** (string: "http://localhost:4646") The HTTP API endpoint for Nomad where all calls will be made. * **-allow-stale** (bool: false) Allow stale consistency mode for requests into nomad. * **-canary-auto-promote** (int: 0) The time period in seconds that Levant should wait for before attempting to promote a canary deployment. * **-consul-address** (string: "localhost:8500") The Consul host and port to use when making Consul KeyValue lookups for template rendering. * **-force** (bool: false) Execute deployment even though there were no changes. * **-force-batch** (bool: false) Forces a new instance of the periodic job. A new instance will be created even if it violates the job's prohibit_overlap settings. * **-force-count** (bool: false) Use the taskgroup count from the Nomad job file instead of the count that is obtained from the running job count. * **-ignore-no-changes** (bool: false) By default if no changes are detected when running a deployment Levant will exit with a status 1 to indicate a deployment didn't happen. This behaviour can be changed using this flag so that Levant will exit cleanly ensuring CD pipelines don't fail when no changes are detected * **-log-level** (string: "INFO") The level at which Levant will log to. Valid values are DEBUG, INFO, WARN, ERROR and FATAL. * **-log-format** (string: "HUMAN") Specify the format of Levant's logs. Valid values are HUMAN or JSON * **-var-file** (string: "") The variables file to render the template with. This flag can be specified multiple times to supply multiple variables files. The `deploy` command also supports passing variables individually on the command line. Multiple commands can be passed in the format of `-var 'key=value'`. Variables passed via the command line take precedence over the same variable declared within a passed variable file. Full example: ``` levant deploy -log-level=debug -address=nomad.devoops -var-file=var.yaml -var 'var=test' example.nomad ``` ### Dispatch: `dispatch` `dispatch` allows you to dispatch an instance of a Nomad parameterized job and utilise Levant's advanced job checking features to ensure the job reaches the correct running state. * **-address** (string: "http://localhost:4646") The HTTP API endpoint for Nomad where all calls will be made. * **-log-level** (string: "INFO") The level at which Levant will log to. Valid values are DEBUG, INFO, WARN, ERROR and FATAL. * **-log-format** (string: "HUMAN") Specify the format of Levant's logs. Valid values are HUMAN or JSON * **-meta** (string: "key=value") The metadata key will be merged into the job's metadata. The job may define a default value for the key which is overridden when dispatching. The flag can be provided more than once to inject multiple metadata key/value pairs. Arbitrary keys are not allowed. The parameterized job must allow the key to be merged. The command also supports the ability to send data payload to the dispatched instance. This can be provided via stdin by using "-" for the input source or by specifying a path to a file. Full example: ``` levant dispatch -log-level=debug -address=nomad.devoops -meta key=value dispatch_job payload_item ``` ### Plan: `plan` `plan` allows you to perform a Nomad plan of a rendered template job. This is useful for seeing the expected changes before larger deploys. * **-address** (string: "http://localhost:4646") The HTTP API endpoint for Nomad where all calls will be made. * **-allow-stale** (bool: false) Allow stale consistency mode for requests into nomad. * **-consul-address** (string: "localhost:8500") The Consul host and port to use when making Consul KeyValue lookups for template rendering. * **-force-count** (bool: false) Use the taskgroup count from the Nomad job file instead of the count that is obtained from the running job count. * **-ignore-no-changes** (bool: false) By default if no changes are detected when running a deployment Levant will exit with a status 1 to indicate a deployment didn't happen. This behaviour can be changed using this flag so that Levant will exit cleanly ensuring CD pipelines don't fail when no changes are detected * **-log-level** (string: "INFO") The level at which Levant will log to. Valid values are DEBUG, INFO, WARN, ERROR and FATAL. * **-log-format** (string: "HUMAN") Specify the format of Levant's logs. Valid values are HUMAN or JSON * **-var-file** (string: "") The variables file to render the template with. This flag can be specified multiple times to supply multiple variables files. The `plan` command also supports passing variables individually on the command line. Multiple commands can be passed in the format of `-var 'key=value'`. Variables passed via the command line take precedence over the same variable declared within a passed variable file. Full example: ``` levant plan -log-level=debug -address=nomad.devoops -var-file=var.yaml -var 'var=test' example.nomad ``` ### Command: `render` `render` allows rendering of a Nomad job template without deploying, useful when testing or debugging. Levant also supports autoloading files by which Levant will look in the current working directory for a `levant.[yaml,yml,tf]` file and a single `*.nomad` file to use for the command actions. * **-consul-address** (string: "localhost:8500") The Consul host and port to use when making Consul KeyValue lookups for template rendering. * **-log-level** (string: "DEBUG") The level at which Levant will log to. Valid values are DEBUG, INFO, WARN, ERROR and FATAL. * **-log-format** (string: "JSON") Specify the format of Levant's logs. Valid values are HUMAN or JSON * **-var-file** (string: "") The variables file to render the template with. This flag can be specified multiple times to supply multiple variables files. * **-out** (string: "") The path to write the rendered template to. The template will be rendered to stdout if this is not set. Like `deploy`, the `render` command also supports passing variables individually on the command line. Multiple vars can be passed in the format of `-var 'key=value'`. Variables passed via the command line take precedence over the same variable declared within a passed variable file. Full example: ``` levant render -var-file=var.yaml -var 'var=test' example.nomad ``` ### Command: `scale-in` The `scale-in` command allows the operator to scale a Nomad job and optional task-group within that job in/down in number. This can be helpful particulary in development and testing of new Nomad jobs or resizing. * **-address** (string: "http://localhost:4646") The HTTP API endpoint for Nomad where all calls will be made. * **-count** (int: 0) The count by which the job and task groups should be scaled in by. Only one of count or percent can be passed. * **-log-level** (string: "INFO") The level at which Levant will log to. Valid values are DEBUG, INFO, WARN, ERROR and FATAL. * **-log-format** (string: "HUMAN") Specify the format of Levant's logs. Valid values are HUMAN or JSON * **-percent** (int: 0) A percentage value by which the job and task groups should be scaled in by. Counts will be rounded up, to ensure required capacity is met. Only one of count or percent can be passed. * **-task-group** (string: "") The name of the task group you wish to target for scaling. If this is not specified, all task groups within the job will be scaled. Full example: ``` levant scale-in -count 3 -task-group cache example ``` ### Command: `scale-out` The `scale-out` command allows the operator to scale a Nomad job and optional task-group within that job out/up in number. This can be helpful particulary in development and testing of new Nomad jobs or resizing. * **-address** (string: "http://localhost:4646") The HTTP API endpoint for Nomad where all calls will be made. * **-count** (int: 0) The count by which the job and task groups should be scaled out by. Only one of count or percent can be passed. * **-log-level** (string: "INFO") The level at which Levant will log to. Valid values are DEBUG, INFO, WARNING, ERROR and FATAL. * **-log-format** (string: "HUMAN") Specify the format of Levant's logs. Valid values are HUMAN or JSON * **-percent** (int: 0) A percentage value by which the job and task groups should be scaled out by. Counts will be rounded up, to ensure required capacity is met. Only one of count or percent can be passed. * **-task-group** (string: "") The name of the task group you wish to target for scaling. If this is not specified, all task groups within the job will be scaled. Full example: ``` levant scale-out -percent 30 -task-group cache example ``` ### Command: `version` The `version` command displays build information about the running binary, including the release version. ================================================ FILE: docs/templates.md ================================================ ## Templates Alongside enhanced deployments of Nomad jobs; Levant provides templating functionality allowing for greater flexibility throughout your Nomad jobs files. It also allows the same job file to be used across each environment you have, meaning your operation maturity is kept high. ### Template Substitution Levant currently supports `.json`, `.tf`, `.yaml`, and `.yml` file extensions for the declaration of template variables and uses opening and closing double squared brackets `[[ ]]` within the templated job file. This is to ensure there is no clash with existing Nomad interpolation which uses the standard `{{ }}` notation. #### JSON JSON as well as YML provide the most flexible variable file format. It allows for descriptive and well organised jobs and variables file as shown below. Example job template: ```hcl resources { cpu = [[.resources.cpu]] memory = [[.resources.memory]] network { mbits = [[.resources.network.mbits]] } } ``` Example variable file: ```json { "resources":{ "cpu":250, "memory":512, "network":{ "mbits":10 } } } ``` #### Terraform Terraform (.tf) is probably the most inflexible of the variable file formats but does provide an easy to follow, descriptive manner in which to work. It may also be advantageous to use this format if you use Terraform for infrastructure as code thus allow you to use a consistant file format. Example job template: ```hcl resources { cpu = [[.resources_cpu]] memory = [[.resources_memory]] network { mbits = [[.resources_network_mbits]] } } ``` Example variable file: ```hcl variable "resources_cpu" { description = "the CPU in MHz to allocate to the task group" type = "string" default = 250 } variable "resources_memory" { description = "the memory in MB to allocate to the task group" type = "string" default = 512 } variable "resources_network_mbits" { description = "the network bandwidth in MBits to allocate" type = "string" default = 10 } ``` #### YAML Example job template: ```hcl resources { cpu = [[.resources.cpu]] memory = [[.resources.memory]] network { mbits = [[.resources.network.mbits]] } } ``` Example variable file: ```yaml --- resources: cpu: 250 memory: 512 network: mbits: 10 ``` ### Template Functions Levant's template rendering supports a number of functions which provide flexibility when deploying jobs. As with the variable substitution, it uses opening and closing double squared brackets `[[ ]]` as not to conflict with Nomad's templating standard. Levant parses job files using the [Go Template library](https://golang.org/pkg/text/template/) which makes available the features of that library as well as the functions described below. If you require any additional functions please raise a feature request against the project. #### consulKey Query Consul for the value at the given key path and render the template with the value. In the below example the value at the Consul KV path `service/config/cpu` would be `250`. Example: ``` [[ consulKey "service/config/cpu" ]] ``` Render: ``` 250 ``` #### consulKeyExists Query Consul for the value at the given key path. If the key exists, this will return true, false otherwise. This is helpful in controlling the template flow, and adding conditional logic into particular sections of the job file. In this example we could try and control where the job is configured with a particular "alerting" setup by checking for the existance of a KV in Consul. Example: ``` {{ if consulKeyExists "service/config/alerting" }} {{ else }} {{ end }} ``` #### consulKeyOrDefault Query Consul for the value at the given key path. If the key does not exist, the default value will be used instead. In the following example we query the Consul KV path `service/config/database-addr` but there is nothing at that location. If a value did exist at the path, the rendered value would be the KV value. This can be helpful when configuring jobs which defaults which are appropriate for local testing and development. Example: ``` [[ consulKeyOrDefault "service/config/database-addr" "localhost:3306" ]] ``` Render: ``` localhost:3306 ``` #### env Returns the value of the given environment variable. Example: ``` [[ env "HOME" ]] [[ or (env "NON_EXISTENT") "foo" ]] ``` Render: ``` /bin/bash foo ``` #### fileContents Reads the entire contents of the specified file and adds it to the template. Example file contents: ``` --- yaml: - is: everywhere ``` Example job template: ``` [[ fileContents "/etc/myapp/config" ]] ``` Render: ``` --- yaml: - is: everywhere ``` #### loop Accepts varying parameters and differs its behavior based on those parameters as detailed below. If loop is given a single int input, it will loop up to, but not including the given integer from index 0: Example: ``` [[ range $i := loop 3 ]] this-is-loop[[ $i ]][[ end ]] ``` Render: ``` this-is-output0 this-is-output1 this-is-output2 ``` If given two integers, this function will begin at the first integer and loop up to but not including the second integer: Example: ``` [[ range $i := loop 3 6 ]] this-is-loop[[ $i ]][[ end ]] ``` Render: ``` this-is-output3 this-is-output4 this-is-output5 ``` #### parseBool Takes the given string and parses it as a boolean value which can be helpful in performing conditional checks. In the below example if the key has a value of "true" we could use it to alter what tags are added to the job: Example: ``` [[ if "true" | parseBool ]][[ "beta-release" ]][[ end ]] ``` Render: ``` beta-release ``` #### parseFloat Takes the given string and parses it as a base-10 float64. Example: ``` [[ "3.14159265359" | parseFloat ]] ``` Render: ``` 3.14159265359 ``` #### parseInt Takes the given string and parses it as a base-10 int64 and is typically combined with other helpers such as loop: Example: ``` [[ with $i := consulKey "service/config/conn_pool" | parseInt ]][[ range $d := loop $i ]] conn-pool-id-[[ $d ]][[ end ]][[ end ]] ``` Render: ``` conn-pool-id-0 conn-pool-id-1 conn-pool-id-2 ``` #### parseJSON Takes the given input and parses the result as JSON. This can allow you to wrap an entire job template as shown below and pull variables from Consul KV for template rendering. The below example is based on the template substitution above and expects the Consul KV to be `{"resources":{"cpu":250,"memory":512,"network":{"mbits":10}}}`: Example: ``` [[ with $data := consulKey "service/config/variables" | parseJSON ]] resources { cpu = [[.resources.cpu]] memory = [[.resources.memory]] network { mbits = [[.resources.network.mbits]] } } [[ end ]] ``` Render: ``` resources { cpu = 250 memory = 512 network { mbits = 10 } } ``` #### parseUint Takes the given string and parses it as a base-10 int64. Example: ``` [[ "100" | parseUint ]] ``` Render: ``` 100 ``` #### replace Replaces all occurrences of the search string with the replacement string. Example: ``` [[ "Batman and Robin" | replace "Robin" "Catwoman" ]] ``` Render: ``` Batman and Catwoman ``` #### timeNow Returns the current ISO_8601 standard timestamp as a string in the timezone of the machine the rendering was triggered on. Example: ``` [[ timeNow ]] ``` Render: ``` 2018-06-25T09:45:08+02:00 ``` #### timeNowUTC Returns the current ISO_8601 standard timestamp as a string in UTC. Example: ``` [[ timeNowUTC ]] ``` Render: ``` 2018-06-25T07:45:08Z ``` #### timeNowTimezone Returns the current ISO_8601 standard timestamp as a string of the timezone specified. The timezone must be specified according to the entries in the IANA Time Zone database, such as "ASIA/SEOUL". Details of the entries can be found on [wikipedia](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) or your local workstation (Mac or BSD) by searching within `/usr/share/zoneinfo`. Example: ``` [[ timeNowTimezone "ASIA/SEOUL" ]] ``` Render: ``` 2018-06-25T16:45:08+09:00 ``` #### toLower Takes the argument as a string and converts it to lowercase. Example: ``` [[ "QUEUE-NAME" | toLower ]] ``` Render: ``` queue-name ``` #### toUpper Takes the argument as a string and converts it to uppercase. Example: ``` [[ "queue-name" | toUpper ]] ``` Render: ``` QUEUE-NAME ``` #### add Returns the sum of the two passed values. Examples: ``` [[ add 5 2 ]] ``` Render: ``` 7 ``` #### subtract Returns the difference of the second value from the first. Example: ``` [[ subtract 2 5 ]] ``` Render: ``` 3 ``` #### multiply Returns the product of the two values. Example: ``` [[ multiply 4 4 ]] ``` Render: ``` 16 ``` #### divide Returns the division of the second value from the first. Example: ``` [[ divide 2 6 ]] ``` Render: ``` 3 ``` #### modulo Returns the modulo of the second value from the first. Example: ``` [[ modulo 2 5 ]] ``` Render: ``` 1 ``` #### Access Variable Globally Example config file: ```yaml my_i32: 1 my_array: - "a" - "b" - "c" my_nested: my_data1: "lorempium" my_data2: "faker" ``` Template: ``` [[ $.my_i32 ]] [[ range $c := $.my_array ]][[ $c ]]-[[ $.my_i32 ]],[[ end ]] ``` Render: ``` 1 a1,b1,c1, ``` #### Sprig Template More about Sprig here: [Sprig](https://masterminds.github.io/sprig/) #### Sprig Join String Template: ``` [[ $.my_array | sprigJoin `-` ]] ``` Render: ``` a-b-c ``` #### Define variable Template: ``` [[ with $data := $.my_nested ]] ENV_x1=[[ $data.my_data1 ]] ENV_x2=[[ $data.my_data2 ]] [[ end ]] ``` Render: ``` ENV_x1=lorempium ENV_x2=faker ``` ================================================ FILE: go.mod ================================================ module github.com/hashicorp/levant go 1.24.4 // Use the same version of go-metrics as Nomad. replace github.com/armon/go-metrics => github.com/armon/go-metrics v0.0.0-20230509193637-d9ca9af9f1f9 require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/hashicorp/consul/api v1.32.1 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/nomad v1.10.2 github.com/hashicorp/nomad/api v0.0.0-20250620152331-1030760d3f77 github.com/hashicorp/terraform v0.13.5 github.com/mattn/go-isatty v0.0.20 github.com/mitchellh/cli v1.1.5 github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.6.0 github.com/sean-/conswriter v0.0.0-20180208195008-f5ae3917a627 github.com/stretchr/testify v1.10.0 gopkg.in/yaml.v2 v2.4.0 ) require ( dario.cat/mergo v1.0.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/agext/levenshtein v1.2.2 // indirect github.com/apparentlymart/go-cidr v1.1.0 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/apparentlymart/go-versions v1.0.0 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/armon/go-radix v1.0.0 // indirect github.com/bgentry/speakeasy v0.1.0 // indirect github.com/bmatcuk/doublestar v1.1.5 // indirect github.com/fatih/color v1.18.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/cronexpr v1.1.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cty-funcs v0.0.0-20200930094925-2721b1e36840 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-metrics v0.5.4 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/hcl/v2 v2.20.2-0.20240517235513-55d9c02d147d // indirect github.com/hashicorp/hil v0.0.0-20210521165536-27a72121fd40 // indirect github.com/hashicorp/serf v0.10.2 // indirect github.com/hashicorp/terraform-svchost v0.0.0-20191011084731-65d371908596 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/posener/complete v1.2.3 // indirect github.com/sean-/pager v0.0.0-20180208200047-666be9bf53b5 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/afero v1.2.2 // indirect github.com/spf13/cast v1.7.0 // indirect github.com/zclconf/go-cty v1.16.3 // indirect github.com/zclconf/go-cty-yaml v1.1.0 // indirect golang.org/x/crypto v0.38.0 // indirect golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect golang.org/x/mod v0.25.0 // indirect golang.org/x/net v0.40.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.15.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect golang.org/x/tools v0.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Azure/azure-sdk-for-go v45.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.3/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= github.com/Azure/go-autorest/autorest/azure/cli v0.4.0/go.mod h1:JljT387FplPzBA31vUcvsetLKF3pec5bdAxjVU4kI2s= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= github.com/Azure/go-autorest/autorest/validation v0.3.0/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= github.com/ChrisTrenkamp/goxpath v0.0.0-20190607011252-c5096ec8773d/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM= github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw= github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190329064014-6e358769c32a/go.mod h1:T9M45xf79ahXVelWoOBmH0y4aC1t5kXO5BxwyakgIGA= github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190103054945-8205d1f41e70/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= github.com/aliyun/aliyun-tablestore-go-sdk v4.1.2+incompatible/go.mod h1:LDQHRZylxvcg8H7wBIDfvO5g/cy4/sz1iucBlc2l3Jw= github.com/antchfx/xpath v0.0.0-20190129040759-c8489ed3251e/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk= github.com/antchfx/xquery v0.0.0-20180515051857-ad5b8c7a47b0/go.mod h1:LzD22aAzDP8/dyiCKFp31He4m2GPjl0AFyzDtZzUu9M= github.com/apparentlymart/go-cidr v1.0.1/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0 h1:MzVXffFUye+ZcSR6opIgz9Co7WcDx6ZcY+RjfFHoA0I= github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/apparentlymart/go-userdirs v0.0.0-20200915174352-b0c018a67c13/go.mod h1:7kfpUbyCdGJ9fDRCp3fopPQi5+cKNHgTE4ZuNrO71Cw= github.com/apparentlymart/go-versions v1.0.0 h1:4A4CekGuwDUQqc+uTXCrdb9Y98JZsML2sdfNTeVjsK4= github.com/apparentlymart/go-versions v1.0.0/go.mod h1:YF5j7IQtrOAOnsGkniupEA5bfCjzd7i14yu0shZavyM= github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20230509193637-d9ca9af9f1f9 h1:51N4T44k8crLrlHy1zgBKGdYKjzjquaXw/RPbq/bH+o= github.com/armon/go-metrics v0.0.0-20230509193637-d9ca9af9f1f9/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= github.com/aws/aws-sdk-go v1.31.9/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bmatcuk/doublestar v1.1.5 h1:2bNwBOmhyFEFcoB3tGvTD5xanq+4kyOZlB8wFYbMjkk= github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.0/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/dylanmei/iso8601 v0.1.0/go.mod h1:w9KhXSgIyROl1DefbMYIE7UVSIvELTbMrCfx+QkYnoQ= github.com/dylanmei/winrmtest v0.0.0-20190225150635-99b7fe2fddf1/go.mod h1:lcy9/2gH1jn/VCLouHA6tOEwLoNVd4GW6zhuKLmHC2Y= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/gophercloud/gophercloud v0.6.1-0.20191122030953-d8ac278c1c9d/go.mod h1:ozGNgr9KYOVATV5jsgHl/ceCDXGuguqOZAzoQ/2vcNM= github.com/gophercloud/gophercloud v0.10.1-0.20200424014253-c3bfe50899e5/go.mod h1:gmC5oQqMDOMO1t1gq5DquX/yAU808e/4mzjjDA76+Ss= github.com/gophercloud/utils v0.0.0-20200423144003-7c72efc7435d/go.mod h1:ehWUbLQJPqS0Ep+CxeD559hsm9pthPXadJNKwZkp43w= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/aws-sdk-go-base v0.6.0/go.mod h1:2fRjWDv3jJBeN6mVWFHV6hFTNeFBx2gpDLQaZNxUVAY= github.com/hashicorp/consul v0.0.0-20171026175957-610f3c86a089 h1:1eDpXAxTh0iPv+1kc9/gfSI2pxRERDsTk/lNGolwHn8= github.com/hashicorp/consul v0.0.0-20171026175957-610f3c86a089/go.mod h1:mFrjN1mfidgJfYP1xrJCF+AfRhr6Eaqhb2+sfyn/OOI= github.com/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE= github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4= github.com/hashicorp/consul/sdk v0.16.2 h1:cGX/djeEe9r087ARiKVWwVWCF64J+yW0G6ftZMZYbj0= github.com/hashicorp/consul/sdk v0.16.2/go.mod h1:onxcZjYVsPx5XMveAC/OtoIsdr32fykB7INFltDoRE8= github.com/hashicorp/cronexpr v1.1.2 h1:wG/ZYIKT+RT3QkOdgYc+xsKWVRgnxJ1OJtjjy84fJ9A= github.com/hashicorp/cronexpr v1.1.2/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-azure-helpers v0.12.0/go.mod h1:Zc3v4DNeX6PDdy7NljlYpnrdac1++qNW0I4U+ofGwpg= github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-cty-funcs v0.0.0-20200930094925-2721b1e36840 h1:kgvybwEeu0SXktbB2y3uLHX9lklLo+nzUwh59A3jzQc= github.com/hashicorp/go-cty-funcs v0.0.0-20200930094925-2721b1e36840/go.mod h1:Abjk0jbRkDaNCzsRhOv2iDCofYpX1eVsjozoiK63qLA= github.com/hashicorp/go-getter v1.4.2-0.20200106182914-9813cbd4eb02/go.mod h1:7qxyCd8rBfcShwsvxgIguu4KbS3l8bUCwg2Umn7RjeY= github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v0.0.0-20180129170900-7f3cd4390caa/go.mod h1:6ij3Z20p+OhOkCSrA0gImAWoHYQRGbnlcuk6XYTiaRw= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY= github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI= github.com/hashicorp/go-msgpack v0.5.4/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-msgpack v1.1.6-0.20240304204939-8824e8ccc35f h1:/xqzTen8ftnKv3cKa87WEoOLtsDJYFU0ArjrKaPTTkc= github.com/hashicorp/go-msgpack/v2 v2.1.3 h1:cB1w4Zrk0O3jQBTcFMKqYQWRFfsSQ/TYKNyUUVyCP2c= github.com/hashicorp/go-msgpack/v2 v2.1.3/go.mod h1:SjlwKKFnwBXvxD/I1bEcfJIBbEJ+MCUn39TxymNR5ZU= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.3.0/go.mod h1:F9eH4LrE/ZsRdbwhfjs9k9HoDUwAHnYtXdgmf1AVNs0= github.com/hashicorp/go-retryablehttp v0.5.2/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= github.com/hashicorp/go-slug v0.4.1/go.mod h1:I5tq5Lv0E2xcNXNkmx7BSfzi1PsJ2cNjs3cC3LwyhK8= github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= github.com/hashicorp/go-tfe v0.8.1/go.mod h1:XAV72S4O1iP8BDaqiaPLmL2B4EE6almocnOn8E8stHc= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90= github.com/hashicorp/hcl/v2 v2.6.0/go.mod h1:bQTN5mpo+jewjJgh8jr0JUguIi7qPHUF6yIfAEN3jqY= github.com/hashicorp/hcl/v2 v2.20.2-0.20240517235513-55d9c02d147d h1:7abftkc86B+tlA/0cDy5f6C4LgWfFOCpsGg3RJZsfbw= github.com/hashicorp/hcl/v2 v2.20.2-0.20240517235513-55d9c02d147d/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590/go.mod h1:n2TSygSNwsLJ76m8qFXTSc7beTb+auJxYdqrnoqwZWE= github.com/hashicorp/hil v0.0.0-20210521165536-27a72121fd40 h1:ExwaL+hUy1ys2AWDbsbh/lxQS2EVCYxuj0LoyLTdB3Y= github.com/hashicorp/hil v0.0.0-20210521165536-27a72121fd40/go.mod h1:n2TSygSNwsLJ76m8qFXTSc7beTb+auJxYdqrnoqwZWE= github.com/hashicorp/memberlist v0.1.0/go.mod h1:ncdBp14cuox2iFOq3kDiquKU6fqsTBc3W6JvZwjxxsE= github.com/hashicorp/memberlist v0.5.3 h1:tQ1jOCypD0WvMemw/ZhhtH+PWpzcftQvgCorLu0hndk= github.com/hashicorp/memberlist v0.5.3/go.mod h1:h60o12SZn/ua/j0B6iKAZezA4eDaGsIuPO70eOaJ6WE= github.com/hashicorp/nomad v1.10.2 h1:0yKNxoCgBGWIeDOcYBzXXdLh6JBsuF7nO9td64flC/s= github.com/hashicorp/nomad v1.10.2/go.mod h1:S/FXen41m3/BPsKEXfatDxmGJdp8lMhWg9FLBbTKsz4= github.com/hashicorp/nomad/api v0.0.0-20250620152331-1030760d3f77 h1:5PhbqQJDNugTEhPbA/0uVdK21SKhgkfMmRifTICCqIw= github.com/hashicorp/nomad/api v0.0.0-20250620152331-1030760d3f77/go.mod h1:y4olHzVXiQolzyk6QD/gqJxQTnnchlTf/QtczFFKwOI= github.com/hashicorp/serf v0.0.0-20160124182025-e4ec8cc423bb/go.mod h1:h/Ru6tmZazX7WO/GDmwdpS975F019L4t5ng5IgwbNrE= github.com/hashicorp/serf v0.10.2 h1:m5IORhuNSjaxeljg5DeQVDlQyVkhRIjJDimbkCa8aAc= github.com/hashicorp/serf v0.10.2/go.mod h1:T1CmSGfSeGfnfNy/w0odXQUR1rfECGd2Qdsp84DjOiY= github.com/hashicorp/terraform v0.13.5 h1:QgPgb/pOBclUMymDji9255FlseySf9dRcUMzJws9BAQ= github.com/hashicorp/terraform v0.13.5/go.mod h1:1H1qcnppNc/bBGc7poOfnmmBeQMlF0stEN3haY3emCU= github.com/hashicorp/terraform-config-inspect v0.0.0-20191212124732-c6ae6269b9d7/go.mod h1:p+ivJws3dpqbp1iP84+npOyAmTTOLMgCzrXd3GSdn/A= github.com/hashicorp/terraform-svchost v0.0.0-20191011084731-65d371908596 h1:hjyO2JsNZUKT1ym+FAdlBEkGPevazYsmVgIMw7dVELg= github.com/hashicorp/terraform-svchost v0.0.0-20191011084731-65d371908596/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg= github.com/hashicorp/vault v0.10.4/go.mod h1:KfSyffbKxoVyspOdlaGVjIuwLobi07qD1bAbosPMpP0= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/joyent/triton-go v0.0.0-20180313100802-d8f9c0314926/go.mod h1:U+RSyWxWd04xTqnuOQxnai7XGS2PrPY2cfGoDKtMHjA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/keybase/go-crypto v0.0.0-20161004153544-93f5b35093ba/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/likexian/gokit v0.0.0-20190309162924-0a377eecf7aa/go.mod h1:QdfYv6y6qPA9pbBA2qXtoT8BMKha6UyNbxWGWl/9Jfk= github.com/likexian/gokit v0.0.0-20190418170008-ace88ad0983b/go.mod h1:KKqSnk/VVSW8kEyO2vVCXoanzEutKdlBAPohmGXkxCk= github.com/likexian/gokit v0.0.0-20190501133040-e77ea8b19cdc/go.mod h1:3kvONayqCaj+UgrRZGpgfXzHdMYCAO0KAt4/8n0L57Y= github.com/likexian/gokit v0.20.15/go.mod h1:kn+nTv3tqh6yhor9BC4Lfiu58SmH8NmQ2PmEl+uM6nU= github.com/likexian/simplejson-go v0.0.0-20190409170913-40473a74d76d/go.mod h1:Typ1BfnATYtZ/+/shXfFYLrovhFyuKvzwrdOnIDHlmg= github.com/likexian/simplejson-go v0.0.0-20190419151922-c1f9f0b4f084/go.mod h1:U4O1vIJvIKwbMZKUJ62lppfdvkCdVd2nfMimHK81eec= github.com/likexian/simplejson-go v0.0.0-20190502021454-d8787b4bfa0b/go.mod h1:3BWwtmKP9cXWwYCr5bkoVDEfLywacOv0s06OBEDpyt8= github.com/lusis/go-artifactory v0.0.0-20160115162124-7e4ce345df82/go.mod h1:y54tfGmO3NKssKveTEFFzH8C/akrSOy/iW9qEAUDV84= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= github.com/masterzen/winrm v0.0.0-20200615185753-c42b5136ff88/go.mod h1:a2HXwefeat3evJHxFXSayvRHpYEPJYtErl4uIzfaUqY= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-shellwords v1.0.4/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-linereader v0.0.0-20190213213312-1b945b3263eb/go.mod h1:OaY7UOoTkkrX3wRwjpYRKafIkkyeD0UtweSHAWWiqQM= github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/gox v1.0.1/go.mod h1:ED6BioOGXMswlXa2zxfh/xdd5QhwYliBFn9V18Ap4z4= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/panicwrap v1.0.0/go.mod h1:pKvZHwWrZowLUzftuFq7coarnxbBXU4aQh3N0BJOeeA= github.com/mitchellh/prefixedio v0.0.0-20190213213902-5733675afd51/go.mod h1:kB1naBgV9ORnkiTVeyJOI1DavaJkG4oNIq0Af6ZVKUo= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db/go.mod h1:f6Izs6JvFTdnRbziASagjZ2vmf55NSIkC/weStxCHqk= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.1/go.mod h1:6gapUrK/U1TAN7ciCoNRIdVC5sbdBTUh1DKN0g6uH7E= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/zerolog v1.6.0 h1:MIbb0f94AuCzp0f4qdwfbi20VM8JcjoLbahiBHJEqwY= github.com/rs/zerolog v1.6.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/conswriter v0.0.0-20180208195008-f5ae3917a627 h1:Tn2Iev07a4oOcAuFna8AJxDOF/M+6OkNbpEZLX30D6M= github.com/sean-/conswriter v0.0.0-20180208195008-f5ae3917a627/go.mod h1:7zjs06qF79/FKAJpBvFx3P8Ww4UTIMAe+lpNXDHziac= github.com/sean-/pager v0.0.0-20180208200047-666be9bf53b5 h1:D07EBYJLI26GmLRKNtrs47p8vs/5QqpUX3VcwsAPkEo= github.com/sean-/pager v0.0.0-20180208200047-666be9bf53b5/go.mod h1:BeybITEsBEg6qbIiqJ6/Bqeq25bCLbL7YFmpaFfJDuM= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shoenig/test v1.12.1 h1:mLHfnMv7gmhhP44WrvT+nKSxKkPDiNkIuHGdIGI9RLU= github.com/shoenig/test v1.12.1/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/svanharmelen/jsonapi v0.0.0-20180618144545-0c0828c3f16d/go.mod h1:BSTlc8jOjh0niykqEGVXOLXdi9o0r0kR8tCYiMvjFgw= github.com/tencentcloud/tencentcloud-sdk-go v3.0.82+incompatible/go.mod h1:0PfYow01SHPMhKY31xa+EFz2RStxIqj6JFAJS+IkCi4= github.com/tencentyun/cos-go-sdk-v5 v0.0.0-20190808065407-f07404cefc8c/go.mod h1:wk2XFUg6egk4tSDNZtXeKfe2G6690UVyt163PuUxBZk= github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tombuildsstuff/giovanni v0.12.0/go.mod h1:qJ5dpiYWkRsuOSXO8wHbee7+wElkLNfWVolcf59N84E= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ugorji/go v0.0.0-20180813092308-00b869d2f4a5/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.1+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= github.com/zclconf/go-cty v1.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= github.com/zclconf/go-cty v1.4.0/go.mod h1:nHzOclRkoj++EU9ZjSrZvRG0BXIWt8c7loYc0qXAFGQ= github.com/zclconf/go-cty v1.5.1/go.mod h1:nHzOclRkoj++EU9ZjSrZvRG0BXIWt8c7loYc0qXAFGQ= github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk= github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= github.com/zclconf/go-cty-yaml v1.0.2/go.mod h1:IP3Ylp0wQpYm50IHK8OZWKMu6sPJIUgKa8XhiVHura0= github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0= github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200422194213-44a606286825/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20191009170851-d66e71096ffb/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191203134012-c197fd4bf371/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/api v0.0.0-20190620084959-7cf5895f2711/go.mod h1:TBhBqb1AWbBQbW3XRusr7n7E4v2+5ZY8r8sAMnyFC5A= k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA= k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655/go.mod h1:nL6pwRT8NgfF8TT68DBI8uEePRt89cSvoXUVqbkWHq4= k8s.io/client-go v10.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= k8s.io/utils v0.0.0-20200411171748-3d5a2fe318e4/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= ================================================ FILE: helper/files.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package helper import ( "os" "path/filepath" "github.com/rs/zerolog/log" ) // GetDefaultTmplFile checks the current working directory for *.nomad files. // If only 1 is found we return the match. func GetDefaultTmplFile() (templateFile string) { if matches, _ := filepath.Glob("*.nomad"); matches != nil { if len(matches) == 1 { templateFile = matches[0] log.Debug().Msgf("helper/files: using templatefile `%v`", templateFile) return templateFile } } return "" } // GetDefaultVarFile checks the current working directory for levant.(yaml|yml|tf) files. // The first match is returned. func GetDefaultVarFile() (varFile string) { if _, err := os.Stat("levant.yaml"); !os.IsNotExist(err) { log.Debug().Msg("helper/files: using default var-file `levant.yaml`") return "levant.yaml" } if _, err := os.Stat("levant.yml"); !os.IsNotExist(err) { log.Debug().Msg("helper/files: using default var-file `levant.yml`") return "levant.yml" } if _, err := os.Stat("levant.json"); !os.IsNotExist(err) { log.Debug().Msg("helper/files: using default var-file `levant.json`") return "levant.json" } if _, err := os.Stat("levant.tf"); !os.IsNotExist(err) { log.Debug().Msg("helper/files: using default var-file `levant.tf`") return "levant.tf" } log.Debug().Msg("helper/files: no default var-file found") return "" } ================================================ FILE: helper/files_test.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package helper import ( "os" "reflect" "testing" ) func TestHelper_GetDefaultTmplFile(t *testing.T) { d1 := []byte("Levant Test Job File\n") cases := []struct { TmplFiles []string Output string }{ { []string{"example.nomad", "example1.nomad"}, "", }, { []string{"example.nomad"}, "example.nomad", }, } for _, tc := range cases { for _, f := range tc.TmplFiles { // Use write file as tmpfile adds a prefix which doesn't work with the // GetDefaultTmplFile function. err := os.WriteFile(f, d1, 0600) if err != nil { t.Fatal(err) } } actual := GetDefaultTmplFile() // Call explicit Remove as the function is dependant on the number of files // in the target directory. for _, f := range tc.TmplFiles { os.Remove(f) } if !reflect.DeepEqual(actual, tc.Output) { t.Fatalf("got: %#v, expected %#v", actual, tc.Output) } } } func TestHelper_GetDefaultVarFile(t *testing.T) { d1 := []byte("Levant Test Variable File\n") cases := []struct { VarFile string }{ {"levant.yaml"}, {"levant.yml"}, {"levant.tf"}, {""}, } for _, tc := range cases { if tc.VarFile != "" { // Use write file as tmpfile adds a prefix which doesn't work with the // GetDefaultTmplFile function. err := os.WriteFile(tc.VarFile, d1, 0600) if err != nil { t.Fatal(err) } } actual := GetDefaultVarFile() if !reflect.DeepEqual(actual, tc.VarFile) { t.Fatalf("got: %#v, expected %#v", actual, tc.VarFile) } os.Remove(tc.VarFile) } } ================================================ FILE: helper/kvflag.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package helper import ( "fmt" "strings" ) // Flag is a flag.Value implementation for parsing user variables // from the command-line in the format of '-var key=value'. type Flag map[string]interface{} func (v *Flag) String() string { return "" } // Set takes a flag variable argument and pulls the correct key and value to // create or add to a map. func (v *Flag) Set(raw string) error { split := strings.SplitN(raw, "=", 2) if len(split) != 2 { return fmt.Errorf("no '=' value in arg: %s", raw) } keyRaw, value := split[0], split[1] if *v == nil { *v = make(map[string]interface{}) } // Split the variable key based on the nested delimiter to get a list of // nested keys. keys := strings.Split(keyRaw, ".") lastKeyIdx := len(keys) - 1 // Find the nested map where this value belongs // create missing maps as we go target := *v for i := 0; i < lastKeyIdx; i++ { raw, ok := target[keys[i]] if !ok { raw = make(map[string]interface{}) target[keys[i]] = raw } var newTarget Flag if newTarget, ok = raw.(map[string]interface{}); !ok { return fmt.Errorf("simple value already exists at key %q", strings.Join(keys[:i+1], ".")) } target = newTarget } target[keys[lastKeyIdx]] = value return nil } // FlagStringSlice is a flag.Value implementation for parsing targets from the // command line, e.g. -var-file=aa -var-file=bb type FlagStringSlice []string func (v *FlagStringSlice) String() string { return "" } // Set is used to append a variable file flag argument to a list of file flag // args. func (v *FlagStringSlice) Set(raw string) error { *v = append(*v, raw) return nil } ================================================ FILE: helper/kvflag_test.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package helper import ( "reflect" "testing" "github.com/hashicorp/go-multierror" "github.com/stretchr/testify/require" ) func TestHelper_Set(t *testing.T) { cases := []struct { Label string Inputs []string Output map[string]interface{} Error bool }{ { "simple value", []string{"key=value"}, map[string]interface{}{"key": "value"}, false, }, { "nested replaces simple", []string{"key=1", "key.nested=2"}, nil, true, }, { "simple replaces nested", []string{"key.nested=2", "key=1"}, map[string]interface{}{"key": "1"}, false, }, { "nested siblings", []string{"nested.a=1", "nested.b=2"}, map[string]interface{}{"nested": map[string]interface{}{"a": "1", "b": "2"}}, false, }, { "nested singleton", []string{"nested.key=value"}, map[string]interface{}{"nested": map[string]interface{}{"key": "value"}}, false, }, { "nested with parent", []string{"root=a", "nested.key=value"}, map[string]interface{}{"root": "a", "nested": map[string]interface{}{"key": "value"}}, false, }, { "empty value", []string{"key="}, map[string]interface{}{"key": ""}, false, }, { "value contains equal sign", []string{"key=foo=bar"}, map[string]interface{}{"key": "foo=bar"}, false, }, { "missing equal sign", []string{"key"}, nil, true, }, } for _, tc := range cases { t.Run(tc.Label, func(t *testing.T) { f := new(Flag) mErr := multierror.Error{} for _, input := range tc.Inputs { err := f.Set(input) if err != nil { mErr.Errors = append(mErr.Errors, err) } } if tc.Error { require.Error(t, mErr.ErrorOrNil()) } else { actual := map[string]interface{}(*f) require.True(t, reflect.DeepEqual(actual, tc.Output)) } }) } } ================================================ FILE: helper/nomad/opts.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package nomad import "github.com/hashicorp/nomad/api" // GenerateBlockingQueryOptions generate Nomad API QueryOptions that can be // used for blocking. The namespace parameter will be set, if its non-nil. func GenerateBlockingQueryOptions(ns *string) *api.QueryOptions { q := api.QueryOptions{WaitIndex: 1} if ns != nil { q.Namespace = *ns } return &q } ================================================ FILE: helper/nomad/opts_test.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package nomad import ( "testing" "github.com/hashicorp/nomad/api" "github.com/stretchr/testify/assert" ) func Test_GenerateBlockingQueryOptions(t *testing.T) { testCases := []struct { inputNS *string expectedOutput *api.QueryOptions name string }{ { inputNS: nil, expectedOutput: &api.QueryOptions{ WaitIndex: 1, }, name: "nil input namespace", }, { inputNS: stringToPtr("non-default"), expectedOutput: &api.QueryOptions{ WaitIndex: 1, Namespace: "non-default", }, name: "non-nil input namespace", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { actualOutput := GenerateBlockingQueryOptions(tc.inputNS) assert.Equal(t, tc.expectedOutput, actualOutput, tc.name) }) } } func stringToPtr(s string) *string { return &s } ================================================ FILE: helper/variable.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package helper import ( "github.com/rs/zerolog/log" ) // VariableMerge merges the passed file variables with the flag variabes to // provide a single set of variables. The flagVars will always prevale over file // variables. func VariableMerge(fileVars, flagVars *map[string]interface{}) map[string]interface{} { out := make(map[string]interface{}) for k, v := range *flagVars { log.Info().Msgf("helper/variable: using command line variable with key %s and value %s", k, v) out[k] = v } for k, v := range *fileVars { if _, ok := out[k]; ok { log.Debug().Msgf("helper/variable: variable from file with key %s and value %s overridden by CLI var", k, v) continue } log.Info().Msgf("helper/variable: using variable with key %s and value %v from file", k, v) out[k] = v } return out } ================================================ FILE: helper/variable_test.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package helper import ( "reflect" "testing" ) func TestHelper_VariableMerge(t *testing.T) { flagVars := make(map[string]interface{}) flagVars["job_name"] = "levantExample" flagVars["datacentre"] = "dc13" fileVars := make(map[string]interface{}) fileVars["job_name"] = "levantExampleOverride" fileVars["CPU_MHz"] = 500 expected := make(map[string]interface{}) expected["job_name"] = "levantExample" expected["datacentre"] = "dc13" expected["CPU_MHz"] = 500 res := VariableMerge(&fileVars, &flagVars) if !reflect.DeepEqual(res, expected) { t.Fatalf("expected \n%#v\n\n, got \n\n%#v\n\n", expected, res) } } ================================================ FILE: levant/auto_revert.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package levant import ( "time" "github.com/hashicorp/nomad/api" nomad "github.com/hashicorp/nomad/api" "github.com/rs/zerolog/log" ) func (l *levantDeployment) autoRevert(dep *nomad.Deployment) { // Setup a loop in order to retry a race condition whereby Levant may query // the latest deployment (auto-revert dep) before it has been started. i := 0 for i := 0; i < 5; i++ { revertDep, _, err := l.nomad.Jobs().LatestDeployment(dep.JobID, &api.QueryOptions{Namespace: dep.Namespace}) if err != nil { log.Error().Msgf("levant/auto_revert: unable to query latest deployment of job %s", dep.JobID) return } // Check whether we have got the original deployment ID as a return from // Nomad, and if so, continue the loop to try again. if revertDep.ID == dep.ID { log.Debug().Msgf("levant/auto_revert: auto-revert deployment not triggered for job %s, rechecking", dep.JobID) time.Sleep(1 * time.Second) continue } log.Info().Msgf("levant/auto_revert: beginning deployment watcher for job %s", dep.JobID) success := l.deploymentWatcher(revertDep.ID) if success { log.Info().Msgf("levant/auto_revert: auto-revert of job %s was successful", dep.JobID) break } log.Error().Msgf("levant/auto_revert: auto-revert of job %s failed; POTENTIAL OUTAGE SITUATION", dep.JobID) l.checkFailedDeployment(&revertDep.ID) break } // At this point we have not been able to get the latest deploymentID that // is different from the original so we can't perform auto-revert checking. if i == 5 { log.Error().Msgf("levant/auto_revert: unable to check auto-revert of job %s", dep.JobID) } } // checkAutoRevert inspects a Nomad deployment to determine if any TashGroups // have been auto-reverted. func (l *levantDeployment) checkAutoRevert(dep *nomad.Deployment) { var revert bool // Identify whether any of the TaskGroups are enabled for auto-revert and have // therefore caused the job to enter a deployment to revert to a stable // version. for _, v := range dep.TaskGroups { if v.AutoRevert { revert = true break } } if revert { log.Info().Msgf("levant/auto_revert: job %v has entered auto-revert state; launching auto-revert checker", dep.JobID) // Run the levant autoRevert function. l.autoRevert(dep) } else { log.Info().Msgf("levant/auto_revert: job %v is not in auto-revert; POTENTIAL OUTAGE SITUATION", dep.JobID) } } ================================================ FILE: levant/deploy.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package levant import ( "fmt" "strings" "time" "github.com/hashicorp/levant/client" "github.com/hashicorp/levant/levant/structs" nomad "github.com/hashicorp/nomad/api" "github.com/pkg/errors" "github.com/rs/zerolog/log" ) const ( jobStatusRunning = "running" ) // levantDeployment is the all deployment related objects for this Levant // deployment invocation. type levantDeployment struct { nomad *nomad.Client config *DeployConfig } // DeployConfig is the set of config structs required to run a Levant deploy. type DeployConfig struct { Deploy *structs.DeployConfig Client *structs.ClientConfig Plan *structs.PlanConfig Template *structs.TemplateConfig } // newLevantDeployment sets up the Levant deployment object and Nomad client // to interact with the Nomad API. func newLevantDeployment(config *DeployConfig, nomadClient *nomad.Client) (*levantDeployment, error) { var err error dep := &levantDeployment{} dep.config = config if nomadClient == nil { dep.nomad, err = client.NewNomadClient(config.Client.Addr) if err != nil { return nil, err } } else { dep.nomad = nomadClient } // Add the JobID as a log context field. log.Logger = log.With().Str(structs.JobIDContextField, *config.Template.Job.ID).Logger() return dep, nil } // TriggerDeployment provides the main entry point into a Levant deployment and // is used to setup the clients before triggering the deployment process. func TriggerDeployment(config *DeployConfig, nomadClient *nomad.Client) bool { // Create our new deployment object. levantDep, err := newLevantDeployment(config, nomadClient) if err != nil { log.Error().Err(err).Msg("levant/deploy: unable to setup Levant deployment") return false } // Run the job validation steps and count updater. preDepVal := levantDep.preDeployValidate() if !preDepVal { log.Error().Msg("levant/deploy: pre-deployment validation process failed") return false } // Start the main deployment function. success := levantDep.deploy() if !success { log.Error().Msg("levant/deploy: job deployment failed") return false } log.Info().Msg("levant/deploy: job deployment successful") return true } func (l *levantDeployment) preDeployValidate() (success bool) { // Validate the job to check it is syntactically correct. if _, _, err := l.nomad.Jobs().Validate(l.config.Template.Job, nil); err != nil { log.Error().Err(err).Msg("levant/deploy: job validation failed") return } // If job.Type isn't set we can't continue if l.config.Template.Job.Type == nil { log.Error().Msgf("levant/deploy: Nomad job `type` is not set; should be set to `%s`, `%s` or `%s`", nomad.JobTypeBatch, nomad.JobTypeSystem, nomad.JobTypeService) return } if !l.config.Deploy.ForceCount { if err := l.dynamicGroupCountUpdater(); err != nil { return } } return true } // deploy triggers a register of the job resulting in a Nomad deployment which // is monitored to determine the eventual state. func (l *levantDeployment) deploy() (success bool) { log.Info().Msgf("levant/deploy: triggering a deployment") eval, _, err := l.nomad.Jobs().Register(l.config.Template.Job, nil) if err != nil { log.Error().Err(err).Msg("levant/deploy: unable to register job with Nomad") return } if l.config.Deploy.ForceBatch { if eval.EvalID, err = l.triggerPeriodic(l.config.Template.Job.ID); err != nil { log.Error().Err(err).Msg("levant/deploy: unable to trigger periodic instance of job") return } } // Periodic and parameterized jobs do not return an evaluation and therefore // can't perform the evaluationInspector unless we are forcing an instance of // periodic which will yield an EvalID. if !l.config.Template.Job.IsPeriodic() && !l.config.Template.Job.IsParameterized() || l.config.Template.Job.IsPeriodic() && l.config.Deploy.ForceBatch { // Trigger the evaluationInspector to identify any potential errors in the // Nomad evaluation run. As far as I can tell from testing; a single alloc // failure in an evaluation means no allocs will be placed so we exit here. err = l.evaluationInspector(&eval.EvalID) if err != nil { log.Error().Err(err).Msg("levant/deploy: something") return } } if l.isJobZeroCount() { return true } switch *l.config.Template.Job.Type { case nomad.JobTypeService: // If the service job doesn't have an update stanza, the job will not use // Nomad deployments. if l.config.Template.Job.Update == nil { log.Info().Msg("levant/deploy: job is not configured with update stanza, consider adding to use deployments") return l.jobStatusChecker(&eval.EvalID) } log.Info().Msgf("levant/deploy: beginning deployment watcher for job") // Get the deploymentID from the evaluationID so that we can watch the // deployment for end status. depID, err := l.getDeploymentID(eval.EvalID) if err != nil { log.Error().Err(err).Msgf("levant/deploy: unable to get info of evaluation %s", eval.EvalID) return } // Get the success of the deployment and return if we have success. if success = l.deploymentWatcher(depID); success { return } dep, _, err := l.nomad.Deployments().Info(depID, nil) if err != nil { log.Error().Err(err).Msgf("levant/deploy: unable to query deployment %s for auto-revert check", depID) return } // If the job is not a canary job, then run the auto-revert checker, the // current checking mechanism is slightly hacky and should be updated. // The reason for this is currently the config.Job is populate from the // rendered job and so a user could potentially not set canary meaning // the field shows a null. if l.config.Template.Job.Update.Canary == nil { l.checkAutoRevert(dep) } else if *l.config.Template.Job.Update.Canary == 0 { l.checkAutoRevert(dep) } case nomad.JobTypeBatch: return l.jobStatusChecker(&eval.EvalID) case nomad.JobTypeSystem: return l.jobStatusChecker(&eval.EvalID) default: log.Debug().Msgf("levant/deploy: Levant does not support advanced deployments of job type %s", *l.config.Template.Job.Type) success = true } return } func (l *levantDeployment) evaluationInspector(evalID *string) error { for { evalInfo, _, err := l.nomad.Evaluations().Info(*evalID, nil) if err != nil { return err } switch evalInfo.Status { case "complete", "failed", "canceled": if len(evalInfo.FailedTGAllocs) == 0 { log.Info().Msgf("levant/deploy: evaluation %s finished successfully", *evalID) return nil } for group, metrics := range evalInfo.FailedTGAllocs { // Check if any nodes have been exhausted of resources and therefor are // unable to place allocs. if metrics.NodesExhausted > 0 { var exhausted, dimension []string for e := range metrics.ClassExhausted { exhausted = append(exhausted, e) } for d := range metrics.DimensionExhausted { dimension = append(dimension, d) } log.Error().Msgf("levant/deploy: task group %s failed to place allocs, failed on %v and exhausted %v", group, exhausted, dimension) } // Check if any node classes were filtered causing alloc placement // failures. if len(metrics.ClassFiltered) > 0 { for f := range metrics.ClassFiltered { log.Error().Msgf("levant/deploy: task group %s failed to place %v allocs as class \"%s\" was filtered", group, len(metrics.ClassFiltered), f) } } // Check if any node constraints were filtered causing alloc placement // failures. if len(metrics.ConstraintFiltered) > 0 { for cf := range metrics.ConstraintFiltered { log.Error().Msgf("levant/deploy: task group %s failed to place %v allocs as constraint \"%s\" was filtered", group, len(metrics.ConstraintFiltered), cf) } } } // Do not return an error here; there could well be information from // Nomad detailing filtered nodes but the deployment will still be // successful. GH-220. return nil default: time.Sleep(1 * time.Second) continue } } } func (l *levantDeployment) deploymentWatcher(depID string) (success bool) { var canaryChan chan interface{} deploymentChan := make(chan interface{}) t := time.Now() wt := 5 * time.Second // Setup the canaryChan and launch the autoPromote go routine if autoPromote // has been enabled. if l.config.Deploy.Canary > 0 { canaryChan = make(chan interface{}) go l.canaryAutoPromote(depID, l.config.Deploy.Canary, canaryChan, deploymentChan) } q := &nomad.QueryOptions{WaitIndex: 1, AllowStale: l.config.Client.AllowStale, WaitTime: wt} for { dep, meta, err := l.nomad.Deployments().Info(depID, q) log.Debug().Msgf("levant/deploy: deployment %v running for %.2fs", depID, time.Since(t).Seconds()) // Listen for the deploymentChan closing which indicates Levant should exit // the deployment watcher. select { case <-deploymentChan: return false default: break } if err != nil { log.Error().Err(err).Msgf("levant/deploy: unable to get info of deployment %s", depID) return } if meta.LastIndex <= q.WaitIndex { continue } q.WaitIndex = meta.LastIndex cont, err := l.checkDeploymentStatus(dep, canaryChan) if err != nil { return false } if cont { continue } return true } } func (l *levantDeployment) checkDeploymentStatus(dep *nomad.Deployment, shutdownChan chan interface{}) (bool, error) { switch dep.Status { case "successful": log.Info().Msgf("levant/deploy: deployment %v has completed successfully", dep.ID) return false, nil case jobStatusRunning: return true, nil default: if shutdownChan != nil { log.Debug().Msgf("levant/deploy: deployment %v meaning canary auto promote will shutdown", dep.Status) close(shutdownChan) } log.Error().Msgf("levant/deploy: deployment %v has status %s", dep.ID, dep.Status) // Launch the failure inspector. l.checkFailedDeployment(&dep.ID) return false, fmt.Errorf("deployment failed") } } // canaryAutoPromote handles Levant's canary-auto-promote functionality. func (l *levantDeployment) canaryAutoPromote(depID string, waitTime int, shutdownChan, deploymentChan chan interface{}) { // Setup the AutoPromote timer. autoPromote := time.After(time.Duration(waitTime) * time.Second) for { select { case <-autoPromote: log.Info().Msgf("levant/deploy: auto-promote period %vs has been reached for deployment %s", waitTime, depID) // Check the deployment is healthy before promoting. if healthy := l.checkCanaryDeploymentHealth(depID); !healthy { log.Error().Msgf("levant/deploy: the canary deployment %s has unhealthy allocations, unable to promote", depID) close(deploymentChan) return } log.Info().Msgf("levant/deploy: triggering auto promote of deployment %s", depID) // Promote the deployment. _, _, err := l.nomad.Deployments().PromoteAll(depID, nil) if err != nil { log.Error().Err(err).Msgf("levant/deploy: unable to promote deployment %s", depID) close(deploymentChan) return } case <-shutdownChan: log.Info().Msg("levant/deploy: canary auto promote has been shutdown") return } } } // checkCanaryDeploymentHealth is used to check the health status of each // task-group within a canary deployment. func (l *levantDeployment) checkCanaryDeploymentHealth(depID string) (healthy bool) { var unhealthy int dep, _, err := l.nomad.Deployments().Info(depID, &nomad.QueryOptions{AllowStale: l.config.Client.AllowStale}) if err != nil { log.Error().Err(err).Msgf("levant/deploy: unable to query deployment %s for health", depID) return } // Iterate over each task in the deployment to determine its health status. If an // unhealthy task is found, increment the unhealthy counter. for taskName, taskInfo := range dep.TaskGroups { // skip any task groups which are not configured for canary deployments if taskInfo.DesiredCanaries == 0 { log.Debug().Msgf("levant/deploy: task %s has no desired canaries, skipping health checks in deployment %s", taskName, depID) continue } if taskInfo.DesiredCanaries != taskInfo.HealthyAllocs { log.Error().Msgf("levant/deploy: task %s has unhealthy allocations in deployment %s", taskName, depID) unhealthy++ } } // If zero unhealthy tasks were found, continue with the auto promotion. if unhealthy == 0 { log.Debug().Msgf("levant/deploy: deployment %s has 0 unhealthy allocations", depID) healthy = true } return } // triggerPeriodic is used to force an instance of a periodic job outside of the // planned schedule. This results in an evalID being created that can then be // checked in the same fashion as other jobs. func (l *levantDeployment) triggerPeriodic(jobID *string) (evalID string, err error) { log.Info().Msg("levant/deploy: triggering a run of periodic job") // Trigger the run if possible and just return both the evalID and the err. // There is no need to check this here as the caller does this. evalID, _, err = l.nomad.Jobs().PeriodicForce(*jobID, nil) return } // getDeploymentID finds the Nomad deploymentID associated to a Nomad // evaluationID. This is only needed as sometimes Nomad initially returns eval // info with an empty deploymentID; and a retry is required in order to get the // updated response from Nomad. func (l *levantDeployment) getDeploymentID(evalID string) (depID string, err error) { var evalInfo *nomad.Evaluation timeout := time.NewTicker(time.Second * 60) defer timeout.Stop() for { select { case <-timeout.C: err = errors.New("timeout reached on attempting to find deployment ID") return default: if evalInfo, _, err = l.nomad.Evaluations().Info(evalID, nil); err != nil { return } if evalInfo.DeploymentID != "" { return evalInfo.DeploymentID, nil } log.Debug().Msgf("levant/deploy: Nomad returned an empty deployment for evaluation %v; retrying", evalID) time.Sleep(2 * time.Second) continue } } } // dynamicGroupCountUpdater takes the templated and rendered job and updates the // group counts based on the currently deployed job; if it's running. func (l *levantDeployment) dynamicGroupCountUpdater() error { // Gather information about the current state, if any, of the job on the // Nomad cluster. rJob, _, err := l.nomad.Jobs().Info(*l.config.Template.Job.Name, &nomad.QueryOptions{Namespace: *l.config.Template.Job.Namespace}) // This is a hack due to GH-1849; we check the error string for 404, which // indicates the job is not running, not that there was an error in the API // call. if err != nil && strings.Contains(err.Error(), "404") { log.Info().Msg("levant/deploy: job is not running, using template file group counts") return nil } else if err != nil { log.Error().Err(err).Msg("levant/deploy: unable to perform job evaluation") return err } // Check that the job is actually running and not in a potentially stopped // state. if *rJob.Status != jobStatusRunning { return nil } log.Debug().Msgf("levant/deploy: running dynamic job count updater") // Iterate over the templated job and the Nomad returned job and update group count // based on matches. for _, rGroup := range rJob.TaskGroups { for _, group := range l.config.Template.Job.TaskGroups { if *rGroup.Name == *group.Name { log.Info().Msgf("levant/deploy: using dynamic count %v for group %s", *rGroup.Count, *group.Name) group.Count = rGroup.Count } } } return nil } // isJobZeroCount checks that all task groups have a count bigger than zero. func (l *levantDeployment) isJobZeroCount() bool { for _, tg := range l.config.Template.Job.TaskGroups { if tg.Count == nil { return false } else if *tg.Count > 0 { return false } } return true } ================================================ FILE: levant/dispatch.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package levant import ( "github.com/hashicorp/levant/client" "github.com/hashicorp/levant/levant/structs" nomad "github.com/hashicorp/nomad/api" "github.com/rs/zerolog/log" ) // TriggerDispatch provides the main entry point into a Levant dispatch and // is used to setup the clients before triggering the dispatch process. func TriggerDispatch(job string, metaMap map[string]string, payload []byte, address string) bool { client, err := client.NewNomadClient(address) if err != nil { log.Error().Msgf("levant/dispatch: unable to setup Levant dispatch: %v", err) return false } // TODO: Potential refactor so that dispatch does not need to use the // levantDeployment object. Requires client refactor. dep := &levantDeployment{} dep.nomad = client success := dep.dispatch(job, metaMap, payload) if !success { log.Error().Msgf("levant/dispatch: dispatch of job %v failed", job) return false } log.Info().Msgf("levant/dispatch: dispatch of job %v successful", job) return true } // dispatch triggers a new instance of a parameterized job of the job // resulting in a Nomad job which is monitored to determine the eventual // state. func (l *levantDeployment) dispatch(job string, metaMap map[string]string, payload []byte) bool { // Initiate the dispatch with the passed meta parameters. eval, _, err := l.nomad.Jobs().Dispatch(job, metaMap, payload, "", nil) if err != nil { log.Error().Msgf("levant/dispatch: %v", err) return false } log.Info().Msgf("levant/dispatch: triggering dispatch against job %s", job) // If we didn't get an EvaluationID then we cannot continue. if eval.EvalID == "" { log.Error().Msgf("levant/dispatch: dispatched job %s did not return evaluation", job) return false } // In order to correctly run the jobStatusChecker we need to correctly // assign the dispatched job ID/Name based on the invoked job. l.config = &DeployConfig{ Template: &structs.TemplateConfig{ Job: &nomad.Job{ ID: &eval.DispatchedJobID, Name: &eval.DispatchedJobID, }, }, } // Perform the evaluation inspection to ensure to check for any possible // errors in triggering the dispatch job. err = l.evaluationInspector(&eval.EvalID) if err != nil { log.Error().Msgf("levant/dispatch: %v", err) return false } return l.jobStatusChecker(&eval.EvalID) } ================================================ FILE: levant/failure_inspector.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package levant import ( "fmt" "strings" "sync" nomad "github.com/hashicorp/nomad/api" "github.com/rs/zerolog/log" ) // checkFailedDeployment helps log information about deployment failures. func (l *levantDeployment) checkFailedDeployment(depID *string) { var allocIDs []string allocs, _, err := l.nomad.Deployments().Allocations(*depID, nil) if err != nil { log.Error().Msgf("levant/failure_inspector: unable to query deployment allocations for deployment %v", depID) } // Iterate the allocations on the deployment and create a list of each allocID // we only list the ones that have tasks that are not successful for _, alloc := range allocs { for _, task := range alloc.TaskStates { // we need to test for success both for service style jobs and for batch style jobs if task.State != "started" { allocIDs = append(allocIDs, alloc.ID) // once we add the allocation we don't need to add it again break } } } // Setup a waitgroup so the function doesn't return until all allocations have // been inspected. var wg sync.WaitGroup wg.Add(+len(allocIDs)) // Inspect each allocation. for _, id := range allocIDs { log.Debug().Msgf("levant/failure_inspector: launching allocation inspector for alloc %v", id) go l.allocInspector(id, &wg) } wg.Wait() } // allocInspector inspects an allocations events to log any useful information // which may help debug deployment failures. func (l *levantDeployment) allocInspector(allocID string, wg *sync.WaitGroup) { // Inform the wait group we have finished our task upon completion. defer wg.Done() resp, _, err := l.nomad.Allocations().Info(allocID, nil) if err != nil { log.Error().Msgf("levant/failure_inspector: unable to query alloc %v: %v", allocID, err) return } // Iterate each each Task and Event to log any relevant information which may // help debug deployment failures. for _, task := range resp.TaskStates { for _, event := range task.Events { var desc string switch event.Type { case nomad.TaskFailedValidation: if event.ValidationError != "" { desc = event.ValidationError } else { desc = "validation of task failed" } case nomad.TaskSetupFailure: if event.SetupError != "" { desc = event.SetupError } else { desc = "task setup failed" } case nomad.TaskDriverFailure: if event.DriverError != "" { desc = event.DriverError } else { desc = "failed to start task" } case nomad.TaskArtifactDownloadFailed: if event.DownloadError != "" { desc = event.DownloadError } else { desc = "the task failed to download artifacts" } case nomad.TaskKilling: if event.KillReason != "" { desc = fmt.Sprintf("the task was killed: %v", event.KillReason) } else if event.KillTimeout != 0 { desc = fmt.Sprintf("sent interrupt, waiting %v before force killing", event.KillTimeout) } else { desc = "the task was sent interrupt" } case nomad.TaskKilled: if event.KillError != "" { desc = event.KillError } else { desc = "the task was successfully killed" } case nomad.TaskTerminated: var parts []string parts = append(parts, fmt.Sprintf("exit Code %d", event.ExitCode)) if event.Signal != 0 { parts = append(parts, fmt.Sprintf("signal %d", event.Signal)) } if event.Message != "" { parts = append(parts, fmt.Sprintf("exit message %q", event.Message)) } desc = strings.Join(parts, ", ") case nomad.TaskNotRestarting: if event.RestartReason != "" { desc = event.RestartReason } else { desc = "the task exceeded restart policy" } case nomad.TaskSiblingFailed: if event.FailedSibling != "" { desc = fmt.Sprintf("task's sibling %q failed", event.FailedSibling) } else { desc = "task's sibling failed" } case nomad.TaskLeaderDead: desc = "leader task in group is dead" } // If we have matched and have an updated desc then log the appropriate // information. if desc != "" { log.Error().Msgf("levant/failure_inspector: alloc %s incurred event %s because %s", allocID, strings.ToLower(event.Type), strings.TrimSpace(desc)) } else { log.Error().Msgf("levant/failure_inspector: alloc %s logged for failure; event_type: %s; message: %s", allocID, strings.ToLower(event.Type), strings.ToLower(event.DisplayMessage)) } } } } ================================================ FILE: levant/job_status_checker.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package levant import ( nomadHelper "github.com/hashicorp/levant/helper/nomad" nomad "github.com/hashicorp/nomad/api" "github.com/rs/zerolog/log" ) // TaskCoordinate is a coordinate for an allocation/task combination type TaskCoordinate struct { Alloc string TaskName string } // jobStatusChecker checks the status of a job at least reaches a status of // running. Depending on the type of job and its configuration it can go through // more checks. func (l *levantDeployment) jobStatusChecker(evalID *string) bool { log.Debug().Msgf("levant/job_status_checker: running job status checker for job") // Run the initial job status check to ensure the job reaches a state of // running. jStatus := l.simpleJobStatusChecker() // Periodic and parameterized batch jobs do not produce evaluations and so // can only go through the simplest of checks. if *evalID == "" { return jStatus } // Job registrations that produce an evaluation can be more thoroughly // checked even if they don't support Nomad deployments. if jStatus { return l.jobAllocationChecker(evalID) } return false } // simpleJobStatusChecker is used to check that jobs which do not emit initial // evaluations at least reach a job status of running. func (l *levantDeployment) simpleJobStatusChecker() bool { q := nomadHelper.GenerateBlockingQueryOptions(l.config.Template.Job.Namespace) for { job, meta, err := l.nomad.Jobs().Info(*l.config.Template.Job.Name, q) if err != nil { log.Error().Err(err).Msg("levant/job_status_checker: unable to query job information from Nomad") return false } // If the LastIndex is not greater than our stored LastChangeIndex, we don't // need to do anything. if meta.LastIndex <= q.WaitIndex { continue } // Checks the status of the job and proceed as expected depending on this. switch *job.Status { case "running": log.Info().Msgf("levant/job_status_checker: job has status %s", *job.Status) return true case "pending": log.Debug().Msgf("levant/job_status_checker: job has status %s", *job.Status) q.WaitIndex = meta.LastIndex continue case "dead": log.Error().Msgf("levant/job_status_checker: job has status %s", *job.Status) return false } } } // jobAllocationChecker is the main entry point into the allocation checker for // jobs that do not support Nomad deployments. func (l *levantDeployment) jobAllocationChecker(evalID *string) bool { q := nomadHelper.GenerateBlockingQueryOptions(l.config.Template.Job.Namespace) // Build our small internal checking struct. levantTasks := make(map[TaskCoordinate]string) for { allocs, meta, err := l.nomad.Evaluations().Allocations(*evalID, q) if err != nil { log.Error().Err(err).Msg("levant/job_status_checker: unable to query allocs of job from Nomad") return false } // If the LastIndex is not greater than our stored LastChangeIndex, we don't // need to do anything. if meta.LastIndex <= q.WaitIndex { continue } // If we get here, set the wi to the latest Index. q.WaitIndex = meta.LastIndex complete, deadTasks := allocationStatusChecker(levantTasks, allocs) // depending on how we finished up we report our status // If we have no allocations left to track then we can exit and log // information depending on the success. if complete && deadTasks == 0 { log.Info().Msg("levant/job_status_checker: all allocations in deployment of job are running") return true } else if complete && deadTasks > 0 { return false } } } // allocationStatusChecker is used to check the state of allocations within a // job deployment, an update Levants internal tracking on task status based on // this. This functionality exists as Nomad does not currently support // deployments across all job types. func allocationStatusChecker(levantTasks map[TaskCoordinate]string, allocs []*nomad.AllocationListStub) (bool, int) { complete := true deadTasks := 0 for _, alloc := range allocs { for taskName, task := range alloc.TaskStates { // if the state is one we haven't seen yet then we print a message if levantTasks[TaskCoordinate{alloc.ID, taskName}] != task.State { log.Info().Msgf("levant/job_status_checker: task %s in allocation %s now in %s state", taskName, alloc.ID, task.State) // then we record the new state levantTasks[TaskCoordinate{alloc.ID, taskName}] = task.State } // then we have some case specific actions switch levantTasks[TaskCoordinate{alloc.ID, taskName}] { // if a task is still pendign we are not yet done case "pending": complete = false // if the task is dead we record that case "dead": deadTasks++ } } } return complete, deadTasks } ================================================ FILE: levant/job_status_checker_test.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package levant import ( "testing" nomad "github.com/hashicorp/nomad/api" ) func TestJobStatusChecker_allocationStatusChecker(t *testing.T) { // Build our task status maps levantTasks1 := make(map[TaskCoordinate]string) levantTasks2 := make(map[TaskCoordinate]string) levantTasks3 := make(map[TaskCoordinate]string) // Build a small AllocationListStubs with required information. var allocs1 []*nomad.AllocationListStub taskStates1 := make(map[string]*nomad.TaskState) taskStates1["task1"] = &nomad.TaskState{State: "running"} allocs1 = append(allocs1, &nomad.AllocationListStub{ ID: "10246d87-ecd7-21ad-13b2-f0c564647d64", TaskStates: taskStates1, }) var allocs2 []*nomad.AllocationListStub taskStates2 := make(map[string]*nomad.TaskState) taskStates2["task1"] = &nomad.TaskState{State: "running"} taskStates2["task2"] = &nomad.TaskState{State: "pending"} allocs2 = append(allocs2, &nomad.AllocationListStub{ ID: "20246d87-ecd7-21ad-13b2-f0c564647d64", TaskStates: taskStates2, }) var allocs3 []*nomad.AllocationListStub taskStates3 := make(map[string]*nomad.TaskState) taskStates3["task1"] = &nomad.TaskState{State: "dead"} allocs3 = append(allocs3, &nomad.AllocationListStub{ ID: "30246d87-ecd7-21ad-13b2-f0c564647d64", TaskStates: taskStates3, }) cases := []struct { levantTasks map[TaskCoordinate]string allocs []*nomad.AllocationListStub dead int expectedDead int expectedComplete bool }{ { levantTasks1, allocs1, 0, 0, true, }, { levantTasks2, allocs2, 0, 0, false, }, { levantTasks3, allocs3, 0, 1, true, }, } for _, tc := range cases { complete, dead := allocationStatusChecker(tc.levantTasks, tc.allocs) if complete != tc.expectedComplete { t.Fatalf("expected complete to be %v but got %v", tc.expectedComplete, complete) } if dead != tc.expectedDead { t.Fatalf("expected %v dead task(s) but got %v", tc.expectedDead, dead) } } } ================================================ FILE: levant/plan.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package levant import ( "fmt" "github.com/hashicorp/levant/client" "github.com/hashicorp/levant/levant/structs" nomad "github.com/hashicorp/nomad/api" "github.com/rs/zerolog/log" ) const ( diffTypeAdded = "Added" diffTypeEdited = "Edited" diffTypeNone = "None" ) type levantPlan struct { nomad *nomad.Client config *PlanConfig } // PlanConfig is the set of config structs required to run a Levant plan. type PlanConfig struct { Client *structs.ClientConfig Plan *structs.PlanConfig Template *structs.TemplateConfig } func newPlan(config *PlanConfig) (*levantPlan, error) { var err error plan := &levantPlan{} plan.config = config plan.nomad, err = client.NewNomadClient(config.Client.Addr) if err != nil { return nil, err } return plan, nil } // TriggerPlan initiates a Levant plan run. func TriggerPlan(config *PlanConfig) (bool, bool) { lp, err := newPlan(config) if err != nil { log.Error().Err(err).Msg("levant/plan: unable to setup Levant plan") return false, false } changes, err := lp.plan() if err != nil { log.Error().Err(err).Msg("levant/plan: error when running plan") return false, changes } if !changes && lp.config.Plan.IgnoreNoChanges { log.Info().Msg("levant/plan: no changes found in job but ignore-no-changes flag set to true") } else if !changes && !lp.config.Plan.IgnoreNoChanges { log.Info().Msg("levant/plan: no changes found in job") return false, changes } return true, changes } // plan is the entry point into running the Levant plan function which logs all // changes anticipated by Nomad of the upcoming job registration. If there are // no planned changes here, return false to indicate we should stop the process. func (lp *levantPlan) plan() (bool, error) { log.Debug().Msg("levant/plan: triggering Nomad plan") // Run a plan using the rendered job. resp, _, err := lp.nomad.Jobs().Plan(lp.config.Template.Job, true, nil) if err != nil { log.Error().Err(err).Msg("levant/plan: unable to run a job plan") return false, err } switch resp.Diff.Type { // If the job is new, then don't print the entire diff but just log that it // is a new registration. case diffTypeAdded: log.Info().Msg("levant/plan: job is a new addition to the cluster") return true, nil // If there are no changes, log the message so the user can see this and // exit the deployment. case diffTypeNone: log.Info().Msg("levant/plan: no changes detected for job") return false, nil // If there are changes, run the planDiff function which is responsible for // iterating through the plan and logging all the planned changes. case diffTypeEdited: planDiff(resp.Diff) } return true, nil } func planDiff(plan *nomad.JobDiff) { // Iterate through each TaskGroup. for _, tg := range plan.TaskGroups { if tg.Type != diffTypeEdited { continue } for _, tgo := range tg.Objects { recurseObjDiff(tg.Name, "", tgo) } // Iterate through each Task. for _, t := range tg.Tasks { if t.Type != diffTypeEdited { continue } if len(t.Objects) == 0 { return } for _, o := range t.Objects { recurseObjDiff(tg.Name, t.Name, o) } } } } func recurseObjDiff(g, t string, objDiff *nomad.ObjectDiff) { // If we have reached the end of the object tree, and have an edited type // with field information then we can interate on the fields to find those // which have changed. if len(objDiff.Objects) == 0 && len(objDiff.Fields) > 0 && objDiff.Type == diffTypeEdited { for _, f := range objDiff.Fields { if f.Type != diffTypeEdited { continue } logDiffObj(g, t, objDiff.Name, f.Name, f.Old, f.New) continue } } else { // Continue to interate through the object diff objects until such time // the above is triggered. for _, o := range objDiff.Objects { recurseObjDiff(g, t, o) } } } // logDiffObj is a helper function so Levant can log the most accurate and // useful plan output messages. func logDiffObj(g, t, objName, fName, fOld, fNew string) { var lStart, l string // We will always have at least this information to log. lEnd := fmt.Sprintf("plan indicates change of %s:%s from %s to %s", objName, fName, fOld, fNew) // If we have been passed a group name, use this to start the log line. if g != "" { lStart = fmt.Sprintf("group %s ", g) } // If we have been passed a task name, append this to the group name. if t != "" { lStart = lStart + fmt.Sprintf("and task %s ", t) } // Build the final log message. if lStart != "" { l = lStart + lEnd } else { l = lEnd } log.Info().Msgf("levant/plan: %s", l) } ================================================ FILE: levant/structs/config.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package structs import nomad "github.com/hashicorp/nomad/api" const ( // JobIDContextField is the logging context feild added when interacting // with jobs. JobIDContextField = "job_id" // ScalingDirectionOut represents a scaling out event; adding to the total number. ScalingDirectionOut = "Out" // ScalingDirectionIn represents a scaling in event; removing from the total number. ScalingDirectionIn = "In" // ScalingDirectionTypeCount means the scale event will use a change by count. ScalingDirectionTypeCount = "Count" // ScalingDirectionTypePercent means the scale event will use a percentage of current change. ScalingDirectionTypePercent = "Percent" ) // DeployConfig is the main struct used to configure and run a Levant deployment on // a given target job. type DeployConfig struct { // Canary enables canary autopromote and is the value in seconds to wait // until attempting to perform autopromote. Canary int // Force is a boolean flag that can be used to force a deployment // even though levant didn't detect any changes. Force bool // ForceBatch is a boolean flag that can be used to force a run of a periodic // job upon registration. ForceBatch bool // ForceCount is a boolean flag that can be used to ignore running job counts // and force the count based on the rendered job file. ForceCount bool // EnvVault is a boolean flag that can be used to enable reading the VAULT_TOKEN // from the enviromment. EnvVault bool } // ClientConfig is the config struct which houses all the information needed to connect // to the external services and endpoints. type ClientConfig struct { // Addr is the Nomad API address to use for all calls and must include both // protocol and port. Addr string // ConsulAddr is the Consul API address to use for all calls. ConsulAddr string // AllowStale sets consistency level for nomad query // https://www.nomadproject.io/api/index.html#consistency-modes AllowStale bool } // PlanConfig contains any configuration options that are specific to running a // Nomad plan. type PlanConfig struct { // IgnoreNoChanges is used to allow operators to force Levant to exit cleanly // even if there are no changes found during the plan. IgnoreNoChanges bool } // TemplateConfig contains all the job templating configuration options including // the rendered job. type TemplateConfig struct { // Job represents the Nomad Job definition that will be deployed. Job *nomad.Job // TemplateFile is the job specification template which will be rendered // before being deployed to the cluster. TemplateFile string // VariableFiles contains the variables which will be substituted into the // templateFile before deployment. VariableFiles []string } // ScaleConfig contains all the scaling specific configuration options. type ScaleConfig struct { // Count is the count by which the operator has asked to scale the Nomad job // and optional taskgroup by. Count int // Direction is the direction in which the scaling will take place and is // populated by consts. Direction string // DirectionType is an identifier on whether the operator has specified to // scale using a count increase or percentage. DirectionType string // JobID is the Nomad job which will be interacted with for scaling. JobID string // Percent is the percentage by which the operator has asked to scale the // Nomad job and optional taskgroup by. Percent int // TaskGroup is the Nomad job taskgroup which has been selected for scaling. TaskGroup string } ================================================ FILE: logging/logging.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package logging import ( "fmt" "io" stdlog "log" "os" "strings" isatty "github.com/mattn/go-isatty" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/sean-/conswriter" ) var acceptedLogLevels = []string{"DEBUG", "INFO", "WARN", "ERROR", "FATAL"} var acceptedLogFormat = []string{"HUMAN", "JSON"} // SetupLogger sets the log level and outout format. // Accepted levels are panic, fatal, error, warn, info and debug. // Accepted formats are human or json. func SetupLogger(level, format string) (err error) { if err = setLogFormat(strings.ToUpper(format)); err != nil { return err } if err = setLogLevel(strings.ToUpper(level)); err != nil { return err } return nil } func setLogLevel(level string) error { switch level { case "DEBUG": zerolog.SetGlobalLevel(zerolog.DebugLevel) case "INFO": zerolog.SetGlobalLevel(zerolog.InfoLevel) case "WARN": zerolog.SetGlobalLevel(zerolog.WarnLevel) case "ERROR": zerolog.SetGlobalLevel(zerolog.ErrorLevel) case "FATAL": zerolog.SetGlobalLevel(zerolog.FatalLevel) default: return fmt.Errorf("unsupported log level: %q (supported levels: %s)", level, strings.Join(acceptedLogLevels, " ")) } return nil } func setLogFormat(format string) error { var logWriter io.Writer var zLog zerolog.Logger if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) { logWriter = conswriter.GetTerminal() } else { logWriter = os.Stderr } switch format { case "HUMAN": w := zerolog.ConsoleWriter{ Out: logWriter, NoColor: true, } zLog = zerolog.New(w).With().Timestamp().Logger() case "JSON": zLog = zerolog.New(logWriter).With().Timestamp().Logger() default: return fmt.Errorf("unsupported log format: %q (supported formats: %s)", format, strings.Join(acceptedLogFormat, " ")) } log.Logger = zLog stdlog.SetFlags(0) stdlog.SetOutput(zLog) return nil } ================================================ FILE: main.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package main import ( "fmt" "os" "github.com/mitchellh/cli" ) func main() { os.Exit(Run(os.Args[1:])) } // Run sets up the commands and triggers RunCustom which inacts the correct // run of Levant. func Run(args []string) int { return RunCustom(args, Commands(nil)) } // RunCustom is the main function to trigger a run of Levant. func RunCustom(args []string, commands map[string]cli.CommandFactory) int { // Get the command line args. We shortcut "--version" and "-v" to // just show the version. for _, arg := range args { if arg == "-v" || arg == "-version" || arg == "--version" { newArgs := make([]string, len(args)+1) newArgs[0] = "version" copy(newArgs[1:], args) args = newArgs break } } // Build the commands to include in the help now. commandsInclude := make([]string, 0, len(commands)) for k := range commands { switch k { default: commandsInclude = append(commandsInclude, k) } } cli := &cli.CLI{ Args: args, Commands: commands, HelpFunc: cli.FilteredHelpFunc(commandsInclude, cli.BasicHelpFunc("levant")), } exitCode, err := cli.Run() if err != nil { _, pErr := fmt.Fprintf(os.Stderr, "Error executing CLI: %s\n", err.Error()) // If we are unable to log to stderr; try just printing the error to // provide some insight. if pErr != nil { fmt.Print(pErr) } return 1 } return exitCode } ================================================ FILE: scale/scale.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package scale import ( "github.com/hashicorp/levant/client" "github.com/hashicorp/levant/levant" "github.com/hashicorp/levant/levant/structs" nomad "github.com/hashicorp/nomad/api" "github.com/rs/zerolog/log" ) // Config is the set of config structs required to run a Levant scale. type Config struct { Client *structs.ClientConfig Scale *structs.ScaleConfig } // TriggerScalingEvent provides the exported entry point into performing a job // scale based on user inputs. func TriggerScalingEvent(config *Config) bool { // Add the JobID as a log context field. log.Logger = log.With().Str(structs.JobIDContextField, config.Scale.JobID).Logger() nomadClient, err := client.NewNomadClient(config.Client.Addr) if err != nil { log.Error().Msg("levant/scale: unable to setup Levant scaling event") return false } job := updateJob(nomadClient, config) if job == nil { log.Error().Msg("levant/scale: unable to perform job count update") return false } // Setup a deployment object, as a scaling event is a deployment and should // go through the same process and code upgrades. deploymentConfig := &levant.DeployConfig{} deploymentConfig.Template = &structs.TemplateConfig{Job: job} deploymentConfig.Client = config.Client deploymentConfig.Deploy = &structs.DeployConfig{ForceCount: true} log.Info().Msg("levant/scale: job will now be deployed with updated counts") // Trigger a deployment of the updated job which results in the scaling of // the job and will go through all the deployment tracking until an end // state is reached. return levant.TriggerDeployment(deploymentConfig, nomadClient) } // updateJob gathers information on the current state of the running job and // along with the user defined input updates the in-memory job specification // to reflect the desired scaled state. func updateJob(client *nomad.Client, config *Config) *nomad.Job { job, _, err := client.Jobs().Info(config.Scale.JobID, nil) if err != nil { log.Error().Err(err).Msg("levant/scale: unable to obtain job information from Nomad") return nil } // You can't scale a job that isn't running; or at least you shouldn't in // my current opinion. if *job.Status != "running" { log.Error().Msgf("levant/scale: job is not in running state") return nil } for _, group := range job.TaskGroups { // If the user has specified a taskgroup to scale, ensure we only change // the specific of this. if config.Scale.TaskGroup != "" { if *group.Name == config.Scale.TaskGroup { log.Debug().Msgf("levant/scale: scaling action to be requested on taskgroup %s only", config.Scale.TaskGroup) updateTaskGroup(config, group) } // If no taskgroup has been specified, all found will have their // count updated. } else { log.Debug().Msg("levant/scale: scaling action requested on all taskgroups") updateTaskGroup(config, group) } } return job } // updateTaskGroup is tasked with performing the count update based on the user // configuration when a group is identified as being marked for scaling. func updateTaskGroup(config *Config, group *nomad.TaskGroup) { var c int // If a percentage scale value has been passed, we must convert this to an // int which represents the count to scale by as Nomad job submissions must // be done with group counts as desired ints. switch config.Scale.DirectionType { case structs.ScalingDirectionTypeCount: c = config.Scale.Count case structs.ScalingDirectionTypePercent: c = calculateCountBasedOnPercent(*group.Count, config.Scale.Percent) } // Depending on whether we are scaling-out or scaling-in we need to perform // the correct maths. There is a little duplication here, but that is to // provide better logging. switch config.Scale.Direction { case structs.ScalingDirectionOut: nc := *group.Count + c log.Info().Msgf("levant/scale: task group %s will scale-out from %v to %v", *group.Name, *group.Count, nc) *group.Count = nc case structs.ScalingDirectionIn: nc := *group.Count - c log.Info().Msgf("levant/scale: task group %s will scale-in from %v to %v", *group.Name, *group.Count, nc) *group.Count = nc } } // calculateCountBasedOnPercent is a small helper function to turn a percentage // based scale event into a relative count. func calculateCountBasedOnPercent(count, percent int) int { n := (float64(count) / 100) * float64(percent) return int(n + 0.5) } ================================================ FILE: scale/scale_test.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package scale import ( "testing" "github.com/hashicorp/levant/levant/structs" nomad "github.com/hashicorp/nomad/api" ) func TestScale_updateTaskGroup(t *testing.T) { sOut := structs.ScalingDirectionOut sIn := structs.ScalingDirectionIn sCount := structs.ScalingDirectionTypeCount sPercent := structs.ScalingDirectionTypePercent cases := []struct { Config *Config Group *nomad.TaskGroup EndCount int }{ { buildScalingConfig(sOut, sCount, 100), buildTaskGroup(1000), 1100, }, { buildScalingConfig(sOut, sPercent, 25), buildTaskGroup(100), 125, }, { buildScalingConfig(sIn, sCount, 900), buildTaskGroup(901), 1, }, { buildScalingConfig(sIn, sPercent, 90), buildTaskGroup(100), 10, }, } for _, tc := range cases { updateTaskGroup(tc.Config, tc.Group) if tc.EndCount != *tc.Group.Count { t.Fatalf("got: %#v, expected %#v", *tc.Group.Count, tc.EndCount) } } } func TestScale_calculateCountBasedOnPercent(t *testing.T) { cases := []struct { Count int Percent int Output int }{ { 100, 50, 50, }, { 3, 33, 1, }, { 3, 10, 0, }, } for _, tc := range cases { output := calculateCountBasedOnPercent(tc.Count, tc.Percent) if output != tc.Output { t.Fatalf("got: %#v, expected %#v", output, tc.Output) } } } func buildScalingConfig(direction, dType string, number int) *Config { c := &Config{ Scale: &structs.ScaleConfig{ Direction: direction, DirectionType: dType, }, } switch dType { case structs.ScalingDirectionTypeCount: c.Scale.Count = number case structs.ScalingDirectionTypePercent: c.Scale.Percent = number } return c } func buildTaskGroup(count int) *nomad.TaskGroup { n := "LevantTest" c := count t := &nomad.TaskGroup{ Name: &n, Count: &c, } return t } ================================================ FILE: scripts/docker-entrypoint.sh ================================================ #!/usr/bin/env sh # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: MPL-2.0 set -e if [ "$1" = 'levant' ]; then shift fi exec levant "$@" ================================================ FILE: scripts/version.sh ================================================ #!/usr/bin/env bash # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: MPL-2.0 version_file=$1 version_metadata_file=$2 version=$(awk '$1 == "Version" && $2 == "=" { gsub(/"/, "", $3); print $3 }' <"${version_file}") prerelease=$(awk '$1 == "VersionPrerelease" && $2 == "=" { gsub(/"/, "", $3); print $3 }' <"${version_file}") metadata=$(awk '$1 == "VersionMetadata" && $2 == "=" { gsub(/"/, "", $3); print $3 }' <"${version_metadata_file}") if [ -n "$metadata" ] && [ -n "$prerelease" ]; then echo "${version}-${prerelease}+${metadata}" elif [ -n "$metadata" ]; then echo "${version}+${metadata}" elif [ -n "$prerelease" ]; then echo "${version}-${prerelease}" else echo "${version}" fi ================================================ FILE: template/funcs.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package template import ( "encoding/json" "errors" "fmt" "math" "os" "reflect" "strconv" "strings" "text/template" "time" "unicode" "unicode/utf8" "github.com/Masterminds/sprig/v3" spewLib "github.com/davecgh/go-spew/spew" consul "github.com/hashicorp/consul/api" "github.com/rs/zerolog/log" ) // funcMap builds the template functions and passes the consulClient where this // is required. func funcMap(consulClient *consul.Client) template.FuncMap { r := template.FuncMap{ "consulKey": consulKeyFunc(consulClient), "consulKeyExists": consulKeyExistsFunc(consulClient), "consulKeyOrDefault": consulKeyOrDefaultFunc(consulClient), "env": envFunc(), "fileContents": fileContents(), "loop": loop, "parseBool": parseBool, "parseFloat": parseFloat, "parseInt": parseInt, "parseJSON": parseJSON, "parseUint": parseUint, "replace": replace, "timeNow": timeNowFunc, "timeNowUTC": timeNowUTCFunc, "timeNowTimezone": timeNowTimezoneFunc(), "toLower": toLower, "toUpper": toUpper, // Maths. "add": add, "subtract": subtract, "multiply": multiply, "divide": divide, "modulo": modulo, // Case Helpers "firstRuneToUpper": firstRuneToUpper, "firstRuneToLower": firstRuneToLower, "runeToUpper": runeToUpper, "runeToLower": runeToLower, //debug. "spewDump": spewDump, "spewPrintf": spewPrintf, } // Add the Sprig functions to the funcmap for k, v := range sprig.FuncMap() { // if there is a name conflict, favor sprig and rename original version if origFun, ok := r[k]; ok { if name, err := firstRuneToUpper(k); err == nil { name = "levant" + name log.Debug().Msgf("template/funcs: renaming \"%v\" function to \"%v\"", k, name) r[name] = origFun } else { log.Error().Msgf("template/funcs: could not add \"%v\" function. error:%v", k, err) } } r[k] = v } r["sprigVersion"] = sprigVersionFunc return r } // SprigVersion contains the semver of the included sprig library // it is used in command/version and provided in the sprig_version // template function const SprigVersion = "3.1.0" func sprigVersionFunc() func(string) (string, error) { return func(_ string) (string, error) { return SprigVersion, nil } } func consulKeyFunc(consulClient *consul.Client) func(string) (string, error) { return func(s string) (string, error) { if len(s) == 0 { return "", nil } kv, _, err := consulClient.KV().Get(s, nil) if err != nil { return "", err } if kv == nil { return "", errors.New("Consul KV not found") } v := string(kv.Value[:]) log.Info().Msgf("template/funcs: using Consul KV variable with key %s and value %s", s, v) return v, nil } } func consulKeyExistsFunc(consulClient *consul.Client) func(string) (bool, error) { return func(s string) (bool, error) { if len(s) == 0 { return false, nil } kv, _, err := consulClient.KV().Get(s, nil) if err != nil { return false, err } if kv == nil { return false, nil } log.Info().Msgf("template/funcs: found Consul KV variable with key %s", s) return true, nil } } func consulKeyOrDefaultFunc(consulClient *consul.Client) func(string, string) (string, error) { return func(s, d string) (string, error) { if len(s) == 0 { log.Info().Msgf("template/funcs: using default Consul KV variable with value %s", d) return d, nil } kv, _, err := consulClient.KV().Get(s, nil) if err != nil { return "", err } if kv == nil { log.Info().Msgf("template/funcs: using default Consul KV variable with value %s", d) return d, nil } v := string(kv.Value[:]) log.Info().Msgf("template/funcs: using Consul KV variable with key %s and value %s", s, v) return v, nil } } func loop(ints ...int64) (<-chan int64, error) { var start, stop int64 switch len(ints) { case 1: start, stop = 0, ints[0] case 2: start, stop = ints[0], ints[1] default: return nil, fmt.Errorf("loop: wrong number of arguments, expected 1 or 2"+ ", but got %d", len(ints)) } ch := make(chan int64) go func() { for i := start; i < stop; i++ { ch <- i } close(ch) }() return ch, nil } func parseBool(s string) (bool, error) { if s == "" { return false, nil } result, err := strconv.ParseBool(s) if err != nil { return false, err } return result, nil } func parseFloat(s string) (float64, error) { if s == "" { return 0.0, nil } result, err := strconv.ParseFloat(s, 10) if err != nil { return 0, err } return result, nil } func parseInt(s string) (int64, error) { if s == "" { return 0, nil } result, err := strconv.ParseInt(s, 10, 64) if err != nil { return 0, err } return result, nil } func parseJSON(s string) (interface{}, error) { if s == "" { return map[string]interface{}{}, nil } var data interface{} if err := json.Unmarshal([]byte(s), &data); err != nil { return nil, err } return data, nil } func parseUint(s string) (uint64, error) { if s == "" { return 0, nil } result, err := strconv.ParseUint(s, 10, 64) if err != nil { return 0, err } return result, nil } func replace(input, from, to string) string { return strings.Replace(input, from, to, -1) } func timeNowFunc() string { return time.Now().Format("2006-01-02T15:04:05Z07:00") } func timeNowUTCFunc() string { return time.Now().UTC().Format("2006-01-02T15:04:05Z07:00") } func timeNowTimezoneFunc() func(string) (string, error) { return func(t string) (string, error) { if t == "" { return "", nil } loc, err := time.LoadLocation(t) if err != nil { return "", err } return time.Now().In(loc).Format("2006-01-02T15:04:05Z07:00"), nil } } func toLower(s string) (string, error) { return strings.ToLower(s), nil } func toUpper(s string) (string, error) { return strings.ToUpper(s), nil } func envFunc() func(string) (string, error) { return func(s string) (string, error) { if s == "" { return "", nil } return os.Getenv(s), nil } } func fileContents() func(string) (string, error) { return func(s string) (string, error) { if s == "" { return "", nil } contents, err := os.ReadFile(s) if err != nil { return "", err } return string(contents), nil } } func add(b, a interface{}) (interface{}, error) { av := reflect.ValueOf(a) bv := reflect.ValueOf(b) switch av.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: switch bv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return av.Int() + bv.Int(), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: ub := bv.Uint() if ub > math.MaxInt { return nil, fmt.Errorf("uint value overflows int") } return av.Int() + int64(ub), nil case reflect.Float32, reflect.Float64: return float64(av.Int()) + bv.Float(), nil default: return nil, fmt.Errorf("add: unknown type for %q (%T)", bv, b) } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: switch bv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: ua := av.Uint() if ua > math.MaxInt { return nil, fmt.Errorf("uint value overflows int") } return int64(ua) + bv.Int(), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return av.Uint() + bv.Uint(), nil case reflect.Float32, reflect.Float64: return float64(av.Uint()) + bv.Float(), nil default: return nil, fmt.Errorf("add: unknown type for %q (%T)", bv, b) } case reflect.Float32, reflect.Float64: switch bv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return av.Float() + float64(bv.Int()), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return av.Float() + float64(bv.Uint()), nil case reflect.Float32, reflect.Float64: return av.Float() + bv.Float(), nil default: return nil, fmt.Errorf("add: unknown type for %q (%T)", bv, b) } default: return nil, fmt.Errorf("add: unknown type for %q (%T)", av, a) } } func subtract(b, a interface{}) (interface{}, error) { av := reflect.ValueOf(a) bv := reflect.ValueOf(b) switch av.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: switch bv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return av.Int() - bv.Int(), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: ub := bv.Uint() if ub > math.MaxInt { return nil, fmt.Errorf("uint value overflows int") } return av.Int() - int64(ub), nil case reflect.Float32, reflect.Float64: return float64(av.Int()) - bv.Float(), nil default: return nil, fmt.Errorf("subtract: unknown type for %q (%T)", bv, b) } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: switch bv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: ua := av.Uint() if ua > math.MaxInt { return nil, fmt.Errorf("uint value overflows int") } return int64(ua) - bv.Int(), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return av.Uint() - bv.Uint(), nil case reflect.Float32, reflect.Float64: return float64(av.Uint()) - bv.Float(), nil default: return nil, fmt.Errorf("subtract: unknown type for %q (%T)", bv, b) } case reflect.Float32, reflect.Float64: switch bv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return av.Float() - float64(bv.Int()), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return av.Float() - float64(bv.Uint()), nil case reflect.Float32, reflect.Float64: return av.Float() - bv.Float(), nil default: return nil, fmt.Errorf("subtract: unknown type for %q (%T)", bv, b) } default: return nil, fmt.Errorf("subtract: unknown type for %q (%T)", av, a) } } func multiply(b, a interface{}) (interface{}, error) { av := reflect.ValueOf(a) bv := reflect.ValueOf(b) switch av.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: switch bv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return av.Int() * bv.Int(), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: ub := bv.Uint() if ub > math.MaxInt { return nil, fmt.Errorf("uint value overflows int") } return av.Int() * int64(ub), nil case reflect.Float32, reflect.Float64: return float64(av.Int()) * bv.Float(), nil default: return nil, fmt.Errorf("multiply: unknown type for %q (%T)", bv, b) } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: switch bv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: ua := av.Uint() if ua > math.MaxInt { return nil, fmt.Errorf("uint value overflows int") } return int64(ua) * bv.Int(), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return av.Uint() * bv.Uint(), nil case reflect.Float32, reflect.Float64: return float64(av.Uint()) * bv.Float(), nil default: return nil, fmt.Errorf("multiply: unknown type for %q (%T)", bv, b) } case reflect.Float32, reflect.Float64: switch bv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return av.Float() * float64(bv.Int()), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return av.Float() * float64(bv.Uint()), nil case reflect.Float32, reflect.Float64: return av.Float() * bv.Float(), nil default: return nil, fmt.Errorf("multiply: unknown type for %q (%T)", bv, b) } default: return nil, fmt.Errorf("multiply: unknown type for %q (%T)", av, a) } } func divide(b, a interface{}) (interface{}, error) { av := reflect.ValueOf(a) bv := reflect.ValueOf(b) switch av.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: switch bv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return av.Int() / bv.Int(), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: ub := bv.Uint() if ub > math.MaxInt { return nil, fmt.Errorf("uint value overflows int") } return av.Int() / int64(ub), nil case reflect.Float32, reflect.Float64: return float64(av.Int()) / bv.Float(), nil default: return nil, fmt.Errorf("divide: unknown type for %q (%T)", bv, b) } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: switch bv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: ua := av.Uint() if ua > math.MaxInt { return nil, fmt.Errorf("uint value overflows int") } return int64(ua) / bv.Int(), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return av.Uint() / bv.Uint(), nil case reflect.Float32, reflect.Float64: return float64(av.Uint()) / bv.Float(), nil default: return nil, fmt.Errorf("divide: unknown type for %q (%T)", bv, b) } case reflect.Float32, reflect.Float64: switch bv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return av.Float() / float64(bv.Int()), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return av.Float() / float64(bv.Uint()), nil case reflect.Float32, reflect.Float64: return av.Float() / bv.Float(), nil default: return nil, fmt.Errorf("divide: unknown type for %q (%T)", bv, b) } default: return nil, fmt.Errorf("divide: unknown type for %q (%T)", av, a) } } func modulo(b, a interface{}) (interface{}, error) { av := reflect.ValueOf(a) bv := reflect.ValueOf(b) switch av.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: switch bv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return av.Int() % bv.Int(), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: ub := bv.Uint() if ub > math.MaxInt { return nil, fmt.Errorf("uint value overflows int") } return av.Int() % int64(ub), nil default: return nil, fmt.Errorf("modulo: unknown type for %q (%T)", bv, b) } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: switch bv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: ua := av.Uint() if ua > math.MaxInt { return nil, fmt.Errorf("uint value overflows int") } return int64(ua) % bv.Int(), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return av.Uint() % bv.Uint(), nil default: return nil, fmt.Errorf("modulo: unknown type for %q (%T)", bv, b) } default: return nil, fmt.Errorf("modulo: unknown type for %q (%T)", av, a) } } func firstRuneToUpper(s string) (string, error) { return runeToUpper(s, 0) } func runeToUpper(inString string, runeIndex int) (string, error) { return funcOnRune(unicode.ToUpper, inString, runeIndex) } func firstRuneToLower(s string) (string, error) { return runeToLower(s, 0) } func runeToLower(inString string, runeIndex int) (string, error) { return funcOnRune(unicode.ToLower, inString, runeIndex) } func funcOnRune(inFunc func(rune) rune, inString string, runeIndex int) (string, error) { if !utf8.ValidString(inString) { return "", errors.New("funcOnRune: not a valid UTF-8 string") } runeCount := utf8.RuneCountInString(inString) if runeIndex > runeCount-1 || runeIndex < 0 { return "", fmt.Errorf("funcOnRune: runeIndex out of range (max:%v, provided:%v)", runeCount-1, runeIndex) } runes := []rune(inString) transformedRune := inFunc(runes[runeIndex]) if runes[runeIndex] == transformedRune { return inString, nil } runes[runeIndex] = transformedRune return string(runes), nil } func spewDump(a interface{}) (string, error) { return spewLib.Sdump(a), nil } func spewPrintf(format string, args ...interface{}) (string, error) { return spewLib.Sprintf(format, args), nil } ================================================ FILE: template/render.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package template import ( "bytes" "encoding/json" "fmt" "os" "path" "github.com/hashicorp/levant/client" "github.com/hashicorp/levant/helper" nomad "github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/jobspec2" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs/hcl2shim" "github.com/rs/zerolog/log" yaml "gopkg.in/yaml.v2" ) // RenderJob takes in a template and variables performing a render of the // template followed by Nomad jobspec parse. func RenderJob(templateFile string, variableFiles []string, addr string, flagVars *map[string]interface{}) (job *nomad.Job, err error) { var tpl *bytes.Buffer tpl, err = RenderTemplate(templateFile, variableFiles, addr, flagVars) if err != nil { return } return jobspec2.Parse("", tpl) } // RenderTemplate is the main entry point to render the template based on the // passed variables file. func RenderTemplate(templateFile string, variableFiles []string, addr string, flagVars *map[string]interface{}) (tpl *bytes.Buffer, err error) { t := &tmpl{} t.flagVariables = flagVars t.jobTemplateFile = templateFile t.variableFiles = variableFiles c, err := client.NewConsulClient(addr) if err != nil { return } t.consulClient = c if len(t.variableFiles) == 0 { log.Debug().Msgf("template/render: no variable file passed, trying defaults") if defaultVarFile := helper.GetDefaultVarFile(); defaultVarFile != "" { t.variableFiles = []string{defaultVarFile} log.Debug().Msgf("template/render: found default variable file, using %s", defaultVarFile) } } mergedVariables := make(map[string]interface{}) for _, variableFile := range t.variableFiles { // Process the variable file extension and log DEBUG so the template can be // correctly rendered. var ext string if ext = path.Ext(variableFile); ext != "" { log.Debug().Msgf("template/render: variable file extension %s detected", ext) } var variables map[string]interface{} switch ext { case terraformVarExtension: variables, err = t.parseTFVars(variableFile) case yamlVarExtension, ymlVarExtension: variables, err = t.parseYAMLVars(variableFile) case jsonVarExtension: variables, err = t.parseJSONVars(variableFile) default: err = fmt.Errorf("variables file extension %v not supported", ext) } if err != nil { return } for k, v := range variables { mergedVariables[k] = v } } src, err := os.ReadFile(t.jobTemplateFile) if err != nil { return } // If no command line variables are passed; log this as DEBUG to provide much // greater feedback. if len(*t.flagVariables) == 0 { log.Debug().Msgf("template/render: no command line variables passed") } tpl, err = t.renderTemplate(string(src), mergedVariables) return } func (t *tmpl) parseJSONVars(variableFile string) (variables map[string]interface{}, err error) { jsonFile, err := os.ReadFile(variableFile) if err != nil { return } variables = make(map[string]interface{}) if err = json.Unmarshal(jsonFile, &variables); err != nil { return } return variables, nil } func (t *tmpl) parseTFVars(variableFile string) (map[string]interface{}, error) { tfParser := configs.NewParser(nil) loadedFile, loadDiags := tfParser.LoadConfigFile(variableFile) if loadDiags != nil && loadDiags.HasErrors() { return nil, loadDiags } if loadedFile == nil { return nil, fmt.Errorf("hcl returned nil file") } variables := make(map[string]interface{}) for _, variable := range loadedFile.Variables { variables[variable.Name] = hcl2shim.ConfigValueFromHCL2(variable.Default) } return variables, nil } func (t *tmpl) parseYAMLVars(variableFile string) (variables map[string]interface{}, err error) { yamlFile, err := os.ReadFile(variableFile) if err != nil { return } variables = make(map[string]interface{}) if err = yaml.Unmarshal(yamlFile, &variables); err != nil { return } return variables, nil } func (t *tmpl) renderTemplate(src string, variables map[string]interface{}) (tpl *bytes.Buffer, err error) { tpl = &bytes.Buffer{} // Setup the template file for rendering tmpl := t.newTemplate() if tmpl, err = tmpl.Parse(src); err != nil { return } if variables != nil { // Merge variables passed on the CLI with those passed through a variables file. err = tmpl.Execute(tpl, helper.VariableMerge(&variables, t.flagVariables)) } else { // No variables file passed; render using any passed CLI variables. log.Debug().Msgf("template/render: variable file not passed") err = tmpl.Execute(tpl, t.flagVariables) } return tpl, err } ================================================ FILE: template/render_test.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package template import ( "os" "testing" nomad "github.com/hashicorp/nomad/api" ) const ( testJobName = "levantExample" testJobNameOverwrite = "levantExampleOverwrite" testJobNameOverwrite2 = "levantExampleOverwrite2" testDCName = "dc13" testEnvName = "GROUP_NAME_ENV" testEnvValue = "cache" ) func TestTemplater_RenderTemplate(t *testing.T) { var job *nomad.Job var err error // Start with an empty passed var args map. fVars := make(map[string]interface{}) // Test basic TF template render. job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.tf"}, "", &fVars) if err != nil { t.Fatal(err) } if *job.Name != testJobName { t.Fatalf("expected %s but got %v", testJobName, *job.Name) } if *job.TaskGroups[0].Tasks[0].Resources.CPU != 1313 { t.Fatalf("expected CPU resource %v but got %v", 1313, *job.TaskGroups[0].Tasks[0].Resources.CPU) } // Test basic YAML template render. job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.yaml"}, "", &fVars) if err != nil { t.Fatal(err) } if *job.Name != testJobName { t.Fatalf("expected %s but got %v", testJobName, *job.Name) } if *job.TaskGroups[0].Tasks[0].Resources.CPU != 1313 { t.Fatalf("expected CPU resource %v but got %v", 1313, *job.TaskGroups[0].Tasks[0].Resources.CPU) } // Test multiple var-files job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.yaml", "test-fixtures/test-overwrite.yaml"}, "", &fVars) if err != nil { t.Fatal(err) } if *job.Name != testJobNameOverwrite { t.Fatalf("expected %s but got %v", testJobNameOverwrite, *job.Name) } // Test multiple var-files of different types job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.tf", "test-fixtures/test-overwrite.yaml"}, "", &fVars) if err != nil { t.Fatal(err) } if *job.Name != testJobNameOverwrite { t.Fatalf("expected %s but got %v", testJobNameOverwrite, *job.Name) } // Test multiple var-files with var-args fVars["job_name"] = testJobNameOverwrite2 job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.tf", "test-fixtures/test-overwrite.yaml"}, "", &fVars) if err != nil { t.Fatal(err) } if *job.Name != testJobNameOverwrite2 { t.Fatalf("expected %s but got %v", testJobNameOverwrite2, *job.Name) } // Test empty var-args and empty variable file render. job, err = RenderJob("test-fixtures/none_templated.nomad", []string{}, "", &fVars) if err != nil { t.Fatal(err) } if *job.Name != testJobName { t.Fatalf("expected %s but got %v", testJobName, *job.Name) } // Test var-args only render. fVars = map[string]interface{}{"job_name": testJobName, "task_resource_cpu": "1313"} job, err = RenderJob("test-fixtures/single_templated.nomad", []string{}, "", &fVars) if err != nil { t.Fatal(err) } if *job.Name != testJobName { t.Fatalf("expected %s but got %v", testJobName, *job.Name) } if *job.TaskGroups[0].Tasks[0].Resources.CPU != 1313 { t.Fatalf("expected CPU resource %v but got %v", 1313, *job.TaskGroups[0].Tasks[0].Resources.CPU) } // Test var-args and variables file render. delete(fVars, "job_name") fVars["datacentre"] = testDCName os.Setenv(testEnvName, testEnvValue) job, err = RenderJob("test-fixtures/multi_templated.nomad", []string{"test-fixtures/test.yaml"}, "", &fVars) if err != nil { t.Fatal(err) } if *job.Name != testJobName { t.Fatalf("expected %s but got %v", testJobName, *job.Name) } if job.Datacenters[0] != testDCName { t.Fatalf("expected %s but got %v", testDCName, job.Datacenters[0]) } if *job.TaskGroups[0].Name != testEnvValue { t.Fatalf("expected %s but got %v", testEnvValue, *job.TaskGroups[0].Name) } } ================================================ FILE: template/template.go ================================================ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package template import ( "text/template" consul "github.com/hashicorp/consul/api" ) // tmpl provides everything needed to fully render and job template using // inbuilt functions. type tmpl struct { consulClient *consul.Client flagVariables *map[string]interface{} jobTemplateFile string variableFiles []string } const ( jsonVarExtension = ".json" terraformVarExtension = ".tf" yamlVarExtension = ".yaml" ymlVarExtension = ".yml" rightDelim = "]]" leftDelim = "[[" ) // newTemplate returns an empty template with default options set func (t *tmpl) newTemplate() *template.Template { tmpl := template.New("jobTemplate") tmpl.Delims(leftDelim, rightDelim) tmpl.Option("missingkey=zero") tmpl.Funcs(funcMap(t.consulClient)) return tmpl } ================================================ FILE: template/test-fixtures/missing_var.nomad ================================================ # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: MPL-2.0 job "[[.job_name]]" { datacenters = ["dc1"] type = "service" update { max_parallel = 1 min_healthy_time = "10s" healthy_deadline = "1m" auto_revert = true } group "cache" { count = 1 restart { attempts = 10 interval = "5m" delay = "25s" mode = "delay" } ephemeral_disk { size = 300 } task "redis" { artifact { # .binary_url is not set source = "[[ .binary_url ]]" } driver = "docker" config { image = "redis:3.2" port_map { db = 6379 } } resources { cpu = 500 memory = 256 network { mbits = 10 port "db" {} } } service { name = "global-redis-check" tags = ["global", "cache"] port = "db" check { name = "alive" type = "tcp" interval = "10s" timeout = "2s" } } } } } ================================================ FILE: template/test-fixtures/multi_templated.nomad ================================================ # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: MPL-2.0 job "[[.job_name]]" { datacenters = ["[[.datacentre]]"] type = "service" update { max_parallel = 1 min_healthy_time = "10s" healthy_deadline = "1m" auto_revert = true } group "[[env "GROUP_NAME_ENV"]]" { count = 1 restart { attempts = 10 interval = "5m" delay = "25s" mode = "delay" } ephemeral_disk { size = 300 } task "redis" { driver = "docker" config { image = "redis:3.2" port_map { db = 6379 } } resources { cpu = 500 memory = 256 network { mbits = 10 port "db" {} } } service { name = "global-redis-check" tags = ["global", "cache"] port = "db" check { name = "alive" type = "tcp" interval = "10s" timeout = "2s" } } } } } ================================================ FILE: template/test-fixtures/none_templated.nomad ================================================ # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: MPL-2.0 job "levantExample" { datacenters = ["dc1"] type = "service" update { max_parallel = 1 min_healthy_time = "10s" healthy_deadline = "1m" auto_revert = true } group "cache" { count = 1 restart { attempts = 10 interval = "5m" delay = "25s" mode = "delay" } ephemeral_disk { size = 300 } task "redis" { driver = "docker" config { image = "redis:3.2" port_map { db = 6379 } } resources { cpu = 500 memory = 256 network { mbits = 10 port "db" {} } } service { name = "global-redis-check" tags = ["global", "cache"] port = "db" check { name = "alive" type = "tcp" interval = "10s" timeout = "2s" } } } } } ================================================ FILE: template/test-fixtures/single_templated.nomad ================================================ # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: MPL-2.0 job "[[.job_name]]" { datacenters = ["dc1"] type = "service" update { max_parallel = 1 min_healthy_time = "10s" healthy_deadline = "1m" auto_revert = true } group "cache" { count = 1 restart { attempts = 10 interval = "5m" delay = "25s" mode = "delay" } ephemeral_disk { size = 300 } task "redis" { template { data = <