├── .github ├── PULL_REQUEST_TEMPLATE.md └── dependabot.yml ├── .gitignore ├── .mergify.yml ├── AGENT_VERSION_COMPATIBILITY ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── THIRD-PARTY ├── VERSION ├── buildspec.yml ├── buildspec_verify.yml ├── docs ├── configuration.md ├── features.md └── setup-networking.md ├── examples ├── docker-compose.localstack.yml ├── docker-compose.yml ├── generic │ └── task-metadata.json └── v4 │ └── container-metadata.json ├── go.mod ├── go.sum ├── infra ├── README.md ├── package-lock.json ├── package.json ├── pipeline.ts └── tsconfig.json ├── integ ├── Dockerfile ├── README.md ├── credentials │ └── credentials_test.go ├── docker-compose.yml └── metadata │ ├── v2_test.go │ └── v3_test.go ├── local-container-endpoints ├── clients │ ├── docker │ │ ├── client.go │ │ ├── generate_mock.go │ │ └── mock_docker │ │ │ └── mock.go │ ├── iam │ │ ├── generate_mock.go │ │ └── mock_iamiface │ │ │ └── mock.go │ ├── sts │ │ ├── generate_mock.go │ │ └── mock_stsiface │ │ │ └── mock.go │ └── useragent │ │ └── useragent.go ├── config │ └── config.go ├── handlers │ ├── credentials_handler.go │ ├── credentials_handler_test.go │ ├── functional_tests │ │ ├── credentials_test.go │ │ ├── metadata_v2_test.go │ │ ├── metadata_v3_test.go │ │ └── test_helper.go │ ├── http.go │ ├── metadata.go │ ├── metadata_handler.go │ ├── metadata_test.go │ └── types.go ├── metadata │ ├── metadata.go │ └── metadata_test.go ├── testingutils │ ├── docker_container.go │ └── metadata_container.go ├── utils │ └── utils.go └── version │ ├── appname.go │ ├── formatter.go │ ├── gen │ └── version-gen.go │ └── version.go ├── main.go ├── scripts ├── build_binary.sh └── mockgen.sh └── test.Dockerfile /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: "13:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | bin/ 15 | out/ 16 | *.swp 17 | *.orig 18 | .vscode/* 19 | .idea 20 | .idea/* 21 | .DS_Store 22 | 23 | infra/node_modules 24 | infra/*.js 25 | infra/*.d.ts 26 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: Merge for developers 3 | conditions: 4 | - base~=(mainline|rename-commands) 5 | - "#approved-reviews-by>=2" 6 | - approved-reviews-by=@awslabs/developer-experience 7 | - -approved-reviews-by~=author 8 | - -label~=(WIP|do-not-merge) 9 | - -title~=(WIP|wip) 10 | - -merged 11 | - -closed 12 | - author!=dependabot[bot] 13 | actions: 14 | queue: 15 | name: default 16 | method: squash 17 | commit_message_template: | 18 | {{ title }} (#{{ number }}) 19 | 20 | {{ body }} 21 | - name: Merge for bots 22 | conditions: 23 | - base=mainline 24 | - "#approved-reviews-by>=1" 25 | - "#changes-requested-reviews-by=0" 26 | - author=dependabot[bot] 27 | - -title~=(WIP|wip) 28 | - -label~=(WIP|do-not-merge) 29 | - -merged 30 | - -closed 31 | actions: 32 | review: 33 | type: APPROVE 34 | queue: 35 | name: default 36 | method: squash 37 | commit_message_template: | 38 | {{ title }} (#{{ number }}) 39 | 40 | {{ body }} 41 | 42 | -------------------------------------------------------------------------------- /AGENT_VERSION_COMPATIBILITY: -------------------------------------------------------------------------------- 1 | 1.68.2 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## 1.4.2 5 | * Security - Update security patches (#228) 6 | 7 | ## 1.4.1 8 | * Security - Update security patches (#194) 9 | 10 | ## 1.4.0 11 | * Feature - Add support for ARM64 binaries and docker images (#59) 12 | 13 | ## 1.3.0 14 | * Feature - Add support for V4 and generic metadata endpoints (#38) 15 | 16 | ## 1.2.0 17 | * Feature - Add support for assuming roles in other accounts with path `/role-arn/{role arn}` (#36) 18 | 19 | ## 1.1.0 20 | * Bug - Set expiration timestamp on temporary credentials (#26) 21 | * Feature - Change base image to amazonlinux to support sourcing credentials from an external process (#30) 22 | * Feature - Add support for custom endpoints for STS and IAM (#16) 23 | * Enhancement - Print verbose error messages for credential chain problems (#25) 24 | 25 | ## 1.0.1 26 | * Enhancement - Add custom user agent header for calls made to STS and IAM (#9) 27 | 28 | ## 1.0.0 29 | * Feature - Support vending temporary credentials to containers from a base set of credentials 30 | * Feature - Support vending temporary credentials to containers from an IAM Role 31 | * Feature - [Support Task Metadata V2 Paths](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v2.html): 32 | - Task Metadata - `/v2/metadata` 33 | - Container Metadata - `/v2/metadata/` 34 | - Task Stats - `/v2/stats` 35 | - Container Stats - `/v2/stats/` 36 | * Feature - [Support Task Metadata V3 Paths](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v3.html): 37 | - Container Metadata - `/v3` OR `/v3/containers/` 38 | - Task Metadata - `/v3/task` OR `/v3/containers//task` 39 | - Container Stats - `/v3/stats` OR `/v3/containers//stats` 40 | - Task Stats - `/v3/task/stats` OR `/v3/containers//task/stats` 41 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/awslabs/amazon-ecs-local-container-endpoints/issues), or [recently closed](https://github.com/awslabs/amazon-ecs-local-container-endpoints/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *mainline* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/amazon-ecs-local-container-endpoints/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public GitHub issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/awslabs/amazon-ecs-local-container-endpoints/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use amazonlinux as the base image so that: 2 | # - we have certificates to make calls to the AWS APIs (/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem) 3 | # - it provides 'sh' excutable that is required by aws-sdk-go credential_process 4 | # NOTE: the amazonlinux:2 base image is multi-arch, so docker should be 5 | # able to detect the correct one to use when the image is run 6 | FROM amazonlinux:2 7 | 8 | COPY ["LICENSE", "NOTICE", "THIRD-PARTY", "/"] 9 | 10 | ARG ARCH_DIR 11 | ADD bin/$ARCH_DIR/local-container-endpoints / 12 | 13 | EXPOSE 80 14 | 15 | ENV HOME /home 16 | 17 | CMD ["/local-container-endpoints"] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You 4 | # may not use this file except in compliance with the License. A copy of 5 | # the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is 10 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | # ANY KIND, either express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | ROOT := $(shell pwd) 15 | 16 | all: local-build 17 | 18 | GO_VERSION := 1.17 19 | SCRIPT_PATH := $(ROOT)/scripts/:${PATH} 20 | SOURCES := $(shell find . -name '*.go') 21 | BINARY_NAME := local-container-endpoints 22 | IMAGE_REPO_NAME := amazon/amazon-ecs-local-container-endpoints 23 | LOCAL_BINARY := bin/local/${BINARY_NAME} 24 | 25 | # AMD_DIR and ARM_DIR correspond to ARCH_SUFFIX env var set in each CodeBuild 26 | # project which is used in the image tags. 27 | AMD_DIR := amd64 28 | ARM_DIR := arm64 29 | 30 | AMD_BINARY := bin/${AMD_DIR}/${BINARY_NAME} 31 | ARM_BINARY := bin/${ARM_DIR}/${BINARY_NAME} 32 | VERSION := $(shell cat VERSION) 33 | AGENT_VERSION_COMPATIBILITY := $(shell cat AGENT_VERSION_COMPATIBILITY) 34 | TAG := $(VERSION)-agent$(AGENT_VERSION_COMPATIBILITY)-compatible 35 | 36 | .PHONY: generate 37 | generate: $(SOURCES) 38 | PATH=$(SCRIPT_PATH) go generate ./... 39 | 40 | .PHONY: local-build 41 | local-build: $(LOCAL_BINARY) 42 | 43 | .PHONY: build-local-image 44 | build-local-image: 45 | docker run -v $(shell pwd):/usr/src/app/src/github.com/awslabs/amazon-ecs-local-container-endpoints \ 46 | --workdir=/usr/src/app/src/github.com/awslabs/amazon-ecs-local-container-endpoints \ 47 | --env GOPATH=/usr/src/app \ 48 | --env ECS_RELEASE=cleanbuild \ 49 | golang:$(GO_VERSION) make ${LOCAL_BINARY} 50 | docker build --build-arg ARCH_DIR=local -t $(IMAGE_REPO_NAME):latest-local . 51 | 52 | # Build binaries for each architecture into their own subdirectories 53 | $(LOCAL_BINARY): $(SOURCES) 54 | PATH=${PATH} golint ./local-container-endpoints/... 55 | ./scripts/build_binary.sh ./bin/local 56 | @echo "Built local-container-endpoints" 57 | 58 | $(AMD_BINARY): $(SOURCES) 59 | @mkdir -p ./bin/$(AMD_DIR) 60 | TARGET_GOOS=linux GOARCH=amd64 ./scripts/build_binary.sh ./bin/$(AMD_DIR) 61 | @echo "Built local-container-endpoints for linux-amd64" 62 | 63 | $(ARM_BINARY): $(SOURCES) 64 | @mkdir -p ./bin/$(ARM_DIR) 65 | TARGET_GOOS=linux GOARCH=arm64 ./scripts/build_binary.sh ./bin/$(ARM_DIR) 66 | @echo "Built local-container-endpoints for linux-arm64" 67 | 68 | # Relies on ARCH_SUFFIX environment variable which is set in the build 69 | # environment (e.g. CodeBuild project). Value will either be amd64 or arm64. 70 | .PHONY: build-image 71 | build-image: 72 | docker run -v $(shell pwd):/usr/src/app/src/github.com/awslabs/amazon-ecs-local-container-endpoints \ 73 | --workdir=/usr/src/app/src/github.com/awslabs/amazon-ecs-local-container-endpoints \ 74 | --env GOPATH=/usr/src/app \ 75 | --env ECS_RELEASE=cleanbuild \ 76 | golang:$(GO_VERSION) make bin/${ARCH_SUFFIX}/${BINARY_NAME} 77 | docker build --build-arg ARCH_DIR=$(ARCH_SUFFIX) -t $(IMAGE_REPO_NAME):latest-$(ARCH_SUFFIX) . 78 | docker tag $(IMAGE_REPO_NAME):latest-$(ARCH_SUFFIX) $(IMAGE_REPO_NAME):$(TAG)-$(ARCH_SUFFIX) 79 | docker tag $(IMAGE_REPO_NAME):latest-$(ARCH_SUFFIX) $(IMAGE_REPO_NAME):$(VERSION)-$(ARCH_SUFFIX) 80 | 81 | .PHONY: tag-latest 82 | tag-latest: 83 | docker tag $(IMAGE_REPO_NAME):latest-amd64 $(IMAGE_REPO_NAME):latest 84 | 85 | .PHONY: publish-dockerhub-latest 86 | publish-dockerhub-latest: 87 | docker push $(IMAGE_REPO_NAME):latest 88 | 89 | .PHONY: publish-dockerhub 90 | publish-dockerhub: 91 | docker push $(IMAGE_REPO_NAME):latest-$(ARCH_SUFFIX) 92 | docker push $(IMAGE_REPO_NAME):$(TAG)-$(ARCH_SUFFIX) 93 | docker push $(IMAGE_REPO_NAME):$(VERSION)-$(ARCH_SUFFIX) 94 | 95 | .PHONY: test 96 | test: 97 | # NOTE: unit tests need to run in a linux/windows environment with the current `amazon-ecs-agents` dependency. 98 | # If your dev env is running linux/windows feel free to just: 99 | # go test -timeout=120s -v -cover ./local-container-endpoints/... 100 | docker build -t amazon-ecs-local-container-endpoints-test -f test.Dockerfile . 101 | docker run amazon-ecs-local-container-endpoints-test \ 102 | go test -timeout=120s -v -cover ./local-container-endpoints/... 103 | 104 | .PHONY: functional-test 105 | functional-test: 106 | # NOTE: functional tests need to run in a linux/windows environment with the current `amazon-ecs-agents` dependency. 107 | # If your dev env running linux/windows feel free to just: 108 | # go test -timeout=120s -v -tags functional -cover ./local-container-endpoints/handlers/functional_tests/... 109 | docker build -t amazon-ecs-local-container-endpoints-test -f test.Dockerfile . 110 | docker run amazon-ecs-local-container-endpoints-test \ 111 | go test -timeout=120s -v -tags functional -cover ./local-container-endpoints/handlers/functional_tests/... 112 | 113 | .PHONY: integ 114 | integ: build-local-image 115 | # Prerequisites of running the integration tests: 116 | # 1. Have a [default] profile in ~/.aws 117 | # 2. Have an IAM role named "ecs-local-endpoints-integ-role" in your AWS account and make it assume-able by the [default] profile. 118 | docker build -t amazon-ecs-local-container-endpoints-integ-test:latest -f ./integ/Dockerfile . 119 | docker-compose --file ./integ/docker-compose.yml up --abort-on-container-exit 120 | 121 | .PHONY: verify 122 | verify: 123 | docker pull $(IMAGE_REPO_NAME):latest-$(ARCH_SUFFIX) 124 | docker run -d -p 8000:80 -v /var/run:/var/run -v $(HOME)/.aws/:/home/.aws/ -e "ECS_LOCAL_METADATA_PORT=80" -e "HOME=/home" -e "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=${AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}" --name endpoints $(IMAGE_REPO_NAME):latest-$(ARCH_SUFFIX) 125 | curl -s localhost:8000/creds 126 | curl -s localhost:8000/v2/stats 127 | curl -s localhost:8000/v2/metadata 128 | curl -s localhost:8000/v3 129 | curl -s localhost:8000/v4 130 | docker stop endpoints && docker rm endpoints 131 | docker pull $(IMAGE_REPO_NAME):$(TAG)-$(ARCH_SUFFIX) 132 | docker run -d -p 8000:80 -v /var/run:/var/run -v $(HOME)/.aws/:/home/.aws/ -e "ECS_LOCAL_METADATA_PORT=80" -e "HOME=/home" -e "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=${AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}" --name endpoints $(IMAGE_REPO_NAME):$(TAG)-$(ARCH_SUFFIX) 133 | curl -s localhost:8000/creds 134 | curl -s localhost:8000/v2/stats 135 | curl -s localhost:8000/v3 136 | curl -s localhost:8000/v4 137 | docker stop endpoints && docker rm endpoints 138 | docker pull $(IMAGE_REPO_NAME):$(VERSION)-$(ARCH_SUFFIX) 139 | docker run -d -p 8000:80 -v /var/run:/var/run -v $(HOME)/.aws/:/home/.aws/ -e "ECS_LOCAL_METADATA_PORT=80" -e "HOME=/home" -e "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=${AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}" --name endpoints $(IMAGE_REPO_NAME):$(VERSION)-$(ARCH_SUFFIX) 140 | curl -s localhost:8000/creds 141 | curl -s localhost:8000/v2/stats 142 | curl -s localhost:8000/v2/metadata 143 | curl -s localhost:8000/v3 144 | curl -s localhost:8000/v4 145 | docker stop endpoints && docker rm endpoints 146 | 147 | .PHONY: clean 148 | clean: 149 | rm bin/local/local-container-endpoints 150 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Amazon Ecs Local Container Endpoints 2 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Amazon ECS Local Container Endpoints 2 | ==================================== 3 | 4 | A container that provides local versions of the [ECS Task IAM Roles endpoint](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html) and the [ECS Task Metadata Endpoints](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint.html). This project will help you test applications locally before you deploy to ECS/Fargate. 5 | 6 | This repository contains the source code for the project. To use it, pull the [amazon/amazon-ecs-local-container-endpoints:latest image from ECR](https://gallery.ecr.aws/ecs-local/amazon-ecs-local-container-endpoints). 7 | 8 | #### Table of Contents 9 | * [Tutorial](https://aws.amazon.com/blogs/compute/a-guide-to-locally-testing-containers-with-amazon-ecs-local-endpoints-and-docker-compose/) 10 | * [Setup Networking](docs/setup-networking.md) 11 | * [Option 1: Use a User Defined Docker Bridge Network](docs/setup-networking.md#option-1-use-a-user-defined-docker-bridge-network-recommended) 12 | * [Option 2: Set up iptables rules](docs/setup-networking.md#option-2-set-up-iptables-rules) 13 | * [Configuration](docs/configuration.md) 14 | * [Credentials](docs/configuration.md#credentials) 15 | * [Custom IAM and STS Endpoints](docs/configuration.md#custom-iam-and-sts-endpoints) 16 | * [Docker](docs/configuration.md#docker) 17 | * [Environment Variables](docs/configuration.md#environment-variables) 18 | * [Features](docs/features.md) 19 | * [Vend Credentials to Containers](docs/features.md#vend-credentials-to-containers) 20 | * [Metadata](docs/features.md#metadata) 21 | * [Task Metadata V2](docs/features.md#task-metadata-v2) 22 | * [Task Metadata V3](docs/features.md#task-metadata-v3) 23 | * [Task Metadata V4](docs/features.md#task-metadata-v4) 24 | * [Generic Metadata](docs/features.md#generic-metadata-injection) 25 | 26 | #### Security disclosures 27 | 28 | If you think you’ve found a potential security issue, please do not post it in the Issues. Instead, please follow the instructions [here](https://aws.amazon.com/security/vulnerability-reporting/) or email AWS security directly at [aws-security@amazon.com](mailto:aws-security@amazon.com). 29 | 30 | #### License 31 | 32 | This library is licensed under the Apache 2.0 License. 33 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.4.2 2 | -------------------------------------------------------------------------------- /buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | env: 4 | secrets-manager: 5 | USERNAME: "com.amazonaws.ec2.madison.dockerhub.amazon-ecs-local-container-endpoints.credentials:username" 6 | PASSWORD: "com.amazonaws.ec2.madison.dockerhub.amazon-ecs-local-container-endpoints.credentials:password" 7 | 8 | phases: 9 | install: 10 | commands: 11 | - echo '#!/bin/bash' > /usr/local/bin/ok; echo 'if [[ "$CODEBUILD_BUILD_SUCCEEDING" == "0" ]]; then exit 1; else exit 0; fi' >> /usr/local/bin/ok; chmod +x /usr/local/bin/ok 12 | pre_build: 13 | commands: 14 | - echo "Logging into DockerHub..." 15 | - docker login -u ${USERNAME} --password ${PASSWORD} 16 | build: 17 | # build and tag docker image. This will read ARCH_SUFFIX env var set in the 18 | # Codebuild project. 19 | commands: 20 | - echo Build started on `date` 21 | - echo Building Docker image... 22 | - make build-image 23 | - make publish-dockerhub 24 | - | 25 | if [ $ARCH_SUFFIX = "amd64" ]; then 26 | make tag-latest 27 | make publish-dockerhub-latest 28 | fi 29 | post_build: 30 | commands: 31 | - ok && echo Build completed on `date` 32 | -------------------------------------------------------------------------------- /buildspec_verify.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | env: 4 | secrets-manager: 5 | USERNAME: "com.amazonaws.ec2.madison.dockerhub.amazon-ecs-local-container-endpoints.credentials:username" 6 | PASSWORD: "com.amazonaws.ec2.madison.dockerhub.amazon-ecs-local-container-endpoints.credentials:password" 7 | 8 | phases: 9 | install: 10 | commands: 11 | - echo '#!/bin/bash' > /usr/local/bin/ok; echo 'if [[ "$CODEBUILD_BUILD_SUCCEEDING" == "0" ]]; then exit 1; else exit 0; fi' >> /usr/local/bin/ok; chmod +x /usr/local/bin/ok 12 | pre_build: 13 | commands: 14 | - echo "Logging into DockerHub..." 15 | - docker login -u ${USERNAME} --password ${PASSWORD} 16 | build: 17 | commands: 18 | - echo "Verifying Docker images..." 19 | - make verify 20 | post_build: 21 | commands: 22 | - ok && echo Build completed on `date` 23 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | ## Configuration 2 | 3 | ### Credentials 4 | 5 | The ECS Local Endpoints container uses the AWS SDK for Go, and thus it supports all of its [methods of configuration](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html). We recommend providing credentials via an AWS CLI Profile. To do this, mount `$HOME/.aws/` ([`%UserProfile%\.aws` on Windows](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html)) into the container. As shown in the example Compose file, the container path of the volume should be `/home/.aws/` because the environment variable `HOME` is set to `/home` in the image. This way, inside the container, the SDK will be able to find credentials at `$HOME/.aws/`. To use a non-default profile, set the `AWS_PROFILE` environment variable on the Local Endpoints container. 6 | 7 | The Local Endpoints container will retrieve temporary session credentials from STS. To provide a custom CA bundle for the STS client, mount your certificates file into the Local Endpoints container at any of the following locations: 8 | * `/etc/ssl/certs/ca-certificates.crt` 9 | * `/etc/pki/tls/certs/ca-bundle.crt` 10 | * `/etc/ssl/ca-bundle.pem` 11 | * `/etc/pki/tls/cacert.pem` 12 | * `/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem` 13 | 14 | For example, on an Ubuntu machine, you can mount your machine's certificates file at `/etc/ssl/certs/ca-certificates.crt` into the Local Endpoint container at `/etc/ssl/certs/ca-certificates.crt`. 15 | 16 | ### Custom IAM and STS Endpoints 17 | 18 | Local Endpoints can be configured to use custom IAM and STS endpoints. Simply define the `IAM_ENDPOINT` and `STS_ENDPOINT` environment variables in the Local Endpoints container. 19 | 20 | This may be useful in scenarios where your application container is configured to obtain credentials from ECS (see [Vend Credentials to Containers](features.md#vend-credentials-to-containers)), but you do not want to provide Local Endpoints with AWS credentials. Providing an IAM and STS simulator and configuring the Local Endpoints container with custom IAM and STS endpoints enables testing without an AWS account. 21 | 22 | See [`docker-compose.localstack.yml`](../examples/docker-compose.localstack.yml) for an example. 23 | 24 | ### Docker 25 | 26 | Local Endpoints responds to Metadata requests with real data about the containers running on your machine. In order to do this, you must mount the [Docker socket](https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-socket-option) into the container. Make sure the Local Endpoints container is given a volume with source path `/var/run` and container path `/var/run`. 27 | 28 | ### Environment Variables 29 | 30 | General Configuration: 31 | * `ECS_LOCAL_METADATA_PORT` - Set the port that the container listens at. The default is `80`. 32 | * `IAM_ENDPOINT` - Set the endpoint used by the AWS SDK for IAM. The default is undefined, which results in using the default AWS region. 33 | * `STS_ENDPOINT` - Set the endpoint used by the AWS SDK for STS. The default is undefined, which results in using the default AWS region. 34 | 35 | Task Metadata Configuration: while Local Endpoints returns real runtime information obtained from Docker in metadata requests, some values have no relevance locally and are mocked: 36 | * `CLUSTER_ARN` - Set the 'cluster' name which is returned in Task Metadata responses. Default: `ecs-local-cluster`. 37 | * `TASK_ARN` - Set ARN of the mock local 'task' which your containers will appear to be part of in Task Metadata responses. Default: `arn:aws:ecs:us-west-2:111111111111:task/ecs-local-cluster/37e873f6-37b4-42a7-af47-eac7275c6152`. 38 | * `TASK_DEFINITION_FAMILY` - Set family name for the mock task definition which your containers will appear to be part of in Task Metadata responses. Default: `esc-local-task-definition`. 39 | * `TASK_DEFINITION_REVISION` - Set the Task Definition revision. Default: `1`. 40 | 41 | Credentials Configuration: 42 | * `SHARED_TOKEN_EXPIRATION` - Set an expiration duration (quantity + unit) for shared credentials when a session token is provided. This provides a hint for clients to refresh their credentials periodically. The default is 750s (12.5 minutes), which results in some clients (notably Boto3) opportunistically refreshing credentials in a background thread. 43 | -------------------------------------------------------------------------------- /docs/features.md: -------------------------------------------------------------------------------- 1 | ## Features 2 | 3 | ### Vend Credentials to Containers 4 | 5 | The AWS CLI, and all of the AWS SDKs, will look for the environment variable `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` as part of their [default credential provider chain](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html#credentials-default). 6 | 7 | If the variable exists, then the SDKs will try to obtain credentials by making requests to `http://169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI`. The ECS Agent injects this environment variable into containers running on ECS, and responds to requests at the endpoint. This is how [IAM Roles for Tasks](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html) is implemented under the hood. 8 | 9 | You can set AWS_CONTAINER_CREDENTIALS_RELATIVE_URI to one of three different values on your application container: 10 | * `"/creds"` - With this value, Local Endpoints returns temporary credentials obtained by calling [sts:GetSessionToken](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_request.html#stsapi_comparison). These credentials will have the same permissions as the base credentials given to the Local Endpoints container, with a few exceptions. **The returned credentials will not be able to access the IAM APIs or the STS APIs**, except for sts:AssumeRole and sts:GetCallerIdentity. 11 | * `"/role/{role name}"` - With this value, your application container receives credentials obtained via assuming the given role name. This could be a Task IAM Role, or it could be any other IAM Role. The role must exist in the same AWS account as for your default credentials. 12 | * `"/role-arn/{role arn}"` - With this value, your application container receives credentials obtained via assuming the given role arn. This could be a Task IAM Role, or it could be any other IAM Role. Use this format when the role exists in a different AWS account to your default credentials. 13 | 14 | **Note:** *We do not recommend using production credentials or production roles when testing locally. Modifying the trust policy of a production role changes its security boundary. More importantly, using credentials with access to production when testing locally could lead to accidental changes in your production account. We recommend using a separate account for testing.* 15 | 16 | If you use the second or third options, make sure your IAM Role contains the following trust policy: 17 | ``` 18 | { 19 | "Version": "2012-10-17", 20 | "Statement": [ 21 | { 22 | "Effect": "Allow", 23 | "Principal": { 24 | "AWS": "ARN of your IAM User/identity" 25 | }, 26 | "Action": "sts:AssumeRole" 27 | } 28 | ] 29 | } 30 | ``` 31 | 32 | You can obtain the ARN of your local IAM identity by running: 33 | ``` 34 | aws --profile default sts get-caller-identity 35 | ``` 36 | 37 | ### Metadata 38 | 39 | For both V2 and V3, Local Endpoints defines a local 'task' as all containers running in a single Docker Compose project. If your container is running outside of Compose, then all currently running containers on your machine will be considered to be part of one local 'task'. 40 | 41 | #### Task Metadata V2 42 | 43 | No additional configuration is needed beyond that which is mentioned in the [Configuration](#configuration) section. 44 | 45 | #### Task Metadata V3 46 | 47 | V3 Metadata uses the `ECS_CONTAINER_METADATA_URI` environment variable. Unlike V2 metadata and Credentials, the IP address does not have to be `169.254.170.2`. If you only use V3 metadata, then the Local Endpoints container could listen at any IP address. If you choose this option, replace the IP address in the following examples. 48 | 49 | In most cases, you can set `ECS_CONTAINER_METADATA_URI` to `http://169.254.170.2/v3`. 50 | 51 | However, in a few cases, this will not work. This is because the Local Endpoints container needs to be able to determine which container a request for V3 metadata came from. Local Endpoints attempts to use the IP address in the request to determine this. If you use the [example Docker Compose file](../examples/docker-compose.yml) with a bridge network, then this IP lookup will work. However, if you use different network settings, then the Local Endpoints will not be able to determine which container a request came from. In this case, set `ECS_CONTAINER_METADATA_URI` to `http://169.254.170.2/v3/containers/{container name}`. The value for `container name` can be any unique substring of your container's name. By setting a custom request URL, the Local Endpoints container can determine which container a request came from. 52 | 53 | #### Task Metadata V4 54 | 55 | V4 Metadata uses `ECS_CONTAINER_METADATA_URI_V4` environment variable. In most cases, you can set `ECS_CONTAINER_METADATA_URI_V4` to `http://169.254.170.2/v3`. Similarly, in a few cases when Local Endpoints container doesn't know which container a request came from, you have to set `ECS_CONTAINER_METADATA_URI_V4` to `http://169.254.170.2/v3/containers/{container name}`. Please see the description above on V3 to understand why you'll need to specify the container name in the path. 56 | 57 | However, compared to V3, V4 includes additional network metadata when querying the task metadata endpoint (see [here](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v4.html)). Please refer to this [example](../examples/v4) if you want to include those additional V4 metadata. You can use the generic metadata injection feature (described below) to add the additional metadata fields included in V4. 58 | 59 | #### Generic Metadata Injection 60 | 61 | As mentioned above in the previous section, to inject generic metadata, you'll need to have those additional metadata in JSON files. Then specify paths for the JSON files by using `CONTAINER_METADATA_PATH` and `TASK_METADATA_PATH` environment variables. More specifically, `CONTAINER_METADATA_PATH` is the metadata for each container, which will override their counterparts in the normal response. Also, `TASK_METADATA_PATH` is for task level metadata, which is used only for overriding the top level fields in the task metadata response. If you specify both `CONTAINER_METADATA_PATH` and `TASK_METADATA_PATH`, then the metadata from `CONTAINER_METADATA_PATH` will be included in the `Containers` section of the task metadata response. See example for overriding task metadata response [here](../examples/generic). 62 | -------------------------------------------------------------------------------- /docs/setup-networking.md: -------------------------------------------------------------------------------- 1 | ## Setting Up Networking 2 | 3 | ECS Local Container Endpoints supports 4 endpoints: 4 | * The [ECS Task IAM Roles endpoint](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html) 5 | * The [Task Metadata V2 Endpoint](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v2.html) 6 | * The [Task Metadata V3 Endpoint](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v3.html) 7 | * The [Task Metadata V4 Endpoint](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v4.html) 8 | 9 | The Task Metadata V2 and Credentials endpoints require the Local Endpoints container to be able to receive requests made to the special IP Address, `169.254.170.2`. 10 | 11 | There are two methods to achieve this. 12 | 13 | #### Option 1: Use a User Defined Docker Bridge Network (Recommended) 14 | 15 | If you launch containers into a custom [bridge network](https://docs.docker.com/network/bridge/), you can specify that the ECS Local Endpoints container will receive `169.254.170.2` as its IP address in the network. The endpoints will only be reachable inside this network, so all your containers must run inside of it. The [example Docker Compose file](../examples/docker-compose.yml) in this repository shows how to create this network using Compose. 16 | 17 | This method is the recommended way of using ECS Local Container Endpoints. 18 | 19 | #### Option 2: Set up iptables rules 20 | 21 | If you use Linux, then you can set up routing rules to forward requests for `169.254.170.2`. This is the option used in production ECS, as noted in the [documentation](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html). The following commands must be run to set up routing rules: 22 | 23 | ``` 24 | sudo sysctl -w net.ipv4.conf.all.route_localnet=1 25 | sudo iptables -t nat -A PREROUTING -p tcp -d 169.254.170.2 --dport 80 -j DNAT --to-destination 127.0.0.1:51679 26 | sudo iptables -t nat -A OUTPUT -d 169.254.170.2 -p tcp -m tcp --dport 80 -j REDIRECT --to-ports 51679 27 | sudo iptables-save 28 | ``` 29 | 30 | These commands enable local routing, and create a rule to forward packets sent to `169.254.170.2:80` to `127.0.0.1:51679`. 31 | 32 | Once you set up these rules, you can run the Local Endpoints container as follows: 33 | 34 | ``` 35 | docker run -d -p 51679:51679 \ 36 | -v /var/run:/var/run \ 37 | -v $HOME/.aws/:/home/.aws/ \ 38 | -e "ECS_LOCAL_METADATA_PORT=51679" \ 39 | --name ecs-local-endpoints \ 40 | amazon/amazon-ecs-local-container-endpoints:latest-amd64 41 | ``` 42 | -------------------------------------------------------------------------------- /examples/docker-compose.localstack.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | networks: 4 | # This is the default network. It is used for normal inter-container 5 | # communications. 6 | default: 7 | driver: bridge 8 | ipam: 9 | driver: default 10 | # This special network is configured so that the local metadata 11 | # service can bind to the specific IP address that ECS uses 12 | # in production 13 | credentials_network: 14 | driver: bridge 15 | ipam: 16 | config: 17 | - subnet: "169.254.170.0/24" 18 | gateway: 169.254.170.1 19 | 20 | services: 21 | # This container simulates IAM and STS and vends (fake) credentials 22 | localstack: 23 | environment: 24 | - DEFAULT_REGION=us-east-1 25 | - SERVICES=iam,sts 26 | image: localstack/localstack 27 | networks: 28 | - default 29 | ports: 30 | - '4592:4592' 31 | - '4593:4593' 32 | volumes: 33 | - /var/run/docker.sock:/var/run/docker.sock 34 | 35 | # This container vends credentials to your containers by obtaining (fake) 36 | # credentials from the localstack container. Take note that a real AWS account 37 | # is not necessary when using these simulated endpoints. 38 | ecs-local-endpoints: 39 | # The Amazon ECS Local Container Endpoints Docker Image 40 | image: amazon/amazon-ecs-local-container-endpoints 41 | depends_on: 42 | - localstack 43 | volumes: 44 | # Mount /var/run so we can access docker.sock and talk to Docker 45 | - /var/run:/var/run 46 | environment: 47 | # The simulated IAM and STS endpoints will accept any access key 48 | # and secret key. These values may simply be left as dummy values. 49 | - AWS_ACCESS_KEY_ID=accessKey 50 | - AWS_REGION=us-east-1 51 | - AWS_SECRET_ACCESS_KEY=secretAccessKey 52 | - IAM_CUSTOM_ENDPOINT=http://localstack:4593 53 | - STS_CUSTOM_ENDPOINT=http://localstack:4592 54 | networks: 55 | # This is connected to both networks. This makes Local Endpoints 56 | # accessible on the well-known IP address, and it enables communication 57 | # with the localstack container. 58 | credentials_network: 59 | # This special IP address is recognized by the AWS SDKs and AWS CLI 60 | ipv4_address: "169.254.170.2" 61 | default: 62 | 63 | # Here we configure the application container that we are testing 64 | # You can test multiple containers at a time, simply duplicate this section 65 | # and customize it for each container, and give it a unique IP in 'credentials_network'. 66 | app: 67 | depends_on: 68 | - ecs-local-endpoints 69 | networks: 70 | credentials_network: 71 | ipv4_address: "169.254.170.3" 72 | environment: 73 | - AWS_DEFAULT_REGION=us-east-1 74 | # This ENV VAR enables credentials 75 | - AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=/creds 76 | # Enables V3 Metadata 77 | - ECS_CONTAINER_METADATA_URI=http://169.254.170.2/v3 78 | -------------------------------------------------------------------------------- /examples/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | networks: 3 | # This special network is configured so that the local metadata 4 | # service can bind to the specific IP address that ECS uses 5 | # in production 6 | credentials_network: 7 | driver: bridge 8 | ipam: 9 | config: 10 | - subnet: "169.254.170.0/24" 11 | gateway: 169.254.170.1 12 | services: 13 | # This container vends credentials to your containers 14 | ecs-local-endpoints: 15 | # The Amazon ECS Local Container Endpoints Docker Image 16 | image: amazon/amazon-ecs-local-container-endpoints 17 | volumes: 18 | # Mount /var/run so we can access docker.sock and talk to Docker 19 | - /var/run:/var/run 20 | # Mount the shared configuration directory, used by the AWS CLI and AWS SDKs 21 | # On Windows, this directory can be found at "%UserProfile%\.aws" 22 | # In the endpoints image, $HOME is set to /home. 23 | - $HOME/.aws/:/home/.aws/ 24 | environment: 25 | # You can change which AWS CLI Profile is used 26 | AWS_PROFILE: "default" 27 | networks: 28 | credentials_network: 29 | # This special IP address is recognized by the AWS SDKs and AWS CLI 30 | ipv4_address: "169.254.170.2" 31 | 32 | # Here we configure the application container that we are testing 33 | # You can test multiple containers at a time, simply duplicate this section 34 | # and customize it for each container, and give it a unique IP in 'credentials_network'. 35 | app: 36 | depends_on: 37 | - ecs-local-endpoints 38 | networks: 39 | credentials_network: 40 | ipv4_address: "169.254.170.3" 41 | environment: 42 | AWS_DEFAULT_REGION: "us-east-1" 43 | # This ENV VAR enables credentials 44 | # Set it to "/creds" or "/role/" 45 | AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: "/creds" 46 | # Enables V3 Metadata 47 | ECS_CONTAINER_METADATA_URI: "http://169.254.170.2/v3" 48 | -------------------------------------------------------------------------------- /examples/generic/task-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "Cluster": "arn:aws:ecs:us-west-2:ExampleAWSAccountNo1;:cluster/default", 3 | "TaskARN": "arn:aws:ecs:us-west-2:ExampleAWSAccountNo1;:task/default/febee046097849aba589d4435207c04a", 4 | "Family": "query-metadata", 5 | "Revision": "7", 6 | "DesiredStatus": "RUNNING", 7 | "KnownStatus": "RUNNING", 8 | "Limits": { 9 | "CPU": 0.25, 10 | "Memory": 512 11 | }, 12 | "PullStartedAt": "2020-03-26T22:25:40.420726088Z", 13 | "PullStoppedAt": "2020-03-26T22:26:22.235177616Z", 14 | "AvailabilityZone": "us-west-2c" 15 | } 16 | -------------------------------------------------------------------------------- /examples/v4/container-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "Networks": [ 3 | { 4 | "NetworkMode": "awsvpc", 5 | "IPv4Addresses": [ 6 | "10.0.0.61" 7 | ], 8 | "AttachmentIndex": 0, 9 | "IPv4SubnetCIDRBlock": "10.0.0.0/24", 10 | "MACAddress": "0a:d2:d0:80:b6:b4", 11 | "DomainNameServers": [ 12 | "10.0.0.2" 13 | ], 14 | "DomainNameSearchList": [ 15 | "us-west-2.compute.internal" 16 | ], 17 | "PrivateDNSName": "ip-10-0-0-61.us-west-2.compute.internal", 18 | "SubnetGatewayIpv4Address": "" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/awslabs/amazon-ecs-local-container-endpoints 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/aws/amazon-ecs-agent/agent v0.0.0-20221019221528-5fb8e801e9d1 7 | github.com/aws/aws-sdk-go v1.44.204 8 | github.com/docker/docker v25.0.0-beta.1+incompatible 9 | github.com/fatih/structs v1.1.0 10 | github.com/golang/mock v1.6.0 11 | github.com/gorilla/mux v1.8.0 12 | github.com/peterbourgon/mergemap v0.0.0-20130613134717-e21c03b7a721 13 | github.com/pkg/errors v0.9.1 14 | github.com/sirupsen/logrus v1.9.3 15 | github.com/stretchr/testify v1.8.4 16 | ) 17 | 18 | require ( 19 | github.com/Microsoft/go-winio v0.5.2 // indirect 20 | github.com/awslabs/go-config-generator-for-fluentd-and-fluentbit v0.0.0-20190829210224-55d4fd2e6f35 // indirect 21 | github.com/beorn7/perks v1.0.1 // indirect 22 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 23 | github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect 24 | github.com/cilium/ebpf v0.6.2 // indirect 25 | github.com/containerd/cgroups v1.0.4-0.20220221221032-e710ed6ebb1a // indirect 26 | github.com/containerd/log v0.1.0 // indirect 27 | github.com/containernetworking/cni v0.8.1 // indirect 28 | github.com/containernetworking/plugins v0.9.1 // indirect 29 | github.com/coreos/go-systemd/v22 v22.3.2 // indirect 30 | github.com/davecgh/go-spew v1.1.1 // indirect 31 | github.com/distribution/reference v0.5.0 // indirect 32 | github.com/docker/go-connections v0.4.0 // indirect 33 | github.com/docker/go-units v0.4.0 // indirect 34 | github.com/felixge/httpsnoop v1.0.4 // indirect 35 | github.com/fsnotify/fsnotify v1.5.4 // indirect 36 | github.com/go-logr/logr v1.3.0 // indirect 37 | github.com/go-logr/stdr v1.2.2 // indirect 38 | github.com/godbus/dbus/v5 v5.0.6 // indirect 39 | github.com/gogo/protobuf v1.3.2 // indirect 40 | github.com/golang/protobuf v1.5.3 // indirect 41 | github.com/google/uuid v1.3.1 // indirect 42 | github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95 // indirect 43 | github.com/jmespath/go-jmespath v0.4.0 // indirect 44 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 45 | github.com/moby/term v0.5.0 // indirect 46 | github.com/opencontainers/go-digest v1.0.0 // indirect 47 | github.com/opencontainers/image-spec v1.0.2 // indirect 48 | github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 // indirect 49 | github.com/pborman/uuid v1.2.0 // indirect 50 | github.com/pmezard/go-difflib v1.0.0 // indirect 51 | github.com/prometheus/client_golang v1.11.1 // indirect 52 | github.com/prometheus/client_model v0.2.0 // indirect 53 | github.com/prometheus/common v0.26.0 // indirect 54 | github.com/prometheus/procfs v0.6.0 // indirect 55 | github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852 // indirect 56 | github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect 57 | go.etcd.io/bbolt v1.3.6 // indirect 58 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect 59 | go.opentelemetry.io/otel v1.21.0 // indirect 60 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 // indirect 61 | go.opentelemetry.io/otel/metric v1.21.0 // indirect 62 | go.opentelemetry.io/otel/sdk v1.21.0 // indirect 63 | go.opentelemetry.io/otel/trace v1.21.0 // indirect 64 | golang.org/x/net v0.23.0 // indirect 65 | golang.org/x/sys v0.18.0 // indirect 66 | golang.org/x/text v0.14.0 // indirect 67 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect 68 | google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect 69 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect 70 | google.golang.org/protobuf v1.33.0 // indirect 71 | gopkg.in/yaml.v2 v2.4.0 // indirect 72 | gopkg.in/yaml.v3 v3.0.1 // indirect 73 | gotest.tools/v3 v3.0.3 // indirect 74 | ) 75 | -------------------------------------------------------------------------------- /infra/README.md: -------------------------------------------------------------------------------- 1 | # Continuous delivery pipelines 2 | 3 | This package uses the [AWS Cloud Development Kit (CDK)](https://github.com/awslabs/aws-cdk) to model AWS CodePipeline 4 | pipelines and to provision them with AWS CloudFormation. 5 | 6 | * pipeline.ts: Builds and publishes the base Docker image for amazon/amazon-ecs-local-container-endpoints. 7 | 8 | This creates a CodePipeline pipeline which consists of a source stage that uses a GitHub webhook, and build stages that 9 | use AWS CodeBuild to build, publish and verify Docker images for both amd64 and arm64 architectures to DockerHub. 10 | 11 | +------------+ +----------------------------+ +-----------------------------+ 12 | | SOURCE | | BUILD | | VERIFY | 13 | +------------+ +-------------+--------------+ +--------------+--------------+ 14 | | | | AMD64 | ARM64 | | AMD64 | ARM64 | 15 | | | +-------------+--------------+ +--------------+--------------+ 16 | | GitHub | |docker build | docker build | | docker pull | docker pull | 17 | | webhook |+----->| | |+----->| | | 18 | | | | docker push | docker push | |verify endpts | verify endpts| 19 | | | | | | | | | 20 | +------------+ +-------------+--------------+ +--------------+--------------+ 21 | 22 | ## GitHub Access Token 23 | To release using this pipeline, we use the release account for this repo 24 | (ecs-local-container-endpoints+release@amazon.com). 25 | 26 | The source stage requires a GitHub [personal access token](https://github.com/settings/tokens). For the official 27 | release, this token is owned by the [ecs-cicd-bot account]( https://github.com/ecs-cicd-bot) and is stored in secrets 28 | manager. 29 | 30 | If you want to release a fork of this repo, create a GitHub access token with access to your fork of the repo, including 31 | "admin:repo_hook" and "repo" permissions. Then store the token in Secrets Manager: 32 | 33 | ``` 34 | aws secretsmanager create-secret --region us-west-2 --name EcsDevXGitHubToken --secret-string 35 | ``` 36 | 37 | ## Deploy 38 | Our current release treats the pipeline as a one-time build and push. Extending this pipeline to support full CI is 39 | planned (TBD). Deploying the pipeline stack will build and release the current version of this repo to DockerHub. 40 | 41 | To deploy this pipeline, make sure you have the AWS CDK CLI installed: `npm i -g aws-cdk` 42 | 43 | From the `infra` folder, install and build everything: `npm install && npm run build` 44 | 45 | Using temporary credentials from the release account (ecs-local-container-endpoints+release@amazon.com), deploy the 46 | pipeline stack: 47 | 48 | *NOTE*: You may need to set the `CDK_DEFAULT_ACCOUNT` environment variable to the release account ID locally. 49 | 50 | ``` 51 | cdk deploy --app 'node pipeline.js' 52 | ``` 53 | 54 | See the pipeline in the CodePipeline console. 55 | 56 | Once the pipeline run succeeds, destroy the stack: 57 | 58 | ``` 59 | cdk destroy --app 'node pipeline.js' 60 | ``` 61 | 62 | **NOTE**: When developing and testing, remember that any changes to `pipeline.ts` will require the stack to be re-build 63 | with `npm run build` and redeployed with `cdk deploy --app 'node pipeline.js'` 64 | 65 | -------------------------------------------------------------------------------- /infra/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "local-container-endpoints-pipeline", 3 | "version": "1.0.0", 4 | "description": "Pipeline for publishing Docker image for local container endpoints", 5 | "private": true, 6 | "scripts": { 7 | "build": "tsc", 8 | "watch": "tsc -w", 9 | "cdk": "cdk" 10 | }, 11 | "author": { 12 | "name": "Amazon Web Services", 13 | "url": "https://aws.amazon.com", 14 | "organization": true 15 | }, 16 | "license": "Apache-2.0", 17 | "devDependencies": { 18 | "@types/node": "^12.15.0", 19 | "typescript": "~3.8.3" 20 | }, 21 | "dependencies": { 22 | "@aws-cdk/aws-codebuild": "*", 23 | "@aws-cdk/aws-codepipeline": "*", 24 | "@aws-cdk/aws-codepipeline-actions": "*", 25 | "@aws-cdk/aws-ec2": "*", 26 | "@aws-cdk/aws-ecr": "*", 27 | "@aws-cdk/aws-ecs": "*", 28 | "@aws-cdk/aws-iam": "*", 29 | "@aws-cdk/core": "*" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /infra/pipeline.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import codebuild = require('@aws-cdk/aws-codebuild'); 3 | import codepipeline = require('@aws-cdk/aws-codepipeline'); 4 | import actions = require('@aws-cdk/aws-codepipeline-actions'); 5 | import iam = require('@aws-cdk/aws-iam'); 6 | import cdk = require('@aws-cdk/core'); 7 | 8 | /** 9 | * Simple three-stage pipeline to build the base image for the local container endpoints image. 10 | * [GitHub source] -> [CodeBuild build, pushes image to DockerHub] -> [CodeBuild Verify, verifies the pushed images] 11 | * 12 | * TODO: use Docker manifest to create manifest for images under a common tag and ECR public 13 | */ 14 | class EcsLocalContainerEndpointsImagePipeline extends cdk.Stack { 15 | constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { 16 | super(parent, name, props); 17 | 18 | // Instantiate pipeline 19 | const pipeline = new codepipeline.Pipeline(this, 'Pipeline', { 20 | pipelineName: 'local-container-endpoints-image', 21 | }); 22 | 23 | // Source stage 24 | // Secret under ecs-local-container-endpoints+release@amazon.com 25 | const githubAccessToken = cdk.SecretValue.secretsManager('EcsDevXGitHubToken'); 26 | 27 | const sourceOutput = new codepipeline.Artifact('SourceArtifact'); 28 | const sourceAction = new actions.GitHubSourceAction({ 29 | actionName: 'GitHubSource', 30 | owner: 'awslabs', 31 | repo: 'amazon-ecs-local-container-endpoints', 32 | oauthToken: githubAccessToken, 33 | branch: 'mainline', 34 | output: sourceOutput 35 | }); 36 | 37 | pipeline.addStage({ 38 | stageName: 'Source', 39 | actions: [sourceAction], 40 | }); 41 | 42 | // Build stage containing build project for each architecture image 43 | const buildStage = pipeline.addStage({ 44 | stageName: 'Build', 45 | }); 46 | 47 | const verifyStage = pipeline.addStage({ 48 | stageName: 'Verify', 49 | }); 50 | 51 | const platforms = [ 52 | {'arch': 'amd64', 'buildImage': codebuild.LinuxBuildImage.AMAZON_LINUX_2_3}, 53 | {'arch': 'arm64', 'buildImage': codebuild.LinuxBuildImage.AMAZON_LINUX_2_ARM}, 54 | ]; 55 | 56 | // Create build and verify project for each platform 57 | for (const platform of platforms) { 58 | const arch = platform['arch']; 59 | 60 | const buildProject = new codebuild.PipelineProject(this, `BuildImage-${arch}`, { 61 | buildSpec: codebuild.BuildSpec.fromSourceFilename('./buildspec.yml'), 62 | environment: { 63 | buildImage: platform['buildImage'], 64 | privileged: true, 65 | environmentVariables: { 66 | ARCH_SUFFIX: { value: arch }, 67 | } 68 | } 69 | }); 70 | 71 | const verifyProject = new codebuild.PipelineProject(this, `VerifyImage-${arch}`, { 72 | buildSpec: codebuild.BuildSpec.fromSourceFilename('./buildspec_verify.yml'), 73 | environment: { 74 | buildImage: platform['buildImage'], 75 | privileged: true, 76 | environmentVariables: { 77 | ARCH_SUFFIX: { value: arch }, 78 | } 79 | } 80 | }); 81 | 82 | buildProject.addToRolePolicy(new iam.PolicyStatement({ 83 | actions: [ 84 | "secretsmanager:GetSecretValue", 85 | "sts:GetServiceBearerToken", 86 | "sts:AssumeRole", 87 | ], 88 | resources: [`arn:aws:secretsmanager:us-west-2:${process.env['CDK_DEFAULT_ACCOUNT']}:secret:com.amazonaws.ec2.madison.dockerhub.amazon-ecs-local-container-endpoints.credentials-XIxFhP`] 89 | })); 90 | 91 | verifyProject.addToRolePolicy(new iam.PolicyStatement({ 92 | actions: [ 93 | "secretsmanager:GetSecretValue", 94 | "sts:GetServiceBearerToken", 95 | "sts:AssumeRole", 96 | ], 97 | resources: [`arn:aws:secretsmanager:us-west-2:${process.env['CDK_DEFAULT_ACCOUNT']}:secret:com.amazonaws.ec2.madison.dockerhub.amazon-ecs-local-container-endpoints.credentials-XIxFhP`] 98 | })); 99 | 100 | const buildAction = new actions.CodeBuildAction({ 101 | actionName: `Build-${platform['arch']}`, 102 | project: buildProject, 103 | input: sourceOutput 104 | }); 105 | 106 | const verifyAction = new actions.CodeBuildAction({ 107 | actionName: `Verify-${platform['arch']}`, 108 | project: verifyProject, 109 | input: sourceOutput 110 | }); 111 | 112 | // Add build action for each platform to the build stage 113 | buildStage.addAction(buildAction); 114 | verifyStage.addAction(verifyAction); 115 | } 116 | } 117 | 118 | // TODO Build stage for creating manifest 119 | } 120 | 121 | const app = new cdk.App(); 122 | 123 | new EcsLocalContainerEndpointsImagePipeline(app, 'EcsLocalContainerEndpointsImagePipeline', { 124 | env: { 125 | account: process.env['CDK_DEFAULT_ACCOUNT'], 126 | region: 'us-west-2' 127 | }, 128 | tags: { 129 | project: "amazon-ecs-local-container-endpoints" 130 | } 131 | }); 132 | app.synth(); 133 | -------------------------------------------------------------------------------- /infra/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target":"ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2016", "es2017.object", "es2017.string"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": false, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization":false 20 | }, 21 | "exclude": ["cdk.out"] 22 | } 23 | -------------------------------------------------------------------------------- /integ/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17 2 | 3 | WORKDIR /go/src/github.com/awslabs/amazon-ecs-local-container-endpoints 4 | 5 | COPY go.mod go.sum ./ 6 | ARG GOPROXY=direct 7 | RUN go mod download # The first build will take 2~3 minutes but will be cached for future builds. 8 | 9 | COPY . . 10 | CMD GO111MODULE=on go test -timeout=120s -v -cover ./integ/... || { echo 'Integration Test Failure' ; exit 1; } 11 | -------------------------------------------------------------------------------- /integ/README.md: -------------------------------------------------------------------------------- 1 | ### Setup for running integration tests 2 | 3 | The Local Credentials Service Integration tests use your local `default` AWS Profile for base credentials. 4 | 5 | To test shared temporary credentials (i.e. role-based credentials stored in the 6 | `~/aws/.credentials` file), ensure the `default` profile contains an 7 | `aws_session_token` line containing the session token. 8 | 9 | Create an IAM role named `ecs-local-endpoints-integ-role`. 10 | Attach the `AmazonS3ReadOnlyAccess` policy to it. (This includes the required `"s3:List*"` permissions). 11 | 12 | The trust policy should be the following: 13 | 14 | { 15 | "Version": "2012-10-17", 16 | "Statement": [ 17 | { 18 | "Effect": "Allow", 19 | "Principal": { 20 | "AWS": 21 | }, 22 | "Action": "sts:AssumeRole" 23 | } 24 | ] 25 | } 26 | 27 | You can get the ARN of your local default AWS Profile with: 28 | ``` 29 | aws sts get-caller-identity 30 | ``` 31 | -------------------------------------------------------------------------------- /integ/credentials/credentials_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | // These tests create a session and make a request to AWS to verify that the SDK can uptake credentials 15 | // from the local credentials service. 16 | package credentials 17 | 18 | import ( 19 | "os" 20 | "testing" 21 | 22 | "github.com/aws/aws-sdk-go/aws/session" 23 | "github.com/aws/aws-sdk-go/service/s3" 24 | "github.com/stretchr/testify/assert" 25 | ) 26 | 27 | // The AWS SDKs are designed to request credentials from 28 | // http://169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI 29 | // In the Docker Compose file for this integ test 30 | // We give the endpoints container IP to 169.254.170.2 31 | 32 | func TestCredentials_TemporaryCredentials(t *testing.T) { 33 | os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/creds") 34 | defer os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "") 35 | sess, err := session.NewSession() 36 | assert.NoError(t, err, "Unexpected error creating an SDK session") 37 | 38 | s3Client := s3.New(sess) 39 | 40 | output, err := s3Client.ListBuckets(&s3.ListBucketsInput{}) 41 | assert.NoError(t, err, "Unexpected error calling list buckets") 42 | assert.NotNil(t, output, "Expected list bucket response to be non-nil") 43 | } 44 | 45 | func TestCredentials_RoleCredentials(t *testing.T) { 46 | os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/role/ecs-local-endpoints-integ-role") 47 | defer os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "") 48 | sess, err := session.NewSession() 49 | assert.NoError(t, err, "Unexpected error creating an SDK session") 50 | 51 | s3Client := s3.New(sess) 52 | 53 | output, err := s3Client.ListBuckets(&s3.ListBucketsInput{}) 54 | assert.NoError(t, err, "Unexpected error calling list buckets") 55 | assert.NotNil(t, output, "Expected list bucket response to be non-nil") 56 | } 57 | 58 | // Just to verify that the SDK is taking creds from the Local Credentials Service 59 | // Set AWS_CONTAINER_CREDENTIALS_RELATIVE_URI to something that will throw an error 60 | func TestCredentials_Error(t *testing.T) { 61 | os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/cats") 62 | defer os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "") 63 | sess, err := session.NewSession() 64 | assert.NoError(t, err, "Unexpected error creating an SDK session") 65 | 66 | s3Client := s3.New(sess) 67 | 68 | // Failure will occur on the actual API call 69 | _, err = s3Client.ListBuckets(&s3.ListBucketsInput{}) 70 | assert.Error(t, err, "Expected error calling list buckets") 71 | } 72 | -------------------------------------------------------------------------------- /integ/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | networks: 4 | # This special network is configured so that the local metadata 5 | # service can bind to the specific IP address that ECS uses 6 | # in production 7 | metadata_network: 8 | driver: bridge 9 | ipam: 10 | config: 11 | - subnet: "169.254.170.0/24" 12 | gateway: 169.254.170.1 13 | 14 | # # A generic network interface for everything else. 15 | # app: 16 | # driver: bridge 17 | 18 | services: 19 | # The ECS Local container, which vends credentials and metadata 20 | ecs-local: 21 | image: amazon/amazon-ecs-local-container-endpoints:latest-amd64 22 | ports: 23 | - "80:80" 24 | volumes: 25 | - /var/run:/var/run 26 | - $HOME/.aws/:/home/.aws/ 27 | - /var/run/docker.sock:/var/run/docker.sock 28 | environment: 29 | ECS_LOCAL_METADATA_PORT: "80" 30 | AWS_PROFILE: "default" 31 | networks: 32 | metadata_network: 33 | ipv4_address: "169.254.170.2" 34 | 35 | integration-test: 36 | image: amazon-ecs-local-container-endpoints-integ-test:latest 37 | tty: true 38 | networks: 39 | metadata_network: 40 | ipv4_address: "169.254.170.3" 41 | depends_on: 42 | - ecs-local 43 | environment: 44 | ECS_CONTAINER_METADATA_URI: "http://169.254.170.2/v3/containers/integ" 45 | AWS_REGION: "us-east-1" 46 | 47 | nginx: 48 | image: nginx 49 | networks: 50 | metadata_network: 51 | ipv4_address: "169.254.170.5" 52 | environment: 53 | ECS_CONTAINER_METADATA_URI: "http://169.254.170.2/v3/containers/nginx" 54 | AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: "/tempcreds" 55 | AWS_DEFAULT_REGION: "us-east-1" 56 | -------------------------------------------------------------------------------- /integ/metadata/v2_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "encoding/json" 18 | "io/ioutil" 19 | "net/http" 20 | "testing" 21 | 22 | "github.com/aws/amazon-ecs-agent/agent/handlers/v2" 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestV2Handler_TaskMetadata(t *testing.T) { 27 | res, err := http.Get("http://169.254.170.2/v2/metadata") 28 | assert.NoError(t, err, "Unexpected error making HTTP Request") 29 | response, err := ioutil.ReadAll(res.Body) 30 | res.Body.Close() 31 | assert.NoError(t, err, "Unexpected error reading HTTP response") 32 | 33 | actualMetadata := &v2.TaskResponse{} 34 | err = json.Unmarshal(response, actualMetadata) 35 | assert.NoError(t, err, "Unexpected error unmarshalling response") 36 | 37 | assert.Len(t, actualMetadata.Containers, 3, "Expected 3 containers in response") 38 | 39 | expectedNames := []string{ 40 | "integ_ecs-local_1", 41 | "integ_integration-test_1", 42 | "integ_nginx_1", 43 | } 44 | 45 | actualNames := []string{ 46 | actualMetadata.Containers[0].Name, 47 | actualMetadata.Containers[1].Name, 48 | actualMetadata.Containers[2].Name, 49 | } 50 | 51 | assert.ElementsMatch(t, expectedNames, actualNames, "Expected list of container names to match") 52 | } 53 | -------------------------------------------------------------------------------- /integ/metadata/v3_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | "io/ioutil" 20 | "net/http" 21 | "os" 22 | "testing" 23 | 24 | "github.com/aws/amazon-ecs-agent/agent/handlers/v2" 25 | "github.com/docker/docker/api/types" 26 | "github.com/stretchr/testify/assert" 27 | ) 28 | 29 | func TestV3Handler_TaskMetadata(t *testing.T) { 30 | v3Path := os.Getenv("ECS_CONTAINER_METADATA_URI") 31 | res, err := http.Get(fmt.Sprintf("%s/task", v3Path)) 32 | assert.NoError(t, err, "Unexpected error making HTTP Request") 33 | response, err := ioutil.ReadAll(res.Body) 34 | res.Body.Close() 35 | assert.NoError(t, err, "Unexpected error reading HTTP response") 36 | 37 | actualMetadata := &v2.TaskResponse{} 38 | err = json.Unmarshal(response, actualMetadata) 39 | assert.NoError(t, err, "Unexpected error unmarshalling response") 40 | 41 | assert.Len(t, actualMetadata.Containers, 3, "Expected 3 containers in response") 42 | 43 | expectedNames := []string{ 44 | "integ_ecs-local_1", 45 | "integ_integration-test_1", 46 | "integ_nginx_1", 47 | } 48 | 49 | actualNames := []string{ 50 | actualMetadata.Containers[0].Name, 51 | actualMetadata.Containers[1].Name, 52 | actualMetadata.Containers[2].Name, 53 | } 54 | 55 | assert.ElementsMatch(t, expectedNames, actualNames, "Expected list of container names to match") 56 | } 57 | 58 | func TestV3Handler_ContainerkMetadata(t *testing.T) { 59 | res, err := http.Get(os.Getenv("ECS_CONTAINER_METADATA_URI")) 60 | assert.NoError(t, err, "Unexpected error making HTTP Request") 61 | response, err := ioutil.ReadAll(res.Body) 62 | res.Body.Close() 63 | assert.NoError(t, err, "Unexpected error reading HTTP response") 64 | 65 | actualMetadata := &v2.ContainerResponse{} 66 | err = json.Unmarshal(response, actualMetadata) 67 | assert.NoError(t, err, "Unexpected error unmarshalling response") 68 | 69 | assert.Equal(t, "integ_integration-test_1", actualMetadata.Name) 70 | } 71 | 72 | func TestV3Handler_ContainerkStats(t *testing.T) { 73 | res, err := http.Get(os.Getenv("ECS_CONTAINER_METADATA_URI") + "/stats") 74 | assert.NoError(t, err, "Unexpected error making HTTP Request") 75 | response, err := ioutil.ReadAll(res.Body) 76 | res.Body.Close() 77 | assert.NoError(t, err, "Unexpected error reading HTTP response") 78 | 79 | actual := types.Stats{} 80 | err = json.Unmarshal(response, &actual) 81 | assert.NoError(t, err, "Unexpected error unmarshalling response") 82 | } 83 | -------------------------------------------------------------------------------- /local-container-endpoints/clients/docker/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | // Package docker includes a wrapper of the Docker Go SDK Client 15 | package docker 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "os" 21 | 22 | "github.com/docker/docker/api/types" 23 | "github.com/docker/docker/client" 24 | "github.com/pkg/errors" 25 | ) 26 | 27 | const ( 28 | // v1.27 is the oldest API version 29 | // which has all the latest changes to the APIs we use. 30 | minDockerAPIVersion = "1.27" 31 | ) 32 | 33 | // Client is a wrapper for Docker SDK Client 34 | type Client interface { 35 | ContainerList(context.Context) ([]types.Container, error) 36 | ContainerStats(ctx context.Context, longContainerID string) (*types.Stats, error) 37 | } 38 | 39 | type dockerClient struct { 40 | sdkClient *client.Client 41 | } 42 | 43 | // NewDockerClient creates a new wrapper of the Docker Go Client 44 | func NewDockerClient() (Client, error) { 45 | // Using NewEnvClient allows customers to configure Docker via env vars 46 | // However, if DOCKER_API_VERSION is not set, the SDK can pick a version 47 | // which is too new for the local Docker. 48 | if os.Getenv("DOCKER_API_VERSION") == "" { 49 | os.Setenv("DOCKER_API_VERSION", minDockerAPIVersion) 50 | } 51 | sdkClient, err := client.NewEnvClient() 52 | if err != nil { 53 | return nil, err 54 | } 55 | return &dockerClient{ 56 | sdkClient: sdkClient, 57 | }, nil 58 | } 59 | 60 | // ContainerList lists all containers running on the host 61 | func (c *dockerClient) ContainerList(ctx context.Context) ([]types.Container, error) { 62 | return c.sdkClient.ContainerList(ctx, types.ContainerListOptions{}) 63 | } 64 | 65 | func (c *dockerClient) ContainerStats(ctx context.Context, longContainerID string) (*types.Stats, error) { 66 | resp, err := c.sdkClient.ContainerStats(ctx, longContainerID, false) 67 | if err != nil { 68 | return nil, errors.Wrapf(err, "failed to get docker stats for %s", longContainerID) 69 | } 70 | 71 | decoder := json.NewDecoder(resp.Body) 72 | data := new(types.Stats) 73 | err = decoder.Decode(data) 74 | defer resp.Body.Close() 75 | if err != nil { 76 | return nil, errors.Wrapf(err, "failed to get docker stats for %s", longContainerID) 77 | } 78 | return data, nil 79 | } 80 | -------------------------------------------------------------------------------- /local-container-endpoints/clients/docker/generate_mock.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package docker 15 | 16 | //go:generate mockgen.sh github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/clients/docker Client mock_docker/mock.go 17 | -------------------------------------------------------------------------------- /local-container-endpoints/clients/docker/mock_docker/mock.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | // Code generated by MockGen. DO NOT EDIT. 15 | // Source: github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/clients/docker (interfaces: Client) 16 | 17 | // Package mock_docker is a generated GoMock package. 18 | package mock_docker 19 | 20 | import ( 21 | context "context" 22 | reflect "reflect" 23 | 24 | types "github.com/docker/docker/api/types" 25 | gomock "github.com/golang/mock/gomock" 26 | ) 27 | 28 | // MockClient is a mock of Client interface. 29 | type MockClient struct { 30 | ctrl *gomock.Controller 31 | recorder *MockClientMockRecorder 32 | } 33 | 34 | // MockClientMockRecorder is the mock recorder for MockClient. 35 | type MockClientMockRecorder struct { 36 | mock *MockClient 37 | } 38 | 39 | // NewMockClient creates a new mock instance. 40 | func NewMockClient(ctrl *gomock.Controller) *MockClient { 41 | mock := &MockClient{ctrl: ctrl} 42 | mock.recorder = &MockClientMockRecorder{mock} 43 | return mock 44 | } 45 | 46 | // EXPECT returns an object that allows the caller to indicate expected use. 47 | func (m *MockClient) EXPECT() *MockClientMockRecorder { 48 | return m.recorder 49 | } 50 | 51 | // ContainerList mocks base method. 52 | func (m *MockClient) ContainerList(arg0 context.Context) ([]types.Container, error) { 53 | m.ctrl.T.Helper() 54 | ret := m.ctrl.Call(m, "ContainerList", arg0) 55 | ret0, _ := ret[0].([]types.Container) 56 | ret1, _ := ret[1].(error) 57 | return ret0, ret1 58 | } 59 | 60 | // ContainerList indicates an expected call of ContainerList. 61 | func (mr *MockClientMockRecorder) ContainerList(arg0 interface{}) *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerList", reflect.TypeOf((*MockClient)(nil).ContainerList), arg0) 64 | } 65 | 66 | // ContainerStats mocks base method. 67 | func (m *MockClient) ContainerStats(arg0 context.Context, arg1 string) (*types.Stats, error) { 68 | m.ctrl.T.Helper() 69 | ret := m.ctrl.Call(m, "ContainerStats", arg0, arg1) 70 | ret0, _ := ret[0].(*types.Stats) 71 | ret1, _ := ret[1].(error) 72 | return ret0, ret1 73 | } 74 | 75 | // ContainerStats indicates an expected call of ContainerStats. 76 | func (mr *MockClientMockRecorder) ContainerStats(arg0, arg1 interface{}) *gomock.Call { 77 | mr.mock.ctrl.T.Helper() 78 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerStats", reflect.TypeOf((*MockClient)(nil).ContainerStats), arg0, arg1) 79 | } 80 | -------------------------------------------------------------------------------- /local-container-endpoints/clients/iam/generate_mock.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package aws 15 | 16 | //go:generate mockgen.sh github.com/aws/aws-sdk-go/service/iam/iamiface IAMAPI mock_iamiface/mock.go 17 | -------------------------------------------------------------------------------- /local-container-endpoints/clients/sts/generate_mock.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package aws 15 | 16 | //go:generate mockgen.sh github.com/aws/aws-sdk-go/service/sts/stsiface STSAPI mock_stsiface/mock.go 17 | -------------------------------------------------------------------------------- /local-container-endpoints/clients/sts/mock_stsiface/mock.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | // Code generated by MockGen. DO NOT EDIT. 15 | // Source: github.com/aws/aws-sdk-go/service/sts/stsiface (interfaces: STSAPI) 16 | 17 | // Package mock_stsiface is a generated GoMock package. 18 | package mock_stsiface 19 | 20 | import ( 21 | context "context" 22 | reflect "reflect" 23 | 24 | request "github.com/aws/aws-sdk-go/aws/request" 25 | sts "github.com/aws/aws-sdk-go/service/sts" 26 | gomock "github.com/golang/mock/gomock" 27 | ) 28 | 29 | // MockSTSAPI is a mock of STSAPI interface. 30 | type MockSTSAPI struct { 31 | ctrl *gomock.Controller 32 | recorder *MockSTSAPIMockRecorder 33 | } 34 | 35 | // MockSTSAPIMockRecorder is the mock recorder for MockSTSAPI. 36 | type MockSTSAPIMockRecorder struct { 37 | mock *MockSTSAPI 38 | } 39 | 40 | // NewMockSTSAPI creates a new mock instance. 41 | func NewMockSTSAPI(ctrl *gomock.Controller) *MockSTSAPI { 42 | mock := &MockSTSAPI{ctrl: ctrl} 43 | mock.recorder = &MockSTSAPIMockRecorder{mock} 44 | return mock 45 | } 46 | 47 | // EXPECT returns an object that allows the caller to indicate expected use. 48 | func (m *MockSTSAPI) EXPECT() *MockSTSAPIMockRecorder { 49 | return m.recorder 50 | } 51 | 52 | // AssumeRole mocks base method. 53 | func (m *MockSTSAPI) AssumeRole(arg0 *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "AssumeRole", arg0) 56 | ret0, _ := ret[0].(*sts.AssumeRoleOutput) 57 | ret1, _ := ret[1].(error) 58 | return ret0, ret1 59 | } 60 | 61 | // AssumeRole indicates an expected call of AssumeRole. 62 | func (mr *MockSTSAPIMockRecorder) AssumeRole(arg0 interface{}) *gomock.Call { 63 | mr.mock.ctrl.T.Helper() 64 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRole", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRole), arg0) 65 | } 66 | 67 | // AssumeRoleRequest mocks base method. 68 | func (m *MockSTSAPI) AssumeRoleRequest(arg0 *sts.AssumeRoleInput) (*request.Request, *sts.AssumeRoleOutput) { 69 | m.ctrl.T.Helper() 70 | ret := m.ctrl.Call(m, "AssumeRoleRequest", arg0) 71 | ret0, _ := ret[0].(*request.Request) 72 | ret1, _ := ret[1].(*sts.AssumeRoleOutput) 73 | return ret0, ret1 74 | } 75 | 76 | // AssumeRoleRequest indicates an expected call of AssumeRoleRequest. 77 | func (mr *MockSTSAPIMockRecorder) AssumeRoleRequest(arg0 interface{}) *gomock.Call { 78 | mr.mock.ctrl.T.Helper() 79 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleRequest", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleRequest), arg0) 80 | } 81 | 82 | // AssumeRoleWithContext mocks base method. 83 | func (m *MockSTSAPI) AssumeRoleWithContext(arg0 context.Context, arg1 *sts.AssumeRoleInput, arg2 ...request.Option) (*sts.AssumeRoleOutput, error) { 84 | m.ctrl.T.Helper() 85 | varargs := []interface{}{arg0, arg1} 86 | for _, a := range arg2 { 87 | varargs = append(varargs, a) 88 | } 89 | ret := m.ctrl.Call(m, "AssumeRoleWithContext", varargs...) 90 | ret0, _ := ret[0].(*sts.AssumeRoleOutput) 91 | ret1, _ := ret[1].(error) 92 | return ret0, ret1 93 | } 94 | 95 | // AssumeRoleWithContext indicates an expected call of AssumeRoleWithContext. 96 | func (mr *MockSTSAPIMockRecorder) AssumeRoleWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { 97 | mr.mock.ctrl.T.Helper() 98 | varargs := append([]interface{}{arg0, arg1}, arg2...) 99 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleWithContext", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleWithContext), varargs...) 100 | } 101 | 102 | // AssumeRoleWithSAML mocks base method. 103 | func (m *MockSTSAPI) AssumeRoleWithSAML(arg0 *sts.AssumeRoleWithSAMLInput) (*sts.AssumeRoleWithSAMLOutput, error) { 104 | m.ctrl.T.Helper() 105 | ret := m.ctrl.Call(m, "AssumeRoleWithSAML", arg0) 106 | ret0, _ := ret[0].(*sts.AssumeRoleWithSAMLOutput) 107 | ret1, _ := ret[1].(error) 108 | return ret0, ret1 109 | } 110 | 111 | // AssumeRoleWithSAML indicates an expected call of AssumeRoleWithSAML. 112 | func (mr *MockSTSAPIMockRecorder) AssumeRoleWithSAML(arg0 interface{}) *gomock.Call { 113 | mr.mock.ctrl.T.Helper() 114 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleWithSAML", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleWithSAML), arg0) 115 | } 116 | 117 | // AssumeRoleWithSAMLRequest mocks base method. 118 | func (m *MockSTSAPI) AssumeRoleWithSAMLRequest(arg0 *sts.AssumeRoleWithSAMLInput) (*request.Request, *sts.AssumeRoleWithSAMLOutput) { 119 | m.ctrl.T.Helper() 120 | ret := m.ctrl.Call(m, "AssumeRoleWithSAMLRequest", arg0) 121 | ret0, _ := ret[0].(*request.Request) 122 | ret1, _ := ret[1].(*sts.AssumeRoleWithSAMLOutput) 123 | return ret0, ret1 124 | } 125 | 126 | // AssumeRoleWithSAMLRequest indicates an expected call of AssumeRoleWithSAMLRequest. 127 | func (mr *MockSTSAPIMockRecorder) AssumeRoleWithSAMLRequest(arg0 interface{}) *gomock.Call { 128 | mr.mock.ctrl.T.Helper() 129 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleWithSAMLRequest", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleWithSAMLRequest), arg0) 130 | } 131 | 132 | // AssumeRoleWithSAMLWithContext mocks base method. 133 | func (m *MockSTSAPI) AssumeRoleWithSAMLWithContext(arg0 context.Context, arg1 *sts.AssumeRoleWithSAMLInput, arg2 ...request.Option) (*sts.AssumeRoleWithSAMLOutput, error) { 134 | m.ctrl.T.Helper() 135 | varargs := []interface{}{arg0, arg1} 136 | for _, a := range arg2 { 137 | varargs = append(varargs, a) 138 | } 139 | ret := m.ctrl.Call(m, "AssumeRoleWithSAMLWithContext", varargs...) 140 | ret0, _ := ret[0].(*sts.AssumeRoleWithSAMLOutput) 141 | ret1, _ := ret[1].(error) 142 | return ret0, ret1 143 | } 144 | 145 | // AssumeRoleWithSAMLWithContext indicates an expected call of AssumeRoleWithSAMLWithContext. 146 | func (mr *MockSTSAPIMockRecorder) AssumeRoleWithSAMLWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { 147 | mr.mock.ctrl.T.Helper() 148 | varargs := append([]interface{}{arg0, arg1}, arg2...) 149 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleWithSAMLWithContext", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleWithSAMLWithContext), varargs...) 150 | } 151 | 152 | // AssumeRoleWithWebIdentity mocks base method. 153 | func (m *MockSTSAPI) AssumeRoleWithWebIdentity(arg0 *sts.AssumeRoleWithWebIdentityInput) (*sts.AssumeRoleWithWebIdentityOutput, error) { 154 | m.ctrl.T.Helper() 155 | ret := m.ctrl.Call(m, "AssumeRoleWithWebIdentity", arg0) 156 | ret0, _ := ret[0].(*sts.AssumeRoleWithWebIdentityOutput) 157 | ret1, _ := ret[1].(error) 158 | return ret0, ret1 159 | } 160 | 161 | // AssumeRoleWithWebIdentity indicates an expected call of AssumeRoleWithWebIdentity. 162 | func (mr *MockSTSAPIMockRecorder) AssumeRoleWithWebIdentity(arg0 interface{}) *gomock.Call { 163 | mr.mock.ctrl.T.Helper() 164 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleWithWebIdentity", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleWithWebIdentity), arg0) 165 | } 166 | 167 | // AssumeRoleWithWebIdentityRequest mocks base method. 168 | func (m *MockSTSAPI) AssumeRoleWithWebIdentityRequest(arg0 *sts.AssumeRoleWithWebIdentityInput) (*request.Request, *sts.AssumeRoleWithWebIdentityOutput) { 169 | m.ctrl.T.Helper() 170 | ret := m.ctrl.Call(m, "AssumeRoleWithWebIdentityRequest", arg0) 171 | ret0, _ := ret[0].(*request.Request) 172 | ret1, _ := ret[1].(*sts.AssumeRoleWithWebIdentityOutput) 173 | return ret0, ret1 174 | } 175 | 176 | // AssumeRoleWithWebIdentityRequest indicates an expected call of AssumeRoleWithWebIdentityRequest. 177 | func (mr *MockSTSAPIMockRecorder) AssumeRoleWithWebIdentityRequest(arg0 interface{}) *gomock.Call { 178 | mr.mock.ctrl.T.Helper() 179 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleWithWebIdentityRequest", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleWithWebIdentityRequest), arg0) 180 | } 181 | 182 | // AssumeRoleWithWebIdentityWithContext mocks base method. 183 | func (m *MockSTSAPI) AssumeRoleWithWebIdentityWithContext(arg0 context.Context, arg1 *sts.AssumeRoleWithWebIdentityInput, arg2 ...request.Option) (*sts.AssumeRoleWithWebIdentityOutput, error) { 184 | m.ctrl.T.Helper() 185 | varargs := []interface{}{arg0, arg1} 186 | for _, a := range arg2 { 187 | varargs = append(varargs, a) 188 | } 189 | ret := m.ctrl.Call(m, "AssumeRoleWithWebIdentityWithContext", varargs...) 190 | ret0, _ := ret[0].(*sts.AssumeRoleWithWebIdentityOutput) 191 | ret1, _ := ret[1].(error) 192 | return ret0, ret1 193 | } 194 | 195 | // AssumeRoleWithWebIdentityWithContext indicates an expected call of AssumeRoleWithWebIdentityWithContext. 196 | func (mr *MockSTSAPIMockRecorder) AssumeRoleWithWebIdentityWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { 197 | mr.mock.ctrl.T.Helper() 198 | varargs := append([]interface{}{arg0, arg1}, arg2...) 199 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleWithWebIdentityWithContext", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleWithWebIdentityWithContext), varargs...) 200 | } 201 | 202 | // DecodeAuthorizationMessage mocks base method. 203 | func (m *MockSTSAPI) DecodeAuthorizationMessage(arg0 *sts.DecodeAuthorizationMessageInput) (*sts.DecodeAuthorizationMessageOutput, error) { 204 | m.ctrl.T.Helper() 205 | ret := m.ctrl.Call(m, "DecodeAuthorizationMessage", arg0) 206 | ret0, _ := ret[0].(*sts.DecodeAuthorizationMessageOutput) 207 | ret1, _ := ret[1].(error) 208 | return ret0, ret1 209 | } 210 | 211 | // DecodeAuthorizationMessage indicates an expected call of DecodeAuthorizationMessage. 212 | func (mr *MockSTSAPIMockRecorder) DecodeAuthorizationMessage(arg0 interface{}) *gomock.Call { 213 | mr.mock.ctrl.T.Helper() 214 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecodeAuthorizationMessage", reflect.TypeOf((*MockSTSAPI)(nil).DecodeAuthorizationMessage), arg0) 215 | } 216 | 217 | // DecodeAuthorizationMessageRequest mocks base method. 218 | func (m *MockSTSAPI) DecodeAuthorizationMessageRequest(arg0 *sts.DecodeAuthorizationMessageInput) (*request.Request, *sts.DecodeAuthorizationMessageOutput) { 219 | m.ctrl.T.Helper() 220 | ret := m.ctrl.Call(m, "DecodeAuthorizationMessageRequest", arg0) 221 | ret0, _ := ret[0].(*request.Request) 222 | ret1, _ := ret[1].(*sts.DecodeAuthorizationMessageOutput) 223 | return ret0, ret1 224 | } 225 | 226 | // DecodeAuthorizationMessageRequest indicates an expected call of DecodeAuthorizationMessageRequest. 227 | func (mr *MockSTSAPIMockRecorder) DecodeAuthorizationMessageRequest(arg0 interface{}) *gomock.Call { 228 | mr.mock.ctrl.T.Helper() 229 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecodeAuthorizationMessageRequest", reflect.TypeOf((*MockSTSAPI)(nil).DecodeAuthorizationMessageRequest), arg0) 230 | } 231 | 232 | // DecodeAuthorizationMessageWithContext mocks base method. 233 | func (m *MockSTSAPI) DecodeAuthorizationMessageWithContext(arg0 context.Context, arg1 *sts.DecodeAuthorizationMessageInput, arg2 ...request.Option) (*sts.DecodeAuthorizationMessageOutput, error) { 234 | m.ctrl.T.Helper() 235 | varargs := []interface{}{arg0, arg1} 236 | for _, a := range arg2 { 237 | varargs = append(varargs, a) 238 | } 239 | ret := m.ctrl.Call(m, "DecodeAuthorizationMessageWithContext", varargs...) 240 | ret0, _ := ret[0].(*sts.DecodeAuthorizationMessageOutput) 241 | ret1, _ := ret[1].(error) 242 | return ret0, ret1 243 | } 244 | 245 | // DecodeAuthorizationMessageWithContext indicates an expected call of DecodeAuthorizationMessageWithContext. 246 | func (mr *MockSTSAPIMockRecorder) DecodeAuthorizationMessageWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { 247 | mr.mock.ctrl.T.Helper() 248 | varargs := append([]interface{}{arg0, arg1}, arg2...) 249 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecodeAuthorizationMessageWithContext", reflect.TypeOf((*MockSTSAPI)(nil).DecodeAuthorizationMessageWithContext), varargs...) 250 | } 251 | 252 | // GetAccessKeyInfo mocks base method. 253 | func (m *MockSTSAPI) GetAccessKeyInfo(arg0 *sts.GetAccessKeyInfoInput) (*sts.GetAccessKeyInfoOutput, error) { 254 | m.ctrl.T.Helper() 255 | ret := m.ctrl.Call(m, "GetAccessKeyInfo", arg0) 256 | ret0, _ := ret[0].(*sts.GetAccessKeyInfoOutput) 257 | ret1, _ := ret[1].(error) 258 | return ret0, ret1 259 | } 260 | 261 | // GetAccessKeyInfo indicates an expected call of GetAccessKeyInfo. 262 | func (mr *MockSTSAPIMockRecorder) GetAccessKeyInfo(arg0 interface{}) *gomock.Call { 263 | mr.mock.ctrl.T.Helper() 264 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccessKeyInfo", reflect.TypeOf((*MockSTSAPI)(nil).GetAccessKeyInfo), arg0) 265 | } 266 | 267 | // GetAccessKeyInfoRequest mocks base method. 268 | func (m *MockSTSAPI) GetAccessKeyInfoRequest(arg0 *sts.GetAccessKeyInfoInput) (*request.Request, *sts.GetAccessKeyInfoOutput) { 269 | m.ctrl.T.Helper() 270 | ret := m.ctrl.Call(m, "GetAccessKeyInfoRequest", arg0) 271 | ret0, _ := ret[0].(*request.Request) 272 | ret1, _ := ret[1].(*sts.GetAccessKeyInfoOutput) 273 | return ret0, ret1 274 | } 275 | 276 | // GetAccessKeyInfoRequest indicates an expected call of GetAccessKeyInfoRequest. 277 | func (mr *MockSTSAPIMockRecorder) GetAccessKeyInfoRequest(arg0 interface{}) *gomock.Call { 278 | mr.mock.ctrl.T.Helper() 279 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccessKeyInfoRequest", reflect.TypeOf((*MockSTSAPI)(nil).GetAccessKeyInfoRequest), arg0) 280 | } 281 | 282 | // GetAccessKeyInfoWithContext mocks base method. 283 | func (m *MockSTSAPI) GetAccessKeyInfoWithContext(arg0 context.Context, arg1 *sts.GetAccessKeyInfoInput, arg2 ...request.Option) (*sts.GetAccessKeyInfoOutput, error) { 284 | m.ctrl.T.Helper() 285 | varargs := []interface{}{arg0, arg1} 286 | for _, a := range arg2 { 287 | varargs = append(varargs, a) 288 | } 289 | ret := m.ctrl.Call(m, "GetAccessKeyInfoWithContext", varargs...) 290 | ret0, _ := ret[0].(*sts.GetAccessKeyInfoOutput) 291 | ret1, _ := ret[1].(error) 292 | return ret0, ret1 293 | } 294 | 295 | // GetAccessKeyInfoWithContext indicates an expected call of GetAccessKeyInfoWithContext. 296 | func (mr *MockSTSAPIMockRecorder) GetAccessKeyInfoWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { 297 | mr.mock.ctrl.T.Helper() 298 | varargs := append([]interface{}{arg0, arg1}, arg2...) 299 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccessKeyInfoWithContext", reflect.TypeOf((*MockSTSAPI)(nil).GetAccessKeyInfoWithContext), varargs...) 300 | } 301 | 302 | // GetCallerIdentity mocks base method. 303 | func (m *MockSTSAPI) GetCallerIdentity(arg0 *sts.GetCallerIdentityInput) (*sts.GetCallerIdentityOutput, error) { 304 | m.ctrl.T.Helper() 305 | ret := m.ctrl.Call(m, "GetCallerIdentity", arg0) 306 | ret0, _ := ret[0].(*sts.GetCallerIdentityOutput) 307 | ret1, _ := ret[1].(error) 308 | return ret0, ret1 309 | } 310 | 311 | // GetCallerIdentity indicates an expected call of GetCallerIdentity. 312 | func (mr *MockSTSAPIMockRecorder) GetCallerIdentity(arg0 interface{}) *gomock.Call { 313 | mr.mock.ctrl.T.Helper() 314 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCallerIdentity", reflect.TypeOf((*MockSTSAPI)(nil).GetCallerIdentity), arg0) 315 | } 316 | 317 | // GetCallerIdentityRequest mocks base method. 318 | func (m *MockSTSAPI) GetCallerIdentityRequest(arg0 *sts.GetCallerIdentityInput) (*request.Request, *sts.GetCallerIdentityOutput) { 319 | m.ctrl.T.Helper() 320 | ret := m.ctrl.Call(m, "GetCallerIdentityRequest", arg0) 321 | ret0, _ := ret[0].(*request.Request) 322 | ret1, _ := ret[1].(*sts.GetCallerIdentityOutput) 323 | return ret0, ret1 324 | } 325 | 326 | // GetCallerIdentityRequest indicates an expected call of GetCallerIdentityRequest. 327 | func (mr *MockSTSAPIMockRecorder) GetCallerIdentityRequest(arg0 interface{}) *gomock.Call { 328 | mr.mock.ctrl.T.Helper() 329 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCallerIdentityRequest", reflect.TypeOf((*MockSTSAPI)(nil).GetCallerIdentityRequest), arg0) 330 | } 331 | 332 | // GetCallerIdentityWithContext mocks base method. 333 | func (m *MockSTSAPI) GetCallerIdentityWithContext(arg0 context.Context, arg1 *sts.GetCallerIdentityInput, arg2 ...request.Option) (*sts.GetCallerIdentityOutput, error) { 334 | m.ctrl.T.Helper() 335 | varargs := []interface{}{arg0, arg1} 336 | for _, a := range arg2 { 337 | varargs = append(varargs, a) 338 | } 339 | ret := m.ctrl.Call(m, "GetCallerIdentityWithContext", varargs...) 340 | ret0, _ := ret[0].(*sts.GetCallerIdentityOutput) 341 | ret1, _ := ret[1].(error) 342 | return ret0, ret1 343 | } 344 | 345 | // GetCallerIdentityWithContext indicates an expected call of GetCallerIdentityWithContext. 346 | func (mr *MockSTSAPIMockRecorder) GetCallerIdentityWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { 347 | mr.mock.ctrl.T.Helper() 348 | varargs := append([]interface{}{arg0, arg1}, arg2...) 349 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCallerIdentityWithContext", reflect.TypeOf((*MockSTSAPI)(nil).GetCallerIdentityWithContext), varargs...) 350 | } 351 | 352 | // GetFederationToken mocks base method. 353 | func (m *MockSTSAPI) GetFederationToken(arg0 *sts.GetFederationTokenInput) (*sts.GetFederationTokenOutput, error) { 354 | m.ctrl.T.Helper() 355 | ret := m.ctrl.Call(m, "GetFederationToken", arg0) 356 | ret0, _ := ret[0].(*sts.GetFederationTokenOutput) 357 | ret1, _ := ret[1].(error) 358 | return ret0, ret1 359 | } 360 | 361 | // GetFederationToken indicates an expected call of GetFederationToken. 362 | func (mr *MockSTSAPIMockRecorder) GetFederationToken(arg0 interface{}) *gomock.Call { 363 | mr.mock.ctrl.T.Helper() 364 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFederationToken", reflect.TypeOf((*MockSTSAPI)(nil).GetFederationToken), arg0) 365 | } 366 | 367 | // GetFederationTokenRequest mocks base method. 368 | func (m *MockSTSAPI) GetFederationTokenRequest(arg0 *sts.GetFederationTokenInput) (*request.Request, *sts.GetFederationTokenOutput) { 369 | m.ctrl.T.Helper() 370 | ret := m.ctrl.Call(m, "GetFederationTokenRequest", arg0) 371 | ret0, _ := ret[0].(*request.Request) 372 | ret1, _ := ret[1].(*sts.GetFederationTokenOutput) 373 | return ret0, ret1 374 | } 375 | 376 | // GetFederationTokenRequest indicates an expected call of GetFederationTokenRequest. 377 | func (mr *MockSTSAPIMockRecorder) GetFederationTokenRequest(arg0 interface{}) *gomock.Call { 378 | mr.mock.ctrl.T.Helper() 379 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFederationTokenRequest", reflect.TypeOf((*MockSTSAPI)(nil).GetFederationTokenRequest), arg0) 380 | } 381 | 382 | // GetFederationTokenWithContext mocks base method. 383 | func (m *MockSTSAPI) GetFederationTokenWithContext(arg0 context.Context, arg1 *sts.GetFederationTokenInput, arg2 ...request.Option) (*sts.GetFederationTokenOutput, error) { 384 | m.ctrl.T.Helper() 385 | varargs := []interface{}{arg0, arg1} 386 | for _, a := range arg2 { 387 | varargs = append(varargs, a) 388 | } 389 | ret := m.ctrl.Call(m, "GetFederationTokenWithContext", varargs...) 390 | ret0, _ := ret[0].(*sts.GetFederationTokenOutput) 391 | ret1, _ := ret[1].(error) 392 | return ret0, ret1 393 | } 394 | 395 | // GetFederationTokenWithContext indicates an expected call of GetFederationTokenWithContext. 396 | func (mr *MockSTSAPIMockRecorder) GetFederationTokenWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { 397 | mr.mock.ctrl.T.Helper() 398 | varargs := append([]interface{}{arg0, arg1}, arg2...) 399 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFederationTokenWithContext", reflect.TypeOf((*MockSTSAPI)(nil).GetFederationTokenWithContext), varargs...) 400 | } 401 | 402 | // GetSessionToken mocks base method. 403 | func (m *MockSTSAPI) GetSessionToken(arg0 *sts.GetSessionTokenInput) (*sts.GetSessionTokenOutput, error) { 404 | m.ctrl.T.Helper() 405 | ret := m.ctrl.Call(m, "GetSessionToken", arg0) 406 | ret0, _ := ret[0].(*sts.GetSessionTokenOutput) 407 | ret1, _ := ret[1].(error) 408 | return ret0, ret1 409 | } 410 | 411 | // GetSessionToken indicates an expected call of GetSessionToken. 412 | func (mr *MockSTSAPIMockRecorder) GetSessionToken(arg0 interface{}) *gomock.Call { 413 | mr.mock.ctrl.T.Helper() 414 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSessionToken", reflect.TypeOf((*MockSTSAPI)(nil).GetSessionToken), arg0) 415 | } 416 | 417 | // GetSessionTokenRequest mocks base method. 418 | func (m *MockSTSAPI) GetSessionTokenRequest(arg0 *sts.GetSessionTokenInput) (*request.Request, *sts.GetSessionTokenOutput) { 419 | m.ctrl.T.Helper() 420 | ret := m.ctrl.Call(m, "GetSessionTokenRequest", arg0) 421 | ret0, _ := ret[0].(*request.Request) 422 | ret1, _ := ret[1].(*sts.GetSessionTokenOutput) 423 | return ret0, ret1 424 | } 425 | 426 | // GetSessionTokenRequest indicates an expected call of GetSessionTokenRequest. 427 | func (mr *MockSTSAPIMockRecorder) GetSessionTokenRequest(arg0 interface{}) *gomock.Call { 428 | mr.mock.ctrl.T.Helper() 429 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSessionTokenRequest", reflect.TypeOf((*MockSTSAPI)(nil).GetSessionTokenRequest), arg0) 430 | } 431 | 432 | // GetSessionTokenWithContext mocks base method. 433 | func (m *MockSTSAPI) GetSessionTokenWithContext(arg0 context.Context, arg1 *sts.GetSessionTokenInput, arg2 ...request.Option) (*sts.GetSessionTokenOutput, error) { 434 | m.ctrl.T.Helper() 435 | varargs := []interface{}{arg0, arg1} 436 | for _, a := range arg2 { 437 | varargs = append(varargs, a) 438 | } 439 | ret := m.ctrl.Call(m, "GetSessionTokenWithContext", varargs...) 440 | ret0, _ := ret[0].(*sts.GetSessionTokenOutput) 441 | ret1, _ := ret[1].(error) 442 | return ret0, ret1 443 | } 444 | 445 | // GetSessionTokenWithContext indicates an expected call of GetSessionTokenWithContext. 446 | func (mr *MockSTSAPIMockRecorder) GetSessionTokenWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { 447 | mr.mock.ctrl.T.Helper() 448 | varargs := append([]interface{}{arg0, arg1}, arg2...) 449 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSessionTokenWithContext", reflect.TypeOf((*MockSTSAPI)(nil).GetSessionTokenWithContext), varargs...) 450 | } 451 | -------------------------------------------------------------------------------- /local-container-endpoints/clients/useragent/useragent.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | // Package useragent defines the custom user agent for local endpoints 15 | package useragent 16 | 17 | import ( 18 | "fmt" 19 | "runtime" 20 | 21 | "github.com/aws/aws-sdk-go/aws/request" 22 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/version" 23 | ) 24 | 25 | const userAgentHeader = "User-Agent" 26 | 27 | // CustomUserAgentHandler returns a http request handler that sets a custom user agent to all aws requests 28 | func CustomUserAgentHandler() request.NamedHandler { 29 | return request.NamedHandler{ 30 | Name: "ECSLocalEndpointsAgentHandler", 31 | Fn: func(r *request.Request) { 32 | currentAgent := r.HTTPRequest.Header.Get(userAgentHeader) 33 | r.HTTPRequest.Header.Set(userAgentHeader, 34 | fmt.Sprintf("aws-%s/%s (%s) %s", version.AppName, version.Version, runtime.GOOS, currentAgent)) 35 | }, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /local-container-endpoints/config/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | // Package config contains environment variables and default values for Local Endpoints 15 | package config 16 | 17 | // Environment Variables 18 | const ( 19 | // PortVar defines the port that metadata and credentials listen at 20 | PortVar = "ECS_LOCAL_METADATA_PORT" 21 | 22 | // Metadata related 23 | ClusterARNVar = "CLUSTER_ARN" 24 | TaskARNVar = "TASK_ARN" 25 | TDFamilyVar = "TASK_DEFINITION_FAMILY" 26 | TDRevisionVar = "TASK_DEFINITION_REVISION" 27 | ContainerInstanceTagsVar = "CONTAINER_INSTANCE_TAGS" 28 | TaskTagsVar = "TASK_TAGS_VAR" 29 | 30 | // Custom endpoint related 31 | IAMCustomEndpointVar = "IAM_ENDPOINT" 32 | STSCustomEndpointVar = "STS_ENDPOINT" 33 | 34 | // Shared credentials default expiration value when a token is detected. 35 | SharedTokenExpirationVar = "SHARED_TOKEN_EXPIRATION" 36 | 37 | // User-defined, static metadata that overrides/augments the normal response 38 | ContainerMetadataPathVar = "CONTAINER_METADATA_PATH" 39 | TaskMetadataPathVar = "TASK_METADATA_PATH" 40 | ) 41 | 42 | // Defaults 43 | const ( 44 | // DefaultPort is the default port the server listens at 45 | DefaultPort = "80" 46 | 47 | // Metadata related 48 | DefaultContainerType = "NORMAL" 49 | DefaultClusterName = "ecs-local-cluster" 50 | DefaultTaskARN = "arn:aws:ecs:us-west-2:111111111111:task/ecs-local-cluster/37e873f6-37b4-42a7-af47-eac7275c6152" 51 | DefaultTDFamily = "esc-local-task-definition" 52 | DefaultTDRevision = "1" 53 | 54 | // Expire shared credentials with a token in 12.5 minutes. 55 | DefaultSharedTokenExpiration = 750 56 | ) 57 | 58 | // Settings 59 | const ( 60 | HTTPTimeoutDuration = "5s" 61 | ) 62 | 63 | // URL Paths 64 | 65 | // Credentials 66 | const ( 67 | // RoleCredentialsPath is the path for obtaining credentials from a role 68 | RoleCredentialsPath = "/role/{role}" 69 | // RoleCredentialsPathWithSlash adds a trailing slash 70 | RoleCredentialsPathWithSlash = RoleCredentialsPath + "/" 71 | 72 | //RoleArnCredentialsPath is the path for obtaining credentials from a role ARN 73 | RoleArnCredentialsPath = "/role-arn/{roleArn}/{roleName}" 74 | // RoleArnCredentialsPathWithSlash adds a trailing slash 75 | RoleArnCredentialsPathWithSlash = RoleArnCredentialsPath + "/" 76 | 77 | // TempCredentialsPath is the path for obtaining temp creds from sts:GetSessionsToken 78 | TempCredentialsPath = "/creds" 79 | // TempCredentialsPathWithSlash adds a trailing slash 80 | TempCredentialsPathWithSlash = TempCredentialsPath + "/" 81 | ) 82 | 83 | // V3 84 | const ( 85 | // V3ContainerMetadataPath is the path for V3 container metadata 86 | V3ContainerMetadataPath = "/v3" 87 | // V3ContainerMetadataPathWithSlash adds a trailing slash 88 | V3ContainerMetadataPathWithSlash = V3ContainerMetadataPath + "/" 89 | // V3ContainerMetadataPathWithIdentifier is the V3 container metadata path with an identifer specified 90 | V3ContainerMetadataPathWithIdentifier = "/v3/containers/{identifier}" 91 | // V3ContainerMetadataPathWithIdentifierAndSlash adds a trailing slash 92 | V3ContainerMetadataPathWithIdentifierAndSlash = V3ContainerMetadataPathWithIdentifier + "/" 93 | 94 | // V3ContainerStatsPath is the path for V3 container stats 95 | V3ContainerStatsPath = "/v3/stats" 96 | // V3ContainerStatsPathWithSlash adds a trailing slash 97 | V3ContainerStatsPathWithSlash = V3ContainerStatsPath + "/" 98 | // V3ContainerStatsPathWithIdentifier is the V3 container stats path with an identifier 99 | V3ContainerStatsPathWithIdentifier = "/v3/containers/{identifier}/stats" 100 | // V3ContainerStatsPathWithIdentifierAndSlash adds a trailing slash 101 | V3ContainerStatsPathWithIdentifierAndSlash = V3ContainerStatsPathWithIdentifier + "/" 102 | 103 | // V3TaskMetadataPath is the path for V3 task metadata 104 | V3TaskMetadataPath = "/v3/task" 105 | // V3TaskMetadataPathWithSlash adds a trailing slash 106 | V3TaskMetadataPathWithSlash = V3TaskMetadataPath + "/" 107 | // V3TaskMetadataPathWithIdentifier is the v3 task metadata path with an identifier 108 | V3TaskMetadataPathWithIdentifier = "/v3/containers/{identifier}/task" 109 | // V3TaskMetadataPathWithIdentifierWithSlash adds a trailing slash 110 | V3TaskMetadataPathWithIdentifierWithSlash = V3TaskMetadataPathWithIdentifier + "/" 111 | 112 | // V3TaskStatsPath is the path for V3 task stats 113 | V3TaskStatsPath = "/v3/task/stats" 114 | // V3TaskStatsPathWithSlash adds a trailing slash 115 | V3TaskStatsPathWithSlash = V3TaskStatsPath + "/" 116 | // V3TaskStatsPathWithIdentifier is the v3 task stats path with an identifier 117 | V3TaskStatsPathWithIdentifier = "/v3/containers/{identifier}/task/stats" 118 | // V3TaskStatsPathWithIdentifierAndSlash adds a trailing slash 119 | V3TaskStatsPathWithIdentifierAndSlash = V3TaskStatsPathWithIdentifier + "/" 120 | ) 121 | 122 | // V2 123 | const ( 124 | // V2TaskMetadataPath is the V2 Task Metadata path 125 | V2TaskMetadataPath = "/v2/metadata" 126 | // V2TaskMetadataPathWithSlash adds a trailing slash 127 | V2TaskMetadataPathWithSlash = V2TaskMetadataPath + "/" 128 | 129 | // V2ContainerMetadataPath is the V2 Container Metadata path 130 | V2ContainerMetadataPath = "/v2/metadata/{identifier}" 131 | // V2ContainerMetadataPathWithSlash adds a trailing slash 132 | V2ContainerMetadataPathWithSlash = V2ContainerMetadataPath + "/" 133 | 134 | // V2TaskStatsPath is the V2 Task Stats Path 135 | V2TaskStatsPath = "/v2/stats" 136 | // V2TaskStatsPathWithSlash adds a trailing slash 137 | V2TaskStatsPathWithSlash = V2TaskStatsPath + "/" 138 | 139 | // V2ContainerStatsPath is the V2 container stats path 140 | V2ContainerStatsPath = "/v2/stats/{identifier}" 141 | // V2ContainerStatsPathWithSlash adds a trailing slash 142 | V2ContainerStatsPathWithSlash = V2ContainerStatsPath + "/" 143 | ) 144 | -------------------------------------------------------------------------------- /local-container-endpoints/handlers/credentials_handler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package handlers 15 | 16 | import ( 17 | "fmt" 18 | "net/http" 19 | "strconv" 20 | "time" 21 | 22 | "github.com/aws/aws-sdk-go/aws" 23 | "github.com/aws/aws-sdk-go/aws/endpoints" 24 | "github.com/aws/aws-sdk-go/aws/session" 25 | "github.com/aws/aws-sdk-go/service/iam" 26 | "github.com/aws/aws-sdk-go/service/iam/iamiface" 27 | "github.com/aws/aws-sdk-go/service/sts" 28 | "github.com/aws/aws-sdk-go/service/sts/stsiface" 29 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/clients/useragent" 30 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/config" 31 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/utils" 32 | "github.com/gorilla/mux" 33 | "github.com/pkg/errors" 34 | "github.com/sirupsen/logrus" 35 | ) 36 | 37 | const ( 38 | temporaryCredentialsDurationInS = 3600 39 | roleSessionNameLength = 64 40 | ) 41 | 42 | const ( 43 | // CredentialExpirationTimeFormat is the time stamp format used in the Local Credentials Service HTTP response 44 | CredentialExpirationTimeFormat = time.RFC3339 45 | ) 46 | 47 | // CredentialService vends credentials to containers 48 | type CredentialService struct { 49 | iamClient iamiface.IAMAPI 50 | stsClient stsiface.STSAPI 51 | currentSession *session.Session 52 | } 53 | 54 | // NewCredentialService returns a struct that handles credentials requests 55 | func NewCredentialService() (*CredentialService, error) { 56 | iamCustomEndpoint := utils.GetValue("", config.IAMCustomEndpointVar) 57 | if iamCustomEndpoint != "" { 58 | logrus.Infof("Using custom IAM endpoint %s", iamCustomEndpoint) 59 | } 60 | 61 | stsCustomEndpoint := utils.GetValue("", config.STSCustomEndpointVar) 62 | if stsCustomEndpoint != "" { 63 | logrus.Infof("Using custom STS endpoint %s", stsCustomEndpoint) 64 | } 65 | 66 | defaultResolver := endpoints.DefaultResolver() 67 | customResolverFn := func(service, region string, optFns ...func(*endpoints.Options)) (endpoints.ResolvedEndpoint, error) { 68 | if service == endpoints.IamServiceID && iamCustomEndpoint != "" { 69 | return endpoints.ResolvedEndpoint{ 70 | URL: iamCustomEndpoint, 71 | }, nil 72 | } else if service == endpoints.StsServiceID && stsCustomEndpoint != "" { 73 | return endpoints.ResolvedEndpoint{ 74 | URL: stsCustomEndpoint, 75 | }, nil 76 | } 77 | return defaultResolver.EndpointFor(service, region, optFns...) 78 | } 79 | 80 | sess, err := session.NewSessionWithOptions(session.Options{ 81 | Config: aws.Config{ 82 | EndpointResolver: endpoints.ResolverFunc(customResolverFn), 83 | CredentialsChainVerboseErrors: aws.Bool(true), 84 | }, 85 | SharedConfigState: session.SharedConfigEnable, 86 | }) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | iamClient := iam.New(sess) 92 | iamClient.Handlers.Build.PushBackNamed(useragent.CustomUserAgentHandler()) 93 | stsClient := sts.New(sess) 94 | stsClient.Handlers.Build.PushBackNamed(useragent.CustomUserAgentHandler()) 95 | return NewCredentialServiceWithClients(iamClient, stsClient, sess), nil 96 | } 97 | 98 | // NewCredentialServiceWithClients returns a struct that handles credentials requests with the given clients 99 | func NewCredentialServiceWithClients(iamClient iamiface.IAMAPI, stsClient stsiface.STSAPI, currentSession *session.Session) *CredentialService { 100 | return &CredentialService{ 101 | iamClient: iamClient, 102 | stsClient: stsClient, 103 | currentSession: currentSession, 104 | } 105 | } 106 | 107 | // SetupRoutes sets up the credentials paths in mux 108 | func (service *CredentialService) SetupRoutes(router *mux.Router) { 109 | router.HandleFunc(config.RoleCredentialsPath, ServeHTTP(service.getRoleHandler())) 110 | router.HandleFunc(config.RoleCredentialsPathWithSlash, ServeHTTP(service.getRoleHandler())) 111 | 112 | router.HandleFunc(config.RoleArnCredentialsPath, ServeHTTP(service.getRoleArnHandler())) 113 | router.HandleFunc(config.RoleArnCredentialsPathWithSlash, ServeHTTP(service.getRoleArnHandler())) 114 | 115 | router.HandleFunc(config.TempCredentialsPath, ServeHTTP(service.getTemporaryCredentialHandler())) 116 | router.HandleFunc(config.TempCredentialsPathWithSlash, ServeHTTP(service.getTemporaryCredentialHandler())) 117 | } 118 | 119 | // GetRoleHandler returns the Task IAM Role handler 120 | func (service *CredentialService) getRoleHandler() func(w http.ResponseWriter, r *http.Request) error { 121 | return func(w http.ResponseWriter, r *http.Request) error { 122 | logrus.Debug("Received role credentials request") 123 | 124 | vars := mux.Vars(r) 125 | roleName := vars["role"] 126 | if roleName == "" { 127 | return HTTPError{ 128 | Code: http.StatusBadRequest, 129 | Err: fmt.Errorf("Invalid URL path %s; expected '/role/'", r.URL.Path), 130 | } 131 | } 132 | 133 | response, err := service.getRoleCredentials(roleName) 134 | if err != nil { 135 | return err 136 | } 137 | 138 | writeJSONResponse(w, response) 139 | return nil 140 | } 141 | } 142 | 143 | // GetRoleArnHandler returns the Task IAM Role handler for complete role ARNs 144 | func (service *CredentialService) getRoleArnHandler() func(w http.ResponseWriter, r *http.Request) error { 145 | return func(w http.ResponseWriter, r *http.Request) error { 146 | logrus.Debug("Received role credentials request using ARN") 147 | 148 | vars := mux.Vars(r) 149 | roleName := vars["roleName"] 150 | roleArn := fmt.Sprintf("%s/%s", vars["roleArn"], roleName) 151 | if roleArn == "" { 152 | return HTTPError{ 153 | Code: http.StatusBadRequest, 154 | Err: fmt.Errorf("Invalid URL path %s; expected '/role-arn/", r.URL.Path), 155 | } 156 | } 157 | 158 | response, err := service.getRoleCredentialsFromArn(roleArn, roleName) 159 | if err != nil { 160 | return err 161 | } 162 | 163 | writeJSONResponse(w, response) 164 | return nil 165 | } 166 | } 167 | 168 | func (service *CredentialService) getRoleCredentials(roleName string) (*CredentialResponse, error) { 169 | logrus.Debugf("Requesting credentials for %s", roleName) 170 | 171 | output, err := service.iamClient.GetRole(&iam.GetRoleInput{ 172 | RoleName: aws.String(roleName), 173 | }) 174 | if err != nil { 175 | return nil, err 176 | } 177 | 178 | return service.getRoleCredentialsFromArn(aws.StringValue(output.Role.Arn), roleName) 179 | } 180 | 181 | func (service *CredentialService) getRoleCredentialsFromArn(roleArn, roleName string) (*CredentialResponse, error) { 182 | logrus.Debugf("Requesting credentials for role with ARN %s", roleArn) 183 | 184 | creds, err := service.stsClient.AssumeRole(&sts.AssumeRoleInput{ 185 | RoleArn: aws.String(roleArn), 186 | DurationSeconds: aws.Int64(temporaryCredentialsDurationInS), 187 | RoleSessionName: aws.String(utils.Truncate(fmt.Sprintf("ecs-local-%s", roleName), roleSessionNameLength)), 188 | }) 189 | 190 | if err != nil { 191 | return nil, err 192 | } 193 | 194 | return &CredentialResponse{ 195 | AccessKeyID: aws.StringValue(creds.Credentials.AccessKeyId), 196 | SecretAccessKey: aws.StringValue(creds.Credentials.SecretAccessKey), 197 | RoleArn: roleArn, 198 | Token: aws.StringValue(creds.Credentials.SessionToken), 199 | Expiration: creds.Credentials.Expiration.Format(CredentialExpirationTimeFormat), 200 | }, nil 201 | } 202 | 203 | // GetTemporaryCredentialHandler returns a handler which vends temporary credentials for the local IAM identity 204 | func (service *CredentialService) getTemporaryCredentialHandler() func(w http.ResponseWriter, r *http.Request) error { 205 | return func(w http.ResponseWriter, r *http.Request) error { 206 | logrus.Debug("Received temporary local credentials request") 207 | 208 | response, err := service.getTemporaryCredentials() 209 | if err != nil { 210 | return err 211 | } 212 | 213 | writeJSONResponse(w, response) 214 | return nil 215 | } 216 | } 217 | 218 | func (service *CredentialService) getTemporaryCredentials() (*CredentialResponse, error) { 219 | // check if the current session already was built on temp creds 220 | // because temp creds do not have the power to call GetSessionToken 221 | if service.isCurrentSessionTemporary() { 222 | credVal, err := service.currentSession.Config.Credentials.Get() 223 | if err != nil { 224 | return nil, errors.Wrap(err, "Current session is based on temporary credentials, but they were not retrieved.") 225 | } 226 | 227 | logrus.Debug("Current session contains temporary credentials") 228 | response := CredentialResponse{ 229 | AccessKeyID: credVal.AccessKeyID, 230 | SecretAccessKey: credVal.SecretAccessKey, 231 | Token: credVal.SessionToken, 232 | } 233 | 234 | expiration, err := service.currentSession.Config.Credentials.ExpiresAt() 235 | 236 | // It is valid for a credential provider to not return an expiration; 237 | // however, we need to have an expiration if a token is present to 238 | // satsify various client SDKs. In this case, we return an expiration 239 | // timestamp a fixed point in the future. 240 | // https://github.com/awslabs/amazon-ecs-local-container-endpoints/issues/26 241 | if err != nil && len(response.Token) > 0 { 242 | expiration, err = getSharedTokenExpiration() 243 | } 244 | 245 | if err == nil { 246 | response.Expiration = expiration.Format(CredentialExpirationTimeFormat) 247 | } 248 | 249 | return &response, nil 250 | } 251 | 252 | // current session is not temp creds, so we can call GetSessionToken 253 | creds, err := service.stsClient.GetSessionToken(&sts.GetSessionTokenInput{ 254 | DurationSeconds: aws.Int64(temporaryCredentialsDurationInS), 255 | }) 256 | 257 | if err != nil { 258 | return nil, err 259 | } 260 | 261 | response := CredentialResponse{ 262 | AccessKeyID: aws.StringValue(creds.Credentials.AccessKeyId), 263 | SecretAccessKey: aws.StringValue(creds.Credentials.SecretAccessKey), 264 | Token: aws.StringValue(creds.Credentials.SessionToken), 265 | Expiration: creds.Credentials.Expiration.Format(CredentialExpirationTimeFormat), 266 | } 267 | 268 | return &response, nil 269 | } 270 | 271 | func (service *CredentialService) isCurrentSessionTemporary() bool { 272 | if service.currentSession != nil && service.currentSession.Config != nil && service.currentSession.Config.Credentials != nil { 273 | credVal, err := service.currentSession.Config.Credentials.Get() 274 | 275 | if err == nil && credVal.SessionToken != "" { // current session is already temp creds 276 | return true 277 | } 278 | } 279 | return false 280 | } 281 | 282 | // Return an expiration date a set point in the future. error is currently 283 | // always nil (we gracefully fail back to the 12.5 minute default), but we 284 | // reserve it for future use in case there are valid reasons to error out. 285 | func getSharedTokenExpiration() (time.Time, error) { 286 | durationStr := utils.GetValue(fmt.Sprintf("%ds", config.DefaultSharedTokenExpiration), config.SharedTokenExpirationVar) 287 | duration, err := time.ParseDuration(durationStr) 288 | 289 | if err != nil { 290 | // If they didn't provide a unit, try to parse this as seconds. 291 | durationSeconds, err := strconv.ParseInt(durationStr, 0, 64) 292 | if err != nil { 293 | logrus.Warnf( 294 | "Could not parse SHARED_TOKEN_EXPIRATION value, defaulting to %d seconds: %s", 295 | config.DefaultSharedTokenExpiration, durationStr) 296 | durationSeconds = config.DefaultSharedTokenExpiration 297 | } 298 | 299 | duration = time.Duration(durationSeconds) * time.Second 300 | } 301 | 302 | // Make sure the duration is always in the future. 303 | if duration <= 0 { 304 | logrus.Warnf( 305 | "SHARED_TOKEN_EXPIRATION value must be positive, forcing to %d seconds: %s", 306 | config.DefaultSharedTokenExpiration, durationStr) 307 | duration = config.DefaultSharedTokenExpiration * time.Second 308 | } 309 | 310 | return time.Now().UTC().Add(duration), nil 311 | } 312 | -------------------------------------------------------------------------------- /local-container-endpoints/handlers/credentials_handler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package handlers 15 | 16 | import ( 17 | "fmt" 18 | "testing" 19 | "time" 20 | 21 | "github.com/aws/aws-sdk-go/aws" 22 | "github.com/aws/aws-sdk-go/aws/credentials" 23 | "github.com/aws/aws-sdk-go/aws/session" 24 | "github.com/aws/aws-sdk-go/service/iam" 25 | "github.com/aws/aws-sdk-go/service/sts" 26 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/clients/iam/mock_iamiface" 27 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/clients/sts/mock_stsiface" 28 | "github.com/golang/mock/gomock" 29 | "github.com/stretchr/testify/assert" 30 | ) 31 | 32 | const ( 33 | roleName = "clyde_task_role" 34 | roleARN = "arn:aws:iam::111111111111111:role/clyde_task_role" 35 | secretKey = "SKID" 36 | accessKey = "AKID" 37 | sessionToken = "token" 38 | expirationTimeString = "2009-11-10T23:00:00Z" 39 | ) 40 | 41 | func TestGetRoleCredentials(t *testing.T) { 42 | iamMock, stsMock := setupMocks(t) 43 | 44 | credsService := newCredentialServiceInTest(iamMock, stsMock) 45 | 46 | expiration, _ := time.Parse(CredentialExpirationTimeFormat, expirationTimeString) 47 | 48 | gomock.InOrder( 49 | iamMock.EXPECT().GetRole(gomock.Any()).Do(func(x interface{}) { 50 | input := x.(*iam.GetRoleInput) 51 | assert.Equal(t, roleName, aws.StringValue(input.RoleName), "Expected role name to match") 52 | }).Return(&iam.GetRoleOutput{ 53 | Role: &iam.Role{ 54 | Arn: aws.String(roleARN), 55 | }, 56 | }, nil), 57 | stsMock.EXPECT().AssumeRole(gomock.Any()).Do(func(x interface{}) { 58 | input := x.(*sts.AssumeRoleInput) 59 | assert.Equal(t, roleARN, aws.StringValue(input.RoleArn), "Expected role ARN to match") 60 | }).Return(&sts.AssumeRoleOutput{ 61 | Credentials: &sts.Credentials{ 62 | AccessKeyId: aws.String(accessKey), 63 | SecretAccessKey: aws.String(secretKey), 64 | SessionToken: aws.String(sessionToken), 65 | Expiration: &expiration, 66 | }, 67 | }, nil), 68 | ) 69 | 70 | response, err := credsService.getRoleCredentials(roleName) 71 | assert.NoError(t, err, "Unexpected error calling getRoleCredentials") 72 | assert.Equal(t, response.AccessKeyID, accessKey, "Expected access key to match") 73 | assert.Equal(t, response.SecretAccessKey, secretKey, "Expected secret key to match") 74 | assert.Equal(t, response.Token, sessionToken, "Expected session token to match") 75 | assert.Equal(t, response.Expiration, expirationTimeString, "Expected expiration to match") 76 | assert.Equal(t, response.RoleArn, roleARN, "Expected role ARN to match") 77 | 78 | } 79 | 80 | func TestGetRoleCredentialsGetRoleError(t *testing.T) { 81 | iamMock, stsMock := setupMocks(t) 82 | 83 | credsService := newCredentialServiceInTest(iamMock, stsMock) 84 | 85 | gomock.InOrder( 86 | iamMock.EXPECT().GetRole(gomock.Any()).Do(func(x interface{}) { 87 | input := x.(*iam.GetRoleInput) 88 | assert.Equal(t, roleName, aws.StringValue(input.RoleName), "Expected role name to match") 89 | }).Return(nil, fmt.Errorf("Some API Error")), 90 | ) 91 | 92 | _, err := credsService.getRoleCredentials(roleName) 93 | assert.Error(t, err, "Expected error calling getRoleCredentials") 94 | 95 | } 96 | 97 | func TestGetRoleCredentialsSTSError(t *testing.T) { 98 | iamMock, stsMock := setupMocks(t) 99 | 100 | credsService := newCredentialServiceInTest(iamMock, stsMock) 101 | 102 | gomock.InOrder( 103 | iamMock.EXPECT().GetRole(gomock.Any()).Do(func(x interface{}) { 104 | input := x.(*iam.GetRoleInput) 105 | assert.Equal(t, roleName, aws.StringValue(input.RoleName), "Expected role name to match") 106 | }).Return(&iam.GetRoleOutput{ 107 | Role: &iam.Role{ 108 | Arn: aws.String(roleARN), 109 | }, 110 | }, nil), 111 | stsMock.EXPECT().AssumeRole(gomock.Any()).Do(func(x interface{}) { 112 | input := x.(*sts.AssumeRoleInput) 113 | assert.Equal(t, roleARN, aws.StringValue(input.RoleArn), "Expected role ARN to match") 114 | }).Return(nil, fmt.Errorf("Some API Error")), 115 | ) 116 | 117 | _, err := credsService.getRoleCredentials(roleName) 118 | assert.Error(t, err, "Expected error calling getRoleCredentials") 119 | 120 | } 121 | 122 | func TestGetTemporaryCredentials(t *testing.T) { 123 | iamMock, stsMock := setupMocks(t) 124 | 125 | credsService := newCredentialServiceInTest(iamMock, stsMock) 126 | 127 | expiration, _ := time.Parse(CredentialExpirationTimeFormat, expirationTimeString) 128 | 129 | gomock.InOrder( 130 | stsMock.EXPECT().GetSessionToken(gomock.Any()).Return(&sts.GetSessionTokenOutput{ 131 | Credentials: &sts.Credentials{ 132 | AccessKeyId: aws.String(accessKey), 133 | SecretAccessKey: aws.String(secretKey), 134 | SessionToken: aws.String(sessionToken), 135 | Expiration: &expiration, 136 | }, 137 | }, nil), 138 | ) 139 | 140 | response, err := credsService.getTemporaryCredentials() 141 | assert.NoError(t, err, "Unexpected error calling getRoleCredentials") 142 | assert.Equal(t, response.AccessKeyID, accessKey, "Expected access key to match") 143 | assert.Equal(t, response.SecretAccessKey, secretKey, "Expected secret key to match") 144 | assert.Equal(t, response.Token, sessionToken, "Expected session token to match") 145 | assert.Equal(t, response.Expiration, expirationTimeString, "Expected expiration to match") 146 | 147 | } 148 | 149 | func TestGetTemporaryCredentialsErrorCase(t *testing.T) { 150 | iamMock, stsMock := setupMocks(t) 151 | 152 | credsService := newCredentialServiceInTest(iamMock, stsMock) 153 | 154 | gomock.InOrder( 155 | stsMock.EXPECT().GetSessionToken(gomock.Any()).Return(nil, fmt.Errorf("Some API Error")), 156 | ) 157 | 158 | _, err := credsService.getTemporaryCredentials() 159 | assert.Error(t, err, "Expected error calling getRoleCredentials") 160 | 161 | } 162 | 163 | type CustomProvider struct { 164 | expiration time.Time 165 | creds credentials.Value 166 | } 167 | 168 | func (m *CustomProvider) Retrieve() (credentials.Value, error) { 169 | return m.creds, nil 170 | } 171 | func (m *CustomProvider) IsExpired() bool { 172 | return false 173 | } 174 | func (m *CustomProvider) ExpiresAt() time.Time { 175 | return m.expiration 176 | } 177 | 178 | func TestGetTemporaryCredentialsExistingTempCreds(t *testing.T) { 179 | expiration, _ := time.Parse(CredentialExpirationTimeFormat, expirationTimeString) 180 | 181 | provider := &CustomProvider{ 182 | expiration: expiration, 183 | creds: credentials.Value{ 184 | AccessKeyID: accessKey, 185 | SecretAccessKey: secretKey, 186 | SessionToken: sessionToken, 187 | }, 188 | } 189 | 190 | creds := credentials.NewCredentials(provider) 191 | svcConfig := aws.NewConfig().WithCredentials(creds) 192 | sess, err := session.NewSession(svcConfig) 193 | assert.NoError(t, err, "Unexpected error creating new session") 194 | 195 | credsService := &CredentialService{ 196 | currentSession: sess, 197 | } 198 | 199 | response, err := credsService.getTemporaryCredentials() 200 | assert.NoError(t, err, "Unexpected error calling getRoleCredentials") 201 | assert.Equal(t, response.AccessKeyID, accessKey, "Expected access key to match") 202 | assert.Equal(t, response.SecretAccessKey, secretKey, "Expected secret key to match") 203 | assert.Equal(t, response.Token, sessionToken, "Expected session token to match") 204 | assert.Equal(t, response.Expiration, expirationTimeString, "Expected expiration to match") 205 | 206 | } 207 | 208 | func setupMocks(t *testing.T) (*mock_iamiface.MockIAMAPI, *mock_stsiface.MockSTSAPI) { 209 | ctrl := gomock.NewController(t) 210 | iamMock := mock_iamiface.NewMockIAMAPI(ctrl) 211 | stsMock := mock_stsiface.NewMockSTSAPI(ctrl) 212 | return iamMock, stsMock 213 | } 214 | 215 | func newCredentialServiceInTest(iamMock *mock_iamiface.MockIAMAPI, stsMock *mock_stsiface.MockSTSAPI) *CredentialService { 216 | return &CredentialService{ 217 | stsClient: stsMock, 218 | iamClient: iamMock, 219 | currentSession: nil, 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /local-container-endpoints/handlers/functional_tests/credentials_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package functionaltests 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | "io/ioutil" 20 | "net/http" 21 | "net/http/httptest" 22 | "testing" 23 | "time" 24 | 25 | "github.com/gorilla/mux" 26 | 27 | "github.com/aws/aws-sdk-go/aws" 28 | "github.com/aws/aws-sdk-go/service/iam" 29 | "github.com/aws/aws-sdk-go/service/sts" 30 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/clients/iam/mock_iamiface" 31 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/clients/sts/mock_stsiface" 32 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/handlers" 33 | "github.com/golang/mock/gomock" 34 | "github.com/stretchr/testify/assert" 35 | ) 36 | 37 | const ( 38 | roleName = "clyde_task_role" 39 | roleARN = "arn:aws:iam::111111111111111:role/clyde_task_role" 40 | secretKey = "SKID" 41 | accessKey = "AKID" 42 | sessionToken = "token" 43 | expirationTimeString = "2009-11-10T23:00:00Z" 44 | ) 45 | 46 | func TestGetRoleCredentials(t *testing.T) { 47 | iamMock, stsMock := setupMocks(t) 48 | 49 | credsService := newCredentialServiceInTest(iamMock, stsMock) 50 | 51 | expiration, _ := time.Parse(handlers.CredentialExpirationTimeFormat, expirationTimeString) 52 | 53 | gomock.InOrder( 54 | iamMock.EXPECT().GetRole(gomock.Any()).Do(func(input *iam.GetRoleInput) { 55 | assert.Equal(t, roleName, aws.StringValue(input.RoleName), "Expected role name to match") 56 | }).Return(&iam.GetRoleOutput{ 57 | Role: &iam.Role{ 58 | Arn: aws.String(roleARN), 59 | }, 60 | }, nil), 61 | stsMock.EXPECT().AssumeRole(gomock.Any()).Do(func(input *sts.AssumeRoleInput) { 62 | assert.Equal(t, roleARN, aws.StringValue(input.RoleArn), "Expected role ARN to match") 63 | }).Return(&sts.AssumeRoleOutput{ 64 | Credentials: &sts.Credentials{ 65 | AccessKeyId: aws.String(accessKey), 66 | SecretAccessKey: aws.String(secretKey), 67 | SessionToken: aws.String(sessionToken), 68 | Expiration: &expiration, 69 | }, 70 | }, nil), 71 | ) 72 | 73 | router := mux.NewRouter() 74 | credsService.SetupRoutes(router) 75 | ts := httptest.NewServer(router) 76 | defer ts.Close() 77 | 78 | res, err := http.Get(fmt.Sprintf("%s/role/%s", ts.URL, roleName)) 79 | assert.NoError(t, err, "Unexpected error making HTTP Request") 80 | response, err := ioutil.ReadAll(res.Body) 81 | res.Body.Close() 82 | assert.NoError(t, err, "Unexpected error reading HTTP response") 83 | 84 | creds := &handlers.CredentialResponse{} 85 | err = json.Unmarshal(response, creds) 86 | assert.NoError(t, err, "Unexpected error unmarshalling response") 87 | assert.Equal(t, creds.AccessKeyID, accessKey, "Expected access key to match") 88 | assert.Equal(t, creds.SecretAccessKey, secretKey, "Expected secret key to match") 89 | assert.Equal(t, creds.Token, sessionToken, "Expected session token to match") 90 | assert.Equal(t, creds.Expiration, expirationTimeString, "Expected expiration to match") 91 | assert.Equal(t, creds.RoleArn, roleARN, "Expected role ARN to match") 92 | } 93 | 94 | func TestGetTemporaryCredentials(t *testing.T) { 95 | iamMock, stsMock := setupMocks(t) 96 | 97 | credsService := newCredentialServiceInTest(iamMock, stsMock) 98 | 99 | expiration, _ := time.Parse(handlers.CredentialExpirationTimeFormat, expirationTimeString) 100 | 101 | gomock.InOrder( 102 | stsMock.EXPECT().GetSessionToken(gomock.Any()).Return(&sts.GetSessionTokenOutput{ 103 | Credentials: &sts.Credentials{ 104 | AccessKeyId: aws.String(accessKey), 105 | SecretAccessKey: aws.String(secretKey), 106 | SessionToken: aws.String(sessionToken), 107 | Expiration: &expiration, 108 | }, 109 | }, nil), 110 | ) 111 | 112 | router := mux.NewRouter() 113 | credsService.SetupRoutes(router) 114 | ts := httptest.NewServer(router) 115 | defer ts.Close() 116 | 117 | res, err := http.Get(fmt.Sprintf("%s/creds", ts.URL)) 118 | assert.NoError(t, err, "Unexpected error making HTTP Request") 119 | response, err := ioutil.ReadAll(res.Body) 120 | res.Body.Close() 121 | assert.NoError(t, err, "Unexpected error reading HTTP response") 122 | 123 | creds := &handlers.CredentialResponse{} 124 | err = json.Unmarshal(response, creds) 125 | 126 | assert.NoError(t, err, "Unexpected error calling getRoleCredentials") 127 | assert.Equal(t, creds.AccessKeyID, accessKey, "Expected access key to match") 128 | assert.Equal(t, creds.SecretAccessKey, secretKey, "Expected secret key to match") 129 | assert.Equal(t, creds.Token, sessionToken, "Expected session token to match") 130 | assert.Equal(t, creds.Expiration, expirationTimeString, "Expected expiration to match") 131 | 132 | } 133 | 134 | func setupMocks(t *testing.T) (*mock_iamiface.MockIAMAPI, *mock_stsiface.MockSTSAPI) { 135 | ctrl := gomock.NewController(t) 136 | iamMock := mock_iamiface.NewMockIAMAPI(ctrl) 137 | stsMock := mock_stsiface.NewMockSTSAPI(ctrl) 138 | return iamMock, stsMock 139 | } 140 | 141 | func newCredentialServiceInTest(iamMock *mock_iamiface.MockIAMAPI, stsMock *mock_stsiface.MockSTSAPI) *handlers.CredentialService { 142 | return handlers.NewCredentialServiceWithClients(iamMock, stsMock, nil) 143 | } 144 | -------------------------------------------------------------------------------- /local-container-endpoints/handlers/functional_tests/test_helper.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | // Package functionaltests includes tests that make http requests to the handlers using net/http/test 15 | package functionaltests 16 | 17 | import ( 18 | "math/rand" 19 | 20 | "github.com/docker/docker/api/types" 21 | ) 22 | 23 | func getMockStats() *types.Stats { 24 | return &types.Stats{ 25 | CPUStats: types.CPUStats{ 26 | SystemUsage: uint64(rand.Intn(10000)), 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /local-container-endpoints/handlers/http.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | // Package handlers containers the HTTP Handlers for Local Metadata and Credentials 15 | package handlers 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "net/http" 21 | 22 | "github.com/sirupsen/logrus" 23 | ) 24 | 25 | // Error wraps built-in error and adds a status code 26 | type Error interface { 27 | error 28 | Status() int 29 | } 30 | 31 | // HTTPError represents an error with a HTTP status code. 32 | type HTTPError struct { 33 | Code int 34 | Err error 35 | } 36 | 37 | // Error satisfies the error interface. 38 | func (herr HTTPError) Error() string { 39 | return herr.Err.Error() 40 | } 41 | 42 | // Status retutns the HTTP status code. 43 | func (herr HTTPError) Status() int { 44 | return herr.Code 45 | } 46 | 47 | // ServeHTTP wraps an HTTP Handler 48 | func ServeHTTP(handler func(w http.ResponseWriter, r *http.Request) error) func(w http.ResponseWriter, r *http.Request) { 49 | return func(w http.ResponseWriter, r *http.Request) { 50 | err := handler(w, r) 51 | if err != nil { 52 | switch e := err.(type) { 53 | case Error: 54 | // Return the specific error code and error message 55 | logrus.Errorf("HTTP %d - %s", e.Status(), err) 56 | http.Error(w, e.Error(), e.Status()) 57 | default: 58 | // default to HTTP 500 for all other errors 59 | logrus.Errorf("HTTP 500 - %s", err) 60 | // Internal Server Error: 61 | http.Error(w, fmt.Sprintf("%s: %s", http.StatusText(http.StatusInternalServerError), err.Error()), 62 | http.StatusInternalServerError) 63 | } 64 | } 65 | } 66 | } 67 | 68 | func writeJSONResponse(w http.ResponseWriter, response interface{}) { 69 | w.Header().Set("Content-Type", "application/json") 70 | w.WriteHeader(http.StatusOK) 71 | json.NewEncoder(w).Encode(response) 72 | } 73 | -------------------------------------------------------------------------------- /local-container-endpoints/handlers/metadata.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package handlers 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "net/http" 20 | "os" 21 | "strings" 22 | "time" 23 | 24 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/config" 25 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/metadata" 26 | "github.com/docker/docker/api/types" 27 | "github.com/fatih/structs" 28 | "github.com/peterbourgon/mergemap" 29 | "github.com/pkg/errors" 30 | "github.com/sirupsen/logrus" 31 | ) 32 | 33 | const ( 34 | composeProjectNameLabel = "com.docker.compose.project" 35 | ) 36 | 37 | const ( 38 | requestTypeContainerMetadata = iota + 1 39 | requestTypeContainerStats 40 | requestTypeTaskMetadata 41 | requestTypeTaskStats 42 | ) 43 | 44 | func (service *MetadataService) containerStatsResponse(w http.ResponseWriter, identifier string, callerIP string) error { 45 | timeout, _ := time.ParseDuration(config.HTTPTimeoutDuration) 46 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 47 | defer cancel() 48 | 49 | containers, err := service.dockerClient.ContainerList(ctx) 50 | if err != nil { 51 | return errors.Wrap(err, "failed to list running containers") 52 | } 53 | 54 | container, err := findContainer(containers, identifier, callerIP) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | stats, err := service.dockerClient.ContainerStats(ctx, container.ID) 60 | if err != nil { 61 | return errors.Wrap(err, "failed to get container stats") 62 | } 63 | 64 | writeJSONResponse(w, stats) 65 | return nil 66 | } 67 | 68 | func (service *MetadataService) containerMetadataResponse(w http.ResponseWriter, identifier string, callerIP string) error { 69 | timeout, _ := time.ParseDuration(config.HTTPTimeoutDuration) 70 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 71 | defer cancel() 72 | 73 | containers, err := service.dockerClient.ContainerList(ctx) 74 | if err != nil { 75 | return errors.Wrap(err, "Failed to list running containers") 76 | } 77 | container, err := findContainer(containers, identifier, callerIP) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | data := metadata.GetContainerMetadata(container) 83 | 84 | if service.baseContainerMetadata != nil { 85 | response := structs.Map(data) 86 | // Merges, with baseContainerMetadata taking priority on conflicts 87 | response = mergemap.Merge(response, service.baseContainerMetadata) 88 | writeJSONResponse(w, response) 89 | } else { 90 | writeJSONResponse(w, data) 91 | } 92 | 93 | return nil 94 | } 95 | 96 | func (service *MetadataService) taskMetadataResponse(w http.ResponseWriter, identifier string, callerIP string) error { 97 | timeout, _ := time.ParseDuration(config.HTTPTimeoutDuration) 98 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 99 | defer cancel() 100 | 101 | containers, err := service.dockerClient.ContainerList(ctx) 102 | if err != nil { 103 | return err 104 | } 105 | taskContainers := getTaskContainers(containers, identifier, callerIP) 106 | 107 | data := metadata.GetTaskMetadata(taskContainers, service.containerInstanceTags, service.taskTags) 108 | 109 | if service.baseContainerMetadata == nil && service.baseTaskMetadata == nil { 110 | writeJSONResponse(w, data) 111 | return nil 112 | } 113 | 114 | response := structs.Map(data) 115 | if service.baseContainerMetadata != nil { 116 | rawContainers, _ := response["Containers"] 117 | jsonContainers := rawContainers.([]interface{}) 118 | var mergedContainers []map[string]interface{} 119 | for _, container := range jsonContainers { 120 | // Merges, with baseContainerMetadata taking priority on conflicts 121 | cont := container.(map[string]interface{}) 122 | cont = mergemap.Merge(cont, service.baseContainerMetadata) 123 | mergedContainers = append(mergedContainers, cont) 124 | } 125 | response["Containers"] = mergedContainers 126 | } 127 | 128 | if service.baseTaskMetadata != nil { 129 | // Merges, with baseTaskMetadata taking priority on conflicts 130 | response = mergemap.Merge(response, service.baseTaskMetadata) 131 | } 132 | 133 | writeJSONResponse(w, response) 134 | return nil 135 | } 136 | 137 | func (service *MetadataService) taskStatsResponse(w http.ResponseWriter, identifier string, callerIP string) error { 138 | timeout, _ := time.ParseDuration(config.HTTPTimeoutDuration) 139 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 140 | defer cancel() 141 | 142 | containers, err := service.dockerClient.ContainerList(ctx) 143 | if err != nil { 144 | return err 145 | } 146 | response := make(map[string]types.Stats) 147 | 148 | statsChan := make(chan dockerStats, len(containers)) 149 | 150 | for _, container := range containers { 151 | go service.getContainerStatsWithChannel(ctx, statsChan, container.ID) 152 | } 153 | 154 | for range containers { 155 | select { 156 | case <-ctx.Done(): 157 | return ctx.Err() 158 | case stats := <-statsChan: 159 | if stats.err != nil { 160 | // cancel the context 161 | cancel() 162 | // Question for @sharanyad and @clareliguori: it's safe to return here, right? 163 | // Any remaining goroutines will write to the buffered channel, and terminate. 164 | // Then the buffered channel will get garbage collected. 165 | // Also calling cancel() ends the context, 166 | // so none of the Docker API requests can get stuck. 167 | // This also applies for the above case where we return ctx.Err(). 168 | return stats.err 169 | } 170 | response[stats.containerID] = *stats.stats 171 | } 172 | } 173 | 174 | writeJSONResponse(w, response) 175 | return nil 176 | } 177 | 178 | // simple struct that () sends over a channel 179 | type dockerStats struct { 180 | containerID string 181 | stats *types.Stats 182 | err error 183 | } 184 | 185 | func (service *MetadataService) getContainerStatsWithChannel(ctx context.Context, statsChan chan dockerStats, containerID string) { 186 | stats, err := service.dockerClient.ContainerStats(ctx, containerID) 187 | 188 | response := dockerStats{ 189 | stats: stats, 190 | err: err, 191 | containerID: containerID, 192 | } 193 | // send the response on the channel 194 | statsChan <- response 195 | } 196 | 197 | // A Local 'Task' is defined as all containers in the same Docker Compose Project as the caller container 198 | // OR all containers running on this machine if the user is not using Compose 199 | func getTaskContainers(allContainers []types.Container, identifier string, callerIP string) []types.Container { 200 | callerContainer, err := findContainer(allContainers, identifier, callerIP) 201 | if err != nil { 202 | logrus.Warn(err) 203 | logrus.Info("Will use all containers to represent one 'local task'") 204 | return allContainers 205 | } 206 | 207 | projectName := callerContainer.Labels[composeProjectNameLabel] 208 | 209 | if projectName == "" { 210 | logrus.Info("Will use all containers to represent one 'local task': The container which made the request is not in a Docker Compose Project") 211 | return allContainers 212 | } 213 | 214 | return filterByComposeProject(allContainers, projectName) 215 | } 216 | 217 | func filterByComposeProject(dockerContainers []types.Container, projectName string) []types.Container { 218 | var filteredContainers []types.Container 219 | 220 | for _, container := range dockerContainers { 221 | if container.Labels[composeProjectNameLabel] == projectName { 222 | filteredContainers = append(filteredContainers, container) 223 | } 224 | } 225 | 226 | if len(filteredContainers) > 0 { 227 | return filteredContainers 228 | } 229 | 230 | return dockerContainers 231 | } 232 | 233 | // Algorithm: 234 | // 1. Given a list of all running containers 235 | // 2. Filter the list by the if it was present in the request URI. If this leaves only one container, then we have found our match. 236 | // a. First we check if the identifier was is a prefix for the container ID (i.e. it is the container short ID or the full ID), and then we check if it was a subset of the container name 237 | // 3. Filter the remaining results in the list by the request IP. If this leaves only one container, then we have found our match. 238 | // 4. Filter the remaining results by the docker networks that the endpoint container is in. A container can only call the endpoints if it is in the same docker network as the endpoints container. 239 | // a. Determine which Docker Networks the Endpoints container is in by determining which container it is (We can do this using $HOSTNAME, which will be our container short ID) and then use the output of Docker API's ContainerList (https://godoc.org/github.com/docker/docker/client#Client.ContainerList) to find its networks. 240 | // b. Filter the remaining containers by selecting those containers which have the callerIP in one of the endpoints container's networks. 241 | // 5. If no container is found, or more than one container matches, we return an error. 242 | func findContainer(dockerContainers []types.Container, identifier string, callerIP string) (*types.Container, error) { 243 | var filteredList = dockerContainers 244 | 245 | if identifier != "" { 246 | filteredList = filterContainersByIdentifier(dockerContainers, identifier) 247 | if len(filteredList) == 1 { // we found the container 248 | return &filteredList[0], nil 249 | } 250 | } 251 | 252 | if callerIP != "" { 253 | filteredList = filterContainersByRequestIP(filteredList, callerIP) 254 | if len(filteredList) == 1 { // we found the container 255 | return &filteredList[0], nil 256 | } 257 | } 258 | 259 | filteredList = filterContainersByMyNetworks(filteredList, dockerContainers, callerIP) 260 | if len(filteredList) == 1 { // we found the container 261 | return &filteredList[0], nil 262 | } 263 | 264 | return nil, fmt.Errorf("Failed to find the container which the request came from. Narrowed down search to %d containers", len(filteredList)) 265 | } 266 | 267 | func filterContainersByIdentifier(dockerContainers []types.Container, identifier string) []types.Container { 268 | var filteredList []types.Container 269 | for _, container := range dockerContainers { 270 | if strings.HasPrefix(container.ID, identifier) { 271 | filteredList = append(filteredList, container) 272 | continue 273 | } 274 | 275 | for _, name := range container.Names { 276 | if strings.Contains(name, identifier) { 277 | filteredList = append(filteredList, container) 278 | } 279 | } 280 | } 281 | if len(filteredList) > 0 { 282 | return filteredList 283 | } 284 | return dockerContainers 285 | 286 | } 287 | 288 | func filterContainersByRequestIP(dockerContainers []types.Container, callerIP string) []types.Container { 289 | var filteredList []types.Container 290 | for _, container := range dockerContainers { 291 | if container.NetworkSettings == nil { 292 | continue 293 | } 294 | for _, settings := range container.NetworkSettings.Networks { 295 | if settings != nil && settings.IPAddress == callerIP { 296 | filteredList = append(filteredList, container) 297 | } 298 | } 299 | 300 | } 301 | 302 | if len(filteredList) > 0 { 303 | return filteredList 304 | } 305 | return dockerContainers 306 | } 307 | 308 | // filter the list by the networks which the endpoints container is in 309 | func filterContainersByMyNetworks(filteredContainerList []types.Container, allContainers []types.Container, callerIP string) []types.Container { 310 | // find endpoints containers 311 | var endpointContainer *types.Container 312 | shortID := os.Getenv("HOSTNAME") 313 | for _, container := range allContainers { 314 | if strings.HasPrefix(container.ID, shortID) { 315 | endpointContainer = &container 316 | } 317 | } 318 | 319 | if endpointContainer == nil || endpointContainer.NetworkSettings == nil { 320 | logrus.Warn("Failed to find endpoints container among running containers") 321 | // Return the list we were given, since we can't filter it any further 322 | return filteredContainerList 323 | } 324 | 325 | var finalList []types.Container 326 | 327 | // containers can only make request to the endpoint container from within one of its networks 328 | var networksToSearch []string 329 | for network, settings := range endpointContainer.NetworkSettings.Networks { 330 | networksToSearch = append(networksToSearch, network) 331 | if settings != nil { 332 | networksToSearch = append(networksToSearch, settings.Aliases...) 333 | } 334 | } 335 | 336 | for _, container := range filteredContainerList { 337 | if container.NetworkSettings == nil { 338 | continue 339 | } 340 | for network, settings := range container.NetworkSettings.Networks { 341 | if settings != nil && networkMatches(network, settings.Aliases, networksToSearch) && settings.IPAddress == callerIP { 342 | // This container is in one of the right networks and has the caller IP in that network 343 | finalList = append(finalList, container) 344 | } 345 | } 346 | } 347 | 348 | return finalList 349 | } 350 | 351 | // Returns true if the networkName of any alias is in the list networksToSearch 352 | func networkMatches(networkName string, aliases []string, networksToSearch []string) bool { 353 | for _, check := range networksToSearch { 354 | if networkName == check { 355 | return true 356 | } 357 | for _, alias := range aliases { 358 | if alias == check { 359 | return true 360 | } 361 | } 362 | } 363 | 364 | return false 365 | } 366 | -------------------------------------------------------------------------------- /local-container-endpoints/handlers/metadata_handler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package handlers 15 | 16 | import ( 17 | "fmt" 18 | "net" 19 | "net/http" 20 | 21 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/clients/docker" 22 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/config" 23 | "github.com/gorilla/mux" 24 | ) 25 | 26 | // MetadataService vends docker metadata to containers 27 | type MetadataService struct { 28 | dockerClient docker.Client 29 | baseTaskMetadata map[string]interface{} 30 | baseContainerMetadata map[string]interface{} 31 | containerInstanceTags map[string]string 32 | taskTags map[string]string 33 | } 34 | 35 | // NewMetadataService returns a struct that handles metadata requests 36 | func NewMetadataService(taskMetadata, contMetadata map[string]interface{}) (*MetadataService, error) { 37 | dockerClient, err := docker.NewDockerClient() 38 | if err != nil { 39 | return nil, err 40 | } 41 | return NewMetadataServiceWithClient(dockerClient, taskMetadata, contMetadata) 42 | } 43 | 44 | // NewMetadataServiceWithClient returns a struct that handles metadata requests using the given Docker Client 45 | func NewMetadataServiceWithClient(dockerClient docker.Client, taskMetadata, contMetadata map[string]interface{}) (*MetadataService, error) { 46 | metadata := &MetadataService{ 47 | dockerClient: dockerClient, 48 | } 49 | 50 | metadata.baseContainerMetadata = contMetadata 51 | metadata.baseTaskMetadata = taskMetadata 52 | 53 | // TODO: re-enable tagging when supporting the new V2 and V3 metdata with Tags paths 54 | // if ciTagVal := os.Getenv(config.ContainerInstanceTagsVar); ciTagVal != "" { 55 | // tags, err := utils.GetTagsMap(ciTagVal) 56 | // if err != nil { 57 | // return nil, err 58 | // } 59 | // metadata.containerInstanceTags = tags 60 | // } 61 | // 62 | // if taskTagVal := os.Getenv(config.TaskTagsVar); taskTagVal != "" { 63 | // tags, err := utils.GetTagsMap(taskTagVal) 64 | // if err != nil { 65 | // return nil, err 66 | // } 67 | // metadata.taskTags = tags 68 | // } 69 | 70 | return metadata, nil 71 | } 72 | 73 | // SetupV2Routes sets up the V2 Metadata routes 74 | func (service *MetadataService) SetupV2Routes(router *mux.Router) { 75 | router.HandleFunc(config.V2TaskMetadataPath, ServeHTTP(service.getMetadataHandler(requestTypeTaskMetadata))) 76 | router.HandleFunc(config.V2TaskMetadataPathWithSlash, ServeHTTP(service.getMetadataHandler(requestTypeTaskMetadata))) 77 | 78 | router.HandleFunc(config.V2TaskStatsPath, ServeHTTP(service.getMetadataHandler(requestTypeTaskStats))) 79 | router.HandleFunc(config.V2TaskStatsPathWithSlash, ServeHTTP(service.getMetadataHandler(requestTypeTaskStats))) 80 | 81 | router.HandleFunc(config.V2ContainerMetadataPath, ServeHTTP(service.getMetadataHandler(requestTypeContainerMetadata))) 82 | router.HandleFunc(config.V2ContainerMetadataPathWithSlash, ServeHTTP(service.getMetadataHandler(requestTypeContainerMetadata))) 83 | 84 | router.HandleFunc(config.V2ContainerStatsPath, ServeHTTP(service.getMetadataHandler(requestTypeContainerStats))) 85 | router.HandleFunc(config.V2ContainerStatsPathWithSlash, ServeHTTP(service.getMetadataHandler(requestTypeContainerStats))) 86 | } 87 | 88 | // SetupV3Routes sets up the V3 Metadata routes 89 | func (service *MetadataService) SetupV3Routes(router *mux.Router) { 90 | router.HandleFunc(config.V3ContainerMetadataPath, ServeHTTP(service.getMetadataHandler(requestTypeContainerMetadata))) 91 | router.HandleFunc(config.V3ContainerMetadataPathWithSlash, ServeHTTP(service.getMetadataHandler(requestTypeContainerMetadata))) 92 | router.HandleFunc(config.V3ContainerMetadataPathWithIdentifier, ServeHTTP(service.getMetadataHandler(requestTypeContainerMetadata))) 93 | router.HandleFunc(config.V3ContainerMetadataPathWithIdentifierAndSlash, ServeHTTP(service.getMetadataHandler(requestTypeContainerMetadata))) 94 | 95 | router.HandleFunc(config.V3ContainerStatsPath, ServeHTTP(service.getMetadataHandler(requestTypeContainerStats))) 96 | router.HandleFunc(config.V3ContainerStatsPathWithSlash, ServeHTTP(service.getMetadataHandler(requestTypeContainerStats))) 97 | router.HandleFunc(config.V3ContainerStatsPathWithIdentifier, ServeHTTP(service.getMetadataHandler(requestTypeContainerStats))) 98 | router.HandleFunc(config.V3ContainerStatsPathWithIdentifierAndSlash, ServeHTTP(service.getMetadataHandler(requestTypeContainerStats))) 99 | 100 | router.HandleFunc(config.V3TaskMetadataPath, ServeHTTP(service.getMetadataHandler(requestTypeTaskMetadata))) 101 | router.HandleFunc(config.V3TaskMetadataPathWithSlash, ServeHTTP(service.getMetadataHandler(requestTypeTaskMetadata))) 102 | router.HandleFunc(config.V3TaskMetadataPathWithIdentifier, ServeHTTP(service.getMetadataHandler(requestTypeTaskMetadata))) 103 | router.HandleFunc(config.V3TaskMetadataPathWithIdentifierWithSlash, ServeHTTP(service.getMetadataHandler(requestTypeTaskMetadata))) 104 | 105 | router.HandleFunc(config.V3TaskStatsPath, ServeHTTP(service.getMetadataHandler(requestTypeTaskStats))) 106 | router.HandleFunc(config.V3TaskStatsPathWithSlash, ServeHTTP(service.getMetadataHandler(requestTypeTaskStats))) 107 | router.HandleFunc(config.V3TaskStatsPathWithIdentifier, ServeHTTP(service.getMetadataHandler(requestTypeTaskStats))) 108 | router.HandleFunc(config.V3TaskStatsPathWithIdentifierAndSlash, ServeHTTP(service.getMetadataHandler(requestTypeTaskStats))) 109 | } 110 | 111 | // getMetadataHandler returns a metadata handler given a requestType 112 | func (service *MetadataService) getMetadataHandler(requestType int) func(w http.ResponseWriter, r *http.Request) error { 113 | return func(w http.ResponseWriter, r *http.Request) error { 114 | callerIP, _, err := net.SplitHostPort(r.RemoteAddr) 115 | if err != nil { 116 | // Failed to get the callerIP 117 | callerIP = "" 118 | } 119 | vars := mux.Vars(r) 120 | identifier := vars["identifier"] 121 | return service.handleRequest(requestType, w, identifier, callerIP) 122 | } 123 | } 124 | 125 | func (service *MetadataService) handleRequest(requestType int, w http.ResponseWriter, identifier string, callerIP string) error { 126 | switch requestType { 127 | case requestTypeTaskMetadata: 128 | return service.taskMetadataResponse(w, identifier, callerIP) 129 | case requestTypeTaskStats: 130 | return service.taskStatsResponse(w, identifier, callerIP) 131 | case requestTypeContainerStats: 132 | return service.containerStatsResponse(w, identifier, callerIP) 133 | case requestTypeContainerMetadata: 134 | return service.containerMetadataResponse(w, identifier, callerIP) 135 | } 136 | 137 | // This should never run, but explicitly returning an error here helps make it easy to find bugs 138 | return fmt.Errorf("There's a bug in this code: Invalid request type %d", requestType) 139 | } 140 | -------------------------------------------------------------------------------- /local-container-endpoints/handlers/metadata_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package handlers 15 | 16 | import ( 17 | "os" 18 | "testing" 19 | 20 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/testingutils" 21 | "github.com/docker/docker/api/types" 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | const ( 26 | endpointsShortID = "56771b9219b5" 27 | endpointsLongID = "56771b9219b58c8b6a286830667b62475e79753db34a0b82a98efafb20718c0f9" 28 | shortID1 = "e18ab3d25b38" 29 | longID1 = "e18ab3d25b38c8b6a287831767b62475a79853dc38a0b92a98efabb20718c0d90" 30 | longID2 = "457129ed3bd03f1fc70125c3be7bcbee760d5edf092e32155a5c6a730cd32020" 31 | longID3 = "0756a2371cad1976b07954490660f07d240a6a6f52d17594ed691799915695f7" 32 | containerName1 = "container1-puddles" 33 | containerName2 = "container2-pudding" 34 | containerName3 = "clyde-container3-dumpling" 35 | badName = "tum-tum" 36 | ipAddress = "169.254.170.2" 37 | ipAddress1 = "172.17.0.2" 38 | ipAddress2 = "172.17.0.3" 39 | ipAddress3 = "172.17.0.4" 40 | network1 = "metadata-network" 41 | network2 = "app-network" 42 | projectName = "project" 43 | projectName2 = "operation-clyde-undercover" 44 | ) 45 | 46 | func TestFindContainerWithIdentifierID(t *testing.T) { 47 | endpointsContainer := testingutils.BaseDockerContainer("endpoints", endpointsLongID).Get() 48 | container1 := testingutils.BaseDockerContainer("caller", longID1).Get() 49 | container2 := testingutils.BaseDockerContainer("pudding", longID2).Get() 50 | container3 := testingutils.BaseDockerContainer("dumpling", longID3).Get() 51 | 52 | containers := []types.Container{ 53 | endpointsContainer, 54 | container1, 55 | container2, 56 | container3, 57 | } 58 | 59 | var testCases = []struct { 60 | identifier string 61 | expectedContainer *types.Container 62 | }{ 63 | { 64 | identifier: shortID1, 65 | expectedContainer: &container1, 66 | }, 67 | { 68 | identifier: longID2, 69 | expectedContainer: &container2, 70 | }, 71 | { 72 | identifier: longID3, 73 | expectedContainer: &container3, 74 | }, 75 | } 76 | 77 | for _, testCase := range testCases { 78 | t.Run(testCase.identifier, func(t *testing.T) { 79 | actual, err := findContainer(containers, testCase.identifier, "") 80 | assert.NoError(t, err, "Unexpected error from findContainer") 81 | assert.Equal(t, testCase.expectedContainer, actual, "Expected findContainer to find the correct container") 82 | }) 83 | } 84 | 85 | } 86 | 87 | func TestFindContainerWithIdentifierName(t *testing.T) { 88 | endpointsContainer := testingutils.BaseDockerContainer("endpoints", endpointsLongID).Get() 89 | container1 := testingutils.BaseDockerContainer(containerName1, longID1).Get() 90 | container2 := testingutils.BaseDockerContainer(containerName2, longID2).Get() 91 | container3 := testingutils.BaseDockerContainer(containerName3, longID3).Get() 92 | 93 | containers := []types.Container{ 94 | container2, 95 | container1, 96 | endpointsContainer, 97 | container3, 98 | } 99 | 100 | var testCases = []struct { 101 | identifier string 102 | expectedContainer *types.Container 103 | }{ 104 | { 105 | identifier: containerName1, 106 | expectedContainer: &container1, 107 | }, 108 | { 109 | identifier: containerName2, 110 | expectedContainer: &container2, 111 | }, 112 | { 113 | identifier: containerName3, 114 | expectedContainer: &container3, 115 | }, 116 | } 117 | 118 | for _, testCase := range testCases { 119 | t.Run(testCase.identifier, func(t *testing.T) { 120 | actual, err := findContainer(containers, testCase.identifier, "") 121 | assert.NoError(t, err, "Unexpected error from findContainer") 122 | assert.Equal(t, testCase.expectedContainer, actual, "Expected findContainer to find the correct container") 123 | }) 124 | } 125 | 126 | } 127 | 128 | func TestFindContainerWithCallerIP(t *testing.T) { 129 | endpointsContainer := testingutils.BaseDockerContainer("endpoints", endpointsLongID).WithNetwork("bridge", ipAddress).Get() 130 | container1 := testingutils.BaseDockerContainer(containerName1, longID1).WithNetwork("bridge", ipAddress1).Get() 131 | container2 := testingutils.BaseDockerContainer(containerName2, longID2).WithNetwork("bridge", ipAddress2).Get() 132 | container3 := testingutils.BaseDockerContainer(containerName3, longID3).WithNetwork("bridge", ipAddress3).Get() 133 | 134 | containers := []types.Container{ 135 | container3, 136 | container1, 137 | container2, 138 | endpointsContainer, 139 | } 140 | 141 | var testCases = []struct { 142 | callerIP string 143 | expectedContainer *types.Container 144 | }{ 145 | { 146 | callerIP: ipAddress1, 147 | expectedContainer: &container1, 148 | }, 149 | { 150 | callerIP: ipAddress2, 151 | expectedContainer: &container2, 152 | }, 153 | { 154 | callerIP: ipAddress3, 155 | expectedContainer: &container3, 156 | }, 157 | } 158 | 159 | for _, testCase := range testCases { 160 | t.Run(testCase.callerIP, func(t *testing.T) { 161 | actual, err := findContainer(containers, "", testCase.callerIP) 162 | assert.NoError(t, err, "Unexpected error from findContainer") 163 | assert.Equal(t, testCase.expectedContainer, actual, "Expected findContainer to find the correct container") 164 | }) 165 | } 166 | 167 | } 168 | 169 | func TestFindContainerWithCallerIPAndNetworks(t *testing.T) { 170 | os.Setenv("HOSTNAME", endpointsShortID) 171 | defer os.Unsetenv("HOSTNAME") 172 | 173 | endpointsContainer := testingutils.BaseDockerContainer("endpoints", endpointsLongID).WithNetwork(network1, ipAddress).Get() 174 | container1 := testingutils.BaseDockerContainer(containerName1, longID1).WithNetwork(network2, ipAddress1).Get() 175 | container2 := testingutils.BaseDockerContainer(containerName2, longID2).WithNetwork(network1, ipAddress2).Get() 176 | container3 := testingutils.BaseDockerContainer(containerName3, longID3).WithNetwork(network1, ipAddress1).Get() 177 | 178 | containers := []types.Container{ 179 | container3, 180 | container1, 181 | container2, 182 | endpointsContainer, 183 | } 184 | 185 | actual, err := findContainer(containers, "", ipAddress1) 186 | assert.NoError(t, err, "Unexpected error from findContainer") 187 | assert.Equal(t, &container3, actual, "Expected findContainer to find the correct container") 188 | 189 | } 190 | 191 | func TestFindContainerWithCallerIPAndNetworksFailure(t *testing.T) { 192 | endpointsContainer := testingutils.BaseDockerContainer("endpoints", endpointsLongID).WithNetwork("bridge", ipAddress).Get() 193 | container1 := testingutils.BaseDockerContainer(containerName1, longID1).WithNetwork(network2, ipAddress1).Get() 194 | container2 := testingutils.BaseDockerContainer(containerName2, longID2).WithNetwork(network1, ipAddress2).Get() 195 | container3 := testingutils.BaseDockerContainer(containerName3, longID3).WithNetwork(network1, ipAddress1).Get() 196 | 197 | containers := []types.Container{ 198 | container3, 199 | container1, 200 | container2, 201 | endpointsContainer, 202 | } 203 | 204 | _, err := findContainer(containers, "", ipAddress1) 205 | // No container matches 206 | assert.Error(t, err, "Expected error from findContainer") 207 | 208 | } 209 | 210 | // An unlikely scenario in which all of the checks (identifier, IP, and networks) must work correctly in order for the right container to be returned 211 | func TestFindContainerWithAllChecks(t *testing.T) { 212 | endpointsContainer := testingutils.BaseDockerContainer("endpoints", endpointsLongID).WithNetwork(network1, ipAddress).WithNetwork(network2, ipAddress).Get() 213 | container1 := testingutils.BaseDockerContainer(containerName1, longID1).WithNetwork(network2, ipAddress1).Get() 214 | container2 := testingutils.BaseDockerContainer(containerName2, longID2).WithNetwork(network1, ipAddress2).Get() 215 | container3 := testingutils.BaseDockerContainer(containerName3, longID3).WithNetwork(network1, ipAddress1).Get() 216 | 217 | containers := []types.Container{ 218 | container3, 219 | container1, 220 | container2, 221 | endpointsContainer, 222 | } 223 | 224 | // container 1 & 2 are matched by the identifier "pud", and container 1 & 3 have ipAddress1 in a valid network 225 | actual, err := findContainer(containers, "pud", ipAddress1) 226 | assert.NoError(t, err, "Unexpected error from findContainer") 227 | assert.Equal(t, &container1, actual, "Expected findContainer to find the correct container") 228 | 229 | // error cases to prove that both identifier and ip were needed: 230 | _, err = findContainer(containers, "pud", "") 231 | assert.Error(t, err, "Expected error from findContainer") 232 | 233 | _, err = findContainer(containers, "", ipAddress1) 234 | assert.Error(t, err, "Expected error from findContainer") 235 | 236 | } 237 | 238 | func TestFindContainerFailureMoreThanOneMatches(t *testing.T) { 239 | endpointsContainer := testingutils.BaseDockerContainer("endpoints", endpointsLongID).WithNetwork(network1, ipAddress).WithNetwork(network2, ipAddress).Get() 240 | container1 := testingutils.BaseDockerContainer(containerName1, longID1).WithNetwork(network2, ipAddress1).Get() 241 | container2 := testingutils.BaseDockerContainer(containerName2, longID2).WithNetwork(network1, ipAddress2).Get() 242 | container3 := testingutils.BaseDockerContainer(containerName3, longID3).WithNetwork(network1, ipAddress1).Get() 243 | 244 | containers := []types.Container{ 245 | container3, 246 | container1, 247 | container2, 248 | endpointsContainer, 249 | } 250 | 251 | // all the containers have 'container' in their name, and endpoints has two networks so the IPAddress doesn't identify the container either 252 | _, err := findContainer(containers, "container", ipAddress1) 253 | // No container matches 254 | assert.Error(t, err, "Expected error from findContainer") 255 | 256 | } 257 | 258 | func TestFindContainerWithIdentifierFailure(t *testing.T) { 259 | endpointsContainer := testingutils.BaseDockerContainer("endpoints", endpointsLongID).WithNetwork("bridge", ipAddress).Get() 260 | container1 := testingutils.BaseDockerContainer(containerName1, longID1).Get() 261 | container2 := testingutils.BaseDockerContainer(containerName2, longID2).Get() 262 | container3 := testingutils.BaseDockerContainer(containerName3, longID3).Get() 263 | 264 | containers := []types.Container{ 265 | container3, 266 | container1, 267 | container2, 268 | endpointsContainer, 269 | } 270 | 271 | _, err := findContainer(containers, badName, "") 272 | // No container matches 273 | assert.Error(t, err, "Expected error from findContainer") 274 | 275 | } 276 | 277 | func TestGetTaskContainers(t *testing.T) { 278 | endpointsContainer := testingutils.BaseDockerContainer("endpoints", endpointsLongID).WithNetwork(network1, ipAddress).WithComposeProject(projectName).Get() 279 | container1 := testingutils.BaseDockerContainer(containerName1, longID1).WithNetwork(network2, ipAddress1).WithComposeProject(projectName2).Get() 280 | container2 := testingutils.BaseDockerContainer(containerName2, longID2).WithNetwork(network1, ipAddress2).WithComposeProject(projectName).Get() 281 | container3 := testingutils.BaseDockerContainer(containerName3, longID3).WithNetwork(network1, ipAddress1).WithComposeProject(projectName).Get() 282 | 283 | containers := []types.Container{ 284 | container3, 285 | container1, 286 | container2, 287 | endpointsContainer, 288 | } 289 | 290 | expected := []types.Container{ 291 | container3, 292 | container2, 293 | endpointsContainer, 294 | } 295 | 296 | result := getTaskContainers(containers, "", ipAddress1) 297 | 298 | assert.ElementsMatch(t, expected, result, "Expected containers returned by getTaskContainers to be from the correct compose project") 299 | 300 | } 301 | 302 | // Pass in nil for any value which is allowed to be nil, to verify that the code can never panic 303 | func TestGetTaskContainersNilTest(t *testing.T) { 304 | defer func() { 305 | if r := recover(); r != nil { 306 | t.Error(r) 307 | } 308 | }() 309 | endpointsContainer := testingutils.BaseDockerContainer("endpoints", endpointsLongID).WithNetwork(network1, ipAddress).WithNetwork(network2, ipAddress).WithNetwork("bridge", ipAddress).WithComposeProject(projectName2).Get() 310 | container1 := testingutils.BaseDockerContainer(containerName3, longID1).WithNetwork(network2, ipAddress1).WithComposeProject(projectName2).Get() 311 | container2 := testingutils.BaseDockerContainer(containerName3, longID2).WithNetwork(network1, ipAddress2).WithComposeProject(projectName2).Get() 312 | container3 := testingutils.BaseDockerContainer(containerName3, longID3).WithNetwork(network1, ipAddress1).WithComposeProject(projectName).Get() 313 | 314 | endpointsContainer.NetworkSettings.Networks["bridge"] = nil 315 | container1.NetworkSettings.Networks[network1] = nil 316 | container2.Names = nil 317 | container3.Labels = nil 318 | 319 | containers := []types.Container{ 320 | container3, 321 | container1, 322 | container2, 323 | endpointsContainer, 324 | } 325 | 326 | getTaskContainers(containers, containerName3, ipAddress1) 327 | } 328 | 329 | func TestGetTaskContainersOneContainerReturned(t *testing.T) { 330 | // technically 331 | endpointsContainer := testingutils.BaseDockerContainer("endpoints", endpointsLongID).WithNetwork(network1, ipAddress).WithComposeProject(projectName2).Get() 332 | container1 := testingutils.BaseDockerContainer(containerName1, longID1).WithNetwork(network2, ipAddress1).WithComposeProject(projectName2).Get() 333 | container2 := testingutils.BaseDockerContainer(containerName2, longID2).WithNetwork(network1, ipAddress2).WithComposeProject(projectName2).Get() 334 | container3 := testingutils.BaseDockerContainer(containerName3, longID3).WithNetwork(network1, ipAddress1).WithComposeProject(projectName).Get() 335 | 336 | containers := []types.Container{ 337 | container3, 338 | container1, 339 | container2, 340 | endpointsContainer, 341 | } 342 | 343 | expected := []types.Container{ 344 | container3, 345 | } 346 | 347 | result := getTaskContainers(containers, containerName3, ipAddress1) 348 | 349 | assert.ElementsMatch(t, expected, result, "Expected containers returned by getTaskContainers to be from the correct compose project") 350 | 351 | } 352 | 353 | // TODO: re-enable test once metadata with Tags field is added 354 | // func TestNewMetadataServiceWithTags(t *testing.T) { 355 | // os.Setenv(config.ContainerInstanceTagsVar, "mitchell=webb,thats=numberwang") 356 | // os.Setenv(config.TaskTagsVar, "hello=goodbye,get=back,come=together") 357 | // defer os.Clearenv() 358 | // 359 | // expectedCITags := map[string]string{ 360 | // "mitchell": "webb", 361 | // "thats": "numberwang", 362 | // } 363 | // expectedTaskTags := map[string]string{ 364 | // "hello": "goodbye", 365 | // "get": "back", 366 | // "come": "together", 367 | // } 368 | // 369 | // service, err := NewMetadataService() 370 | // assert.NoError(t, err, "Unexpected error calling NewMetadataService") 371 | // assert.Equal(t, expectedCITags, service.containerInstanceTags, "Expected container instance tags to match") 372 | // assert.Equal(t, expectedTaskTags, service.taskTags, "Expected task tags to match") 373 | // } 374 | -------------------------------------------------------------------------------- /local-container-endpoints/handlers/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package handlers 15 | 16 | // CredentialResponse is used to marshal the JSON response for the Credentials Service 17 | type CredentialResponse struct { 18 | AccessKeyID string `json:"AccessKeyId"` 19 | Expiration string 20 | RoleArn string 21 | SecretAccessKey string 22 | Token string 23 | } 24 | -------------------------------------------------------------------------------- /local-container-endpoints/metadata/metadata.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | // Package metadata creates metadata responses 15 | package metadata 16 | 17 | import ( 18 | "strings" 19 | "time" 20 | 21 | "github.com/aws/amazon-ecs-agent/agent/containermetadata" 22 | "github.com/aws/amazon-ecs-agent/agent/handlers/v1" 23 | "github.com/aws/amazon-ecs-agent/agent/handlers/v2" 24 | "github.com/aws/aws-sdk-go/service/ecs" 25 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/config" 26 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/utils" 27 | "github.com/docker/docker/api/types" 28 | ) 29 | 30 | // GetTaskMetadata returns the task metadata for the given containers 31 | func GetTaskMetadata(dockerContainers []types.Container, containerInstanceTags, taskTags map[string]string) *v2.TaskResponse { 32 | response := newLocalTaskResponse(containerInstanceTags, taskTags) 33 | ecsContainers := response.Containers 34 | for _, container := range dockerContainers { 35 | ecsContainer := GetContainerMetadata(&container) 36 | ecsContainers = append(ecsContainers, *ecsContainer) 37 | } 38 | response.Containers = ecsContainers 39 | return response 40 | } 41 | 42 | // GetContainerMetadata creates a container metadata response using info from the docker API, 43 | // with other values mocked 44 | func GetContainerMetadata(dockerContainer *types.Container) *v2.ContainerResponse { 45 | response := newLocalContainerResponse() 46 | response.ID = dockerContainer.ID 47 | response.Name = getContainerName(dockerContainer) 48 | response.DockerName = getContainerName(dockerContainer) 49 | response.Image = dockerContainer.Image 50 | response.ImageID = dockerContainer.ImageID 51 | response.Ports = convertPorts(dockerContainer.Ports) 52 | response.Labels = dockerContainer.Labels 53 | createTime := time.Unix(dockerContainer.Created, 0) 54 | response.CreatedAt = &createTime 55 | // we can't know the actual start time, but we err on the side of having as many values in the response as possible 56 | response.StartedAt = response.CreatedAt 57 | response.Networks = convertNetworks(dockerContainer.NetworkSettings) 58 | response.Volumes = convertVolumes(dockerContainer.Mounts) 59 | 60 | return response 61 | } 62 | 63 | func newLocalContainerResponse() *v2.ContainerResponse { 64 | return &v2.ContainerResponse{ 65 | DesiredStatus: ecs.DesiredStatusRunning, 66 | KnownStatus: ecs.DesiredStatusRunning, 67 | Type: config.DefaultContainerType, 68 | } 69 | } 70 | 71 | func newLocalTaskResponse(containerInstanceTags, taskTags map[string]string) *v2.TaskResponse { 72 | return &v2.TaskResponse{ 73 | Cluster: utils.GetValue(config.DefaultClusterName, config.ClusterARNVar), 74 | TaskARN: utils.GetValue(config.DefaultTaskARN, config.TaskARNVar), 75 | Family: utils.GetValue(config.DefaultTDFamily, config.TDFamilyVar), 76 | Revision: utils.GetValue(config.DefaultTDRevision, config.TDRevisionVar), 77 | DesiredStatus: ecs.DesiredStatusRunning, 78 | KnownStatus: ecs.DesiredStatusRunning, 79 | TaskTags: taskTags, 80 | ContainerInstanceTags: containerInstanceTags, 81 | } 82 | } 83 | 84 | func convertVolumes(mounts []types.MountPoint) []v1.VolumeResponse { 85 | var ecsVolumes []v1.VolumeResponse 86 | for _, mount := range mounts { 87 | ecsVolumes = append(ecsVolumes, v1.VolumeResponse{ 88 | DockerName: mount.Name, 89 | Source: mount.Source, 90 | Destination: mount.Destination, 91 | }) 92 | } 93 | return ecsVolumes 94 | } 95 | 96 | func convertNetworks(dockerNetworkSettings *types.SummaryNetworkSettings) []containermetadata.Network { 97 | var ecsNetworks []containermetadata.Network 98 | for netMode, netSettings := range dockerNetworkSettings.Networks { 99 | ecsNet := containermetadata.Network{ 100 | NetworkMode: netMode, 101 | } 102 | if netSettings.IPAddress != "" { 103 | ecsNet.IPv4Addresses = []string{ 104 | netSettings.IPAddress, 105 | } 106 | } 107 | if netSettings.GlobalIPv6Address != "" { 108 | ecsNet.IPv6Addresses = []string{ 109 | netSettings.GlobalIPv6Address, 110 | } 111 | } 112 | ecsNetworks = append(ecsNetworks, ecsNet) 113 | } 114 | return ecsNetworks 115 | } 116 | 117 | func convertPorts(dockerPorts []types.Port) []v1.PortResponse { 118 | var ecsPorts []v1.PortResponse 119 | for _, port := range dockerPorts { 120 | ecsPorts = append(ecsPorts, v1.PortResponse{ 121 | ContainerPort: port.PrivatePort, 122 | HostPort: port.PublicPort, 123 | Protocol: port.Type, 124 | }) 125 | } 126 | return ecsPorts 127 | } 128 | 129 | // Docker API returns a list of container names, each prefixed by a slash 130 | // This function returns the first name in the list, and removes the slash (which is not present in the ECS Metadata response) 131 | func getContainerName(dockerContainer *types.Container) string { 132 | if len(dockerContainer.Names) > 0 { 133 | return strings.Trim(dockerContainer.Names[0], "/") 134 | } 135 | return "" 136 | } 137 | -------------------------------------------------------------------------------- /local-container-endpoints/metadata/metadata_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package metadata 15 | 16 | import ( 17 | "os" 18 | "testing" 19 | 20 | "github.com/aws/amazon-ecs-agent/agent/handlers/v2" 21 | "github.com/aws/aws-sdk-go/service/ecs" 22 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/config" 23 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/testingutils" 24 | "github.com/docker/docker/api/types" 25 | "github.com/stretchr/testify/assert" 26 | ) 27 | 28 | const ( 29 | cluster = "meow-cluster" 30 | taskARN = "arn:aws-cats:ecs:us-west-2:111111111111:task/meow-cluster/37e873f6-37b4-42a7-af47-eac7275c6152" 31 | family = "the-internet-is-for-cats" 32 | revision = "2" 33 | projectName = "meow-zedong" 34 | ipAddress = "127.0.0.5" 35 | containerID = "c3439823c17dc7a35c7e272b7dc51cb2dcdedcef428242fcd0f5473d2c724d0" 36 | containerName = "ecs-local-endpoints" 37 | ) 38 | 39 | func TestnewLocalTaskResponseWithEnvVars(t *testing.T) { 40 | expected := &v2.TaskResponse{ 41 | Cluster: cluster, 42 | TaskARN: taskARN, 43 | Family: family, 44 | Revision: revision, 45 | DesiredStatus: ecs.DesiredStatusRunning, 46 | KnownStatus: ecs.DesiredStatusRunning, 47 | } 48 | 49 | os.Setenv(config.ClusterARNVar, cluster) 50 | os.Setenv(config.TaskARNVar, taskARN) 51 | os.Setenv(config.TDFamilyVar, family) 52 | os.Setenv(config.TDRevisionVar, revision) 53 | defer os.Clearenv() 54 | 55 | actual := newLocalTaskResponse(nil, nil) 56 | assert.Equal(t, expected, actual, "Expected TaskResponse to match") 57 | } 58 | 59 | func TestGetTaskMetadata(t *testing.T) { 60 | dockerContainer := testingutils.BaseDockerContainer(containerName, containerID). 61 | WithComposeProject(projectName). 62 | WithNetwork("bridge", ipAddress). 63 | Get() 64 | 65 | expectedContainer := testingutils.BaseMetadataContainer(containerName, containerID). 66 | WithComposeProject(projectName). 67 | WithNetwork("bridge", ipAddress). 68 | Get() 69 | 70 | taskTags := map[string]string{ 71 | "task": "tags", 72 | } 73 | containerInstanceTags := map[string]string{ 74 | "containerInstance": "tags", 75 | } 76 | 77 | expected := &v2.TaskResponse{ 78 | TaskTags: taskTags, 79 | ContainerInstanceTags: containerInstanceTags, 80 | Cluster: config.DefaultClusterName, 81 | TaskARN: config.DefaultTaskARN, 82 | Family: config.DefaultTDFamily, 83 | Revision: config.DefaultTDRevision, 84 | DesiredStatus: ecs.DesiredStatusRunning, 85 | KnownStatus: ecs.DesiredStatusRunning, 86 | Containers: []v2.ContainerResponse{ 87 | expectedContainer, 88 | }, 89 | } 90 | 91 | actual := GetTaskMetadata([]types.Container{dockerContainer}, containerInstanceTags, taskTags) 92 | assert.Equal(t, expected, actual, "Expected task response to match") 93 | } 94 | -------------------------------------------------------------------------------- /local-container-endpoints/testingutils/docker_container.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | // Package testingutils provides functionality that is useful in tests accross this project 15 | package testingutils 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/docker/docker/api/types" 21 | "github.com/docker/docker/api/types/network" 22 | ) 23 | 24 | const ( 25 | image = "ecs-local-metadata_shell" 26 | imageID = "sha256:11edcbc416845013254cbab0726bb65abcc6eea1981254a888659381a630aa20" 27 | publicPort = 8000 28 | privatePort = 80 29 | protocol = "tcp" 30 | networkName = "bridge" 31 | ipAddress = "172.17.0.2" 32 | volumeName = "volume0" 33 | volumeSource = "/var/run" 34 | volumeDestination = "/run" 35 | createdAt = 1552368275 36 | ) 37 | 38 | // DockerContainer wraps types.Container, and makes it easy to create 39 | // mock responses in tests 40 | type DockerContainer struct { 41 | container types.Container 42 | } 43 | 44 | // BaseDockerContainer returns a base container that can be customized 45 | func BaseDockerContainer(name, containerID string) *DockerContainer { 46 | dockerContainer := types.Container{ 47 | ID: containerID, 48 | Names: []string{ 49 | fmt.Sprintf("/%s", name), 50 | }, 51 | Image: image, 52 | ImageID: imageID, 53 | Ports: []types.Port{ 54 | types.Port{ 55 | IP: "0.0.0.0", 56 | PrivatePort: privatePort, 57 | PublicPort: publicPort, 58 | Type: protocol, 59 | }, 60 | }, 61 | Created: createdAt, 62 | Mounts: []types.MountPoint{ 63 | types.MountPoint{ 64 | Name: volumeName, 65 | Source: volumeSource, 66 | Destination: volumeDestination, 67 | }, 68 | }, 69 | } 70 | 71 | return &DockerContainer{ 72 | container: dockerContainer, 73 | } 74 | } 75 | 76 | // WithComposeProject adds docker compose labels and returns the container for chaining 77 | func (apiContainer *DockerContainer) WithComposeProject(projectName string) *DockerContainer { 78 | labels := map[string]string{ 79 | "com.docker.compose.config-hash": "0e48fcb738f3d237e6681f0e22f32a04172949211dee8290da691925e8ed937c", 80 | "com.docker.compose.container-number": "1", 81 | "com.docker.compose.oneoff": "False", 82 | "com.docker.compose.project": projectName, 83 | "com.docker.compose.service": "ecs-local", 84 | "com.docker.compose.version": "1.23.2", 85 | } 86 | 87 | apiContainer.container.Labels = labels 88 | return apiContainer 89 | } 90 | 91 | // WithNetwork adds a Docker Network and returns the container for chaining 92 | func (apiContainer *DockerContainer) WithNetwork(networkName, ipAddress string) *DockerContainer { 93 | if apiContainer.container.NetworkSettings == nil { 94 | apiContainer.container.NetworkSettings = &types.SummaryNetworkSettings{ 95 | Networks: make(map[string]*network.EndpointSettings), 96 | } 97 | } 98 | apiContainer.container.NetworkSettings.Networks[networkName] = &network.EndpointSettings{ 99 | NetworkID: "e8884d2d5eb158e35d2d78d012e265834fb0da9cd42a288b6a5d70bfc735c84c", 100 | Gateway: "172.17.0.1", 101 | IPAddress: ipAddress, 102 | } 103 | return apiContainer 104 | } 105 | 106 | // Get returns the underlying types.Container 107 | func (apiContainer *DockerContainer) Get() types.Container { 108 | return apiContainer.container 109 | } 110 | -------------------------------------------------------------------------------- /local-container-endpoints/testingutils/metadata_container.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | // Package testingutils provides functionality that is useful in tests accross this project 15 | package testingutils 16 | 17 | import ( 18 | "time" 19 | 20 | "github.com/aws/amazon-ecs-agent/agent/containermetadata" 21 | "github.com/aws/amazon-ecs-agent/agent/handlers/v1" 22 | "github.com/aws/amazon-ecs-agent/agent/handlers/v2" 23 | "github.com/aws/aws-sdk-go/service/ecs" 24 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/config" 25 | ) 26 | 27 | // MetadataContainer wraps v2.ContainerResponse, and makes it easy to create 28 | // mock responses in tests 29 | type MetadataContainer struct { 30 | container v2.ContainerResponse 31 | } 32 | 33 | // BaseMetadataContainer returns a base container that can be customized 34 | func BaseMetadataContainer(name, containerID string) *MetadataContainer { 35 | createTime := time.Unix(createdAt, 0) 36 | container := v2.ContainerResponse{ 37 | DesiredStatus: ecs.DesiredStatusRunning, 38 | KnownStatus: ecs.DesiredStatusRunning, 39 | Type: config.DefaultContainerType, 40 | ID: containerID, 41 | Name: name, 42 | DockerName: name, 43 | Image: image, 44 | ImageID: imageID, 45 | Ports: []v1.PortResponse{ 46 | v1.PortResponse{ 47 | ContainerPort: privatePort, 48 | HostPort: publicPort, 49 | Protocol: protocol, 50 | }, 51 | }, 52 | CreatedAt: &createTime, 53 | StartedAt: &createTime, 54 | Volumes: []v1.VolumeResponse{ 55 | v1.VolumeResponse{ 56 | DockerName: volumeName, 57 | Source: volumeSource, 58 | Destination: volumeDestination, 59 | }, 60 | }, 61 | } 62 | 63 | return &MetadataContainer{ 64 | container: container, 65 | } 66 | } 67 | 68 | // WithComposeProject adds docker compose labels and returns the container for chaining 69 | func (c *MetadataContainer) WithComposeProject(projectName string) *MetadataContainer { 70 | labels := map[string]string{ 71 | "com.docker.compose.config-hash": "0e48fcb738f3d237e6681f0e22f32a04172949211dee8290da691925e8ed937c", 72 | "com.docker.compose.container-number": "1", 73 | "com.docker.compose.oneoff": "False", 74 | "com.docker.compose.project": projectName, 75 | "com.docker.compose.service": "ecs-local", 76 | "com.docker.compose.version": "1.23.2", 77 | } 78 | 79 | c.container.Labels = labels 80 | return c 81 | } 82 | 83 | // WithNetwork adds a Docker Network and returns the container for chaining 84 | func (c *MetadataContainer) WithNetwork(networkName, ipAddress string) *MetadataContainer { 85 | c.container.Networks = append(c.container.Networks, containermetadata.Network{ 86 | NetworkMode: networkName, 87 | IPv4Addresses: []string{ 88 | ipAddress, 89 | }, 90 | }) 91 | return c 92 | } 93 | 94 | // Get returns the underlying v2.ContainerResponse 95 | func (c *MetadataContainer) Get() v2.ContainerResponse { 96 | return c.container 97 | } 98 | -------------------------------------------------------------------------------- /local-container-endpoints/utils/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | // Package utils is a grab bag of things that don't belong anywhere else 15 | package utils 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "strings" 21 | ) 22 | 23 | // Truncate truncates a string 24 | func Truncate(s string, length int) string { 25 | if len(s) > length { 26 | return s[0:length] 27 | } 28 | 29 | return s 30 | } 31 | 32 | // GetTagsMap parses tags in the format key1=value1,key2=value2 33 | func GetTagsMap(value string) (map[string]string, error) { 34 | tags := make(map[string]string) 35 | keyValPairs := strings.Split(value, ",") 36 | for _, pair := range keyValPairs { 37 | split := strings.Split(pair, "=") 38 | if len(split) != 2 { 39 | return nil, fmt.Errorf("Tag input not formatted correctly: %s", pair) 40 | } 41 | tags[split[0]] = split[1] 42 | } 43 | return tags, nil 44 | } 45 | 46 | // GetValue Returns the value of the envVar, or the default 47 | func GetValue(defaultVal, envVar string) string { 48 | if val := os.Getenv(envVar); val != "" { 49 | return val 50 | } 51 | 52 | return defaultVal 53 | } 54 | -------------------------------------------------------------------------------- /local-container-endpoints/version/appname.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package version 15 | 16 | const ( 17 | // AppName is the name of this app 18 | AppName = "ecs-local-container-endpoints" 19 | ) 20 | -------------------------------------------------------------------------------- /local-container-endpoints/version/formatter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package version 15 | 16 | // String produces a human-readable string showing the local container endpoints version. 17 | func String() string { 18 | ret := AppName + " " + Version + " (" 19 | if GitDirty { 20 | ret += "*" 21 | } 22 | return ret + GitShortHash + ") ECS Agent " + AgentVersionCompatibility + " compatible" 23 | } 24 | -------------------------------------------------------------------------------- /local-container-endpoints/version/gen/version-gen.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "io/ioutil" 18 | "log" 19 | "os" 20 | "os/exec" 21 | "path/filepath" 22 | "strings" 23 | "text/template" 24 | ) 25 | 26 | const versiongoTemplate = `// This is an autogenerated file and should not be edited. 27 | 28 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 29 | // 30 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 31 | // not use this file except in compliance with the License. A copy of the 32 | // License is located at 33 | // 34 | // http://aws.amazon.com/apache2.0/ 35 | // 36 | // or in the "license" file accompanying this file. This file is distributed 37 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 38 | // express or implied. See the License for the specific language governing 39 | // permissions and limitations under the License. 40 | 41 | // Package version contains constants to indicate the current version of 42 | // ECS Local Container Endpoints. It is autogenerated 43 | package version 44 | 45 | // Please DO NOT commit any changes to this file unless you know what 46 | // you are doing. 47 | 48 | // Version is the version of ECS Local Container Endpoints 49 | const Version = "{{.Version}}" 50 | 51 | // AgentVersionCompatibility is the Latest Agent Version that ECS Local Container Endpoints is compatible with 52 | const AgentVersionCompatibility = "{{.AgentVersionCompatibility}}" 53 | 54 | // GitDirty indicates the cleanliness of the git repo when it was built 55 | const GitDirty = {{.Dirty}} 56 | 57 | // GitShortHash is the short hash of this build 58 | const GitShortHash = "{{.Hash}}" 59 | ` 60 | 61 | type versionInfo struct { 62 | Version string 63 | AgentVersionCompatibility string 64 | Dirty bool 65 | Hash string 66 | } 67 | 68 | func gitDirty() bool { 69 | cmd := exec.Command("git", "status", "--porcelain") 70 | err := cmd.Run() 71 | if err == nil { 72 | return false 73 | } 74 | return true 75 | } 76 | 77 | func gitHash() string { 78 | cmd := exec.Command("git", "rev-parse", "--short=7", "HEAD") 79 | hash, err := cmd.Output() 80 | if err != nil { 81 | return "UNKNOWN" 82 | } 83 | return strings.TrimSpace(string(hash)) 84 | } 85 | 86 | // version-gen is a simple program that generates the version file, 87 | // containing information about the version, commit hash, and repository 88 | // cleanliness. 89 | func main() { 90 | 91 | versionStr, err := ioutil.ReadFile(filepath.Join("..", "..", "VERSION")) 92 | if err != nil { 93 | log.Fatal(err) 94 | } 95 | 96 | agentversionStr, err := ioutil.ReadFile(filepath.Join("..", "..", "AGENT_VERSION_COMPATIBILITY")) 97 | if err != nil { 98 | log.Fatal(err) 99 | } 100 | 101 | // default values 102 | info := versionInfo{ 103 | Version: strings.TrimSpace(string(versionStr)), 104 | AgentVersionCompatibility: strings.TrimSpace(string(agentversionStr)), 105 | Dirty: true, 106 | Hash: "UNKNOWN", 107 | } 108 | 109 | if strings.TrimSpace(os.Getenv("ECS_RELEASE")) == "cleanbuild" { 110 | // 'clean' release; all other releases assumed dirty 111 | info.Dirty = gitDirty() 112 | } 113 | if os.Getenv("ECS_UNKNOWN_VERSION") == "" { 114 | // When the version file is updated, the above is set 115 | // Setting UNKNOWN version allows the version committed in git to never 116 | // have a commit hash so that it does not churn with every commit. This 117 | // env var should not be set when building, and go generate should be 118 | // run before any build, such that the commithash will be set correctly. 119 | info.Hash = gitHash() 120 | } 121 | 122 | outFile, err := os.Create("version.go") 123 | if err != nil { 124 | log.Fatalf("Unable to create output version file: %v", err) 125 | } 126 | t := template.Must(template.New("version").Parse(versiongoTemplate)) 127 | 128 | err = t.Execute(outFile, info) 129 | if err != nil { 130 | log.Fatalf("Error applying template: %v", err) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /local-container-endpoints/version/version.go: -------------------------------------------------------------------------------- 1 | // This is an autogenerated file and should not be edited. 2 | 3 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 6 | // not use this file except in compliance with the License. A copy of the 7 | // License is located at 8 | // 9 | // http://aws.amazon.com/apache2.0/ 10 | // 11 | // or in the "license" file accompanying this file. This file is distributed 12 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 13 | // express or implied. See the License for the specific language governing 14 | // permissions and limitations under the License. 15 | 16 | // Package version contains constants to indicate the current version of 17 | // ECS Local Container Endpoints. It is autogenerated 18 | package version 19 | 20 | // Please DO NOT commit any changes to this file unless you know what 21 | // you are doing. 22 | 23 | // Version is the version of ECS Local Container Endpoints 24 | const Version = "1.4.2" 25 | 26 | // AgentVersionCompatibility is the Latest Agent Version that ECS Local Container Endpoints is compatible with 27 | const AgentVersionCompatibility = "1.68.2" 28 | 29 | // GitDirty indicates the cleanliness of the git repo when it was built 30 | const GitDirty = true 31 | 32 | // GitShortHash is the short hash of this build 33 | const GitShortHash = "3e9f852" 34 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0/ 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | "io/ioutil" 20 | "net/http" 21 | "os" 22 | 23 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/config" 24 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/handlers" 25 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/utils" 26 | "github.com/awslabs/amazon-ecs-local-container-endpoints/local-container-endpoints/version" 27 | "github.com/gorilla/mux" 28 | "github.com/sirupsen/logrus" 29 | ) 30 | 31 | func main() { 32 | logrus.Info(version.String()) 33 | logrus.Info("Running...") 34 | credentialsService, err := handlers.NewCredentialService() 35 | if err != nil { 36 | logrus.Fatal("Failed to create Credentials Service: ", err) 37 | } 38 | 39 | contMetadata := getBaseMetadata(config.ContainerMetadataPathVar) 40 | taskMetadata := getBaseMetadata(config.TaskMetadataPathVar) 41 | 42 | metadataService, err := handlers.NewMetadataService(taskMetadata, contMetadata) 43 | if err != nil { 44 | logrus.Fatal("Failed to create Metadata Service: ", err) 45 | } 46 | 47 | port := utils.GetValue(config.DefaultPort, config.PortVar) 48 | 49 | router := mux.NewRouter() 50 | metadataService.SetupV2Routes(router) 51 | metadataService.SetupV3Routes(router) 52 | credentialsService.SetupRoutes(router) 53 | 54 | server := http.Server{ 55 | Addr: fmt.Sprintf(":%s", port), 56 | Handler: router, 57 | } 58 | err = server.ListenAndServe() 59 | if err != nil { 60 | logrus.Fatal("HTTP Server exited with error: ", err) 61 | } 62 | } 63 | 64 | func getBaseMetadata(pathVar string) map[string]interface{} { 65 | path := os.Getenv(pathVar) 66 | if path == "" { 67 | return nil 68 | } 69 | 70 | metadataFile, err := os.Open(path) 71 | if err != nil { 72 | logrus.Error("Failed to read user defined metadata file: ", err) 73 | return nil 74 | } 75 | 76 | bits, err := ioutil.ReadAll(metadataFile) 77 | if err != nil { 78 | logrus.Error("Failed to read user defined metadata file: ", err) 79 | return nil 80 | } 81 | 82 | var metadata map[string]interface{} 83 | err = json.Unmarshal(bits, &metadata) 84 | if err != nil { 85 | logrus.Error("Failed to read user defined metadata file: ", err) 86 | return nil 87 | } 88 | 89 | return metadata 90 | } 91 | -------------------------------------------------------------------------------- /scripts/build_binary.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). You 5 | # may not use this file except in compliance with the License. A copy of 6 | # the License is located at 7 | # 8 | # http://aws.amazon.com/apache2.0/ 9 | # 10 | # or in the "license" file accompanying this file. This file is 11 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 12 | # ANY KIND, either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | # Normalize to working directory being build root (up one level from ./scripts) 16 | ROOT=$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd ) 17 | cd "${ROOT}" 18 | 19 | # Builds the binary from source in the specified destination paths. 20 | mkdir -p $1 21 | 22 | # Versioning stuff. We run the generator to setup the version and then always 23 | # restore ourselves to a clean state 24 | cp local-container-endpoints/version/version.go local-container-endpoints/version/_version.go 25 | trap "cd \"${ROOT}\"; mv local-container-endpoints/version/_version.go local-container-endpoints/version/version.go" EXIT SIGHUP SIGINT SIGTERM 26 | 27 | cd local-container-endpoints/version/ 28 | go run gen/version-gen.go 29 | 30 | cd "${ROOT}" 31 | 32 | GOOS=$TARGET_GOOS GOARCH=$GOARCH CGO_ENABLED=0 GO111MODULE=on go build -installsuffix cgo -a -ldflags '-s' -o $1/local-container-endpoints ./ 33 | -------------------------------------------------------------------------------- /scripts/mockgen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the 5 | # "License"). You may not use this file except in compliance 6 | # with the License. A copy of the License is located at 7 | # 8 | # http://aws.amazon.com/apache2.0/ 9 | # 10 | # or in the "license" file accompanying this file. This file is 11 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 12 | # CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # This script wraps the mockgen tool and inserts licensing information. 17 | 18 | set -e 19 | package=${1?Must provide package} 20 | interfaces=${2?Must provide interface names} 21 | outputfile=${3?Must provide an output file} 22 | 23 | export PATH="${GOPATH//://bin:}/bin:$PATH" 24 | 25 | data=$( 26 | cat << EOF 27 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 28 | // 29 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 30 | // not use this file except in compliance with the License. A copy of the 31 | // License is located at 32 | // 33 | // http://aws.amazon.com/apache2.0/ 34 | // 35 | // or in the "license" file accompanying this file. This file is distributed 36 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 37 | // express or implied. See the License for the specific language governing 38 | // permissions and limitations under the License. 39 | 40 | $(mockgen "$package" "$interfaces") 41 | EOF 42 | ) 43 | 44 | echo "$data" | goimports > "${outputfile}" 45 | -------------------------------------------------------------------------------- /test.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17 2 | 3 | WORKDIR /go/src/github.com/awslabs/amazon-ecs-local-container-endpoints 4 | 5 | COPY go.mod go.sum ./ 6 | ARG GOPROXY=direct 7 | RUN go mod download # The first build will take 2~3 minutes but will be cached for future builds. 8 | 9 | COPY . . 10 | 11 | CMD ["echo", "run some tests please"] 12 | --------------------------------------------------------------------------------