├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── clossing-issues.yml │ ├── comments.yml │ ├── main.yml │ ├── move-closed-issues.yml │ ├── pr-review-hack.yml │ ├── pr-reviews-requested.yml │ ├── pr-reviews.yml │ ├── reasign.yml │ ├── stale.yml │ └── triage.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── SECURITY.md ├── cmd.go ├── cmd_test.go ├── go.mod ├── go.sum ├── handlebars_renderer.go ├── handlebars_renderer_test.go ├── helpers_test.go ├── main.go ├── main_test.go ├── template_data.go ├── template_data_test.go └── vars.mk /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug 2 | description: Create a report to help us improve 3 | labels: ["tech-issues"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for reporting an issue. Before you open the bug report please review the README file present in the root of this repository. 9 | 10 | Please fill in as much of the following form as you're able to. 11 | - type: textarea 12 | attributes: 13 | label: What steps will reproduce the bug? 14 | description: Enter details about your bug. 15 | placeholder: | 16 | 1. In this environment... 17 | 2. With this config... 18 | 3. Run '...' 19 | 4. See error... 20 | validations: 21 | required: true 22 | - type: textarea 23 | attributes: 24 | label: What is the expected behavior? 25 | description: If possible please provide textual output instead of screenshots. 26 | - type: textarea 27 | attributes: 28 | label: What do you see instead? 29 | description: If possible please provide textual output instead of screenshots. 30 | validations: 31 | required: true 32 | - type: textarea 33 | attributes: 34 | label: Additional information 35 | description: Tell us anything else you think we should know. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F680 Feature request" 2 | description: Suggest an idea for this project 3 | labels: ["feature-request"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for suggesting an idea to improve this tool. 9 | Please fill in as much of the following form as you're able to. 10 | - type: textarea 11 | attributes: 12 | label: What is the problem this feature will solve? 13 | validations: 14 | required: true 15 | - type: textarea 16 | attributes: 17 | label: What is the feature you are proposing to solve the problem? 18 | description: Describe the requests. If you already have something in mind... PRs are welcome! 19 | validations: 20 | required: true 21 | - type: textarea 22 | attributes: 23 | label: What alternatives have you considered? 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | ### Description of the change 14 | 15 | 16 | 17 | ### Benefits 18 | 19 | 20 | 21 | ### Possible drawbacks 22 | 23 | 24 | 25 | ### Applicable issues 26 | 27 | 28 | - fixes # 29 | 30 | ### Additional information 31 | 32 | 33 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Copyright Broadcom, Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: APACHE-2.0 3 | 4 | version: 2 5 | # Check for updates to GitHub Actions every week 6 | updates: 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/clossing-issues.yml: -------------------------------------------------------------------------------- 1 | # Copyright Broadcom, Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: APACHE-2.0 3 | 4 | # NOTE: This workflow is maintained in the https://github.com/bitnami/support repository 5 | name: '[Support] Close Solved issues' 6 | on: 7 | schedule: 8 | # Hourly 9 | - cron: '0 * * * *' 10 | # Remove all permissions by default. Actions are performed by Bitnami Bot 11 | permissions: {} 12 | jobs: 13 | stale: 14 | runs-on: ubuntu-latest 15 | if: ${{ github.repository_owner == 'bitnami' }} 16 | steps: 17 | - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 18 | with: 19 | any-of-labels: 'solved' 20 | stale-issue-label: 'solved' 21 | days-before-stale: 0 22 | days-before-close: 0 23 | repo-token: ${{ secrets.BITNAMI_SUPPORT_BOARD_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/comments.yml: -------------------------------------------------------------------------------- 1 | # Copyright Broadcom, Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: APACHE-2.0 3 | 4 | # NOTE: This workflow is maintained in the https://github.com/bitnami/support repository 5 | name: '[Support] Comments based card movements' 6 | on: 7 | issue_comment: 8 | types: 9 | - created 10 | permissions: 11 | contents: read 12 | pull-requests: write 13 | issues: write 14 | # Avoid concurrency over the same issue 15 | concurrency: 16 | group: card-movement-${{ github.event.issue.number }} 17 | jobs: 18 | call-comments-workflow: 19 | if: ${{ github.repository_owner == 'bitnami' }} 20 | uses: bitnami/support/.github/workflows/comment-created.yml@main 21 | secrets: inherit -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the main branch 6 | push: 7 | branches: 8 | - main 9 | 10 | release: 11 | types: [published] 12 | 13 | pull_request: 14 | 15 | # Allows you to run this workflow manually from the Actions tab 16 | workflow_dispatch: 17 | # Remove all permissions by default 18 | permissions: {} 19 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 20 | jobs: 21 | # This workflow contains a single job called "build" 22 | build-and-test: 23 | runs-on: ubuntu-latest 24 | name: Build and Test 25 | # Steps represent a sequence of tasks that will be executed as part of the job 26 | steps: 27 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 28 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 29 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 30 | with: 31 | go-version: '^1.22.4' # The Go version to download (if necessary) and use. 32 | - name: Install Build Dependencies 33 | run: make get-build-deps 34 | - name: Download required modules 35 | run: make download 36 | - name: Vet 37 | run: make vet 38 | - name: Lint 39 | run: make lint 40 | - name: Cover 41 | run: make cover 42 | - name: Build 43 | run: | 44 | set -ex 45 | for dist in amd64 arm64; do 46 | target=out/render-template-linux-$dist 47 | rm -rf "$target" 48 | make build/$dist TOOL_PATH="$target" 49 | file $target 50 | tar -C "$(dirname "$target")" -czf "$target.tar.gz" "$(basename "$target")" 51 | done 52 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 53 | with: 54 | name: built-binaries 55 | path: | 56 | out/*.tar.gz 57 | release: 58 | needs: [ 'build-and-test' ] 59 | if: github.repository == 'bitnami/render-template' && startsWith(github.ref, 'refs/tags/') 60 | runs-on: ubuntu-latest 61 | steps: 62 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 63 | - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 64 | with: 65 | path: ./artifacts 66 | - name: Set tag name 67 | id: vars 68 | run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} 69 | - name: Release 70 | run: | 71 | set -e 72 | create_digest_file() { 73 | local digest_file=${1:?You must provide the digest file path} 74 | shift 75 | for file in "$@"; do 76 | ( 77 | cd "$(dirname "$file")" 78 | sha256sum "$(basename "$file")" 79 | ) >> "$digest_file" 80 | done 81 | } 82 | assets=( ./artifacts/built-binaries/*.gz ) 83 | 84 | tag_name="${{ steps.vars.outputs.tag }}" 85 | checksums_file="${tag_name}_checksums.txt" 86 | create_digest_file "$checksums_file" "${assets[@]}" 87 | assets+=( "$checksums_file" ) 88 | if gh release view "$tag_name" >/dev/null 2>/dev/null; then 89 | echo "Release $tag_name already exists. Updating" 90 | gh release upload "$tag_name" "${assets[@]}" 91 | else 92 | echo "Creating new release $tag_name" 93 | # Format checksums for the release text 94 | printf '```\n%s\n```' "$(<"$checksums_file")" > release.txt 95 | gh release create -t "$tag_name" "$tag_name" -F release.txt "${assets[@]}" 96 | fi 97 | env: 98 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 99 | notify: 100 | name: Send notification 101 | needs: 102 | - release 103 | if: ${{ always() && needs.release.result == 'failure' }} 104 | uses: bitnami/support/.github/workflows/gchat-notification.yml@main 105 | with: 106 | workflow: ${{ github.workflow }} 107 | job-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 108 | secrets: 109 | webhook-url: ${{ secrets.GCHAT_CONTENT_ALERTS_WEBHOOK_URL }} 110 | -------------------------------------------------------------------------------- /.github/workflows/move-closed-issues.yml: -------------------------------------------------------------------------------- 1 | # Copyright Broadcom, Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: APACHE-2.0 3 | 4 | # NOTE: This workflow is maintained in the https://github.com/bitnami/support repository 5 | name: '[Support] Move closed issues' 6 | on: 7 | issues: 8 | types: 9 | - closed 10 | pull_request_target: 11 | types: 12 | - closed 13 | permissions: 14 | issues: write 15 | pull-requests: write 16 | # Avoid concurrency over the same issue 17 | concurrency: 18 | group: card-movement-${{ github.event.issue != null && github.event.issue.number || github.event.number }} 19 | jobs: 20 | call-move-closed-workflow: 21 | if: ${{ github.repository_owner == 'bitnami' }} 22 | uses: bitnami/support/.github/workflows/item-closed.yml@main 23 | secrets: inherit -------------------------------------------------------------------------------- /.github/workflows/pr-review-hack.yml: -------------------------------------------------------------------------------- 1 | # Copyright Broadcom, Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: APACHE-2.0 3 | 4 | # This is a hack to run reusable workflows in the main repo context and not from the forked repository. 5 | # We this hack we can use secrets configured in the organization. 6 | # NOTE: This workflow is maintained in the https://github.com/bitnami/support repository 7 | name: '[Support] PR review comment trigger' 8 | on: 9 | workflow_run: 10 | workflows: 11 | - '\[Support\] PR review comment card movements' 12 | types: 13 | - completed 14 | permissions: {} 15 | jobs: 16 | pr-info: 17 | runs-on: ubuntu-latest 18 | permissions: 19 | pull-requests: read 20 | actions: read 21 | outputs: 22 | author: ${{ steps.get-info.outputs.author }} 23 | actor: ${{ steps.get-info.outputs.actor }} 24 | review_state: ${{ steps.get-info.outputs.review_state }} 25 | labels: ${{ steps.get-info.outputs.labels }} 26 | resource_url: ${{ steps.get-info.outputs.resource_url }} 27 | if: ${{ github.repository_owner == 'bitnami' && github.event.workflow_run.conclusion == 'success' }} 28 | steps: 29 | - id: get-info 30 | env: 31 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 32 | run: | 33 | actor="${{ github.event.workflow_run.actor.login }}" 34 | download_url="$(gh api "${{ github.event.workflow_run.artifacts_url }}" | jq -cr '.artifacts[] | select(.name == "pull_request_info.json") | .archive_download_url')" 35 | curl -sSL -o pull_request_info.zip -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" -H "Authorization: Bearer $GITHUB_TOKEN" $download_url 36 | unzip pull_request_info.zip 37 | pull_request_number="$(jq -cr '.issue.number' pull_request_info.json)" 38 | issue_review_state="$(jq -cr '.review.state' pull_request_info.json)" 39 | pull_request="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${pull_request_number}")" 40 | author="$(echo $pull_request | jq -cr '.user.login')" 41 | author_association="$(echo $pull_request | jq -cr '.author_association')" 42 | labels="$(echo $pull_request | jq -cr '[.labels[].name]')" 43 | resource_url="$(echo $pull_request | jq -cr '.html_url')" 44 | 45 | echo "::notice:: Managing PR #${pull_request_number}" 46 | echo "actor=${actor}" >> $GITHUB_OUTPUT 47 | echo "author=${author}" >> $GITHUB_OUTPUT 48 | echo "author_association=${author_association}" >> $GITHUB_OUTPUT 49 | echo "review_state=${issue_review_state}" >> $GITHUB_OUTPUT 50 | echo "labels=${labels}" >> $GITHUB_OUTPUT 51 | echo "resource_url=${resource_url}" >> $GITHUB_OUTPUT 52 | call-pr-review-comment: 53 | uses: bitnami/support/.github/workflows/pr-review-comment.yml@main 54 | needs: pr-info 55 | permissions: 56 | contents: read 57 | secrets: inherit 58 | with: 59 | author: ${{ needs.pr-info.outputs.author }} 60 | actor: ${{ needs.pr-info.outputs.actor }} 61 | labels: ${{ needs.pr-info.outputs.labels }} 62 | review_state: ${{ needs.pr-info.outputs.review_state }} 63 | resource_url: ${{ needs.pr-info.outputs.resource_url }} 64 | -------------------------------------------------------------------------------- /.github/workflows/pr-reviews-requested.yml: -------------------------------------------------------------------------------- 1 | # Copyright Broadcom, Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: APACHE-2.0 3 | 4 | # NOTE: This workflow is maintained in the https://github.com/bitnami/support repository 5 | name: '[Support] Review based card movements' 6 | on: 7 | pull_request_target: 8 | types: 9 | - review_requested 10 | - synchronize 11 | permissions: 12 | contents: read 13 | # Avoid concurrency over the same issue 14 | concurrency: 15 | group: card-movement-${{ github.event.number }} 16 | jobs: 17 | call-pr-review-workflow: 18 | if: ${{ github.repository_owner == 'bitnami' }} 19 | uses: bitnami/support/.github/workflows/pr-review-requested-sync.yml@main 20 | secrets: inherit -------------------------------------------------------------------------------- /.github/workflows/pr-reviews.yml: -------------------------------------------------------------------------------- 1 | # Copyright Broadcom, Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: APACHE-2.0 3 | 4 | # NOTE: This workflow is maintained in the https://github.com/bitnami/support repository 5 | name: '[Support] PR review comment card movements' 6 | on: 7 | pull_request_review_comment: 8 | types: 9 | - created 10 | pull_request_review: 11 | types: 12 | - submitted 13 | - dismissed 14 | permissions: {} 15 | # Avoid concurrency over the same issue 16 | concurrency: 17 | group: card-movement-${{ github.event.pull_request.number }} 18 | jobs: 19 | just-notice: 20 | # This is a dummy workflow that triggers a workflow_run 21 | runs-on: ubuntu-latest 22 | if: ${{ github.repository_owner == 'bitnami' }} 23 | steps: 24 | - run: | 25 | echo "::notice:: Comment on PR #${{ github.event.pull_request.number }}" 26 | jq -n --arg issue '${{ github.event.pull_request.number }}' --arg state '${{ github.event.review != null && github.event.review.state || '' }}' '{"issue": {"number": $issue }, "review": { "state": $state }}' > pull_request_info.json 27 | - name: Upload the PR info 28 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 29 | with: 30 | name: pull_request_info.json 31 | path: ./pull_request_info.json -------------------------------------------------------------------------------- /.github/workflows/reasign.yml: -------------------------------------------------------------------------------- 1 | # Copyright Broadcom, Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: APACHE-2.0 3 | 4 | # NOTE: This workflow is maintained in the https://github.com/bitnami/support repository 5 | name: '[Support] Review based card movements' 6 | on: 7 | pull_request_target: 8 | types: 9 | - labeled 10 | issues: 11 | types: 12 | - labeled 13 | permissions: 14 | contents: read 15 | pull-requests: write 16 | issues: write 17 | # Avoid concurrency over the same issue 18 | concurrency: 19 | group: card-movement-${{ github.event.issue != null && github.event.issue.number || github.event.number }} 20 | jobs: 21 | call-reasign-workflow: 22 | if: ${{ github.repository_owner == 'bitnami' }} 23 | uses: bitnami/support/.github/workflows/item-labeled.yml@main 24 | secrets: inherit -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # Copyright Broadcom, Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: APACHE-2.0 3 | 4 | # NOTE: This workflow is maintained in the https://github.com/bitnami/support repository 5 | name: '[Support] Close stale issues and PRs' 6 | on: 7 | workflow_dispatch: 8 | schedule: 9 | - cron: '0 1 * * *' 10 | # Remove all permissions by default 11 | permissions: {} 12 | # This job won't trigger any additional event. All actions are performed with GITHUB_TOKEN 13 | jobs: 14 | stale: 15 | runs-on: ubuntu-latest 16 | if: ${{ github.repository_owner == 'bitnami' }} 17 | permissions: 18 | issues: write 19 | pull-requests: write 20 | steps: 21 | # This step will add the stale comment and label for the first 15 days without activity. It won't close any task 22 | - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 23 | with: 24 | repo-token: ${{ secrets.GITHUB_TOKEN }} 25 | stale-issue-message: 'This Issue has been automatically marked as "stale" because it has not had recent activity (for 15 days). It will be closed if no further activity occurs. Thanks for the feedback.' 26 | stale-pr-message: 'This Pull Request has been automatically marked as "stale" because it has not had recent activity (for 15 days). It will be closed if no further activity occurs. Thank you for your contribution.' 27 | days-before-stale: 15 28 | days-before-close: -1 29 | exempt-issue-labels: 'on-hold' 30 | exempt-pr-labels: 'on-hold' 31 | operations-per-run: 500 32 | # This step will add the 'solved' label and the last comment before closing the issue or PR. Note that it won't close any issue or PR, they will be closed by the clossing-issues workflow 33 | - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 34 | with: 35 | repo-token: ${{ secrets.GITHUB_TOKEN }} 36 | stale-issue-message: 'Due to the lack of activity in the last 5 days since it was marked as "stale", we proceed to close this Issue. Do not hesitate to reopen it later if necessary.' 37 | stale-pr-message: 'Due to the lack of activity in the last 5 days since it was marked as "stale", we proceed to close this Pull Request. Do not hesitate to reopen it later if necessary.' 38 | any-of-labels: 'stale' 39 | stale-issue-label: 'solved' 40 | stale-pr-label: 'solved' 41 | days-before-stale: 5 42 | days-before-close: -1 43 | exempt-issue-labels: 'on-hold' 44 | exempt-pr-labels: 'on-hold' 45 | operations-per-run: 200 -------------------------------------------------------------------------------- /.github/workflows/triage.yml: -------------------------------------------------------------------------------- 1 | # Copyright Broadcom, Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: APACHE-2.0 3 | 4 | # This workflow is built to manage the triage support by using GH issues. 5 | # NOTE: This workflow is maintained in the https://github.com/bitnami/support repository 6 | name: '[Support] Organize triage' 7 | on: 8 | issues: 9 | types: 10 | - reopened 11 | - opened 12 | pull_request_target: 13 | types: 14 | - reopened 15 | - opened 16 | permissions: 17 | contents: read 18 | pull-requests: write 19 | issues: write 20 | # Avoid concurrency over the same issue 21 | concurrency: 22 | group: card-movement-${{ github.event.issue != null && github.event.issue.number || github.event.number }} 23 | jobs: 24 | call-triage-workflow: 25 | if: ${{ github.repository_owner == 'bitnami' }} 26 | uses: bitnami/support/.github/workflows/item-opened.yml@main 27 | secrets: inherit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | out/ 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities. 4 | 5 | Communication through any of Bitnami's channels (GitHub, mailing lists, Twitter, and so on) must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 6 | 7 | We promise to extend courtesy and respect to everyone involved in this project, regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to this project to do the same. 8 | 9 | If any member of the community violates this code of conduct, the maintainers of this project may take action, including removing issues, comments, and PRs or blocking accounts, as deemed appropriate. 10 | 11 | If you are subjected to or witness unacceptable behavior, or have any other concerns, please communicate with us. 12 | 13 | If you have suggestions to improve this Code of Conduct, please submit an issue or PR. 14 | 15 | **Attribution** 16 | 17 | This Code of Conduct is adapted from the Angular project available at this page: https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Contributions are welcome via GitHub Pull Requests. This document outlines the process to help get your contribution accepted. 4 | 5 | Any type of contribution is welcome: new features, bug fixes, documentation improvements, etc. 6 | 7 | ## How to Contribute 8 | 9 | 1. Fork this repository, develop, and test your changes. 10 | 2. Submit a pull request. 11 | 12 | ### Requirements 13 | 14 | When submitting a PR make sure that: 15 | - It must pass CI jobs for linting and test the changes (if any). 16 | - The title of the PR is clear enough. 17 | - If necessary, add information to the repository's `README.md`. 18 | 19 | #### Sign Your Work 20 | 21 | The sign-off is a simple line at the end of the explanation for a commit. All commits needs to be signed. Your signature certifies that you wrote the patch or otherwise have the right to contribute the material. The rules are pretty simple, you only need to certify the guidelines from [developercertificate.org](https://developercertificate.org/). 22 | 23 | Then you just add a line to every git commit message: 24 | 25 | Signed-off-by: Joe Smith 26 | 27 | Use your real name (sorry, no pseudonyms or anonymous contributions.) 28 | 29 | If you set your `user.name` and `user.email` git configs, you can sign your commit automatically with `git commit -s`. 30 | 31 | Note: If your git config information is set properly then viewing the `git log` information for your commit will look something like this: 32 | 33 | ``` 34 | Author: Joe Smith 35 | Date: Thu Feb 2 11:41:15 2018 -0800 36 | 37 | Update README 38 | 39 | Signed-off-by: Joe Smith 40 | ``` 41 | 42 | Notice the `Author` and `Signed-off-by` lines match. If they don't your PR will be rejected by the automated DCO check. 43 | 44 | ### PR Approval and Release Process 45 | 46 | 1. Changes are manually reviewed by Bitnami team members usually within a business day. 47 | 2. Once the changes are accepted, the PR is tested (if needed) into the Bitnami CI pipeline. 48 | 3. The PR is merged by the reviewer(s) in the GitHub `main` branch. 49 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # render-template in a container 2 | # 3 | # docker run --rm -i -e WHO=bitnami bitnami/render-template <<<"hello {{WHO}}" 4 | # 5 | FROM golang:1.22-bullseye as build 6 | 7 | RUN apt-get update && apt-get install -y --no-install-recommends \ 8 | git make upx \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | WORKDIR /go/src/app 12 | COPY . . 13 | 14 | RUN rm -rf out 15 | 16 | RUN make build 17 | 18 | RUN upx --ultra-brute out/render-template 19 | 20 | FROM bitnami/minideb:bullseye 21 | 22 | COPY --from=build /go/src/app/out/render-template /usr/local/bin/ 23 | 24 | ENTRYPOINT ["/usr/local/bin/render-template"] 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all build clean download get-build-deps vet lint test cover 2 | 3 | TOOL_NAME := render-template 4 | 5 | # Load relative to the common.mk file 6 | include $(dir $(lastword $(MAKEFILE_LIST)))/vars.mk 7 | 8 | include ./vars.mk 9 | 10 | all: 11 | @$(MAKE) get-build-deps 12 | @$(MAKE) download 13 | @$(MAKE) vet 14 | @$(MAKE) lint 15 | @$(MAKE) cover 16 | @$(MAKE) build 17 | 18 | build/%: 19 | @echo "Building GOARCH=$(*F)" 20 | @GOARCH=$(*F) go build -ldflags=$(LDFLAGS) -o $(TOOL_PATH) 21 | @echo "*** Binary created under $(TOOL_PATH) ***" 22 | 23 | build: build/amd64 24 | 25 | clean: 26 | @rm -rf $(BUILD_DIR) 27 | 28 | download: 29 | $(GO_MOD) download 30 | 31 | get-build-deps: 32 | @echo "+ Downloading build dependencies" 33 | @go install golang.org/x/tools/cmd/goimports@latest 34 | @go install honnef.co/go/tools/cmd/staticcheck@latest 35 | 36 | vet: 37 | @echo "+ Vet" 38 | @go vet ./... 39 | 40 | lint: 41 | @echo "+ Linting package" 42 | @staticcheck ./... 43 | $(call fmtcheck, .) 44 | 45 | test: 46 | @echo "+ Testing package" 47 | $(GO_TEST) . 48 | 49 | cover: test 50 | @echo "+ Tests Coverage" 51 | @mkdir -p $(BUILD_DIR) 52 | @touch $(BUILD_DIR)/cover.out 53 | @go test -coverprofile=$(BUILD_DIR)/cover.out 54 | @go tool cover -html=$(BUILD_DIR)/cover.out -o=$(BUILD_DIR)/coverage.html 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Report Card](https://goreportcard.com/badge/github.com/bitnami/render-template)](https://goreportcard.com/report/github.com/bitnami/render-template) 2 | [![CI](https://github.com/bitnami/render-template/actions/workflows/main.yml/badge.svg)](https://github.com/bitnami/render-template/actions/workflows/main.yml) 3 | 4 | # render-template 5 | 6 | This tool allows rendering Handlebars 3.0 templates, using as context data the current environment variables or a provided data file. 7 | 8 | # Basic usage 9 | 10 | ```console 11 | $ render-template --help 12 | Usage: 13 | render-template [OPTIONS] [template-file] 14 | 15 | Application Options: 16 | -f, --data-file=DATA_FILE Properties file containing the replacements for the template 17 | 18 | Help Options: 19 | -h, --help Show this help message 20 | 21 | Arguments: 22 | template-file: File containing the template to render. Its contents can be also passed through stdin 23 | ``` 24 | 25 | The tool supports rendering templates from a file or stdin (for convenience). 26 | 27 | The source data is taken from the environment variables or a data file, with properties-file format (key=value, a line for each pair). When a variable is defined both as an environment variable and in the data file, the latter will take precedence. 28 | 29 | # Examples 30 | 31 | ## Render data from a template file with environment variables 32 | 33 | ```console 34 | # Create the template 35 | $ echo 'hello {{who}}' > template.tpl 36 | # Render it without 'who' variable 37 | $ render-template template.tpl 38 | hello 39 | # Render it with 'who' variable defined 40 | $ who=bitnami render-template template.tpl 41 | hello bitnami 42 | ``` 43 | 44 | ## Render data from stdin with environment variables 45 | 46 | ```console 47 | $ log_file=/tmp/stout.log port=8080 pid_file=/tmp/my.pid render-template <<"EOF" 48 | # My service log file 49 | log_file "{{log_file}}" 50 | 51 | # HTTP port 52 | port {{port}} 53 | 54 | # My service pid file 55 | pid_file "{{pid_file}}" 56 | EOF 57 | ``` 58 | 59 | Outputs: 60 | 61 | ``` 62 | # My servide log file 63 | log_file "/tmp/stout.log" 64 | 65 | # HTTP port 66 | port 8080 67 | 68 | # My service pid file 69 | pid_file "/tmp/my.pid" 70 | ``` 71 | 72 | ## Render data from stdin with data file 73 | 74 | ```console 75 | # write data file 76 | $ cat > data.properties <<"EOF" 77 | log_file=/tmp/stout.log 78 | port=8080 79 | pid_file=/tmp/my.pid 80 | EOF 81 | 82 | $ render-template --data-file ./data.properties <<"EOF" 83 | # My servide log file 84 | log_file "{{log_file}}" 85 | 86 | # HTTP port 87 | port {{port}} 88 | 89 | # My service pid file 90 | pid_file "{{pid_file}}" 91 | EOF 92 | ``` 93 | 94 | Outputs: 95 | 96 | ``` 97 | # My service log file 98 | log_file "/tmp/stout.log" 99 | 100 | # HTTP port 101 | port 8080 102 | 103 | # My service pid file 104 | pid_file "/tmp/my.pid" 105 | ``` 106 | 107 | ## Overriding environment variables in a data file 108 | 109 | ```console 110 | # Let's define some environment variables 111 | $ export name=foo 112 | $ export company=bar 113 | $ export year=3000 114 | 115 | # And write a template 116 | $ cat > data.tpl <<"EOF" 117 | {{name}} works at {{company}} 118 | since {{year}} 119 | EOF 120 | 121 | # Rendering from the environment would yield 122 | $ render-template data.tpl 123 | foo works at bar 124 | since 3000 125 | 126 | # But we can override it from a data file, either partially, to get a mix: 127 | 128 | $ echo "name=mike" > data.properties 129 | $ render-template --data-file data.properties data.tpl 130 | mike works at bar 131 | since 3000 132 | 133 | # Or completely: 134 | 135 | $ cat > data.properties <<"EOF" 136 | name=mike 137 | company=Bitnami 138 | year=2010 139 | EOF 140 | 141 | $ render-template --data-file data.properties data.tpl 142 | mike works at Bitnami 143 | since 2010 144 | ``` 145 | 146 | ## Using helpers 147 | 148 | The tool supports all the standard handlebar helpers: https://handlebarsjs.com/builtin_helpers.html 149 | 150 | ```console 151 | $ render-template <<"EOF" 152 | {{#if author}} 153 | {{firstName}} {{lastName}} 154 | {{else}} 155 | Unknown Author 156 | {{/if}} 157 | EOF 158 | 159 | # Which outputs 160 | Unknown Author 161 | 162 | $ author=me firsName=foo lastName=bar render-template <<"EOF" 163 | {{#if author}} 164 | {{firstName}} {{lastName}} 165 | {{else}} 166 | Unknown Author 167 | {{/if}} 168 | EOF 169 | 170 | # Outputs: 171 | foo bar 172 | ``` 173 | 174 | In addition, it includes a few custom helpers: 175 | 176 | ### json_escape 177 | 178 | The json_escape helper converts the provided value into a valid JSON string 179 | ```console 180 | $ export VALUE='this is "a string", with quoting 181 | 182 | and some line breaks' 183 | ``` 184 | Without the helper: 185 | 186 | ```console 187 | $ render-template <<<'VALUE={{VALUE}}' 188 | VALUE=this is "a string", with quoting 189 | 190 | and some line breaks 191 | ``` 192 | 193 | Using the helper: 194 | ```console 195 | $ render-template <<<'VALUE={{json_escape VALUE}}' 196 | VALUE="this is \"a string\", with quoting\n\nand some line breaks" 197 | ``` 198 | 199 | ### quote 200 | 201 | The quote helper Quotes a string 202 | 203 | Without the helper: 204 | ```console 205 | $ ARG1="some arg" ARG2="some other \"arg\"" render-template <<"EOF" 206 | ARG1={{ARG1}} ARG2={{ARG2}} 207 | EOF 208 | ARG1=some arg ARG2=some other "arg" 209 | ``` 210 | 211 | With the helper 212 | 213 | ```console 214 | ARG1="some arg" ARG2="some other \"arg\"" render-template <<"EOF" 215 | ARG1={{quote ARG1}} ARG2={{quote ARG2}} 216 | EOF 217 | ARG1="some arg" ARG2="some other \"arg\"" 218 | ``` 219 | 220 | ### or 221 | 222 | This helper allows using the "or" logical operation over two values (a value will be true if not empty) 223 | 224 | To render a block when either "firstName" or "lastName" values are not empty: 225 | 226 | ```console 227 | $ cat > data.tpl <<"EOF" 228 | {{#if (or firstName lastName)}} 229 | {{firstName}} {{lastName}} 230 | {{else}} 231 | Unknown Author 232 | {{/if}} 233 | EOF 234 | 235 | $ render-template data.tpl 236 | Unknown Author 237 | 238 | $ firstName=foo render-template data.tpl 239 | foo 240 | 241 | $ lastName=bar render-template data.tpl 242 | bar 243 | 244 | $ firstName=foo lastName=bar render-template data.tpl 245 | foo bar 246 | ``` 247 | 248 | This helper can also be used to provide defaults for your template variables: 249 | 250 | ```console 251 | # No value provided, so we fallback to the second "or" argument 252 | $ render-template <<<'VALUE={{or ENV_VALUE "default value"}}' 253 | VALUE=default value 254 | 255 | # ENV_VALUE is defined, so we take it 256 | $ ENV_VALUE="customized value" render-template <<<'VALUE={{or ENV_VALUE "default value"}}' 257 | VALUE=customized value 258 | ``` 259 | 260 | ## License 261 | 262 | Copyright © 2025 Broadcom. The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. 263 | 264 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 265 | 266 | You may obtain a copy of the License at 267 | 268 | http://www.apache.org/licenses/LICENSE-2.0 269 | 270 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 271 | See the License for the specific language governing permissions and limitations under the License. 272 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Release Process 2 | 3 | The community has adopted this security disclosure and response policy to ensure we responsibly handle critical issues. 4 | 5 | 6 | ## Supported Versions 7 | 8 | For a list of support versions that this project will potentially create security fixes for, please refer to the Releases page on this project's GitHub and/or project related documentation on release cadence and support. 9 | 10 | 11 | ## Reporting a Vulnerability - Private Disclosure Process 12 | 13 | Security is of the highest importance and all security vulnerabilities or suspected security vulnerabilities should be reported to this project privately, to minimize attacks against current users before they are fixed. Vulnerabilities will be investigated and patched on the next patch (or minor) release as soon as possible. This information could be kept entirely internal to the project. 14 | 15 | If you know of a publicly disclosed security vulnerability for this project, please **IMMEDIATELY** contact the maintainers of this project privately. The use of encrypted email is encouraged. 16 | 17 | 18 | **IMPORTANT: Do not file public issues on GitHub for security vulnerabilities** 19 | 20 | To report a vulnerability or a security-related issue, please contact the maintainers with enough details through one of the following channels: 21 | * Directly via their individual email addresses 22 | * Open a [GitHub Security Advisory](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability). This allows for anyone to report security vulnerabilities directly and privately to the maintainers via GitHub. Note that this option may not be present for every repository. 23 | 24 | The report will be fielded by the maintainers who have committer and release permissions. Feedback will be sent within 3 business days, including a detailed plan to investigate the issue and any potential workarounds to perform in the meantime. 25 | 26 | Do not report non-security-impacting bugs through this channel. Use GitHub issues for all non-security-impacting bugs. 27 | 28 | 29 | ## Proposed Report Content 30 | 31 | Provide a descriptive title and in the description of the report include the following information: 32 | 33 | * Basic identity information, such as your name and your affiliation or company. 34 | * Detailed steps to reproduce the vulnerability (POC scripts, screenshots, and logs are all helpful to us). 35 | * Description of the effects of the vulnerability on this project and the related hardware and software configurations, so that the maintainers can reproduce it. 36 | * How the vulnerability affects this project's usage and an estimation of the attack surface, if there is one. 37 | * List other projects or dependencies that were used in conjunction with this project to produce the vulnerability. 38 | 39 | 40 | ## When to report a vulnerability 41 | 42 | * When you think this project has a potential security vulnerability. 43 | * When you suspect a potential vulnerability but you are unsure that it impacts this project. 44 | * When you know of or suspect a potential vulnerability on another project that is used by this project. 45 | 46 | 47 | ## Patch, Release, and Disclosure 48 | 49 | The maintainers will respond to vulnerability reports as follows: 50 | 51 | 1. The maintainers will investigate the vulnerability and determine its effects and criticality. 52 | 2. If the issue is not deemed to be a vulnerability, the maintainers will follow up with a detailed reason for rejection. 53 | 3. The maintainers will initiate a conversation with the reporter within 3 business days. 54 | 4. If a vulnerability is acknowledged and the timeline for a fix is determined, the maintainers will work on a plan to communicate with the appropriate community, including identifying mitigating steps that affected users can take to protect themselves until the fix is rolled out. 55 | 5. The maintainers will also create a [Security Advisory](https://docs.github.com/en/code-security/repository-security-advisories/publishing-a-repository-security-advisory) using the [CVSS Calculator](https://www.first.org/cvss/calculator/3.0), if it is not created yet. The maintainers make the final call on the calculated CVSS; it is better to move quickly than making the CVSS perfect. Issues may also be reported to [Mitre](https://cve.mitre.org/) using this [scoring calculator](https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator). The draft advisory will initially be set to private. 56 | 6. The maintainers will work on fixing the vulnerability and perform internal testing before preparing to roll out the fix. 57 | 7. Once the fix is confirmed, the maintainers will patch the vulnerability in the next patch or minor release, and backport a patch release into all earlier supported releases. 58 | 59 | 60 | ## Public Disclosure Process 61 | 62 | The maintainers publish the public advisory to this project's community via GitHub. In most cases, additional communication via Slack, Twitter, mailing lists, blog, and other channels will assist in educating the project's users and rolling out the patched release to affected users. 63 | 64 | The maintainers will also publish any mitigating steps users can take until the fix can be applied to their instances. This project's distributors will handle creating and publishing their own security advisories. 65 | 66 | 67 | ## Confidentiality, integrity and availability 68 | 69 | We consider vulnerabilities leading to the compromise of data confidentiality, elevation of privilege, or integrity to be our highest priority concerns. Availability, in particular in areas relating to DoS and resource exhaustion, is also a serious security concern. The maintainer team takes all vulnerabilities, potential vulnerabilities, and suspected vulnerabilities seriously and will investigate them in an urgent and expeditious manner. 70 | 71 | Note that we do not currently consider the default settings for this project to be secure-by-default. It is necessary for operators to explicitly configure settings, role based access control, and other resource related features in this project to provide a hardened environment. We will not act on any security disclosure that relates to a lack of safe defaults. Over time, we will work towards improved safe-by-default configuration, taking into account backwards compatibility. 72 | -------------------------------------------------------------------------------- /cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | ) 8 | 9 | // RenderTemplateCmd allows rendering templates 10 | type RenderTemplateCmd struct { 11 | DataFile string `short:"f" long:"data-file" description:"Properties file containing the replacements for the template" value-name:"DATA_FILE"` 12 | Args struct { 13 | TemplateFile string `positional-arg-name:"template-file" description:"File containing the template to render. Its contents can be also passed through stdin"` 14 | } `positional-args:"yes"` 15 | OutWriter io.Writer 16 | } 17 | 18 | // NewRenderTemplateCmd returns a new RenderTemplateCmd 19 | func NewRenderTemplateCmd() *RenderTemplateCmd { 20 | return &RenderTemplateCmd{OutWriter: os.Stdout} 21 | } 22 | 23 | func (c *RenderTemplateCmd) getTemplateData() (*templateData, error) { 24 | d := newTemplateData() 25 | 26 | if c.DataFile != "" { 27 | if err := d.LoadBatchDataFile(c.DataFile); err != nil { 28 | return nil, fmt.Errorf("cannot read template data file %q: %v", c.DataFile, err) 29 | } 30 | } 31 | return d, nil 32 | } 33 | 34 | // Execute performs rendering 35 | func (c *RenderTemplateCmd) Execute(args []string) (err error) { 36 | var in io.Reader 37 | if c.Args.TemplateFile != "" { 38 | fh, err := os.Open(c.Args.TemplateFile) 39 | if err != nil { 40 | return fmt.Errorf("cannot open template file: %v", err) 41 | } 42 | defer fh.Close() 43 | in = fh 44 | } else if hasPipedStdin() { 45 | in = os.Stdin 46 | } else { 47 | return fmt.Errorf("you must provide a template file as an argument or through stdin") 48 | } 49 | 50 | data, err := c.getTemplateData() 51 | if err != nil { 52 | return err 53 | } 54 | renderer := newHandlerbarsRenderer() 55 | str, err := renderer.RenderTemplate(in, data) 56 | if err != nil { 57 | return err 58 | } 59 | _, err = fmt.Fprint(c.OutWriter, str) 60 | return err 61 | } 62 | 63 | func hasPipedStdin() bool { 64 | stat, _ := os.Stdin.Stat() 65 | return (stat.Mode() & os.ModeCharDevice) == 0 66 | } 67 | -------------------------------------------------------------------------------- /cmd_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | tu "github.com/bitnami/gonit/testutils" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | type tplTest struct { 13 | tplText string 14 | env map[string]string 15 | data map[string]string 16 | expectedResult string 17 | } 18 | 19 | func TestRenderTemplateCmd_Execute(t *testing.T) { 20 | tests := []struct { 21 | name string 22 | test tplTest 23 | wantErr bool 24 | expectedErr interface{} 25 | }{ 26 | { 27 | name: "Simple template", 28 | test: tplTest{ 29 | tplText: `hello {{to}}{{#if EXTRA}}and EXTRA{{/if}}`, 30 | env: map[string]string{"to": "world"}, 31 | data: map[string]string{}, 32 | expectedResult: `hello world`, 33 | }, 34 | }, 35 | { 36 | name: "Simple template with extra data", 37 | test: tplTest{ 38 | tplText: `hello {{to}}{{#if EXTRA}} and {{EXTRA}}{{/if}}`, 39 | env: map[string]string{"to": "world"}, 40 | data: map[string]string{"EXTRA": "bitnami"}, 41 | expectedResult: `hello world and bitnami`, 42 | }, 43 | }, 44 | { 45 | name: "Multiline Template", 46 | test: tplTest{ 47 | tplText: "This is a {{how_much}} complex\nmultiline {{what}}\n{{empty_value}}\nCreated to test {{program}}\n", 48 | env: map[string]string{"how_much": "very", "what": "example", "program": "render-template"}, 49 | expectedResult: "This is a very complex\nmultiline example\n\nCreated to test render-template\n", 50 | }, 51 | }, 52 | { 53 | name: "Quote helper", 54 | test: tplTest{ 55 | tplText: `Quoted {{quote string}}`, 56 | env: map[string]string{"string": `this " has some "quoted" words ' and chars`}, 57 | expectedResult: `Quoted "this \" has some \"quoted\" words ' and chars"`, 58 | }, 59 | }, 60 | { 61 | name: "or helper", 62 | test: tplTest{ 63 | tplText: `{{#if (or A B)}}A-OR-B{{/if}};{{#if (or B A)}}B-OR-A{{/if}};{{#if (or B C)}}EMPTY{{/if}};{{#if (or D E)}}UNDEFINED{{/if}};{{#if (or D A)}}D-OR-A{{/if}}`, 64 | env: map[string]string{"A": "yes", "B": "", "C": ""}, 65 | expectedResult: `A-OR-B;B-OR-A;;;D-OR-A`, 66 | }, 67 | }, 68 | { 69 | name: "json_escape helper", 70 | test: tplTest{ 71 | tplText: `val1={{json_escape VAL1}};val2={{json_escape VAL2}}`, 72 | env: map[string]string{"VAL1": `t''"`, "VAL2": `hello world`}, 73 | expectedResult: `val1="t''\"";val2="hello world"`, 74 | }, 75 | }, 76 | { 77 | name: "Malformed template", 78 | test: tplTest{ 79 | tplText: `{{if `, 80 | }, 81 | wantErr: true, 82 | expectedErr: "cannot parse template: Parse error on line 1", 83 | }, 84 | } 85 | for _, tt := range tests { 86 | sb := tu.NewSandbox() 87 | defer sb.Cleanup() 88 | file, err := sb.Write("my.conf.tpl", tt.test.tplText) 89 | require.NoError(t, err) 90 | cmd := NewRenderTemplateCmd() 91 | cmd.Args.TemplateFile = file 92 | 93 | if len(tt.test.data) > 0 { 94 | dataFile := sb.TempFile("data.txt") 95 | writeDataFile(dataFile, tt.test.data) 96 | cmd.DataFile = dataFile 97 | } 98 | 99 | t.Run(tt.name, func(t *testing.T) { 100 | var err error 101 | cb := setenv(tt.test.env) 102 | defer cb() 103 | b := &bytes.Buffer{} 104 | cmd.OutWriter = b 105 | 106 | err = cmd.Execute([]string{}) 107 | stdout := b.String() 108 | 109 | if (err != nil) != tt.wantErr { 110 | t.Errorf("RenderTemplateCmd.Execute() error = %v, wantErr %v", err, tt.wantErr) 111 | } 112 | if err != nil { 113 | if tt.expectedErr != nil { 114 | assert.Regexp(t, tt.expectedErr, err, "Expected error %v to match %v", err, tt.expectedErr) 115 | } 116 | } else { 117 | assert.Equal(t, tt.test.expectedResult, stdout) 118 | } 119 | }) 120 | } 121 | } 122 | 123 | func TestRenderTemplateCmd_Execute_Errors(t *testing.T) { 124 | sb := tu.NewSandbox() 125 | defer sb.Cleanup() 126 | existentDataFile := sb.Touch("exitent-data.txt") 127 | nonExistentDataFile := sb.Normalize("non-exitent-data.txt") 128 | 129 | existentTemplate := sb.Touch("exitent-template.tpl") 130 | nonExistentTemplate := sb.Normalize("non-exitent-template.tpl") 131 | 132 | tests := []struct { 133 | name string 134 | expectedErr interface{} 135 | tplFile string 136 | dataFile string 137 | // TODO: provide stdin 138 | stdin string 139 | }{ 140 | { 141 | name: "Template file does not exists", 142 | tplFile: nonExistentTemplate, 143 | dataFile: existentDataFile, 144 | expectedErr: "cannot open template file", 145 | }, 146 | { 147 | name: "Data file does not exists", 148 | dataFile: nonExistentDataFile, 149 | tplFile: existentTemplate, 150 | expectedErr: "cannot read template data file", 151 | }, 152 | } 153 | for _, tt := range tests { 154 | cmd := NewRenderTemplateCmd() 155 | cmd.Args.TemplateFile = tt.tplFile 156 | cmd.DataFile = tt.dataFile 157 | t.Run(tt.name, func(t *testing.T) { 158 | 159 | err := cmd.Execute([]string{}) 160 | 161 | if err == nil { 162 | t.Errorf("RenderTemplateCmd.Execute() was expected to fail") 163 | } else { 164 | if tt.expectedErr != nil { 165 | assert.Regexp(t, tt.expectedErr, err, "Expected error %v to match %v", err, tt.expectedErr) 166 | } 167 | } 168 | }) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bitnami/render-template 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/aymerick/raymond v2.0.2+incompatible 7 | github.com/bitnami/gonit v0.1.0 8 | github.com/jessevdk/go-flags v1.6.1 9 | github.com/juamedgod/cliassert v0.0.0-20180320011200-425256f2bb0b 10 | github.com/mmikulicic/multierror v0.0.0-20170428094957-c1ad6b5ecd26 11 | github.com/stretchr/testify v1.2.1 12 | ) 13 | 14 | require ( 15 | github.com/cesanta/errors v0.0.0-20160612174407-5adec772d663 // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/pmezard/go-difflib v1.0.0 // indirect 18 | golang.org/x/sys v0.21.0 // indirect 19 | gopkg.in/yaml.v2 v2.4.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aymerick/raymond v2.0.2+incompatible h1:VEp3GpgdAnv9B2GFyTvqgcKvY+mfKMjPOA3SbKLtnU0= 2 | github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= 3 | github.com/bitnami/gonit v0.1.0 h1:f0+5lCjX/1ar4iPrvlcE91TDE2nElxibNwvWXXy1sEU= 4 | github.com/bitnami/gonit v0.1.0/go.mod h1:rWUClL7qwHeUIIP7SkokjfvE3RonglnANqJtzUpyHyU= 5 | github.com/cesanta/errors v0.0.0-20160612174407-5adec772d663 h1:yxe0VAaLkwh7cXoC9IhNhUuJa91au1BMEOWWKxgjK94= 6 | github.com/cesanta/errors v0.0.0-20160612174407-5adec772d663/go.mod h1:5V6uYRtJZjiJla602WCKuuB+JiHZDG1E3KOHWmBxr48= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= 10 | github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= 11 | github.com/juamedgod/cliassert v0.0.0-20180320011200-425256f2bb0b h1:j2GwaFh0vduPA3PilmBcsdWv0reobJHyIxGJnh9gIAA= 12 | github.com/juamedgod/cliassert v0.0.0-20180320011200-425256f2bb0b/go.mod h1:+N11eVKRhj1RNqjc9l+QMib0/XYixFOywXFH1zoGucg= 13 | github.com/mmikulicic/multierror v0.0.0-20170428094957-c1ad6b5ecd26 h1:Sk/rGhRF3TAxGR8Ldp3lAUw8GJehJct+5LVU79F13DI= 14 | github.com/mmikulicic/multierror v0.0.0-20170428094957-c1ad6b5ecd26/go.mod h1:GNJ15ZAuaQsmIEb/VWpp0pn83vYzb6PglASRGNhEPww= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/stretchr/testify v1.2.1 h1:52QO5WkIUcHGIR7EnGagH88x1bUzqGXTC5/1bDTUQ7U= 18 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 19 | golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 20 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 22 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 23 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 24 | -------------------------------------------------------------------------------- /handlebars_renderer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/aymerick/raymond" 9 | ) 10 | 11 | func orHelper(a interface{}, b interface{}, options *raymond.Options) interface{} { 12 | aStr := raymond.Str(a) 13 | bStr := raymond.Str(b) 14 | 15 | if aStr != "" { 16 | return aStr 17 | } else if bStr != "" { 18 | return bStr 19 | } 20 | return "" 21 | } 22 | 23 | func quoteHelper(s string) raymond.SafeString { 24 | return raymond.SafeString(fmt.Sprintf("%q", s)) 25 | } 26 | 27 | func jsonEscapeHelper(s string) raymond.SafeString { 28 | b, err := json.Marshal(s) 29 | // raymond does not allow returning errors from helpers 30 | // TODO: Setup recovering from the panic when calling Exec 31 | if err != nil { 32 | panic(err) 33 | } 34 | return raymond.SafeString(b) 35 | } 36 | 37 | func init() { 38 | raymond.RegisterHelper("or", orHelper) 39 | raymond.RegisterHelper("quote", quoteHelper) 40 | raymond.RegisterHelper("json_escape", jsonEscapeHelper) 41 | } 42 | 43 | type renderer interface { 44 | RenderTemplate(in io.Reader, data *templateData) (string, error) 45 | } 46 | 47 | type handlerbarsRenderer struct { 48 | } 49 | 50 | func newHandlerbarsRenderer() renderer { 51 | return &handlerbarsRenderer{} 52 | } 53 | 54 | func (h *handlerbarsRenderer) RenderTemplate(in io.Reader, data *templateData) (string, error) { 55 | tpl, err := h.parseTemplate(in) 56 | if err != nil { 57 | return "", fmt.Errorf("cannot parse template: %v", err) 58 | } 59 | parsedData := h.convertTemplateData(data) 60 | 61 | return tpl.Exec(parsedData) 62 | } 63 | 64 | func (h *handlerbarsRenderer) parseTemplate(in io.Reader) (*raymond.Template, error) { 65 | b, err := io.ReadAll(in) 66 | if err != nil { 67 | return nil, err 68 | } 69 | return raymond.Parse(string(b)) 70 | } 71 | 72 | // strings chars are escaped as they are considerd unsafe for HTML. We need to mark them as safe 73 | func (h *handlerbarsRenderer) convertTemplateData(data *templateData) map[string]raymond.SafeString { 74 | rawData := data.Data() 75 | res := make(map[string]raymond.SafeString, len(rawData)) 76 | for key, value := range rawData { 77 | res[key] = raymond.SafeString(value) 78 | } 79 | return res 80 | } 81 | -------------------------------------------------------------------------------- /handlebars_renderer_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/aymerick/raymond" 9 | ) 10 | 11 | func Test_handlerbarsRenderer_RenderTemplate(t *testing.T) { 12 | type args struct { 13 | in io.Reader 14 | data *templateData 15 | } 16 | tests := []struct { 17 | name string 18 | h *handlerbarsRenderer 19 | args args 20 | want string 21 | wantErr bool 22 | }{} 23 | for _, tt := range tests { 24 | t.Run(tt.name, func(t *testing.T) { 25 | got, err := tt.h.RenderTemplate(tt.args.in, tt.args.data) 26 | if (err != nil) != tt.wantErr { 27 | t.Errorf("handlerbarsRenderer.RenderTemplate() error = %v, wantErr %v", err, tt.wantErr) 28 | return 29 | } 30 | if got != tt.want { 31 | t.Errorf("handlerbarsRenderer.RenderTemplate() = %v, want %v", got, tt.want) 32 | } 33 | }) 34 | } 35 | } 36 | 37 | func Test_handlerbarsRenderer_parseTemplate(t *testing.T) { 38 | type args struct { 39 | in io.Reader 40 | } 41 | tests := []struct { 42 | name string 43 | h *handlerbarsRenderer 44 | args args 45 | want *raymond.Template 46 | wantErr bool 47 | }{} 48 | for _, tt := range tests { 49 | t.Run(tt.name, func(t *testing.T) { 50 | got, err := tt.h.parseTemplate(tt.args.in) 51 | if (err != nil) != tt.wantErr { 52 | t.Errorf("handlerbarsRenderer.parseTemplate() error = %v, wantErr %v", err, tt.wantErr) 53 | return 54 | } 55 | if !reflect.DeepEqual(got, tt.want) { 56 | t.Errorf("handlerbarsRenderer.parseTemplate() = %v, want %v", got, tt.want) 57 | } 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /helpers_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | /* 11 | func getCliArguments(port int, state string, host string, timeout int) []string { 12 | return []string{ 13 | "--state", state, 14 | "--host", host, 15 | "--timeout", fmt.Sprintf("%d", timeout), 16 | fmt.Sprintf("%d", port), 17 | } 18 | } 19 | 20 | func testViaStruct(port int, state string, host string, timeout int, t *testing.T) error { 21 | cmd := NewWaitForPortCmd() 22 | cmd.State = state 23 | cmd.Host = host 24 | cmd.Timeout = timeout 25 | cmd.Args.Port = port 26 | 27 | return cmd.Execute([]string{}) 28 | } 29 | 30 | func testViaCli(port int, state string, host string, timeout int, t *testing.T) error { 31 | cliArgs := getCliArguments(port, state, host, timeout) 32 | res := RunTool(cliArgs...) 33 | if !res.Success() { 34 | return fmt.Errorf("%s", res.Stderr()) 35 | } 36 | return nil 37 | } 38 | */ 39 | 40 | // setenv modify the environment with the new provided map and 41 | // returns a callback to set it back to the original state 42 | func setenv(env map[string]string) (restoreCn func()) { 43 | toRestore := make(map[string]string) 44 | toUnset := []string{} 45 | for k, v := range env { 46 | oldValue, ok := os.LookupEnv(k) 47 | if ok { 48 | toRestore[k] = oldValue 49 | } else { 50 | toUnset = append(toUnset, k) 51 | } 52 | os.Setenv(k, v) 53 | } 54 | return func() { 55 | for k, v := range toRestore { 56 | os.Setenv(k, v) 57 | } 58 | for _, k := range toUnset { 59 | os.Unsetenv(k) 60 | } 61 | } 62 | } 63 | func replaceEnv(env map[string]string) (restoreCn func()) { 64 | toRestore := make(map[string]string) 65 | for _, envLine := range os.Environ() { 66 | data := strings.SplitN(envLine, "=", 2) 67 | if len(data) != 2 { 68 | continue 69 | } 70 | key := data[0] 71 | value := data[1] 72 | toRestore[key] = value 73 | } 74 | os.Clearenv() 75 | for k, v := range env { 76 | os.Setenv(k, v) 77 | } 78 | return func() { 79 | for k, v := range toRestore { 80 | os.Setenv(k, v) 81 | } 82 | } 83 | } 84 | 85 | func mergeMaps(args ...map[string]string) map[string]string { 86 | new := make(map[string]string) 87 | for _, m := range args { 88 | for k, v := range m { 89 | new[k] = v 90 | } 91 | } 92 | return new 93 | } 94 | func writeDataFile(file string, data map[string]string) error { 95 | fh, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) 96 | if err != nil { 97 | return fmt.Errorf("cannot open file %q: %v", file, err) 98 | } 99 | defer fh.Close() 100 | for k, v := range data { 101 | if _, err := io.WriteString(fh, fmt.Sprintf("%s=%s\n", k, v)); err != nil { 102 | return fmt.Errorf("cannot write data to file: %v", err) 103 | } 104 | 105 | } 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | flags "github.com/jessevdk/go-flags" 8 | ) 9 | 10 | func main() { 11 | cmd := NewRenderTemplateCmd() 12 | parser := flags.NewParser(cmd, flags.HelpFlag|flags.PassDoubleDash) 13 | 14 | args, err := parser.Parse() 15 | if err != nil { 16 | fmt.Fprintf(os.Stderr, "%v\n", err) 17 | os.Exit(1) 18 | } 19 | 20 | if err := cmd.Execute(args); err != nil { 21 | fmt.Fprintf(os.Stderr, "%v\n", err) 22 | os.Exit(1) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | "testing" 7 | 8 | tu "github.com/bitnami/gonit/testutils" 9 | ca "github.com/juamedgod/cliassert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func Test_main(t *testing.T) { 14 | templateData1 := "hello, {{name}}.\nWelcome to {{company}}" 15 | expectedResult1 := "hello, user.\nWelcome to bitnami" 16 | tests := []struct { 17 | name string 18 | tplText string 19 | tplFile string 20 | 21 | dataText string 22 | env map[string]string 23 | wantErr bool 24 | stdin string 25 | expectedErr interface{} 26 | expectedResult string 27 | }{ 28 | { 29 | name: "Template from stdin", 30 | stdin: templateData1, 31 | env: map[string]string{"name": "user", "company": "bitnami"}, 32 | expectedResult: expectedResult1, 33 | }, 34 | { 35 | name: "No template from file nor stdin", 36 | wantErr: true, 37 | expectedErr: "you must provide a template file as an argument or through stdin", 38 | }, 39 | 40 | { 41 | name: "Invalid template file", 42 | wantErr: true, 43 | tplFile: "some_nonexistent_file.txt", 44 | expectedErr: "cannot open template file", 45 | }, 46 | } 47 | for _, tt := range tests { 48 | t.Run(tt.name, func(t *testing.T) { 49 | sb := tu.NewSandbox() 50 | defer sb.Cleanup() 51 | cb := replaceEnv(tt.env) 52 | defer cb() 53 | 54 | args := []string{} 55 | 56 | if len(tt.dataText) > 0 { 57 | dataFile, err := sb.Write("data.txt", tt.dataText) 58 | require.NoError(t, err) 59 | args = append(args, "-f", dataFile) 60 | } 61 | 62 | if tt.stdin == "" { 63 | if tt.tplText != "" { 64 | file, err := sb.Write("template.conf.tpl", tt.tplText) 65 | require.NoError(t, err) 66 | args = append(args, file) 67 | } else if tt.tplFile != "" { 68 | args = append(args, tt.tplFile) 69 | 70 | } 71 | } 72 | res := runTool(os.Args[0], args, tt.stdin) 73 | if tt.wantErr { 74 | if res.Success() { 75 | t.Errorf("the command was expected to fail but succeeded") 76 | } else if tt.expectedErr != nil { 77 | res.AssertErrorMatch(t, tt.expectedErr) 78 | } 79 | } else { 80 | res.AssertSuccessMatch(t, tt.expectedResult) 81 | } 82 | }) 83 | } 84 | } 85 | 86 | func TestMain(m *testing.M) { 87 | if os.Getenv("BE_TOOL") == "1" { 88 | main() 89 | os.Exit(0) 90 | return 91 | } 92 | flag.Parse() 93 | c := m.Run() 94 | os.Exit(c) 95 | } 96 | 97 | func runTool(bin string, args []string, stdin string) ca.CmdResult { 98 | cmd := ca.NewCommand() 99 | if stdin != "" { 100 | cmd.SetStdin(stdin) 101 | } 102 | os.Setenv("BE_TOOL", "1") 103 | defer os.Unsetenv("BE_TOOL") 104 | return cmd.Exec(bin, args...) 105 | } 106 | 107 | func RunTool(args ...string) ca.CmdResult { 108 | return runTool(os.Args[0], args, "") 109 | } 110 | -------------------------------------------------------------------------------- /template_data.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "github.com/mmikulicic/multierror" 11 | ) 12 | 13 | // TemplateData provides different sources of data to use when rendering the templates 14 | type templateData struct { 15 | data map[string]string 16 | envData map[string]string 17 | } 18 | 19 | func newTemplateData() *templateData { 20 | return &templateData{ 21 | data: make(map[string]string), 22 | } 23 | } 24 | 25 | func (d *templateData) EnvData() map[string]string { 26 | if len(d.envData) == 0 { 27 | d.loadEnvVars() 28 | } 29 | return d.envData 30 | } 31 | func (d *templateData) CustomData() map[string]string { 32 | return d.data 33 | } 34 | func (d *templateData) Data() map[string]string { 35 | envData := d.EnvData() 36 | customData := d.CustomData() 37 | res := make(map[string]string, len(envData)+len(customData)) 38 | for k, v := range envData { 39 | res[k] = v 40 | } 41 | // custom data takes precedence over the environment 42 | for k, v := range customData { 43 | res[k] = v 44 | } 45 | return res 46 | } 47 | 48 | func (d *templateData) AddData(k string, v string) { 49 | d.data[k] = v 50 | } 51 | 52 | func (d *templateData) LoadBatchDataFile(file string) error { 53 | fh, err := os.Open(file) 54 | if err != nil { 55 | return fmt.Errorf("cannot read data file %q: %v", file, err) 56 | } 57 | defer fh.Close() 58 | return d.LoadBatchData(fh) 59 | } 60 | 61 | func (d *templateData) LoadBatchData(in io.Reader) error { 62 | var errs error 63 | scanner := bufio.NewScanner(in) 64 | for scanner.Scan() { 65 | line := strings.TrimSpace(scanner.Text()) 66 | // Allow comments and blank lines 67 | if line == "" || strings.HasPrefix(line, "#") { 68 | continue 69 | } 70 | kv := strings.SplitN(line, "=", 2) 71 | if len(kv) != 2 { 72 | errs = multierror.Append(errs, fmt.Errorf("malformed line %q: could not find '=' separator", truncateString(line, 50))) 73 | continue 74 | } 75 | d.AddData(kv[0], kv[1]) 76 | } 77 | // If the scanner failed just return that error instead of mixing with possible malformed lines 78 | if scanner.Err() != nil { 79 | return fmt.Errorf("cannot read template data: %v", scanner.Err()) 80 | } 81 | return errs 82 | } 83 | func (d *templateData) loadEnvVars() { 84 | envStrs := os.Environ() 85 | d.envData = make(map[string]string, len(envStrs)) 86 | for _, envLine := range envStrs { 87 | kv := strings.SplitN(envLine, "=", 2) 88 | if len(kv) != 2 { 89 | continue 90 | } 91 | d.envData[kv[0]] = kv[1] 92 | } 93 | } 94 | 95 | func truncateString(str string, num int) string { 96 | if len(str) <= num { 97 | return str 98 | } 99 | if len(str) > 3 { 100 | num -= 3 101 | } 102 | return str[0:num] + "..." 103 | } 104 | -------------------------------------------------------------------------------- /template_data_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | "regexp" 6 | "testing" 7 | 8 | tu "github.com/bitnami/gonit/testutils" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | type templateDataTest struct { 14 | name string 15 | inputEnv map[string]string 16 | dataText string 17 | expectedResult map[string]string 18 | wantErr bool 19 | expectedErr *regexp.Regexp 20 | } 21 | 22 | func Test_templateData_Data(t *testing.T) { 23 | env1 := map[string]string{"A": "Value1", "a": "another value", "INSTALLDIR": "/opt/bitnami"} 24 | tests := []templateDataTest{ 25 | 26 | { 27 | name: "Only env", 28 | inputEnv: env1, 29 | expectedResult: env1, 30 | }, 31 | { 32 | name: "Env and data", 33 | inputEnv: env1, 34 | dataText: ` 35 | USER=bitnami 36 | HOME=/home/bitnami 37 | `, 38 | expectedResult: mergeMaps(env1, map[string]string{"USER": "bitnami", "HOME": "/home/bitnami"}), 39 | }, 40 | { 41 | name: "Data with comments and malformed", 42 | dataText: ` 43 | A=Value a 44 | #B=Value b 45 | 46 | C=Value c 47 | 48 | Some value 49 | 50 | D=Value d 51 | 52 | `, 53 | expectedResult: map[string]string{"A": "Value a", "C": "Value c", "D": "Value d"}, 54 | wantErr: true, 55 | expectedErr: regexp.MustCompile(`^malformed line "Some value": could not find '=' separator$`), 56 | }, 57 | { 58 | name: "Data overrides env", 59 | inputEnv: map[string]string{"A": "value a", "B": "value b"}, 60 | expectedResult: map[string]string{"A": "value a", "B": "new value b", "C": "value c"}, 61 | dataText: "B=new value b\nC=value c", 62 | }, 63 | } 64 | for _, tt := range tests { 65 | t.Run(tt.name, func(t *testing.T) { 66 | 67 | sb := tu.NewSandbox() 68 | defer sb.Cleanup() 69 | file, err := sb.Write("data.txt", tt.dataText) 70 | require.NoError(t, err) 71 | 72 | cb := replaceEnv(tt.inputEnv) 73 | defer cb() 74 | d := newTemplateData() 75 | 76 | err = d.LoadBatchDataFile(file) 77 | if tt.wantErr { 78 | if tt.expectedErr != nil { 79 | tu.AssertErrorMatch(t, err, tt.expectedErr) 80 | } else { 81 | assert.Error(t, err) 82 | } 83 | } else { 84 | assert.NoError(t, err) 85 | } 86 | res := d.Data() 87 | assert.True(t, reflect.DeepEqual(tt.expectedResult, res), "expected %v to be equal to %v", res, tt.expectedResult) 88 | }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /vars.mk: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | GOPATH ?= $(shell go env GOPATH) 3 | PATH := $(GOPATH)/bin:$(PATH) 4 | 5 | BUILD_DIR := $(abspath ./out) 6 | TOOL_NAME ?= $(shell basename $(CURDIR)) 7 | TOOL_PATH ?= $(BUILD_DIR)/$(TOOL_NAME) 8 | 9 | BUILD_DATE := $(shell date -u '+%Y-%m-%d %I:%M:%S UTC' 2> /dev/null) 10 | GIT_HASH := $(shell git rev-parse HEAD 2> /dev/null) 11 | LDFLAGS="-X 'main.buildDate=$(BUILD_DATE)' -X main.commit=$(GIT_HASH) -s -w" 12 | 13 | DEBUG ?= 0 14 | 15 | ifeq ($(DEBUG),1) 16 | GO_TEST := @go test -v 17 | else 18 | GO_TEST := @go test 19 | endif 20 | 21 | GO_MOD := @go mod 22 | 23 | # Do not do goimport of the vendor dir 24 | go_files=$$(find $(1) -type f -name '*.go' -not -path "./vendor/*") 25 | fmtcheck = @if goimports -l $(go_files) | read var; then echo "goimports check failed for $(1):\n `goimports -d $(go_files)`"; exit 1; fi 26 | --------------------------------------------------------------------------------