├── .github ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ └── check.yml ├── .gitignore ├── .mergify.yml ├── CODE_OF_CONDUCT.md ├── COMPATIBILITY.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── THIRD-PARTY-LICENSES.txt ├── cmd └── oam-ecs │ └── main.go ├── demo.gif ├── demo.sh ├── examples ├── example-app.yaml ├── server-component.yaml └── worker-component.yaml ├── go.mod ├── go.sum ├── integ-tests ├── integ_tests_suite_test.go ├── schematics │ ├── application-scope.yaml │ ├── complex.backend.expected.yaml │ ├── complex.frontend.expected.yaml │ ├── complex.yaml │ ├── example-server.expected.yaml │ ├── example-worker.expected.yaml │ ├── extended-workload-type.yaml │ ├── manually-scaled-frontend.expected.yaml │ ├── manually-scaled-frontend.yaml │ ├── nginx.yaml │ ├── trait.yaml │ ├── twitter-bot.backend.expected.yaml │ ├── twitter-bot.frontend.expected.yaml │ ├── twitter-bot.yaml │ ├── webserver.backend.expected.yaml │ ├── webserver.frontend.expected.yaml │ ├── webserver.yaml │ ├── worker.expected.yaml │ ├── worker.yaml │ ├── wrong-api-version.yaml │ ├── wrong-kind.yaml │ └── wrong-workload-type.yaml └── template_generation_test.go ├── internal └── pkg │ ├── aws │ └── session │ │ └── session.go │ ├── cli │ ├── app.go │ ├── app_delete.go │ ├── app_deploy.go │ ├── app_show.go │ ├── cli.go │ ├── env.go │ ├── env_delete.go │ ├── env_deploy.go │ ├── env_show.go │ ├── flag.go │ └── progress.go │ ├── deploy │ └── cloudformation │ │ ├── changeset.go │ │ ├── cloudformation.go │ │ ├── component.go │ │ ├── deploy.go │ │ ├── env.go │ │ ├── errors.go │ │ ├── stack │ │ ├── component.go │ │ ├── env.go │ │ ├── errors.go │ │ ├── tags.go │ │ └── template_functions.go │ │ ├── stack_status.go │ │ └── types │ │ ├── component.go │ │ └── env.go │ ├── term │ ├── color │ │ ├── color.go │ │ └── color_test.go │ ├── cursor │ │ └── cursor.go │ ├── log │ │ ├── log.go │ │ ├── log_test.go │ │ ├── prefix.go │ │ └── prefix_windows.go │ └── progress │ │ ├── charset.go │ │ ├── charset_windows.go │ │ ├── deploy │ │ └── cloudformation │ │ │ ├── deploy.go │ │ │ └── deploy_test.go │ │ ├── mocks │ │ └── mock_spinner.go │ │ ├── progress.go │ │ ├── spinner.go │ │ └── spinner_test.go │ ├── version │ └── version.go │ └── workload │ └── workload.go └── templates ├── core.oam.dev └── cf.yml ├── environment └── cf.yml └── templates.go /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Issue #, if available: 2 | 3 | Description of changes: 4 | 5 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | time: "13:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | on: 2 | [pull_request, push] 3 | 4 | name: Check 5 | 6 | jobs: 7 | check: 8 | name: Run Tests 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | 14 | - name: Setup go 15 | uses: actions/setup-go@v1 16 | with: 17 | go-version: '1.17' 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v1 21 | with: 22 | python-version: '3.8' 23 | 24 | - name: Build 25 | run: make 26 | 27 | - name: Run tests 28 | run: make test 29 | 30 | - name: Lint integ test and example templates 31 | run: | 32 | pip install cfn-lint 33 | cfn-lint integ-tests/schematics/*.expected.yaml 34 | ./bin/local/oam-ecs app deploy --dry-run -f examples/example-app.yaml -f examples/worker-component.yaml -f examples/server-component.yaml 35 | cfn-lint oam-ecs-dry-run-results/* 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | bin/ 18 | 19 | templates/packrd/ 20 | templates/templates-packr.go 21 | 22 | oam-ecs-dry-run-results/ 23 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: Automatically merge on CI success and review approval 3 | conditions: 4 | - base=master 5 | - "#approved-reviews-by>=1" 6 | - approved-reviews-by=@awslabs/developer-experience 7 | - -approved-reviews-by~=author 8 | - status-success=Run Tests 9 | - label!=work-in-progress 10 | - -title~=(WIP|wip) 11 | - -merged 12 | - -closed 13 | - author!=dependabot[bot] 14 | actions: 15 | merge: 16 | method: squash 17 | 18 | - name: Automatically approve and merge Dependabot PRs 19 | conditions: 20 | - base=master 21 | - author=dependabot[bot] 22 | - label=dependencies 23 | - status-success=Run Tests 24 | - label!=work-in-progress 25 | - -title~=(WIP|wip) 26 | - -label~=(blocked|do-not-merge) 27 | - -merged 28 | - -closed 29 | actions: 30 | review: 31 | type: APPROVE 32 | merge: 33 | method: squash 34 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, 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 *master* 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 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY_NAME=oam-ecs 2 | PACKAGES=./internal... 3 | GOBIN=${PWD}/bin/tools 4 | COVERAGE=coverage.out 5 | 6 | DESTINATION=./bin/local/${BINARY_NAME} 7 | VERSION=$(shell git describe --always --tags) 8 | 9 | LINKER_FLAGS=-X github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/version.Version=${VERSION} 10 | # RELEASE_BUILD_LINKER_FLAGS disables DWARF and symbol table generation to reduce binary size 11 | RELEASE_BUILD_LINKER_FLAGS=-s -w 12 | 13 | all: build 14 | 15 | .PHONY: build 16 | build: format packr-build compile-local packr-clean 17 | 18 | .PHONY: stage-release 19 | stage-release: packr-build compile-darwin compile-linux compile-windows packr-clean 20 | 21 | .PHONY: format 22 | format: 23 | go fmt ./... 24 | 25 | packr-build: tools 26 | @echo "Packaging static files" &&\ 27 | env -i PATH=$$PATH:${GOBIN} GOCACHE=$$(go env GOCACHE) GOPATH=$$(go env GOPATH) \ 28 | go generate ./... 29 | 30 | packr-clean: tools 31 | @echo "Cleaning up static files generated code" &&\ 32 | cd templates &&\ 33 | ${GOBIN}/packr2 clean &&\ 34 | cd .. &&\ 35 | go mod tidy 36 | 37 | .PHONY: tools 38 | tools: 39 | GOBIN=${GOBIN} go get github.com/gobuffalo/packr/v2/packr2 40 | GOBIN=${GOBIN} go get github.com/onsi/ginkgo/ginkgo 41 | GOBIN=${GOBIN} go get github.com/onsi/gomega/... 42 | 43 | compile-local: 44 | go build -ldflags "${LINKER_FLAGS}" -o ${DESTINATION} ./cmd/oam-ecs 45 | 46 | compile-windows: 47 | CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags "${LINKER_FLAGS} ${RELEASE_BUILD_LINKER_FLAGS}" -o ${DESTINATION}.exe ./cmd/oam-ecs 48 | 49 | compile-linux: 50 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "${LINKER_FLAGS} ${RELEASE_BUILD_LINKER_FLAGS}" -o ${DESTINATION}-amd64 ./cmd/oam-ecs 51 | 52 | compile-darwin: 53 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "${LINKER_FLAGS} ${RELEASE_BUILD_LINKER_FLAGS}" -o ${DESTINATION} ./cmd/oam-ecs 54 | 55 | .PHONY: test 56 | test: format packr-build compile-local run-unit-test run-e2e-test packr-clean 57 | 58 | run-unit-test: 59 | go test -race -cover -count=1 -coverprofile ${COVERAGE} ${PACKAGES} 60 | 61 | run-e2e-test: 62 | go test ./integ-tests... 63 | 64 | generate-coverage: ${COVERAGE} 65 | go tool cover -html=${COVERAGE} 66 | 67 | ${COVERAGE}: test 68 | 69 | .PHONY: clean 70 | clean: 71 | - rm -rf ./bin 72 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon ECS for Open Application Model 2 | 3 | The oam-ecs CLI is a proof-of-concept that partially implements the [Open Application Model](https://oam.dev/) (OAM) specification, version v1alpha1. 4 | 5 | The oam-ecs CLI provisions two of the core OAM workload types as Amazon ECS services running on AWS Fargate using AWS CloudFormation. A workload of type `core.oam.dev/v1alpha1.Worker` will deploy a CloudFormation stack containing an ECS service running in private VPC subnets with no accessible endpoint. A workload of type `core.oam.dev/v1alpha1.Server` will deploy a CloudFormation stack containing an ECS service running in private VPC subnets, behind a publicly-accessible network load balancer. 6 | 7 | For a full comparison with the OAM specification, see the [Compatibility](COMPATIBILITY.md) page. 8 | 9 | See the [full demo here](https://raw.githubusercontent.com/awslabs/amazon-ecs-for-open-application-model/master/demo.gif). 10 | 11 | >⚠️ Note that this project is a proof-of-concept and should not be used with production workloads. Use the `--dry-run` option to review all CloudFormation templates generated by this tool before deploying them to your AWS account. 12 | 13 | **Table of Contents** 14 | 15 | 16 | 17 | - [Build the CLI](#build--test) 18 | - [Deploy an environment](#deploy-an-oam-ecs-environment) 19 | - [Deploy an application](#deploy-oam-workloads-with-oam-ecs) 20 | - [Upgrade and scale an application](#upgrade-and-scale-oam-workloads-with-oam-ecs) 21 | - [Tear down](#tear-down) 22 | - [Specify AWS credentials and region](#credentials-and-region) 23 | - [Security Disclosures](#security-disclosures) 24 | - [License](#license) 25 | 26 | 27 | 28 | ## Build & test 29 | 30 | ``` 31 | make 32 | make test 33 | export PATH="$PATH:./bin/local" 34 | oam-ecs --help 35 | ``` 36 | 37 | To customize the CloudFormation templates generated by this tool (for example, to change the VPC configuration or security group rules), edit the following files and re-compile: 38 | * [environment template](templates/environment/cf.yml) 39 | * [workload template](templates/core.oam.dev/cf.yml) 40 | 41 | ## Deploy an oam-ecs environment 42 | 43 | The oam-ecs environment deployment creates a VPC with public and private subnets where OAM workloads can be deployed. 44 | 45 | ``` 46 | oam-ecs env deploy 47 | ``` 48 | 49 | The environment attributes like VPC ID and ECS cluster name can be described. 50 | 51 | ``` 52 | oam-ecs env show 53 | ``` 54 | 55 | The CloudFormation template deployed by this command can be [seen here](templates/environment/cf.yml). 56 | 57 | ## Deploy OAM workloads with oam-ecs 58 | 59 | The dry-run step outputs the CloudFormation template that represents the given OAM workloads. The CloudFormation templates are written to the `./oam-ecs-dry-run-results` directory. 60 | 61 | ``` 62 | oam-ecs app deploy --dry-run \ 63 | -f examples/example-app.yaml \ 64 | -f examples/worker-component.yaml \ 65 | -f examples/server-component.yaml 66 | ``` 67 | 68 | Then the CloudFormation resources, including load balancers and ECS services running on Fargate, can be deployed: 69 | 70 | ``` 71 | oam-ecs app deploy \ 72 | -f examples/example-app.yaml \ 73 | -f examples/worker-component.yaml \ 74 | -f examples/server-component.yaml 75 | ``` 76 | 77 | The application component instances' attributes like ECS service name and endpoint DNS name can be described. 78 | 79 | ``` 80 | oam-ecs app show -f examples/example-app.yaml 81 | ``` 82 | 83 | ## Upgrade and scale OAM workloads with oam-ecs 84 | 85 | To change operational settings like the scale of a component instance or to add new component instances to an application, update the application configuration file (e.g. `example-app.yaml`) and re-run the `oam-ecs deploy` command with the same inputs. The existing CloudFormation stacks for the application will be updated with the new settings. 86 | 87 | ``` 88 | oam-ecs app deploy \ 89 | -f examples/example-app.yaml \ 90 | -f examples/worker-component.yaml \ 91 | -f examples/server-component.yaml 92 | ``` 93 | 94 | To upgrade a component to a new image tag, you can update the image tag in the component schematic file (e.g. `server-component.yaml`), and re-run the `oam-ecs deploy` command with the same inputs as above. The existing CloudFormation stack for the updated component instance will be updated with the new image tag. 95 | 96 | The oam-ecs tool does not require following the [OAM spec guidance](https://github.com/oam-dev/spec/blob/4af9e65769759c408193445baf99eadd93f3426a/6.application_configuration.md#releases) that component schematics be treated as immutable. To follow the spec guidance when upgrading to a new image tag, create a new component schematic (e.g. `server-component-v2.yaml` with name `server-v2`) and update the component instance in the application configuration (e.g. update the `componentName` to `server-v2` for the instance `example-server` in `example-app.yaml`). Running `oam-ecs deploy` with the new component schematic will update that component instance's CloudFormation stack with the new image tag. Do NOT update the `instanceName` in the application configuration, as that will result in creating a new CloudFormation stack and will not delete the previous CloudFormation stack. 97 | 98 | ``` 99 | oam-ecs app deploy \ 100 | -f examples/example-app.yaml \ 101 | -f examples/worker-component.yaml \ 102 | -f examples/server-component-v2.yaml 103 | ``` 104 | 105 | ## Tear down 106 | 107 | To delete all infrastructure provisioned by oam-ecs, first delete the deployed applications: 108 | 109 | ``` 110 | oam-ecs app delete -f examples/example-app.yaml 111 | ``` 112 | 113 | You can delete the infrastructure for individual component instances by creating an application configuration file containing only that component instance, and running the above `oam-ecs delete` command. Note that the `oam-ecs deploy` command does NOT comply with the [OAM spec requirement](https://github.com/oam-dev/spec/blob/4af9e65769759c408193445baf99eadd93f3426a/6.application_configuration.md#releases) to automatically delete the infrastructure for component instances that have been removed in an updated application configuration. 114 | 115 | To delete the environment infrastructure, once all applications are deleted: 116 | 117 | ``` 118 | oam-ecs env delete 119 | ``` 120 | 121 | ## Credentials and Region 122 | 123 | oam-ecs will look for credentials in the following order, using the [default provider chain](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials) in the AWS SDK for Go. 124 | 125 | 1. Environment variables. 126 | 1. Shared credentials file. Profiles can be specified using the `AWS_PROFILE` environment variable. 127 | 1. If running on Amazon ECS (with task role) or AWS CodeBuild, IAM role from the container credentials endpoint. 128 | 1. If running on an Amazon EC2 instance, IAM role for Amazon EC2. 129 | 130 | No credentials are required for dry-runs of the oam-ecs tool. 131 | 132 | oam-ecs will determine the region in the following order, using the [default behavior](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-the-region) in the AWS SDK for Go. 133 | 134 | 1. From the `AWS_REGION` environment variable. 135 | 1. From the `config` file in the `.aws/` folder in your home directory. 136 | 137 | ## Security Disclosures 138 | 139 | If you would like to report a potential security issue in this project, please do not create a GitHub issue. Instead, please follow the instructions [here](https://aws.amazon.com/security/vulnerability-reporting/) or [email AWS Security directly](mailto:aws-security@amazon.com). 140 | 141 | ## License 142 | 143 | This project is licensed under the Apache-2.0 License. 144 | -------------------------------------------------------------------------------- /cmd/oam-ecs/main.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package main 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/cli" 11 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/version" 12 | ) 13 | 14 | func init() { 15 | cobra.EnableCommandSorting = false // Maintain the order in which we add commands. 16 | } 17 | 18 | func main() { 19 | cmd := buildRootCmd() 20 | if err := cmd.Execute(); err != nil { 21 | os.Exit(1) 22 | } 23 | } 24 | 25 | func buildRootCmd() *cobra.Command { 26 | cmd := &cobra.Command{ 27 | Use: "oam-ecs", 28 | Short: "Provision core Open Application Model (OAM) v1alpha1 workload types as Amazon ECS services", 29 | Example: ` 30 | Display the help menu for the app deploy command 31 | $ oam-ecs app deploy --help`, 32 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 33 | // If we don't set a Run() function the help menu doesn't show up. 34 | // See https://github.com/spf13/cobra/issues/790 35 | }, 36 | SilenceUsage: true, 37 | } 38 | 39 | // Sets version for --version flag. Version command gives more detailed 40 | // version information. 41 | cmd.Version = version.Version 42 | cmd.SetVersionTemplate("oam-ecs version: {{.Version}}\n") 43 | 44 | // Commands (in the order they will show up in the help menu) 45 | cmd.AddCommand(cli.BuildAppCmd()) 46 | cmd.AddCommand(cli.BuildEnvCmd()) 47 | 48 | return cmd 49 | } 50 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/amazon-ecs-for-open-application-model/df23a766b398ac387b3a38c95fc6a57a5ccd4c81/demo.gif -------------------------------------------------------------------------------- /demo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export PATH="$PATH:$PWD/bin/local" 4 | PS1="$ " 5 | 6 | . ../demo-magic/demo-magic.sh 7 | 8 | clear 9 | 10 | # Clean up previous runs 11 | 12 | rm -rf examples/oam-ecs-dry-run-results 13 | 14 | # Look and feel 15 | 16 | TYPE_SPEED=20 17 | DEMO_COMMENT_COLOR=$CYAN 18 | NO_WAIT=false 19 | 20 | # Start the demo 21 | 22 | PROMPT_TIMEOUT=0 23 | p "# Welcome to oam-ecs!" 24 | PROMPT_TIMEOUT=1 25 | 26 | NO_WAIT=true 27 | p "# The oam-ecs CLI is a proof-of-concept that partially implements the Open Application Model (OAM)" 28 | p "# specification, version v1alpha1. oam-ecs takes OAM definitions as input, translates them into AWS" 29 | p "# CloudFormation templates, and deploys them as Amazon ECS services running on AWS Fargate.\n" 30 | NO_WAIT=false 31 | 32 | p "# Let's walk through an example!" 33 | 34 | pe "cd examples/" 35 | 36 | pe "ls -1" 37 | 38 | p "# oam-ecs works with two types of files: component configuration and application configuration." 39 | 40 | p "# Components are things like a backend API service, a frontend web application, or a database." 41 | 42 | pe "ls -1 *-component.yaml" 43 | 44 | NO_WAIT=true 45 | p "# Here I have two components: a frontend 'server' workload that has an HTTP endpoint, and a " 46 | p "# backend 'worker' workload that processes data." 47 | NO_WAIT=false 48 | 49 | pe "head -n 15 server-component.yaml" 50 | 51 | NO_WAIT=true 52 | p "# Component configuration like the example above contains information from the developer about " 53 | p "# requirements for running their application code, like the image and resource requirements." 54 | NO_WAIT=false 55 | 56 | pe "cat example-app.yaml" 57 | 58 | NO_WAIT=true 59 | p "# Application configuration like the example above deploys new instances of the developer's " 60 | p "# components. It contains information from the operator that is specific to each instance of the " 61 | p "# component, like environment variable values and scale.\n" 62 | 63 | p "# Note that neither the component config nor the application config specified *any* infrastructure!" 64 | p "# The OAM format is platform-agnostic, so infrastructure operators decide what infrastructure this " 65 | p "# configuration should translate into. For example, oam-ecs runs OAM workloads with ECS on Fargate.\n" 66 | NO_WAIT=false 67 | 68 | p "# So, let's deploy this application to ECS and Fargate with oam-ecs!" 69 | 70 | p "# I already created an oam-ecs environment, which contains shared infrastructure like the VPC." 71 | 72 | pe "oam-ecs env show" 73 | 74 | NO_WAIT=true 75 | p "# I can do a dry-run and inspect the CloudFormation templates that my OAM configuration produces, " 76 | p "# before deploying them." 77 | NO_WAIT=false 78 | 79 | pe "oam-ecs app deploy --dry-run -f example-app.yaml -f worker-component.yaml -f server-component.yaml" 80 | 81 | pe "ls -1 oam-ecs-dry-run-results" 82 | 83 | pe "less oam-ecs-dry-run-results/oam-ecs-example-app-example-server-template.yaml" 84 | 85 | p "# Now I will deploy the infrastructure and my application containers into my AWS account." 86 | 87 | pe "oam-ecs app deploy -f example-app.yaml -f worker-component.yaml -f server-component.yaml" 88 | 89 | NO_WAIT=true 90 | p "# I can see details about the deployed resources, including the endpoint that was created for my " 91 | p "# server workload.\n" 92 | NO_WAIT=false 93 | 94 | PROMPT_TIMEOUT=0 95 | p "# Enjoy deploying OAM applications to ECS on Fargate with oam-ecs!" 96 | -------------------------------------------------------------------------------- /examples/example-app.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1alpha1 2 | kind: ApplicationConfiguration 3 | metadata: 4 | name: example-app 5 | spec: 6 | components: 7 | - componentName: worker-v1 8 | instanceName: example-worker 9 | traits: 10 | - name: manual-scaler 11 | properties: 12 | replicaCount: 2 13 | - componentName: server-v1 14 | instanceName: example-server 15 | parameterValues: 16 | - name: WorldValue 17 | value: Everyone 18 | -------------------------------------------------------------------------------- /examples/server-component.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1alpha1 2 | kind: ComponentSchematic 3 | metadata: 4 | name: server-v1 5 | spec: 6 | workloadType: core.oam.dev/v1alpha1.Server 7 | osType: linux 8 | containers: 9 | - name: server 10 | image: nginxdemos/hello 11 | resources: 12 | cpu: 13 | required: 0.1 14 | memory: 15 | required: "128" 16 | args: 17 | - "nginx-debug" 18 | - "-g" 19 | - "daemon off;" 20 | env: 21 | - name: TEST 22 | value: Hello 23 | - name: PARAM 24 | fromParam: WorldValue 25 | ports: 26 | - name: port 27 | containerPort: 80 28 | -------------------------------------------------------------------------------- /examples/worker-component.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1alpha1 2 | kind: ComponentSchematic 3 | metadata: 4 | name: worker-v1 5 | spec: 6 | workloadType: core.oam.dev/v1alpha1.Worker 7 | osType: linux 8 | containers: 9 | - name: worker 10 | image: nginxdemos/hello:plain-text 11 | resources: 12 | cpu: 13 | required: 0.1 14 | memory: 15 | required: "128" 16 | livenessProbe: 17 | timeoutSeconds: 2 18 | exec: 19 | command: 20 | - "wget" 21 | - "-qO" 22 | - "-" 23 | - "http://localhost" 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/awslabs/amazon-ecs-for-open-application-model 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/AlecAivazis/survey/v2 v2.3.2 7 | github.com/Masterminds/goutils v1.1.0 // indirect 8 | github.com/Masterminds/semver v1.5.0 // indirect 9 | github.com/Masterminds/sprig v2.22.0+incompatible 10 | github.com/aws/aws-sdk-go v1.44.46 11 | github.com/briandowns/spinner v1.18.1 12 | github.com/fatih/color v1.13.0 13 | github.com/fsnotify/fsnotify v1.5.1 // indirect 14 | github.com/gobuffalo/packd v1.0.1 15 | github.com/gobuffalo/packr/v2 v2.8.3 16 | github.com/golang/mock v1.6.0 17 | github.com/google/go-cmp v0.5.6 // indirect 18 | github.com/google/gofuzz v1.2.0 // indirect 19 | github.com/google/uuid v1.3.0 20 | github.com/huandu/xstrings v1.3.2 // indirect 21 | github.com/iancoleman/strcase v0.2.0 22 | github.com/imdario/mergo v0.3.11 // indirect 23 | github.com/mattn/go-colorable v0.1.12 // indirect 24 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 25 | github.com/mitchellh/copystructure v1.0.0 // indirect 26 | github.com/mitchellh/reflectwalk v1.0.1 // indirect 27 | github.com/oam-dev/oam-go-sdk v0.0.0-20200908114024-f2801ed3a711 28 | github.com/olekukonko/tablewriter v0.0.5 29 | github.com/onsi/ginkgo v1.16.5 30 | github.com/onsi/gomega v1.19.0 31 | github.com/spf13/cobra v1.5.0 32 | github.com/stretchr/testify v1.8.0 33 | k8s.io/apimachinery v0.24.2 34 | k8s.io/client-go v0.24.2 35 | sigs.k8s.io/controller-runtime v0.6.4 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /integ-tests/integ_tests_suite_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package template_generation_test 4 | 5 | import ( 6 | "testing" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | func TestIntegTests(t *testing.T) { 13 | RegisterFailHandler(Fail) 14 | RunSpecs(t, "IntegTests Suite") 15 | } 16 | 17 | func BeforeAll(fn func()) { 18 | first := true 19 | BeforeEach(func() { 20 | if first { 21 | fn() 22 | first = false 23 | } 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /integ-tests/schematics/application-scope.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1alpha1 2 | kind: ApplicationScope 3 | metadata: 4 | name: network 5 | annotations: 6 | version: v1.0.0 7 | description: "network boundary that a group components reside in" 8 | spec: 9 | type: core.oam.dev/v1.NetworkScope 10 | allowComponentOverlap: false 11 | parameters: 12 | - name: network-id 13 | description: The id of the network, e.g. vpc-id, VNet name. 14 | type: string 15 | required: Y 16 | - name: subnet-ids 17 | description: > 18 | A comma separated list of IDs of the subnets within the network. For example, "vsw-123" or ""vsw-123,vsw-456". 19 | There could be more than one subnet because there is a limit in the number of IPs in a subnet. 20 | If IPs are taken up, operators need to add another subnet into this network. 21 | type: string 22 | required: Y 23 | - name: internet-gateway-type 24 | description: The type of the gateway, options are 'public', 'nat'. Empty string means no gateway. 25 | type: string 26 | required: N -------------------------------------------------------------------------------- /integ-tests/schematics/complex.backend.expected.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: Amazon ECS infrastructure for complex-example backend 3 | 4 | Resources: 5 | LogGroup: 6 | Type: AWS::Logs::LogGroup 7 | Properties: 8 | LogGroupName: oam-ecs-complex-example-backend 9 | 10 | TaskDefinition: 11 | Type: AWS::ECS::TaskDefinition 12 | Properties: 13 | Family: oam-ecs-complex-example-backend 14 | NetworkMode: awsvpc 15 | RequiresCompatibilities: 16 | - FARGATE 17 | Cpu: 4.00 vcpu 18 | Memory: '10240' 19 | ExecutionRoleArn: !GetAtt ExecutionRole.Arn 20 | ContainerDefinitions: 21 | - Name: server 22 | Image: nginx:latest 23 | ResourceRequirements: 24 | - Type: GPU 25 | Value: '1' 26 | MountPoints: 27 | - ContainerPath: /etc/config 28 | ReadOnly: true 29 | SourceVolume: configuration 30 | EntryPoint: 31 | - "nginx" 32 | Command: 33 | - "--debug" 34 | HealthCheck: 35 | Command: 36 | - "wget" 37 | - "-qO" 38 | - "-" 39 | - "http://localhost" 40 | Interval: 12 41 | Retries: 4 42 | StartPeriod: 5 43 | Timeout: 3 44 | LogConfiguration: 45 | LogDriver: awslogs 46 | Options: 47 | awslogs-region: !Ref AWS::Region 48 | awslogs-group: !Ref LogGroup 49 | awslogs-stream-prefix: oam-ecs 50 | Volumes: 51 | - Name: configuration 52 | 53 | ExecutionRole: 54 | Type: AWS::IAM::Role 55 | Properties: 56 | AssumeRolePolicyDocument: 57 | Statement: 58 | - Effect: Allow 59 | Principal: 60 | Service: ecs-tasks.amazonaws.com 61 | Action: 'sts:AssumeRole' 62 | 63 | ManagedPolicyArns: 64 | - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' 65 | 66 | ContainerSecurityGroup: 67 | Type: AWS::EC2::SecurityGroup 68 | Properties: 69 | GroupDescription: oam-ecs-complex-example-backend-ContainerSecurityGroup 70 | VpcId: 71 | Fn::ImportValue: oam-ecs-VpcId 72 | 73 | Service: 74 | Type: AWS::ECS::Service 75 | Properties: 76 | Cluster: 77 | Fn::ImportValue: oam-ecs-ECSCluster 78 | TaskDefinition: !Ref TaskDefinition 79 | DeploymentConfiguration: 80 | MinimumHealthyPercent: 100 81 | MaximumPercent: 200 82 | DesiredCount: 1 83 | LaunchType: FARGATE 84 | NetworkConfiguration: 85 | AwsvpcConfiguration: 86 | AssignPublicIp: DISABLED 87 | Subnets: 88 | Fn::Split: 89 | - ',' 90 | - Fn::ImportValue: oam-ecs-PrivateSubnets 91 | SecurityGroups: 92 | - !Ref ContainerSecurityGroup 93 | 94 | 95 | 96 | Outputs: 97 | CloudFormationStackConsole: 98 | Description: The AWS console deep-link for the CloudFormation stack 99 | Value: !Sub https://console.aws.amazon.com/cloudformation/home?region=${AWS::Region}#/stacks/stackinfo?stackId=${AWS::StackName} 100 | 101 | ECSServiceConsole: 102 | Description: The AWS console deep-link for the ECS service 103 | Value: !Sub https://console.aws.amazon.com/ecs/home?region=${AWS::Region}#/clusters/oam-ecs/services/${Service.Name} 104 | 105 | -------------------------------------------------------------------------------- /integ-tests/schematics/complex.frontend.expected.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: Amazon ECS infrastructure for complex-example web-front-end 3 | 4 | Resources: 5 | LogGroup: 6 | Type: AWS::Logs::LogGroup 7 | Properties: 8 | LogGroupName: oam-ecs-complex-example-web-front-end 9 | 10 | TaskDefinition: 11 | Type: AWS::ECS::TaskDefinition 12 | Properties: 13 | Family: oam-ecs-complex-example-web-front-end 14 | NetworkMode: awsvpc 15 | RequiresCompatibilities: 16 | - FARGATE 17 | Cpu: 2.00 vcpu 18 | Memory: '14336' 19 | ExecutionRoleArn: !GetAtt ExecutionRole.Arn 20 | ContainerDefinitions: 21 | - Name: server 22 | Image: nginx:latest 23 | ResourceRequirements: 24 | - Type: GPU 25 | Value: '2' 26 | MountPoints: 27 | - ContainerPath: /etc/config 28 | ReadOnly: true 29 | SourceVolume: configuration 30 | EntryPoint: 31 | - "nginx" 32 | Command: 33 | - "--debug" 34 | PortMappings: 35 | - ContainerPort: 8080 36 | Protocol: tcp 37 | LogConfiguration: 38 | LogDriver: awslogs 39 | Options: 40 | awslogs-region: !Ref AWS::Region 41 | awslogs-group: !Ref LogGroup 42 | awslogs-stream-prefix: oam-ecs 43 | Volumes: 44 | - Name: configuration 45 | 46 | ExecutionRole: 47 | Type: AWS::IAM::Role 48 | Properties: 49 | AssumeRolePolicyDocument: 50 | Statement: 51 | - Effect: Allow 52 | Principal: 53 | Service: ecs-tasks.amazonaws.com 54 | Action: 'sts:AssumeRole' 55 | 56 | ManagedPolicyArns: 57 | - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' 58 | 59 | ContainerSecurityGroup: 60 | Type: AWS::EC2::SecurityGroup 61 | Properties: 62 | GroupDescription: oam-ecs-complex-example-web-front-end-ContainerSecurityGroup 63 | VpcId: 64 | Fn::ImportValue: oam-ecs-VpcId 65 | 66 | Service: 67 | Type: AWS::ECS::Service 68 | Properties: 69 | Cluster: 70 | Fn::ImportValue: oam-ecs-ECSCluster 71 | TaskDefinition: !Ref TaskDefinition 72 | DeploymentConfiguration: 73 | MinimumHealthyPercent: 100 74 | MaximumPercent: 200 75 | DesiredCount: 1 76 | LaunchType: FARGATE 77 | NetworkConfiguration: 78 | AwsvpcConfiguration: 79 | AssignPublicIp: DISABLED 80 | Subnets: 81 | Fn::Split: 82 | - ',' 83 | - Fn::ImportValue: oam-ecs-PrivateSubnets 84 | SecurityGroups: 85 | - !Ref ContainerSecurityGroup 86 | LoadBalancers: 87 | - ContainerName: server 88 | ContainerPort: 8080 89 | TargetGroupArn: !Ref TargetGroupServer8080 90 | HealthCheckGracePeriodSeconds: 5 91 | DependsOn: 92 | - LBListenerServer8080 93 | 94 | 95 | 96 | SGLoadBalancerToContainers: 97 | Type: AWS::EC2::SecurityGroupIngress 98 | Properties: 99 | Description: Ingress from anywhere on the internet through the public NLB 100 | GroupId: !Ref ContainerSecurityGroup 101 | IpProtocol: '-1' 102 | CidrIp: 0.0.0.0/0 103 | 104 | PublicLoadBalancer: 105 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 106 | Properties: 107 | Type: network 108 | Scheme: internet-facing 109 | Subnets: 110 | Fn::Split: 111 | - ',' 112 | - Fn::ImportValue: oam-ecs-PublicSubnets 113 | 114 | LBListenerServer8080: 115 | Type: AWS::ElasticLoadBalancingV2::Listener 116 | Properties: 117 | DefaultActions: 118 | - TargetGroupArn: !Ref TargetGroupServer8080 119 | Type: 'forward' 120 | LoadBalancerArn: !Ref 'PublicLoadBalancer' 121 | Port: 8080 122 | Protocol: TCP 123 | 124 | TargetGroupServer8080: 125 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 126 | Properties: 127 | Protocol: TCP 128 | TargetType: ip 129 | Port: 8080 130 | VpcId: 131 | Fn::ImportValue: oam-ecs-VpcId 132 | TargetGroupAttributes: 133 | - Key: deregistration_delay.timeout_seconds 134 | Value: '30' 135 | 136 | HealthCheckProtocol: HTTP 137 | HealthCheckPath: /ok 138 | HealthCheckPort: '8081' 139 | HealthCheckTimeoutSeconds: 3 140 | 141 | HealthCheckIntervalSeconds: 12 142 | HealthyThresholdCount: 2 143 | UnhealthyThresholdCount: 4 144 | 145 | 146 | 147 | Outputs: 148 | CloudFormationStackConsole: 149 | Description: The AWS console deep-link for the CloudFormation stack 150 | Value: !Sub https://console.aws.amazon.com/cloudformation/home?region=${AWS::Region}#/stacks/stackinfo?stackId=${AWS::StackName} 151 | 152 | ECSServiceConsole: 153 | Description: The AWS console deep-link for the ECS service 154 | Value: !Sub https://console.aws.amazon.com/ecs/home?region=${AWS::Region}#/clusters/oam-ecs/services/${Service.Name} 155 | 156 | ServerPort8080Endpoint: 157 | Description: The endpoint for container Server on port 8080 158 | Value: !Sub '${PublicLoadBalancer.DNSName}:8080' 159 | 160 | -------------------------------------------------------------------------------- /integ-tests/schematics/complex.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1alpha1 2 | kind: ComponentSchematic 3 | metadata: 4 | name: nginx-replicated 5 | labels: 6 | app: my-nginx-replicated-app 7 | annotations: 8 | version: "1.0.1" 9 | description: A worker that runs nginx 10 | spec: 11 | workloadType: core.oam.dev/v1alpha1.Worker 12 | osType: linux 13 | arch: arm64 14 | containers: 15 | - name: server 16 | image: nginx:latest 17 | cmd: 18 | - nginx 19 | args: 20 | - "--debug" 21 | config: 22 | - path: "/etc/access/default_user.txt" 23 | value: "admin" 24 | - path: "/var/run/db-data" 25 | fromParam: "sourceData" 26 | resources: 27 | cpu: 28 | required: 4 29 | memory: 30 | required: 10G 31 | gpu: 32 | required: 1.0 33 | volumes: 34 | - name: "configuration" 35 | mountPath: /etc/config 36 | accessMode: RO 37 | sharingPolicy: Shared 38 | disk: 39 | required: "2G" 40 | ephemeral: n 41 | livenessProbe: 42 | exec: 43 | command: 44 | - "wget" 45 | - "-qO" 46 | - "-" 47 | - "http://localhost" 48 | initialDelaySeconds: 5 49 | periodSeconds: 12 50 | timeoutSeconds: 3 51 | successThreshold: 2 52 | failureThreshold: 4 53 | --- 54 | apiVersion: core.oam.dev/v1alpha1 55 | kind: ComponentSchematic 56 | metadata: 57 | name: nginx-replicated-server 58 | labels: 59 | app: my-nginx-replicated-app-server 60 | annotations: 61 | version: "1.0.1" 62 | description: A worker that runs nginx 63 | spec: 64 | workloadType: core.oam.dev/v1alpha1.Server 65 | osType: linux 66 | arch: arm64 67 | containers: 68 | - name: server 69 | image: nginx:latest 70 | cmd: 71 | - nginx 72 | args: 73 | - "--debug" 74 | config: 75 | - path: "/etc/access/default_user.txt" 76 | value: "admin" 77 | - path: "/var/run/db-data" 78 | fromParam: "sourceData" 79 | resources: 80 | cpu: 81 | required: 2 82 | memory: 83 | required: 14G 84 | gpu: 85 | required: 2.0 86 | volumes: 87 | - name: "configuration" 88 | mountPath: /etc/config 89 | accessMode: RO 90 | sharingPolicy: Shared 91 | disk: 92 | required: "2G" 93 | ephemeral: n 94 | ports: 95 | - name: http 96 | containerPort: 8080 97 | protocol: tcp 98 | livenessProbe: 99 | httpGet: 100 | port: 8081 101 | path: /ok 102 | initialDelaySeconds: 5 103 | periodSeconds: 12 104 | timeoutSeconds: 3 105 | successThreshold: 2 106 | failureThreshold: 4 107 | readinessProbe: 108 | httpGet: 109 | port: 8084 110 | path: /ok-ready 111 | initialDelaySeconds: 1 112 | periodSeconds: 2 113 | timeoutSeconds: 4 114 | successThreshold: 3 115 | failureThreshold: 5 116 | --- 117 | apiVersion: core.oam.dev/v1alpha1 118 | kind: ApplicationConfiguration 119 | metadata: 120 | name: complex-example 121 | annotations: 122 | version: v1.0.0 123 | description: "Complex worker and server example" 124 | spec: 125 | variables: 126 | components: 127 | - componentName: nginx-replicated-server 128 | instanceName: web-front-end 129 | - componentName: nginx-replicated 130 | instanceName: backend 131 | -------------------------------------------------------------------------------- /integ-tests/schematics/example-server.expected.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: Amazon ECS infrastructure for example-app example-server 3 | 4 | Resources: 5 | LogGroup: 6 | Type: AWS::Logs::LogGroup 7 | Properties: 8 | LogGroupName: oam-ecs-example-app-example-server 9 | 10 | TaskDefinition: 11 | Type: AWS::ECS::TaskDefinition 12 | Properties: 13 | Family: oam-ecs-example-app-example-server 14 | NetworkMode: awsvpc 15 | RequiresCompatibilities: 16 | - FARGATE 17 | Cpu: 0.25 vcpu 18 | Memory: '512' 19 | ExecutionRoleArn: !GetAtt ExecutionRole.Arn 20 | ContainerDefinitions: 21 | - Name: server 22 | Image: nginxdemos/hello 23 | Command: 24 | - "nginx-debug" 25 | - "-g" 26 | - "daemon off;" 27 | Environment: 28 | - Name: TEST 29 | Value: "Hello" 30 | - Name: PARAM 31 | Value: "Everyone" 32 | PortMappings: 33 | - ContainerPort: 80 34 | Protocol: tcp 35 | LogConfiguration: 36 | LogDriver: awslogs 37 | Options: 38 | awslogs-region: !Ref AWS::Region 39 | awslogs-group: !Ref LogGroup 40 | awslogs-stream-prefix: oam-ecs 41 | 42 | ExecutionRole: 43 | Type: AWS::IAM::Role 44 | Properties: 45 | AssumeRolePolicyDocument: 46 | Statement: 47 | - Effect: Allow 48 | Principal: 49 | Service: ecs-tasks.amazonaws.com 50 | Action: 'sts:AssumeRole' 51 | 52 | ManagedPolicyArns: 53 | - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' 54 | 55 | ContainerSecurityGroup: 56 | Type: AWS::EC2::SecurityGroup 57 | Properties: 58 | GroupDescription: oam-ecs-example-app-example-server-ContainerSecurityGroup 59 | VpcId: 60 | Fn::ImportValue: oam-ecs-VpcId 61 | 62 | Service: 63 | Type: AWS::ECS::Service 64 | Properties: 65 | Cluster: 66 | Fn::ImportValue: oam-ecs-ECSCluster 67 | TaskDefinition: !Ref TaskDefinition 68 | DeploymentConfiguration: 69 | MinimumHealthyPercent: 100 70 | MaximumPercent: 200 71 | DesiredCount: 1 72 | LaunchType: FARGATE 73 | NetworkConfiguration: 74 | AwsvpcConfiguration: 75 | AssignPublicIp: DISABLED 76 | Subnets: 77 | Fn::Split: 78 | - ',' 79 | - Fn::ImportValue: oam-ecs-PrivateSubnets 80 | SecurityGroups: 81 | - !Ref ContainerSecurityGroup 82 | LoadBalancers: 83 | - ContainerName: server 84 | ContainerPort: 80 85 | TargetGroupArn: !Ref TargetGroupServer80 86 | HealthCheckGracePeriodSeconds: 0 87 | DependsOn: 88 | - LBListenerServer80 89 | 90 | 91 | 92 | SGLoadBalancerToContainers: 93 | Type: AWS::EC2::SecurityGroupIngress 94 | Properties: 95 | Description: Ingress from anywhere on the internet through the public NLB 96 | GroupId: !Ref ContainerSecurityGroup 97 | IpProtocol: '-1' 98 | CidrIp: 0.0.0.0/0 99 | 100 | PublicLoadBalancer: 101 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 102 | Properties: 103 | Type: network 104 | Scheme: internet-facing 105 | Subnets: 106 | Fn::Split: 107 | - ',' 108 | - Fn::ImportValue: oam-ecs-PublicSubnets 109 | 110 | LBListenerServer80: 111 | Type: AWS::ElasticLoadBalancingV2::Listener 112 | Properties: 113 | DefaultActions: 114 | - TargetGroupArn: !Ref TargetGroupServer80 115 | Type: 'forward' 116 | LoadBalancerArn: !Ref 'PublicLoadBalancer' 117 | Port: 80 118 | Protocol: TCP 119 | 120 | TargetGroupServer80: 121 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 122 | Properties: 123 | Protocol: TCP 124 | TargetType: ip 125 | Port: 80 126 | VpcId: 127 | Fn::ImportValue: oam-ecs-VpcId 128 | TargetGroupAttributes: 129 | - Key: deregistration_delay.timeout_seconds 130 | Value: '30' 131 | 132 | 133 | 134 | Outputs: 135 | CloudFormationStackConsole: 136 | Description: The AWS console deep-link for the CloudFormation stack 137 | Value: !Sub https://console.aws.amazon.com/cloudformation/home?region=${AWS::Region}#/stacks/stackinfo?stackId=${AWS::StackName} 138 | 139 | ECSServiceConsole: 140 | Description: The AWS console deep-link for the ECS service 141 | Value: !Sub https://console.aws.amazon.com/ecs/home?region=${AWS::Region}#/clusters/oam-ecs/services/${Service.Name} 142 | 143 | ServerPort80Endpoint: 144 | Description: The endpoint for container Server on port 80 145 | Value: !Sub '${PublicLoadBalancer.DNSName}:80' 146 | 147 | -------------------------------------------------------------------------------- /integ-tests/schematics/example-worker.expected.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: Amazon ECS infrastructure for example-app example-worker 3 | 4 | Resources: 5 | LogGroup: 6 | Type: AWS::Logs::LogGroup 7 | Properties: 8 | LogGroupName: oam-ecs-example-app-example-worker 9 | 10 | TaskDefinition: 11 | Type: AWS::ECS::TaskDefinition 12 | Properties: 13 | Family: oam-ecs-example-app-example-worker 14 | NetworkMode: awsvpc 15 | RequiresCompatibilities: 16 | - FARGATE 17 | Cpu: 0.25 vcpu 18 | Memory: '512' 19 | ExecutionRoleArn: !GetAtt ExecutionRole.Arn 20 | ContainerDefinitions: 21 | - Name: worker 22 | Image: nginxdemos/hello:plain-text 23 | HealthCheck: 24 | Command: 25 | - "wget" 26 | - "-qO" 27 | - "-" 28 | - "http://localhost" 29 | Interval: 10 30 | Retries: 3 31 | StartPeriod: 0 32 | Timeout: 2 33 | LogConfiguration: 34 | LogDriver: awslogs 35 | Options: 36 | awslogs-region: !Ref AWS::Region 37 | awslogs-group: !Ref LogGroup 38 | awslogs-stream-prefix: oam-ecs 39 | 40 | ExecutionRole: 41 | Type: AWS::IAM::Role 42 | Properties: 43 | AssumeRolePolicyDocument: 44 | Statement: 45 | - Effect: Allow 46 | Principal: 47 | Service: ecs-tasks.amazonaws.com 48 | Action: 'sts:AssumeRole' 49 | 50 | ManagedPolicyArns: 51 | - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' 52 | 53 | ContainerSecurityGroup: 54 | Type: AWS::EC2::SecurityGroup 55 | Properties: 56 | GroupDescription: oam-ecs-example-app-example-worker-ContainerSecurityGroup 57 | VpcId: 58 | Fn::ImportValue: oam-ecs-VpcId 59 | 60 | Service: 61 | Type: AWS::ECS::Service 62 | Properties: 63 | Cluster: 64 | Fn::ImportValue: oam-ecs-ECSCluster 65 | TaskDefinition: !Ref TaskDefinition 66 | DeploymentConfiguration: 67 | MinimumHealthyPercent: 100 68 | MaximumPercent: 200 69 | DesiredCount: 2 70 | LaunchType: FARGATE 71 | NetworkConfiguration: 72 | AwsvpcConfiguration: 73 | AssignPublicIp: DISABLED 74 | Subnets: 75 | Fn::Split: 76 | - ',' 77 | - Fn::ImportValue: oam-ecs-PrivateSubnets 78 | SecurityGroups: 79 | - !Ref ContainerSecurityGroup 80 | 81 | 82 | 83 | Outputs: 84 | CloudFormationStackConsole: 85 | Description: The AWS console deep-link for the CloudFormation stack 86 | Value: !Sub https://console.aws.amazon.com/cloudformation/home?region=${AWS::Region}#/stacks/stackinfo?stackId=${AWS::StackName} 87 | 88 | ECSServiceConsole: 89 | Description: The AWS console deep-link for the ECS service 90 | Value: !Sub https://console.aws.amazon.com/ecs/home?region=${AWS::Region}#/clusters/oam-ecs/services/${Service.Name} 91 | 92 | -------------------------------------------------------------------------------- /integ-tests/schematics/extended-workload-type.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1alpha1 2 | kind: ComponentSchematic 3 | metadata: 4 | name: worker-with-extended-workload-type 5 | spec: 6 | workloadType: ecs.amazonaws.com/v1.ECSService 7 | osType: linux 8 | containers: 9 | - name: worker 10 | image: nginx:latest 11 | -------------------------------------------------------------------------------- /integ-tests/schematics/manually-scaled-frontend.expected.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: Amazon ECS infrastructure for manual-scaler-app web-front-end 3 | 4 | Resources: 5 | LogGroup: 6 | Type: AWS::Logs::LogGroup 7 | Properties: 8 | LogGroupName: oam-ecs-manual-scaler-app-web-front-end 9 | 10 | TaskDefinition: 11 | Type: AWS::ECS::TaskDefinition 12 | Properties: 13 | Family: oam-ecs-manual-scaler-app-web-front-end 14 | NetworkMode: awsvpc 15 | RequiresCompatibilities: 16 | - FARGATE 17 | Cpu: 4.00 vcpu 18 | Memory: '10240' 19 | ExecutionRoleArn: !GetAtt ExecutionRole.Arn 20 | ContainerDefinitions: 21 | - Name: server 22 | Image: nginx:latest 23 | PortMappings: 24 | - ContainerPort: 9001 25 | Protocol: tcp 26 | LogConfiguration: 27 | LogDriver: awslogs 28 | Options: 29 | awslogs-region: !Ref AWS::Region 30 | awslogs-group: !Ref LogGroup 31 | awslogs-stream-prefix: oam-ecs 32 | 33 | ExecutionRole: 34 | Type: AWS::IAM::Role 35 | Properties: 36 | AssumeRolePolicyDocument: 37 | Statement: 38 | - Effect: Allow 39 | Principal: 40 | Service: ecs-tasks.amazonaws.com 41 | Action: 'sts:AssumeRole' 42 | 43 | ManagedPolicyArns: 44 | - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' 45 | 46 | ContainerSecurityGroup: 47 | Type: AWS::EC2::SecurityGroup 48 | Properties: 49 | GroupDescription: oam-ecs-manual-scaler-app-web-front-end-ContainerSecurityGroup 50 | VpcId: 51 | Fn::ImportValue: oam-ecs-VpcId 52 | 53 | Service: 54 | Type: AWS::ECS::Service 55 | Properties: 56 | Cluster: 57 | Fn::ImportValue: oam-ecs-ECSCluster 58 | TaskDefinition: !Ref TaskDefinition 59 | DeploymentConfiguration: 60 | MinimumHealthyPercent: 100 61 | MaximumPercent: 200 62 | DesiredCount: 5 63 | LaunchType: FARGATE 64 | NetworkConfiguration: 65 | AwsvpcConfiguration: 66 | AssignPublicIp: DISABLED 67 | Subnets: 68 | Fn::Split: 69 | - ',' 70 | - Fn::ImportValue: oam-ecs-PrivateSubnets 71 | SecurityGroups: 72 | - !Ref ContainerSecurityGroup 73 | LoadBalancers: 74 | - ContainerName: server 75 | ContainerPort: 9001 76 | TargetGroupArn: !Ref TargetGroupServer9001 77 | HealthCheckGracePeriodSeconds: 0 78 | DependsOn: 79 | - LBListenerServer9001 80 | 81 | 82 | 83 | SGLoadBalancerToContainers: 84 | Type: AWS::EC2::SecurityGroupIngress 85 | Properties: 86 | Description: Ingress from anywhere on the internet through the public NLB 87 | GroupId: !Ref ContainerSecurityGroup 88 | IpProtocol: '-1' 89 | CidrIp: 0.0.0.0/0 90 | 91 | PublicLoadBalancer: 92 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 93 | Properties: 94 | Type: network 95 | Scheme: internet-facing 96 | Subnets: 97 | Fn::Split: 98 | - ',' 99 | - Fn::ImportValue: oam-ecs-PublicSubnets 100 | 101 | LBListenerServer9001: 102 | Type: AWS::ElasticLoadBalancingV2::Listener 103 | Properties: 104 | DefaultActions: 105 | - TargetGroupArn: !Ref TargetGroupServer9001 106 | Type: 'forward' 107 | LoadBalancerArn: !Ref 'PublicLoadBalancer' 108 | Port: 9001 109 | Protocol: TCP 110 | 111 | TargetGroupServer9001: 112 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 113 | Properties: 114 | Protocol: TCP 115 | TargetType: ip 116 | Port: 9001 117 | VpcId: 118 | Fn::ImportValue: oam-ecs-VpcId 119 | TargetGroupAttributes: 120 | - Key: deregistration_delay.timeout_seconds 121 | Value: '30' 122 | 123 | 124 | 125 | Outputs: 126 | CloudFormationStackConsole: 127 | Description: The AWS console deep-link for the CloudFormation stack 128 | Value: !Sub https://console.aws.amazon.com/cloudformation/home?region=${AWS::Region}#/stacks/stackinfo?stackId=${AWS::StackName} 129 | 130 | ECSServiceConsole: 131 | Description: The AWS console deep-link for the ECS service 132 | Value: !Sub https://console.aws.amazon.com/ecs/home?region=${AWS::Region}#/clusters/oam-ecs/services/${Service.Name} 133 | 134 | ServerPort9001Endpoint: 135 | Description: The endpoint for container Server on port 9001 136 | Value: !Sub '${PublicLoadBalancer.DNSName}:9001' 137 | 138 | -------------------------------------------------------------------------------- /integ-tests/schematics/manually-scaled-frontend.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1alpha1 2 | kind: ApplicationConfiguration 3 | metadata: 4 | name: manual-scaler-app 5 | annotations: 6 | version: v1.0.0 7 | description: "Manually scaled simple app" 8 | spec: 9 | variables: 10 | components: 11 | - componentName: nginx-replicated 12 | instanceName: web-front-end 13 | parameterValues: 14 | traits: 15 | - name: manual-scaler 16 | properties: 17 | replicaCount: 5 18 | -------------------------------------------------------------------------------- /integ-tests/schematics/nginx.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1alpha1 2 | kind: ComponentSchematic 3 | metadata: 4 | name: nginx-replicated 5 | labels: 6 | app: my-nginx-replicated-app 7 | annotations: 8 | version: "1.0.1" 9 | description: A server that runs nginx 10 | spec: 11 | workloadType: core.oam.dev/v1alpha1.Server 12 | osType: linux 13 | containers: 14 | - name: server 15 | image: nginx:latest 16 | resources: 17 | cpu: 18 | required: 4 19 | memory: 20 | required: 10G 21 | ports: 22 | - name: http 23 | containerPort: 9001 24 | protocol: TCP 25 | -------------------------------------------------------------------------------- /integ-tests/schematics/trait.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1alpha1 2 | kind: Trait 3 | metadata: 4 | name: manual-scaler 5 | annotations: 6 | version: v1.0.0 7 | description: "Allow operators to manually scale a workloads that allow multiple replicas." 8 | spec: 9 | appliesTo: 10 | - core.oam.dev/v1alpha1.Server 11 | - core.oam.dev/v1alpha1.Worker 12 | - core.oam.dev/v1alpha1.Task 13 | properties: | 14 | { 15 | "$schema": "http://json-schema.org/draft-07/schema#", 16 | "type": "object", 17 | "required": [ 18 | "replicaCount" 19 | ], 20 | "properties": { 21 | "replicaCount": { 22 | "type": "integer", 23 | "description": "the target number of replicas to scale a component to.", 24 | "minimum": 0 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /integ-tests/schematics/twitter-bot.backend.expected.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: Amazon ECS infrastructure for twitter-bot backend-svc 3 | 4 | Resources: 5 | LogGroup: 6 | Type: AWS::Logs::LogGroup 7 | Properties: 8 | LogGroupName: oam-ecs-twitter-bot-backend-svc 9 | 10 | TaskDefinition: 11 | Type: AWS::ECS::TaskDefinition 12 | Properties: 13 | Family: oam-ecs-twitter-bot-backend-svc 14 | NetworkMode: awsvpc 15 | RequiresCompatibilities: 16 | - FARGATE 17 | Cpu: 1.00 vcpu 18 | Memory: '2048' 19 | ExecutionRoleArn: !GetAtt ExecutionRole.Arn 20 | ContainerDefinitions: 21 | - Name: my-twitter-bot-backend 22 | Image: example/my-twitter-bot-backend@sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b 23 | MountPoints: 24 | - ContainerPath: /var/lib/my-twitter-bot/conf 25 | ReadOnly: false 26 | SourceVolume: config 27 | Environment: 28 | - Name: TWITTER_CONSUMER_KEY 29 | Value: "key" 30 | - Name: TWITTER_CONSUMER_SECRET 31 | Value: "secret" 32 | - Name: TWITTER_ACCESS_TOKEN 33 | Value: "token" 34 | - Name: TWITTER_ACCESS_TOKEN_SECRET 35 | Value: "token-secret" 36 | PortMappings: 37 | - ContainerPort: 8080 38 | Protocol: tcp 39 | LogConfiguration: 40 | LogDriver: awslogs 41 | Options: 42 | awslogs-region: !Ref AWS::Region 43 | awslogs-group: !Ref LogGroup 44 | awslogs-stream-prefix: oam-ecs 45 | Volumes: 46 | - Name: config 47 | 48 | ExecutionRole: 49 | Type: AWS::IAM::Role 50 | Properties: 51 | AssumeRolePolicyDocument: 52 | Statement: 53 | - Effect: Allow 54 | Principal: 55 | Service: ecs-tasks.amazonaws.com 56 | Action: 'sts:AssumeRole' 57 | 58 | ManagedPolicyArns: 59 | - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' 60 | 61 | ContainerSecurityGroup: 62 | Type: AWS::EC2::SecurityGroup 63 | Properties: 64 | GroupDescription: oam-ecs-twitter-bot-backend-svc-ContainerSecurityGroup 65 | VpcId: 66 | Fn::ImportValue: oam-ecs-VpcId 67 | 68 | Service: 69 | Type: AWS::ECS::Service 70 | Properties: 71 | Cluster: 72 | Fn::ImportValue: oam-ecs-ECSCluster 73 | TaskDefinition: !Ref TaskDefinition 74 | DeploymentConfiguration: 75 | MinimumHealthyPercent: 100 76 | MaximumPercent: 200 77 | DesiredCount: 1 78 | LaunchType: FARGATE 79 | NetworkConfiguration: 80 | AwsvpcConfiguration: 81 | AssignPublicIp: DISABLED 82 | Subnets: 83 | Fn::Split: 84 | - ',' 85 | - Fn::ImportValue: oam-ecs-PrivateSubnets 86 | SecurityGroups: 87 | - !Ref ContainerSecurityGroup 88 | LoadBalancers: 89 | - ContainerName: my-twitter-bot-backend 90 | ContainerPort: 8080 91 | TargetGroupArn: !Ref TargetGroupMyTwitterBotBackend8080 92 | HealthCheckGracePeriodSeconds: 0 93 | DependsOn: 94 | - LBListenerMyTwitterBotBackend8080 95 | 96 | 97 | 98 | SGLoadBalancerToContainers: 99 | Type: AWS::EC2::SecurityGroupIngress 100 | Properties: 101 | Description: Ingress from anywhere on the internet through the public NLB 102 | GroupId: !Ref ContainerSecurityGroup 103 | IpProtocol: '-1' 104 | CidrIp: 0.0.0.0/0 105 | 106 | PublicLoadBalancer: 107 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 108 | Properties: 109 | Type: network 110 | Scheme: internet-facing 111 | Subnets: 112 | Fn::Split: 113 | - ',' 114 | - Fn::ImportValue: oam-ecs-PublicSubnets 115 | 116 | LBListenerMyTwitterBotBackend8080: 117 | Type: AWS::ElasticLoadBalancingV2::Listener 118 | Properties: 119 | DefaultActions: 120 | - TargetGroupArn: !Ref TargetGroupMyTwitterBotBackend8080 121 | Type: 'forward' 122 | LoadBalancerArn: !Ref 'PublicLoadBalancer' 123 | Port: 8080 124 | Protocol: TCP 125 | 126 | TargetGroupMyTwitterBotBackend8080: 127 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 128 | Properties: 129 | Protocol: TCP 130 | TargetType: ip 131 | Port: 8080 132 | VpcId: 133 | Fn::ImportValue: oam-ecs-VpcId 134 | TargetGroupAttributes: 135 | - Key: deregistration_delay.timeout_seconds 136 | Value: '30' 137 | 138 | HealthCheckProtocol: HTTP 139 | HealthCheckPath: /healthz 140 | HealthCheckPort: '8080' 141 | HealthCheckTimeoutSeconds: 6 142 | 143 | HealthCheckIntervalSeconds: 10 144 | HealthyThresholdCount: 2 145 | UnhealthyThresholdCount: 3 146 | 147 | 148 | 149 | Outputs: 150 | CloudFormationStackConsole: 151 | Description: The AWS console deep-link for the CloudFormation stack 152 | Value: !Sub https://console.aws.amazon.com/cloudformation/home?region=${AWS::Region}#/stacks/stackinfo?stackId=${AWS::StackName} 153 | 154 | ECSServiceConsole: 155 | Description: The AWS console deep-link for the ECS service 156 | Value: !Sub https://console.aws.amazon.com/ecs/home?region=${AWS::Region}#/clusters/oam-ecs/services/${Service.Name} 157 | 158 | MyTwitterBotBackendPort8080Endpoint: 159 | Description: The endpoint for container MyTwitterBotBackend on port 8080 160 | Value: !Sub '${PublicLoadBalancer.DNSName}:8080' 161 | 162 | -------------------------------------------------------------------------------- /integ-tests/schematics/twitter-bot.frontend.expected.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: Amazon ECS infrastructure for twitter-bot web-front-end 3 | 4 | Resources: 5 | LogGroup: 6 | Type: AWS::Logs::LogGroup 7 | Properties: 8 | LogGroupName: oam-ecs-twitter-bot-web-front-end 9 | 10 | TaskDefinition: 11 | Type: AWS::ECS::TaskDefinition 12 | Properties: 13 | Family: oam-ecs-twitter-bot-web-front-end 14 | NetworkMode: awsvpc 15 | RequiresCompatibilities: 16 | - FARGATE 17 | Cpu: 2.00 vcpu 18 | Memory: '4096' 19 | ExecutionRoleArn: !GetAtt ExecutionRole.Arn 20 | ContainerDefinitions: 21 | - Name: my-twitter-bot-frontend 22 | Image: example/my-twitter-bot-frontend@sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b 23 | Environment: 24 | - Name: USERNAME 25 | Value: "hello" 26 | - Name: PASSWORD 27 | Value: "world" 28 | - Name: BACKEND_ADDRESS 29 | Value: "http://hello.world" 30 | PortMappings: 31 | - ContainerPort: 8080 32 | Protocol: tcp 33 | LogConfiguration: 34 | LogDriver: awslogs 35 | Options: 36 | awslogs-region: !Ref AWS::Region 37 | awslogs-group: !Ref LogGroup 38 | awslogs-stream-prefix: oam-ecs 39 | 40 | ExecutionRole: 41 | Type: AWS::IAM::Role 42 | Properties: 43 | AssumeRolePolicyDocument: 44 | Statement: 45 | - Effect: Allow 46 | Principal: 47 | Service: ecs-tasks.amazonaws.com 48 | Action: 'sts:AssumeRole' 49 | 50 | ManagedPolicyArns: 51 | - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' 52 | 53 | ContainerSecurityGroup: 54 | Type: AWS::EC2::SecurityGroup 55 | Properties: 56 | GroupDescription: oam-ecs-twitter-bot-web-front-end-ContainerSecurityGroup 57 | VpcId: 58 | Fn::ImportValue: oam-ecs-VpcId 59 | 60 | Service: 61 | Type: AWS::ECS::Service 62 | Properties: 63 | Cluster: 64 | Fn::ImportValue: oam-ecs-ECSCluster 65 | TaskDefinition: !Ref TaskDefinition 66 | DeploymentConfiguration: 67 | MinimumHealthyPercent: 100 68 | MaximumPercent: 200 69 | DesiredCount: 1 70 | LaunchType: FARGATE 71 | NetworkConfiguration: 72 | AwsvpcConfiguration: 73 | AssignPublicIp: DISABLED 74 | Subnets: 75 | Fn::Split: 76 | - ',' 77 | - Fn::ImportValue: oam-ecs-PrivateSubnets 78 | SecurityGroups: 79 | - !Ref ContainerSecurityGroup 80 | LoadBalancers: 81 | - ContainerName: my-twitter-bot-frontend 82 | ContainerPort: 8080 83 | TargetGroupArn: !Ref TargetGroupMyTwitterBotFrontend8080 84 | HealthCheckGracePeriodSeconds: 0 85 | DependsOn: 86 | - LBListenerMyTwitterBotFrontend8080 87 | 88 | 89 | 90 | SGLoadBalancerToContainers: 91 | Type: AWS::EC2::SecurityGroupIngress 92 | Properties: 93 | Description: Ingress from anywhere on the internet through the public NLB 94 | GroupId: !Ref ContainerSecurityGroup 95 | IpProtocol: '-1' 96 | CidrIp: 0.0.0.0/0 97 | 98 | PublicLoadBalancer: 99 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 100 | Properties: 101 | Type: network 102 | Scheme: internet-facing 103 | Subnets: 104 | Fn::Split: 105 | - ',' 106 | - Fn::ImportValue: oam-ecs-PublicSubnets 107 | 108 | LBListenerMyTwitterBotFrontend8080: 109 | Type: AWS::ElasticLoadBalancingV2::Listener 110 | Properties: 111 | DefaultActions: 112 | - TargetGroupArn: !Ref TargetGroupMyTwitterBotFrontend8080 113 | Type: 'forward' 114 | LoadBalancerArn: !Ref 'PublicLoadBalancer' 115 | Port: 8080 116 | Protocol: TCP 117 | 118 | TargetGroupMyTwitterBotFrontend8080: 119 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 120 | Properties: 121 | Protocol: TCP 122 | TargetType: ip 123 | Port: 8080 124 | VpcId: 125 | Fn::ImportValue: oam-ecs-VpcId 126 | TargetGroupAttributes: 127 | - Key: deregistration_delay.timeout_seconds 128 | Value: '30' 129 | 130 | HealthCheckProtocol: HTTP 131 | HealthCheckPath: /healthz 132 | HealthCheckPort: '8080' 133 | HealthCheckTimeoutSeconds: 6 134 | 135 | HealthCheckIntervalSeconds: 10 136 | HealthyThresholdCount: 2 137 | UnhealthyThresholdCount: 3 138 | 139 | 140 | 141 | Outputs: 142 | CloudFormationStackConsole: 143 | Description: The AWS console deep-link for the CloudFormation stack 144 | Value: !Sub https://console.aws.amazon.com/cloudformation/home?region=${AWS::Region}#/stacks/stackinfo?stackId=${AWS::StackName} 145 | 146 | ECSServiceConsole: 147 | Description: The AWS console deep-link for the ECS service 148 | Value: !Sub https://console.aws.amazon.com/ecs/home?region=${AWS::Region}#/clusters/oam-ecs/services/${Service.Name} 149 | 150 | MyTwitterBotFrontendPort8080Endpoint: 151 | Description: The endpoint for container MyTwitterBotFrontend on port 8080 152 | Value: !Sub '${PublicLoadBalancer.DNSName}:8080' 153 | 154 | -------------------------------------------------------------------------------- /integ-tests/schematics/twitter-bot.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1alpha1 2 | kind: ApplicationConfiguration 3 | metadata: 4 | name: twitter-bot 5 | annotations: 6 | version: v1.0.0 7 | description: "Example Twitter bot app" 8 | spec: 9 | components: 10 | - componentName: frontend 11 | instanceName: web-front-end 12 | parameterValues: 13 | - name: username 14 | value: hello 15 | - name: password 16 | value: world 17 | - name: backend-address 18 | value: http://hello.world 19 | 20 | - componentName: admin-backend 21 | instanceName: backend-svc 22 | parameterValues: 23 | - name: twitter-consumer-key 24 | value: key 25 | - name: twitter-consumer-secret 26 | value: secret 27 | - name: twitter-access-token 28 | value: token 29 | - name: twitter-access-token-secret 30 | value: token-secret 31 | --- 32 | apiVersion: core.oam.dev/v1alpha1 33 | kind: ComponentSchematic 34 | metadata: 35 | name: frontend 36 | annotations: 37 | version: v1.0.0 38 | description: > 39 | Sample component schematic that describes the administrative interface for our Twitter bot. 40 | spec: 41 | workloadType: core.oam.dev/v1alpha1.Server 42 | osType: linux 43 | parameters: 44 | - name: username 45 | description: Basic auth username for accessing the administrative interface 46 | type: string 47 | required: true 48 | - name: password 49 | description: Basic auth password for accessing the administrative interface 50 | type: string 51 | required: true 52 | - name: backend-address 53 | description: Host name or IP of the backend 54 | type: string 55 | required: true 56 | containers: 57 | - name: my-twitter-bot-frontend 58 | image: example/my-twitter-bot-frontend@sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b 59 | resources: 60 | cpu: 61 | required: 2.0 62 | memory: 63 | required: 1G 64 | ports: 65 | - name: http 66 | containerPort: 8080 67 | env: 68 | - name: USERNAME 69 | fromParam: 'username' 70 | - name: PASSWORD 71 | fromParam: 'password' 72 | - name: BACKEND_ADDRESS 73 | fromParam: 'backend-address' 74 | livenessProbe: 75 | httpGet: 76 | port: 8080 77 | path: /healthz 78 | readinessProbe: 79 | httpGet: 80 | port: 8080 81 | path: /healthz 82 | --- 83 | apiVersion: core.oam.dev/v1alpha1 84 | kind: ComponentSchematic 85 | metadata: 86 | name: admin-backend 87 | annotations: 88 | version: v1.0.0 89 | description: > 90 | Sample component schematic that describes the backend for our Twitter bot. 91 | spec: 92 | workloadType: core.oam.dev/v1alpha1.Server 93 | osType: linux 94 | parameters: 95 | - name: twitter-consumer-key 96 | description: Twitter API consumer key 97 | type: string 98 | required: true 99 | - name: twitter-consumer-secret 100 | description: Twitter API consumer secret 101 | type: string 102 | required: true 103 | - name: twitter-access-token 104 | description: Twitter API access token 105 | type: string 106 | required: true 107 | - name: twitter-access-token-secret 108 | description: Twitter API access token secret 109 | type: string 110 | required: true 111 | containers: 112 | - name: my-twitter-bot-backend 113 | image: example/my-twitter-bot-backend@sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b 114 | resources: 115 | cpu: 116 | required: 1.0 117 | memory: 118 | required: 100M 119 | volumes: 120 | - name: config 121 | mountPath: /var/lib/my-twitter-bot/conf 122 | accessMode: RW 123 | sharingPolicy: Exclusive 124 | ports: 125 | - name: http 126 | containerPort: 8080 127 | env: 128 | - name: TWITTER_CONSUMER_KEY 129 | fromParam: 'twitter-consumer-key' 130 | - name: TWITTER_CONSUMER_SECRET 131 | fromParam: 'twitter-consumer-secret' 132 | - name: TWITTER_ACCESS_TOKEN 133 | fromParam: 'twitter-access-token' 134 | - name: TWITTER_ACCESS_TOKEN_SECRET 135 | fromParam: 'twitter-access-token-secret' 136 | livenessProbe: 137 | httpGet: 138 | port: 8080 139 | path: /healthz 140 | readinessProbe: 141 | httpGet: 142 | port: 8080 143 | path: /healthz -------------------------------------------------------------------------------- /integ-tests/schematics/webserver.backend.expected.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: Amazon ECS infrastructure for webserver-app backend-svc 3 | 4 | Resources: 5 | LogGroup: 6 | Type: AWS::Logs::LogGroup 7 | Properties: 8 | LogGroupName: oam-ecs-webserver-app-backend-svc 9 | 10 | TaskDefinition: 11 | Type: AWS::ECS::TaskDefinition 12 | Properties: 13 | Family: oam-ecs-webserver-app-backend-svc 14 | NetworkMode: awsvpc 15 | RequiresCompatibilities: 16 | - FARGATE 17 | Cpu: 2.00 vcpu 18 | Memory: '4096' 19 | ExecutionRoleArn: !GetAtt ExecutionRole.Arn 20 | ContainerDefinitions: 21 | - Name: web 22 | Image: example/backend-api:latest 23 | PortMappings: 24 | - ContainerPort: 4000 25 | Protocol: udp 26 | RepositoryCredentials: 27 | CredentialsParameter: "dockerhub-creds" 28 | LogConfiguration: 29 | LogDriver: awslogs 30 | Options: 31 | awslogs-region: !Ref AWS::Region 32 | awslogs-group: !Ref LogGroup 33 | awslogs-stream-prefix: oam-ecs 34 | - Name: sidecar 35 | Image: example/backend-sidecar:latest 36 | PortMappings: 37 | - ContainerPort: 4001 38 | Protocol: tcp 39 | RepositoryCredentials: 40 | CredentialsParameter: "other-dockerhub-creds" 41 | LogConfiguration: 42 | LogDriver: awslogs 43 | Options: 44 | awslogs-region: !Ref AWS::Region 45 | awslogs-group: !Ref LogGroup 46 | awslogs-stream-prefix: oam-ecs 47 | 48 | ExecutionRole: 49 | Type: AWS::IAM::Role 50 | Properties: 51 | AssumeRolePolicyDocument: 52 | Statement: 53 | - Effect: Allow 54 | Principal: 55 | Service: ecs-tasks.amazonaws.com 56 | Action: 'sts:AssumeRole' 57 | 58 | Policies: 59 | - PolicyName: PrivateRegistryCreds 60 | PolicyDocument: 61 | Version: '2012-10-17' 62 | Statement: 63 | - Effect: 'Allow' 64 | Action: 65 | - 'secretsmanager:GetSecretValue' 66 | Resource: 67 | - !Sub 'arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:dockerhub-creds-??????' 68 | - !Sub 'arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:other-dockerhub-creds-??????' 69 | ManagedPolicyArns: 70 | - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' 71 | 72 | ContainerSecurityGroup: 73 | Type: AWS::EC2::SecurityGroup 74 | Properties: 75 | GroupDescription: oam-ecs-webserver-app-backend-svc-ContainerSecurityGroup 76 | VpcId: 77 | Fn::ImportValue: oam-ecs-VpcId 78 | 79 | Service: 80 | Type: AWS::ECS::Service 81 | Properties: 82 | Cluster: 83 | Fn::ImportValue: oam-ecs-ECSCluster 84 | TaskDefinition: !Ref TaskDefinition 85 | DeploymentConfiguration: 86 | MinimumHealthyPercent: 100 87 | MaximumPercent: 200 88 | DesiredCount: 1 89 | LaunchType: FARGATE 90 | NetworkConfiguration: 91 | AwsvpcConfiguration: 92 | AssignPublicIp: DISABLED 93 | Subnets: 94 | Fn::Split: 95 | - ',' 96 | - Fn::ImportValue: oam-ecs-PrivateSubnets 97 | SecurityGroups: 98 | - !Ref ContainerSecurityGroup 99 | LoadBalancers: 100 | - ContainerName: web 101 | ContainerPort: 4000 102 | TargetGroupArn: !Ref TargetGroupWeb4000 103 | - ContainerName: sidecar 104 | ContainerPort: 4001 105 | TargetGroupArn: !Ref TargetGroupSidecar4001 106 | HealthCheckGracePeriodSeconds: 0 107 | DependsOn: 108 | - LBListenerWeb4000 109 | 110 | - LBListenerSidecar4001 111 | 112 | 113 | 114 | SGLoadBalancerToContainers: 115 | Type: AWS::EC2::SecurityGroupIngress 116 | Properties: 117 | Description: Ingress from anywhere on the internet through the public NLB 118 | GroupId: !Ref ContainerSecurityGroup 119 | IpProtocol: '-1' 120 | CidrIp: 0.0.0.0/0 121 | 122 | PublicLoadBalancer: 123 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 124 | Properties: 125 | Type: network 126 | Scheme: internet-facing 127 | Subnets: 128 | Fn::Split: 129 | - ',' 130 | - Fn::ImportValue: oam-ecs-PublicSubnets 131 | 132 | LBListenerWeb4000: 133 | Type: AWS::ElasticLoadBalancingV2::Listener 134 | Properties: 135 | DefaultActions: 136 | - TargetGroupArn: !Ref TargetGroupWeb4000 137 | Type: 'forward' 138 | LoadBalancerArn: !Ref 'PublicLoadBalancer' 139 | Port: 4000 140 | Protocol: UDP 141 | 142 | TargetGroupWeb4000: 143 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 144 | Properties: 145 | Protocol: UDP 146 | TargetType: ip 147 | Port: 4000 148 | VpcId: 149 | Fn::ImportValue: oam-ecs-VpcId 150 | TargetGroupAttributes: 151 | - Key: deregistration_delay.timeout_seconds 152 | Value: '30' 153 | 154 | 155 | LBListenerSidecar4001: 156 | Type: AWS::ElasticLoadBalancingV2::Listener 157 | Properties: 158 | DefaultActions: 159 | - TargetGroupArn: !Ref TargetGroupSidecar4001 160 | Type: 'forward' 161 | LoadBalancerArn: !Ref 'PublicLoadBalancer' 162 | Port: 4001 163 | Protocol: TCP 164 | 165 | TargetGroupSidecar4001: 166 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 167 | Properties: 168 | Protocol: TCP 169 | TargetType: ip 170 | Port: 4001 171 | VpcId: 172 | Fn::ImportValue: oam-ecs-VpcId 173 | TargetGroupAttributes: 174 | - Key: deregistration_delay.timeout_seconds 175 | Value: '30' 176 | 177 | 178 | 179 | Outputs: 180 | CloudFormationStackConsole: 181 | Description: The AWS console deep-link for the CloudFormation stack 182 | Value: !Sub https://console.aws.amazon.com/cloudformation/home?region=${AWS::Region}#/stacks/stackinfo?stackId=${AWS::StackName} 183 | 184 | ECSServiceConsole: 185 | Description: The AWS console deep-link for the ECS service 186 | Value: !Sub https://console.aws.amazon.com/ecs/home?region=${AWS::Region}#/clusters/oam-ecs/services/${Service.Name} 187 | 188 | WebPort4000Endpoint: 189 | Description: The endpoint for container Web on port 4000 190 | Value: !Sub '${PublicLoadBalancer.DNSName}:4000' 191 | 192 | SidecarPort4001Endpoint: 193 | Description: The endpoint for container Sidecar on port 4001 194 | Value: !Sub '${PublicLoadBalancer.DNSName}:4001' 195 | 196 | -------------------------------------------------------------------------------- /integ-tests/schematics/webserver.frontend.expected.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: Amazon ECS infrastructure for webserver-app web-front-end 3 | 4 | Resources: 5 | LogGroup: 6 | Type: AWS::Logs::LogGroup 7 | Properties: 8 | LogGroupName: oam-ecs-webserver-app-web-front-end 9 | 10 | TaskDefinition: 11 | Type: AWS::ECS::TaskDefinition 12 | Properties: 13 | Family: oam-ecs-webserver-app-web-front-end 14 | NetworkMode: awsvpc 15 | RequiresCompatibilities: 16 | - FARGATE 17 | Cpu: 2.00 vcpu 18 | Memory: '4096' 19 | ExecutionRoleArn: !GetAtt ExecutionRole.Arn 20 | ContainerDefinitions: 21 | - Name: web 22 | Image: example/frontend-svc:latest 23 | Environment: 24 | - Name: MESSAGE 25 | Value: "[fromVariable(message)]" 26 | - Name: TITLE 27 | Value: "Hey you" 28 | PortMappings: 29 | - ContainerPort: 80 30 | Protocol: tcp 31 | RepositoryCredentials: 32 | CredentialsParameter: "dockerhub-creds" 33 | LogConfiguration: 34 | LogDriver: awslogs 35 | Options: 36 | awslogs-region: !Ref AWS::Region 37 | awslogs-group: !Ref LogGroup 38 | awslogs-stream-prefix: oam-ecs 39 | 40 | ExecutionRole: 41 | Type: AWS::IAM::Role 42 | Properties: 43 | AssumeRolePolicyDocument: 44 | Statement: 45 | - Effect: Allow 46 | Principal: 47 | Service: ecs-tasks.amazonaws.com 48 | Action: 'sts:AssumeRole' 49 | 50 | Policies: 51 | - PolicyName: PrivateRegistryCreds 52 | PolicyDocument: 53 | Version: '2012-10-17' 54 | Statement: 55 | - Effect: 'Allow' 56 | Action: 57 | - 'secretsmanager:GetSecretValue' 58 | Resource: 59 | - !Sub 'arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:dockerhub-creds-??????' 60 | ManagedPolicyArns: 61 | - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' 62 | 63 | ContainerSecurityGroup: 64 | Type: AWS::EC2::SecurityGroup 65 | Properties: 66 | GroupDescription: oam-ecs-webserver-app-web-front-end-ContainerSecurityGroup 67 | VpcId: 68 | Fn::ImportValue: oam-ecs-VpcId 69 | 70 | Service: 71 | Type: AWS::ECS::Service 72 | Properties: 73 | Cluster: 74 | Fn::ImportValue: oam-ecs-ECSCluster 75 | TaskDefinition: !Ref TaskDefinition 76 | DeploymentConfiguration: 77 | MinimumHealthyPercent: 100 78 | MaximumPercent: 200 79 | DesiredCount: 1 80 | LaunchType: FARGATE 81 | NetworkConfiguration: 82 | AwsvpcConfiguration: 83 | AssignPublicIp: DISABLED 84 | Subnets: 85 | Fn::Split: 86 | - ',' 87 | - Fn::ImportValue: oam-ecs-PrivateSubnets 88 | SecurityGroups: 89 | - !Ref ContainerSecurityGroup 90 | LoadBalancers: 91 | - ContainerName: web 92 | ContainerPort: 80 93 | TargetGroupArn: !Ref TargetGroupWeb80 94 | HealthCheckGracePeriodSeconds: 0 95 | DependsOn: 96 | - LBListenerWeb80 97 | 98 | 99 | 100 | SGLoadBalancerToContainers: 101 | Type: AWS::EC2::SecurityGroupIngress 102 | Properties: 103 | Description: Ingress from anywhere on the internet through the public NLB 104 | GroupId: !Ref ContainerSecurityGroup 105 | IpProtocol: '-1' 106 | CidrIp: 0.0.0.0/0 107 | 108 | PublicLoadBalancer: 109 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 110 | Properties: 111 | Type: network 112 | Scheme: internet-facing 113 | Subnets: 114 | Fn::Split: 115 | - ',' 116 | - Fn::ImportValue: oam-ecs-PublicSubnets 117 | 118 | LBListenerWeb80: 119 | Type: AWS::ElasticLoadBalancingV2::Listener 120 | Properties: 121 | DefaultActions: 122 | - TargetGroupArn: !Ref TargetGroupWeb80 123 | Type: 'forward' 124 | LoadBalancerArn: !Ref 'PublicLoadBalancer' 125 | Port: 80 126 | Protocol: TCP 127 | 128 | TargetGroupWeb80: 129 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 130 | Properties: 131 | Protocol: TCP 132 | TargetType: ip 133 | Port: 80 134 | VpcId: 135 | Fn::ImportValue: oam-ecs-VpcId 136 | TargetGroupAttributes: 137 | - Key: deregistration_delay.timeout_seconds 138 | Value: '30' 139 | 140 | 141 | 142 | Outputs: 143 | CloudFormationStackConsole: 144 | Description: The AWS console deep-link for the CloudFormation stack 145 | Value: !Sub https://console.aws.amazon.com/cloudformation/home?region=${AWS::Region}#/stacks/stackinfo?stackId=${AWS::StackName} 146 | 147 | ECSServiceConsole: 148 | Description: The AWS console deep-link for the ECS service 149 | Value: !Sub https://console.aws.amazon.com/ecs/home?region=${AWS::Region}#/clusters/oam-ecs/services/${Service.Name} 150 | 151 | WebPort80Endpoint: 152 | Description: The endpoint for container Web on port 80 153 | Value: !Sub '${PublicLoadBalancer.DNSName}:80' 154 | -------------------------------------------------------------------------------- /integ-tests/schematics/webserver.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1alpha1 2 | kind: ApplicationConfiguration 3 | metadata: 4 | name: webserver-app 5 | annotations: 6 | version: v1.0.0 7 | description: "Example webserver app" 8 | spec: 9 | variables: 10 | - name: message 11 | value: "Well hello there" 12 | components: 13 | - componentName: frontend 14 | instanceName: web-front-end 15 | parameterValues: 16 | - name: message 17 | value: "[fromVariable(message)]" 18 | 19 | - componentName: backend 20 | instanceName: backend-svc 21 | --- 22 | apiVersion: core.oam.dev/v1alpha1 23 | kind: ComponentSchematic 24 | metadata: 25 | name: frontend 26 | annotations: 27 | version: v1.0.0 28 | description: "A simple webserver" 29 | spec: 30 | workloadType: core.oam.dev/v1alpha1.Server 31 | parameters: 32 | - name: message 33 | description: The message to display in the web app. 34 | type: string 35 | default: "Hello from my app, too" 36 | - name: title 37 | description: The title to display in the web app. 38 | type: string 39 | default: "Hey you" 40 | containers: 41 | - name: web 42 | env: 43 | - name: MESSAGE 44 | fromParam: message 45 | - name: TITLE 46 | fromParam: title 47 | image: example/frontend-svc:latest 48 | imagePullSecret: dockerhub-creds 49 | resources: 50 | cpu: 51 | required: 1.5 52 | memory: 53 | required: 128M 54 | ports: 55 | - name: port 56 | containerPort: 80 57 | --- 58 | apiVersion: core.oam.dev/v1alpha1 59 | kind: ComponentSchematic 60 | metadata: 61 | name: backend 62 | annotations: 63 | version: v1.0.0 64 | description: "A backend webserver" 65 | spec: 66 | workloadType: core.oam.dev/v1alpha1.Server 67 | containers: 68 | - name: web 69 | image: example/backend-api:latest 70 | imagePullSecret: dockerhub-creds 71 | resources: 72 | cpu: 73 | required: 1.25 74 | memory: 75 | required: 768M 76 | ports: 77 | - name: port 78 | containerPort: 4000 79 | protocol: UDP 80 | - name: sidecar 81 | image: example/backend-sidecar:latest 82 | imagePullSecret: other-dockerhub-creds 83 | resources: 84 | cpu: 85 | required: 0.5 86 | memory: 87 | required: 128M 88 | ports: 89 | - name: port 90 | containerPort: 4001 91 | protocol: TCP 92 | -------------------------------------------------------------------------------- /integ-tests/schematics/worker.expected.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: Amazon ECS infrastructure for simple-worker web-front-end 3 | 4 | Resources: 5 | LogGroup: 6 | Type: AWS::Logs::LogGroup 7 | Properties: 8 | LogGroupName: oam-ecs-simple-worker-web-front-end 9 | 10 | TaskDefinition: 11 | Type: AWS::ECS::TaskDefinition 12 | Properties: 13 | Family: oam-ecs-simple-worker-web-front-end 14 | NetworkMode: awsvpc 15 | RequiresCompatibilities: 16 | - FARGATE 17 | Cpu: 0.25 vcpu 18 | Memory: '512' 19 | ExecutionRoleArn: !GetAtt ExecutionRole.Arn 20 | ContainerDefinitions: 21 | - Name: server 22 | Image: nginx:latest 23 | LogConfiguration: 24 | LogDriver: awslogs 25 | Options: 26 | awslogs-region: !Ref AWS::Region 27 | awslogs-group: !Ref LogGroup 28 | awslogs-stream-prefix: oam-ecs 29 | 30 | ExecutionRole: 31 | Type: AWS::IAM::Role 32 | Properties: 33 | AssumeRolePolicyDocument: 34 | Statement: 35 | - Effect: Allow 36 | Principal: 37 | Service: ecs-tasks.amazonaws.com 38 | Action: 'sts:AssumeRole' 39 | 40 | ManagedPolicyArns: 41 | - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' 42 | 43 | ContainerSecurityGroup: 44 | Type: AWS::EC2::SecurityGroup 45 | Properties: 46 | GroupDescription: oam-ecs-simple-worker-web-front-end-ContainerSecurityGroup 47 | VpcId: 48 | Fn::ImportValue: oam-ecs-VpcId 49 | 50 | Service: 51 | Type: AWS::ECS::Service 52 | Properties: 53 | Cluster: 54 | Fn::ImportValue: oam-ecs-ECSCluster 55 | TaskDefinition: !Ref TaskDefinition 56 | DeploymentConfiguration: 57 | MinimumHealthyPercent: 100 58 | MaximumPercent: 200 59 | DesiredCount: 1 60 | LaunchType: FARGATE 61 | NetworkConfiguration: 62 | AwsvpcConfiguration: 63 | AssignPublicIp: DISABLED 64 | Subnets: 65 | Fn::Split: 66 | - ',' 67 | - Fn::ImportValue: oam-ecs-PrivateSubnets 68 | SecurityGroups: 69 | - !Ref ContainerSecurityGroup 70 | 71 | 72 | 73 | Outputs: 74 | CloudFormationStackConsole: 75 | Description: The AWS console deep-link for the CloudFormation stack 76 | Value: !Sub https://console.aws.amazon.com/cloudformation/home?region=${AWS::Region}#/stacks/stackinfo?stackId=${AWS::StackName} 77 | 78 | ECSServiceConsole: 79 | Description: The AWS console deep-link for the ECS service 80 | Value: !Sub https://console.aws.amazon.com/ecs/home?region=${AWS::Region}#/clusters/oam-ecs/services/${Service.Name} 81 | 82 | -------------------------------------------------------------------------------- /integ-tests/schematics/worker.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1alpha1 2 | kind: ComponentSchematic 3 | metadata: 4 | name: nginx-replicated 5 | labels: 6 | app: my-nginx-replicated-app 7 | annotations: 8 | version: "1.0.1" 9 | description: A worker that runs nginx 10 | spec: 11 | workloadType: core.oam.dev/v1alpha1.Worker 12 | osType: linux 13 | containers: 14 | - name: server 15 | image: nginx:latest 16 | --- 17 | apiVersion: core.oam.dev/v1alpha1 18 | kind: ApplicationConfiguration 19 | metadata: 20 | name: simple-worker 21 | annotations: 22 | version: v1.0.0 23 | description: "Simple worker example" 24 | spec: 25 | variables: 26 | components: 27 | - componentName: nginx-replicated 28 | instanceName: web-front-end 29 | -------------------------------------------------------------------------------- /integ-tests/schematics/wrong-api-version.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/hello 2 | kind: ComponentSchematic 3 | metadata: 4 | name: worker-with-bad-api-version 5 | spec: 6 | workloadType: core.oam.dev/v1alpha1.Worker 7 | osType: linux 8 | containers: 9 | - name: worker 10 | image: nginx:latest 11 | -------------------------------------------------------------------------------- /integ-tests/schematics/wrong-kind.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1alpha1 2 | kind: HelloWorld 3 | metadata: 4 | name: worker-with-bad-kind 5 | spec: 6 | workloadType: core.oam.dev/v1alpha1.Worker 7 | osType: linux 8 | containers: 9 | - name: worker 10 | image: nginx:latest 11 | -------------------------------------------------------------------------------- /integ-tests/schematics/wrong-workload-type.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1alpha1 2 | kind: ComponentSchematic 3 | metadata: 4 | name: worker-with-bad-workload-type 5 | spec: 6 | workloadType: core.oam.dev/v1alpha1.HelloWorld 7 | osType: linux 8 | containers: 9 | - name: worker 10 | image: nginx:latest 11 | -------------------------------------------------------------------------------- /internal/pkg/aws/session/session.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package session provides functions that return AWS sessions to use in the AWS SDK. 5 | package session 6 | 7 | import ( 8 | "fmt" 9 | 10 | "github.com/aws/aws-sdk-go/aws" 11 | "github.com/aws/aws-sdk-go/aws/credentials/stscreds" 12 | "github.com/aws/aws-sdk-go/aws/session" 13 | ) 14 | 15 | // Default returns a session configured against the "default" AWS profile. 16 | func Default() (*session.Session, error) { 17 | return session.NewSessionWithOptions(session.Options{ 18 | Config: aws.Config{ 19 | CredentialsChainVerboseErrors: aws.Bool(true), 20 | }, 21 | SharedConfigState: session.SharedConfigEnable, 22 | }) 23 | } 24 | 25 | // DefaultWithRegion returns a session configured against the "default" AWS profile and the input region. 26 | func DefaultWithRegion(region string) (*session.Session, error) { 27 | return session.NewSession(&aws.Config{ 28 | Region: aws.String(region), 29 | }) 30 | } 31 | 32 | // FromProfile returns a session configured against the input profile name. 33 | func FromProfile(name string) (*session.Session, error) { 34 | return session.NewSessionWithOptions(session.Options{ 35 | Config: aws.Config{ 36 | CredentialsChainVerboseErrors: aws.Bool(true), 37 | }, 38 | SharedConfigState: session.SharedConfigEnable, 39 | Profile: name, 40 | }) 41 | } 42 | 43 | // FromRole returns a session configured against the input role and region. 44 | func FromRole(roleARN string, region string) (*session.Session, error) { 45 | defaultSession, err := Default() 46 | 47 | if err != nil { 48 | return nil, fmt.Errorf("error creating default session: %w", err) 49 | } 50 | 51 | creds := stscreds.NewCredentials(defaultSession, roleARN) 52 | return session.NewSession(&aws.Config{ 53 | CredentialsChainVerboseErrors: aws.Bool(true), 54 | Credentials: creds, 55 | Region: ®ion, 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /internal/pkg/cli/app.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package cli 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // BuildAppCmd is the top level command for applications 11 | func BuildAppCmd() *cobra.Command { 12 | cmd := &cobra.Command{ 13 | Use: "app", 14 | Short: "Application commands", 15 | Long: `Commands for working with an oam-ecs application.`, 16 | } 17 | 18 | cmd.AddCommand(BuildDeployAppCmd()) 19 | cmd.AddCommand(BuildShowAppCmd()) 20 | cmd.AddCommand(BuildDeleteAppCmd()) 21 | 22 | return cmd 23 | } 24 | -------------------------------------------------------------------------------- /internal/pkg/cli/app_delete.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package cli contains the oam-ecs subcommands. 5 | package cli 6 | 7 | import ( 8 | "fmt" 9 | 10 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/aws/session" 11 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/deploy/cloudformation" 12 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/deploy/cloudformation/types" 13 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/term/log" 14 | termprogress "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/term/progress" 15 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/workload" 16 | "github.com/oam-dev/oam-go-sdk/apis/core.oam.dev/v1alpha1" 17 | "github.com/spf13/cobra" 18 | ) 19 | 20 | const ( 21 | deleteComponentStart = "Deleting the infrastructure for the component instance %s." 22 | deleteComponentFailed = "Failed to delete the infrastructure for the component instance %s." 23 | deleteComponentSucceeded = "Deleted the infrastructure for component instance %s in CloudFormation stack %s." 24 | ) 25 | 26 | type cfComponentDeleter interface { 27 | DeleteComponent(component *types.ComponentInput) (*types.Component, error) 28 | } 29 | 30 | // DeleteAppOpts holds the configuration needed to delete an application. 31 | type DeleteAppOpts struct { 32 | // Fields with matching flags 33 | OamFile string 34 | 35 | prog progress 36 | ComponentDeleter cfComponentDeleter 37 | } 38 | 39 | // NewDeleteAppOpts initiates the fields to delete an application. 40 | func NewDeleteAppOpts() *DeleteAppOpts { 41 | return &DeleteAppOpts{ 42 | prog: termprogress.NewSpinner(), 43 | } 44 | } 45 | 46 | func (opts *DeleteAppOpts) newComponentInput(application *v1alpha1.ApplicationConfiguration, componentInstance *v1alpha1.ComponentConfiguration) (*types.ComponentInput, error) { 47 | environment := &types.ComponentEnvironment{ 48 | Name: environmentName, 49 | } 50 | 51 | return &types.ComponentInput{ 52 | ApplicationConfiguration: application, 53 | ComponentConfiguration: componentInstance, 54 | Environment: environment, 55 | }, nil 56 | } 57 | 58 | func (opts *DeleteAppOpts) deleteComponentInstance(application *v1alpha1.ApplicationConfiguration, componentInstance *v1alpha1.ComponentConfiguration) error { 59 | componentInput, err := opts.newComponentInput(application, componentInstance) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | opts.prog.Start(fmt.Sprintf(deleteComponentStart, componentInstance.InstanceName)) 65 | 66 | component, err := opts.ComponentDeleter.DeleteComponent(componentInput) 67 | if err != nil { 68 | opts.prog.Stop(log.Serrorf(deleteComponentFailed, componentInstance.InstanceName)) 69 | return err 70 | } 71 | 72 | opts.prog.Stop(log.Ssuccessf(deleteComponentSucceeded, componentInstance.InstanceName, component.StackName)) 73 | 74 | return nil 75 | } 76 | 77 | // Execute parses the OAM files and deletes the infrastructure for the application configuration 78 | func (opts *DeleteAppOpts) Execute() error { 79 | oamWorkload, err := workload.NewOamWorkload( 80 | &workload.OamWorkloadProps{ 81 | OamFiles: []string{opts.OamFile}, 82 | }) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | // Delete the application components 88 | for _, componentInstance := range oamWorkload.ApplicationConfiguration.Spec.Components { 89 | err = opts.deleteComponentInstance(oamWorkload.ApplicationConfiguration, &componentInstance) 90 | 91 | if err != nil { 92 | break 93 | } 94 | } 95 | 96 | return err 97 | } 98 | 99 | // BuildDeleteAppCmd builds the command for deleting an application. 100 | func BuildDeleteAppCmd() *cobra.Command { 101 | opts := NewDeleteAppOpts() 102 | cmd := &cobra.Command{ 103 | Use: "delete", 104 | Short: "Delete the application's deployed components", 105 | Long: `Removes the infrastructure for the application defined in an Open Application Model application configuration file.`, 106 | Example: ` 107 | Delete the deployed application components, using an application configuration file: 108 | $ oam-ecs app delete -f config.yml`, 109 | PreRunE: runCmdE(func(cmd *cobra.Command, args []string) error { 110 | session, err := session.Default() 111 | if err != nil { 112 | return err 113 | } 114 | opts.ComponentDeleter = cloudformation.New(session) 115 | return nil 116 | }), 117 | RunE: runCmdE(func(cmd *cobra.Command, args []string) error { 118 | return opts.Execute() 119 | }), 120 | } 121 | 122 | cmd.Flags().StringVarP(&opts.OamFile, oamFileFlag, oamFileFlagShort, "", appConfigFileFlagDescription) 123 | cmd.MarkFlagRequired(oamFileFlag) 124 | 125 | return cmd 126 | } 127 | -------------------------------------------------------------------------------- /internal/pkg/cli/app_deploy.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package cli contains the oam-ecs subcommands. 5 | package cli 6 | 7 | import ( 8 | "fmt" 9 | 10 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/aws/session" 11 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/deploy/cloudformation" 12 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/deploy/cloudformation/types" 13 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/term/log" 14 | termprogress "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/term/progress" 15 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/workload" 16 | "github.com/oam-dev/oam-go-sdk/apis/core.oam.dev/v1alpha1" 17 | "github.com/spf13/cobra" 18 | ) 19 | 20 | const ( 21 | environmentName = "oam-ecs" 22 | 23 | dryRunComponentSucceeded = "Wrote infrastructure template to disk for component instance %s: %s" 24 | deployComponentStart = "Deploying infrastructure changes for the component instance %s." 25 | deployComponentFailed = "Failed to deploy infrastructure changes for the component instance %s." 26 | deployComponentSucceeded = "Deployed component instance %s in CloudFormation stack %s." 27 | ) 28 | 29 | type cfComponentDeployer interface { 30 | DeployComponent(component *types.ComponentInput) (*types.Component, error) 31 | DryRunComponent(component *types.ComponentInput) (string, error) 32 | } 33 | 34 | // DeployAppOpts holds the configuration needed to provision an application. 35 | type DeployAppOpts struct { 36 | // Fields with matching flags 37 | OamFiles []string 38 | DryRun bool 39 | 40 | prog progress 41 | ComponentDeployer cfComponentDeployer 42 | } 43 | 44 | // NewDeployAppOpts initiates the fields to provision an application. 45 | func NewDeployAppOpts() *DeployAppOpts { 46 | return &DeployAppOpts{ 47 | prog: termprogress.NewSpinner(), 48 | } 49 | } 50 | 51 | func (opts *DeployAppOpts) newComponentInput(application *v1alpha1.ApplicationConfiguration, componentInstance *v1alpha1.ComponentConfiguration, schematic *v1alpha1.ComponentSchematic) (*types.ComponentInput, error) { 52 | // TODO validate that following are not set: osType, arch, volume disk, volume sharing policy, 53 | // container extended resource, container config file, container readiness probe, 54 | // container liveness probe failure threshold/httpGet/tcpSocket 55 | 56 | ecsSettings := &types.ECSWorkloadSettings{} 57 | 58 | environment := &types.ComponentEnvironment{ 59 | Name: environmentName, 60 | } 61 | 62 | return &types.ComponentInput{ 63 | ApplicationConfiguration: application, 64 | ComponentConfiguration: componentInstance, 65 | Component: schematic, 66 | WorkloadSettings: ecsSettings, 67 | Environment: environment, 68 | }, nil 69 | } 70 | 71 | func (opts *DeployAppOpts) dryRunComponentInstance(application *v1alpha1.ApplicationConfiguration, componentInstance *v1alpha1.ComponentConfiguration, schematic *v1alpha1.ComponentSchematic) error { 72 | deployComponentInput, err := opts.newComponentInput(application, componentInstance, schematic) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | file, err := opts.ComponentDeployer.DryRunComponent(deployComponentInput) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | log.Successln(fmt.Sprintf(dryRunComponentSucceeded, componentInstance.InstanceName, file)) 83 | 84 | return nil 85 | } 86 | 87 | func (opts *DeployAppOpts) deployComponentInstance(application *v1alpha1.ApplicationConfiguration, componentInstance *v1alpha1.ComponentConfiguration, schematic *v1alpha1.ComponentSchematic) error { 88 | deployComponentInput, err := opts.newComponentInput(application, componentInstance, schematic) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | opts.prog.Start(fmt.Sprintf(deployComponentStart, componentInstance.InstanceName)) 94 | 95 | component, err := opts.ComponentDeployer.DeployComponent(deployComponentInput) 96 | if err != nil { 97 | opts.prog.Stop(log.Serrorf(deployComponentFailed, componentInstance.InstanceName)) 98 | return err 99 | } 100 | 101 | opts.prog.Stop(log.Ssuccessf(deployComponentSucceeded, componentInstance.InstanceName, component.StackName)) 102 | 103 | component.Display() 104 | 105 | return nil 106 | } 107 | 108 | // Execute parses the OAM files, translates them into infrastructure definitions, and deploys the infrastructure 109 | func (opts *DeployAppOpts) Execute() error { 110 | oamWorkload, err := workload.NewOamWorkload( 111 | &workload.OamWorkloadProps{ 112 | OamFiles: opts.OamFiles, 113 | }) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | // Validate we have app config and component schematics that go together 119 | for _, component := range oamWorkload.ApplicationConfiguration.Spec.Components { 120 | _, ok := oamWorkload.ComponentSchematics[component.ComponentName] 121 | if !ok { 122 | log.Errorf("Could not find the component schematic for %s\n", component.ComponentName) 123 | return fmt.Errorf("Application configuration refers to component %s, but no file provided the component schematic", component.ComponentName) 124 | } 125 | } 126 | 127 | // Deploy or dry-run the application components 128 | for _, componentInstance := range oamWorkload.ApplicationConfiguration.Spec.Components { 129 | schematic, _ := oamWorkload.ComponentSchematics[componentInstance.ComponentName] 130 | if opts.DryRun { 131 | err = opts.dryRunComponentInstance(oamWorkload.ApplicationConfiguration, &componentInstance, schematic) 132 | } else { 133 | err = opts.deployComponentInstance(oamWorkload.ApplicationConfiguration, &componentInstance, schematic) 134 | } 135 | 136 | if err != nil { 137 | break 138 | } 139 | } 140 | 141 | return err 142 | } 143 | 144 | // BuildDeployAppCmd builds the command for deploying an application. 145 | func BuildDeployAppCmd() *cobra.Command { 146 | opts := NewDeployAppOpts() 147 | cmd := &cobra.Command{ 148 | Use: "deploy", 149 | Short: "Deploy the application", 150 | Long: `Provisions (or updates) the Amazon ECS infrastructure for the application defined using the Open Application Model spec. All component schematics and the application configuration file for the application must be provided every time the 'app deploy' command runs (this CLI does not save any state).`, 151 | Example: ` 152 | Deploy the application's OAM component schematic files and application configuration file: 153 | $ oam-ecs app deploy -f component1.yml,component2.yml,config.yml`, 154 | PreRunE: runCmdE(func(cmd *cobra.Command, args []string) error { 155 | session, err := session.Default() 156 | if err != nil { 157 | return err 158 | } 159 | opts.ComponentDeployer = cloudformation.New(session) 160 | return nil 161 | }), 162 | RunE: runCmdE(func(cmd *cobra.Command, args []string) error { 163 | return opts.Execute() 164 | }), 165 | } 166 | 167 | cmd.Flags().StringSliceVarP(&opts.OamFiles, oamFileFlag, oamFileFlagShort, []string{}, oamFileFlagDescription) 168 | cmd.MarkFlagRequired(oamFileFlag) 169 | cmd.Flags().BoolVarP(&opts.DryRun, dryRunFlag, "", false, dryRunFlagDescription) 170 | 171 | return cmd 172 | } 173 | -------------------------------------------------------------------------------- /internal/pkg/cli/app_show.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package cli contains the oam-ecs subcommands. 5 | package cli 6 | 7 | import ( 8 | "fmt" 9 | 10 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/aws/session" 11 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/deploy/cloudformation" 12 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/deploy/cloudformation/types" 13 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/term/log" 14 | termprogress "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/term/progress" 15 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/workload" 16 | "github.com/oam-dev/oam-go-sdk/apis/core.oam.dev/v1alpha1" 17 | "github.com/spf13/cobra" 18 | ) 19 | 20 | const ( 21 | showComponentStart = "Retrieving the infrastructure information for the component instance %s." 22 | showComponentFailed = "Failed to retrieve the infrastructure information for the component instance %s." 23 | showComponentSucceeded = "Retrieved the infrastructure information for component instance %s in CloudFormation stack %s." 24 | ) 25 | 26 | type cfComponentDescriber interface { 27 | DescribeComponent(component *types.ComponentInput) (*types.Component, error) 28 | } 29 | 30 | // ShowAppOpts holds the configuration needed to describe an application. 31 | type ShowAppOpts struct { 32 | // Fields with matching flags 33 | OamFile string 34 | 35 | prog progress 36 | ComponentDescriber cfComponentDescriber 37 | } 38 | 39 | // NewShowAppOpts initiates the fields to describe an application. 40 | func NewShowAppOpts() *ShowAppOpts { 41 | return &ShowAppOpts{ 42 | prog: termprogress.NewSpinner(), 43 | } 44 | } 45 | 46 | func (opts *ShowAppOpts) newComponentInput(application *v1alpha1.ApplicationConfiguration, componentInstance *v1alpha1.ComponentConfiguration) (*types.ComponentInput, error) { 47 | environment := &types.ComponentEnvironment{ 48 | Name: environmentName, 49 | } 50 | 51 | return &types.ComponentInput{ 52 | ApplicationConfiguration: application, 53 | ComponentConfiguration: componentInstance, 54 | Environment: environment, 55 | }, nil 56 | } 57 | 58 | func (opts *ShowAppOpts) showComponentInstance(application *v1alpha1.ApplicationConfiguration, componentInstance *v1alpha1.ComponentConfiguration) error { 59 | componentInput, err := opts.newComponentInput(application, componentInstance) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | opts.prog.Start(fmt.Sprintf(showComponentStart, componentInstance.InstanceName)) 65 | 66 | component, err := opts.ComponentDescriber.DescribeComponent(componentInput) 67 | if err != nil { 68 | opts.prog.Stop(log.Serrorf(showComponentFailed, componentInstance.InstanceName)) 69 | return err 70 | } 71 | 72 | opts.prog.Stop(log.Ssuccessf(showComponentSucceeded, componentInstance.InstanceName, component.StackName)) 73 | 74 | component.Display() 75 | 76 | return nil 77 | } 78 | 79 | // Execute parses the OAM files and shows the infrastructure for the application configuration 80 | func (opts *ShowAppOpts) Execute() error { 81 | oamWorkload, err := workload.NewOamWorkload( 82 | &workload.OamWorkloadProps{ 83 | OamFiles: []string{opts.OamFile}, 84 | }) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | // Show the application components 90 | for _, componentInstance := range oamWorkload.ApplicationConfiguration.Spec.Components { 91 | err = opts.showComponentInstance(oamWorkload.ApplicationConfiguration, &componentInstance) 92 | 93 | if err != nil { 94 | break 95 | } 96 | } 97 | 98 | return err 99 | } 100 | 101 | // BuildShowAppCmd builds the command for showing an application. 102 | func BuildShowAppCmd() *cobra.Command { 103 | opts := NewShowAppOpts() 104 | cmd := &cobra.Command{ 105 | Use: "show", 106 | Short: "Describe the application's deployed components", 107 | Long: `Retrieves and displays the attributes of the infrastructure for the application defined in an Open Application Model application configuration file.`, 108 | Example: ` 109 | Show the deployed application components, using an application configuration file: 110 | $ oam-ecs app show -f config.yml`, 111 | PreRunE: runCmdE(func(cmd *cobra.Command, args []string) error { 112 | session, err := session.Default() 113 | if err != nil { 114 | return err 115 | } 116 | opts.ComponentDescriber = cloudformation.New(session) 117 | return nil 118 | }), 119 | RunE: runCmdE(func(cmd *cobra.Command, args []string) error { 120 | return opts.Execute() 121 | }), 122 | } 123 | 124 | cmd.Flags().StringVarP(&opts.OamFile, oamFileFlag, oamFileFlagShort, "", appConfigFileFlagDescription) 125 | cmd.MarkFlagRequired(oamFileFlag) 126 | 127 | return cmd 128 | } 129 | -------------------------------------------------------------------------------- /internal/pkg/cli/cli.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package cli contains the ecs-preview subcommands. 5 | package cli 6 | 7 | import ( 8 | "os" 9 | 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | // runCmdE wraps one of the run error methods, PreRunE, RunE, of a cobra command so that if a user 14 | // types "help" in the arguments the usage string is printed instead of running the command. 15 | func runCmdE(f func(cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) error { 16 | return func(cmd *cobra.Command, args []string) error { 17 | if len(args) == 1 && args[0] == "help" { 18 | _ = cmd.Help() // Help always returns nil. 19 | os.Exit(0) 20 | } 21 | return f(cmd, args) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/pkg/cli/env.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package cli 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // BuildEnvCmd is the top level command for environments 11 | func BuildEnvCmd() *cobra.Command { 12 | cmd := &cobra.Command{ 13 | Use: "env", 14 | Short: "Environment commands", 15 | Long: `Commands for working with the oam-ecs environment.`, 16 | } 17 | 18 | cmd.AddCommand(BuildDeployEnvironmentCmd()) 19 | cmd.AddCommand(BuildShowEnvironmentCmd()) 20 | cmd.AddCommand(BuildDeleteEnvironmentCmd()) 21 | 22 | return cmd 23 | } 24 | -------------------------------------------------------------------------------- /internal/pkg/cli/env_delete.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package cli 4 | 5 | import ( 6 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/aws/session" 7 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/deploy/cloudformation" 8 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/deploy/cloudformation/types" 9 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/term/log" 10 | termprogress "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/term/progress" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | const ( 15 | deleteEnvStart = "Deleting the infrastructure for the environment." 16 | deleteEnvFailed = "Failed to delete the infrastructure for the environment." 17 | deleteEnvSucceeded = "Deleted the environment infrastructure in CloudFormation stack %s." 18 | ) 19 | 20 | type cfEnvironmentDeleter interface { 21 | DeleteEnvironment(env *types.EnvironmentInput) (*types.Environment, error) 22 | } 23 | 24 | // DeleteEnvironmentOpts holds the configuration needed to deletes the oam-ecs environment. 25 | type DeleteEnvironmentOpts struct { 26 | prog progress 27 | envDeleter cfEnvironmentDeleter 28 | } 29 | 30 | // DeleteEnvironmentOpts initiates the fields to provision an environment. 31 | func NewDeleteEnvironmentOpts() *DeleteEnvironmentOpts { 32 | return &DeleteEnvironmentOpts{ 33 | prog: termprogress.NewSpinner(), 34 | } 35 | } 36 | 37 | // Execute deletes the environment CloudFormation stack 38 | func (opts *DeleteEnvironmentOpts) Execute() error { 39 | deleteEnvInput := &types.EnvironmentInput{} 40 | 41 | opts.prog.Start(deleteEnvStart) 42 | 43 | env, err := opts.envDeleter.DeleteEnvironment(deleteEnvInput) 44 | if err != nil { 45 | opts.prog.Stop(log.Serror(deleteEnvFailed)) 46 | return err 47 | } 48 | 49 | opts.prog.Stop(log.Ssuccessf(deleteEnvSucceeded, env.StackName)) 50 | 51 | return nil 52 | } 53 | 54 | // BuildDeleteEnvironmentCmd builds the command for creating a new pipeline. 55 | func BuildDeleteEnvironmentCmd() *cobra.Command { 56 | opts := NewDeleteEnvironmentOpts() 57 | cmd := &cobra.Command{ 58 | Use: "delete", 59 | Short: "Delete the oam-ecs environment", 60 | Long: `Removes the shared infrastructure, including a VPC and ECS cluster, for oam-ecs applications. All deployed components must already be deleted.`, 61 | Example: ` 62 | Delete the oam-ecs environment: 63 | $ oam-ecs env delete`, 64 | PreRunE: runCmdE(func(cmd *cobra.Command, args []string) error { 65 | session, err := session.Default() 66 | if err != nil { 67 | return err 68 | } 69 | opts.envDeleter = cloudformation.New(session) 70 | return nil 71 | }), 72 | RunE: runCmdE(func(cmd *cobra.Command, args []string) error { 73 | return opts.Execute() 74 | }), 75 | } 76 | 77 | return cmd 78 | } 79 | -------------------------------------------------------------------------------- /internal/pkg/cli/env_deploy.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package cli 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/aws/session" 9 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/deploy/cloudformation" 10 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/deploy/cloudformation/types" 11 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/term/log" 12 | termprogress "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/term/progress" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | const ( 17 | dryRunEnvironmentSucceeded = "Wrote infrastructure template to disk for the environment: %s" 18 | deployEnvStart = "Deploying the infrastructure for the environment." 19 | deployEnvFailed = "Failed to deploy the infrastructure for the environment." 20 | deployEnvSucceeded = "Deployed the environment infrastructure in CloudFormation stack %s." 21 | ) 22 | 23 | type cfEnvironmentDeployer interface { 24 | DeployEnvironment(env *types.EnvironmentInput) (*types.Environment, error) 25 | DryRunEnvironment(env *types.EnvironmentInput) (string, error) 26 | } 27 | 28 | // DeployEnvironmentOpts holds the configuration needed to deploy the oam-ecs environment. 29 | type DeployEnvironmentOpts struct { 30 | DryRun bool 31 | 32 | prog progress 33 | envDeployer cfEnvironmentDeployer 34 | } 35 | 36 | // DeployEnvironmentOpts initiates the fields to provision an environment. 37 | func NewDeployEnvironmentOpts() *DeployEnvironmentOpts { 38 | return &DeployEnvironmentOpts{ 39 | prog: termprogress.NewSpinner(), 40 | } 41 | } 42 | 43 | func (opts *DeployEnvironmentOpts) dryRunEnvironment() error { 44 | deployEnvInput := &types.EnvironmentInput{} 45 | 46 | file, err := opts.envDeployer.DryRunEnvironment(deployEnvInput) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | log.Successln(fmt.Sprintf(dryRunEnvironmentSucceeded, file)) 52 | 53 | return nil 54 | } 55 | 56 | func (opts *DeployEnvironmentOpts) deployEnvironment() error { 57 | deployEnvInput := &types.EnvironmentInput{} 58 | 59 | opts.prog.Start(deployEnvStart) 60 | 61 | env, err := opts.envDeployer.DeployEnvironment(deployEnvInput) 62 | if err != nil { 63 | opts.prog.Stop(log.Serror(deployEnvFailed)) 64 | return err 65 | } 66 | 67 | opts.prog.Stop(log.Ssuccessf(deployEnvSucceeded, env.StackName)) 68 | 69 | env.Display() 70 | 71 | return nil 72 | } 73 | 74 | // Execute deploys the environment CloudFormation stack 75 | func (opts *DeployEnvironmentOpts) Execute() error { 76 | if opts.DryRun { 77 | return opts.dryRunEnvironment() 78 | } else { 79 | return opts.deployEnvironment() 80 | } 81 | } 82 | 83 | // BuildDeployEnvironmentCmd builds the command for creating a new pipeline. 84 | func BuildDeployEnvironmentCmd() *cobra.Command { 85 | opts := NewDeployEnvironmentOpts() 86 | cmd := &cobra.Command{ 87 | Use: "deploy", 88 | Short: "Deploy the oam-ecs environment", 89 | Long: `Creates (or updates) the shared infrastructure, including a VPC and ECS cluster, for oam-ecs applications`, 90 | Example: ` 91 | Create the oam-ecs environment: 92 | $ oam-ecs env deploy`, 93 | PreRunE: runCmdE(func(cmd *cobra.Command, args []string) error { 94 | session, err := session.Default() 95 | if err != nil { 96 | return err 97 | } 98 | opts.envDeployer = cloudformation.New(session) 99 | return nil 100 | }), 101 | RunE: runCmdE(func(cmd *cobra.Command, args []string) error { 102 | return opts.Execute() 103 | }), 104 | } 105 | 106 | cmd.Flags().BoolVarP(&opts.DryRun, dryRunFlag, "", false, dryRunFlagDescription) 107 | 108 | return cmd 109 | } 110 | -------------------------------------------------------------------------------- /internal/pkg/cli/env_show.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package cli 4 | 5 | import ( 6 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/aws/session" 7 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/deploy/cloudformation" 8 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/deploy/cloudformation/types" 9 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/term/log" 10 | termprogress "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/term/progress" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | const ( 15 | showEnvStart = "Retrieving the infrastructure information for the environment." 16 | showEnvFailed = "Failed to retrieve the infrastructure information for the environment." 17 | showEnvSucceeded = "Retrieved the infrastructure information for CloudFormation stack %s." 18 | ) 19 | 20 | type cfEnvironmentDescriber interface { 21 | DescribeEnvironment(env *types.EnvironmentInput) (*types.Environment, error) 22 | } 23 | 24 | type ShowEnvironmentOpts struct { 25 | prog progress 26 | envDescriber cfEnvironmentDescriber 27 | } 28 | 29 | func NewShowEnvironmentOpts() *ShowEnvironmentOpts { 30 | return &ShowEnvironmentOpts{ 31 | prog: termprogress.NewSpinner(), 32 | } 33 | } 34 | 35 | func (opts *ShowEnvironmentOpts) Execute() error { 36 | describeEnvInput := &types.EnvironmentInput{} 37 | 38 | opts.prog.Start(showEnvStart) 39 | 40 | env, err := opts.envDescriber.DescribeEnvironment(describeEnvInput) 41 | if err != nil { 42 | opts.prog.Stop(log.Serror(showEnvFailed)) 43 | return err 44 | } 45 | 46 | opts.prog.Stop(log.Ssuccessf(showEnvSucceeded, env.StackName)) 47 | 48 | env.Display() 49 | 50 | return nil 51 | } 52 | 53 | func BuildShowEnvironmentCmd() *cobra.Command { 54 | opts := NewShowEnvironmentOpts() 55 | cmd := &cobra.Command{ 56 | Use: "show", 57 | Short: "Describe the oam-ecs environment", 58 | Long: `Retrieves and displays the attributes of the oam-ecs default environment`, 59 | Example: ` 60 | Show the oam-ecs environment: 61 | $ oam-ecs env show`, 62 | PreRunE: runCmdE(func(cmd *cobra.Command, args []string) error { 63 | session, err := session.Default() 64 | if err != nil { 65 | return err 66 | } 67 | opts.envDescriber = cloudformation.New(session) 68 | return nil 69 | }), 70 | RunE: runCmdE(func(cmd *cobra.Command, args []string) error { 71 | return opts.Execute() 72 | }), 73 | } 74 | 75 | return cmd 76 | } 77 | -------------------------------------------------------------------------------- /internal/pkg/cli/flag.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package cli 4 | 5 | // Long flag names. 6 | const ( 7 | oamFileFlag = "filename" 8 | dryRunFlag = "dry-run" 9 | ) 10 | 11 | // Short flag names. 12 | // A short flag only exists if the flag is mandatory by the command. 13 | const ( 14 | oamFileFlagShort = "f" 15 | ) 16 | 17 | // Descriptions for flags. 18 | const ( 19 | oamFileFlagDescription = "Path to a file containing OAM component schematics or OAM application configuration. Multiple files can be provided either by repeating the flag for each file, or with a comma-delimited list of files." 20 | dryRunFlagDescription = "Write out an infrastructure template to a file instead of deploying the infrastructure" 21 | appConfigFileFlagDescription = "Path to a file containing an OAM application configuration." 22 | ) 23 | -------------------------------------------------------------------------------- /internal/pkg/cli/progress.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package cli 5 | 6 | import termprogress "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/term/progress" 7 | 8 | // progress is the interface to inform the user that a long operation is taking place. 9 | type progress interface { 10 | // Start starts displaying progress with a label. 11 | Start(label string) 12 | // Stop ends displaying progress with a label. 13 | Stop(label string) 14 | // Events writes additional information in between the start and stop stages. 15 | Events([]termprogress.TabRow) 16 | } 17 | -------------------------------------------------------------------------------- /internal/pkg/deploy/cloudformation/changeset.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package cloudformation 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/aws/aws-sdk-go/aws" 10 | "github.com/aws/aws-sdk-go/aws/request" 11 | "github.com/aws/aws-sdk-go/service/cloudformation" 12 | "github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface" 13 | "github.com/google/uuid" 14 | ) 15 | 16 | // Status reasons that can occur if the change set execution status is "FAILED". 17 | const ( 18 | noChangesReason = "NO_CHANGES_REASON" 19 | noUpdatesReason = "NO_UPDATES_REASON" 20 | ) 21 | 22 | // changeSet represents a CloudFormation Change Set. 23 | // See https://aws.amazon.com/blogs/aws/new-change-sets-for-aws-cloudformation/ 24 | type changeSet struct { 25 | name string // required 26 | stackID string // required 27 | executionStatus string 28 | statusReason string 29 | changes []*cloudformation.Change 30 | 31 | c cloudformationiface.CloudFormationAPI 32 | waiters []request.WaiterOption 33 | } 34 | 35 | func (set *changeSet) String() string { 36 | return fmt.Sprintf("name=%s, stackID=%s", set.name, set.stackID) 37 | } 38 | 39 | func (set *changeSet) waitForCreation() error { 40 | describeChangeSetInput := &cloudformation.DescribeChangeSetInput{ 41 | ChangeSetName: aws.String(set.name), 42 | StackName: aws.String(set.stackID), 43 | } 44 | 45 | if err := set.c.WaitUntilChangeSetCreateCompleteWithContext(context.Background(), describeChangeSetInput, set.waiters...); err != nil { 46 | return fmt.Errorf("failed to wait for changeSet creation %s: %w", set, err) 47 | } 48 | return nil 49 | } 50 | 51 | // describe updates the change set with its latest values. 52 | func (set *changeSet) describe() error { 53 | var executionStatus, statusReason string 54 | var changes []*cloudformation.Change 55 | var nextToken *string 56 | for { 57 | out, err := set.c.DescribeChangeSet(&cloudformation.DescribeChangeSetInput{ 58 | ChangeSetName: aws.String(set.name), 59 | StackName: aws.String(set.stackID), 60 | NextToken: nextToken, 61 | }) 62 | if err != nil { 63 | return fmt.Errorf("failed to describe changeSet %s: %w", set, err) 64 | } 65 | executionStatus = aws.StringValue(out.ExecutionStatus) 66 | statusReason = aws.StringValue(out.StatusReason) 67 | changes = append(changes, out.Changes...) 68 | nextToken = out.NextToken 69 | 70 | if nextToken == nil { // no more results left 71 | break 72 | } 73 | } 74 | set.executionStatus = executionStatus 75 | set.statusReason = statusReason 76 | set.changes = changes 77 | return nil 78 | } 79 | 80 | func (set *changeSet) execute() error { 81 | if err := set.describe(); err != nil { 82 | return err 83 | } 84 | if set.executionStatus != cloudformation.ExecutionStatusAvailable { 85 | // Ignore execute request if the change set does not contain any modifications. 86 | if set.statusReason == noChangesReason { 87 | return nil 88 | } 89 | if set.statusReason == noUpdatesReason { 90 | return nil 91 | } 92 | return &ErrNotExecutableChangeSet{ 93 | set: set, 94 | } 95 | } 96 | if _, err := set.c.ExecuteChangeSet(&cloudformation.ExecuteChangeSetInput{ 97 | ChangeSetName: aws.String(set.name), 98 | StackName: aws.String(set.stackID), 99 | }); err != nil { 100 | return fmt.Errorf("failed to execute changeSet %s: %w", set, err) 101 | } 102 | return nil 103 | } 104 | 105 | func (set *changeSet) delete() error { 106 | if _, err := set.c.DeleteChangeSet(&cloudformation.DeleteChangeSetInput{ 107 | ChangeSetName: aws.String(set.name), 108 | StackName: aws.String(set.stackID), 109 | }); err != nil { 110 | return fmt.Errorf("failed to delete changeSet %s: %w", set, err) 111 | } 112 | return nil 113 | } 114 | 115 | // createChangeSetOpt is a functional option to add additional settings to a CreateChangeSetInput. 116 | type createChangeSetOpt func(in *cloudformation.CreateChangeSetInput) 117 | 118 | func createChangeSetInput(stackName, templateBody string, options ...createChangeSetOpt) (*cloudformation.CreateChangeSetInput, error) { 119 | id, err := uuid.NewRandom() 120 | if err != nil { 121 | return nil, fmt.Errorf("failed to generate random id for changeSet: %w", err) 122 | } 123 | 124 | // The change set name must match the regex [a-zA-Z][-a-zA-Z0-9]*. The generated UUID can start with a number, 125 | // by prefixing the uuid with a word we guarantee that we start with a letter. 126 | name := fmt.Sprintf("%s-%s", "oam-ecs", id.String()) 127 | 128 | in := &cloudformation.CreateChangeSetInput{ 129 | Capabilities: aws.StringSlice([]string{ 130 | cloudformation.CapabilityCapabilityIam, 131 | }), 132 | ChangeSetName: aws.String(name), 133 | StackName: aws.String(stackName), 134 | TemplateBody: aws.String(templateBody), 135 | } 136 | for _, option := range options { 137 | option(in) 138 | } 139 | return in, nil 140 | } 141 | 142 | func withParameters(params []*cloudformation.Parameter) createChangeSetOpt { 143 | return func(in *cloudformation.CreateChangeSetInput) { 144 | in.Parameters = params 145 | } 146 | } 147 | 148 | func withChangeSetType(csType string) createChangeSetOpt { 149 | return func(in *cloudformation.CreateChangeSetInput) { 150 | in.ChangeSetType = aws.String(csType) 151 | } 152 | } 153 | 154 | func withTags(tags []*cloudformation.Tag) createChangeSetOpt { 155 | return func(in *cloudformation.CreateChangeSetInput) { 156 | in.Tags = tags 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /internal/pkg/deploy/cloudformation/component.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package cloudformation provides functionality to deploy oam-ecs resources with AWS CloudFormation. 5 | package cloudformation 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "os" 11 | "path/filepath" 12 | 13 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/deploy/cloudformation/stack" 14 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/deploy/cloudformation/types" 15 | ) 16 | 17 | const ( 18 | templateFileDirectoryName = "oam-ecs-dry-run-results" 19 | ) 20 | 21 | // DeployComponent creates the CloudFormation stack for a component instance by creating and executing a change set. 22 | // 23 | // If the deployment succeeds, returns nil. 24 | // If the stack already exists, update the stack. 25 | // If the change set to create/update the stack cannot be executed, returns a ErrNotExecutableChangeSet. 26 | // Otherwise, returns a wrapped error. 27 | func (cf CloudFormation) DeployComponent(component *types.ComponentInput) (*types.Component, error) { 28 | componentConfig := stack.NewComponentStackConfig(component, cf.box) 29 | 30 | // Try to create the stack 31 | if _, err := cf.create(componentConfig); err != nil { 32 | var existsErr *ErrStackAlreadyExists 33 | if errors.As(err, &existsErr) { 34 | // Stack already exists, update the stack 35 | deployStarted, err := cf.update(componentConfig) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | if deployStarted { 41 | // Wait for the stack to finish updating 42 | stack, err := cf.waitForStackUpdate(componentConfig) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return componentConfig.ToComponent(stack) 47 | } else { 48 | // nothing to deploy 49 | stack, err := cf.describe(componentConfig) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return componentConfig.ToComponent(stack) 54 | } 55 | } else { 56 | return nil, err 57 | } 58 | } 59 | 60 | // Wait for the stack to finish creation 61 | stack, err := cf.waitForStackCreation(componentConfig) 62 | if err != nil { 63 | return nil, err 64 | } 65 | return componentConfig.ToComponent(stack) 66 | } 67 | 68 | func (cf CloudFormation) DryRunComponent(component *types.ComponentInput) (string, error) { 69 | stackConfig := stack.NewComponentStackConfig(component, cf.box) 70 | template, err := stackConfig.Template() 71 | if err != nil { 72 | return "", fmt.Errorf("template creation: %w", err) 73 | } 74 | 75 | templateFileDir := filepath.Join(".", templateFileDirectoryName) 76 | if _, err := os.Stat(templateFileDir); os.IsNotExist(err) { 77 | err = os.Mkdir(templateFileDir, os.ModePerm) 78 | if err != nil { 79 | return "", fmt.Errorf("could not create directory %s: %w", templateFileDir, err) 80 | } 81 | } 82 | 83 | templateFileAbsDir, err := filepath.Abs(templateFileDir) 84 | if err != nil { 85 | return "", fmt.Errorf("could not get absolute path for directory %s: %w", templateFileDir, err) 86 | } 87 | 88 | templateFilePath := filepath.Join(templateFileAbsDir, stackConfig.StackName()+"-template.yaml") 89 | 90 | f, err := os.Create(templateFilePath) 91 | if err != nil { 92 | return "", err 93 | } 94 | defer f.Close() 95 | 96 | _, err = f.WriteString(template) 97 | if err != nil { 98 | return "", err 99 | } 100 | f.Sync() 101 | 102 | return templateFilePath, nil 103 | } 104 | 105 | // DescribeComponent describes the existing CloudFormation stack for a component instance 106 | func (cf CloudFormation) DescribeComponent(component *types.ComponentInput) (*types.Component, error) { 107 | stackConfig := stack.NewComponentStackConfig(component, cf.box) 108 | stack, err := cf.describe(stackConfig) 109 | if err != nil { 110 | return nil, err 111 | } 112 | return stackConfig.ToComponent(stack) 113 | } 114 | 115 | // DeleteComponent deletes the CloudFormation stack for a component instance 116 | func (cf CloudFormation) DeleteComponent(component *types.ComponentInput) (*types.Component, error) { 117 | stackConfig := stack.NewComponentStackConfig(component, cf.box) 118 | stack, err := cf.describe(stackConfig) 119 | if err != nil { 120 | var notFoundErr *ErrStackNotFound 121 | if errors.As(err, ¬FoundErr) { 122 | // Stack was not found, don't return an error, since it's deleted already 123 | return &types.Component{ 124 | StackName: stackConfig.StackName(), 125 | }, nil 126 | } else { 127 | return nil, err 128 | } 129 | } 130 | err = cf.delete(*stack.StackId) 131 | if err != nil { 132 | return nil, err 133 | } 134 | return stackConfig.ToComponent(stack) 135 | } 136 | -------------------------------------------------------------------------------- /internal/pkg/deploy/cloudformation/deploy.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package deploy holds the structures to deploy infrastructure resources. 5 | package cloudformation 6 | 7 | // Resource represents an AWS resource. 8 | type Resource struct { 9 | LogicalName string 10 | Type string 11 | } 12 | 13 | // ResourceEvent represents a status update for an AWS resource during a deployment. 14 | type ResourceEvent struct { 15 | Resource 16 | Status string 17 | StatusReason string 18 | } 19 | -------------------------------------------------------------------------------- /internal/pkg/deploy/cloudformation/env.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package cloudformation provides functionality to deploy oam-ecs resources with AWS CloudFormation. 5 | package cloudformation 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "os" 11 | "path/filepath" 12 | 13 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/deploy/cloudformation/stack" 14 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/deploy/cloudformation/types" 15 | ) 16 | 17 | // DeployEnvironment creates the CloudFormation stack for an environment by creating and executing a change set. 18 | // 19 | // If the deployment succeeds, returns nil. 20 | // If the stack already exists, update the stack. 21 | // If the change set to create/update the stack cannot be executed, returns a ErrNotExecutableChangeSet. 22 | // Otherwise, returns a wrapped error. 23 | func (cf CloudFormation) DeployEnvironment(env *types.EnvironmentInput) (*types.Environment, error) { 24 | envConfig := stack.NewEnvStackConfig(env, cf.box) 25 | 26 | // Try to create the stack 27 | if _, err := cf.create(envConfig); err != nil { 28 | var existsErr *ErrStackAlreadyExists 29 | if errors.As(err, &existsErr) { 30 | // Stack already exists, update the stack 31 | deployStarted, err := cf.update(envConfig) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | if deployStarted { 37 | // Wait for the stack to finish updating 38 | stack, err := cf.waitForStackUpdate(envConfig) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return envConfig.ToEnv(stack) 43 | } else { 44 | // nothing to deploy 45 | stack, err := cf.describe(envConfig) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return envConfig.ToEnv(stack) 50 | } 51 | } else { 52 | return nil, err 53 | } 54 | } 55 | 56 | // Wait for the stack to finish creation 57 | stack, err := cf.waitForStackCreation(envConfig) 58 | if err != nil { 59 | return nil, err 60 | } 61 | return envConfig.ToEnv(stack) 62 | } 63 | 64 | func (cf CloudFormation) DryRunEnvironment(env *types.EnvironmentInput) (string, error) { 65 | stackConfig := stack.NewEnvStackConfig(env, cf.box) 66 | template, err := stackConfig.Template() 67 | if err != nil { 68 | return "", fmt.Errorf("template creation: %w", err) 69 | } 70 | 71 | templateFileDir := filepath.Join(".", templateFileDirectoryName) 72 | if _, err := os.Stat(templateFileDir); os.IsNotExist(err) { 73 | err = os.Mkdir(templateFileDir, os.ModePerm) 74 | if err != nil { 75 | return "", fmt.Errorf("could not create directory %s: %w", templateFileDir, err) 76 | } 77 | } 78 | 79 | templateFileAbsDir, err := filepath.Abs(templateFileDir) 80 | if err != nil { 81 | return "", fmt.Errorf("could not get absolute path for directory %s: %w", templateFileDir, err) 82 | } 83 | 84 | templateFilePath := filepath.Join(templateFileAbsDir, stackConfig.StackName()+"-template.yaml") 85 | 86 | f, err := os.Create(templateFilePath) 87 | if err != nil { 88 | return "", err 89 | } 90 | defer f.Close() 91 | 92 | _, err = f.WriteString(template) 93 | if err != nil { 94 | return "", err 95 | } 96 | f.Sync() 97 | 98 | return templateFilePath, nil 99 | } 100 | 101 | // DescribeEnvironment describes the existing CloudFormation stack for an environment 102 | func (cf CloudFormation) DescribeEnvironment(env *types.EnvironmentInput) (*types.Environment, error) { 103 | envConfig := stack.NewEnvStackConfig(env, cf.box) 104 | stack, err := cf.describe(envConfig) 105 | if err != nil { 106 | return nil, err 107 | } 108 | return envConfig.ToEnv(stack) 109 | } 110 | 111 | // DeleteEnvironment deletes the CloudFormation stack for an environment 112 | func (cf CloudFormation) DeleteEnvironment(env *types.EnvironmentInput) (*types.Environment, error) { 113 | envConfig := stack.NewEnvStackConfig(env, cf.box) 114 | stack, err := cf.describe(envConfig) 115 | if err != nil { 116 | var notFoundErr *ErrStackNotFound 117 | if errors.As(err, ¬FoundErr) { 118 | // Stack was not found, don't return an error, since it's deleted already 119 | return &types.Environment{ 120 | StackName: envConfig.StackName(), 121 | }, nil 122 | } else { 123 | return nil, err 124 | } 125 | } 126 | err = cf.delete(*stack.StackId) 127 | if err != nil { 128 | return nil, err 129 | } 130 | return envConfig.ToEnv(stack) 131 | } 132 | -------------------------------------------------------------------------------- /internal/pkg/deploy/cloudformation/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package cloudformation 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | ) 9 | 10 | // ErrStackAlreadyExists occurs when a CloudFormation stack already exists with a given name. 11 | type ErrStackAlreadyExists struct { 12 | stackName string 13 | parentErr error 14 | } 15 | 16 | func (err *ErrStackAlreadyExists) Error() string { 17 | return fmt.Sprintf("stack %s already exists", err.stackName) 18 | } 19 | 20 | // Unwrap returns the original CloudFormation error. 21 | func (err *ErrStackAlreadyExists) Unwrap() error { 22 | return err.parentErr 23 | } 24 | 25 | // ErrStackNotFound occurs when we can't find a particular CloudFormation stack. 26 | type ErrStackNotFound struct { 27 | stackName string 28 | } 29 | 30 | func (err *ErrStackNotFound) Error() string { 31 | return fmt.Sprintf("failed to find a stack named %s", err.stackName) 32 | } 33 | 34 | // ErrStackUpdateInProgress occurs when we try to update a stack that's already being updated. 35 | type ErrStackUpdateInProgress struct { 36 | stackName string 37 | stackStatus string 38 | } 39 | 40 | func (err *ErrStackUpdateInProgress) Error() string { 41 | return fmt.Sprintf("stack %s is currently being updated (status %s) and cannot be deployed to", err.stackName, err.stackStatus) 42 | } 43 | 44 | // ErrNotExecutableChangeSet occurs when the change set cannot be executed. 45 | type ErrNotExecutableChangeSet struct { 46 | set *changeSet 47 | } 48 | 49 | func (err *ErrNotExecutableChangeSet) Error() string { 50 | return fmt.Sprintf("cannot execute change set %s because status is %s with reason %s", err.set, err.set.executionStatus, err.set.statusReason) 51 | } 52 | 53 | // ErrTemplateNotFound occurs when we can't find a predefined template. 54 | type ErrTemplateNotFound struct { 55 | templateLocation string 56 | parentErr error 57 | } 58 | 59 | func (err *ErrTemplateNotFound) Error() string { 60 | return fmt.Sprintf("find the cloudformation template at %s", err.templateLocation) 61 | } 62 | 63 | // Is returns true if the target's template location and parent error are equal to this error's template location and parent error. 64 | func (err *ErrTemplateNotFound) Is(target error) bool { 65 | t, ok := target.(*ErrTemplateNotFound) 66 | if !ok { 67 | return false 68 | } 69 | return (err.templateLocation == t.templateLocation) && 70 | (errors.Is(err.parentErr, t.parentErr)) 71 | } 72 | 73 | // Unwrap returns the original error. 74 | func (err *ErrTemplateNotFound) Unwrap() error { 75 | return err.parentErr 76 | } 77 | 78 | // ErrStackSetOutOfDate occurs when we try to read and then update a StackSet but 79 | // between reading it and actually updating it, someone else either started or completed 80 | // an update. 81 | type ErrStackSetOutOfDate struct { 82 | projectName string 83 | parentErr error 84 | } 85 | 86 | func (err *ErrStackSetOutOfDate) Error() string { 87 | return fmt.Sprintf("cannot update project resources for project %s because the stack set update was out of date (feel free to try again)", err.projectName) 88 | } 89 | 90 | // Is returns true if the target's template location and parent error are equal to this error's template location and parent error. 91 | func (err *ErrStackSetOutOfDate) Is(target error) bool { 92 | t, ok := target.(*ErrStackSetOutOfDate) 93 | if !ok { 94 | return false 95 | } 96 | return err.projectName == t.projectName 97 | } 98 | -------------------------------------------------------------------------------- /internal/pkg/deploy/cloudformation/stack/component.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package stack 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "html/template" 9 | 10 | "github.com/Masterminds/sprig" 11 | "github.com/aws/aws-sdk-go/aws" 12 | "github.com/aws/aws-sdk-go/service/cloudformation" 13 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/deploy/cloudformation/types" 14 | "github.com/gobuffalo/packd" 15 | ) 16 | 17 | const ( 18 | templatePath = "core.oam.dev/cf.yml" 19 | ) 20 | 21 | // ComponentStackConfig is for providing all the values to set up an 22 | // component instance stack and to interpret the outputs from it. 23 | type ComponentStackConfig struct { 24 | *types.ComponentInput 25 | box packd.Box 26 | } 27 | 28 | // NewComponentStackConfig sets up a struct which can provide values to CloudFormation for 29 | // spinning up a component instance. 30 | func NewComponentStackConfig(input *types.ComponentInput, box packd.Box) *ComponentStackConfig { 31 | return &ComponentStackConfig{ 32 | ComponentInput: input, 33 | box: box, 34 | } 35 | } 36 | 37 | // Template returns the component instance CloudFormation template. 38 | func (e *ComponentStackConfig) Template() (string, error) { 39 | workloadTemplate, err := e.box.FindString(templatePath) 40 | if err != nil { 41 | return "", &ErrTemplateNotFound{templateLocation: templatePath, parentErr: err} 42 | } 43 | 44 | template, err := template.New("template"). 45 | Funcs(templateFunctions). 46 | Funcs(sprig.FuncMap()). 47 | Parse(workloadTemplate) 48 | if err != nil { 49 | return "", err 50 | } 51 | 52 | var buf bytes.Buffer 53 | if err := template.Execute(&buf, e.ComponentInput); err != nil { 54 | return "", err 55 | } 56 | 57 | return string(buf.Bytes()), nil 58 | } 59 | 60 | // Parameters returns the parameters to be passed into a component instance CloudFormation template. 61 | func (e *ComponentStackConfig) Parameters() []*cloudformation.Parameter { 62 | return []*cloudformation.Parameter{} 63 | } 64 | 65 | // Tags returns the tags that should be applied to the component instance CloudFormation stack. 66 | func (e *ComponentStackConfig) Tags() []*cloudformation.Tag { 67 | return []*cloudformation.Tag{ 68 | { 69 | Key: aws.String(ComponentTagKey), 70 | Value: aws.String(e.ComponentConfiguration.InstanceName), 71 | }, 72 | { 73 | Key: aws.String(AppTagKey), 74 | Value: aws.String(e.ApplicationConfiguration.Name), 75 | }, 76 | { 77 | Key: aws.String(EnvTagKey), 78 | Value: aws.String(EnvTagValue), 79 | }, 80 | } 81 | } 82 | 83 | // StackName returns the name of the CloudFormation stack (hard-coded). 84 | func (e *ComponentStackConfig) StackName() string { 85 | const maxLen = 128 86 | stackName := fmt.Sprintf("oam-ecs-%s-%s", e.ApplicationConfiguration.Name, e.ComponentConfiguration.InstanceName) 87 | if len(stackName) > maxLen { 88 | return stackName[len(stackName)-maxLen:] 89 | } 90 | return stackName 91 | } 92 | 93 | // ToComponent inspects a component instance cloudformation stack and constructs a component instance 94 | // struct out of it 95 | func (e *ComponentStackConfig) ToComponent(stack *cloudformation.Stack) (*types.Component, error) { 96 | outputs := map[string]string{} 97 | 98 | for _, output := range stack.Outputs { 99 | key := *output.OutputKey 100 | value := *output.OutputValue 101 | outputs[key] = value 102 | } 103 | 104 | createdComponent := types.Component{ 105 | StackName: e.StackName(), 106 | StackOutputs: outputs, 107 | } 108 | 109 | return &createdComponent, nil 110 | } 111 | -------------------------------------------------------------------------------- /internal/pkg/deploy/cloudformation/stack/env.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package stack 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/service/cloudformation" 10 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/deploy/cloudformation/types" 11 | "github.com/gobuffalo/packd" 12 | ) 13 | 14 | // EnvStackConfig is for providing all the values to set up an 15 | // environment stack and to interpret the outputs from it. 16 | type EnvStackConfig struct { 17 | *types.EnvironmentInput 18 | box packd.Box 19 | } 20 | 21 | const ( 22 | // EnvTemplatePath is the path where the cloudformation for the environment is written. 23 | EnvTemplatePath = "environment/cf.yml" 24 | ) 25 | 26 | // NewEnvStackConfig sets up a struct which can provide values to CloudFormation for 27 | // spinning up an environment. 28 | func NewEnvStackConfig(input *types.EnvironmentInput, box packd.Box) *EnvStackConfig { 29 | return &EnvStackConfig{ 30 | EnvironmentInput: input, 31 | box: box, 32 | } 33 | } 34 | 35 | // Template returns the environment CloudFormation template. 36 | func (e *EnvStackConfig) Template() (string, error) { 37 | environmentTemplate, err := e.box.FindString(EnvTemplatePath) 38 | if err != nil { 39 | return "", &ErrTemplateNotFound{templateLocation: EnvTemplatePath, parentErr: err} 40 | } 41 | 42 | return environmentTemplate, nil 43 | } 44 | 45 | // Parameters returns the parameters to be passed into a environment CloudFormation template. 46 | func (e *EnvStackConfig) Parameters() []*cloudformation.Parameter { 47 | return []*cloudformation.Parameter{} 48 | } 49 | 50 | // Tags returns the tags that should be applied to the environment CloudFormation stack. 51 | func (e *EnvStackConfig) Tags() []*cloudformation.Tag { 52 | return []*cloudformation.Tag{ 53 | { 54 | Key: aws.String(EnvTagKey), 55 | Value: aws.String(EnvTagValue), 56 | }, 57 | } 58 | } 59 | 60 | // StackName returns the name of the CloudFormation stack (hard-coded). 61 | func (e *EnvStackConfig) StackName() string { 62 | return fmt.Sprintf("%s-%s", EnvTagKey, EnvTagValue) 63 | } 64 | 65 | // ToEnv inspects an environment cloudformation stack and constructs an environment 66 | // struct out of it 67 | func (e *EnvStackConfig) ToEnv(stack *cloudformation.Stack) (*types.Environment, error) { 68 | outputs := map[string]string{} 69 | 70 | for _, output := range stack.Outputs { 71 | key := *output.OutputKey 72 | value := *output.OutputValue 73 | outputs[key] = value 74 | } 75 | 76 | createdEnv := types.Environment{ 77 | StackName: e.StackName(), 78 | StackOutputs: outputs, 79 | } 80 | 81 | return &createdEnv, nil 82 | } 83 | -------------------------------------------------------------------------------- /internal/pkg/deploy/cloudformation/stack/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package stack 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | ) 9 | 10 | // ErrTemplateNotFound occurs when we can't find a predefined template. 11 | type ErrTemplateNotFound struct { 12 | templateLocation string 13 | parentErr error 14 | } 15 | 16 | func (err *ErrTemplateNotFound) Error() string { 17 | return fmt.Sprintf("failed to find the cloudformation template at %s", err.templateLocation) 18 | } 19 | 20 | // Is returns true if the target's template location and parent error are equal to this error's template location and parent error. 21 | func (err *ErrTemplateNotFound) Is(target error) bool { 22 | t, ok := target.(*ErrTemplateNotFound) 23 | if !ok { 24 | return false 25 | } 26 | return (err.templateLocation == t.templateLocation) && 27 | (errors.Is(err.parentErr, t.parentErr)) 28 | } 29 | 30 | // Unwrap returns the original error. 31 | func (err *ErrTemplateNotFound) Unwrap() error { 32 | return err.parentErr 33 | } 34 | -------------------------------------------------------------------------------- /internal/pkg/deploy/cloudformation/stack/tags.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package stack 4 | 5 | // Tag keys used while creating stacks. 6 | const ( 7 | EnvTagKey = "oam-ecs-environment" 8 | EnvTagValue = "default" 9 | AppTagKey = "oam-ecs-application" 10 | ComponentTagKey = "oam-ecs-component" 11 | ) 12 | -------------------------------------------------------------------------------- /internal/pkg/deploy/cloudformation/stack/template_functions.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package stack 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/oam-dev/oam-go-sdk/apis/core.oam.dev/v1alpha1" 9 | ) 10 | 11 | var templateFunctions = map[string]interface{}{ 12 | "ResolveParameterValue": resolveOAMParameterValue, 13 | "ResolveTraitValue": resolveOAMTraitValue, 14 | "TaskCPU": resolveTaskCpuValue, 15 | "TaskMemory": resolveTaskMemoryValue, 16 | "RequiresVolumes": hasAnyVolumes, 17 | "RequiresPrivateRegistryAuth": hasAnyPullSecrets, 18 | "HealthCheckGracePeriod": resolveHealthCheckGracePeriod, 19 | } 20 | 21 | // resolveOAMParameterValue finds the value of a named parameter 22 | // for a given component instance configuration 23 | func resolveOAMParameterValue(paramName string, componentConfiguration *v1alpha1.ComponentConfiguration, componentSpec *v1alpha1.ComponentSpec) (string, error) { 24 | for _, paramValue := range componentConfiguration.ParameterValues { 25 | if paramValue.Name == paramName { 26 | return paramValue.Value, nil 27 | } 28 | } 29 | 30 | for _, paramSpec := range componentSpec.Parameters { 31 | if paramSpec.Name == paramName { 32 | if paramSpec.Default != "" { 33 | return paramSpec.Default, nil 34 | } 35 | break 36 | } 37 | } 38 | 39 | return "", fmt.Errorf("Could not find parameter value for name %s", paramName) 40 | } 41 | 42 | // resolveOAMTraitValue finds the value of a named parameter 43 | // for a given component instance configuration 44 | func resolveOAMTraitValue(traitName string, propertyName string, defaultValue int32, componentConfiguration *v1alpha1.ComponentConfiguration) int32 { 45 | if componentConfiguration.ExistTrait(traitName) { 46 | _, _, properties := componentConfiguration.ExtractTrait(traitName) 47 | 48 | if val, ok := properties[propertyName]; ok { 49 | return int32(val.(float64)) 50 | } 51 | } 52 | 53 | return defaultValue 54 | } 55 | 56 | // hasAnyVolumes checks whether at least one of the containers requires a volume 57 | func hasAnyVolumes(containers []v1alpha1.Container) bool { 58 | hasVolumes := false 59 | 60 | for _, container := range containers { 61 | if len(container.Resources.Volumes) > 0 { 62 | hasVolumes = true 63 | break 64 | } 65 | } 66 | 67 | return hasVolumes 68 | } 69 | 70 | // hasAnyPullSecrets checks whether at least one of the containers provides an image pull secret 71 | func hasAnyPullSecrets(containers []v1alpha1.Container) bool { 72 | hasAnyPullSecrets := false 73 | 74 | for _, container := range containers { 75 | if container.ImagePullSecret != "" { 76 | hasAnyPullSecrets = true 77 | break 78 | } 79 | } 80 | 81 | return hasAnyPullSecrets 82 | } 83 | 84 | // resolveHealthCheckGracePeriod finds the max grace period across all containers 85 | func resolveHealthCheckGracePeriod(containers []v1alpha1.Container) int32 { 86 | gracePeriod := int32(0) 87 | 88 | for _, container := range containers { 89 | if container.LivenessProbe != nil && (container.LivenessProbe.HttpGet != nil || container.LivenessProbe.TcpSocket != nil) { 90 | if container.LivenessProbe.InitialDelaySeconds > gracePeriod { 91 | gracePeriod = container.LivenessProbe.InitialDelaySeconds 92 | } 93 | } 94 | } 95 | 96 | return gracePeriod 97 | } 98 | 99 | type fargateTaskSize struct { 100 | cpuShare float64 101 | memoryMiB int64 102 | } 103 | 104 | func getValidFargateTaskSizes() []*fargateTaskSize { 105 | // .25 vCPU 106 | taskSizes := []*fargateTaskSize{ 107 | &fargateTaskSize{cpuShare: .25, memoryMiB: 512}, 108 | &fargateTaskSize{cpuShare: .25, memoryMiB: 1024}, 109 | &fargateTaskSize{cpuShare: .25, memoryMiB: 2048}, 110 | } 111 | 112 | // .5 vCPU 113 | for i := int64(1); i <= 4; i++ { 114 | taskSizes = append(taskSizes, &fargateTaskSize{cpuShare: .5, memoryMiB: i * 1024}) 115 | } 116 | 117 | // 1 vCPU 118 | for i := int64(2); i <= 8; i++ { 119 | taskSizes = append(taskSizes, &fargateTaskSize{cpuShare: 1, memoryMiB: i * 1024}) 120 | } 121 | 122 | // 2 vCPU 123 | for i := int64(4); i <= 16; i++ { 124 | taskSizes = append(taskSizes, &fargateTaskSize{cpuShare: 2, memoryMiB: i * 1024}) 125 | } 126 | 127 | // 4 vCPU 128 | for i := int64(8); i <= 30; i++ { 129 | taskSizes = append(taskSizes, &fargateTaskSize{cpuShare: 4, memoryMiB: i * 1024}) 130 | } 131 | 132 | return taskSizes 133 | } 134 | 135 | func getNearestFargateTaskSize(containers []v1alpha1.Container) (*fargateTaskSize, error) { 136 | containersCpuShare := float64(0) 137 | containersMemoryMiB := int64(0) 138 | 139 | for _, container := range containers { 140 | containersCpuShare += float64(container.Resources.Cpu.Required.MilliValue()) / float64(1000) 141 | containersMemoryMiB += container.Resources.Memory.Required.MilliValue() / int64(1000) / int64(1000000) 142 | } 143 | 144 | for _, taskSize := range getValidFargateTaskSizes() { 145 | if containersCpuShare <= taskSize.cpuShare && containersMemoryMiB <= taskSize.memoryMiB { 146 | return taskSize, nil 147 | } 148 | } 149 | 150 | return nil, fmt.Errorf("Could not find valid Fargate task size for the given CPU and memory requirements: %f CPU shares, %d MiB memory", containersCpuShare, containersMemoryMiB) 151 | } 152 | 153 | // resolveTaskCpuValue finds the closest Fargate size for the containers' CPU and memory requirements, 154 | // and returns the Fargate CPU size 155 | func resolveTaskCpuValue(containers []v1alpha1.Container) (string, error) { 156 | taskSize, err := getNearestFargateTaskSize(containers) 157 | if err != nil { 158 | return "", err 159 | } 160 | return fmt.Sprintf("%.2f vcpu", taskSize.cpuShare), nil 161 | } 162 | 163 | // resolveTaskMemoryValue finds the closest Fargate size for the containers' CPU and memory requirements, 164 | // and returns the Fargate memory size 165 | func resolveTaskMemoryValue(containers []v1alpha1.Container) (string, error) { 166 | taskSize, err := getNearestFargateTaskSize(containers) 167 | if err != nil { 168 | return "", err 169 | } 170 | return fmt.Sprintf("%d", taskSize.memoryMiB), nil 171 | } 172 | -------------------------------------------------------------------------------- /internal/pkg/deploy/cloudformation/stack_status.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package cloudformation 4 | 5 | import ( 6 | "strings" 7 | 8 | "github.com/aws/aws-sdk-go/service/cloudformation" 9 | ) 10 | 11 | // StackStatus stacks 12 | type StackStatus string 13 | 14 | // RequiresCleanup indicates that the stack was created, but failed. 15 | // It should be deleted. 16 | func (s StackStatus) RequiresCleanup() bool { 17 | return cloudformation.StackStatusRollbackComplete == string(s) || 18 | cloudformation.StackStatusRollbackFailed == string(s) 19 | } 20 | 21 | // InProgress that the stack is currently being updated. 22 | func (s StackStatus) InProgress() bool { 23 | return strings.HasSuffix(string(s), "IN_PROGRESS") 24 | } 25 | -------------------------------------------------------------------------------- /internal/pkg/deploy/cloudformation/types/component.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package types 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "sort" 9 | "strings" 10 | 11 | "github.com/iancoleman/strcase" 12 | "github.com/oam-dev/oam-go-sdk/apis/core.oam.dev/v1alpha1" 13 | "github.com/olekukonko/tablewriter" 14 | ) 15 | 16 | // ComponentInput holds the fields required to deploy an component instance. 17 | type ComponentInput struct { 18 | ApplicationConfiguration *v1alpha1.ApplicationConfiguration 19 | ComponentConfiguration *v1alpha1.ComponentConfiguration 20 | Component *v1alpha1.ComponentSchematic 21 | Environment *ComponentEnvironment 22 | WorkloadSettings *ECSWorkloadSettings 23 | } 24 | 25 | // ECSWorkloadSettings holds fields that are needed to define services in ECS, which are not part of the core OAM types 26 | type ECSWorkloadSettings struct { 27 | TaskCPU string 28 | TaskMemory string 29 | } 30 | 31 | // Environment represents attributes about the environment where the component will be deployed 32 | type ComponentEnvironment struct { 33 | Name string 34 | } 35 | 36 | // Component represents the configuration of a particular component instance 37 | type Component struct { 38 | StackName string 39 | StackOutputs map[string]string 40 | } 41 | 42 | // DeployComponenttResponse holds the created component instance on successful deployment. 43 | // Otherwise, the component is set to nil and a descriptive error is returned. 44 | type DeployComponentResponse struct { 45 | Component *Component 46 | Err error 47 | } 48 | 49 | func (component *Component) Display() { 50 | fmt.Printf("\nComponent Instance: %s\n\n", component.StackName) 51 | 52 | table := tablewriter.NewWriter(os.Stdout) 53 | table.SetHeader([]string{"Component Instance Attribute", "Value"}) 54 | table.SetBorder(false) 55 | 56 | keys := make([]string, 0, len(component.StackOutputs)) 57 | for key := range component.StackOutputs { 58 | keys = append(keys, key) 59 | } 60 | sort.Strings(keys) 61 | 62 | for _, key := range keys { 63 | formattedKey := strings.Title(strings.ToLower(strcase.ToDelimited(key, ' '))) 64 | formattedKey = strings.ReplaceAll(formattedKey, "Cloud Formation", "CloudFormation") 65 | formattedKey = strings.ReplaceAll(formattedKey, "Ecs", "ECS") 66 | table.Append([]string{formattedKey, component.StackOutputs[key]}) 67 | } 68 | 69 | table.Render() 70 | fmt.Println("") 71 | } 72 | -------------------------------------------------------------------------------- /internal/pkg/deploy/cloudformation/types/env.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package types 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "sort" 9 | "strings" 10 | 11 | "github.com/iancoleman/strcase" 12 | "github.com/olekukonko/tablewriter" 13 | ) 14 | 15 | // EnvironmentInput holds the fields required to interact with an environment. 16 | type EnvironmentInput struct { 17 | } 18 | 19 | // Environment represents the configuration of a particular environment 20 | type Environment struct { 21 | StackName string 22 | StackOutputs map[string]string 23 | } 24 | 25 | // CreateEnvironmentResponse holds the created environment on successful deployment. 26 | // Otherwise, the environment is set to nil and a descriptive error is returned. 27 | type CreateEnvironmentResponse struct { 28 | Env *Environment 29 | Err error 30 | } 31 | 32 | func (env *Environment) Display() { 33 | fmt.Printf("\nEnvironment: %s\n\n", env.StackName) 34 | 35 | table := tablewriter.NewWriter(os.Stdout) 36 | table.SetHeader([]string{"Environment Attribute", "Value"}) 37 | table.SetBorder(false) 38 | 39 | keys := make([]string, 0, len(env.StackOutputs)) 40 | for key := range env.StackOutputs { 41 | keys = append(keys, key) 42 | } 43 | sort.Strings(keys) 44 | 45 | for _, key := range keys { 46 | formattedKey := strings.Title(strings.ToLower(strcase.ToDelimited(key, ' '))) 47 | formattedKey = strings.ReplaceAll(formattedKey, "Cloud Formation", "CloudFormation") 48 | formattedKey = strings.ReplaceAll(formattedKey, "Ecs", "ECS") 49 | table.Append([]string{formattedKey, env.StackOutputs[key]}) 50 | } 51 | 52 | table.Render() 53 | fmt.Println("") 54 | } 55 | -------------------------------------------------------------------------------- /internal/pkg/term/color/color.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package color provides functionality to displayed colored text on the terminal. 5 | package color 6 | 7 | import ( 8 | "os" 9 | "strings" 10 | 11 | "github.com/AlecAivazis/survey/v2/core" 12 | "github.com/fatih/color" 13 | ) 14 | 15 | // Predefined colors. 16 | // Refer to https://en.wikipedia.org/wiki/ANSI_escape_code to validate if colors would 17 | // be visible on white or black screen backgrounds. 18 | var ( 19 | Grey = color.New(color.FgWhite) 20 | Red = color.New(color.FgHiRed) 21 | Cyan = color.New(color.FgCyan) 22 | BoldUnderline = color.New(color.Bold, color.Underline) 23 | Magenta = color.New(color.FgMagenta) 24 | Blue = color.New(color.FgBlue) 25 | ) 26 | 27 | const colorEnvVar = "COLOR" 28 | 29 | var lookupEnv = os.LookupEnv 30 | 31 | // DisableColorBasedOnEnvVar determines whether the CLI will produce color 32 | // output based on the environment variable, COLOR. 33 | func DisableColorBasedOnEnvVar() { 34 | value, exists := lookupEnv(colorEnvVar) 35 | if !exists { 36 | // if the COLOR environment variable is not set 37 | // then follow the settings in the color library 38 | // since it's dynamically set based on the type of terminal 39 | // and whether stdout is connected to a terminal or not. 40 | core.DisableColor = color.NoColor 41 | return 42 | } 43 | 44 | if strings.ToLower(value) == "false" { 45 | core.DisableColor = true 46 | color.NoColor = true 47 | } else if strings.ToLower(value) == "true" { 48 | core.DisableColor = false 49 | color.NoColor = false 50 | } 51 | } 52 | 53 | // HighlightUserInput colors the string to denote it as an input from standard input, and returns it. 54 | func HighlightUserInput(s string) string { 55 | return Cyan.Sprint(s) 56 | } 57 | 58 | // HighlightResource colors the string to denote it as a resource created by the CLI, and returns it. 59 | func HighlightResource(s string) string { 60 | return BoldUnderline.Sprint(s) 61 | } 62 | 63 | // HighlightCode wraps the string with the ` character, colors it to denote it's a code block, and returns it. 64 | func HighlightCode(s string) string { 65 | return Magenta.Sprintf("`%s`", s) 66 | } 67 | -------------------------------------------------------------------------------- /internal/pkg/term/color/color_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package color 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/AlecAivazis/survey/v2/core" 9 | "github.com/fatih/color" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | type envVar struct { 14 | env map[string]string 15 | } 16 | 17 | func (e *envVar) lookupEnv(key string) (string, bool) { 18 | v, ok := e.env[key] 19 | return v, ok 20 | } 21 | 22 | func TestColorEnvVarSetToFalse(t *testing.T) { 23 | env := &envVar{ 24 | env: map[string]string{colorEnvVar: "false"}, 25 | } 26 | lookupEnv = env.lookupEnv 27 | 28 | DisableColorBasedOnEnvVar() 29 | 30 | require.True(t, core.DisableColor, "expected to be true when COLOR is disabled") 31 | require.True(t, color.NoColor, "expected to be true when COLOR is disabled") 32 | } 33 | 34 | func TestColorEnvVarSetToTrue(t *testing.T) { 35 | env := &envVar{ 36 | env: map[string]string{colorEnvVar: "true"}, 37 | } 38 | lookupEnv = env.lookupEnv 39 | 40 | DisableColorBasedOnEnvVar() 41 | 42 | require.False(t, core.DisableColor, "expected to be false when COLOR is enabled") 43 | require.False(t, color.NoColor, "expected to be true when COLOR is enabled") 44 | } 45 | 46 | func TestColorEnvVarNotSet(t *testing.T) { 47 | env := &envVar{ 48 | env: make(map[string]string), 49 | } 50 | lookupEnv = env.lookupEnv 51 | 52 | DisableColorBasedOnEnvVar() 53 | 54 | require.Equal(t, core.DisableColor, color.NoColor, "expected to be the same as color.NoColor") 55 | } 56 | -------------------------------------------------------------------------------- /internal/pkg/term/cursor/cursor.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package cursor provides functionality to interact with the terminal cursor. 5 | package cursor 6 | 7 | import ( 8 | "os" 9 | 10 | "github.com/AlecAivazis/survey/v2/terminal" 11 | ) 12 | 13 | type cursor interface { 14 | Up(n int) 15 | Down(n int) 16 | } 17 | 18 | // Cursor represents the terminal's cursor. 19 | type Cursor struct { 20 | c cursor 21 | } 22 | 23 | // New creates a new cursor that writes to stderr and reads from stdin. 24 | func New() *Cursor { 25 | return &Cursor{ 26 | c: &terminal.Cursor{ 27 | In: os.Stdin, 28 | Out: os.Stderr, 29 | }, 30 | } 31 | } 32 | 33 | // Up moves the cursor n lines. 34 | func (c *Cursor) Up(n int) { 35 | c.c.Up(n) 36 | } 37 | 38 | // Down moves the cursor n lines. 39 | func (c *Cursor) Down(n int) { 40 | c.c.Down(n) 41 | } 42 | 43 | // EraseLine deletes the contents of the current line. 44 | func (c *Cursor) EraseLine() { 45 | if cur, ok := c.c.(*terminal.Cursor); ok { 46 | terminal.EraseLine(cur.Out, terminal.ERASE_LINE_ALL) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /internal/pkg/term/log/log.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package log is a wrapper around the fmt package to print messages to the terminal. 5 | package log 6 | 7 | import ( 8 | "fmt" 9 | 10 | "github.com/fatih/color" 11 | ) 12 | 13 | // Colored string formatting functions. 14 | var ( 15 | successSprintf = color.HiGreenString 16 | errorSprintf = color.HiRedString 17 | warningSprintf = color.YellowString 18 | debugSprintf = color.WhiteString 19 | ) 20 | 21 | // Wrapper writers around standard error and standard output that work on windows. 22 | var ( 23 | DiagnosticWriter = color.Error 24 | OutputWriter = color.Output 25 | ) 26 | 27 | // Log message prefixes. 28 | const ( 29 | warningPrefix = "Note:" 30 | ) 31 | 32 | // Success prefixes the message with a green "✔ Success!", and writes to standard error. 33 | func Success(args ...interface{}) { 34 | msg := fmt.Sprintf("%s %s", successSprintf(successPrefix), fmt.Sprint(args...)) 35 | fmt.Fprint(DiagnosticWriter, msg) 36 | } 37 | 38 | // Successln prefixes the message with a green "✔ Success!", and writes to standard error with a new line. 39 | func Successln(args ...interface{}) { 40 | msg := fmt.Sprintf("%s %s", successSprintf(successPrefix), fmt.Sprint(args...)) 41 | fmt.Fprintln(DiagnosticWriter, msg) 42 | } 43 | 44 | // Successf formats according to the specifier, prefixes the message with a green "✔ Success!", and writes to standard error. 45 | func Successf(format string, args ...interface{}) { 46 | wrappedFormat := fmt.Sprintf("%s %s", successSprintf(successPrefix), format) 47 | fmt.Fprintf(DiagnosticWriter, wrappedFormat, args...) 48 | } 49 | 50 | // Ssuccess prefixes the message with a green "✔ Success!", and returns it. 51 | func Ssuccess(args ...interface{}) string { 52 | return fmt.Sprintf("%s %s", successSprintf(successPrefix), fmt.Sprint(args...)) 53 | } 54 | 55 | // Ssuccessln prefixes the message with a green "✔ Success!", appends a new line, and returns it. 56 | func Ssuccessln(args ...interface{}) string { 57 | msg := fmt.Sprintf("%s %s", successSprintf(successPrefix), fmt.Sprint(args...)) 58 | return fmt.Sprintln(msg) 59 | } 60 | 61 | // Ssuccessf formats according to the specifier, prefixes the message with a green "✔ Success!", and returns it. 62 | func Ssuccessf(format string, args ...interface{}) string { 63 | wrappedFormat := fmt.Sprintf("%s %s", successSprintf(successPrefix), format) 64 | return fmt.Sprintf(wrappedFormat, args...) 65 | } 66 | 67 | // Error prefixes the message with a red "✘ Error!", and writes to standard error. 68 | func Error(args ...interface{}) { 69 | msg := fmt.Sprintf("%s %s", errorSprintf(errorPrefix), fmt.Sprint(args...)) 70 | fmt.Fprint(DiagnosticWriter, msg) 71 | } 72 | 73 | // Errorln prefixes the message with a red "✘ Error!", and writes to standard error with a new line. 74 | func Errorln(args ...interface{}) { 75 | msg := fmt.Sprintf("%s %s", errorSprintf(errorPrefix), fmt.Sprint(args...)) 76 | fmt.Fprintln(DiagnosticWriter, msg) 77 | } 78 | 79 | // Errorf formats according to the specifier, prefixes the message with a red "✘ Error!", and writes to standard error. 80 | func Errorf(format string, args ...interface{}) { 81 | wrappedFormat := fmt.Sprintf("%s %s", errorSprintf(errorPrefix), format) 82 | fmt.Fprintf(DiagnosticWriter, wrappedFormat, args...) 83 | } 84 | 85 | // Serror prefixes the message with a red "✘ Error!", and returns it. 86 | func Serror(args ...interface{}) string { 87 | return fmt.Sprintf("%s %s", errorSprintf(errorPrefix), fmt.Sprint(args...)) 88 | } 89 | 90 | // Serrorln prefixes the message with a red "✘ Error!", appends a new line, and returns it. 91 | func Serrorln(args ...interface{}) string { 92 | msg := fmt.Sprintf("%s %s", errorSprintf(errorPrefix), fmt.Sprint(args...)) 93 | return fmt.Sprintln(msg) 94 | } 95 | 96 | // Serrorf formats according to the specifier, prefixes the message with a red "✘ Error!", and returns it. 97 | func Serrorf(format string, args ...interface{}) string { 98 | wrappedFormat := fmt.Sprintf("%s %s", errorSprintf(errorPrefix), format) 99 | return fmt.Sprintf(wrappedFormat, args...) 100 | } 101 | 102 | // Warning prefixes the message with a "Note:", colors the *entire* message in yellow, writes to standard error. 103 | func Warning(args ...interface{}) { 104 | msg := fmt.Sprint(args...) 105 | fmt.Fprint(DiagnosticWriter, warningSprintf(fmt.Sprintf("%s %s", warningPrefix, msg))) 106 | } 107 | 108 | // Warningln prefixes the message with a "Note:", colors the *entire* message in yellow, writes to standard error with a new line. 109 | func Warningln(args ...interface{}) { 110 | msg := fmt.Sprint(args...) 111 | fmt.Fprintln(DiagnosticWriter, warningSprintf(fmt.Sprintf("%s %s", warningPrefix, msg))) 112 | } 113 | 114 | // Warningf formats according to the specifier, prefixes the message with a "Note:", colors the *entire* message in yellow, and writes to standard error. 115 | func Warningf(format string, args ...interface{}) { 116 | wrappedFormat := fmt.Sprintf("%s %s", warningPrefix, format) 117 | fmt.Fprintf(DiagnosticWriter, warningSprintf(wrappedFormat, args...)) 118 | } 119 | 120 | // Info writes the message to standard error with the default color. 121 | func Info(args ...interface{}) { 122 | fmt.Fprint(DiagnosticWriter, args...) 123 | } 124 | 125 | // Infoln writes the message to standard error with the default color and new line. 126 | func Infoln(args ...interface{}) { 127 | fmt.Fprintln(DiagnosticWriter, args...) 128 | } 129 | 130 | // Infof formats according to the specifier, and writes to standard error with the default color. 131 | func Infof(format string, args ...interface{}) { 132 | fmt.Fprintf(DiagnosticWriter, format, args...) 133 | } 134 | 135 | // Debug writes the message to standard error in grey. 136 | func Debug(args ...interface{}) { 137 | fmt.Fprint(DiagnosticWriter, debugSprintf(fmt.Sprint(args...))) 138 | } 139 | 140 | // Debugln writes the message to standard error in grey and with a new line. 141 | func Debugln(args ...interface{}) { 142 | fmt.Fprintln(DiagnosticWriter, debugSprintf(fmt.Sprint(args...))) 143 | } 144 | 145 | // Debugf formats according to the specifier, colors the message in grey, and writes to standard error. 146 | func Debugf(format string, args ...interface{}) { 147 | fmt.Fprint(DiagnosticWriter, debugSprintf(format, args...)) 148 | } 149 | -------------------------------------------------------------------------------- /internal/pkg/term/log/log_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package log 5 | 6 | import ( 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestSuccess(t *testing.T) { 14 | // GIVEN 15 | b := &strings.Builder{} 16 | DiagnosticWriter = b 17 | 18 | // WHEN 19 | Success("hello", " world") 20 | 21 | // THEN 22 | require.Contains(t, b.String(), "Success!") 23 | require.Contains(t, b.String(), "hello world") 24 | } 25 | 26 | func TestSuccessln(t *testing.T) { 27 | // GIVEN 28 | b := &strings.Builder{} 29 | DiagnosticWriter = b 30 | 31 | // WHEN 32 | Successln("hello", " world") 33 | 34 | // THEN 35 | require.Contains(t, b.String(), "Success!") 36 | require.Contains(t, b.String(), "hello world\n") 37 | } 38 | 39 | func TestSuccessf(t *testing.T) { 40 | // GIVEN 41 | b := &strings.Builder{} 42 | DiagnosticWriter = b 43 | 44 | // WHEN 45 | Successf("%s %s\n", "hello", "world") 46 | 47 | // THEN 48 | require.Contains(t, b.String(), "Success!") 49 | require.Contains(t, b.String(), "hello world\n") 50 | } 51 | 52 | func TestSsuccess(t *testing.T) { 53 | s := Ssuccess("hello", " world") 54 | 55 | require.Contains(t, s, "Success!") 56 | require.Contains(t, s, "hello world") 57 | } 58 | 59 | func TestSsuccessln(t *testing.T) { 60 | s := Ssuccessln("hello", " world") 61 | 62 | // THEN 63 | require.Contains(t, s, "Success!") 64 | require.Contains(t, s, "hello world\n") 65 | } 66 | 67 | func TestSsuccessf(t *testing.T) { 68 | s := Ssuccessf("%s %s\n", "hello", "world") 69 | 70 | require.Contains(t, s, "Success!") 71 | require.Contains(t, s, "hello world\n") 72 | } 73 | 74 | func TestError(t *testing.T) { 75 | // GIVEN 76 | b := &strings.Builder{} 77 | DiagnosticWriter = b 78 | 79 | // WHEN 80 | Error("hello", " world") 81 | 82 | // THEN 83 | require.Contains(t, b.String(), "Error!") 84 | require.Contains(t, b.String(), "hello world") 85 | } 86 | 87 | func TestErrorln(t *testing.T) { 88 | // GIVEN 89 | b := &strings.Builder{} 90 | DiagnosticWriter = b 91 | 92 | // WHEN 93 | Errorln("hello", " world") 94 | 95 | // THEN 96 | require.Contains(t, b.String(), "Error!") 97 | require.Contains(t, b.String(), "hello world\n") 98 | } 99 | 100 | func TestErrorf(t *testing.T) { 101 | // GIVEN 102 | b := &strings.Builder{} 103 | DiagnosticWriter = b 104 | 105 | // WHEN 106 | Errorf("%s %s\n", "hello", "world") 107 | 108 | // THEN 109 | require.Contains(t, b.String(), "Error!") 110 | require.Contains(t, b.String(), "hello world\n") 111 | } 112 | 113 | func TestSerror(t *testing.T) { 114 | s := Serror("hello", " world") 115 | 116 | require.Contains(t, s, "Error!") 117 | require.Contains(t, s, "hello world") 118 | } 119 | 120 | func TestSerrorln(t *testing.T) { 121 | s := Serrorln("hello", " world") 122 | 123 | require.Contains(t, s, "Error!") 124 | require.Contains(t, s, "hello world\n") 125 | } 126 | 127 | func TestSerrorf(t *testing.T) { 128 | s := Serrorf("%s %s\n", "hello", "world") 129 | 130 | require.Contains(t, s, "Error!") 131 | require.Contains(t, s, "hello world\n") 132 | } 133 | 134 | func TestWarning(t *testing.T) { 135 | // GIVEN 136 | b := &strings.Builder{} 137 | DiagnosticWriter = b 138 | 139 | // WHEN 140 | Warning("hello", " world") 141 | 142 | // THEN 143 | require.Contains(t, b.String(), "Note:") 144 | require.Contains(t, b.String(), "hello world") 145 | } 146 | 147 | func TestWarningln(t *testing.T) { 148 | // GIVEN 149 | b := &strings.Builder{} 150 | DiagnosticWriter = b 151 | 152 | // WHEN 153 | Warningln("hello", " world") 154 | 155 | // THEN 156 | require.Contains(t, b.String(), "Note:") 157 | require.Contains(t, b.String(), "hello world\n") 158 | } 159 | 160 | func TestWarningf(t *testing.T) { 161 | // GIVEN 162 | b := &strings.Builder{} 163 | DiagnosticWriter = b 164 | 165 | // WHEN 166 | Warningf("%s %s\n", "hello", "world") 167 | 168 | // THEN 169 | require.Contains(t, b.String(), "Note:") 170 | require.Contains(t, b.String(), "hello world\n") 171 | } 172 | 173 | func TestInfo(t *testing.T) { 174 | // GIVEN 175 | b := &strings.Builder{} 176 | DiagnosticWriter = b 177 | 178 | // WHEN 179 | Info("hello", " world") 180 | 181 | // THEN 182 | require.Equal(t, "hello world", b.String()) 183 | } 184 | 185 | func TestInfoln(t *testing.T) { 186 | // GIVEN 187 | b := &strings.Builder{} 188 | DiagnosticWriter = b 189 | 190 | // WHEN 191 | Infoln("hello", "world") 192 | 193 | // THEN 194 | require.Equal(t, "hello world\n", b.String()) 195 | } 196 | 197 | func TestInfof(t *testing.T) { 198 | // GIVEN 199 | b := &strings.Builder{} 200 | DiagnosticWriter = b 201 | 202 | // WHEN 203 | Infof("%s %s\n", "hello", "world") 204 | 205 | // THEN 206 | require.Equal(t, "hello world\n", b.String()) 207 | } 208 | 209 | func TestDebug(t *testing.T) { 210 | // GIVEN 211 | b := &strings.Builder{} 212 | DiagnosticWriter = b 213 | 214 | // WHEN 215 | Debug("hello", " world") 216 | 217 | // THEN 218 | require.Contains(t, b.String(), "hello world") 219 | } 220 | 221 | func TestDebugln(t *testing.T) { 222 | // GIVEN 223 | b := &strings.Builder{} 224 | DiagnosticWriter = b 225 | 226 | // WHEN 227 | Debugln("hello", " world") 228 | 229 | // THEN 230 | require.Contains(t, b.String(), "hello world\n") 231 | } 232 | 233 | func TestDebugf(t *testing.T) { 234 | // GIVEN 235 | b := &strings.Builder{} 236 | DiagnosticWriter = b 237 | 238 | // WHEN 239 | Debugf("%s %s\n", "hello", "world") 240 | 241 | // THEN 242 | require.Contains(t, b.String(), "hello world\n") 243 | } 244 | -------------------------------------------------------------------------------- /internal/pkg/term/log/prefix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 5 | // SPDX-License-Identifier: Apache-2.0 6 | 7 | package log 8 | 9 | // Log message prefixes. 10 | const ( 11 | successPrefix = "✔ Success!" 12 | errorPrefix = "✘ Error!" 13 | ) 14 | -------------------------------------------------------------------------------- /internal/pkg/term/log/prefix_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package log 5 | 6 | const ( 7 | successPrefix = "√ Success!" 8 | errorPrefix = "X Error!" 9 | ) 10 | -------------------------------------------------------------------------------- /internal/pkg/term/progress/charset.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 5 | // SPDX-License-Identifier: Apache-2.0 6 | 7 | package progress 8 | 9 | import ( 10 | spin "github.com/briandowns/spinner" 11 | ) 12 | 13 | var charset = spin.CharSets[14] 14 | -------------------------------------------------------------------------------- /internal/pkg/term/progress/charset_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package progress 4 | 5 | var charset = []string{"/", "-", "\\"} 6 | -------------------------------------------------------------------------------- /internal/pkg/term/progress/deploy/cloudformation/deploy.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package cloudformation 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | 10 | deploy "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/deploy/cloudformation" 11 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/term/color" 12 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/term/progress" 13 | ) 14 | 15 | // ResourceMatcher is a function that returns true if the resource event matches a criteria. 16 | type ResourceMatcher func(deploy.Resource) bool 17 | 18 | // HumanizeResourceEvents groups raw deploy events under human-friendly tab-separated texts 19 | // that can be passed into the Events() method. Every text to display starts with status in progress. 20 | // For every resource event that belongs to a text, we preserve failure events if there was one. 21 | // Otherwise, the text remains in progress until the expected number of resources reach the complete status. 22 | func HumanizeResourceEvents(orderedTexts []progress.Text, resourceEvents []deploy.ResourceEvent, matcher map[progress.Text]ResourceMatcher, wantedCount map[progress.Text]int) []progress.TabRow { 23 | // Assign a status to text from all matched events. 24 | statuses := make(map[progress.Text]progress.Status) 25 | reasons := make(map[progress.Text]string) 26 | for text, matches := range matcher { 27 | statuses[text] = progress.StatusInProgress 28 | for _, resourceEvent := range resourceEvents { 29 | if !matches(resourceEvent.Resource) { 30 | continue 31 | } 32 | if oldStatus, ok := statuses[text]; ok && oldStatus == progress.StatusFailed { 33 | // There was a failure event, keep its status. 34 | continue 35 | } 36 | status := toStatus(resourceEvent.Status) 37 | if status == progress.StatusComplete || status == progress.StatusSkipped { 38 | // If there are more resources that needs to have StatusComplete then the text should remain in StatusInProgress. 39 | wantedCount[text] = wantedCount[text] - 1 40 | if wantedCount[text] > 0 { 41 | status = progress.StatusInProgress 42 | } 43 | } 44 | statuses[text] = status 45 | reasons[text] = resourceEvent.StatusReason 46 | } 47 | } 48 | 49 | // Serialize the text and status to a format digestible by Events(). 50 | var rows []progress.TabRow 51 | for _, text := range orderedTexts { 52 | status, ok := statuses[text] 53 | if !ok { 54 | continue 55 | } 56 | coloredStatus := fmt.Sprintf("[%s]", status) 57 | if status == progress.StatusInProgress { 58 | coloredStatus = color.Grey.Sprint(coloredStatus) 59 | } 60 | if status == progress.StatusFailed { 61 | coloredStatus = color.Red.Sprint(coloredStatus) 62 | } 63 | 64 | rows = append(rows, progress.TabRow(fmt.Sprintf("%s\t%s", color.Grey.Sprint(text), coloredStatus))) 65 | if status == progress.StatusFailed { 66 | rows = append(rows, progress.TabRow(fmt.Sprintf(" %s\t", reasons[text]))) 67 | } 68 | } 69 | return rows 70 | } 71 | 72 | func toStatus(s string) progress.Status { 73 | if strings.HasSuffix(s, "FAILED") { 74 | return progress.StatusFailed 75 | } 76 | if strings.HasSuffix(s, "COMPLETE") { 77 | return progress.StatusComplete 78 | } 79 | if strings.HasSuffix(s, "SKIPPED") { 80 | return progress.StatusSkipped 81 | } 82 | return progress.StatusInProgress 83 | } 84 | -------------------------------------------------------------------------------- /internal/pkg/term/progress/deploy/cloudformation/deploy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package cloudformation 5 | 6 | import ( 7 | "testing" 8 | 9 | deploy "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/deploy/cloudformation" 10 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/term/progress" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestHumanizeResourceEvents(t *testing.T) { 15 | testCases := map[string]struct { 16 | inResourceEvents []deploy.ResourceEvent 17 | inDisplayOrder []progress.Text 18 | inMatcher map[progress.Text]ResourceMatcher 19 | 20 | wantedEvents []progress.TabRow 21 | }{ 22 | "grabs the first failure": { 23 | inResourceEvents: []deploy.ResourceEvent{ 24 | { 25 | Resource: deploy.Resource{ 26 | LogicalName: "VPC", 27 | Type: "AWS::EC2::VPC", 28 | }, 29 | Status: "CREATE_FAILED", 30 | StatusReason: "first failure", 31 | }, 32 | { 33 | Resource: deploy.Resource{ 34 | LogicalName: "VPC", 35 | Type: "AWS::EC2::VPC", 36 | }, 37 | Status: "CREATE_FAILED", 38 | StatusReason: "second failure", 39 | }, 40 | }, 41 | inDisplayOrder: []progress.Text{"vpc"}, 42 | inMatcher: map[progress.Text]ResourceMatcher{ 43 | "vpc": func(resource deploy.Resource) bool { 44 | return resource.Type == "AWS::EC2::VPC" 45 | }, 46 | }, 47 | 48 | wantedEvents: []progress.TabRow{"vpc\t[Failed]", " first failure\t"}, 49 | }, 50 | } 51 | 52 | for name, tc := range testCases { 53 | t.Run(name, func(t *testing.T) { 54 | got := HumanizeResourceEvents(tc.inDisplayOrder, tc.inResourceEvents, tc.inMatcher, nil) 55 | 56 | require.Equal(t, tc.wantedEvents, got) 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/pkg/term/progress/mocks/mock_spinner.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ./internal/pkg/term/progress/spinner.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | reflect "reflect" 10 | ) 11 | 12 | // MockstartStopper is a mock of startStopper interface 13 | type MockstartStopper struct { 14 | ctrl *gomock.Controller 15 | recorder *MockstartStopperMockRecorder 16 | } 17 | 18 | // MockstartStopperMockRecorder is the mock recorder for MockstartStopper 19 | type MockstartStopperMockRecorder struct { 20 | mock *MockstartStopper 21 | } 22 | 23 | // NewMockstartStopper creates a new mock instance 24 | func NewMockstartStopper(ctrl *gomock.Controller) *MockstartStopper { 25 | mock := &MockstartStopper{ctrl: ctrl} 26 | mock.recorder = &MockstartStopperMockRecorder{mock} 27 | return mock 28 | } 29 | 30 | // EXPECT returns an object that allows the caller to indicate expected use 31 | func (m *MockstartStopper) EXPECT() *MockstartStopperMockRecorder { 32 | return m.recorder 33 | } 34 | 35 | // Start mocks base method 36 | func (m *MockstartStopper) Start() { 37 | m.ctrl.T.Helper() 38 | m.ctrl.Call(m, "Start") 39 | } 40 | 41 | // Start indicates an expected call of Start 42 | func (mr *MockstartStopperMockRecorder) Start() *gomock.Call { 43 | mr.mock.ctrl.T.Helper() 44 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockstartStopper)(nil).Start)) 45 | } 46 | 47 | // Stop mocks base method 48 | func (m *MockstartStopper) Stop() { 49 | m.ctrl.T.Helper() 50 | m.ctrl.Call(m, "Stop") 51 | } 52 | 53 | // Stop indicates an expected call of Stop 54 | func (mr *MockstartStopperMockRecorder) Stop() *gomock.Call { 55 | mr.mock.ctrl.T.Helper() 56 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockstartStopper)(nil).Stop)) 57 | } 58 | 59 | // Mockmover is a mock of mover interface 60 | type Mockmover struct { 61 | ctrl *gomock.Controller 62 | recorder *MockmoverMockRecorder 63 | } 64 | 65 | // MockmoverMockRecorder is the mock recorder for Mockmover 66 | type MockmoverMockRecorder struct { 67 | mock *Mockmover 68 | } 69 | 70 | // NewMockmover creates a new mock instance 71 | func NewMockmover(ctrl *gomock.Controller) *Mockmover { 72 | mock := &Mockmover{ctrl: ctrl} 73 | mock.recorder = &MockmoverMockRecorder{mock} 74 | return mock 75 | } 76 | 77 | // EXPECT returns an object that allows the caller to indicate expected use 78 | func (m *Mockmover) EXPECT() *MockmoverMockRecorder { 79 | return m.recorder 80 | } 81 | 82 | // Up mocks base method 83 | func (m *Mockmover) Up(n int) { 84 | m.ctrl.T.Helper() 85 | m.ctrl.Call(m, "Up", n) 86 | } 87 | 88 | // Up indicates an expected call of Up 89 | func (mr *MockmoverMockRecorder) Up(n interface{}) *gomock.Call { 90 | mr.mock.ctrl.T.Helper() 91 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Up", reflect.TypeOf((*Mockmover)(nil).Up), n) 92 | } 93 | 94 | // Down mocks base method 95 | func (m *Mockmover) Down(n int) { 96 | m.ctrl.T.Helper() 97 | m.ctrl.Call(m, "Down", n) 98 | } 99 | 100 | // Down indicates an expected call of Down 101 | func (mr *MockmoverMockRecorder) Down(n interface{}) *gomock.Call { 102 | mr.mock.ctrl.T.Helper() 103 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Down", reflect.TypeOf((*Mockmover)(nil).Down), n) 104 | } 105 | 106 | // EraseLine mocks base method 107 | func (m *Mockmover) EraseLine() { 108 | m.ctrl.T.Helper() 109 | m.ctrl.Call(m, "EraseLine") 110 | } 111 | 112 | // EraseLine indicates an expected call of EraseLine 113 | func (mr *MockmoverMockRecorder) EraseLine() *gomock.Call { 114 | mr.mock.ctrl.T.Helper() 115 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EraseLine", reflect.TypeOf((*Mockmover)(nil).EraseLine)) 116 | } 117 | 118 | // MockwriteFlusher is a mock of writeFlusher interface 119 | type MockwriteFlusher struct { 120 | ctrl *gomock.Controller 121 | recorder *MockwriteFlusherMockRecorder 122 | } 123 | 124 | // MockwriteFlusherMockRecorder is the mock recorder for MockwriteFlusher 125 | type MockwriteFlusherMockRecorder struct { 126 | mock *MockwriteFlusher 127 | } 128 | 129 | // NewMockwriteFlusher creates a new mock instance 130 | func NewMockwriteFlusher(ctrl *gomock.Controller) *MockwriteFlusher { 131 | mock := &MockwriteFlusher{ctrl: ctrl} 132 | mock.recorder = &MockwriteFlusherMockRecorder{mock} 133 | return mock 134 | } 135 | 136 | // EXPECT returns an object that allows the caller to indicate expected use 137 | func (m *MockwriteFlusher) EXPECT() *MockwriteFlusherMockRecorder { 138 | return m.recorder 139 | } 140 | 141 | // Write mocks base method 142 | func (m *MockwriteFlusher) Write(p []byte) (int, error) { 143 | m.ctrl.T.Helper() 144 | ret := m.ctrl.Call(m, "Write", p) 145 | ret0, _ := ret[0].(int) 146 | ret1, _ := ret[1].(error) 147 | return ret0, ret1 148 | } 149 | 150 | // Write indicates an expected call of Write 151 | func (mr *MockwriteFlusherMockRecorder) Write(p interface{}) *gomock.Call { 152 | mr.mock.ctrl.T.Helper() 153 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockwriteFlusher)(nil).Write), p) 154 | } 155 | 156 | // Flush mocks base method 157 | func (m *MockwriteFlusher) Flush() error { 158 | m.ctrl.T.Helper() 159 | ret := m.ctrl.Call(m, "Flush") 160 | ret0, _ := ret[0].(error) 161 | return ret0 162 | } 163 | 164 | // Flush indicates an expected call of Flush 165 | func (mr *MockwriteFlusherMockRecorder) Flush() *gomock.Call { 166 | mr.mock.ctrl.T.Helper() 167 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Flush", reflect.TypeOf((*MockwriteFlusher)(nil).Flush)) 168 | } 169 | -------------------------------------------------------------------------------- /internal/pkg/term/progress/progress.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package progress provides data and functionality to display updates to the terminal. 5 | package progress 6 | 7 | // Text is a description of the progress update. 8 | type Text string 9 | 10 | // Status is the condition of the progress update. 11 | type Status string 12 | 13 | // Common progression life-cycle for an update. 14 | const ( 15 | StatusInProgress Status = "In Progress" 16 | StatusFailed Status = "Failed" 17 | StatusComplete Status = "Complete" 18 | StatusSkipped Status = "Skipped" 19 | ) 20 | -------------------------------------------------------------------------------- /internal/pkg/term/progress/spinner.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package progress 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "os" 10 | "text/tabwriter" 11 | "time" 12 | 13 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/term/cursor" 14 | "github.com/briandowns/spinner" 15 | ) 16 | 17 | // Events display settings. 18 | const ( 19 | minCellWidth = 20 // minimum number of characters in a table's cell. 20 | tabWidth = 4 // number of characters in between columns. 21 | cellPaddingWidth = 2 // number of padding characters added by default to a cell. 22 | paddingChar = ' ' // character in between columns. 23 | noAdditionalFormatting = 0 24 | ) 25 | 26 | // TabRow represents a row in a table where columns are separated with a "\t" character. 27 | type TabRow string 28 | 29 | // startStopper is the interface to interact with the spinner. 30 | type startStopper interface { 31 | Start() 32 | Stop() 33 | } 34 | 35 | // mover is the interface to interact with the cursor. 36 | type mover interface { 37 | Up(n int) 38 | Down(n int) 39 | EraseLine() 40 | } 41 | 42 | type writeFlusher interface { 43 | io.Writer 44 | Flush() error 45 | } 46 | 47 | // Spinner represents an indicator that an asynchronous operation is taking place. 48 | // 49 | // For short operations, less than 4 seconds, display only the spinner with the Start and Stop methods. 50 | // For longer operations, display intermediate progress events using the Events method. 51 | type Spinner struct { 52 | spin startStopper 53 | cur mover 54 | 55 | pastEvents []TabRow // Already written entries. 56 | eventsWriter writeFlusher // Writer to pretty format events in a table. 57 | } 58 | 59 | // NewSpinner returns a spinner that outputs to stderr. 60 | func NewSpinner() *Spinner { 61 | s := spinner.New(charset, 125*time.Millisecond, spinner.WithHiddenCursor(true)) 62 | s.Writer = os.Stderr 63 | return &Spinner{ 64 | spin: s, 65 | cur: cursor.New(), 66 | eventsWriter: tabwriter.NewWriter(s.Writer, minCellWidth, tabWidth, cellPaddingWidth, paddingChar, noAdditionalFormatting), 67 | } 68 | } 69 | 70 | // Start starts the spinner suffixed with a label. 71 | func (s *Spinner) Start(label string) { 72 | s.suffix(fmt.Sprintf(" %s", label)) 73 | s.spin.Start() 74 | } 75 | 76 | // Stop stops the spinner and replaces it with a label. 77 | func (s *Spinner) Stop(label string) { 78 | s.finalMSG(fmt.Sprintln(label)) 79 | s.spin.Stop() 80 | 81 | // Maintain old progress entries on the screen. 82 | for _, event := range s.pastEvents { 83 | fmt.Fprintf(s.eventsWriter, "%s\n", event) 84 | } 85 | s.eventsWriter.Flush() 86 | // Reset event entries once the spinner stops. 87 | s.pastEvents = nil 88 | } 89 | 90 | // Events writes additional information below the spinner while the spinner is still in progress. 91 | // If there are already existing events under the spinner, it replaces them with the new information. 92 | // 93 | // An event is displayed in a table, where columns are separated with the '\t' character. 94 | func (s *Spinner) Events(events []TabRow) { 95 | done := make(chan struct{}) 96 | go func() { 97 | s.lock() 98 | defer s.unlock() 99 | // Erase previous entries, and move the cursor back to the spinner. 100 | for i := 0; i < len(s.pastEvents); i++ { 101 | s.cur.Down(1) 102 | s.cur.EraseLine() 103 | } 104 | if len(s.pastEvents) > 0 { 105 | s.cur.Up(len(s.pastEvents)) 106 | } 107 | 108 | // Add new status updates, and move cursor back to the spinner. 109 | for _, event := range events { 110 | fmt.Fprintf(s.eventsWriter, "\n%s", event) 111 | } 112 | s.eventsWriter.Flush() 113 | if len(events) > 0 { 114 | s.cur.Up(len(events)) 115 | } 116 | // Move the cursor to the beginning so the spinner can delete the existing line. 117 | fmt.Fprintf(s.eventsWriter, "\r") 118 | s.eventsWriter.Flush() 119 | s.pastEvents = events 120 | close(done) 121 | }() 122 | <-done 123 | } 124 | 125 | func (s *Spinner) lock() { 126 | if spinner, ok := s.spin.(*spinner.Spinner); ok { 127 | spinner.Lock() 128 | } 129 | } 130 | 131 | func (s *Spinner) unlock() { 132 | if spinner, ok := s.spin.(*spinner.Spinner); ok { 133 | spinner.Unlock() 134 | } 135 | } 136 | 137 | func (s *Spinner) suffix(label string) { 138 | s.lock() 139 | defer s.unlock() 140 | if spinner, ok := s.spin.(*spinner.Spinner); ok { 141 | spinner.Suffix = label 142 | } 143 | } 144 | 145 | func (s *Spinner) finalMSG(label string) { 146 | s.lock() 147 | defer s.unlock() 148 | if spinner, ok := s.spin.(*spinner.Spinner); ok { 149 | spinner.FinalMSG = label 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /internal/pkg/term/progress/spinner_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package progress 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "os" 10 | "testing" 11 | "time" 12 | 13 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/term/progress/mocks" 14 | spin "github.com/briandowns/spinner" 15 | "github.com/golang/mock/gomock" 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | type mockWriteFlusher struct { 20 | buf *bytes.Buffer 21 | } 22 | 23 | func (m *mockWriteFlusher) Write(p []byte) (n int, err error) { 24 | return m.buf.Write(p) 25 | } 26 | 27 | func (m *mockWriteFlusher) Flush() error { 28 | return nil 29 | } 30 | 31 | type mockCursor struct { 32 | buf *bytes.Buffer 33 | } 34 | 35 | func (m *mockCursor) Up(n int) { 36 | s := fmt.Sprintf("[up%d]", n) 37 | m.buf.Write([]byte(s)) 38 | } 39 | 40 | func (m *mockCursor) Down(n int) { 41 | s := fmt.Sprintf("[down%d]", n) 42 | m.buf.Write([]byte(s)) 43 | } 44 | 45 | func (m *mockCursor) EraseLine() { 46 | m.buf.Write([]byte("erase")) 47 | } 48 | 49 | func TestNew(t *testing.T) { 50 | t.Run("it should initialize the spin spinner", func(t *testing.T) { 51 | got := NewSpinner() 52 | 53 | v, ok := got.spin.(*spin.Spinner) 54 | require.True(t, ok) 55 | 56 | require.Equal(t, os.Stderr, v.Writer) 57 | require.Equal(t, 125*time.Millisecond, v.Delay) 58 | }) 59 | } 60 | 61 | func TestSpinner_Start(t *testing.T) { 62 | ctrl := gomock.NewController(t) 63 | defer ctrl.Finish() 64 | mockSpinner := mocks.NewMockstartStopper(ctrl) 65 | 66 | s := &Spinner{ 67 | spin: mockSpinner, 68 | } 69 | 70 | mockSpinner.EXPECT().Start() 71 | 72 | s.Start("start") 73 | } 74 | 75 | func TestSpinner_Stop(t *testing.T) { 76 | ctrl := gomock.NewController(t) 77 | defer ctrl.Finish() 78 | testCases := map[string]struct { 79 | eventsBuf *bytes.Buffer 80 | pastEvents []TabRow 81 | wantedSEvents string 82 | }{ 83 | "without existing events": { 84 | eventsBuf: &bytes.Buffer{}, 85 | wantedSEvents: "", 86 | }, 87 | "with exiting events": { 88 | eventsBuf: &bytes.Buffer{}, 89 | pastEvents: []TabRow{"hello", "world"}, 90 | wantedSEvents: "hello\nworld\n", 91 | }, 92 | } 93 | 94 | for name, tc := range testCases { 95 | t.Run(name, func(t *testing.T) { 96 | // GIVEN 97 | mockSpinner := mocks.NewMockstartStopper(ctrl) 98 | mockWriter := &mockWriteFlusher{buf: tc.eventsBuf} 99 | s := &Spinner{ 100 | spin: mockSpinner, 101 | pastEvents: tc.pastEvents, 102 | eventsWriter: mockWriter, 103 | } 104 | mockSpinner.EXPECT().Stop() 105 | 106 | // WHEN 107 | s.Stop("stop") 108 | 109 | // THEN 110 | require.Equal(t, tc.wantedSEvents, tc.eventsBuf.String()) 111 | require.Nil(t, s.pastEvents) 112 | }) 113 | } 114 | } 115 | 116 | func TestSpinner_Events(t *testing.T) { 117 | ctrl := gomock.NewController(t) 118 | defer ctrl.Finish() 119 | 120 | testCases := map[string]struct { 121 | eventsBuf *bytes.Buffer 122 | 123 | pastEvents []TabRow 124 | newEvents []TabRow 125 | 126 | wantedSEvents string 127 | }{ 128 | "without existing events": { 129 | eventsBuf: &bytes.Buffer{}, 130 | newEvents: []TabRow{"hello", "world"}, 131 | 132 | wantedSEvents: "\nhello\nworld[up2]\r", 133 | }, 134 | "with existing events": { 135 | eventsBuf: &bytes.Buffer{}, 136 | pastEvents: []TabRow{"hello", "world"}, 137 | newEvents: []TabRow{"this", "is", "fine"}, 138 | 139 | wantedSEvents: "[down1]erase[down1]erase[up2]\nthis\nis\nfine[up3]\r", // write new events 140 | }, 141 | } 142 | 143 | for name, tc := range testCases { 144 | t.Run(name, func(t *testing.T) { 145 | // GIVEN 146 | mockSpinner := mocks.NewMockstartStopper(ctrl) 147 | s := &Spinner{ 148 | spin: mockSpinner, 149 | cur: &mockCursor{buf: tc.eventsBuf}, 150 | pastEvents: tc.pastEvents, 151 | eventsWriter: &mockWriteFlusher{buf: tc.eventsBuf}, 152 | } 153 | 154 | // WHEN 155 | s.Events(tc.newEvents) 156 | 157 | // THEN 158 | require.Equal(t, tc.wantedSEvents, tc.eventsBuf.String()) 159 | }) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /internal/pkg/version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package version holds variables for generating version information 5 | package version 6 | 7 | var Version string 8 | -------------------------------------------------------------------------------- /internal/pkg/workload/workload.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package workload defines OAM workload descriptions 5 | package workload 6 | 7 | import ( 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "strings" 12 | 13 | "github.com/awslabs/amazon-ecs-for-open-application-model/internal/pkg/term/log" 14 | "github.com/oam-dev/oam-go-sdk/apis/core.oam.dev/v1alpha1" 15 | "k8s.io/apimachinery/pkg/util/yaml" 16 | "k8s.io/client-go/kubernetes/scheme" 17 | ) 18 | 19 | const ( 20 | workerComponentWorkloadType = "core.oam.dev/v1alpha1.Worker" 21 | serverComponentWorkloadType = "core.oam.dev/v1alpha1.Server" 22 | ) 23 | 24 | type OamWorkloadProps struct { 25 | OamFiles []string 26 | } 27 | 28 | type OamWorkload struct { 29 | ApplicationConfiguration *v1alpha1.ApplicationConfiguration 30 | ComponentSchematics map[string]*v1alpha1.ComponentSchematic 31 | } 32 | 33 | func NewOamWorkload(input *OamWorkloadProps) (*OamWorkload, error) { 34 | var applicationConfiguration *v1alpha1.ApplicationConfiguration 35 | componentSchematics := make(map[string]*v1alpha1.ComponentSchematic) 36 | 37 | v1alpha1.SchemeBuilder.AddToScheme(scheme.Scheme) 38 | decode := scheme.Codecs.UniversalDeserializer().Decode 39 | 40 | // Parse all of the app config and component schematics from the given files 41 | for _, fileLocation := range input.OamFiles { 42 | fileContents, err := ioutil.ReadFile(fileLocation) 43 | if err != nil { 44 | log.Errorf("Failed to read file %s\n", fileLocation) 45 | return nil, err 46 | } 47 | 48 | // Split the file into potentially multiple YAML documents delimited by '\n---' 49 | reader := yaml.NewDocumentDecoder(ioutil.NopCloser(strings.NewReader(string(fileContents)))) 50 | for { 51 | chunk := make([]byte, len(fileContents)) 52 | n, err := reader.Read(chunk) 53 | if err != nil { 54 | if err == io.EOF { 55 | break 56 | } 57 | log.Errorf("Failed to read file %s\n", fileLocation) 58 | return nil, err 59 | } 60 | chunk = chunk[:n] 61 | 62 | obj, kind, err := decode(chunk, nil, nil) 63 | if err != nil { 64 | log.Errorf("Failed to parse file %s\n", fileLocation) 65 | return nil, err 66 | } 67 | 68 | switch obj.(type) { 69 | case *v1alpha1.ApplicationConfiguration: 70 | if applicationConfiguration != nil { 71 | log.Errorf("File %s contains an ApplicationConfiguration, but one has already been found\n", fileLocation) 72 | return nil, fmt.Errorf("Multiple application configuration files found, only one is allowed per application") 73 | } 74 | applicationConfiguration = obj.(*v1alpha1.ApplicationConfiguration) 75 | case *v1alpha1.ComponentSchematic: 76 | schematic := obj.(*v1alpha1.ComponentSchematic) 77 | 78 | if schematic.Spec.WorkloadType != workerComponentWorkloadType && 79 | schematic.Spec.WorkloadType != serverComponentWorkloadType { 80 | log.Errorf("Component schematic %s is an invalid workload type\n", schematic.Name) 81 | return nil, fmt.Errorf("Workload type is %s, only %s and %s are supported", 82 | schematic.Spec.WorkloadType, 83 | workerComponentWorkloadType, 84 | serverComponentWorkloadType) 85 | } 86 | 87 | componentSchematics[schematic.Name] = schematic 88 | default: 89 | log.Errorf("Found invalid object in file %s\n", fileLocation) 90 | return nil, fmt.Errorf("Object type %s is not supported", kind) 91 | } 92 | log.Successf("Read %s from file %s\n", kind, fileLocation) 93 | } 94 | } 95 | 96 | if applicationConfiguration == nil { 97 | log.Errorf("No application configuration found in given files %s\n", strings.Join(input.OamFiles, ", ")) 98 | return nil, fmt.Errorf("Application configuration is required") 99 | } 100 | 101 | return &OamWorkload{ 102 | ApplicationConfiguration: applicationConfiguration, 103 | ComponentSchematics: componentSchematics, 104 | }, nil 105 | } 106 | -------------------------------------------------------------------------------- /templates/core.oam.dev/cf.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: Amazon ECS infrastructure for {{.ApplicationConfiguration.Name}} {{.ComponentConfiguration.InstanceName}} 3 | 4 | Resources: 5 | LogGroup: 6 | Type: AWS::Logs::LogGroup 7 | Properties: 8 | LogGroupName: {{.Environment.Name}}-{{.ApplicationConfiguration.Name}}-{{.ComponentConfiguration.InstanceName}} 9 | 10 | TaskDefinition: 11 | Type: AWS::ECS::TaskDefinition 12 | Properties: 13 | Family: {{.Environment.Name}}-{{.ApplicationConfiguration.Name}}-{{.ComponentConfiguration.InstanceName}} 14 | NetworkMode: awsvpc 15 | RequiresCompatibilities: 16 | - FARGATE 17 | Cpu: {{TaskCPU $.Component.Spec.Containers}} 18 | Memory: '{{TaskMemory $.Component.Spec.Containers}}' 19 | ExecutionRoleArn: !GetAtt ExecutionRole.Arn 20 | ContainerDefinitions: {{range $container := $.Component.Spec.Containers}} 21 | - Name: {{$container.Name}} 22 | Image: {{$container.Image}} {{if $container.Resources.Gpu}} {{if not $container.Resources.Gpu.Required.IsZero}} 23 | ResourceRequirements: 24 | - Type: GPU 25 | Value: '{{$container.Resources.Gpu.Required.AsDec}}'{{end}}{{end}} {{if $container.Resources.Volumes}} 26 | MountPoints: {{range $volume := $container.Resources.Volumes}} 27 | - ContainerPath: {{$volume.MountPath}} 28 | ReadOnly: {{if eq $volume.AccessMode "RO"}} true {{else}} false {{end}} 29 | SourceVolume: {{$volume.Name}} {{end}} {{end}} {{if $container.Cmd}} 30 | EntryPoint: {{range $cmd := $container.Cmd}} 31 | - "{{$cmd}}" {{end}} {{end}} {{if $container.Args}} 32 | Command: {{range $arg := $container.Args}} 33 | - "{{$arg}}" {{end}} {{end}} {{if $container.Env}} 34 | Environment: {{range $env := $container.Env}} 35 | - Name: {{$env.Name}} 36 | Value: {{if $env.FromParam}} "{{ResolveParameterValue $env.FromParam $.ComponentConfiguration $.Component.Spec}}" {{else}} "{{$env.Value}}" {{end}} {{end}} {{end}} {{if eq $.Component.Spec.WorkloadType "core.oam.dev/v1alpha1.Server"}} {{if $container.Ports}} 37 | PortMappings: {{range $port := $container.Ports}} 38 | - ContainerPort: {{$port.ContainerPort}} 39 | Protocol: {{if $port.Protocol}} {{$port.Protocol | toString | lower}} {{else}} tcp {{end}} {{end}} {{end}} {{end}} {{if $container.ImagePullSecret}} 40 | RepositoryCredentials: 41 | CredentialsParameter: "{{$container.ImagePullSecret}}" {{end}} {{if $container.LivenessProbe}} {{if $container.LivenessProbe.Exec}} 42 | HealthCheck: 43 | Command: {{range $cmd := $container.LivenessProbe.Exec.Command}} 44 | - "{{$cmd}}" {{end}} 45 | Interval: {{if $container.LivenessProbe.PeriodSeconds}} {{$container.LivenessProbe.PeriodSeconds}} {{else}} 10 {{end}} 46 | Retries: {{if $container.LivenessProbe.FailureThreshold}} {{$container.LivenessProbe.FailureThreshold}} {{else}} 3 {{end}} 47 | StartPeriod: {{if $container.LivenessProbe.InitialDelaySeconds}} {{$container.LivenessProbe.InitialDelaySeconds}} {{else}} 0 {{end}} 48 | Timeout: {{if $container.LivenessProbe.TimeoutSeconds}} {{$container.LivenessProbe.TimeoutSeconds}} {{else}} 2 {{end}} {{end}} {{end}} 49 | LogConfiguration: 50 | LogDriver: awslogs 51 | Options: 52 | awslogs-region: !Ref AWS::Region 53 | awslogs-group: !Ref LogGroup 54 | awslogs-stream-prefix: oam-ecs {{end}} {{if RequiresVolumes $.Component.Spec.Containers}} 55 | Volumes: {{range $container := $.Component.Spec.Containers}} {{range $volume := $container.Resources.Volumes}} 56 | - Name: {{$volume.Name}} {{end}}{{end}}{{end}} 57 | 58 | ExecutionRole: 59 | Type: AWS::IAM::Role 60 | Properties: 61 | AssumeRolePolicyDocument: 62 | Statement: 63 | - Effect: Allow 64 | Principal: 65 | Service: ecs-tasks.amazonaws.com 66 | Action: 'sts:AssumeRole' 67 | {{if RequiresPrivateRegistryAuth $.Component.Spec.Containers}} 68 | Policies: 69 | - PolicyName: PrivateRegistryCreds 70 | PolicyDocument: 71 | Version: '2012-10-17' 72 | Statement: 73 | - Effect: 'Allow' 74 | Action: 75 | - 'secretsmanager:GetSecretValue' 76 | Resource: {{range $container := $.Component.Spec.Containers}} {{if $container.ImagePullSecret}} 77 | - !Sub 'arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:{{$container.ImagePullSecret}}-??????'{{end}} {{end}} {{end}} 78 | ManagedPolicyArns: 79 | - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' 80 | 81 | ContainerSecurityGroup: 82 | Type: AWS::EC2::SecurityGroup 83 | Properties: 84 | GroupDescription: {{.Environment.Name}}-{{.ApplicationConfiguration.Name}}-{{.ComponentConfiguration.InstanceName}}-ContainerSecurityGroup 85 | VpcId: 86 | Fn::ImportValue: {{.Environment.Name}}-VpcId 87 | 88 | Service: 89 | Type: AWS::ECS::Service 90 | Properties: 91 | Cluster: 92 | Fn::ImportValue: {{.Environment.Name}}-ECSCluster 93 | TaskDefinition: !Ref TaskDefinition 94 | DeploymentConfiguration: 95 | MinimumHealthyPercent: 100 96 | MaximumPercent: 200 97 | DesiredCount: {{ResolveTraitValue "manual-scaler" "replicaCount" 1 .ComponentConfiguration}} 98 | LaunchType: FARGATE 99 | NetworkConfiguration: 100 | AwsvpcConfiguration: 101 | AssignPublicIp: DISABLED 102 | Subnets: 103 | Fn::Split: 104 | - ',' 105 | - Fn::ImportValue: {{.Environment.Name}}-PrivateSubnets 106 | SecurityGroups: 107 | - !Ref ContainerSecurityGroup {{if eq $.Component.Spec.WorkloadType "core.oam.dev/v1alpha1.Server"}} 108 | LoadBalancers: {{range $container := $.Component.Spec.Containers}} {{range $port := $container.Ports}} 109 | - ContainerName: {{$container.Name}} 110 | ContainerPort: {{$port.ContainerPort}} 111 | TargetGroupArn: !Ref TargetGroup{{camelcase $container.Name}}{{$port.ContainerPort}} {{end}} {{end}} 112 | HealthCheckGracePeriodSeconds: {{HealthCheckGracePeriod $.Component.Spec.Containers}} 113 | DependsOn: {{range $container := $.Component.Spec.Containers}} {{range $port := $container.Ports}} 114 | - LBListener{{camelcase $container.Name}}{{$port.ContainerPort}} 115 | {{end}} {{end}} {{end}} 116 | 117 | {{if eq $.Component.Spec.WorkloadType "core.oam.dev/v1alpha1.Server"}} 118 | SGLoadBalancerToContainers: 119 | Type: AWS::EC2::SecurityGroupIngress 120 | Properties: 121 | Description: Ingress from anywhere on the internet through the public NLB 122 | GroupId: !Ref ContainerSecurityGroup 123 | IpProtocol: '-1' 124 | CidrIp: 0.0.0.0/0 125 | 126 | PublicLoadBalancer: 127 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 128 | Properties: 129 | Type: network 130 | Scheme: internet-facing 131 | Subnets: 132 | Fn::Split: 133 | - ',' 134 | - Fn::ImportValue: {{.Environment.Name}}-PublicSubnets 135 | {{range $container := $.Component.Spec.Containers}} {{range $port := $container.Ports}} 136 | LBListener{{camelcase $container.Name}}{{$port.ContainerPort}}: 137 | Type: AWS::ElasticLoadBalancingV2::Listener 138 | Properties: 139 | DefaultActions: 140 | - TargetGroupArn: !Ref TargetGroup{{camelcase $container.Name}}{{$port.ContainerPort}} 141 | Type: 'forward' 142 | LoadBalancerArn: !Ref 'PublicLoadBalancer' 143 | Port: {{$port.ContainerPort}} 144 | Protocol: {{if $port.Protocol}} {{$port.Protocol | toString | upper}} {{else}} TCP {{end}} 145 | 146 | TargetGroup{{camelcase $container.Name}}{{$port.ContainerPort}}: 147 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 148 | Properties: 149 | Protocol: {{if $port.Protocol}} {{$port.Protocol | toString | upper}} {{else}} TCP {{end}} 150 | TargetType: ip 151 | Port: {{$port.ContainerPort}} 152 | VpcId: 153 | Fn::ImportValue: {{$.Environment.Name}}-VpcId 154 | TargetGroupAttributes: 155 | - Key: deregistration_delay.timeout_seconds 156 | Value: '30' 157 | {{if $container.LivenessProbe}} {{if $container.LivenessProbe.HttpGet}} 158 | HealthCheckProtocol: HTTP 159 | HealthCheckPath: {{$container.LivenessProbe.HttpGet.Path}} 160 | HealthCheckPort: '{{$container.LivenessProbe.HttpGet.Port}}' 161 | HealthCheckTimeoutSeconds: {{if $container.LivenessProbe.TimeoutSeconds}} {{$container.LivenessProbe.TimeoutSeconds}} {{else}} 6 {{end}} 162 | {{else if $container.LivenessProbe.TcpSocket}} 163 | HealthCheckProtocol: TCP 164 | HealthCheckPort: '{{$container.LivenessProbe.TcpSocket.Port}}' 165 | HealthCheckTimeoutSeconds: {{if $container.LivenessProbe.TimeoutSeconds}} {{$container.LivenessProbe.TimeoutSeconds}} {{else}} 10 {{end}} 166 | {{end}} 167 | HealthCheckIntervalSeconds: {{if $container.LivenessProbe.PeriodSeconds}} {{$container.LivenessProbe.PeriodSeconds}} {{else}} 10 {{end}} 168 | HealthyThresholdCount: {{if $container.LivenessProbe.SuccessThreshold}} {{$container.LivenessProbe.SuccessThreshold}} {{else}} 2 {{end}} 169 | UnhealthyThresholdCount: {{if $container.LivenessProbe.FailureThreshold}} {{$container.LivenessProbe.FailureThreshold}} {{else}} 3 {{end}} 170 | {{end}} 171 | {{end}} {{end}} {{end}} 172 | 173 | Outputs: 174 | CloudFormationStackConsole: 175 | Description: The AWS console deep-link for the CloudFormation stack 176 | Value: !Sub https://console.aws.amazon.com/cloudformation/home?region=${AWS::Region}#/stacks/stackinfo?stackId=${AWS::StackName} 177 | 178 | ECSServiceConsole: 179 | Description: The AWS console deep-link for the ECS service 180 | Value: !Sub https://console.aws.amazon.com/ecs/home?region=${AWS::Region}#/clusters/oam-ecs/services/${Service.Name} 181 | {{if eq $.Component.Spec.WorkloadType "core.oam.dev/v1alpha1.Server"}} {{range $container := $.Component.Spec.Containers}} {{range $port := $container.Ports}} 182 | {{camelcase $container.Name}}Port{{$port.ContainerPort}}Endpoint: 183 | Description: The endpoint for container {{camelcase $container.Name}} on port {{$port.ContainerPort}} 184 | Value: !Sub '${PublicLoadBalancer.DNSName}:{{$port.ContainerPort}}' 185 | {{end}} {{end}} {{end}} 186 | -------------------------------------------------------------------------------- /templates/environment/cf.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: Creates the deployment environment for the core OAM workload types, like the VPC. For this proof of concept, oam-ecs creates a single environment where all applications are deployed. 3 | 4 | Parameters: 5 | EnvironmentName: 6 | Description: An environment name that is prefixed to resource names 7 | Type: String 8 | Default: oam-ecs 9 | 10 | VpcCIDR: 11 | Type: String 12 | Default: 10.0.0.0/16 13 | 14 | PublicSubnet1CIDR: 15 | Type: String 16 | Default: 10.0.0.0/24 17 | 18 | PublicSubnet2CIDR: 19 | Type: String 20 | Default: 10.0.1.0/24 21 | 22 | PrivateSubnet1CIDR: 23 | Type: String 24 | Default: 10.0.2.0/24 25 | 26 | PrivateSubnet2CIDR: 27 | Type: String 28 | Default: 10.0.3.0/24 29 | 30 | Resources: 31 | VPC: 32 | Type: AWS::EC2::VPC 33 | Properties: 34 | CidrBlock: !Ref VpcCIDR 35 | EnableDnsHostnames: true 36 | EnableDnsSupport: true 37 | Tags: 38 | - Key: Name 39 | Value: !Ref EnvironmentName 40 | 41 | InternetGateway: 42 | Type: AWS::EC2::InternetGateway 43 | Properties: 44 | Tags: 45 | - Key: Name 46 | Value: !Ref EnvironmentName 47 | 48 | InternetGatewayAttachment: 49 | Type: AWS::EC2::VPCGatewayAttachment 50 | Properties: 51 | InternetGatewayId: !Ref InternetGateway 52 | VpcId: !Ref VPC 53 | 54 | PublicSubnet1: 55 | Type: AWS::EC2::Subnet 56 | Properties: 57 | CidrBlock: !Ref PublicSubnet1CIDR 58 | VpcId: !Ref VPC 59 | AvailabilityZone: !Select [ 0, !GetAZs '' ] 60 | MapPublicIpOnLaunch: true 61 | Tags: 62 | - Key: Name 63 | Value: !Sub ${EnvironmentName} Public Subnet (AZ1) 64 | 65 | PublicSubnet2: 66 | Type: AWS::EC2::Subnet 67 | Properties: 68 | CidrBlock: !Ref PublicSubnet2CIDR 69 | VpcId: !Ref VPC 70 | AvailabilityZone: !Select [ 1, !GetAZs '' ] 71 | MapPublicIpOnLaunch: true 72 | Tags: 73 | - Key: Name 74 | Value: !Sub ${EnvironmentName} Public Subnet (AZ2) 75 | 76 | PrivateSubnet1: 77 | Type: AWS::EC2::Subnet 78 | Properties: 79 | CidrBlock: !Ref PrivateSubnet1CIDR 80 | VpcId: !Ref VPC 81 | AvailabilityZone: !Select [ 0, !GetAZs '' ] 82 | MapPublicIpOnLaunch: false 83 | Tags: 84 | - Key: Name 85 | Value: !Sub ${EnvironmentName} Private Subnet (AZ1) 86 | 87 | PrivateSubnet2: 88 | Type: AWS::EC2::Subnet 89 | Properties: 90 | CidrBlock: !Ref PrivateSubnet2CIDR 91 | VpcId: !Ref VPC 92 | AvailabilityZone: !Select [ 1, !GetAZs '' ] 93 | MapPublicIpOnLaunch: false 94 | Tags: 95 | - Key: Name 96 | Value: !Sub ${EnvironmentName} Private Subnet (AZ2) 97 | 98 | NatGateway1EIP: 99 | Type: AWS::EC2::EIP 100 | DependsOn: InternetGatewayAttachment 101 | Properties: 102 | Domain: vpc 103 | 104 | NatGateway2EIP: 105 | Type: AWS::EC2::EIP 106 | DependsOn: InternetGatewayAttachment 107 | Properties: 108 | Domain: vpc 109 | 110 | NATGateway1: 111 | Type: AWS::EC2::NatGateway 112 | Properties: 113 | AllocationId: !GetAtt NatGateway1EIP.AllocationId 114 | SubnetId: !Ref PublicSubnet1 115 | 116 | NATGateway2: 117 | Type: AWS::EC2::NatGateway 118 | Properties: 119 | AllocationId: !GetAtt NatGateway2EIP.AllocationId 120 | SubnetId: !Ref PublicSubnet2 121 | 122 | PublicRouteTable: 123 | Type: AWS::EC2::RouteTable 124 | Properties: 125 | VpcId: !Ref VPC 126 | Tags: 127 | - Key: Name 128 | Value: !Sub ${EnvironmentName} Public Routes 129 | 130 | DefaultPublicRoute: 131 | Type: AWS::EC2::Route 132 | DependsOn: InternetGatewayAttachment 133 | Properties: 134 | RouteTableId: !Ref PublicRouteTable 135 | DestinationCidrBlock: 0.0.0.0/0 136 | GatewayId: !Ref InternetGateway 137 | 138 | PublicSubnet1RouteTableAssociation: 139 | Type: AWS::EC2::SubnetRouteTableAssociation 140 | Properties: 141 | RouteTableId: !Ref PublicRouteTable 142 | SubnetId: !Ref PublicSubnet1 143 | 144 | PublicSubnet2RouteTableAssociation: 145 | Type: AWS::EC2::SubnetRouteTableAssociation 146 | Properties: 147 | RouteTableId: !Ref PublicRouteTable 148 | SubnetId: !Ref PublicSubnet2 149 | 150 | PrivateRouteTable1: 151 | Type: AWS::EC2::RouteTable 152 | Properties: 153 | VpcId: !Ref VPC 154 | Tags: 155 | - Key: Name 156 | Value: !Sub ${EnvironmentName} Private Routes (AZ1) 157 | 158 | DefaultPrivateRoute1: 159 | Type: AWS::EC2::Route 160 | Properties: 161 | RouteTableId: !Ref PrivateRouteTable1 162 | DestinationCidrBlock: 0.0.0.0/0 163 | NatGatewayId: !Ref NATGateway1 164 | 165 | PrivateSubnet1RouteTableAssociation: 166 | Type: AWS::EC2::SubnetRouteTableAssociation 167 | Properties: 168 | RouteTableId: !Ref PrivateRouteTable1 169 | SubnetId: !Ref PrivateSubnet1 170 | 171 | PrivateRouteTable2: 172 | Type: AWS::EC2::RouteTable 173 | Properties: 174 | VpcId: !Ref VPC 175 | Tags: 176 | - Key: Name 177 | Value: !Sub ${EnvironmentName} Private Routes (AZ2) 178 | 179 | DefaultPrivateRoute2: 180 | Type: AWS::EC2::Route 181 | Properties: 182 | RouteTableId: !Ref PrivateRouteTable2 183 | DestinationCidrBlock: 0.0.0.0/0 184 | NatGatewayId: !Ref NATGateway2 185 | 186 | PrivateSubnet2RouteTableAssociation: 187 | Type: AWS::EC2::SubnetRouteTableAssociation 188 | Properties: 189 | RouteTableId: !Ref PrivateRouteTable2 190 | SubnetId: !Ref PrivateSubnet2 191 | 192 | Cluster: 193 | Type: AWS::ECS::Cluster 194 | Properties: 195 | ClusterName: !Ref EnvironmentName 196 | 197 | Outputs: 198 | CloudFormationStackConsole: 199 | Value: !Sub https://console.aws.amazon.com/cloudformation/home?region=${AWS::Region}#/stacks/stackinfo?stackId=${AWS::StackName} 200 | 201 | ECSClusterConsole: 202 | Value: !Sub https://console.aws.amazon.com/ecs/home?region=${AWS::Region}#/clusters/${Cluster} 203 | 204 | VpcId: 205 | Value: !Ref VPC 206 | Export: 207 | Name: !Sub ${EnvironmentName}-VpcId 208 | 209 | PublicSubnets: 210 | Value: !Join [ ',', [ !Ref PublicSubnet1, !Ref PublicSubnet2 ] ] 211 | Export: 212 | Name: !Sub ${EnvironmentName}-PublicSubnets 213 | 214 | PrivateSubnets: 215 | Value: !Join [ ',', [ !Ref PrivateSubnet1, !Ref PrivateSubnet2 ] ] 216 | Export: 217 | Name: !Sub ${EnvironmentName}-PrivateSubnets 218 | 219 | ECSCluster: 220 | Value: !Ref Cluster 221 | Export: 222 | Name: !Sub ${EnvironmentName}-ECSCluster 223 | -------------------------------------------------------------------------------- /templates/templates.go: -------------------------------------------------------------------------------- 1 | //go:generate packr2 2 | 3 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | // SPDX-License-Identifier: Apache-2.0 5 | 6 | package templates 7 | 8 | import "github.com/gobuffalo/packr/v2" 9 | 10 | // Box can be used to read in templates from the templates directory. 11 | // For example, templates.Box().FindString("core.oam.dev/v1alpha1.Server/cf.yml"). 12 | func Box() *packr.Box { 13 | return packr.New("templates", "./") 14 | } 15 | --------------------------------------------------------------------------------