├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── build-and-test.yaml │ ├── helm-chart-test.yaml │ ├── release.yaml │ └── stale.yaml ├── .gitignore ├── BUILD.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.windows ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── THIRD_PARTY_LICENSES.md ├── cmd └── ec2-metadata-mock │ └── main.go ├── docs ├── configuration.md ├── defaults.md └── usage.md ├── go.mod ├── go.sum ├── helm ├── amazon-ec2-metadata-mock │ ├── .helmignore │ ├── Chart.yaml │ ├── README.md │ ├── ci │ │ ├── configmap-values.yaml │ │ ├── default-values.yaml │ │ ├── local-image-values.yaml │ │ └── service-config-values.yaml │ ├── templates │ │ ├── NOTES.txt │ │ ├── _helpers.tpl │ │ ├── clusterrole.yaml │ │ ├── clusterrolebinding.yaml │ │ ├── deployment.linux.yaml │ │ ├── deployment.windows.yaml │ │ ├── psp.yaml │ │ ├── service.yaml │ │ ├── serviceaccount.yaml │ │ └── tests │ │ │ ├── test-aemm-service.yaml │ │ │ └── test-config-map.yaml │ └── values.yaml └── aws-logo.png ├── pkg ├── cmd │ ├── asglifecycle │ │ ├── asglifecycle.go │ │ └── asglifecycle_test.go │ ├── cmdutil │ │ └── cmdutil.go │ ├── events │ │ ├── events.go │ │ └── events_test.go │ ├── root │ │ ├── globalflags │ │ │ └── globalflags.go │ │ ├── root.go │ │ ├── root_test.go │ │ └── version.txt │ └── spot │ │ ├── spot.go │ │ └── spot_test.go ├── config │ ├── config.go │ ├── defaults │ │ ├── aemm-metadata-default-values.json │ │ └── defaults.go │ ├── dynamic.go │ ├── metadata.go │ ├── server.go │ ├── types.go │ └── userdata.go ├── error │ └── error.go ├── mock │ ├── asglifecycle │ │ └── asglifecycle.go │ ├── dynamic │ │ ├── dynamic.go │ │ └── types │ │ │ └── types.go │ ├── events │ │ ├── config │ │ │ └── config.go │ │ ├── events.go │ │ └── internal │ │ │ └── types │ │ │ └── types.go │ ├── handlers │ │ └── handlers.go │ ├── imdsv2 │ │ ├── imdsv2_test.go │ │ ├── tokengenerator.go │ │ └── tokenvalidator.go │ ├── root │ │ └── root.go │ ├── spot │ │ ├── config │ │ │ └── config.go │ │ ├── internal │ │ │ └── types │ │ │ │ └── types.go │ │ └── spot.go │ ├── static │ │ ├── static.go │ │ └── types │ │ │ └── types.go │ └── userdata │ │ └── userdata.go └── server │ ├── httpserver.go │ ├── httpserver_test.go │ └── swapper.go ├── scripts ├── build-binaries ├── build-docker-images ├── create-local-tag-for-release ├── ecr-public-login ├── ecr-template-for-helm-chart.json ├── generate-helm-chart-archives ├── generate-k8s-yaml ├── helm-login ├── install-amazon-ecr-credential-helper ├── prepare-for-release ├── push-docker-images ├── push-helm-chart ├── retag-docker-images ├── sync-catalog-information-for-helm-chart ├── sync-readme-to-ecr-public ├── sync-to-aws-homebrew-tap ├── update-versions-for-release ├── upload-resources-to-github └── validators │ ├── json-validator │ └── release-version-validator ├── templates └── third-party-licenses.tmpl └── test ├── e2e ├── cmd │ ├── asglifecycle-test │ ├── dynamic-test │ ├── events-test │ ├── handlers-test │ ├── imdsv2-test │ ├── root-test │ ├── spot-test │ ├── static-test │ └── userdata-test ├── golden │ ├── 400_bad_request.golden │ ├── 400_response.golden │ ├── 401_response.golden │ ├── 404_response.golden │ ├── asglifecycle │ │ └── latest │ │ │ └── meta-data │ │ │ └── index.golden │ ├── default │ │ ├── index.golden │ │ └── latest │ │ │ └── meta-data │ │ │ └── index.golden │ ├── dynamic │ │ ├── fws.golden │ │ ├── index.golden │ │ └── instance-identity.golden │ ├── events │ │ └── latest │ │ │ └── meta-data │ │ │ ├── index.golden │ │ │ └── network │ │ │ └── interfaces │ │ │ └── macs │ │ │ └── 0e_49_61_0f_c3_11 │ │ │ └── index.golden │ └── spot │ │ └── latest │ │ └── meta-data │ │ ├── index.golden │ │ └── spot.golden ├── run-tests └── testdata │ ├── aemm-config-integ.json │ └── output │ └── aemm-config-used.json ├── helm ├── chart-test.sh ├── ct.yaml ├── kind-config.yaml └── mock-ip-count-test │ ├── mock-ip-test │ ├── test-pod-404.yaml │ └── test-pod.yaml ├── helpers.go ├── readme-test ├── run-readme-spellcheck └── spellcheck-Dockerfile └── shellcheck └── run-shellcheck /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A concise description of what the bug is. 12 | 13 | **Steps to reproduce** 14 | A step-by-step description on how to reproduce the problem. 15 | 16 | **Expected outcome** 17 | A concise description of what you expected to happen. 18 | 19 | **Application Logs** 20 | The log output when experiencing the issue. 21 | 22 | 23 | **Environment** 24 | 25 | * AEMM Version: 26 | * OS/Arch: 27 | * Installation method: 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a feature/enhancement for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the feature** 11 | A concise description of the feature and desired behavior. 12 | 13 | **Is the feature request related to a problem?** 14 | A description of what the problem is. For example: I'm frustrated when [...] 15 | 16 | **Describe alternatives you've considered** 17 | A description of any alternative solutions or features you've considered. 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Issue #, if available: 2 | 3 | Description of changes: 4 | 5 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "monday" 8 | time: "09:00" 9 | timezone: "America/Chicago" 10 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yaml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags-ignore: 8 | - "v*.*.*" 9 | pull_request: 10 | workflow_dispatch: 11 | # Run M-F at 2pm CDT 12 | schedule: 13 | - cron: '0 19 * * 1-5' 14 | 15 | env: 16 | DEFAULT_GO_VERSION: ^1.23 17 | DEFAULT_PY_VERSION: "3.9" 18 | IS_PUSH: ${{ github.event_name == 'push' }} 19 | 20 | jobs: 21 | buildAndTest: 22 | name: Build and Test 23 | runs-on: ubuntu-24.04 24 | steps: 25 | - name: Set up Go 1.x 26 | uses: actions/setup-go@v2 27 | with: 28 | go-version: ${{ env.DEFAULT_GO_VERSION }} 29 | 30 | - name: Set up Python ${{ env.DEFAULT_PY_VERSION }} 31 | uses: actions/setup-python@v2 32 | with: 33 | python-version: ${{ env.DEFAULT_PY_VERSION }} 34 | 35 | - name: Check out code into the Go module directory 36 | uses: actions/checkout@v2 37 | 38 | - name: Build 39 | run: make build 40 | 41 | - name: Unit Tests 42 | run: make unit-test 43 | 44 | - name: Lints 45 | run: make spellcheck shellcheck 46 | 47 | - name: Brew Sync Dry run 48 | run: make homebrew-sync-dry-run 49 | 50 | - name: License Test 51 | run: make license-test 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | 55 | - name: E2E Tests 56 | run: make e2e-test 57 | 58 | - name: Mock IP Count Test 59 | run: make helm-mock-ip-count-test 60 | 61 | - name: Build Release Assets 62 | run: make build-release-assets 63 | 64 | - name: Build Docker Images Linux 65 | run: make build-docker-images-linux 66 | 67 | buildWindows: 68 | name: Build Docker Images Windows 69 | runs-on: windows-2025 70 | steps: 71 | - name: Set up Go 1.x 72 | uses: actions/setup-go@v2 73 | with: 74 | go-version: ${{ env.DEFAULT_GO_VERSION }} 75 | 76 | - name: Check out code into the Go module directory 77 | uses: actions/checkout@v2 78 | 79 | - name: Build Docker Images Windows 80 | run: make build-docker-images-windows 81 | -------------------------------------------------------------------------------- /.github/workflows/helm-chart-test.yaml: -------------------------------------------------------------------------------- 1 | name: Helm Chart Tests 2 | 3 | on: 4 | # Run M-F at 2pm CDT 5 | schedule: 6 | - cron: '0 19 * * 1-5' 7 | 8 | jobs: 9 | chartTests: 10 | name: Helm Chart Tests 11 | runs-on: ubuntu-20.04 12 | strategy: 13 | matrix: 14 | k8sVersion: ["1.16", "1.17", "1.18", "1.19", "1.20", "1.21"] 15 | steps: 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ${{ env.DEFAULT_GO_VERSION }} 20 | 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v2 23 | 24 | - name: Helm Chart Tests 25 | run: test/helm/chart-test.sh -i -k ${{ matrix.k8sVersion }} 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | permissions: 9 | contents: write # required for uploading releases 10 | 11 | env: 12 | DEFAULT_GO_VERSION: ^1.23 13 | DEFAULT_PY_VERSION: "3.9" 14 | GITHUB_USERNAME: ${{ secrets.EC2_BOT_GITHUB_USERNAME }} 15 | GITHUB_TOKEN: ${{ secrets.EC2_BOT_GITHUB_TOKEN }} 16 | 17 | jobs: 18 | release: 19 | name: Release 20 | runs-on: ubuntu-20.04 21 | steps: 22 | - name: Set up Go 1.x 23 | uses: actions/setup-go@v2 24 | with: 25 | go-version: ${{ env.DEFAULT_GO_VERSION }} 26 | 27 | - name: Check out code into the Go module directory 28 | uses: actions/checkout@v2 29 | 30 | - name: Validate Release Version 31 | run: make validate-release-version 32 | 33 | - name: Set Release Version 34 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 35 | 36 | - name: Build Artifacts 37 | run: make build-release-assets 38 | 39 | - name: Upload Artifacts to GitHub 40 | uses: softprops/action-gh-release@v1 41 | with: 42 | files: | 43 | build/bin/* 44 | build/k8s-resources/${{ env.RELEASE_VERSION }}/individual-resources.tar 45 | build/k8s-resources/${{ env.RELEASE_VERSION }}/all-resources.yaml 46 | build/k8s-resources/${{ env.RELEASE_VERSION }}/helm-chart-archives/* 47 | 48 | - name: Release Docker Linux 49 | run: make release-docker-linux 50 | env: 51 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 52 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 53 | AWS_SESSION_TOKEN: ${{ secrets.AWS_SESSION_TOKEN }} 54 | 55 | releaseWindows: 56 | name: Release Windows 57 | runs-on: windows-2025 58 | steps: 59 | - name: Set up Go 1.x 60 | uses: actions/setup-go@v2 61 | with: 62 | go-version: ${{ env.DEFAULT_GO_VERSION }} 63 | 64 | - name: Check out code into the Go module directory 65 | uses: actions/checkout@v2 66 | 67 | - name: Release Windows Docker Image 68 | run: make release-docker-windows 69 | env: 70 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 71 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 72 | AWS_SESSION_TOKEN: ${{ secrets.AWS_SESSION_TOKEN }} 73 | 74 | postRelease: 75 | name: Post Release 76 | runs-on: ubuntu-20.04 77 | needs: [release, releaseWindows] 78 | steps: 79 | - name: Set up Go 1.x 80 | uses: actions/setup-go@v2 81 | with: 82 | go-version: ${{ env.DEFAULT_GO_VERSION }} 83 | 84 | - name: Check out code into the Go module directory 85 | uses: actions/checkout@v2 86 | 87 | - name: Sync to Homebrew 88 | run: make homebrew-sync 89 | 90 | - name: Sync Helm Chart Catalog information 91 | run: make sync-catalog-information-for-helm-chart 92 | env: 93 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 94 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 95 | AWS_SESSION_TOKEN: ${{ secrets.AWS_SESSION_TOKEN }} 96 | 97 | - name: Sync Helm Chart to ECR Public 98 | run: make push-helm-chart 99 | env: 100 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 101 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 102 | AWS_SESSION_TOKEN: ${{ secrets.AWS_SESSION_TOKEN }} 103 | 104 | helmLint: 105 | name: Helm Lint Test 106 | runs-on: ubuntu-20.04 107 | needs: [release, releaseWindows] 108 | steps: 109 | - name: Set up Go 1.x 110 | uses: actions/setup-go@v2 111 | with: 112 | go-version: ${{ env.DEFAULT_GO_VERSION }} 113 | 114 | - name: Check out code into the Go module directory 115 | uses: actions/checkout@v2 116 | 117 | - name: Helm Lint Test 118 | run: make validate-release-version 119 | -------------------------------------------------------------------------------- /.github/workflows/stale.yaml: -------------------------------------------------------------------------------- 1 | name: Stale Issues / PRs 2 | 3 | on: 4 | schedule: 5 | - cron: "0 17 * * *" # Runs every day at 12:00PM CST 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | steps: 11 | # 15+5 day stale policy for PRs 12 | # * Except PRs marked as "stalebot-ignore" 13 | - name: Stale PRs policy 14 | uses: actions/stale@v4.0.0 15 | with: 16 | repo-token: ${{ secrets.GITHUB_TOKEN }} 17 | exempt-pr-labels: "stalebot-ignore" 18 | days-before-stale: 15 19 | days-before-close: 5 20 | days-before-issue-stale: -1 21 | days-before-issue-close: -1 22 | remove-stale-when-updated: true 23 | stale-pr-label: "stale" 24 | operations-per-run: 100 25 | stale-pr-message: > 26 | This PR has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. 27 | If you want this PR to never become stale, please ask a maintainer to apply the "stalebot-ignore" label. 28 | close-pr-message: > 29 | This PR was closed because it has become stale with no activity. 30 | 31 | # 30+5 day stale policy for open issues 32 | # * Except Issues marked as "stalebot-ignore" 33 | - name: Stale Issues policy 34 | uses: actions/stale@v4.0.0 35 | with: 36 | repo-token: ${{ secrets.GITHUB_TOKEN }} 37 | exempt-issue-labels: "stalebot-ignore" 38 | days-before-stale: 30 39 | days-before-close: 5 40 | days-before-pr-stale: -1 41 | days-before-pr-close: -1 42 | remove-stale-when-updated: true 43 | stale-issue-label: "stale" 44 | operations-per-run: 100 45 | stale-issue-message: > 46 | This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. 47 | If you want this issue to never become stale, please ask a maintainer to apply the "stalebot-ignore" label. 48 | close-issue-message: > 49 | This issue was closed because it has become stale with no activity. 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | bin/ 3 | -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- 1 | # Amazon EC2 Metadata Mock: Build Instructions 2 | 3 | ## Install Go version 1.23+ 4 | 5 | There are several options for installing go: 6 | 7 | 1. If you're on mac, you can simply `brew install go` 8 | 2. If you'd like a flexible go installation manager consider using gvm https://github.com/moovweb/gvm 9 | 3. For all other situations use the official go getting started guide: https://golang.org/doc/install 10 | 11 | ## Compile 12 | 13 | This project uses `make` to organize compilation, build, and test targets. 14 | 15 | To compile cmd/amazon-ec2-metadata-mock.go which will build the full static binary and pull in dependent packages: 16 | ``` 17 | make build 18 | ``` 19 | 20 | The resulting binary will be in the generated `build/` dir 21 | 22 | ``` 23 | $ make build 24 | 25 | $ ls build/ 26 | ec2-metadata-mock 27 | ``` 28 | 29 | ## Test 30 | 31 | You can execute the unit tests for AEMM with `make`: 32 | 33 | ``` 34 | make unit-test 35 | ``` 36 | 37 | 38 | ### Run All Tests 39 | 40 | The full suite includes unit tests, integration tests, and more. See the full list in the [makefile](https://github.com/aws/amazon-ec2-metadata-mock/blob/main/Makefile): 41 | 42 | ``` 43 | make test 44 | ``` 45 | 46 | ## Format 47 | 48 | To keep our code readable with go conventions, we use `goimports` to format the source code. 49 | Make sure to run `goimports` before you submit a PR or you'll be caught by our tests! 50 | 51 | You can use the `make fmt` target as a convenience 52 | ``` 53 | make fmt 54 | ``` 55 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Require approvals from someone in the owner team before merging 2 | # More information here: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 3 | 4 | * @aws/ec2-guacamole 5 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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, or recently closed, 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 *main* 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' 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](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23 as builder 2 | 3 | ## GOLANG env 4 | ARG GOPROXY="https://proxy.golang.org|direct" 5 | ARG GO111MODULE="on" 6 | 7 | # Copy go.mod and download dependencies 8 | WORKDIR /amazon-ec2-metadata-mock 9 | COPY go.mod . 10 | COPY go.sum . 11 | RUN go mod download 12 | 13 | ## GOLANG env 14 | ARG CGO_ENABLED=0 15 | ARG GOOS=linux 16 | ARG GOARCH=amd64 17 | 18 | # Build 19 | COPY . . 20 | RUN make build 21 | # In case the target is build for testing: 22 | # $ docker build --target=builder -t test . 23 | ENTRYPOINT ["/amazon-ec2-metadata-mock/build/ec2-metadata-mock"] 24 | 25 | # Build the final image with only the binary 26 | FROM scratch 27 | WORKDIR / 28 | COPY --from=builder /amazon-ec2-metadata-mock/build/ec2-metadata-mock . 29 | COPY THIRD_PARTY_LICENSES.md . 30 | ENTRYPOINT ["/ec2-metadata-mock"] 31 | -------------------------------------------------------------------------------- /Dockerfile.windows: -------------------------------------------------------------------------------- 1 | ARG WINDOWS_VERSION=1809 2 | 3 | # Build the manager binary 4 | FROM golang:1.23 as builder 5 | 6 | # GOLANG env 7 | ARG GOPROXY="https://proxy.golang.org|direct" 8 | ARG GO111MODULE="on" 9 | ARG CGO_ENABLED=0 10 | ARG GOOS=windows 11 | ARG GOARCH=amd64 12 | 13 | # Copy go.mod and download dependencies 14 | WORKDIR /amazon-ec2-metadata-mock 15 | COPY go.mod . 16 | COPY go.sum . 17 | RUN go mod download 18 | 19 | # Build 20 | COPY . . 21 | RUN go build -a -tags aemm-windows -o ./build/ec2-metadata-mock.exe ./cmd/... 22 | # In case the target is build for testing: 23 | # $ docker build --target=builder -t test . 24 | ENTRYPOINT ["/amazon-ec2-metadata-mock/build/ec2-metadata-mock.exe"] 25 | 26 | # Copy the controller-manager into a thin image 27 | FROM mcr.microsoft.com/windows/nanoserver:${WINDOWS_VERSION} 28 | WORKDIR / 29 | COPY --from=builder /amazon-ec2-metadata-mock/build/ec2-metadata-mock.exe . 30 | COPY THIRD_PARTY_LICENSES.md . 31 | ENTRYPOINT ["/ec2-metadata-mock.exe"] 32 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Amazon-ec2-metadata-mock 2 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. -------------------------------------------------------------------------------- /THIRD_PARTY_LICENSES.md: -------------------------------------------------------------------------------- 1 | # Third-party Licenses 2 | 3 | - github.com/aws/amazon-ec2-metadata-mock Unknown [Apache-2.0](https://github.com/aws/amazon-ec2-metadata-mock/blob/HEAD/LICENSE) 4 | - github.com/fsnotify/fsnotify v1.6.0 [BSD-3-Clause](https://github.com/fsnotify/fsnotify/blob/v1.6.0/LICENSE) 5 | - github.com/gorilla/mux v1.8.0 [BSD-3-Clause](https://github.com/gorilla/mux/blob/v1.8.0/LICENSE) 6 | - github.com/hashicorp/hcl v1.0.0 [MPL-2.0](https://github.com/hashicorp/hcl/blob/v1.0.0/LICENSE) 7 | - github.com/magiconair/properties v1.8.7 [BSD-2-Clause](https://github.com/magiconair/properties/blob/v1.8.7/LICENSE.md) 8 | - github.com/mitchellh/go-homedir v1.1.0 [MIT](https://github.com/mitchellh/go-homedir/blob/v1.1.0/LICENSE) 9 | - github.com/mitchellh/mapstructure v1.5.0 [MIT](https://github.com/mitchellh/mapstructure/blob/v1.5.0/LICENSE) 10 | - github.com/pelletier/go-toml/v2 v2.0.6 [MIT](https://github.com/pelletier/go-toml/blob/v2.0.6/LICENSE) 11 | - github.com/spf13/afero v1.9.3 [Apache-2.0](https://github.com/spf13/afero/blob/v1.9.3/LICENSE.txt) 12 | - github.com/spf13/cast v1.5.0 [MIT](https://github.com/spf13/cast/blob/v1.5.0/LICENSE) 13 | - github.com/spf13/cobra v1.6.1 [Apache-2.0](https://github.com/spf13/cobra/blob/v1.6.1/LICENSE.txt) 14 | - github.com/spf13/jwalterweatherman v1.1.0 [MIT](https://github.com/spf13/jwalterweatherman/blob/v1.1.0/LICENSE) 15 | - github.com/spf13/pflag v1.0.5 [BSD-3-Clause](https://github.com/spf13/pflag/blob/v1.0.5/LICENSE) 16 | - github.com/spf13/viper v1.15.0 [MIT](https://github.com/spf13/viper/blob/v1.15.0/LICENSE) 17 | - github.com/subosito/gotenv v1.4.2 [MIT](https://github.com/subosito/gotenv/blob/v1.4.2/LICENSE) 18 | - golang.org/x/sys/unix v0.3.0 [BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.3.0:LICENSE) 19 | - golang.org/x/text v0.5.0 [BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.5.0:LICENSE) 20 | - gopkg.in/ini.v1 v1.67.0 [Apache-2.0](https://github.com/go-ini/ini/blob/v1.67.0/LICENSE) 21 | - gopkg.in/yaml.v3 v3.0.1 [MIT](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE) 22 | -------------------------------------------------------------------------------- /cmd/ec2-metadata-mock/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 | "fmt" 18 | 19 | "github.com/aws/amazon-ec2-metadata-mock/pkg/cmd/root" 20 | ) 21 | 22 | func main() { 23 | rootCmd := root.NewCmd() 24 | if err := rootCmd.Execute(); err != nil { 25 | panic(fmt.Errorf("Fatal error while executing the root command: %s", err)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # AEMM Configuration 2 | This page serves as documentation for AEMM's configuration details. 3 | 4 | ## Configuring AEMM 5 | The tool can be configured in various ways: 6 | 7 | 1. CLI flags 8 | 9 | Use help commands to learn more 10 | 11 | 2. Env variables 12 | ``` 13 | $ export AEMM_MOCK_DELAY_SEC=12 14 | $ export AEMM_SPOT_ITN_ACTION=stop 15 | $ env | grep AEMM // To list the tool's env variables 16 | ``` 17 | 18 | > NOTE the translation of config key `spot.action` to `AEMM_SPOT_ACTION` env variable. 19 | 20 | 3. Configuration file in JSON format at `path/to/config-overrides.json` 21 | ``` 22 | { 23 | "metadata": { 24 | "paths": { 25 | "ipv4-associations": "/latest/meta-data/network/interfaces/macs/0e:49:61:0f:c3:77/ipv4-associations/192.0.2.54" 26 | }, 27 | "values": { 28 | "mac": "0e:49:61:0f:c3:77", 29 | "public-ipv4": "54.92.157.77" 30 | } 31 | }, 32 | "spot": { 33 | "action": "terminate", 34 | "time": "2020-01-07T01:03:47Z" 35 | } 36 | } 37 | ``` 38 | 39 | Use the `-c` flag to consume the configuration file and the `-s` flag to save an output of the configurations used by AEMM after precedence has been applied: 40 | 41 | ``` 42 | $ ec2-metadata-mock -c path/to/config-overrides.json -s 43 | Successfully saved final configuration to local file /path/to/home/.ec2-metadata-mock/.aemm-config-used.json 44 | 45 | 46 | $ cat $HOME/.ec2-metadata-mock/.aemm-config-used.json 47 | (truncated for readability) 48 | 49 | { 50 | "config-file": "path/to/config-overrides.json", 51 | "metadata": { 52 | "paths": { 53 | "ipv4-associations": "/latest/meta-data/network/interfaces/macs/0e:49:61:0f:c3:77/ipv4-associations/192.0.2.54" 54 | }, 55 | "values": { 56 | "mac": "0e:49:61:0f:c3:77", 57 | "public-ipv4": "54.92.157.77" 58 | } 59 | }, 60 | "mock-delay-sec": 12, 61 | "save-config-to-file": true, 62 | "server": { 63 | "hostname": "localhost", 64 | "port": "1338" 65 | }, 66 | "spot": { 67 | "action": "stop", 68 | "time": "2020-01-07T01:03:47Z" 69 | } 70 | } 71 | 72 | ``` 73 | 74 | ## Precedence (Highest to Lowest) 75 | 1. overrides 76 | 2. flag 77 | 3. env 78 | 4. config 79 | 5. key/value store 80 | 6. default 81 | 82 | For example, if values from the following sources were loaded: 83 | ``` 84 | Defaults in code: 85 | { 86 | "config-file": "$HOME/aemm-config.json", # by default, AEMM looks here for a config file 87 | "server": { 88 | "port": "1338" 89 | }, 90 | "mock-delay-sec": 0, 91 | "save-config-to-file": false, 92 | "spot": { 93 | "action": "terminate" 94 | } 95 | } 96 | 97 | Env variables: 98 | export AEMM_MOCK_DELAY_SEC=12 99 | export AEMM_SPOT_ITN_ACTION=hibernate 100 | export AEMM_CONFIG_FILE=/path/to/my-custom-aemm-config.json 101 | 102 | Config File (at /path/to/my-custom-aemm-config.json): 103 | { 104 | "imdsv2": true, 105 | "server": { 106 | "port": "1550" 107 | }, 108 | "spot": { 109 | "action": "stop" 110 | } 111 | } 112 | 113 | CLI Flags: 114 | { 115 | "mock-delay-sec": 8 116 | } 117 | ``` 118 | 119 | The resulting config will have the following values (non-overridden values are truncated for readability): 120 | ``` 121 | { 122 | "mock-delay-sec": 8, # from CLI flag 123 | "config-file": "/path/to/my-custom-aemm-config.json", # from env 124 | "spot": { 125 | "action": "hibernate" # from env 126 | } 127 | "imdsv2": true, # from custom config file at /path/to/my-custom-aemm-config.json 128 | "server": { 129 | "port": "1550" # from custom config file at /path/to/my-custom-aemm-config.json 130 | }, 131 | "save-config-to-file": false, # from defaults in code 132 | } 133 | ``` 134 | 135 | AEMM is built using Viper which is where the precedence is sourced from. More details can be found in their documentation: 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /docs/defaults.md: -------------------------------------------------------------------------------- 1 | # Default configuration 2 | 3 | ## AEMM configuration 4 | Key | Value 5 | --- | --- 6 | hostname | 0.0.0.0 7 | port | 1338 8 | input config file | $HOME/aemm-config.json 9 | output / used config file | {$HOME or working dir}/.ec2-metadata-mock/.aemm-config-used.json 10 | mock delay seconds | 0 11 | IMDS v2 only (http requests require a session token) | false 12 | spot instance action | terminate 13 | spot termination time | request time + 2 minutes in UTC 14 | scheduled events code | systemReboot 15 | scheduled events state | active 16 | scheduled events not-before | application start time in UTC 17 | scheduled events not-after | application start time + 7 days in UTC 18 | scheduled events not-before-deadline | application start time + 9 days in UTC 19 | 20 | ## Default metadata 21 | Key | Value 22 | --- | --- 23 | ami-id | ami-0a887e401f7654935 24 | ami-launch-index | 0 25 | ami-manifest-path | (unknown) 26 | block-device-mapping-ami | /dev/xvda 27 | block-device-mapping-ebs | sdb 28 | block-device-mapping-ephemeral | sdb 29 | block-device-mapping-root | /dev/xvda 30 | block-device-mapping-swap | sdcs 31 | elastic-inference-accelerator-id | eia-bfa21c7904f64a82a21b9f4540169ce1 32 | elastic-inference-accelerator-type | eia1.medium 33 | elastic-inference-associations | eia-bfa21c7904f64a82a21b9f4540169ce1 34 | event-id | instance-event-1234567890abcdef0 35 | hostname | ip-172-16-34-43.ec2.internal 36 | iam-info code | Success 37 | iam-info instanceprofilearn | arn:aws:iam::896453262835:instance-profile/baskinc-role 38 | iam-info instanceprofileid | AIPA5BOGHHXZELSK34VU4 39 | iam-info lastupdated | 2020-04-02T18:50:40Z 40 | iam-security-credentials accesskeyid | 12345678901 41 | iam-security-credentials code | Success 42 | iam-security-credentials expiration | 2020-04-02T00:49:51Z 43 | iam-security-credentials lastupdated | 2020-04-02T18:50:40Z 44 | iam-security-credentials secretaccesskey | v/12345678901 45 | iam-security-credentials token | TEST92test48TEST+y6RpoTEST92test48TEST/8oWVAiBqTEsT5Ky7ty2tEStxC1T== 46 | iam-security-credentials-role | baskinc-role 47 | instance-action | none 48 | instance-id | i-1234567890abcdef0 49 | instance-type | m4.xlarge 50 | local-hostname | ip-172-16-34-43.ec2.internal 51 | local-ipv4 | 172.16.34.43 52 | mac | 0e:49:61:0f:c3:11 53 | mac-device-number | 0 54 | mac-ipv4-associations | 192.0.2.54 55 | mac-ipv6-associations | 2001:db8:8:4::2 56 | mac-local-hostname | ip-172-16-34-43.ec2.internal 57 | mac-local-ipv4s | 172.16.34.43 58 | mac-mac | 0e:49:61:0f:c3:11 59 | mac-network-interface-id | eni-0f95d3625f5c521cc 60 | mac-owner-id | 515336597381 61 | mac-public-hostname | ec2-192-0-2-54.compute-1.amazonaws.com 62 | mac-public-ipv4s | 192.0.2.54 63 | mac-security-group-ids | sg-0b07f8f6cb485d4df 64 | mac-security-groups | ura-launch-wizard-harry-1 65 | mac-subnet-id | subnet-0ac62554 66 | mac-subnet-ipv4-cidr-block | 192.0.2.0/24 67 | mac-subnet-ipv6-cidr-blocks | 2001:db8::/32 68 | mac-vpc-id | vpc-d295a6a7 69 | mac-vpc-ipv4-cidr-block | 192.0.2.0/24 70 | mac-vpc-ipv4-cidr-blocks | 192.0.2.0/24 71 | mac-vpc-ipv6-cidr-blocks | 2001:db8::/32 72 | placement-availability-zone | us-east-1a 73 | placement-availability-zone-id | use1-az4 74 | placement-group-name | a-placement-group 75 | placement-host-id | h-0da999999f9999fb9 76 | placement-partition-number | 1 77 | placement-region | us-east-1 78 | product-codes | 3iplms73etrdhxdepv72l6ywj 79 | public-hostname | ec2-192-0-2-54.compute-1.amazonaws.com 80 | public-ipv4 | 192.0.2.54 81 | public-key | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC/JxGByvHDHgQAU+0nRFWdvMPi22OgNUn9ansrI8QN1ZJGxD1ML8DRnJ3Q3zFKqqjGucfNWW0xpVib+ttkIBp8G9P/EOcX9C3FF63O3SnnIUHJsp5faRAZsTJPx0G5HUbvhBvnAcCtSqQgmr02c1l582vAWx48pOmeXXMkl9qe9V/s7K3utmeZkRLo9DqnbsDlg5GWxLC/rWKYaZR66CnMEyZ7yBy3v3abKaGGRovLkHNAgWjSSgmUTI1nT5/S2OLxxuDnsC7+BiABLPaqlIE70SzcWZ0swx68Bo2AY9T9ymGqeAM/1T4yRtg0sPB98TpT7WrY5A3iia2UVtLO/xcTt test 82 | reservation-id | r-046cb3eca3e201d2f 83 | security-groups | ura-launch-wizard-harry-1 84 | services-domain | amazonaws.com 85 | services-partition | aws 86 | tags-instance-name | test-instance 87 | tags-instance-test | test-tag -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aws/amazon-ec2-metadata-mock 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.8.0 7 | github.com/gorilla/mux v1.8.1 8 | github.com/spf13/cobra v1.9.1 9 | github.com/spf13/pflag v1.0.6 10 | github.com/spf13/viper v1.19.0 11 | ) 12 | 13 | require ( 14 | github.com/hashicorp/hcl v1.0.0 // indirect 15 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 16 | github.com/magiconair/properties v1.8.9 // indirect 17 | github.com/mitchellh/mapstructure v1.5.0 // indirect 18 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 19 | github.com/sagikazarmark/locafero v0.6.0 // indirect 20 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 21 | github.com/sourcegraph/conc v0.3.0 // indirect 22 | github.com/spf13/afero v1.11.0 // indirect 23 | github.com/spf13/cast v1.7.0 // indirect 24 | github.com/subosito/gotenv v1.6.0 // indirect 25 | go.uber.org/multierr v1.11.0 // indirect 26 | golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect 27 | golang.org/x/sys v0.28.0 // indirect 28 | golang.org/x/text v0.21.0 // indirect 29 | gopkg.in/ini.v1 v1.67.0 // indirect 30 | gopkg.in/yaml.v3 v3.0.1 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 4 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 6 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 7 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= 8 | github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 9 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 10 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 11 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 12 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 13 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 14 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 15 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 16 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 17 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 18 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 19 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 20 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 21 | github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= 22 | github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 23 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 24 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 25 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 26 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 27 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 28 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 29 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 30 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 31 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 32 | github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= 33 | github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= 34 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 35 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 36 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 37 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 38 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 39 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 40 | github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= 41 | github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 42 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 43 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 44 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 45 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 46 | github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= 47 | github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= 48 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 49 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 50 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 51 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 52 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 53 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 54 | golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= 55 | golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= 56 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 57 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 58 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 59 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 60 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 61 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 62 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 63 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 64 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 65 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 66 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 67 | -------------------------------------------------------------------------------- /helm/amazon-ec2-metadata-mock/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /helm/amazon-ec2-metadata-mock/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | name: amazon-ec2-metadata-mock 3 | description: A Helm chart for Amazon EC2 Metadata Mock 4 | version: 1.13.0 5 | home: https://github.com/aws/amazon-ec2-metadata-mock 6 | icon: https://raw.githubusercontent.com/aws/eks-charts/master/docs/logo/aws.png 7 | sources: 8 | - https://github.com/aws/amazon-ec2-metadata-mock 9 | maintainers: 10 | - name: pdk27 11 | url: https://github.com/pdk27 12 | email: pdk27@users.noreply.github.com 13 | - name: brycahta 14 | url: https://github.com/brycahta 15 | email: brycahta@users.noreply.github.com 16 | keywords: 17 | - ec2 18 | - aws-ec2 19 | - imds 20 | - ec2-instance-metadata 21 | - ec2-instance-metadata-mock 22 | - spot-interruption-mock 23 | - ec2-rebalance-recommendation 24 | -------------------------------------------------------------------------------- /helm/amazon-ec2-metadata-mock/ci/configmap-values.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | configMap: "test-aemm-configmap" 3 | configMapFileName: "test-aemm-config.yaml" 4 | -------------------------------------------------------------------------------- /helm/amazon-ec2-metadata-mock/ci/default-values.yaml: -------------------------------------------------------------------------------- 1 | # empty values.yaml file must be present for Helm chart tests to run with default values 2 | # https://github.com/helm/charts/blob/master/test/README.md#providing-custom-test-values 3 | -------------------------------------------------------------------------------- /helm/amazon-ec2-metadata-mock/ci/local-image-values.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | image: 3 | repository: "amazon-ec2-metadata-mock" 4 | tag: "test-latest" 5 | pullPolicy: "Never" 6 | -------------------------------------------------------------------------------- /helm/amazon-ec2-metadata-mock/ci/service-config-values.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | servicePort: 1550 3 | serviceName: "my-aemm" 4 | -------------------------------------------------------------------------------- /helm/amazon-ec2-metadata-mock/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | {{ .Release.Name }} has been {{- if .Release.IsInstall }} installed {{ else }} updated. {{- end}} 2 | 3 | Some useful commands: 4 | kubectl get pods --namespace {{ .Release.Namespace }} 5 | kubectl port-forward service/amazon-ec2-metadata-mock 1338 6 | -------------------------------------------------------------------------------- /helm/amazon-ec2-metadata-mock/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "amazon-ec2-metadata-mock.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "amazon-ec2-metadata-mock.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Equivalent to "amazon-ec2-metadata-mock.fullname" except that "-win" indicator is appended to the end. 29 | Name will not exceed 63 characters. 30 | */}} 31 | {{- define "amazon-ec2-metadata-mock.fullname.windows" -}} 32 | {{- include "amazon-ec2-metadata-mock.fullname" . | trunc 59 | trimSuffix "-" | printf "%s-win" -}} 33 | {{- end -}} 34 | 35 | {{/* 36 | Common labels 37 | */}} 38 | {{- define "amazon-ec2-metadata-mock.labels" -}} 39 | app.kubernetes.io/name: {{ include "amazon-ec2-metadata-mock.name" . }} 40 | helm.sh/chart: {{ include "amazon-ec2-metadata-mock.chart" . }} 41 | app.kubernetes.io/instance: {{ .Release.Name }} 42 | {{- if .Chart.AppVersion }} 43 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 44 | {{- end }} 45 | app.kubernetes.io/managed-by: {{ .Release.Service }} 46 | {{- end -}} 47 | 48 | {{/* 49 | Create chart name and version as used by the chart label. 50 | */}} 51 | {{- define "amazon-ec2-metadata-mock.chart" -}} 52 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 53 | {{- end -}} 54 | 55 | {{/* 56 | Create the name of the service account to use 57 | */}} 58 | {{- define "amazon-ec2-metadata-mock.serviceAccountName" -}} 59 | {{- if .Values.serviceAccount.create -}} 60 | {{ default (include "amazon-ec2-metadata-mock.fullname" .) .Values.serviceAccount.name }} 61 | {{- else -}} 62 | {{ default "default" .Values.serviceAccount.name }} 63 | {{- end -}} 64 | {{- end -}} 65 | 66 | {{/* 67 | Get the default node selector term prefix. 68 | 69 | In 1.14 "beta.kubernetes.io" was deprecated and is scheduled for removal in 1.18. 70 | See https://v1-14.docs.kubernetes.io/docs/setup/release/notes/#deprecations 71 | */}} 72 | {{- define "amazon-ec2-metadata-mock.defaultNodeSelectorTermsPrefix" -}} 73 | {{- $k8sVersion := printf "%s.%s" .Capabilities.KubeVersion.Major .Capabilities.KubeVersion.Minor | replace "+" "" -}} 74 | {{- semverCompare "<1.18" $k8sVersion | ternary "beta.kubernetes.io" "kubernetes.io" -}} 75 | {{- end -}} 76 | 77 | {{/* 78 | Get the default node selector OS term. 79 | */}} 80 | {{- define "amazon-ec2-metadata-mock.defaultNodeSelectorTermsOs" -}} 81 | {{- list (include "amazon-ec2-metadata-mock.defaultNodeSelectorTermsPrefix" .) "os" | join "/" -}} 82 | {{- end -}} 83 | 84 | {{/* 85 | Get the default node selector Arch term. 86 | */}} 87 | {{- define "amazon-ec2-metadata-mock.defaultNodeSelectorTermsArch" -}} 88 | {{- list (include "amazon-ec2-metadata-mock.defaultNodeSelectorTermsPrefix" .) "arch" | join "/" -}} 89 | {{- end -}} 90 | 91 | {{/* 92 | Get the node selector OS term. 93 | */}} 94 | {{- define "amazon-ec2-metadata-mock.nodeSelectorTermsOs" -}} 95 | {{- or .Values.nodeSelectorTermsOs (include "amazon-ec2-metadata-mock.defaultNodeSelectorTermsOs" .) -}} 96 | {{- end -}} 97 | 98 | {{/* 99 | Get the node selector Arch term. 100 | */}} 101 | {{- define "amazon-ec2-metadata-mock.nodeSelectorTermsArch" -}} 102 | {{- or .Values.nodeSelectorTermsArch (include "amazon-ec2-metadata-mock.defaultNodeSelectorTermsArch" .) -}} 103 | {{- end -}} -------------------------------------------------------------------------------- /helm/amazon-ec2-metadata-mock/templates/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | # ClusterRole without any permissions for AEMM 2 | 3 | kind: ClusterRole 4 | apiVersion: rbac.authorization.k8s.io/v1 5 | metadata: 6 | name: {{ include "amazon-ec2-metadata-mock.fullname" . }} 7 | rules: [] # empty rules array to disallow all permissions for AEMM -------------------------------------------------------------------------------- /helm/amazon-ec2-metadata-mock/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | kind: ClusterRoleBinding 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: {{ include "amazon-ec2-metadata-mock.fullname" . }} 5 | subjects: 6 | - kind: ServiceAccount 7 | name: {{ template "amazon-ec2-metadata-mock.serviceAccountName" . }} 8 | namespace: {{ .Release.Namespace }} 9 | roleRef: 10 | kind: ClusterRole 11 | name: {{ include "amazon-ec2-metadata-mock.fullname" . }} 12 | apiGroup: rbac.authorization.k8s.io 13 | -------------------------------------------------------------------------------- /helm/amazon-ec2-metadata-mock/templates/psp.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.pspEnabled }} 2 | apiVersion: policy/v1beta1 3 | kind: PodSecurityPolicy 4 | metadata: 5 | name: {{ template "amazon-ec2-metadata-mock.fullname" . }} 6 | labels: 7 | {{ include "amazon-ec2-metadata-mock.labels" . | indent 4 }} 8 | annotations: 9 | seccomp.security.alpha.kubernetes.io/allowedProfileNames: '*' 10 | spec: 11 | privileged: false 12 | hostIPC: false 13 | hostNetwork: false # turn off host network to prevent undesired exposure of AEMM web server 14 | hostPorts: 15 | - min: 1024 16 | max: 65535 17 | hostPID: false 18 | readOnlyRootFilesystem: false 19 | allowPrivilegeEscalation: false 20 | allowedCapabilities: 21 | - '*' 22 | fsGroup: 23 | rule: RunAsAny 24 | runAsUser: 25 | rule: RunAsAny 26 | seLinux: 27 | rule: RunAsAny 28 | supplementalGroups: 29 | rule: RunAsAny 30 | volumes: 31 | - '*' 32 | --- 33 | kind: ClusterRole 34 | apiVersion: rbac.authorization.k8s.io/v1 35 | metadata: 36 | name: {{ template "amazon-ec2-metadata-mock.fullname" . }}-psp 37 | labels: 38 | {{ include "amazon-ec2-metadata-mock.labels" . | indent 4 }} 39 | rules: 40 | - apiGroups: ['policy'] 41 | resources: ['podsecuritypolicies'] 42 | verbs: ['use'] 43 | resourceNames: 44 | - {{ template "amazon-ec2-metadata-mock.fullname" . }} 45 | --- 46 | apiVersion: rbac.authorization.k8s.io/v1 47 | kind: RoleBinding 48 | metadata: 49 | name: {{ template "amazon-ec2-metadata-mock.fullname" . }}-psp 50 | labels: 51 | {{ include "amazon-ec2-metadata-mock.labels" . | indent 4 }} 52 | roleRef: 53 | apiGroup: rbac.authorization.k8s.io 54 | kind: ClusterRole 55 | name: {{ template "amazon-ec2-metadata-mock.fullname" . }}-psp 56 | subjects: 57 | - kind: ServiceAccount 58 | name: {{ template "amazon-ec2-metadata-mock.serviceAccountName" . }} 59 | namespace: {{ .Release.Namespace }} 60 | {{- end }} 61 | -------------------------------------------------------------------------------- /helm/amazon-ec2-metadata-mock/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ .Values.serviceName }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | {{ include "amazon-ec2-metadata-mock.labels" . | indent 4 }} 8 | spec: 9 | type: "ClusterIP" 10 | selector: 11 | app.kubernetes.io/instance: {{ .Release.Name }} 12 | ports: 13 | - protocol: TCP 14 | port: {{ .Values.servicePort | default 1338 }} 15 | targetPort: 1338 16 | -------------------------------------------------------------------------------- /helm/amazon-ec2-metadata-mock/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: {{ template "amazon-ec2-metadata-mock.serviceAccountName" . }} 5 | namespace: {{ .Release.Namespace }} 6 | {{- with .Values.serviceAccount.annotations }} 7 | annotations: 8 | {{ toYaml . | indent 4 }} 9 | {{- end }} 10 | labels: 11 | {{ include "amazon-ec2-metadata-mock.labels" . | indent 4 }} -------------------------------------------------------------------------------- /helm/amazon-ec2-metadata-mock/templates/tests/test-aemm-service.yaml: -------------------------------------------------------------------------------- 1 | # E2E tests to test the following post Helm chart installation: 2 | ## a simple http request to the service 3 | ## configmap setup, if set in the values file 4 | 5 | # The tests are run for each *values.yaml file in helm/amazon-ec2-metadata-mock/ci folder. 6 | # https://github.com/helm/charts/blob/master/test/README.md#providing-custom-test-values 7 | 8 | apiVersion: v1 9 | kind: Pod 10 | metadata: 11 | name: "{{ .Release.Name }}-helm-e2e-test" 12 | annotations: 13 | "helm.sh/hook": "test" 14 | "helm.sh/hook-delete-policy": "before-hook-creation" 15 | "helm.sh/hook-weight": "1" # create config-map first 16 | spec: 17 | restartPolicy: Never 18 | {{- if .Values.configMap }} 19 | volumes: 20 | - name: "aemm-config" 21 | configMap: 22 | name: {{ .Values.configMap }} 23 | {{- end }} 24 | containers: 25 | - name: simple-service-test 26 | imagePullPolicy: "{{ .Values.test.pullPolicy }}" 27 | image: "{{ .Values.test.image }}:{{ .Values.test.imageTag }}" 28 | command: 29 | - "bash" 30 | - "-c" 31 | - | 32 | SERVICE_NAME=$(echo {{ .Values.serviceName }} | tr '-' '_' | tr [:lower:] [:upper:]) 33 | HOST_VAR=$(echo "${SERVICE_NAME}_SERVICE_HOST") 34 | PORT_VAR=$(echo "${SERVICE_NAME}_SERVICE_PORT") 35 | ACTUAL=$(curl http://${!HOST_VAR}:${!PORT_VAR}/latest/meta-data/services/domain) 36 | EXPECTED="amazonaws.com" 37 | [[ "$ACTUAL" == "$EXPECTED" ]] && exit 0 || exit 1 38 | {{- if .Values.configMap }} 39 | - name: config-map-test 40 | imagePullPolicy: "{{ .Values.test.pullPolicy }}" 41 | image: "{{ .Values.test.image }}:{{ .Values.test.imageTag }}" 42 | volumeMounts: 43 | - name: "aemm-config" 44 | mountPath: "config/{{ .Values.configMapFileName }}" 45 | subPath: {{ .Values.configMapFileName }} 46 | readOnly: true 47 | command: 48 | - "bash" 49 | - "-c" 50 | - | 51 | SERVICE_NAME=$(echo {{ .Values.serviceName }} | tr '-' '_' | tr [:lower:] [:upper:]) 52 | HOST_VAR=$(echo "${SERVICE_NAME}_SERVICE_HOST") 53 | PORT_VAR=$(echo "${SERVICE_NAME}_SERVICE_PORT") 54 | ACTUAL=$(curl http://${!HOST_VAR}:${!PORT_VAR}/latest/meta-data/spot/termination-time) 55 | EXPECTED="1994-05-15T00:00:00Z" 56 | [[ "$ACTUAL" == "$EXPECTED" ]] && exit 0 || exit 1 57 | {{- end }} 58 | -------------------------------------------------------------------------------- /helm/amazon-ec2-metadata-mock/templates/tests/test-config-map.yaml: -------------------------------------------------------------------------------- 1 | # Configmap used for E2E testing 2 | # The tests are run for each *values.yaml file in helm/amazon-ec2-metadata-mock/ci folder. 3 | 4 | {{- if .Values.configMap }} 5 | apiVersion: v1 6 | kind: ConfigMap 7 | metadata: 8 | name: {{ .Values.configMap }} 9 | namespace: {{ .Release.Namespace }} 10 | annotations: 11 | "helm.sh/hook": "test" 12 | "helm.sh/hook": "pre-install" 13 | "helm.sh/hook-weight": "-1" # # create config-map before the test pod 14 | "helm.sh/hook-delete-policy": "before-hook-creation" 15 | data: 16 | {{ .Values.configMapFileName }}: | 17 | spot: 18 | time: "1994-05-15T00:00:00Z" 19 | {{- end }} -------------------------------------------------------------------------------- /helm/amazon-ec2-metadata-mock/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values to be passed into the chart's templates. 2 | 3 | image: 4 | repository: "public.ecr.aws/aws-ec2/amazon-ec2-metadata-mock" 5 | tag: "v1.13.0" 6 | pullPolicy: "IfNotPresent" 7 | 8 | # replicaCount defines the number of pods to replicate 9 | replicaCount: 1 10 | 11 | # nameOverride overrides the name of the helm chart 12 | nameOverride: "" 13 | # fullnameOverride overrides the name of the application 14 | fullnameOverride: "" 15 | 16 | # targetNodeOs creates node-OS specific deployments (e.g. "linux", "windows", "linux windows") 17 | targetNodeOs: "linux" 18 | 19 | resources: 20 | requests: 21 | memory: "64Mi" 22 | cpu: "50m" 23 | limits: 24 | memory: "128Mi" 25 | cpu: "100m" 26 | 27 | # nodeSelector tells both linux and windows deployments where to place the amazon-ec2-metadata-mock pods 28 | # By default, this value is empty and every node will receive a pod. 29 | nodeSelector: {} 30 | # linuxNodeSelector tells the linux deployments where to place the amazon-ec2-metadata-mock pods 31 | # pods. By default, this value is empty and every linux node will receive a pod. 32 | linuxNodeSelector: {} 33 | # windowsNodeSelector tells the windows deployments where to place the amazon-ec2-metadata-mock pods 34 | # pods. By default, this value is empty and every windows node will receive a pod. 35 | windowsNodeSelector: {} 36 | 37 | nodeSelectorTermsOs: "" 38 | nodeSelectorTermsArch: "" 39 | 40 | # podAnnotations define annotations to add to each pod 41 | podAnnotations: {} 42 | linuxAnnotations: {} 43 | windowsAnnotations: {} 44 | 45 | # tolerations specify taints that a pod tolerates so that it can be scheduled to a node with that taint 46 | tolerations: [] 47 | linuxTolerations: [] 48 | windowsTolerations: [] 49 | 50 | # arguments represent CLI args to use when starting amazon-ec2-metadata-mock 51 | arguments: [] 52 | linuxArguments: [] 53 | windowsArguments: [] 54 | 55 | # updateStrategy represents the update strategy for a Deployment 56 | updateStrategy: "RollingUpdate" 57 | linuxUpdateStrategy: "" 58 | windowsUpdateStrategy: "" 59 | 60 | rbac: 61 | # rbac.pspEnabled, if `true` a restricted pod security policy is created and used 62 | pspEnabled: false 63 | 64 | serviceAccount: 65 | # create represents whether a service account should be created 66 | create: true 67 | # name is the name of the service account to use. If name is not set and create is true, 68 | # a name is generated using fullname template 69 | name: "amazon-ec2-metadata-mock-service-account" 70 | annotations: {} 71 | 72 | securityContext: 73 | runAsUserID: "1000" 74 | runAsGroupID: "1000" 75 | 76 | # configMap represents the name of an EXISTING configMap to use 77 | # configMap can be used to pass a config file with the complete set of AEMM configuration overrides, not just limited to AEMM CLI flags. Learn more in README. 78 | configMap: "" 79 | 80 | # configMapFileName represents the name of the file used to create the configMap. Learn more in README. 81 | # supported file extenstions - https://github.com/spf13/viper/blob/master/viper.go#L328 82 | configMapFileName: "aemm-config.json" 83 | 84 | # servicePort represents the port to run the AEMM K8s service on. This can be any port of user's choice. 85 | # note: this port is different from the native AEMM config - aemm.server.port which is not supported when AEMM is run as a K8s service. Learn more in README. 86 | servicePort: "1338" 87 | 88 | serviceName: "amazon-ec2-metadata-mock-service" 89 | 90 | # aemm represents all the CLI flag configuration for Amazon EC2 Metadata Mock (AEMM) 91 | # Null / empty values here means that AEMM will run with defaults configured in the tool 92 | # Refer to the readme for descriptions and defaults - https://github.com/aws/amazon-ec2-metadata-mock/blob/main/helm/amazon-ec2-metadata-mock/README.md 93 | aemm: 94 | server: 95 | hostname: "" 96 | mockDelaySec: 0 97 | mockTriggerTime: "" 98 | mockIPCount: 2 99 | imdsv2: false 100 | rebalanceDelaySec: 0 101 | rebalanceTriggerTime: "" 102 | spot: 103 | action: "" 104 | time: "" 105 | rebalanceRecTime: "" 106 | events: 107 | code: "" 108 | notAfter: "" 109 | notBefore: "" 110 | notBeforeDeadline: "" 111 | state: "" 112 | 113 | # test configuration 114 | test: 115 | image: "centos" 116 | imageTag: "latest" 117 | pullPolicy: "IfNotPresent" 118 | -------------------------------------------------------------------------------- /helm/aws-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws/amazon-ec2-metadata-mock/6162391291863dd1b2bea75ec7440b86a8fc3a77/helm/aws-logo.png -------------------------------------------------------------------------------- /pkg/cmd/asglifecycle/asglifecycle.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 asglifecycle 15 | 16 | import ( 17 | "errors" 18 | "fmt" 19 | "log" 20 | "strings" 21 | 22 | cmdutil "github.com/aws/amazon-ec2-metadata-mock/pkg/cmd/cmdutil" 23 | cfg "github.com/aws/amazon-ec2-metadata-mock/pkg/config" 24 | se "github.com/aws/amazon-ec2-metadata-mock/pkg/mock/asglifecycle" 25 | 26 | "github.com/spf13/cobra" 27 | ) 28 | 29 | const ( 30 | cfgPrefix = "asglifecycle." 31 | ) 32 | 33 | var ( 34 | c cfg.Config 35 | 36 | // Command represents the CLI command 37 | Command *cobra.Command 38 | 39 | // defaults 40 | defaultCfg = map[string]interface{}{} 41 | ) 42 | 43 | func init() { 44 | cobra.OnInitialize(initConfig) 45 | Command = newCmd() 46 | } 47 | 48 | func initConfig() { 49 | cfg.LoadConfigFromDefaults(defaultCfg) 50 | } 51 | 52 | func newCmd() *cobra.Command { 53 | var cmd = &cobra.Command{ 54 | Use: "asglifecycle [--code CODE] [--state STATE] [--not-after] [--not-before-deadline]", 55 | Aliases: []string{"asglifecycle", "autoscaling", "asg"}, 56 | PreRunE: preRun, 57 | Example: fmt.Sprintf(" %s asglifecycle -h \tasglifecycle help \n %s asglifecycle -t target-lifecycle-state\t\tmocks asg lifecycle target lifecycle states", cmdutil.BinName, cmdutil.BinName), 58 | Run: run, 59 | Short: "Mock EC2 ASG Lifecycle target-lifecycle-state", 60 | Long: "Mock EC2 ASG Lifecycle target-lifecycle-state", 61 | } 62 | 63 | // bind local flags to config 64 | cfg.BindFlagSetWithKeyPrefix(cmd.Flags(), cfgPrefix) 65 | return cmd 66 | } 67 | 68 | // SetConfig sets the local config 69 | func SetConfig(config cfg.Config) { 70 | c = config 71 | } 72 | 73 | func preRun(cmd *cobra.Command, args []string) error { 74 | if cfgErrors := ValidateLocalConfig(); cfgErrors != nil { 75 | return errors.New(strings.Join(cfgErrors, "")) 76 | } 77 | return nil 78 | } 79 | 80 | // ValidateLocalConfig validates all local config and returns a slice of error messages 81 | func ValidateLocalConfig() []string { 82 | // no-op 83 | return nil 84 | } 85 | 86 | func run(cmd *cobra.Command, args []string) { 87 | log.Printf("Initiating %s for EC2 ASG Lifecycle on port %s\n", cmdutil.BinName, c.Server.Port) 88 | cmdutil.PrintFlags(cmd.Flags()) 89 | cmdutil.RegisterHandlers(cmd, c) 90 | se.Mock(c) 91 | } 92 | -------------------------------------------------------------------------------- /pkg/cmd/asglifecycle/asglifecycle_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 asglifecycle 15 | 16 | import ( 17 | "bytes" 18 | "fmt" 19 | "testing" 20 | 21 | h "github.com/aws/amazon-ec2-metadata-mock/test" 22 | 23 | "github.com/spf13/pflag" 24 | ) 25 | 26 | func TestNewCmdName(t *testing.T) { 27 | expected := "asglifecycle" 28 | actual := newCmd().Name() 29 | h.Assert(t, expected == actual, fmt.Sprintf("Expected the name for asglifecycle command to be %s, but was %s", expected, actual)) 30 | } 31 | func TestNewCmdLocalFlags(t *testing.T) { 32 | expectedFlags := []string{} 33 | 34 | cmd := newCmd() 35 | actualFlagSet := cmd.LocalFlags() 36 | 37 | var actualFlags []string 38 | actualFlagSet.VisitAll(func(flag *pflag.Flag) { 39 | actualFlags = append(actualFlags, flag.Name) 40 | }) 41 | 42 | h.ItemsMatch(t, expectedFlags, actualFlags) 43 | } 44 | 45 | func TestNewCmdHasPreRunE(t *testing.T) { 46 | pre := newCmd().PreRunE 47 | h.Assert(t, pre != nil, "Expected a non nil PreRunE for the asglifecycle command") 48 | } 49 | 50 | func TestNewCmdHasRun(t *testing.T) { 51 | run := newCmd().Run 52 | h.Assert(t, run != nil, "Expected a non nil Run for the asglifecycle command") 53 | } 54 | func TestNewCmdHasExample(t *testing.T) { 55 | hasExample := newCmd().HasExample() 56 | h.Assert(t, hasExample, "Expected asglifecycle command to have an example, but wasn't found") 57 | } 58 | func TestExecuteHelpExists(t *testing.T) { 59 | cmd := newCmd() 60 | buf := new(bytes.Buffer) 61 | cmd.SetOutput(buf) 62 | cmd.SetArgs([]string{"-h"}) 63 | err := cmd.Execute() 64 | h.Ok(t, err) 65 | 66 | output := buf.String() 67 | h.Assert(t, output != "", "Expected help subcommand for asglifecycle, but wasn't found") 68 | } 69 | -------------------------------------------------------------------------------- /pkg/cmd/cmdutil/cmdutil.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 cmdutil 15 | 16 | import ( 17 | "fmt" 18 | "strings" 19 | "time" 20 | 21 | cfg "github.com/aws/amazon-ec2-metadata-mock/pkg/config" 22 | e "github.com/aws/amazon-ec2-metadata-mock/pkg/error" 23 | "github.com/aws/amazon-ec2-metadata-mock/pkg/mock/asglifecycle" 24 | "github.com/aws/amazon-ec2-metadata-mock/pkg/mock/dynamic" 25 | "github.com/aws/amazon-ec2-metadata-mock/pkg/mock/events" 26 | "github.com/aws/amazon-ec2-metadata-mock/pkg/mock/handlers" 27 | "github.com/aws/amazon-ec2-metadata-mock/pkg/mock/imdsv2" 28 | "github.com/aws/amazon-ec2-metadata-mock/pkg/mock/spot" 29 | "github.com/aws/amazon-ec2-metadata-mock/pkg/mock/static" 30 | "github.com/aws/amazon-ec2-metadata-mock/pkg/mock/userdata" 31 | "github.com/aws/amazon-ec2-metadata-mock/pkg/server" 32 | 33 | "github.com/spf13/cobra" 34 | "github.com/spf13/pflag" 35 | ) 36 | 37 | // BinName is the name of this tool's binary 38 | const BinName = "ec2-metadata-mock" 39 | 40 | // handlerPair holds a tuple of a path and its associated handler 41 | type handlerPair struct { 42 | path string 43 | handler server.HandlerType 44 | } 45 | 46 | // Contains finds a string in the given array 47 | func Contains(slice []string, val string) bool { 48 | for _, item := range slice { 49 | if item == val { 50 | return true 51 | } 52 | } 53 | return false 54 | } 55 | 56 | // PrintFlags prints all flags of a command, if set 57 | func PrintFlags(flags *pflag.FlagSet) { 58 | f := make(map[string]interface{}) 59 | flags.Visit(func(flag *pflag.Flag) { 60 | f[flag.Name] = flag.Value 61 | }) 62 | 63 | if len(f) > 0 { 64 | fmt.Println("\nFlags:") 65 | for key, value := range f { 66 | fmt.Printf("%s: %s\n", key, value) 67 | } 68 | fmt.Println() 69 | } 70 | } 71 | 72 | // ValidateRFC3339TimeFormat validates an input time matches RFC3339 format 73 | func ValidateRFC3339TimeFormat(flagName string, input string) error { 74 | if _, err := time.Parse(time.RFC3339, input); err != nil { 75 | return e.FlagValidationError{ 76 | FlagName: flagName, 77 | Allowed: "time in RFC3339 format, e.g. 2020-01-07T01:03:47Z", 78 | InvalidValue: input} 79 | } 80 | return nil 81 | } 82 | 83 | // RegisterHandlers binds paths to handlers for ALL commands 84 | func RegisterHandlers(cmd *cobra.Command, config cfg.Config) { 85 | handlerPairsToRegister := getHandlerPairs(cmd, config) 86 | for _, handlerPair := range handlerPairsToRegister { 87 | if config.Imdsv2Required { 88 | server.HandleFunc(handlerPair.path, imdsv2.ValidateToken(handlerPair.handler)) 89 | } else { 90 | server.HandleFunc(handlerPair.path, handlerPair.handler) 91 | } 92 | } 93 | 94 | static.RegisterHandlers(config) 95 | dynamic.RegisterHandlers(config) 96 | userdata.RegisterHandlers(config) 97 | 98 | // paths without explicit handler bindings will fallback to CatchAllHandler 99 | server.HandleFuncPrefix("/", handlers.CatchAllHandler) 100 | } 101 | 102 | // getHandlerPairs returns a slice of {paths, handlers} to register 103 | func getHandlerPairs(cmd *cobra.Command, config cfg.Config) []handlerPair { 104 | // always register these paths 105 | handlerPairs := []handlerPair{ 106 | {path: "/", handler: handlers.ListRoutesHandler}, 107 | {path: "/latest", handler: handlers.ListRoutesHandler}, 108 | {path: static.ServicePath, handler: handlers.ListRoutesHandler}, 109 | {path: dynamic.ServicePath, handler: handlers.ListRoutesHandler}, 110 | } 111 | 112 | isSpot := strings.Contains(cmd.Name(), "spot") 113 | isEvents := strings.Contains(cmd.Name(), "events") 114 | isASGLifecycle := strings.Contains(cmd.Name(), "asglifecycle") 115 | 116 | subCommandHandlers := map[string][]handlerPair{ 117 | "spot": {{path: config.Metadata.Paths.Spot, handler: spot.Handler}, 118 | {path: config.Metadata.Paths.SpotTerminationTime, handler: spot.Handler}, 119 | {path: config.Metadata.Paths.RebalanceRecTime, handler: spot.Handler}}, 120 | "events": {{path: config.Metadata.Paths.Events, handler: events.Handler}}, 121 | "asglifecycle": {{path: config.Metadata.Paths.ASGLifecycle, handler: asglifecycle.Handler}}, 122 | } 123 | 124 | if isSpot { 125 | handlerPairs = append(handlerPairs, subCommandHandlers["spot"]...) 126 | } else if isEvents { 127 | handlerPairs = append(handlerPairs, subCommandHandlers["events"]...) 128 | } else if isASGLifecycle { 129 | handlerPairs = append(handlerPairs, subCommandHandlers["asglifecycle"]...) 130 | } else { 131 | // root registers all subcommands 132 | for k := range subCommandHandlers { 133 | handlerPairs = append(handlerPairs, subCommandHandlers[k]...) 134 | } 135 | } 136 | 137 | return handlerPairs 138 | } 139 | -------------------------------------------------------------------------------- /pkg/cmd/events/events_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 events 15 | 16 | import ( 17 | "bytes" 18 | "fmt" 19 | "testing" 20 | 21 | h "github.com/aws/amazon-ec2-metadata-mock/test" 22 | 23 | "github.com/spf13/pflag" 24 | ) 25 | 26 | func TestNewCmdName(t *testing.T) { 27 | expected := "events" 28 | actual := newCmd().Name() 29 | h.Assert(t, expected == actual, fmt.Sprintf("Expected the name for events command to be %s, but was %s", expected, actual)) 30 | } 31 | func TestNewCmdLocalFlags(t *testing.T) { 32 | expectedFlags := []string{"code", "state", "not-before", "not-after", "not-before-deadline"} 33 | 34 | cmd := newCmd() 35 | actualFlagSet := cmd.LocalFlags() 36 | 37 | var actualFlags []string 38 | actualFlagSet.VisitAll(func(flag *pflag.Flag) { 39 | actualFlags = append(actualFlags, flag.Name) 40 | }) 41 | 42 | h.ItemsMatch(t, expectedFlags, actualFlags) 43 | } 44 | 45 | func TestNewCmdHasPreRunE(t *testing.T) { 46 | pre := newCmd().PreRunE 47 | h.Assert(t, pre != nil, "Expected a non nil PreRunE for the events command") 48 | } 49 | 50 | func TestNewCmdHasRun(t *testing.T) { 51 | run := newCmd().Run 52 | h.Assert(t, run != nil, "Expected a non nil Run for the events command") 53 | } 54 | func TestNewCmdHasExample(t *testing.T) { 55 | hasExample := newCmd().HasExample() 56 | h.Assert(t, hasExample, "Expected events command to have an example, but wasn't found") 57 | } 58 | func TestExecuteHelpExists(t *testing.T) { 59 | cmd := newCmd() 60 | buf := new(bytes.Buffer) 61 | cmd.SetOutput(buf) 62 | cmd.SetArgs([]string{"-h"}) 63 | err := cmd.Execute() 64 | h.Ok(t, err) 65 | 66 | output := buf.String() 67 | h.Assert(t, output != "", "Expected help subcommand for events, but wasn't found") 68 | } 69 | -------------------------------------------------------------------------------- /pkg/cmd/root/globalflags/globalflags.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 globalflags represents the global flags (to be used with all sub-commands) 15 | package globalflags 16 | 17 | const ( 18 | // ConfigFileFlag - config file for cli input parameters in json format 19 | ConfigFileFlag = "config-file" 20 | 21 | // SaveConfigToFileFlag - whether to save processed config from all input sources 22 | SaveConfigToFileFlag = "save-config-to-file" 23 | 24 | // WatchConfigFileFlag - whether to watch the config file for changes 25 | WatchConfigFileFlag = "watch-config-file" 26 | 27 | // MockDelayInSecFlag - spot itn delay in seconds, relative to the application start time 28 | MockDelayInSecFlag = "mock-delay-sec" 29 | 30 | // MockTriggerTimeFlag - spot itn trigger time in RFC3339 31 | MockTriggerTimeFlag = "mock-trigger-time" 32 | 33 | // MockIPCountFlag - the number of nodes in a cluster that can receive Spot ITNs 34 | MockIPCountFlag = "mock-ip-count" 35 | 36 | // HostNameFlag - the HTTP hostname for the mock url 37 | HostNameFlag = "hostname" 38 | 39 | // PortFlag - the HTTP port where the mock runs 40 | PortFlag = "port" 41 | 42 | // Imdsv2Flag - whether to enable IMDSv2 only requiring a session token when submitting requests 43 | Imdsv2Flag = "imdsv2" 44 | 45 | // RebalanceDelayInSecFlag - rebalance rec delay in seconds, relative to the application start time 46 | RebalanceDelayInSecFlag = "rebalance-delay-sec" 47 | 48 | // RebalanceTriggerTimeFlag - rebalance rec trigger time in RFC3339 49 | RebalanceTriggerTimeFlag = "rebalance-trigger-time" 50 | 51 | ASGTerminationDelayInSecFlag = "asg-termination-delay-sec" 52 | 53 | ASGTerminationTriggerTimeFlag = "asg-termination-trigger-time" 54 | ) 55 | 56 | // GetTopLevelFlags returns the top level global flags 57 | func GetTopLevelFlags() []string { 58 | return []string{ConfigFileFlag, SaveConfigToFileFlag, WatchConfigFileFlag, MockDelayInSecFlag, MockTriggerTimeFlag, MockIPCountFlag, Imdsv2Flag, RebalanceDelayInSecFlag, RebalanceTriggerTimeFlag, ASGTerminationDelayInSecFlag, ASGTerminationTriggerTimeFlag} 59 | } 60 | -------------------------------------------------------------------------------- /pkg/cmd/root/root_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 root 15 | 16 | import ( 17 | "bytes" 18 | "fmt" 19 | "strings" 20 | "testing" 21 | 22 | h "github.com/aws/amazon-ec2-metadata-mock/test" 23 | 24 | "github.com/spf13/pflag" 25 | ) 26 | 27 | func TestNewCmdName(t *testing.T) { 28 | expected := "ec2-metadata-mock" 29 | actual := NewCmd().Name() 30 | 31 | h.Assert(t, expected == actual, fmt.Sprintf("Expected the name for root command to be %s, but was %s", expected, actual)) 32 | } 33 | func TestNewCmdFlags(t *testing.T) { 34 | expectedFlags := []string{"config-file", "save-config-to-file", "watch-config-file", "mock-delay-sec", "mock-trigger-time", "mock-ip-count", "hostname", "port", "imdsv2", "rebalance-delay-sec", "rebalance-trigger-time", "asg-termination-delay-sec", "asg-termination-trigger-time"} 35 | 36 | cmd := NewCmd() 37 | actualFlagSet := cmd.PersistentFlags() 38 | var actualFlags []string 39 | actualFlagSet.VisitAll(func(flag *pflag.Flag) { 40 | actualFlags = append(actualFlags, flag.Name) 41 | }) 42 | 43 | h.ItemsMatch(t, expectedFlags, actualFlags) 44 | } 45 | func TestNewCmdHasSubcommands(t *testing.T) { 46 | expSubcommandNames := []string{"spot", "events", "asglifecycle"} 47 | 48 | cmd := NewCmd() 49 | actSubcommands := cmd.Commands() 50 | var actSubcommandsNames []string 51 | for _, cmd := range actSubcommands { 52 | n := strings.Split(cmd.Use, " ")[0] 53 | actSubcommandsNames = append(actSubcommandsNames, n) 54 | } 55 | 56 | h.ItemsMatch(t, expSubcommandNames, actSubcommandsNames) 57 | } 58 | func TestNewCmdHasPersistentRunE(t *testing.T) { 59 | ppe := NewCmd().PersistentPreRunE 60 | h.Assert(t, ppe != nil, "Expected a non nil PersistentPreRunE for the root command") 61 | } 62 | func TestNewCmdHasPreRunE(t *testing.T) { 63 | pe := NewCmd().PreRunE 64 | h.Assert(t, pe != nil, "Expected a non nil PreRunE for the root command") 65 | } 66 | func TestNewCmdHasRun(t *testing.T) { 67 | run := NewCmd().Run 68 | h.Assert(t, run != nil, "Expected a non nil Run for the root command") 69 | } 70 | func TestNewCmdHasExample(t *testing.T) { 71 | hasExample := NewCmd().HasExample() 72 | h.Assert(t, hasExample, "Expected root command to have an example, but wasn't found") 73 | } 74 | func TestExecuteHelpExists(t *testing.T) { 75 | cmd := NewCmd() 76 | buf := new(bytes.Buffer) 77 | cmd.SetOutput(buf) 78 | cmd.SetArgs([]string{"-h"}) 79 | err := cmd.Execute() 80 | h.Ok(t, err) 81 | 82 | output := buf.String() 83 | h.Assert(t, output != "", "Expected help subcommand for root, but wasn't found") 84 | } 85 | -------------------------------------------------------------------------------- /pkg/cmd/root/version.txt: -------------------------------------------------------------------------------- 1 | v1.13.0 -------------------------------------------------------------------------------- /pkg/cmd/spot/spot.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 spot 15 | 16 | import ( 17 | "errors" 18 | "fmt" 19 | "log" 20 | "strings" 21 | 22 | cmdutil "github.com/aws/amazon-ec2-metadata-mock/pkg/cmd/cmdutil" 23 | cfg "github.com/aws/amazon-ec2-metadata-mock/pkg/config" 24 | e "github.com/aws/amazon-ec2-metadata-mock/pkg/error" 25 | s "github.com/aws/amazon-ec2-metadata-mock/pkg/mock/spot" 26 | 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | const ( 31 | cfgPrefix = "spot." 32 | 33 | // local flags 34 | instanceActionFlagName = "action" 35 | terminationTimeFlagName = "time" 36 | rebalanceRecTimeFlagName = "rebalance-rec-time" 37 | 38 | // instance actions 39 | terminate = "terminate" 40 | hibernate = "hibernate" 41 | stop = "stop" 42 | ) 43 | 44 | var ( 45 | c cfg.Config 46 | 47 | // Command represents the CLI command 48 | Command *cobra.Command 49 | 50 | // constraints 51 | validInstanceActions = []string{terminate, hibernate, stop} 52 | 53 | // defaults 54 | defaultCfg = map[string]interface{}{ 55 | cfgPrefix + instanceActionFlagName: terminate, 56 | } 57 | ) 58 | 59 | func init() { 60 | cobra.OnInitialize(initConfig) 61 | Command = newCmd() 62 | } 63 | 64 | func initConfig() { 65 | cfg.LoadConfigFromDefaults(defaultCfg) 66 | } 67 | 68 | func newCmd() *cobra.Command { 69 | var cmd = &cobra.Command{ 70 | Use: "spot [--action ACTION]", 71 | Aliases: []string{"spot"}, 72 | PreRunE: preRun, 73 | Example: fmt.Sprintf(" %s spot -h \tspot help \n %s spot -d 5 --action terminate\t\tmocks spot interruption only", cmdutil.BinName, cmdutil.BinName), 74 | Run: run, 75 | Short: "Mock EC2 Spot interruption notice", 76 | Long: "Mock EC2 Spot interruption notice", 77 | } 78 | 79 | // local flags 80 | cmd.Flags().StringP(instanceActionFlagName, "a", "", "action in the spot interruption notice (default: terminate)\naction can be one of the following: "+strings.Join(validInstanceActions, ",")) 81 | cmd.Flags().StringP(terminationTimeFlagName, "t", "", "termination time specifies the approximate time when the spot instance will receive the shutdown signal in RFC3339 format to execute instance action E.g. 2020-01-07T01:03:47Z (default: request time + 2 minutes in UTC)") 82 | cmd.Flags().StringP(rebalanceRecTimeFlagName, "r", "", "rebalance rec time specifies the approximate time when the rebalance recommendation notification will be emitted in RFC3339 format") 83 | 84 | // bind local flags to config 85 | cfg.BindFlagSetWithKeyPrefix(cmd.Flags(), cfgPrefix) 86 | return cmd 87 | } 88 | 89 | // SetConfig sets the local config 90 | func SetConfig(config cfg.Config) { 91 | c = config 92 | } 93 | 94 | func preRun(cmd *cobra.Command, args []string) error { 95 | if cfgErrors := ValidateLocalConfig(); cfgErrors != nil { 96 | return errors.New(strings.Join(cfgErrors, "")) 97 | } 98 | return nil 99 | } 100 | 101 | // ValidateLocalConfig validates all local config and returns a slice of error messages 102 | func ValidateLocalConfig() []string { 103 | var errStrings []string 104 | c := c.SpotConfig 105 | 106 | // validate instance-action 107 | if ok := cmdutil.Contains(validInstanceActions, c.InstanceAction); !ok { 108 | errStrings = append(errStrings, e.FlagValidationError{ 109 | FlagName: instanceActionFlagName, 110 | Allowed: strings.Join(validInstanceActions, ","), 111 | InvalidValue: c.InstanceAction}.Error(), 112 | ) 113 | } 114 | // validate time, if override provided 115 | if c.TerminationTime != "" { 116 | if err := cmdutil.ValidateRFC3339TimeFormat(terminationTimeFlagName, c.TerminationTime); err != nil { 117 | errStrings = append(errStrings, err.Error()) 118 | } 119 | } 120 | // validate noticeTime, if override provided 121 | if c.RebalanceRecTime != "" { 122 | if err := cmdutil.ValidateRFC3339TimeFormat(rebalanceRecTimeFlagName, c.RebalanceRecTime); err != nil { 123 | errStrings = append(errStrings, err.Error()) 124 | } 125 | } 126 | return errStrings 127 | } 128 | 129 | func run(cmd *cobra.Command, args []string) { 130 | log.Printf("Initiating %s for EC2 Spot interruption notice on port %s\n", cmdutil.BinName, c.Server.Port) 131 | cmdutil.PrintFlags(cmd.Flags()) 132 | cmdutil.RegisterHandlers(cmd, c) 133 | s.Mock(c) 134 | } 135 | -------------------------------------------------------------------------------- /pkg/cmd/spot/spot_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 spot 15 | 16 | import ( 17 | "bytes" 18 | "fmt" 19 | "testing" 20 | 21 | h "github.com/aws/amazon-ec2-metadata-mock/test" 22 | 23 | "github.com/spf13/pflag" 24 | ) 25 | 26 | func TestNewCmdName(t *testing.T) { 27 | expected := "spot" 28 | actual := newCmd().Name() 29 | h.Assert(t, expected == actual, fmt.Sprintf("Expected the name for spot command to be %s, but was %s", expected, actual)) 30 | } 31 | func TestNewCmdLocalFlags(t *testing.T) { 32 | expectedFlags := []string{"action", "time", "rebalance-rec-time"} 33 | 34 | cmd := newCmd() 35 | actualFlagSet := cmd.LocalFlags() 36 | 37 | var actualFlags []string 38 | actualFlagSet.VisitAll(func(flag *pflag.Flag) { 39 | actualFlags = append(actualFlags, flag.Name) 40 | }) 41 | 42 | h.ItemsMatch(t, expectedFlags, actualFlags) 43 | } 44 | func TestNewCmdHasPreRunE(t *testing.T) { 45 | pre := newCmd().PreRunE 46 | h.Assert(t, pre != nil, "Expected a non nil PreRunE for the spot command") 47 | } 48 | func TestNewCmdHasRun(t *testing.T) { 49 | run := newCmd().Run 50 | h.Assert(t, run != nil, "Expected a non nil Run for the spot command") 51 | } 52 | func TestNewCmdHasExample(t *testing.T) { 53 | hasExample := newCmd().HasExample() 54 | h.Assert(t, hasExample, "Expected spot command to have an example, but wasn't found") 55 | } 56 | func TestExecuteHelpExists(t *testing.T) { 57 | cmd := newCmd() 58 | buf := new(bytes.Buffer) 59 | cmd.SetOutput(buf) 60 | cmd.SetArgs([]string{"-h"}) 61 | err := cmd.Execute() 62 | h.Ok(t, err) 63 | 64 | output := buf.String() 65 | h.Assert(t, output != "", "Expected help subcommand for spot, but wasn't found") 66 | } 67 | -------------------------------------------------------------------------------- /pkg/config/defaults/defaults.go: -------------------------------------------------------------------------------- 1 | // Copyright 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 defaults 15 | 16 | import ( 17 | // Blank import else compiler complains it's unused 18 | _ "embed" 19 | ) 20 | 21 | //go:embed aemm-metadata-default-values.json 22 | var defaultValues []byte 23 | 24 | // GetDefaultValues returns default metadata values populated via aemm-metadata-default-values.json 25 | func GetDefaultValues() []byte { 26 | return defaultValues 27 | } 28 | -------------------------------------------------------------------------------- /pkg/config/dynamic.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 15 | 16 | import ( 17 | "encoding/json" 18 | 19 | "github.com/spf13/pflag" 20 | ) 21 | 22 | const ( 23 | // dynamic keys 24 | instanceIdentityDocument = "instance-identity-document" 25 | ) 26 | 27 | var ( 28 | dyCfgPrefix = "dynamic." 29 | dyPathsCfgPrefix = dyCfgPrefix + "paths." 30 | dyValuesCfgPrefix = dyCfgPrefix + "values." 31 | 32 | // mapping of dyValue KEYS to dyPath KEYS requiring path substitutions on override 33 | dyValueToPlaceholderPathsKeyMap = map[string][]string{} 34 | 35 | // supported URL paths to run a mock 36 | dyPathsDefaults = map[string]interface{}{} 37 | 38 | // values in mock responses 39 | dyValuesDefaults = map[string]interface{}{} 40 | 41 | // mapping of dynamic value keys to its nested struct 42 | dyNestedValues = map[string]interface{}{} 43 | ) 44 | 45 | // GetCfgDnValPrefix returns the prefix to use to access dynamic values in config 46 | func GetCfgDnValPrefix() string { 47 | return dyCfgPrefix + "." + dyValuesCfgPrefix + "." 48 | } 49 | 50 | // GetCfgDnPathsPrefix returns the prefix to use to access dynamic values in config 51 | func GetCfgDnPathsPrefix() string { 52 | return dyCfgPrefix + "." + dyPathsCfgPrefix + "." 53 | } 54 | 55 | // BindDynamicCfg binds a flag that represents a dynamic value to configuration 56 | func BindDynamicCfg(flag *pflag.Flag) { 57 | bindFlagWithKeyPrefix(flag, dyValuesCfgPrefix) 58 | } 59 | 60 | // SetDynamicDefaults sets config defaults for dynamic paths and values 61 | func SetDynamicDefaults(jsonWithDefaults []byte) { 62 | // Unmarshal to map to preserve keys for Paths and Values 63 | var defaultsMap map[string]interface{} 64 | json.Unmarshal(jsonWithDefaults, &defaultsMap) 65 | 66 | dyPaths := defaultsMap["dynamic"].(map[string]interface{})["paths"].(map[string]interface{}) 67 | dyValues := defaultsMap["dynamic"].(map[string]interface{})["values"].(map[string]interface{}) 68 | 69 | for k, v := range dyPaths { 70 | newKey := dyPathsCfgPrefix + k 71 | // ex: "dynamic.paths.instance-identity-document": "/latest/dynamic/instance-identity/document" 72 | dyPathsDefaults[newKey] = v 73 | } 74 | 75 | for k, v := range dyValues { 76 | newKey := dyValuesCfgPrefix + k 77 | // ex: "dynamic.values.instance-identity-document": {"accountId" ...} 78 | dyValuesDefaults[newKey] = v 79 | 80 | // if dyvalue is a nested struct, then re-unmarshal json data to correct type 81 | if nestedStruct, ok := dyNestedValues[newKey]; ok { 82 | updatedVal, err := unmarshalToNestedStruct(v, nestedStruct) 83 | if err == nil { 84 | dyValuesDefaults[newKey] = updatedVal 85 | } 86 | } 87 | } 88 | 89 | LoadConfigFromDefaults(dyPathsDefaults) 90 | LoadConfigFromDefaults(dyValuesDefaults) 91 | } 92 | 93 | // GetDynamicDefaults returns config defaults for dynamic paths and values 94 | func GetDynamicDefaults() (map[string]interface{}, map[string]interface{}) { 95 | return dyPathsDefaults, dyValuesDefaults 96 | } 97 | 98 | // GetDynamicValueToPlaceholderPathsKeyMap returns collection of dynamic values that are substituted into paths 99 | func GetDynamicValueToPlaceholderPathsKeyMap() map[string][]string { 100 | return dyValueToPlaceholderPathsKeyMap 101 | } 102 | -------------------------------------------------------------------------------- /pkg/config/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 15 | 16 | import ( 17 | "github.com/spf13/pflag" 18 | ) 19 | 20 | var ( 21 | serverCfgPrefix = "server." 22 | serverCfgDefaults = map[string]interface{}{ 23 | serverCfgPrefix + "hostname": "0.0.0.0", 24 | serverCfgPrefix + "port": "1338", 25 | } 26 | ) 27 | 28 | // BindServerCfg binds a flag that represents a server config to configuration 29 | func BindServerCfg(flag *pflag.Flag) { 30 | bindFlagWithKeyPrefix(flag, serverCfgPrefix) 31 | } 32 | 33 | // SetServerCfgDefaults sets config defaults for server config 34 | func SetServerCfgDefaults() { 35 | LoadConfigFromDefaults(serverCfgDefaults) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/config/userdata.go: -------------------------------------------------------------------------------- 1 | // Copyright 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 15 | 16 | import ( 17 | "encoding/json" 18 | 19 | "github.com/spf13/pflag" 20 | ) 21 | 22 | var ( 23 | udCfgPrefix = "userdata." 24 | udPathsCfgPrefix = udCfgPrefix + "paths." 25 | udValuesCfgPrefix = udCfgPrefix + "values." 26 | 27 | // mapping of udValue KEYS to udPath KEYS requiring path substitutions on override 28 | udValueToPlaceholderPathsKeyMap = map[string][]string{} 29 | 30 | // supported URL paths to run a mock 31 | udPathsDefaults = map[string]interface{}{} 32 | 33 | // values in mock responses 34 | udValuesDefaults = map[string]interface{}{} 35 | ) 36 | 37 | // GetCfgUdValPrefix returns the prefix to use to access userdata values in config 38 | func GetCfgUdValPrefix() string { 39 | return udCfgPrefix + "." + udValuesCfgPrefix + "." 40 | } 41 | 42 | // GetCfgUdPathsPrefix returns the prefix to use to access userdata values in config 43 | func GetCfgUdPathsPrefix() string { 44 | return udCfgPrefix + "." + udPathsCfgPrefix + "." 45 | } 46 | 47 | // BindUserdataCfg binds a flag that represents a userdata value to configuration 48 | func BindUserdataCfg(flag *pflag.Flag) { 49 | bindFlagWithKeyPrefix(flag, udValuesCfgPrefix) 50 | } 51 | 52 | // SetUserdataDefaults sets config defaults for userdata paths and values 53 | func SetUserdataDefaults(jsonWithDefaults []byte) { 54 | // Unmarshal to map to preserve keys for Paths and Values 55 | var defaultsMap map[string]interface{} 56 | json.Unmarshal(jsonWithDefaults, &defaultsMap) 57 | udPaths := defaultsMap["userdata"].(map[string]interface{})["paths"].(map[string]interface{}) 58 | 59 | udValues := defaultsMap["userdata"].(map[string]interface{})["values"].(map[string]interface{}) 60 | 61 | for k, v := range udPaths { 62 | newKey := udPathsCfgPrefix + k 63 | // ex: "userdata": "/latest/user-data" 64 | udPathsDefaults[newKey] = v 65 | } 66 | 67 | for k, v := range udValues { 68 | newKey := udValuesCfgPrefix + k 69 | // ex: "userdata": "1234,john,reboot,true|4512,richard,|173,,," 70 | udValuesDefaults[newKey] = v 71 | } 72 | 73 | LoadConfigFromDefaults(udPathsDefaults) 74 | LoadConfigFromDefaults(udValuesDefaults) 75 | } 76 | 77 | // GetUserdataDefaults returns config defaults for userdata paths and values 78 | func GetUserdataDefaults() (map[string]interface{}, map[string]interface{}) { 79 | return udPathsDefaults, udValuesDefaults 80 | } 81 | 82 | // GetUserdataValueToPlaceholderPathsKeyMap returns collection of userdata values that are substituted into paths 83 | func GetUserdataValueToPlaceholderPathsKeyMap() map[string][]string { 84 | return udValueToPlaceholderPathsKeyMap 85 | } 86 | -------------------------------------------------------------------------------- /pkg/error/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 errors 15 | 16 | import ( 17 | "fmt" 18 | ) 19 | 20 | // FlagValidationError denotes an error encountered while validating CLI flags 21 | type FlagValidationError struct { 22 | FlagName string 23 | Allowed string 24 | InvalidValue string 25 | } 26 | 27 | // Error returns the formatted flag validation error. 28 | func (ve FlagValidationError) Error() string { 29 | return fmt.Sprintf("Invalid CLI input \"%s\" for flag %s. Allowed value(s): %s.\n", ve.InvalidValue, ve.FlagName, ve.Allowed) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/mock/asglifecycle/asglifecycle.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 asglifecycle 15 | 16 | import ( 17 | "log" 18 | "net/http" 19 | "strings" 20 | "time" 21 | 22 | cfg "github.com/aws/amazon-ec2-metadata-mock/pkg/config" 23 | "github.com/aws/amazon-ec2-metadata-mock/pkg/server" 24 | ) 25 | 26 | const ( 27 | targetLifeCycleStatePath = "/latest/meta-data/autoscaling/target-lifecycle-state" 28 | ) 29 | 30 | var ( 31 | eligibleIPs = make(map[string]bool) 32 | c cfg.Config 33 | asgStartTime int64 = time.Now().Unix() 34 | state = "InService" 35 | ) 36 | 37 | func Mock(config cfg.Config) { 38 | SetConfig(config) 39 | server.ListenAndServe(config.Server.HostName, config.Server.Port) 40 | } 41 | 42 | // SetConfig sets the local config 43 | func SetConfig(config cfg.Config) { 44 | c = config 45 | } 46 | 47 | // Handler processes http requests 48 | func Handler(res http.ResponseWriter, req *http.Request) { 49 | if c.MockIPCount >= 0 { 50 | // req.RemoteAddr is formatted as IP:port 51 | requestIP := strings.Split(req.RemoteAddr, ":")[0] 52 | if !eligibleIPs[requestIP] { 53 | if len(eligibleIPs) < c.MockIPCount { 54 | eligibleIPs[requestIP] = true 55 | } else { 56 | log.Printf("Requesting IP %s is not eligible for ASG Lifecycle State because the max number of IPs configured (%d) has been reached.\n", requestIP, c.MockIPCount) 57 | server.ReturnNotFoundResponse(res) 58 | return 59 | } 60 | } 61 | } 62 | 63 | switch req.URL.Path { 64 | case targetLifeCycleStatePath: 65 | handleASGTargetLifecycleState(res, req) 66 | } 67 | } 68 | 69 | func handleASGTargetLifecycleState(res http.ResponseWriter, req *http.Request) { 70 | requestTime := time.Now().Unix() 71 | if c.ASGTerminationTriggerTime != "" { 72 | triggerTime, _ := time.Parse(time.RFC3339, c.ASGTerminationTriggerTime) 73 | delayRemaining := triggerTime.Unix() - requestTime 74 | if delayRemaining <= 0 { 75 | state = "Terminated" 76 | } 77 | } else { 78 | delayInSeconds := c.ASGTerminationDelayInSec 79 | delayRemaining := delayInSeconds - (requestTime - asgStartTime) 80 | if delayRemaining <= 0 { 81 | state = "Terminated" 82 | } 83 | } 84 | 85 | switch req.Method { 86 | case "GET": 87 | server.FormatAndReturnTextResponse(res, state) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /pkg/mock/dynamic/dynamic.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 dynamic 15 | 16 | import ( 17 | "log" 18 | "net/http" 19 | "reflect" 20 | 21 | cfg "github.com/aws/amazon-ec2-metadata-mock/pkg/config" 22 | "github.com/aws/amazon-ec2-metadata-mock/pkg/mock/imdsv2" 23 | "github.com/aws/amazon-ec2-metadata-mock/pkg/server" 24 | ) 25 | 26 | var ( 27 | supportedPaths = make(map[string]interface{}) 28 | response interface{} 29 | jsonTextResponse = map[string]bool{} 30 | 31 | // ServicePath defines the dynamic service path 32 | ServicePath = "/latest/dynamic" 33 | ) 34 | 35 | // Handler processes http requests 36 | func Handler(res http.ResponseWriter, req *http.Request) { 37 | log.Println("Received request to mock dynamic metadata:", req.URL.Path) 38 | 39 | if val, ok := supportedPaths[req.URL.Path]; ok { 40 | response = val 41 | } else { 42 | response = "Something went wrong with: " + req.URL.Path 43 | } 44 | 45 | switch response.(type) { 46 | // static metadata values are either string or JSON EXCEPT FOR elastic-inference associations 47 | case string: 48 | server.FormatAndReturnTextResponse(res, response.(string)) 49 | default: 50 | if jsonTextResponse[req.URL.Path] { 51 | server.FormatAndReturnJSONTextResponse(res, response) 52 | } else { 53 | server.FormatAndReturnJSONResponse(res, response) 54 | } 55 | } 56 | } 57 | 58 | // RegisterHandlers registers handlers for dynamic paths 59 | func RegisterHandlers(config cfg.Config) { 60 | pathValues := reflect.ValueOf(config.Dynamic.Paths) 61 | dyValues := reflect.ValueOf(config.Dynamic.Values) 62 | 63 | // Iterate over fields in config.Dynamic.Paths to 64 | // determine intersections with config.Dynamic.Values. 65 | // Intersections represent which paths and values to bind. 66 | for i := 0; i < pathValues.NumField(); i++ { 67 | pathFieldName := pathValues.Type().Field(i).Name 68 | dyValueFieldName := dyValues.FieldByName(pathFieldName) 69 | if dyValueFieldName.IsValid() { 70 | path := pathValues.Field(i).Interface().(string) 71 | value := dyValueFieldName.Interface() 72 | if path != "" && value != nil { 73 | // Ex: "/latest/dynamic/instance-identity/document" 74 | supportedPaths[path] = value 75 | if config.Imdsv2Required { 76 | server.HandleFunc(path, imdsv2.ValidateToken(Handler)) 77 | } else { 78 | server.HandleFunc(path, Handler) 79 | } 80 | } else { 81 | log.Printf("There was an issue registering path %v with dyValue: %v", path, value) 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pkg/mock/dynamic/types/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 types 15 | 16 | // InstanceIdentityDocument structure for mock json response parsing 17 | type InstanceIdentityDocument struct { 18 | AccountId string `json:"accountId"` 19 | ImageId string `json:"imageId"` 20 | AvailabilityZone string `json:"availabilityZone"` 21 | RamdiskId *string `json:"ramdiskId"` 22 | KernelId *string `json:"kernelId"` 23 | DevpayProductCodes *string `json:"devpayProductCodes"` 24 | MarketplaceProductCodes []string `json:"marketplaceProductCodes"` 25 | Version string `json:"version"` 26 | PrivateIp string `json:"privateIp"` 27 | BillingProducts *string `json:"billingProducts"` 28 | InstanceId string `json:"instanceId"` 29 | PendingTime string `json:"pendingTime"` 30 | Architecture string `json:"architecture"` 31 | InstanceType string `json:"instanceType"` 32 | Region string `json:"region"` 33 | } 34 | -------------------------------------------------------------------------------- /pkg/mock/events/config/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 15 | 16 | // Config represents the configuration for the mock 17 | type Config struct { 18 | EventCode string `mapstructure:"code"` 19 | EventState string `mapstructure:"state"` 20 | NotBefore string `mapstructure:"not-before"` // The earliest start time for the scheduled event 21 | NotAfter string `mapstructure:"not-after"` // The latest end time for the scheduled event 22 | NotBeforeDeadline string `mapstructure:"not-before-deadline"` // The deadline for starting the event 23 | } 24 | -------------------------------------------------------------------------------- /pkg/mock/events/events.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 events 15 | 16 | import ( 17 | "log" 18 | "net/http" 19 | "strings" 20 | "time" 21 | 22 | cfg "github.com/aws/amazon-ec2-metadata-mock/pkg/config" 23 | t "github.com/aws/amazon-ec2-metadata-mock/pkg/mock/events/internal/types" 24 | "github.com/aws/amazon-ec2-metadata-mock/pkg/server" 25 | ) 26 | 27 | const ( 28 | descriptionPrefix = "The instance is scheduled for " 29 | timeLayout = "2 Jan 2006 15:04:05 GMT" 30 | ) 31 | 32 | var ( 33 | eligibleIPs = make(map[string]bool) 34 | appStartTime int64 = time.Now().Unix() 35 | c cfg.Config 36 | ) 37 | 38 | // Mock starts scheduled events mock 39 | func Mock(config cfg.Config) { 40 | SetConfig(config) 41 | server.ListenAndServe(config.Server.HostName, config.Server.Port) 42 | } 43 | 44 | // SetConfig sets the local config 45 | func SetConfig(config cfg.Config) { 46 | c = config 47 | } 48 | 49 | // Handler processes http requests 50 | func Handler(res http.ResponseWriter, req *http.Request) { 51 | log.Printf("RemoteAddr: %s sent request to mock scheduled event: %s\n", req.URL.Path, req.RemoteAddr) 52 | 53 | // specify negative value to disable this feature 54 | if c.MockIPCount >= 0 { 55 | // req.RemoteAddr is formatted as IP:port 56 | requestIP := strings.Split(req.RemoteAddr, ":")[0] 57 | if !eligibleIPs[requestIP] { 58 | if len(eligibleIPs) < c.MockIPCount { 59 | eligibleIPs[requestIP] = true 60 | } else { 61 | log.Printf("Requesting IP %s is not eligible for Scheduled Event because the max number of IPs configured (%d) has been reached.\n", requestIP, c.MockIPCount) 62 | server.ReturnNotFoundResponse(res) 63 | return 64 | } 65 | } 66 | } 67 | 68 | requestTime := time.Now().Unix() 69 | 70 | if c.MockTriggerTime != "" { 71 | triggerTime, _ := time.Parse(time.RFC3339, c.MockTriggerTime) 72 | 73 | delayRemaining := triggerTime.Unix() - requestTime 74 | if delayRemaining > 0 { 75 | log.Printf("MockTriggerTime %s was not reached yet. The mock response will be available in %ds. Returning `notFoundResponse` for now", triggerTime, delayRemaining) 76 | server.ReturnNotFoundResponse(res) 77 | return 78 | } 79 | } else { 80 | delayInSeconds := c.MockDelayInSec 81 | delayRemaining := delayInSeconds - (requestTime - appStartTime) 82 | if delayRemaining > 0 { 83 | log.Printf("Delaying the response by %ds as requested. The mock response will be available in %ds. Returning `notFoundResponse` for now", delayInSeconds, delayRemaining) 84 | server.ReturnNotFoundResponse(res) 85 | return 86 | } 87 | } 88 | 89 | // return mock response after the delay or trigger time has elapsed 90 | server.FormatAndReturnJSONResponse(res, getMetadata()) 91 | } 92 | 93 | func getMetadata() []t.Event { 94 | md := c.Metadata.Values 95 | se := c.EventsConfig 96 | 97 | b, _ := time.Parse(time.RFC3339, se.NotBefore) 98 | a, _ := time.Parse(time.RFC3339, se.NotAfter) 99 | bd, _ := time.Parse(time.RFC3339, se.NotBeforeDeadline) 100 | eventResp := t.Event{ 101 | Code: se.EventCode, 102 | Description: descriptionPrefix + se.EventCode, 103 | EventID: md.EventID, 104 | State: se.EventState, 105 | NotBefore: b.Format(timeLayout), 106 | NotAfter: a.Format(timeLayout), 107 | NotBeforeDeadline: bd.Format(timeLayout), 108 | } 109 | // supports 1 scheduled event for now 110 | return []t.Event{eventResp} 111 | } 112 | -------------------------------------------------------------------------------- /pkg/mock/events/internal/types/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 types 15 | 16 | // Event represents the metadata structure for mock json response parsing 17 | type Event struct { 18 | Code string `mapstructure:"code"` 19 | Description string `mapstructure:"description"` 20 | State string `mapstructure:"state"` // State of the scheduled event 21 | EventID string `mapstructure:"event-id" json:"EventId"` 22 | NotBefore string `mapstructure:"not-before"` // The earliest start time for the scheduled event 23 | NotAfter string `mapstructure:"not-after,omitempty"` // The latest end time for the scheduled event 24 | NotBeforeDeadline string `mapstructure:"not-before-deadline,omitempty"` // The deadline for starting the event 25 | } 26 | -------------------------------------------------------------------------------- /pkg/mock/imdsv2/imdsv2_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 imdsv2 15 | 16 | import ( 17 | "fmt" 18 | "io/ioutil" 19 | "net/http" 20 | "net/http/httptest" 21 | "regexp" 22 | "strconv" 23 | "strings" 24 | "testing" 25 | "time" 26 | 27 | "github.com/aws/amazon-ec2-metadata-mock/pkg/server" 28 | h "github.com/aws/amazon-ec2-metadata-mock/test" 29 | ) 30 | 31 | const ( 32 | testURL = "http://test-example.com/" 33 | tokenRegex = "^[a-zA-Z0-9+/]{43}=" 34 | successMockResponse = "Success!" 35 | ) 36 | 37 | func MockHandler(res http.ResponseWriter, req *http.Request) { 38 | server.FormatAndReturnTextResponse(res, successMockResponse) 39 | } 40 | 41 | // Token Generator Tests 42 | func TestGenerateToken(t *testing.T) { 43 | req := httptest.NewRequest("PUT", testURL, nil) 44 | req.Header.Set(tokenTTLHeader, "21500") 45 | generateTokenResp := executeTestHTTPRequest(req, GenerateToken) 46 | token, ttl := parseGenTokenResp(generateTokenResp) 47 | h.Assert(t, isTokenValid(token), fmt.Sprintf("Expected valid token generation, but was %s", token)) 48 | h.Assert(t, ttl == 21500, fmt.Sprintf("Expected Token TTL header to equal requested value, but was %d", ttl)) 49 | } 50 | func TestInvalidGenerateTokenRequestGet(t *testing.T) { 51 | req := httptest.NewRequest("GET", testURL, nil) 52 | req.Header.Set(tokenTTLHeader, "21500") 53 | generateTokenResp := executeTestHTTPRequest(req, GenerateToken) 54 | token, _ := parseGenTokenResp(generateTokenResp) 55 | h.Assert(t, token == "", fmt.Sprintf("Expected no token, but was %s", token)) 56 | } 57 | func TestInvalidGenerateTokenInvalidTTL(t *testing.T) { 58 | req := httptest.NewRequest("PUT", testURL, nil) 59 | req.Header.Set(tokenTTLHeader, "0") 60 | generateTokenResp := executeTestHTTPRequest(req, GenerateToken) 61 | respContent, _ := parseGenTokenResp(generateTokenResp) 62 | h.Assert(t, respContent == server.BadRequestResponse, fmt.Sprintf("Expected 400 -- Bad Request, but was %s", respContent)) 63 | } 64 | func TestInvalidGenerateTokenNoTTL(t *testing.T) { 65 | req := httptest.NewRequest("PUT", testURL, nil) 66 | generateTokenResp := executeTestHTTPRequest(req, GenerateToken) 67 | respContent, _ := parseGenTokenResp(generateTokenResp) 68 | h.Assert(t, respContent == server.BadRequestResponse, fmt.Sprintf("Expected 400 -- Bad Request, but was %s", respContent)) 69 | } 70 | 71 | // Token Validator Tests 72 | func TestValidateToken(t *testing.T) { 73 | req := httptest.NewRequest("PUT", testURL, nil) 74 | req.Header.Set(tokenTTLHeader, "21500") 75 | generateTokenResp := executeTestHTTPRequest(req, GenerateToken) 76 | validTestToken, _ := parseGenTokenResp(generateTokenResp) 77 | 78 | req = httptest.NewRequest("GET", testURL, nil) 79 | req.Header.Set(tokenRequestHeader, validTestToken) 80 | validateTokenResp := executeTestHTTPRequest(req, http.HandlerFunc(ValidateToken(MockHandler))) 81 | respContent, _ := ioutil.ReadAll(validateTokenResp.Body) 82 | h.Assert(t, strings.TrimSpace(string(respContent)) == successMockResponse, fmt.Sprintf("Expected successful token validation, but was %s", respContent)) 83 | } 84 | func TestValidateTokenNoToken(t *testing.T) { 85 | req := httptest.NewRequest("GET", testURL, nil) 86 | validateTokenResp := executeTestHTTPRequest(req, http.HandlerFunc(ValidateToken(MockHandler))) 87 | respContent, _ := ioutil.ReadAll(validateTokenResp.Body) 88 | h.Assert(t, strings.TrimSpace(string(respContent)) == server.UnauthorizedResponse, fmt.Sprintf("Expected 401 -- Unauthorized for no token, but was %s", respContent)) 89 | } 90 | func TestValidateTokenInvalidToken(t *testing.T) { 91 | invalidTestToken := "ThisTokenIsNotValid!" 92 | req := httptest.NewRequest("GET", testURL, nil) 93 | req.Header.Set(tokenRequestHeader, invalidTestToken) 94 | validateTokenResp := executeTestHTTPRequest(req, http.HandlerFunc(ValidateToken(MockHandler))) 95 | respContent, _ := ioutil.ReadAll(validateTokenResp.Body) 96 | h.Assert(t, strings.TrimSpace(string(respContent)) == server.UnauthorizedResponse, fmt.Sprintf("401 -- Unauthorized for invalid token, but was %s", respContent)) 97 | } 98 | func TestValidateTokenExpiredToken(t *testing.T) { 99 | req := httptest.NewRequest("PUT", testURL, nil) 100 | req.Header.Set(tokenTTLHeader, "1") 101 | generateTokenResp := executeTestHTTPRequest(req, GenerateToken) 102 | expiredTestToken, _ := parseGenTokenResp(generateTokenResp) 103 | 104 | time.Sleep(1 * time.Second) 105 | 106 | req = httptest.NewRequest("GET", testURL, nil) 107 | req.Header.Set(tokenRequestHeader, expiredTestToken) 108 | validateTokenResp := executeTestHTTPRequest(req, http.HandlerFunc(ValidateToken(MockHandler))) 109 | respContent, _ := ioutil.ReadAll(validateTokenResp.Body) 110 | h.Assert(t, strings.TrimSpace(string(respContent)) == server.UnauthorizedResponse, fmt.Sprintf("401 -- Unauthorized for expired token, but was %s", respContent)) 111 | } 112 | 113 | // Test Helpers 114 | func isTokenValid(token string) bool { 115 | matched, _ := regexp.Match(tokenRegex, []byte(token)) 116 | return matched 117 | } 118 | 119 | func executeTestHTTPRequest(req *http.Request, handler http.HandlerFunc) *http.Response { 120 | w := httptest.NewRecorder() 121 | handler.ServeHTTP(w, req) 122 | resp := w.Result() 123 | return resp 124 | } 125 | 126 | func parseGenTokenResp(resp *http.Response) (string, int) { 127 | token, _ := ioutil.ReadAll(resp.Body) 128 | tokenString := strings.TrimSpace(string(token)) 129 | ttl := resp.Header.Get(tokenTTLHeader) 130 | ttlInt := -1 131 | if ttl != "" { 132 | ttlInt, _ = strconv.Atoi(ttl) 133 | } 134 | return tokenString, ttlInt 135 | } 136 | -------------------------------------------------------------------------------- /pkg/mock/imdsv2/tokengenerator.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 imdsv2 15 | 16 | import ( 17 | "encoding/base64" 18 | "errors" 19 | "log" 20 | "math/rand" 21 | "net/http" 22 | "strconv" 23 | "time" 24 | 25 | "github.com/aws/amazon-ec2-metadata-mock/pkg/server" 26 | ) 27 | 28 | const ( 29 | tokenTTLHeader = "X-aws-ec2-metadata-token-ttl-seconds" 30 | maxTTL = 21600 // 6 hours 31 | ) 32 | 33 | var ( 34 | generatedTokens = make(map[string]v2Token) 35 | ) 36 | 37 | type v2Token struct { 38 | Value string // actual token value 39 | TTL int // token ttl 40 | CreatedAt time.Time // time the token was created 41 | } 42 | 43 | // GenerateToken returns a token with the specified TTL used for IMDSv2 requests 44 | func GenerateToken(res http.ResponseWriter, req *http.Request) { 45 | log.Printf("GenerateToken Received request: %v", req) 46 | // only valid with PUT 47 | if req.Method != http.MethodPut { 48 | return 49 | } 50 | // check that header contains valid ttl 51 | requestedTTL := req.Header.Get(tokenTTLHeader) 52 | validTTL, err := extractValidTTL(requestedTTL) 53 | if err != nil { 54 | log.Printf("Something went wrong with ttl validation: %v with requested TTL: %v", err.Error(), requestedTTL) 55 | server.ReturnBadRequestResponse(res) 56 | return 57 | } 58 | 59 | key := make([]byte, 32) 60 | _, err = rand.Read(key) 61 | if err != nil { 62 | server.FormatAndReturnTextResponse(res, "Something went wrong with token creation") 63 | return 64 | } 65 | 66 | tokenValue := base64.StdEncoding.EncodeToString(key) 67 | token := v2Token{ 68 | Value: tokenValue, 69 | TTL: validTTL, 70 | CreatedAt: time.Now(), 71 | } 72 | generatedTokens[token.Value] = token 73 | res.Header().Set(tokenTTLHeader, strconv.Itoa(token.TTL)) 74 | server.FormatAndReturnTextResponse(res, token.Value) 75 | } 76 | 77 | func extractValidTTL(reqTTL string) (int, error) { 78 | if reqTTL == "" { 79 | log.Printf("TTL is required. requested TTL: %v", reqTTL) 80 | return 0, errors.New("TTL is nil") 81 | } 82 | 83 | intTTL, err := strconv.Atoi(reqTTL) 84 | 85 | if err != nil { 86 | log.Printf("Something went wrong with ttl conversion. requested TTL: %v", reqTTL) 87 | return 0, err 88 | } 89 | if intTTL <= 0 || intTTL > maxTTL { 90 | return 0, errors.New("TTL needs to be between 0-21600 seconds") 91 | } 92 | 93 | return intTTL, nil 94 | } 95 | -------------------------------------------------------------------------------- /pkg/mock/imdsv2/tokenvalidator.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 imdsv2 15 | 16 | import ( 17 | "log" 18 | "net/http" 19 | "time" 20 | 21 | "github.com/aws/amazon-ec2-metadata-mock/pkg/server" 22 | ) 23 | 24 | const ( 25 | tokenRequestHeader = "X-aws-ec2-metadata-token" 26 | ) 27 | 28 | // ValidateToken is a wrapper to validate token before passing request to provided handler 29 | func ValidateToken(pathHandler server.HandlerType) server.HandlerType { 30 | return func(res http.ResponseWriter, req *http.Request) { 31 | log.Printf("ValidateToken Received request: %v", req) 32 | providedToken := req.Header.Get(tokenRequestHeader) 33 | if providedToken == "" { 34 | log.Println("Token required; No token provided.") 35 | server.ReturnUnauthorizedResponse(res) 36 | return 37 | } 38 | 39 | if actualToken, ok := generatedTokens[providedToken]; ok { 40 | accessTime := time.Now() 41 | duration := accessTime.Sub(actualToken.CreatedAt) 42 | if int(duration.Seconds()) >= actualToken.TTL { 43 | log.Println("Token has expired") 44 | delete(generatedTokens, providedToken) 45 | server.ReturnUnauthorizedResponse(res) 46 | return 47 | } 48 | log.Println("Token validated!") 49 | pathHandler(res, req) 50 | } else { 51 | log.Printf("Invalid token provided: %v", providedToken) 52 | server.ReturnUnauthorizedResponse(res) 53 | return 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pkg/mock/root/root.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 root 15 | 16 | import ( 17 | cfg "github.com/aws/amazon-ec2-metadata-mock/pkg/config" 18 | "github.com/aws/amazon-ec2-metadata-mock/pkg/mock/asglifecycle" 19 | "github.com/aws/amazon-ec2-metadata-mock/pkg/mock/events" 20 | "github.com/aws/amazon-ec2-metadata-mock/pkg/mock/spot" 21 | "github.com/aws/amazon-ec2-metadata-mock/pkg/server" 22 | ) 23 | 24 | // Mock serves all subcommand handlers 25 | func Mock(config cfg.Config) { 26 | 27 | // set configs for subcommands 28 | spot.SetConfig(config) 29 | events.SetConfig(config) 30 | asglifecycle.SetConfig(config) 31 | 32 | server.ListenAndServe(config.Server.HostName, config.Server.Port) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/mock/spot/config/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 15 | 16 | // Config represents the configuration for the mock 17 | type Config struct { 18 | InstanceAction string `mapstructure:"action"` 19 | TerminationTime string `mapstructure:"time"` 20 | RebalanceRecTime string `mapstructure:"rebalance-rec-time"` 21 | } 22 | -------------------------------------------------------------------------------- /pkg/mock/spot/internal/types/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 types 15 | 16 | // InstanceActionResponse metadata structure for mock json response parsing 17 | type InstanceActionResponse struct { 18 | Action string `json:"action"` 19 | Time string `json:"time"` 20 | } 21 | 22 | // RebalanceRecommendationResponse metadata structure for mock json response parsing 23 | type RebalanceRecommendationResponse struct { 24 | NoticeTime string `json:"noticeTime"` 25 | } 26 | -------------------------------------------------------------------------------- /pkg/mock/spot/spot.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 spot 15 | 16 | import ( 17 | "log" 18 | "net/http" 19 | "strings" 20 | "time" 21 | 22 | cfg "github.com/aws/amazon-ec2-metadata-mock/pkg/config" 23 | t "github.com/aws/amazon-ec2-metadata-mock/pkg/mock/spot/internal/types" 24 | "github.com/aws/amazon-ec2-metadata-mock/pkg/server" 25 | ) 26 | 27 | const ( 28 | instanceActionPath = "/latest/meta-data/spot/instance-action" 29 | terminationTimePath = "/latest/meta-data/spot/termination-time" 30 | rebalanceRecPath = "/latest/meta-data/events/recommendations/rebalance" 31 | ) 32 | 33 | var ( 34 | eligibleIPs = make(map[string]bool) 35 | spotItnStartTime int64 = time.Now().Unix() 36 | c cfg.Config 37 | ) 38 | 39 | // Mock starts spot itn mock 40 | func Mock(config cfg.Config) { 41 | SetConfig(config) 42 | server.ListenAndServe(config.Server.HostName, config.Server.Port) 43 | } 44 | 45 | // SetConfig sets the local config 46 | func SetConfig(config cfg.Config) { 47 | c = config 48 | } 49 | 50 | // Handler processes http requests 51 | func Handler(res http.ResponseWriter, req *http.Request) { 52 | // specify negative value to disable this feature 53 | if c.MockIPCount >= 0 { 54 | // req.RemoteAddr is formatted as IP:port 55 | requestIP := strings.Split(req.RemoteAddr, ":")[0] 56 | if !eligibleIPs[requestIP] { 57 | if len(eligibleIPs) < c.MockIPCount { 58 | eligibleIPs[requestIP] = true 59 | } else { 60 | log.Printf("Requesting IP %s is not eligible for Spot ITN or Rebalance Recommendation because the max number of IPs configured (%d) has been reached.\n", requestIP, c.MockIPCount) 61 | server.ReturnNotFoundResponse(res) 62 | return 63 | } 64 | } 65 | } 66 | switch req.URL.Path { 67 | case instanceActionPath, terminationTimePath: 68 | handleSpotITN(res, req) 69 | case rebalanceRecPath: 70 | handleRebalance(res, req) 71 | } 72 | } 73 | 74 | func handleSpotITN(res http.ResponseWriter, req *http.Request) { 75 | requestTime := time.Now().Unix() 76 | if c.MockTriggerTime != "" { 77 | triggerTime, _ := time.Parse(time.RFC3339, c.MockTriggerTime) 78 | delayRemaining := triggerTime.Unix() - requestTime 79 | if delayRemaining > 0 { 80 | log.Printf("MockTriggerTime %s was not reached yet. The spot itn will be available in %ds. Returning `notFoundResponse` for now", triggerTime, delayRemaining) 81 | server.ReturnNotFoundResponse(res) 82 | return 83 | } 84 | } else { 85 | delayInSeconds := c.MockDelayInSec 86 | delayRemaining := delayInSeconds - (requestTime - spotItnStartTime) 87 | if delayRemaining > 0 { 88 | log.Printf("Delaying the response by %ds as requested. The spot itn will be available in %ds. Returning `notFoundResponse` for now", delayInSeconds, delayRemaining) 89 | server.ReturnNotFoundResponse(res) 90 | return 91 | } 92 | } 93 | // default time to requestTime + 2min, unless overridden 94 | mockResponseTime := time.Now().UTC().Add(time.Minute * time.Duration(2)).Format(time.RFC3339) 95 | if c.SpotConfig.TerminationTime != "" { 96 | mockResponseTime = c.SpotConfig.TerminationTime 97 | } 98 | // return mock response after the delay or trigger time has elapsed 99 | switch req.URL.Path { 100 | case instanceActionPath: 101 | server.FormatAndReturnJSONResponse(res, getInstanceActionResponse(mockResponseTime)) 102 | case terminationTimePath: 103 | server.FormatAndReturnTextResponse(res, mockResponseTime) 104 | } 105 | } 106 | 107 | func handleRebalance(res http.ResponseWriter, req *http.Request) { 108 | requestTime := time.Now().Unix() 109 | if c.RebalanceTriggerTime != "" { 110 | triggerTime, _ := time.Parse(time.RFC3339, c.RebalanceTriggerTime) 111 | delayRemaining := triggerTime.Unix() - requestTime 112 | if delayRemaining > 0 { 113 | log.Printf("RebalanceTriggerTime %s was not reached yet. The rebalance rec will be available in %ds. Returning `notFoundResponse` for now", triggerTime, delayRemaining) 114 | server.ReturnNotFoundResponse(res) 115 | return 116 | } 117 | } else { 118 | delayInSeconds := c.RebalanceDelayInSec 119 | delayRemaining := delayInSeconds - (requestTime - spotItnStartTime) 120 | if delayRemaining > 0 { 121 | log.Printf("Delaying the response by %ds as requested. The rebalance rec will be available in %ds. Returning `notFoundResponse` for now", delayInSeconds, delayRemaining) 122 | server.ReturnNotFoundResponse(res) 123 | return 124 | } 125 | } 126 | // default time to requestTime, unless overridden 127 | mockResponseTime := time.Now().UTC().Format(time.RFC3339) 128 | if c.SpotConfig.RebalanceRecTime != "" { 129 | mockResponseTime = c.SpotConfig.RebalanceRecTime 130 | } 131 | server.FormatAndReturnJSONResponse(res, t.RebalanceRecommendationResponse{NoticeTime: mockResponseTime}) 132 | } 133 | 134 | func getInstanceActionResponse(time string) t.InstanceActionResponse { 135 | return t.InstanceActionResponse{ 136 | Action: c.SpotConfig.InstanceAction, 137 | Time: time, 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /pkg/mock/static/static.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 static 15 | 16 | import ( 17 | "log" 18 | "net/http" 19 | "reflect" 20 | 21 | cfg "github.com/aws/amazon-ec2-metadata-mock/pkg/config" 22 | "github.com/aws/amazon-ec2-metadata-mock/pkg/mock/imdsv2" 23 | "github.com/aws/amazon-ec2-metadata-mock/pkg/server" 24 | ) 25 | 26 | var ( 27 | supportedPaths = make(map[string]interface{}) 28 | response interface{} 29 | jsonTextResponse = map[string]bool{"/latest/meta-data/elastic-inference/associations/eia-bfa21c7904f64a82a21b9f4540169ce1": true} 30 | 31 | // ServicePath defines the static service path 32 | ServicePath = "/latest/meta-data" 33 | ) 34 | 35 | // Handler processes http requests 36 | func Handler(res http.ResponseWriter, req *http.Request) { 37 | log.Println("Received request to mock static metadata:", req.URL.Path) 38 | 39 | if val, ok := supportedPaths[req.URL.Path]; ok { 40 | response = val 41 | } else { 42 | response = "Something went wrong with: " + req.URL.Path 43 | } 44 | 45 | switch response.(type) { 46 | // static metadata values are either string or JSON EXCEPT FOR elastic-inference associations 47 | case string: 48 | server.FormatAndReturnTextResponse(res, response.(string)) 49 | default: 50 | if jsonTextResponse[req.URL.Path] { 51 | server.FormatAndReturnJSONTextResponse(res, response) 52 | } else { 53 | server.FormatAndReturnJSONResponse(res, response) 54 | } 55 | } 56 | } 57 | 58 | // RegisterHandlers registers handlers for static paths 59 | func RegisterHandlers(config cfg.Config) { 60 | server.HandleFunc("/latest/api/token", imdsv2.GenerateToken) 61 | 62 | pathValues := reflect.ValueOf(config.Metadata.Paths) 63 | mdValues := reflect.ValueOf(config.Metadata.Values) 64 | 65 | // Iterate over fields in config.Metadata.Paths to 66 | // determine intersections with config.Metadata.Values. 67 | // Intersections represent which paths and values to bind. 68 | for i := 0; i < pathValues.NumField(); i++ { 69 | pathFieldName := pathValues.Type().Field(i).Name 70 | mdValueFieldName := mdValues.FieldByName(pathFieldName) 71 | if mdValueFieldName.IsValid() { 72 | path := pathValues.Field(i).Interface().(string) 73 | value := mdValueFieldName.Interface() 74 | if path != "" && value != nil { 75 | // Ex: "/latest/meta-data/instance-id" : "i-1234567890abcdef0" 76 | supportedPaths[path] = value 77 | if config.Imdsv2Required { 78 | server.HandleFunc(path, imdsv2.ValidateToken(Handler)) 79 | } else { 80 | server.HandleFunc(path, Handler) 81 | } 82 | } else { 83 | log.Printf("There was an issue registering path %v with mdValue: %v", path, value) 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /pkg/mock/static/types/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 types 15 | 16 | // IamInformation metadata structure for mock json response parsing 17 | type IamInformation struct { 18 | Code string `json:"Code"` 19 | LastUpdated string `json:"LastUpdated"` 20 | InstanceProfileArn string `json:"InstanceProfileArn"` 21 | InstanceProfileId string `json:"InstanceProfileId"` 22 | } 23 | 24 | // IamSecurityCredentials metadata structure for mock json response parsing 25 | type IamSecurityCredentials struct { 26 | Code string `json:"Code"` 27 | LastUpdated string `json:"LastUpdated"` 28 | Type string `json:"Type"` 29 | AccessKeyId string `json:"AccessKeyId"` 30 | SecretAccessKey string `json:"SecretAccessKey"` 31 | Token string `json:"Token"` 32 | Expiration string `json:"Expiration"` 33 | } 34 | 35 | // ElasticInferenceAccelerator metadata structure for mock json response parsing 36 | type ElasticInferenceAccelerator struct { 37 | Version elasticInferenceAcceleratorMetadata `json:"version_2018_04_12"` 38 | } 39 | 40 | type elasticInferenceAcceleratorMetadata struct { 41 | ElasticInferenceAcceleratorId string `json:"elastic-inference-accelerator-id"` 42 | ElasticInferenceAcceleratorType string `json:"elastic-inference-accelerator-type"` 43 | } 44 | -------------------------------------------------------------------------------- /pkg/mock/userdata/userdata.go: -------------------------------------------------------------------------------- 1 | // Copyright 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 userdata 15 | 16 | import ( 17 | "encoding/base64" 18 | "fmt" 19 | "log" 20 | "net/http" 21 | "reflect" 22 | 23 | cfg "github.com/aws/amazon-ec2-metadata-mock/pkg/config" 24 | "github.com/aws/amazon-ec2-metadata-mock/pkg/mock/imdsv2" 25 | "github.com/aws/amazon-ec2-metadata-mock/pkg/server" 26 | ) 27 | 28 | var ( 29 | supportedPaths = make(map[string]interface{}) 30 | response interface{} 31 | // ServicePath defines the userdata service path 32 | ServicePath = "/latest/user-data" 33 | ) 34 | 35 | // Handler processes http requests 36 | func Handler(res http.ResponseWriter, req *http.Request) { 37 | log.Println("Received request to mock userdata:", req.URL.Path) 38 | 39 | if val, ok := supportedPaths[req.URL.Path]; ok { 40 | 41 | response = val 42 | } else { 43 | response = "Something went wrong with: " + req.URL.Path 44 | } 45 | server.FormatAndReturnOctetResponse(res, response.(string)) 46 | 47 | } 48 | 49 | // RegisterHandlers registers handlers for userdata paths 50 | func RegisterHandlers(config cfg.Config) { 51 | pathValues := reflect.ValueOf(config.Userdata.Paths) 52 | udValues := reflect.ValueOf(config.Userdata.Values) 53 | // Iterate over fields in config.Userdata.Paths to 54 | // determine intersections with config.Userdata.Values. 55 | // Intersections represent which paths and values to bind. 56 | for i := 0; i < pathValues.NumField(); i++ { 57 | pathFieldName := pathValues.Type().Field(i).Name 58 | udValueFieldName := udValues.FieldByName(pathFieldName) 59 | if udValueFieldName.IsValid() { 60 | path := pathValues.Field(i).Interface().(string) 61 | value := udValueFieldName.Interface() 62 | if path != "" && value != nil { 63 | // Ex: "/latest/meta-data/instance-id" : "i-1234567890abcdef0" 64 | bvalue, err := base64.StdEncoding.DecodeString(config.Userdata.Values.Userdata) 65 | value = string(bvalue) 66 | 67 | if err != nil { 68 | fmt.Println("There was an issue decoding base64 data from config") 69 | panic(err) 70 | } 71 | supportedPaths[path] = value 72 | if config.Imdsv2Required { 73 | server.HandleFunc(path, imdsv2.ValidateToken(Handler)) 74 | } else { 75 | 76 | server.HandleFunc(path, Handler) 77 | } 78 | } else { 79 | log.Printf("There was an issue registering path %v with udValue: %v", path, value) 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /pkg/server/httpserver_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 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 server 15 | 16 | import ( 17 | "net/http/httptest" 18 | "testing" 19 | 20 | "github.com/aws/amazon-ec2-metadata-mock/pkg/mock/dynamic/types" 21 | h "github.com/aws/amazon-ec2-metadata-mock/test" 22 | ) 23 | 24 | func TestFormatAndReturnJSONResponse(t *testing.T) { 25 | expected := `{ 26 | "accountId": "123456789012", 27 | "imageId": "ami-02f471c4f805553d3", 28 | "availabilityZone": "us-east-1a", 29 | "ramdiskId": null, 30 | "kernelId": null, 31 | "devpayProductCodes": null, 32 | "marketplaceProductCodes": ["4i20ezfza3p7xx2kt2g8weu2u","entry"], 33 | "version": "2017-09-30", 34 | "privateIp": "172.31.85.190", 35 | "billingProducts": null, 36 | "instanceId": "i-048bcb15d2686eec7", 37 | "pendingTime": "2022-06-23T06:21:55Z", 38 | "architecture": "x86_64", 39 | "instanceType": "t2.nano", 40 | "region": "us-east-1" 41 | }` 42 | 43 | testDoc := types.InstanceIdentityDocument{ 44 | AccountId: "123456789012", 45 | ImageId: "ami-02f471c4f805553d3", 46 | AvailabilityZone: "us-east-1a", 47 | RamdiskId: nil, 48 | KernelId: nil, 49 | DevpayProductCodes: nil, 50 | MarketplaceProductCodes: []string{"4i20ezfza3p7xx2kt2g8weu2u", "entry"}, 51 | Version: "2017-09-30", 52 | PrivateIp: "172.31.85.190", 53 | BillingProducts: nil, 54 | InstanceId: "i-048bcb15d2686eec7", 55 | PendingTime: "2022-06-23T06:21:55Z", 56 | Architecture: "x86_64", 57 | InstanceType: "t2.nano", 58 | Region: "us-east-1", 59 | } 60 | rr := httptest.NewRecorder() 61 | FormatAndReturnJSONResponse(rr, testDoc) 62 | actual := rr.Body.String() 63 | h.Assert(t, expected == actual, "FormatAndReturnJSONResponse did not format InstanceIdentityDocument as expected.") 64 | } 65 | -------------------------------------------------------------------------------- /pkg/server/swapper.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | "sync" 6 | 7 | "github.com/gorilla/mux" 8 | ) 9 | 10 | var _ http.Handler = (*swapper)(nil) 11 | 12 | // swapper is an `http.Handler` which allows the user to swap out routers while the server is running. 13 | // This enables us to reset routes as the program is running, allowing for functionality like updating the config. 14 | type swapper struct { 15 | mu sync.Mutex 16 | router *mux.Router 17 | } 18 | 19 | func NewSwapper() *swapper { 20 | return &swapper{ 21 | mu: sync.Mutex{}, 22 | router: mux.NewRouter(), 23 | } 24 | } 25 | 26 | func (rs *swapper) Reset() { 27 | rs.mu.Lock() 28 | rs.router = mux.NewRouter() 29 | rs.mu.Unlock() 30 | } 31 | 32 | func (rs *swapper) Walk(f func(route *mux.Route, r *mux.Router, ancestors []*mux.Route) error) { 33 | rs.mu.Lock() 34 | rs.router.Walk(f) 35 | rs.mu.Unlock() 36 | } 37 | 38 | func (rs *swapper) HandleFuncPrefix(pattern string, requestHandler HandlerType) { 39 | rs.mu.Lock() 40 | rs.router.PathPrefix(pattern).HandlerFunc(requestHandler) 41 | rs.mu.Unlock() 42 | } 43 | 44 | func (rs *swapper) HandleFunc(pattern string, requestHandler HandlerType) { 45 | rs.mu.Lock() 46 | rs.router.HandleFunc(pattern, requestHandler) 47 | rs.mu.Unlock() 48 | } 49 | 50 | func (rs *swapper) Swap(newRouter *mux.Router) { 51 | rs.mu.Lock() 52 | rs.router = newRouter 53 | rs.mu.Unlock() 54 | } 55 | 56 | func (rs *swapper) ServeHTTP(w http.ResponseWriter, r *http.Request) { 57 | rs.mu.Lock() 58 | router := rs.router 59 | rs.mu.Unlock() 60 | router.ServeHTTP(w, r) 61 | } 62 | -------------------------------------------------------------------------------- /scripts/build-binaries: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" 5 | 6 | REPO_ROOT_PATH=$SCRIPTPATH/../ 7 | MAKE_FILE_PATH=$REPO_ROOT_PATH/Makefile 8 | BIN_DIR=$SCRIPTPATH/../build/bin 9 | mkdir -p $BIN_DIR 10 | 11 | VERSION=$(make -s -f $MAKE_FILE_PATH version) 12 | BASE_BIN_NAME=$(make -s -f $MAKE_FILE_PATH binary-name) 13 | PLATFORMS=("linux/amd64") 14 | PASS_THRU_ARGS="" 15 | 16 | USAGE=$(cat << 'EOM' 17 | Usage: build-binaries [-p ] 18 | Builds static binaries for the platform pairs passed in 19 | 20 | Example: build-binaries -p "linux/amd64,linux/arm" 21 | Optional: 22 | -b Base bin name [DEFAULT: output of "make binary-name"] 23 | -p Platform pair list (os/architecture) [DEFAULT: linux/amd64] 24 | -d DIRECT: Set GOPROXY=direct to bypass go proxies 25 | -v VERSION: The application version of the docker image [DEFAULT: output of `make version`] 26 | EOM 27 | ) 28 | 29 | # Process our input arguments 30 | while getopts "dp:v:b:" opt; do 31 | case ${opt} in 32 | p ) # Platform Pairs 33 | IFS=',' read -ra PLATFORMS <<< "$OPTARG" 34 | ;; 35 | d ) # sets GOPROXY=direct 36 | PASS_THRU_ARGS="$PASS_THRU_ARGS -d" 37 | ;; 38 | v ) # Image Version 39 | VERSION="$OPTARG" 40 | ;; 41 | b ) # Base bin name 42 | BASE_BIN_NAME="$OPTARG" 43 | ;; 44 | \? ) 45 | echo "$USAGE" 1>&2 46 | exit 47 | ;; 48 | esac 49 | done 50 | 51 | for os_arch in "${PLATFORMS[@]}"; do 52 | os=$(echo $os_arch | cut -d'/' -f1) 53 | arch=$(echo $os_arch | cut -d'/' -f2) 54 | container_name="build-$BASE_BIN_NAME-$os-$arch" 55 | repo_name="bin-build" 56 | 57 | if [[ $os == "windows" ]]; then 58 | bin_name="$BASE_BIN_NAME-$os-$arch.exe" 59 | else 60 | bin_name="$BASE_BIN_NAME-$os-$arch" 61 | fi 62 | 63 | docker container rm $container_name || : 64 | $SCRIPTPATH/build-docker-images -p $os_arch -v $VERSION -r $repo_name $PASS_THRU_ARGS 65 | docker container create --rm --name $container_name "$repo_name:$VERSION-$os-$arch" 66 | docker container cp $container_name:/ec2-metadata-mock $BIN_DIR/$bin_name 67 | 68 | if [[ $os == "windows" ]]; then 69 | ## Create zip archive with binary taking into account windows .exe 70 | cp ${BIN_DIR}/${bin_name} ${BIN_DIR}/${BASE_BIN_NAME}.exe 71 | ## Can't reuse bin_name below because it includes .exe 72 | curr_dir=$(pwd) 73 | cd "${BIN_DIR}" 74 | zip -9 -q ${BASE_BIN_NAME}-$os-$arch.zip ${BASE_BIN_NAME}.exe 75 | cd "${curr_dir}" 76 | rm -f ${BIN_DIR}/${BASE_BIN_NAME}.exe 77 | else 78 | ## Create tar.gz archive with binary 79 | cp ${BIN_DIR}/${bin_name} ${BIN_DIR}/${BASE_BIN_NAME} 80 | tar -zcvf ${BIN_DIR}/${bin_name}.tar.gz -C ${BIN_DIR} ${BASE_BIN_NAME} 81 | rm -f ${BIN_DIR}/${BASE_BIN_NAME} 82 | fi 83 | done -------------------------------------------------------------------------------- /scripts/build-docker-images: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" 5 | 6 | REPO_ROOT_PATH=$SCRIPTPATH/../ 7 | MAKE_FILE_PATH=$REPO_ROOT_PATH/Makefile 8 | DOCKERFILE_PATH=$REPO_ROOT_PATH/Dockerfile 9 | 10 | VERSION=$(make -s -f $MAKE_FILE_PATH version) 11 | PLATFORMS=("linux/amd64") 12 | GOPROXY="https://proxy.golang.org,direct" 13 | 14 | 15 | USAGE=$(cat << 'EOM' 16 | Usage: build-docker-images [-p ] 17 | Builds docker images for the platform pair 18 | 19 | Example: build-docker-images -p "linux/amd64,linux/arm" 20 | Optional: 21 | -p Platform pair list (os/architecture) [DEFAULT: linux/amd64] 22 | -d DIRECT: Set GOPROXY=direct to bypass go proxies 23 | -r IMAGE REPO: set the docker image repo 24 | -v VERSION: The application version of the docker image [DEFAULT: output of `make version`] 25 | EOM 26 | ) 27 | 28 | # Process our input arguments 29 | while getopts "dp:r:v:" opt; do 30 | case ${opt} in 31 | p ) # Platform Pairs 32 | IFS=',' read -ra PLATFORMS <<< "$OPTARG" 33 | ;; 34 | d ) # GOPROXY=direct 35 | GOPROXY="direct" 36 | ;; 37 | r ) # Image Repo 38 | IMAGE_REPO="$OPTARG" 39 | ;; 40 | v ) # Image Version 41 | VERSION="$OPTARG" 42 | ;; 43 | \? ) 44 | echo "$USAGE" 1>&2 45 | exit 46 | ;; 47 | esac 48 | done 49 | 50 | for os_arch in "${PLATFORMS[@]}"; do 51 | os=$(echo $os_arch | cut -d'/' -f1) 52 | arch=$(echo $os_arch | cut -d'/' -f2) 53 | 54 | img_tag="$IMAGE_REPO:$VERSION-$os-$arch" 55 | 56 | dockerfile="$DOCKERFILE_PATH" 57 | if [[ $os = "windows" ]]; then 58 | # Linux Dockerfile is valid for building Windows binary 59 | if [[ $IMAGE_REPO != "bin-build" ]]; then 60 | dockerfile="${dockerfile}.windows" 61 | fi 62 | fi 63 | 64 | docker build \ 65 | --file "${dockerfile}" \ 66 | --build-arg GOOS=${os} \ 67 | --build-arg GOARCH=${arch} \ 68 | --build-arg GOPROXY=${GOPROXY} \ 69 | --tag ${img_tag} \ 70 | ${REPO_ROOT_PATH} 71 | done -------------------------------------------------------------------------------- /scripts/create-local-tag-for-release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to create a new local tag in preparation for a release 4 | # This script is idempotent i.e. it always fetches remote tags to create the new tag. 5 | # E.g. If the current remote release tag is v1.0.0, 6 | ## 1) running `create-local-tag-for-release -p` will create a new tag v1.0.1 7 | ## 2) immediately running `create-local-tag-for-release -m` will create a new tag v2.0.0 8 | 9 | set -euo pipefail 10 | 11 | REPO_ROOT_PATH="$( cd "$(dirname "$0")"; cd ../; pwd -P )" 12 | MAKEFILE_PATH=$REPO_ROOT_PATH/Makefile 13 | TAG_REGEX="^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]*)?$" 14 | 15 | HELP=$(cat << 'EOM' 16 | Create a new local tag in preparation for a release. This script is idempotent i.e. it always fetches remote tags to create the new tag. 17 | 18 | Usage: create-local-tag-for-release [options] 19 | 20 | Options: 21 | -v new tag / version number. The script relies on the user to specify a valid and accurately incremented tag. 22 | -m increment major version 23 | -i increment minor version 24 | -p increment patch version 25 | -h help 26 | 27 | Examples: 28 | create-local-tag-for-release -v v1.0.0 Create local tag for new version v1.0.0 29 | create-local-tag-for-release -i Create local tag for new version by incrementing minor version only (previous tag=v1.0.0, new tag=v1.1.0) 30 | EOM 31 | ) 32 | 33 | MAJOR_INC=false 34 | MINOR_INC=false 35 | PATCH_INC=false 36 | NEW_TAG="" 37 | CURR_REMOTE_RELEASE_TAG="" 38 | 39 | process_args() { 40 | while getopts "hmipv:" opt; do 41 | case ${opt} in 42 | h ) 43 | echo -e "$HELP" 1>&2 44 | exit 0 45 | ;; 46 | m ) 47 | MAJOR_INC=true 48 | ;; 49 | i ) 50 | MINOR_INC=true 51 | ;; 52 | p ) 53 | PATCH_INC=true 54 | ;; 55 | v ) 56 | NEW_TAG="${OPTARG}" 57 | ;; 58 | \? ) 59 | echo "$HELP" 1>&2 60 | exit 0 61 | ;; 62 | esac 63 | done 64 | } 65 | 66 | validate_args() { 67 | if [[ ! -z $NEW_TAG ]]; then 68 | if ! [[ $NEW_TAG =~ $TAG_REGEX ]]; then 69 | echo "❌ Invalid new tag specified $NEW_TAG. Examples: v1.2.3, v1.2.3-dirty" 70 | exit 1 71 | fi 72 | 73 | echo "🥑 Using the new tag specified with -v flag. All other flags, if specified, will be ignored." 74 | echo " NOTE:The script relies on the user to specify a valid and accurately incremented tag." 75 | return 76 | fi 77 | 78 | if ($MAJOR_INC && $MINOR_INC) || ($MAJOR_INC && $PATCH_INC) || ($MINOR_INC && $PATCH_INC); then 79 | echo "❌ Invalid arguments passed. Specify only one of 3 tag parts to increment for the new tag: -m (major) or -i (minor) or -p (patch)." 80 | exit 1 81 | fi 82 | 83 | if $MAJOR_INC || $MINOR_INC || $PATCH_INC; then 84 | return 85 | fi 86 | 87 | echo -e "❌ Invalid arguments passed. Specify atleast one argument.\n$HELP" 88 | exit 1 89 | } 90 | 91 | sync_local_tags_from_remote() { 92 | # setup remote upstream tracking to fetch tags 93 | git remote add the-real-upstream https://github.com/aws/amazon-ec2-metadata-mock.git &> /dev/null || true 94 | git fetch the-real-upstream 95 | 96 | # delete all local tags 97 | git tag -l | xargs git tag -d 98 | 99 | # fetch remote tags 100 | git fetch the-real-upstream --tags 101 | 102 | # record the latest release tag in remote, before creating a new tag 103 | CURR_REMOTE_RELEASE_TAG=$(get_latest_tag) 104 | 105 | # clean up tracking 106 | git remote remove the-real-upstream 107 | } 108 | 109 | create_tag() { 110 | git tag $NEW_TAG 111 | echo -e "\n✅ Created new tag $NEW_TAG (Current latest release tag in remote: v$CURR_REMOTE_RELEASE_TAG)\n" 112 | exit 0 113 | } 114 | 115 | get_latest_tag() { 116 | make -s -f $MAKEFILE_PATH latest-release-tag | cut -b 2- 117 | } 118 | 119 | main() { 120 | process_args "$@" 121 | validate_args 122 | 123 | sync_local_tags_from_remote 124 | 125 | # if new tag is specified, create it 126 | if [[ ! -z $NEW_TAG ]]; then 127 | create_tag 128 | fi 129 | 130 | # increment version 131 | if $MAJOR_INC || $MINOR_INC || $PATCH_INC; then 132 | curr_major_v=$(echo $CURR_REMOTE_RELEASE_TAG | tr '.' '\n' | head -1) 133 | curr_minor_v=$(echo $CURR_REMOTE_RELEASE_TAG | tr '.' '\n' | head -2 | tail -1) 134 | curr_patch_v=$(echo $CURR_REMOTE_RELEASE_TAG | tr '.' '\n' | tail -1) 135 | 136 | if [[ $MAJOR_INC == true ]]; then 137 | new_major_v=$(echo $(($curr_major_v + 1))) 138 | NEW_TAG=$(echo v$new_major_v.0.0) 139 | elif [[ $MINOR_INC == true ]]; then 140 | new_minor_v=$(echo $(($curr_minor_v + 1))) 141 | NEW_TAG=$(echo v$curr_major_v.$new_minor_v.0) 142 | elif [[ $PATCH_INC == true ]]; then 143 | new_patch_v=$(echo $(($curr_patch_v + 1))) 144 | NEW_TAG=$(echo v$curr_major_v.$curr_minor_v.$new_patch_v) 145 | fi 146 | create_tag 147 | fi 148 | } 149 | 150 | main "$@" -------------------------------------------------------------------------------- /scripts/ecr-public-login: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" 5 | BUILD_DIR=$SCRIPTPATH/../build/ 6 | export PATH="${BUILD_DIR}:${PATH}" 7 | 8 | if [[ -z "${ECR_REGISTRY}" ]]; then 9 | echo "The env var ECR_REGISTRY must be set" 10 | exit 1 11 | fi 12 | 13 | function exit_and_fail() { 14 | echo "❌ Failed to login to ECR Public Repo!" 15 | } 16 | 17 | trap exit_and_fail INT TERM ERR 18 | 19 | docker login --username AWS --password="$(aws ecr-public get-login-password --region us-east-1)" "${ECR_REGISTRY}" 20 | -------------------------------------------------------------------------------- /scripts/ecr-template-for-helm-chart.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Helm Charts for Amazon EC2 Metadata Mock", 3 | "aboutText": "# Helm Chart for Amazon EC2 Metadata Mock\n\nAWS EC2 Instance metadata is data about your instance that you can use to configure or manage the running instance. Instance metadata is divided into categories like hostname, instance id, maintenance events, spot instance action. The instance metadata can be accessed from within the instance. Some instance metadata is available only when an instance is affected by the event.\n\nThis project attempts to bridge the gaps by providing mocks for most of these metadata categories. The mock responses are designed to replicate those from the actual instance metadata service for accurate, local testing.\n\nThis repository contains helm-charts for Amazon EC2 Metadata Mock.\n\nFor more information on this project, see the project repo at [Amazon EC2 Metadata Mock](https://github.com/aws/amazon-ec2-metadata-mock)", 4 | "usageText": "# We can install AEMM using the helm chart from this repository.\n\nWe need to authenticate our helm client to ECR registry and install AEMM chart using helm chart URI, detailed information on how to install helm chart can be found here [HelmChart ReadMe](https://github.com/aws/amazon-ec2-metadata-mock/blob/main/helm/amazon-ec2-metadata-mock/README.md)" 5 | } -------------------------------------------------------------------------------- /scripts/generate-helm-chart-archives: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to generate the helm chart archives for charts in REPO/helm 4 | 5 | set -euo pipefail 6 | 7 | REPO_ROOT_PATH="$( cd "$(dirname "$0")"; cd ../ ; pwd -P )" 8 | MAKEFILE_PATH=$REPO_ROOT_PATH/Makefile 9 | VERSION=$(make -s -f $MAKEFILE_PATH version) 10 | HELM_CHART_PATH=$REPO_ROOT_PATH/helm 11 | BUILD_DIR=$REPO_ROOT_PATH/build/k8s-resources/$VERSION 12 | HELM_CHART_ARCHIVES_DIR=$BUILD_DIR/helm-chart-archives 13 | mkdir -p $HELM_CHART_ARCHIVES_DIR 14 | 15 | for c in $HELM_CHART_PATH/*; do 16 | if [ -d $c ]; then 17 | chart_dir=$(echo $c | tr '/' '\n' | tail -1) 18 | echo "Generating Helm archive for $chart_dir" 19 | chart_version=`sed -n 's/version: //p' $c/Chart.yaml` 20 | 21 | archive_name="$chart_dir-$chart_version.tgz" 22 | cd $HELM_CHART_ARCHIVES_DIR 23 | tar -C $HELM_CHART_PATH -zcf $archive_name $chart_dir 24 | fi 25 | done 26 | 27 | echo -e "\nGenerated Helm chart archives in $HELM_CHART_ARCHIVES_DIR:" 28 | for archive in $HELM_CHART_ARCHIVES_DIR/*; do 29 | echo " - $(echo $archive | tr '/' '\n' | tail -1)" 30 | done 31 | -------------------------------------------------------------------------------- /scripts/generate-k8s-yaml: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" 5 | 6 | PLATFORM=$(uname | tr '[:upper:]' '[:lower:]') 7 | HELM_VERSION="3.2.0" 8 | NAMESPACE="default" 9 | 10 | MAKEFILEPATH=$SCRIPTPATH/../Makefile 11 | VERSION=$(make -s -f $MAKEFILEPATH version) 12 | BUILD_DIR=$SCRIPTPATH/../build/k8s-resources/$VERSION 13 | INDV_RESOURCES_DIR=$BUILD_DIR/individual-resources 14 | TAR_RESOURCES_FILE=$BUILD_DIR/individual-resources.tar 15 | AGG_RESOURCES_YAML=$BUILD_DIR/all-resources.yaml 16 | mkdir -p $INDV_RESOURCES_DIR 17 | 18 | USAGE=$(cat << 'EOM' 19 | Usage: generate-k8s-yaml [-n ] 20 | Generates the kubernetes yaml resource files from the helm chart 21 | and places them into the build dir. 22 | Example: generate-k8s-yaml -n kube-system 23 | Optional: 24 | -n Kubernetes namespace 25 | EOM 26 | ) 27 | 28 | # Process our input arguments 29 | while getopts "n:" opt; do 30 | case ${opt} in 31 | n ) # K8s namespace 32 | NAMESPACE=$OPTARG 33 | ;; 34 | \? ) 35 | echoerr "$USAGE" 1>&2 36 | exit 37 | ;; 38 | esac 39 | done 40 | 41 | curl -L https://get.helm.sh/helm-v$HELM_VERSION-$PLATFORM-amd64.tar.gz | tar zxf - -C $BUILD_DIR 42 | mv $BUILD_DIR/$PLATFORM-amd64/helm $BUILD_DIR/. 43 | rm -rf $BUILD_DIR/$PLATFORM-amd64 44 | chmod +x $BUILD_DIR/helm 45 | 46 | $BUILD_DIR/helm template amazon-ec2-metadata-mock \ 47 | --no-hooks \ 48 | --namespace $NAMESPACE \ 49 | $SCRIPTPATH/../helm/amazon-ec2-metadata-mock/ > $AGG_RESOURCES_YAML 50 | 51 | # remove helm annotations from template 52 | cat $AGG_RESOURCES_YAML | grep -v 'helm.sh\|app.kubernetes.io/managed-by: Helm' > $BUILD_DIR/helm_annotations_removed.yaml 53 | mv $BUILD_DIR/helm_annotations_removed.yaml $AGG_RESOURCES_YAML 54 | 55 | $BUILD_DIR/helm template amazon-ec2-metadata-mock \ 56 | --no-hooks \ 57 | --namespace $NAMESPACE \ 58 | --set targetNodeOs="linux windows" \ 59 | --output-dir $INDV_RESOURCES_DIR/ \ 60 | $SCRIPTPATH/../helm/amazon-ec2-metadata-mock/ 61 | 62 | # remove helm annotations from template 63 | for i in $INDV_RESOURCES_DIR/amazon-ec2-metadata-mock/templates/*; do 64 | if [ -f $i ]; then # ignore helm tests 65 | cat $i | grep -v 'helm.sh\|app.kubernetes.io/managed-by: Helm' > $BUILD_DIR/helm_annotations_removed.yaml 66 | mv $BUILD_DIR/helm_annotations_removed.yaml $i 67 | fi 68 | done 69 | 70 | cd $INDV_RESOURCES_DIR/amazon-ec2-metadata-mock/ && tar cvf $TAR_RESOURCES_FILE templates/* 71 | cd $SCRIPTPATH 72 | 73 | echo "Generated amazon-ec2-metadata-mock kubernetes yaml resources files in:" 74 | echo " - $AGG_RESOURCES_YAML" 75 | echo " - $TAR_RESOURCES_FILE" 76 | 77 | -------------------------------------------------------------------------------- /scripts/helm-login: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" 5 | BUILD_DIR=$SCRIPTPATH/../build/ 6 | export PATH="${BUILD_DIR}:${PATH}" 7 | 8 | if [[ -z "${ECR_REGISTRY}" ]]; then 9 | echo "The env var ECR_REGISTRY must be set" 10 | exit 1 11 | fi 12 | 13 | function exit_and_fail() { 14 | echo "❌ Failed to login to ECR Public Repo!" 15 | } 16 | 17 | trap exit_and_fail INT TERM ERR 18 | 19 | export HELM_EXPERIMENTAL_OCI=1 20 | helm registry login --username AWS --password="$(aws ecr-public get-login-password --region us-east-1)" "${ECR_REGISTRY}" -------------------------------------------------------------------------------- /scripts/install-amazon-ecr-credential-helper: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | usage=$(cat << EOM 6 | Download and install amazon-ecr-credential-helper for Docker client. 7 | usage: $(basename $0) [-h] VERSION 8 | Options: 9 | -h Print help message then exit 10 | Arguments: 11 | VERSION Version number of amazon-ecr-login-helper to download and install (e.g. 0.7.1) 12 | EOM 13 | ) 14 | 15 | function display_help { 16 | echo "${usage}" 1<&2 17 | } 18 | 19 | while getopts "h" arg; do 20 | case "${arg}" in 21 | h ) display_help 22 | exit 0 23 | ;; 24 | 25 | * ) display_help 26 | exit 1 27 | ;; 28 | esac 29 | done 30 | shift $((OPTIND-1)) 31 | 32 | version="${1:-}" 33 | if [[ -z "${version}" ]]; then 34 | echo "❌ no version given" 35 | display_help 36 | exit 1 37 | fi 38 | 39 | install_path="$(dirname "$(which docker-credential-wincred.exe)")" 40 | curl -Lo "${install_path}/docker-credential-ecr-login.exe" "https://amazon-ecr-credential-helper-releases.s3.us-east-2.amazonaws.com/${version}/windows-amd64/docker-credential-ecr-login.exe" 41 | 42 | # Update Docker to use ecr-login instead of wincred. 43 | modified_config="$(mktemp)" 44 | jq '.credsStore="ecr-login"' ~/.docker/config.json > "${modified_config}" 45 | mv -f "${modified_config}" ~/.docker/config.json -------------------------------------------------------------------------------- /scripts/push-docker-images: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" 5 | 6 | REPO_ROOT_PATH=$SCRIPTPATH/../ 7 | MAKE_FILE_PATH=$REPO_ROOT_PATH/Makefile 8 | 9 | VERSION=$(make -s -f $MAKE_FILE_PATH version) 10 | PLATFORMS=("linux/amd64") 11 | MANIFEST_IMAGES=() 12 | MANIFEST="" 13 | DOCKER_CLI_CONFIG="$HOME/.docker/config.json" 14 | 15 | USAGE=$(cat << 'EOM' 16 | Usage: push-docker-images [-p ] 17 | Pushes docker images for the platform pairs passed in w/ a container manifest 18 | Example: push-docker-images -p "linux/amd64,linux/arm" 19 | Optional: 20 | -p Platform pair list (os/architecture) [DEFAULT: linux/amd64] 21 | -r IMAGE REPO: set the docker image repo 22 | -v VERSION: The application version of the docker image [DEFAULT: output of `make version`] 23 | -m Create a docker manifest 24 | EOM 25 | ) 26 | 27 | # Process our input arguments 28 | while getopts "mp:r:v:" opt; do 29 | case ${opt} in 30 | p ) # Platform Pairs 31 | IFS=',' read -ra PLATFORMS <<< "$OPTARG" 32 | ;; 33 | r ) # Image Repo 34 | IMAGE_REPO="$OPTARG" 35 | ;; 36 | v ) # Image Version 37 | VERSION="$OPTARG" 38 | ;; 39 | m ) # Docker manifest 40 | MANIFEST="true" 41 | ;; 42 | \? ) 43 | echo "$USAGE" 1>&2 44 | exit 45 | ;; 46 | esac 47 | done 48 | 49 | if [[ ${#PLATFORMS[@]} -gt 1 && $MANIFEST != "true" ]]; then 50 | echo "Only one platform can be pushed if you do not create a manifest." 51 | echo "Try again with the -m option" 52 | exit 1 53 | fi 54 | 55 | # Existing manifests cannot be updated only overwritten; therefore, 56 | # if manifest exists already, fetch existing platforms so "updated" manifest includes images 57 | # that were there previously 58 | if [[ $MANIFEST == "true" ]]; then 59 | if [[ ! -f $DOCKER_CLI_CONFIG ]]; then 60 | echo '{"experimental":"enabled"}' > $DOCKER_CLI_CONFIG 61 | echo "Created docker config file" 62 | fi 63 | cat <<< "$(jq '.+{"experimental":"enabled"}' $DOCKER_CLI_CONFIG)" > $DOCKER_CLI_CONFIG 64 | echo "Enabled experimental CLI features to execute docker manifest commands" 65 | manifest_exists=$(docker manifest inspect $IMAGE_REPO:$VERSION > /dev/null ; echo $?) 66 | if [[ manifest_exists -eq 0 ]]; then 67 | echo "manifest already exists" 68 | EXISTING_IMAGES=() 69 | while IFS='' read -r line; do 70 | EXISTING_IMAGES+=("$line"); 71 | done < <(docker manifest inspect $IMAGE_REPO:$VERSION | jq -r '.manifests[] | "\(.platform.os)-\(.platform.architecture)"') 72 | # treat separate from PLATFORMS because existing images don't need to be tagged and pushed 73 | for os_arch in "${EXISTING_IMAGES[@]}"; do 74 | img_tag="$IMAGE_REPO:$VERSION-$os_arch" 75 | MANIFEST_IMAGES+=("$img_tag") 76 | done 77 | echo "images already in manifest: ${MANIFEST_IMAGES[*]}" 78 | fi 79 | fi 80 | 81 | for os_arch in "${PLATFORMS[@]}"; do 82 | os=$(echo $os_arch | cut -d'/' -f1) 83 | arch=$(echo $os_arch | cut -d'/' -f2) 84 | 85 | img_tag_w_platform="$IMAGE_REPO:$VERSION-$os-$arch" 86 | 87 | if [[ $MANIFEST == "true" ]]; then 88 | img_tag=$img_tag_w_platform 89 | else 90 | img_tag="$IMAGE_REPO:$VERSION" 91 | docker tag $img_tag_w_platform $img_tag 92 | fi 93 | docker push $img_tag 94 | MANIFEST_IMAGES+=("$img_tag") 95 | done 96 | 97 | if [[ $MANIFEST == "true" ]]; then 98 | current_os=$(uname) 99 | # Windows will append '\r' to the end of $img which 100 | # results in docker failing to create the manifest due to invalid reference format. 101 | # However, MacOS does not recognize '\r' as carriage return 102 | # and attempts to remove literal 'r' chars; therefore, made this so portable 103 | for img in "${MANIFEST_IMAGES[@]}"; do 104 | if [[ $current_os == "Darwin" ]]; then 105 | updated_img=$img 106 | else 107 | updated_img=$(echo $img | sed -e 's/\r$//') 108 | fi 109 | echo "creating manifest for $updated_img" 110 | docker manifest create $IMAGE_REPO:$VERSION $updated_img --amend 111 | 112 | os_arch=$(echo ${updated_img//$IMAGE_REPO:$VERSION-/}) 113 | os=$(echo $os_arch | cut -d'-' -f1) 114 | arch=$(echo $os_arch | cut -d'-' -f2) 115 | 116 | echo "annotating manifest" 117 | docker manifest annotate $IMAGE_REPO:$VERSION $updated_img --arch $arch --os $os 118 | done 119 | 120 | echo "pushing manifest" 121 | docker manifest push --purge $IMAGE_REPO:$VERSION 122 | fi -------------------------------------------------------------------------------- /scripts/push-helm-chart: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | SCRIPTPATH="$( cd "$(dirname "$0")";pwd -P )" 5 | 6 | REPO_ROOT_PATH=$SCRIPTPATH/../ 7 | MAKE_FILE_PATH=$REPO_ROOT_PATH/Makefile 8 | CHART_VERSION=$(make -s -f $MAKE_FILE_PATH chart-version) 9 | HELM_CHART_PATH=$REPO_ROOT_PATH/helm/amazon-ec2-metadata-mock 10 | 11 | USAGE=$(cat << 'EOM' 12 | Usage: push-helm-chart 13 | Pushes helm charts 14 | Optional: 15 | -h HELM CHART REGISTRY: set the helm chart registry 16 | -v CHART VERSION: The chart version [DEFAULT: output of `make chart-version`] 17 | -r HELM CHART REPOSITORY: Set the helm chart repository 18 | EOM 19 | ) 20 | 21 | # Process our input arguments 22 | while getopts "r:v:h:" opt; do 23 | case ${opt} in 24 | r ) # Helm Chart Repository 25 | HELM_CHART_REPOSITORY="$OPTARG" 26 | ;; 27 | v ) # Image Version 28 | CHART_VERSION="$OPTARG" 29 | ;; 30 | h ) # Helm Chart Registry 31 | ECR_REGISTRY="$OPTARG" 32 | ;; 33 | \? ) 34 | echo "$USAGE" 1>&2 35 | exit 36 | ;; 37 | esac 38 | done 39 | 40 | CHART_EXISTS=$(aws ecr-public describe-images --repository-name "helm/$HELM_CHART_REPOSITORY" --region us-east-1 --query "imageDetails[?contains(imageTags, '$CHART_VERSION')].imageTags[]" --output text) 41 | 42 | if [[ -n "$CHART_EXISTS" ]]; then 43 | echo "chart with version $CHART_VERSION already exists in the repository, skipping pushing of chart..." 44 | exit 0 45 | fi 46 | 47 | echo "chart with version $CHART_VERSION not found in repository, pushing new chart..." 48 | #Package the chart 49 | helm package $HELM_CHART_PATH --destination $REPO_ROOT_PATH/build 50 | #Pushing helm chart 51 | helm push $REPO_ROOT_PATH/build/$HELM_CHART_REPOSITORY-$CHART_VERSION.tgz oci://$ECR_REGISTRY/helm -------------------------------------------------------------------------------- /scripts/retag-docker-images: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" 5 | 6 | REPO_ROOT_PATH=$SCRIPTPATH/../ 7 | MAKE_FILE_PATH=$REPO_ROOT_PATH/Makefile 8 | 9 | VERSION=$(make -s -f $MAKE_FILE_PATH version) 10 | PLATFORMS=("linux/amd64") 11 | 12 | 13 | USAGE=$(cat << 'EOM' 14 | Usage: retag-docker-images [-p ] 15 | Tags created docker images with a new prefix 16 | 17 | Example: retag-docker-images -p "linux/amd64,linux/arm" -o -n 18 | Optional: 19 | -p Platform pair list (os/architecture) [DEFAULT: linux/amd64] 20 | -o OLD IMAGE REPO to retag 21 | -n NEW IMAGE REPO to tag with 22 | -v VERSION: The application version of the docker image [DEFAULT: output of `make version`] 23 | EOM 24 | ) 25 | 26 | # Process our input arguments 27 | while getopts "p:o:n:v:" opt; do 28 | case ${opt} in 29 | p ) # Platform Pairs 30 | IFS=',' read -ra PLATFORMS <<< "$OPTARG" 31 | ;; 32 | o ) # Old Image Repo 33 | OLD_IMAGE_REPO="$OPTARG" 34 | ;; 35 | n ) # New Image Repo 36 | NEW_IMAGE_REPO="$OPTARG" 37 | ;; 38 | v ) # Image Version 39 | VERSION="$OPTARG" 40 | ;; 41 | \? ) 42 | echo "$USAGE" 1>&2 43 | exit 44 | ;; 45 | esac 46 | done 47 | 48 | function exit_and_fail() { 49 | echo "❌ Failed retagging docker images" 50 | } 51 | 52 | trap "exit_and_fail" INT TERM ERR 53 | 54 | for os_arch in "${PLATFORMS[@]}"; do 55 | os=$(echo $os_arch | cut -d'/' -f1) 56 | arch=$(echo $os_arch | cut -d'/' -f2) 57 | 58 | old_img_tag="$OLD_IMAGE_REPO:$VERSION-$os-$arch" 59 | new_img_tag="$NEW_IMAGE_REPO:$VERSION-$os-$arch" 60 | 61 | current_os=$(uname) 62 | # Windows will append '\r' to the end of $img which 63 | # results in docker failing to create the manifest due to invalid reference format. 64 | # However, MacOS does not recognize '\r' as carriage return 65 | # and attempts to remove literal 'r' chars; therefore, made this so portable 66 | if [[ $current_os != "Darwin" ]]; then 67 | old_img_tag=$(echo $old_img_tag | sed -e 's/\r//') 68 | new_img_tag=$(echo $new_img_tag | sed -e 's/\r//') 69 | fi 70 | 71 | docker tag ${old_img_tag} ${new_img_tag} 72 | echo "✅ Successfully retagged docker image $old_img_tag to $new_img_tag" 73 | done 74 | 75 | echo "✅ Done Retagging!" -------------------------------------------------------------------------------- /scripts/sync-catalog-information-for-helm-chart: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" 5 | REPO_NAME="helm/amazon-ec2-metadata-mock" 6 | REPO_ROOT_PATH=$SCRIPTPATH/../ 7 | TEMPLATE_PATH=$REPO_ROOT_PATH/scripts/ecr-template-for-helm-chart.json 8 | CATALOG_DATA=$(cat "$TEMPLATE_PATH") 9 | 10 | if aws ecr-public describe-repositories --region us-east-1 --repository-names "$REPO_NAME" > /dev/null 2>&1; then 11 | echo "The repository $REPO_NAME exists, update it with template..." 12 | aws ecr-public put-repository-catalog-data --region us-east-1 --repository-name "$REPO_NAME" --catalog-data "$CATALOG_DATA" 13 | else 14 | echo "The repository $REPO_NAME does not exist, create it with template..." 15 | aws ecr-public create-repository --region us-east-1 --repository-name "$REPO_NAME" --catalog-data "$CATALOG_DATA" 16 | fi -------------------------------------------------------------------------------- /scripts/sync-readme-to-ecr-public: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" 5 | REPO_NAME="amazon-ec2-metadata-mock" 6 | #about and usage section char max 7 | MAX_CHAR_COUNT=10240 8 | USAGE_TEXT="See About section" 9 | ADDITIONAL_MSG="... 10 | 11 | **truncated due to char limits**... 12 | A complete version of the ReadMe can be found [here](https://github.com/aws/amazon-ec2-metadata-mock#amazon-ec2-metadata-mock)\"" 13 | 14 | # converting to json to insert esc chars, then replace newlines for proper markdown render 15 | raw_about=$(jq -n --arg msg "$(<$SCRIPTPATH/../README.md)" '{"usageText": $msg}' | jq '.usageText' | sed 's/\\n/\ 16 | /g') 17 | char_to_trunc="$(($MAX_CHAR_COUNT-${#ADDITIONAL_MSG}))" 18 | raw_truncated="${raw_about:0:$char_to_trunc}" 19 | raw_truncated+="$ADDITIONAL_MSG" 20 | aws ecr-public put-repository-catalog-data --repository-name="${REPO_NAME}" --catalog-data aboutText="${raw_truncated}",usageText="${USAGE_TEXT}" --region us-east-1 21 | 22 | echo "README sync to ecr-public succeeded!" -------------------------------------------------------------------------------- /scripts/update-versions-for-release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to update AEMM and Helm chart versions. 4 | # The following files are updated: 5 | ## - helm charts' Chart.yaml 6 | ## - helm charts' values.yaml 7 | ## - helm charts' README 8 | ## - README.md 9 | ## - version.txt 10 | 11 | # This script is run AFTER creating a new local tag (see `make release-prep`). 12 | 13 | set -euo pipefail 14 | 15 | REPO_ROOT_PATH="$( cd "$(dirname "$0")"; cd ../; pwd -P )" 16 | MAKEFILE_PATH=$REPO_ROOT_PATH/Makefile 17 | LATEST_VERSION=$(make -s -f $MAKEFILE_PATH latest-release-tag | cut -b 2- ) 18 | PREVIOUS_VERSION=$(make -s -f $MAKEFILE_PATH previous-release-tag | cut -b 2- ) 19 | 20 | # Files to update 21 | REPO_README=$REPO_ROOT_PATH/README.md 22 | APP_VERSION=$REPO_ROOT_PATH/pkg/cmd/root/version.txt 23 | CHART=$REPO_ROOT_PATH/helm/amazon-ec2-metadata-mock/Chart.yaml 24 | CHART_VALUES=$REPO_ROOT_PATH/helm/amazon-ec2-metadata-mock/values.yaml 25 | CHART_README=$REPO_ROOT_PATH/helm/amazon-ec2-metadata-mock/README.md 26 | 27 | FILES=("$REPO_README" "$CHART" "$CHART_README" "$CHART_VALUES" "$APP_VERSION") 28 | FILES_CHANGED=() 29 | 30 | echo -e "🥑 Attempting to update AEMM release version and Helm chart version in preparation for a new release.\n Previous version: $PREVIOUS_VERSION ---> Latest version: $LATEST_VERSION" 31 | 32 | for f in "${FILES[@]}"; do 33 | has_incorrect_version=$(cat $f | grep $PREVIOUS_VERSION) 34 | if [[ ! -z $has_incorrect_version ]]; then 35 | sed -i '' "s/$PREVIOUS_VERSION/$LATEST_VERSION/g" $f 36 | FILES_CHANGED+=("$f") 37 | fi 38 | done 39 | 40 | if [[ ${#FILES_CHANGED[@]} -gt 0 ]]; then 41 | echo -e "\n✅ Updated versions from $PREVIOUS_VERSION to $LATEST_VERSION in files: \n$(echo "${FILES_CHANGED[@]}" | tr ' ' '\n')" 42 | echo -e "\n🥑 To see changes, run \`git diff ${FILES_CHANGED[*]}\`" 43 | else 44 | echo -e "\n✅ All files already use the latest release version $LATEST_VERSION. No files were modified." 45 | fi -------------------------------------------------------------------------------- /scripts/upload-resources-to-github: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Script to upload release assets to Github. 5 | # This script cleans up after itself in cases of parital failures. i.e. either all assets are uploaded or none 6 | 7 | SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" 8 | VERSION=$(make -s -f $SCRIPTPATH/../Makefile version) 9 | BUILD_DIR=$SCRIPTPATH/../build/k8s-resources/$VERSION 10 | BINARY_DIR=$SCRIPTPATH/../build/bin 11 | INDV_K8S_RESOURCES=$BUILD_DIR/individual-resources.tar 12 | AGG_K8S_RESOURCES=$BUILD_DIR/all-resources.yaml 13 | HELM_ARCHIVES_DIR=$BUILD_DIR/helm-chart-archives 14 | 15 | RELEASE_ID=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ 16 | https://api.github.com/repos/aws/amazon-ec2-metadata-mock/releases | \ 17 | jq --arg VERSION "$VERSION" '.[] | select(.tag_name==$VERSION) | .id') 18 | 19 | [[ -z $TERM ]] || export TERM=linux 20 | RED=$(tput setaf 1) 21 | RESET_FMT=$(tput sgr 0) 22 | 23 | ASSET_IDS_UPLOADED=() 24 | 25 | trap 'handle_errors_and_cleanup $?' EXIT 26 | 27 | handle_errors_and_cleanup() { 28 | if [ $1 -eq "0" ]; then 29 | exit 0 30 | fi 31 | 32 | if [[ ${#ASSET_IDS_UPLOADED[@]} -ne 0 ]]; then 33 | echo -e "\nCleaning up assets uploaded in the current execution of the script" 34 | for asset_id in "${ASSET_IDS_UPLOADED[@]}"; do 35 | echo "Deleting asset $asset_id" 36 | curl -X DELETE \ 37 | -H "Authorization: token $GITHUB_TOKEN" \ 38 | "https://api.github.com/repos/aws/amazon-ec2-metadata-mock/releases/assets/$asset_id" 39 | done 40 | exit $1 41 | fi 42 | } 43 | 44 | gather_assets_to_upload() { 45 | local resources=("$INDV_K8S_RESOURCES" "$AGG_K8S_RESOURCES") 46 | for archive in $HELM_ARCHIVES_DIR/*; do 47 | resources+=("$archive") 48 | done 49 | 50 | for binary in $BINARY_DIR/*; do 51 | resources+=("$binary") 52 | done 53 | echo "${resources[@]}" 54 | } 55 | 56 | # $1: absolute path to asset 57 | upload_asset() { 58 | resp=$(curl --write-out '%{http_code}' --silent \ 59 | -H "Authorization: token $GITHUB_TOKEN" \ 60 | -H "Content-Type: $(file -b --mime-type $1)" \ 61 | --data-binary @$1 \ 62 | "https://uploads.github.com/repos/aws/amazon-ec2-metadata-mock/releases/$RELEASE_ID/assets?name=$(basename $1)") 63 | 64 | response_code=$(echo $resp | sed 's/\(.*\)}//') 65 | response_content=$(echo $resp | sed "s/$response_code//") 66 | 67 | # HTTP success code expected - 201 Created 68 | if [[ $response_code -eq 201 ]]; then 69 | asset_id=$(echo $response_content | jq '.id') 70 | ASSET_IDS_UPLOADED+=("$asset_id") 71 | echo "Created asset ID $asset_id successfully" 72 | else 73 | echo -e "❌ ${RED}Upload failed with response code $response_code and message \n$response_content${RESET_FMT} ❌" 74 | exit 1 75 | fi 76 | } 77 | 78 | ASSETS=$(gather_assets_to_upload) 79 | COUNT=1 80 | echo -e "\nUploading release assets for release id '$RELEASE_ID' to Github" 81 | for asset in $ASSETS; do 82 | name=$(echo $asset | tr '/' '\n' | tail -1) 83 | echo -e "\n $((COUNT++)). $name" 84 | upload_asset $asset 85 | done -------------------------------------------------------------------------------- /scripts/validators/json-validator: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" 6 | # Find all JSON files but ignore any that are also ignored by git (e.g. IDE settings files). 7 | JSON_FILES=$(find \ 8 | $SCRIPTPATH/../../ \ 9 | -name "*.json" \ 10 | -type f \ 11 | -not -exec git check-ignore --quiet {} \; \ 12 | -print) 13 | FAILED_FILES=() 14 | 15 | if [[ -z `which python` ]] && [[ ! -z `which python3` ]]; then 16 | PY=python3 17 | else 18 | PY=python 19 | fi 20 | 21 | for j in $JSON_FILES; do 22 | echo "validating $j" 23 | $PY -mjson.tool "$j" > /dev/null || FAILED_FILES+=("$j") 24 | done 25 | 26 | if [[ ${#FAILED_FILES[@]} -eq 0 ]]; then 27 | echo "✅ json-validator found no errors!" 28 | else 29 | echo "❌ JSON syntax errors found in these files: ${FAILED_FILES[*]}" 30 | exit 1 31 | fi 32 | -------------------------------------------------------------------------------- /scripts/validators/release-version-validator: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to verify that the versions are updated to reflect the latest release tag in: 4 | ## - helm charts' Chart.yaml 5 | ## - helm charts' values.yaml 6 | ## - helm charts' README 7 | ## - README.md 8 | ## - version.txt 9 | 10 | set -euo pipefail 11 | 12 | REPO_ROOT_PATH="$( cd "$(dirname "$0")"; cd ../../; pwd -P )" 13 | MAKEFILE_PATH=$REPO_ROOT_PATH/Makefile 14 | LATEST_VERSION=$(make -s -f $MAKEFILE_PATH latest-release-tag | cut -b 2- ) 15 | PREVIOUS_VERSION=$(make -s -f $MAKEFILE_PATH previous-release-tag | cut -b 2- ) 16 | UPDATE_NEEDED=false 17 | 18 | print_note() { 19 | if [ $UPDATE_NEEDED == true ]; then 20 | echo "NOTE: Use \`make update-versions-for-release\` to update AEMM and Helm chart versions in preparation for a release." 21 | else 22 | echo "✅ Version ($LATEST_VERSION) was successfully verified in README and Helm chart files ✅" 23 | fi 24 | exit 0 25 | } 26 | 27 | trap 'print_note' EXIT 28 | 29 | # Verify version in README 30 | README_HAS_INCORRECT_VERSION=$(cat $REPO_ROOT_PATH/README.md | grep $PREVIOUS_VERSION) 31 | if [[ ! -z $README_HAS_INCORRECT_VERSION ]]; then 32 | echo "❌ Please update AEMM version in README.md. Expected version v$LATEST_VERSION, but got v$PREVIOUS_VERSION ❌" 33 | UPDATE_NEEDED=true 34 | fi 35 | 36 | # Verify version in version.txt 37 | APP_HAS_INCORRECT_VERSION=$(cat $REPO_ROOT_PATH/pkg/cmd/root/version.txt | grep $PREVIOUS_VERSION) 38 | if [[ ! -z $APP_HAS_INCORRECT_VERSION ]]; then 39 | echo "❌ Please update AEMM version in pkg/cmd/root/version.txt. Expected version v$LATEST_VERSION, but got v$PREVIOUS_VERSION ❌" 40 | UPDATE_NEEDED=true 41 | fi 42 | 43 | # Verify versions in Helm charts 44 | HELM_CHART_PATH=$REPO_ROOT_PATH/helm 45 | for c in $HELM_CHART_PATH/*; do 46 | chart_name=$(echo $c | tr '/' '\n' | tail -1) 47 | if [ -d $c ]; then 48 | 49 | # verify version in values.yaml 50 | values_image_tag=$(sed -n 's/[^\s]tag: //p' $c/values.yaml | tr -d '""' | tr -d ' ') 51 | if [[ $values_image_tag != "$LATEST_VERSION" ]]; then 52 | echo "❌ Please update AEMM version in $chart_name helm chart's values.yaml, image.tag field. Expected version $LATEST_VERSION, but got $values_image_tag ❌" 53 | UPDATE_NEEDED=true 54 | fi 55 | 56 | # verify version in Chart.yaml 57 | chart_version=$(sed -n 's/version: //p' $c/Chart.yaml) 58 | if [[ $chart_version != "$LATEST_VERSION" ]]; then 59 | echo "❌ Please update the chart's version in $chart_name chart's Chart.yaml. Expected version $LATEST_VERSION, but got $chart_version ❌" 60 | UPDATE_NEEDED=true 61 | fi 62 | 63 | # verify version in the chart's README.md 64 | chart_readme_has_incorrect_version=$(cat $c/README.md | grep $PREVIOUS_VERSION) 65 | if [[ ! -z $chart_readme_has_incorrect_version ]]; then 66 | echo "❌ Please update the chart's version in $chart_name chart's README.md. Expected version $LATEST_VERSION, but got $PREVIOUS_VERSION ❌" 67 | UPDATE_NEEDED=true 68 | fi 69 | fi 70 | done -------------------------------------------------------------------------------- /templates/third-party-licenses.tmpl: -------------------------------------------------------------------------------- 1 | # Third-party Licenses 2 | 3 | {{ range . -}} 4 | - {{ .Name }} {{ .Version }} [{{ .LicenseName }}]({{ .LicenseURL }}) 5 | {{ end -}} -------------------------------------------------------------------------------- /test/e2e/cmd/asglifecycle-test: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | LIFECYCLE_STATE_TEST_PATH="http://$HOSTNAME:$AEMM_PORT/latest/meta-data/autoscaling/target-lifecycle-state" 6 | TERMINATION_DELAY=10 7 | 8 | function test_asglifecycle_paths() { 9 | pid=$1 10 | tput setaf $MAGENTA 11 | health_check $LIFECYCLE_STATE_TEST_PATH 12 | TOKEN=$(get_v2Token $MAX_TOKEN_TTL $AEMM_PORT) 13 | 14 | actual_paths=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://$HOSTNAME:$AEMM_PORT/latest/meta-data) 15 | expected_paths=$(cat $SCRIPTPATH/golden/asglifecycle/latest/meta-data/index.golden) 16 | 17 | assert_value "$actual_paths" "$expected_paths" "test_asglifecycle_paths" 18 | 19 | clean_up $pid 20 | } 21 | 22 | function test_asg_termination_delay() { 23 | pid=$1 24 | delay_in_sec=$2 25 | tput setaf $MAGENTA 26 | health_check $LIFECYCLE_STATE_TEST_PATH 27 | TOKEN=$(get_v2Token $MAX_TOKEN_TTL $AEMM_PORT) 28 | 29 | expected_state="InService" 30 | response=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $LIFECYCLE_STATE_TEST_PATH) 31 | assert_value "$response" "$expected_state" "test_asglifecycle::asg-pre-delay_response" 32 | 33 | # ensure no impact to asg termination delay functionality 34 | echo "⏲ Waiting on delay ⏲" 35 | sleep $delay_in_sec 36 | expected_state="Terminated" 37 | response=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $LIFECYCLE_STATE_TEST_PATH) 38 | assert_value "$response" "$expected_state" "test_asglifecycle::asg-post-delay_response" 39 | 40 | # Confirm response isn't empty 41 | if [[ ! -z $response ]]; then 42 | echo "✅ Verified post-delay response" 43 | else 44 | echo "❌ Failed delay: there should be a lifecycle change to Terminated after delay" 45 | EXIT_CODE_TO_RETURN=1 46 | fi 47 | 48 | clean_up $pid 49 | } 50 | 51 | tput setaf $YELLOW 52 | echo "======================================================================================================" 53 | echo "🥑 Starting asg lifecycle integration tests $METADATA_VERSION" 54 | echo "======================================================================================================" 55 | 56 | start_cmd=$(create_cmd $METADATA_VERSION asglifecycle --port $AEMM_PORT) 57 | $start_cmd & 58 | ASG_PID=$! 59 | test_asglifecycle_paths $ASG_PID 60 | 61 | start_cmd=$(create_cmd $METADATA_VERSION asglifecycle --port $AEMM_PORT --asg-termination-delay-sec $TERMINATION_DELAY) 62 | $start_cmd & 63 | ASG_PID=$! 64 | test_asg_termination_delay $ASG_PID $TERMINATION_DELAY 65 | 66 | exit $EXIT_CODE_TO_RETURN 67 | 68 | -------------------------------------------------------------------------------- /test/e2e/cmd/dynamic-test: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | TEST_CONFIG_FILE="$SCRIPTPATH/testdata/aemm-config-integ.json" 6 | DYNAMIC_TEST_PATH="http://$HOSTNAME:$AEMM_PORT/latest/dynamic" 7 | DYNAMIC_IDC_TEST_PATH="$DYNAMIC_TEST_PATH/instance-identity/document" 8 | 9 | DYNAMIC_IDC_ACCOUNT_ID_DEFAULT="0123456789" 10 | DYNAMIC_IDC_ACCOUNT_ID_OVERRIDDEN="9876543210" 11 | DYNAMIC_IDC_INSTANCE_ID_DEFAULT="i-1234567890abcdef0" 12 | DYNAMIC_IDC_INSTANCE_ID_OVERRIDDEN="i-0fedcba0987654321" 13 | 14 | function test_dynamic_paths() { 15 | pid=$1 16 | tput setaf $YELLOW 17 | health_check $DYNAMIC_TEST_PATH 18 | TOKEN=$(get_v2Token $MAX_TOKEN_TTL $AEMM_PORT) 19 | 20 | actual_paths=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $DYNAMIC_TEST_PATH) 21 | expected_paths=$(cat $SCRIPTPATH/golden/dynamic/index.golden) 22 | 23 | assert_value "$actual_paths" "$expected_paths" "test_dynamic_paths" 24 | 25 | clean_up $pid 26 | } 27 | 28 | function test_dynamic_subpath_fws() { 29 | pid=$1 30 | tput setaf $YELLOW 31 | health_check $DYNAMIC_TEST_PATH 32 | TOKEN=$(get_v2Token $MAX_TOKEN_TTL $AEMM_PORT) 33 | 34 | actual_paths=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $DYNAMIC_TEST_PATH/fws) 35 | expected_paths=$(cat $SCRIPTPATH/golden/dynamic/fws.golden) 36 | 37 | assert_value "$actual_paths" "$expected_paths" "test_dynamic_subpath::/latest/dynamic/fws" 38 | 39 | clean_up $pid 40 | } 41 | 42 | 43 | function test_dynamic_subpath_instance-identity() { 44 | pid=$1 45 | tput setaf $YELLOW 46 | health_check $DYNAMIC_TEST_PATH 47 | TOKEN=$(get_v2Token $MAX_TOKEN_TTL $AEMM_PORT) 48 | 49 | actual_paths=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $DYNAMIC_TEST_PATH/instance-identity) 50 | expected_paths=$(cat $SCRIPTPATH/golden/dynamic/instance-identity.golden) 51 | 52 | assert_value "$actual_paths" "$expected_paths" "test_dynamic_subpath::/latest/dynamic/instance-identity" 53 | 54 | clean_up $pid 55 | } 56 | 57 | function test_dynamic_idc_defaults() { 58 | pid=$1 59 | tput setaf $YELLOW 60 | health_check $DYNAMIC_TEST_PATH 61 | TOKEN=$(get_v2Token $MAX_TOKEN_TTL $AEMM_PORT) 62 | 63 | response=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $DYNAMIC_IDC_TEST_PATH) 64 | actual_account_id=$(get_value '"accountId"' "$response") 65 | actual_instance_id=$(get_value '"instanceId"' "$response") 66 | 67 | assert_value "$actual_account_id" $DYNAMIC_IDC_ACCOUNT_ID_DEFAULT 'Default dynamic_idc::accountId val' 68 | assert_value "$actual_instance_id" $DYNAMIC_IDC_INSTANCE_ID_DEFAULT 'Default dynamic_idc::instanceId val' 69 | 70 | clean_up $pid 71 | } 72 | 73 | function test_dynamic_idc_overrides() { 74 | pid=$1 75 | expected_account_id=$2 76 | expected_instance_id=$3 77 | expected_mp_codes=[\""4i20ezfza3p7xx2kt2g8weu2u\""] 78 | tput setaf $YELLOW 79 | health_check $DYNAMIC_TEST_PATH 80 | TOKEN=$(get_v2Token $MAX_TOKEN_TTL $AEMM_PORT) 81 | 82 | response=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $DYNAMIC_IDC_TEST_PATH) 83 | actual_account_id=$(get_value '"accountId"' "$response") 84 | actual_instance_id=$(get_value '"instanceId"' "$response") 85 | # use '--compact-output' to return the value on a single line (i.e. do not pretty print) 86 | actual_marketplace_codes=$(echo "$response" | jq -c '.marketplaceProductCodes') 87 | 88 | assert_value "$actual_account_id" $expected_account_id 'Override dynamic_idc::accountId' 89 | assert_value "$actual_instance_id" $expected_instance_id 'Override dynamic_idc::instanceId' 90 | assert_value "$actual_marketplace_codes" "$expected_mp_codes" 'Override dynamic_idc::marketplaceProductCodes' 91 | 92 | clean_up $pid 93 | } 94 | 95 | tput setaf $YELLOW 96 | echo "======================================================================================================" 97 | echo "🥑 Starting dynamic metadata integration tests $METADATA_VERSION" 98 | echo "======================================================================================================" 99 | 100 | start_cmd=$(create_cmd $METADATA_VERSION --port $AEMM_PORT) 101 | $start_cmd & 102 | DYNAMIC_PID=$! 103 | test_dynamic_paths $DYNAMIC_PID 104 | 105 | start_cmd=$(create_cmd $METADATA_VERSION --port $AEMM_PORT) 106 | $start_cmd & 107 | DYNAMIC_PID=$! 108 | test_dynamic_subpath_fws $DYNAMIC_PID 109 | 110 | start_cmd=$(create_cmd $METADATA_VERSION --port $AEMM_PORT) 111 | $start_cmd & 112 | DYNAMIC_PID=$! 113 | test_dynamic_subpath_instance-identity $DYNAMIC_PID 114 | 115 | $start_cmd & 116 | DYNAMIC_PID=$! 117 | test_dynamic_idc_defaults $DYNAMIC_PID 118 | 119 | # dynamic data overrides 120 | start_cmd=$(create_cmd $METADATA_VERSION --port $AEMM_PORT -c $TEST_CONFIG_FILE) 121 | $start_cmd & 122 | DYNAMIC_PID=$! 123 | test_dynamic_idc_overrides $DYNAMIC_PID $DYNAMIC_IDC_ACCOUNT_ID_OVERRIDDEN $DYNAMIC_IDC_INSTANCE_ID_OVERRIDDEN 124 | 125 | exit $EXIT_CODE_TO_RETURN 126 | -------------------------------------------------------------------------------- /test/e2e/cmd/handlers-test: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | SPOT_TEST_PATH="http://$HOSTNAME:$AEMM_PORT/latest/meta-data/spot" 6 | SPOT_TEST_PATH_TRAILING="http://$HOSTNAME:$AEMM_PORT/latest/meta-data/spot/" 7 | SPOT_IA_TEST_PATH_TRAILING="http://$HOSTNAME:$AEMM_PORT/latest/meta-data/spot/instance-action/" 8 | SPOT_TT_TEST_PATH_TRAILING="http://$HOSTNAME:$AEMM_PORT/latest/meta-data/spot/termination-time/" 9 | 10 | function test_subpaths() { 11 | pid=$1 12 | tput setaf $LAVENDER 13 | health_check $SPOT_TEST_PATH 14 | TOKEN=$(get_v2Token $MAX_TOKEN_TTL $AEMM_PORT) 15 | 16 | actual_subpaths=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $SPOT_TEST_PATH) 17 | expected_subpaths=$(cat $SCRIPTPATH/golden/spot/latest/meta-data/spot.golden) 18 | 19 | assert_value "$actual_subpaths" "$expected_subpaths" "test_spot_subpaths" 20 | 21 | clean_up $pid 22 | } 23 | 24 | function test_trailing_slash() { 25 | pid=$1 26 | tput setaf $LAVENDER 27 | health_check $SPOT_TEST_PATH 28 | TOKEN=$(get_v2Token $MAX_TOKEN_TTL $AEMM_PORT) 29 | 30 | actual_subpaths=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $SPOT_TEST_PATH_TRAILING) 31 | expected_subpaths=$(cat $SCRIPTPATH/golden/spot/latest/meta-data/spot.golden) 32 | assert_value "$actual_subpaths" "$expected_subpaths" "test_spot_subpaths_trailing" 33 | 34 | 35 | response=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $SPOT_IA_TEST_PATH_TRAILING) 36 | actual_inst_action=$(get_value '"action"' "$response") 37 | actual_ia_time=$(get_value '"time"' "$response") 38 | 39 | assert_value "$actual_inst_action" $SPOT_INSTANCE_ACTION_DEFAULT 'test_spot_trailing::instance_action' 40 | assert_format "$actual_ia_time" $SPOT_DATE_REGEX 'test_spot_trailing::tt_format' 41 | 42 | actual_term_time=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $SPOT_TT_TEST_PATH_TRAILING) 43 | actual_term_time_sec=$(convert_RFC3339_to_sec $actual_term_time) 44 | actual_ia_time_sec=$(convert_RFC3339_to_sec $actual_ia_time) 45 | 46 | # times should be within 5 second range 47 | assert_value_within_range $actual_term_time_sec $actual_ia_time_sec 5 48 | 49 | clean_up $pid 50 | } 51 | 52 | tput setaf $LAVENDER 53 | echo "======================================================================================================" 54 | echo "🥑 Starting handlers integration tests $METADATA_VERSION" 55 | echo "======================================================================================================" 56 | 57 | start_cmd=$(create_cmd $METADATA_VERSION --port $AEMM_PORT) 58 | $start_cmd & 59 | HANDLERS_PID=$! 60 | test_subpaths $HANDLERS_PID 61 | 62 | start_cmd=$(create_cmd $METADATA_VERSION --port $AEMM_PORT) 63 | $start_cmd & 64 | HANDLERS_PID=$! 65 | test_trailing_slash $HANDLERS_PID -------------------------------------------------------------------------------- /test/e2e/cmd/imdsv2-test: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | function test_imdsv2() { 6 | pid=$1 7 | expected_value=$(cat $2) 8 | token_TTL=$3 9 | test_name=$4 10 | tput setaf $ORANGE 11 | health_check "$HOSTNAME:$AEMM_PORT" 12 | 13 | TOKEN=$(get_v2Token $token_TTL $AEMM_PORT) 14 | sleep 1 15 | actual_value=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" "$HOSTNAME:$AEMM_PORT/latest/meta-data") 16 | 17 | assert_value "$actual_value" "$expected_value" "$test_name" 18 | 19 | clean_up $pid 20 | } 21 | 22 | # run these tests once 23 | if [[ "$METADATA_VERSION" == "v2" ]]; then 24 | tput setaf $ORANGE 25 | echo "======================================================================================================" 26 | echo "🥑 Starting imdsv2 integration tests." 27 | echo "======================================================================================================" 28 | 29 | $SCRIPTPATH/../../build/$BIN --imdsv2 --port $AEMM_PORT & 30 | IMDSV2_PID=$! 31 | test_imdsv2 $IMDSV2_PID "$SCRIPTPATH/golden/default/latest/meta-data/index.golden" "21600" "imdsv2::valid_token" 32 | 33 | $SCRIPTPATH/../../build/$BIN --imdsv2 --port $AEMM_PORT & 34 | IMDSV2_PID=$! 35 | test_imdsv2 $IMDSV2_PID "$SCRIPTPATH/golden/400_bad_request.golden" "0" "imdsv2::invalid_token" 36 | 37 | $SCRIPTPATH/../../build/$BIN --imdsv2 --port $AEMM_PORT & 38 | IMDSV2_PID=$! 39 | test_imdsv2 $IMDSV2_PID "$SCRIPTPATH/golden/401_response.golden" "1" "imdsv2::expired_token" 40 | fi 41 | 42 | exit $EXIT_CODE_TO_RETURN 43 | -------------------------------------------------------------------------------- /test/e2e/cmd/root-test: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | ROOT_TEST_PATH="http://$HOSTNAME:$AEMM_PORT" 6 | DELAY_IN_SEC=3 7 | 8 | function test_root() { 9 | pid=$1 10 | query_path=$2 11 | golden_file=$3 12 | test_name=$4 13 | 14 | tput setaf $CYAN 15 | health_check $ROOT_TEST_PATH 16 | 17 | TOKEN=$(get_v2Token $MAX_TOKEN_TTL $AEMM_PORT) 18 | actual_paths=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $query_path) 19 | expected_paths=$(cat $golden_file) 20 | 21 | assert_value "$actual_paths" "$expected_paths" "test_root::$test_name" 22 | 23 | clean_up $pid 24 | } 25 | 26 | function test_root_delay() { 27 | pid=$1 28 | delay_in_sec=$2 29 | tput setaf $CYAN 30 | 31 | # Give server time to startup 32 | sleep 1 33 | expected_delayed_response=$(cat $SCRIPTPATH/golden/404_response.golden) 34 | 35 | # Subcommand paths are delayed 36 | TOKEN=$(get_v2Token $MAX_TOKEN_TTL $AEMM_PORT) 37 | actual_response=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $ROOT_TEST_PATH/latest/meta-data/spot/instance-action || :) 38 | assert_value "$actual_response" "$expected_delayed_response" "test_root_delay::pre-delay_response" 39 | 40 | # Send request after delay duration 41 | echo "⏲ Waiting on delay ⏲" 42 | sleep $delay_in_sec 43 | actual_response=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $ROOT_TEST_PATH/latest/meta-data/spot/instance-action || :) 44 | assert_not_equal "$actual_response" "$expected_delayed_response" "test_root_delay::post-delay_response" 45 | 46 | # Confirm response isn't empty 47 | if [[ ! -z $actual_response ]]; then 48 | echo "✅ Verified post-delay response" 49 | else 50 | echo "❌ Failed delay: there should be a response after delay" 51 | EXIT_CODE_TO_RETURN=1 52 | fi 53 | 54 | clean_up $pid 55 | } 56 | 57 | function test_root_mock_trigger_time() { 58 | pid=$1 59 | delay_in_sec=$2 60 | mock_trigger_time=$3 61 | tput setaf $CYAN 62 | 63 | # Give server time to startup 64 | sleep 1 65 | expected_delayed_response=$(cat $SCRIPTPATH/golden/404_response.golden) 66 | 67 | # Subcommand paths are delayed 68 | TOKEN=$(get_v2Token $MAX_TOKEN_TTL $AEMM_PORT) 69 | actual_response=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $ROOT_TEST_PATH/latest/meta-data/spot/instance-action || :) 70 | assert_value "$actual_response" "$expected_delayed_response" "test_root_mock_trigger_time::pre-trigger_response" 71 | 72 | # Send request after mock_trigger_time 73 | echo "⏲ Waiting on mock_trigger_time ⏲" 74 | sleep $delay_in_sec 75 | actual_response=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $ROOT_TEST_PATH/latest/meta-data/spot/instance-action || :) 76 | assert_not_equal "$actual_response" "$expected_delayed_response" "test_root_mock_trigger_time::post-trigger_response" 77 | 78 | # Confirm response isn't empty 79 | if [[ ! -z $actual_response ]]; then 80 | echo "✅ Verified post-mock_trigger_time response" 81 | else 82 | echo "❌ Failed delay: there should be a response after mock_trigger_time" 83 | EXIT_CODE_TO_RETURN=1 84 | fi 85 | 86 | clean_up $pid 87 | } 88 | 89 | tput setaf $CYAN 90 | echo "======================================================================================================" 91 | echo "🥑 Starting root integration tests $METADATA_VERSION" 92 | echo "======================================================================================================" 93 | 94 | start_cmd=$(create_cmd $METADATA_VERSION --port $AEMM_PORT) 95 | $start_cmd & 96 | ROOT_PID=$! 97 | test_root $ROOT_PID "$ROOT_TEST_PATH/latest/meta-data" "$SCRIPTPATH/golden/default/latest/meta-data/index.golden" "paths" 98 | 99 | start_cmd=$(create_cmd $METADATA_VERSION --port $AEMM_PORT) 100 | $start_cmd & 101 | ROOT_PID=$! 102 | test_root $ROOT_PID "$ROOT_TEST_PATH/latest/dynamic" "$SCRIPTPATH/golden/dynamic/index.golden" "dynamic_paths" 103 | 104 | $start_cmd & 105 | ROOT_PID=$! 106 | test_root $ROOT_PID "$ROOT_TEST_PATH" "$SCRIPTPATH/golden/default/index.golden" "versions" 107 | 108 | $start_cmd & 109 | ROOT_PID=$! 110 | test_root $ROOT_PID "$ROOT_TEST_PATH/womp" "$SCRIPTPATH/golden/404_response.golden" "unsupported_paths" 111 | 112 | # Delay should not affect path query 113 | start_cmd=$(create_cmd $METADATA_VERSION --port $AEMM_PORT --mock-delay-sec $DELAY_IN_SEC) 114 | $start_cmd & 115 | ROOT_PID=$! 116 | test_root $ROOT_PID "$ROOT_TEST_PATH/latest/meta-data" "$SCRIPTPATH/golden/default/latest/meta-data/index.golden" "paths_with_delay" 117 | 118 | $start_cmd & 119 | ROOT_PID=$! 120 | test_root_delay $ROOT_PID $DELAY_IN_SEC 121 | 122 | if [[ "$OSTYPE" == "linux-gnu"* ]]; then 123 | mock_trigger_time=$(date --date "${DELAY_IN_SEC} seconds" +%Y-%m-%dT%T%:z) 124 | elif [[ "$OSTYPE" == "darwin"* ]]; then 125 | mock_trigger_time=$(date -v+${DELAY_IN_SEC}S +%Y-%m-%dT%T%z | sed 's@^.\{22\}@&:@') 126 | fi 127 | start_cmd=$(create_cmd $METADATA_VERSION --port $AEMM_PORT --mock-trigger-time "$mock_trigger_time") 128 | $start_cmd & 129 | ROOT_PID=$! 130 | test_root_mock_trigger_time $ROOT_PID $DELAY_IN_SEC "$mock_trigger_time" 131 | 132 | exit $EXIT_CODE_TO_RETURN 133 | -------------------------------------------------------------------------------- /test/e2e/cmd/static-test: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | TEST_CONFIG_FILE="$SCRIPTPATH/testdata/aemm-config-integ.json" 6 | 7 | MAC_ADDRESS_DEFAULT="0e:49:61:0f:c3:11" 8 | MAC_ADDRESS_OVERRIDDEN="0e:49:61:0f:c3:77" 9 | PUBLIC_IPV4_DEFAULT="192.0.2.54" 10 | PUBLIC_IPV4_OVERRIDDEN="54.92.157.77" 11 | NETWORK_INTERFACE_ID_DEFAULT="eni-0f95d3625f5c521cc" 12 | IPV4_ASSOCIATION_DEFAULT="192.0.2.54" 13 | 14 | ROOT_PATH="http://$HOSTNAME:$AEMM_PORT" 15 | MAC_ADDRESS_PATH="$ROOT_PATH/latest/meta-data/mac" 16 | NETWORK_INTERFACE_ID_PATH_DEFAULT="$ROOT_PATH/latest/meta-data/network/interfaces/macs/$MAC_ADDRESS_DEFAULT/interface-id" 17 | NETWORK_INTERFACE_ID_PATH_OVERRIDDEN="$ROOT_PATH/latest/meta-data/network/interfaces/macs/$MAC_ADDRESS_OVERRIDDEN/interface-id" 18 | IPV4_ASSOCIATIONS_PATH_DEFAULT="$ROOT_PATH/latest/meta-data/network/interfaces/macs/$MAC_ADDRESS_DEFAULT/ipv4-associations/$PUBLIC_IPV4_DEFAULT" 19 | IPV4_ASSOCIATIONS_PATH_OVERRIDDEN="$ROOT_PATH/latest/meta-data/network/interfaces/macs/$MAC_ADDRESS_OVERRIDDEN/ipv4-associations/$PUBLIC_IPV4_OVERRIDDEN" 20 | SPOT_TEST_PATH="$ROOT_PATH/latest/meta-data/spot/instance-action" 21 | EVENTS_TEST_PATH="$ROOT_PATH/latest/meta-data/events/maintenance/scheduled" 22 | DYNAMIC_TEST_PATH="$ROOT_PATH/latest/dynamic" 23 | 24 | function test_static_metadata_available() { 25 | pid=$1 26 | test_url=$2 27 | test_name=$3 28 | tput setaf $BLUE 29 | health_check $test_url 30 | TOKEN=$(get_v2Token $MAX_TOKEN_TTL $AEMM_PORT) 31 | 32 | actual_mac_address=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $MAC_ADDRESS_PATH) 33 | assert_value "$actual_mac_address" $MAC_ADDRESS_DEFAULT "mac_address $test_name" 34 | 35 | actual_network_interface_id=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $NETWORK_INTERFACE_ID_PATH_DEFAULT) 36 | assert_value "$actual_network_interface_id" $NETWORK_INTERFACE_ID_DEFAULT "network_interface_id $test_name" 37 | 38 | actual_ipv4_association=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $IPV4_ASSOCIATIONS_PATH_DEFAULT) 39 | assert_value "$actual_ipv4_association" $IPV4_ASSOCIATION_DEFAULT "ipv4_association $test_name" 40 | 41 | clean_up $pid 42 | } 43 | 44 | function test_static_metadata_with_config_file() { 45 | pid=$1 46 | test_url=$2 47 | test_name=$3 48 | tput setaf $BLUE 49 | health_check $test_url 50 | TOKEN=$(get_v2Token $MAX_TOKEN_TTL $AEMM_PORT) 51 | 52 | updated_mac_address=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $MAC_ADDRESS_PATH) 53 | assert_value "$updated_mac_address" $MAC_ADDRESS_OVERRIDDEN "updated_mac_address $test_name" 54 | 55 | # Default paths should no longer be valid 56 | invalid_network_interface_id=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $NETWORK_INTERFACE_ID_PATH_DEFAULT) 57 | assert_not_equal "$invalid_network_interface_id" $NETWORK_INTERFACE_ID_DEFAULT "invalid_networkId $test_name" 58 | invalid_ipv4_association=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $IPV4_ASSOCIATIONS_PATH_DEFAULT) 59 | assert_not_equal "$invalid_ipv4_association" $IPV4_ASSOCIATION_DEFAULT "invalid_ipv4Association $test_name" 60 | 61 | # Updated paths return expected 62 | actual_network_interface_id=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $NETWORK_INTERFACE_ID_PATH_OVERRIDDEN) 63 | assert_value "$actual_network_interface_id" $NETWORK_INTERFACE_ID_DEFAULT "networkId $test_name" 64 | # Double substitution 65 | actual_ipv4_association=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $IPV4_ASSOCIATIONS_PATH_OVERRIDDEN) 66 | assert_value "$actual_ipv4_association" $IPV4_ASSOCIATION_DEFAULT "ipv4_association $test_name" 67 | 68 | clean_up $pid 69 | } 70 | 71 | tput setaf $BLUE 72 | echo "======================================================================================================" 73 | echo "🥑 Starting static metadata integration tests $METADATA_VERSION" 74 | echo "======================================================================================================" 75 | 76 | # Static metadata should ALWAYS be available 77 | start_cmd=$(create_cmd $METADATA_VERSION --port $AEMM_PORT) 78 | $start_cmd & 79 | STATIC_PID=$! 80 | test_static_metadata_available $STATIC_PID $ROOT_PATH "static_metadata::root" 81 | 82 | start_cmd=$(create_cmd $METADATA_VERSION --port $AEMM_PORT) 83 | $start_cmd & 84 | STATIC_PID=$! 85 | test_static_metadata_available $STATIC_PID $DYNAMIC_TEST_PATH "static_metadata::dynamic" 86 | 87 | start_cmd=$(create_cmd $METADATA_VERSION spot -a hibernate --port $AEMM_PORT) 88 | $start_cmd & 89 | STATIC_PID=$! 90 | test_static_metadata_available $STATIC_PID $SPOT_TEST_PATH "static_metadata::spot" 91 | 92 | start_cmd=$(create_cmd $METADATA_VERSION events --code instance-stop --port $AEMM_PORT) 93 | $start_cmd & 94 | STATIC_PID=$! 95 | test_static_metadata_available $STATIC_PID $EVENTS_TEST_PATH "static_metadata::events" 96 | 97 | # Static metadata paths should reflect updated placeholder values 98 | start_cmd=$(create_cmd $METADATA_VERSION --port $AEMM_PORT -c $TEST_CONFIG_FILE) 99 | $start_cmd & 100 | STATIC_PID=$! 101 | test_static_metadata_with_config_file $STATIC_PID $ROOT_PATH "static_metadata::config" 102 | 103 | exit $EXIT_CODE_TO_RETURN 104 | -------------------------------------------------------------------------------- /test/e2e/cmd/userdata-test: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | TEST_CONFIG_FILE="$SCRIPTPATH/testdata/aemm-config-integ.json" 6 | 7 | USERDATA_DEFAULT="1234,john,reboot,true" 8 | USERDATA_OVERRIDDEN="1234,john,reboot,true" 9 | 10 | ROOT_PATH="http://$HOSTNAME:$AEMM_PORT" 11 | USERDATA_TEST_PATH="$ROOT_PATH/latest/user-data" 12 | 13 | function test_userdata_defaults() { 14 | pid=$1 15 | test_url=$2 16 | test_name=$3 17 | tput setaf $BLUE 18 | health_check $test_url 19 | TOKEN=$(get_v2Token $MAX_TOKEN_TTL $AEMM_PORT) 20 | 21 | actual_userdata=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $USERDATA_TEST_PATH) 22 | assert_value "$actual_userdata" $USERDATA_DEFAULT "userdata $test_name" 23 | 24 | clean_up $pid 25 | } 26 | 27 | function test_userdata_overrides() { 28 | pid=$1 29 | test_url=$2 30 | test_name=$3 31 | tput setaf $BLUE 32 | health_check $test_url 33 | TOKEN=$(get_v2Token $MAX_TOKEN_TTL $AEMM_PORT) 34 | 35 | updated_userdata=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $USERDATA_TEST_PATH) 36 | assert_value "$updated_userdata" $USERDATA_OVERRIDDEN "userdata $test_name" 37 | 38 | clean_up $pid 39 | } 40 | 41 | tput setaf $BLUE 42 | echo "======================================================================================================" 43 | echo "🥑 Starting userdata integration tests $METADATA_VERSION" 44 | echo "======================================================================================================" 45 | 46 | 47 | # userdata data defaults 48 | start_cmd=$(create_cmd $METADATA_VERSION --port $AEMM_PORT) 49 | $start_cmd & 50 | USERDATA_PID=$! 51 | test_userdata_defaults $USERDATA_PID $USERDATA_TEST_PATH $USERDATA_DEFAULT 52 | 53 | # userdata data overrides 54 | start_cmd=$(create_cmd $METADATA_VERSION --port $AEMM_PORT -c $TEST_CONFIG_FILE) 55 | $start_cmd & 56 | USERDATA_PID=$! 57 | test_userdata_overrides $USERDATA_PID $USERDATA_TEST_PATH $USERDATA_OVERRIDDEN 58 | 59 | exit $EXIT_CODE_TO_RETURN 60 | -------------------------------------------------------------------------------- /test/e2e/golden/400_bad_request.golden: -------------------------------------------------------------------------------- 1 | 400 Bad Request -------------------------------------------------------------------------------- /test/e2e/golden/400_response.golden: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 400 - Bad Request 6 | 7 | 8 |

400 - Bad Request

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/e2e/golden/401_response.golden: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 401 - Unauthorized 6 | 7 | 8 |

401 - Unauthorized

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/e2e/golden/404_response.golden: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 404 - Not Found 7 | 8 | 9 |

404 - Not Found

10 | 11 | 12 | -------------------------------------------------------------------------------- /test/e2e/golden/asglifecycle/latest/meta-data/index.golden: -------------------------------------------------------------------------------- 1 | ami-id 2 | ami-launch-index 3 | ami-manifest-path 4 | autoscaling/ 5 | block-device-mapping/ 6 | elastic-inference/ 7 | hostname 8 | iam/ 9 | instance-action 10 | instance-id 11 | instance-life-cycle 12 | instance-type 13 | kernel-id 14 | local-hostname 15 | local-ipv4 16 | mac 17 | network/ 18 | placement/ 19 | product-codes 20 | public-hostname 21 | public-ipv4 22 | public-keys/ 23 | ramdisk-id 24 | reservation-id 25 | security-groups 26 | services/ 27 | tags/ -------------------------------------------------------------------------------- /test/e2e/golden/default/index.golden: -------------------------------------------------------------------------------- 1 | latest -------------------------------------------------------------------------------- /test/e2e/golden/default/latest/meta-data/index.golden: -------------------------------------------------------------------------------- 1 | ami-id 2 | ami-launch-index 3 | ami-manifest-path 4 | autoscaling/ 5 | block-device-mapping/ 6 | elastic-inference/ 7 | events/ 8 | hostname 9 | iam/ 10 | instance-action 11 | instance-id 12 | instance-life-cycle 13 | instance-type 14 | kernel-id 15 | local-hostname 16 | local-ipv4 17 | mac 18 | network/ 19 | placement/ 20 | product-codes 21 | public-hostname 22 | public-ipv4 23 | public-keys/ 24 | ramdisk-id 25 | reservation-id 26 | security-groups 27 | services/ 28 | spot/ 29 | tags/ -------------------------------------------------------------------------------- /test/e2e/golden/dynamic/fws.golden: -------------------------------------------------------------------------------- 1 | instance-monitoring -------------------------------------------------------------------------------- /test/e2e/golden/dynamic/index.golden: -------------------------------------------------------------------------------- 1 | fws/ 2 | instance-identity/ -------------------------------------------------------------------------------- /test/e2e/golden/dynamic/instance-identity.golden: -------------------------------------------------------------------------------- 1 | document 2 | pkcs7 3 | signature -------------------------------------------------------------------------------- /test/e2e/golden/events/latest/meta-data/index.golden: -------------------------------------------------------------------------------- 1 | ami-id 2 | ami-launch-index 3 | ami-manifest-path 4 | block-device-mapping/ 5 | elastic-inference/ 6 | events/ 7 | hostname 8 | iam/ 9 | instance-action 10 | instance-id 11 | instance-life-cycle 12 | instance-type 13 | kernel-id 14 | local-hostname 15 | local-ipv4 16 | mac 17 | network/ 18 | placement/ 19 | product-codes 20 | public-hostname 21 | public-ipv4 22 | public-keys/ 23 | ramdisk-id 24 | reservation-id 25 | security-groups 26 | services/ 27 | tags/ -------------------------------------------------------------------------------- /test/e2e/golden/events/latest/meta-data/network/interfaces/macs/0e_49_61_0f_c3_11/index.golden: -------------------------------------------------------------------------------- 1 | device-number 2 | interface-id 3 | ipv4-associations/ 4 | ipv6s 5 | local-hostname 6 | local-ipv4s 7 | mac 8 | network-card-index 9 | owner-id 10 | public-hostname 11 | public-ipv4s 12 | security-group-ids 13 | security-groups 14 | subnet-id 15 | subnet-ipv4-cidr-block 16 | subnet-ipv6-cidr-blocks 17 | vpc-id 18 | vpc-ipv4-cidr-block 19 | vpc-ipv4-cidr-blocks 20 | vpc-ipv6-cidr-blocks -------------------------------------------------------------------------------- /test/e2e/golden/spot/latest/meta-data/index.golden: -------------------------------------------------------------------------------- 1 | ami-id 2 | ami-launch-index 3 | ami-manifest-path 4 | block-device-mapping/ 5 | elastic-inference/ 6 | events/ 7 | hostname 8 | iam/ 9 | instance-action 10 | instance-id 11 | instance-life-cycle 12 | instance-type 13 | kernel-id 14 | local-hostname 15 | local-ipv4 16 | mac 17 | network/ 18 | placement/ 19 | product-codes 20 | public-hostname 21 | public-ipv4 22 | public-keys/ 23 | ramdisk-id 24 | reservation-id 25 | security-groups 26 | services/ 27 | spot/ 28 | tags/ -------------------------------------------------------------------------------- /test/e2e/golden/spot/latest/meta-data/spot.golden: -------------------------------------------------------------------------------- 1 | instance-action 2 | termination-time -------------------------------------------------------------------------------- /test/e2e/testdata/aemm-config-integ.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "paths": { 4 | "ipv4-associations": "/latest/meta-data/network/interfaces/macs/0e:49:61:0f:c3:77/ipv4-associations/192.0.2.54" 5 | }, 6 | "values": { 7 | "mac": "0e:49:61:0f:c3:77", 8 | "public-ipv4": "54.92.157.77" 9 | } 10 | }, 11 | "spot": { 12 | "action": "terminate", 13 | "time": "2020-01-07T01:03:47Z" 14 | }, 15 | "dynamic": { 16 | "values": { 17 | "instance-identity-document": { 18 | "accountId": "9876543210", 19 | "imageId": "ami-0b69ea66ff7391e80", 20 | "availabilityZone": "us-east-1f", 21 | "ramdiskId": null, 22 | "kernelId": null, 23 | "devpayProductCodes": null, 24 | "marketplaceProductCodes": ["4i20ezfza3p7xx2kt2g8weu2u"], 25 | "version": "2017-09-30", 26 | "privateIp": "10.0.7.10", 27 | "billingProducts": null, 28 | "instanceId": "i-0fedcba0987654321", 29 | "pendingTime": "2019-10-31T07:02:24Z", 30 | "architecture": "x86_64", 31 | "instanceType": "m4.xlarge", 32 | "region": "us-east-1" } 33 | } 34 | }, 35 | "userdata": { 36 | "paths": { 37 | "userdata": "/latest/user-data" 38 | }, 39 | "values": { 40 | "userdata": "MTIzNCxqb2huLHJlYm9vdCx0cnVlCg==" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/helm/ct.yaml: -------------------------------------------------------------------------------- 1 | chart-dirs: 2 | - helm 3 | all: true 4 | check-version-increment: false 5 | validate-chart-schema: false 6 | validate-maintainers: true 7 | validate-yaml: true 8 | debug: false 9 | helm-extra-args: "--timeout 600s --namespace default --kubeconfig /root/.kube/config" 10 | -------------------------------------------------------------------------------- /test/helm/kind-config.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | - role: worker -------------------------------------------------------------------------------- /test/helm/mock-ip-count-test/mock-ip-test: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | SCRIPTPATH="$( 6 | cd "$(dirname "$0")" 7 | pwd -P 8 | )" 9 | EXIT_CODE_TO_RETURN=0 10 | STATUS_CHECK_CYCLES=30 11 | STATUS_CHECK_SLEEP=2 12 | 13 | function get_status() { 14 | pod=$1 15 | status=$(kubectl describe pod $pod | grep "Status:") 16 | status=${status//"Status:"/} 17 | status=$(echo $status | xargs) 18 | echo $status 19 | } 20 | 21 | function assert_value() { 22 | # assert actual == expected 23 | if [[ $1 == "$2" ]]; then 24 | echo "✅ Verified $3" 25 | else 26 | echo "❌ Failed $3 verification. Actual: $1 Expected: $2" 27 | EXIT_CODE_TO_RETURN=1 28 | fi 29 | } 30 | 31 | function clean_up() { 32 | kubectl delete pods/test-pod pods/test-pod-404 33 | } 34 | 35 | function test() { 36 | mock_ip_count=$1 37 | expected_test_pod_status=$2 38 | expected_test_pod_404_status=$3 39 | echo "executing mock-ip-count test with mockIPCount=$mock_ip_count" 40 | 41 | helm upgrade --install "$CLUSTER_NAME-aemm" \ 42 | $AEMM_HELM_REPO \ 43 | --wait \ 44 | --namespace default \ 45 | --values $AEMM_HELM_REPO/ci/local-image-values.yaml \ 46 | --set aemm.spot.time="1994-05-15T00:00:00Z" \ 47 | --set aemm.mockIPCount=$mock_ip_count 48 | 49 | # Deploy pods 50 | kubectl apply -f "$SCRIPTPATH/test-pod.yaml" 51 | sleep 1 52 | kubectl apply -f "$SCRIPTPATH/test-pod-404.yaml" 53 | 54 | # Proceed with copying only after pod is Running 55 | for i in `seq 1 $STATUS_CHECK_CYCLES`; do 56 | test_pod_404_status=$(get_status test-pod-404) 57 | if [[ "$test_pod_404_status" == "Running" ]]; then 58 | echo "✅ Verified test-pod-404 is created and running" 59 | break 60 | fi 61 | echo "test-pod-404 still provisioning with status: $test_pod_404_status . status check $i/$STATUS_CHECK_CYCLES, sleeping for $STATUS_CHECK_SLEEP seconds" 62 | sleep $STATUS_CHECK_SLEEP 63 | done 64 | 65 | echo "copying 404_response.golden to 404-pod" 66 | kubectl cp "$SCRIPTPATH/../../e2e/golden/404_response.golden" test-pod-404:/tmp/404_response.golden 67 | 68 | # Query status until tests succeed or fail 69 | for i in `seq 1 $STATUS_CHECK_CYCLES`; do 70 | test_pod_status=$(get_status test-pod) 71 | test_pod_404_status=$(get_status test-pod-404) 72 | if [[ "$test_pod_status" != "Running" ]] && [[ "$test_pod_404_status" != "Running" ]]; then 73 | echo "✅ Verified both pods are no longer Running" 74 | break 75 | fi 76 | echo "Pods are still Running. test-pod status: $test_pod_status test-pod-404 status: $test_pod_404_status" 77 | echo "status check $i/$STATUS_CHECK_CYCLES, sleeping for $STATUS_CHECK_SLEEP seconds" 78 | sleep $STATUS_CHECK_SLEEP 79 | done 80 | 81 | assert_value "$test_pod_status" "$expected_test_pod_status" "test-pod $expected_test_pod_status w/mockIPCount=$mock_ip_count" 82 | assert_value "$test_pod_404_status" "$expected_test_pod_404_status" "test-pod-404 $expected_test_pod_404_status w/mockIPCount=$mock_ip_count" 83 | 84 | clean_up 85 | } 86 | 87 | echo "======================================================================================================" 88 | echo "🥑 Starting AEMM mock-ip-count test" 89 | echo "======================================================================================================" 90 | 91 | test 1 "Succeeded" "Succeeded" # 404 returned after ONE IP Spot request 92 | test 2 "Succeeded" "Failed" # 404 returned after TWO IP Spot requests 93 | 94 | exit $EXIT_CODE_TO_RETURN -------------------------------------------------------------------------------- /test/helm/mock-ip-count-test/test-pod-404.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "test-pod-404" 5 | spec: 6 | restartPolicy: Never 7 | containers: 8 | - name: mock-ip-test 9 | imagePullPolicy: "IfNotPresent" 10 | image: "quay.io/centos/centos:latest" 11 | command: 12 | - "bash" 13 | - "-c" 14 | - | 15 | SERVICE_NAME="AMAZON_EC2_METADATA_MOCK_SERVICE" 16 | HOST_VAR=$(echo "${SERVICE_NAME}_SERVICE_HOST") 17 | PORT_VAR=$(echo "${SERVICE_NAME}_SERVICE_PORT") 18 | sleep 5 19 | ACTUAL=$(curl http://${!HOST_VAR}:${!PORT_VAR}/latest/meta-data/spot/termination-time) 20 | EXPECTED=$(cat /tmp/404_response.golden) 21 | [[ "$ACTUAL" == "$EXPECTED" ]] && exit 0 || exit 1 22 | -------------------------------------------------------------------------------- /test/helm/mock-ip-count-test/test-pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "test-pod" 5 | spec: 6 | restartPolicy: Never 7 | containers: 8 | - name: mock-ip-test 9 | imagePullPolicy: "IfNotPresent" 10 | image: "quay.io/centos/centos:latest" 11 | command: 12 | - "bash" 13 | - "-c" 14 | - | 15 | SERVICE_NAME="AMAZON_EC2_METADATA_MOCK_SERVICE" 16 | HOST_VAR=$(echo "${SERVICE_NAME}_SERVICE_HOST") 17 | PORT_VAR=$(echo "${SERVICE_NAME}_SERVICE_PORT") 18 | sleep 3 19 | ACTUAL=$(curl http://${!HOST_VAR}:${!PORT_VAR}/latest/meta-data/spot/termination-time) 20 | EXPECTED="1994-05-15T00:00:00Z" 21 | [[ "$ACTUAL" == "$EXPECTED" ]] && exit 0 || exit 1 -------------------------------------------------------------------------------- /test/helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 test 15 | 16 | import ( 17 | "fmt" 18 | "path/filepath" 19 | "runtime" 20 | "testing" 21 | ) 22 | 23 | // ItemsMatch fails the test if the items in exp and act slices dont match. 24 | // A nil argument is equivalent to an empty slice. 25 | func ItemsMatch(tb testing.TB, exp, act []string) { 26 | if len(exp) != len(act) { 27 | tb.Errorf(fmt.Sprintf("Expected %d items in slice, but was %d", len(exp), len(act))) 28 | } 29 | for _, v := range exp { 30 | if !Contains(act, v) { 31 | tb.Errorf(fmt.Sprintf("Expected to find item %s in slice, but was not found", v)) 32 | } 33 | } 34 | } 35 | 36 | // Contains returns a bool indicating whether the slice contains the given val 37 | func Contains(slice []string, val string) bool { 38 | for _, item := range slice { 39 | if item == val { 40 | return true 41 | } 42 | } 43 | return false 44 | } 45 | 46 | // Assert fails the test if the condition is false. 47 | func Assert(tb testing.TB, condition bool, msg string, v ...interface{}) { 48 | if !condition { 49 | _, file, line, _ := runtime.Caller(1) 50 | fmt.Printf("\033[31m%s:%d: "+msg+"\033[39m\n\n", append([]interface{}{filepath.Base(file), line}, v...)...) 51 | tb.FailNow() 52 | } 53 | } 54 | 55 | // Ok fails the test if an err is not nil. 56 | func Ok(tb testing.TB, err error) { 57 | if err != nil { 58 | _, file, line, _ := runtime.Caller(1) 59 | fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error()) 60 | tb.FailNow() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/readme-test/run-readme-spellcheck: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" 5 | 6 | function exit_and_fail() { 7 | echo "❌ Test Failed! Found a markdown file with spelling errors." 8 | exit 1 9 | } 10 | trap exit_and_fail INT ERR TERM 11 | 12 | docker build -t misspell -f $SCRIPTPATH/spellcheck-Dockerfile $SCRIPTPATH/ 13 | docker run -i --rm -v $SCRIPTPATH/../../:/app misspell /bin/bash -c 'find /app/ -type f -name "*.md" -not -path "build" | grep -v "/build/" | xargs misspell -error -debug' 14 | echo "✅ Markdown file spell check passed!" -------------------------------------------------------------------------------- /test/readme-test/spellcheck-Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17 2 | 3 | RUN go install github.com/client9/misspell/cmd/misspell@v0.3.4 4 | 5 | CMD [ "/go/bin/misspell" ] 6 | -------------------------------------------------------------------------------- /test/shellcheck/run-shellcheck: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" 6 | BUILD_DIR="${SCRIPTPATH}/../../build" 7 | 8 | KERNEL=$(uname -s | tr '[:upper:]' '[:lower:]') 9 | SHELLCHECK_VERSION="0.7.1" 10 | 11 | function exit_and_fail() { 12 | echo "❌ Test Failed! Found a shell script with errors." 13 | exit 1 14 | } 15 | trap exit_and_fail INT ERR TERM 16 | 17 | curl -Lo ${BUILD_DIR}/shellcheck.tar.xz "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.${KERNEL}.x86_64.tar.xz" 18 | tar -C ${BUILD_DIR} -xvf "${BUILD_DIR}/shellcheck.tar.xz" 19 | export PATH="${BUILD_DIR}/shellcheck-v${SHELLCHECK_VERSION}:$PATH" 20 | 21 | shell_files=() 22 | while IFS='' read -r line; do 23 | shell_files+=("$line"); 24 | done < <(grep -Rnl --exclude-dir=build --exclude-dir=docs -e '#!.*/bin/bash' -e '#!.*/usr/bin/env bash' ${SCRIPTPATH}/../../) 25 | shellcheck -S warning "${shell_files[@]}" 26 | 27 | echo "✅ All shell scripts look good! 😎" --------------------------------------------------------------------------------