├── .copywrite.hcl ├── .github ├── CODEOWNERS ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── build.yml │ ├── ci.yml │ └── lint.yml ├── .gitignore ├── .golangci.yml ├── .release ├── ci.hcl ├── docker-entrypoint.sh ├── release-metadata.hcl └── security-scan.hcl ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── child ├── child.go ├── child_test.go ├── command-prep.go ├── command-prep_test.go ├── command-prep_windows.go ├── sys_nix.go └── sys_windows.go ├── cli.go ├── cli_test.go ├── config ├── auth.go ├── auth_test.go ├── config.go ├── config_test.go ├── consul.go ├── consul_test.go ├── convert.go ├── convert_test.go ├── dedup.go ├── dedup_test.go ├── default_delimiters.go ├── default_delimiters_test.go ├── env.go ├── env_test.go ├── exec.go ├── exec_test.go ├── logfile.go ├── mapstructure.go ├── mapstructure_test.go ├── nomad.go ├── nomad_test.go ├── retry.go ├── retry_test.go ├── ssl.go ├── ssl_test.go ├── syslog.go ├── syslog_test.go ├── template.go ├── template_test.go ├── testdata │ └── foo ├── transport.go ├── transport_test.go ├── vault.go ├── vault_test.go ├── wait.go └── wait_test.go ├── dependency ├── catalog_datacenters.go ├── catalog_datacenters_test.go ├── catalog_node.go ├── catalog_node_test.go ├── catalog_nodes.go ├── catalog_nodes_test.go ├── catalog_service.go ├── catalog_service_test.go ├── catalog_services.go ├── catalog_services_test.go ├── client_set.go ├── client_set_test.go ├── connect_ca.go ├── connect_ca_test.go ├── connect_leaf.go ├── connect_leaf_test.go ├── consul_common_test.go ├── consul_exported_services.go ├── consul_exported_services_test.go ├── consul_partitions.go ├── consul_partitions_test.go ├── consul_peering.go ├── consul_peering_test.go ├── dependency.go ├── dependency_test.go ├── errors.go ├── file.go ├── file_test.go ├── health_service.go ├── health_service_test.go ├── kv_get.go ├── kv_get_test.go ├── kv_keys.go ├── kv_keys_test.go ├── kv_list.go ├── kv_list_test.go ├── nomad_service.go ├── nomad_service_test.go ├── nomad_services.go ├── nomad_services_test.go ├── nomad_var_get.go ├── nomad_var_get_test.go ├── nomad_var_list.go ├── nomad_var_list_test.go ├── nomad_var_structs.go ├── set.go ├── vault_agent_token.go ├── vault_agent_token_test.go ├── vault_common.go ├── vault_common_test.go ├── vault_list.go ├── vault_list_test.go ├── vault_pki.go ├── vault_pki_test.go ├── vault_read.go ├── vault_read_test.go ├── vault_token.go ├── vault_token_test.go ├── vault_write.go └── vault_write_test.go ├── docs ├── configuration.md ├── modes.md ├── observability.md ├── plugins.md └── templating-language.md ├── examples ├── apache.md ├── haproxy-connect-proxy.md ├── haproxy-connect-proxy │ └── run-haproxy-connect-proxy ├── haproxy.md ├── join.md ├── nginx-connect-proxy.md ├── nginx-connect-proxy │ └── run-nginx-connect-proxy ├── nginx.md ├── services.md ├── varnish.md ├── vault-pki.md └── vault-transit.md ├── flags.go ├── go.mod ├── go.sum ├── logging ├── logfile.go ├── logfile_test.go ├── logging.go ├── logging_test.go ├── syslog.go └── syslog_test.go ├── main.go ├── main_test.go ├── manager ├── dedup.go ├── dedup_test.go ├── errors.go ├── example_extfuncmap_test.go ├── example_manager_test.go ├── manager_test.go ├── runner.go └── runner_test.go ├── renderer ├── file_ownership.go ├── file_ownership_windows.go ├── file_perms.go ├── file_perms_windows.go ├── renderer.go └── renderer_test.go ├── scripts └── readme-toc.sh ├── service_os ├── service.go └── service_windows.go ├── signals ├── mapstructure.go ├── mapstructure_test.go ├── signals.go ├── signals_test.go ├── signals_unix.go └── signals_windows.go ├── template ├── brain.go ├── brain_test.go ├── funcs.go ├── funcs_test.go ├── nomad_funcs.go ├── scratch.go ├── scratch_test.go ├── template.go ├── template_test.go └── testdata │ └── sandbox │ └── path │ └── to │ ├── bad-symlink │ ├── file │ └── ok-symlink ├── test ├── helpers.go ├── tenancy_helper.go ├── tenancy_helper_test.go └── testdata │ └── nomad.json ├── version └── version.go └── watch ├── dependencies_test.go ├── vault_token.go ├── vault_token_test.go ├── view.go ├── view_test.go ├── watch_test.go ├── watcher.go └── watcher_test.go /.copywrite.hcl: -------------------------------------------------------------------------------- 1 | schema_version = 1 2 | 3 | project { 4 | license = "MPL-2.0" 5 | copyright_year = 2014 6 | 7 | # (OPTIONAL) A list of globs that should not have copyright/license headers. 8 | # Supports doublestar glob patterns for more flexibility in defining which 9 | # files or folders should be ignored 10 | header_ignore = [ 11 | # "vendors/**", 12 | # "**autogen**", 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # default PR reviews to the team 2 | * @hashicorp/consul-selfmanage-maintainers 3 | 4 | # release configuration 5 | /.release/ @hashicorp/team-selfmanaged-releng @hashicorp/consul-selfmanage-maintainers 6 | /.github/workflows/build.yml @hashicorp/team-selfmanaged-releng @hashicorp/consul-selfmanage-maintainers 7 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | We like to encourage you to contribute to the repository. This should be as easy 4 | as possible for you but there are a few things to consider when contributing. 5 | The following guidelines for contribution should be followed if you want to 6 | submit a pull request. 7 | 8 | ## How to prepare 9 | 10 | * You need a [GitHub account](https://github.com/signup/free) 11 | * Submit an [issue ticket](https://github.com/hashicorp/consul-template/issues) 12 | for your issue if there is not one yet. 13 | * Describe the issue and include steps to reproduce when it's a bug. 14 | * Ensure to mention the earliest version that you know is affected. 15 | * If you plan on submitting a bug report, please submit debug-level logs along 16 | with the report using [gist](https://gist.github.com/) or some other paste 17 | service by appending `-log-level=debug` to your command. 18 | * Fork the repository on GitHub 19 | 20 | ## Make Changes 21 | 22 | * In your forked repository, create a topic branch for your upcoming patch. 23 | * Usually this is based on the main branch. 24 | * Create a branch based on main; `git branch 25 | fix/main/my_contribution main` then checkout the new branch with `git 26 | checkout fix/main/my_contribution`. Please avoid working directly on the `main` branch. 27 | * Make commits of logical units and describe them properly. 28 | * Check for unnecessary whitespace with `git diff --check` before committing. 29 | 30 | * If possible, submit tests to your patch / new feature so it can be tested easily. 31 | * Assure nothing is broken by running all the tests. 32 | 33 | ## Submit Changes 34 | 35 | * Push your changes to a topic branch in your fork of the repository. 36 | * Open a pull request to the original repository and choose the right original branch you want to patch. 37 | * If not done in commit messages (which you really should do) please reference and update your issue with the code changes. 38 | * Even if you have write access to the repository, do not directly push or merge pull-requests. Let another team member review your pull request and approve. 39 | 40 | # Additional Resources 41 | 42 | * [General GitHub documentation](https://help.github.com/) 43 | * [GitHub pull request documentation](https://help.github.com/send-pull-requests/) 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please note that the Consul Template issue tracker is reserved 2 | for bug reports and enhancements. For general usage questions, 3 | please use the Consul Community Portal or the Consul mailing list: 4 | 5 | https://discuss.hashicorp.com/c/consul 6 | https://groups.google.com/forum/#!forum/consul-tool 7 | 8 | Please try to simplify the issue as much as possible and include all the 9 | details to replicate it. The shorter and simpler the bug is to reproduce the 10 | quicker it can be addressed. Thanks. 11 | 12 | ### Consul Template version 13 | 14 | Run `consul-template -v` to show the version. If you are not 15 | running the latest version, please upgrade before submitting an 16 | issue. 17 | 18 | 19 | ### Configuration 20 | 21 | ```hcl 22 | # Copy-paste your configuration files here. Only include what is necessary or 23 | # what you've changed from defaults. Include all referenced configurations. 24 | 25 | ``` 26 | 27 | ```liquid 28 | # Copy-paste your Consul Template template here 29 | ``` 30 | 31 | ```liquid 32 | # Include sample data you reference in the template from Consul or Vault here. 33 | ``` 34 | 35 | ### Command 36 | 37 | ```shell 38 | # Place your Consul Template command here 39 | ``` 40 | 41 | ### Debug output 42 | 43 | Provide a link to a GitHub Gist containing the complete debug 44 | output by running with `-log-level=trace`. 45 | 46 | ### Expected behavior 47 | 48 | What should have happened? 49 | 50 | ### Actual behavior 51 | 52 | What actually happened? 53 | 54 | 55 | ### Steps to reproduce 56 | 57 | 1. 58 | 2. 59 | 3. 60 | 61 | ### References 62 | 63 | Are there any other GitHub issues (open or closed) that should 64 | be linked here? For example: 65 | - GH-1234 66 | - ... 67 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | # 0 here disables PRs, 5 is default 6 | open-pull-requests-limit: 5 7 | schedule: 8 | interval: "daily" 9 | time: "06:10" 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | time: "06:10" 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | - release/** 9 | 10 | env: 11 | CONSUL_LICENSE: ${{ secrets.CONSUL_LICENSE }} 12 | 13 | jobs: 14 | run-tests: 15 | name: Run test cases (with consul${{ matrix.consul-ent-tag }}) 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | os: [ubuntu-latest] 21 | go: [^1] 22 | consul-ent-tag: ["", "-enterprise"] 23 | 24 | steps: 25 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 26 | 27 | - name: Set up Go 28 | uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 29 | with: 30 | go-version: ${{ matrix.go }} 31 | cache: false 32 | 33 | - name: Install Consul${{ matrix.consul-ent-tag }}, Vault and Nomad for integration testing 34 | run: | 35 | curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - 36 | sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" 37 | sudo apt-get update && sudo apt-get install consul${{ matrix.consul-ent-tag }} vault nomad 38 | 39 | - name: Run tests 40 | run: | 41 | make test-race 42 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | run-linters: 8 | name: Run linters 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 13 | 14 | - name: Set up Go 15 | uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 16 | with: 17 | go-version-file: 'go.mod' 18 | 19 | - name: Run linters 20 | uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # v6.0.1 21 | with: 22 | version: v1.62.0 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Custom ### 2 | /bin 3 | /pkg 4 | /vendor 5 | consul-template 6 | 7 | ### Golang ### 8 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 9 | *.o 10 | *.a 11 | *.so 12 | 13 | # Folders 14 | _obj 15 | _test 16 | 17 | # Architecture specific extensions/prefixes 18 | *.[568vq] 19 | [568vq].out 20 | 21 | *.cgo1.go 22 | *.cgo2.c 23 | _cgo_defun.c 24 | _cgo_gotypes.go 25 | _cgo_export.* 26 | 27 | _testmain.go 28 | 29 | *.exe 30 | *.test 31 | .proc 32 | 33 | # IDE files 34 | *.iml 35 | *.idea 36 | 37 | ### OSX ### 38 | *.DS_Store 39 | .AppleDouble 40 | .LSOverride 41 | 42 | # Icon must end with two \r 43 | Icon 44 | # Thumbnails 45 | ._* 46 | # Files that might appear in the root of a volume 47 | .DocumentRevisions-V100 48 | .fseventsd 49 | .Spotlight-V100 50 | .TemporaryItems 51 | .Trashes 52 | .VolumeIcon.icns 53 | .com.apple.timemachine.donotpresent 54 | # Directories potentially created on remote AFP share 55 | .AppleDB 56 | .AppleDesktop 57 | Network Trash Folder 58 | Temporary Items 59 | .apdisk 60 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | linters: 5 | disable-all: true 6 | enable: 7 | - gofmt 8 | - govet 9 | - unconvert 10 | - staticcheck 11 | - ineffassign 12 | - unparam 13 | - forbidigo 14 | - gomodguard 15 | - gosimple 16 | - depguard 17 | 18 | issues: 19 | # Disable the default exclude list so that all excludes are explicitly 20 | # defined in this file. 21 | exclude-use-default: false 22 | exclude-rules: 23 | - text: 'shadow: declaration of "(err|ctx)" shadows declaration at' 24 | linters: [ govet ] 25 | exclude-dirs-use-default: false 26 | 27 | linters-settings: 28 | govet: 29 | enable-all: true 30 | disable: 31 | - fieldalignment 32 | - nilness 33 | - unusedwrite 34 | forbidigo: 35 | # Forbid the following identifiers (list of regexp). 36 | forbid: 37 | - '\bioutil\b(# Use io and os packages instead of ioutil)?' 38 | - '\brequire\.New\b(# Use package-level functions with explicit TestingT)?' 39 | - '\bassert\.New\b(# Use package-level functions with explicit TestingT)?' 40 | # Exclude godoc examples from forbidigo checks. 41 | # Default: true 42 | exclude_godoc_examples: false 43 | gofmt: 44 | simplify: true 45 | gomodguard: 46 | blocked: 47 | # List of blocked modules. 48 | modules: 49 | # Blocked module. 50 | - github.com/hashicorp/go-msgpack: 51 | recommendations: 52 | - github.com/hashicorp/consul-net-rpc/go-msgpack 53 | - github.com/golang/protobuf: 54 | recommendations: 55 | - google.golang.org/protobuf 56 | depguard: 57 | rules: 58 | main: 59 | # List of file globs that will match this list of settings to compare against. 60 | # Default: $all 61 | files: 62 | - $all 63 | # List of allowed packages. 64 | allow: 65 | - $gostd 66 | - github.com/BurntSushi/toml 67 | - github.com/Masterminds/sprig/v3 68 | - github.com/davecgh/go-spew/spew 69 | - github.com/hashicorp/consul-template 70 | - github.com/hashicorp/consul/api 71 | - github.com/hashicorp/consul/sdk/testutil 72 | - github.com/hashicorp/go-gatedio 73 | - github.com/hashicorp/go-hclog 74 | - github.com/hashicorp/go-multierror 75 | - github.com/hashicorp/go-rootcerts 76 | - github.com/hashicorp/go-sockaddr/template 77 | - github.com/hashicorp/go-syslog 78 | - github.com/hashicorp/hcl 79 | - github.com/hashicorp/logutils 80 | - github.com/hashicorp/nomad/api 81 | - github.com/hashicorp/vault/api 82 | - dario.cat/mergo 83 | - github.com/mitchellh/go-homedir 84 | - github.com/mitchellh/hashstructure 85 | - github.com/mitchellh/mapstructure 86 | - github.com/pkg/errors 87 | - github.com/stretchr/testify/assert 88 | - github.com/stretchr/testify/require 89 | - github.com/coreos/go-systemd 90 | 91 | run: 92 | timeout: 10m 93 | concurrency: 4 -------------------------------------------------------------------------------- /.release/ci.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | schema = "1" 5 | 6 | project "consul-template" { 7 | // the team key is not used by CRT currently 8 | team = "consul-core" 9 | slack { 10 | notification_channel = "C9KPKPKRN" 11 | } 12 | github { 13 | organization = "hashicorp" 14 | repository = "consul-template" 15 | release_branches = [ 16 | "main", 17 | "release/**", 18 | ] 19 | } 20 | } 21 | 22 | event "merge" { 23 | // "entrypoint" to use if build is not run automatically 24 | // i.e. send "merge" complete signal to orchestrator to trigger build 25 | } 26 | 27 | event "build" { 28 | depends = ["merge"] 29 | action "build" { 30 | organization = "hashicorp" 31 | repository = "consul-template" 32 | workflow = "build" 33 | } 34 | } 35 | 36 | event "prepare" { 37 | depends = ["build"] 38 | action "prepare" { 39 | organization = "hashicorp" 40 | repository = "crt-workflows-common" 41 | workflow = "prepare" 42 | depends = ["build"] 43 | } 44 | 45 | notification { 46 | on = "fail" 47 | } 48 | } 49 | 50 | ## These are promotion and post-publish events 51 | ## they should be added to the end of the file after the verify event stanza. 52 | 53 | event "trigger-staging" { 54 | // This event is dispatched by the bob trigger-promotion command 55 | // and is required - do not delete. 56 | } 57 | 58 | event "promote-staging" { 59 | depends = ["trigger-staging"] 60 | action "promote-staging" { 61 | organization = "hashicorp" 62 | repository = "crt-workflows-common" 63 | workflow = "promote-staging" 64 | config = "release-metadata.hcl" 65 | } 66 | 67 | notification { 68 | on = "always" 69 | } 70 | } 71 | 72 | event "promote-staging-docker" { 73 | depends = ["promote-staging"] 74 | action "promote-staging-docker" { 75 | organization = "hashicorp" 76 | repository = "crt-workflows-common" 77 | workflow = "promote-staging-docker" 78 | } 79 | 80 | notification { 81 | on = "always" 82 | } 83 | } 84 | 85 | event "trigger-production" { 86 | // This event is dispatched by the bob trigger-promotion command 87 | // and is required - do not delete. 88 | } 89 | 90 | event "promote-production" { 91 | depends = ["trigger-production"] 92 | action "promote-production" { 93 | organization = "hashicorp" 94 | repository = "crt-workflows-common" 95 | workflow = "promote-production" 96 | } 97 | 98 | notification { 99 | on = "always" 100 | } 101 | } 102 | 103 | event "promote-production-docker" { 104 | depends = ["promote-production"] 105 | action "promote-production-docker" { 106 | organization = "hashicorp" 107 | repository = "crt-workflows-common" 108 | workflow = "promote-production-docker" 109 | } 110 | 111 | notification { 112 | on = "always" 113 | } 114 | } 115 | 116 | event "promote-production-packaging" { 117 | depends = ["promote-production-docker"] 118 | action "promote-production-packaging" { 119 | organization = "hashicorp" 120 | repository = "crt-workflows-common" 121 | workflow = "promote-production-packaging" 122 | } 123 | 124 | notification { 125 | on = "always" 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /.release/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | # Don't use dumb-init as it isn't required and the end-user has the option 7 | # to set it via the `--init` option. 8 | 9 | set -e 10 | 11 | # If the user is trying to run consul-template directly with some arguments, 12 | # then pass them to consul-template. 13 | # On alpine /bin/sh is busybox which supports this bashism. 14 | if [ "${1:0:1}" = '-' ] 15 | then 16 | set -- /bin/consul-template "$@" 17 | fi 18 | 19 | # MUST exec here for consul-template to replace the shell as PID 1 in order 20 | # to properly propagate signals from the OS to the consul-template process. 21 | exec "$@" 22 | -------------------------------------------------------------------------------- /.release/release-metadata.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | url_source_repository = "https://github.com/hashicorp/consul-template" 5 | url_license = "https://github.com/hashicorp/consul-template/blob/main/LICENSE" 6 | -------------------------------------------------------------------------------- /.release/security-scan.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | container { 5 | dependencies = true 6 | alpine_secdb = true 7 | secrets = true 8 | } 9 | 10 | binary { 11 | secrets = true 12 | go_modules = true 13 | osv = true 14 | oss_index = false 15 | nvd = false 16 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # =================================== 5 | # 6 | # Release image 7 | # 8 | # =================================== 9 | FROM alpine:latest AS release-default 10 | 11 | ARG BIN_NAME=consul-template 12 | # Export BIN_NAME for the CMD below, it can't see ARGs directly. 13 | ENV BIN_NAME=$BIN_NAME 14 | ARG PRODUCT_VERSION 15 | ARG PRODUCT_REVISION 16 | ARG PRODUCT_NAME=$BIN_NAME 17 | # TARGETARCH and TARGETOS are set automatically when --platform is provided. 18 | ARG TARGETOS TARGETARCH 19 | ENV PRODUCT_NAME=$BIN_NAME 20 | 21 | LABEL maintainer="Consul Team " 22 | # version label is required for build process 23 | LABEL version=$PRODUCT_VERSION 24 | LABEL revision=$PRODUCT_REVISION 25 | LABEL licenses="MPL-2.0" 26 | 27 | # These are the defaults, this makes them explicit and overridable. 28 | ARG UID=100 29 | ARG GID=1000 30 | # Create a non-root user to run the software. 31 | RUN addgroup -g ${GID} ${BIN_NAME} \ 32 | && adduser -u ${UID} -S -G ${BIN_NAME} ${BIN_NAME} 33 | 34 | # where the build system stores the builds 35 | COPY ./dist/$TARGETOS/$TARGETARCH/$BIN_NAME /bin/ 36 | COPY LICENSE /usr/share/doc/$PRODUCT_NAME/LICENSE.txt 37 | 38 | # entrypoint 39 | COPY ./.release/docker-entrypoint.sh /bin/ 40 | ENTRYPOINT ["/bin/docker-entrypoint.sh"] 41 | 42 | USER ${BIN_NAME}:${BIN_NAME} 43 | CMD /bin/$BIN_NAME 44 | 45 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Metadata about this makefile and position 2 | MKFILE_PATH := $(lastword $(MAKEFILE_LIST)) 3 | CURRENT_DIR := $(patsubst %/,%,$(dir $(realpath $(MKFILE_PATH)))) 4 | 5 | # Tags specific for building 6 | GOTAGS ?= 7 | 8 | # Get the project metadata 9 | OWNER := "hashicorp" 10 | NAME := "consul-template" 11 | PROJECT := $(shell go list -m | awk "/${NAME}/ {print $0}" ) 12 | GIT_COMMIT ?= $(shell git rev-parse --short HEAD || echo release) 13 | VERSION := $(shell awk -F\" '/^[ \t]+Version/ { print $$2; exit }' "${CURRENT_DIR}/version/version.go") 14 | PRERELEASE := $(shell awk -F\" '/^[ \t]+VersionPrerelease/ { print $$2; exit }' "${CURRENT_DIR}/version/version.go") 15 | 16 | # Current system information 17 | GOOS ?= $(shell go env GOOS) 18 | GOARCH ?= $(shell go env GOARCH) 19 | 20 | # List of ldflags 21 | LD_FLAGS ?= \ 22 | -s -w \ 23 | -X ${PROJECT}/version.GitCommit=${GIT_COMMIT} 24 | 25 | # for CRT build process 26 | version: 27 | @echo ${VERSION}${PRERELEASE} 28 | .PHONY: version 29 | 30 | # dev builds and installs the project locally. 31 | dev: 32 | @echo "==> Installing ${NAME} for ${GOOS}/${GOARCH}" 33 | @env \ 34 | CGO_ENABLED="0" \ 35 | go install \ 36 | -ldflags "${LD_FLAGS}" \ 37 | -tags "${GOTAGS}" 38 | .PHONY: dev 39 | 40 | # dev docker builds 41 | docker: 42 | @env CGO_ENABLED="0" go build -ldflags "${LD_FLAGS}" -o $(NAME) 43 | mkdir -p dist/linux/amd64/ 44 | cp consul-template dist/linux/amd64/ 45 | env DOCKER_BUILDKIT=1 docker build -t consul-template . 46 | .PHONY: docker 47 | 48 | # test runs the test suite. 49 | test: 50 | @echo "==> Testing ${NAME}" 51 | @go test -count=1 -timeout=30s -parallel=20 -failfast -tags="${GOTAGS}" ./... ${TESTARGS} 52 | .PHONY: test 53 | 54 | # test-race runs the test suite. 55 | test-race: 56 | @echo "==> Testing ${NAME} (race)" 57 | @go test -v -timeout=120s -race -tags="${GOTAGS}" ./... ${TESTARGS} 58 | .PHONY: test-race 59 | 60 | # _cleanup removes any previous binaries 61 | clean: 62 | @rm -rf "${CURRENT_DIR}/dist/" 63 | @rm -f "consul-template" 64 | .PHONY: clean 65 | 66 | # Add/Update the "Table Of Contents" in the README.md 67 | toc: 68 | @./scripts/readme-toc.sh 69 | .PHONY: toc 70 | 71 | # noop command to get build pipeline working 72 | dev-tree: 73 | @true 74 | .PHONY: dev-tree 75 | 76 | # lint 77 | lint: 78 | @echo "==> Running golangci-lint" 79 | GOWORK=off golangci-lint run --build-tags '$(GOTAGS)' 80 | .PHONY: lint -------------------------------------------------------------------------------- /child/command-prep.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build !windows 5 | // +build !windows 6 | 7 | package child 8 | 9 | import ( 10 | "os/exec" 11 | "strings" 12 | ) 13 | 14 | // Evaluates the command slice for the different possible formats. 15 | // Returns the command slice ready to pass to exec.Command. 16 | // Returns a boolean 'true' if it wrapped the call in 'sh -c' so the caller 17 | // knows it needs to setpgid to get signal propagation. 18 | func CommandPrep(command []string) ([]string, bool, error) { 19 | switch { 20 | case len(command) == 1 && len(strings.Fields(command[0])) > 1: 21 | // command is []string{"command using arguments or shell features"} 22 | shell := "sh" 23 | // default to 'sh' on path, else try a couple common absolute paths 24 | if _, err := exec.LookPath(shell); err != nil { 25 | shell = "" 26 | for _, sh := range []string{"/bin/sh", "/usr/bin/sh"} { 27 | if absPath, err := exec.LookPath(sh); err == nil { 28 | shell = absPath 29 | break 30 | } 31 | } 32 | } 33 | if shell == "" { 34 | return []string{}, false, exec.ErrNotFound 35 | } 36 | cmd := []string{shell, "-c", command[0]} 37 | return cmd, true, nil 38 | case len(command) >= 1 && len(strings.TrimSpace(command[0])) > 0: 39 | // command is already good ([]string{"foo"}, []string{"foo", "bar"}, ..) 40 | return command, false, nil 41 | default: 42 | // command is []string{} or []string{""} 43 | return []string{}, false, exec.ErrNotFound 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /child/command-prep_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package child 5 | 6 | import ( 7 | "os/exec" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func Test_CommandPrep(t *testing.T) { 13 | type cmd []string 14 | cases := []struct { 15 | n string 16 | in cmd 17 | out cmd 18 | subsh bool 19 | err error 20 | }{ 21 | {n: "empty", in: cmd{}, out: cmd{}, err: exec.ErrNotFound}, 22 | {n: "''", in: cmd{""}, out: cmd{}, err: exec.ErrNotFound}, 23 | {n: "' '", in: cmd{" "}, out: cmd{}, err: exec.ErrNotFound}, 24 | {n: "'f'", in: cmd{"foo"}, out: cmd{"foo"}, err: nil}, 25 | {n: "'f b'", in: cmd{"foo bar"}, subsh: true, out: cmd{"sh", "-c", "foo bar"}, err: nil}, 26 | {n: "'f','b'", in: cmd{"foo", "bar"}, out: cmd{"foo", "bar"}, err: nil}, 27 | {n: "'f','b','z'", in: cmd{"foo", "bar", "zed"}, out: cmd{"foo", "bar", "zed"}, err: nil}, 28 | } 29 | for _, tc := range cases { 30 | t.Run(tc.n, func(t *testing.T) { 31 | out, subsh, err := CommandPrep(tc.in) 32 | if !reflect.DeepEqual(cmd(out), tc.out) { 33 | t.Errorf("bad commandPrep command output;"+ 34 | "wanted: %#v, got %#v", tc.out, out) 35 | } 36 | if err != tc.err { 37 | t.Errorf("bad prepCommand error. wanted: %v, got %v", tc.err, err) 38 | } 39 | if tc.subsh != subsh { 40 | t.Errorf("incorrectly marked as using subshell") 41 | } 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /child/command-prep_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build windows 5 | // +build windows 6 | 7 | package child 8 | 9 | import ( 10 | "os/exec" 11 | "strings" 12 | ) 13 | 14 | // Simplified command prep for windows. Only supports single or slice commands 15 | // and doesn't wrap anything in a shell call (so bool is always false). 16 | func CommandPrep(command []string) ([]string, bool, error) { 17 | switch { 18 | case len(command) == 1 && len(strings.Fields(command[0])) == 1: 19 | // command is []string{"foo"} 20 | return []string{command[0]}, false, nil 21 | case len(command) > 1: 22 | // command is []string{"foo", "bar"} 23 | return command, false, nil 24 | default: 25 | // command is []string{}, []string{""}, []string{"foo bar"} 26 | return []string{}, false, exec.ErrNotFound 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /child/sys_nix.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build !windows 5 | // +build !windows 6 | 7 | package child 8 | 9 | import ( 10 | "os/exec" 11 | "syscall" 12 | ) 13 | 14 | func setSysProcAttr(cmd *exec.Cmd, setpgid, setsid bool) { 15 | cmd.SysProcAttr = &syscall.SysProcAttr{ 16 | Setpgid: setpgid, 17 | Setsid: setsid, 18 | } 19 | } 20 | 21 | func processNotFoundErr(err error) bool { 22 | // ESRCH == no such process, ie. already exited 23 | return err == syscall.ESRCH 24 | } 25 | -------------------------------------------------------------------------------- /child/sys_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build windows 5 | // +build windows 6 | 7 | package child 8 | 9 | import "os/exec" 10 | 11 | func setSysProcAttr(cmd *exec.Cmd, setpgid, setsid bool) {} 12 | 13 | func processNotFoundErr(err error) bool { 14 | return false 15 | } 16 | -------------------------------------------------------------------------------- /config/auth.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package config 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "strings" 10 | ) 11 | 12 | // ErrAuthStringEmpty is the error returned with authentication is provided, 13 | // but empty. 14 | var ErrAuthStringEmpty = errors.New("auth: cannot be empty") 15 | 16 | // AuthConfig is the HTTP basic authentication data. 17 | // Skip passwords in json output that is used for logging. 18 | type AuthConfig struct { 19 | Enabled *bool `mapstructure:"enabled"` 20 | Username *string `mapstructure:"username"` 21 | Password *string `mapstructure:"password" json:"-"` 22 | } 23 | 24 | // DefaultAuthConfig is the default configuration. 25 | func DefaultAuthConfig() *AuthConfig { 26 | return &AuthConfig{} 27 | } 28 | 29 | // ParseAuthConfig parses the auth into username:password. 30 | func ParseAuthConfig(s string) (*AuthConfig, error) { 31 | if s == "" { 32 | return nil, ErrAuthStringEmpty 33 | } 34 | 35 | var a AuthConfig 36 | 37 | if strings.Contains(s, ":") { 38 | split := strings.SplitN(s, ":", 2) 39 | a.Username = String(split[0]) 40 | a.Password = String(split[1]) 41 | } else { 42 | a.Username = String(s) 43 | } 44 | 45 | return &a, nil 46 | } 47 | 48 | // Copy returns a deep copy of this configuration. 49 | func (c *AuthConfig) Copy() *AuthConfig { 50 | if c == nil { 51 | return nil 52 | } 53 | 54 | var o AuthConfig 55 | o.Enabled = c.Enabled 56 | o.Username = c.Username 57 | o.Password = c.Password 58 | return &o 59 | } 60 | 61 | // Merge combines all values in this configuration with the values in the other 62 | // configuration, with values in the other configuration taking precedence. 63 | // Maps and slices are merged, most other values are overwritten. Complex 64 | // structs define their own merge functionality. 65 | func (c *AuthConfig) Merge(o *AuthConfig) *AuthConfig { 66 | if c == nil { 67 | if o == nil { 68 | return nil 69 | } 70 | return o.Copy() 71 | } 72 | 73 | if o == nil { 74 | return c.Copy() 75 | } 76 | 77 | r := c.Copy() 78 | 79 | if o.Enabled != nil { 80 | r.Enabled = o.Enabled 81 | } 82 | 83 | if o.Username != nil { 84 | r.Username = o.Username 85 | } 86 | 87 | if o.Password != nil { 88 | r.Password = o.Password 89 | } 90 | 91 | return r 92 | } 93 | 94 | // Finalize ensures there no nil pointers. 95 | func (c *AuthConfig) Finalize() { 96 | if c.Enabled == nil { 97 | c.Enabled = Bool(false || 98 | StringPresent(c.Username) || 99 | StringPresent(c.Password)) 100 | } 101 | if c.Username == nil { 102 | c.Username = String("") 103 | } 104 | 105 | if c.Password == nil { 106 | c.Password = String("") 107 | } 108 | 109 | if c.Enabled == nil { 110 | c.Enabled = Bool(*c.Username != "" || *c.Password != "") 111 | } 112 | } 113 | 114 | // GoString defines the printable version of this struct. 115 | func (c *AuthConfig) GoString() string { 116 | if c == nil { 117 | return "(*AuthConfig)(nil)" 118 | } 119 | 120 | return fmt.Sprintf("&AuthConfig{"+ 121 | "Enabled:%s, "+ 122 | "Username:%s, "+ 123 | "Password:%s"+ 124 | "}", 125 | BoolGoString(c.Enabled), 126 | StringGoString(c.Username), 127 | StringGoString(c.Password), 128 | ) 129 | } 130 | 131 | // String is the string representation of this authentication. If authentication 132 | // is not enabled, this returns the empty string. The username and password will 133 | // be separated by a colon. 134 | func (c *AuthConfig) String() string { 135 | if !BoolVal(c.Enabled) { 136 | return "" 137 | } 138 | 139 | if c.Password != nil { 140 | return fmt.Sprintf("%s:%s", StringVal(c.Username), StringVal(c.Password)) 141 | } 142 | 143 | return StringVal(c.Username) 144 | } 145 | -------------------------------------------------------------------------------- /config/dedup.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package config 5 | 6 | import ( 7 | "fmt" 8 | "time" 9 | ) 10 | 11 | const ( 12 | // DefaultDedupPrefix is the default prefix used for deduplication mode. 13 | DefaultDedupPrefix = "consul-template/dedup/" 14 | 15 | // DefaultDedupTTL is the default TTL for deduplicate mode. 16 | DefaultDedupTTL = 15 * time.Second 17 | 18 | // DefaultDedupMaxStale is the default max staleness for the deduplication 19 | // manager. 20 | DefaultDedupMaxStale = DefaultMaxStale 21 | 22 | // DefaultDedupBlockQueryWaitTime is the default amount of time to do a blocking query for the deduplication 23 | DefaultDedupBlockQueryWaitTime = 60 * time.Second 24 | ) 25 | 26 | // DedupConfig is used to enable the de-duplication mode, which depends 27 | // on electing a leader per-template and watching of a key. This is used 28 | // to reduce the cost of many instances of CT running the same template. 29 | type DedupConfig struct { 30 | // Controls if deduplication mode is enabled 31 | Enabled *bool `mapstructure:"enabled"` 32 | 33 | // MaxStale is the maximum amount of time to allow for stale queries. 34 | MaxStale *time.Duration `mapstructure:"max_stale"` 35 | 36 | // Controls the KV prefix used. Defaults to defaultDedupPrefix 37 | Prefix *string `mapstructure:"prefix"` 38 | 39 | // TTL is the Session TTL used for lock acquisition, defaults to 15 seconds. 40 | TTL *time.Duration `mapstructure:"ttl"` 41 | 42 | // BlockQueryWaitTime is amount of time to do a blocking query for, defaults to 60 seconds. 43 | BlockQueryWaitTime *time.Duration `mapstructure:"block_query_wait"` 44 | } 45 | 46 | // DefaultDedupConfig returns a configuration that is populated with the 47 | // default values. 48 | func DefaultDedupConfig() *DedupConfig { 49 | return &DedupConfig{} 50 | } 51 | 52 | // Copy returns a deep copy of this configuration. 53 | func (c *DedupConfig) Copy() *DedupConfig { 54 | if c == nil { 55 | return nil 56 | } 57 | 58 | var o DedupConfig 59 | o.Enabled = c.Enabled 60 | o.MaxStale = c.MaxStale 61 | o.Prefix = c.Prefix 62 | o.TTL = c.TTL 63 | o.BlockQueryWaitTime = c.BlockQueryWaitTime 64 | return &o 65 | } 66 | 67 | // Merge combines all values in this configuration with the values in the other 68 | // configuration, with values in the other configuration taking precedence. 69 | // Maps and slices are merged, most other values are overwritten. Complex 70 | // structs define their own merge functionality. 71 | func (c *DedupConfig) Merge(o *DedupConfig) *DedupConfig { 72 | if c == nil { 73 | if o == nil { 74 | return nil 75 | } 76 | return o.Copy() 77 | } 78 | 79 | if o == nil { 80 | return c.Copy() 81 | } 82 | 83 | r := c.Copy() 84 | 85 | if o.Enabled != nil { 86 | r.Enabled = o.Enabled 87 | } 88 | 89 | if o.MaxStale != nil { 90 | r.MaxStale = o.MaxStale 91 | } 92 | 93 | if o.Prefix != nil { 94 | r.Prefix = o.Prefix 95 | } 96 | 97 | if o.TTL != nil { 98 | r.TTL = o.TTL 99 | } 100 | 101 | if o.BlockQueryWaitTime != nil { 102 | r.BlockQueryWaitTime = o.BlockQueryWaitTime 103 | } 104 | 105 | return r 106 | } 107 | 108 | // Finalize ensures there no nil pointers. 109 | func (c *DedupConfig) Finalize() { 110 | if c.Enabled == nil { 111 | c.Enabled = Bool(false || 112 | TimeDurationPresent(c.MaxStale) || 113 | StringPresent(c.Prefix) || 114 | TimeDurationPresent(c.TTL) || 115 | TimeDurationPresent(c.BlockQueryWaitTime)) 116 | } 117 | 118 | if c.MaxStale == nil { 119 | c.MaxStale = TimeDuration(DefaultDedupMaxStale) 120 | } 121 | 122 | if c.Prefix == nil { 123 | c.Prefix = String(DefaultDedupPrefix) 124 | } 125 | 126 | if c.TTL == nil { 127 | c.TTL = TimeDuration(DefaultDedupTTL) 128 | } 129 | 130 | if c.BlockQueryWaitTime == nil { 131 | c.BlockQueryWaitTime = TimeDuration(DefaultDedupBlockQueryWaitTime) 132 | } 133 | } 134 | 135 | // GoString defines the printable version of this struct. 136 | func (c *DedupConfig) GoString() string { 137 | if c == nil { 138 | return "(*DedupConfig)(nil)" 139 | } 140 | return fmt.Sprintf("&DedupConfig{"+ 141 | "Enabled:%s, "+ 142 | "MaxStale:%s, "+ 143 | "Prefix:%s, "+ 144 | "TTL:%s, "+ 145 | "BlockQueryWaitTime:%s"+ 146 | "}", 147 | BoolGoString(c.Enabled), 148 | TimeDurationGoString(c.MaxStale), 149 | StringGoString(c.Prefix), 150 | TimeDurationGoString(c.TTL), 151 | TimeDurationGoString(c.BlockQueryWaitTime), 152 | ) 153 | } 154 | -------------------------------------------------------------------------------- /config/default_delimiters.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package config 5 | 6 | // DefaultDelims is used to configure the default delimiters used for all templates 7 | type DefaultDelims struct { 8 | // Left is the left delimiter for templating 9 | Left *string `mapstructure:"left"` 10 | 11 | // Right is the right delimiter for templating 12 | Right *string `mapstructure:"right"` 13 | } 14 | 15 | // DefaultDefaultDelims returns the default DefaultDelims 16 | func DefaultDefaultDelims() *DefaultDelims { 17 | return &DefaultDelims{} 18 | } 19 | 20 | // Copy returns a copy of the DefaultDelims 21 | func (c *DefaultDelims) Copy() *DefaultDelims { 22 | if c == nil { 23 | return nil 24 | } 25 | 26 | return &DefaultDelims{ 27 | Left: c.Left, 28 | Right: c.Right, 29 | } 30 | } 31 | 32 | // Merge merges the DefaultDelims 33 | func (c *DefaultDelims) Merge(o *DefaultDelims) *DefaultDelims { 34 | if c == nil { 35 | if o == nil { 36 | return nil 37 | } 38 | return o.Copy() 39 | } 40 | 41 | if o == nil { 42 | return c.Copy() 43 | } 44 | 45 | r := c.Copy() 46 | 47 | if o.Left != nil { 48 | r.Left = o.Left 49 | } 50 | 51 | if o.Right != nil { 52 | r.Right = o.Right 53 | } 54 | 55 | return r 56 | } 57 | -------------------------------------------------------------------------------- /config/default_delimiters_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package config 5 | 6 | import ( 7 | "fmt" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestDefaultDelims_Copy(t *testing.T) { 13 | cases := []struct { 14 | name string 15 | a *DefaultDelims 16 | }{ 17 | { 18 | "nil", 19 | nil, 20 | }, 21 | { 22 | "empty", 23 | &DefaultDelims{}, 24 | }, 25 | { 26 | "copy", 27 | &DefaultDelims{ 28 | Left: String("<<"), 29 | Right: String(">>"), 30 | }, 31 | }, 32 | } 33 | 34 | for i, tc := range cases { 35 | t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) { 36 | r := tc.a.Copy() 37 | if !reflect.DeepEqual(tc.a, r) { 38 | t.Errorf("\nexp: %#v\nact: %#v", tc.a, r) 39 | } 40 | }) 41 | } 42 | } 43 | 44 | func TestDefaultDelims_Merge(t *testing.T) { 45 | cases := []struct { 46 | name string 47 | a *DefaultDelims 48 | b *DefaultDelims 49 | r *DefaultDelims 50 | }{ 51 | { 52 | "nil_a", 53 | nil, 54 | &DefaultDelims{}, 55 | &DefaultDelims{}, 56 | }, 57 | { 58 | "nil_b", 59 | &DefaultDelims{}, 60 | nil, 61 | &DefaultDelims{}, 62 | }, 63 | { 64 | "nil_both", 65 | nil, 66 | nil, 67 | nil, 68 | }, 69 | { 70 | "empty", 71 | &DefaultDelims{}, 72 | &DefaultDelims{}, 73 | &DefaultDelims{}, 74 | }, 75 | { 76 | "left_delim_l", 77 | &DefaultDelims{Left: String("<<")}, 78 | &DefaultDelims{}, 79 | &DefaultDelims{Left: String("<<")}, 80 | }, 81 | { 82 | "left_delim_r", 83 | &DefaultDelims{}, 84 | &DefaultDelims{Left: String("<<")}, 85 | &DefaultDelims{Left: String("<<")}, 86 | }, 87 | { 88 | "left_delim_r2", 89 | &DefaultDelims{Left: String(">>")}, 90 | &DefaultDelims{Left: String("<<")}, 91 | &DefaultDelims{Left: String("<<")}, 92 | }, 93 | { 94 | "right_delim_l", 95 | &DefaultDelims{Right: String(">>")}, 96 | &DefaultDelims{}, 97 | &DefaultDelims{Right: String(">>")}, 98 | }, 99 | { 100 | "right_delim_r", 101 | &DefaultDelims{}, 102 | &DefaultDelims{Right: String(">>")}, 103 | &DefaultDelims{Right: String(">>")}, 104 | }, 105 | { 106 | "right_delim_r2", 107 | &DefaultDelims{Right: String("<<")}, 108 | &DefaultDelims{Right: String(">>")}, 109 | &DefaultDelims{Right: String(">>")}, 110 | }, 111 | } 112 | 113 | for i, tc := range cases { 114 | t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) { 115 | r := tc.a.Merge(tc.b) 116 | if !reflect.DeepEqual(tc.r, r) { 117 | t.Errorf("\nexp: %#v\nact: %#v", tc.r, r) 118 | } 119 | }) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /config/logfile.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package config 5 | 6 | import ( 7 | "fmt" 8 | "time" 9 | 10 | "github.com/hashicorp/consul-template/version" 11 | ) 12 | 13 | var ( 14 | // DefaultLogFileName is the default filename if the user didn't specify one 15 | // which means that the user specified a directory to log to 16 | DefaultLogFileName = fmt.Sprintf("%s.log", version.Name) 17 | 18 | // DefaultLogRotateDuration is the default time taken by the agent to rotate logs 19 | DefaultLogRotateDuration = 24 * time.Hour 20 | ) 21 | 22 | type LogFileConfig struct { 23 | // LogFilePath is the path to the file the logs get written to 24 | LogFilePath *string `mapstructure:"path"` 25 | 26 | // LogRotateBytes is the maximum number of bytes that should be written to a log 27 | // file 28 | LogRotateBytes *int `mapstructure:"log_rotate_bytes"` 29 | 30 | // LogRotateDuration is the time after which log rotation needs to be performed 31 | LogRotateDuration *time.Duration `mapstructure:"log_rotate_duration"` 32 | 33 | // LogRotateMaxFiles is the maximum number of log file archives to keep 34 | LogRotateMaxFiles *int `mapstructure:"log_rotate_max_files"` 35 | } 36 | 37 | // DefaultLogFileConfig returns a configuration that is populated with the 38 | // default values. 39 | func DefaultLogFileConfig() *LogFileConfig { 40 | return &LogFileConfig{} 41 | } 42 | 43 | // Copy returns a deep copy of this configuration. 44 | func (c *LogFileConfig) Copy() *LogFileConfig { 45 | if c == nil { 46 | return nil 47 | } 48 | 49 | var o LogFileConfig 50 | o.LogFilePath = c.LogFilePath 51 | o.LogRotateBytes = c.LogRotateBytes 52 | o.LogRotateDuration = c.LogRotateDuration 53 | o.LogRotateMaxFiles = c.LogRotateMaxFiles 54 | return &o 55 | } 56 | 57 | // Merge combines all values in this configuration with the values in the other 58 | // configuration, with values in the other configuration taking precedence. 59 | // Maps and slices are merged, most other values are overwritten. Complex 60 | // structs define their own merge functionality. 61 | func (c *LogFileConfig) Merge(o *LogFileConfig) *LogFileConfig { 62 | if c == nil { 63 | if o == nil { 64 | return nil 65 | } 66 | return o.Copy() 67 | } 68 | 69 | if o == nil { 70 | return c.Copy() 71 | } 72 | 73 | r := c.Copy() 74 | 75 | if o.LogFilePath != nil { 76 | r.LogFilePath = o.LogFilePath 77 | } 78 | 79 | if o.LogRotateBytes != nil { 80 | r.LogRotateBytes = o.LogRotateBytes 81 | } 82 | 83 | if o.LogRotateDuration != nil { 84 | r.LogRotateDuration = o.LogRotateDuration 85 | } 86 | 87 | if o.LogRotateMaxFiles != nil { 88 | r.LogRotateMaxFiles = o.LogRotateMaxFiles 89 | } 90 | 91 | return r 92 | } 93 | 94 | // Finalize ensures there no nil pointers. 95 | func (c *LogFileConfig) Finalize() { 96 | if c.LogFilePath == nil { 97 | c.LogFilePath = String("") 98 | } 99 | 100 | if c.LogRotateBytes == nil { 101 | c.LogRotateBytes = Int(0) 102 | } 103 | 104 | if c.LogRotateDuration == nil { 105 | c.LogRotateDuration = TimeDuration(DefaultLogRotateDuration) 106 | } 107 | 108 | if c.LogRotateMaxFiles == nil { 109 | c.LogRotateMaxFiles = Int(0) 110 | } 111 | } 112 | 113 | // GoString defines the printable version of this struct. 114 | func (c *LogFileConfig) GoString() string { 115 | if c == nil { 116 | return "(*LogFileConfig)(nil)" 117 | } 118 | 119 | return fmt.Sprintf("&LogFileConfig{"+ 120 | "LogFilePath:%s, "+ 121 | "LogRotateBytes:%s, "+ 122 | "LogRotateDuration:%s, "+ 123 | "LogRotateMaxFiles:%s, "+ 124 | "}", 125 | StringGoString(c.LogFilePath), 126 | IntGoString(c.LogRotateBytes), 127 | TimeDurationGoString(c.LogRotateDuration), 128 | IntGoString(c.LogRotateMaxFiles), 129 | ) 130 | } 131 | -------------------------------------------------------------------------------- /config/mapstructure.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package config 5 | 6 | import ( 7 | "log" 8 | "os" 9 | "reflect" 10 | "strconv" 11 | 12 | "github.com/mitchellh/mapstructure" 13 | ) 14 | 15 | // StringToFileModeFunc returns a function that converts strings to os.FileMode 16 | // value. This is designed to be used with mapstructure for parsing out a 17 | // filemode value. 18 | func StringToFileModeFunc() mapstructure.DecodeHookFunc { 19 | return func( 20 | f reflect.Type, 21 | t reflect.Type, 22 | data interface{}, 23 | ) (interface{}, error) { 24 | if f.Kind() != reflect.String { 25 | return data, nil 26 | } 27 | if t != reflect.TypeOf(os.FileMode(0)) { 28 | return data, nil 29 | } 30 | 31 | // Convert it by parsing 32 | v, err := strconv.ParseUint(data.(string), 8, 12) 33 | if err != nil { 34 | return data, err 35 | } 36 | return os.FileMode(v), nil 37 | } 38 | } 39 | 40 | // StringToWaitDurationHookFunc returns a function that converts strings to wait 41 | // value. This is designed to be used with mapstructure for parsing out a wait 42 | // value. 43 | func StringToWaitDurationHookFunc() mapstructure.DecodeHookFunc { 44 | return func( 45 | f reflect.Type, 46 | t reflect.Type, 47 | data interface{}, 48 | ) (interface{}, error) { 49 | if f.Kind() != reflect.String { 50 | return data, nil 51 | } 52 | if t != reflect.TypeOf(WaitConfig{}) { 53 | return data, nil 54 | } 55 | 56 | // Convert it by parsing 57 | return ParseWaitConfig(data.(string)) 58 | } 59 | } 60 | 61 | // ConsulStringToStructFunc checks if the value set for the key should actually 62 | // be a struct and sets the appropriate value in the struct. This is for 63 | // backwards-compatability with older versions of Consul Template. 64 | func ConsulStringToStructFunc() mapstructure.DecodeHookFunc { 65 | return func( 66 | f reflect.Type, 67 | t reflect.Type, 68 | data interface{}, 69 | ) (interface{}, error) { 70 | if t == reflect.TypeOf(ConsulConfig{}) && f.Kind() == reflect.String { 71 | log.Println("[WARN] consul now accepts a stanza instead of a string. " + 72 | "Update your configuration files and change consul = \"\" to " + 73 | "consul { } instead.") 74 | return &ConsulConfig{ 75 | Address: String(data.(string)), 76 | }, nil 77 | } 78 | 79 | return data, nil 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /config/mapstructure_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package config 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "reflect" 10 | "testing" 11 | "time" 12 | 13 | "github.com/mitchellh/mapstructure" 14 | ) 15 | 16 | func TestStringToFileModeFunc(t *testing.T) { 17 | hookFunc := StringToFileModeFunc() 18 | fileModeVal := reflect.ValueOf(os.FileMode(0)) 19 | 20 | cases := []struct { 21 | name string 22 | f, t reflect.Value 23 | expected interface{} 24 | err bool 25 | }{ 26 | {"owner_only", reflect.ValueOf("0600"), fileModeVal, os.FileMode(0o600), false}, 27 | {"high_bits", reflect.ValueOf("4600"), fileModeVal, os.FileMode(0o4600), false}, 28 | 29 | // Prepends 0 automatically 30 | {"add_zero", reflect.ValueOf("600"), fileModeVal, os.FileMode(0o600), false}, 31 | 32 | // Invalid file mode 33 | {"bad_mode", reflect.ValueOf("12345"), fileModeVal, "12345", true}, 34 | 35 | // Invalid syntax 36 | {"bad_syntax", reflect.ValueOf("abcd"), fileModeVal, "abcd", true}, 37 | 38 | // Different type 39 | {"two_strs", reflect.ValueOf("0600"), reflect.ValueOf(""), "0600", false}, 40 | {"uint32", reflect.ValueOf("0600"), reflect.ValueOf(uint32(0)), "0600", false}, 41 | } 42 | 43 | for i, tc := range cases { 44 | t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) { 45 | actual, err := mapstructure.DecodeHookExec(hookFunc, tc.f, tc.t) 46 | if (err != nil) != tc.err { 47 | t.Fatalf("%s", err) 48 | } 49 | if !reflect.DeepEqual(actual, tc.expected) { 50 | t.Errorf("\nexp: %#v\nact: %#v", tc.expected, actual) 51 | } 52 | }) 53 | } 54 | } 55 | 56 | func TestStringToWaitDurationHookFunc(t *testing.T) { 57 | f := StringToWaitDurationHookFunc() 58 | waitVal := reflect.ValueOf(WaitConfig{}) 59 | 60 | cases := []struct { 61 | name string 62 | f, t reflect.Value 63 | expected interface{} 64 | err bool 65 | }{ 66 | { 67 | "min", 68 | reflect.ValueOf("5s"), waitVal, 69 | &WaitConfig{ 70 | Min: TimeDuration(5 * time.Second), 71 | Max: TimeDuration(20 * time.Second), 72 | }, 73 | false, 74 | }, 75 | { 76 | "min_max", 77 | reflect.ValueOf("5s:10s"), waitVal, 78 | &WaitConfig{ 79 | Min: TimeDuration(5 * time.Second), 80 | Max: TimeDuration(10 * time.Second), 81 | }, 82 | false, 83 | }, 84 | { 85 | "not_string", 86 | waitVal, waitVal, 87 | WaitConfig{}, 88 | false, 89 | }, 90 | { 91 | "not_wait", 92 | reflect.ValueOf("test"), reflect.ValueOf(""), 93 | "test", 94 | false, 95 | }, 96 | { 97 | "bad_wait", 98 | reflect.ValueOf("nope"), waitVal, 99 | (*WaitConfig)(nil), 100 | true, 101 | }, 102 | } 103 | 104 | for i, tc := range cases { 105 | t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) { 106 | actual, err := mapstructure.DecodeHookExec(f, tc.f, tc.t) 107 | if (err != nil) != tc.err { 108 | t.Fatalf("%s", err) 109 | } 110 | if !reflect.DeepEqual(tc.expected, actual) { 111 | t.Errorf("\nexp: %#v\nact: %#v", tc.expected, actual) 112 | } 113 | }) 114 | } 115 | } 116 | 117 | func TestConsulStringToStructFunc(t *testing.T) { 118 | f := ConsulStringToStructFunc() 119 | consulVal := reflect.ValueOf(ConsulConfig{}) 120 | 121 | cases := []struct { 122 | name string 123 | f, t reflect.Value 124 | expected interface{} 125 | err bool 126 | }{ 127 | { 128 | "address", 129 | reflect.ValueOf("1.2.3.4"), consulVal, 130 | &ConsulConfig{ 131 | Address: String("1.2.3.4"), 132 | }, 133 | false, 134 | }, 135 | { 136 | "not_string", 137 | consulVal, consulVal, 138 | ConsulConfig{}, 139 | false, 140 | }, 141 | { 142 | "not_consul", 143 | reflect.ValueOf("test"), reflect.ValueOf(""), 144 | "test", 145 | false, 146 | }, 147 | } 148 | 149 | for i, tc := range cases { 150 | t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) { 151 | actual, err := mapstructure.DecodeHookExec(f, tc.f, tc.t) 152 | if (err != nil) != tc.err { 153 | t.Fatalf("%s", err) 154 | } 155 | if !reflect.DeepEqual(tc.expected, actual) { 156 | t.Errorf("\nexp: %#v\nact: %#v", tc.expected, actual) 157 | } 158 | }) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /config/ssl.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package config 5 | 6 | import "fmt" 7 | 8 | const ( 9 | // DefaultSSLVerify is the default value for SSL verification. 10 | DefaultSSLVerify = true 11 | ) 12 | 13 | // SSLConfig is the configuration for SSL. 14 | type SSLConfig struct { 15 | CaCert *string `mapstructure:"ca_cert"` 16 | CaCertBytes *string `mapstructure:"ca_cert_bytes"` 17 | CaPath *string `mapstructure:"ca_path"` 18 | Cert *string `mapstructure:"cert"` 19 | Enabled *bool `mapstructure:"enabled"` 20 | Key *string `mapstructure:"key"` 21 | ServerName *string `mapstructure:"server_name"` 22 | Verify *bool `mapstructure:"verify"` 23 | } 24 | 25 | // DefaultSSLConfig returns a configuration that is populated with the 26 | // default values. 27 | func DefaultSSLConfig() *SSLConfig { 28 | return &SSLConfig{} 29 | } 30 | 31 | // Copy returns a deep copy of this configuration. 32 | func (c *SSLConfig) Copy() *SSLConfig { 33 | if c == nil { 34 | return nil 35 | } 36 | 37 | var o SSLConfig 38 | o.CaCert = c.CaCert 39 | o.CaCertBytes = c.CaCertBytes 40 | o.CaPath = c.CaPath 41 | o.Cert = c.Cert 42 | o.Enabled = c.Enabled 43 | o.Key = c.Key 44 | o.ServerName = c.ServerName 45 | o.Verify = c.Verify 46 | return &o 47 | } 48 | 49 | // Merge combines all values in this configuration with the values in the other 50 | // configuration, with values in the other configuration taking precedence. 51 | // Maps and slices are merged, most other values are overwritten. Complex 52 | // structs define their own merge functionality. 53 | func (c *SSLConfig) Merge(o *SSLConfig) *SSLConfig { 54 | if c == nil { 55 | if o == nil { 56 | return nil 57 | } 58 | return o.Copy() 59 | } 60 | 61 | if o == nil { 62 | return c.Copy() 63 | } 64 | 65 | r := c.Copy() 66 | 67 | if o.Cert != nil { 68 | r.Cert = o.Cert 69 | } 70 | 71 | if o.CaCert != nil { 72 | r.CaCert = o.CaCert 73 | } 74 | 75 | if o.CaCertBytes != nil { 76 | r.CaCertBytes = o.CaCertBytes 77 | } 78 | 79 | if o.CaPath != nil { 80 | r.CaPath = o.CaPath 81 | } 82 | 83 | if o.Enabled != nil { 84 | r.Enabled = o.Enabled 85 | } 86 | 87 | if o.Key != nil { 88 | r.Key = o.Key 89 | } 90 | 91 | if o.ServerName != nil { 92 | r.ServerName = o.ServerName 93 | } 94 | 95 | if o.Verify != nil { 96 | r.Verify = o.Verify 97 | } 98 | 99 | return r 100 | } 101 | 102 | // Finalize ensures there no nil pointers. 103 | func (c *SSLConfig) Finalize() { 104 | if c.Enabled == nil { 105 | c.Enabled = Bool(false || 106 | StringPresent(c.Cert) || 107 | StringPresent(c.CaCert) || 108 | StringPresent(c.CaCertBytes) || 109 | StringPresent(c.CaPath) || 110 | StringPresent(c.Key) || 111 | StringPresent(c.ServerName) || 112 | BoolPresent(c.Verify)) 113 | } 114 | 115 | if c.Cert == nil { 116 | c.Cert = String("") 117 | } 118 | 119 | if c.CaCert == nil { 120 | c.CaCert = String("") 121 | } 122 | 123 | if c.CaCertBytes == nil { 124 | c.CaCertBytes = String("") 125 | } 126 | 127 | if c.CaPath == nil { 128 | c.CaPath = String("") 129 | } 130 | 131 | if c.Key == nil { 132 | c.Key = String("") 133 | } 134 | 135 | if c.ServerName == nil { 136 | c.ServerName = String("") 137 | } 138 | 139 | if c.Verify == nil { 140 | c.Verify = Bool(DefaultSSLVerify) 141 | } 142 | } 143 | 144 | // GoString defines the printable version of this struct. 145 | func (c *SSLConfig) GoString() string { 146 | if c == nil { 147 | return "(*SSLConfig)(nil)" 148 | } 149 | 150 | return fmt.Sprintf("&SSLConfig{"+ 151 | "CaCert:%s, "+ 152 | "CaCertBytes:%s, "+ 153 | "CaPath:%s, "+ 154 | "Cert:%s, "+ 155 | "Enabled:%s, "+ 156 | "Key:%s, "+ 157 | "ServerName:%s, "+ 158 | "Verify:%s"+ 159 | "}", 160 | StringGoString(c.CaCert), 161 | StringGoString(c.CaCertBytes), 162 | StringGoString(c.CaPath), 163 | StringGoString(c.Cert), 164 | BoolGoString(c.Enabled), 165 | StringGoString(c.Key), 166 | StringGoString(c.ServerName), 167 | BoolGoString(c.Verify), 168 | ) 169 | } 170 | -------------------------------------------------------------------------------- /config/syslog.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package config 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/hashicorp/consul-template/version" 10 | ) 11 | 12 | const ( 13 | // DefaultSyslogFacility is the default facility to log to. 14 | DefaultSyslogFacility = "LOCAL0" 15 | ) 16 | 17 | // DefaultSyslogName is the default app name in syslog. 18 | var DefaultSyslogName = version.Name 19 | 20 | // SyslogConfig is the configuration for syslog. 21 | type SyslogConfig struct { 22 | Enabled *bool `mapstructure:"enabled"` 23 | Facility *string `mapstructure:"facility"` 24 | Name *string `mapstructure:"name"` 25 | } 26 | 27 | // DefaultSyslogConfig returns a configuration that is populated with the 28 | // default values. 29 | func DefaultSyslogConfig() *SyslogConfig { 30 | return &SyslogConfig{} 31 | } 32 | 33 | // Copy returns a deep copy of this configuration. 34 | func (c *SyslogConfig) Copy() *SyslogConfig { 35 | if c == nil { 36 | return nil 37 | } 38 | 39 | var o SyslogConfig 40 | o.Enabled = c.Enabled 41 | o.Facility = c.Facility 42 | o.Name = c.Name 43 | return &o 44 | } 45 | 46 | // Merge combines all values in this configuration with the values in the other 47 | // configuration, with values in the other configuration taking precedence. 48 | // Maps and slices are merged, most other values are overwritten. Complex 49 | // structs define their own merge functionality. 50 | func (c *SyslogConfig) Merge(o *SyslogConfig) *SyslogConfig { 51 | if c == nil { 52 | if o == nil { 53 | return nil 54 | } 55 | return o.Copy() 56 | } 57 | 58 | if o == nil { 59 | return c.Copy() 60 | } 61 | 62 | r := c.Copy() 63 | 64 | if o.Enabled != nil { 65 | r.Enabled = o.Enabled 66 | } 67 | 68 | if o.Facility != nil { 69 | r.Facility = o.Facility 70 | } 71 | 72 | if o.Name != nil { 73 | r.Name = o.Name 74 | } 75 | 76 | return r 77 | } 78 | 79 | // Finalize ensures there no nil pointers. 80 | func (c *SyslogConfig) Finalize() { 81 | if c.Enabled == nil { 82 | c.Enabled = Bool(StringPresent(c.Facility) || StringPresent(c.Name)) 83 | } 84 | 85 | if c.Facility == nil { 86 | c.Facility = String(DefaultSyslogFacility) 87 | } 88 | 89 | if c.Name == nil { 90 | c.Name = String(DefaultSyslogName) 91 | } 92 | } 93 | 94 | // GoString defines the printable version of this struct. 95 | func (c *SyslogConfig) GoString() string { 96 | if c == nil { 97 | return "(*SyslogConfig)(nil)" 98 | } 99 | 100 | return fmt.Sprintf("&SyslogConfig{"+ 101 | "Enabled:%s, "+ 102 | "Facility:%s"+ 103 | "Name:%s"+ 104 | "}", 105 | BoolGoString(c.Enabled), 106 | StringGoString(c.Facility), 107 | StringGoString(c.Name), 108 | ) 109 | } 110 | -------------------------------------------------------------------------------- /config/testdata/foo: -------------------------------------------------------------------------------- 1 | bar 2 | -------------------------------------------------------------------------------- /dependency/catalog_datacenters.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | import ( 7 | "log" 8 | "net/url" 9 | "sort" 10 | "time" 11 | 12 | "github.com/hashicorp/consul/api" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | var ( 17 | // Ensure implements 18 | _ Dependency = (*CatalogDatacentersQuery)(nil) 19 | 20 | // CatalogDatacentersQuerySleepTime is the amount of time to sleep between 21 | // queries, since the endpoint does not support blocking queries. 22 | CatalogDatacentersQuerySleepTime = DefaultNonBlockingQuerySleepTime 23 | ) 24 | 25 | // CatalogDatacentersQuery is the dependency to query all datacenters 26 | type CatalogDatacentersQuery struct { 27 | ignoreFailing bool 28 | 29 | stopCh chan struct{} 30 | } 31 | 32 | // NewCatalogDatacentersQuery creates a new datacenter dependency. 33 | func NewCatalogDatacentersQuery(ignoreFailing bool) (*CatalogDatacentersQuery, error) { 34 | return &CatalogDatacentersQuery{ 35 | ignoreFailing: ignoreFailing, 36 | stopCh: make(chan struct{}, 1), 37 | }, nil 38 | } 39 | 40 | // Fetch queries the Consul API defined by the given client and returns a slice 41 | // of strings representing the datacenters 42 | func (d *CatalogDatacentersQuery) Fetch(clients *ClientSet, opts *QueryOptions) (interface{}, *ResponseMetadata, error) { 43 | opts = opts.Merge(&QueryOptions{}) 44 | 45 | log.Printf("[TRACE] %s: GET %s", d, &url.URL{ 46 | Path: "/v1/catalog/datacenters", 47 | RawQuery: opts.String(), 48 | }) 49 | 50 | // This is certainly not elegant, but the datacenters endpoint does not support 51 | // blocking queries, so we are going to "fake it until we make it". When we 52 | // first query, the LastIndex will be "0", meaning we should immediately 53 | // return data, but future calls will include a LastIndex. If we have a 54 | // LastIndex in the query metadata, sleep for 15 seconds before asking Consul 55 | // again. 56 | // 57 | // This is probably okay given the frequency in which datacenters actually 58 | // change, but is technically not edge-triggering. 59 | if opts.WaitIndex != 0 { 60 | log.Printf("[TRACE] %s: long polling for %s", d, CatalogDatacentersQuerySleepTime) 61 | 62 | select { 63 | case <-d.stopCh: 64 | return nil, nil, ErrStopped 65 | case <-time.After(CatalogDatacentersQuerySleepTime): 66 | } 67 | } 68 | 69 | result, err := clients.Consul().Catalog().Datacenters() 70 | if err != nil { 71 | return nil, nil, errors.Wrap(err, d.String()) 72 | } 73 | 74 | // If the user opted in for skipping "down" datacenters, figure out which 75 | // datacenters are down. 76 | if d.ignoreFailing { 77 | dcs := make([]string, 0, len(result)) 78 | for _, dc := range result { 79 | if _, _, err := clients.Consul().Catalog().Services(&api.QueryOptions{ 80 | Datacenter: dc, 81 | AllowStale: false, 82 | RequireConsistent: true, 83 | }); err == nil { 84 | dcs = append(dcs, dc) 85 | } 86 | } 87 | result = dcs 88 | } 89 | 90 | log.Printf("[TRACE] %s: returned %d results", d, len(result)) 91 | 92 | sort.Strings(result) 93 | 94 | return respWithMetadata(result) 95 | } 96 | 97 | // CanShare returns if this dependency is shareable. 98 | func (d *CatalogDatacentersQuery) CanShare() bool { 99 | return true 100 | } 101 | 102 | // String returns the human-friendly version of this dependency. 103 | func (d *CatalogDatacentersQuery) String() string { 104 | return "catalog.datacenters" 105 | } 106 | 107 | // Stop terminates this dependency's fetch. 108 | func (d *CatalogDatacentersQuery) Stop() { 109 | close(d.stopCh) 110 | } 111 | 112 | // Type returns the type of this dependency. 113 | func (d *CatalogDatacentersQuery) Type() Type { 114 | return TypeConsul 115 | } 116 | -------------------------------------------------------------------------------- /dependency/catalog_datacenters_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | import ( 7 | "fmt" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func init() { 15 | CatalogDatacentersQuerySleepTime = 50 * time.Millisecond 16 | } 17 | 18 | func TestNewCatalogDatacentersQuery(t *testing.T) { 19 | cases := []struct { 20 | name string 21 | exp *CatalogDatacentersQuery 22 | err bool 23 | }{ 24 | { 25 | "empty", 26 | &CatalogDatacentersQuery{}, 27 | false, 28 | }, 29 | } 30 | 31 | for i, tc := range cases { 32 | t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) { 33 | act, err := NewCatalogDatacentersQuery(false) 34 | if (err != nil) != tc.err { 35 | t.Fatal(err) 36 | } 37 | 38 | if act != nil { 39 | act.stopCh = nil 40 | } 41 | 42 | assert.Equal(t, tc.exp, act) 43 | }) 44 | } 45 | } 46 | 47 | func TestCatalogDatacentersQuery_Fetch(t *testing.T) { 48 | cases := []struct { 49 | name string 50 | exp []string 51 | }{ 52 | { 53 | "default", 54 | []string{"dc1"}, 55 | }, 56 | } 57 | 58 | for i, tc := range cases { 59 | t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) { 60 | d, err := NewCatalogDatacentersQuery(false) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | 65 | act, _, err := d.Fetch(testClients, nil) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | assert.Equal(t, tc.exp, act) 71 | }) 72 | } 73 | 74 | t.Run("stops", func(t *testing.T) { 75 | d, err := NewCatalogDatacentersQuery(false) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | 80 | dataCh := make(chan interface{}, 1) 81 | errCh := make(chan error, 1) 82 | go func() { 83 | for { 84 | data, _, err := d.Fetch(testClients, &QueryOptions{WaitIndex: 10}) 85 | if err != nil { 86 | errCh <- err 87 | return 88 | } 89 | dataCh <- data 90 | } 91 | }() 92 | 93 | select { 94 | case err := <-errCh: 95 | t.Fatal(err) 96 | case <-dataCh: 97 | } 98 | 99 | d.Stop() 100 | 101 | select { 102 | case err := <-errCh: 103 | if err != ErrStopped { 104 | t.Fatal(err) 105 | } 106 | case <-time.After(100 * time.Millisecond): 107 | t.Errorf("did not stop") 108 | } 109 | }) 110 | 111 | t.Run("fires_changes", func(t *testing.T) { 112 | d, err := NewCatalogDatacentersQuery(false) 113 | if err != nil { 114 | t.Fatal(err) 115 | } 116 | 117 | _, qm, err := d.Fetch(testClients, nil) 118 | if err != nil { 119 | t.Fatal(err) 120 | } 121 | 122 | dataCh := make(chan interface{}, 1) 123 | errCh := make(chan error, 1) 124 | go func() { 125 | data, _, err := d.Fetch(testClients, &QueryOptions{WaitIndex: qm.LastIndex}) 126 | if err != nil { 127 | errCh <- err 128 | return 129 | } 130 | dataCh <- data 131 | }() 132 | 133 | select { 134 | case err := <-errCh: 135 | t.Fatal(err) 136 | case <-dataCh: 137 | } 138 | }) 139 | } 140 | 141 | func TestCatalogDatacentersQuery_String(t *testing.T) { 142 | cases := []struct { 143 | name string 144 | exp string 145 | }{ 146 | { 147 | "empty", 148 | "catalog.datacenters", 149 | }, 150 | } 151 | 152 | for i, tc := range cases { 153 | t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) { 154 | d, err := NewCatalogDatacentersQuery(false) 155 | if err != nil { 156 | t.Fatal(err) 157 | } 158 | assert.Equal(t, tc.exp, d.String()) 159 | }) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /dependency/catalog_services.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | import ( 7 | "encoding/gob" 8 | "fmt" 9 | "log" 10 | "net/url" 11 | "regexp" 12 | "sort" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | var ( 18 | // Ensure implements 19 | _ Dependency = (*CatalogServicesQuery)(nil) 20 | 21 | // CatalogServicesQueryRe is the regular expression to use for CatalogServicesQuery. 22 | CatalogServicesQueryRe = regexp.MustCompile(`\A` + queryRe + dcRe + `\z`) 23 | ) 24 | 25 | func init() { 26 | gob.Register([]*CatalogSnippet{}) 27 | } 28 | 29 | // CatalogSnippet is a catalog entry in Consul. 30 | type CatalogSnippet struct { 31 | Name string 32 | Tags ServiceTags 33 | } 34 | 35 | // CatalogServicesQuery is the representation of a requested catalog service 36 | // dependency from inside a template. 37 | type CatalogServicesQuery struct { 38 | stopCh chan struct{} 39 | 40 | dc string 41 | namespace string 42 | partition string 43 | } 44 | 45 | // NewCatalogServicesQuery parses a string of the format @dc. 46 | func NewCatalogServicesQuery(s string) (*CatalogServicesQuery, error) { 47 | if !CatalogServicesQueryRe.MatchString(s) { 48 | return nil, fmt.Errorf("catalog.services: invalid format: %q", s) 49 | } 50 | 51 | m := regexpMatch(CatalogServicesQueryRe, s) 52 | queryParams, err := GetConsulQueryOpts(m, "catalog.services") 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | return &CatalogServicesQuery{ 58 | stopCh: make(chan struct{}, 1), 59 | dc: m["dc"], 60 | namespace: queryParams.Get(QueryNamespace), 61 | partition: queryParams.Get(QueryPartition), 62 | }, nil 63 | } 64 | 65 | // Fetch queries the Consul API defined by the given client and returns a slice 66 | // of CatalogService objects. 67 | func (d *CatalogServicesQuery) Fetch(clients *ClientSet, opts *QueryOptions) (interface{}, *ResponseMetadata, error) { 68 | select { 69 | case <-d.stopCh: 70 | return nil, nil, ErrStopped 71 | default: 72 | } 73 | 74 | // default to the query params present while creating NewCatalogServicesQuery 75 | // and then merge with the query params present in the query 76 | defaultOpts := &QueryOptions{ 77 | Datacenter: d.dc, 78 | ConsulPartition: d.partition, 79 | ConsulNamespace: d.namespace, 80 | } 81 | 82 | opts = defaultOpts.Merge(opts) 83 | 84 | log.Printf("[TRACE] %s: GET %s", d, &url.URL{ 85 | Path: "/v1/catalog/services", 86 | RawQuery: opts.String(), 87 | }) 88 | 89 | entries, qm, err := clients.Consul().Catalog().Services(opts.ToConsulOpts()) 90 | if err != nil { 91 | return nil, nil, errors.Wrap(err, d.String()) 92 | } 93 | 94 | log.Printf("[TRACE] %s: returned %d results", d, len(entries)) 95 | 96 | var catalogServices []*CatalogSnippet 97 | for name, tags := range entries { 98 | catalogServices = append(catalogServices, &CatalogSnippet{ 99 | Name: name, 100 | Tags: ServiceTags(deepCopyAndSortTags(tags)), 101 | }) 102 | } 103 | 104 | sort.Stable(ByName(catalogServices)) 105 | 106 | rm := &ResponseMetadata{ 107 | LastIndex: qm.LastIndex, 108 | LastContact: qm.LastContact, 109 | } 110 | 111 | return catalogServices, rm, nil 112 | } 113 | 114 | // CanShare returns a boolean if this dependency is shareable. 115 | func (d *CatalogServicesQuery) CanShare() bool { 116 | return true 117 | } 118 | 119 | // String returns the human-friendly version of this dependency. 120 | func (d *CatalogServicesQuery) String() string { 121 | var name string 122 | if d.dc != "" { 123 | name = name + "@" + d.dc 124 | } 125 | if d.partition != "" { 126 | name = name + "@partition=" + d.partition 127 | } 128 | if d.namespace != "" { 129 | name = name + "@ns=" + d.namespace 130 | } 131 | 132 | if len(name) == 0 { 133 | return "catalog.services" 134 | } 135 | 136 | return fmt.Sprintf("catalog.services(%s)", name) 137 | } 138 | 139 | // Stop halts the dependency's fetch function. 140 | func (d *CatalogServicesQuery) Stop() { 141 | close(d.stopCh) 142 | } 143 | 144 | // Type returns the type of this dependency. 145 | func (d *CatalogServicesQuery) Type() Type { 146 | return TypeConsul 147 | } 148 | 149 | // ByName is a sortable slice of CatalogService structs. 150 | type ByName []*CatalogSnippet 151 | 152 | func (s ByName) Len() int { return len(s) } 153 | func (s ByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 154 | func (s ByName) Less(i, j int) bool { return s[i].Name < s[j].Name } 155 | -------------------------------------------------------------------------------- /dependency/connect_ca.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | import ( 7 | "log" 8 | "net/url" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // Ensure implements 14 | var _ Dependency = (*ConnectCAQuery)(nil) 15 | 16 | type ConnectCAQuery struct { 17 | stopCh chan struct{} 18 | } 19 | 20 | func NewConnectCAQuery() *ConnectCAQuery { 21 | return &ConnectCAQuery{ 22 | stopCh: make(chan struct{}, 1), 23 | } 24 | } 25 | 26 | func (d *ConnectCAQuery) Fetch(clients *ClientSet, opts *QueryOptions) ( 27 | interface{}, *ResponseMetadata, error, 28 | ) { 29 | select { 30 | case <-d.stopCh: 31 | return nil, nil, ErrStopped 32 | default: 33 | } 34 | 35 | opts = opts.Merge(nil) 36 | log.Printf("[TRACE] %s: GET %s", d, &url.URL{ 37 | Path: "/v1/agent/connect/ca/roots", 38 | RawQuery: opts.String(), 39 | }) 40 | 41 | certs, md, err := clients.Consul().Agent().ConnectCARoots( 42 | opts.ToConsulOpts()) 43 | if err != nil { 44 | return nil, nil, errors.Wrap(err, d.String()) 45 | } 46 | 47 | log.Printf("[TRACE] %s: returned %d results", d, len(certs.Roots)) 48 | log.Printf("[TRACE] %s: %#v ", d, md) 49 | 50 | rm := &ResponseMetadata{ 51 | LastIndex: md.LastIndex, 52 | LastContact: md.LastContact, 53 | } 54 | 55 | return certs.Roots, rm, nil 56 | } 57 | 58 | func (d *ConnectCAQuery) Stop() { 59 | close(d.stopCh) 60 | } 61 | 62 | func (d *ConnectCAQuery) CanShare() bool { 63 | return false 64 | } 65 | 66 | func (d *ConnectCAQuery) Type() Type { 67 | return TypeConsul 68 | } 69 | 70 | func (d *ConnectCAQuery) String() string { 71 | return "connect.caroots" 72 | } 73 | -------------------------------------------------------------------------------- /dependency/connect_ca_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/hashicorp/consul/api" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestConnectCAQuery_Fetch(t *testing.T) { 14 | d := NewConnectCAQuery() 15 | raw, _, err := d.Fetch(testClients, nil) 16 | assert.NoError(t, err) 17 | act := raw.([]*api.CARoot) 18 | if assert.Len(t, act, 1) { 19 | ca := act[0] 20 | // Root CA name can vary 21 | valid := []string{"Consul CA Root Cert", "Consul CA Primary Cert"} 22 | assert.Contains(t, valid, ca.Name) 23 | assert.True(t, ca.Active) 24 | assert.NotEmpty(t, ca.RootCertPEM) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /dependency/connect_leaf.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "net/url" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // Ensure implements 15 | var _ Dependency = (*ConnectLeafQuery)(nil) 16 | 17 | type ConnectLeafQuery struct { 18 | stopCh chan struct{} 19 | 20 | service string 21 | } 22 | 23 | func NewConnectLeafQuery(service string) *ConnectLeafQuery { 24 | return &ConnectLeafQuery{ 25 | stopCh: make(chan struct{}, 1), 26 | service: service, 27 | } 28 | } 29 | 30 | func (d *ConnectLeafQuery) Fetch(clients *ClientSet, opts *QueryOptions) ( 31 | interface{}, *ResponseMetadata, error, 32 | ) { 33 | select { 34 | case <-d.stopCh: 35 | return nil, nil, ErrStopped 36 | default: 37 | } 38 | opts = opts.Merge(nil) 39 | log.Printf("[TRACE] %s: GET %s", d, &url.URL{ 40 | Path: "/v1/agent/connect/ca/leaf/" + d.service, 41 | RawQuery: opts.String(), 42 | }) 43 | 44 | cert, md, err := clients.Consul().Agent().ConnectCALeaf(d.service, 45 | opts.ToConsulOpts()) 46 | if err != nil { 47 | return nil, nil, errors.Wrap(err, d.String()) 48 | } 49 | 50 | log.Printf("[TRACE] %s: returned response", d) 51 | 52 | rm := &ResponseMetadata{ 53 | LastIndex: md.LastIndex, 54 | LastContact: md.LastContact, 55 | } 56 | 57 | return cert, rm, nil 58 | } 59 | 60 | func (d *ConnectLeafQuery) Stop() { 61 | close(d.stopCh) 62 | } 63 | 64 | func (d *ConnectLeafQuery) CanShare() bool { 65 | return false 66 | } 67 | 68 | func (d *ConnectLeafQuery) Type() Type { 69 | return TypeConsul 70 | } 71 | 72 | func (d *ConnectLeafQuery) String() string { 73 | if d.service != "" { 74 | return fmt.Sprintf("connect.caleaf(%s)", d.service) 75 | } 76 | return "connect.caleaf" 77 | } 78 | -------------------------------------------------------------------------------- /dependency/connect_leaf_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | import ( 7 | "fmt" 8 | "math/rand" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | "github.com/hashicorp/consul/api" 14 | 15 | "github.com/pkg/errors" 16 | "github.com/stretchr/testify/assert" 17 | ) 18 | 19 | func TestNewConnectLeafQuery(t *testing.T) { 20 | act := NewConnectLeafQuery("foo") 21 | act.stopCh = nil 22 | exp := &ConnectLeafQuery{service: "foo"} 23 | assert.Equal(t, exp, act) 24 | } 25 | 26 | func TestConnectLeafQuery_Fetch(t *testing.T) { 27 | // leaf tests require new/unique names to generate the certs correctly 28 | uniqueName := func(name string) string { 29 | return fmt.Sprintf("%s_%d", name, rand.Int31()) 30 | } 31 | 32 | t.Run("empty-service", func(t *testing.T) { 33 | d := NewConnectLeafQuery("") 34 | 35 | _, _, err := d.Fetch(testClients, nil) 36 | prefix := "Unexpected response code: 500 (URI must be either" 37 | if !strings.HasPrefix(errors.Cause(err).Error(), prefix) { 38 | t.Fatalf("Unexpected error: %v", err) 39 | } 40 | }) 41 | t.Run("with-service", func(t *testing.T) { 42 | name := uniqueName("foo") 43 | d := NewConnectLeafQuery(name) 44 | raw, _, err := d.Fetch(testClients, nil) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | cert := raw.(*api.LeafCert) 49 | if cert.Service != name { 50 | t.Fatalf("Unexpected service: %v", cert.Service) 51 | } 52 | if cert.CertPEM == "" { 53 | t.Fatal("Empty cert PEM") 54 | } 55 | if cert.ValidAfter.After(time.Now()) { 56 | t.Fatalf("Bad cert: (bad ValidAfter: %v)", cert.ValidAfter) 57 | } 58 | if cert.ValidBefore.Before(time.Now()) { 59 | t.Fatalf("Bad cert: (bad ValidBefore: %v)", cert.ValidBefore) 60 | } 61 | }) 62 | t.Run("double-check", func(t *testing.T) { 63 | name := uniqueName("foo") 64 | d1 := NewConnectLeafQuery(name) 65 | raw1, _, err := d1.Fetch(testClients, nil) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | cert1 := raw1.(*api.LeafCert) 70 | d2 := NewConnectLeafQuery(name) 71 | raw2, _, err := d2.Fetch(testClients, nil) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | cert2 := raw2.(*api.LeafCert) 76 | if cert1.CertPEM != cert2.CertPEM { 77 | t.Fatalf("Certs should match:\n%v\n%v", 78 | cert1.CertPEM, cert2.CertPEM) 79 | } 80 | }) 81 | } 82 | 83 | func TestConnectLeafQuery_String(t *testing.T) { 84 | cases := []struct { 85 | name string 86 | service string 87 | exp string 88 | }{ 89 | { 90 | "empty", 91 | "", 92 | "connect.caleaf", 93 | }, 94 | { 95 | "service", 96 | "foo", 97 | "connect.caleaf(foo)", 98 | }, 99 | } 100 | 101 | for i, tc := range cases { 102 | t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) { 103 | d := NewConnectLeafQuery(tc.service) 104 | assert.Equal(t, tc.exp, d.String()) 105 | }) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /dependency/consul_common_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | // filter is used as a helper for filtering values out of maps. 7 | func filter(data map[string]string, remove []string) map[string]string { 8 | if data == nil { 9 | return make(map[string]string) 10 | } 11 | for _, k := range remove { 12 | delete(data, k) 13 | } 14 | return data 15 | } 16 | 17 | // filterVersionMeta filters out all version information from the returned 18 | // metadata. It allocates the meta map if it is nil to make the tests backward 19 | // compatible with versions < 1.5.2. 20 | func filterVersionMeta(meta map[string]string) map[string]string { 21 | filteredMeta := []string{ 22 | "raft_version", "serf_protocol_current", 23 | "serf_protocol_min", "serf_protocol_max", "version", 24 | "non_voter", "read_replica", "grpc_port", "grpc_tls_port", "consul-version", 25 | "consul-network-segment", 26 | } 27 | return filter(meta, filteredMeta) 28 | } 29 | 30 | // filterAddresses filters out consul >1.7 ipv4/ipv6 specific entries 31 | // from TaggedAddresses entries on nodes, catlog and health services. 32 | func filterAddresses(addrs map[string]string) map[string]string { 33 | ipvKeys := []string{"lan_ipv4", "wan_ipv4", "lan_ipv6", "wan_ipv6", "lan", "wan"} 34 | return filter(addrs, ipvKeys) 35 | } 36 | -------------------------------------------------------------------------------- /dependency/consul_exported_services.go: -------------------------------------------------------------------------------- 1 | package dependency 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/url" 7 | "slices" 8 | "strings" 9 | 10 | capi "github.com/hashicorp/consul/api" 11 | ) 12 | 13 | const exportedServicesEndpointLabel = "list.exportedServices" 14 | 15 | // Ensure implements 16 | var _ Dependency = (*ListExportedServicesQuery)(nil) 17 | 18 | // ListExportedServicesQuery is the representation of a requested exported services 19 | // dependency from inside a template. 20 | type ListExportedServicesQuery struct { 21 | stopCh chan struct{} 22 | partition string 23 | } 24 | 25 | type ExportedService struct { 26 | // Name of the service 27 | Service string 28 | 29 | // Partition of the service 30 | Partition string 31 | 32 | // Namespace of the service 33 | Namespace string 34 | 35 | // Consumers is a list of downstream consumers of the service. 36 | Consumers ResolvedConsumers 37 | } 38 | 39 | type ResolvedConsumers struct { 40 | Peers []string 41 | Partitions []string 42 | } 43 | 44 | func fromConsulExportedService(svc capi.ResolvedExportedService) ExportedService { 45 | exportedService := ExportedService{ 46 | Service: svc.Service, 47 | Consumers: ResolvedConsumers{ 48 | Partitions: []string{}, 49 | Peers: []string{}, 50 | }, 51 | } 52 | 53 | if len(svc.Consumers.Partitions) > 0 { 54 | exportedService.Consumers.Partitions = slices.Clone(svc.Consumers.Partitions) 55 | } 56 | 57 | if len(svc.Consumers.Peers) > 0 { 58 | exportedService.Consumers.Peers = slices.Clone(svc.Consumers.Peers) 59 | } 60 | 61 | return exportedService 62 | } 63 | 64 | // NewListExportedServicesQuery parses a string of the format @dc. 65 | func NewListExportedServicesQuery(s string) (*ListExportedServicesQuery, error) { 66 | return &ListExportedServicesQuery{ 67 | stopCh: make(chan struct{}), 68 | partition: s, 69 | }, nil 70 | } 71 | 72 | func (c *ListExportedServicesQuery) Fetch(clients *ClientSet, opts *QueryOptions) (interface{}, *ResponseMetadata, error) { 73 | select { 74 | case <-c.stopCh: 75 | return nil, nil, ErrStopped 76 | default: 77 | } 78 | 79 | opts = opts.Merge(&QueryOptions{ 80 | ConsulPartition: c.partition, 81 | }) 82 | 83 | log.Printf("[TRACE] %s: GET %s", c, &url.URL{ 84 | Path: "/v1/exported-services", 85 | RawQuery: opts.String(), 86 | }) 87 | 88 | consulExportedServices, qm, err := clients.Consul().ExportedServices(opts.ToConsulOpts()) 89 | if err != nil { 90 | return nil, nil, fmt.Errorf("%s: %w", c.String(), err) 91 | } 92 | 93 | exportedServices := make([]ExportedService, 0, len(consulExportedServices)) 94 | for _, exportedService := range consulExportedServices { 95 | exportedServices = append(exportedServices, fromConsulExportedService(exportedService)) 96 | } 97 | 98 | log.Printf("[TRACE] %s: returned %d results", c, len(exportedServices)) 99 | 100 | slices.SortStableFunc(exportedServices, func(i, j ExportedService) int { 101 | return strings.Compare(i.Service, j.Service) 102 | }) 103 | 104 | rm := &ResponseMetadata{ 105 | LastContact: qm.LastContact, 106 | LastIndex: qm.LastIndex, 107 | } 108 | 109 | return exportedServices, rm, nil 110 | } 111 | 112 | // CanShare returns if this dependency is shareable when consul-template is running in de-duplication mode. 113 | func (c *ListExportedServicesQuery) CanShare() bool { 114 | return true 115 | } 116 | 117 | func (c *ListExportedServicesQuery) String() string { 118 | return fmt.Sprintf("%s(%s)", exportedServicesEndpointLabel, c.partition) 119 | } 120 | 121 | func (c *ListExportedServicesQuery) Stop() { 122 | close(c.stopCh) 123 | } 124 | 125 | func (c *ListExportedServicesQuery) Type() Type { 126 | return TypeConsul 127 | } 128 | -------------------------------------------------------------------------------- /dependency/consul_partitions.go: -------------------------------------------------------------------------------- 1 | package dependency 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/url" 8 | "slices" 9 | "strings" 10 | "time" 11 | 12 | "github.com/hashicorp/consul/api" 13 | ) 14 | 15 | // Ensure implements 16 | var ( 17 | _ Dependency = (*ListPartitionsQuery)(nil) 18 | 19 | // ListPartitionsQuerySleepTime is the amount of time to sleep between 20 | // queries, since the endpoint does not support blocking queries. 21 | ListPartitionsQuerySleepTime = DefaultNonBlockingQuerySleepTime 22 | ) 23 | 24 | // Partition is a partition in Consul. 25 | type Partition struct { 26 | Name string 27 | Description string 28 | } 29 | 30 | // ListPartitionsQuery is the representation of a requested partitions 31 | // dependency from inside a template. 32 | type ListPartitionsQuery struct { 33 | stopCh chan struct{} 34 | } 35 | 36 | func NewListPartitionsQuery() (*ListPartitionsQuery, error) { 37 | return &ListPartitionsQuery{ 38 | stopCh: make(chan struct{}, 1), 39 | }, nil 40 | } 41 | 42 | func (c *ListPartitionsQuery) Fetch(clients *ClientSet, opts *QueryOptions) (interface{}, *ResponseMetadata, error) { 43 | opts = opts.Merge(&QueryOptions{}) 44 | 45 | log.Printf("[TRACE] %s: GET %s", c, &url.URL{ 46 | Path: "/v1/partitions", 47 | RawQuery: opts.String(), 48 | }) 49 | 50 | // This is certainly not elegant, but the partitions endpoint does not support 51 | // blocking queries, so we are going to "fake it until we make it". When we 52 | // first query, the LastIndex will be "0", meaning we should immediately 53 | // return data, but future calls will include a LastIndex. If we have a 54 | // LastIndex in the query metadata, sleep for 15 seconds before asking Consul 55 | // again. 56 | // 57 | // This is probably okay given the frequency in which partitions actually 58 | // change, but is technically not edge-triggering. 59 | if opts.WaitIndex != 0 { 60 | log.Printf("[TRACE] %s: long polling for %s", c, ListPartitionsQuerySleepTime) 61 | 62 | select { 63 | case <-c.stopCh: 64 | return nil, nil, ErrStopped 65 | case <-time.After(ListPartitionsQuerySleepTime): 66 | } 67 | } 68 | 69 | partitions, _, err := clients.Consul().Partitions().List(context.Background(), opts.ToConsulOpts()) 70 | if err != nil { 71 | if strings.Contains(err.Error(), "Invalid URL path") { 72 | return nil, nil, fmt.Errorf("%s: Partitions are an enterprise feature: %w", c.String(), err) 73 | } 74 | 75 | return nil, nil, fmt.Errorf("%s: %w", c.String(), err) 76 | } 77 | 78 | log.Printf("[TRACE] %s: returned %d results", c, len(partitions)) 79 | 80 | slices.SortFunc(partitions, func(i, j *api.Partition) int { 81 | return strings.Compare(i.Name, j.Name) 82 | }) 83 | 84 | resp := []*Partition{} 85 | for _, partition := range partitions { 86 | if partition != nil { 87 | resp = append(resp, &Partition{ 88 | Name: partition.Name, 89 | Description: partition.Description, 90 | }) 91 | } 92 | } 93 | 94 | // Use respWithMetadata which always increments LastIndex and results 95 | // in fetching new data for endpoints that don't support blocking queries 96 | return respWithMetadata(resp) 97 | } 98 | 99 | // CanShare returns if this dependency is shareable when consul-template is running in de-duplication mode. 100 | func (c *ListPartitionsQuery) CanShare() bool { 101 | return true 102 | } 103 | 104 | func (c *ListPartitionsQuery) String() string { 105 | return "list.partitions" 106 | } 107 | 108 | func (c *ListPartitionsQuery) Stop() { 109 | close(c.stopCh) 110 | } 111 | 112 | func (c *ListPartitionsQuery) Type() Type { 113 | return TypeConsul 114 | } 115 | -------------------------------------------------------------------------------- /dependency/consul_partitions_test.go: -------------------------------------------------------------------------------- 1 | package dependency 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func init() { 12 | ListPartitionsQuerySleepTime = 50 * time.Millisecond 13 | } 14 | 15 | func TestListPartitionsQuery_Fetch(t *testing.T) { 16 | if !tenancyHelper.IsConsulEnterprise() { 17 | t.Skip("Enterprise only test") 18 | } 19 | 20 | expected := []*Partition{ 21 | { 22 | Name: "default", 23 | Description: "Builtin Default Partition", 24 | }, 25 | { 26 | Name: "foo", 27 | Description: "", 28 | }, 29 | } 30 | 31 | d, err := NewListPartitionsQuery() 32 | require.NoError(t, err) 33 | 34 | act, _, err := d.Fetch(testClients, nil) 35 | require.NoError(t, err) 36 | assert.Equal(t, expected, act) 37 | } 38 | 39 | func TestListPartitionsQuery_FetchError(t *testing.T) { 40 | if tenancyHelper.IsConsulEnterprise() { 41 | t.Skip("CE only test") 42 | } 43 | 44 | d, err := NewListPartitionsQuery() 45 | require.NoError(t, err) 46 | 47 | _, _, err = d.Fetch(testClients, nil) 48 | require.Error(t, err) 49 | } 50 | -------------------------------------------------------------------------------- /dependency/consul_peering_test.go: -------------------------------------------------------------------------------- 1 | package dependency 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/hashicorp/consul-template/test" 7 | "github.com/hashicorp/consul/api" 8 | "github.com/stretchr/testify/require" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestListPeeringsQuery(t *testing.T) { 16 | cases := []struct { 17 | name string 18 | i string 19 | exp *ListPeeringQuery 20 | err bool 21 | }{ 22 | { 23 | "empty", 24 | "", 25 | &ListPeeringQuery{}, 26 | false, 27 | }, 28 | { 29 | "invalid query param (unsupported key)", 30 | "?unsupported=foo", 31 | nil, 32 | true, 33 | }, 34 | { 35 | "peerings", 36 | "peerings", 37 | nil, 38 | true, 39 | }, 40 | { 41 | "partition", 42 | "?partition=foo", 43 | &ListPeeringQuery{ 44 | partition: "foo", 45 | }, 46 | false, 47 | }, 48 | } 49 | 50 | for i, tc := range cases { 51 | t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) { 52 | act, err := NewListPeeringQuery(tc.i) 53 | if (err != nil) != tc.err { 54 | t.Fatal(err) 55 | } 56 | 57 | if act != nil { 58 | act.stopCh = nil 59 | } 60 | 61 | assert.Equal(t, tc.exp, act) 62 | }) 63 | } 64 | } 65 | 66 | func TestListPeeringsQuery_Fetch(t *testing.T) { 67 | // the peering generated has random IDs, 68 | // we can't assert on the full response, 69 | // we can assert on the peering names though. 70 | expectedPeerNames := []string{ 71 | "bar", 72 | "foo", 73 | } 74 | 75 | p, err := NewListPeeringQuery("") 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | 80 | res, meta, err := p.Fetch(testClients, nil) 81 | require.NoError(t, err) 82 | require.NotNil(t, res) 83 | peerNames := make([]string, 0) 84 | for _, peering := range res.([]*Peering) { 85 | peerNames = append(peerNames, peering.Name) 86 | } 87 | assert.Equal(t, expectedPeerNames, peerNames) 88 | 89 | client := testClients.Consul() 90 | th, err := test.NewTenancyHelper(client) 91 | require.NoError(t, err) 92 | if th.IsConsulEnterprise() { 93 | // set up blocking query with last index 94 | dataCh := make(chan interface{}, 1) 95 | errCh := make(chan error, 1) 96 | go func() { 97 | data, _, err := p.Fetch(testClients, &QueryOptions{WaitIndex: meta.LastIndex}) 98 | if err != nil { 99 | errCh <- err 100 | return 101 | } 102 | dataCh <- data 103 | }() 104 | 105 | tenancy := th.Tenancy("default.baz") 106 | ap := &api.Partition{Name: tenancy.Partition} 107 | partition, _, err := client.Partitions().Create(context.Background(), ap, nil) 108 | defer func() { 109 | _, _ = client.Partitions().Delete(context.Background(), partition.Name, nil) 110 | }() 111 | require.NoError(t, err) 112 | generateReq := api.PeeringGenerateTokenRequest{PeerName: "baz", Partition: tenancy.Partition} 113 | _, _, err = client.Peerings().GenerateToken(context.Background(), generateReq, &api.WriteOptions{}) 114 | require.NoError(t, err) 115 | defer func() { 116 | _, _ = client.Peerings().Delete(context.Background(), generateReq.PeerName, nil) 117 | }() 118 | 119 | // create another peer 120 | err = testClients.createConsulPeerings(tenancy) 121 | require.NoError(t, err) 122 | 123 | select { 124 | case err := <-errCh: 125 | if err != ErrStopped { 126 | t.Fatal(err) 127 | } 128 | case <-time.After(1 * time.Minute): 129 | t.Errorf("did not stop") 130 | case val := <-dataCh: 131 | if val != nil { 132 | require.Equal(t, 3, len(val.([]*Peering))) 133 | } 134 | } 135 | } 136 | } 137 | 138 | func TestListPeeringsQuery_String(t *testing.T) { 139 | cases := []struct { 140 | name string 141 | i string 142 | exp string 143 | }{ 144 | { 145 | "empty", 146 | "", 147 | "list.peerings", 148 | }, 149 | { 150 | "partition", 151 | "?partition=foo", 152 | "list.peerings?partition=foo", 153 | }, 154 | } 155 | 156 | for i, tc := range cases { 157 | t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) { 158 | d, err := NewListPeeringQuery(tc.i) 159 | if err != nil { 160 | t.Fatal(err) 161 | } 162 | str := d.String() 163 | assert.Equal(t, tc.exp, str) 164 | }) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /dependency/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | import "errors" 7 | 8 | // ErrStopped is a special error that is returned when a dependency is 9 | // prematurely stopped, usually due to a configuration reload or a process 10 | // interrupt. 11 | var ErrStopped = errors.New("dependency stopped") 12 | 13 | // ErrContinue is a special error which says to continue (retry) on error. 14 | var ErrContinue = errors.New("dependency continue") 15 | 16 | var ErrLeaseExpired = errors.New("lease expired or is not renewable") 17 | -------------------------------------------------------------------------------- /dependency/file.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "os" 10 | "strings" 11 | "time" 12 | 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | var ( 17 | // Ensure implements 18 | _ Dependency = (*FileQuery)(nil) 19 | 20 | // FileQuerySleepTime is the amount of time to sleep between queries, since 21 | // the fsnotify library is not compatible with solaris and other OSes yet. 22 | FileQuerySleepTime = 2 * time.Second 23 | ) 24 | 25 | // FileQuery represents a local file dependency. 26 | type FileQuery struct { 27 | stopCh chan struct{} 28 | 29 | path string 30 | stat os.FileInfo 31 | } 32 | 33 | // NewFileQuery creates a file dependency from the given path. 34 | func NewFileQuery(s string) (*FileQuery, error) { 35 | s = strings.TrimSpace(s) 36 | if s == "" { 37 | return nil, fmt.Errorf("file: invalid format: %q", s) 38 | } 39 | 40 | return &FileQuery{ 41 | stopCh: make(chan struct{}, 1), 42 | path: s, 43 | }, nil 44 | } 45 | 46 | // Fetch retrieves this dependency and returns the result or any errors that 47 | // occur in the process. 48 | func (d *FileQuery) Fetch(clients *ClientSet, opts *QueryOptions) (interface{}, *ResponseMetadata, error) { 49 | log.Printf("[TRACE] %s: READ %s", d, d.path) 50 | 51 | select { 52 | case <-d.stopCh: 53 | log.Printf("[TRACE] %s: stopped", d) 54 | return "", nil, ErrStopped 55 | case r := <-d.watch(d.stat): 56 | if r.err != nil { 57 | return "", nil, errors.Wrap(r.err, d.String()) 58 | } 59 | 60 | log.Printf("[TRACE] %s: reported change", d) 61 | 62 | data, err := os.ReadFile(d.path) 63 | if err != nil { 64 | return "", nil, errors.Wrap(err, d.String()) 65 | } 66 | 67 | d.stat = r.stat 68 | return respWithMetadata(string(data)) 69 | } 70 | } 71 | 72 | // CanShare returns a boolean if this dependency is shareable. 73 | func (d *FileQuery) CanShare() bool { 74 | return false 75 | } 76 | 77 | // Stop halts the dependency's fetch function. 78 | func (d *FileQuery) Stop() { 79 | close(d.stopCh) 80 | } 81 | 82 | // String returns the human-friendly version of this dependency. 83 | func (d *FileQuery) String() string { 84 | return fmt.Sprintf("file(%s)", d.path) 85 | } 86 | 87 | // Type returns the type of this dependency. 88 | func (d *FileQuery) Type() Type { 89 | return TypeLocal 90 | } 91 | 92 | type watchResult struct { 93 | stat os.FileInfo 94 | err error 95 | } 96 | 97 | // watch watchers the file for changes 98 | func (d *FileQuery) watch(lastStat os.FileInfo) <-chan *watchResult { 99 | ch := make(chan *watchResult, 1) 100 | 101 | go func(lastStat os.FileInfo) { 102 | for { 103 | stat, err := os.Stat(d.path) 104 | if err != nil { 105 | select { 106 | case <-d.stopCh: 107 | return 108 | case ch <- &watchResult{err: err}: 109 | return 110 | } 111 | } 112 | 113 | changed := lastStat == nil || 114 | lastStat.Size() != stat.Size() || 115 | lastStat.ModTime() != stat.ModTime() 116 | 117 | if changed { 118 | select { 119 | case <-d.stopCh: 120 | return 121 | case ch <- &watchResult{stat: stat}: 122 | return 123 | } 124 | } 125 | 126 | time.Sleep(FileQuerySleepTime) 127 | } 128 | }(lastStat) 129 | 130 | return ch 131 | } 132 | -------------------------------------------------------------------------------- /dependency/kv_get.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "net/url" 10 | "regexp" 11 | 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | var ( 16 | // Ensure implements 17 | _ Dependency = (*KVGetQuery)(nil) 18 | 19 | // KVGetQueryRe is the regular expression to use. 20 | KVGetQueryRe = regexp.MustCompile(`\A` + keyRe + queryRe + dcRe + `\z`) 21 | ) 22 | 23 | // KVGetQuery queries the KV store for a single key. 24 | type KVGetQuery struct { 25 | stopCh chan struct{} 26 | 27 | dc string 28 | key string 29 | blockOnNil bool 30 | namespace string 31 | partition string 32 | } 33 | 34 | // NewKVGetQuery parses a string into a dependency. 35 | func NewKVGetQuery(s string) (*KVGetQuery, error) { 36 | if s != "" && !KVGetQueryRe.MatchString(s) { 37 | return nil, fmt.Errorf("kv.get: invalid format: %q", s) 38 | } 39 | 40 | m := regexpMatch(KVGetQueryRe, s) 41 | queryParams, err := GetConsulQueryOpts(m, "kv.get") 42 | if err != nil { 43 | return nil, err 44 | } 45 | return &KVGetQuery{ 46 | stopCh: make(chan struct{}, 1), 47 | dc: m["dc"], 48 | key: m["key"], 49 | namespace: queryParams.Get(QueryNamespace), 50 | partition: queryParams.Get(QueryPartition), 51 | }, nil 52 | } 53 | 54 | // Fetch queries the Consul API defined by the given client. 55 | func (d *KVGetQuery) Fetch(clients *ClientSet, opts *QueryOptions) (interface{}, *ResponseMetadata, error) { 56 | select { 57 | case <-d.stopCh: 58 | return nil, nil, ErrStopped 59 | default: 60 | } 61 | 62 | opts = opts.Merge(&QueryOptions{ 63 | Datacenter: d.dc, 64 | ConsulPartition: d.partition, 65 | ConsulNamespace: d.namespace, 66 | }) 67 | 68 | log.Printf("[TRACE] %s: GET %s", d, &url.URL{ 69 | Path: "/v1/kv/" + d.key, 70 | RawQuery: opts.String(), 71 | }) 72 | 73 | // NOTE that the Consul HTTP KV API returns a 404 on failed gets, but the 74 | // Consul Go API Package ignores those for KV Gets, returning nil data and 75 | // a nil error (ie. only qm will have a value). 76 | pair, qm, err := clients.Consul().KV().Get(d.key, opts.ToConsulOpts()) 77 | if err != nil { 78 | return nil, nil, errors.Wrap(err, d.String()) 79 | } 80 | 81 | rm := &ResponseMetadata{ 82 | LastIndex: qm.LastIndex, 83 | LastContact: qm.LastContact, 84 | BlockOnNil: d.blockOnNil, 85 | } 86 | 87 | if pair == nil { 88 | log.Printf("[TRACE] %s: returned nil", d) 89 | return nil, rm, nil 90 | } 91 | 92 | value := string(pair.Value) 93 | log.Printf("[TRACE] %s: returned %q", d, value) 94 | return value, rm, nil 95 | } 96 | 97 | // EnableBlocking turns this into a blocking KV query. 98 | func (d *KVGetQuery) EnableBlocking() { 99 | d.blockOnNil = true 100 | } 101 | 102 | // CanShare returns a boolean if this dependency is shareable. 103 | func (d *KVGetQuery) CanShare() bool { 104 | return true 105 | } 106 | 107 | // String returns the human-friendly version of this dependency. 108 | func (d *KVGetQuery) String() string { 109 | key := d.key 110 | if d.dc != "" { 111 | key = key + "@" + d.dc 112 | } 113 | if d.partition != "" { 114 | key = key + "@partition=" + d.partition 115 | } 116 | if d.namespace != "" { 117 | key = key + "@ns=" + d.namespace 118 | } 119 | 120 | if d.blockOnNil { 121 | return fmt.Sprintf("kv.block(%s)", key) 122 | } 123 | return fmt.Sprintf("kv.get(%s)", key) 124 | } 125 | 126 | // Stop halts the dependency's fetch function. 127 | func (d *KVGetQuery) Stop() { 128 | close(d.stopCh) 129 | } 130 | 131 | // Type returns the type of this dependency. 132 | func (d *KVGetQuery) Type() Type { 133 | return TypeConsul 134 | } 135 | -------------------------------------------------------------------------------- /dependency/kv_keys.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "net/url" 10 | "regexp" 11 | "strings" 12 | 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | var ( 17 | // Ensure implements 18 | _ Dependency = (*KVKeysQuery)(nil) 19 | 20 | // KVKeysQueryRe is the regular expression to use. 21 | KVKeysQueryRe = regexp.MustCompile(`\A` + prefixRe + queryRe + dcRe + `\z`) 22 | ) 23 | 24 | // KVKeysQuery queries the KV store for a single key. 25 | type KVKeysQuery struct { 26 | stopCh chan struct{} 27 | 28 | dc string 29 | prefix string 30 | namespace string 31 | partition string 32 | } 33 | 34 | // NewKVKeysQuery parses a string into a dependency. 35 | func NewKVKeysQuery(s string) (*KVKeysQuery, error) { 36 | if s != "" && !KVKeysQueryRe.MatchString(s) { 37 | return nil, fmt.Errorf("kv.keys: invalid format: %q", s) 38 | } 39 | 40 | m := regexpMatch(KVKeysQueryRe, s) 41 | queryParams, err := GetConsulQueryOpts(m, "kv.keys") 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return &KVKeysQuery{ 47 | stopCh: make(chan struct{}, 1), 48 | dc: m["dc"], 49 | prefix: m["prefix"], 50 | namespace: queryParams.Get(QueryNamespace), 51 | partition: queryParams.Get(QueryPartition), 52 | }, nil 53 | } 54 | 55 | // Fetch queries the Consul API defined by the given client. 56 | func (d *KVKeysQuery) Fetch(clients *ClientSet, opts *QueryOptions) (interface{}, *ResponseMetadata, error) { 57 | select { 58 | case <-d.stopCh: 59 | return nil, nil, ErrStopped 60 | default: 61 | } 62 | 63 | opts = opts.Merge(&QueryOptions{ 64 | Datacenter: d.dc, 65 | ConsulPartition: d.partition, 66 | ConsulNamespace: d.namespace, 67 | }) 68 | 69 | log.Printf("[TRACE] %s: GET %s", d, &url.URL{ 70 | Path: "/v1/kv/" + d.prefix, 71 | RawQuery: opts.String(), 72 | }) 73 | 74 | list, qm, err := clients.Consul().KV().Keys(d.prefix, "", opts.ToConsulOpts()) 75 | if err != nil { 76 | return nil, nil, errors.Wrap(err, d.String()) 77 | } 78 | 79 | keys := make([]string, len(list)) 80 | for i, v := range list { 81 | v = strings.TrimPrefix(v, d.prefix) 82 | v = strings.TrimLeft(v, "/") 83 | keys[i] = v 84 | } 85 | 86 | log.Printf("[TRACE] %s: returned %d results", d, len(list)) 87 | 88 | rm := &ResponseMetadata{ 89 | LastIndex: qm.LastIndex, 90 | LastContact: qm.LastContact, 91 | } 92 | 93 | return keys, rm, nil 94 | } 95 | 96 | // CanShare returns a boolean if this dependency is shareable. 97 | func (d *KVKeysQuery) CanShare() bool { 98 | return true 99 | } 100 | 101 | // String returns the human-friendly version of this dependency. 102 | func (d *KVKeysQuery) String() string { 103 | prefix := d.prefix 104 | if d.dc != "" { 105 | prefix = prefix + "@" + d.dc 106 | } 107 | if d.partition != "" { 108 | prefix = prefix + "@partition=" + d.partition 109 | } 110 | if d.namespace != "" { 111 | prefix = prefix + "@ns=" + d.namespace 112 | } 113 | return fmt.Sprintf("kv.keys(%s)", prefix) 114 | } 115 | 116 | // Stop halts the dependency's fetch function. 117 | func (d *KVKeysQuery) Stop() { 118 | close(d.stopCh) 119 | } 120 | 121 | // Type returns the type of this dependency. 122 | func (d *KVKeysQuery) Type() Type { 123 | return TypeConsul 124 | } 125 | -------------------------------------------------------------------------------- /dependency/kv_list.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | import ( 7 | "encoding/gob" 8 | "fmt" 9 | "log" 10 | "net/url" 11 | "regexp" 12 | "strings" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | var ( 18 | // Ensure implements 19 | _ Dependency = (*KVListQuery)(nil) 20 | 21 | // KVListQueryRe is the regular expression to use. 22 | KVListQueryRe = regexp.MustCompile(`\A` + prefixRe + queryRe + dcRe + `\z`) 23 | ) 24 | 25 | func init() { 26 | gob.Register([]*KeyPair{}) 27 | } 28 | 29 | // KeyPair is a simple Key-Value pair 30 | type KeyPair struct { 31 | Path string 32 | Key string 33 | Value string 34 | 35 | // Lesser-used, but still valuable keys from api.KV 36 | CreateIndex uint64 37 | ModifyIndex uint64 38 | LockIndex uint64 39 | Flags uint64 40 | Session string 41 | } 42 | 43 | // KVListQuery queries the KV store for a single key. 44 | type KVListQuery struct { 45 | stopCh chan struct{} 46 | 47 | dc string 48 | prefix string 49 | namespace string 50 | partition string 51 | } 52 | 53 | // NewKVListQuery parses a string into a dependency. 54 | func NewKVListQuery(s string) (*KVListQuery, error) { 55 | if s != "" && !KVListQueryRe.MatchString(s) { 56 | return nil, fmt.Errorf("kv.list: invalid format: %q", s) 57 | } 58 | 59 | m := regexpMatch(KVListQueryRe, s) 60 | queryParams, err := GetConsulQueryOpts(m, "kv.list") 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | return &KVListQuery{ 66 | stopCh: make(chan struct{}, 1), 67 | dc: m["dc"], 68 | prefix: m["prefix"], 69 | namespace: queryParams.Get(QueryNamespace), 70 | partition: queryParams.Get(QueryPartition), 71 | }, nil 72 | } 73 | 74 | // Fetch queries the Consul API defined by the given client. 75 | func (d *KVListQuery) Fetch(clients *ClientSet, opts *QueryOptions) (interface{}, *ResponseMetadata, error) { 76 | select { 77 | case <-d.stopCh: 78 | return nil, nil, ErrStopped 79 | default: 80 | } 81 | 82 | opts = opts.Merge(&QueryOptions{ 83 | Datacenter: d.dc, 84 | ConsulPartition: d.partition, 85 | ConsulNamespace: d.namespace, 86 | }) 87 | 88 | log.Printf("[TRACE] %s: GET %s", d, &url.URL{ 89 | Path: "/v1/kv/" + d.prefix, 90 | RawQuery: opts.String(), 91 | }) 92 | 93 | list, qm, err := clients.Consul().KV().List(d.prefix, opts.ToConsulOpts()) 94 | if err != nil { 95 | return nil, nil, errors.Wrap(err, d.String()) 96 | } 97 | 98 | log.Printf("[TRACE] %s: returned %d pairs", d, len(list)) 99 | 100 | pairs := make([]*KeyPair, 0, len(list)) 101 | for _, pair := range list { 102 | key := strings.TrimPrefix(pair.Key, d.prefix) 103 | key = strings.TrimLeft(key, "/") 104 | 105 | pairs = append(pairs, &KeyPair{ 106 | Path: pair.Key, 107 | Key: key, 108 | Value: string(pair.Value), 109 | CreateIndex: pair.CreateIndex, 110 | ModifyIndex: pair.ModifyIndex, 111 | LockIndex: pair.LockIndex, 112 | Flags: pair.Flags, 113 | Session: pair.Session, 114 | }) 115 | } 116 | 117 | rm := &ResponseMetadata{ 118 | LastIndex: qm.LastIndex, 119 | LastContact: qm.LastContact, 120 | } 121 | 122 | return pairs, rm, nil 123 | } 124 | 125 | // CanShare returns a boolean if this dependency is shareable. 126 | func (d *KVListQuery) CanShare() bool { 127 | return true 128 | } 129 | 130 | // String returns the human-friendly version of this dependency. 131 | func (d *KVListQuery) String() string { 132 | prefix := d.prefix 133 | if d.dc != "" { 134 | prefix = prefix + "@" + d.dc 135 | } 136 | if d.partition != "" { 137 | prefix = prefix + "@partition=" + d.partition 138 | } 139 | if d.namespace != "" { 140 | prefix = prefix + "@ns=" + d.namespace 141 | } 142 | return fmt.Sprintf("kv.list(%s)", prefix) 143 | } 144 | 145 | // Stop halts the dependency's fetch function. 146 | func (d *KVListQuery) Stop() { 147 | close(d.stopCh) 148 | } 149 | 150 | // Type returns the type of this dependency. 151 | func (d *KVListQuery) Type() Type { 152 | return TypeConsul 153 | } 154 | -------------------------------------------------------------------------------- /dependency/nomad_services.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | import ( 7 | "encoding/gob" 8 | "fmt" 9 | "log" 10 | "net/url" 11 | "regexp" 12 | "sort" 13 | 14 | nomadapi "github.com/hashicorp/nomad/api" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | var ( 19 | // Ensure NomadServiceQuery meets the Dependency interface. 20 | _ Dependency = (*NomadServicesQuery)(nil) 21 | 22 | // NomadServicesQueryRe is the regex that is used to understand a service 23 | // listing Nomad query. 24 | NomadServicesQueryRe = regexp.MustCompile(`\A` + regionRe + `\z`) 25 | ) 26 | 27 | func init() { 28 | gob.Register([]*NomadServicesSnippet{}) 29 | } 30 | 31 | // NomadServicesSnippet is a stub service entry in Nomad. 32 | type NomadServicesSnippet struct { 33 | Name string 34 | Tags ServiceTags 35 | } 36 | 37 | // nomadSortableSnippet is a sortable slice of NomadServicesSnippet structs. 38 | type nomadSortableSnippet []*NomadServicesSnippet 39 | 40 | func (s nomadSortableSnippet) Len() int { return len(s) } 41 | func (s nomadSortableSnippet) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 42 | func (s nomadSortableSnippet) Less(i, j int) bool { return s[i].Name < s[j].Name } 43 | 44 | // NomadServicesQuery is the representation of a requested Nomad service 45 | // dependency from inside a template. 46 | type NomadServicesQuery struct { 47 | stopCh chan struct{} 48 | 49 | region string 50 | } 51 | 52 | // NewNomadServicesQuery parses a string into a NomadServicesQuery which is 53 | // used to list services registered within Nomad. 54 | func NewNomadServicesQuery(s string) (*NomadServicesQuery, error) { 55 | if !NomadServicesQueryRe.MatchString(s) { 56 | return nil, fmt.Errorf("nomad.services: invalid format: %q", s) 57 | } 58 | 59 | m := regexpMatch(NomadServicesQueryRe, s) 60 | return &NomadServicesQuery{ 61 | stopCh: make(chan struct{}, 1), 62 | region: m["region"], 63 | }, nil 64 | } 65 | 66 | // CanShare returns true since Nomad service dependencies are shareable. 67 | func (*NomadServicesQuery) CanShare() bool { 68 | return true 69 | } 70 | 71 | // Fetch queries the Nomad API defined by the given client and returns a slice 72 | // of NomadServiceSnippet objects. 73 | func (d *NomadServicesQuery) Fetch(clients *ClientSet, opts *QueryOptions) (interface{}, *ResponseMetadata, error) { 74 | select { 75 | case <-d.stopCh: 76 | return nil, nil, ErrStopped 77 | default: 78 | } 79 | 80 | opts = opts.Merge(&QueryOptions{ 81 | Region: d.region, 82 | }) 83 | 84 | log.Printf("[TRACE] %s: GET %s", d, &url.URL{ 85 | Path: "/v1/services", 86 | RawQuery: opts.String(), 87 | }) 88 | 89 | namespaces, qm, err := clients.Nomad().Services().List(opts.ToNomadOpts()) 90 | if err != nil { 91 | return nil, nil, errors.Wrap(err, d.String()) 92 | } 93 | 94 | // Cross namespaces queries aren't allowed via consul-template, so only 95 | // the namespace the client is configured for will be returned. 96 | var entries []*nomadapi.ServiceRegistrationStub 97 | if len(namespaces) > 0 { 98 | entries = namespaces[0].Services 99 | } 100 | 101 | log.Printf("[TRACE] %s: returned %d results", d, len(entries)) 102 | 103 | services := make([]*NomadServicesSnippet, len(entries)) 104 | for i, s := range entries { 105 | services[i] = &NomadServicesSnippet{ 106 | Name: s.ServiceName, 107 | Tags: deepCopyAndSortTags(s.Tags), 108 | } 109 | } 110 | 111 | sort.Stable(nomadSortableSnippet(services)) 112 | 113 | rm := &ResponseMetadata{ 114 | LastIndex: qm.LastIndex, 115 | LastContact: qm.LastContact, 116 | } 117 | 118 | return services, rm, nil 119 | } 120 | 121 | // String returns the human-friendly version of this dependency. 122 | func (d *NomadServicesQuery) String() string { 123 | if d.region != "" { 124 | return fmt.Sprintf("nomad.services(@%s)", d.region) 125 | } 126 | return "nomad.services" 127 | } 128 | 129 | // Stop halts the dependency's fetch function. 130 | func (d *NomadServicesQuery) Stop() { 131 | close(d.stopCh) 132 | } 133 | 134 | // Type returns the type of this dependency. 135 | func (d *NomadServicesQuery) Type() Type { 136 | return TypeNomad 137 | } 138 | -------------------------------------------------------------------------------- /dependency/nomad_services_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | import ( 7 | "fmt" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestNewNomadServicesQueryQuery(t *testing.T) { 14 | cases := []struct { 15 | name string 16 | i string 17 | exp *NomadServicesQuery 18 | err bool 19 | }{ 20 | { 21 | "empty", 22 | "", 23 | &NomadServicesQuery{}, 24 | false, 25 | }, 26 | { 27 | "node", 28 | "node", 29 | nil, 30 | true, 31 | }, 32 | { 33 | "region", 34 | "@us-east-1", 35 | &NomadServicesQuery{ 36 | region: "us-east-1", 37 | }, 38 | false, 39 | }, 40 | } 41 | 42 | for i, tc := range cases { 43 | t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) { 44 | act, err := NewNomadServicesQuery(tc.i) 45 | if (err != nil) != tc.err { 46 | t.Fatal(err) 47 | } 48 | 49 | if act != nil { 50 | act.stopCh = nil 51 | } 52 | 53 | require.Equal(t, tc.exp, act) 54 | }) 55 | } 56 | } 57 | 58 | func TestNomadServicesQuery_Fetch_1arg(t *testing.T) { 59 | cases := []struct { 60 | name string 61 | service string 62 | exp []*NomadServicesSnippet 63 | }{ 64 | { 65 | name: "all", 66 | service: "", 67 | exp: []*NomadServicesSnippet{ 68 | { 69 | Name: "example-cache", 70 | Tags: ServiceTags([]string{"tag1", "tag2"}), 71 | }, 72 | }, 73 | }, 74 | } 75 | 76 | for i, tc := range cases { 77 | t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) { 78 | d, err := NewNomadServicesQuery(tc.service) 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | 83 | act, _, err := d.Fetch(testClients, nil) 84 | if err != nil { 85 | t.Fatal(err) 86 | } 87 | 88 | require.Equal(t, tc.exp, act) 89 | }) 90 | } 91 | } 92 | 93 | func TestNomadServicesQuery_String(t *testing.T) { 94 | cases := []struct { 95 | name string 96 | i string 97 | exp string 98 | }{ 99 | { 100 | "empty", 101 | "", 102 | "nomad.services", 103 | }, 104 | { 105 | "region", 106 | "@us-east-1", 107 | "nomad.services(@us-east-1)", 108 | }, 109 | } 110 | 111 | for i, tc := range cases { 112 | t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) { 113 | d, err := NewNomadServicesQuery(tc.i) 114 | if err != nil { 115 | t.Fatal(err) 116 | } 117 | require.Equal(t, tc.exp, d.String()) 118 | }) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /dependency/nomad_var_get.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "net/url" 10 | "regexp" 11 | "strings" 12 | 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | var ( 17 | // Ensure implements 18 | _ Dependency = (*NVGetQuery)(nil) 19 | 20 | // NVGetQueryRe is the regular expression to use. 21 | NVGetQueryRe = regexp.MustCompile(`\A` + nvPathRe + nvNamespaceRe + nvRegionRe + `\z`) 22 | ) 23 | 24 | // NVGetQuery queries the KV store for a single key. 25 | type NVGetQuery struct { 26 | stopCh chan struct{} 27 | 28 | path string 29 | namespace string 30 | region string 31 | 32 | blockOnNil bool 33 | } 34 | 35 | // NewNVGetQuery parses a string into a dependency. 36 | func NewNVGetQuery(ns, s string) (*NVGetQuery, error) { 37 | s = strings.TrimSpace(s) 38 | s = strings.Trim(s, "/") 39 | 40 | if s != "" && !NVGetQueryRe.MatchString(s) { 41 | return nil, fmt.Errorf("nomad.var.get: invalid format: %q", s) 42 | } 43 | 44 | m := regexpMatch(NVGetQueryRe, s) 45 | out := &NVGetQuery{ 46 | stopCh: make(chan struct{}, 1), 47 | path: m["path"], 48 | namespace: m["namespace"], 49 | region: m["region"], 50 | } 51 | if out.namespace == "" && ns != "" { 52 | out.namespace = ns 53 | } 54 | return out, nil 55 | } 56 | 57 | // Fetch queries the Nomad API defined by the given client. 58 | func (d *NVGetQuery) Fetch(clients *ClientSet, opts *QueryOptions) (interface{}, *ResponseMetadata, error) { 59 | select { 60 | case <-d.stopCh: 61 | return nil, nil, ErrStopped 62 | default: 63 | } 64 | 65 | opts = opts.Merge(&QueryOptions{}) 66 | 67 | log.Printf("[TRACE] %s: GET %s", d, &url.URL{ 68 | Path: "/v1/var/" + d.path, 69 | RawQuery: opts.String(), 70 | }) 71 | 72 | nOpts := opts.ToNomadOpts() 73 | nOpts.Namespace = d.namespace 74 | nOpts.Region = d.region 75 | // NOTE: The Peek method of the Nomad Variables API will check a value, 76 | // return it if it exists, but return a nil value and NO error if it is 77 | // not found. 78 | nVar, qm, err := clients.Nomad().Variables().Peek(d.path, nOpts) 79 | if err != nil { 80 | return nil, nil, errors.Wrap(err, d.String()) 81 | } 82 | 83 | rm := &ResponseMetadata{ 84 | LastIndex: qm.LastIndex, 85 | LastContact: qm.LastContact, 86 | BlockOnNil: d.blockOnNil, 87 | } 88 | 89 | if nVar == nil { 90 | log.Printf("[TRACE] %s: returned nil", d) 91 | return nil, rm, nil 92 | } 93 | 94 | items := &NewNomadVariable(nVar).Items 95 | log.Printf("[TRACE] %s: returned %q", d, nVar.Path) 96 | return items, rm, nil 97 | } 98 | 99 | // EnableBlocking turns this into a blocking KV query. 100 | func (d *NVGetQuery) EnableBlocking() { 101 | d.blockOnNil = true 102 | } 103 | 104 | // CanShare returns a boolean if this dependency is shareable. 105 | func (d *NVGetQuery) CanShare() bool { 106 | return true 107 | } 108 | 109 | // String returns the human-friendly version of this dependency. 110 | // This value is also used to disambiguate multiple instances in the Brain 111 | func (d *NVGetQuery) String() string { 112 | ns := d.namespace 113 | if ns == "" { 114 | ns = "default" 115 | } 116 | region := d.region 117 | if region == "" { 118 | region = "global" 119 | } 120 | path := d.path 121 | key := fmt.Sprintf("%s@%s.%s", path, ns, region) 122 | if d.blockOnNil { 123 | return fmt.Sprintf("nomad.var.block(%s)", key) 124 | } 125 | return fmt.Sprintf("nomad.var.get(%s)", key) 126 | } 127 | 128 | // Stop halts the dependency's fetch function. 129 | func (d *NVGetQuery) Stop() { 130 | close(d.stopCh) 131 | } 132 | 133 | // Type returns the type of this dependency. 134 | func (d *NVGetQuery) Type() Type { 135 | return TypeNomad 136 | } 137 | -------------------------------------------------------------------------------- /dependency/nomad_var_list.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | import ( 7 | "encoding/gob" 8 | "fmt" 9 | "log" 10 | "net/url" 11 | "regexp" 12 | "strings" 13 | 14 | "github.com/hashicorp/nomad/api" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | var ( 19 | // Ensure implements 20 | _ Dependency = (*NVListQuery)(nil) 21 | 22 | // NVListQueryRe is the regular expression to use. 23 | NVListQueryRe = regexp.MustCompile(`\A` + nvListPrefixRe + nvListNSRe + nvRegionRe + `\z`) 24 | ) 25 | 26 | func init() { 27 | gob.Register([]*NomadVarMeta{}) 28 | } 29 | 30 | // NVListQuery queries the SV store for the metadata for keys matching the given 31 | // prefix. 32 | type NVListQuery struct { 33 | stopCh chan struct{} 34 | namespace string 35 | region string 36 | prefix string 37 | } 38 | 39 | // NewNVListQuery parses a string into a dependency. 40 | func NewNVListQuery(ns, s string) (*NVListQuery, error) { 41 | if s != "" && !NVListQueryRe.MatchString(s) { 42 | return nil, fmt.Errorf("nomad.var.list: invalid format: %q", s) 43 | } 44 | 45 | m := regexpMatch(NVListQueryRe, s) 46 | out := &NVListQuery{ 47 | stopCh: make(chan struct{}, 1), 48 | namespace: m["namespace"], 49 | region: m["region"], 50 | prefix: m["prefix"], 51 | } 52 | 53 | // Handle paths that are only slashes and discard them 54 | if strings.Trim(out.prefix, "/") == "" { 55 | out.prefix = "" 56 | } 57 | 58 | if out.namespace == "" && ns != "" { 59 | out.namespace = ns 60 | } 61 | 62 | return out, nil 63 | } 64 | 65 | // Fetch queries the Nomad API defined by the given client. 66 | func (d *NVListQuery) Fetch(clients *ClientSet, opts *QueryOptions) (interface{}, *ResponseMetadata, error) { 67 | select { 68 | case <-d.stopCh: 69 | return nil, nil, ErrStopped 70 | default: 71 | } 72 | 73 | opts = opts.Merge(&QueryOptions{}) 74 | 75 | log.Printf("[TRACE] %s: GET %s", d, &url.URL{ 76 | Path: "/v1/vars/", 77 | RawQuery: opts.String(), 78 | }) 79 | 80 | nOpts := opts.ToNomadOpts() 81 | nOpts.Namespace = d.namespace 82 | nOpts.Region = d.region 83 | list, qm, err := clients.Nomad().Variables().PrefixList(d.prefix, nOpts) 84 | if err != nil && !strings.Contains(err.Error(), "Permission denied") { 85 | return nil, nil, errors.Wrap(err, d.String()) 86 | } 87 | 88 | log.Printf("[TRACE] %s: returned %d paths", d, len(list)) 89 | 90 | vars := make([]*NomadVarMeta, 0, len(list)) 91 | for _, nVar := range list { 92 | vars = append(vars, NewNomadVarMeta(nVar)) 93 | } 94 | 95 | // 404's don't return QueryMeta. 96 | if qm == nil { 97 | qm = &api.QueryMeta{ 98 | LastIndex: 1, 99 | } 100 | } 101 | 102 | rm := &ResponseMetadata{ 103 | LastIndex: qm.LastIndex, 104 | LastContact: qm.LastContact, 105 | } 106 | 107 | return vars, rm, nil 108 | } 109 | 110 | // CanShare returns a boolean if this dependency is shareable. 111 | func (d *NVListQuery) CanShare() bool { 112 | return true 113 | } 114 | 115 | // String returns the human-friendly version of this dependency. 116 | func (d *NVListQuery) String() string { 117 | ns := d.namespace 118 | if ns == "" { 119 | ns = "default" 120 | } 121 | region := d.region 122 | if region == "" { 123 | region = "global" 124 | } 125 | prefix := d.prefix 126 | key := fmt.Sprintf("%s@%s.%s", prefix, ns, region) 127 | 128 | return fmt.Sprintf("nomad.var.list(%s)", key) 129 | } 130 | 131 | // Stop halts the dependency's fetch function. 132 | func (d *NVListQuery) Stop() { 133 | close(d.stopCh) 134 | } 135 | 136 | // Type returns the type of this dependency. 137 | func (d *NVListQuery) Type() Type { 138 | return TypeNomad 139 | } 140 | -------------------------------------------------------------------------------- /dependency/set.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | import ( 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | // Set is a dependency-specific set implementation. Relative ordering is 12 | // preserved. 13 | type Set struct { 14 | once sync.Once 15 | sync.RWMutex 16 | list []string 17 | set map[string]Dependency 18 | } 19 | 20 | // Add adds a new element to the set if it does not already exist. 21 | func (s *Set) Add(d Dependency) bool { 22 | s.init() 23 | s.Lock() 24 | defer s.Unlock() 25 | if _, ok := s.set[d.String()]; !ok { 26 | s.list = append(s.list, d.String()) 27 | s.set[d.String()] = d 28 | return true 29 | } 30 | return false 31 | } 32 | 33 | // Get retrieves a single element from the set by name. 34 | func (s *Set) Get(v string) Dependency { 35 | s.RLock() 36 | defer s.RUnlock() 37 | return s.set[v] 38 | } 39 | 40 | // List returns the insertion-ordered list of dependencies. 41 | func (s *Set) List() []Dependency { 42 | s.RLock() 43 | defer s.RUnlock() 44 | r := make([]Dependency, len(s.list)) 45 | for i, k := range s.list { 46 | r[i] = s.set[k] 47 | } 48 | return r 49 | } 50 | 51 | // Len is the size of the set. 52 | func (s *Set) Len() int { 53 | s.RLock() 54 | defer s.RUnlock() 55 | return len(s.list) 56 | } 57 | 58 | // String is a string representation of the set. 59 | func (s *Set) String() string { 60 | s.RLock() 61 | defer s.RUnlock() 62 | return strings.Join(s.list, ", ") 63 | } 64 | 65 | func (s *Set) init() { 66 | s.once.Do(func() { 67 | if s.list == nil { 68 | s.list = make([]string, 0, 8) 69 | } 70 | 71 | if s.set == nil { 72 | s.set = make(map[string]Dependency) 73 | } 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /dependency/vault_agent_token.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | import ( 7 | "log" 8 | "os" 9 | "time" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // Ensure implements 15 | var _ Dependency = (*VaultAgentTokenQuery)(nil) 16 | 17 | const ( 18 | // VaultAgentTokenSleepTime is the amount of time to sleep between queries, since 19 | // the fsnotify library is not compatible with solaris and other OSes yet. 20 | VaultAgentTokenSleepTime = DefaultNonBlockingQuerySleepTime 21 | ) 22 | 23 | // VaultAgentTokenQuery is the dependency to Vault Agent token 24 | type VaultAgentTokenQuery struct { 25 | stopCh chan struct{} 26 | path string 27 | stat os.FileInfo 28 | } 29 | 30 | // NewVaultAgentTokenQuery creates a new dependency. 31 | func NewVaultAgentTokenQuery(path string) (*VaultAgentTokenQuery, error) { 32 | return &VaultAgentTokenQuery{ 33 | stopCh: make(chan struct{}, 1), 34 | path: path, 35 | }, nil 36 | } 37 | 38 | // Fetch retrieves this dependency and returns the result or any errors that 39 | // occur in the process. 40 | func (d *VaultAgentTokenQuery) Fetch(clients *ClientSet, opts *QueryOptions) (interface{}, *ResponseMetadata, error) { 41 | log.Printf("[TRACE] %s: READ %s", d, d.path) 42 | 43 | var token string 44 | select { 45 | case <-d.stopCh: 46 | log.Printf("[TRACE] %s: stopped", d) 47 | return "", nil, ErrStopped 48 | case r := <-d.watch(d.stat): 49 | if r.err != nil { 50 | return "", nil, errors.Wrap(r.err, d.String()) 51 | } 52 | 53 | log.Printf("[TRACE] %s: reported change", d) 54 | 55 | raw_token, err := os.ReadFile(d.path) 56 | if err != nil { 57 | return "", nil, errors.Wrap(err, d.String()) 58 | } 59 | d.stat = r.stat 60 | token = string(raw_token) 61 | } 62 | 63 | return respWithMetadata(token) 64 | } 65 | 66 | // CanShare returns if this dependency is sharable. 67 | func (d *VaultAgentTokenQuery) CanShare() bool { 68 | return false 69 | } 70 | 71 | // Stop halts the dependency's fetch function. 72 | func (d *VaultAgentTokenQuery) Stop() { 73 | close(d.stopCh) 74 | } 75 | 76 | // String returns the human-friendly version of this dependency. 77 | func (d *VaultAgentTokenQuery) String() string { 78 | return "vault-agent.token" 79 | } 80 | 81 | // Type returns the type of this dependency. 82 | func (d *VaultAgentTokenQuery) Type() Type { 83 | return TypeVault 84 | } 85 | 86 | // watch watches the file for changes 87 | func (d *VaultAgentTokenQuery) watch(lastStat os.FileInfo) <-chan *watchResult { 88 | ch := make(chan *watchResult, 1) 89 | 90 | go func(lastStat os.FileInfo) { 91 | for { 92 | stat, err := os.Stat(d.path) 93 | if err != nil { 94 | select { 95 | case <-d.stopCh: 96 | return 97 | case ch <- &watchResult{err: err}: 98 | return 99 | } 100 | } 101 | 102 | changed := lastStat == nil || 103 | lastStat.Size() != stat.Size() || 104 | lastStat.ModTime() != stat.ModTime() 105 | 106 | if changed { 107 | select { 108 | case <-d.stopCh: 109 | return 110 | case ch <- &watchResult{stat: stat}: 111 | return 112 | } 113 | } 114 | 115 | time.Sleep(VaultAgentTokenSleepTime) 116 | } 117 | }(lastStat) 118 | 119 | return ch 120 | } 121 | -------------------------------------------------------------------------------- /dependency/vault_agent_token_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | import ( 7 | "os" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/hashicorp/consul-template/renderer" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestVaultAgentTokenQuery_Fetch(t *testing.T) { 16 | // Don't use t.Parallel() here as the SetToken() calls are global and break 17 | // other tests if run in parallel 18 | 19 | // Set up the Vault token file. 20 | tokenFile, err := os.CreateTemp("", "token1") 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | defer os.Remove(tokenFile.Name()) 25 | renderer.AtomicWrite(tokenFile.Name(), false, []byte("token"), 0o644, false) 26 | 27 | d, err := NewVaultAgentTokenQuery(tokenFile.Name()) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | clientSet := testClients 33 | token, _, err := d.Fetch(clientSet, nil) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | assert.Equal(t, "token", token) 39 | 40 | // Update the contents. 41 | renderer.AtomicWrite( 42 | tokenFile.Name(), false, []byte("another_token"), 0o644, false) 43 | token, _, err = d.Fetch(clientSet, nil) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | assert.Equal(t, "another_token", token) 49 | } 50 | 51 | func TestVaultAgentTokenQuery_Fetch_missingFile(t *testing.T) { 52 | // Use a non-existant token file path. 53 | d, err := NewVaultAgentTokenQuery("/tmp/invalid-file") 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | 58 | clientSet := NewClientSet() 59 | clientSet.CreateVaultClient(&CreateVaultClientInput{ 60 | Token: "foo", 61 | }) 62 | _, _, err = d.Fetch(clientSet, nil) 63 | if err == nil || !strings.Contains(err.Error(), "no such file") { 64 | t.Fatal(err) 65 | } 66 | 67 | // Token should be unaffected. 68 | assert.Equal(t, "foo", clientSet.Vault().Token()) 69 | } 70 | -------------------------------------------------------------------------------- /dependency/vault_list.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "net/url" 10 | "path" 11 | "sort" 12 | "strings" 13 | "time" 14 | 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | // Ensure implements 19 | var _ Dependency = (*VaultListQuery)(nil) 20 | 21 | // VaultListQuery is the dependency to Vault for a secret 22 | type VaultListQuery struct { 23 | stopCh chan struct{} 24 | 25 | path string 26 | } 27 | 28 | // NewVaultListQuery creates a new datacenter dependency. 29 | func NewVaultListQuery(s string) (*VaultListQuery, error) { 30 | s = strings.TrimSpace(s) 31 | s = strings.Trim(s, "/") 32 | if s == "" { 33 | return nil, fmt.Errorf("vault.list: invalid format: %q", s) 34 | } 35 | 36 | return &VaultListQuery{ 37 | stopCh: make(chan struct{}, 1), 38 | path: s, 39 | }, nil 40 | } 41 | 42 | // Fetch queries the Vault API 43 | func (d *VaultListQuery) Fetch(clients *ClientSet, opts *QueryOptions) (interface{}, *ResponseMetadata, error) { 44 | select { 45 | case <-d.stopCh: 46 | return nil, nil, ErrStopped 47 | default: 48 | } 49 | 50 | opts = opts.Merge(&QueryOptions{}) 51 | 52 | // If this is not the first query, poll to simulate blocking-queries. 53 | if opts.WaitIndex != 0 { 54 | dur := VaultDefaultLeaseDuration 55 | log.Printf("[TRACE] %s: long polling for %s", d, dur) 56 | 57 | select { 58 | case <-d.stopCh: 59 | return nil, nil, ErrStopped 60 | case <-time.After(dur): 61 | } 62 | } 63 | 64 | secretsPath := d.path 65 | 66 | // Checking secret engine version. If it's v2, we should shim /metadata/ 67 | // to secret path if necessary. 68 | mountPath, isV2, _ := isKVv2(clients.Vault(), secretsPath) 69 | if isV2 { 70 | secretsPath = shimKvV2ListPath(secretsPath, mountPath) 71 | } 72 | 73 | // If we got this far, we either didn't have a secret to renew, the secret was 74 | // not renewable, or the renewal failed, so attempt a fresh list. 75 | log.Printf("[TRACE] %s: LIST %s", d, &url.URL{ 76 | Path: "/v1/" + secretsPath, 77 | RawQuery: opts.String(), 78 | }) 79 | secret, err := clients.Vault().Logical().List(secretsPath) 80 | if err != nil { 81 | return nil, nil, errors.Wrap(err, d.String()) 82 | } 83 | 84 | var result []string 85 | 86 | // The secret could be nil if it does not exist. 87 | if secret == nil || secret.Data == nil { 88 | log.Printf("[TRACE] %s: no data", d) 89 | return respWithMetadata(result) 90 | } 91 | 92 | // This is a weird thing that happened once... 93 | keys, ok := secret.Data["keys"] 94 | if !ok { 95 | log.Printf("[TRACE] %s: no keys", d) 96 | return respWithMetadata(result) 97 | } 98 | 99 | list, ok := keys.([]interface{}) 100 | if !ok { 101 | log.Printf("[TRACE] %s: not list", d) 102 | return nil, nil, fmt.Errorf("%s: unexpected response", d) 103 | } 104 | 105 | for _, v := range list { 106 | typed, ok := v.(string) 107 | if !ok { 108 | return nil, nil, fmt.Errorf("%s: non-string in list", d) 109 | } 110 | result = append(result, typed) 111 | } 112 | sort.Strings(result) 113 | 114 | log.Printf("[TRACE] %s: returned %d results", d, len(result)) 115 | 116 | return respWithMetadata(result) 117 | } 118 | 119 | // CanShare returns if this dependency is shareable. 120 | func (d *VaultListQuery) CanShare() bool { 121 | return false 122 | } 123 | 124 | // Stop halts the given dependency's fetch. 125 | func (d *VaultListQuery) Stop() { 126 | close(d.stopCh) 127 | } 128 | 129 | // String returns the human-friendly version of this dependency. 130 | func (d *VaultListQuery) String() string { 131 | return fmt.Sprintf("vault.list(%s)", d.path) 132 | } 133 | 134 | // Type returns the type of this dependency. 135 | func (d *VaultListQuery) Type() Type { 136 | return TypeVault 137 | } 138 | 139 | // shimKvV2ListPath aligns the supported legacy path to KV v2 specs by inserting 140 | // /metadata/ into the path for listing secrets. Paths with /metadata/ are not modified. 141 | func shimKvV2ListPath(rawPath, mountPath string) string { 142 | mountPath = strings.TrimSuffix(mountPath, "/") 143 | 144 | if strings.HasPrefix(rawPath, path.Join(mountPath, "metadata")) { 145 | // It doesn't need modifying. 146 | return rawPath 147 | } 148 | 149 | switch { 150 | case rawPath == mountPath: 151 | return path.Join(mountPath, "metadata") 152 | default: 153 | rawPath = strings.TrimPrefix(rawPath, mountPath) 154 | return path.Join(mountPath, "metadata", rawPath) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /dependency/vault_token.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | import ( 7 | "github.com/hashicorp/vault/api" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // Ensure implements 12 | var _ Dependency = (*VaultTokenQuery)(nil) 13 | 14 | // VaultTokenQuery is the dependency to Vault for a secret 15 | type VaultTokenQuery struct { 16 | stopCh chan struct{} 17 | secret *Secret 18 | vaultSecret *api.Secret 19 | } 20 | 21 | // NewVaultTokenQuery creates a new dependency. 22 | func NewVaultTokenQuery(token string) (*VaultTokenQuery, error) { 23 | vaultSecret := &api.Secret{ 24 | Auth: &api.SecretAuth{ 25 | ClientToken: token, 26 | Renewable: true, 27 | LeaseDuration: 1, 28 | }, 29 | } 30 | return &VaultTokenQuery{ 31 | stopCh: make(chan struct{}, 1), 32 | vaultSecret: vaultSecret, 33 | secret: transformSecret(vaultSecret), 34 | }, nil 35 | } 36 | 37 | // Fetch queries the Vault API 38 | func (d *VaultTokenQuery) Fetch(clients *ClientSet, opts *QueryOptions, 39 | ) (interface{}, *ResponseMetadata, error) { 40 | select { 41 | case <-d.stopCh: 42 | return nil, nil, ErrStopped 43 | default: 44 | } 45 | 46 | if vaultSecretRenewable(d.secret) { 47 | err := renewSecret(clients, d) 48 | if err != nil { 49 | return nil, nil, errors.Wrap(err, d.String()) 50 | } 51 | } 52 | 53 | return nil, nil, ErrLeaseExpired 54 | } 55 | 56 | func (d *VaultTokenQuery) stopChan() chan struct{} { 57 | return d.stopCh 58 | } 59 | 60 | func (d *VaultTokenQuery) secrets() (*Secret, *api.Secret) { 61 | return d.secret, d.vaultSecret 62 | } 63 | 64 | // CanShare returns if this dependency is shareable. 65 | func (d *VaultTokenQuery) CanShare() bool { 66 | return false 67 | } 68 | 69 | // Stop halts the dependency's fetch function. 70 | func (d *VaultTokenQuery) Stop() { 71 | close(d.stopCh) 72 | } 73 | 74 | // String returns the human-friendly version of this dependency. 75 | func (d *VaultTokenQuery) String() string { 76 | return "vault.token" 77 | } 78 | 79 | // Type returns the type of this dependency. 80 | func (d *VaultTokenQuery) Type() Type { 81 | return TypeVault 82 | } 83 | -------------------------------------------------------------------------------- /dependency/vault_token_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dependency 5 | 6 | import ( 7 | "fmt" 8 | "testing" 9 | 10 | "github.com/hashicorp/vault/api" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestNewVaultTokenQuery(t *testing.T) { 15 | cases := []struct { 16 | name string 17 | exp *VaultTokenQuery 18 | err bool 19 | }{ 20 | { 21 | "default", 22 | &VaultTokenQuery{ 23 | secret: &Secret{ 24 | Auth: &SecretAuth{ 25 | ClientToken: "my-token", 26 | Renewable: true, 27 | LeaseDuration: 1, 28 | }, 29 | }, 30 | vaultSecret: &api.Secret{ 31 | Auth: &api.SecretAuth{ 32 | ClientToken: "my-token", 33 | Renewable: true, 34 | LeaseDuration: 1, 35 | }, 36 | }, 37 | }, 38 | false, 39 | }, 40 | } 41 | 42 | for i, tc := range cases { 43 | t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) { 44 | act, err := NewVaultTokenQuery("my-token") 45 | if (err != nil) != tc.err { 46 | t.Fatal(err) 47 | } 48 | 49 | if act != nil { 50 | act.stopCh = nil 51 | } 52 | 53 | assert.Equal(t, tc.exp, act) 54 | }) 55 | } 56 | } 57 | 58 | func TestVaultTokenQuery_String(t *testing.T) { 59 | cases := []struct { 60 | name string 61 | exp string 62 | }{ 63 | { 64 | "default", 65 | "vault.token", 66 | }, 67 | } 68 | 69 | for i, tc := range cases { 70 | t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) { 71 | d, err := NewVaultTokenQuery("my-token") 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | assert.Equal(t, tc.exp, d.String()) 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /docs/observability.md: -------------------------------------------------------------------------------- 1 | # Observability Options 2 | 3 | ## Logging 4 | 5 | Consul Template can print verbose debugging output. To set the log level for 6 | Consul Template, use the `-log-level` flag: 7 | 8 | ```shell 9 | $ consul-template -log-level info ... 10 | ``` 11 | 12 | Or set it via the `CONSUL_TEMPLATE_LOG_LEVEL` environment variable. 13 | 14 | ```text 15 | [INFO] (cli) received redis from Watcher 16 | [INFO] (cli) invoking Runner 17 | # ... 18 | ``` 19 | 20 | You can also specify the level as debug: 21 | 22 | ```shell 23 | $ consul-template -log-level debug ... 24 | ``` 25 | 26 | ```text 27 | [DEBUG] (cli) creating Runner 28 | [DEBUG] (cli) creating Consul API client 29 | [DEBUG] (cli) creating Watcher 30 | [DEBUG] (cli) looping for data 31 | [DEBUG] (watcher) starting watch 32 | [DEBUG] (watcher) all pollers have started, waiting for finish 33 | [DEBUG] (redis) starting poll 34 | [DEBUG] (service redis) querying Consul with &{...} 35 | [DEBUG] (service redis) Consul returned 2 services 36 | [DEBUG] (redis) writing data to channel 37 | [DEBUG] (redis) starting poll 38 | [INFO] (cli) received redis from Watcher 39 | [INFO] (cli) invoking Runner 40 | [DEBUG] (service redis) querying Consul with &{...} 41 | # ... 42 | ``` 43 | 44 | ## Logging to file 45 | 46 | Consul Template can log to file as well. 47 | Particularly useful in use cases where it's not trivial to capture *stdout* and/or *stderr* 48 | like for example when Consul Template is deployed as a Windows Service. 49 | 50 | These are the relevant CLI flags: 51 | 52 | - `-log-file` - writes all the Consul Template log messages 53 | to a file. This value is used as a prefix for the log file name. The current timestamp 54 | is appended to the file name. If the value ends in a path separator, `consul-template-` 55 | will be appended to the value. If the file name is missing an extension, `.log` 56 | is appended. For example, setting `log-file` to `/var/log/` would result in a log 57 | file path of `/var/log/consul-template-{timestamp}.log`. `log-file` can be combined with 58 | `-log-rotate-bytes` and `-log-rotate-duration` 59 | for a fine-grained log rotation experience. 60 | 61 | - `-log-rotate-bytes` - to specify the number of 62 | bytes that should be written to a log before it needs to be rotated. Unless specified, 63 | there is no limit to the number of bytes that can be written to a log file. 64 | 65 | - `-log-rotate-duration` - to specify the maximum 66 | duration a log should be written to before it needs to be rotated. Must be a duration 67 | value such as 30s. Defaults to 24h. 68 | 69 | - `-log-rotate-max-files` - to specify the maximum 70 | number of older log file archives to keep. Defaults to 0 (no files are ever deleted). 71 | Set to -1 to discard old log files when a new one is created. -------------------------------------------------------------------------------- /docs/plugins.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | ## Authoring Plugins 4 | 5 | For some use cases, it may be necessary to write a plugin that offloads work to 6 | another system. This is especially useful for things that may not fit in the 7 | "standard library" of Consul Template, but still need to be shared across 8 | multiple instances. 9 | 10 | Consul Template plugins must have the following API: 11 | 12 | ```shell 13 | $ NAME [INPUT...] 14 | ``` 15 | 16 | - `NAME` - the name of the plugin - this is also the name of the binary, either 17 | a full path or just the program name. It will be executed in a shell with the 18 | inherited `PATH` so e.g. the plugin `cat` will run the first executable `cat` 19 | that is found on the `PATH`. 20 | 21 | - `INPUT` - input from the template. There will be one INPUT for every argument passed 22 | to the `plugin` function. If the arguments contain whitespace, that whitespace 23 | will be passed as if the argument were quoted by the shell. 24 | 25 | ### Important Notes 26 | 27 | - Plugins execute user-provided scripts and pass in potentially sensitive data 28 | from Consul or Vault. Nothing is validated or protected by Consul Template, 29 | so all necessary precautions and considerations should be made by template 30 | authors 31 | 32 | - Plugin output must be returned as a string on stdout. Only stdout will be 33 | parsed for output. Be sure to log all errors, debugging messages onto stderr 34 | to avoid errors when Consul Template returns the value. Note that output to 35 | stderr will only be output if the plugin returns a non-zero exit code. 36 | 37 | - Always `exit 0` or Consul Template will assume the plugin failed to execute 38 | 39 | - Ensure the empty input case is handled correctly (see [Multi-phase execution](#multi-phase-execution)) 40 | 41 | - Data piped into the plugin is appended after any parameters given explicitly (eg `{{ "sample-data" | plugin "my-plugin" "some-parameter"}}` will call `my-plugin some-parameter sample-data`) 42 | 43 | Here is a sample plugin in a few different languages that removes any JSON keys 44 | that start with an underscore and returns the JSON string: 45 | 46 | ```ruby 47 | #! /usr/bin/env ruby 48 | require "json" 49 | 50 | if ARGV.empty? 51 | puts JSON.fast_generate({}) 52 | Kernel.exit(0) 53 | end 54 | 55 | hash = JSON.parse(ARGV.first) 56 | hash.reject! { |k, _| k.start_with?("_") } 57 | puts JSON.fast_generate(hash) 58 | Kernel.exit(0) 59 | ``` 60 | 61 | ```go 62 | func main() { 63 | arg := []byte(os.Args[1]) 64 | 65 | var parsed map[string]interface{} 66 | if err := json.Unmarshal(arg, &parsed); err != nil { 67 | fmt.Fprintln(os.Stderr, fmt.Sprintf("err: %s", err)) 68 | os.Exit(1) 69 | } 70 | 71 | for k, _ := range parsed { 72 | if string(k[0]) == "_" { 73 | delete(parsed, k) 74 | } 75 | } 76 | 77 | result, err := json.Marshal(parsed) 78 | if err != nil { 79 | fmt.Fprintln(os.Stderr, fmt.Sprintf("err: %s", err)) 80 | os.Exit(1) 81 | } 82 | 83 | fmt.Fprintln(os.Stdout, fmt.Sprintf("%s", result)) 84 | os.Exit(0) 85 | } 86 | ``` 87 | -------------------------------------------------------------------------------- /examples/apache.md: -------------------------------------------------------------------------------- 1 | Apache Consul Template Example 2 | ------------------------------ 3 | Apache httpd is a popular web server. You can read more about the Apache httpd configuration file syntax in the [Apache httpd documentation](https://httpd.apache.org/docs/). 4 | 5 | 6 | ## Reverse Proxy based on Service Tags 7 | Here is an example template for rendering part of an Apache httpd configuration file that is responsible for configuring a reverse proxy with dynamic end points based on service tags with Consul Template: 8 | 9 | ```liquid 10 | {{range $tag, $service := service "web" | byTag}} 11 | # "{{$tag}}" api providers. 12 | 13 | {{range $service}} BalancerMember http://{{.Address}}:{{.Port}} 14 | {{end}} ProxySet lbmethod=bybusyness 15 | 16 | Redirect permanent /api/{{$tag}} /api/{{$tag}}/ 17 | ProxyPass /api/{{$tag}}/ balancer://{{$tag}}/ 18 | ProxyPassReverse /api/{{$tag}}/ balancer://{{$tag}}/ 19 | {{end}} 20 | ``` 21 | 22 | Save this file to disk at a place reachable by the Consul Template process like `/tmp/httpd.conf.ctmpl` and run Consul Template: 23 | 24 | ```shell 25 | $ consul-template \ 26 | -template="/tmp/httpd.conf.ctmpl:/etc/httpd/sites-available/balancer.conf" 27 | ``` 28 | 29 | Here is an example of what the file may render: 30 | 31 | ```text 32 | # "frontend" api providers. 33 | 34 | BalancerMember http://104.131.109.106:8080 35 | BalancerMember http://104.131.109.113:8081 36 | ProxySet lbmethod=bybusyness 37 | 38 | Redirect permanent /api/frontend /api/frontend/ 39 | ProxyPass /api/frontend/ balancer://frontend/ 40 | ProxyPassReverse /api/frontend/ balancer://frontend/ 41 | 42 | # "api" api providers. 43 | 44 | BalancerMember http://104.131.108.11:8500 45 | ProxySet lbmethod=bybusyness 46 | 47 | Redirect permanent /api/api /api/api/ 48 | ProxyPass /api/api/ balancer://api/ 49 | ProxyPassReverse /api/api/ balancer://api/ 50 | ``` 51 | 52 | - For a list of functions, please see the [Consul Template README](https://github.com/hashicorp/consul-template) 53 | - For template syntax, please see [the golang text/template documentation](https://golang.org/pkg/text/template/) 54 | -------------------------------------------------------------------------------- /examples/haproxy-connect-proxy/run-haproxy-connect-proxy: -------------------------------------------------------------------------------- 1 | ../nginx-connect-proxy/run-nginx-connect-proxy -------------------------------------------------------------------------------- /examples/haproxy.md: -------------------------------------------------------------------------------- 1 | HAProxy Consul Template Example 2 | ------------------------------- 3 | HAProxy is a very common load balancer. You can read more about the HAProxy configuration file syntax in the [HAProxy documentation](http://www.haproxy.org/). 4 | 5 | ## Global Service Load Balancer 6 | Here is an example template for rendering an HAProxy configuration file with Consul Template: 7 | 8 | ```liquid 9 | global 10 | daemon 11 | maxconn {{key "service/haproxy/maxconn"}} 12 | 13 | defaults 14 | mode {{key "service/haproxy/mode"}}{{range ls "service/haproxy/timeouts"}} 15 | timeout {{.Key}} {{.Value}}{{end}} 16 | 17 | listen http-in 18 | bind *:8000{{range service "release.web"}} 19 | server {{.Node}} {{.Address}}:{{.Port}}{{end}} 20 | ``` 21 | 22 | Save this file to disk at a place reachable by the Consul Template process like `/tmp/haproxy.conf.ctmpl` and run Consul Template: 23 | 24 | ```shell 25 | $ consul-template \ 26 | -template="/tmp/haproxy.conf.ctmpl:/etc/haproxy/haproxy.conf" 27 | ``` 28 | 29 | Here is an example of what the file may render: 30 | 31 | ```text 32 | global 33 | daemon 34 | maxconn 4 35 | 36 | defaults 37 | mode default 38 | timeout 5 39 | 40 | listen http-in 41 | bind *:8000 42 | server nyc3-worker-2 104.131.109.224:80 43 | server nyc3-worker-3 104.131.59.59:80 44 | server nyc3-worker-1 104.131.86.92:80 45 | ``` 46 | 47 | - For a list of functions, please see the [Consul Template README](https://github.com/hashicorp/consul-template) 48 | - For template syntax, please see [the golang text/template documentation](https://golang.org/pkg/text/template/) 49 | -------------------------------------------------------------------------------- /examples/join.md: -------------------------------------------------------------------------------- 1 | Joining Structures with Consul Template 2 | --------------------------------------- 3 | Consul Template has built-in support for joining existing arrays and lists on a given separator, but there is no built-in support for complex map-reduce functions. This section details some common join techniques. 4 | 5 | ## Joining Service Addresses 6 | Sometimes you require all service addresses to be listed in a comma-separated list. Memcached and other tools usually accept this as an environment variable. 7 | 8 | 9 | ```liquid 10 | export MEMCACHED_SERVERS="{{range $index, $service := service "memcached" }}{{if ne $index 0}},{{end}}{{$service.Address}}:{{$service.Port}}{{end}}" 11 | ``` 12 | 13 | Save this file to disk at a place reachable by the Consul Template process like `/tmp/memcached.ctmpl` and run Consul Template: 14 | 15 | ```shell 16 | $ consul-template \ 17 | -template="/tmp/memcached.ctmpl:/etc/profile.d/memcached" 18 | ``` 19 | 20 | Here is an example of what the file may render: 21 | 22 | ```text 23 | export MEMCACHED_SERVERS="1.2.3.4,5.6.7.8" 24 | ``` 25 | 26 | - For a list of functions, please see the [Consul Template README](https://github.com/hashicorp/consul-template) 27 | - For template syntax, please see [the golang text/template documentation](https://golang.org/pkg/text/template/) 28 | -------------------------------------------------------------------------------- /examples/nginx-connect-proxy/run-nginx-connect-proxy: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script is here to allow easy testing of the config files embedded in the 4 | # document and to help users try it out. 5 | 6 | name=$(basename ${0}) 7 | proxy_connect_document="${name#run-}.md" 8 | 9 | cat << EOF 10 | 11 | This script will extract the configuration files from $proxy_connect_document, 12 | save them to files then run the commands to start up all the processes in a 13 | screen session. Requires basic Unix shell utilities (cat, sed, grep) and GNU 14 | Screen. 15 | 16 | To switch between the different screen windows type control-a then the number 17 | 0-4 (eg. 'ctrl-a 0' will take you to the first window, the web-server). When 18 | done just exit each application (ctrl-c) and each window (type 'exit' or 19 | ctrl-d), exiting the last will exit screen. 20 | 21 | Press Enter to continue (ctrl-c to abort)... 22 | EOF 23 | read ignore 24 | 25 | # extracts all config files out of document 26 | sh -c "$(cat ../$proxy_connect_document \ 27 | | sed -n -e '/```shell/,/```/g;/```/,/```/p' |grep -v '```')" 28 | 29 | # to just extract the files to play with, uncomment this exit line.. 30 | #exit 31 | 32 | # create a screen session 33 | screen -dmS connect 34 | 35 | # function for opening new screen windows in that session 36 | window=0 37 | open() { 38 | [ $window -ne 0 ] && screen -S connect -X screen $window 39 | screen -S connect -p $window -X stuff "$1\n" 40 | window=$(($window + 1)) 41 | } 42 | 43 | # sleeps are needed when services depend on others already running 44 | open "consul agent -dev -log-level=warn -config-file=consul-services.json" 45 | open "python -m SimpleHTTPServer" 46 | # change to "python3 -m http.server" if you only have python3 47 | sleep 0.3 48 | open "consul connect proxy -sidecar-for webserver" 49 | sleep 0.3 50 | open "consul-template -config ingress-config.hcl -log-level=info" 51 | # last session is to test things 52 | open "clear && echo 'Run these to test routing and intentions... 53 | curl http://localhost:8080 54 | consul intention create -deny ingress webserver 55 | curl http://localhost:8080 56 | consul intention delete ingress webserver 57 | curl http://localhost:8080'" 58 | 59 | screen -r connect 60 | 61 | # clean up the generated files 62 | git clean -f . 63 | -------------------------------------------------------------------------------- /examples/nginx.md: -------------------------------------------------------------------------------- 1 | nginx Consul Template Example 2 | ----------------------------- 3 | nginx is popular open source web server, reverse proxy, and load balancer. You can read more about nginx's configuration file syntax in the [nginx documentation](https://nginx.org/en/docs/). 4 | 5 | ## Global Load Balancer 6 | Here is an example template for rendering an nginx configuration file with Consul Template: 7 | 8 | ```liquid 9 | {{range services}} {{$name := .Name}} {{$service := service .Name}} 10 | upstream {{$name}} { 11 | zone upstream-{{$name}} 64k; 12 | {{range $service}}server {{.Address}}:{{.Port}} max_fails=3 fail_timeout=60 weight=1; 13 | {{else}}server 127.0.0.1:65535; # force a 502{{end}} 14 | } {{end}} 15 | 16 | server { 17 | listen 80 default_server; 18 | 19 | location / { 20 | root /usr/share/nginx/html/; 21 | index index.html; 22 | } 23 | 24 | location /stub_status { 25 | stub_status; 26 | } 27 | 28 | {{range services}} {{$name := .Name}} 29 | location /{{$name}} { 30 | proxy_pass http://{{$name}}; 31 | } 32 | {{end}} 33 | } 34 | ``` 35 | 36 | Save this file to disk at a place reachable by the Consul Template process like `/tmp/nginx.conf.ctmpl` and run Consul Template: 37 | 38 | 39 | ```shell 40 | $ consul-template \ 41 | -template="/tmp/nginx.conf.ctmpl:/etc/nginx/conf.d/default.conf" 42 | ``` 43 | 44 | You should see output similar to the following: 45 | 46 | ```text 47 | upstream service { 48 | zone upstream-service 64k; 49 | least_conn; 50 | server 172.17.0.3:80 max_fails=3 fail_timeout=60 weight=1; 51 | } 52 | 53 | server { 54 | listen 80 default_server; 55 | 56 | location / { 57 | root /usr/share/nginx/html/; 58 | index index.html; 59 | } 60 | 61 | location /stub_status { 62 | stub_status; 63 | } 64 | 65 | location /service { 66 | proxy_pass http://service; 67 | } 68 | } 69 | ``` 70 | 71 | - For a list of functions, please see the [Consul Template README](https://github.com/hashicorp/consul-template) 72 | - For template syntax, please see [the golang text/template documentation](https://golang.org/pkg/text/template/) 73 | -------------------------------------------------------------------------------- /examples/services.md: -------------------------------------------------------------------------------- 1 | Querying all services with Consul Template 2 | ------------------------------------------ 3 | As of Consul Template 0.6.0, it is possible to have a complex dependency graph with dependent services. As such, it is possible to query and watch all services in Consul: 4 | 5 | ## Query All Services 6 | 7 | ```liquid 8 | {{range services}}# {{.Name}}{{range service .Name}} 9 | {{.Address}}{{end}} 10 | 11 | {{end}} 12 | ``` 13 | 14 | Save this file to disk at a place reachable by the Consul Template process like `/tmp/all.ctmpl` and run Consul Template: 15 | 16 | ```shell 17 | $ consul-template \ 18 | -template="/tmp/all.ctmpl:/tmp/all" 19 | ``` 20 | 21 | Here is an example of what the file may render: 22 | 23 | ```text 24 | # consul 25 | 104.131.121.232 26 | 27 | # redis 28 | 104.131.86.92 29 | 104.131.109.224 30 | 104.131.59.59 31 | 32 | # web 33 | 104.131.86.92 34 | 104.131.109.224 35 | 104.131.59.59 36 | ``` 37 | -------------------------------------------------------------------------------- /examples/varnish.md: -------------------------------------------------------------------------------- 1 | Varnish Consul Template Example 2 | ------------------------------- 3 | Varnish is an common caching engine that can also act as a proxy. You can read more about the Varnish configuration file syntax in the [Varnish documentation](https://varnish-cache.org/docs/). 4 | 5 | ## Backend Router 6 | Here is an example template for rendering a Varnish configuration file with Consul Template: 7 | 8 | ```liquid 9 | import directors; 10 | {{range service "consul"}} 11 | backend {{.Name}}_{{.ID}} { 12 | .host = "{{.Address}}"; 13 | .port = "{{.Port}}"; 14 | }{{end}} 15 | 16 | sub vcl_init { 17 | new bar = directors.round_robin(); 18 | {{range service "consul"}} 19 | bar.add_backend({{.Name}}_{{.ID}});{{end}} 20 | } 21 | 22 | sub vcl_recv { 23 | set req.backend_hint = bar.backend(); 24 | } 25 | ``` 26 | 27 | Save this file to disk at a place reachable by the Consul Template process like `/tmp/varnish.conf.ctmpl` and run Consul Template: 28 | 29 | ```shell 30 | $ consul-template \ 31 | -template="/tmp/varnish.conf.ctmpl:/etc/varnish/varnish.conf" 32 | ``` 33 | 34 | Here is an example of what the file may render: 35 | 36 | ```text 37 | import directors; 38 | 39 | backend consul_consul { 40 | .host = "104.131.109.106"; 41 | .port = "8300";" 42 | } 43 | 44 | sub vcl_init { 45 | new bar = directors.round_robin(); 46 | 47 | bar.add_backend(consul_consul); 48 | } 49 | 50 | sub vcl_recv { 51 | set req.backend_hint = bar.backend(); 52 | } 53 | ``` 54 | 55 | - For a list of functions, please see the [Consul Template README](https://github.com/hashicorp/consul-template) 56 | - For template syntax, please see [the golang text/template documentation](https://golang.org/pkg/text/template/) 57 | -------------------------------------------------------------------------------- /examples/vault-pki.md: -------------------------------------------------------------------------------- 1 | Rendering PKI Certificates from Vault with Consul Template 2 | ---------------------------------------------------------- 3 | [Vault][vault] is a popular open source tool for managing secrets. In addition 4 | to acting as an encrypted KV store, Vault can also generate dynamic secrets, 5 | like PKI/TLS certificates. 6 | 7 | When generating PKI certificates with Vault, the certificate, private key, and 8 | any intermediate certs are all returned as part of the same API call. Most 9 | software requires these files be placed in separate files on the system. 10 | 11 | **Note:** In previous versions of consul-template [`generate_lease`][generate_lease] needed 12 | to be set to `true` (non-default) on the Vault PKI role. Without the lease the automatic 13 | certificate renewal wouldn't work properly based on the expiration date of the certificate alone. 14 | As of v0.22.0 the certificate expiration details are now also used to monitor the renewal time 15 | without needing an associated lease in Vault. If you are issuing a very large number of certificates 16 | there may be a performance advantage to not tracking every lease when leaving the default setting 17 | of [`generate_lease`][generate_lease] set to `false`. 18 | 19 | [vault]: https://www.vaultproject.io/ "Vault by HashiCorp" 20 | [generate_lease]: https://www.vaultproject.io/api/secret/pki/index.html#generate_lease 21 | 22 | ## Multiple Output Files 23 | 24 | Consul Template can run more than one template. At boot, all dependencies 25 | (external API requests) are mapped into a single list. This means that multiple 26 | templates watching the same path return the same data. 27 | 28 | Consider the following three templates: 29 | 30 | ```liquid 31 | {{- /* /tmp/cert.tpl */ -}} 32 | {{ with secret "pki/issue/my-domain-dot-com" "common_name=foo.example.com" }} 33 | {{ .Data.certificate }}{{ end }} 34 | ``` 35 | 36 | ```liquid 37 | {{- /* /tmp/ca.tpl */ -}} 38 | {{ with secret "pki/issue/my-domain-dot-com" "common_name=foo.example.com" }} 39 | {{ .Data.issuing_ca }}{{ end }} 40 | ``` 41 | 42 | ```liquid 43 | {{- /* /tmp/key.tpl */ -}} 44 | {{ with secret "pki/issue/my-domain-dot-com" "common_name=foo.example.com" }} 45 | {{ .Data.private_key }}{{ end }} 46 | ``` 47 | 48 | These are three different input templates, but when run under the same Consul 49 | Template process, they are compressed into a single API call, sharing the 50 | resulting data. 51 | 52 | Here is an example Consul Template configuration: 53 | 54 | ```hcl 55 | template { 56 | source = "/tmp/cert.tpl" 57 | destination = "/opt/my-app/ssl/my-app.crt" 58 | } 59 | 60 | template { 61 | source = "/tmp/ca.tpl" 62 | destination = "/opt/my-app/ssl/ca.crt" 63 | } 64 | 65 | template { 66 | source = "/tmp/key.tpl" 67 | destination = "/opt/my-app/ssl/my-app.key" 68 | } 69 | ``` 70 | 71 | To generate multiple certificates of the same path, use multiple Consul Template 72 | processes. 73 | -------------------------------------------------------------------------------- /flags.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "strconv" 8 | "time" 9 | ) 10 | 11 | // funcVar is a type of flag that accepts a function that is the string given 12 | // by the user. 13 | type funcVar func(s string) error 14 | 15 | func (f funcVar) Set(s string) error { return f(s) } 16 | func (f funcVar) String() string { return "" } 17 | func (f funcVar) IsBoolFlag() bool { return false } 18 | 19 | // funcBoolVar is a type of flag that accepts a function, converts the user's 20 | // value to a bool, and then calls the given function. 21 | type funcBoolVar func(b bool) error 22 | 23 | func (f funcBoolVar) Set(s string) error { 24 | v, err := strconv.ParseBool(s) 25 | if err != nil { 26 | return err 27 | } 28 | return f(v) 29 | } 30 | func (f funcBoolVar) String() string { return "" } 31 | func (f funcBoolVar) IsBoolFlag() bool { return true } 32 | 33 | // funcDurationVar is a type of flag that accepts a function, converts the 34 | // user's value to a duration, and then calls the given function. 35 | type funcDurationVar func(d time.Duration) error 36 | 37 | func (f funcDurationVar) Set(s string) error { 38 | v, err := time.ParseDuration(s) 39 | if err != nil { 40 | return err 41 | } 42 | return f(v) 43 | } 44 | func (f funcDurationVar) String() string { return "" } 45 | func (f funcDurationVar) IsBoolFlag() bool { return false } 46 | 47 | // funcIntVar is a type of flag that accepts a function, converts the 48 | // user's value to a int, and then calls the given function. 49 | type funcIntVar func(i int) error 50 | 51 | func (f funcIntVar) Set(s string) error { 52 | v, err := strconv.ParseInt(s, 10, 32) 53 | if err != nil { 54 | return err 55 | } 56 | return f(int(v)) 57 | } 58 | func (f funcIntVar) String() string { return "" } 59 | func (f funcIntVar) IsBoolFlag() bool { return false } 60 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/consul-template 2 | 3 | go 1.22 4 | 5 | toolchain go1.22.4 6 | 7 | require ( 8 | github.com/BurntSushi/toml v1.3.2 9 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc 10 | github.com/hashicorp/consul/api v1.29.1 11 | github.com/hashicorp/consul/sdk v0.16.1 12 | github.com/hashicorp/go-gatedio v0.5.0 13 | github.com/hashicorp/go-hclog v1.6.3 14 | github.com/hashicorp/go-multierror v1.1.1 15 | github.com/hashicorp/go-rootcerts v1.0.2 16 | github.com/hashicorp/go-sockaddr v1.0.6 17 | github.com/hashicorp/go-syslog v1.0.0 18 | github.com/hashicorp/hcl v1.0.0 19 | github.com/hashicorp/logutils v1.0.0 20 | github.com/hashicorp/nomad/api v0.0.0-20230103221135-ce00d683f9be 21 | github.com/hashicorp/serf v0.10.1 // indirect 22 | github.com/hashicorp/vault/api v1.10.0 23 | github.com/mitchellh/go-homedir v1.1.0 24 | github.com/mitchellh/hashstructure v1.1.0 25 | github.com/mitchellh/mapstructure v1.5.0 26 | github.com/pkg/errors v0.9.1 27 | github.com/stretchr/testify v1.8.4 28 | golang.org/x/crypto v0.32.0 // indirect 29 | golang.org/x/sys v0.29.0 30 | gopkg.in/yaml.v2 v2.4.0 31 | ) 32 | 33 | require ( 34 | dario.cat/mergo v1.0.0 35 | github.com/Masterminds/sprig/v3 v3.2.3 36 | github.com/hashicorp/vault/api/auth/kubernetes v0.5.0 37 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 38 | golang.org/x/text v0.21.0 39 | ) 40 | 41 | require ( 42 | github.com/Masterminds/goutils v1.1.1 // indirect 43 | github.com/Masterminds/semver/v3 v3.2.0 // indirect 44 | github.com/armon/go-metrics v0.4.1 // indirect 45 | github.com/cenkalti/backoff/v3 v3.2.2 // indirect 46 | github.com/coreos/go-systemd/v22 v22.5.0 47 | github.com/fatih/color v1.17.0 // indirect 48 | github.com/go-jose/go-jose/v3 v3.0.3 // indirect 49 | github.com/google/uuid v1.3.0 // indirect 50 | github.com/gorilla/websocket v1.5.0 // indirect 51 | github.com/hashicorp/cronexpr v1.1.1 // indirect 52 | github.com/hashicorp/errwrap v1.1.0 // indirect 53 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 54 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 55 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 56 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 // indirect 57 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 58 | github.com/hashicorp/go-uuid v1.0.3 // indirect 59 | github.com/hashicorp/go-version v1.6.0 // indirect 60 | github.com/hashicorp/golang-lru v1.0.2 // indirect 61 | github.com/huandu/xstrings v1.4.0 // indirect 62 | github.com/imdario/mergo v0.3.11 // indirect 63 | github.com/mattn/go-colorable v0.1.13 // indirect 64 | github.com/mattn/go-isatty v0.0.20 // indirect 65 | github.com/miekg/dns v1.1.50 // indirect 66 | github.com/mitchellh/copystructure v1.2.0 // indirect 67 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 68 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 69 | github.com/ryanuber/go-glob v1.0.0 // indirect 70 | github.com/shopspring/decimal v1.3.1 // indirect 71 | github.com/spf13/cast v1.5.0 // indirect 72 | golang.org/x/net v0.34.0 73 | golang.org/x/time v0.3.0 // indirect 74 | gopkg.in/yaml.v3 v3.0.1 // indirect 75 | ) 76 | -------------------------------------------------------------------------------- /logging/logfile.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package logging 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | "sort" 11 | "strconv" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/hashicorp/logutils" 17 | ) 18 | 19 | type LogFile struct { 20 | // Name of the log file 21 | fileName string 22 | 23 | // Path to the log file 24 | logPath string 25 | 26 | // Duration between each file rotation operation 27 | duration time.Duration 28 | 29 | // LastCreated represents the creation time of the latest log 30 | LastCreated time.Time 31 | 32 | // FileInfo is the pointer to the current file being written to 33 | FileInfo *os.File 34 | 35 | // MaxBytes is the maximum number of desired bytes for a log file 36 | MaxBytes int 37 | 38 | // BytesWritten is the number of bytes written in the current log file 39 | BytesWritten int64 40 | 41 | // Max rotated files to keep before removing them. 42 | MaxFiles int 43 | 44 | // filt is used to filter log messages depending on their level 45 | filt *logutils.LevelFilter 46 | 47 | // acquire is the mutex utilized to ensure we have no concurrency issues 48 | acquire sync.Mutex 49 | } 50 | 51 | func (l *LogFile) fileNamePattern() string { 52 | // Extract the file extension 53 | fileExt := filepath.Ext(l.fileName) 54 | // If we have no file extension we append .log 55 | if fileExt == "" { 56 | fileExt = ".log" 57 | } 58 | // Remove the file extension from the filename 59 | return strings.TrimSuffix(l.fileName, fileExt) + "-%s" + fileExt 60 | } 61 | 62 | func (l *LogFile) openNew() error { 63 | fileNamePattern := l.fileNamePattern() 64 | 65 | createTime := time.Now() 66 | newfileName := fmt.Sprintf(fileNamePattern, strconv.FormatInt(createTime.UnixNano(), 10)) 67 | newfilePath := filepath.Join(l.logPath, newfileName) 68 | 69 | // Try creating a file. We truncate the file because we are the only authority to write the logs 70 | filePointer, err := os.OpenFile(newfilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o640) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | l.FileInfo = filePointer 76 | // New file, new bytes tracker, new creation time :) 77 | l.LastCreated = createTime 78 | l.BytesWritten = 0 79 | return nil 80 | } 81 | 82 | func (l *LogFile) rotate() error { 83 | // Get the time from the last point of contact 84 | timeElapsed := time.Since(l.LastCreated) 85 | // Rotate if we hit the byte file limit or the time limit 86 | if (l.BytesWritten >= int64(l.MaxBytes) && (l.MaxBytes > 0)) || timeElapsed >= l.duration { 87 | l.FileInfo.Close() 88 | if err := l.pruneFiles(); err != nil { 89 | return err 90 | } 91 | return l.openNew() 92 | } 93 | return nil 94 | } 95 | 96 | func (l *LogFile) pruneFiles() error { 97 | if l.MaxFiles == 0 { 98 | return nil 99 | } 100 | 101 | pattern := filepath.Join(l.logPath, fmt.Sprintf(l.fileNamePattern(), "*")) 102 | matches, err := filepath.Glob(pattern) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | switch { 108 | case l.MaxFiles < 0: 109 | return removeFiles(matches) 110 | case len(matches) < l.MaxFiles: 111 | return nil 112 | } 113 | 114 | sort.Strings(matches) 115 | last := len(matches) - l.MaxFiles 116 | return removeFiles(matches[:last]) 117 | } 118 | 119 | func removeFiles(files []string) error { 120 | for _, file := range files { 121 | if err := os.Remove(file); err != nil { 122 | return err 123 | } 124 | } 125 | return nil 126 | } 127 | 128 | // Write is used to implement io.Writer. 129 | func (l *LogFile) Write(b []byte) (int, error) { 130 | l.acquire.Lock() 131 | defer l.acquire.Unlock() 132 | 133 | // Skip if the log level doesn't apply 134 | if l.filt != nil && !l.filt.Check(b) { 135 | return 0, nil 136 | } 137 | 138 | // Create a new file if we have no file to write to 139 | if l.FileInfo == nil { 140 | if err := l.openNew(); err != nil { 141 | return 0, err 142 | } 143 | } 144 | // Check for the last contact and rotate if necessary 145 | if err := l.rotate(); err != nil { 146 | return 0, err 147 | } 148 | l.BytesWritten += int64(len(b)) 149 | return l.FileInfo.Write(b) 150 | } 151 | -------------------------------------------------------------------------------- /logging/logging_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package logging 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "io" 10 | "strings" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | func TestNow(t *testing.T) { 16 | checkTime := func(timeStamp string) { 17 | logTime, err := time.Parse(timeFmt, timeStamp) 18 | if err != nil { 19 | t.Fatal("log time failed to parse:", err) 20 | } 21 | if time.Now().Before(logTime) { 22 | t.Fatal("log happened in the future?") 23 | } 24 | } 25 | // we just want to be sure now() returns a valid date string 26 | checkTime(now()) 27 | 28 | // pull apart log message and check timestamp 29 | var buf bytes.Buffer 30 | config := newConfig(&buf) 31 | writer, err := newWriter(config) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | writer.Write([]byte("XXX")) 36 | logSplit := strings.Split(buf.String(), " ") 37 | checkTime(logSplit[0]) 38 | if logSplit[1] != "XXX" { 39 | t.Fatalf("Where'd the XXX go?\n(%#v)", logSplit) 40 | } 41 | } 42 | 43 | func newConfig(w io.Writer) *Config { 44 | return &Config{ 45 | Level: "INFO", 46 | Writer: w, 47 | } 48 | } 49 | 50 | func TestWriter(t *testing.T) { 51 | // mock/de-mock now() func 52 | defer func(orig func() string) { now = orig }(now) 53 | now = func() string { return "*NOW*" } 54 | 55 | type testCase struct { 56 | name string 57 | input, output string 58 | } 59 | runTest := func(tc testCase) { 60 | var buf bytes.Buffer 61 | config := newConfig(&buf) 62 | writer, err := newWriter(config) 63 | if err != nil { 64 | t.Error(err) 65 | } 66 | n, err := writer.Write([]byte(tc.input)) 67 | if err != nil { 68 | t.Error(err) 69 | } 70 | if n != len(tc.input) { 71 | t.Errorf("byte count (%d) doesn't match output len (%d).", 72 | n, len(tc.input)) 73 | } 74 | if buf.String() != tc.output { 75 | t.Errorf("unexpected log output string: '%s'", buf.String()) 76 | } 77 | } 78 | 79 | for i, tc := range []testCase{ 80 | { 81 | name: "null", 82 | input: "", 83 | output: "", 84 | }, 85 | { 86 | name: "err", 87 | input: "[ERR] (test) should write", 88 | output: "*NOW* [ERR] (test) should write", 89 | }, 90 | { 91 | name: "warn", 92 | input: "[WARN] (test) should write", 93 | output: "*NOW* [WARN] (test) should write", 94 | }, 95 | { 96 | name: "info", 97 | input: "[INFO] (test) should write", 98 | output: "*NOW* [INFO] (test) should write", 99 | }, 100 | { 101 | name: "debug", 102 | input: "[DEBUG] (test) should not write", 103 | output: "", 104 | }, 105 | { 106 | name: "trace", 107 | input: "[TRACE] (test) should not write", 108 | output: "", 109 | }, 110 | } { 111 | t.Run(fmt.Sprintf("%d_%s", i, tc.name), 112 | func(t *testing.T) { 113 | runTest(tc) 114 | }) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /logging/syslog.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package logging 5 | 6 | import ( 7 | "bytes" 8 | 9 | "github.com/hashicorp/go-syslog" 10 | "github.com/hashicorp/logutils" 11 | ) 12 | 13 | // syslogPriorityMap is used to map a log level to a syslog priority level. 14 | var syslogPriorityMap = map[string]gsyslog.Priority{ 15 | "DEBUG": gsyslog.LOG_INFO, 16 | "INFO": gsyslog.LOG_NOTICE, 17 | "WARN": gsyslog.LOG_WARNING, 18 | "ERR": gsyslog.LOG_ERR, 19 | } 20 | 21 | // SyslogWrapper is used to cleanup log messages before writing them to a 22 | // Syslogger. Implements the io.Writer interface. 23 | type SyslogWrapper struct { 24 | l gsyslog.Syslogger 25 | filt *logutils.LevelFilter 26 | } 27 | 28 | // Write is used to implement io.Writer. 29 | func (s *SyslogWrapper) Write(p []byte) (int, error) { 30 | // Skip syslog if the log level doesn't apply 31 | if !s.filt.Check(p) { 32 | return 0, nil 33 | } 34 | 35 | // Extract log level 36 | var level string 37 | afterLevel := p 38 | x := bytes.IndexByte(p, '[') 39 | if x >= 0 { 40 | y := bytes.IndexByte(p[x:], ']') 41 | if y >= 0 { 42 | level = string(p[x+1 : x+y]) 43 | afterLevel = p[x+y+2:] 44 | } 45 | } 46 | 47 | // Each log level will be handled by a specific syslog priority. 48 | priority, ok := syslogPriorityMap[level] 49 | if !ok { 50 | priority = gsyslog.LOG_NOTICE 51 | } 52 | 53 | // Attempt the write 54 | err := s.l.WriteLevel(priority, afterLevel) 55 | return len(p), err 56 | } 57 | -------------------------------------------------------------------------------- /logging/syslog_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package logging 5 | 6 | import ( 7 | "io" 8 | "os" 9 | "runtime" 10 | "testing" 11 | 12 | gsyslog "github.com/hashicorp/go-syslog" 13 | "github.com/hashicorp/logutils" 14 | ) 15 | 16 | func TestSyslogFilter(t *testing.T) { 17 | if runtime.GOOS == "windows" { 18 | t.SkipNow() 19 | } 20 | 21 | // Travis does not support syslog for some reason 22 | for _, ci_env := range []string{"TRAVIS", "CIRCLECI"} { 23 | if ci := os.Getenv(ci_env); ci != "" { 24 | t.SkipNow() 25 | } 26 | } 27 | 28 | l, err := gsyslog.NewLogger(gsyslog.LOG_NOTICE, "LOCAL0", "consul-template") 29 | if err != nil { 30 | t.Fatalf("err: %s", err) 31 | } 32 | 33 | filt, err := newLogFilter(io.Discard, logutils.LogLevel("INFO")) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | s := &SyslogWrapper{l, filt} 39 | infotest := []byte("[INFO] test") 40 | n, err := s.Write(infotest) 41 | if err != nil { 42 | t.Fatalf("err: %s", err) 43 | } 44 | if n == 0 { 45 | t.Fatalf("should have logged") 46 | } 47 | if n != len(infotest) { 48 | t.Fatalf("byte count (%d) doesn't match output len (%d).", 49 | n, len(infotest)) 50 | } 51 | 52 | n, err = s.Write([]byte("[DEBUG] test")) 53 | if err != nil { 54 | t.Fatalf("err: %s", err) 55 | } 56 | if n != 0 { 57 | t.Fatalf("should not have logged") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main // import "github.com/hashicorp/consul-template" 5 | 6 | import "os" 7 | 8 | func main() { 9 | cli := NewCLI(os.Stdout, os.Stderr) 10 | os.Exit(cli.Run(os.Args)) 11 | } 12 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | "testing" 12 | 13 | dep "github.com/hashicorp/consul-template/dependency" 14 | "github.com/hashicorp/consul-template/test" 15 | "github.com/hashicorp/consul/sdk/testutil" 16 | ) 17 | 18 | var ( 19 | testConsul *testutil.TestServer 20 | testClients *dep.ClientSet 21 | ) 22 | 23 | func TestMain(m *testing.M) { 24 | tb := &test.TestingTB{} 25 | consul, err := testutil.NewTestServerConfigT(tb, 26 | func(c *testutil.TestServerConfig) { 27 | c.LogLevel = "warn" 28 | c.Stdout = io.Discard 29 | c.Stderr = io.Discard 30 | }) 31 | if err != nil { 32 | log.Fatal(fmt.Errorf("failed to start consul server: %v", err)) 33 | } 34 | testConsul = consul 35 | log.SetOutput(io.Discard) 36 | 37 | clients := dep.NewClientSet() 38 | if err := clients.CreateConsulClient(&dep.CreateConsulClientInput{ 39 | Address: testConsul.HTTPAddr, 40 | }); err != nil { 41 | testConsul.Stop() 42 | log.Fatal(err) 43 | } 44 | testClients = clients 45 | 46 | exitCh := make(chan int, 1) 47 | func() { 48 | defer func() { 49 | // Attempt to recover from a panic and stop the server. If we don't stop 50 | // it, the panic will cause the server to remain running in the 51 | // background. Here we catch the panic and the re-raise it. 52 | if r := recover(); r != nil { 53 | testConsul.Stop() 54 | panic(r) 55 | } 56 | }() 57 | 58 | exitCh <- m.Run() 59 | }() 60 | 61 | exit := <-exitCh 62 | 63 | tb.DoCleanup() 64 | testConsul.Stop() 65 | os.Exit(exit) 66 | } 67 | -------------------------------------------------------------------------------- /manager/dedup_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package manager 5 | 6 | import ( 7 | "testing" 8 | "time" 9 | 10 | "github.com/hashicorp/consul-template/dependency" 11 | "github.com/hashicorp/consul-template/template" 12 | ) 13 | 14 | func TestDedup_StartStop(t *testing.T) { 15 | dedup := testDedupManager(t, nil) 16 | 17 | // Start and stop 18 | if err := dedup.Start(); err != nil { 19 | t.Fatal(err) 20 | } 21 | if err := dedup.Stop(); err != nil { 22 | t.Fatal(err) 23 | } 24 | } 25 | 26 | func TestDedup_IsLeader(t *testing.T) { 27 | sessionCreateRetry = 100 * time.Millisecond 28 | 29 | // Create a template 30 | tmpl, err := template.NewTemplate(&template.NewTemplateInput{ 31 | Contents: `template-1 {{ range service "consul" }}{{ .Node }}{{ end }}`, 32 | }) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | dedup := testDedupManager(t, []*template.Template{tmpl}) 38 | if err := dedup.Start(); err != nil { 39 | t.Fatal(err) 40 | } 41 | defer dedup.Stop() 42 | 43 | // Wait until we are leader 44 | select { 45 | case <-dedup.UpdateCh(): 46 | case <-time.After(4 * time.Second): 47 | t.Fatalf("timeout") 48 | } 49 | 50 | // Check that we are the leader 51 | if !dedup.IsLeader(tmpl) { 52 | t.Fatalf("should be leader") 53 | } 54 | } 55 | 56 | func TestDedup_UpdateDeps(t *testing.T) { 57 | // Create a template 58 | tmpl, err := template.NewTemplate(&template.NewTemplateInput{ 59 | Contents: `template-2 {{ range service "consul" }}{{ .Node }}{{ end }}`, 60 | }) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | 65 | dedup := testDedupManager(t, []*template.Template{tmpl}) 66 | if err := dedup.Start(); err != nil { 67 | t.Fatal(err) 68 | } 69 | defer dedup.Stop() 70 | 71 | // Wait until we are leader 72 | select { 73 | case <-dedup.UpdateCh(): 74 | case <-time.After(2 * time.Second): 75 | t.Fatalf("timeout") 76 | } 77 | 78 | // Create the dependency 79 | dep, err := dependency.NewHealthServiceQuery("consul") 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | 84 | // Inject data into the brain 85 | dedup.brain.Remember(dep, 123) 86 | 87 | // Update the dependencies 88 | err = dedup.UpdateDeps(tmpl, []dependency.Dependency{dep}) 89 | if err != nil { 90 | t.Fatal(err) 91 | } 92 | } 93 | 94 | func TestDedup_FollowerUpdate(t *testing.T) { 95 | // hangs on this sometimes, so make as short as possible 96 | lockWaitTime = 100 * time.Millisecond 97 | 98 | // Create a template 99 | tmpl, err := template.NewTemplate(&template.NewTemplateInput{ 100 | Contents: `template-3 {{ range service "consul" }}{{ .Node }}{{ end }}`, 101 | }) 102 | if err != nil { 103 | t.Fatal(err) 104 | } 105 | 106 | dedup1 := testDedupManager(t, []*template.Template{tmpl}) 107 | if err := dedup1.Start(); err != nil { 108 | t.Fatal(err) 109 | } 110 | defer dedup1.Stop() 111 | 112 | dedup2 := testDedupManager(t, []*template.Template{tmpl}) 113 | if err := dedup2.Start(); err != nil { 114 | t.Fatal(err) 115 | } 116 | defer dedup2.Stop() 117 | 118 | // Wait until we have a leader 119 | var leader, follow *DedupManager 120 | select { 121 | case <-dedup1.UpdateCh(): 122 | if dedup1.IsLeader(tmpl) { 123 | leader = dedup1 124 | follow = dedup2 125 | } 126 | case <-dedup2.UpdateCh(): 127 | if dedup2.IsLeader(tmpl) { 128 | leader = dedup2 129 | follow = dedup1 130 | } 131 | case <-time.After(2 * time.Second): 132 | t.Fatalf("timeout") 133 | } 134 | 135 | // Create the dependency 136 | dep, err := dependency.NewHealthServiceQuery("consul") 137 | if err != nil { 138 | t.Fatal(err) 139 | } 140 | 141 | // Inject data into the brain 142 | leader.brain.Remember(dep, 123) 143 | 144 | // Update the dependencies 145 | err = leader.UpdateDeps(tmpl, []dependency.Dependency{dep}) 146 | if err != nil { 147 | t.Fatal(err) 148 | } 149 | 150 | // Follower should get an update 151 | select { 152 | case <-follow.UpdateCh(): 153 | case <-time.After(2 * time.Second): 154 | t.Fatalf("timeout") 155 | } 156 | 157 | // Recall from the brain 158 | data, ok := follow.brain.Recall(dep) 159 | if !ok { 160 | t.Fatalf("missing data") 161 | } 162 | if data != 123 { 163 | t.Fatalf("bad: %v", data) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /manager/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package manager 5 | 6 | import "fmt" 7 | 8 | // ErrExitable is an interface that defines an integer ExitStatus() function. 9 | type ErrExitable interface { 10 | ExitStatus() int 11 | } 12 | 13 | var ( 14 | _ error = new(ErrChildDied) 15 | _ ErrExitable = new(ErrChildDied) 16 | ) 17 | 18 | // ErrChildDied is the error returned when the child process prematurely dies. 19 | type ErrChildDied struct { 20 | code int 21 | } 22 | 23 | // NewErrChildDied creates a new error with the given exit code. 24 | func NewErrChildDied(c int) *ErrChildDied { 25 | return &ErrChildDied{code: c} 26 | } 27 | 28 | // Error implements the error interface. 29 | func (e *ErrChildDied) Error() string { 30 | return fmt.Sprintf("child process exited with code %d", e.code) 31 | } 32 | 33 | // ExitStatus implements the ErrExitable interface. 34 | func (e *ErrChildDied) ExitStatus() int { 35 | return e.code 36 | } 37 | -------------------------------------------------------------------------------- /manager/example_extfuncmap_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package manager_test 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | "path" 12 | "text/template" 13 | 14 | "github.com/hashicorp/consul-template/config" 15 | "github.com/hashicorp/consul-template/manager" 16 | ) 17 | 18 | // ExampleCustomFuncMap demonstrates a minimum [consul-template/manager] 19 | // configuration and supply custom templates to consul-template's internal 20 | // [text/template] based renderer. 21 | // 22 | // It is not comprehensive and does not demonstrate the dependencies, polling, 23 | // and rerendering features available in the manager 24 | func Example_customFuncMap() { 25 | // Consul-template uses the standard logger, which needs to be silenced 26 | // in this example 27 | log.SetOutput(io.Discard) 28 | 29 | // Create a simple function to add to CT 30 | greet := func(n string) string { return "Hello, " + n + "!" } 31 | fm := template.FuncMap{"greet": greet} 32 | 33 | // Define a template that uses the new function 34 | tmpl := `{{greet "World"}}` 35 | 36 | // Make a destination path to write the rendered template to 37 | outPath := path.Join(os.TempDir(), "tmpl.out") // Use the temp dir 38 | defer os.RemoveAll(outPath) // Defer the file cleanup 39 | 40 | // Create a TemplateConfig 41 | tc1 := config.DefaultTemplateConfig() // Start with the default configuration 42 | tc1.ExtFuncMap = fm // Use the ExtFuncMap to add greet 43 | tc1.Contents = &tmpl // Add the template to the configuration 44 | tc1.Destination = &outPath // Set the output destination 45 | tc1.Finalize() // Finalize the template config 46 | 47 | // Create the (consul-template) Config 48 | cfg := config.DefaultConfig() // Start with default configuration 49 | cfg.Once = true // Perform a one-shot render 50 | cfg.Templates = &config.TemplateConfigs{tc1} // Add the template created earlier 51 | cfg.Finalize() // Finalize the consul-template configuration 52 | 53 | // Instantiate a runner with the config and with `dry` == false 54 | runner, err := manager.NewRunner(cfg, false) 55 | if err != nil { 56 | fmt.Printf("[ERROR] %s\n", err.Error()) 57 | return 58 | } 59 | 60 | go runner.Start() // The runner blocks, so must be started in a goroutine 61 | defer runner.Stop() 62 | 63 | select { 64 | 65 | // When the runner is successfully done, it will emit a message on DoneCh 66 | case <-runner.DoneCh: 67 | break 68 | 69 | // When the runner encounters an error, it will emit an error on ErrCh and 70 | // then return. 71 | case err := <-runner.ErrCh: 72 | fmt.Printf("[ERROR] %s\n", err.Error()) 73 | return 74 | } 75 | 76 | // Read the rendered template from disk 77 | if b, e := os.ReadFile(outPath); e == nil { 78 | fmt.Println(string(b)) 79 | } else { 80 | fmt.Printf("[ERROR] %s\n", err.Error()) 81 | return 82 | } 83 | 84 | // Output: 85 | // Hello, World! 86 | } 87 | -------------------------------------------------------------------------------- /manager/example_manager_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package manager_test 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | "path" 12 | 13 | "github.com/hashicorp/consul-template/config" 14 | "github.com/hashicorp/consul-template/manager" 15 | ) 16 | 17 | // This example demonstrates a minimum configuration to create, configure, and 18 | // use a consul-template/manager from code. It is not comprehensive and does not 19 | // demonstrate the dependencies, polling, and rerendering features available in 20 | // the manager 21 | func Example() { 22 | // Consul-template uses the standard logger, which needs to be silenced 23 | // in this example 24 | log.SetOutput(io.Discard) 25 | 26 | // Define a template 27 | tmpl := `{{ "foo\nbar\n" | split "\n" | toJSONPretty }}` 28 | 29 | // Make a destination path to write the rendered template to 30 | outPath := path.Join(os.TempDir(), "tmpl.out") // Use the temp dir 31 | defer os.RemoveAll(outPath) // Defer the file cleanup 32 | 33 | // Create a TemplateConfig 34 | tCfg := config.DefaultTemplateConfig() // Start with the default configuration 35 | tCfg.Contents = &tmpl // Add the template to the configuration 36 | tCfg.Destination = &outPath // Set the output destination 37 | tCfg.Finalize() // Finalize the template config 38 | 39 | // Create the (consul-template) Config 40 | cfg := config.DefaultConfig() // Start with default configuration 41 | cfg.Once = true // Perform a one-shot render 42 | cfg.Templates = &config.TemplateConfigs{tCfg} // Add the template created earlier 43 | cfg.Finalize() // Finalize the consul-template configuration 44 | 45 | // Instantiate a runner with the config and with `dry` == false 46 | runner, err := manager.NewRunner(cfg, false) 47 | if err != nil { 48 | fmt.Printf("[ERROR] %s\n", err.Error()) 49 | return 50 | } 51 | 52 | go runner.Start() // The runner blocks, so must be started in a goroutine 53 | defer runner.Stop() 54 | 55 | select { 56 | // When the runner is successfully done, it will emit a message on DoneCh 57 | case <-runner.DoneCh: 58 | break 59 | 60 | // When the runner encounters an error, it will emit an error on ErrCh and 61 | // then return. 62 | case err := <-runner.ErrCh: 63 | fmt.Printf("[ERROR] %s\n", err.Error()) 64 | return 65 | } 66 | 67 | // Read the rendered template from disk 68 | if b, e := os.ReadFile(outPath); e == nil { 69 | fmt.Println(string(b)) 70 | } else { 71 | fmt.Printf("[ERROR] %s\n", err.Error()) 72 | return 73 | } 74 | // Output: 75 | // [ 76 | // "foo", 77 | // "bar" 78 | // ] 79 | } 80 | -------------------------------------------------------------------------------- /manager/manager_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package manager 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | "testing" 12 | 13 | "github.com/hashicorp/consul-template/config" 14 | dep "github.com/hashicorp/consul-template/dependency" 15 | "github.com/hashicorp/consul-template/template" 16 | "github.com/hashicorp/consul-template/test" 17 | "github.com/hashicorp/consul/sdk/testutil" 18 | ) 19 | 20 | var ( 21 | testConsul *testutil.TestServer 22 | testClients *dep.ClientSet 23 | ) 24 | 25 | func TestMain(m *testing.M) { 26 | log.SetOutput(io.Discard) 27 | tb := &test.TestingTB{} 28 | consul, err := testutil.NewTestServerConfigT(tb, 29 | func(c *testutil.TestServerConfig) { 30 | c.LogLevel = "warn" 31 | c.Stdout = io.Discard 32 | c.Stderr = io.Discard 33 | }) 34 | if err != nil { 35 | log.Fatal(fmt.Errorf("failed to start consul server: %v", err)) 36 | } 37 | testConsul = consul 38 | 39 | consulConfig := config.DefaultConsulConfig() 40 | consulConfig.Address = &testConsul.HTTPAddr 41 | clients, err := NewClientSet(&config.Config{ 42 | Consul: consulConfig, 43 | Vault: config.DefaultVaultConfig(), 44 | Nomad: config.DefaultNomadConfig(), 45 | }) 46 | if err != nil { 47 | testConsul.Stop() 48 | log.Fatal(fmt.Errorf("failed to start clients: %v", err)) 49 | } 50 | testClients = clients 51 | 52 | exitCh := make(chan int, 1) 53 | func() { 54 | defer func() { 55 | // Attempt to recover from a panic and stop the server. If we don't stop 56 | // it, the panic will cause the server to remain running in the 57 | // background. Here we catch the panic and the re-raise it. 58 | if r := recover(); r != nil { 59 | testConsul.Stop() 60 | panic(r) 61 | } 62 | }() 63 | 64 | exitCh <- m.Run() 65 | }() 66 | 67 | exit := <-exitCh 68 | 69 | tb.DoCleanup() 70 | testConsul.Stop() 71 | os.Exit(exit) 72 | } 73 | 74 | func testDedupManager(t *testing.T, tmpls []*template.Template) *DedupManager { 75 | brain := template.NewBrain() 76 | dedupConfig := config.TestConfig(nil).Dedup 77 | dedup, err := NewDedupManager(dedupConfig, testClients, brain, tmpls) 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | return dedup 82 | } 83 | -------------------------------------------------------------------------------- /renderer/file_ownership.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build !windows 5 | // +build !windows 6 | 7 | package renderer 8 | 9 | import ( 10 | "os" 11 | "os/user" 12 | "strconv" 13 | "syscall" 14 | ) 15 | 16 | func getFileOwnership(path string) (int, int, error) { 17 | fileInfo, err := os.Stat(path) 18 | if err != nil { 19 | return 0, 0, err 20 | } 21 | 22 | fileSys := fileInfo.Sys() 23 | st := fileSys.(*syscall.Stat_t) 24 | return int(st.Uid), int(st.Gid), nil 25 | } 26 | 27 | func setFileOwnership(path string, uid, gid int) error { 28 | if uid == -1 && gid == -1 { 29 | return nil // noop 30 | } 31 | return os.Chown(path, uid, gid) 32 | } 33 | 34 | func isChownNeeded(path string, uid, gid int) (bool, error) { 35 | if uid == -1 && gid == -1 { 36 | return false, nil 37 | } 38 | 39 | currUid, currGid, err := getFileOwnership(path) 40 | if err != nil { 41 | return false, err 42 | } 43 | 44 | switch { 45 | case uid == -1: 46 | currUid = -1 47 | case gid == -1: 48 | currGid = -1 49 | } 50 | 51 | return uid != currUid || gid != currGid, nil 52 | } 53 | 54 | // parseUidGid parses the uid/gid so that it can be input to os.Chown 55 | func parseUidGid(s string) (int, error) { 56 | if s == "" { 57 | return -1, nil 58 | } 59 | return strconv.Atoi(s) 60 | } 61 | 62 | func lookupUser(s string) (int, error) { 63 | if id, err := parseUidGid(s); err == nil { 64 | return id, nil 65 | } 66 | u, err := user.Lookup(s) 67 | if err != nil { 68 | return 0, err 69 | } 70 | return strconv.Atoi(u.Uid) 71 | } 72 | 73 | func lookupGroup(s string) (int, error) { 74 | if id, err := parseUidGid(s); err == nil { 75 | return id, nil 76 | } 77 | u, err := user.LookupGroup(s) 78 | if err != nil { 79 | return 0, err 80 | } 81 | return strconv.Atoi(u.Gid) 82 | } 83 | -------------------------------------------------------------------------------- /renderer/file_ownership_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build windows 5 | // +build windows 6 | 7 | package renderer 8 | 9 | import ( 10 | "fmt" 11 | ) 12 | 13 | var notSupportedError error = fmt.Errorf("managing file ownership is not supported on Windows") 14 | 15 | func setFileOwnership(path string, uid, gid int) error { 16 | if uid == -1 && gid == -1 { 17 | return nil 18 | } 19 | return notSupportedError 20 | } 21 | 22 | func isChownNeeded(path string, wantedUid, wantedGid int) (bool, error) { 23 | if wantedUid == -1 && wantedGid == -1 { 24 | return false, nil 25 | } 26 | return false, notSupportedError 27 | } 28 | 29 | func lookupUser(user string) (int, error) { 30 | if user == "" { 31 | return -1, nil 32 | } 33 | return 0, notSupportedError 34 | } 35 | 36 | func lookupGroup(group string) (int, error) { 37 | if group == "" { 38 | return -1, nil 39 | } 40 | return 0, notSupportedError 41 | } 42 | -------------------------------------------------------------------------------- /renderer/file_perms.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build !windows 5 | // +build !windows 6 | 7 | package renderer 8 | 9 | import ( 10 | "os" 11 | "syscall" 12 | ) 13 | 14 | func preserveFilePermissions(path string, fileInfo os.FileInfo) error { 15 | sysInfo := fileInfo.Sys() 16 | if sysInfo != nil { 17 | stat, ok := sysInfo.(*syscall.Stat_t) 18 | if ok { 19 | if err := os.Chown(path, int(stat.Uid), int(stat.Gid)); err != nil { 20 | return err 21 | } 22 | } 23 | } 24 | 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /renderer/file_perms_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build windows 5 | // +build windows 6 | 7 | package renderer 8 | 9 | import "os" 10 | 11 | // Not done as Windows doedsn't realiably support permissions setting. 12 | // https://github.com/google/renameio/issues/17 13 | func preserveFilePermissions(path string, fileInfo os.FileInfo) error { 14 | return nil 15 | } 16 | -------------------------------------------------------------------------------- /scripts/readme-toc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | TOC="## Table of Contents" 7 | FIRST="## Community Support" 8 | 9 | tmpfile=$(dirname $0)/tmp.md 10 | 11 | # prints everything up to TOC header 12 | sed -n "0,/^${TOC}/p" < README.md > $tmpfile 13 | # print the TOC 14 | printf "\n" >> $tmpfile 15 | cat README.md \ 16 | `# strip out code blocks` \ 17 | | sed -e '/```/ r pf' -e '/```/,/```/d' \ 18 | `# pull out header lines, skipping first 2` \ 19 | | grep "^#" \ 20 | | tail -n +3 \ 21 | `# strip out bad characters` \ 22 | | tr -d '`' \ 23 | `# format as [header](link)` \ 24 | | sed -e 's/# \([a-zA-Z0-9. -]\+\)/- [\1](#\L\1)/' \ 25 | `# replace spaces in '(link)' with dashes` \ 26 | | awk -F'(' '{for(i=2;i<=NF;i++)if(i==2)gsub(" ","-",$i);}1' OFS='(' \ 27 | `# remove dots '.' in '(link)'` \ 28 | | awk -F'(' '{for(i=2;i<=NF;i++)if(i==2)gsub("\.","",$i);}1' OFS='(' \ 29 | `# convert header to indention (brute force)` \ 30 | | sed -e 's/^####/ /' \ 31 | | sed -e 's/^###/ /' \ 32 | | sed -e 's/^##/ /' \ 33 | | sed -e 's/^#//' \ 34 | >> $tmpfile 35 | printf "\n\n" >> $tmpfile 36 | # print the rest of the file, starting with FIRST header 37 | sed -n "/^${FIRST}/,$ p" < README.md >> $tmpfile 38 | # copy over original 39 | mv $tmpfile README.md 40 | -------------------------------------------------------------------------------- /service_os/service.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package service_os 5 | 6 | var chanGraceExit = make(chan int) 7 | 8 | // ShutdownChannel returns a channel that sends a message that a shutdown 9 | // signal has been received for the service. 10 | func Shutdown_Channel() <-chan int { 11 | return chanGraceExit 12 | } 13 | -------------------------------------------------------------------------------- /service_os/service_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build windows 5 | // +build windows 6 | 7 | package service_os 8 | 9 | import ( 10 | wsvc "golang.org/x/sys/windows/svc" 11 | ) 12 | 13 | type serviceWindows struct{} 14 | 15 | func init() { 16 | interactive, err := wsvc.IsAnInteractiveSession() 17 | if err != nil { 18 | panic(err) 19 | } 20 | // Cannot run as a service when running interactively 21 | if interactive { 22 | return 23 | } 24 | go wsvc.Run("", serviceWindows{}) 25 | } 26 | 27 | // Execute implements the Windows service Handler type. It will be 28 | // called at the start of the service, and the service will exit 29 | // once Execute completes. 30 | func (serviceWindows) Execute(args []string, r <-chan wsvc.ChangeRequest, s chan<- wsvc.Status) (svcSpecificEC bool, exitCode uint32) { 31 | const accCommands = wsvc.AcceptStop | wsvc.AcceptShutdown 32 | s <- wsvc.Status{State: wsvc.Running, Accepts: accCommands} 33 | for { 34 | c := <-r 35 | switch c.Cmd { 36 | case wsvc.Interrogate: 37 | s <- c.CurrentStatus 38 | case wsvc.Stop, wsvc.Shutdown: 39 | s <- wsvc.Status{State: wsvc.StopPending} 40 | chanGraceExit <- 1 41 | return false, 0 42 | } 43 | } 44 | 45 | return false, 0 46 | } 47 | -------------------------------------------------------------------------------- /signals/mapstructure.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package signals 5 | 6 | import ( 7 | "reflect" 8 | 9 | "github.com/mitchellh/mapstructure" 10 | ) 11 | 12 | // StringToSignalFunc parses a string as a signal based on the signal lookup 13 | // table. If the user supplied an empty string or nil, a special "nil signal" 14 | // is returned. Clients should check for this value and set the response back 15 | // nil after mapstructure finishes parsing. 16 | func StringToSignalFunc() mapstructure.DecodeHookFunc { 17 | return func( 18 | f reflect.Type, 19 | t reflect.Type, 20 | data interface{}, 21 | ) (interface{}, error) { 22 | if f.Kind() != reflect.String { 23 | return data, nil 24 | } 25 | 26 | if t.String() != "os.Signal" { 27 | return data, nil 28 | } 29 | 30 | if data == nil || data.(string) == "" { 31 | return SIGNULL, nil 32 | } 33 | 34 | return Parse(data.(string)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /signals/mapstructure_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package signals 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "reflect" 10 | "syscall" 11 | "testing" 12 | 13 | "github.com/mitchellh/mapstructure" 14 | ) 15 | 16 | func TestStringToSignalFunc(t *testing.T) { 17 | f := StringToSignalFunc() 18 | sigType := reflect.ValueOf(&os.Interrupt).Elem() 19 | 20 | cases := []struct { 21 | name string 22 | f, t reflect.Value 23 | expected interface{} 24 | err bool 25 | }{ 26 | {"sigterm", reflect.ValueOf("SIGTERM"), sigType, syscall.SIGTERM, false}, 27 | {"sigint", reflect.ValueOf("SIGINT"), sigType, syscall.SIGINT, false}, 28 | 29 | // Invalid signal name 30 | {"bad", reflect.ValueOf("BACON"), sigType, nil, true}, 31 | } 32 | 33 | for i, tc := range cases { 34 | t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) { 35 | actual, err := mapstructure.DecodeHookExec(f, tc.f, tc.t) 36 | if (err != nil) != tc.err { 37 | t.Errorf("case %d: %v", i, err) 38 | } 39 | if !reflect.DeepEqual(actual, tc.expected) { 40 | t.Errorf("case %d: expected %#v (%T) to be %#v (%T)", 41 | i, actual, actual, tc.expected, tc.expected) 42 | } 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /signals/signals.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package signals 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "sort" 10 | "strings" 11 | "syscall" 12 | ) 13 | 14 | // SIGNULL is the nil/zero signal. 15 | var SIGNULL os.Signal = syscall.Signal(0) 16 | 17 | // ValidSignals is the list of all valid signals. This is built at runtime 18 | // because it is OS-dependent. 19 | var ValidSignals []string 20 | var MonitoredSignals []os.Signal 21 | 22 | func init() { 23 | valid := make([]string, 0, len(SignalLookup)) 24 | monitored := make([]os.Signal, 0, len(SignalLookup)) 25 | for k, v := range SignalLookup { 26 | valid = append(valid, k) 27 | monitored = append(monitored, v) 28 | } 29 | sort.Strings(valid) 30 | ValidSignals = valid 31 | MonitoredSignals = monitored 32 | } 33 | 34 | // Parse parses the given string as a signal. If the signal is not found, 35 | // an error is returned. 36 | func Parse(s string) (os.Signal, error) { 37 | sig, ok := SignalLookup[strings.ToUpper(s)] 38 | if !ok { 39 | return nil, fmt.Errorf("invalid signal %q - valid signals are %q", 40 | s, ValidSignals) 41 | } 42 | return sig, nil 43 | } 44 | -------------------------------------------------------------------------------- /signals/signals_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package signals 5 | 6 | import ( 7 | "strings" 8 | "syscall" 9 | "testing" 10 | ) 11 | 12 | func TestParse(t *testing.T) { 13 | s, err := Parse("SIGHUP") 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | if s != syscall.SIGHUP { 18 | t.Errorf("expected %#v to be %#v", s, syscall.SIGHUP) 19 | } 20 | } 21 | 22 | func TestParse_case(t *testing.T) { 23 | s, err := Parse("sighup") 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | if s != syscall.SIGHUP { 28 | t.Errorf("expected %#v to be %#v", s, syscall.SIGHUP) 29 | } 30 | } 31 | 32 | func TestParse_invalid(t *testing.T) { 33 | _, err := Parse("neverasignalnope") 34 | if err == nil { 35 | t.Fatal("expected error") 36 | } 37 | if !strings.Contains(err.Error(), "valid signals") { 38 | t.Errorf("bad error: %s", err) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /signals/signals_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build linux || darwin || freebsd || openbsd || solaris || netbsd 5 | // +build linux darwin freebsd openbsd solaris netbsd 6 | 7 | package signals 8 | 9 | import ( 10 | "os" 11 | "syscall" 12 | ) 13 | 14 | //// Ignored Signals 15 | // SIGCHLD - don't propagate these to child process as we manage it instead 16 | // SIGURG - used by the golang scheduler for parallel runtime. 17 | 18 | var SignalLookup = map[string]os.Signal{ 19 | "SIGNULL": SIGNULL, 20 | "SIGABRT": syscall.SIGABRT, 21 | "SIGALRM": syscall.SIGALRM, 22 | "SIGBUS": syscall.SIGBUS, 23 | "SIGCONT": syscall.SIGCONT, 24 | "SIGFPE": syscall.SIGFPE, 25 | "SIGHUP": syscall.SIGHUP, 26 | "SIGILL": syscall.SIGILL, 27 | "SIGINT": syscall.SIGINT, 28 | "SIGIO": syscall.SIGIO, 29 | "SIGIOT": syscall.SIGIOT, 30 | "SIGKILL": syscall.SIGKILL, 31 | "SIGPIPE": syscall.SIGPIPE, 32 | "SIGPROF": syscall.SIGPROF, 33 | "SIGQUIT": syscall.SIGQUIT, 34 | "SIGSEGV": syscall.SIGSEGV, 35 | "SIGSTOP": syscall.SIGSTOP, 36 | "SIGSYS": syscall.SIGSYS, 37 | "SIGTERM": syscall.SIGTERM, 38 | "SIGTRAP": syscall.SIGTRAP, 39 | "SIGTSTP": syscall.SIGTSTP, 40 | "SIGTTIN": syscall.SIGTTIN, 41 | "SIGTTOU": syscall.SIGTTOU, 42 | "SIGUSR1": syscall.SIGUSR1, 43 | "SIGUSR2": syscall.SIGUSR2, 44 | "SIGWINCH": syscall.SIGWINCH, 45 | "SIGXCPU": syscall.SIGXCPU, 46 | "SIGXFSZ": syscall.SIGXFSZ, 47 | } 48 | -------------------------------------------------------------------------------- /signals/signals_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build windows 5 | // +build windows 6 | 7 | package signals 8 | 9 | import ( 10 | "os" 11 | "syscall" 12 | ) 13 | 14 | var SignalLookup = map[string]os.Signal{ 15 | "SIGNULL": SIGNULL, 16 | "SIGABRT": syscall.SIGABRT, 17 | "SIGALRM": syscall.SIGALRM, 18 | "SIGBUS": syscall.SIGBUS, 19 | "SIGFPE": syscall.SIGFPE, 20 | "SIGHUP": syscall.SIGHUP, 21 | "SIGILL": syscall.SIGILL, 22 | "SIGINT": syscall.SIGINT, 23 | "SIGKILL": syscall.SIGKILL, 24 | "SIGPIPE": syscall.SIGPIPE, 25 | "SIGQUIT": syscall.SIGQUIT, 26 | "SIGSEGV": syscall.SIGSEGV, 27 | "SIGTERM": syscall.SIGTERM, 28 | "SIGTRAP": syscall.SIGTRAP, 29 | } 30 | -------------------------------------------------------------------------------- /template/brain.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package template 5 | 6 | import ( 7 | "sync" 8 | 9 | dep "github.com/hashicorp/consul-template/dependency" 10 | ) 11 | 12 | // Brain is what Template uses to determine the values that are 13 | // available for template parsing. 14 | type Brain struct { 15 | sync.RWMutex 16 | 17 | // data is the map of individual dependencies and the most recent data for 18 | // that dependency. 19 | data map[string]interface{} 20 | 21 | // receivedData is an internal tracker of which dependencies have stored data 22 | // in the brain. 23 | receivedData map[string]struct{} 24 | } 25 | 26 | // NewBrain creates a new Brain with empty values for each 27 | // of the key structs. 28 | func NewBrain() *Brain { 29 | return &Brain{ 30 | data: make(map[string]interface{}), 31 | receivedData: make(map[string]struct{}), 32 | } 33 | } 34 | 35 | // Remember accepts a dependency and the data to store associated with that 36 | // dep. This function converts the given data to a proper type and stores 37 | // it internally. 38 | func (b *Brain) Remember(d dep.Dependency, data interface{}) { 39 | b.Lock() 40 | defer b.Unlock() 41 | 42 | b.data[d.String()] = data 43 | b.receivedData[d.String()] = struct{}{} 44 | } 45 | 46 | // Recall gets the current value for the given dependency in the Brain. 47 | func (b *Brain) Recall(d dep.Dependency) (interface{}, bool) { 48 | b.RLock() 49 | defer b.RUnlock() 50 | 51 | // If we have not received data for this dependency, return now. 52 | if _, ok := b.receivedData[d.String()]; !ok { 53 | return nil, false 54 | } 55 | 56 | return b.data[d.String()], true 57 | } 58 | 59 | // ForceSet is used to force set the value of a dependency 60 | // for a given hash code 61 | func (b *Brain) ForceSet(hashCode string, data interface{}) { 62 | b.Lock() 63 | defer b.Unlock() 64 | 65 | b.data[hashCode] = data 66 | b.receivedData[hashCode] = struct{}{} 67 | } 68 | 69 | // Forget accepts a dependency and removes all associated data with this 70 | // dependency. It also resets the "receivedData" internal map. 71 | func (b *Brain) Forget(d dep.Dependency) { 72 | b.Lock() 73 | defer b.Unlock() 74 | 75 | delete(b.data, d.String()) 76 | delete(b.receivedData, d.String()) 77 | } 78 | -------------------------------------------------------------------------------- /template/brain_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package template 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | 10 | dep "github.com/hashicorp/consul-template/dependency" 11 | ) 12 | 13 | func TestNewBrain(t *testing.T) { 14 | b := NewBrain() 15 | 16 | if b.data == nil { 17 | t.Errorf("expected data to not be nil") 18 | } 19 | 20 | if b.receivedData == nil { 21 | t.Errorf("expected receivedData to not be nil") 22 | } 23 | } 24 | 25 | func TestRecall(t *testing.T) { 26 | b := NewBrain() 27 | 28 | d, err := dep.NewCatalogNodesQuery("") 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | nodes := []*dep.Node{ 34 | { 35 | Node: "node", 36 | Address: "address", 37 | }, 38 | } 39 | 40 | b.Remember(d, nodes) 41 | 42 | data, ok := b.Recall(d) 43 | if !ok { 44 | t.Fatal("expected data from brain") 45 | } 46 | 47 | result := data.([]*dep.Node) 48 | if !reflect.DeepEqual(result, nodes) { 49 | t.Errorf("expected %#v to be %#v", result, nodes) 50 | } 51 | } 52 | 53 | func TestForceSet(t *testing.T) { 54 | b := NewBrain() 55 | 56 | d, err := dep.NewCatalogNodesQuery("") 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | nodes := []*dep.Node{ 62 | { 63 | Node: "node", 64 | Address: "address", 65 | }, 66 | } 67 | 68 | b.ForceSet(d.String(), nodes) 69 | 70 | data, ok := b.Recall(d) 71 | if !ok { 72 | t.Fatal("expected data from brain") 73 | } 74 | 75 | result := data.([]*dep.Node) 76 | if !reflect.DeepEqual(result, nodes) { 77 | t.Errorf("expected %#v to be %#v", result, nodes) 78 | } 79 | } 80 | 81 | func TestForget(t *testing.T) { 82 | b := NewBrain() 83 | 84 | d, err := dep.NewCatalogNodesQuery("") 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | 89 | nodes := []*dep.Node{ 90 | { 91 | Node: "node", 92 | Address: "address", 93 | }, 94 | } 95 | 96 | b.Remember(d, nodes) 97 | b.Forget(d) 98 | 99 | if _, ok := b.Recall(d); ok { 100 | t.Errorf("expected %#v to not be forgotten", d) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /template/scratch.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package template 5 | 6 | import ( 7 | "fmt" 8 | "sort" 9 | "sync" 10 | ) 11 | 12 | // Scratch is a wrapper around a map which is used by the template. 13 | type Scratch struct { 14 | once sync.Once 15 | sync.RWMutex 16 | values map[string]interface{} 17 | } 18 | 19 | // Key returns a boolean indicating whether the given key exists in the map. 20 | func (s *Scratch) Key(k string) bool { 21 | s.RLock() 22 | defer s.RUnlock() 23 | _, ok := s.values[k] 24 | return ok 25 | } 26 | 27 | // Get returns a value previously set by Add or Set 28 | func (s *Scratch) Get(k string) interface{} { 29 | s.RLock() 30 | defer s.RUnlock() 31 | return s.values[k] 32 | } 33 | 34 | // Set stores the value v at the key k. It will overwrite an existing value 35 | // if present. 36 | func (s *Scratch) Set(k string, v interface{}) string { 37 | s.init() 38 | 39 | s.Lock() 40 | defer s.Unlock() 41 | s.values[k] = v 42 | return "" 43 | } 44 | 45 | // SetX behaves the same as Set, except it will not overwrite existing keys if 46 | // already present. 47 | func (s *Scratch) SetX(k string, v interface{}) string { 48 | s.init() 49 | 50 | s.Lock() 51 | defer s.Unlock() 52 | if _, ok := s.values[k]; !ok { 53 | s.values[k] = v 54 | } 55 | return "" 56 | } 57 | 58 | // MapSet stores the value v into a key mk in the map named k. 59 | func (s *Scratch) MapSet(k, mk string, v interface{}) (string, error) { 60 | s.init() 61 | 62 | s.Lock() 63 | defer s.Unlock() 64 | return s.mapSet(k, mk, v, true) 65 | } 66 | 67 | // MapSetX behaves the same as MapSet, except it will not overwrite the map 68 | // key if it already exists. 69 | func (s *Scratch) MapSetX(k, mk string, v interface{}) (string, error) { 70 | s.init() 71 | 72 | s.Lock() 73 | defer s.Unlock() 74 | return s.mapSet(k, mk, v, false) 75 | } 76 | 77 | // mapSet is sets the value in the map, overwriting if o is true. This function 78 | // does not perform locking; callers should lock before invoking. 79 | // 80 | //nolint:unparam 81 | func (s *Scratch) mapSet(k, mk string, v interface{}, o bool) (string, error) { 82 | if _, ok := s.values[k]; !ok { 83 | s.values[k] = make(map[string]interface{}) 84 | } 85 | 86 | typed, ok := s.values[k].(map[string]interface{}) 87 | if !ok { 88 | return "", fmt.Errorf("%q is not a map", k) 89 | } 90 | 91 | if _, ok := typed[mk]; o || !ok { 92 | typed[mk] = v 93 | } 94 | return "", nil 95 | } 96 | 97 | // MapValues returns the list of values in the map sorted by key. 98 | func (s *Scratch) MapValues(k string) ([]interface{}, error) { 99 | s.init() 100 | 101 | s.Lock() 102 | defer s.Unlock() 103 | if s.values == nil { 104 | return nil, nil 105 | } 106 | 107 | typed, ok := s.values[k].(map[string]interface{}) 108 | if !ok { 109 | return nil, nil 110 | } 111 | 112 | keys := make([]string, 0, len(typed)) 113 | for k := range typed { 114 | keys = append(keys, k) 115 | } 116 | sort.Strings(keys) 117 | 118 | sorted := make([]interface{}, len(keys)) 119 | for i, k := range keys { 120 | sorted[i] = typed[k] 121 | } 122 | return sorted, nil 123 | } 124 | 125 | // init initializes the scratch. 126 | func (s *Scratch) init() { 127 | if s.values == nil { 128 | s.values = make(map[string]interface{}) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /template/testdata/sandbox/path/to/bad-symlink: -------------------------------------------------------------------------------- 1 | ../../../../funcs_test.go -------------------------------------------------------------------------------- /template/testdata/sandbox/path/to/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp/consul-template/21bd866a78ef669f9849ee8e8baaf4ba3f316c96/template/testdata/sandbox/path/to/file -------------------------------------------------------------------------------- /template/testdata/sandbox/path/to/ok-symlink: -------------------------------------------------------------------------------- 1 | file -------------------------------------------------------------------------------- /test/helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package test 5 | 6 | import ( 7 | "os" 8 | "strings" 9 | "sync" 10 | "testing" 11 | "time" 12 | 13 | "github.com/hashicorp/consul/sdk/testutil" 14 | "github.com/stretchr/testify/assert" 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | // CreateTempfile that will be closed and removed at the end of the test. 19 | func CreateTempfile(tb testing.TB, b []byte) *os.File { 20 | tb.Helper() 21 | 22 | f, err := os.CreateTemp(os.TempDir(), "") 23 | require.NoError(tb, err) 24 | 25 | tb.Cleanup(func() { 26 | fName := f.Name() 27 | assert.NoError(tb, f.Close()) 28 | assert.NoError(tb, os.Remove(fName)) 29 | }) 30 | 31 | if len(b) > 0 { 32 | _, err = f.Write(b) 33 | assert.NoError(tb, err) 34 | } 35 | 36 | return f 37 | } 38 | 39 | func DeleteTempfile(f *os.File, t *testing.T) { 40 | if err := os.Remove(f.Name()); err != nil { 41 | t.Fatal(err) 42 | } 43 | } 44 | 45 | func WaitForContents(t *testing.T, d time.Duration, p, c string) { 46 | errCh := make(chan error, 1) 47 | matchCh := make(chan struct{}, 1) 48 | stopCh := make(chan struct{}, 1) 49 | var last string 50 | 51 | go func() { 52 | for { 53 | select { 54 | case <-stopCh: 55 | return 56 | default: 57 | } 58 | 59 | actual, err := os.ReadFile(p) 60 | if err != nil && !os.IsNotExist(err) { 61 | errCh <- err 62 | return 63 | } 64 | 65 | last = string(actual) 66 | if strings.EqualFold(last, c) { 67 | close(matchCh) 68 | return 69 | } 70 | 71 | time.Sleep(10 * time.Millisecond) 72 | } 73 | }() 74 | 75 | select { 76 | case <-matchCh: 77 | case err := <-errCh: 78 | t.Fatal(err) 79 | case <-time.After(d): 80 | close(stopCh) 81 | t.Errorf("contents not present after %s, expected: %q, actual: %q", 82 | d, c, last) 83 | } 84 | } 85 | 86 | // Meets consul/sdk/testutil/TestingTB interface 87 | var _ testutil.TestingTB = (*TestingTB)(nil) 88 | 89 | type TestingTB struct { 90 | cleanup func() 91 | sync.Mutex 92 | } 93 | 94 | func (t *TestingTB) DoCleanup() { 95 | t.Lock() 96 | defer t.Unlock() 97 | t.cleanup() 98 | } 99 | 100 | func (*TestingTB) Failed() bool { return false } 101 | func (*TestingTB) Logf(string, ...interface{}) {} 102 | func (*TestingTB) Fatalf(string, ...interface{}) {} 103 | func (*TestingTB) Name() string { return "TestingTB" } 104 | func (*TestingTB) Helper() {} 105 | func (t *TestingTB) Cleanup(f func()) { 106 | t.Lock() 107 | defer t.Unlock() 108 | prev := t.cleanup 109 | t.cleanup = func() { 110 | f() 111 | if prev != nil { 112 | prev() 113 | } 114 | } 115 | } 116 | func (*TestingTB) Error(...any) {} 117 | func (*TestingTB) Errorf(string, ...any) {} 118 | func (*TestingTB) Fail() {} 119 | func (*TestingTB) FailNow() {} 120 | func (*TestingTB) Fatal(...any) {} 121 | func (*TestingTB) Log(...any) {} 122 | func (*TestingTB) Setenv(string, string) {} 123 | func (*TestingTB) TempDir() string { return "" } 124 | -------------------------------------------------------------------------------- /test/tenancy_helper.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package test 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | "sync" 10 | "testing" 11 | 12 | "github.com/hashicorp/consul/api" 13 | ) 14 | 15 | type Tenancy struct { 16 | Partition string 17 | Namespace string 18 | } 19 | 20 | type TenancyHelper struct { 21 | once sync.Once 22 | isConsulEnterprise bool 23 | consulClient *api.Client 24 | } 25 | 26 | func NewTenancyHelper(consulClient *api.Client) (*TenancyHelper, error) { 27 | t := &TenancyHelper{ 28 | consulClient: consulClient, 29 | } 30 | 31 | err := t.init() 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return t, nil 37 | } 38 | 39 | // TestTenancies returns a list of tenancies which represent 40 | // the namespace and partition combinations that can be used in unit tests 41 | func (t *TenancyHelper) TestTenancies() []*Tenancy { 42 | tenancies := []*Tenancy{ 43 | t.Tenancy("default.default"), 44 | } 45 | 46 | if t.isConsulEnterprise { 47 | tenancies = append(tenancies, t.Tenancy("default.bar"), t.Tenancy("foo.default"), t.Tenancy("foo.bar")) 48 | } 49 | 50 | return tenancies 51 | } 52 | 53 | func (t *TenancyHelper) IsConsulEnterprise() bool { 54 | return t.isConsulEnterprise 55 | } 56 | 57 | // Tenancy constructs a Tenancy from a concise string representation 58 | // suitable for use in unit tests. 59 | // 60 | // - "" : partition="" namespace="" 61 | // - "foo" : partition="foo" namespace="" 62 | // - "foo.bar" : partition="foo" namespace="bar" 63 | // - : partition="BAD" namespace="BAD" 64 | func (t *TenancyHelper) Tenancy(s string) *Tenancy { 65 | parts := strings.Split(s, ".") 66 | switch len(parts) { 67 | case 0: 68 | return &Tenancy{} 69 | case 1: 70 | return &Tenancy{ 71 | Partition: parts[0], 72 | } 73 | case 2: 74 | return &Tenancy{ 75 | Partition: parts[0], 76 | Namespace: parts[1], 77 | } 78 | default: 79 | return &Tenancy{Partition: "BAD", Namespace: "BAD"} 80 | } 81 | } 82 | 83 | func (t *TenancyHelper) init() error { 84 | var versionErr error 85 | 86 | t.once.Do(func() { 87 | v, err := t.consulClient.Agent().Version() 88 | if err != nil { 89 | versionErr = err 90 | return 91 | } 92 | 93 | if version, ok := v["HumanVersion"].(string); ok { 94 | // if type is string & the key is present 95 | // then check whether the version contains "ent" 96 | // example: "1.8.0+ent" is enterprise 97 | // otherwise it is CE 98 | t.isConsulEnterprise = strings.Contains(version, "ent") 99 | } 100 | }) 101 | 102 | return versionErr 103 | } 104 | 105 | func (t *TenancyHelper) AppendTenancyInfo(name string, tenancy *Tenancy) string { 106 | return fmt.Sprintf("%s_%s_Namespace_%s_Partition", name, tenancy.Namespace, tenancy.Partition) 107 | } 108 | 109 | func (t *TenancyHelper) RunWithTenancies(testFunc func(tenancy *Tenancy), test *testing.T, testName string) { 110 | for _, tenancy := range t.TestTenancies() { 111 | test.Run(t.AppendTenancyInfo(testName, tenancy), func(t *testing.T) { 112 | testFunc(tenancy) 113 | }) 114 | } 115 | } 116 | 117 | func (t *TenancyHelper) GenerateTenancyTests(generationFunc func(tenancy *Tenancy) []interface{}) []interface{} { 118 | cases := make([]interface{}, 0) 119 | for _, tenancy := range t.TestTenancies() { 120 | cases = append(cases, generationFunc(tenancy)...) 121 | } 122 | return cases 123 | } 124 | 125 | func (t *TenancyHelper) GenerateNonDefaultTenancyTests(generationFunc func(tenancy *Tenancy) []interface{}) []interface{} { 126 | cases := make([]interface{}, 0) 127 | for _, tenancy := range t.TestTenancies() { 128 | if tenancy.Partition != "default" { 129 | cases = append(cases, generationFunc(tenancy)...) 130 | } 131 | } 132 | return cases 133 | } 134 | 135 | func (t *TenancyHelper) GenerateDefaultTenancyTests(generationFunc func(tenancy *Tenancy) []interface{}) []interface{} { 136 | cases := make([]interface{}, 0) 137 | for _, tenancy := range t.TestTenancies() { 138 | if tenancy.Partition == "default" { 139 | cases = append(cases, generationFunc(tenancy)...) 140 | } 141 | } 142 | return cases 143 | } 144 | 145 | func (t *TenancyHelper) GetUniquePartitions() map[api.Partition]interface{} { 146 | partitions := make(map[api.Partition]interface{}) 147 | for _, tenancy := range t.TestTenancies() { 148 | partitions[api.Partition{ 149 | Name: tenancy.Partition, 150 | }] = nil 151 | } 152 | return partitions 153 | } 154 | -------------------------------------------------------------------------------- /test/testdata/nomad.json: -------------------------------------------------------------------------------- 1 | { 2 | "Datacenters": [ 3 | "dc1" 4 | ], 5 | "ID": "example", 6 | "Name": "example", 7 | "Namespace": "default", 8 | "Region": "global", 9 | "Type": "service", 10 | "TaskGroups": [ 11 | { 12 | "Count": 1, 13 | "Name": "cache", 14 | "Networks": [ 15 | { 16 | "DynamicPorts": [ 17 | { 18 | "Mode": "host", 19 | "Label": "redis", 20 | "To": 6379 21 | } 22 | ] 23 | } 24 | ], 25 | "Services": [ 26 | { 27 | "PortLabel": "redis", 28 | "Provider": "nomad", 29 | "Tags": ["tag1", "tag2"] 30 | } 31 | ], 32 | "Tasks": [ 33 | { 34 | "Config": { 35 | "command": "sleep", 36 | "args": ["10000"] 37 | }, 38 | "Driver": "raw_exec", 39 | "Name": "redis", 40 | "Resources": { 41 | "CPU": 100, 42 | "MemoryMB": 100 43 | } 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package version 5 | 6 | import "fmt" 7 | 8 | const ( 9 | Version = "0.40.0" 10 | VersionPrerelease = "" // "-dev", "-beta", "-rc1", etc. (include dash) 11 | ) 12 | 13 | var ( 14 | Name string = "consul-template" 15 | GitCommit string 16 | 17 | HumanVersion = fmt.Sprintf("%s v%s%s (%s)", 18 | Name, Version, VersionPrerelease, GitCommit) 19 | ) 20 | -------------------------------------------------------------------------------- /watch/watcher_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package watch 5 | 6 | import ( 7 | "fmt" 8 | "testing" 9 | 10 | dep "github.com/hashicorp/consul-template/dependency" 11 | ) 12 | 13 | func TestAdd_updatesMap(t *testing.T) { 14 | w := NewWatcher(&NewWatcherInput{ 15 | Clients: dep.NewClientSet(), 16 | Once: true, 17 | }) 18 | 19 | d := &TestDep{} 20 | if _, err := w.Add(d); err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | _, exists := w.depViewMap[d.String()] 25 | if !exists { 26 | t.Errorf("expected Add to append to map") 27 | } 28 | } 29 | 30 | func TestAdd_exists(t *testing.T) { 31 | w := NewWatcher(&NewWatcherInput{ 32 | Clients: dep.NewClientSet(), 33 | Once: true, 34 | }) 35 | 36 | d := &TestDep{} 37 | w.depViewMap[d.String()] = &View{} 38 | 39 | added, err := w.Add(d) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | if added != false { 45 | t.Errorf("expected Add to return false") 46 | } 47 | } 48 | 49 | func TestAdd_stopped(t *testing.T) { 50 | w := NewWatcher(&NewWatcherInput{ 51 | Clients: dep.NewClientSet(), 52 | Once: true, 53 | }) 54 | 55 | w.Stop() 56 | 57 | d := &TestDep{} 58 | added, err := w.Add(d) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | 63 | if added != false { 64 | t.Errorf("expected Add to return false when watcher is stopped") 65 | } 66 | 67 | if w.Watching(d) { 68 | t.Errorf("expected Add not to add dependency when watcher is stopped") 69 | } 70 | } 71 | 72 | func TestAdd_startsViewPoll(t *testing.T) { 73 | w := NewWatcher(&NewWatcherInput{ 74 | Clients: dep.NewClientSet(), 75 | Once: true, 76 | }) 77 | 78 | added, err := w.Add(&TestDep{}) 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | 83 | if added != true { 84 | t.Errorf("expected Add to return true") 85 | } 86 | 87 | select { 88 | case err := <-w.errCh: 89 | t.Fatal(err) 90 | case <-w.dataCh: 91 | // Got data, which means the poll was started 92 | } 93 | } 94 | 95 | func TestWatching_notExists(t *testing.T) { 96 | w := NewWatcher(&NewWatcherInput{ 97 | Clients: dep.NewClientSet(), 98 | Once: true, 99 | }) 100 | 101 | d := &TestDep{} 102 | if w.Watching(d) == true { 103 | t.Errorf("expected to not be watching") 104 | } 105 | } 106 | 107 | func TestWatching_exists(t *testing.T) { 108 | w := NewWatcher(&NewWatcherInput{ 109 | Clients: dep.NewClientSet(), 110 | Once: true, 111 | }) 112 | 113 | d := &TestDep{} 114 | if _, err := w.Add(d); err != nil { 115 | t.Fatal(err) 116 | } 117 | 118 | if w.Watching(d) == false { 119 | t.Errorf("expected to be watching") 120 | } 121 | } 122 | 123 | func TestRemove_exists(t *testing.T) { 124 | w := NewWatcher(&NewWatcherInput{ 125 | Clients: dep.NewClientSet(), 126 | Once: true, 127 | }) 128 | 129 | d := &TestDep{} 130 | if _, err := w.Add(d); err != nil { 131 | t.Fatal(err) 132 | } 133 | 134 | removed := w.Remove(d) 135 | if removed != true { 136 | t.Error("expected Remove to return true") 137 | } 138 | 139 | if _, ok := w.depViewMap[d.String()]; ok { 140 | t.Error("expected dependency to be removed") 141 | } 142 | } 143 | 144 | func TestRemove_doesNotExist(t *testing.T) { 145 | w := NewWatcher(&NewWatcherInput{ 146 | Clients: dep.NewClientSet(), 147 | Once: true, 148 | }) 149 | 150 | removed := w.Remove(&TestDep{}) 151 | if removed != false { 152 | t.Fatal("expected Remove to return false") 153 | } 154 | } 155 | 156 | func TestSize_empty(t *testing.T) { 157 | w := NewWatcher(&NewWatcherInput{ 158 | Clients: dep.NewClientSet(), 159 | Once: true, 160 | }) 161 | 162 | if w.Size() != 0 { 163 | t.Errorf("expected %d to be %d", w.Size(), 0) 164 | } 165 | } 166 | 167 | func TestSize_returnsNumViews(t *testing.T) { 168 | w := NewWatcher(&NewWatcherInput{ 169 | Clients: dep.NewClientSet(), 170 | Once: true, 171 | }) 172 | 173 | for i := 0; i < 10; i++ { 174 | d := &TestDep{name: fmt.Sprintf("%d", i)} 175 | if _, err := w.Add(d); err != nil { 176 | t.Fatal(err) 177 | } 178 | } 179 | 180 | if w.Size() != 10 { 181 | t.Errorf("expected %d to be %d", w.Size(), 10) 182 | } 183 | } 184 | --------------------------------------------------------------------------------