├── .github ├── CODEOWNERS └── workflows │ ├── build.yml │ └── go-tests.yml ├── .gitignore ├── .go-version ├── .release ├── ci.hcl ├── release-metadata.hcl └── security-scan.hcl ├── CHANGELOG.md ├── GNUmakefile ├── LICENSE ├── README.md ├── demo ├── .gitignore ├── README.md ├── nomad │ ├── client-1.hcl │ ├── client-2.hcl │ ├── demo-ecs.nomad │ └── server.hcl └── terraform │ ├── ecs.tf │ ├── files │ ├── base-demo.json │ └── updated-demo.json │ ├── outputs.tf │ ├── provider.tf │ ├── variables.tf │ ├── version.tf │ └── vpc.tf ├── ecs ├── driver.go ├── ecs.go ├── handle.go ├── state.go └── state_test.go ├── go.mod ├── go.sum ├── main.go ├── scripts └── version.sh └── version └── version.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # release configuration 2 | /.release/ @hashicorp/nomad-eng 3 | /.github/workflows/build.yml @hashicorp/nomad-eng 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | workflow_dispatch: 6 | 7 | env: 8 | PKG_NAME: "nomad-driver-ecs" 9 | 10 | jobs: 11 | get-go-version: 12 | name: "Determine Go toolchain version" 13 | runs-on: ubuntu-20.04 14 | outputs: 15 | go-version: ${{ steps.get-go-version.outputs.go-version }} 16 | steps: 17 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 18 | - name: Determine Go version 19 | id: get-go-version 20 | run: | 21 | echo "Building with Go $(cat .go-version)" 22 | echo "::set-output name=go-version::$(cat .go-version)" 23 | 24 | get-product-version: 25 | runs-on: ubuntu-20.04 26 | outputs: 27 | product-version: ${{ steps.get-product-version.outputs.product-version }} 28 | steps: 29 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 30 | - name: get product version 31 | id: get-product-version 32 | run: | 33 | make version 34 | echo "::set-output name=product-version::$(make version)" 35 | 36 | generate-metadata-file: 37 | needs: get-product-version 38 | runs-on: ubuntu-20.04 39 | outputs: 40 | filepath: ${{ steps.generate-metadata-file.outputs.filepath }} 41 | steps: 42 | - name: "Checkout directory" 43 | uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 44 | - name: Generate metadata file 45 | id: generate-metadata-file 46 | uses: hashicorp/actions-generate-metadata@v1 47 | with: 48 | version: ${{ needs.get-product-version.outputs.product-version }} 49 | product: ${{ env.PKG_NAME }} 50 | repositoryOwner: "hashicorp" 51 | - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 52 | if: ${{ !env.ACT }} 53 | with: 54 | name: metadata.json 55 | path: ${{ steps.generate-metadata-file.outputs.filepath }} 56 | 57 | build-linux: 58 | needs: 59 | - get-go-version 60 | - get-product-version 61 | runs-on: ubuntu-20.04 62 | strategy: 63 | matrix: 64 | goos: ["linux"] 65 | goarch: ["386", "arm", "arm64", "amd64"] 66 | fail-fast: true 67 | 68 | name: Go ${{ needs.get-go-version.outputs.go-version }} ${{ matrix.goos }} ${{ matrix.goarch }} build 69 | 70 | steps: 71 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 72 | - name: Setup go 73 | uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0 74 | with: 75 | go-version: ${{ needs.get-go-version.outputs.go-version }} 76 | - name: Build 77 | env: 78 | GOOS: ${{ matrix.goos }} 79 | GOARCH: ${{ matrix.goarch }} 80 | run: | 81 | make pkg/${{ matrix.goos }}_${{ matrix.goarch }}.zip 82 | mv \ 83 | pkg/${{ matrix.goos }}_${{ matrix.goarch }}.zip \ 84 | ${{ env.PKG_NAME }}_${{ needs.get-product-version.outputs.product-version }}_${{ matrix.goos }}_${{ matrix.goarch }}.zip 85 | - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 86 | if: ${{ !env.ACT }} 87 | with: 88 | name: ${{ env.PKG_NAME }}_${{ needs.get-product-version.outputs.product-version }}_${{ matrix.goos }}_${{ matrix.goarch }}.zip 89 | path: ${{ env.PKG_NAME }}_${{ needs.get-product-version.outputs.product-version }}_${{ matrix.goos }}_${{ matrix.goarch }}.zip 90 | 91 | build-darwin: 92 | needs: 93 | - get-go-version 94 | - get-product-version 95 | runs-on: macos-11 96 | strategy: 97 | matrix: 98 | goos: ["darwin"] 99 | goarch: ["amd64", "arm64"] 100 | fail-fast: true 101 | 102 | name: Go ${{ needs.get-go-version.outputs.go-version }} ${{ matrix.goos }} ${{ matrix.goarch }} build 103 | 104 | steps: 105 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 106 | - name: Setup go 107 | uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0 108 | with: 109 | go-version: ${{ needs.get-go-version.outputs.go-version }} 110 | - name: Build 111 | env: 112 | GOOS: ${{ matrix.goos }} 113 | GOARCH: ${{ matrix.goarch }} 114 | run: | 115 | make pkg/${{ matrix.goos }}_${{ matrix.goarch }}.zip 116 | mv \ 117 | pkg/${{ matrix.goos }}_${{ matrix.goarch }}.zip \ 118 | ${{ env.PKG_NAME }}_${{ needs.get-product-version.outputs.product-version }}_${{ matrix.goos }}_${{ matrix.goarch }}.zip 119 | - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 120 | if: ${{ !env.ACT }} 121 | with: 122 | name: ${{ env.PKG_NAME }}_${{ needs.get-product-version.outputs.product-version }}_${{ matrix.goos }}_${{ matrix.goarch }}.zip 123 | path: ${{ env.PKG_NAME }}_${{ needs.get-product-version.outputs.product-version }}_${{ matrix.goos }}_${{ matrix.goarch }}.zip 124 | 125 | build-other: 126 | needs: 127 | - get-go-version 128 | - get-product-version 129 | runs-on: ubuntu-latest 130 | strategy: 131 | matrix: 132 | goos: ["windows"] 133 | goarch: ["386", "amd64"] 134 | fail-fast: true 135 | 136 | name: Go ${{ needs.get-go-version.outputs.go-version }} ${{ matrix.goos }} ${{ matrix.goarch }} build 137 | 138 | steps: 139 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 140 | - name: Setup go 141 | uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0 142 | with: 143 | go-version: ${{ needs.get-go-version.outputs.go-version }} 144 | - name: Build 145 | env: 146 | GOOS: ${{ matrix.goos }} 147 | GOARCH: ${{ matrix.goarch }} 148 | run: | 149 | make pkg/${{ matrix.goos }}_${{ matrix.goarch }}.zip 150 | mv \ 151 | pkg/${{ matrix.goos }}_${{ matrix.goarch }}.zip \ 152 | ${{ env.PKG_NAME }}_${{ needs.get-product-version.outputs.product-version }}_${{ matrix.goos }}_${{ matrix.goarch }}.zip 153 | - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 154 | if: ${{ !env.ACT }} 155 | with: 156 | name: ${{ env.PKG_NAME }}_${{ needs.get-product-version.outputs.product-version }}_${{ matrix.goos }}_${{ matrix.goarch }}.zip 157 | path: ${{ env.PKG_NAME }}_${{ needs.get-product-version.outputs.product-version }}_${{ matrix.goos }}_${{ matrix.goarch }}.zip 158 | -------------------------------------------------------------------------------- /.github/workflows/go-tests.yml: -------------------------------------------------------------------------------- 1 | name: Go Tests 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | test: 8 | name: Test 9 | runs-on: ubuntu-20.04 10 | steps: 11 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 12 | - name: Determine Go version 13 | id: get-go-version 14 | run: | 15 | echo "Building with Go $(cat .go-version)" 16 | echo "::set-output name=go-version::$(cat .go-version)" 17 | - uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0 18 | with: 19 | go-version: ${{ steps.get-go-version.outputs.go-version }} 20 | - name: Run tests 21 | run: | 22 | make test 23 | -------------------------------------------------------------------------------- /.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 | # Release binary output directory 15 | pkg/ 16 | 17 | # Dev binary output directory 18 | bin/ 19 | 20 | # Dependency directories (remove the comment below to include it) 21 | # vendor/ 22 | .vscode/ 23 | .DS_Store 24 | -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- 1 | 1.17.9 2 | -------------------------------------------------------------------------------- /.release/ci.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | schema = "1" 5 | 6 | project "nomad-driver-ecs" { 7 | // the team key is not used by CRT currently 8 | team = "nomad" 9 | slack { 10 | notification_channel = "C03B5EWFW01" 11 | } 12 | github { 13 | organization = "hashicorp" 14 | repository = "nomad-driver-ecs" 15 | release_branches = [ 16 | "main", 17 | ] 18 | } 19 | } 20 | 21 | event "merge" { 22 | // "entrypoint" to use if build is not run automatically 23 | // i.e. send "merge" complete signal to orchestrator to trigger build 24 | } 25 | 26 | event "build" { 27 | depends = ["merge"] 28 | action "build" { 29 | organization = "hashicorp" 30 | repository = "nomad-driver-ecs" 31 | workflow = "build" 32 | } 33 | } 34 | 35 | event "upload-dev" { 36 | depends = ["build"] 37 | action "upload-dev" { 38 | organization = "hashicorp" 39 | repository = "crt-workflows-common" 40 | workflow = "upload-dev" 41 | depends = ["build"] 42 | } 43 | 44 | notification { 45 | on = "fail" 46 | } 47 | } 48 | 49 | event "security-scan-binaries" { 50 | depends = ["upload-dev"] 51 | action "security-scan-binaries" { 52 | organization = "hashicorp" 53 | repository = "crt-workflows-common" 54 | workflow = "security-scan-binaries" 55 | config = "security-scan.hcl" 56 | } 57 | 58 | notification { 59 | on = "fail" 60 | } 61 | } 62 | 63 | event "notarize-darwin-amd64" { 64 | depends = ["security-scan-binaries"] 65 | action "notarize-darwin-amd64" { 66 | organization = "hashicorp" 67 | repository = "crt-workflows-common" 68 | workflow = "notarize-darwin-amd64" 69 | } 70 | 71 | notification { 72 | on = "fail" 73 | } 74 | } 75 | 76 | event "notarize-darwin-arm64" { 77 | depends = ["notarize-darwin-amd64"] 78 | action "notarize-darwin-arm64" { 79 | organization = "hashicorp" 80 | repository = "crt-workflows-common" 81 | workflow = "notarize-darwin-arm64" 82 | } 83 | 84 | notification { 85 | on = "fail" 86 | } 87 | } 88 | 89 | event "notarize-windows-amd64" { 90 | depends = ["notarize-darwin-arm64"] 91 | action "notarize-windows-amd64" { 92 | organization = "hashicorp" 93 | repository = "crt-workflows-common" 94 | workflow = "notarize-windows-amd64" 95 | } 96 | 97 | notification { 98 | on = "fail" 99 | } 100 | } 101 | 102 | event "sign" { 103 | depends = ["notarize-windows-amd64"] 104 | action "sign" { 105 | organization = "hashicorp" 106 | repository = "crt-workflows-common" 107 | workflow = "sign" 108 | } 109 | 110 | notification { 111 | on = "fail" 112 | } 113 | } 114 | 115 | event "verify" { 116 | depends = ["sign"] 117 | action "verify" { 118 | organization = "hashicorp" 119 | repository = "crt-workflows-common" 120 | workflow = "verify" 121 | } 122 | 123 | notification { 124 | on = "always" 125 | } 126 | } 127 | 128 | event "fossa-scan" { 129 | depends = ["verify"] 130 | action "fossa-scan" { 131 | organization = "hashicorp" 132 | repository = "crt-workflows-common" 133 | workflow = "fossa-scan" 134 | } 135 | } 136 | 137 | ## These are promotion and post-publish events 138 | ## they should be added to the end of the file after the verify event stanza. 139 | 140 | event "trigger-staging" { 141 | // This event is dispatched by the bob trigger-promotion command 142 | // and is required - do not delete. 143 | } 144 | 145 | event "promote-staging" { 146 | depends = ["trigger-staging"] 147 | action "promote-staging" { 148 | organization = "hashicorp" 149 | repository = "crt-workflows-common" 150 | workflow = "promote-staging" 151 | } 152 | 153 | notification { 154 | on = "always" 155 | } 156 | } 157 | 158 | event "trigger-production" { 159 | // This event is dispatched by the bob trigger-promotion command 160 | // and is required - do not delete. 161 | } 162 | 163 | event "promote-production" { 164 | depends = ["trigger-production"] 165 | action "promote-production" { 166 | organization = "hashicorp" 167 | repository = "crt-workflows-common" 168 | workflow = "promote-production" 169 | } 170 | 171 | notification { 172 | on = "always" 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /.release/release-metadata.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | url_license = "https://github.com/hashicorp/nomad-driver-ecs/blob/main/LICENSE" 5 | url_project_website = "https://www.nomadproject.io/plugins/drivers/remote/ecs" 6 | url_source_repository = "https://github.com/hashicorp/nomad-driver-ecs" 7 | -------------------------------------------------------------------------------- /.release/security-scan.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | container { 5 | dependencies = false 6 | alpine_secdb = false 7 | secrets = false 8 | } 9 | 10 | binary { 11 | secrets = true 12 | go_modules = true 13 | osv = true 14 | oss_index = false 15 | nvd = false 16 | } 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## UNRELEASED 2 | 3 | BUG FIXES: 4 | 5 | * config: Create ECS task with the the value for `assign_public_ip` as specified in the job [[GH-11](https://github.com/hashicorp/nomad-driver-ecs/pull/11)] 6 | 7 | ## 0.1.0 (May 12, 2021) 8 | 9 | * Initial Nomad AWS ECS Driver release for Nomad v1.1.0 10 | -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | SHELL = bash 2 | default: help 3 | 4 | GIT_COMMIT := $(shell git rev-parse --short HEAD) 5 | GIT_DIRTY := $(if $(shell git status --porcelain),+CHANGES) 6 | 7 | GO_LDFLAGS := "-X github.com/hashicorp/nomad-driver-ecs/version.GitCommit=$(GIT_COMMIT)$(GIT_DIRTY)" 8 | 9 | HELP_FORMAT=" \033[36m%-25s\033[0m %s\n" 10 | .PHONY: help 11 | help: ## Display this usage information 12 | @echo "Valid targets:" 13 | @grep -E '^[^ ]+:.*?## .*$$' $(MAKEFILE_LIST) | \ 14 | sort | \ 15 | awk 'BEGIN {FS = ":.*?## "}; \ 16 | {printf $(HELP_FORMAT), $$1, $$2}' 17 | @echo "" 18 | 19 | pkg/%/nomad-driver-ecs: GO_OUT ?= $@ 20 | pkg/windows_%/nomad-driver-ecs: GO_OUT = $@.exe 21 | pkg/%/nomad-driver-ecs: ## Build nomad-driver-ecs plugin for GOOS_GOARCH, e.g. pkg/linux_amd64/nomad 22 | @echo "==> Building $@ with tags $(GO_TAGS)..." 23 | @CGO_ENABLED=0 \ 24 | GOOS=$(firstword $(subst _, ,$*)) \ 25 | GOARCH=$(lastword $(subst _, ,$*)) \ 26 | go build -trimpath -ldflags $(GO_LDFLAGS) -tags "$(GO_TAGS)" -o $(GO_OUT) 27 | 28 | .PRECIOUS: pkg/%/nomad-driver-ecs 29 | pkg/%.zip: pkg/%/nomad-driver-ecs ## Build and zip nomad-driver-ecs plugin for GOOS_GOARCH, e.g. pkg/linux_amd64.zip 30 | @echo "==> Packaging for $@..." 31 | zip -j $@ $(dir $<)* 32 | 33 | .PHONY: dev 34 | dev: ## Build for the current development version 35 | @echo "==> Building nomad-driver-ecs..." 36 | @CGO_ENABLED=0 \ 37 | go build \ 38 | -ldflags $(GO_LDFLAGS) \ 39 | -o ./bin/nomad-driver-ecs 40 | @echo "==> Done" 41 | 42 | .PHONY: test 43 | test: ## Run tests 44 | go test -v -race ./... 45 | 46 | .PHONY: version 47 | version: 48 | ifneq (,$(wildcard version/version_ent.go)) 49 | @$(CURDIR)/scripts/version.sh version/version.go version/version_ent.go 50 | else 51 | @$(CURDIR)/scripts/version.sh version/version.go version/version.go 52 | endif 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 HashiCorp, Inc. 2 | 3 | Mozilla Public License, version 2.0 4 | 5 | 1. Definitions 6 | 7 | 1.1. "Contributor" 8 | 9 | means each individual or legal entity that creates, contributes to the 10 | creation of, or owns Covered Software. 11 | 12 | 1.2. "Contributor Version" 13 | 14 | means the combination of the Contributions of others (if any) used by a 15 | Contributor and that particular Contributor's Contribution. 16 | 17 | 1.3. "Contribution" 18 | 19 | means Covered Software of a particular Contributor. 20 | 21 | 1.4. "Covered Software" 22 | 23 | means Source Code Form to which the initial Contributor has attached the 24 | notice in Exhibit A, the Executable Form of such Source Code Form, and 25 | Modifications of such Source Code Form, in each case including portions 26 | thereof. 27 | 28 | 1.5. "Incompatible With Secondary Licenses" 29 | means 30 | 31 | a. that the initial Contributor has attached the notice described in 32 | Exhibit B to the Covered Software; or 33 | 34 | b. that the Covered Software was made available under the terms of 35 | version 1.1 or earlier of the License, but not also under the terms of 36 | a Secondary License. 37 | 38 | 1.6. "Executable Form" 39 | 40 | means any form of the work other than Source Code Form. 41 | 42 | 1.7. "Larger Work" 43 | 44 | means a work that combines Covered Software with other material, in a 45 | separate file or files, that is not Covered Software. 46 | 47 | 1.8. "License" 48 | 49 | means this document. 50 | 51 | 1.9. "Licensable" 52 | 53 | means having the right to grant, to the maximum extent possible, whether 54 | at the time of the initial grant or subsequently, any and all of the 55 | rights conveyed by this License. 56 | 57 | 1.10. "Modifications" 58 | 59 | means any of the following: 60 | 61 | a. any file in Source Code Form that results from an addition to, 62 | deletion from, or modification of the contents of Covered Software; or 63 | 64 | b. any new file in Source Code Form that contains any Covered Software. 65 | 66 | 1.11. "Patent Claims" of a Contributor 67 | 68 | means any patent claim(s), including without limitation, method, 69 | process, and apparatus claims, in any patent Licensable by such 70 | Contributor that would be infringed, but for the grant of the License, 71 | by the making, using, selling, offering for sale, having made, import, 72 | or transfer of either its Contributions or its Contributor Version. 73 | 74 | 1.12. "Secondary License" 75 | 76 | means either the GNU General Public License, Version 2.0, the GNU Lesser 77 | General Public License, Version 2.1, the GNU Affero General Public 78 | License, Version 3.0, or any later versions of those licenses. 79 | 80 | 1.13. "Source Code Form" 81 | 82 | means the form of the work preferred for making modifications. 83 | 84 | 1.14. "You" (or "Your") 85 | 86 | means an individual or a legal entity exercising rights under this 87 | License. For legal entities, "You" includes any entity that controls, is 88 | controlled by, or is under common control with You. For purposes of this 89 | definition, "control" means (a) the power, direct or indirect, to cause 90 | the direction or management of such entity, whether by contract or 91 | otherwise, or (b) ownership of more than fifty percent (50%) of the 92 | outstanding shares or beneficial ownership of such entity. 93 | 94 | 95 | 2. License Grants and Conditions 96 | 97 | 2.1. Grants 98 | 99 | Each Contributor hereby grants You a world-wide, royalty-free, 100 | non-exclusive license: 101 | 102 | a. under intellectual property rights (other than patent or trademark) 103 | Licensable by such Contributor to use, reproduce, make available, 104 | modify, display, perform, distribute, and otherwise exploit its 105 | Contributions, either on an unmodified basis, with Modifications, or 106 | as part of a Larger Work; and 107 | 108 | b. under Patent Claims of such Contributor to make, use, sell, offer for 109 | sale, have made, import, and otherwise transfer either its 110 | Contributions or its Contributor Version. 111 | 112 | 2.2. Effective Date 113 | 114 | The licenses granted in Section 2.1 with respect to any Contribution 115 | become effective for each Contribution on the date the Contributor first 116 | distributes such Contribution. 117 | 118 | 2.3. Limitations on Grant Scope 119 | 120 | The licenses granted in this Section 2 are the only rights granted under 121 | this License. No additional rights or licenses will be implied from the 122 | distribution or licensing of Covered Software under this License. 123 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 124 | Contributor: 125 | 126 | a. for any code that a Contributor has removed from Covered Software; or 127 | 128 | b. for infringements caused by: (i) Your and any other third party's 129 | modifications of Covered Software, or (ii) the combination of its 130 | Contributions with other software (except as part of its Contributor 131 | Version); or 132 | 133 | c. under Patent Claims infringed by Covered Software in the absence of 134 | its Contributions. 135 | 136 | This License does not grant any rights in the trademarks, service marks, 137 | or logos of any Contributor (except as may be necessary to comply with 138 | the notice requirements in Section 3.4). 139 | 140 | 2.4. Subsequent Licenses 141 | 142 | No Contributor makes additional grants as a result of Your choice to 143 | distribute the Covered Software under a subsequent version of this 144 | License (see Section 10.2) or under the terms of a Secondary License (if 145 | permitted under the terms of Section 3.3). 146 | 147 | 2.5. Representation 148 | 149 | Each Contributor represents that the Contributor believes its 150 | Contributions are its original creation(s) or it has sufficient rights to 151 | grant the rights to its Contributions conveyed by this License. 152 | 153 | 2.6. Fair Use 154 | 155 | This License is not intended to limit any rights You have under 156 | applicable copyright doctrines of fair use, fair dealing, or other 157 | equivalents. 158 | 159 | 2.7. Conditions 160 | 161 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 162 | Section 2.1. 163 | 164 | 165 | 3. Responsibilities 166 | 167 | 3.1. Distribution of Source Form 168 | 169 | All distribution of Covered Software in Source Code Form, including any 170 | Modifications that You create or to which You contribute, must be under 171 | the terms of this License. You must inform recipients that the Source 172 | Code Form of the Covered Software is governed by the terms of this 173 | License, and how they can obtain a copy of this License. You may not 174 | attempt to alter or restrict the recipients' rights in the Source Code 175 | Form. 176 | 177 | 3.2. Distribution of Executable Form 178 | 179 | If You distribute Covered Software in Executable Form then: 180 | 181 | a. such Covered Software must also be made available in Source Code Form, 182 | as described in Section 3.1, and You must inform recipients of the 183 | Executable Form how they can obtain a copy of such Source Code Form by 184 | reasonable means in a timely manner, at a charge no more than the cost 185 | of distribution to the recipient; and 186 | 187 | b. You may distribute such Executable Form under the terms of this 188 | License, or sublicense it under different terms, provided that the 189 | license for the Executable Form does not attempt to limit or alter the 190 | recipients' rights in the Source Code Form under this License. 191 | 192 | 3.3. Distribution of a Larger Work 193 | 194 | You may create and distribute a Larger Work under terms of Your choice, 195 | provided that You also comply with the requirements of this License for 196 | the Covered Software. If the Larger Work is a combination of Covered 197 | Software with a work governed by one or more Secondary Licenses, and the 198 | Covered Software is not Incompatible With Secondary Licenses, this 199 | License permits You to additionally distribute such Covered Software 200 | under the terms of such Secondary License(s), so that the recipient of 201 | the Larger Work may, at their option, further distribute the Covered 202 | Software under the terms of either this License or such Secondary 203 | License(s). 204 | 205 | 3.4. Notices 206 | 207 | You may not remove or alter the substance of any license notices 208 | (including copyright notices, patent notices, disclaimers of warranty, or 209 | limitations of liability) contained within the Source Code Form of the 210 | Covered Software, except that You may alter any license notices to the 211 | extent required to remedy known factual inaccuracies. 212 | 213 | 3.5. Application of Additional Terms 214 | 215 | You may choose to offer, and to charge a fee for, warranty, support, 216 | indemnity or liability obligations to one or more recipients of Covered 217 | Software. However, You may do so only on Your own behalf, and not on 218 | behalf of any Contributor. You must make it absolutely clear that any 219 | such warranty, support, indemnity, or liability obligation is offered by 220 | You alone, and You hereby agree to indemnify every Contributor for any 221 | liability incurred by such Contributor as a result of warranty, support, 222 | indemnity or liability terms You offer. You may include additional 223 | disclaimers of warranty and limitations of liability specific to any 224 | jurisdiction. 225 | 226 | 4. Inability to Comply Due to Statute or Regulation 227 | 228 | If it is impossible for You to comply with any of the terms of this License 229 | with respect to some or all of the Covered Software due to statute, 230 | judicial order, or regulation then You must: (a) comply with the terms of 231 | this License to the maximum extent possible; and (b) describe the 232 | limitations and the code they affect. Such description must be placed in a 233 | text file included with all distributions of the Covered Software under 234 | this License. Except to the extent prohibited by statute or regulation, 235 | such description must be sufficiently detailed for a recipient of ordinary 236 | skill to be able to understand it. 237 | 238 | 5. Termination 239 | 240 | 5.1. The rights granted under this License will terminate automatically if You 241 | fail to comply with any of its terms. However, if You become compliant, 242 | then the rights granted under this License from a particular Contributor 243 | are reinstated (a) provisionally, unless and until such Contributor 244 | explicitly and finally terminates Your grants, and (b) on an ongoing 245 | basis, if such Contributor fails to notify You of the non-compliance by 246 | some reasonable means prior to 60 days after You have come back into 247 | compliance. Moreover, Your grants from a particular Contributor are 248 | reinstated on an ongoing basis if such Contributor notifies You of the 249 | non-compliance by some reasonable means, this is the first time You have 250 | received notice of non-compliance with this License from such 251 | Contributor, and You become compliant prior to 30 days after Your receipt 252 | of the notice. 253 | 254 | 5.2. If You initiate litigation against any entity by asserting a patent 255 | infringement claim (excluding declaratory judgment actions, 256 | counter-claims, and cross-claims) alleging that a Contributor Version 257 | directly or indirectly infringes any patent, then the rights granted to 258 | You by any and all Contributors for the Covered Software under Section 259 | 2.1 of this License shall terminate. 260 | 261 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 262 | license agreements (excluding distributors and resellers) which have been 263 | validly granted by You or Your distributors under this License prior to 264 | termination shall survive termination. 265 | 266 | 6. Disclaimer of Warranty 267 | 268 | Covered Software is provided under this License on an "as is" basis, 269 | without warranty of any kind, either expressed, implied, or statutory, 270 | including, without limitation, warranties that the Covered Software is free 271 | of defects, merchantable, fit for a particular purpose or non-infringing. 272 | The entire risk as to the quality and performance of the Covered Software 273 | is with You. Should any Covered Software prove defective in any respect, 274 | You (not any Contributor) assume the cost of any necessary servicing, 275 | repair, or correction. This disclaimer of warranty constitutes an essential 276 | part of this License. No use of any Covered Software is authorized under 277 | this License except under this disclaimer. 278 | 279 | 7. Limitation of Liability 280 | 281 | Under no circumstances and under no legal theory, whether tort (including 282 | negligence), contract, or otherwise, shall any Contributor, or anyone who 283 | distributes Covered Software as permitted above, be liable to You for any 284 | direct, indirect, special, incidental, or consequential damages of any 285 | character including, without limitation, damages for lost profits, loss of 286 | goodwill, work stoppage, computer failure or malfunction, or any and all 287 | other commercial damages or losses, even if such party shall have been 288 | informed of the possibility of such damages. This limitation of liability 289 | shall not apply to liability for death or personal injury resulting from 290 | such party's negligence to the extent applicable law prohibits such 291 | limitation. Some jurisdictions do not allow the exclusion or limitation of 292 | incidental or consequential damages, so this exclusion and limitation may 293 | not apply to You. 294 | 295 | 8. Litigation 296 | 297 | Any litigation relating to this License may be brought only in the courts 298 | of a jurisdiction where the defendant maintains its principal place of 299 | business and such litigation shall be governed by laws of that 300 | jurisdiction, without reference to its conflict-of-law provisions. Nothing 301 | in this Section shall prevent a party's ability to bring cross-claims or 302 | counter-claims. 303 | 304 | 9. Miscellaneous 305 | 306 | This License represents the complete agreement concerning the subject 307 | matter hereof. If any provision of this License is held to be 308 | unenforceable, such provision shall be reformed only to the extent 309 | necessary to make it enforceable. Any law or regulation which provides that 310 | the language of a contract shall be construed against the drafter shall not 311 | be used to construe this License against a Contributor. 312 | 313 | 314 | 10. Versions of the License 315 | 316 | 10.1. New Versions 317 | 318 | Mozilla Foundation is the license steward. Except as provided in Section 319 | 10.3, no one other than the license steward has the right to modify or 320 | publish new versions of this License. Each version will be given a 321 | distinguishing version number. 322 | 323 | 10.2. Effect of New Versions 324 | 325 | You may distribute the Covered Software under the terms of the version 326 | of the License under which You originally received the Covered Software, 327 | or under the terms of any subsequent version published by the license 328 | steward. 329 | 330 | 10.3. Modified Versions 331 | 332 | If you create software not governed by this License, and you want to 333 | create a new license for such software, you may create and use a 334 | modified version of this License if you rename the license and remove 335 | any references to the name of the license steward (except to note that 336 | such modified license differs from this License). 337 | 338 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 339 | Licenses If You choose to distribute Source Code Form that is 340 | Incompatible With Secondary Licenses under the terms of this version of 341 | the License, the notice described in Exhibit B of this License must be 342 | attached. 343 | 344 | Exhibit A - Source Code Form License Notice 345 | 346 | This Source Code Form is subject to the 347 | terms of the Mozilla Public License, v. 348 | 2.0. If a copy of the MPL was not 349 | distributed with this file, You can 350 | obtain one at 351 | http://mozilla.org/MPL/2.0/. 352 | 353 | If it is not possible or desirable to put the notice in a particular file, 354 | then You may include the notice in a location (such as a LICENSE file in a 355 | relevant directory) where a recipient would be likely to look for such a 356 | notice. 357 | 358 | You may add additional accurate notices of copyright ownership. 359 | 360 | Exhibit B - "Incompatible With Secondary Licenses" Notice 361 | 362 | This Source Code Form is "Incompatible 363 | With Secondary Licenses", as defined by 364 | the Mozilla Public License, v. 2.0. 365 | 366 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nomad ECS Driver Plugin (Experimental) 2 | The Nomad ECS driver plugin is an experimental type of remote driver plugin. Whereas traditional Nomad driver plugins rely on running processes locally to the client, this experiment allows for the control of tasks at a remote destination. The driver is responsible for the lifecycle management of the remote process, as well as performing health checks and health decisions. 3 | 4 | **Warning: this is an experimental feature and is therefore supplied without guarantees and is subject to change without warning. Do not run this in production.** 5 | 6 | Nomad v1.1.0-beta1 or later is required. 7 | 8 | ## Demo 9 | A full demo can be found within the [demo directory](./demo) that will run through the full lifecycle of a task run under the ECS driver. It includes Terraform code to build the required AWS resources, as well as the Nomad configuration files and job specifications needed. 10 | 11 | ## Driver Configuration 12 | In order to use the ECS driver, the binary needs to be executable and placed within the Nomad client's plugin directory. Please refer to the [Nomad plugin documentation](https://nomadproject.io/docs/configuration/plugin/) for more detail regarding the configuration. 13 | 14 | The Nomad ECS driver plugin supports the following configuration parameters: 15 | * `enabled` - (bool: false) A boolean flag to control whether the plugin is enabled. 16 | * `cluster` - (string: """) The ECS cluster name where tasks will be run. 17 | * `region` - (string: "") The AWS region to send all requests to. 18 | 19 | A example client plugin stanza looks like the following: 20 | 21 | ```hcl 22 | plugin "nomad-driver-ecs" { 23 | config { 24 | enabled = true 25 | cluster = "nomad-remote-driver-cluster" 26 | region = "us-east-1" 27 | } 28 | } 29 | ``` 30 | 31 | ## ECS Task Configuration 32 | The Nomad ECS drivers includes the functionality to run [ECS tasks](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definitions.html) via exposing configuration parameters within the Nomad jobspec. Please note, the ECS task definition is not created as part of the Nomad workflow and must be created prior to running a driver task. The below configuration summarises the current options, for further details about each parameter please refer to the [AWS sdk](https://github.com/aws/aws-sdk-go-v2/blob/9fc62ee75d1acca973ac777e51993fce74f6a27f/service/ecs/api_op_RunTask.go#L13). 33 | 34 | In order to configure a ECS task within a Nomad task stanza, the config requires an initial `task` block as so: 35 | ```hcl 36 | config { 37 | task { 38 | ... 39 | } 40 | } 41 | ``` 42 | 43 | #### Top Level Task Config Options 44 | * `launch_type` - The launch type on which to run your task. 45 | * `task_definition` - The family and revision (family:revision) or full ARN of the task definition to run. 46 | * `network_configuration` - The network configuration for the task. 47 | 48 | #### network_configuration Config Options 49 | * `aws_vpc_configuration` - The VPC subnets and security groups associated with a task. 50 | 51 | #### aws_vpc_configuration Config Options 52 | * `assign_public_ip` - Whether the task's elastic network interface receives a public IP address. 53 | * `security_groups` - The security groups associated with the task or service. 54 | * `subnets` - The subnets associated with the task or service. 55 | 56 | A full example of a Nomad task stanza which runs an ECS task: 57 | ```hcl 58 | task "http-server" { 59 | driver = "ecs" 60 | 61 | config { 62 | task { 63 | launch_type = "FARGATE" 64 | task_definition = "my-task-definition:1" 65 | network_configuration { 66 | aws_vpc_configuration { 67 | assign_public_ip = "ENABLED" 68 | security_groups = ["sg-05f444f6c0dda876d"] 69 | subnets = ["subnet-0cd4b2ec21331a144", "subnet-0da9019dcab8ae2f1"] 70 | } 71 | } 72 | } 73 | } 74 | } 75 | ``` 76 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | # ignore the Terraform state files 2 | terraform.tfstate* 3 | 4 | # ignore the Terraform local state directory 5 | .terraform/ 6 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # ECS Remote Driver Demo 2 | The ECS remote driver demo shows how Nomad can be used to run, monitor and maintain tasks running on an AWS ECS cluster. 3 | 4 | ## ECS Driver Main Responsibilities 5 | The remote driver is built in the same way, using the same interfaces as another other Nomad task driver. Its behaviour can differ slightly, depending on the remote endpoint. Therefore below is a short overview on the main task which the ECS driver must perform: 6 | * Driver Health: driver health is performed by performing a describe call on the ECS cluster 7 | * Driver Run: the main run function of the driver is responsible polling the ECS task, describing its health. If the task is in a terminal state, the driver exist its current loop and passes this information back to Nomad. 8 | 9 | ## Requirements 10 | In order to run this demo, you will need the following items available. 11 | 12 | * An AWS account and API access credentials (specific policy requirements TBC) 13 | * Terraform > 0.12.0 - https://www.terraform.io/downloads.html 14 | * [Nomad v1.1.0-beta1 or later](https://releases.hashicorp.com/nomad/) 15 | 16 | ## Assumptions / Rough Edges 17 | The demo makes some assumptions because of the quick, and local nature of it. 18 | * AWS access credentials will be available via environment variables or default profile. This is needed by Terraform and Nomad. Please see the [AWS documentation](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html) for more details on setting this up. 19 | * The ECS cluster is running within `us-east-1`. If you need to change the AWS region please update `./nomad/{client-1,client-2,server}.hcl` files. 20 | 21 | ## Build Out 22 | When running this demo, a small number of AWS resources will be created. The majority of these do not incur direct costs, however, the ECS task does. For mre information regarding this, please visit the [Fargate pricing document](https://aws.amazon.com/fargate/pricing/). Also beware AWS data transfer costs. 23 | 24 | 1. Change directory into the Terraform directory: 25 | ``` 26 | $ cd ./terraform 27 | ``` 28 | 1. Modify the Terraform variables file with any custom configuration. The file is located at `./terraform/variables.tf`. 29 | 1. Perform the Terraform initialisation: 30 | ``` 31 | $ terraform init 32 | ``` 33 | 1. Inspect the Terraform plan output and verify it is as expected: 34 | ``` 35 | $ terraform plan -out=nomad-task-driver-demo 36 | ``` 37 | 1. Apply the Terraform plan to build out the AWS resources: 38 | ``` 39 | $ terraform apply -auto-approve nomad-task-driver-demo 40 | ``` 41 | 1. The Terraform output will contain `demo_subnet_id` and `demo_security_group_id` values, these should be noted for later use. 42 | 1. Start the Nomad server and clients. Ideally each command is run in a separate terminal allowing for easy following of logs: 43 | ``` 44 | $ cd ../nomad 45 | $ nomad agent -config=server.hcl 46 | $ nomad agent -config=client-1.hcl -plugin-dir=$(pwd)/plugins 47 | $ nomad agent -config=client-2.hcl -plugin-dir=$(pwd)/plugins 48 | ``` 49 | 1. Check the ECS driver status on a client node: 50 | ``` 51 | $ nomad node status # To see Node IDs 52 | $ nomad node status |grep "Driver Status" 53 | ``` 54 | 55 | ## Demo 56 | The following steps will demonstrate how Nomad, and the remote driver handle multiple situations that operators will likely come across during day-to-day cluster management. Notably, how Nomad attempts to minimise the impact of task availability even when its availability is degraded. 57 | 58 | 1. Using the Terraform output from before, update the `nomad/demo-ecs.nomad` file to reflect these details. In particular these two parameters need updating: 59 | ``` 60 | security_groups = ["sg-0d647d4c7ce15034f"] 61 | subnets = ["subnet-010b03f1a021887ff"] 62 | ``` 63 | 1. Submit the remote task driver job to the cluster: 64 | ``` 65 | $ nomad run demo-ecs.nomad 66 | ``` 67 | 1. Check the allocation status, and the logs to show the client is remotely monitoring the task: 68 | ``` 69 | $ nomad status nomad-ecs-demo 70 | $ nomad logs -f 71 | ``` 72 | 1. Navigate to the AWS ECS console and check the running tasks on the cluster. The URL will look like `https://console.aws.amazon.com/ecs/home?region=us-east-1#/clusters/nomad-remote-driver-demo/tasks`, but be sure to change the region if needed. 73 | 1. Drain the node on which the remote task is currently being monitored from. This will cause Nomad to create a new allocation, but will not impact the remote task: 74 | ``` 75 | $ nomad node drain -enable 76 | ``` 77 | 1. Here you can again check the logs of the new allocation and the AWS console to check the status of the ECS task. You should notice the remote task remains running, and the new allocation logs attach and monitor the same task as the previous allocation. 78 | 1. Remove the drain status from the previously drained node so that it is available for scheduling again: 79 | ``` 80 | $ nomad node drain -disable 81 | ``` 82 | 1. Kill the Nomad client which is currently running to simulate a lost node situation. This can be done either by control-c of the process, or using kill -9. 83 | 1. Check the logs of the new allocation and the AWS console to check the status of the ECS task. You should notice the remote task remains running, and the new allocation logs attach, and monitor the same task as the previous allocation. 84 | 1. Now updated the ECS task definition. This process has been wrapped via Terraform using variables: 85 | ``` 86 | $ cd ../terraform 87 | $ terraform apply -var 'ecs_task_definition_file=./files/updated-demo.json' -auto-approve 88 | ``` 89 | 1. Update the job specification in order to deploy to new, updated task definition: 90 | ``` 91 | $ cd ../nomad 92 | $ sed -ie "s/nomad-remote-driver-demo:1/nomad-remote-driver-demo:2/g" demo-ecs.nomad 93 | ``` 94 | 1. Register the updated job on the Nomad cluster: 95 | ``` 96 | $ nomad run demo-ecs.nomad 97 | ``` 98 | 1. Observing the AWS console, there will be a new task provisioning. Filtering by status `stopped` shows the previous task in `stopping` status. Nomad has successfully deployed the new version of the task. 99 | 1. Stop the Nomad job and observe the task stopping within AWS: 100 | ``` 101 | $ nomad stop nomad-ecs-demo 102 | ``` 103 | 104 | ## Tear Down 105 | 1. Stop the Nomad clients and server processes, either by control-c or killing the process IDs. 106 | 1. Destroy the created AWS resources, performing a plan and checking the destroy is targeting the expected resources: 107 | ``` 108 | $ cd ../terraform 109 | $ terraform plan -destroy 110 | $ terraform destroy -auto-approve 111 | ``` 112 | -------------------------------------------------------------------------------- /demo/nomad/client-1.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | log_level = "DEBUG" 5 | datacenter = "dc1" 6 | 7 | data_dir = "/tmp/nomad-client-1" 8 | name = "nomad-client-1" 9 | 10 | client { 11 | enabled = true 12 | servers = ["127.0.0.1:4647"] 13 | max_kill_timeout = "3m" // increased from default to accomodate ECS. 14 | } 15 | 16 | ports { 17 | http = 5656 18 | rpc = 5657 19 | serf = 5658 20 | } 21 | 22 | plugin "nomad-driver-ecs" { 23 | config { 24 | enabled = true 25 | cluster = "nomad-remote-driver-demo" 26 | region = "us-east-1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /demo/nomad/client-2.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | log_level = "DEBUG" 5 | datacenter = "dc1" 6 | 7 | data_dir = "/tmp/nomad-client-2" 8 | name = "nomad-client-2" 9 | 10 | client { 11 | enabled = true 12 | servers = ["localhost:4647"] 13 | max_kill_timeout = "3m" // increased from default to accomodate ECS. 14 | } 15 | 16 | ports { 17 | http = 6656 18 | } 19 | 20 | plugin "nomad-driver-ecs" { 21 | config { 22 | enabled = true 23 | cluster = "nomad-remote-driver-demo" 24 | region = "us-east-1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /demo/nomad/demo-ecs.nomad: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | job "nomad-ecs-demo" { 5 | datacenters = ["dc1"] 6 | 7 | group "ecs-remote-task-demo" { 8 | restart { 9 | attempts = 0 10 | mode = "fail" 11 | } 12 | 13 | reschedule { 14 | delay = "5s" 15 | } 16 | 17 | task "http-server" { 18 | driver = "ecs" 19 | kill_timeout = "1m" // increased from default to accomodate ECS. 20 | 21 | config { 22 | task { 23 | launch_type = "FARGATE" 24 | task_definition = "nomad-remote-driver-demo:1" 25 | network_configuration { 26 | aws_vpc_configuration { 27 | assign_public_ip = "ENABLED" 28 | security_groups = ["sg-0d647d4c7ce15034f"] 29 | subnets = ["subnet-010b03f1a021887ff"] 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /demo/nomad/server.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | log_level = "DEBUG" 5 | datacenter = "dc1" 6 | data_dir = "/tmp/nomad-server-1" 7 | name = "nomad-server-1" 8 | 9 | server { 10 | enabled = true 11 | bootstrap_expect = 1 12 | num_schedulers = 1 13 | } 14 | -------------------------------------------------------------------------------- /demo/terraform/ecs.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | resource "aws_ecs_cluster" "nomad_remote_driver_demo" { 5 | name = "nomad-remote-driver-demo" 6 | } 7 | 8 | resource "aws_ecs_task_definition" "nomad_remote_driver_demo" { 9 | family = "nomad-remote-driver-demo" 10 | container_definitions = file(var.ecs_task_definition_file) 11 | network_mode = "awsvpc" 12 | requires_compatibilities = ["FARGATE"] 13 | cpu = 256 14 | memory = 512 15 | } 16 | -------------------------------------------------------------------------------- /demo/terraform/files/base-demo.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": [ 4 | "/bin/sh -c \"echo ' Amazon ECS Sample App

Amazon ECS Sample App

Congratulations!

Your application is now running on a container in Amazon ECS.

' > /usr/local/apache2/htdocs/index.html && httpd-foreground\"" 5 | ], 6 | "entryPoint": [ 7 | "sh", 8 | "-c" 9 | ], 10 | "essential": true, 11 | "image": "httpd:2.4", 12 | "name": "nomad-remote-driver-demo", 13 | "portMappings": [ 14 | { 15 | "containerPort": 80, 16 | "hostPort": 80, 17 | "protocol": "tcp" 18 | } 19 | ] 20 | } 21 | ] 22 | -------------------------------------------------------------------------------- /demo/terraform/files/updated-demo.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": [ 4 | "/bin/sh -c \"echo ' Updated Amazon ECS Sample App

Updated Amazon ECS Sample App

Congratulations!

Your application is now running on a container in Amazon ECS.

' > /usr/local/apache2/htdocs/index.html && httpd-foreground\"" 5 | ], 6 | "entryPoint": [ 7 | "sh", 8 | "-c" 9 | ], 10 | "essential": true, 11 | "image": "httpd:2.4", 12 | "name": "nomad-remote-driver-demo", 13 | "portMappings": [ 14 | { 15 | "containerPort": 80, 16 | "hostPort": 80, 17 | "protocol": "tcp" 18 | } 19 | ] 20 | } 21 | ] 22 | -------------------------------------------------------------------------------- /demo/terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | output "demo_subnet_id" { 5 | value = aws_subnet.nomad_remote_driver_demo.id 6 | } 7 | 8 | output "demo_security_group_id" { 9 | value = aws_security_group.nomad_remote_driver_demo.id 10 | } 11 | -------------------------------------------------------------------------------- /demo/terraform/provider.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | provider "aws" { 5 | region = var.region 6 | } 7 | -------------------------------------------------------------------------------- /demo/terraform/variables.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | variable "vpc_cidr_block" { 5 | description = "The CIDR block range to use when creating the VPC." 6 | type = string 7 | default = "10.0.0.0/24" 8 | } 9 | 10 | variable "ecs_task_definition_file" { 11 | description = "The file that contains the ECS task definition, used as a deployment/update trick." 12 | type = string 13 | default = "./files/base-demo.json" 14 | } 15 | 16 | variable "region" { 17 | description = "AWS region for ECS cluster. Update Nomad config if not using the default." 18 | type = string 19 | default = "us-east-1" 20 | } 21 | -------------------------------------------------------------------------------- /demo/terraform/version.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | terraform { 5 | required_providers { 6 | aws = { 7 | source = "hashicorp/aws" 8 | version = "~> 3.0" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /demo/terraform/vpc.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | resource "aws_vpc" "nomad_remote_driver_demo" { 5 | cidr_block = var.vpc_cidr_block 6 | } 7 | 8 | resource "aws_subnet" "nomad_remote_driver_demo" { 9 | cidr_block = var.vpc_cidr_block 10 | vpc_id = aws_vpc.nomad_remote_driver_demo.id 11 | } 12 | 13 | resource "aws_internet_gateway" "nomad_remote_driver_demo" { 14 | vpc_id = aws_vpc.nomad_remote_driver_demo.id 15 | } 16 | 17 | resource "aws_route_table" "nomad_remote_driver_demo" { 18 | vpc_id = aws_vpc.nomad_remote_driver_demo.id 19 | } 20 | 21 | resource "aws_route" "nomad_remote_driver_demo" { 22 | route_table_id = aws_route_table.nomad_remote_driver_demo.id 23 | destination_cidr_block = "0.0.0.0/0" 24 | gateway_id = aws_internet_gateway.nomad_remote_driver_demo.id 25 | } 26 | 27 | resource "aws_route_table_association" "nomad_remote_driver_demo" { 28 | subnet_id = aws_subnet.nomad_remote_driver_demo.id 29 | route_table_id = aws_route_table.nomad_remote_driver_demo.id 30 | } 31 | 32 | resource "aws_security_group" "nomad_remote_driver_demo" { 33 | vpc_id = aws_vpc.nomad_remote_driver_demo.id 34 | 35 | ingress { 36 | from_port = 80 37 | to_port = 80 38 | protocol = "tcp" 39 | cidr_blocks = ["0.0.0.0/0"] 40 | } 41 | 42 | egress { 43 | from_port = 0 44 | to_port = 65535 45 | protocol = "tcp" 46 | cidr_blocks = ["0.0.0.0/0"] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ecs/driver.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ecs 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "time" 10 | 11 | "github.com/aws/aws-sdk-go-v2/aws/external" 12 | "github.com/aws/aws-sdk-go-v2/service/ecs" 13 | "github.com/hashicorp/go-hclog" 14 | "github.com/hashicorp/nomad-driver-ecs/version" 15 | "github.com/hashicorp/nomad/client/structs" 16 | "github.com/hashicorp/nomad/drivers/shared/eventer" 17 | "github.com/hashicorp/nomad/plugins/base" 18 | "github.com/hashicorp/nomad/plugins/drivers" 19 | "github.com/hashicorp/nomad/plugins/shared/hclspec" 20 | pstructs "github.com/hashicorp/nomad/plugins/shared/structs" 21 | ) 22 | 23 | const ( 24 | // pluginName is the name of the plugin. 25 | pluginName = "ecs" 26 | 27 | // fingerprintPeriod is the interval at which the driver will send 28 | // fingerprint responses. 29 | fingerprintPeriod = 30 * time.Second 30 | 31 | // taskHandleVersion is the version of task handle which this plugin sets 32 | // and understands how to decode. This is used to allow modification and 33 | // migration of the task schema used by the plugin. 34 | taskHandleVersion = 1 35 | ) 36 | 37 | var ( 38 | // pluginInfo is the response returned for the PluginInfo RPC. 39 | pluginInfo = &base.PluginInfoResponse{ 40 | Type: base.PluginTypeDriver, 41 | PluginApiVersions: []string{drivers.ApiVersion010}, 42 | PluginVersion: version.Version, 43 | Name: pluginName, 44 | } 45 | 46 | // pluginConfigSpec is the hcl specification returned by the ConfigSchema RPC. 47 | pluginConfigSpec = hclspec.NewObject(map[string]*hclspec.Spec{ 48 | "enabled": hclspec.NewAttr("enabled", "bool", false), 49 | "cluster": hclspec.NewAttr("cluster", "string", false), 50 | "region": hclspec.NewAttr("region", "string", false), 51 | }) 52 | 53 | // taskConfigSpec represents an ECS task configuration object. 54 | // https://docs.aws.amazon.com/AmazonECS/latest/developerguide/scheduling_tasks.html 55 | taskConfigSpec = hclspec.NewObject(map[string]*hclspec.Spec{ 56 | "task": hclspec.NewBlock("task", false, awsECSTaskConfigSpec), 57 | }) 58 | 59 | // awsECSTaskConfigSpec are the high level configuration options for 60 | // configuring and ECS task. 61 | awsECSTaskConfigSpec = hclspec.NewObject(map[string]*hclspec.Spec{ 62 | "launch_type": hclspec.NewAttr("launch_type", "string", false), 63 | "task_definition": hclspec.NewAttr("task_definition", "string", false), 64 | "network_configuration": hclspec.NewBlock("network_configuration", false, awsECSNetworkConfigSpec), 65 | }) 66 | 67 | // awsECSNetworkConfigSpec is the network configuration for the task. 68 | awsECSNetworkConfigSpec = hclspec.NewObject(map[string]*hclspec.Spec{ 69 | "aws_vpc_configuration": hclspec.NewBlock("aws_vpc_configuration", false, awsECSVPCConfigSpec), 70 | }) 71 | 72 | // awsECSVPCConfigSpec is the object representing the networking details 73 | // for an ECS task or service. 74 | awsECSVPCConfigSpec = hclspec.NewObject(map[string]*hclspec.Spec{ 75 | "assign_public_ip": hclspec.NewAttr("assign_public_ip", "string", false), 76 | "security_groups": hclspec.NewAttr("security_groups", "list(string)", false), 77 | "subnets": hclspec.NewAttr("subnets", "list(string)", false), 78 | }) 79 | 80 | // capabilities is returned by the Capabilities RPC and indicates what 81 | // optional features this driver supports 82 | capabilities = &drivers.Capabilities{ 83 | SendSignals: false, 84 | Exec: false, 85 | FSIsolation: drivers.FSIsolationImage, 86 | RemoteTasks: true, 87 | } 88 | ) 89 | 90 | // Driver is a driver for running ECS containers 91 | type Driver struct { 92 | // eventer is used to handle multiplexing of TaskEvents calls such that an 93 | // event can be broadcast to all callers 94 | eventer *eventer.Eventer 95 | 96 | // config is the driver configuration set by the SetConfig RPC 97 | config *DriverConfig 98 | 99 | // nomadConfig is the client config from nomad 100 | nomadConfig *base.ClientDriverConfig 101 | 102 | // tasks is the in memory datastore mapping taskIDs to rawExecDriverHandles 103 | tasks *taskStore 104 | 105 | // ctx is the context for the driver. It is passed to other subsystems to 106 | // coordinate shutdown 107 | ctx context.Context 108 | 109 | // signalShutdown is called when the driver is shutting down and cancels the 110 | // ctx passed to any subsystems 111 | signalShutdown context.CancelFunc 112 | 113 | // logger will log to the Nomad agent 114 | logger hclog.Logger 115 | 116 | // ecsClientInterface is the interface used for communicating with AWS ECS 117 | client ecsClientInterface 118 | } 119 | 120 | // DriverConfig is the driver configuration set by the SetConfig RPC call 121 | type DriverConfig struct { 122 | Enabled bool `codec:"enabled"` 123 | Cluster string `codec:"cluster"` 124 | Region string `codec:"region"` 125 | } 126 | 127 | // TaskConfig is the driver configuration of a task within a job 128 | type TaskConfig struct { 129 | Task ECSTaskConfig `codec:"task"` 130 | } 131 | 132 | type ECSTaskConfig struct { 133 | LaunchType string `codec:"launch_type"` 134 | TaskDefinition string `codec:"task_definition"` 135 | NetworkConfiguration TaskNetworkConfiguration `codec:"network_configuration"` 136 | } 137 | 138 | type TaskNetworkConfiguration struct { 139 | TaskAWSVPCConfiguration TaskAWSVPCConfiguration `codec:"aws_vpc_configuration"` 140 | } 141 | 142 | type TaskAWSVPCConfiguration struct { 143 | AssignPublicIP string `codec:"assign_public_ip"` 144 | SecurityGroups []string `codec:"security_groups"` 145 | Subnets []string `codec:"subnets"` 146 | } 147 | 148 | // TaskState is the state which is encoded in the handle returned in 149 | // StartTask. This information is needed to rebuild the task state and handler 150 | // during recovery. 151 | type TaskState struct { 152 | TaskConfig *drivers.TaskConfig 153 | ContainerName string 154 | ARN string 155 | StartedAt time.Time 156 | } 157 | 158 | // NewECSDriver returns a new DriverPlugin implementation 159 | func NewPlugin(logger hclog.Logger) drivers.DriverPlugin { 160 | ctx, cancel := context.WithCancel(context.Background()) 161 | logger = logger.Named(pluginName) 162 | return &Driver{ 163 | eventer: eventer.NewEventer(ctx, logger), 164 | config: &DriverConfig{}, 165 | tasks: newTaskStore(), 166 | ctx: ctx, 167 | signalShutdown: cancel, 168 | logger: logger, 169 | } 170 | } 171 | 172 | func (d *Driver) PluginInfo() (*base.PluginInfoResponse, error) { 173 | return pluginInfo, nil 174 | } 175 | 176 | func (d *Driver) ConfigSchema() (*hclspec.Spec, error) { 177 | return pluginConfigSpec, nil 178 | } 179 | 180 | func (d *Driver) SetConfig(cfg *base.Config) error { 181 | var config DriverConfig 182 | if len(cfg.PluginConfig) != 0 { 183 | if err := base.MsgPackDecode(cfg.PluginConfig, &config); err != nil { 184 | return err 185 | } 186 | } 187 | 188 | d.config = &config 189 | if cfg.AgentConfig != nil { 190 | d.nomadConfig = cfg.AgentConfig.Driver 191 | } 192 | 193 | client, err := d.getAwsSdk(config.Cluster) 194 | if err != nil { 195 | return fmt.Errorf("failed to get AWS SDK client: %v", err) 196 | } 197 | d.client = client 198 | 199 | return nil 200 | } 201 | 202 | func (d *Driver) getAwsSdk(cluster string) (ecsClientInterface, error) { 203 | awsCfg, err := external.LoadDefaultAWSConfig() 204 | if err != nil { 205 | return nil, fmt.Errorf("unable to load SDK config: %v", err) 206 | } 207 | 208 | if d.config.Region != "" { 209 | awsCfg.Region = d.config.Region 210 | } 211 | 212 | return awsEcsClient{ 213 | cluster: cluster, 214 | ecsClient: ecs.New(awsCfg), 215 | }, nil 216 | } 217 | 218 | func (d *Driver) Shutdown(ctx context.Context) error { 219 | d.signalShutdown() 220 | return nil 221 | } 222 | 223 | func (d *Driver) TaskConfigSchema() (*hclspec.Spec, error) { 224 | return taskConfigSpec, nil 225 | } 226 | 227 | func (d *Driver) Capabilities() (*drivers.Capabilities, error) { 228 | return capabilities, nil 229 | } 230 | 231 | func (d *Driver) Fingerprint(ctx context.Context) (<-chan *drivers.Fingerprint, error) { 232 | ch := make(chan *drivers.Fingerprint) 233 | go d.handleFingerprint(ctx, ch) 234 | return ch, nil 235 | } 236 | 237 | func (d *Driver) handleFingerprint(ctx context.Context, ch chan<- *drivers.Fingerprint) { 238 | defer close(ch) 239 | ticker := time.NewTimer(0) 240 | for { 241 | select { 242 | case <-ctx.Done(): 243 | return 244 | case <-d.ctx.Done(): 245 | return 246 | case <-ticker.C: 247 | ticker.Reset(fingerprintPeriod) 248 | ch <- d.buildFingerprint(ctx) 249 | } 250 | } 251 | } 252 | 253 | func (d *Driver) buildFingerprint(ctx context.Context) *drivers.Fingerprint { 254 | var health drivers.HealthState 255 | var desc string 256 | attrs := map[string]*pstructs.Attribute{} 257 | 258 | if d.config.Enabled { 259 | if err := d.client.DescribeCluster(ctx); err != nil { 260 | health = drivers.HealthStateUnhealthy 261 | desc = err.Error() 262 | attrs["driver.ecs"] = pstructs.NewBoolAttribute(false) 263 | } else { 264 | health = drivers.HealthStateHealthy 265 | desc = "Healthy" 266 | attrs["driver.ecs"] = pstructs.NewBoolAttribute(true) 267 | } 268 | } else { 269 | health = drivers.HealthStateUndetected 270 | desc = "disabled" 271 | } 272 | 273 | return &drivers.Fingerprint{ 274 | Attributes: attrs, 275 | Health: health, 276 | HealthDescription: desc, 277 | } 278 | } 279 | 280 | func (d *Driver) RecoverTask(handle *drivers.TaskHandle) error { 281 | d.logger.Info("recovering ecs task", "version", handle.Version, 282 | "task_config.id", handle.Config.ID, "task_state", handle.State, 283 | "driver_state_bytes", len(handle.DriverState)) 284 | if handle == nil { 285 | return fmt.Errorf("handle cannot be nil") 286 | } 287 | 288 | // If already attached to handle there's nothing to recover. 289 | if _, ok := d.tasks.Get(handle.Config.ID); ok { 290 | d.logger.Info("no ecs task to recover; task already exists", 291 | "task_id", handle.Config.ID, 292 | "task_name", handle.Config.Name, 293 | ) 294 | return nil 295 | } 296 | 297 | // Handle doesn't already exist, try to reattach 298 | var taskState TaskState 299 | if err := handle.GetDriverState(&taskState); err != nil { 300 | d.logger.Error("failed to decode task state from handle", "error", err, "task_id", handle.Config.ID) 301 | return fmt.Errorf("failed to decode task state from handle: %v", err) 302 | } 303 | 304 | d.logger.Info("ecs task recovered", "arn", taskState.ARN, 305 | "started_at", taskState.StartedAt) 306 | 307 | h := newTaskHandle(d.logger, taskState, handle.Config, d.client) 308 | 309 | d.tasks.Set(handle.Config.ID, h) 310 | 311 | go h.run() 312 | return nil 313 | } 314 | 315 | func (d *Driver) StartTask(cfg *drivers.TaskConfig) (*drivers.TaskHandle, *drivers.DriverNetwork, error) { 316 | if !d.config.Enabled { 317 | return nil, nil, fmt.Errorf("disabled") 318 | } 319 | 320 | if _, ok := d.tasks.Get(cfg.ID); ok { 321 | return nil, nil, fmt.Errorf("task with ID %q already started", cfg.ID) 322 | } 323 | 324 | var driverConfig TaskConfig 325 | if err := cfg.DecodeDriverConfig(&driverConfig); err != nil { 326 | return nil, nil, fmt.Errorf("failed to decode driver config: %v", err) 327 | } 328 | 329 | d.logger.Info("starting ecs task", "driver_cfg", hclog.Fmt("%+v", driverConfig)) 330 | handle := drivers.NewTaskHandle(taskHandleVersion) 331 | handle.Config = cfg 332 | 333 | arn, err := d.client.RunTask(context.Background(), driverConfig) 334 | if err != nil { 335 | return nil, nil, fmt.Errorf("failed to start ECS task: %v", err) 336 | } 337 | 338 | driverState := TaskState{ 339 | TaskConfig: cfg, 340 | StartedAt: time.Now(), 341 | ARN: arn, 342 | } 343 | 344 | d.logger.Info("ecs task started", "arn", driverState.ARN, "started_at", driverState.StartedAt) 345 | 346 | h := newTaskHandle(d.logger, driverState, cfg, d.client) 347 | 348 | if err := handle.SetDriverState(&driverState); err != nil { 349 | d.logger.Error("failed to start task, error setting driver state", "error", err) 350 | h.stop(false) 351 | return nil, nil, fmt.Errorf("failed to set driver state: %v", err) 352 | } 353 | 354 | d.tasks.Set(cfg.ID, h) 355 | 356 | go h.run() 357 | return handle, nil, nil 358 | } 359 | 360 | func (d *Driver) WaitTask(ctx context.Context, taskID string) (<-chan *drivers.ExitResult, error) { 361 | d.logger.Info("WaitTask() called", "task_id", taskID) 362 | handle, ok := d.tasks.Get(taskID) 363 | if !ok { 364 | return nil, drivers.ErrTaskNotFound 365 | } 366 | 367 | ch := make(chan *drivers.ExitResult) 368 | go d.handleWait(ctx, handle, ch) 369 | 370 | return ch, nil 371 | } 372 | 373 | func (d *Driver) handleWait(ctx context.Context, handle *taskHandle, ch chan *drivers.ExitResult) { 374 | defer close(ch) 375 | 376 | var result *drivers.ExitResult 377 | select { 378 | case <-ctx.Done(): 379 | return 380 | case <-d.ctx.Done(): 381 | return 382 | case <-handle.doneCh: 383 | result = &drivers.ExitResult{ 384 | ExitCode: handle.exitResult.ExitCode, 385 | Signal: handle.exitResult.Signal, 386 | Err: nil, 387 | } 388 | } 389 | 390 | select { 391 | case <-ctx.Done(): 392 | return 393 | case <-d.ctx.Done(): 394 | return 395 | case ch <- result: 396 | } 397 | } 398 | 399 | func (d *Driver) StopTask(taskID string, timeout time.Duration, signal string) error { 400 | d.logger.Info("stopping ecs task", "task_id", taskID, "timeout", timeout, "signal", signal) 401 | handle, ok := d.tasks.Get(taskID) 402 | if !ok { 403 | return drivers.ErrTaskNotFound 404 | } 405 | 406 | // Detach is that's the signal, otherwise kill 407 | detach := signal == drivers.DetachSignal 408 | handle.stop(detach) 409 | 410 | // Wait for handle to finish 411 | select { 412 | case <-handle.doneCh: 413 | case <-time.After(timeout): 414 | return fmt.Errorf("timed out waiting for ecs task (id=%s) to stop (detach=%t)", 415 | taskID, detach) 416 | } 417 | 418 | d.logger.Info("ecs task stopped", "task_id", taskID, "timeout", timeout, 419 | "signal", signal) 420 | return nil 421 | } 422 | 423 | func (d *Driver) DestroyTask(taskID string, force bool) error { 424 | d.logger.Info("destroying ecs task", "task_id", taskID, "force", force) 425 | handle, ok := d.tasks.Get(taskID) 426 | if !ok { 427 | return drivers.ErrTaskNotFound 428 | } 429 | 430 | if handle.IsRunning() && !force { 431 | return fmt.Errorf("cannot destroy running task") 432 | } 433 | 434 | // Safe to always kill here as detaching will have already happened 435 | handle.stop(false) 436 | 437 | d.tasks.Delete(taskID) 438 | d.logger.Info("ecs task destroyed", "task_id", taskID, "force", force) 439 | return nil 440 | } 441 | 442 | func (d *Driver) InspectTask(taskID string) (*drivers.TaskStatus, error) { 443 | handle, ok := d.tasks.Get(taskID) 444 | if !ok { 445 | return nil, drivers.ErrTaskNotFound 446 | } 447 | return handle.TaskStatus(), nil 448 | } 449 | 450 | func (d *Driver) TaskStats(ctx context.Context, taskID string, interval time.Duration) (<-chan *structs.TaskResourceUsage, error) { 451 | d.logger.Info("sending ecs task stats", "task_id", taskID) 452 | _, ok := d.tasks.Get(taskID) 453 | if !ok { 454 | return nil, drivers.ErrTaskNotFound 455 | } 456 | 457 | ch := make(chan *drivers.TaskResourceUsage) 458 | 459 | go func() { 460 | defer d.logger.Info("stopped sending ecs task stats", "task_id", taskID) 461 | defer close(ch) 462 | for { 463 | select { 464 | case <-time.After(interval): 465 | 466 | // Nomad core does not currently have any resource based 467 | // support for remote drivers. Once this changes, we may be 468 | // able to report actual usage here. 469 | // 470 | // This is required, otherwise the driver panics. 471 | ch <- &structs.TaskResourceUsage{ 472 | ResourceUsage: &drivers.ResourceUsage{ 473 | MemoryStats: &drivers.MemoryStats{}, 474 | CpuStats: &drivers.CpuStats{}, 475 | }, 476 | Timestamp: time.Now().UTC().UnixNano(), 477 | } 478 | case <-ctx.Done(): 479 | return 480 | } 481 | 482 | } 483 | }() 484 | 485 | return ch, nil 486 | } 487 | 488 | func (d *Driver) TaskEvents(ctx context.Context) (<-chan *drivers.TaskEvent, error) { 489 | d.logger.Info("retrieving task events") 490 | return d.eventer.TaskEvents(ctx) 491 | } 492 | 493 | func (d *Driver) SignalTask(_ string, _ string) error { 494 | return fmt.Errorf("ECS driver does not support signals") 495 | } 496 | 497 | func (d *Driver) ExecTask(_ string, _ []string, _ time.Duration) (*drivers.ExecTaskResult, error) { 498 | return nil, fmt.Errorf("ECS driver does not support exec") 499 | } 500 | -------------------------------------------------------------------------------- /ecs/ecs.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ecs 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | 10 | "github.com/aws/aws-sdk-go-v2/aws" 11 | "github.com/aws/aws-sdk-go-v2/service/ecs" 12 | ) 13 | 14 | // ecsClientInterface encapsulates all the required AWS functionality to 15 | // successfully run tasks via this plugin. 16 | type ecsClientInterface interface { 17 | 18 | // DescribeCluster is used to determine the health of the plugin by 19 | // querying AWS for the cluster and checking its current status. A status 20 | // other than ACTIVE is considered unhealthy. 21 | DescribeCluster(ctx context.Context) error 22 | 23 | // DescribeTaskStatus attempts to return the current health status of the 24 | // ECS task and should be used for health checking. 25 | DescribeTaskStatus(ctx context.Context, taskARN string) (string, error) 26 | 27 | // RunTask is used to trigger the running of a new ECS task based on the 28 | // provided configuration. The ARN of the task, as well as any errors are 29 | // returned to the caller. 30 | RunTask(ctx context.Context, cfg TaskConfig) (string, error) 31 | 32 | // StopTask stops the running ECS task, adding a custom message which can 33 | // be viewed via the AWS console specifying it was this Nomad driver which 34 | // performed the action. 35 | StopTask(ctx context.Context, taskARN string) error 36 | } 37 | 38 | type awsEcsClient struct { 39 | cluster string 40 | ecsClient *ecs.Client 41 | } 42 | 43 | // DescribeCluster satisfies the ecs.ecsClientInterface DescribeCluster 44 | // interface function. 45 | func (c awsEcsClient) DescribeCluster(ctx context.Context) error { 46 | input := ecs.DescribeClustersInput{Clusters: []string{c.cluster}} 47 | 48 | resp, err := c.ecsClient.DescribeClustersRequest(&input).Send(ctx) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | if len(resp.Clusters) > 1 || len(resp.Clusters) < 1 { 54 | return fmt.Errorf("AWS returned %v ECS clusters, expected 1", len(resp.Clusters)) 55 | } 56 | 57 | if *resp.Clusters[0].Status != "ACTIVE" { 58 | return fmt.Errorf("ECS cluster status: %s", *resp.Clusters[0].Status) 59 | } 60 | 61 | return nil 62 | } 63 | 64 | // DescribeTaskStatus satisfies the ecs.ecsClientInterface DescribeTaskStatus 65 | // interface function. 66 | func (c awsEcsClient) DescribeTaskStatus(ctx context.Context, taskARN string) (string, error) { 67 | input := ecs.DescribeTasksInput{ 68 | Cluster: aws.String(c.cluster), 69 | Tasks: []string{taskARN}, 70 | } 71 | 72 | resp, err := c.ecsClient.DescribeTasksRequest(&input).Send(ctx) 73 | if err != nil { 74 | return "", err 75 | } 76 | return *resp.Tasks[0].LastStatus, nil 77 | } 78 | 79 | // RunTask satisfies the ecs.ecsClientInterface RunTask interface function. 80 | func (c awsEcsClient) RunTask(ctx context.Context, cfg TaskConfig) (string, error) { 81 | input := c.buildTaskInput(cfg) 82 | 83 | if err := input.Validate(); err != nil { 84 | return "", fmt.Errorf("failed to validate: %w", err) 85 | } 86 | 87 | resp, err := c.ecsClient.RunTaskRequest(input).Send(ctx) 88 | if err != nil { 89 | return "", err 90 | } 91 | return *resp.RunTaskOutput.Tasks[0].TaskArn, nil 92 | } 93 | 94 | // buildTaskInput is used to convert the jobspec supplied configuration input 95 | // into the appropriate ecs.RunTaskInput object. 96 | func (c awsEcsClient) buildTaskInput(cfg TaskConfig) *ecs.RunTaskInput { 97 | input := ecs.RunTaskInput{ 98 | Cluster: aws.String(c.cluster), 99 | Count: aws.Int64(1), 100 | StartedBy: aws.String("nomad-ecs-driver"), 101 | NetworkConfiguration: &ecs.NetworkConfiguration{AwsvpcConfiguration: &ecs.AwsVpcConfiguration{}}, 102 | } 103 | 104 | if cfg.Task.LaunchType != "" { 105 | if cfg.Task.LaunchType == "EC2" { 106 | input.LaunchType = ecs.LaunchTypeEc2 107 | } else if cfg.Task.LaunchType == "FARGATE" { 108 | input.LaunchType = ecs.LaunchTypeFargate 109 | } 110 | } 111 | 112 | if cfg.Task.TaskDefinition != "" { 113 | input.TaskDefinition = aws.String(cfg.Task.TaskDefinition) 114 | } 115 | 116 | // Handle the task networking setup. 117 | if cfg.Task.NetworkConfiguration.TaskAWSVPCConfiguration.AssignPublicIP != "" { 118 | assignPublicIp := cfg.Task.NetworkConfiguration.TaskAWSVPCConfiguration.AssignPublicIP 119 | if assignPublicIp == "ENABLED" { 120 | input.NetworkConfiguration.AwsvpcConfiguration.AssignPublicIp = ecs.AssignPublicIpEnabled 121 | } else if assignPublicIp == "DISABLED" { 122 | input.NetworkConfiguration.AwsvpcConfiguration.AssignPublicIp = ecs.AssignPublicIpDisabled 123 | } 124 | } 125 | if len(cfg.Task.NetworkConfiguration.TaskAWSVPCConfiguration.SecurityGroups) > 0 { 126 | input.NetworkConfiguration.AwsvpcConfiguration.SecurityGroups = cfg.Task.NetworkConfiguration.TaskAWSVPCConfiguration.SecurityGroups 127 | } 128 | if len(cfg.Task.NetworkConfiguration.TaskAWSVPCConfiguration.Subnets) > 0 { 129 | input.NetworkConfiguration.AwsvpcConfiguration.Subnets = cfg.Task.NetworkConfiguration.TaskAWSVPCConfiguration.Subnets 130 | } 131 | 132 | return &input 133 | } 134 | 135 | // StopTask satisfies the ecs.ecsClientInterface StopTask interface function. 136 | func (c awsEcsClient) StopTask(ctx context.Context, taskARN string) error { 137 | input := ecs.StopTaskInput{ 138 | Cluster: aws.String(c.cluster), 139 | Task: &taskARN, 140 | Reason: aws.String("stopped by nomad-ecs-driver automation"), 141 | } 142 | 143 | _, err := c.ecsClient.StopTaskRequest(&input).Send(ctx) 144 | return err 145 | } 146 | -------------------------------------------------------------------------------- /ecs/handle.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ecs 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "sync" 10 | "time" 11 | 12 | "github.com/hashicorp/go-hclog" 13 | "github.com/hashicorp/nomad/client/lib/fifo" 14 | "github.com/hashicorp/nomad/client/stats" 15 | "github.com/hashicorp/nomad/plugins/drivers" 16 | ) 17 | 18 | // These represent the ECS task terminal lifecycle statuses. 19 | const ( 20 | ecsTaskStatusDeactivating = "DEACTIVATING" 21 | ecsTaskStatusStopping = "STOPPING" 22 | ecsTaskStatusDeprovisioning = "DEPROVISIONING" 23 | ecsTaskStatusStopped = "STOPPED" 24 | ) 25 | 26 | type taskHandle struct { 27 | arn string 28 | logger hclog.Logger 29 | ecsClient ecsClientInterface 30 | 31 | totalCpuStats *stats.CpuStats 32 | userCpuStats *stats.CpuStats 33 | systemCpuStats *stats.CpuStats 34 | 35 | // stateLock syncs access to all fields below 36 | stateLock sync.RWMutex 37 | 38 | taskConfig *drivers.TaskConfig 39 | procState drivers.TaskState 40 | startedAt time.Time 41 | completedAt time.Time 42 | exitResult *drivers.ExitResult 43 | doneCh chan struct{} 44 | 45 | // detach from ecs task instead of killing it if true. 46 | detach bool 47 | 48 | ctx context.Context 49 | cancel context.CancelFunc 50 | } 51 | 52 | func newTaskHandle(logger hclog.Logger, ts TaskState, taskConfig *drivers.TaskConfig, ecsClient ecsClientInterface) *taskHandle { 53 | ctx, cancel := context.WithCancel(context.Background()) 54 | logger = logger.Named("handle").With("arn", ts.ARN) 55 | 56 | h := &taskHandle{ 57 | arn: ts.ARN, 58 | ecsClient: ecsClient, 59 | taskConfig: taskConfig, 60 | procState: drivers.TaskStateRunning, 61 | startedAt: ts.StartedAt, 62 | exitResult: &drivers.ExitResult{}, 63 | logger: logger, 64 | doneCh: make(chan struct{}), 65 | detach: false, 66 | ctx: ctx, 67 | cancel: cancel, 68 | } 69 | 70 | return h 71 | } 72 | 73 | func (h *taskHandle) TaskStatus() *drivers.TaskStatus { 74 | h.stateLock.RLock() 75 | defer h.stateLock.RUnlock() 76 | 77 | return &drivers.TaskStatus{ 78 | ID: h.taskConfig.ID, 79 | Name: h.taskConfig.Name, 80 | State: h.procState, 81 | StartedAt: h.startedAt, 82 | CompletedAt: h.completedAt, 83 | ExitResult: h.exitResult, 84 | DriverAttributes: map[string]string{ 85 | "arn": h.arn, 86 | }, 87 | } 88 | } 89 | 90 | func (h *taskHandle) IsRunning() bool { 91 | h.stateLock.RLock() 92 | defer h.stateLock.RUnlock() 93 | return h.procState == drivers.TaskStateRunning 94 | } 95 | 96 | func (h *taskHandle) run() { 97 | defer close(h.doneCh) 98 | h.stateLock.Lock() 99 | if h.exitResult == nil { 100 | h.exitResult = &drivers.ExitResult{} 101 | } 102 | h.stateLock.Unlock() 103 | 104 | // Open the tasks StdoutPath so we can write task health status updates. 105 | f, err := fifo.OpenWriter(h.taskConfig.StdoutPath) 106 | if err != nil { 107 | h.handleRunError(err, "failed to open task stdout path") 108 | return 109 | } 110 | 111 | // Run the deferred close in an anonymous routine so we can see any errors. 112 | defer func() { 113 | if err := f.Close(); err != nil { 114 | h.logger.Error("failed to close task stdout handle correctly", "error", err) 115 | } 116 | }() 117 | 118 | // Block until stopped. 119 | for h.ctx.Err() == nil { 120 | select { 121 | case <-time.After(5 * time.Second): 122 | 123 | status, err := h.ecsClient.DescribeTaskStatus(h.ctx, h.arn) 124 | if err != nil { 125 | h.handleRunError(err, "failed to find ECS task") 126 | return 127 | } 128 | 129 | // Write the health status before checking what it is ensures the 130 | // alloc logs include the health during the ECS tasks terminal 131 | // phase. 132 | now := time.Now().Format(time.RFC3339) 133 | if _, err := fmt.Fprintf(f, "[%s] - client is remotely monitoring ECS task: %v with status %v\n", 134 | now, h.arn, status); err != nil { 135 | h.handleRunError(err, "failed to write to stdout") 136 | } 137 | 138 | // ECS task has terminal status phase, meaning the task is going to 139 | // stop. If we are in this phase, the driver should exit and pass 140 | // this to the servers so that a new allocation, and ECS task can 141 | // be started. 142 | if status == ecsTaskStatusDeactivating || status == ecsTaskStatusStopping || 143 | status == ecsTaskStatusDeprovisioning || status == ecsTaskStatusStopped { 144 | h.handleRunError(fmt.Errorf("ECS task status in terminal phase"), "task status: "+status) 145 | return 146 | } 147 | 148 | case <-h.ctx.Done(): 149 | } 150 | } 151 | 152 | h.stateLock.Lock() 153 | defer h.stateLock.Unlock() 154 | 155 | // Only stop task if we're not detaching. 156 | if !h.detach { 157 | if err := h.stopTask(); err != nil { 158 | h.handleRunError(err, "failed to stop ECS task correctly") 159 | return 160 | } 161 | } 162 | 163 | h.procState = drivers.TaskStateExited 164 | h.exitResult.ExitCode = 0 165 | h.exitResult.Signal = 0 166 | h.completedAt = time.Now() 167 | } 168 | 169 | func (h *taskHandle) stop(detach bool) { 170 | h.stateLock.Lock() 171 | defer h.stateLock.Unlock() 172 | 173 | // Only allow transitioning from not-detaching to detaching. 174 | if !h.detach && detach { 175 | h.detach = detach 176 | } 177 | h.cancel() 178 | } 179 | 180 | // handleRunError is a convenience function to easily and correctly handle 181 | // terminal errors during the task run lifecycle. 182 | func (h *taskHandle) handleRunError(err error, context string) { 183 | h.stateLock.Lock() 184 | h.completedAt = time.Now() 185 | h.exitResult.ExitCode = 1 186 | h.exitResult.Err = fmt.Errorf("%s: %v", context, err) 187 | h.stateLock.Unlock() 188 | } 189 | 190 | // stopTask is used to stop the ECS task, and monitor its status until it 191 | // reaches the stopped state. 192 | func (h *taskHandle) stopTask() error { 193 | if err := h.ecsClient.StopTask(context.TODO(), h.arn); err != nil { 194 | return err 195 | } 196 | 197 | for { 198 | select { 199 | case <-time.After(5 * time.Second): 200 | status, err := h.ecsClient.DescribeTaskStatus(context.TODO(), h.arn) 201 | if err != nil { 202 | return err 203 | } 204 | 205 | // Check whether the status is in its final state, and log to provide 206 | // operator visibility. 207 | if status == ecsTaskStatusStopped { 208 | h.logger.Info("ecs task has successfully been stopped") 209 | return nil 210 | } 211 | h.logger.Debug("continuing to monitor ecs task shutdown", "status", status) 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /ecs/state.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ecs 5 | 6 | import ( 7 | "sync" 8 | ) 9 | 10 | // taskStore is where we store individual task handles using a lock to ensure 11 | // updates are safe. 12 | type taskStore struct { 13 | store map[string]*taskHandle 14 | lock sync.RWMutex 15 | } 16 | 17 | // newTaskStore builds a new taskStore for use. 18 | func newTaskStore() *taskStore { 19 | return &taskStore{store: map[string]*taskHandle{}} 20 | } 21 | 22 | // Set is used to insert, safely, an entry into the taskStore. 23 | func (ts *taskStore) Set(id string, handle *taskHandle) { 24 | ts.lock.Lock() 25 | defer ts.lock.Unlock() 26 | ts.store[id] = handle 27 | } 28 | 29 | // Get returns a taskHandle, if it exists in the taskStore based on a passed 30 | // identifier. 31 | func (ts *taskStore) Get(id string) (*taskHandle, bool) { 32 | ts.lock.RLock() 33 | defer ts.lock.RUnlock() 34 | t, ok := ts.store[id] 35 | return t, ok 36 | } 37 | 38 | // Delete removes an entry from the taskStore if it exists. 39 | func (ts *taskStore) Delete(id string) { 40 | ts.lock.Lock() 41 | defer ts.lock.Unlock() 42 | delete(ts.store, id) 43 | } 44 | -------------------------------------------------------------------------------- /ecs/state_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ecs 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func Test_taskStore(t *testing.T) { 13 | 14 | // Setup the new store and check it is empty. 15 | s := newTaskStore() 16 | assert.Empty(t, s.store, "new task store is not empty") 17 | 18 | // Test setting a new task handle and then reading it back out. 19 | s.Set("test-set-1", &taskHandle{}) 20 | testHandle1, ok := s.Get("test-set-1") 21 | assert.NotNil(t, testHandle1, "test-set-1 is nil") 22 | assert.True(t, ok, "failed to get test-set-1") 23 | 24 | // Delete and read it back to ensure its gone. 25 | s.Delete("test-set-1") 26 | testHandle1, ok = s.Get("test-set-1") 27 | assert.Nil(t, testHandle1, "test-set-1 is not nil") 28 | assert.False(t, ok, "test-set-1 should not be available") 29 | } 30 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/nomad-driver-ecs 2 | 3 | go 1.17 4 | 5 | replace ( 6 | github.com/Microsoft/go-winio => github.com/endocrimes/go-winio v0.4.13-0.20190628114223-fb47a8b41948 7 | github.com/hashicorp/nomad/api => github.com/hashicorp/nomad/api v0.0.0-20220407202126-2eba643965c4 8 | ) 9 | 10 | require ( 11 | github.com/aws/aws-sdk-go-v2 v0.19.0 12 | github.com/hashicorp/go-hclog v1.2.0 13 | github.com/hashicorp/nomad v1.3.0-rc.1 14 | github.com/stretchr/testify v1.7.1 15 | ) 16 | 17 | require ( 18 | github.com/LK4D4/joincontext v0.0.0-20171026170139-1724345da6d5 // indirect 19 | github.com/Microsoft/go-winio v0.4.17 // indirect 20 | github.com/armon/go-metrics v0.3.10 // indirect 21 | github.com/armon/go-radix v1.0.0 // indirect 22 | github.com/cenkalti/backoff/v3 v3.2.2 // indirect 23 | github.com/container-storage-interface/spec v1.4.0 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/fatih/color v1.13.0 // indirect 26 | github.com/frankban/quicktest v1.14.0 // indirect 27 | github.com/fsnotify/fsnotify v1.4.9 // indirect 28 | github.com/go-ole/go-ole v1.2.6 // indirect 29 | github.com/go-test/deep v1.0.3 // indirect 30 | github.com/golang/protobuf v1.5.2 // indirect 31 | github.com/golang/snappy v0.0.4 // indirect 32 | github.com/grpc-ecosystem/go-grpc-middleware v1.2.1-0.20200228141219-3ce3d519df39 // indirect 33 | github.com/hashicorp/consul/api v1.12.0 // indirect 34 | github.com/hashicorp/cronexpr v1.1.1 // indirect 35 | github.com/hashicorp/errwrap v1.1.0 // indirect 36 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 37 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 38 | github.com/hashicorp/go-msgpack v1.1.5 // indirect 39 | github.com/hashicorp/go-multierror v1.1.1 // indirect 40 | github.com/hashicorp/go-plugin v1.4.3 // indirect 41 | github.com/hashicorp/go-retryablehttp v0.7.0 // indirect 42 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 43 | github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 // indirect 44 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.4 // indirect 45 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 46 | github.com/hashicorp/go-sockaddr v1.0.2 // indirect 47 | github.com/hashicorp/go-uuid v1.0.2 // indirect 48 | github.com/hashicorp/go-version v1.4.0 // indirect 49 | github.com/hashicorp/golang-lru v0.5.4 // indirect 50 | github.com/hashicorp/hcl v1.0.1-vault-3 // indirect 51 | github.com/hashicorp/raft v1.3.5 // indirect 52 | github.com/hashicorp/serf v0.9.7 // indirect 53 | github.com/hashicorp/vault/api v1.4.1 // indirect 54 | github.com/hashicorp/vault/sdk v0.4.1 // indirect 55 | github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect 56 | github.com/hpcloud/tail v1.0.1-0.20170814160653-37f427138745 // indirect 57 | github.com/jmespath/go-jmespath v0.4.0 // indirect 58 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 59 | github.com/mattn/go-colorable v0.1.12 // indirect 60 | github.com/mattn/go-isatty v0.0.14 // indirect 61 | github.com/miekg/dns v1.1.41 // indirect 62 | github.com/mitchellh/copystructure v1.2.0 // indirect 63 | github.com/mitchellh/go-homedir v1.1.0 // indirect 64 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 65 | github.com/mitchellh/hashstructure v1.1.0 // indirect 66 | github.com/mitchellh/mapstructure v1.4.3 // indirect 67 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 68 | github.com/moby/sys/mount v0.3.0 // indirect 69 | github.com/moby/sys/mountinfo v0.6.0 // indirect 70 | github.com/oklog/run v1.1.0 // indirect 71 | github.com/pierrec/lz4 v2.6.1+incompatible // indirect 72 | github.com/pmezard/go-difflib v1.0.0 // indirect 73 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 74 | github.com/ryanuber/go-glob v1.0.0 // indirect 75 | github.com/shirou/gopsutil/v3 v3.21.12 // indirect 76 | github.com/tklauser/go-sysconf v0.3.9 // indirect 77 | github.com/tklauser/numcpus v0.3.0 // indirect 78 | github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect 79 | github.com/vmihailenco/tagparser v0.1.2 // indirect 80 | github.com/yusufpapurcu/wmi v1.2.2 // indirect 81 | github.com/zclconf/go-cty v1.8.0 // indirect 82 | go.uber.org/atomic v1.9.0 // indirect 83 | golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect 84 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect 85 | golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 // indirect 86 | golang.org/x/text v0.3.7 // indirect 87 | golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect 88 | google.golang.org/appengine v1.6.7 // indirect 89 | google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106 // indirect 90 | google.golang.org/grpc v1.45.0 // indirect 91 | google.golang.org/protobuf v1.27.1 // indirect 92 | gopkg.in/fsnotify.v1 v1.4.7 // indirect 93 | gopkg.in/square/go-jose.v2 v2.6.0 // indirect 94 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 95 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect 96 | ) 97 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | log "github.com/hashicorp/go-hclog" 8 | "github.com/hashicorp/nomad-driver-ecs/ecs" 9 | "github.com/hashicorp/nomad/plugins" 10 | ) 11 | 12 | func main() { 13 | // Serve the plugin 14 | plugins.Serve(factory) 15 | } 16 | 17 | // factory returns a new instance of a nomad driver plugin 18 | func factory(log log.Logger) interface{} { 19 | return ecs.NewPlugin(log) 20 | } 21 | -------------------------------------------------------------------------------- /scripts/version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | version_file=$1 7 | version_metadata_file=$2 8 | version=$(awk '$1 == "Version" && $2 == "=" { gsub(/"/, "", $3); print $3 }' <"${version_file}") 9 | prerelease=$(awk '$1 == "VersionPrerelease" && $2 == "=" { gsub(/"/, "", $3); print $3 }' <"${version_file}") 10 | metadata=$(awk '$1 == "VersionMetadata" && $2 == "=" { gsub(/"/, "", $3); print $3 }' <"${version_metadata_file}") 11 | 12 | if [ -n "$metadata" ] && [ -n "$prerelease" ]; then 13 | echo "${version}-${prerelease}+${metadata}" 14 | elif [ -n "$metadata" ]; then 15 | echo "${version}+${metadata}" 16 | elif [ -n "$prerelease" ]; then 17 | echo "${version}-${prerelease}" 18 | else 19 | echo "${version}" 20 | fi 21 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package version 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | var ( 12 | // The git commit that was compiled. These will be filled in by the compiler. 13 | GitCommit string 14 | GitDescribe string 15 | 16 | // The main version number that is being run at the moment. 17 | // 18 | // Version must conform to the format expected by 19 | // github.com/hashicorp/go-version for tests to work. 20 | Version = "0.1.1" 21 | 22 | // A pre-release marker for the version. If this is "" (empty string) 23 | // then it means that it is a final release. Otherwise, this is a pre-release 24 | // such as "dev" (in development), "beta", "rc1", etc. 25 | VersionPrerelease = "dev" 26 | ) 27 | 28 | // GetHumanVersion composes the parts of the version in a way that's suitable 29 | // for displaying to humans. 30 | func GetHumanVersion() string { 31 | version := Version 32 | if GitDescribe != "" { 33 | version = GitDescribe 34 | } 35 | 36 | release := VersionPrerelease 37 | if GitDescribe == "" && release == "" { 38 | release = "dev" 39 | } 40 | 41 | if release != "" { 42 | if !strings.HasSuffix(version, "-"+release) { 43 | // if we tagged a prerelease version then the release is in the version already 44 | version += fmt.Sprintf("-%s", release) 45 | } 46 | if GitCommit != "" { 47 | version += fmt.Sprintf(" (%s)", GitCommit) 48 | } 49 | } 50 | 51 | // Strip off any single quotes added by the git information. 52 | return strings.Replace(version, "'", "", -1) 53 | } 54 | --------------------------------------------------------------------------------