├── .deepsource.toml ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── ci.yml │ ├── config.yml │ ├── docs.yml │ └── new-feature.yml ├── dependabot.yml ├── stale.yml └── workflows │ ├── ci.yml │ ├── code-security.yml │ ├── codeql.yml │ ├── docs.yml │ ├── nsv.yml │ ├── release.yml │ └── shellcheck.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── Taskfile.yaml ├── cmd ├── context.go ├── imds.go ├── imds_test.go ├── man.go ├── root.go ├── root_test.go ├── tag.go ├── tag_test.go ├── template.go └── util.go ├── docs ├── configure │ ├── auto-attachment.md │ ├── broadcast-ec2.md │ ├── custom-domain.md │ ├── exposing-tags.md │ ├── iam.md │ ├── list-tags.md │ └── tracing-requests.md ├── index.md ├── install │ ├── binary.md │ └── source.md ├── license.md ├── overrides │ └── main.html ├── reference │ ├── cli │ │ ├── dns53-imds.md │ │ ├── dns53-tags.md │ │ └── dns53.md │ └── templating.md ├── static │ ├── auto-attachment-flow.png │ ├── dns53-auto-attach.mp4 │ ├── dns53-auto-attach.webm │ ├── dns53-phzid.mp4 │ ├── dns53-phzid.webm │ ├── dns53.mp4 │ ├── dns53.webm │ ├── favicon.ico │ └── logo.png └── stylesheets │ └── extra.css ├── go.mod ├── go.sum ├── htmltest.yml ├── internal ├── ec2 │ ├── ec2.go │ ├── ec2_test.go │ └── ec2mock │ │ └── mock.go ├── imds │ ├── imdsstub │ │ └── stub.go │ ├── metadata.go │ └── metadata_test.go ├── r53 │ ├── r53.go │ ├── r53_test.go │ └── r53mock │ │ └── mock.go └── tui │ ├── component │ ├── errorpanel.go │ ├── filteredlist.go │ ├── footer.go │ ├── header.go │ ├── model.go │ └── tracer.go │ ├── keymap │ └── keymap.go │ ├── message │ └── message.go │ ├── page │ ├── dashboard.go │ ├── model.go │ └── wizard.go │ └── ui.go ├── main.go ├── mkdocs.yml └── scripts ├── completions.sh ├── fury-upload.sh ├── install └── manpages.sh /.deepsource.toml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 - 2023 Purple Clay 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | # in the Software without restriction, including without limitation the rights 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | version = 1 21 | 22 | test_patterns = [ 23 | "**/*_test.go" 24 | ] 25 | 26 | [[analyzers]] 27 | name = "go" 28 | enabled = true 29 | 30 | [analyzers.meta] 31 | import_root = "github.com/purpleclay/dns53" 32 | 33 | [[analyzers]] 34 | name = "secret" 35 | enabled = true 36 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @purpleclay -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 - 2023 Purple Clay 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | # in the Software without restriction, including without limitation the rights 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | name: Bug Report 22 | description: File a bug report 23 | title: "[Bug]: " 24 | labels: [bug, triage] 25 | assignees: 26 | - purpleclay 27 | body: 28 | - type: markdown 29 | attributes: 30 | value: | 31 | Thanks for taking the time to fill out this bug report. Please be as descriptive and concise as possible. We value all input from the community. 32 | - type: textarea 33 | id: what-happened 34 | attributes: 35 | label: What happened? 36 | description: A clear and concise description of what happened. 37 | placeholder: Tell us what happened? 38 | validations: 39 | required: true 40 | - type: textarea 41 | id: steps 42 | attributes: 43 | label: Steps to reproduce it 44 | description: Please list all of the steps taken to reproduce this bug. 45 | value: | 46 | 1. 47 | 2. 48 | 3. 49 | ... 50 | validations: 51 | required: true 52 | - type: input 53 | id: version 54 | attributes: 55 | label: Which version? 56 | description: Which version of dns53 are you using? 57 | placeholder: dns53 version 58 | validations: 59 | required: true 60 | - type: dropdown 61 | id: os 62 | attributes: 63 | label: Which operating system(s) are you using? 64 | multiple: true 65 | options: 66 | - Linux 67 | - Mac 68 | - Windows 69 | - All 70 | validations: 71 | required: true 72 | - type: checkboxes 73 | id: terms 74 | attributes: 75 | label: Code of Conduct 76 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/purpleclay/dns53/blob/main/CODE_OF_CONDUCT.md) 77 | options: 78 | - label: I agree to follow this project's Code of Conduct 79 | required: true 80 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ci.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022-2023 Purple Clay 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | # in the Software without restriction, including without limitation the rights 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | name: CI Enhancement 22 | description: Suggest a new CI enhancement 23 | title: "[CI]: " 24 | labels: [ci, triage] 25 | assignees: 26 | - purpleclay 27 | body: 28 | - type: markdown 29 | attributes: 30 | value: | 31 | Thanks for taking the time to fill out this new CI enhancement request. Please be as descriptive and concise as possible. We value all input from the community. 32 | - type: textarea 33 | id: describe-enhancement 34 | attributes: 35 | label: Describe your enhancement 36 | description: A clear and concise description of the CI enhancement you are requesting. 37 | placeholder: Your enhancement? 38 | validations: 39 | required: true 40 | - type: checkboxes 41 | id: terms 42 | attributes: 43 | label: Code of Conduct 44 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/purpleclay/dns53/blob/main/CODE_OF_CONDUCT.md) 45 | options: 46 | - label: I agree to follow this project's Code of Conduct 47 | required: true 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022-2023 Purple Clay 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | # in the Software without restriction, including without limitation the rights 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | blank_issues_enabled: false 22 | contact_links: 23 | - name: Ask a question 24 | url: https://github.com/purpleclay/dns53/discussions 25 | about: Please ask and answer questions here with other members of the community 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/docs.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022-2023 Purple Clay 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | # in the Software without restriction, including without limitation the rights 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | name: Documentation Edit 22 | description: Suggest an edit to the documentation 23 | title: "[Docs]: " 24 | labels: [documentation, triage] 25 | assignees: 26 | - purpleclay 27 | body: 28 | - type: markdown 29 | attributes: 30 | value: | 31 | Thanks for taking the time to fill out this new documentation edit request. Please be as descriptive and concise as possible. We value all input from the community. 32 | - type: textarea 33 | id: describe-edit 34 | attributes: 35 | label: Describe your edit 36 | description: A clear and concise description of the documentation edit you are requesting. 37 | placeholder: Your edit? 38 | validations: 39 | required: true 40 | - type: checkboxes 41 | id: terms 42 | attributes: 43 | label: Code of Conduct 44 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/purpleclay/dns53/blob/main/CODE_OF_CONDUCT.md) 45 | options: 46 | - label: I agree to follow this project's Code of Conduct 47 | required: true 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-feature.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 - 2023 Purple Clay 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | # in the Software without restriction, including without limitation the rights 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | name: New Feature 22 | description: Suggest a new feature 23 | title: "[Feature]: " 24 | labels: [enhancement, triage] 25 | assignees: 26 | - purpleclay 27 | body: 28 | - type: markdown 29 | attributes: 30 | value: | 31 | Thanks for taking the time to fill out this new feature request. Please be as descriptive and concise as possible. We value all input from the community. 32 | - type: textarea 33 | id: describe-feature 34 | attributes: 35 | label: Describe your feature 36 | description: A clear and concise description of the feature you are requesting. 37 | placeholder: Your feature? 38 | validations: 39 | required: true 40 | - type: textarea 41 | id: additional-info 42 | attributes: 43 | label: Any additional information? 44 | description: Please provide any additional information about your feature request here 45 | placeholder: Any additional information? 46 | validations: 47 | required: false 48 | - type: checkboxes 49 | id: terms 50 | attributes: 51 | label: Code of Conduct 52 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/purpleclay/dns53/blob/main/CODE_OF_CONDUCT.md) 53 | options: 54 | - label: I agree to follow this project's Code of Conduct 55 | required: true 56 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 - 2023 Purple Clay 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | # in the Software without restriction, including without limitation the rights 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | version: 2 22 | updates: 23 | - package-ecosystem: "gomod" 24 | directory: "/" 25 | schedule: 26 | interval: "daily" 27 | labels: 28 | - "dependabot" 29 | - "pinned" 30 | commit-message: 31 | prefix: "feat" 32 | include: "scope" 33 | - package-ecosystem: "github-actions" 34 | directory: "/" 35 | schedule: 36 | interval: "daily" 37 | labels: 38 | - "dependabot" 39 | - "pinned" 40 | commit-message: 41 | prefix: "chore" 42 | include: "scope" 43 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 - 2023 Purple Clay 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | # in the Software without restriction, including without limitation the rights 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | # Configuration for probot-stale - https://github.com/probot/stale 22 | 23 | # Number of days of inactivity before an issue becomes stale 24 | daysUntilStale: 14 25 | 26 | # Number of days of inactivity before a stale issue is closed 27 | daysUntilClose: 7 28 | 29 | # Issues with these labels will never be considered stale 30 | exemptLabels: 31 | - pinned 32 | - security 33 | 34 | # Label to use when marking an issue as stale 35 | staleLabel: wontfix 36 | 37 | # Comment to post when marking an issue as stale. Set to `false` to disable 38 | markComment: > 39 | This issue has been automatically marked as stale because it has not had 40 | recent activity. It will be closed if no further activity occurs. Thank you 41 | for your contributions. 42 | 43 | # Comment to post when closing a stale issue. Set to `false` to disable 44 | closeComment: false 45 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 - 2023 Purple Clay 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | # in the Software without restriction, including without limitation the rights 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | name: ci 22 | on: 23 | push: 24 | branches: 25 | - main 26 | paths: 27 | - "**/*.go" 28 | - "*.go" 29 | - "go.mod" 30 | - "go.sum" 31 | pull_request: 32 | branches: 33 | - main 34 | paths: 35 | - .github/workflows/ci.yml 36 | - "**/*.go" 37 | - "*.go" 38 | - "go.mod" 39 | - "go.sum" 40 | 41 | permissions: 42 | contents: read 43 | 44 | jobs: 45 | # By splitting testing into its own job will ensure the needs: clause for 46 | # static-analysis runs without waiting on the entire matrix. Jobs that run 47 | # against macos and windows are considerably slower 48 | test: 49 | uses: purpleclay/github/.github/workflows/go-test.yml@main 50 | strategy: 51 | matrix: 52 | os: [ubuntu-latest, macos-latest, windows-latest] 53 | with: 54 | go-version: ${{ vars.GO_VERSION }} 55 | secrets: 56 | github-token: ${{ secrets.GITHUB_TOKEN }} 57 | 58 | lint: 59 | uses: purpleclay/github/.github/workflows/golangci-lint.yml@main 60 | with: 61 | version: ${{ vars.GOLANGCI_LINT_VERSION }} 62 | go-version: ${{ vars.GO_VERSION }} 63 | -------------------------------------------------------------------------------- /.github/workflows/code-security.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 - 2023 Purple Clay 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | # in the Software without restriction, including without limitation the rights 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | name: code-security 22 | on: 23 | push: 24 | branches: 25 | - main 26 | paths: 27 | - "**/*.go" 28 | - "*.go" 29 | - "go.mod" 30 | - "go.sum" 31 | pull_request: 32 | branches: 33 | - main 34 | paths: 35 | - "**/*.go" 36 | - "*.go" 37 | - "go.mod" 38 | - "go.sum" 39 | 40 | permissions: 41 | actions: read 42 | contents: read 43 | security-events: write 44 | 45 | jobs: 46 | security-checks: 47 | if: ${{ github.actor != 'dependabot[bot]' }} 48 | uses: purpleclay/github/.github/workflows/code-security.yml@main 49 | with: 50 | go-version: ${{ vars.GO_VERSION }} 51 | secrets: 52 | github-token: ${{ secrets.GITHUB_TOKEN }} 53 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 - 2023 Purple Clay 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | # in the Software without restriction, including without limitation the rights 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | name: code-quality 22 | 23 | on: 24 | push: 25 | branches: 26 | - main 27 | paths: 28 | - "**/*.go" 29 | - "*.go" 30 | pull_request: 31 | paths: 32 | - "**/*.go" 33 | - "*.go" 34 | schedule: 35 | - cron: "36 4 * * 6" 36 | 37 | jobs: 38 | analyze: 39 | name: Analyze 40 | runs-on: ubuntu-latest 41 | permissions: 42 | actions: read 43 | contents: read 44 | security-events: write 45 | 46 | strategy: 47 | fail-fast: false 48 | matrix: 49 | language: ["go"] 50 | 51 | steps: 52 | - name: Checkout repository 53 | uses: actions/checkout@v4 54 | 55 | - name: Initialize CodeQL 56 | uses: github/codeql-action/init@v3 57 | with: 58 | languages: ${{ matrix.language }} 59 | 60 | - name: Autobuild 61 | uses: github/codeql-action/autobuild@v3 62 | 63 | - name: Perform CodeQL Analysis 64 | uses: github/codeql-action/analyze@v3 65 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 - 2023 Purple Clay 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | # in the Software without restriction, including without limitation the rights 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | name: docs 22 | on: 23 | pull_request: 24 | paths: 25 | - "docs/**" 26 | - "mkdocs.yml" 27 | push: 28 | branches: 29 | - main 30 | tags: 31 | - "v*.*.*" 32 | paths: 33 | - "docs/**" 34 | - "mkdocs.yml" 35 | workflow_dispatch: 36 | 37 | permissions: 38 | contents: write 39 | 40 | jobs: 41 | build-docs: 42 | runs-on: ubuntu-latest 43 | steps: 44 | - name: Checkout repository 45 | uses: actions/checkout@v4 46 | with: 47 | fetch-depth: 0 48 | 49 | - name: GHCR Login 50 | uses: docker/login-action@v3 51 | with: 52 | registry: ghcr.io 53 | username: ${{ github.repository_owner }} 54 | password: ${{ secrets.GH_GHCR }} 55 | 56 | - run: docker pull ghcr.io/purpleclay/mkdocs-material-insiders:${{ vars.MKDOCS_MATERIAL_INSIDERS_VERSION }} 57 | 58 | - name: Build 59 | run: docker run --rm -i -v ${PWD}:/docs ghcr.io/purpleclay/mkdocs-material-insiders:${{ vars.MKDOCS_MATERIAL_INSIDERS_VERSION }} build 60 | env: 61 | CI: true 62 | 63 | - name: HTML Test 64 | uses: wjdp/htmltest-action@master 65 | with: 66 | path: site 67 | config: htmltest.yml 68 | 69 | - name: Patch mkdocs.yml Site URL 70 | if: ${{ startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch') }} 71 | uses: jacobtomlinson/gha-find-replace@v3 72 | with: 73 | find: 'site_url: ""' 74 | replace: 'site_url: "https://docs.purpleclay.dev/${{ github.event.repository.name }}/"' 75 | regex: false 76 | include: mkdocs.yml 77 | 78 | - name: Patch mkdocs.yml Edit URI 79 | if: ${{ startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch') }} 80 | uses: jacobtomlinson/gha-find-replace@v3 81 | with: 82 | find: 'edit_uri: ""' 83 | replace: 'edit_uri: "edit/main/docs"' 84 | regex: false 85 | include: mkdocs.yml 86 | 87 | - name: Deploy documentation 88 | if: ${{ startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch') }} 89 | run: docker run --rm -i -v ${PWD}:/docs ghcr.io/purpleclay/mkdocs-material-insiders:${{ vars.MKDOCS_MATERIAL_INSIDERS_VERSION }} gh-deploy --force 90 | env: 91 | CI: true 92 | -------------------------------------------------------------------------------- /.github/workflows/nsv.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Purple Clay 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | # in the Software without restriction, including without limitation the rights 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | name: nsv 22 | on: 23 | workflow_dispatch: 24 | 25 | jobs: 26 | nsv: 27 | runs-on: ubuntu-latest 28 | permissions: 29 | contents: write 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | with: 34 | fetch-depth: 0 35 | token: ${{ secrets.GH_NSV }} 36 | 37 | - name: Tag 38 | uses: purpleclay/nsv-action@v1 39 | with: 40 | token: ${{ secrets.GH_NSV }} 41 | env: 42 | GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" 43 | GPG_PASSPHRASE: "${{ secrets.GPG_PASSPHRASE }}" 44 | GPG_TRUST_LEVEL: "${{ secrets.GPG_TRUST_LEVEL }}" 45 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 - 2023 Purple Clay 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | # in the Software without restriction, including without limitation the rights 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | name: release 22 | on: 23 | push: 24 | tags: 25 | - "v*.*.*" 26 | 27 | permissions: 28 | actions: read 29 | contents: write 30 | id-token: write 31 | packages: write 32 | 33 | jobs: 34 | goreleaser: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: Git Clone 38 | uses: actions/checkout@v4 39 | with: 40 | fetch-depth: 0 41 | 42 | - name: Setup Go 43 | uses: actions/setup-go@v5 44 | with: 45 | go-version: ${{ vars.GO_VERSION }} 46 | cache: true 47 | 48 | - name: Install Cosign 49 | uses: sigstore/cosign-installer@main 50 | 51 | - name: Download Syft 52 | uses: anchore/sbom-action/download-syft@v0 53 | 54 | - name: GoReleaser 55 | uses: goreleaser/goreleaser-action@v5 56 | with: 57 | version: latest 58 | args: release --clean 59 | env: 60 | GITHUB_TOKEN: "${{ secrets.GH_GORELEASER }}" 61 | FURY_TOKEN: "${{ secrets.GH_FURY_TOKEN }}" 62 | AUR_KEY: "${{ secrets.GH_AUR_KEY }}" 63 | -------------------------------------------------------------------------------- /.github/workflows/shellcheck.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 - 2023 Purple Clay 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | # in the Software without restriction, including without limitation the rights 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | name: shellcheck 22 | on: 23 | push: 24 | branches: 25 | - main 26 | paths: 27 | - "scripts/**" 28 | pull_request: 29 | branches: 30 | - main 31 | paths: 32 | - "scripts/**" 33 | 34 | permissions: 35 | contents: read 36 | 37 | jobs: 38 | shellcheck: 39 | runs-on: ubuntu-latest 40 | name: shellcheck 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v4 44 | 45 | - name: ShellCheck 46 | uses: ludeeus/action-shellcheck@master 47 | with: 48 | scandir: './scripts' 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Built dependencies 18 | completions/ 19 | manpages/ 20 | dns53 21 | reports/ 22 | 23 | # GoReleaser build output 24 | dist/ 25 | 26 | # Mkdocs 27 | .cache/ 28 | site/ 29 | *.DS_Store 30 | 31 | # VSCode 32 | .vscode/ 33 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 - 2023 Purple Clay 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | # in the Software without restriction, including without limitation the rights 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | linters: 22 | disable-all: true 23 | enable: 24 | - errname 25 | - forbidigo 26 | - godox 27 | - goerr113 28 | - gofumpt 29 | - ireturn 30 | - misspell 31 | - musttag 32 | - revive 33 | - tagliatelle 34 | - thelper 35 | - unused 36 | 37 | linters-settings: 38 | ireturn: 39 | allow: 40 | - tea.Msg 41 | - page.Model 42 | - bubbletea.Model 43 | - component.Model 44 | - error 45 | forbidigo: 46 | forbid: 47 | - 'ioutil\.*' 48 | tagliatelle: 49 | case: 50 | use-field-name: false 51 | rules: 52 | json: snake 53 | toml: snake 54 | yaml: snake 55 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 - 2023 Purple Clay 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | # in the Software without restriction, including without limitation the rights 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | before: 22 | hooks: 23 | - ./scripts/completions.sh 24 | - ./scripts/manpages.sh 25 | 26 | builds: 27 | - id: dns53 28 | ldflags: 29 | - -s -w 30 | - -X main.version={{.Tag}} 31 | - -X main.gitCommit={{.Commit}} 32 | - -X main.gitBranch=main 33 | - -X main.buildDate={{.Date}} 34 | env: 35 | - CGO_ENABLED=0 36 | goos: 37 | - linux 38 | - darwin 39 | - windows 40 | goarch: 41 | - amd64 42 | - "386" 43 | - arm 44 | - arm64 45 | goarm: 46 | - "7" 47 | ignore: 48 | - goos: darwin 49 | goarch: "386" 50 | 51 | archives: 52 | - id: dns53-archive 53 | name_template: >- 54 | {{ .ProjectName }}_ 55 | {{- .Version }}_ 56 | {{- .Os }}- 57 | {{- if eq .Arch "amd64" }}x86_64 58 | {{- else }}{{ .Arch }}{{ end }} 59 | {{- if .Arm }}v{{ .Arm }}{{ end }} 60 | builds: 61 | - dns53 62 | rlcp: true 63 | format_overrides: 64 | - goos: windows 65 | format: zip 66 | files: 67 | - README.md 68 | - LICENSE 69 | - completions/* 70 | - manpages/* 71 | 72 | checksum: 73 | name_template: "checksums.txt" 74 | 75 | changelog: 76 | sort: desc 77 | use: github 78 | filters: 79 | exclude: 80 | - "^test" 81 | - "^chore" 82 | - "^ci" 83 | groups: 84 | - title: "Dependency Updates" 85 | regexp: "^.*feat\\(deps\\)*:+.*$" 86 | order: 30 87 | - title: "New Features" 88 | regexp: "^.*feat[(\\w)]*:+.*$" 89 | order: 10 90 | - title: "Bug Fixes" 91 | regexp: "^.*fix[(\\w)]*:+.*$" 92 | order: 20 93 | - title: "Documentation Updates" 94 | regexp: "^.*docs[(\\w)]*:+.*$" 95 | order: 40 96 | - title: "Other Work" 97 | order: 99 98 | 99 | sboms: 100 | - artifacts: archive 101 | 102 | signs: 103 | - cmd: cosign 104 | certificate: "${artifact}.pem" 105 | output: true 106 | artifacts: checksum 107 | args: 108 | - sign-blob 109 | - "--output-certificate=${certificate}" 110 | - "--output-signature=${signature}" 111 | - "${artifact}" 112 | - --yes 113 | 114 | brews: 115 | - name: dns53 116 | tap: 117 | owner: purpleclay 118 | name: homebrew-tap 119 | folder: Formula 120 | homepage: "https://github.com/purpleclay/dns53" 121 | description: "Dynamic DNS within Amazon Route53. Expose your EC2 quickly, easily and privately" 122 | license: MIT 123 | install: | 124 | bin.install "dns53" 125 | 126 | bash_output = Utils.safe_popen_read(bin/"dns53", "completion", "bash") 127 | (bash_completion/"dns53").write bash_output 128 | 129 | zsh_output = Utils.safe_popen_read(bin/"dns53", "completion", "zsh") 130 | (zsh_completion/"_dns53").write zsh_output 131 | 132 | fish_output = Utils.safe_popen_read(bin/"dns53", "completion", "fish") 133 | (fish_completion/"dns53.fish").write fish_output 134 | 135 | man1.install "manpages/dns53.1.gz" 136 | test: | 137 | installed_version = shell_output("#{bin}/dns53 version --short 2>&1") 138 | assert_match "v#{version}", installed_version 139 | 140 | scoop: 141 | bucket: 142 | owner: purpleclay 143 | name: scoop-bucket 144 | homepage: "https://github.com/purpleclay/dns53" 145 | description: "Dynamic DNS within Amazon Route53. Expose your EC2 quickly, easily and privately" 146 | license: MIT 147 | 148 | nfpms: 149 | - file_name_template: "{{ .ConventionalFileName }}" 150 | id: packages 151 | homepage: "https://github.com/purpleclay/dns53" 152 | description: "Dynamic DNS within Amazon Route53. Expose your EC2 quickly, easily and privately" 153 | maintainer: Purple Clay 154 | license: MIT 155 | vendor: Purple Clay 156 | bindir: /usr/bin 157 | section: utils 158 | contents: 159 | - src: ./completions/dns53.bash 160 | dst: /usr/share/bash-completion/completions/dns53 161 | file_info: 162 | mode: 0644 163 | - src: ./completions/dns53.fish 164 | dst: /usr/share/fish/completions/dns53.fish 165 | file_info: 166 | mode: 0644 167 | - src: ./completions/dns53.zsh 168 | dst: /usr/share/zsh/vendor-completions/_dns53 169 | file_info: 170 | mode: 0644 171 | - src: ./LICENSE 172 | dst: /usr/share/doc/dns53/copyright 173 | file_info: 174 | mode: 0644 175 | - src: ./manpages/dns53.1.gz 176 | dst: /usr/share/man/man1/dns53.1.gz 177 | file_info: 178 | mode: 0644 179 | formats: 180 | - apk 181 | - deb 182 | - rpm 183 | deb: 184 | lintian_overrides: 185 | - statically-linked-binary 186 | - changelog-file-missing-in-native-package 187 | 188 | publishers: 189 | - name: fury.io 190 | ids: 191 | - packages 192 | env: 193 | - "FURY_TOKEN={{ .Env.FURY_TOKEN }}" 194 | cmd: ./scripts/fury-upload.sh {{ .ArtifactName }} 195 | 196 | aurs: 197 | - homepage: "https://github.com/purpleclay/dns53" 198 | description: "Dynamic DNS within Amazon Route53. Expose your EC2 quickly, easily and privately" 199 | maintainers: 200 | - "Purple Clay " 201 | license: MIT 202 | private_key: "{{ .Env.AUR_KEY }}" 203 | git_url: "ssh://aur@aur.archlinux.org/dns53-bin.git" 204 | package: |- 205 | # bin 206 | install -Dm755 "./dns53" "${pkgdir}/usr/bin/dns53" 207 | 208 | # license 209 | install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/dns53/LICENSE" 210 | 211 | # completions 212 | mkdir -p "${pkgdir}/usr/share/bash-completion/completions/" 213 | mkdir -p "${pkgdir}/usr/share/zsh/site-functions/" 214 | mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/" 215 | install -Dm644 "./completions/dns53.bash" "${pkgdir}/usr/share/bash-completion/completions/dns53" 216 | install -Dm644 "./completions/dns53.zsh" "${pkgdir}/usr/share/zsh/site-functions/_dns53" 217 | install -Dm644 "./completions/dns53.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/dns53.fish" 218 | 219 | # manpages 220 | install -Dm644 "./manpages/dns53.1.gz" "${pkgdir}/usr/share/man/man1/dns53.1.gz" 221 | 222 | release: 223 | footer: | 224 | **Full Changelog**: https://github.com/purpleclay/dns53/compare/{{ .PreviousTag }}...{{ .Tag }} 225 | 226 | ## What to do next? 227 | 228 | - Read the [documentation](https://purpleclay.github.io/dns53/) 229 | - Follow me on [Twitter](https://twitter.com/purpleclaydev) 230 | - Follow me on [Fosstodon](https://fosstodon.org/@purpleclaydev) 231 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | purpleclaygh@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 - 2023 Purple Clay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dns53 2 | 3 | Dynamic DNS within Amazon Route 53. Expose your EC2 quickly, easily, and privately within a Route 53 Private Hosted Zone (PHZ). 4 | 5 | Easily collaborate with a colleague by exposing your EC2 within a team VPC. You could even hook up a locally running application to a local k3d cluster using an ExternalName service during development. Once your EC2 is exposed, control how it is accessed through your EC2 security groups. 6 | 7 | Written in Go, dns53 is incredibly small and easy to install. 8 | 9 | https://user-images.githubusercontent.com/106762954/200760788-f0d78147-9e3f-4f7d-8606-1050895597dc.mp4 10 | 11 | ## Badges 12 | 13 | [![Build status](https://img.shields.io/github/actions/workflow/status/purpleclay/dns53/ci.yml?style=flat-square&logo=go)](https://github.com/purpleclay/dns53/actions?workflow=ci) 14 | [![License MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](/LICENSE) 15 | [![Go Report Card](https://goreportcard.com/badge/github.com/purpleclay/dns53?style=flat-square)](https://goreportcard.com/report/github.com/purpleclay/dns53) 16 | [![Go Version](https://img.shields.io/github/go-mod/go-version/purpleclay/dns53.svg?style=flat-square)](go.mod) 17 | 18 | ## Documentation 19 | 20 | Check out the latest [documentation](https://purpleclay.github.io/dns53/) 21 | -------------------------------------------------------------------------------- /Taskfile.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Purple Clay 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | # in the Software without restriction, including without limitation the rights 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | version: "3" 22 | 23 | vars: 24 | GIT_COMMIT: 25 | sh: git rev-parse HEAD 26 | GIT_SHA: 27 | sh: git rev-parse --short HEAD 28 | GIT_BRANCH: 29 | sh: git branch --show-current 30 | LDFLAGS: > 31 | -X main.version=dev-{{.GIT_SHA}} 32 | -X main.gitCommit={{.GIT_COMMIT}} 33 | -X main.gitBranch={{.GIT_BRANCH}} 34 | -X main.buildDate={{now | date "2006-01-02T15:04:05Z07:00"}} 35 | 36 | tasks: 37 | default: 38 | desc: Runs all of the default tasks 39 | cmds: 40 | - task: ci 41 | 42 | ci: 43 | desc: Run all CI tasks 44 | cmds: 45 | - task: deps 46 | - task: unit-test 47 | - task: integration-test 48 | - task: lint 49 | - task: build 50 | 51 | deps: 52 | desc: Install all dependencies 53 | cmds: 54 | - go mod tidy 55 | 56 | unit-test: 57 | desc: Run the unit tests 58 | vars: 59 | TEST_FORMAT: '{{default "" .TEST_FORMAT}}' 60 | COVER_PROFILE: '{{default "coverage.out" .COVER_PROFILE}}' 61 | TEST_OPTIONS: '{{default "-short -race -vet=off -shuffle=on" .TEST_OPTIONS}}' 62 | cmds: 63 | - go test {{.TEST_OPTIONS}} -covermode=atomic -coverprofile={{.COVER_PROFILE}} {{.TEST_FORMAT}} ./... 64 | 65 | integration-test: 66 | desc: Run the integration tests 67 | vars: 68 | TEST_FORMAT: '{{default "" .TEST_FORMAT}}' 69 | COVER_PROFILE: '{{default "integrationtest.out" .COVER_PROFILE}}' 70 | TEST_OPTIONS: '{{default "-short -race -vet=off -shuffle=on" .TEST_OPTIONS}}' 71 | cmds: 72 | - go test -run=Integration {{.TEST_OPTIONS}} -covermode=atomic -coverprofile={{.COVER_PROFILE}} {{.TEST_FORMAT}} ./... 73 | 74 | lint: 75 | desc: Lint the code using golangci-lint 76 | vars: 77 | REPORT_FORMAT: '{{default "colored-line-number" .REPORT_FORMAT}}' 78 | cmds: 79 | - golangci-lint run --timeout 5m0s --out-format {{.REPORT_FORMAT}} 80 | 81 | build: 82 | desc: Build the binary 83 | cmds: 84 | - go build -ldflags '-s -w {{.LDFLAGS}}' . 85 | 86 | format: 87 | desc: Format the code using gofumpt 88 | cmds: 89 | - gofumpt -w -l . 90 | -------------------------------------------------------------------------------- /cmd/context.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package cmd 24 | 25 | import ( 26 | "context" 27 | 28 | "github.com/purpleclay/dns53/internal/ec2" 29 | "github.com/purpleclay/dns53/internal/imds" 30 | "github.com/purpleclay/dns53/internal/r53" 31 | ) 32 | 33 | // Can be used to share internal state between cobra commands 34 | type globalContext struct { 35 | context.Context 36 | 37 | // Support overwriting the clients during within the 38 | // PersistentPreRunE hook for testing 39 | imdsClient *imds.Client 40 | ec2Client *ec2.Client 41 | r53Client *r53.Client 42 | } 43 | -------------------------------------------------------------------------------- /cmd/imds.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package cmd 24 | 25 | import ( 26 | "errors" 27 | "strings" 28 | 29 | "github.com/purpleclay/dns53/internal/ec2" 30 | "github.com/spf13/cobra" 31 | ) 32 | 33 | // Custom type used to toggle any setting "on" or "off" 34 | type toggleSetting string 35 | 36 | const ( 37 | toggleSettingOn toggleSetting = "on" 38 | toggleSettingOff toggleSetting = "off" 39 | ) 40 | 41 | func (t *toggleSetting) String() string { 42 | return string(*t) 43 | } 44 | 45 | //nolint:goerr113 46 | func (t *toggleSetting) Set(v string) error { 47 | setting := strings.ToLower(v) 48 | 49 | switch setting { 50 | case "on", "off": 51 | *t = toggleSetting(setting) 52 | return nil 53 | default: 54 | return errors.New(`supported values are "on" or "off" (case-insensitive)`) 55 | } 56 | } 57 | 58 | func (*toggleSetting) Type() string { 59 | return "string" 60 | } 61 | 62 | type imdsOptions struct { 63 | InstanceMetadataTags toggleSetting 64 | } 65 | 66 | func imdsCommand() *cobra.Command { 67 | opt := imdsOptions{} 68 | 69 | cmd := &cobra.Command{ 70 | Use: "imds", 71 | Short: "Toggle EC2 IMDS features", 72 | Args: cobra.NoArgs, 73 | SilenceUsage: true, 74 | SilenceErrors: true, 75 | RunE: func(cmd *cobra.Command, args []string) error { 76 | ctx := cmd.Context().(*globalContext) 77 | 78 | return toggleMetadataTags(ctx, opt.InstanceMetadataTags) 79 | }, 80 | } 81 | 82 | f := cmd.Flags() 83 | f.Var(&opt.InstanceMetadataTags, "instance-metadata-tags", "toggle the inclusion of EC2 instance tags within IMDS (on|off)") 84 | 85 | cmd.MarkFlagRequired("instance-metadata-tags") 86 | return cmd 87 | } 88 | 89 | func toggleMetadataTags(ctx *globalContext, setting toggleSetting) error { 90 | metadata, err := ctx.imdsClient.InstanceMetadata(ctx) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | var toggle ec2.InstanceMetadataToggle 96 | 97 | switch setting { 98 | case toggleSettingOn: 99 | toggle = ec2.InstanceMetadataToggleEnabled 100 | case toggleSettingOff: 101 | toggle = ec2.InstanceMetadataToggleDisabled 102 | } 103 | 104 | return ctx.ec2Client.ToggleInstanceMetadataTags(ctx, metadata.InstanceID, toggle) 105 | } 106 | -------------------------------------------------------------------------------- /cmd/imds_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package cmd 24 | 25 | import ( 26 | "testing" 27 | 28 | "github.com/stretchr/testify/assert" 29 | "github.com/stretchr/testify/require" 30 | ) 31 | 32 | func TestToggleSettingString(t *testing.T) { 33 | toggle := toggleSetting("on") 34 | assert.Equal(t, "on", toggle.String()) 35 | } 36 | 37 | func TestToggleSettingSet(t *testing.T) { 38 | tests := []struct { 39 | name string 40 | input string 41 | expected string 42 | }{ 43 | { 44 | name: "LowercaseOn", 45 | input: "on", 46 | expected: "on", 47 | }, 48 | { 49 | name: "LowercaseOff", 50 | input: "off", 51 | expected: "off", 52 | }, 53 | { 54 | name: "MixedCaseOn", 55 | input: "oN", 56 | expected: "on", 57 | }, 58 | } 59 | for _, tt := range tests { 60 | t.Run(tt.name, func(t *testing.T) { 61 | var setting toggleSetting 62 | 63 | err := setting.Set(tt.input) 64 | require.NoError(t, err) 65 | 66 | require.Equal(t, tt.expected, string(setting)) 67 | }) 68 | } 69 | } 70 | 71 | func TestToggleSettingSetError(t *testing.T) { 72 | var setting toggleSetting 73 | 74 | err := setting.Set("not-supported") 75 | assert.EqualError(t, err, `supported values are "on" or "off" (case-insensitive)`) 76 | } 77 | 78 | func TestToggleSettingType(t *testing.T) { 79 | toggle := toggleSetting("on") 80 | assert.Equal(t, "string", toggle.Type()) 81 | } 82 | -------------------------------------------------------------------------------- /cmd/man.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package cmd 24 | 25 | import ( 26 | "fmt" 27 | "io" 28 | 29 | mcobra "github.com/muesli/mango-cobra" 30 | "github.com/muesli/roff" 31 | "github.com/spf13/cobra" 32 | ) 33 | 34 | func manPagesCmd(out io.Writer) *cobra.Command { 35 | manCmd := &cobra.Command{ 36 | Use: "man", 37 | Short: "Generate man pages for dns53", 38 | DisableFlagsInUseLine: true, 39 | Hidden: true, 40 | SilenceUsage: true, 41 | SilenceErrors: true, 42 | Args: cobra.NoArgs, 43 | RunE: func(cmd *cobra.Command, args []string) error { 44 | mp, err := mcobra.NewManPage(1, cmd.Root()) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | _, err = fmt.Fprint(out, mp.Build(roff.NewDocument())) 50 | return err 51 | }, 52 | } 53 | 54 | return manCmd 55 | } 56 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package cmd 24 | 25 | import ( 26 | "bytes" 27 | "context" 28 | "encoding/json" 29 | "errors" 30 | "fmt" 31 | "io" 32 | "regexp" 33 | "runtime" 34 | "strings" 35 | "text/template" 36 | 37 | "github.com/aws/aws-sdk-go-v2/aws" 38 | "github.com/aws/aws-sdk-go-v2/config" 39 | awsimds "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" 40 | awsec2 "github.com/aws/aws-sdk-go-v2/service/ec2" 41 | awsr53 "github.com/aws/aws-sdk-go-v2/service/route53" 42 | tea "github.com/charmbracelet/bubbletea" 43 | "github.com/gobeam/stringy" 44 | "github.com/purpleclay/dns53/internal/ec2" 45 | "github.com/purpleclay/dns53/internal/imds" 46 | "github.com/purpleclay/dns53/internal/r53" 47 | "github.com/purpleclay/dns53/internal/tui" 48 | "github.com/spf13/cobra" 49 | ) 50 | 51 | type BuildDetails struct { 52 | Version string `json:"version,omitempty"` 53 | GitBranch string `json:"git_branch,omitempty"` 54 | GitCommit string `json:"git_commit,omitempty"` 55 | Date string `json:"build_date,omitempty"` 56 | } 57 | 58 | const ( 59 | longDesc = `Dynamic DNS within Amazon Route 53. Expose your EC2 quickly, easily, and privately within a Route 60 | 53 Private Hosted Zone (PHZ). 61 | 62 | Your EC2 will be exposed through a dynamically generated resource record that will automatically 63 | be deleted when dns53 exits. Let dns53 name your resource record for you, or customise it to your needs. 64 | 65 | Built using Bubbletea 🧋` 66 | examples = ` # Launch the TUI and use the wizard to select a PHZ 67 | dns53 68 | 69 | # Launch the TUI using a chosen PHZ, effectively skipping the wizard 70 | dns53 --phz-id Z000000000ABCDEFGHIJK 71 | 72 | # Launch the TUI, automatically creating and attaching to a default 73 | # PHZ. This will also skip the wizard 74 | dns53 --auto-attach 75 | 76 | # Launch the TUI with a given domain name 77 | dns53 --domain-name custom.domain 78 | 79 | # Launch the TUI with a templated domain name 80 | dns53 --domain-name "{{.IPv4}}.{{.Region}}"` 81 | ) 82 | 83 | var domainRegex = regexp.MustCompile("[^a-zA-Z0-9-.]+") 84 | 85 | type globalOptions struct { 86 | awsRegion string 87 | awsProfile string 88 | imdsBindAddr string 89 | } 90 | 91 | type options struct { 92 | phzID string 93 | domainName string 94 | autoAttach bool 95 | proxy bool 96 | proxyPort int 97 | } 98 | 99 | type autoAttachment struct { 100 | phzID string 101 | vpc string 102 | region string 103 | createdPhz bool 104 | associatedPhz bool 105 | } 106 | 107 | // Command defines the root DNS 53 cobra command 108 | type Command struct { 109 | ctx *globalContext 110 | } 111 | 112 | // New initialises the root DNS 53 command 113 | func New() *Command { 114 | return &Command{ 115 | ctx: &globalContext{ 116 | Context: context.Background(), 117 | }, 118 | } 119 | } 120 | 121 | // Execute the DNS 53 command 122 | func (c *Command) Execute(out io.Writer, buildInfo BuildDetails) error { 123 | globalOpts := &globalOptions{} 124 | opts := options{} 125 | 126 | // Capture in PreRun lifecycle 127 | var metadata imds.Metadata 128 | 129 | rootCmd := &cobra.Command{ 130 | Use: "dns53", 131 | Short: `Dynamic DNS within Amazon Route 53. Expose your EC2 quickly, easily and privately within a Route 132 | 53 Private Hosted Zone (PHZ)`, 133 | Long: longDesc, 134 | Example: examples, 135 | SilenceUsage: true, 136 | SilenceErrors: true, 137 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 138 | // Construct the required AWS Clients and execute any of the provided options 139 | cfg, err := awsConfig(globalOpts) 140 | if err != nil { 141 | return err 142 | } 143 | 144 | c.ctx.ec2Client = ec2.NewFromAPI(awsec2.NewFromConfig(cfg)) 145 | c.ctx.imdsClient = imds.NewFromAPI(awsimds.NewFromConfig(cfg)) 146 | c.ctx.r53Client = r53.NewFromAPI(awsr53.NewFromConfig(cfg)) 147 | 148 | return nil 149 | }, 150 | PreRunE: func(cmd *cobra.Command, args []string) error { 151 | var err error 152 | if metadata, err = c.ctx.imdsClient.InstanceMetadata(c.ctx); err != nil { 153 | return err 154 | } 155 | 156 | if opts.domainName == "" { 157 | return nil 158 | } 159 | 160 | cleanTags(metadata.Tags) 161 | opts.domainName, err = resolveDomainName(opts.domainName, metadata) 162 | return err 163 | }, 164 | RunE: func(cmd *cobra.Command, args []string) error { 165 | if opts.autoAttach { 166 | attachment, err := autoAttachToZone(c.ctx, "dns53", metadata.VPC, metadata.Region) 167 | if err != nil { 168 | return err 169 | } 170 | opts.phzID = attachment.phzID 171 | 172 | defer removeAttachmentToZone(c.ctx, attachment) 173 | } 174 | 175 | // At the moment there isn't a nicer way to capture this 176 | options := tui.Options{ 177 | About: tui.About{ 178 | Name: "dns53", 179 | Version: buildInfo.Version, 180 | ShortDescription: "Dynamic DNS within Amazon Route 53. Expose your EC2 quickly, easily, and privately.", 181 | }, 182 | R53Client: c.ctx.r53Client, 183 | EC2Metadata: metadata, 184 | HostedZoneID: opts.phzID, 185 | DomainName: opts.domainName, 186 | Proxy: opts.proxy, 187 | ProxyPort: opts.proxyPort, 188 | } 189 | 190 | var err error 191 | p := tea.NewProgram( 192 | tui.New(options), 193 | tea.WithMouseCellMotion(), 194 | tea.WithOutput(out), 195 | tea.WithAltScreen(), 196 | ) 197 | _, err = p.Run() 198 | return err 199 | }, 200 | } 201 | 202 | pf := rootCmd.PersistentFlags() 203 | pf.StringVar(&globalOpts.imdsBindAddr, "imds-bind-addr", "", "the endpoint for all AWS IMDS requests") 204 | pf.StringVar(&globalOpts.awsProfile, "profile", "", "the AWS named profile to use when loading credentials") 205 | pf.StringVar(&globalOpts.awsRegion, "region", "", "the AWS region to use when querying AWS") 206 | 207 | // Allow the imds address to be changed at runtime through a hidden flag 208 | rootCmd.PersistentFlags().MarkHidden("imds-bind-addr") 209 | 210 | f := rootCmd.Flags() 211 | f.BoolVar(&opts.autoAttach, "auto-attach", false, "automatically create and attach a record set to a default private hosted zone") 212 | f.StringVar(&opts.domainName, "domain-name", "", "assign a custom domain name when generating a record set") 213 | f.StringVar(&opts.phzID, "phz-id", "", "an ID of a Route53 private hosted zone to use when generating a record set") 214 | f.BoolVar(&opts.proxy, "proxy", false, "enable a reverse proxy for tracing requests to this ec2") 215 | f.IntVar(&opts.proxyPort, "proxy-port", 10080, "the port assigned to the proxy when enabled") 216 | 217 | rootCmd.AddCommand(versionCmd(out, buildInfo)) 218 | rootCmd.AddCommand(manPagesCmd(out)) 219 | rootCmd.AddCommand(imdsCommand()) 220 | rootCmd.AddCommand(tagsCommand(out)) 221 | 222 | rootCmd.SetUsageTemplate(customUsageTemplate) 223 | return rootCmd.ExecuteContext(c.ctx) 224 | } 225 | 226 | func awsConfig(opts *globalOptions) (aws.Config, error) { 227 | var optsFn []func(*config.LoadOptions) error 228 | if opts.awsProfile != "" { 229 | optsFn = append(optsFn, config.WithSharedConfigProfile(opts.awsProfile)) 230 | } 231 | 232 | if opts.awsRegion != "" { 233 | optsFn = append(optsFn, config.WithRegion(opts.awsRegion)) 234 | } 235 | 236 | if opts.imdsBindAddr != "" { 237 | optsFn = append(optsFn, config.WithEC2IMDSEndpoint(opts.imdsBindAddr)) 238 | } 239 | return config.LoadDefaultConfig(context.Background(), optsFn...) 240 | } 241 | 242 | //nolint:goerr113 243 | func resolveDomainName(domain string, metadata imds.Metadata) (string, error) { 244 | dmn := strings.ReplaceAll(domain, " ", "") 245 | 246 | if strings.Contains(dmn, "{{.Name}}") { 247 | if metadata.Name == "" { 248 | return "", errors.New(`to use metadata within a custom domain name, please enable IMDS instance tags support 249 | for your EC2 instance: 250 | 251 | $ dns53 imds --instance-metadata-tags on 252 | 253 | Or read the official AWS documentation at: 254 | https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#allow-access-to-tags-in-IMDS`) 255 | } 256 | 257 | name := stringy.New(metadata.Name) 258 | metadata.Name = name.KebabCase().ToLower() 259 | } 260 | 261 | // Sanitise the copy of the metadata before resolving the template 262 | metadata.IPv4 = strings.ReplaceAll(metadata.IPv4, ".", "-") 263 | 264 | // Execute the domain template 265 | tmpl, err := template.New("domain").Parse(domain) 266 | if err != nil { 267 | return "", err 268 | } 269 | 270 | var out bytes.Buffer 271 | if err := tmpl.Execute(&out, metadata); err != nil { 272 | return "", err 273 | } 274 | dmn = out.String() 275 | 276 | // Final tidy up of the domain 277 | dmn = strings.ReplaceAll(dmn, "--", "-") 278 | dmn = strings.ReplaceAll(dmn, "..", ".") 279 | dmn = strings.Trim(dmn, "-") 280 | dmn = strings.Trim(dmn, ".") 281 | dmn = domainRegex.ReplaceAllString(dmn, "") 282 | 283 | return dmn, nil 284 | } 285 | 286 | func autoAttachToZone(ctx *globalContext, name, vpc, region string) (autoAttachment, error) { 287 | attachment := autoAttachment{ 288 | vpc: vpc, 289 | region: region, 290 | } 291 | 292 | zone, err := ctx.r53Client.ByName(ctx, name) 293 | if err != nil { 294 | return attachment, err 295 | } 296 | 297 | if zone == nil { 298 | newZone, err := ctx.r53Client.CreatePrivateHostedZone(ctx, "dns53", vpc, region) 299 | if err != nil { 300 | return attachment, err 301 | } 302 | 303 | zone = &newZone 304 | 305 | // Record that this PHZ was created during auto-attachment 306 | attachment.createdPhz = true 307 | } else { 308 | if err := ctx.r53Client.AssociateVPCWithZone(ctx, zone.ID, vpc, region); err != nil { 309 | return attachment, err 310 | } 311 | 312 | // An explicit association has been made between the EC2 VPC and the PHZ during auto-attachment 313 | attachment.associatedPhz = true 314 | } 315 | 316 | attachment.phzID = zone.ID 317 | return attachment, nil 318 | } 319 | 320 | func removeAttachmentToZone(ctx *globalContext, attach autoAttachment) error { 321 | if attach.createdPhz { 322 | return ctx.r53Client.DeletePrivateHostedZone(ctx, attach.phzID) 323 | } 324 | 325 | return ctx.r53Client.DisassociateVPCWithZone(ctx, attach.phzID, attach.vpc, attach.region) 326 | } 327 | 328 | func versionCmd(out io.Writer, buildInfo BuildDetails) *cobra.Command { 329 | var short bool 330 | cmd := &cobra.Command{ 331 | Use: "version", 332 | Short: "Print build time version information", 333 | RunE: func(cmd *cobra.Command, args []string) error { 334 | if short { 335 | fmt.Fprintf(out, buildInfo.Version) 336 | return nil 337 | } 338 | 339 | ver := struct { 340 | Go string `json:"go"` 341 | GoArch string `json:"go_arch"` 342 | GoOS string `json:"go_os"` 343 | BuildDetails 344 | }{ 345 | Go: runtime.Version(), 346 | GoArch: runtime.GOARCH, 347 | GoOS: runtime.GOOS, 348 | BuildDetails: buildInfo, 349 | } 350 | return json.NewEncoder(out).Encode(&ver) 351 | }, 352 | } 353 | 354 | flags := cmd.Flags() 355 | flags.BoolVar(&short, "short", false, "only print the version number") 356 | 357 | return cmd 358 | } 359 | -------------------------------------------------------------------------------- /cmd/root_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package cmd 24 | 25 | import ( 26 | "testing" 27 | 28 | "github.com/purpleclay/dns53/internal/imds" 29 | "github.com/stretchr/testify/assert" 30 | "github.com/stretchr/testify/require" 31 | ) 32 | 33 | func TestResolveDomainName(t *testing.T) { 34 | metadata := imds.Metadata{ 35 | Name: "my-ec2", 36 | } 37 | 38 | tests := []struct { 39 | name string 40 | domain string 41 | expected string 42 | }{ 43 | { 44 | name: "NoTemplating", 45 | domain: "custom.domain", 46 | expected: "custom.domain", 47 | }, 48 | { 49 | name: "WithNameField", 50 | domain: "custom.{{.Name}}", 51 | expected: "custom.my-ec2", 52 | }, 53 | { 54 | name: "WithNameFieldSpaces", 55 | domain: "custom.{{ .Name }}", 56 | expected: "custom.my-ec2", 57 | }, 58 | { 59 | name: "ReplacesDoubleHyphens", 60 | domain: "another--custom.domain", 61 | expected: "another-custom.domain", 62 | }, 63 | { 64 | name: "ReplacesDoubleDots", 65 | domain: "my-custom123..domain", 66 | expected: "my-custom123.domain", 67 | }, 68 | { 69 | name: "RemoveLeadingTrailingHyphen", 70 | domain: "-this-is-a-custom.domain-", 71 | expected: "this-is-a-custom.domain", 72 | }, 73 | { 74 | name: "RemoveLeadingTrailingDot", 75 | domain: ".a-custom.domain.", 76 | expected: "a-custom.domain", 77 | }, 78 | { 79 | name: "TrimUnsupportedCharacters", 80 | domain: "custom@#.doma**in-123", 81 | expected: "custom.domain-123", 82 | }, 83 | } 84 | for _, tt := range tests { 85 | t.Run(tt.name, func(t *testing.T) { 86 | domain, err := resolveDomainName(tt.domain, metadata) 87 | 88 | require.NoError(t, err) 89 | require.Equal(t, tt.expected, domain) 90 | }) 91 | } 92 | } 93 | 94 | func TestResolveDomainNameNoInstanceTags(t *testing.T) { 95 | _, err := resolveDomainName("custom.{{.Name}}", imds.Metadata{}) 96 | 97 | assert.EqualError(t, err, `to use metadata within a custom domain name, please enable IMDS instance tags support 98 | for your EC2 instance: 99 | 100 | $ dns53 imds --instance-metadata-tags on 101 | 102 | Or read the official AWS documentation at: 103 | https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#allow-access-to-tags-in-IMDS`) 104 | } 105 | 106 | func TestResolveDomainNameTransformsNameTagToKebabCase(t *testing.T) { 107 | domain, err := resolveDomainName("first.custom.{{.Name}}", imds.Metadata{Name: "MyEc2 123"}) 108 | 109 | require.NoError(t, err) 110 | assert.Equal(t, "first.custom.my-ec2-123", domain) 111 | } 112 | 113 | func TestResolveDomainNameStripsLeadingTrailingHyphenFromNameTag(t *testing.T) { 114 | domain, err := resolveDomainName("second.custom.{{.Name}}", imds.Metadata{Name: "-MyEc2 123-"}) 115 | 116 | require.NoError(t, err) 117 | assert.Equal(t, "second.custom.my-ec2-123", domain) 118 | } 119 | 120 | func TestCleanTagsAppendsToMap(t *testing.T) { 121 | tags := map[string]string{ 122 | "My+@-key_=,.:1": "A value", 123 | } 124 | cleanTags(tags) 125 | 126 | expected := map[string]string{ 127 | "My+@-key_=,.:1": "a-value", 128 | "MyKey1": "a-value", 129 | } 130 | for k, v := range expected { 131 | assert.Contains(t, tags, k) 132 | assert.Equal(t, v, tags[k]) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /cmd/tag.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package cmd 24 | 25 | import ( 26 | "fmt" 27 | "io" 28 | 29 | "github.com/olekukonko/tablewriter" 30 | "github.com/spf13/cobra" 31 | ) 32 | 33 | func tagsCommand(out io.Writer) *cobra.Command { 34 | cmd := &cobra.Command{ 35 | Use: "tags", 36 | Short: "Lists all available EC2 instance tags and how to use them with Go templating", 37 | Args: cobra.NoArgs, 38 | SilenceUsage: true, 39 | SilenceErrors: true, 40 | RunE: func(cmd *cobra.Command, args []string) error { 41 | ctx := cmd.Context().(*globalContext) 42 | 43 | metadata, err := ctx.imdsClient.InstanceMetadata(ctx) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | table := tablewriter.NewWriter(out) 49 | table.SetHeader([]string{"Tag", "Value", "Property Chaining", "Indexed"}) 50 | 51 | for k, v := range metadata.Tags { 52 | cleanedTag, cleanedValue := cleanTag(k, v) 53 | 54 | table.Append([]string{ 55 | k, 56 | cleanedValue, 57 | fmt.Sprintf("{{.Tags.%s}}", cleanedTag), 58 | fmt.Sprintf("{{index .Tags %q}}", k), 59 | }) 60 | } 61 | 62 | table.Render() 63 | return nil 64 | }, 65 | } 66 | 67 | return cmd 68 | } 69 | -------------------------------------------------------------------------------- /cmd/tag_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package cmd 24 | 25 | import ( 26 | "bytes" 27 | "strings" 28 | "testing" 29 | 30 | "github.com/purpleclay/dns53/internal/imds" 31 | "github.com/purpleclay/dns53/internal/imds/imdsstub" 32 | "github.com/stretchr/testify/assert" 33 | "github.com/stretchr/testify/require" 34 | ) 35 | 36 | func TestTagsCommand(t *testing.T) { 37 | var buf bytes.Buffer 38 | ctx := &globalContext{ 39 | imdsClient: imds.NewFromAPI(imdsstub.New(t)), 40 | } 41 | 42 | cmd := tagsCommand(&buf) 43 | err := cmd.ExecuteContext(ctx) 44 | 45 | require.NoError(t, err) 46 | 47 | // Can't guarantee order of tags so break up the testing of the table 48 | table := buf.String() 49 | 50 | assert.True(t, strings.HasPrefix(table, `+-------------+----------+-----------------------+-------------------------------+ 51 | | TAG | VALUE | PROPERTY CHAINING | INDEXED | 52 | +-------------+----------+-----------------------+-------------------------------+ 53 | `)) 54 | assert.Contains(t, table, "| Name | stub-ec2 | {{.Tags.Name}} | {{index .Tags \"Name\"}} |\n") 55 | assert.Contains(t, table, "| Environment | dev | {{.Tags.Environment}} | {{index .Tags \"Environment\"}} |\n") 56 | assert.True(t, strings.HasSuffix(table, "+-------------+----------+-----------------------+-------------------------------+\n")) 57 | } 58 | -------------------------------------------------------------------------------- /cmd/template.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package cmd 24 | 25 | // Modified from: https://github.com/spf13/cobra/blob/0c72800b8dba637092b57a955ecee75949e79a73/command.go#L539 26 | const customUsageTemplate = `Usage:{{if .Runnable}} 27 | {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} 28 | {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} 29 | 30 | Aliases: 31 | {{.NameAndAliases}}{{end}}{{if .HasExample}} 32 | 33 | Examples: 34 | {{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}} 35 | 36 | Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}} 37 | {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}} 38 | 39 | {{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}} 40 | {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}} 41 | 42 | Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}} 43 | {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} 44 | 45 | Flags: 46 | {{.LocalFlags.FlagUsagesWrapped 100 | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} 47 | 48 | Global Flags: 49 | {{.InheritedFlags.FlagUsagesWrapped 100 | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} 50 | 51 | Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} 52 | {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} 53 | 54 | Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} 55 | ` 56 | -------------------------------------------------------------------------------- /cmd/util.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package cmd 24 | 25 | import ( 26 | "regexp" 27 | 28 | "github.com/gobeam/stringy" 29 | ) 30 | 31 | var tagStripRegex = regexp.MustCompile("[^a-zA-Z0-9-]+") 32 | 33 | func cleanTag(key, value string) (string, string) { 34 | cleanedKey := stringy.New(tagStripRegex.ReplaceAllString(key, "-")).CamelCase() 35 | cleanedValue := stringy.New(value).KebabCase().ToLower() 36 | 37 | return cleanedKey, cleanedValue 38 | } 39 | 40 | func cleanTags(tags map[string]string) { 41 | for k, v := range tags { 42 | cleanedKey, cleanedValue := cleanTag(k, v) 43 | 44 | tags[k] = cleanedValue 45 | tags[cleanedKey] = cleanedValue 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/configure/auto-attachment.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Automatically attach your EC2 to a default Route53 Private Hosted Zone" 3 | icon: material/connection 4 | --- 5 | 6 | # Auto Attachment of your EC2 7 | 8 | Your EC2 can automatically be attached to a default Route53 Private Hosted Zone (PHZ) called `dns53`. 9 | 10 | ```sh 11 | dns53 --auto-attach 12 | ``` 13 | 14 | For this to work as intended, `dns53` handles two attachment scenarios, removing the need for any PHZ management. 👍 15 | 16 |
17 | 21 |
22 | 23 | ## You are the first Auto Attachment 24 | 25 | Congratulations, you beat everyone else in your team or organisation and auto-attached your EC2 to the default `dns53` PHZ. `dns53` will: 26 | 27 | 1. Create the new `dns53` PHZ and associate the launched EC2 VPC with it. 28 | 1. Broadcasts your EC2 as expected, using a custom domain name if provided. 29 | 1. Tidies everything up when you exit. 30 | 31 | ## Auto Attachment has already happened 32 | 33 | If someone in your team, or organisation, was super keen and already auto-attached their EC2 to the default `dns53` PHZ, your attachment will be slightly different. `dns53` will: 34 | 35 | 1. Check if your launched EC2 is within a VPC associated with the PHZ. If not, it will create a new association. 36 | 1. Broadcasts your EC2 as expected, using a custom domain name if provided. 37 | 1. Tidies everything up when you exit. 38 | 39 | ## A 10,000-foot view 40 | 41 | ![Auto Attachment Flow](../static/auto-attachment-flow.png) 42 | -------------------------------------------------------------------------------- /docs/configure/broadcast-ec2.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Broadcasting your EC2 privately within your VPC couldn't be easier" 3 | icon: material/bullhorn-variant-outline 4 | --- 5 | 6 | # Privately Broadcast your EC2 7 | 8 | To broadcast your EC2 privately within your VPC couldn't be easier. Launch the wizard and follow the on-screen prompts: 9 | 10 | ```sh 11 | dns53 12 | ``` 13 | 14 | ## Default Domain Name 15 | 16 | A default domain name will be assigned to your EC2 when `dns53` adds an A-Record to the chosen Route53 Private Hosted Zone (PHZ). 17 | 18 | It follows the format: 19 | 20 | `.dns53.` ~> `10-0-1-182.dns53.testing` 21 | 22 | ## Skipping the Wizard 23 | 24 | If you have the ID of your Route53 PHZ handy, you can skip the wizard and immediately broadcast your EC2: 25 | 26 | ```sh 27 | dns53 --phz-id Z05504861FO8RFR02KU72 28 | ``` 29 | 30 |
31 | 35 |
36 | -------------------------------------------------------------------------------- /docs/configure/custom-domain.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Learn how to customise your EC2 domain name when privately broadcasting your instance" 3 | icon: material/web 4 | --- 5 | 6 | # Using a Custom Domain Name 7 | 8 | If you want complete control of the domain name associated with your EC2, you can customise it in one of two ways. 9 | 10 | !!! tip "Route53 Root Domain is Optional" 11 | 12 | `dns53` will automatically append the Route53 root domain when creating the A-Record. Feel free to omit this when providing a custom domain 13 | 14 | ## Static Domain 15 | 16 | ```sh 17 | dns53 --domain-name "my.ec2" 18 | ``` 19 | 20 | ## Templated Domain 21 | 22 | A templated domain leverages the text templating capabilities of the Go language to replace named fields with concrete values. A list of supported named fields can be found [here](../reference/templating.md). 23 | 24 | ```sh 25 | dns53 --domain-name "{{.IPv4}}.{{.Region}}" 26 | ``` 27 | 28 | ## Domain Validation 29 | 30 | A custom domain must be valid before assigning it to your EC2 instance. A series of checks must pass. 31 | 32 | A domain must: 33 | 34 | - not contain leading or trailing hyphens (`-`) and dots (`.`) 35 | - not contain consecutive hyphens (`--`) or dots (`..`) 36 | - not contain whitespace (` `) 37 | - only contain valid characters from the sequence `[A-Za-z0-9-.]` 38 | 39 | `dns53` will automatically clean any domain name in an attempt to enforce these validation checks. 40 | -------------------------------------------------------------------------------- /docs/configure/exposing-tags.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Expand your domain name customisation options by exposing your EC2 instance tags" 3 | icon: material/tag-outline 4 | --- 5 | 6 | # Exposing EC2 Instance Tags 7 | 8 | By default, EC2 tags are not accessible through the Instance Metadata Service (IMDS) and subsequently by `dns53`. Granting access to EC2 instance tags can be carried out manually[^1] or with the following custom command: 9 | 10 | ```sh 11 | dns53 imds --instance-metadata-tags on 12 | ``` 13 | 14 | ## Cleaning Tag Names 15 | 16 | All tags accessible through IMDS will subsequently be "cleaned" and made available for crafting custom domain names. A Pascal Case naming convention is applied to all tags when stored within an internal map alongside their originally named counterpart, and both are accessible through [templating](../reference/templating.md#dynamic-tags). 17 | 18 | [^1]: Access to EC2 instance tags can be granted directly through the AWS Console or by using the CLI as documented [here](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#allow-access-to-tags-in-IMDS) 19 | -------------------------------------------------------------------------------- /docs/configure/iam.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Understand what Identity Access Management (IAM) permissions are needed for dns53 to run" 3 | icon: material/shield-lock-outline 4 | --- 5 | 6 | # IAM Permissions 7 | 8 | Access to Route53 and EC2 is required for `dns53` to work. Your IAM persona must have the following permissions granted: 9 | 10 | ```json 11 | { 12 | "Version": "2012-10-17", 13 | "Statement": [ 14 | { 15 | "Effect": "Allow", 16 | "Action": [ 17 | "route53:AssociateVPCWithHostedZone", 18 | "route53:ChangeResourceRecordSets", 19 | "route53:DeleteHostedZone", 20 | "route53:DisassociateVPCFromHostedZone", 21 | "route53:GetHostedZone" 22 | ], 23 | "Resource": "arn:aws:route53:::hostedzone/*" 24 | }, 25 | { 26 | "Effect": "Allow", 27 | "Action": [ 28 | "ec2:DescribeVpcs", 29 | "route53:CreateHostedZone", 30 | "route53:ListHostedZonesByName", 31 | "route53:ListHostedZonesByVPC" 32 | ], 33 | "Resource": "*" 34 | }, 35 | { 36 | "Effect": "Allow", 37 | "Action": ["ec2:ModifyInstanceMetadataOptions"], 38 | "Resource": "arn:aws:ec2:::instance/*" // (1)! 39 | } 40 | ] 41 | } 42 | ``` 43 | 44 | 1. Don't forget to replace the `` and `` placeholders with your specific AWS details, e.g. `arn:aws:ec2:eu-west-2:112233445566:instance/*`. You could also lock it down to a specific EC2 instance if you wanted :lock: 45 | 46 | !!! warning "Aim for Least Privilege :lock:" 47 | 48 | It would be best if you fine-tuned this policy further to restrict access and adopt the mantra of "**least privilege**". You accept this policy at your own risk 49 | -------------------------------------------------------------------------------- /docs/configure/list-tags.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Generate a cheat sheet on how to include EC2 instance tags within your custom domain name" 3 | icon: material/tag-search-outline 4 | --- 5 | 6 | # List Available EC2 Instance Tags 7 | 8 | To generate a cheat sheet for including EC2 instance tags within a custom domain using Go templating, run the following command: 9 | 10 | ```sh 11 | dns tags 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/configure/tracing-requests.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Launch a reverse proxy for tracing requests in to your EC2" 3 | icon: material/shoe-print 4 | status: new 5 | --- 6 | 7 | # Tracing Requests with a Reverse Proxy 8 | 9 | To trace requests sent to your broadcasted EC2, `dns53` comes bundled with an internal reverse proxy. To enable proxying: 10 | 11 | ```{ .sh .no-select } 12 | dns53 --proxy 13 | ``` 14 | 15 | Once enabled, set the required environment variables to trace both `HTTP` and `HTTPS` requests. It is advised not to proxy any requests to IMDS on your EC2. 16 | 17 | ```{ .sh .no-select } 18 | export HTTP_PROXY=http://localhost:10080 19 | export HTTPS_PROXY=http://localhost:10080 20 | export NO_PROXY=169.254.169.254 21 | ``` 22 | 23 | ```{ .sh .no-select } 24 | curl http://httpbin.org/headers 25 | ``` 26 | 27 | ```{ .sh .no-select } 28 | curl https://httpbin.org/ip -k 29 | ``` 30 | 31 | If you do not wish to set any of these environment variables, your preferred CLI tool should support request proxying using a dedicated flag. For `curl`, that is `-x`. 32 | 33 | ## Changing the proxy port 34 | 35 | Feel free to change the default proxy port of `:10080` by using the `proxy-port` flag: 36 | 37 | ```{ .sh .no-select } 38 | dns53 --proxy --proxy-port 10888 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # DNS 53 2 | 3 | Dynamic DNS within Amazon Route 53. Expose your EC2 quickly, easily, and privately within a Route 53 Private Hosted Zone (PHZ). 4 | 5 | Easily collaborate with a colleague by exposing your EC2 within a team VPC. You could even hook up a locally running application to a local k3d cluster using an ExternalName service during development. Once your EC2 is exposed, control how it is accessed through your EC2 security groups. 6 | 7 |
8 | 12 |
13 | -------------------------------------------------------------------------------- /docs/install/binary.md: -------------------------------------------------------------------------------- 1 | --- 2 | icon: material/package-variant-closed 3 | --- 4 | 5 | # Installing the Binary 6 | 7 | You can use various package managers to install the `dns53` binary. Take your pick. 8 | 9 | ## Package Managers 10 | 11 | ### Homebrew 12 | 13 | To use [Homebrew](https://brew.sh/): 14 | 15 | ```sh 16 | brew install purpleclay/tap/dns53 17 | ``` 18 | 19 | ### Scoop 20 | 21 | To use [Scoop](https://scoop.sh/): 22 | 23 | ```sh 24 | scoop bucket add purpleclay https://github.com/purpleclay/scoop-bucket.git 25 | scoop install dns53 26 | ``` 27 | 28 | ### Apt 29 | 30 | To install using the [apt](https://ubuntu.com/server/docs/package-management) package manager: 31 | 32 | ```sh 33 | echo 'deb [trusted=yes] https://fury.purpleclay.dev/apt/ /' | sudo tee /etc/apt/sources.list.d/purpleclay.list 34 | sudo apt update 35 | sudo apt install -y dns53 36 | ``` 37 | 38 | You may need to install the `ca-certificates` package if you encounter [trust issues](https://gemfury.com/help/could-not-verify-ssl-certificate/) with regard to the Gemfury certificate: 39 | 40 | ```sh 41 | sudo apt update && sudo apt install -y ca-certificates 42 | ``` 43 | 44 | ### Yum 45 | 46 | To install using the yum package manager: 47 | 48 | ```sh 49 | echo '[purpleclay] 50 | name=purpleclay 51 | baseurl=https://fury.purpleclay.dev/yum/ 52 | enabled=1 53 | gpgcheck=0' | sudo tee /etc/yum.repos.d/purpleclay.repo 54 | sudo yum install -y dns53 55 | ``` 56 | 57 | ### Aur 58 | 59 | To install from the [aur](https://archlinux.org/) using [yay](https://github.com/Jguer/yay): 60 | 61 | ```sh 62 | yay -S dns53-bin 63 | ``` 64 | 65 | ### Linux Packages 66 | 67 | Download and manually install one of the `.deb`, `.rpm` or `.apk` packages from the [Releases](https://github.com/purpleclay/dns53/releases) page. 68 | 69 | === "Apt" 70 | 71 | ```sh 72 | sudo apt install dns53_*.deb 73 | ``` 74 | 75 | === "Yum" 76 | 77 | ```sh 78 | sudo yum localinstall dns53_*.rpm 79 | ``` 80 | 81 | === "Apk" 82 | 83 | ```sh 84 | sudo apk add --no-cache --allow-untrusted dns53_*.apk 85 | ``` 86 | 87 | ### Go Install 88 | 89 | ```sh 90 | go install github.com/purpleclay/dns53@latest 91 | ``` 92 | 93 | ### Bash Script 94 | 95 | To install the latest version using a bash script: 96 | 97 | ```sh 98 | curl https://raw.githubusercontent.com/purpleclay/dns53/main/scripts/install | bash 99 | ``` 100 | 101 | Download a specific version using the `-v` flag. The script uses `sudo` by default but can be disabled through the `--no-sudo` flag. 102 | 103 | ```sh 104 | curl https://raw.githubusercontent.com/purpleclay/dns53/main/scripts/install | bash -s -- -v v0.1.0 --no-sudo 105 | ``` 106 | 107 | ## Manual Download of Binary 108 | 109 | Head over to the [Releases](https://github.com/purpleclay/dns53/releases) page on GitHub and download any release artefact. Unpack the `dns53` binary and add it to your `PATH`. 110 | 111 | ## Verifying a Binary with Cosign 112 | 113 | All binaries can be verified using [cosign](https://github.com/sigstore/cosign). 114 | 115 | 1. Download the checksum files that need to be verified: 116 | 117 | ```sh 118 | curl -sL https://github.com/purpleclay/dns53/releases/download/v0.8.0/checksums.txt -O 119 | curl -sL https://github.com/purpleclay/dns53/releases/download/v0.8.0/checksums.txt.sig -O 120 | curl -sL https://github.com/purpleclay/dns53/releases/download/v0.8.0/checksums.txt.pem -O 121 | ``` 122 | 123 | 1. Verify the signature of the checksum file: 124 | 125 | ```sh 126 | cosign verify-blob --cert checksums.txt.pem --signature checksums.txt.sig checksums.txt 127 | ``` 128 | 129 | 1. Download any release artefact and verify its SHA256 signature matches the entry within the checksum file: 130 | 131 | ```sh 132 | sha256sum --ignore-missing -c checksums.txt 133 | ``` 134 | -------------------------------------------------------------------------------- /docs/install/source.md: -------------------------------------------------------------------------------- 1 | --- 2 | icon: material/file-code-outline 3 | --- 4 | 5 | # Compiling from Source 6 | 7 | Download both [Go 1.21+](https://go.dev/doc/install) and [go-task](https://taskfile.dev/#/installation). Then clone the code from GitHub: 8 | 9 | ```sh 10 | git clone https://github.com/purpleclay/dns53.git 11 | cd dns53 12 | ``` 13 | 14 | Build: 15 | 16 | ```sh 17 | task 18 | ``` 19 | 20 | And check that everything works: 21 | 22 | ```sh 23 | ./dns53 version 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/license.md: -------------------------------------------------------------------------------- 1 | --- 2 | icon: material/file-key-outline 3 | social: 4 | cards: false 5 | --- 6 | 7 | MIT License 8 | 9 | Copyright (c) 2022 - 2023 Purple Clay 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | SOFTWARE. 28 | -------------------------------------------------------------------------------- /docs/overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block extrahead %} 4 | 8 | {% endblock %} 9 | 10 | {% block announce %} 11 | For updates follow @purpleclaydev on 12 | 13 | 16 | Twitter 17 | 18 | and 19 | 20 | 21 | {% include ".icons/fontawesome/brands/mastodon.svg" %} 22 | 23 | Fosstodon 24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /docs/reference/cli/dns53-imds.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Learn how to use the DNS 53 command line to toggle IMDS features" 3 | icon: material/console 4 | social: 5 | cards: false 6 | --- 7 | 8 | # Command Line 9 | 10 | Toggle EC2 IMDS features 11 | 12 | ## Usage 13 | 14 | ```{ .text .no-select .no-copy } 15 | dns53 imds [flags] 16 | ``` 17 | 18 | ## Flags 19 | 20 | ```{ .text .no-select .no-copy } 21 | -h, --help help for imds 22 | --instance-metadata-tags string toggle the inclusion of EC2 instance tags 23 | within IMDS (on|off) 24 | ``` 25 | 26 | ## Global Flags 27 | 28 | ```{ .text .no-select .no-copy } 29 | --profile string the AWS named profile to use when loading credentials 30 | --region string the AWS region to use when querying AWS 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/reference/cli/dns53-tags.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Learn how to use the DNS 53 command line to list available EC2 instance tags" 3 | icon: material/console 4 | social: 5 | cards: false 6 | --- 7 | 8 | # Command Line 9 | 10 | Lists all available EC2 instance tags and how to use them with Go templating 11 | 12 | ## Usage 13 | 14 | ```{ .text .no-select .no-copy } 15 | dns53 tags [flags] 16 | ``` 17 | 18 | ## Flags 19 | 20 | ```{ .text .no-select .no-copy } 21 | -h, --help help for tags 22 | ``` 23 | 24 | ## Global Flags 25 | 26 | ```{ .text .no-select .no-copy } 27 | --profile string the AWS named profile to use when loading credentials 28 | --region string the AWS region to use when querying AWS 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/reference/cli/dns53.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Learn how to use the DNS 53 command line for privately broadcasting your EC2 instance" 3 | icon: material/console 4 | social: 5 | cards: false 6 | status: new 7 | --- 8 | 9 | # Command Line 10 | 11 | Dynamic DNS within Amazon Route 53. Expose your EC2 quickly, easily, and privately within a Route 53 Private Hosted Zone (PHZ). 12 | 13 | Your EC2 will be exposed through a dynamically generated resource record that will automatically be deleted when dns53 exits. Let dns53 name your resource record for you, or customise it to your needs. 14 | 15 | ## Usage 16 | 17 | ```{ .text .no-select .no-copy } 18 | dns53 [flags] 19 | dns53 [command] 20 | ``` 21 | 22 | ## Flags 23 | 24 | ```{ .text .no-select .no-copy } 25 | --auto-attach automatically create and attach a record set to a 26 | default private hosted zone 27 | --domain-name string assign a custom domain name when generating a record 28 | set 29 | -h, --help help for dns53 30 | --phz-id string an ID of a Route53 private hosted zone to use when 31 | generating a record set 32 | --profile string the AWS named profile to use when loading credentials 33 | --proxy enable a reverse proxy for tracing requests to this 34 | ec2 35 | --proxy-port int the port assigned to the proxy when enabled 36 | (default 10080) 37 | --region string the AWS region to use when querying AWS 38 | ``` 39 | 40 | ## Commands 41 | 42 | ```{ .text .no-select .no-copy } 43 | completion Generate the autocompletion script for the specified shell 44 | help Help about any command 45 | imds Toggle EC2 IMDS features 46 | tags Lists all available EC2 instance tags and how to use them with Go 47 | templating 48 | version Print build time version information 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/reference/templating.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Use the awesome power of Go templating to customise the domain name associated with your EC2 instance" 3 | icon: material/application-cog-outline 4 | --- 5 | 6 | # Go Templates 7 | 8 | Full support for Go [templates](https://pkg.go.dev/text/template) through a series of predefined named fields allows `dns53`` to support a dynamic configuration where needed. 9 | 10 | !!! info "Table Key" 11 | 12 | This is a living table and will change as new features are released. 13 | 14 | - :material-pencil-plus-outline:: the metadata was formatted to ensure it is URL compliant 15 | - :material-tag-outline:: the metadata was retrieved from EC2 instance tags; this feature must be [enabled](../configure/exposing-tags.md) 16 | 17 | ## Named Fields 18 | 19 | The following named fields directly access metadata about your EC2 from the Instance Metadata Service (IMDS). 20 | 21 | | Named Field | Description | Example | 22 | | ---------------------------------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------ | 23 | | `{{.IPv4}}` | the private IPv4 address of the EC2 instance | `10-0-1-182` :material-pencil-plus-outline:{title="formatted from 10.0.1.182"} | 24 | | `{{.Region}}` | the region of the EC2 instance | `eu-west-2` | 25 | | `{{.VPC}}` | the VPC ID of where the EC2 instance was launched | `vpc-016d173db537793d1` | 26 | | `{{.AZ}}` | the availability zone (AZ) of the EC2 instance | `eu-west-2a` | 27 | | `{{.InstanceID}}` | the unique ID of the EC2 instance | `i-03e092f544905abb2` | 28 | | `{{.Name}}` :material-tag-outline:{title="retrieved from EC2 instance tags"} | a name assigned to the EC2 instance | `dev-ec2` :material-pencil-plus-outline:{title="formatted from Dev EC2"} | 29 | 30 | ### Dynamic Tags 31 | 32 | As the IMDS service exposes all EC2 instance tags (_once enabled_), you can access them in much the same way as other named fields. Internally `dns53` stores all tags within a `Tags` map and provides access to them in two ways. 33 | 34 | 1. Directly through property chaining[^1], `{{.Tags.Key}}` 35 | 1. Or by using the inbuilt Go templating `index` function, `{{index .Tags "Key"}}` 36 | 37 | | Named Field | Description | Examples | 38 | | --------------------------------------------------------------------------------------------- | -------------------------------------- | ------------- | 39 | | `{{.Tags.Ec2Role}}` :material-tag-outline:{title="retrieved from EC2 instance tags"} | the role assigned to this EC2 instance | `development` | 40 | | `{{index .Tags "ec2:role"}}` :material-tag-outline:{title="retrieved from EC2 instance tags"} | the role assigned to this EC2 instance | `development` | 41 | 42 | [^1]: Amazon's tag naming and usage [guidelines](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#tag-restrictions) permit characters not supported by Go templating for property [chaining](https://pkg.go.dev/text/template#hdr-Functions), namely `[+ - = . , _ : @]`. A best efforts approach was adopted to clean the name of the tag; for further details, please read the following [documentation](../configure/exposing-tags.md). 43 | -------------------------------------------------------------------------------- /docs/static/auto-attachment-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purpleclay/dns53/cd7cedef3b7e266ac1ed648acd8163792603c545/docs/static/auto-attachment-flow.png -------------------------------------------------------------------------------- /docs/static/dns53-auto-attach.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purpleclay/dns53/cd7cedef3b7e266ac1ed648acd8163792603c545/docs/static/dns53-auto-attach.mp4 -------------------------------------------------------------------------------- /docs/static/dns53-auto-attach.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purpleclay/dns53/cd7cedef3b7e266ac1ed648acd8163792603c545/docs/static/dns53-auto-attach.webm -------------------------------------------------------------------------------- /docs/static/dns53-phzid.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purpleclay/dns53/cd7cedef3b7e266ac1ed648acd8163792603c545/docs/static/dns53-phzid.mp4 -------------------------------------------------------------------------------- /docs/static/dns53-phzid.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purpleclay/dns53/cd7cedef3b7e266ac1ed648acd8163792603c545/docs/static/dns53-phzid.webm -------------------------------------------------------------------------------- /docs/static/dns53.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purpleclay/dns53/cd7cedef3b7e266ac1ed648acd8163792603c545/docs/static/dns53.mp4 -------------------------------------------------------------------------------- /docs/static/dns53.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purpleclay/dns53/cd7cedef3b7e266ac1ed648acd8163792603c545/docs/static/dns53.webm -------------------------------------------------------------------------------- /docs/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purpleclay/dns53/cd7cedef3b7e266ac1ed648acd8163792603c545/docs/static/favicon.ico -------------------------------------------------------------------------------- /docs/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purpleclay/dns53/cd7cedef3b7e266ac1ed648acd8163792603c545/docs/static/logo.png -------------------------------------------------------------------------------- /docs/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | .md-banner { 2 | color: var(--md-footer-fg-color--lighter); 3 | } 4 | 5 | .md-banner strong { 6 | color: var(--md-footer-fg-color); 7 | } 8 | 9 | .md-typeset .mastodon { 10 | color: #897ff8; 11 | } 12 | 13 | .md-typeset .twitter { 14 | color: #00acee; 15 | } 16 | 17 | .md-typeset a:hover>span * { 18 | filter: brightness(85%); 19 | } 20 | 21 | .new-feature { 22 | color: #006C39; 23 | } 24 | 25 | .rounded-pill { 26 | background-color: var(--md-primary-fg-color); 27 | color: var(--md-primary-bg-color); 28 | border-radius: 2em; 29 | padding: 0.4em 0.8em; 30 | text-align: center; 31 | font-size: 0.7em; 32 | font-weight: bold; 33 | } 34 | 35 | u { 36 | text-decoration: underline; 37 | text-decoration-style: dotted; 38 | text-underline-offset: 0.3em; 39 | } 40 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/purpleclay/dns53 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go-v2 v1.22.1 7 | github.com/aws/aws-sdk-go-v2/config v1.22.2 8 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.2 9 | github.com/aws/aws-sdk-go-v2/service/ec2 v1.130.0 10 | github.com/aws/aws-sdk-go-v2/service/route53 v1.34.0 11 | github.com/charmbracelet/bubbles v0.16.1 12 | github.com/charmbracelet/bubbletea v0.24.2 13 | github.com/charmbracelet/lipgloss v0.9.1 14 | github.com/gobeam/stringy v0.0.6 15 | github.com/muesli/mango-cobra v1.2.0 16 | github.com/muesli/reflow v0.3.0 17 | github.com/muesli/roff v0.1.0 18 | github.com/muesli/termenv v0.15.2 19 | github.com/olekukonko/tablewriter v0.0.5 20 | github.com/purpleclay/lipgloss-theme v0.1.0 21 | github.com/purpleclay/testcontainers-imds v0.9.0 22 | github.com/spf13/cobra v1.8.0 23 | github.com/stretchr/testify v1.8.4 24 | gopkg.in/elazarl/goproxy.v1 v1.0.0-20180725130230-947c36da3153 25 | ) 26 | 27 | require ( 28 | dario.cat/mergo v1.0.0 // indirect 29 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 30 | github.com/Microsoft/go-winio v0.6.1 // indirect 31 | github.com/Microsoft/hcsshim v0.11.1 // indirect 32 | github.com/atotto/clipboard v0.1.4 // indirect 33 | github.com/aws/aws-sdk-go-v2/credentials v1.15.1 // indirect 34 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.1 // indirect 35 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.1 // indirect 36 | github.com/aws/aws-sdk-go-v2/internal/ini v1.5.1 // indirect 37 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.1 // indirect 38 | github.com/aws/aws-sdk-go-v2/service/sso v1.17.0 // indirect 39 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.19.0 // indirect 40 | github.com/aws/aws-sdk-go-v2/service/sts v1.25.0 // indirect 41 | github.com/aws/smithy-go v1.16.0 // indirect 42 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 43 | github.com/bytedance/sonic v1.9.1 // indirect 44 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 45 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 46 | github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect 47 | github.com/containerd/containerd v1.7.7 // indirect 48 | github.com/containerd/log v0.1.0 // indirect 49 | github.com/cpuguy83/dockercfg v0.3.1 // indirect 50 | github.com/creasty/defaults v1.7.0 // indirect 51 | github.com/davecgh/go-spew v1.1.1 // indirect 52 | github.com/docker/distribution v2.8.2+incompatible // indirect 53 | github.com/docker/docker v24.0.7+incompatible // indirect 54 | github.com/docker/go-connections v0.4.0 // indirect 55 | github.com/docker/go-units v0.5.0 // indirect 56 | github.com/elazarl/goproxy v0.0.0-20231031074852-3ec07828be7a // indirect 57 | github.com/elazarl/goproxy/ext v0.0.0-20231031074852-3ec07828be7a // indirect 58 | github.com/evanphx/json-patch/v5 v5.6.0 // indirect 59 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 60 | github.com/gin-contrib/sse v0.1.0 // indirect 61 | github.com/gin-gonic/gin v1.9.1 // indirect 62 | github.com/go-ole/go-ole v1.2.6 // indirect 63 | github.com/go-playground/locales v0.14.1 // indirect 64 | github.com/go-playground/universal-translator v0.18.1 // indirect 65 | github.com/go-playground/validator/v10 v10.14.0 // indirect 66 | github.com/goccy/go-json v0.10.2 // indirect 67 | github.com/gogo/protobuf v1.3.2 // indirect 68 | github.com/golang/protobuf v1.5.3 // indirect 69 | github.com/google/uuid v1.3.1 // indirect 70 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 71 | github.com/jmespath/go-jmespath v0.4.0 // indirect 72 | github.com/json-iterator/go v1.1.12 // indirect 73 | github.com/klauspost/compress v1.16.0 // indirect 74 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 75 | github.com/leodido/go-urn v1.2.4 // indirect 76 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 77 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 78 | github.com/magiconair/properties v1.8.7 // indirect 79 | github.com/mattn/go-isatty v0.0.19 // indirect 80 | github.com/mattn/go-localereader v0.0.1 // indirect 81 | github.com/mattn/go-runewidth v0.0.15 // indirect 82 | github.com/moby/patternmatcher v0.6.0 // indirect 83 | github.com/moby/sys/sequential v0.5.0 // indirect 84 | github.com/moby/term v0.5.0 // indirect 85 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 86 | github.com/modern-go/reflect2 v1.0.2 // indirect 87 | github.com/morikuni/aec v1.0.0 // indirect 88 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect 89 | github.com/muesli/cancelreader v0.2.2 // indirect 90 | github.com/muesli/mango v0.1.0 // indirect 91 | github.com/muesli/mango-pflag v0.1.0 // indirect 92 | github.com/opencontainers/go-digest v1.0.0 // indirect 93 | github.com/opencontainers/image-spec v1.1.0-rc5 // indirect 94 | github.com/opencontainers/runc v1.1.5 // indirect 95 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 96 | github.com/pkg/errors v0.9.1 // indirect 97 | github.com/pmezard/go-difflib v1.0.0 // indirect 98 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 99 | github.com/purpleclay/imds-mock v0.3.1 // indirect 100 | github.com/rivo/uniseg v0.2.0 // indirect 101 | github.com/sahilm/fuzzy v0.1.0 // indirect 102 | github.com/shirou/gopsutil/v3 v3.23.9 // indirect 103 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 104 | github.com/sirupsen/logrus v1.9.3 // indirect 105 | github.com/spf13/pflag v1.0.5 // indirect 106 | github.com/stretchr/objx v0.5.0 // indirect 107 | github.com/testcontainers/testcontainers-go v0.26.0 // indirect 108 | github.com/tidwall/gjson v1.14.3 // indirect 109 | github.com/tidwall/match v1.1.1 // indirect 110 | github.com/tidwall/pretty v1.2.0 // indirect 111 | github.com/tklauser/go-sysconf v0.3.12 // indirect 112 | github.com/tklauser/numcpus v0.6.1 // indirect 113 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 114 | github.com/ugorji/go/codec v1.2.11 // indirect 115 | github.com/yusufpapurcu/wmi v1.2.3 // indirect 116 | go.uber.org/atomic v1.7.0 // indirect 117 | go.uber.org/multierr v1.6.0 // indirect 118 | go.uber.org/zap v1.23.0 // indirect 119 | golang.org/x/arch v0.3.0 // indirect 120 | golang.org/x/crypto v0.17.0 // indirect 121 | golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect 122 | golang.org/x/mod v0.9.0 // indirect 123 | golang.org/x/net v0.17.0 // indirect 124 | golang.org/x/sync v0.3.0 // indirect 125 | golang.org/x/sys v0.15.0 // indirect 126 | golang.org/x/term v0.15.0 // indirect 127 | golang.org/x/text v0.14.0 // indirect 128 | golang.org/x/tools v0.7.0 // indirect 129 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect 130 | google.golang.org/grpc v1.57.1 // indirect 131 | google.golang.org/protobuf v1.30.0 // indirect 132 | gopkg.in/yaml.v3 v3.0.1 // indirect 133 | ) 134 | -------------------------------------------------------------------------------- /htmltest.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 - 2023 Purple Clay 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | # in the Software without restriction, including without limitation the rights 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | IgnoreURLs: 22 | - fonts.gstatic.com 23 | - https://twitter.com/purpleclaydev 24 | IgnoreDirectoryMissingTrailingSlash: true 25 | IgnoreAltMissing: true 26 | IgnoreInternalEmptyHash: true 27 | ExternalTimeout: 60 28 | CheckDoctype: false 29 | HTTPHeaders: 30 | "Range": "bytes=0-10" 31 | "Accept": "*/*" 32 | -------------------------------------------------------------------------------- /internal/ec2/ec2.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package ec2 24 | 25 | import ( 26 | "context" 27 | 28 | "github.com/aws/aws-sdk-go-v2/aws" 29 | awsec2 "github.com/aws/aws-sdk-go-v2/service/ec2" 30 | "github.com/aws/aws-sdk-go-v2/service/ec2/types" 31 | ) 32 | 33 | // InstanceMetadataToggle allows the enabling and disabling of EC2 instance tags within IMDS 34 | type InstanceMetadataToggle string 35 | 36 | const ( 37 | InstanceMetadataToggleEnabled InstanceMetadataToggle = "enabled" 38 | InstanceMetadataToggleDisabled InstanceMetadataToggle = "disabled" 39 | ) 40 | 41 | // ClientAPI defines the API for interacting with the Amazon EC2 service 42 | type ClientAPI interface { 43 | // ModifyInstanceMetadataOptions modifies the parameters of a running EC2 instance, 44 | // by toggling the availability of EC2 instances tags within the Instance Metadata 45 | // Service (IMDS) 46 | ModifyInstanceMetadataOptions(ctx context.Context, params *awsec2.ModifyInstanceMetadataOptionsInput, optFns ...func(*awsec2.Options)) (*awsec2.ModifyInstanceMetadataOptionsOutput, error) 47 | } 48 | 49 | // Client defines the client for interacting with the Amazon EC2 service 50 | type Client struct { 51 | api ClientAPI 52 | } 53 | 54 | // NewFromAPI returns a new client from the provided EC2 API implementation 55 | func NewFromAPI(api ClientAPI) *Client { 56 | return &Client{api: api} 57 | } 58 | 59 | // ToggleInstanceMetadataTags will modify the parameters of a running EC2 instance, 60 | // by toggling the availability of EC2 instance tags within the Instance Metadata 61 | // Service. 62 | // 63 | // The equivalent operation can be achieved through the CLI using: 64 | // 65 | // aws ec2 modify-instance-metadata-options --instance-id --instance-metadata-tags enabled 66 | func (c *Client) ToggleInstanceMetadataTags(ctx context.Context, id string, toggle InstanceMetadataToggle) error { 67 | _, err := c.api.ModifyInstanceMetadataOptions(ctx, &awsec2.ModifyInstanceMetadataOptionsInput{ 68 | InstanceId: aws.String(id), 69 | InstanceMetadataTags: types.InstanceMetadataTagsState(toggle), 70 | }) 71 | 72 | return err 73 | } 74 | -------------------------------------------------------------------------------- /internal/ec2/ec2_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package ec2_test 24 | 25 | import ( 26 | "context" 27 | "testing" 28 | 29 | awsec2 "github.com/aws/aws-sdk-go-v2/service/ec2" 30 | "github.com/aws/aws-sdk-go-v2/service/ec2/types" 31 | "github.com/purpleclay/dns53/internal/ec2" 32 | "github.com/purpleclay/dns53/internal/ec2/ec2mock" 33 | "github.com/stretchr/testify/assert" 34 | "github.com/stretchr/testify/mock" 35 | ) 36 | 37 | func TestToggleInstanceMetadataTags(t *testing.T) { 38 | tests := []struct { 39 | name string 40 | instanceID string 41 | toggle ec2.InstanceMetadataToggle 42 | }{ 43 | { 44 | name: "On", 45 | instanceID: "12345", 46 | toggle: ec2.InstanceMetadataToggleEnabled, 47 | }, 48 | { 49 | name: "Off", 50 | instanceID: "12345", 51 | toggle: ec2.InstanceMetadataToggleDisabled, 52 | }, 53 | } 54 | for _, tt := range tests { 55 | t.Run(tt.name, func(t *testing.T) { 56 | api := ec2mock.New(t) 57 | api.On("ModifyInstanceMetadataOptions", 58 | mock.Anything, 59 | mock.MatchedBy(func(req *awsec2.ModifyInstanceMetadataOptionsInput) bool { 60 | return *req.InstanceId == tt.instanceID && 61 | req.InstanceMetadataTags == types.InstanceMetadataTagsState(tt.toggle) 62 | }), 63 | mock.Anything).Return(&awsec2.ModifyInstanceMetadataOptionsOutput{}, nil) 64 | 65 | client := ec2.NewFromAPI(api) 66 | err := client.ToggleInstanceMetadataTags(context.Background(), tt.instanceID, tt.toggle) 67 | 68 | assert.NoError(t, err) 69 | api.AssertExpectations(t) 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/ec2/ec2mock/mock.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package ec2mock 24 | 25 | import ( 26 | "context" 27 | "testing" 28 | 29 | "github.com/aws/aws-sdk-go-v2/service/ec2" 30 | "github.com/stretchr/testify/mock" 31 | ) 32 | 33 | type ClientAPI struct { 34 | mock.Mock 35 | } 36 | 37 | func (m *ClientAPI) ModifyInstanceMetadataOptions(ctx context.Context, params *ec2.ModifyInstanceMetadataOptionsInput, optFns ...func(*ec2.Options)) (*ec2.ModifyInstanceMetadataOptionsOutput, error) { 38 | args := m.Called(ctx, params, optFns) 39 | return args.Get(0).(*ec2.ModifyInstanceMetadataOptionsOutput), args.Error(1) 40 | } 41 | 42 | func New(tb testing.TB) *ClientAPI { 43 | tb.Helper() 44 | 45 | mock := &ClientAPI{} 46 | mock.Mock.Test(tb) 47 | 48 | tb.Cleanup(func() { 49 | mock.AssertExpectations(tb) 50 | }) 51 | 52 | return mock 53 | } 54 | -------------------------------------------------------------------------------- /internal/imds/imdsstub/stub.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package imdsstub 24 | 25 | import ( 26 | "context" 27 | "fmt" 28 | "io" 29 | "strings" 30 | "testing" 31 | 32 | "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" 33 | ) 34 | 35 | var defaultMetadata = map[string]string{ 36 | "": "local-ipv4\nmac\nplacement-region\nplacement/availability-zone\ninstance-id\ntags/instance", 37 | "local-ipv4": "10.0.1.100", 38 | "mac": "06:e5:43:29:8f:08", 39 | "network/interfaces/macs/06:e5:43:29:8f:08/vpc-id": "vpc-016d173db537793d1", 40 | "placement/region": "us-east-1", 41 | "placement/availability-zone": "us-east-1a", 42 | "instance-id": "i-0decb1524582da041", 43 | "tags/instance": "Name\nEnvironment", 44 | "tags/instance/Name": "stub-ec2", 45 | "tags/instance/Environment": "dev", 46 | } 47 | 48 | type Client struct { 49 | tb testing.TB 50 | metadata map[string]string 51 | err error 52 | } 53 | 54 | func New(tb testing.TB) *Client { 55 | tb.Helper() 56 | return &Client{tb: tb, metadata: defaultMetadata} 57 | } 58 | 59 | func NewWithoutTags(tb testing.TB) *Client { 60 | tb.Helper() 61 | 62 | // Remove all traces of tags from the default metadata 63 | noTags := defaultMetadata 64 | noTags[""] = "local-ipv4\nmac\nplacement-region\nplacement/availability-zone\ninstance-id" 65 | delete(noTags, "tags/instance") 66 | delete(noTags, "tags/instance/Name") 67 | delete(noTags, "tags/instance/Environment") 68 | 69 | return &Client{tb: tb, metadata: noTags} 70 | } 71 | 72 | func NewWithError(tb testing.TB, err error) *Client { 73 | tb.Helper() 74 | return &Client{tb: tb, err: err} 75 | } 76 | 77 | //nolint:goerr113 78 | func (c *Client) GetMetadata(_ context.Context, params *imds.GetMetadataInput, _ ...func(*imds.Options)) (*imds.GetMetadataOutput, error) { 79 | c.tb.Helper() 80 | 81 | if c.err != nil { 82 | return &imds.GetMetadataOutput{}, c.err 83 | } 84 | 85 | if category, ok := c.metadata[params.Path]; ok { 86 | return wrapOutput(category), nil 87 | } 88 | 89 | return &imds.GetMetadataOutput{}, fmt.Errorf("unexpected instance category %s", params.Path) 90 | } 91 | 92 | func wrapOutput(value string) *imds.GetMetadataOutput { 93 | return &imds.GetMetadataOutput{ 94 | Content: io.NopCloser(strings.NewReader(value)), 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /internal/imds/metadata.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package imds 24 | 25 | import ( 26 | "context" 27 | "fmt" 28 | "io" 29 | "strings" 30 | 31 | awsimds "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" 32 | ) 33 | 34 | const ( 35 | pathIPv4 = "local-ipv4" 36 | pathMacAddress = "mac" 37 | pathPlacementRegion = "placement/region" 38 | pathPlacementAZ = "placement/availability-zone" 39 | pathInstanceID = "instance-id" 40 | pathTagsInstance = "tags/instance" 41 | ) 42 | 43 | // ClientAPI defines the API for interacting with the Amazon 44 | // EC2 Instance Metadata Service (IMDS) 45 | type ClientAPI interface { 46 | // GetMetadata uses the path provided to request information from the Amazon 47 | // EC2 Instance Metadata Service 48 | GetMetadata(ctx context.Context, params *awsimds.GetMetadataInput, optFns ...func(*awsimds.Options)) (*awsimds.GetMetadataOutput, error) 49 | } 50 | 51 | // Client defines the client for interacting with the Amazon EC2 Instance 52 | // Metadata Service (IMDS) 53 | type Client struct { 54 | api ClientAPI 55 | } 56 | 57 | // Metadata contains metadata associated with an EC2 instance 58 | type Metadata struct { 59 | // IPv4 is the private IPv4 address of the launched instance 60 | IPv4 string 61 | 62 | // Region of where the EC2 instance was launched 63 | Region string 64 | 65 | // VPC ID of where the EC2 instance was launched 66 | VPC string 67 | 68 | // AZ is the availability zone where the instance was launched 69 | AZ string 70 | 71 | // InstanceID is the unique ID of this instance 72 | InstanceID string 73 | 74 | // Name associated with the EC2 instance. This will be blank unless 75 | // tags have been enabled within IMDS for this EC2 instance 76 | Name string 77 | 78 | // Tags contains a map of all tags associated with the EC2 instance 79 | Tags map[string]string 80 | } 81 | 82 | // NewFromAPI returns a new client from the provided IMDS API implementation 83 | func NewFromAPI(api ClientAPI) *Client { 84 | return &Client{api: api} 85 | } 86 | 87 | // InstanceMetadata attempts to retrieve useful metadata associated with 88 | // the current EC2 instance by querying IMDS 89 | func (c *Client) InstanceMetadata(ctx context.Context) (Metadata, error) { 90 | if err := checkRoot(ctx, c.api); err != nil { 91 | return Metadata{}, err 92 | } 93 | 94 | md := Metadata{} 95 | md.AZ, _ = get(ctx, c.api, pathPlacementAZ) 96 | md.InstanceID, _ = get(ctx, c.api, pathInstanceID) 97 | md.IPv4, _ = get(ctx, c.api, pathIPv4) 98 | md.Tags = tags(ctx, c.api) 99 | md.Region, _ = get(ctx, c.api, pathPlacementRegion) 100 | md.VPC = vpc(ctx, c.api) 101 | 102 | // Extract the name from the map if it exists 103 | md.Name = md.Tags["Name"] 104 | 105 | return md, nil 106 | } 107 | 108 | func checkRoot(ctx context.Context, api ClientAPI) error { 109 | _, err := api.GetMetadata(ctx, &awsimds.GetMetadataInput{}) 110 | return err 111 | } 112 | 113 | func get(ctx context.Context, api ClientAPI, path string) (string, error) { 114 | out, err := api.GetMetadata(ctx, &awsimds.GetMetadataInput{ 115 | Path: path, 116 | }) 117 | if err != nil { 118 | return "", err 119 | } 120 | 121 | data, err := io.ReadAll(out.Content) 122 | if err != nil { 123 | return "", err 124 | } 125 | 126 | return string(data), out.Content.Close() 127 | } 128 | 129 | func vpc(ctx context.Context, api ClientAPI) string { 130 | mac, _ := get(ctx, api, pathMacAddress) 131 | vpcID, _ := get(ctx, api, fmt.Sprintf("network/interfaces/macs/%s/vpc-id", mac)) 132 | return vpcID 133 | } 134 | 135 | func tags(ctx context.Context, api ClientAPI) map[string]string { 136 | tagPaths, err := get(ctx, api, pathTagsInstance) 137 | if err != nil { 138 | return map[string]string{} 139 | } 140 | 141 | tags := map[string]string{} 142 | for _, tagName := range strings.Split(tagPaths, "\n") { 143 | tag, _ := get(ctx, api, pathTagsInstance+"/"+tagName) 144 | tags[tagName] = tag 145 | } 146 | 147 | return tags 148 | } 149 | -------------------------------------------------------------------------------- /internal/imds/metadata_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package imds_test 24 | 25 | import ( 26 | "context" 27 | "testing" 28 | 29 | "github.com/aws/aws-sdk-go-v2/config" 30 | awsimds "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" 31 | "github.com/purpleclay/dns53/internal/imds" 32 | imdsmock "github.com/purpleclay/testcontainers-imds" 33 | "github.com/stretchr/testify/assert" 34 | "github.com/stretchr/testify/require" 35 | ) 36 | 37 | func TestIntegrationInstanceMetadataExcludeTags(t *testing.T) { 38 | CheckIntegration(t) 39 | 40 | ctx := context.Background() 41 | container := imdsmock.MustStartWith(ctx, imdsmock.Options{ 42 | ExcludeInstanceTags: true, 43 | }) 44 | defer container.Terminate(ctx) 45 | 46 | cfg, err := config.LoadDefaultConfig(ctx, config.WithEC2IMDSEndpoint(container.URL())) 47 | require.NoError(t, err) 48 | 49 | client := imds.NewFromAPI(awsimds.NewFromConfig(cfg)) 50 | metadata, err := client.InstanceMetadata(ctx) 51 | require.NoError(t, err) 52 | 53 | assert.Equal(t, imdsmock.ValueLocalIPv4, metadata.IPv4) 54 | assert.Equal(t, imdsmock.ValuePlacementRegion, metadata.Region) 55 | assert.Equal(t, imdsmock.ValueNetworkInterfaces0VPCID, metadata.VPC) 56 | assert.Equal(t, imdsmock.ValuePlacementAvailabilityZone, metadata.AZ) 57 | assert.Equal(t, imdsmock.ValueInstanceID, metadata.InstanceID) 58 | assert.Empty(t, metadata.Name) 59 | assert.Empty(t, metadata.Tags) 60 | } 61 | 62 | func TestIntegrationInstanceMetadataWithTags(t *testing.T) { 63 | CheckIntegration(t) 64 | 65 | ctx := context.Background() 66 | container := imdsmock.MustStartWith(ctx, imdsmock.Options{ 67 | InstanceTags: map[string]string{ 68 | "Name": "dns53-ec2", 69 | }, 70 | }) 71 | defer container.Terminate(ctx) 72 | 73 | cfg, err := config.LoadDefaultConfig(ctx, config.WithEC2IMDSEndpoint(container.URL())) 74 | require.NoError(t, err) 75 | 76 | client := imds.NewFromAPI(awsimds.NewFromConfig(cfg)) 77 | metadata, err := client.InstanceMetadata(ctx) 78 | require.NoError(t, err) 79 | 80 | require.Len(t, metadata.Tags, 1) 81 | assert.Equal(t, "dns53-ec2", metadata.Tags["Name"]) 82 | } 83 | 84 | func CheckIntegration(t *testing.T) { 85 | t.Helper() 86 | 87 | if testing.Short() { 88 | t.Skip("skip running of integration test") 89 | } 90 | } 91 | 92 | func TestInstanceMetadataNoEndpoint(t *testing.T) { 93 | ctx := context.Background() 94 | cfg, err := config.LoadDefaultConfig(ctx, config.WithEC2IMDSEndpoint("http://localhost/no-metadata-endpoint")) 95 | require.NoError(t, err) 96 | 97 | client := imds.NewFromAPI(awsimds.NewFromConfig(cfg)) 98 | _, err = client.InstanceMetadata(ctx) 99 | 100 | require.Error(t, err) 101 | } 102 | -------------------------------------------------------------------------------- /internal/r53/r53mock/mock.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package r53mock 24 | 25 | import ( 26 | "context" 27 | "testing" 28 | 29 | "github.com/aws/aws-sdk-go-v2/service/route53" 30 | "github.com/stretchr/testify/mock" 31 | ) 32 | 33 | type ClientAPI struct { 34 | mock.Mock 35 | } 36 | 37 | func (m *ClientAPI) CreateHostedZone(ctx context.Context, params *route53.CreateHostedZoneInput, optFns ...func(*route53.Options)) (*route53.CreateHostedZoneOutput, error) { 38 | args := m.Called(ctx, params, optFns) 39 | return args.Get(0).(*route53.CreateHostedZoneOutput), args.Error(1) 40 | } 41 | 42 | func (m *ClientAPI) DeleteHostedZone(ctx context.Context, params *route53.DeleteHostedZoneInput, optFns ...func(*route53.Options)) (*route53.DeleteHostedZoneOutput, error) { 43 | args := m.Called(ctx, params, optFns) 44 | return args.Get(0).(*route53.DeleteHostedZoneOutput), args.Error(1) 45 | } 46 | 47 | func (m *ClientAPI) GetHostedZone(ctx context.Context, params *route53.GetHostedZoneInput, optFns ...func(*route53.Options)) (*route53.GetHostedZoneOutput, error) { 48 | args := m.Called(ctx, params, optFns) 49 | return args.Get(0).(*route53.GetHostedZoneOutput), args.Error(1) 50 | } 51 | 52 | func (m *ClientAPI) ListHostedZonesByVPC(ctx context.Context, params *route53.ListHostedZonesByVPCInput, optFns ...func(*route53.Options)) (*route53.ListHostedZonesByVPCOutput, error) { 53 | args := m.Called(ctx, params, optFns) 54 | return args.Get(0).(*route53.ListHostedZonesByVPCOutput), args.Error(1) 55 | } 56 | 57 | func (m *ClientAPI) ListHostedZonesByName(ctx context.Context, params *route53.ListHostedZonesByNameInput, optFns ...func(*route53.Options)) (*route53.ListHostedZonesByNameOutput, error) { 58 | args := m.Called(ctx, params, optFns) 59 | return args.Get(0).(*route53.ListHostedZonesByNameOutput), args.Error(1) 60 | } 61 | 62 | func (m *ClientAPI) ChangeResourceRecordSets(ctx context.Context, params *route53.ChangeResourceRecordSetsInput, optFns ...func(*route53.Options)) (*route53.ChangeResourceRecordSetsOutput, error) { 63 | args := m.Called(ctx, params, optFns) 64 | return args.Get(0).(*route53.ChangeResourceRecordSetsOutput), args.Error(1) 65 | } 66 | 67 | func (m *ClientAPI) AssociateVPCWithHostedZone(ctx context.Context, params *route53.AssociateVPCWithHostedZoneInput, optFns ...func(*route53.Options)) (*route53.AssociateVPCWithHostedZoneOutput, error) { 68 | args := m.Called(ctx, params, optFns) 69 | return args.Get(0).(*route53.AssociateVPCWithHostedZoneOutput), args.Error(1) 70 | } 71 | 72 | func (m *ClientAPI) DisassociateVPCFromHostedZone(ctx context.Context, params *route53.DisassociateVPCFromHostedZoneInput, optFns ...func(*route53.Options)) (*route53.DisassociateVPCFromHostedZoneOutput, error) { 73 | args := m.Called(ctx, params, optFns) 74 | return args.Get(0).(*route53.DisassociateVPCFromHostedZoneOutput), args.Error(1) 75 | } 76 | 77 | func New(tb testing.TB) *ClientAPI { 78 | tb.Helper() 79 | 80 | mock := &ClientAPI{} 81 | mock.Mock.Test(tb) 82 | 83 | tb.Cleanup(func() { 84 | mock.AssertExpectations(tb) 85 | }) 86 | 87 | return mock 88 | } 89 | -------------------------------------------------------------------------------- /internal/tui/component/errorpanel.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package component 24 | 25 | import ( 26 | "strings" 27 | 28 | tea "github.com/charmbracelet/bubbletea" 29 | "github.com/charmbracelet/lipgloss" 30 | "github.com/muesli/reflow/wordwrap" 31 | "github.com/purpleclay/lipgloss-theme" 32 | ) 33 | 34 | var ( 35 | label = lipgloss.NewStyle(). 36 | Background(lipgloss.Color("#a61414")). 37 | Padding(0, 2). 38 | Bold(true). 39 | Render("Error") 40 | 41 | borderLeft = lipgloss.NewStyle(). 42 | Border(lipgloss.NormalBorder(), false, false, false, true). 43 | BorderForeground(theme.S600) 44 | 45 | marginLeft = lipgloss.NewStyle().MarginLeft(1) 46 | panelMargin = lipgloss.NewStyle().Margin(0, 0, 1, 1) 47 | ) 48 | 49 | type ErrorPanel struct { 50 | reason string 51 | cause string 52 | width int 53 | } 54 | 55 | func NewErrorPanel() *ErrorPanel { 56 | return &ErrorPanel{} 57 | } 58 | 59 | func (*ErrorPanel) Init() tea.Cmd { 60 | return nil 61 | } 62 | 63 | func (m *ErrorPanel) Update(_ tea.Msg) (tea.Model, tea.Cmd) { 64 | return m, nil 65 | } 66 | 67 | func (m *ErrorPanel) View() string { 68 | var b strings.Builder 69 | 70 | reason := lipgloss.JoinHorizontal( 71 | lipgloss.Left, 72 | label, 73 | theme.B.Render(": "+m.reason), 74 | ) 75 | 76 | desc := marginLeft.Render(wordwrap.String(m.cause, m.width)) 77 | 78 | panel := lipgloss.JoinVertical( 79 | lipgloss.Top, 80 | panelMargin.Render(wordwrap.String(reason, m.width)), 81 | desc, 82 | ) 83 | 84 | b.WriteString(borderLeft.Render(panel)) 85 | return b.String() 86 | } 87 | 88 | func (m *ErrorPanel) RaiseError(reason string, cause error) *ErrorPanel { 89 | m.reason = reason 90 | if cause != nil { 91 | m.cause = cause.Error() 92 | } 93 | 94 | return m 95 | } 96 | 97 | func (m *ErrorPanel) Resize(width, _ int) Model { 98 | // Restrict the error panel to be 3/4 the width of the containing component 99 | m.width = int(float32(width) * 0.75) 100 | return m 101 | } 102 | 103 | func (m *ErrorPanel) Width() int { 104 | return m.width 105 | } 106 | 107 | func (m *ErrorPanel) Height() int { 108 | return lipgloss.Height(m.View()) 109 | } 110 | -------------------------------------------------------------------------------- /internal/tui/component/filteredlist.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package component 24 | 25 | import ( 26 | "github.com/charmbracelet/bubbles/list" 27 | "github.com/charmbracelet/lipgloss" 28 | "github.com/purpleclay/dns53/internal/tui/keymap" 29 | "github.com/purpleclay/lipgloss-theme" 30 | ) 31 | 32 | func NewFilteredList(items []list.Item, width, height int) list.Model { 33 | delegate := list.NewDefaultDelegate() 34 | 35 | // Override the colours within the existing styles 36 | delegate.Styles.SelectedTitle = delegate.Styles.SelectedTitle. 37 | BorderForeground(theme.S100). 38 | Foreground(theme.S100). 39 | Bold(true) 40 | 41 | delegate.Styles.SelectedDesc = delegate.Styles.SelectedDesc. 42 | BorderForeground(theme.S100). 43 | Foreground(theme.S50). 44 | Faint(true) 45 | 46 | delegate.Styles.FilterMatch = lipgloss.NewStyle(). 47 | Underline(true). 48 | Bold(true) 49 | 50 | filteredList := list.New(items, delegate, width, height) 51 | 52 | // Override the colours within the existing styles 53 | filteredList.Styles.FilterPrompt = filteredList.Styles.FilterPrompt. 54 | Foreground(theme.S100) 55 | 56 | filteredList.Styles.FilterCursor = filteredList.Styles.FilterCursor. 57 | Foreground(theme.S100) 58 | 59 | filteredList.SetShowTitle(false) 60 | filteredList.SetShowHelp(false) 61 | filteredList.DisableQuitKeybindings() 62 | 63 | // Override key bindings to force expected behaviour 64 | filteredList.KeyMap.GoToEnd.SetEnabled(false) 65 | filteredList.KeyMap.GoToStart.SetEnabled(false) 66 | 67 | filteredList.KeyMap.CursorUp = keymap.Up 68 | filteredList.KeyMap.CursorDown = keymap.Down 69 | filteredList.KeyMap.NextPage = keymap.Right 70 | filteredList.KeyMap.PrevPage = keymap.Left 71 | 72 | return filteredList 73 | } 74 | -------------------------------------------------------------------------------- /internal/tui/component/footer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package component 24 | 25 | import ( 26 | "strings" 27 | 28 | "github.com/charmbracelet/bubbles/help" 29 | tea "github.com/charmbracelet/bubbletea" 30 | "github.com/charmbracelet/lipgloss" 31 | "github.com/purpleclay/lipgloss-theme" 32 | ) 33 | 34 | var ( 35 | faint = lipgloss.NewStyle().Faint(true) 36 | borderTop = lipgloss.NewStyle(). 37 | Border(lipgloss.NormalBorder(), true, false, false, false). 38 | BorderForeground(theme.S600) 39 | ) 40 | 41 | type Footer struct { 42 | help help.Model 43 | keymap help.KeyMap 44 | width int 45 | } 46 | 47 | func NewFooter(keymap help.KeyMap) *Footer { 48 | help := help.New() 49 | help.Styles.ShortSeparator = lipgloss.NewStyle().Foreground(theme.S100) 50 | help.Styles.ShortKey = lipgloss.NewStyle() 51 | help.Styles.ShortDesc = faint.Copy() 52 | 53 | return &Footer{ 54 | help: help, 55 | keymap: keymap, 56 | } 57 | } 58 | 59 | func (*Footer) Init() tea.Cmd { 60 | return nil 61 | } 62 | 63 | func (m *Footer) Update(_ tea.Msg) (tea.Model, tea.Cmd) { 64 | return m, nil 65 | } 66 | 67 | func (m *Footer) View() string { 68 | var b strings.Builder 69 | panel := lipgloss.JoinVertical(lipgloss.Top, m.help.View(m.keymap)) 70 | 71 | b.WriteString(borderTop.Width(m.width).Render(panel)) 72 | return b.String() 73 | } 74 | 75 | func (m *Footer) Resize(width, _ int) Model { 76 | m.width = width 77 | return m 78 | } 79 | 80 | func (m *Footer) Width() int { 81 | return m.width 82 | } 83 | 84 | func (m *Footer) Height() int { 85 | return lipgloss.Height(m.View()) 86 | } 87 | 88 | func (m *Footer) SetKeyMap(keymap help.KeyMap) *Footer { 89 | m.keymap = keymap 90 | return m 91 | } 92 | -------------------------------------------------------------------------------- /internal/tui/component/header.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package component 24 | 25 | import ( 26 | "strings" 27 | 28 | tea "github.com/charmbracelet/bubbletea" 29 | "github.com/charmbracelet/lipgloss" 30 | "github.com/purpleclay/lipgloss-theme" 31 | ) 32 | 33 | var borderBottom = lipgloss.NewStyle(). 34 | Border(lipgloss.NormalBorder(), false, false, true, false). 35 | BorderForeground(theme.S600). 36 | MarginBottom(1) 37 | 38 | type Header struct { 39 | name string 40 | version string 41 | description string 42 | width int 43 | } 44 | 45 | func NewHeader(name, version, description string) *Header { 46 | return &Header{ 47 | name: name, 48 | description: description, 49 | version: version, 50 | } 51 | } 52 | 53 | func (*Header) Init() tea.Cmd { 54 | return nil 55 | } 56 | 57 | func (m *Header) Update(_ tea.Msg) (tea.Model, tea.Cmd) { 58 | return m, nil 59 | } 60 | 61 | func (m *Header) View() string { 62 | var b strings.Builder 63 | 64 | nameVersion := lipgloss.JoinHorizontal( 65 | lipgloss.Left, 66 | theme.H2.Render(m.name), 67 | theme.H4.Render(m.version), 68 | ) 69 | 70 | banner := lipgloss.JoinVertical( 71 | lipgloss.Top, 72 | nameVersion, 73 | "", 74 | faint.Render(m.description), 75 | ) 76 | 77 | b.WriteString(borderBottom.Width(m.width).Render(banner)) 78 | return b.String() 79 | } 80 | 81 | func (m *Header) Resize(width, _ int) Model { 82 | m.width = width 83 | return m 84 | } 85 | 86 | func (m *Header) Width() int { 87 | return m.width 88 | } 89 | 90 | func (m *Header) Height() int { 91 | return lipgloss.Height(m.View()) 92 | } 93 | -------------------------------------------------------------------------------- /internal/tui/component/model.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package component 24 | 25 | import tea "github.com/charmbracelet/bubbletea" 26 | 27 | type Model interface { 28 | tea.Model 29 | 30 | Resize(width, height int) Model 31 | Width() int 32 | Height() int 33 | } 34 | -------------------------------------------------------------------------------- /internal/tui/component/tracer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package component 24 | 25 | import ( 26 | "context" 27 | "errors" 28 | "fmt" 29 | "net/http" 30 | "strconv" 31 | "strings" 32 | "time" 33 | 34 | "github.com/charmbracelet/bubbles/key" 35 | "github.com/charmbracelet/bubbles/viewport" 36 | tea "github.com/charmbracelet/bubbletea" 37 | "github.com/charmbracelet/lipgloss" 38 | "github.com/purpleclay/dns53/internal/tui/keymap" 39 | "github.com/purpleclay/dns53/internal/tui/message" 40 | theme "github.com/purpleclay/lipgloss-theme" 41 | "gopkg.in/elazarl/goproxy.v1" 42 | ) 43 | 44 | var ( 45 | traceBanner = faint.Copy().Margin(1, 0, 1, 0) 46 | traceSeparator = faint.Render("-") 47 | ) 48 | 49 | var LogConnect goproxy.FuncHttpsHandler = func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) { 50 | // Enable MITM (man in the middle) to capture the response output from HTTPS requests 51 | return goproxy.MitmConnect, host 52 | } 53 | 54 | type traceRequestMsg struct { 55 | method string 56 | path string 57 | statusCode int 58 | } 59 | 60 | func (t traceRequestMsg) String() string { 61 | statusCode := strconv.Itoa(t.statusCode) 62 | var status string 63 | if statusCode[0] == '2' { 64 | status = theme.H6.Render(statusCode) 65 | } else if statusCode[0] == '5' { 66 | status = theme.H1.Render(statusCode) 67 | } else { 68 | status = theme.H3.Render(statusCode) 69 | } 70 | 71 | return fmt.Sprintf("%s %s %-7s %s\n", status, traceSeparator, t.method, t.path) 72 | } 73 | 74 | type RequestTracer struct { 75 | proxyPort int 76 | server *http.Server 77 | traces chan traceRequestMsg 78 | viewport viewport.Model 79 | tracingOn string 80 | trace strings.Builder 81 | } 82 | 83 | func NewRequestTracer(port int) *RequestTracer { 84 | return &RequestTracer{ 85 | proxyPort: port, 86 | traces: make(chan traceRequestMsg), 87 | tracingOn: traceBanner.Render(fmt.Sprintf("proxing on :%d", port)), 88 | viewport: viewport.New(0, 0), 89 | } 90 | } 91 | 92 | func (m *RequestTracer) Init() tea.Cmd { 93 | return tea.Batch( 94 | func() tea.Msg { 95 | proxy := goproxy.NewProxyHttpServer() 96 | proxy.OnRequest().HandleConnect(LogConnect) 97 | proxy.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { 98 | m.traces <- traceRequestMsg{ 99 | method: resp.Request.Method, 100 | path: resp.Request.URL.Path, 101 | statusCode: resp.StatusCode, 102 | } 103 | return resp 104 | }) 105 | m.server = &http.Server{ 106 | Addr: ":" + strconv.Itoa(m.proxyPort), 107 | Handler: proxy, 108 | ReadHeaderTimeout: 3 * time.Second, 109 | } 110 | if err := m.server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { 111 | return message.ErrorMsg{ 112 | Reason: "failed to start reverse proxy for tracing requests", 113 | Cause: err, 114 | } 115 | } 116 | 117 | return nil 118 | }, 119 | m.waitForTrace()) 120 | } 121 | 122 | func (m *RequestTracer) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 123 | var ( 124 | cmd tea.Cmd 125 | cmds []tea.Cmd 126 | ) 127 | 128 | switch msg := msg.(type) { 129 | case traceRequestMsg: 130 | m.trace.WriteString(msg.String()) 131 | m.viewport.GotoBottom() 132 | cmds = append(cmds, m.waitForTrace()) 133 | case tea.KeyMsg: 134 | if key.Matches(msg, keymap.Quit) { 135 | if m.server != nil { 136 | m.server.Shutdown(context.Background()) 137 | } 138 | 139 | close(m.traces) 140 | } 141 | } 142 | 143 | m.viewport, cmd = m.viewport.Update(msg) 144 | cmds = append(cmds, cmd) 145 | 146 | return m, tea.Batch(cmds...) 147 | } 148 | 149 | func (m *RequestTracer) View() string { 150 | m.viewport.SetContent(m.trace.String()) 151 | 152 | return lipgloss.JoinVertical( 153 | lipgloss.Top, 154 | m.tracingOn, 155 | m.viewport.View(), 156 | ) 157 | } 158 | 159 | func (m *RequestTracer) Resize(width, height int) Model { 160 | m.viewport.Width = width 161 | m.viewport.Height = height - lipgloss.Height(m.tracingOn) 162 | return m 163 | } 164 | 165 | func (m *RequestTracer) Width() int { 166 | return m.viewport.Width 167 | } 168 | 169 | func (m *RequestTracer) Height() int { 170 | return m.viewport.Height + lipgloss.Height(m.tracingOn) 171 | } 172 | 173 | func (m *RequestTracer) waitForTrace() tea.Cmd { 174 | return func() tea.Msg { 175 | return <-m.traces 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /internal/tui/keymap/keymap.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package keymap 24 | 25 | import "github.com/charmbracelet/bubbles/key" 26 | 27 | var ( 28 | Up = key.NewBinding( 29 | key.WithKeys("up"), 30 | key.WithHelp("↑", "up"), 31 | ) 32 | 33 | Down = key.NewBinding( 34 | key.WithKeys("down"), 35 | key.WithHelp("↓", "down"), 36 | ) 37 | 38 | UpDown = key.NewBinding( 39 | key.WithKeys("up", "down"), 40 | key.WithHelp("↑↓", "up/down"), 41 | ) 42 | 43 | Left = key.NewBinding( 44 | key.WithKeys("left"), 45 | key.WithHelp("←", "prev"), 46 | ) 47 | 48 | Right = key.NewBinding( 49 | key.WithKeys("right"), 50 | key.WithHelp("→", "next"), 51 | ) 52 | 53 | LeftRight = key.NewBinding( 54 | key.WithKeys("left", "right"), 55 | key.WithHelp("← →", "prev/next page"), 56 | ) 57 | 58 | Enter = key.NewBinding( 59 | key.WithKeys("enter"), 60 | key.WithHelp("↲", "select"), 61 | ) 62 | 63 | ForwardSlash = key.NewBinding( 64 | key.WithKeys("/"), 65 | key.WithHelp("/", "filter"), 66 | ) 67 | 68 | Escape = key.NewBinding( 69 | key.WithKeys("esc"), 70 | key.WithHelp("esc", "back"), 71 | ) 72 | 73 | Quit = key.NewBinding( 74 | key.WithKeys("ctrl+c", "q"), 75 | key.WithHelp("ctrl+c / q", "quit"), 76 | ) 77 | 78 | Copy = key.NewBinding( 79 | key.WithKeys("c"), 80 | key.WithHelp("c", "copy dns"), 81 | ) 82 | ) 83 | -------------------------------------------------------------------------------- /internal/tui/message/message.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package message 24 | 25 | import ( 26 | tea "github.com/charmbracelet/bubbletea" 27 | "github.com/purpleclay/dns53/internal/r53" 28 | ) 29 | 30 | // R53ZoneSelectedMsg signals that a R53 Private Hosted Zone has 31 | // been selected and an association can now be made between it and 32 | // the EC2 33 | type R53ZoneSelectedMsg struct { 34 | HostedZone r53.PrivateHostedZone 35 | } 36 | 37 | // ErrorMsg should be sent to notify a user of an unrecoverable error 38 | type ErrorMsg struct { 39 | Reason string 40 | Cause error 41 | } 42 | 43 | // RefreshKeymapMsg should be sent when the keymap associated with the 44 | // help view needs updating 45 | type RefreshKeymapMsg struct{} 46 | 47 | // RefreshKeyMapCmd provides a utility method that wraps the RefreshKeymapMsg 48 | // message for use within a update method 49 | func RefreshKeyMapCmd() tea.Msg { 50 | return RefreshKeymapMsg{} 51 | } 52 | 53 | // ClipboardStatusMsg should be sent to update the message within the 54 | // clipboard status panel 55 | type ClipboardStatusMsg struct { 56 | Status string 57 | } 58 | -------------------------------------------------------------------------------- /internal/tui/page/dashboard.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package page 24 | 25 | import ( 26 | "context" 27 | "fmt" 28 | "strings" 29 | "time" 30 | 31 | "github.com/charmbracelet/bubbles/key" 32 | "github.com/charmbracelet/bubbles/stopwatch" 33 | "github.com/charmbracelet/bubbles/viewport" 34 | tea "github.com/charmbracelet/bubbletea" 35 | "github.com/charmbracelet/lipgloss" 36 | "github.com/muesli/termenv" 37 | "github.com/purpleclay/dns53/internal/imds" 38 | "github.com/purpleclay/dns53/internal/r53" 39 | "github.com/purpleclay/dns53/internal/tui/component" 40 | "github.com/purpleclay/dns53/internal/tui/keymap" 41 | "github.com/purpleclay/dns53/internal/tui/message" 42 | theme "github.com/purpleclay/lipgloss-theme" 43 | ) 44 | 45 | var ( 46 | spacing = lipgloss.NewStyle().Faint(true).Render(strings.Repeat(".", 12)) 47 | faint = lipgloss.NewStyle().Faint(true) 48 | highlight = lipgloss.NewStyle().Foreground(theme.S50).Bold(true) 49 | primaryLabel = theme.H6.Copy().Padding(0, 3).MarginRight(8) 50 | secondaryLabel = theme.H2.Copy().PaddingLeft(2).Width(11) 51 | pending = lipgloss.NewStyle().Foreground(lipgloss.Color("#e68a35")).Bold(true) 52 | active = lipgloss.NewStyle().Foreground(lipgloss.Color("#26a621")).Bold(true) 53 | ) 54 | 55 | // Common layout for the dashboard 56 | const dashboardLine = "%s %s %s" 57 | 58 | type r53AssociatedMsg struct{} 59 | 60 | type DashboardOptions struct { 61 | Client *r53.Client 62 | Metadata imds.Metadata 63 | DomainName string 64 | Output *termenv.Output 65 | Proxy bool 66 | ProxyPort int 67 | } 68 | 69 | type Dashboard struct { 70 | info viewport.Model 71 | details viewport.Model 72 | options DashboardOptions 73 | domainName string 74 | clipboardStatus string 75 | clipboardTimeout stopwatch.Model 76 | selected r53.PrivateHostedZone 77 | connected bool 78 | elapsed stopwatch.Model 79 | errorPanel *component.ErrorPanel 80 | errorRaised bool 81 | requestTracer *component.RequestTracer 82 | } 83 | 84 | func NewDashboard(opts DashboardOptions) *Dashboard { 85 | info := viewport.New(0, 12) 86 | info.MouseWheelEnabled = false 87 | 88 | details := viewport.New(0, 0) 89 | details.MouseWheelEnabled = false 90 | 91 | return &Dashboard{ 92 | clipboardTimeout: stopwatch.NewWithInterval(time.Second * 2), 93 | info: info, 94 | details: details, 95 | options: opts, 96 | elapsed: stopwatch.New(), 97 | errorPanel: component.NewErrorPanel(), 98 | } 99 | } 100 | 101 | func (*Dashboard) Init() tea.Cmd { 102 | return nil 103 | } 104 | 105 | func (m *Dashboard) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 106 | var ( 107 | cmd tea.Cmd 108 | cmds []tea.Cmd 109 | ) 110 | 111 | switch msg := msg.(type) { 112 | case message.R53ZoneSelectedMsg: 113 | m.selected = msg.HostedZone 114 | m.domainName = m.resolveDomainName() 115 | 116 | cmds = append(cmds, m.initAssociation) 117 | case r53AssociatedMsg: 118 | m.connected = true 119 | cmds = append(cmds, m.elapsed.Start(), message.RefreshKeyMapCmd) 120 | 121 | if m.options.Proxy { 122 | m.requestTracer = component.NewRequestTracer(m.options.ProxyPort) 123 | m.requestTracer = m.requestTracer.Resize(m.details.Width, m.details.Height).(*component.RequestTracer) 124 | cmds = append(cmds, m.requestTracer.Init()) 125 | } 126 | case message.ErrorMsg: 127 | m.errorPanel = m.errorPanel.RaiseError(msg.Reason, msg.Cause) 128 | m.errorRaised = true 129 | case stopwatch.TickMsg: 130 | if msg.ID == m.clipboardTimeout.ID() { 131 | m.clipboardStatus = "" 132 | cmds = append(cmds, m.clipboardTimeout.Stop()) 133 | } 134 | case tea.KeyMsg: 135 | switch { 136 | case key.Matches(msg, keymap.Quit): 137 | if m.connected { 138 | record := r53.ResourceRecord{ 139 | PhzID: m.selected.ID, 140 | Name: m.domainName, 141 | Resource: m.options.Metadata.IPv4, 142 | } 143 | 144 | m.options.Client.DisassociateRecord(context.Background(), record) 145 | } 146 | case key.Matches(msg, keymap.Copy): 147 | m.options.Output.Copy(m.domainName) 148 | m.clipboardStatus = "(copied to clipboard)" 149 | 150 | // Trigger the timer to clear the clipboard message 151 | cmds = append(cmds, m.clipboardTimeout.Reset(), m.clipboardTimeout.Start()) 152 | } 153 | } 154 | 155 | if m.connected { 156 | m.elapsed, cmd = m.elapsed.Update(msg) 157 | cmds = append(cmds, cmd) 158 | 159 | if m.clipboardTimeout.Running() { 160 | m.clipboardTimeout, cmd = m.clipboardTimeout.Update(msg) 161 | cmds = append(cmds, cmd) 162 | } 163 | } 164 | 165 | if m.requestTracer != nil { 166 | model, cmd := m.requestTracer.Update(msg) 167 | m.requestTracer = model.(*component.RequestTracer) 168 | cmds = append(cmds, cmd) 169 | } 170 | 171 | return m, tea.Batch(cmds...) 172 | } 173 | 174 | func (m *Dashboard) View() string { 175 | if m.errorRaised { 176 | m.details.SetContent(m.errorPanel.View()) 177 | } 178 | 179 | phzData := lipgloss.JoinVertical( 180 | lipgloss.Top, 181 | fmt.Sprintf(dashboardLine, secondaryLabel.Render("Name:"), spacing, m.selected.Name), 182 | fmt.Sprintf(dashboardLine, secondaryLabel.Render("ID:"), spacing, m.selected.ID), 183 | ) 184 | 185 | phz := lipgloss.JoinHorizontal( 186 | lipgloss.Left, 187 | primaryLabel.Render("PHZ:"), 188 | phzData, 189 | ) 190 | 191 | ec2Data := lipgloss.JoinVertical( 192 | lipgloss.Top, 193 | fmt.Sprintf(dashboardLine, secondaryLabel.Render("IPv4:"), spacing, m.options.Metadata.IPv4), 194 | fmt.Sprintf(dashboardLine, secondaryLabel.Render("Region:"), spacing, m.options.Metadata.Region), 195 | fmt.Sprintf(dashboardLine, secondaryLabel.Render("VPC:"), spacing, m.options.Metadata.VPC), 196 | ) 197 | 198 | ec2 := lipgloss.JoinHorizontal( 199 | lipgloss.Left, 200 | primaryLabel.Render("EC2:"), 201 | ec2Data, 202 | ) 203 | 204 | status := pending.Render("pending") 205 | if m.connected { 206 | status = lipgloss.JoinHorizontal(lipgloss.Left, active.Render("active"), fmt.Sprintf(" (%s)", m.elapsed.View())) 207 | } 208 | 209 | dnsData := lipgloss.JoinVertical( 210 | lipgloss.Top, 211 | fmt.Sprintf(dashboardLine+" [A] %s", 212 | secondaryLabel.Render("Record:"), 213 | spacing, 214 | highlight.Render(m.domainName), 215 | faint.Render(m.clipboardStatus)), 216 | fmt.Sprintf(dashboardLine, secondaryLabel.Render("Status:"), spacing, status), 217 | ) 218 | 219 | dns := lipgloss.JoinHorizontal( 220 | lipgloss.Left, 221 | primaryLabel.Render("DNS:"), 222 | dnsData, 223 | ) 224 | 225 | if m.requestTracer != nil { 226 | m.details.SetContent(m.requestTracer.View()) 227 | } 228 | 229 | dashboard := lipgloss.JoinVertical( 230 | lipgloss.Top, 231 | lipgloss.NewStyle().MarginBottom(2).Render(phz), 232 | lipgloss.NewStyle().MarginBottom(2).Render(ec2), 233 | dns, 234 | ) 235 | m.info.SetContent(dashboard) 236 | 237 | return lipgloss.JoinVertical(lipgloss.Top, m.info.View(), m.details.View()) 238 | } 239 | 240 | func (m *Dashboard) ShortHelp() []key.Binding { 241 | bindings := []key.Binding{keymap.Quit} 242 | if m.connected { 243 | bindings = append(bindings, keymap.Copy) 244 | } 245 | 246 | return bindings 247 | } 248 | 249 | func (*Dashboard) FullHelp() [][]key.Binding { 250 | return [][]key.Binding{} 251 | } 252 | 253 | func (m *Dashboard) Resize(width, height int) Model { 254 | m.info.Width = width 255 | m.details.Width = width 256 | m.details.Height = height - m.info.Height 257 | 258 | if m.requestTracer != nil { 259 | m.requestTracer = m.requestTracer.Resize(width, m.details.Height).(*component.RequestTracer) 260 | } 261 | 262 | m.errorPanel = m.errorPanel.Resize(width, height).(*component.ErrorPanel) 263 | return m 264 | } 265 | 266 | func (m *Dashboard) Width() int { 267 | return m.info.Width 268 | } 269 | 270 | func (m *Dashboard) Height() int { 271 | return m.info.Height + m.details.Height 272 | } 273 | 274 | func (m *Dashboard) resolveDomainName() string { 275 | name := m.options.DomainName 276 | if name == "" { 277 | name = fmt.Sprintf("%s.dns53.%s", strings.ReplaceAll(m.options.Metadata.IPv4, ".", "-"), m.selected.Name) 278 | 279 | // If attaching to the dns53 domain, strip off the duplicate suffix 280 | if strings.Count(name, "dns53") > 1 { 281 | name = strings.TrimSuffix(name, ".dns53") 282 | } 283 | } else if !strings.HasSuffix(name, "."+m.selected.Name) { 284 | // Ensure root domain is appended as a suffix 285 | name = fmt.Sprintf("%s.%s", name, m.selected.Name) 286 | } 287 | 288 | return name 289 | } 290 | 291 | func (m *Dashboard) initAssociation() tea.Msg { 292 | record := r53.ResourceRecord{ 293 | PhzID: m.selected.ID, 294 | Name: m.domainName, 295 | Resource: m.options.Metadata.IPv4, 296 | } 297 | 298 | if err := m.options.Client.AssociateRecord(context.Background(), record); err != nil { 299 | return message.ErrorMsg{ 300 | Reason: fmt.Sprintf("associating EC2 with PHZ %s", m.selected.Name), 301 | Cause: err, 302 | } 303 | } 304 | 305 | return r53AssociatedMsg{} 306 | } 307 | -------------------------------------------------------------------------------- /internal/tui/page/model.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package page 24 | 25 | import ( 26 | "github.com/charmbracelet/bubbles/help" 27 | tea "github.com/charmbracelet/bubbletea" 28 | ) 29 | 30 | type Model interface { 31 | tea.Model 32 | help.KeyMap 33 | 34 | Resize(width, height int) Model 35 | Width() int 36 | Height() int 37 | } 38 | -------------------------------------------------------------------------------- /internal/tui/page/wizard.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package page 24 | 25 | import ( 26 | "context" 27 | "fmt" 28 | 29 | "github.com/charmbracelet/bubbles/key" 30 | "github.com/charmbracelet/bubbles/list" 31 | "github.com/charmbracelet/bubbles/spinner" 32 | "github.com/charmbracelet/bubbles/viewport" 33 | tea "github.com/charmbracelet/bubbletea" 34 | "github.com/charmbracelet/lipgloss" 35 | "github.com/purpleclay/dns53/internal/imds" 36 | "github.com/purpleclay/dns53/internal/r53" 37 | "github.com/purpleclay/dns53/internal/tui/component" 38 | "github.com/purpleclay/dns53/internal/tui/keymap" 39 | "github.com/purpleclay/dns53/internal/tui/message" 40 | "github.com/purpleclay/lipgloss-theme" 41 | ) 42 | 43 | type zoneSelectionMsg struct { 44 | hostedZones []r53.PrivateHostedZone 45 | } 46 | 47 | type hostedZoneItem struct { 48 | name string 49 | id string 50 | } 51 | 52 | func (i hostedZoneItem) Title() string { return i.name } 53 | func (i hostedZoneItem) Description() string { return i.id } 54 | func (i hostedZoneItem) FilterValue() string { return i.name } 55 | 56 | type WizardOptions struct { 57 | Client *r53.Client 58 | Metadata imds.Metadata 59 | HostedZoneID string 60 | DomainName string 61 | } 62 | 63 | type Wizard struct { 64 | viewport viewport.Model 65 | loading spinner.Model 66 | selection list.Model 67 | errorPanel *component.ErrorPanel 68 | errorRaised bool 69 | options WizardOptions 70 | } 71 | 72 | func NewWizard(opts WizardOptions) *Wizard { 73 | loading := spinner.New() 74 | loading.Spinner = spinner.Dot 75 | loading.Style = lipgloss.NewStyle(). 76 | Foreground(theme.S100). 77 | Bold(true) 78 | 79 | return &Wizard{ 80 | viewport: viewport.New(0, 0), 81 | loading: loading, 82 | selection: component.NewFilteredList([]list.Item{}, 40, 20), 83 | errorPanel: component.NewErrorPanel(), 84 | options: opts, 85 | } 86 | } 87 | 88 | func (m *Wizard) Init() tea.Cmd { 89 | return tea.Batch( 90 | m.viewport.Init(), 91 | m.loading.Tick, 92 | func() tea.Msg { 93 | if m.options.HostedZoneID == "" { 94 | return m.queryHostedZones() 95 | } 96 | return m.queryHostedZone() 97 | }, 98 | ) 99 | } 100 | 101 | func (m *Wizard) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 102 | var ( 103 | cmd tea.Cmd 104 | cmds []tea.Cmd 105 | ) 106 | 107 | switch msg := msg.(type) { 108 | case zoneSelectionMsg: 109 | // PHZ have been successfully retrieved. Load them into the list 110 | items := make([]list.Item, 0, len(msg.hostedZones)) 111 | for _, phz := range msg.hostedZones { 112 | items = append(items, hostedZoneItem{name: phz.Name, id: phz.ID}) 113 | } 114 | m.selection.SetItems(items) 115 | 116 | // Refresh the keymap based on the list being populated 117 | cmds = append(cmds, message.RefreshKeyMapCmd) 118 | case message.ErrorMsg: 119 | m.errorPanel = m.errorPanel.RaiseError(msg.Reason, msg.Cause) 120 | m.errorRaised = true 121 | case tea.KeyMsg: 122 | switch { 123 | case key.Matches(msg, keymap.Enter): 124 | selected := m.selection.SelectedItem().(hostedZoneItem) 125 | 126 | cmds = append(cmds, func() tea.Msg { 127 | return message.R53ZoneSelectedMsg{ 128 | HostedZone: r53.PrivateHostedZone{ID: selected.id, Name: selected.name}, 129 | } 130 | }) 131 | case key.Matches(msg, keymap.ForwardSlash): 132 | fallthrough 133 | case key.Matches(msg, keymap.Escape): 134 | // Refresh the keymap based on the list being populated 135 | cmds = append(cmds, message.RefreshKeyMapCmd) 136 | } 137 | } 138 | 139 | m.loading, cmd = m.loading.Update(msg) 140 | cmds = append(cmds, cmd) 141 | 142 | m.selection, cmd = m.selection.Update(msg) 143 | cmds = append(cmds, cmd) 144 | 145 | m.viewport, cmd = m.viewport.Update(msg) 146 | cmds = append(cmds, cmd) 147 | 148 | return m, tea.Batch(cmds...) 149 | } 150 | 151 | func (m *Wizard) View() string { 152 | var page string 153 | if len(m.selection.Items()) == 0 { 154 | zoneLabel := "Zones" 155 | if m.options.HostedZoneID != "" { 156 | zoneLabel = "Zone" 157 | } 158 | 159 | page = lipgloss.JoinHorizontal( 160 | lipgloss.Left, 161 | m.loading.View(), 162 | fmt.Sprintf(" Retrieving Private Hosted %s from AWS...", zoneLabel), 163 | ) 164 | } else { 165 | page = lipgloss.JoinVertical( 166 | lipgloss.Top, 167 | "Please select a Private Hosted Zone:", 168 | m.selection.View(), 169 | ) 170 | } 171 | 172 | var err string 173 | if m.errorRaised { 174 | err = "\n" + m.errorPanel.View() 175 | } 176 | 177 | view := lipgloss.JoinVertical(lipgloss.Top, page, err) 178 | 179 | m.viewport.SetContent(view) 180 | return m.viewport.View() 181 | } 182 | 183 | func (m *Wizard) ShortHelp() []key.Binding { 184 | kb := make([]key.Binding, 0) 185 | 186 | kb = append(kb, keymap.Quit) 187 | 188 | // Respond to the selection being populated with items 189 | if len(m.selection.Items()) > 0 { 190 | if m.selection.FilterState() == list.Filtering { 191 | kb = append(kb, keymap.Enter, keymap.Escape) 192 | } else { 193 | kb = append(kb, keymap.UpDown) 194 | 195 | if m.selection.Paginator.TotalPages > 1 { 196 | kb = append(kb, keymap.LeftRight) 197 | } 198 | 199 | kb = append(kb, keymap.Enter, keymap.ForwardSlash) 200 | } 201 | } 202 | 203 | return kb 204 | } 205 | 206 | func (*Wizard) FullHelp() [][]key.Binding { 207 | return [][]key.Binding{} 208 | } 209 | 210 | func (m *Wizard) Resize(width, height int) Model { 211 | m.viewport.Width = width 212 | m.viewport.Height = height 213 | 214 | m.errorPanel = m.errorPanel.Resize(width, height).(*component.ErrorPanel) 215 | return m 216 | } 217 | 218 | func (m *Wizard) Width() int { 219 | return m.viewport.Width 220 | } 221 | 222 | func (m *Wizard) Height() int { 223 | return m.viewport.Height 224 | } 225 | 226 | func (m *Wizard) queryHostedZones() tea.Msg { 227 | metadata := m.options.Metadata 228 | phzs, err := m.options.Client.ByVPC(context.Background(), metadata.VPC, metadata.Region) 229 | if err != nil { 230 | return message.ErrorMsg{ 231 | Reason: fmt.Sprintf("querying private hosted zones for VPC %s in region %s", metadata.VPC, metadata.Region), 232 | Cause: err, 233 | } 234 | } 235 | 236 | return zoneSelectionMsg{hostedZones: phzs} 237 | } 238 | 239 | func (m *Wizard) queryHostedZone() tea.Msg { 240 | phz, err := m.options.Client.ByID(context.Background(), m.options.HostedZoneID) 241 | if err != nil { 242 | return message.ErrorMsg{ 243 | Reason: fmt.Sprintf("querying private hosted zone %s", m.options.HostedZoneID), 244 | Cause: err, 245 | } 246 | } 247 | 248 | return message.R53ZoneSelectedMsg{HostedZone: phz} 249 | } 250 | -------------------------------------------------------------------------------- /internal/tui/ui.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package tui 24 | 25 | import ( 26 | "os" 27 | 28 | "github.com/charmbracelet/bubbles/key" 29 | tea "github.com/charmbracelet/bubbletea" 30 | "github.com/charmbracelet/lipgloss" 31 | "github.com/muesli/termenv" 32 | "github.com/purpleclay/dns53/internal/imds" 33 | "github.com/purpleclay/dns53/internal/r53" 34 | "github.com/purpleclay/dns53/internal/tui/component" 35 | "github.com/purpleclay/dns53/internal/tui/keymap" 36 | "github.com/purpleclay/dns53/internal/tui/message" 37 | "github.com/purpleclay/dns53/internal/tui/page" 38 | ) 39 | 40 | var framed = lipgloss.NewStyle().Margin(1) 41 | 42 | type pageIndex int 43 | 44 | const ( 45 | wizardPage pageIndex = iota 46 | dashboardPage 47 | ) 48 | 49 | type Options struct { 50 | About About 51 | R53Client *r53.Client 52 | EC2Metadata imds.Metadata 53 | DomainName string 54 | HostedZoneID string 55 | Proxy bool 56 | ProxyPort int 57 | } 58 | 59 | type About struct { 60 | Name string 61 | Version string 62 | ShortDescription string 63 | } 64 | 65 | type UI struct { 66 | header component.Model 67 | pages []page.Model 68 | pageIndex pageIndex 69 | footer component.Model 70 | } 71 | 72 | func New(opts Options) *UI { 73 | output := termenv.NewOutput(os.Stderr) 74 | 75 | pages := []page.Model{ 76 | page.NewWizard(page.WizardOptions{ 77 | Client: opts.R53Client, 78 | Metadata: opts.EC2Metadata, 79 | HostedZoneID: opts.HostedZoneID, 80 | DomainName: opts.DomainName, 81 | }), 82 | page.NewDashboard(page.DashboardOptions{ 83 | Client: opts.R53Client, 84 | Metadata: opts.EC2Metadata, 85 | DomainName: opts.DomainName, 86 | Proxy: opts.Proxy, 87 | ProxyPort: opts.ProxyPort, 88 | Output: output, 89 | }), 90 | } 91 | 92 | index := wizardPage 93 | 94 | return &UI{ 95 | header: component.NewHeader(opts.About.Name, opts.About.Version, opts.About.ShortDescription), 96 | pages: pages, 97 | pageIndex: index, 98 | footer: component.NewFooter(pages[index]), 99 | } 100 | } 101 | 102 | func (u *UI) Init() tea.Cmd { 103 | cmds := make([]tea.Cmd, 0) 104 | cmds = append(cmds, u.header.Init(), u.footer.Init()) 105 | 106 | for i := range u.pages { 107 | cmds = append(cmds, u.pages[i].Init()) 108 | } 109 | 110 | return tea.Batch(cmds...) 111 | } 112 | 113 | func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 114 | var ( 115 | cmd tea.Cmd 116 | cmds []tea.Cmd 117 | ) 118 | 119 | switch msg := msg.(type) { 120 | case tea.WindowSizeMsg: 121 | x, y := u.margins() 122 | 123 | // Only need to resize the width of both the header and footer 124 | u.header = u.header.Resize(msg.Width-x, u.header.Height()) 125 | u.footer = u.footer.Resize(msg.Width-x, u.footer.Height()) 126 | 127 | // Viewport need resizing in both axis within the available space 128 | pageX := msg.Width - x 129 | pageY := msg.Height - (y + u.header.Height() + u.footer.Height()) 130 | 131 | for i := range u.pages { 132 | u.pages[i] = u.pages[i].Resize(pageX, pageY) 133 | } 134 | case message.R53ZoneSelectedMsg: 135 | u.pageIndex += dashboardPage 136 | 137 | u = u.refreshFooterKeyMap() 138 | case message.RefreshKeymapMsg: 139 | u = u.refreshFooterKeyMap() 140 | case tea.KeyMsg: 141 | if key.Matches(msg, keymap.Quit) { 142 | u.pages[u.pageIndex].Update(msg) 143 | return u, tea.Quit 144 | } 145 | } 146 | 147 | var currentPage tea.Model 148 | currentPage, cmd = u.pages[u.pageIndex].Update(msg) 149 | u.pages[u.pageIndex] = currentPage.(page.Model) 150 | cmds = append(cmds, cmd) 151 | 152 | return u, tea.Batch(cmds...) 153 | } 154 | 155 | func (u *UI) View() string { 156 | view := lipgloss.JoinVertical( 157 | lipgloss.Left, 158 | u.header.View(), 159 | u.pages[u.pageIndex].View(), 160 | u.footer.View(), 161 | ) 162 | 163 | return framed.Render(view) 164 | } 165 | 166 | func (*UI) margins() (int, int) { 167 | s := framed.Copy() 168 | return s.GetHorizontalFrameSize(), s.GetVerticalFrameSize() 169 | } 170 | 171 | func (u *UI) refreshFooterKeyMap() *UI { 172 | footer := u.footer.(*component.Footer) 173 | u.footer = footer.SetKeyMap(u.pages[u.pageIndex]) 174 | return u 175 | } 176 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 - 2023 Purple Clay 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package main 24 | 25 | import ( 26 | "fmt" 27 | "os" 28 | 29 | "github.com/purpleclay/dns53/cmd" 30 | ) 31 | 32 | var ( 33 | // The current built version 34 | version = "" 35 | // The git branch associated with the current built version 36 | gitBranch = "" 37 | // The git SHA1 of the commit 38 | gitCommit = "" 39 | // The date associated with the current built version 40 | buildDate = "" 41 | ) 42 | 43 | func main() { 44 | err := cmd.New().Execute(os.Stdout, cmd.BuildDetails{ 45 | Version: version, 46 | GitBranch: gitBranch, 47 | GitCommit: gitCommit, 48 | Date: buildDate, 49 | }) 50 | if err != nil { 51 | fmt.Println(err.Error()) 52 | os.Exit(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 - 2023 Purple Clay 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | # in the Software without restriction, including without limitation the rights 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | site_name: DNS 53 22 | site_description: Dynamic DNS within Amazon Route53. Expose your EC2 quickly, easily and privately 23 | copyright: Building tools to make developers' lives easier 24 | repo_name: purpleclay/dns53 25 | repo_url: https://github.com/purpleclay/dns53 26 | # Only set during a release to ensure htmltest doesn't break due to non existent directories 27 | site_url: "" 28 | edit_uri: "" 29 | 30 | theme: 31 | name: material 32 | custom_dir: docs/overrides 33 | logo: static/logo.png 34 | favicon: static/favicon.ico 35 | palette: 36 | primary: deep purple 37 | accent: purple 38 | features: 39 | - announce.dismiss 40 | - content.code.annotate 41 | - content.code.copy 42 | - content.code.select 43 | - content.tooltips 44 | - navigation.indexes 45 | - navigation.sections 46 | - navigation.tabs 47 | - navigation.top 48 | - navigation.tracking 49 | - search.highlight 50 | - search.share 51 | - search.suggest 52 | - toc.follow 53 | icon: 54 | repo: fontawesome/brands/github 55 | font: 56 | text: Roboto 57 | code: Roboto Mono 58 | 59 | extra_css: 60 | - stylesheets/extra.css 61 | 62 | nav: 63 | - Home: index.md 64 | - Getting Started: 65 | - Broadcast your EC2: configure/broadcast-ec2.md 66 | - Custom Domain: configure/custom-domain.md 67 | - EC2 Auto Attachment: configure/auto-attachment.md 68 | - Expose EC2 Tags: configure/exposing-tags.md 69 | - List EC2 Tags: configure/list-tags.md 70 | - IAM: configure/iam.md 71 | - Tracing Requests: configure/tracing-requests.md 72 | - Installation: 73 | - Binary: install/binary.md 74 | - From Source: install/source.md 75 | - Other Bits: 76 | - License: license.md 77 | - Reference: 78 | - Templating: reference/templating.md 79 | - CLI: 80 | - dns53: reference/cli/dns53.md 81 | - dns53 imds: reference/cli/dns53-imds.md 82 | - dns53 tags: reference/cli/dns53-tags.md 83 | 84 | extra: 85 | social: 86 | - icon: fontawesome/brands/github 87 | link: https://github.com/purpleclay 88 | name: Purple Clay on GitHub 89 | - icon: fontawesome/brands/twitter 90 | link: https://twitter.com/purpleclaydev 91 | name: Purple Clay on Twitter 92 | - icon: fontawesome/brands/mastodon 93 | link: https://fosstodon.org/@purpleclaydev 94 | name: Purple Clay on Fosstodon 95 | - icon: fontawesome/brands/docker 96 | link: https://hub.docker.com/u/purpleclay 97 | name: Purple Clay on Docker Hub 98 | status: 99 | new: New Features Added 100 | deprecated: No Longer Supported 101 | 102 | plugins: 103 | - git-revision-date-localized: 104 | enabled: !ENV [CI, false] 105 | enable_creation_date: true 106 | type: timeago 107 | - git-committers: 108 | enabled: !ENV [CI, false] 109 | repository: purpleclay/nsv 110 | branch: main 111 | - minify: 112 | minify_html: !ENV [CI, false] 113 | - search 114 | - social 115 | - typeset 116 | 117 | markdown_extensions: 118 | - abbr 119 | - admonition 120 | - attr_list 121 | - def_list 122 | - footnotes 123 | - pymdownx.betterem: 124 | smart_enable: all 125 | - pymdownx.caret 126 | - pymdownx.critic 127 | - pymdownx.details 128 | - pymdownx.emoji: 129 | emoji_index: !!python/name:materialx.emoji.twemoji 130 | emoji_generator: !!python/name:materialx.emoji.to_svg 131 | - pymdownx.highlight: 132 | anchor_linenums: true 133 | line_spans: __span 134 | pygments_lang_class: true 135 | - pymdownx.inlinehilite 136 | - pymdownx.mark 137 | - pymdownx.snippets 138 | - pymdownx.superfences 139 | - pymdownx.tabbed: 140 | alternate_style: true 141 | - pymdownx.tilde 142 | - md_in_html 143 | - meta 144 | - toc: 145 | permalink: true 146 | -------------------------------------------------------------------------------- /scripts/completions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright (c) 2022 - 2023 Purple Clay 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # in the Software without restriction, including without limitation the rights 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | # Borrowed from: https://raw.githubusercontent.com/goreleaser/goreleaser/main/scripts/completions.sh 24 | set -e 25 | rm -rf completions 26 | mkdir completions 27 | 28 | # Directly invoke dns53 and generate the shell completion scripts 29 | for SH in bash zsh fish; do 30 | go run main.go completion "${SH}" > "completions/dns53.${SH}" 31 | done 32 | -------------------------------------------------------------------------------- /scripts/fury-upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2022 - 2023 Purple Clay 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # in the Software without restriction, including without limitation the rights 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | # Borrowed from: https://raw.githubusercontent.com/goreleaser/goreleaser/main/scripts/fury-upload.sh 24 | set -e 25 | if [ "${1: -4}" == ".deb" ] || [ "${1: -4}" == ".rpm" ]; then 26 | cd dist 27 | echo "uploading $1" 28 | status="$(curl -s -q -o /dev/null -w "%{http_code}" -F package="@$1" "https://${FURY_TOKEN}@push.fury.io/purpleclay/")" 29 | echo "got: $status" 30 | if [ "$status" == "200" ] || [ "$status" == "409" ]; then 31 | exit 0 32 | fi 33 | exit 1 34 | fi 35 | -------------------------------------------------------------------------------- /scripts/install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright (c) 2022 - 2023 Purple Clay 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # in the Software without restriction, including without limitation the rights 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | # Install script is heavily based on: https://github.com/Masterminds/glide.sh/blob/master/get 24 | 25 | : "${USE_SUDO:=true}" 26 | : "${INSTALL_DIR:=/usr/local/bin}" 27 | 28 | APP_NAME="dns53" 29 | HAS_CURL="$(type curl >/dev/null && echo true || echo false)" 30 | HAS_WGET="$(type wget >/dev/null && echo true || echo false)" 31 | 32 | initArch() { 33 | ARCH=$(uname -m) 34 | case $ARCH in 35 | armv5*) ARCH="arm";; 36 | armv6*) ARCH="arm";; 37 | armv7*) ARCH="arm";; 38 | aarch64) ARCH="arm64";; 39 | x86) ARCH="i386";; 40 | x86_64) ARCH="x86_64";; 41 | i686) ARCH="i386";; 42 | i386) ARCH="i386";; 43 | ppc64le) ARCH="ppc64le";; 44 | esac 45 | } 46 | 47 | initOS() { 48 | OS=$(uname|tr '[:upper:]' '[:lower:]') 49 | case "$OS" in 50 | # Minimalist GNU for Windows 51 | mingw*) OS='windows';; 52 | msys*) OS='windows';; 53 | esac 54 | } 55 | 56 | canDownload() { 57 | _supported="darwin-amd64\ndarwin-x86_64\nlinux-arm\nlinux-arm64\nlinux-arm386\nlinux-i386\nlinux-ppc64le\nlinux-x86_64\nwindows-arm\nwindows-i386\nwindows-x86_64" 58 | if ! echo "${_supported}" | grep -q "${OS}-${ARCH}"; then 59 | echo "No prebuilt binary currently exists for ${OS}-${ARCH}." 60 | exit 1 61 | fi 62 | 63 | if [ "${HAS_CURL}" != "true" ] && [ "${HAS_WGET}" != "true" ]; then 64 | echo "Either curl or wget is required to download binary. Please install and try again" 65 | exit 1 66 | fi 67 | } 68 | 69 | download() { 70 | if [ -z "$DESIRED_VERSION" ]; then 71 | if [ "${HAS_CURL}" = "true" ]; then 72 | TAG="v$(curl -s https://api.github.com/repos/purpleclay/$APP_NAME/releases/latest | grep "tag_name" | cut -d'v' -f2 | cut -d'"' -f1)" 73 | elif [ "${HAS_WGET}" = "true" ]; then 74 | TAG="v$(wget -q https://api.github.com/repos/purpleclay/$APP_NAME/releases/latest -O - 2>&1 | grep "tag_name" | cut -d'v' -f2 | cut -d'"' -f1)" 75 | fi 76 | else 77 | TAG=${DESIRED_VERSION} 78 | fi 79 | 80 | echo "Attempting to download ${APP_NAME} version ${TAG}..." 81 | 82 | PACKAGE_TYPE="tar.gz" 83 | if [ "${OS}" = "windows" ]; then 84 | PACKAGE_TYPE="zip" 85 | fi 86 | 87 | _archive="${APP_NAME}_${TAG#v}_${OS}-${ARCH}.${PACKAGE_TYPE}" 88 | 89 | DOWNLOAD_URL="https://github.com/purpleclay/${APP_NAME}/releases/download/${TAG}/${_archive}" 90 | DOWNLOAD_DIR="$(mktemp -dt ${APP_NAME}-install-XXXXXXX)" 91 | DOWNLOAD_FILE="${DOWNLOAD_DIR}/${_archive}" 92 | 93 | if [ "${HAS_CURL}" = "true" ]; then 94 | curl -L "$DOWNLOAD_URL" -o "$DOWNLOAD_FILE" 95 | elif [ "${HAS_WGET}" = "true" ]; then 96 | wget -q -O "$DOWNLOAD_FILE" "$DOWNLOAD_URL" 97 | fi 98 | } 99 | 100 | install() { 101 | echo "Installing ${APP_NAME}..." 102 | test ! -d "$INSTALL_DIR" && mkdir -p "$INSTALL_DIR" 103 | 104 | _extract_dir="$DOWNLOAD_DIR/${APP_NAME}-${TAG}" 105 | mkdir -p "$_extract_dir" 106 | tar xf "$DOWNLOAD_FILE" -C "${_extract_dir}" 107 | runAsRoot cp "${_extract_dir}/${APP_NAME}" "${INSTALL_DIR}/${APP_NAME}" 108 | 109 | echo "Installed ${APP_NAME} to ${INSTALL_DIR}" 110 | } 111 | 112 | runAsRoot() { 113 | if [ "$(id -u)" -ne 0 ] && [ "$USE_SUDO" = "true" ]; then 114 | sudo "${@}" 115 | else 116 | "${@}" 117 | fi 118 | } 119 | 120 | tidy() { 121 | if [ -d "${DOWNLOAD_DIR:-}" ]; then 122 | rm -rf "$DOWNLOAD_DIR" 123 | fi 124 | } 125 | 126 | verify() { 127 | set +e 128 | type "$APP_NAME" >/dev/null 129 | if [ "$?" = "1" ]; then 130 | echo "${APP_NAME} not found. Is ${INSTALL_DIR} on your PATH?" 131 | exit 1 132 | fi 133 | 134 | # Test version 135 | INSTALLED_VERSION="$($APP_NAME version --short)" 136 | if [ "${INSTALLED_VERSION}" != "${TAG}" ]; then 137 | echo "Found version ${INSTALLED_VERSION} of ${APP_NAME} and not expected installed version of $TAG" 138 | exit 1 139 | fi 140 | set -e 141 | } 142 | 143 | bye() { 144 | _result=$? 145 | if [ "$_result" != "0" ]; then 146 | echo "Failed to install ${APP_NAME}" 147 | fi 148 | tidy 149 | exit $_result 150 | } 151 | 152 | help () { 153 | echo "${APP_NAME} installer" 154 | echo 155 | echo "Flags:" 156 | echo " -d, --dir a directory where the binary will be installed (default '$INSTALL_DIR')" 157 | echo " --no-sudo install without using sudo" 158 | echo " -v, --version download and install a specific version (default 'latest')" 159 | echo " -h, --help Print help for the installer" 160 | } 161 | 162 | trap "bye" EXIT 163 | set -e 164 | 165 | # Parsing input arguments (if any) 166 | set -u 167 | while [ $# -gt 0 ]; do 168 | case $1 in 169 | '--version'|-v) 170 | shift 171 | if [ $# -ne 0 ]; then 172 | export DESIRED_VERSION="${1}" 173 | else 174 | echo "Please provide a valid version: e.g. --version v0.1.0 or -v v0.1.0" 175 | exit 0 176 | fi 177 | ;; 178 | '--dir'|-d) 179 | shift 180 | if [ $# -ne 0 ]; then 181 | INSTALL_DIR="${1}" 182 | else 183 | echo "Please provide a valid location for the install directory" 184 | exit 0 185 | fi 186 | ;; 187 | '--no-sudo') 188 | USE_SUDO="false" 189 | ;; 190 | '--help'|-h) 191 | help 192 | exit 0 193 | ;; 194 | *) help 195 | echo 196 | exit 1 197 | ;; 198 | esac 199 | shift 200 | done 201 | set +u 202 | 203 | initArch 204 | initOS 205 | canDownload 206 | download 207 | install 208 | verify 209 | tidy 210 | -------------------------------------------------------------------------------- /scripts/manpages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright (c) 2022 - 2023 Purple Clay 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # in the Software without restriction, including without limitation the rights 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | # Borrowed from: https://raw.githubusercontent.com/goreleaser/goreleaser/main/scripts/manpages.sh 24 | set -e 25 | rm -rf manpages 26 | mkdir manpages 27 | go run . man | gzip -c -9 > manpages/dns53.1.gz 28 | --------------------------------------------------------------------------------