├── .circleci └── config.yml ├── .dockerignore ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── acm ├── certificate.go ├── certificate_test.go ├── main.go └── mock │ ├── client │ └── client.go │ └── sdk │ ├── acmiface.go │ └── paginators.go ├── cloudwatchlogs ├── log_group.go └── main.go ├── cmd ├── certificate.go ├── certificate_destroy.go ├── certificate_destroy_test.go ├── certificate_import.go ├── certificate_import_test.go ├── certificate_info.go ├── certificate_info_test.go ├── certificate_list.go ├── certificate_list_test.go ├── certificate_request.go ├── certificate_request_test.go ├── certificate_test.go ├── certificate_validate.go ├── certificate_validate_test.go ├── lb.go ├── lb_alias.go ├── lb_alias_test.go ├── lb_create.go ├── lb_create_test.go ├── lb_destroy.go ├── lb_info.go ├── lb_list.go ├── lb_list_test.go ├── logs.go ├── mock │ └── output.go ├── output.go ├── output_test.go ├── port.go ├── port_test.go ├── root.go ├── root_test.go ├── service.go ├── service_create.go ├── service_deploy.go ├── service_destroy.go ├── service_env.go ├── service_env_list.go ├── service_env_set.go ├── service_env_unset.go ├── service_info.go ├── service_list.go ├── service_logs.go ├── service_ps.go ├── service_restart.go ├── service_scale.go ├── service_update.go ├── string_util.go ├── string_util_test.go ├── task.go ├── task_info.go ├── task_list.go ├── task_logs.go ├── task_ps.go ├── task_run.go ├── task_stop.go ├── testdata │ ├── certificate.crt │ ├── chain.crt │ └── private.key ├── vpc_operation.go └── vpc_operation_test.go ├── console └── main.go ├── doc └── website │ ├── apple.png │ ├── fargate.png │ ├── github.png │ ├── index.html │ ├── linux.png │ └── styles.css ├── docker └── main.go ├── ec2 ├── eni.go ├── main.go ├── mock │ ├── client │ │ └── client.go │ └── sdk │ │ └── ec2iface.go ├── vpc.go └── vpc_test.go ├── ecr ├── main.go └── repository.go ├── ecs ├── cluster.go ├── main.go ├── service.go ├── task.go └── task_definition.go ├── elbv2 ├── listener.go ├── listener_test.go ├── load_balancer.go ├── load_balancer_test.go ├── main.go ├── mock │ ├── client │ │ └── client.go │ └── sdk │ │ ├── elbv2iface.go │ │ └── paginators.go ├── target_group.go └── target_group_test.go ├── git ├── main.go └── main_test.go ├── go.mod ├── go.sum ├── iam ├── main.go └── role.go ├── main.go └── route53 ├── hosted_zone.go ├── hosted_zone_test.go ├── main.go └── mock ├── client └── client.go └── sdk ├── paginators.go └── route53iface.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/golang:1.10 6 | working_directory: /go/src/github.com/awslabs/fargatecli 7 | steps: 8 | - checkout 9 | 10 | - run: git config --global user.email circleci@pignata.com 11 | - run: git config --global user.name CircleCI 12 | 13 | - run: go get -u github.com/golang/dep/cmd/dep 14 | - run: dep ensure 15 | 16 | - run: make test 17 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | bin 3 | fargate 4 | dist 5 | fargatecli 6 | vendor 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.1 (2019-05-09) 2 | 3 | ### Enhancements 4 | 5 | - Added support for ap-south-1, ca-central-1 and ap-northeast-2 regions 6 | 7 | ## 0.3.0 (2019-03-14) 8 | 9 | ### Enhancements 10 | 11 | - Console output reworked for consistency and brevity 12 | - macOS users get emoji as a type prefix in console output :tada: -- disable 13 | with --no-emoji if you're not into fun 14 | - Requests and responses from AWS are displayed in full when --verbose is 15 | passed 16 | - Added additional AWS Fargate regions 17 | - Added --task-command flag to allow overriding of the Docker command used by the container 18 | - If you have a region set in your AWS credentials, use that by default and only fall back to us-east-1 if no region is set 19 | - Added --assign-public-ip boolean flag, which allows you to control whether a task has a public IP address (default: true) 20 | 21 | ### Bug Fixes 22 | 23 | - Environment variable service commands now return a polite error message when 24 | invoked without the service name. ([#22][issue-22]) 25 | - Certificate import command re-implemented to work correctly. Previously calls 26 | to this command always returned "private key not supported" as we were 27 | incorrectly encoding it to base64 before passing it to the AWS SDK. 28 | 29 | ### Chores 30 | 31 | - Utilize `dep` for dependency management 32 | - Add contributor guide, updated license to repo 33 | 34 | ## 0.2.3 (2018-01-19) 35 | 36 | ### Features 37 | 38 | - Support **--task-role** flag in service create and task run to allow passing 39 | a role name for the tasks to assume. ([#8][issue-8]) 40 | 41 | ### Enhancements 42 | 43 | - Use the `ForceNewDeployment` feature of `UpdateService` in service restart 44 | instead of incrementing the task definition. ([#14][issue-14]) 45 | 46 | ### Bug Fixes 47 | 48 | - Fixed issue where we'd stomp on an existing task role on service updates like 49 | deployments or environment variable changes. ([#8][issue-8]) 50 | 51 | ## 0.2.2 (2018-01-11) 52 | 53 | ### Bug Fixes 54 | 55 | - Fix service update operation to properly validate and run. ([#11][issue-11]) 56 | - Bail out early in service info if the requested service is not active meaning 57 | it has been previously destroyed. 58 | 59 | ## 0.2.1 (2018-01-02) 60 | 61 | ### Bug Fixes 62 | 63 | - service create will not run if a load balancer is configured without a port. 64 | - service create and task run will no longer create a repository if an image is 65 | explictly passed. 66 | - service destroy will remove all references the service's target group and 67 | delete it. 68 | - Fix git repo detection to properly use a git sha image tag rather than a 69 | time stamp tag. ([#6][issue-6]) 70 | - Fail fast if a user attempts to destroy a service scaled above 0. 71 | 72 | ## 0.2.0 (2017-12-31) 73 | 74 | ### Features 75 | 76 | - Added **--cluster** global flag to allow running commands against other 77 | clusters rather than the default. If omitted, the default **fargate** cluster 78 | is used. ([#2][issue-2]) 79 | - lb create, service create, and task run now accept an optional **--subnet-id** 80 | flag to place resources in different VPCs and subnets rather than the 81 | defaults. If omitted, resources will be placed within the default subnets 82 | within the default VPC. ([#2][issue-2]) 83 | - lb create, service create, and task run now accept an optional 84 | **--security-group-id** flag to allow applying more restrictive security 85 | groups to load balancers, services, and tasks. This flag can be passed 86 | multiple times to apply multiple security groups. If omitted, a permissive 87 | security group will be applied. 88 | 89 | ### Bug Fixes 90 | 91 | - Resolved crashes with certificates missing resource records. Certificates that 92 | fail to be issued immediately after request would cause crashes in lb info and 93 | lb list as the resource record was never generated. 94 | 95 | [issue-2]: https://github.com/awslabs/fargatecli/issues/2 96 | [issue-6]: https://github.com/awslabs/fargatecli/issues/6 97 | [issue-8]: https://github.com/awslabs/fargatecli/issues/8 98 | [issue-11]: https://github.com/awslabs/fargatecli/issues/11 99 | [issue-14]: https://github.com/awslabs/fargatecli/issues/14 100 | [issue-22]: https://github.com/awslabs/fargatecli/issues/22 101 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Amazon Open Source Code of Conduct 2 | 3 | This code of conduct provides guidance on participation in Amazon-managed open 4 | source communities, and outlines the process for reporting unacceptable 5 | behavior. As an organization and community, we are committed to providing an 6 | inclusive environment for everyone. Anyone violating this code of conduct may be 7 | removed and banned from the community. 8 | 9 | **Our open source communities endeavor to:** 10 | - Use welcoming and inclusive language; 11 | - Be respectful of differing viewpoints at all times; 12 | - Accept constructive criticism and work together toward decisions; 13 | - Focus on what is best for the community and users. 14 | 15 | **Our Responsibility.** As contributors, members, or bystanders we each 16 | individually have the responsibility to behave professionally and respectfully 17 | at all times. Disrespectful and unacceptable behaviors include, but are not 18 | limited to: The use of violent threats, abusive, discriminatory, or derogatory 19 | language; 20 | - Offensive comments related to gender, gender identity and expression, sexual 21 | orientation, disability, mental illness, race, political or religious 22 | affiliation; 23 | - Posting of sexually explicit or violent content; 24 | - The use of sexualized language and unwelcome sexual attention or advances; 25 | - Public or private 26 | [harassment](http://todogroup.org/opencodeofconduct/#definitions) of any kind; 27 | - Publishing private information, such as physical or electronic address, 28 | without permission; 29 | - Other conduct which could reasonably be considered inappropriate in a 30 | professional setting; 31 | - Advocating for or encouraging any of the above behaviors. 32 | 33 | **Enforcement and Reporting Code of Conduct Issues.** Instances of abusive, 34 | harassing, or otherwise unacceptable behavior may be reported by contacting 35 | opensource-codeofconduct@amazon.com. All complaints will be reviewed and 36 | investigated and will result in a response that is deemed necessary and 37 | appropriate to the circumstances. 38 | 39 | **Attribution.** _This code of conduct is based on the 40 | [template](http://todogroup.org/opencodeofconduct) established by the [TODO 41 | Group](http://todogroup.org/) and the Scope section from the [Contributor 42 | Covenant version 1.4](http://contributor-covenant.org/version/1/4/)._ 43 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | **Welcome!** Thank you for considering contributing to this project! If I can 2 | help in anyway to get you going, please feel free to reach out. I'm available by 3 | email and Google Hangouts at john@pignata.com. 4 | 5 | # Contributing 6 | 7 | ## Workflow 8 | 9 | - **Did you find a bug?** 10 | 11 | Awesome! Please feel free to open an issue first, or if you have a fix open a 12 | pull request that describes the bug with code that demonstrates the bug in a 13 | test and addresses it. 14 | 15 | - **Do you want to add a feature?** 16 | 17 | Features begin life as a proposal. Please open a pull request with a proposal 18 | that explains the feature, its use case, considerations, and design. This will 19 | allow interested contributors to weigh in, refine the idea, and ensure there's 20 | no wasted time in the event a feature doesn't fit with our direction. 21 | 22 | ## Setup 23 | 24 | - Ensure you're using golang 1.9+. 25 | 26 | ```console 27 | go version 28 | ``` 29 | 30 | - Install [`dep`][dep] if not present on your system. See their [installation 31 | instructions][dep-install] and [releases page][dep-releases] for details. 32 | 33 | - Install the source code from GitHub 34 | 35 | ```console 36 | go get github.com/awslabs/fargatecli 37 | ``` 38 | 39 | - Run `dep ensure` to install required dependencies 40 | 41 | ```console 42 | cd $GOPATH/src/github.com/awslabs/fargatecli 43 | dep ensure 44 | ``` 45 | 46 | - Make sure you can run the tests 47 | 48 | ```console 49 | make test 50 | ``` 51 | 52 | ## Testing 53 | 54 | - Tests can be run via `go test` or `make test` 55 | 56 | - To generate mocks as you add functionality, run `make mocks` or use `go 57 | generate` directly 58 | 59 | ## Building 60 | 61 | - To build a binary for your platform run `make` 62 | 63 | - For cross-building for all supported platforms, run `make dist` which builds 64 | binaries for darwin (64-bit) and linux (Arm, 32-bit, 64-bit). 65 | 66 | ## Licensing 67 | 68 | This project is released under the [Apache 2.0 license][apache]. 69 | 70 | ## Code of Conduct 71 | 72 | This project abides by the [Amazon Open Source Code of Conduct][amzn-coc]. 73 | Please be nice. 74 | 75 | [dep]: https://golang.github.io/dep 76 | [dep-install]: https://golang.github.io/dep/docs/installation.html 77 | [dep-releases]: https://github.com/golang/dep/releases 78 | [amzn-coc]: https://aws.github.io/code-of-conduct 79 | [apache]: http://aws.amazon.com/apache-2-0/ 80 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine 2 | 3 | RUN apk add --no-cache git upx 4 | 5 | WORKDIR /fargate 6 | 7 | ADD go.mod . 8 | RUN go mod download 9 | 10 | ADD . /fargate 11 | RUN go build -ldflags="-s -w" 12 | RUN upx --brute fargate 13 | 14 | FROM alpine 15 | 16 | RUN apk add --no-cache ca-certificates 17 | 18 | COPY --from=0 /fargate/fargate /usr/local/bin/ 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: mocks test build dist 2 | 3 | PACKAGES := $(shell go list ./... | grep -v /mock) 4 | 5 | mocks: 6 | go get github.com/golang/mock/mockgen 7 | go generate $(PACKAGES) 8 | 9 | test: 10 | go test -race -cover $(PACKAGES) 11 | 12 | build: 13 | go build -o bin/fargate main.go 14 | 15 | dist: 16 | GOOS=darwin GOARCH=amd64 go build -o dist/build/fargate-darwin-amd64/fargate main.go 17 | GOOS=linux GOARCH=amd64 go build -o dist/build/fargate-linux-amd64/fargate main.go 18 | GOOS=linux GOARCH=386 go build -o dist/build/fargate-linux-386/fargate main.go 19 | GOOS=linux GOARCH=arm go build -o dist/build/fargate-linux-arm/fargate main.go 20 | GOOS=windows go get -u github.com/spf13/cobra 21 | GOOS=windows GOARCH=amd64 go build -o dist/build/fargate-windows-amd64/fargate.exe main.go 22 | GOOS=windows GOARCH=386 go build -o dist/build/fargate-windows-386/fargate.exe main.go 23 | 24 | cd dist/build/fargate-darwin-amd64 && zip fargate-${FARGATE_VERSION}-darwin-amd64.zip fargate 25 | cd dist/build/fargate-linux-amd64 && zip fargate-${FARGATE_VERSION}-linux-amd64.zip fargate 26 | cd dist/build/fargate-linux-386 && zip fargate-${FARGATE_VERSION}-linux-386.zip fargate 27 | cd dist/build/fargate-linux-arm && zip fargate-${FARGATE_VERSION}-linux-arm.zip fargate 28 | cd dist/build/fargate-windows-amd64 && zip fargate-${FARGATE_VERSION}-windows-amd64.zip fargate.exe 29 | cd dist/build/fargate-windows-386 && zip fargate-${FARGATE_VERSION}-windows-386.zip fargate.exe 30 | 31 | find dist/build -name *.zip -exec mv {} dist \; 32 | 33 | rm -rf dist/build 34 | -------------------------------------------------------------------------------- /acm/main.go: -------------------------------------------------------------------------------- 1 | package acm 2 | 3 | //go:generate mockgen -package client -destination=mock/client/client.go github.com/awslabs/fargatecli/acm Client 4 | //go:generate mockgen -package sdk -source ../vendor/github.com/aws/aws-sdk-go/service/acm/acmiface/interface.go -destination=mock/sdk/acmiface.go github.com/aws/aws-sdk-go/service/acm/acmiface ACMAPI 5 | 6 | import ( 7 | "github.com/aws/aws-sdk-go/aws/session" 8 | "github.com/aws/aws-sdk-go/service/acm" 9 | "github.com/aws/aws-sdk-go/service/acm/acmiface" 10 | ) 11 | 12 | // Client represents a method for accessing AWS Certificate Manager. 13 | type Client interface { 14 | DeleteCertificate(string) error 15 | InflateCertificate(*Certificate) error 16 | ListCertificates() (Certificates, error) 17 | RequestCertificate(string, []string) (string, error) 18 | ImportCertificate([]byte, []byte, []byte) (string, error) 19 | } 20 | 21 | // SDKClient implements access to AWS Certificate Manager via the AWS SDK. 22 | type SDKClient struct { 23 | client acmiface.ACMAPI 24 | } 25 | 26 | // New returns an SDKClient configured with the given session. 27 | func New(sess *session.Session) SDKClient { 28 | return SDKClient{ 29 | client: acm.New(sess), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /acm/mock/client/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/awslabs/fargatecli/acm (interfaces: Client) 3 | 4 | // Package client is a generated GoMock package. 5 | package client 6 | 7 | import ( 8 | acm "github.com/awslabs/fargatecli/acm" 9 | gomock "github.com/golang/mock/gomock" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockClient is a mock of Client interface 14 | type MockClient struct { 15 | ctrl *gomock.Controller 16 | recorder *MockClientMockRecorder 17 | } 18 | 19 | // MockClientMockRecorder is the mock recorder for MockClient 20 | type MockClientMockRecorder struct { 21 | mock *MockClient 22 | } 23 | 24 | // NewMockClient creates a new mock instance 25 | func NewMockClient(ctrl *gomock.Controller) *MockClient { 26 | mock := &MockClient{ctrl: ctrl} 27 | mock.recorder = &MockClientMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockClient) EXPECT() *MockClientMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // DeleteCertificate mocks base method 37 | func (m *MockClient) DeleteCertificate(arg0 string) error { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "DeleteCertificate", arg0) 40 | ret0, _ := ret[0].(error) 41 | return ret0 42 | } 43 | 44 | // DeleteCertificate indicates an expected call of DeleteCertificate 45 | func (mr *MockClientMockRecorder) DeleteCertificate(arg0 interface{}) *gomock.Call { 46 | mr.mock.ctrl.T.Helper() 47 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCertificate", reflect.TypeOf((*MockClient)(nil).DeleteCertificate), arg0) 48 | } 49 | 50 | // ImportCertificate mocks base method 51 | func (m *MockClient) ImportCertificate(arg0, arg1, arg2 []byte) (string, error) { 52 | m.ctrl.T.Helper() 53 | ret := m.ctrl.Call(m, "ImportCertificate", arg0, arg1, arg2) 54 | ret0, _ := ret[0].(string) 55 | ret1, _ := ret[1].(error) 56 | return ret0, ret1 57 | } 58 | 59 | // ImportCertificate indicates an expected call of ImportCertificate 60 | func (mr *MockClientMockRecorder) ImportCertificate(arg0, arg1, arg2 interface{}) *gomock.Call { 61 | mr.mock.ctrl.T.Helper() 62 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImportCertificate", reflect.TypeOf((*MockClient)(nil).ImportCertificate), arg0, arg1, arg2) 63 | } 64 | 65 | // InflateCertificate mocks base method 66 | func (m *MockClient) InflateCertificate(arg0 *acm.Certificate) error { 67 | m.ctrl.T.Helper() 68 | ret := m.ctrl.Call(m, "InflateCertificate", arg0) 69 | ret0, _ := ret[0].(error) 70 | return ret0 71 | } 72 | 73 | // InflateCertificate indicates an expected call of InflateCertificate 74 | func (mr *MockClientMockRecorder) InflateCertificate(arg0 interface{}) *gomock.Call { 75 | mr.mock.ctrl.T.Helper() 76 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InflateCertificate", reflect.TypeOf((*MockClient)(nil).InflateCertificate), arg0) 77 | } 78 | 79 | // ListCertificates mocks base method 80 | func (m *MockClient) ListCertificates() (acm.Certificates, error) { 81 | m.ctrl.T.Helper() 82 | ret := m.ctrl.Call(m, "ListCertificates") 83 | ret0, _ := ret[0].(acm.Certificates) 84 | ret1, _ := ret[1].(error) 85 | return ret0, ret1 86 | } 87 | 88 | // ListCertificates indicates an expected call of ListCertificates 89 | func (mr *MockClientMockRecorder) ListCertificates() *gomock.Call { 90 | mr.mock.ctrl.T.Helper() 91 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCertificates", reflect.TypeOf((*MockClient)(nil).ListCertificates)) 92 | } 93 | 94 | // RequestCertificate mocks base method 95 | func (m *MockClient) RequestCertificate(arg0 string, arg1 []string) (string, error) { 96 | m.ctrl.T.Helper() 97 | ret := m.ctrl.Call(m, "RequestCertificate", arg0, arg1) 98 | ret0, _ := ret[0].(string) 99 | ret1, _ := ret[1].(error) 100 | return ret0, ret1 101 | } 102 | 103 | // RequestCertificate indicates an expected call of RequestCertificate 104 | func (mr *MockClientMockRecorder) RequestCertificate(arg0, arg1 interface{}) *gomock.Call { 105 | mr.mock.ctrl.T.Helper() 106 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestCertificate", reflect.TypeOf((*MockClient)(nil).RequestCertificate), arg0, arg1) 107 | } 108 | -------------------------------------------------------------------------------- /acm/mock/sdk/paginators.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/service/acm" 5 | "github.com/aws/aws-sdk-go/service/acm/acmiface" 6 | ) 7 | 8 | type MockListCertificatesPagesClient struct { 9 | acmiface.ACMAPI 10 | Resp *acm.ListCertificatesOutput 11 | Error error 12 | } 13 | 14 | func (m MockListCertificatesPagesClient) ListCertificatesPages(in *acm.ListCertificatesInput, fn func(*acm.ListCertificatesOutput, bool) bool) error { 15 | if m.Error != nil { 16 | return m.Error 17 | } 18 | 19 | fn(m.Resp, true) 20 | 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /cloudwatchlogs/log_group.go: -------------------------------------------------------------------------------- 1 | package cloudwatchlogs 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/aws/awserr" 9 | awscwl "github.com/aws/aws-sdk-go/service/cloudwatchlogs" 10 | "github.com/awslabs/fargatecli/console" 11 | ) 12 | 13 | type GetLogsInput struct { 14 | Filter string 15 | LogGroupName string 16 | LogStreamNames []string 17 | EndTime time.Time 18 | StartTime time.Time 19 | } 20 | 21 | type LogLine struct { 22 | EventId string 23 | LogStreamName string 24 | Message string 25 | Timestamp time.Time 26 | } 27 | 28 | func (cwl *CloudWatchLogs) CreateLogGroup(logGroupName string, a ...interface{}) string { 29 | formattedLogGroupName := fmt.Sprintf(logGroupName, a...) 30 | _, err := cwl.svc.CreateLogGroup( 31 | &awscwl.CreateLogGroupInput{ 32 | LogGroupName: aws.String(formattedLogGroupName), 33 | }, 34 | ) 35 | 36 | if err != nil { 37 | if awsErr, ok := err.(awserr.Error); ok { 38 | switch awsErr.Code() { 39 | case awscwl.ErrCodeResourceAlreadyExistsException: 40 | return formattedLogGroupName 41 | default: 42 | console.ErrorExit(awsErr, "Could not create Cloudwatch Logs log group") 43 | } 44 | } 45 | } 46 | 47 | return formattedLogGroupName 48 | } 49 | 50 | func (cwl *CloudWatchLogs) GetLogs(i *GetLogsInput) []LogLine { 51 | var logLines []LogLine 52 | 53 | input := &awscwl.FilterLogEventsInput{ 54 | LogGroupName: aws.String(i.LogGroupName), 55 | Interleaved: aws.Bool(true), 56 | } 57 | 58 | if !i.StartTime.IsZero() { 59 | input.SetStartTime(i.StartTime.UTC().UnixNano() / int64(time.Millisecond)) 60 | } 61 | 62 | if !i.EndTime.IsZero() { 63 | input.SetEndTime(i.EndTime.UTC().UnixNano() / int64(time.Millisecond)) 64 | } 65 | 66 | if i.Filter != "" { 67 | input.SetFilterPattern(i.Filter) 68 | } 69 | 70 | if len(i.LogStreamNames) > 0 { 71 | input.SetLogStreamNames(aws.StringSlice(i.LogStreamNames)) 72 | } 73 | 74 | err := cwl.svc.FilterLogEventsPages( 75 | input, 76 | func(resp *awscwl.FilterLogEventsOutput, lastPage bool) bool { 77 | for _, event := range resp.Events { 78 | logLines = append(logLines, 79 | LogLine{ 80 | EventId: aws.StringValue(event.EventId), 81 | Message: aws.StringValue(event.Message), 82 | LogStreamName: aws.StringValue(event.LogStreamName), 83 | Timestamp: time.Unix(0, aws.Int64Value(event.Timestamp)*int64(time.Millisecond)), 84 | }, 85 | ) 86 | 87 | } 88 | 89 | return true 90 | }, 91 | ) 92 | 93 | if err != nil { 94 | console.ErrorExit(err, "Could not get logs") 95 | } 96 | 97 | return logLines 98 | } 99 | -------------------------------------------------------------------------------- /cloudwatchlogs/main.go: -------------------------------------------------------------------------------- 1 | package cloudwatchlogs 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/session" 5 | "github.com/aws/aws-sdk-go/service/cloudwatchlogs" 6 | ) 7 | 8 | type CloudWatchLogs struct { 9 | svc *cloudwatchlogs.CloudWatchLogs 10 | } 11 | 12 | func New(sess *session.Session) CloudWatchLogs { 13 | return CloudWatchLogs{ 14 | svc: cloudwatchlogs.New(sess), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cmd/certificate.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/awslabs/fargatecli/acm" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | type certificateOperation struct { 11 | acm acm.Client 12 | output Output 13 | } 14 | 15 | func (o certificateOperation) findCertificate(domainName string) (acm.Certificate, error) { 16 | o.output.Debug("Listing certificates [API=acm Action=ListCertificate]") 17 | certificates, err := o.acm.ListCertificates() 18 | 19 | if err != nil { 20 | return acm.Certificate{}, err 21 | } 22 | 23 | certificates = certificates.GetCertificates(domainName) 24 | 25 | switch { 26 | case len(certificates) == 0: 27 | return acm.Certificate{}, errCertificateNotFound 28 | case len(certificates) > 1: 29 | return acm.Certificate{}, errCertificateTooManyFound 30 | } 31 | 32 | o.output.Debug("Describing certificate [API=acm Action=DescribeCertificate ARN=%s]", certificates[0].ARN) 33 | 34 | if err := o.acm.InflateCertificate(&certificates[0]); err != nil { 35 | return acm.Certificate{}, err 36 | } 37 | 38 | return certificates[0], nil 39 | } 40 | 41 | var ( 42 | errCertificateNotFound = errors.New("certificate not found") 43 | errCertificateTooManyFound = errors.New("too many certificates found") 44 | 45 | certificateCmd = &cobra.Command{ 46 | Use: "certificate", 47 | Short: "Manage certificates", 48 | Long: `Manages certificate 49 | 50 | Certificates are TLS certificates issued by or imported into AWS Certificate 51 | Manager for use in securing traffic between load balancers and end users. ACM 52 | provides TLS certificates free of charge for use within AWS resources.`, 53 | } 54 | ) 55 | 56 | func init() { 57 | rootCmd.AddCommand(certificateCmd) 58 | } 59 | -------------------------------------------------------------------------------- /cmd/certificate_destroy.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/awslabs/fargatecli/acm" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | type certificateDestroyOperation struct { 9 | certificateOperation 10 | domainName string 11 | output Output 12 | } 13 | 14 | func (o certificateDestroyOperation) execute() { 15 | certificate, err := o.findCertificate(o.domainName) 16 | 17 | if err != nil { 18 | switch err { 19 | case errCertificateNotFound: 20 | o.output.Fatal(err, "Could not find certificate for %s", o.domainName) 21 | return 22 | case errCertificateTooManyFound: 23 | o.output.Fatal(err, "Multiple certificates found for %s, for safety please destroy the one you intend via the AWS CLI", o.domainName) 24 | return 25 | default: 26 | o.output.Fatal(err, "Could not destroy certificate") 27 | return 28 | } 29 | } 30 | 31 | o.output.Debug("Deleting certificate [API=acm Action=DeleteCertificate ARN=%s]", certificate.ARN) 32 | if err := o.acm.DeleteCertificate(certificate.ARN); err != nil { 33 | o.output.Fatal(err, "Could not destroy certificate") 34 | return 35 | } 36 | 37 | o.output.Info("Destroyed certificate %s", o.domainName) 38 | } 39 | 40 | var certificateDestroyCmd = &cobra.Command{ 41 | Use: "destroy ", 42 | Short: "Destroy certificate", 43 | Long: `Destroy certificate 44 | 45 | In order to destroy a certificate, it must not be in use by any load balancers or 46 | any other AWS resources.`, 47 | Args: cobra.ExactArgs(1), 48 | Run: func(cmd *cobra.Command, args []string) { 49 | certificateDestroyOperation{ 50 | certificateOperation: certificateOperation{acm: acm.New(sess), output: output}, 51 | domainName: args[0], 52 | output: output, 53 | }.execute() 54 | }, 55 | } 56 | 57 | func init() { 58 | certificateCmd.AddCommand(certificateDestroyCmd) 59 | } 60 | -------------------------------------------------------------------------------- /cmd/certificate_destroy_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/golang/mock/gomock" 8 | "github.com/awslabs/fargatecli/acm" 9 | "github.com/awslabs/fargatecli/acm/mock/client" 10 | "github.com/awslabs/fargatecli/cmd/mock" 11 | ) 12 | 13 | func TestCertificateDestroyOperation(t *testing.T) { 14 | certificateARN := "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012" 15 | domainName := "example.com" 16 | certificate := acm.Certificate{ 17 | ARN: certificateARN, 18 | DomainName: domainName, 19 | } 20 | 21 | mockCtrl := gomock.NewController(t) 22 | defer mockCtrl.Finish() 23 | 24 | mockClient := client.NewMockClient(mockCtrl) 25 | mockOutput := &mock.Output{} 26 | 27 | mockClient.EXPECT().ListCertificates().Return(acm.Certificates{certificate}, nil) 28 | mockClient.EXPECT().InflateCertificate(&certificate).Return(nil) 29 | mockClient.EXPECT().DeleteCertificate(certificateARN).Return(nil) 30 | 31 | certificateDestroyOperation{ 32 | certificateOperation: certificateOperation{ 33 | acm: mockClient, 34 | output: mockOutput, 35 | }, 36 | domainName: domainName, 37 | output: mockOutput, 38 | }.execute() 39 | 40 | if len(mockOutput.FatalMsgs) > 0 { 41 | for _, fatal := range mockOutput.FatalMsgs { 42 | t.Errorf(fatal.Msg, fatal.Errors) 43 | } 44 | } 45 | 46 | if len(mockOutput.InfoMsgs) == 0 { 47 | t.Errorf("Expected info output from operation, got none") 48 | } 49 | } 50 | 51 | func TestCertificateDestroyOperationCertNotFound(t *testing.T) { 52 | domainName := "example.com" 53 | 54 | mockCtrl := gomock.NewController(t) 55 | defer mockCtrl.Finish() 56 | 57 | mockClient := client.NewMockClient(mockCtrl) 58 | mockOutput := &mock.Output{} 59 | 60 | mockClient.EXPECT().ListCertificates().Return(acm.Certificates{}, nil) 61 | 62 | certificateDestroyOperation{ 63 | certificateOperation: certificateOperation{ 64 | acm: mockClient, 65 | output: mockOutput, 66 | }, 67 | domainName: domainName, 68 | output: mockOutput, 69 | }.execute() 70 | 71 | if len(mockOutput.FatalMsgs) == 0 { 72 | t.Errorf("Expected fatal output from operation, got none") 73 | } 74 | 75 | if !mockOutput.Exited { 76 | t.Errorf("Expected premature exit; didn't") 77 | } 78 | } 79 | 80 | func TestCertificateDestroyOperationMoreThanOneCertFound(t *testing.T) { 81 | certificateARN1 := "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012" 82 | certificateARN2 := "arn:aws:acm:us-east-1:123456789012:certificate/abcdef01-2345-6789-0abc-def012345678" 83 | domainName := "example.com" 84 | certificate1 := acm.Certificate{ 85 | ARN: certificateARN1, 86 | DomainName: domainName, 87 | } 88 | certificate2 := acm.Certificate{ 89 | ARN: certificateARN2, 90 | DomainName: domainName, 91 | } 92 | 93 | mockCtrl := gomock.NewController(t) 94 | defer mockCtrl.Finish() 95 | 96 | mockClient := client.NewMockClient(mockCtrl) 97 | mockOutput := &mock.Output{} 98 | 99 | mockClient.EXPECT().ListCertificates().Return(acm.Certificates{certificate1, certificate2}, nil) 100 | 101 | certificateDestroyOperation{ 102 | certificateOperation: certificateOperation{ 103 | acm: mockClient, 104 | output: mockOutput, 105 | }, 106 | domainName: domainName, 107 | output: mockOutput, 108 | }.execute() 109 | 110 | if len(mockOutput.FatalMsgs) == 0 { 111 | t.Errorf("Expected fatal output from operation, got none") 112 | } 113 | 114 | if !mockOutput.Exited { 115 | t.Errorf("Expected premature exit; didn't") 116 | } 117 | } 118 | 119 | func TestCertificateDestroyOperationListError(t *testing.T) { 120 | domainName := "example.com" 121 | 122 | mockCtrl := gomock.NewController(t) 123 | defer mockCtrl.Finish() 124 | 125 | mockClient := client.NewMockClient(mockCtrl) 126 | mockOutput := &mock.Output{} 127 | 128 | mockClient.EXPECT().ListCertificates().Return(acm.Certificates{}, errors.New("something went boom")) 129 | 130 | certificateDestroyOperation{ 131 | certificateOperation: certificateOperation{ 132 | acm: mockClient, 133 | output: mockOutput, 134 | }, 135 | domainName: domainName, 136 | output: mockOutput, 137 | }.execute() 138 | 139 | if len(mockOutput.FatalMsgs) == 0 { 140 | t.Errorf("Expected fatal output from operation, got none") 141 | } 142 | 143 | if !mockOutput.Exited { 144 | t.Errorf("Expected premature exit; didn't") 145 | } 146 | } 147 | 148 | func TestCertificateDestroyOperationDeleteError(t *testing.T) { 149 | certificateARN := "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012" 150 | domainName := "example.com" 151 | certificate := acm.Certificate{ 152 | ARN: certificateARN, 153 | DomainName: domainName, 154 | } 155 | 156 | mockCtrl := gomock.NewController(t) 157 | defer mockCtrl.Finish() 158 | 159 | mockClient := client.NewMockClient(mockCtrl) 160 | mockOutput := &mock.Output{} 161 | 162 | mockClient.EXPECT().ListCertificates().Return(acm.Certificates{certificate}, nil) 163 | mockClient.EXPECT().InflateCertificate(&certificate).Return(nil) 164 | mockClient.EXPECT().DeleteCertificate(certificateARN).Return(errors.New(":-(")) 165 | 166 | certificateDestroyOperation{ 167 | certificateOperation: certificateOperation{ 168 | acm: mockClient, 169 | output: mockOutput, 170 | }, 171 | domainName: domainName, 172 | output: mockOutput, 173 | }.execute() 174 | 175 | if len(mockOutput.FatalMsgs) == 0 { 176 | t.Errorf("Expected fatal output from operation, got none") 177 | } 178 | 179 | if !mockOutput.Exited { 180 | t.Errorf("Expected premature exit; didn't") 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /cmd/certificate_import.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | 7 | "github.com/awslabs/fargatecli/acm" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | type certificateImportOperation struct { 12 | acm acm.Client 13 | certificate []byte 14 | certificateChain []byte 15 | certificateChainFile string 16 | certificateFile string 17 | output Output 18 | privateKey []byte 19 | privateKeyFile string 20 | } 21 | 22 | func (o certificateImportOperation) execute() { 23 | if errs := o.validate(); len(errs) > 0 { 24 | o.output.Fatals(errs, "Invalid certificate import parameters") 25 | return 26 | } 27 | 28 | if errs := o.readFiles(); len(errs) > 0 { 29 | o.output.Fatals(errs, "Could not read file(s)") 30 | return 31 | } 32 | 33 | o.output.Debug("Importing certificate [API=acm Action=ImportCertificate]") 34 | arn, err := o.acm.ImportCertificate(o.certificate, o.privateKey, o.certificateChain) 35 | 36 | if err != nil { 37 | o.output.Fatal(err, "Could not import certificate") 38 | return 39 | } 40 | 41 | o.output.Info("Imported certificate [ARN=%s]", arn) 42 | } 43 | 44 | func (o certificateImportOperation) validate() []error { 45 | var errs []error 46 | 47 | if o.certificateFile == "" { 48 | errs = append(errs, fmt.Errorf("--certificate is required")) 49 | } 50 | 51 | if o.privateKeyFile == "" { 52 | errs = append(errs, fmt.Errorf("--key is required")) 53 | } 54 | 55 | return errs 56 | } 57 | 58 | func (o *certificateImportOperation) readFiles() []error { 59 | var errs []error 60 | 61 | o.output.Debug("Reading certificate [File=%s]", o.certificateFile) 62 | if certificate, err := ioutil.ReadFile(o.certificateFile); err == nil { 63 | o.certificate = certificate 64 | } else { 65 | errs = append(errs, err) 66 | } 67 | 68 | o.output.Debug("Reading private key [File=%s]", o.privateKeyFile) 69 | if privateKey, err := ioutil.ReadFile(o.privateKeyFile); err == nil { 70 | o.privateKey = privateKey 71 | } else { 72 | errs = append(errs, err) 73 | } 74 | 75 | if o.certificateChainFile != "" { 76 | o.output.Debug("Reading certificate chain [File=%s]", o.certificateChainFile) 77 | if certificateChain, err := ioutil.ReadFile(o.certificateChainFile); err == nil { 78 | o.certificateChain = certificateChain 79 | } else { 80 | errs = append(errs, err) 81 | } 82 | } 83 | 84 | return errs 85 | } 86 | 87 | var certificateImportCmd = &cobra.Command{ 88 | Use: "import --certificate --key [--chain ]", 89 | Short: "Import a certificate", 90 | Long: `Import a certificate 91 | 92 | Upload a certificate from a certificate file, a private key file, and optionally 93 | an intermediate certificate chain file. The files must be PEM-encoded and the 94 | private key must not be encrypted or protected by a passphrase. See 95 | http://docs.aws.amazon.com/acm/latest/APIReference/API_ImportCertificate.html 96 | for more details.`, 97 | Run: func(cmd *cobra.Command, args []string) { 98 | certificateImportOperation{ 99 | acm: acm.New(sess), 100 | certificateChainFile: certificateImportFlags.chain, 101 | certificateFile: certificateImportFlags.certificate, 102 | output: output, 103 | privateKeyFile: certificateImportFlags.key, 104 | }.execute() 105 | }, 106 | } 107 | 108 | var certificateImportFlags struct { 109 | certificate, key, chain string 110 | } 111 | 112 | func init() { 113 | certificateImportCmd.Flags().StringVarP(&certificateImportFlags.certificate, "certificate", "c", "", 114 | "Filename of the certificate to import") 115 | certificateImportCmd.Flags().StringVarP(&certificateImportFlags.key, "key", "k", "", 116 | "Filename of the private key used to generate the certificate") 117 | certificateImportCmd.Flags().StringVar(&certificateImportFlags.chain, "chain", "", 118 | "Filename of intermediate certificate chain") 119 | 120 | certificateCmd.AddCommand(certificateImportCmd) 121 | } 122 | -------------------------------------------------------------------------------- /cmd/certificate_import_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/golang/mock/gomock" 10 | "github.com/awslabs/fargatecli/acm/mock/client" 11 | "github.com/awslabs/fargatecli/cmd/mock" 12 | ) 13 | 14 | func TestCertificateImportOperation(t *testing.T) { 15 | certificateARN := "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012" 16 | certificate := readFile("testdata/certificate.crt", t) 17 | privateKey := readFile("testdata/private.key", t) 18 | certificateChain := readFile("testdata/chain.crt", t) 19 | 20 | mockCtrl := gomock.NewController(t) 21 | defer mockCtrl.Finish() 22 | 23 | mockClient := client.NewMockClient(mockCtrl) 24 | mockOutput := &mock.Output{} 25 | 26 | mockClient.EXPECT().ImportCertificate(certificate, privateKey, certificateChain).Return(certificateARN, nil) 27 | 28 | certificateImportOperation{ 29 | acm: mockClient, 30 | certificateChainFile: "testdata/chain.crt", 31 | certificateFile: "testdata/certificate.crt", 32 | output: mockOutput, 33 | privateKeyFile: "testdata/private.key", 34 | }.execute() 35 | 36 | if len(mockOutput.FatalMsgs) > 0 { 37 | for _, fatal := range mockOutput.FatalMsgs { 38 | t.Errorf(fatal.Msg, fatal.Errors) 39 | } 40 | } 41 | 42 | if len(mockOutput.InfoMsgs) == 0 { 43 | t.Errorf("Expected info output from operation, got none") 44 | } 45 | 46 | if !strings.Contains(mockOutput.InfoMsgs[0], "Imported certificate") { 47 | t.Errorf("Expected info output to say 'Imported certificate [ARN=%s]', got: %s", certificateARN, mockOutput.InfoMsgs[0]) 48 | } 49 | } 50 | 51 | func TestCertificateImportOperationSansChain(t *testing.T) { 52 | var certificateChain []byte 53 | 54 | certificateARN := "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012" 55 | certificate := readFile("testdata/certificate.crt", t) 56 | privateKey := readFile("testdata/private.key", t) 57 | 58 | mockCtrl := gomock.NewController(t) 59 | defer mockCtrl.Finish() 60 | 61 | mockClient := client.NewMockClient(mockCtrl) 62 | mockOutput := &mock.Output{} 63 | 64 | mockClient.EXPECT().ImportCertificate(certificate, privateKey, certificateChain).Return(certificateARN, nil) 65 | 66 | certificateImportOperation{ 67 | acm: mockClient, 68 | certificateFile: "testdata/certificate.crt", 69 | output: mockOutput, 70 | privateKeyFile: "testdata/private.key", 71 | }.execute() 72 | 73 | if len(mockOutput.FatalMsgs) > 0 { 74 | for _, fatal := range mockOutput.FatalMsgs { 75 | t.Errorf(fatal.Msg, fatal.Errors) 76 | } 77 | } 78 | 79 | if len(mockOutput.InfoMsgs) == 0 { 80 | t.Errorf("Expected info output from operation, got none") 81 | } 82 | 83 | if !strings.Contains(mockOutput.InfoMsgs[0], "Imported certificate") { 84 | t.Errorf("Expected info output to say 'Imported certificate [ARN=%s]', got: %s", certificateARN, mockOutput.InfoMsgs[0]) 85 | } 86 | } 87 | 88 | func TestCertificateImportOperationMissingParameters(t *testing.T) { 89 | mockCtrl := gomock.NewController(t) 90 | defer mockCtrl.Finish() 91 | 92 | mockClient := client.NewMockClient(mockCtrl) 93 | mockOutput := &mock.Output{} 94 | 95 | certificateImportOperation{ 96 | acm: mockClient, 97 | output: mockOutput, 98 | }.execute() 99 | 100 | if len(mockOutput.FatalMsgs) == 0 { 101 | t.Errorf("Expected fatal output from operation, got none") 102 | } 103 | 104 | if !mockOutput.Exited { 105 | t.Errorf("Expected premature exit, didn't") 106 | } 107 | 108 | if mockOutput.FatalMsgs[0].Msg != "Invalid certificate import parameters" { 109 | t.Errorf("Expected fatal output 'Invalid certificate import parameters', got: %s", mockOutput.FatalMsgs[0].Msg) 110 | } 111 | } 112 | 113 | func TestCertificateImportOperationBadFiles(t *testing.T) { 114 | mockCtrl := gomock.NewController(t) 115 | defer mockCtrl.Finish() 116 | 117 | mockClient := client.NewMockClient(mockCtrl) 118 | mockOutput := &mock.Output{} 119 | 120 | certificateImportOperation{ 121 | acm: mockClient, 122 | certificateChainFile: "pretend", 123 | certificateFile: "pretend", 124 | output: mockOutput, 125 | privateKeyFile: "pretend", 126 | }.execute() 127 | 128 | if len(mockOutput.FatalMsgs) == 0 { 129 | t.Errorf("Expected fatal output from operation, got none") 130 | } 131 | 132 | if !mockOutput.Exited { 133 | t.Errorf("Expected premature exit, didn't") 134 | } 135 | 136 | if mockOutput.FatalMsgs[0].Msg != "Could not read file(s)" { 137 | t.Errorf("Expected fatal output 'Could not read file(s)', got: %s", mockOutput.FatalMsgs[0].Msg) 138 | } 139 | } 140 | 141 | func readFile(fileName string, t *testing.T) []byte { 142 | contents, err := ioutil.ReadFile(fileName) 143 | 144 | if err != nil { 145 | t.Errorf(err.Error()) 146 | } 147 | 148 | return contents 149 | } 150 | 151 | func TestCertificateImportOperationError(t *testing.T) { 152 | certificate := readFile("testdata/certificate.crt", t) 153 | privateKey := readFile("testdata/private.key", t) 154 | certificateChain := readFile("testdata/chain.crt", t) 155 | 156 | mockCtrl := gomock.NewController(t) 157 | defer mockCtrl.Finish() 158 | 159 | mockClient := client.NewMockClient(mockCtrl) 160 | mockOutput := &mock.Output{} 161 | 162 | mockClient.EXPECT().ImportCertificate(certificate, privateKey, certificateChain).Return("", errors.New(":-(")) 163 | 164 | certificateImportOperation{ 165 | acm: mockClient, 166 | certificateChainFile: "testdata/chain.crt", 167 | certificateFile: "testdata/certificate.crt", 168 | output: mockOutput, 169 | privateKeyFile: "testdata/private.key", 170 | }.execute() 171 | 172 | if len(mockOutput.FatalMsgs) == 0 { 173 | t.Errorf("Expected fatal output from operation, got none") 174 | } 175 | 176 | if !mockOutput.Exited { 177 | t.Errorf("Expected premature exit, didn't") 178 | } 179 | 180 | if mockOutput.FatalMsgs[0].Msg != "Could not import certificate" { 181 | t.Errorf("Expected fatal output 'Could not import certificate', got: %s", mockOutput.FatalMsgs[0].Msg) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /cmd/certificate_info.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/awslabs/fargatecli/acm" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | type certificateInfoOperation struct { 11 | certificateOperation 12 | domainName string 13 | output Output 14 | } 15 | 16 | func (o certificateInfoOperation) execute() { 17 | certificate, err := o.findCertificate(o.domainName) 18 | 19 | if err != nil { 20 | switch err { 21 | case errCertificateNotFound: 22 | o.output.Info("No certificate found for %s", o.domainName) 23 | case errCertificateTooManyFound: 24 | o.output.Fatal(nil, "Multiple certificates found for %s", o.domainName) 25 | default: 26 | o.output.Fatal(err, "Could not find certificate for %s", o.domainName) 27 | } 28 | 29 | return 30 | } 31 | 32 | o.display(certificate) 33 | } 34 | 35 | func (o certificateInfoOperation) display(certificate acm.Certificate) { 36 | o.output.KeyValue("Domain Name", certificate.DomainName, 0) 37 | o.output.KeyValue("Status", Titleize(certificate.Status), 0) 38 | o.output.KeyValue("Type", Titleize(certificate.Type), 0) 39 | o.output.KeyValue("Subject Alternative Names", strings.Join(certificate.SubjectAlternativeNames, ", "), 0) 40 | 41 | if len(certificate.Validations) > 0 { 42 | rows := [][]string{ 43 | []string{"DOMAIN NAME", "STATUS", "RECORD"}, 44 | } 45 | 46 | for _, v := range certificate.Validations { 47 | rows = append(rows, []string{v.DomainName, Titleize(v.Status), v.ResourceRecordString()}) 48 | } 49 | 50 | o.output.LineBreak() 51 | o.output.Table("Validations", rows) 52 | } 53 | } 54 | 55 | var certificateInfoCmd = &cobra.Command{ 56 | Use: "info ", 57 | Short: "Inspect certificate", 58 | Long: `Inspect certificate 59 | 60 | Show extended information for a certificate. Includes each validation for the 61 | certificate which shows DNS records which must be created to validate domain 62 | ownership.`, 63 | Args: cobra.ExactArgs(1), 64 | Run: func(cmd *cobra.Command, args []string) { 65 | certificateInfoOperation{ 66 | certificateOperation: certificateOperation{acm: acm.New(sess), output: output}, 67 | domainName: args[0], 68 | output: output, 69 | }.execute() 70 | }, 71 | } 72 | 73 | func init() { 74 | certificateCmd.AddCommand(certificateInfoCmd) 75 | } 76 | -------------------------------------------------------------------------------- /cmd/certificate_list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "sort" 6 | "strings" 7 | "sync" 8 | 9 | "github.com/awslabs/fargatecli/acm" 10 | "github.com/spf13/cobra" 11 | "golang.org/x/time/rate" 12 | ) 13 | 14 | type certificateListOperation struct { 15 | acm acm.Client 16 | output Output 17 | } 18 | 19 | func (o certificateListOperation) execute() { 20 | certificates, err := o.find() 21 | 22 | if err != nil { 23 | o.output.Fatal(err, "Could not list certificates") 24 | return 25 | } 26 | 27 | if len(certificates) == 0 { 28 | o.output.Info("No certificates found") 29 | return 30 | } 31 | 32 | rows := [][]string{ 33 | []string{"CERTIFICATE", "TYPE", "STATUS", "SUBJECT ALTERNATIVE NAMES"}, 34 | } 35 | 36 | sort.Slice(certificates, func(i, j int) bool { 37 | return certificates[i].DomainName < certificates[j].DomainName 38 | }) 39 | 40 | for _, certificate := range certificates { 41 | rows = append(rows, 42 | []string{ 43 | certificate.DomainName, 44 | Titleize(certificate.Type), 45 | Titleize(certificate.Status), 46 | strings.Join(certificate.SubjectAlternativeNames, ", "), 47 | }, 48 | ) 49 | } 50 | 51 | o.output.Table("", rows) 52 | } 53 | 54 | func (o certificateListOperation) find() (acm.Certificates, error) { 55 | var wg sync.WaitGroup 56 | 57 | o.output.Debug("Listing certificates [API=acm Action=ListCertificates]") 58 | certificates, err := o.acm.ListCertificates() 59 | 60 | if err != nil { 61 | return acm.Certificates{}, err 62 | } 63 | 64 | errs := make(chan error) 65 | done := make(chan bool) 66 | limiter := rate.NewLimiter(describeRequestLimitRate, 1) 67 | 68 | for i := 0; i < len(certificates); i++ { 69 | wg.Add(1) 70 | 71 | go func(index int) { 72 | defer wg.Done() 73 | 74 | if err := limiter.Wait(context.Background()); err == nil { 75 | o.output.Debug("Describing certificate [API=acm Action=DescribeCertificate ARN=%s]", certificates[index].ARN) 76 | if err := o.acm.InflateCertificate(&certificates[index]); err != nil { 77 | errs <- err 78 | } 79 | } 80 | }(i) 81 | } 82 | 83 | go func() { 84 | wg.Wait() 85 | done <- true 86 | }() 87 | 88 | select { 89 | case err := <-errs: 90 | return acm.Certificates{}, err 91 | case <-done: 92 | return certificates, nil 93 | } 94 | } 95 | 96 | func (o certificateListOperation) display(certificates []acm.Certificate) { 97 | } 98 | 99 | var certificateListCmd = &cobra.Command{ 100 | Use: "list", 101 | Short: "List certificates", 102 | Run: func(cmd *cobra.Command, args []string) { 103 | certificateListOperation{ 104 | acm: acm.New(sess), 105 | output: output, 106 | }.execute() 107 | }, 108 | } 109 | 110 | func init() { 111 | certificateCmd.AddCommand(certificateListCmd) 112 | } 113 | -------------------------------------------------------------------------------- /cmd/certificate_request.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/awslabs/fargatecli/acm" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | type certificateRequestOperation struct { 9 | acm acm.Client 10 | aliases []string 11 | output Output 12 | domainName string 13 | } 14 | 15 | func (o certificateRequestOperation) execute() { 16 | if errs := o.validate(); len(errs) > 0 { 17 | o.output.Fatals(errs, "Invalid certificate request parameters") 18 | 19 | return 20 | } 21 | 22 | o.output.Debug("Requesting certificate [API=acm Action=RequestCertificate]") 23 | 24 | if arn, err := o.acm.RequestCertificate(o.domainName, o.aliases); err == nil { 25 | o.output.Debug("Requested certificate [ARN=%s]", arn) 26 | } else { 27 | o.output.Fatal(err, "Could not request certificate") 28 | 29 | return 30 | } 31 | 32 | o.output.Info("Requested certificate for %s", o.domainName) 33 | o.output.LineBreak() 34 | o.output.Say("You must validate ownership of the domain name for the certificate to be issued.", 0) 35 | o.output.LineBreak() 36 | o.output.Say("If your domain is hosted using Amazon Route 53, this can be done automatically by running:", 0) 37 | o.output.Say("fargate certificate validate %s", 1, o.domainName) 38 | o.output.LineBreak() 39 | o.output.Say("If not, you must manually create the DNS records returned by running:", 0) 40 | o.output.Say("fargate certificate info %s", 1, o.domainName) 41 | } 42 | 43 | func (o certificateRequestOperation) validate() []error { 44 | var errors []error 45 | 46 | if err := acm.ValidateDomainName(o.domainName); err != nil { 47 | errors = append(errors, err) 48 | } 49 | 50 | for _, alias := range o.aliases { 51 | if err := acm.ValidateAlias(alias); err != nil { 52 | errors = append(errors, err) 53 | } 54 | } 55 | 56 | return errors 57 | } 58 | 59 | var certificateRequestCmd = &cobra.Command{ 60 | Use: "request ", 61 | Short: "Request a certificate", 62 | Long: `Request a certificate 63 | 64 | Certificates can be for a fully qualified domain name (e.g. www.example.com) or 65 | a wildcard domain name (e.g. *.example.com). You can add aliases to a 66 | certificate by specifying additional domain names via the --alias flag. To add 67 | multiple aliases, pass --alias multiple times. By default, AWS Certificate 68 | Manager has a limit of 10 domain names per certificate, but this limit can be 69 | raised by AWS support.`, 70 | Args: cobra.ExactArgs(1), 71 | Run: func(cmd *cobra.Command, args []string) { 72 | certificateRequestOperation{ 73 | acm: acm.New(sess), 74 | aliases: certificateRequestFlags.aliases, 75 | output: output, 76 | domainName: args[0], 77 | }.execute() 78 | }, 79 | } 80 | 81 | var certificateRequestFlags struct { 82 | aliases []string 83 | } 84 | 85 | func init() { 86 | certificateRequestCmd.Flags().StringSliceVarP(&certificateRequestFlags.aliases, "alias", "a", []string{}, 87 | `Additional domain names to be included in the certificate (can be specified multiple times)`) 88 | 89 | certificateCmd.AddCommand(certificateRequestCmd) 90 | } 91 | -------------------------------------------------------------------------------- /cmd/certificate_request_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/golang/mock/gomock" 9 | "github.com/awslabs/fargatecli/acm/mock/client" 10 | "github.com/awslabs/fargatecli/cmd/mock" 11 | ) 12 | 13 | func TestCertificateRequestOperation(t *testing.T) { 14 | certificateARN := "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012" 15 | domainName := "example.com" 16 | aliases := []string{"www.example.com"} 17 | 18 | mockCtrl := gomock.NewController(t) 19 | defer mockCtrl.Finish() 20 | 21 | mockClient := client.NewMockClient(mockCtrl) 22 | mockOutput := &mock.Output{} 23 | 24 | operation := certificateRequestOperation{ 25 | acm: mockClient, 26 | aliases: aliases, 27 | domainName: domainName, 28 | output: mockOutput, 29 | } 30 | 31 | mockClient.EXPECT().RequestCertificate(domainName, aliases).Return(certificateARN, nil) 32 | 33 | operation.execute() 34 | 35 | if len(mockOutput.InfoMsgs) == 0 { 36 | t.Errorf("Expected info output from operation, got none") 37 | } 38 | } 39 | 40 | func TestCertificateRequestOperationError(t *testing.T) { 41 | domainName := "example.com" 42 | aliases := []string{} 43 | 44 | mockCtrl := gomock.NewController(t) 45 | defer mockCtrl.Finish() 46 | 47 | mockClient := client.NewMockClient(mockCtrl) 48 | mockOutput := &mock.Output{} 49 | 50 | operation := certificateRequestOperation{ 51 | acm: mockClient, 52 | aliases: aliases, 53 | domainName: domainName, 54 | output: mockOutput, 55 | } 56 | 57 | mockClient.EXPECT().RequestCertificate(domainName, aliases).Return("", fmt.Errorf("oops, something went wrong")) 58 | 59 | operation.execute() 60 | 61 | if !mockOutput.Exited { 62 | t.Errorf("Expected premature exit; didn't") 63 | } 64 | 65 | if len(mockOutput.FatalMsgs) == 0 { 66 | t.Errorf("Expected error output from operation, got none") 67 | } 68 | } 69 | 70 | func TestCertificateRequestOperationInvalid(t *testing.T) { 71 | mockCtrl := gomock.NewController(t) 72 | defer mockCtrl.Finish() 73 | 74 | mockClient := client.NewMockClient(mockCtrl) 75 | mockOutput := &mock.Output{} 76 | 77 | operation := certificateRequestOperation{ 78 | acm: mockClient, 79 | domainName: "z", // Invalid 80 | output: mockOutput, 81 | } 82 | 83 | operation.execute() 84 | 85 | if !mockOutput.Exited { 86 | t.Errorf("Expected premature exit; didn't") 87 | } 88 | } 89 | 90 | func TestCertificateRequestOperationValidateInvalidDomainName(t *testing.T) { 91 | operation := certificateRequestOperation{ 92 | domainName: "z", // Invalid 93 | } 94 | 95 | errs := operation.validate() 96 | 97 | if len(errs) != 1 { 98 | t.Errorf("Invalid number of errors; want 1, got: %d", len(errs)) 99 | } 100 | 101 | if strings.Index(errs[0].Error(), "The domain name requires at least 2 octets") == -1 { 102 | t.Errorf("Unexpected error; want: 'The domain name requires at leasr 2 octets', got: %s", errs[0].Error()) 103 | } 104 | } 105 | 106 | func TestCertificateRequestOperationValidateInvalidAlias(t *testing.T) { 107 | operation := certificateRequestOperation{ 108 | domainName: "example.com", 109 | aliases: []string{"z"}, // Invalid 110 | } 111 | 112 | errs := operation.validate() 113 | 114 | if len(errs) != 1 { 115 | t.Errorf("Invalid number of errors; want 1, got: %d", len(errs)) 116 | } 117 | 118 | if strings.Index(errs[0].Error(), "An alias requires at least 2 octets") == -1 { 119 | t.Errorf("Unexpected error; want: 'An alias requires at least 2 octets', got: %s", errs[0].Error()) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /cmd/certificate_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/golang/mock/gomock" 8 | "github.com/awslabs/fargatecli/acm" 9 | "github.com/awslabs/fargatecli/acm/mock/client" 10 | "github.com/awslabs/fargatecli/cmd/mock" 11 | ) 12 | 13 | func TestFindCertificate(t *testing.T) { 14 | certificate := acm.Certificate{ 15 | DomainName: "www.example.com", 16 | ARN: "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012", 17 | Status: "ISSUED", 18 | Type: "AMAZON_ISSUED", 19 | } 20 | 21 | mockCtrl := gomock.NewController(t) 22 | defer mockCtrl.Finish() 23 | 24 | mockClient := client.NewMockClient(mockCtrl) 25 | mockOutput := &mock.Output{} 26 | 27 | mockClient.EXPECT().ListCertificates().Return(acm.Certificates{certificate}, nil) 28 | mockClient.EXPECT().InflateCertificate(&certificate).Return(nil) 29 | 30 | operation := certificateOperation{ 31 | acm: mockClient, 32 | output: mockOutput, 33 | } 34 | foundCertificate, err := operation.findCertificate("www.example.com") 35 | 36 | if err != nil { 37 | t.Errorf("Expected no error, got %v", err) 38 | } 39 | 40 | if !reflect.DeepEqual(foundCertificate, certificate) { 41 | t.Errorf("Expected to find %+v, got: %v", certificate, foundCertificate) 42 | } 43 | } 44 | 45 | func TestFindCertificateNotFound(t *testing.T) { 46 | mockCtrl := gomock.NewController(t) 47 | defer mockCtrl.Finish() 48 | 49 | mockClient := client.NewMockClient(mockCtrl) 50 | mockOutput := &mock.Output{} 51 | 52 | mockClient.EXPECT().ListCertificates().Return(acm.Certificates{}, nil) 53 | 54 | operation := certificateOperation{ 55 | acm: mockClient, 56 | output: mockOutput, 57 | } 58 | foundCertificate, err := operation.findCertificate("www.example.com") 59 | 60 | if err != errCertificateNotFound { 61 | t.Errorf("Expected errCertificateNotFound, got %v", err) 62 | } 63 | 64 | if !reflect.DeepEqual(foundCertificate, acm.Certificate{}) { 65 | t.Errorf("Expected empty Certificate, got: %v", foundCertificate) 66 | } 67 | } 68 | 69 | func TestFindCertificateTooManyFound(t *testing.T) { 70 | certificates := acm.Certificates{ 71 | acm.Certificate{DomainName: "www.example.com", ARN: "arn:1"}, 72 | acm.Certificate{DomainName: "www.example.com", ARN: "arn:2"}, 73 | } 74 | 75 | mockCtrl := gomock.NewController(t) 76 | defer mockCtrl.Finish() 77 | 78 | mockClient := client.NewMockClient(mockCtrl) 79 | mockOutput := &mock.Output{} 80 | 81 | mockClient.EXPECT().ListCertificates().Return(certificates, nil) 82 | 83 | operation := certificateOperation{ 84 | acm: mockClient, 85 | output: mockOutput, 86 | } 87 | foundCertificate, err := operation.findCertificate("www.example.com") 88 | 89 | if err != errCertificateTooManyFound { 90 | t.Errorf("Expected errCertificateTooManyFound, got %v", err) 91 | } 92 | 93 | if !reflect.DeepEqual(foundCertificate, acm.Certificate{}) { 94 | t.Errorf("Expected empty Certificate, got: %v", foundCertificate) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /cmd/certificate_validate.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/awslabs/fargatecli/acm" 7 | "github.com/awslabs/fargatecli/route53" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | type certificateValidateOperation struct { 12 | certificateOperation 13 | domainName string 14 | output Output 15 | route53 route53.Client 16 | } 17 | 18 | func (o certificateValidateOperation) execute() { 19 | certificate, err := o.findCertificate(o.domainName) 20 | 21 | if err != nil { 22 | o.output.Fatal(err, "Could not validate certificate") 23 | return 24 | } 25 | 26 | if !certificate.IsPendingValidation() { 27 | o.output.Fatal(fmt.Errorf("certificate %s is in state %s", o.domainName, Humanize(certificate.Status)), "Could not validate certificate") 28 | return 29 | } 30 | 31 | o.output.Debug("Listing hosted zones [API=route53 Action=ListHostedZones]") 32 | hostedZones, err := o.route53.ListHostedZones() 33 | 34 | if err != nil { 35 | o.output.Fatal(err, "Could not validate certificate") 36 | return 37 | } 38 | 39 | for _, v := range certificate.Validations { 40 | switch { 41 | case v.IsPendingValidation(): 42 | if zone, ok := hostedZones.FindSuperDomainOf(v.DomainName); ok { 43 | o.output.Debug("Creating resource record [API=route53 Action=ChangeResourceRecordSets HostedZone=%s]", zone.ID) 44 | id, err := o.route53.CreateResourceRecord( 45 | route53.CreateResourceRecordInput{ 46 | HostedZoneID: zone.ID, 47 | RecordType: v.ResourceRecord.Type, 48 | Name: v.ResourceRecord.Name, 49 | Value: v.ResourceRecord.Value, 50 | }, 51 | ) 52 | 53 | if err != nil { 54 | o.output.Fatal(err, "Could not validate certificate") 55 | return 56 | } 57 | 58 | o.output.Debug("Created resource record [ChangeID=%s]", id) 59 | o.output.Info("[%s] created validation record", v.DomainName) 60 | } else { 61 | o.output.Warn("[%s] could not find zone in Amazon Route 53", v.DomainName) 62 | } 63 | case v.IsSuccess(): 64 | o.output.Info("[%s] already validated", v.DomainName) 65 | case v.IsFailed(): 66 | o.output.Fatal(nil, "[%s] failed validation", v.DomainName) 67 | return 68 | default: 69 | o.output.Warn("[%s] unexpected status: %s", v.DomainName, Humanize(v.Status)) 70 | } 71 | } 72 | } 73 | 74 | var certificateValidateCmd = &cobra.Command{ 75 | Use: "validate ", 76 | Args: cobra.ExactArgs(1), 77 | Short: "Validate certificate ownership", 78 | Long: `Validate certificate ownership 79 | 80 | fargate will automatically create DNS validation record to verify ownership for 81 | any domain names that are hosted within Amazon Route 53. If your certificate 82 | has aliases, a validation record will be attempted per alias. Any records whose 83 | domains are hosted in other DNS hosting providers or in other DNS accounts 84 | and cannot be automatically validated will have the necessary records output. 85 | These records are also available in fargate certificate info \. 86 | 87 | AWS Certificate Manager may take up to several hours after the DNS records are 88 | created to complete validation and issue the certificate.`, 89 | Run: func(cmd *cobra.Command, args []string) { 90 | certificateValidateOperation{ 91 | certificateOperation: certificateOperation{acm: acm.New(sess), output: output}, 92 | domainName: args[0], 93 | output: output, 94 | route53: route53.New(sess), 95 | }.execute() 96 | }, 97 | } 98 | 99 | func init() { 100 | certificateCmd.AddCommand(certificateValidateCmd) 101 | } 102 | -------------------------------------------------------------------------------- /cmd/lb.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/awslabs/fargatecli/elbv2" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | const defaultTargetGroupFormat = "%s-default" 11 | 12 | type lbOperation struct { 13 | elbv2 elbv2.Client 14 | output Output 15 | } 16 | 17 | func (o lbOperation) findLB(lbName string) (elbv2.LoadBalancer, error) { 18 | o.output.Debug("Finding load balancer[API=elb2 Action=DescribeLoadBalancers]") 19 | loadBalancers, err := o.elbv2.DescribeLoadBalancersByName([]string{lbName}) 20 | 21 | if err != nil { 22 | return elbv2.LoadBalancer{}, err 23 | } 24 | 25 | switch { 26 | case len(loadBalancers) == 0: 27 | return elbv2.LoadBalancer{}, errLBNotFound 28 | case len(loadBalancers) > 1: 29 | return elbv2.LoadBalancer{}, errLBTooManyFound 30 | } 31 | 32 | return loadBalancers[0], nil 33 | } 34 | 35 | var ( 36 | errLBNotFound = errors.New("load balancer not found") 37 | errLBTooManyFound = errors.New("too many load balancers found") 38 | 39 | lbCmd = &cobra.Command{ 40 | Use: "lb", 41 | Short: "Manage load balancers", 42 | Long: `Manage load balancers 43 | 44 | Load balancers distribute incoming traffic between the tasks within a service 45 | for HTTP/HTTPS and TCP applications. HTTP/HTTPS load balancers can route to 46 | multiple services based upon rules you specify when you create a new service.`, 47 | } 48 | ) 49 | 50 | func init() { 51 | rootCmd.AddCommand(lbCmd) 52 | } 53 | -------------------------------------------------------------------------------- /cmd/lb_alias.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/awslabs/fargatecli/elbv2" 5 | "github.com/awslabs/fargatecli/route53" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | type lbAliasOperation struct { 10 | lbOperation 11 | aliasDomain string 12 | lbName string 13 | output Output 14 | route53 route53.Client 15 | } 16 | 17 | func (o lbAliasOperation) execute() { 18 | loadBalancer, err := o.findLB(o.lbName) 19 | 20 | if err != nil { 21 | o.output.Fatal(err, "Could not alias load balancer") 22 | return 23 | } 24 | 25 | hostedZones, err := o.route53.ListHostedZones() 26 | 27 | if err != nil { 28 | o.output.Fatal(err, "Could not alias load balancer") 29 | return 30 | } 31 | 32 | if hostedZone, ok := hostedZones.FindSuperDomainOf(o.aliasDomain); ok { 33 | o.output.Debug("Creating alias record [API=route53 Action=CreateResourceRecordSet]") 34 | id, err := o.route53.CreateAlias( 35 | route53.CreateAliasInput{ 36 | HostedZoneID: hostedZone.ID, 37 | RecordType: "A", 38 | Name: o.aliasDomain, 39 | Target: loadBalancer.DNSName, 40 | TargetHostedZoneID: loadBalancer.HostedZoneID, 41 | }, 42 | ) 43 | 44 | if err != nil { 45 | o.output.Fatal(err, "Could not alias load balancer") 46 | return 47 | } 48 | 49 | o.output.Debug("Created alias record [ChangeID=%s]", id) 50 | o.output.Info("Created alias record (%s -> %s)", o.aliasDomain, loadBalancer.DNSName) 51 | } else { 52 | o.output.Warn("Could not find hosted zone for %s", o.aliasDomain) 53 | o.output.Say("If you're hosting this domain elsewhere or in another AWS account, please manually create the alias record:", 1) 54 | o.output.Say("%s -> %s", 1, o.aliasDomain, loadBalancer.DNSName) 55 | } 56 | } 57 | 58 | var lbAliasCmd = &cobra.Command{ 59 | Use: "alias ", 60 | Args: cobra.ExactArgs(2), 61 | Short: "Create a load balancer alias record", 62 | Long: `Create a load balancer alias record 63 | 64 | Create an alias record to the load balancer for domains that are hosted within 65 | Amazon Route 53 and within the same AWS account. If you're using another DNS 66 | provider or host your domains in a different account, you will need to manually 67 | create this record. `, 68 | Run: func(cmd *cobra.Command, args []string) { 69 | lbAliasOperation{ 70 | aliasDomain: args[1], 71 | lbName: args[0], 72 | lbOperation: lbOperation{elbv2: elbv2.New(sess), output: output}, 73 | output: output, 74 | route53: route53.New(sess), 75 | }.execute() 76 | }, 77 | } 78 | 79 | func init() { 80 | lbCmd.AddCommand(lbAliasCmd) 81 | } 82 | -------------------------------------------------------------------------------- /cmd/lb_destroy.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/awslabs/fargatecli/console" 7 | ELBV2 "github.com/awslabs/fargatecli/elbv2" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | type LoadBalancerDestroyOperation struct { 12 | LoadBalancerName string 13 | } 14 | 15 | var loadBalancerDestroyCmd = &cobra.Command{ 16 | Use: "destroy ", 17 | Short: "Destroy load balancer", 18 | Args: cobra.ExactArgs(1), 19 | Run: func(cmd *cobra.Command, args []string) { 20 | operation := &LoadBalancerDestroyOperation{ 21 | LoadBalancerName: args[0], 22 | } 23 | 24 | destroyLoadBalancer(operation) 25 | }, 26 | } 27 | 28 | func init() { 29 | lbCmd.AddCommand(loadBalancerDestroyCmd) 30 | } 31 | 32 | func destroyLoadBalancer(operation *LoadBalancerDestroyOperation) { 33 | elbv2 := ELBV2.New(sess) 34 | 35 | elbv2.DeleteLoadBalancer(operation.LoadBalancerName) 36 | elbv2.DeleteTargetGroup(fmt.Sprintf(defaultTargetGroupFormat, operation.LoadBalancerName)) 37 | console.Info("Destroyed load balancer %s", operation.LoadBalancerName) 38 | } 39 | -------------------------------------------------------------------------------- /cmd/lb_info.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sort" 7 | "strings" 8 | "text/tabwriter" 9 | 10 | ACM "github.com/awslabs/fargatecli/acm" 11 | "github.com/awslabs/fargatecli/console" 12 | ECS "github.com/awslabs/fargatecli/ecs" 13 | ELBV2 "github.com/awslabs/fargatecli/elbv2" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | type LbInfoOperation struct { 18 | LoadBalancerName string 19 | } 20 | 21 | var lbInfoCmd = &cobra.Command{ 22 | Use: "info ", 23 | Short: "Inspect load balancer", 24 | Args: cobra.ExactArgs(1), 25 | Run: func(cmd *cobra.Command, args []string) { 26 | operation := &LbInfoOperation{ 27 | LoadBalancerName: args[0], 28 | } 29 | 30 | getLoadBalancerInfo(operation) 31 | }, 32 | } 33 | 34 | func init() { 35 | lbCmd.AddCommand(lbInfoCmd) 36 | } 37 | 38 | func getLoadBalancerInfo(operation *LbInfoOperation) { 39 | elbv2 := ELBV2.New(sess) 40 | acm := ACM.New(sess) 41 | ecs := ECS.New(sess, clusterName) 42 | loadBalancer := elbv2.DescribeLoadBalancer(operation.LoadBalancerName) 43 | services := ecs.ListServices() 44 | 45 | console.KeyValue("Load Balancer Name", "%s\n", loadBalancer.Name) 46 | console.KeyValue("Status", "%s\n", Humanize(loadBalancer.Status)) 47 | console.KeyValue("Type", "%s\n", Humanize(loadBalancer.Type)) 48 | console.KeyValue("DNS Name", "%s\n", loadBalancer.DNSName) 49 | console.KeyValue("Subnets", "%s\n", strings.Join(loadBalancer.SubnetIDs, ", ")) 50 | console.KeyValue("Security Groups", "%s\n", strings.Join(loadBalancer.SecurityGroupIDs, ", ")) 51 | console.KeyValue("Ports", "\n") 52 | 53 | for _, listener := range elbv2.GetListeners(loadBalancer.ARN) { 54 | var ruleCount int 55 | 56 | console.KeyValue(" "+listener.String(), "\n") 57 | 58 | if len(listener.CertificateARNs) > 0 { 59 | certificateDomains := acm.ListCertificateDomainNames(listener.CertificateARNs) 60 | console.KeyValue(" Certificates", "%s\n", strings.Join(certificateDomains, ", ")) 61 | } 62 | 63 | w := new(tabwriter.Writer) 64 | w.Init(os.Stdout, 4, 2, 2, ' ', 0) 65 | 66 | console.KeyValue(" Rules", "\n") 67 | 68 | rules := elbv2.DescribeRules(listener.ARN) 69 | 70 | sort.Slice(rules, func(i, j int) bool { return rules[i].Priority > rules[j].Priority }) 71 | 72 | for _, rule := range rules { 73 | serviceName := fmt.Sprintf("Unknown (%s)", rule.TargetGroupARN) 74 | 75 | if strings.Contains(rule.TargetGroupARN, fmt.Sprintf("/%s-default/", loadBalancer.Name)) { 76 | continue 77 | } 78 | 79 | for _, service := range services { 80 | if service.TargetGroupArn == rule.TargetGroupARN { 81 | serviceName = service.Name 82 | } 83 | } 84 | 85 | fmt.Fprintf(w, " %d\t%s\t%s\n", rule.Priority, rule.String(), serviceName) 86 | 87 | ruleCount++ 88 | } 89 | 90 | if ruleCount == 0 { 91 | fmt.Println(" None") 92 | } 93 | 94 | w.Flush() 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /cmd/lb_list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sort" 7 | "sync" 8 | 9 | "github.com/awslabs/fargatecli/elbv2" 10 | "github.com/spf13/cobra" 11 | "golang.org/x/time/rate" 12 | ) 13 | 14 | type lbListOperation struct { 15 | elbv2 elbv2.Client 16 | output Output 17 | } 18 | 19 | func (o lbListOperation) execute() { 20 | loadBalancers, err := o.find() 21 | 22 | if err != nil { 23 | o.output.Fatal(err, "Could not list load balancers") 24 | return 25 | } 26 | 27 | if len(loadBalancers) == 0 { 28 | o.output.Info("No load balancers found") 29 | return 30 | } 31 | 32 | rows := [][]string{ 33 | []string{"NAME", "TYPE", "STATUS", "DNS NAME", "PORTS"}, 34 | } 35 | 36 | sort.Slice(loadBalancers, func(i, j int) bool { 37 | return loadBalancers[i].Name < loadBalancers[j].Name 38 | }) 39 | 40 | for _, loadBalancer := range loadBalancers { 41 | rows = append(rows, 42 | []string{ 43 | loadBalancer.Name, 44 | Titleize(loadBalancer.Type), 45 | Titleize(loadBalancer.Status), 46 | loadBalancer.DNSName, 47 | fmt.Sprintf("%s", loadBalancer.Listeners), 48 | }, 49 | ) 50 | } 51 | 52 | o.output.Table("", rows) 53 | } 54 | 55 | func (o lbListOperation) find() (elbv2.LoadBalancers, error) { 56 | var wg sync.WaitGroup 57 | 58 | o.output.Debug("Describing Load Balancers [API=elbv2 Action=DescribeLoadBalancers]") 59 | loadBalancers, err := o.elbv2.DescribeLoadBalancers() 60 | 61 | if err != nil { 62 | return elbv2.LoadBalancers{}, err 63 | } 64 | 65 | errs := make(chan error) 66 | done := make(chan bool) 67 | limiter := rate.NewLimiter(describeRequestLimitRate, 1) 68 | 69 | for i := 0; i < len(loadBalancers); i++ { 70 | wg.Add(1) 71 | 72 | go func(index int) { 73 | defer wg.Done() 74 | 75 | if err := limiter.Wait(context.Background()); err == nil { 76 | o.output.Debug("Describing Listeners [API=elbv2 Action=DescribeListeners LoadBalancerArn=%s]", loadBalancers[index].ARN) 77 | listeners, err := o.elbv2.DescribeListeners(loadBalancers[index].ARN) 78 | 79 | if err != nil { 80 | errs <- err 81 | } 82 | 83 | loadBalancers[index].Listeners = listeners 84 | } 85 | }(i) 86 | } 87 | 88 | go func() { 89 | wg.Wait() 90 | done <- true 91 | }() 92 | 93 | select { 94 | case err := <-errs: 95 | return elbv2.LoadBalancers{}, err 96 | case <-done: 97 | return loadBalancers, nil 98 | } 99 | } 100 | 101 | var lbListCmd = &cobra.Command{ 102 | Use: "list", 103 | Short: "List load balancers", 104 | Run: func(cmd *cobra.Command, args []string) { 105 | lbListOperation{ 106 | elbv2: elbv2.New(sess), 107 | output: output, 108 | }.execute() 109 | }, 110 | } 111 | 112 | func init() { 113 | lbCmd.AddCommand(lbListCmd) 114 | } 115 | -------------------------------------------------------------------------------- /cmd/lb_list_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/golang/mock/gomock" 9 | "github.com/awslabs/fargatecli/cmd/mock" 10 | "github.com/awslabs/fargatecli/elbv2" 11 | elbv2client "github.com/awslabs/fargatecli/elbv2/mock/client" 12 | ) 13 | 14 | func TestLBListOperation(t *testing.T) { 15 | mockCtrl := gomock.NewController(t) 16 | defer mockCtrl.Finish() 17 | 18 | mockClient := elbv2client.NewMockClient(mockCtrl) 19 | mockOutput := &mock.Output{} 20 | 21 | loadBalancer1 := elbv2.LoadBalancer{ 22 | ARN: "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/lb/50dc6c495c0c9188", 23 | DNSName: "test-12345678.us-east-1.elb.amazonaws.com", 24 | Name: "test", 25 | Type: "application", 26 | Status: "active", 27 | } 28 | loadBalancer2 := elbv2.LoadBalancer{ 29 | ARN: "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/lb/93fa3d386bec918a", 30 | DNSName: "test-abcdef.us-east-1.elb.amazonaws.com", 31 | Name: "test2", 32 | Type: "application", 33 | Status: "active", 34 | } 35 | listener1 := elbv2.Listener{ 36 | ARN: "arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2", 37 | Port: 80, 38 | Protocol: "HTTP", 39 | } 40 | listener2 := elbv2.Listener{ 41 | ARN: "arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2", 42 | Port: 8080, 43 | Protocol: "HTTP", 44 | } 45 | loadBalancers := elbv2.LoadBalancers{loadBalancer1, loadBalancer2} 46 | listeners1 := elbv2.Listeners{listener1} 47 | listeners2 := elbv2.Listeners{listener2} 48 | 49 | mockClient.EXPECT().DescribeLoadBalancers().Return(loadBalancers, nil) 50 | mockClient.EXPECT().DescribeListeners(loadBalancer1.ARN).Return(listeners1, nil) 51 | mockClient.EXPECT().DescribeListeners(loadBalancer2.ARN).Return(listeners2, nil) 52 | 53 | lbListOperation{ 54 | elbv2: mockClient, 55 | output: mockOutput, 56 | }.execute() 57 | 58 | if len(mockOutput.Tables) == 0 { 59 | t.Fatalf("expected table, got none") 60 | } 61 | 62 | if len(mockOutput.Tables[0].Rows) != 3 { 63 | t.Errorf("expected table with 3 rows, got %d", len(mockOutput.Tables[0].Rows)) 64 | } 65 | 66 | if expected, got := []string{"NAME", "TYPE", "STATUS", "DNS NAME", "PORTS"}, mockOutput.Tables[0].Rows[0]; !reflect.DeepEqual(expected, got) { 67 | t.Errorf("expected column headers: %v, got: %v", expected, got) 68 | } 69 | 70 | row1 := mockOutput.Tables[0].Rows[1] 71 | 72 | if row1[0] != loadBalancer1.Name { 73 | t.Errorf("expected name: %s, got: %s", loadBalancer1.Name, row1[0]) 74 | } 75 | 76 | if expected := Titleize(loadBalancer1.Type); row1[1] != expected { 77 | t.Errorf("expected type: %s, got: %s", expected, row1[1]) 78 | } 79 | 80 | if expected := Titleize(loadBalancer1.Status); row1[2] != expected { 81 | t.Errorf("expected status: %s, got: %s", expected, row1[2]) 82 | } 83 | 84 | if row1[3] != loadBalancer1.DNSName { 85 | t.Errorf("expected DNS name: %s, got: %s", loadBalancer1.DNSName, row1[3]) 86 | } 87 | 88 | if expected := "HTTP:80"; row1[4] != expected { 89 | t.Errorf("expected ports: %s, got: %s", expected, row1[4]) 90 | } 91 | 92 | row2 := mockOutput.Tables[0].Rows[2] 93 | 94 | if row2[0] != loadBalancer2.Name { 95 | t.Errorf("expected name: %s, got: %s", loadBalancer2.Name, row2[0]) 96 | } 97 | 98 | if expected := Titleize(loadBalancer2.Type); row2[1] != expected { 99 | t.Errorf("expected type: %s, got: %s", expected, row2[1]) 100 | } 101 | 102 | if expected := Titleize(loadBalancer2.Status); row2[2] != expected { 103 | t.Errorf("expected status: %s, got: %s", expected, row2[2]) 104 | } 105 | 106 | if row2[3] != loadBalancer2.DNSName { 107 | t.Errorf("expected DNS name: %s, got: %s", loadBalancer1.DNSName, row2[3]) 108 | } 109 | 110 | if expected := "HTTP:8080"; row2[4] != expected { 111 | t.Errorf("expected ports: %s, got: %s", expected, row2[4]) 112 | } 113 | } 114 | 115 | func TestLBListOperationLBDescribeError(t *testing.T) { 116 | mockCtrl := gomock.NewController(t) 117 | defer mockCtrl.Finish() 118 | 119 | mockClient := elbv2client.NewMockClient(mockCtrl) 120 | mockOutput := &mock.Output{} 121 | 122 | mockClient.EXPECT().DescribeLoadBalancers().Return(elbv2.LoadBalancers{}, errors.New("boom")) 123 | 124 | lbListOperation{ 125 | elbv2: mockClient, 126 | output: mockOutput, 127 | }.execute() 128 | 129 | if len(mockOutput.FatalMsgs) == 0 { 130 | t.Fatalf("expected fatal output, got none") 131 | } 132 | 133 | if expected, got := "Could not list load balancers", mockOutput.FatalMsgs[0].Msg; got != expected { 134 | t.Errorf("expected fatal output: %s, got: %s", expected, got) 135 | } 136 | } 137 | 138 | func TestLBListOperationListenerDescribeError(t *testing.T) { 139 | mockCtrl := gomock.NewController(t) 140 | defer mockCtrl.Finish() 141 | 142 | mockClient := elbv2client.NewMockClient(mockCtrl) 143 | mockOutput := &mock.Output{} 144 | 145 | loadBalancers := elbv2.LoadBalancers{ 146 | elbv2.LoadBalancer{ARN: "lbARN"}, 147 | } 148 | 149 | mockClient.EXPECT().DescribeLoadBalancers().Return(loadBalancers, nil) 150 | mockClient.EXPECT().DescribeListeners("lbARN").Return(elbv2.Listeners{}, errors.New("boom")) 151 | 152 | lbListOperation{ 153 | elbv2: mockClient, 154 | output: mockOutput, 155 | }.execute() 156 | 157 | if len(mockOutput.FatalMsgs) == 0 { 158 | t.Fatalf("expected fatal output, got none") 159 | } 160 | 161 | if expected, got := "Could not list load balancers", mockOutput.FatalMsgs[0].Msg; got != expected { 162 | t.Errorf("expected fatal output: %s, got: %s", expected, got) 163 | } 164 | } 165 | 166 | func TestLBListOperationNoneFound(t *testing.T) { 167 | mockCtrl := gomock.NewController(t) 168 | defer mockCtrl.Finish() 169 | 170 | mockClient := elbv2client.NewMockClient(mockCtrl) 171 | mockOutput := &mock.Output{} 172 | 173 | mockClient.EXPECT().DescribeLoadBalancers().Return(elbv2.LoadBalancers{}, nil) 174 | 175 | lbListOperation{ 176 | elbv2: mockClient, 177 | output: mockOutput, 178 | }.execute() 179 | 180 | if len(mockOutput.InfoMsgs) == 0 { 181 | t.Fatalf("expected info output, got none") 182 | } 183 | 184 | if expected, got := "No load balancers found", mockOutput.InfoMsgs[0]; got != expected { 185 | t.Errorf("expected info output: %s, got: %s", expected, got) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /cmd/logs.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "strings" 7 | "time" 8 | 9 | lru "github.com/hashicorp/golang-lru" 10 | CWL "github.com/awslabs/fargatecli/cloudwatchlogs" 11 | "github.com/awslabs/fargatecli/console" 12 | ) 13 | 14 | const ( 15 | timeFormat = "2006-01-02 15:04:05" 16 | timeFormatWithZone = "2006-01-02 15:04:05 MST" 17 | logStreamNameFormat = "fargate/%s/%s" 18 | eventCacheSize = 10000 19 | ) 20 | 21 | type Empty struct{} 22 | 23 | type GetLogsOperation struct { 24 | LogGroupName string 25 | Namespace string 26 | EndTime time.Time 27 | Filter string 28 | Follow bool 29 | LogStreamColors map[string]int 30 | LogStreamNames []string 31 | StartTime time.Time 32 | EventCache *lru.Cache 33 | } 34 | 35 | func (o *GetLogsOperation) AddStartTime(rawStartTime string) { 36 | if rawStartTime != "" { 37 | o.StartTime = o.parseTime(rawStartTime) 38 | } 39 | } 40 | 41 | func (o *GetLogsOperation) AddEndTime(rawEndTime string) { 42 | if rawEndTime != "" { 43 | o.EndTime = o.parseTime(rawEndTime) 44 | } 45 | } 46 | 47 | func (o *GetLogsOperation) AddTasks(tasks []string) { 48 | for _, task := range tasks { 49 | logStreamName := fmt.Sprintf(logStreamNameFormat, o.Namespace, task) 50 | o.LogStreamNames = append(o.LogStreamNames, logStreamName) 51 | } 52 | } 53 | 54 | func (o *GetLogsOperation) Validate() { 55 | if o.Follow && !o.EndTime.IsZero() { 56 | console.ErrorExit(fmt.Errorf("--end-time cannot be specified if following"), "Invalid command line flags") 57 | } 58 | } 59 | 60 | func (o *GetLogsOperation) GetStreamColor(logStreamName string) int { 61 | if o.LogStreamColors == nil { 62 | o.LogStreamColors = make(map[string]int) 63 | } 64 | 65 | if o.LogStreamColors[logStreamName] == 0 { 66 | o.LogStreamColors[logStreamName] = rand.Intn(256) 67 | } 68 | 69 | return o.LogStreamColors[logStreamName] 70 | } 71 | 72 | func (o *GetLogsOperation) SeenEvent(eventId string) bool { 73 | if o.EventCache == nil { 74 | o.EventCache, _ = lru.New(eventCacheSize) 75 | } 76 | 77 | if !o.EventCache.Contains(eventId) { 78 | o.EventCache.Add(eventId, Empty{}) 79 | return false 80 | } else { 81 | return true 82 | } 83 | } 84 | 85 | func (o *GetLogsOperation) parseTime(rawTime string) time.Time { 86 | var t time.Time 87 | 88 | if duration, err := time.ParseDuration(strings.ToLower(rawTime)); err == nil { 89 | return time.Now().Add(duration) 90 | } 91 | 92 | if t, err := time.Parse(timeFormat, rawTime); err == nil { 93 | return t 94 | } 95 | 96 | if t, err := time.Parse(timeFormatWithZone, rawTime); err == nil { 97 | return t 98 | } 99 | 100 | console.ErrorExit(fmt.Errorf("Could not parse %s", rawTime), "Invalid command line flags") 101 | 102 | return t 103 | } 104 | 105 | func GetLogs(operation *GetLogsOperation) { 106 | rand.Seed(time.Now().UTC().UnixNano()) 107 | 108 | if operation.Follow { 109 | followLogs(operation) 110 | } else { 111 | getLogs(operation) 112 | } 113 | } 114 | 115 | func followLogs(operation *GetLogsOperation) { 116 | ticker := time.NewTicker(time.Second) 117 | 118 | if operation.StartTime.IsZero() { 119 | operation.StartTime = time.Now() 120 | } 121 | 122 | for { 123 | getLogs(operation) 124 | 125 | if newStartTime := time.Now().Add(-10 * time.Second); newStartTime.After(operation.StartTime) { 126 | operation.StartTime = newStartTime 127 | } 128 | 129 | <-ticker.C 130 | } 131 | } 132 | 133 | func getLogs(operation *GetLogsOperation) { 134 | cwl := CWL.New(sess) 135 | input := &CWL.GetLogsInput{ 136 | LogStreamNames: operation.LogStreamNames, 137 | LogGroupName: operation.LogGroupName, 138 | Filter: operation.Filter, 139 | StartTime: operation.StartTime, 140 | EndTime: operation.EndTime, 141 | } 142 | 143 | for _, logLine := range cwl.GetLogs(input) { 144 | streamColor := operation.GetStreamColor(logLine.LogStreamName) 145 | 146 | if !operation.SeenEvent(logLine.EventId) { 147 | console.LogLine(logLine.LogStreamName, logLine.Message, streamColor) 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /cmd/mock/output.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | type Output struct { 9 | DebugMsgs []string 10 | Exited bool 11 | FatalMsgs []Fatal 12 | InfoMsgs []string 13 | KeyValueMsgs map[string]string 14 | SayMsgs []string 15 | Tables []Table 16 | WarnMsgs []string 17 | lock sync.Mutex 18 | } 19 | 20 | type Table struct { 21 | Header string 22 | Rows [][]string 23 | } 24 | 25 | type Fatal struct { 26 | Errors []error 27 | Msg string 28 | } 29 | 30 | func (o *Output) Info(msg string, a ...interface{}) { 31 | o.lock.Lock() 32 | defer o.lock.Unlock() 33 | 34 | o.InfoMsgs = append(o.InfoMsgs, fmt.Sprintf(msg, a...)) 35 | } 36 | 37 | func (o *Output) Warn(msg string, a ...interface{}) { 38 | o.lock.Lock() 39 | defer o.lock.Unlock() 40 | 41 | o.WarnMsgs = append(o.WarnMsgs, fmt.Sprintf(msg, a...)) 42 | } 43 | 44 | func (o *Output) Fatal(err error, msg string, a ...interface{}) { 45 | o.Fatals([]error{err}, msg, a...) 46 | } 47 | 48 | func (o *Output) Fatals(errs []error, msg string, a ...interface{}) { 49 | o.lock.Lock() 50 | defer o.lock.Unlock() 51 | 52 | o.FatalMsgs = append(o.FatalMsgs, Fatal{Msg: fmt.Sprintf(msg, a...), Errors: errs}) 53 | o.Exited = true 54 | } 55 | 56 | func (o *Output) Say(msg string, indent int, a ...interface{}) { 57 | o.lock.Lock() 58 | defer o.lock.Unlock() 59 | 60 | o.SayMsgs = append(o.SayMsgs, fmt.Sprintf(msg, a...)) 61 | } 62 | 63 | func (o *Output) Debug(msg string, a ...interface{}) { 64 | o.lock.Lock() 65 | defer o.lock.Unlock() 66 | 67 | o.DebugMsgs = append(o.DebugMsgs, fmt.Sprintf(msg, a...)) 68 | } 69 | 70 | func (o *Output) KeyValue(key, value string, indent int, a ...interface{}) { 71 | o.lock.Lock() 72 | defer o.lock.Unlock() 73 | 74 | if o.KeyValueMsgs == nil { 75 | o.KeyValueMsgs = make(map[string]string) 76 | } 77 | 78 | o.KeyValueMsgs[key] = value 79 | } 80 | 81 | func (o *Output) Table(header string, rows [][]string) { 82 | o.lock.Lock() 83 | defer o.lock.Unlock() 84 | 85 | o.Tables = append(o.Tables, Table{Header: header, Rows: rows}) 86 | } 87 | 88 | func (o *Output) LineBreak() { 89 | } 90 | -------------------------------------------------------------------------------- /cmd/output.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "text/tabwriter" 8 | 9 | "github.com/kyokomi/emoji" 10 | "github.com/mgutz/ansi" 11 | ) 12 | 13 | var ( 14 | debug = emoji.Sprintf(" :wrench: ") 15 | info = emoji.Sprintf(" :information_source: ") 16 | warning = emoji.Sprintf(" :warning: ") 17 | 18 | blue = ansi.ColorCode("blue+bh") 19 | orange = ansi.ColorCode("214+bh") 20 | red = ansi.ColorCode("red+bh") 21 | reset = ansi.ColorCode("reset") 22 | white = ansi.ColorCode("white+bh") 23 | ) 24 | 25 | // Output represents a channel for sending messages to a user. 26 | type Output interface { 27 | Debug(string, ...interface{}) 28 | Fatal(error, string, ...interface{}) 29 | Fatals([]error, string, ...interface{}) 30 | Info(string, ...interface{}) 31 | KeyValue(string, string, int, ...interface{}) 32 | LineBreak() 33 | Say(string, int, ...interface{}) 34 | Table(string, [][]string) 35 | Warn(string, ...interface{}) 36 | } 37 | 38 | // ConsoleOutput implements a channel for sending messages to a user over standard output. 39 | type ConsoleOutput struct { 40 | Color bool 41 | Emoji bool 42 | Verbose bool 43 | Test bool 44 | } 45 | 46 | // Debug prints a formatted message to standard output if `Verbose` is set to `true`. Messages are 47 | // prefixed to indicate they are for debugging with :wrench: or [d]. 48 | func (c ConsoleOutput) Debug(msg string, a ...interface{}) { 49 | if c.Verbose { 50 | switch { 51 | case c.Emoji && c.Color: 52 | fmt.Printf(debug+orange+msg+reset+"\n", a...) 53 | case c.Emoji: 54 | fmt.Printf(debug+msg+"\n", a...) 55 | case c.Color: 56 | fmt.Printf("["+orange+"d"+reset+"] "+orange+msg+reset+"\n", a...) 57 | default: 58 | fmt.Printf("[d] "+msg+"\n", a...) 59 | } 60 | } 61 | } 62 | 63 | // Say prints an optionally indented, formatted message followed by a line break to standard 64 | // output. 65 | func (c ConsoleOutput) Say(msg string, indent int, a ...interface{}) { 66 | for i := 0; i < indent; i++ { 67 | fmt.Print(strings.Repeat(" ", 4)) 68 | } 69 | 70 | fmt.Printf(msg+"\n", a...) 71 | } 72 | 73 | // Info prints a formatted message to standard output. Messages are prefixed to indicate they are 74 | // informational with :information_source: or [i]. 75 | func (c ConsoleOutput) Info(msg string, a ...interface{}) { 76 | switch { 77 | case c.Emoji && c.Color: 78 | fmt.Printf(info+white+msg+reset+"\n", a...) 79 | case c.Emoji: 80 | fmt.Printf(info+msg+"\n", a...) 81 | case c.Color: 82 | fmt.Printf("["+blue+"i"+reset+"] "+white+msg+reset+"\n", a...) 83 | default: 84 | fmt.Printf("[i] "+msg+"\n", a...) 85 | } 86 | } 87 | 88 | // Warn prints a formatted message to standard output. Messages are prefixed to indicate they are 89 | // warnings with :warning: or [!]. 90 | func (c ConsoleOutput) Warn(msg string, a ...interface{}) { 91 | switch { 92 | case c.Emoji && c.Color: 93 | fmt.Printf(warning+red+msg+reset+"\n", a...) 94 | case c.Emoji: 95 | fmt.Printf(warning+msg+"\n", a...) 96 | case c.Color: 97 | fmt.Printf("["+red+"!"+reset+"] "+red+msg+reset+"\n", a...) 98 | default: 99 | fmt.Printf("[!] "+msg+"\n", a...) 100 | } 101 | } 102 | 103 | // Fatal prints a formatted message and an error string to standard output Messages are prefixed 104 | // to indicate they are fatals with :warning: or [!]. 105 | func (c ConsoleOutput) Fatal(err error, msg string, a ...interface{}) { 106 | c.Fatals([]error{err}, msg, a...) 107 | } 108 | 109 | // Fatals prints a formatted message and one or more error strings to standard output. Messages 110 | // are prefixed to indicate they are fatals with :warning: or [!]. 111 | func (c ConsoleOutput) Fatals(errs []error, msg string, a ...interface{}) { 112 | c.Warn(msg, a...) 113 | 114 | for _, err := range errs { 115 | c.Say("- "+err.Error(), 1) 116 | } 117 | 118 | if !c.Test { 119 | os.Exit(1) 120 | } 121 | } 122 | 123 | // KeyValue prints a formatted, optionally indented key and value pair to standard output. 124 | func (c ConsoleOutput) KeyValue(key, value string, indent int, a ...interface{}) { 125 | if c.Color { 126 | c.Say(white+key+reset+": "+value, indent, a...) 127 | } else { 128 | c.Say(key+": "+value, indent, a...) 129 | } 130 | } 131 | 132 | // Table prints a formatted table with optional header to standard output. 133 | func (c ConsoleOutput) Table(header string, rows [][]string) { 134 | if len(header) > 0 { 135 | if c.Color { 136 | c.Say(white+header+reset, 0) 137 | } else { 138 | c.Say(header, 0) 139 | } 140 | 141 | c.LineBreak() 142 | } 143 | 144 | w := new(tabwriter.Writer) 145 | defer w.Flush() 146 | 147 | w.Init(os.Stdout, 0, 8, 1, '\t', 0) 148 | 149 | for _, row := range rows { 150 | for i, column := range row { 151 | fmt.Fprint(w, column) 152 | 153 | if i != len(row)-1 { 154 | fmt.Fprint(w, "\t") 155 | } 156 | } 157 | 158 | fmt.Fprint(w, "\n") 159 | } 160 | 161 | } 162 | 163 | // LineBreak prints a single line break. 164 | func (c ConsoleOutput) LineBreak() { 165 | fmt.Print("\n") 166 | } 167 | -------------------------------------------------------------------------------- /cmd/output_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "errors" 4 | 5 | var consoleOutput = ConsoleOutput{ 6 | Color: false, 7 | Emoji: false, 8 | Verbose: true, 9 | Test: true, 10 | } 11 | 12 | func ExampleConsoleOutput_Debug() { 13 | consoleOutput.Debug("PC LOAD LETTER") 14 | // Output: [d] PC LOAD LETTER 15 | } 16 | 17 | func ExampleConsoleOutput_Info() { 18 | consoleOutput.Info("Welcome! Everything is %s.", "fine") 19 | // Output: [i] Welcome! Everything is fine. 20 | } 21 | 22 | func ExampleConsoleOutput_Fatal() { 23 | err := errors.New("OXY2_TANK_EXPLOSION") 24 | consoleOutput.Fatal(err, "Houston, we've had a problem.") 25 | // Output: 26 | // [!] Houston, we've had a problem. 27 | // - OXY2_TANK_EXPLOSION 28 | } 29 | 30 | func ExampleConsoleOutput_Fatals() { 31 | errs := []error{ 32 | errors.New("OXY2_TANK_EXPLOSION"), 33 | errors.New("PRIM_FUEL_CELL_FAILURE"), 34 | errors.New("SEC_FUEL_CELL_FAILURE"), 35 | } 36 | consoleOutput.Fatals(errs, "Houston, we've had a problem.") 37 | // Output: 38 | // [!] Houston, we've had a problem. 39 | // - OXY2_TANK_EXPLOSION 40 | // - PRIM_FUEL_CELL_FAILURE 41 | // - SEC_FUEL_CELL_FAILURE 42 | } 43 | 44 | func ExampleConsoleOutput_KeyValue() { 45 | population := 468730 46 | 47 | consoleOutput.KeyValue("Name", "Staten Island", 0) 48 | consoleOutput.KeyValue("County", "Richmond", 1) 49 | consoleOutput.KeyValue("Population", "%d", 1, population) 50 | // Output: 51 | // Name: Staten Island 52 | // County: Richmond 53 | // Population: 468730 54 | } 55 | 56 | func ExampleConsoleOutput_Say() { 57 | username := "Werner Brandes" 58 | consoleOutput.Say("Hi, my name is %s. My voice is my passport. Verify Me.", 0, username) 59 | // Output: Hi, my name is Werner Brandes. My voice is my passport. Verify Me. 60 | } 61 | 62 | func ExampleConsoleOutput_Warn() { 63 | consoleOutput.Warn("Keep it secret, keep it safe.") 64 | // Output: [!] Keep it secret, keep it safe. 65 | } 66 | 67 | func ExampleConsoleOutput_Table() { 68 | rows := [][]string{ 69 | {"NAME", "ALLEGIANCE"}, 70 | {"Butterbumps", "House Tyrell"}, 71 | {"Jinglebell", "House Frey"}, 72 | {"Moon Boy", "House Baratheon"}, 73 | } 74 | 75 | consoleOutput.Table("Fools of Westeros", rows) 76 | // Output: 77 | // Fools of Westeros 78 | // 79 | // NAME ALLEGIANCE 80 | // Butterbumps House Tyrell 81 | // Jinglebell House Frey 82 | // Moon Boy House Baratheon 83 | } 84 | -------------------------------------------------------------------------------- /cmd/port.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type Port struct { 11 | Number int64 12 | Protocol string 13 | } 14 | 15 | func (p Port) Empty() bool { 16 | return p.Number == 0 || p.Protocol == "" 17 | } 18 | 19 | func (p Port) String() string { 20 | if p.Empty() { 21 | return "" 22 | } 23 | 24 | return fmt.Sprintf("%s:%d", p.Protocol, p.Number) 25 | } 26 | 27 | var validProtocol = regexp.MustCompile("(?i)\\ATCP|HTTP(S)?\\z") 28 | 29 | func inflatePort(portExpr string) (Port, error) { 30 | switch { 31 | case portExpr == "80": 32 | return buildPort(portExpr, "HTTP") 33 | case portExpr == "443": 34 | return buildPort(portExpr, "HTTPS") 35 | case strings.Index(portExpr, ":") > 1: 36 | parts := strings.Split(portExpr, ":") 37 | protocol, number := strings.ToUpper(parts[0]), parts[1] 38 | 39 | return buildPort(number, protocol) 40 | default: 41 | return buildPort(portExpr, "TCP") 42 | } 43 | } 44 | 45 | func buildPort(inputNumber, inputProtocol string) (Port, error) { 46 | number, err := strconv.ParseInt(inputNumber, 10, 64) 47 | 48 | if err != nil { 49 | if _, ok := err.(*strconv.NumError); ok { 50 | return Port{}, fmt.Errorf("could not parse port number from %s", inputNumber) 51 | } else { 52 | return Port{}, err 53 | } 54 | } 55 | 56 | return Port{number, inputProtocol}, nil 57 | } 58 | 59 | func inflatePorts(portExprs []string) ([]Port, []error) { 60 | var ports []Port 61 | var errs []error 62 | 63 | for _, portExpr := range portExprs { 64 | if port, err := inflatePort(portExpr); err == nil { 65 | ports = append(ports, port) 66 | } else { 67 | errs = append(errs, err) 68 | } 69 | } 70 | 71 | return ports, errs 72 | } 73 | 74 | func validatePort(port Port) (errs []error) { 75 | if !validProtocol.MatchString(port.Protocol) { 76 | errs = append(errs, fmt.Errorf("invalid protocol %s (specify TCP, HTTP, or HTTPS)", port.Protocol)) 77 | } 78 | 79 | if port.Number < 1 || port.Number > 65535 { 80 | errs = append(errs, fmt.Errorf("invalid port %d (specify within 1 - 65535)", port.Number)) 81 | } 82 | 83 | return 84 | } 85 | -------------------------------------------------------------------------------- /cmd/port_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestEmpty(t *testing.T) { 8 | var tests = []struct { 9 | port Port 10 | empty bool 11 | }{ 12 | {Port{}, true}, 13 | {Port{80, ""}, true}, 14 | {Port{0, "HTTP"}, true}, 15 | {Port{80, "HTTP"}, false}, 16 | } 17 | 18 | for _, test := range tests { 19 | if test.port.Empty() != test.empty { 20 | t.Errorf("expected port %s empty == %t, got %t", test.port, test.empty, test.port.Empty()) 21 | } 22 | } 23 | } 24 | 25 | func TestString(t *testing.T) { 26 | var tests = []struct { 27 | port Port 28 | out string 29 | }{ 30 | {Port{}, ""}, 31 | {Port{80, ""}, ""}, 32 | {Port{0, "HTTP"}, ""}, 33 | {Port{80, "HTTP"}, "HTTP:80"}, 34 | {Port{25, "TCP"}, "TCP:25"}, 35 | } 36 | 37 | for _, test := range tests { 38 | if test.port.String() != test.out { 39 | t.Errorf("expected port %s == %s, got %s", test.port, test.out, test.port.String()) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /cmd/root_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "testing" 4 | 5 | var validateCpuAndMemoryTests = []struct { 6 | CpuUnits string 7 | Mebibytes string 8 | Out error 9 | }{ 10 | // 0.25 vCpu 11 | {"256", "512", nil}, 12 | {"256", "1024", nil}, 13 | {"256", "2048", nil}, 14 | {"256", "0", InvalidCpuAndMemoryCombination}, 15 | {"256", "768", InvalidCpuAndMemoryCombination}, 16 | {"256", "2151", InvalidCpuAndMemoryCombination}, 17 | {"256", "3072", InvalidCpuAndMemoryCombination}, 18 | 19 | // 0.5 vCpu 20 | {"512", "1024", nil}, 21 | {"512", "2048", nil}, 22 | {"512", "3072", nil}, 23 | {"512", "4096", nil}, 24 | {"512", "512", InvalidCpuAndMemoryCombination}, 25 | 26 | // 1 vCpu 27 | {"1024", "2048", nil}, 28 | {"1024", "5120", nil}, 29 | {"1024", "8192", nil}, 30 | {"1024", "1024", InvalidCpuAndMemoryCombination}, 31 | {"1024", "9216", InvalidCpuAndMemoryCombination}, 32 | 33 | // 2 vCpu 34 | {"2048", "4096", nil}, 35 | {"2048", "10240", nil}, 36 | {"2048", "16384", nil}, 37 | {"2048", "3072", InvalidCpuAndMemoryCombination}, 38 | {"2048", "17408", InvalidCpuAndMemoryCombination}, 39 | 40 | // 4 vCpu 41 | {"4096", "8192", nil}, 42 | {"4096", "15360", nil}, 43 | {"4096", "30720", nil}, 44 | {"4096", "1024", InvalidCpuAndMemoryCombination}, 45 | {"4096", "31744", InvalidCpuAndMemoryCombination}, 46 | } 47 | 48 | func TestValidateCpuAndMemoryWithValidParameters(t *testing.T) { 49 | err := validateCpuAndMemory("256", "512") 50 | 51 | if err != nil { 52 | t.Errorf("Validation failed, got %s, want nil", err) 53 | } 54 | } 55 | 56 | func TestValidateCpuAndMemoryWithInvalidParameters(t *testing.T) { 57 | err := validateCpuAndMemory("5", "23849") 58 | 59 | if err == nil { 60 | t.Errorf("Validation failed, got nil, want %s", InvalidCpuAndMemoryCombination) 61 | } 62 | } 63 | 64 | func TestValidateCpuAndMemory(t *testing.T) { 65 | for _, test := range validateCpuAndMemoryTests { 66 | s := validateCpuAndMemory(test.CpuUnits, test.Mebibytes) 67 | 68 | if s != test.Out { 69 | t.Errorf("validateCpuAndMemory(%s, %s) => %#v, want %s", test.CpuUnits, test.Mebibytes, s, test.Out) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /cmd/service.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | const serviceLogGroupFormat = "/fargate/service/%s" 8 | 9 | var serviceCmd = &cobra.Command{ 10 | Use: "service", 11 | Short: "Manage services", 12 | Long: `Manage services 13 | 14 | Services manage long-lived instances of your containers that are run on AWS 15 | Fargate. If your container exits for any reason, the service scheduler will 16 | restart your containers and ensure your service has the desired number of 17 | tasks running. Services can be used in concert with a load balancer to 18 | distribute traffic amongst the tasks in your service.`, 19 | } 20 | 21 | func init() { 22 | rootCmd.AddCommand(serviceCmd) 23 | } 24 | -------------------------------------------------------------------------------- /cmd/service_deploy.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/awslabs/fargatecli/console" 5 | "github.com/awslabs/fargatecli/docker" 6 | ECR "github.com/awslabs/fargatecli/ecr" 7 | ECS "github.com/awslabs/fargatecli/ecs" 8 | "github.com/awslabs/fargatecli/git" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | type ServiceDeployOperation struct { 13 | ServiceName string 14 | Image string 15 | } 16 | 17 | var flagServiceDeployImage string 18 | 19 | var serviceDeployCmd = &cobra.Command{ 20 | Use: "deploy ", 21 | Short: "Deploy new image to service", 22 | Long: `Deploy new image to service 23 | 24 | The Docker container image to use in the service can be optionally specified 25 | via the --image flag. If not specified, fargate will build a new Docker 26 | container image from the current working directory and push it to Amazon ECR in 27 | a repository named for the task group. If the current working directory is a 28 | git repository, the container image will be tagged with the short ref of the 29 | HEAD commit. If not, a timestamp in the format of YYYYMMDDHHMMSS will be used.`, 30 | Args: cobra.ExactArgs(1), 31 | Run: func(cmd *cobra.Command, args []string) { 32 | operation := &ServiceDeployOperation{ 33 | ServiceName: args[0], 34 | Image: flagServiceDeployImage, 35 | } 36 | 37 | deployService(operation) 38 | }, 39 | } 40 | 41 | func init() { 42 | serviceDeployCmd.Flags().StringVarP(&flagServiceDeployImage, "image", "i", "", "Docker image to run in the service; if omitted Fargate will build an image from the Dockerfile in the current directory") 43 | 44 | serviceCmd.AddCommand(serviceDeployCmd) 45 | } 46 | 47 | func deployService(operation *ServiceDeployOperation) { 48 | ecs := ECS.New(sess, clusterName) 49 | service := ecs.DescribeService(operation.ServiceName) 50 | 51 | if operation.Image == "" { 52 | var tag string 53 | 54 | ecr := ECR.New(sess) 55 | repositoryUri := ecr.GetRepositoryUri(operation.ServiceName) 56 | repository := docker.Repository{Uri: repositoryUri} 57 | username, password := ecr.GetUsernameAndPassword() 58 | 59 | if git.IsCwdGitRepo() { 60 | tag = git.GetShortSha() 61 | } else { 62 | tag = docker.GenerateTag() 63 | } 64 | 65 | repository.Login(username, password) 66 | repository.Build(tag) 67 | repository.Push(tag) 68 | 69 | operation.Image = repository.UriFor(tag) 70 | } 71 | 72 | taskDefinitionArn := ecs.UpdateTaskDefinitionImage(service.TaskDefinitionArn, operation.Image) 73 | ecs.UpdateServiceTaskDefinition(operation.ServiceName, taskDefinitionArn) 74 | console.Info("Deployed %s to service %s", operation.Image, operation.ServiceName) 75 | } 76 | -------------------------------------------------------------------------------- /cmd/service_destroy.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/awslabs/fargatecli/console" 7 | ECS "github.com/awslabs/fargatecli/ecs" 8 | ELBV2 "github.com/awslabs/fargatecli/elbv2" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | type ServiceDestroyOperation struct { 13 | ServiceName string 14 | } 15 | 16 | var serviceDestroyCmd = &cobra.Command{ 17 | Use: "destroy ", 18 | Short: "Destroy a service", 19 | Long: `Destroy service 20 | 21 | In order to destroy a service, it must first be scaled to 0 running tasks.`, 22 | Args: cobra.ExactArgs(1), 23 | Run: func(cmd *cobra.Command, args []string) { 24 | operation := &ServiceDestroyOperation{ 25 | ServiceName: args[0], 26 | } 27 | 28 | destroyService(operation) 29 | }, 30 | } 31 | 32 | func init() { 33 | serviceCmd.AddCommand(serviceDestroyCmd) 34 | } 35 | 36 | func destroyService(operation *ServiceDestroyOperation) { 37 | elbv2 := ELBV2.New(sess) 38 | ecs := ECS.New(sess, clusterName) 39 | service := ecs.DescribeService(operation.ServiceName) 40 | 41 | if service.DesiredCount > 0 { 42 | err := fmt.Errorf("%d tasks running, scale service to 0", service.DesiredCount) 43 | console.ErrorExit(err, "Cannot destroy service %s", operation.ServiceName) 44 | } 45 | 46 | if service.TargetGroupArn != "" { 47 | loadBalancerArn := elbv2.GetTargetGroupLoadBalancerArn(service.TargetGroupArn) 48 | loadBalancer := elbv2.DescribeLoadBalancerByARN(loadBalancerArn) 49 | listeners := elbv2.GetListeners(loadBalancerArn) 50 | 51 | for _, listener := range listeners { 52 | for _, rule := range elbv2.DescribeRules(listener.ARN) { 53 | if rule.TargetGroupARN == service.TargetGroupArn { 54 | if rule.IsDefault { 55 | defaultTargetGroupName := fmt.Sprintf(defaultTargetGroupFormat, loadBalancer.Name) 56 | defaultTargetGroupArn := elbv2.GetTargetGroupArn(defaultTargetGroupName) 57 | 58 | if defaultTargetGroupArn == "" { 59 | defaultTargetGroupArn, _ = elbv2.CreateTargetGroup( 60 | ELBV2.CreateTargetGroupParameters{ 61 | Name: defaultTargetGroupName, 62 | Port: listeners[0].Port, 63 | Protocol: listeners[0].Protocol, 64 | VPCID: loadBalancer.VPCID, 65 | }, 66 | ) 67 | } 68 | 69 | elbv2.ModifyListenerDefaultAction(listener.ARN, defaultTargetGroupArn) 70 | } else { 71 | elbv2.DeleteRule(rule.ARN) 72 | } 73 | } 74 | } 75 | } 76 | 77 | elbv2.DeleteTargetGroupByArn(service.TargetGroupArn) 78 | } 79 | 80 | ecs.DestroyService(operation.ServiceName) 81 | console.Info("Destroyed service %s", operation.ServiceName) 82 | } 83 | -------------------------------------------------------------------------------- /cmd/service_env.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var serviceEnvCmd = &cobra.Command{ 8 | Use: "env", 9 | Short: "Manage environment variables", 10 | } 11 | 12 | func init() { 13 | serviceCmd.AddCommand(serviceEnvCmd) 14 | } 15 | -------------------------------------------------------------------------------- /cmd/service_env_list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | ECS "github.com/awslabs/fargatecli/ecs" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | type ServiceEnvListOperation struct { 10 | ServiceName string 11 | } 12 | 13 | var serviceEnvListCmd = &cobra.Command{ 14 | Use: "list ", 15 | Short: "Show environment variables", 16 | Args: cobra.ExactArgs(1), 17 | Run: func(cmd *cobra.Command, args []string) { 18 | operation := &ServiceEnvListOperation{ 19 | ServiceName: args[0], 20 | } 21 | 22 | serviceEnvList(operation) 23 | }, 24 | } 25 | 26 | func init() { 27 | serviceEnvCmd.AddCommand(serviceEnvListCmd) 28 | } 29 | 30 | func serviceEnvList(operation *ServiceEnvListOperation) { 31 | ecs := ECS.New(sess, clusterName) 32 | service := ecs.DescribeService(operation.ServiceName) 33 | envVars := ecs.GetEnvVarsFromTaskDefinition(service.TaskDefinitionArn) 34 | 35 | for _, envVar := range envVars { 36 | fmt.Printf("%s=%s\n", envVar.Key, envVar.Value) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cmd/service_env_set.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/awslabs/fargatecli/console" 5 | ECS "github.com/awslabs/fargatecli/ecs" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | type ServiceEnvSetOperation struct { 10 | ServiceName string 11 | EnvVars []ECS.EnvVar 12 | } 13 | 14 | func (o *ServiceEnvSetOperation) Validate() { 15 | if len(o.EnvVars) == 0 { 16 | console.IssueExit("No environment variables specified") 17 | } 18 | } 19 | 20 | func (o *ServiceEnvSetOperation) SetEnvVars(inputEnvVars []string) { 21 | o.EnvVars = extractEnvVars(inputEnvVars) 22 | } 23 | 24 | var flagServiceEnvSetEnvVars []string 25 | 26 | var serviceEnvSetCmd = &cobra.Command{ 27 | Use: "set --env [--env ] ...", 28 | Short: "Set environment variables", 29 | Long: `Set environment variables 30 | 31 | At least one environment variable must be specified via the --env flag. Specify 32 | --env with a key=value parameter multiple times to add multiple variables.`, 33 | Args: cobra.ExactArgs(1), 34 | Run: func(cmd *cobra.Command, args []string) { 35 | operation := &ServiceEnvSetOperation{ 36 | ServiceName: args[0], 37 | } 38 | 39 | operation.SetEnvVars(flagServiceEnvSetEnvVars) 40 | operation.Validate() 41 | serviceEnvSet(operation) 42 | }, 43 | } 44 | 45 | func init() { 46 | serviceEnvSetCmd.Flags().StringArrayVarP(&flagServiceEnvSetEnvVars, "env", "e", []string{}, "Environment variables to set [e.g. KEY=value]") 47 | 48 | serviceEnvCmd.AddCommand(serviceEnvSetCmd) 49 | } 50 | 51 | func serviceEnvSet(operation *ServiceEnvSetOperation) { 52 | ecs := ECS.New(sess, clusterName) 53 | service := ecs.DescribeService(operation.ServiceName) 54 | taskDefinitionArn := ecs.AddEnvVarsToTaskDefinition(service.TaskDefinitionArn, operation.EnvVars) 55 | 56 | ecs.UpdateServiceTaskDefinition(operation.ServiceName, taskDefinitionArn) 57 | 58 | console.Info("Set %s environment variables:", operation.ServiceName) 59 | 60 | for _, envVar := range operation.EnvVars { 61 | console.Info("- %s=%s", envVar.Key, envVar.Value) 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /cmd/service_env_unset.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/awslabs/fargatecli/console" 7 | ECS "github.com/awslabs/fargatecli/ecs" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | type ServiceEnvUnsetOperation struct { 12 | ServiceName string 13 | Keys []string 14 | } 15 | 16 | func (o *ServiceEnvUnsetOperation) Validate() { 17 | if len(o.Keys) == 0 { 18 | console.IssueExit("No keys specified") 19 | } 20 | } 21 | 22 | func (o *ServiceEnvUnsetOperation) SetKeys(keys []string) { 23 | o.Keys = Map(keys, strings.ToUpper) 24 | } 25 | 26 | var serviceEnvUnsetCmd = &cobra.Command{ 27 | Use: "unset --key [--key ] ...", 28 | Short: "Unset environment variables", 29 | Long: `Unset environment variables 30 | 31 | Unsets the environment variable specified via the --key flag. Specify --key with 32 | a key name multiple times to unset multiple variables.`, 33 | Args: cobra.ExactArgs(1), 34 | Run: func(cmd *cobra.Command, args []string) { 35 | operation := &ServiceEnvUnsetOperation{ 36 | ServiceName: args[0], 37 | } 38 | 39 | operation.SetKeys(flagServiceEnvUnsetKeys) 40 | operation.Validate() 41 | serviceEnvUnset(operation) 42 | }, 43 | } 44 | 45 | var flagServiceEnvUnsetKeys []string 46 | 47 | func init() { 48 | serviceEnvUnsetCmd.Flags().StringSliceVarP(&flagServiceEnvUnsetKeys, "key", "k", []string{}, "Environment variable keys to unset [e.g. KEY, NGINX_PORT]") 49 | 50 | serviceEnvCmd.AddCommand(serviceEnvUnsetCmd) 51 | } 52 | 53 | func serviceEnvUnset(operation *ServiceEnvUnsetOperation) { 54 | ecs := ECS.New(sess, clusterName) 55 | service := ecs.DescribeService(operation.ServiceName) 56 | taskDefinitionArn := ecs.RemoveEnvVarsFromTaskDefinition(service.TaskDefinitionArn, operation.Keys) 57 | 58 | ecs.UpdateServiceTaskDefinition(operation.ServiceName, taskDefinitionArn) 59 | 60 | console.Info("Unset %s environment variables:", operation.ServiceName) 61 | 62 | for _, key := range operation.Keys { 63 | console.Info("- %s", key) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /cmd/service_info.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sort" 7 | "strings" 8 | "text/tabwriter" 9 | 10 | ACM "github.com/awslabs/fargatecli/acm" 11 | "github.com/awslabs/fargatecli/console" 12 | EC2 "github.com/awslabs/fargatecli/ec2" 13 | ECS "github.com/awslabs/fargatecli/ecs" 14 | ELBV2 "github.com/awslabs/fargatecli/elbv2" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | const statusActive = "ACTIVE" 19 | 20 | type ServiceInfoOperation struct { 21 | ServiceName string 22 | } 23 | 24 | var serviceInfoCmd = &cobra.Command{ 25 | Use: "info ", 26 | Short: "Inspect service", 27 | Long: `Inspect service 28 | 29 | Show extended information for a service including load balancer configuration, 30 | active deployments, and environment variables. 31 | 32 | Deployments show active versions of your service that are running. Multiple 33 | deployments are shown if a service is transitioning due to a deployment or 34 | update to configuration such a CPU, memory, or environment variables.`, 35 | Args: cobra.ExactArgs(1), 36 | Run: func(cmd *cobra.Command, args []string) { 37 | operation := &ServiceInfoOperation{ 38 | ServiceName: args[0], 39 | } 40 | 41 | getServiceInfo(operation) 42 | }, 43 | } 44 | 45 | func init() { 46 | serviceCmd.AddCommand(serviceInfoCmd) 47 | } 48 | 49 | func getServiceInfo(operation *ServiceInfoOperation) { 50 | var eniIds []string 51 | 52 | acm := ACM.New(sess) 53 | ecs := ECS.New(sess, clusterName) 54 | ec2 := EC2.New(sess) 55 | elbv2 := ELBV2.New(sess) 56 | service := ecs.DescribeService(operation.ServiceName) 57 | tasks := ecs.DescribeTasksForService(operation.ServiceName) 58 | 59 | if service.Status != statusActive { 60 | console.InfoExit("Service not found") 61 | } 62 | 63 | console.KeyValue("Service Name", "%s\n", operation.ServiceName) 64 | console.KeyValue("Status", "\n") 65 | console.KeyValue(" Desired", "%d\n", service.DesiredCount) 66 | console.KeyValue(" Running", "%d\n", service.RunningCount) 67 | console.KeyValue(" Pending", "%d\n", service.PendingCount) 68 | console.KeyValue("Image", "%s\n", service.Image) 69 | console.KeyValue("Cpu", "%s\n", service.Cpu) 70 | console.KeyValue("Memory", "%s\n", service.Memory) 71 | 72 | if service.TaskRole != "" { 73 | console.KeyValue("Task Role", "%s\n", service.TaskRole) 74 | } 75 | 76 | console.KeyValue("Subnets", "%s\n", strings.Join(service.SubnetIds, ", ")) 77 | console.KeyValue("Security Groups", "%s\n", strings.Join(service.SecurityGroupIds, ", ")) 78 | 79 | if service.TargetGroupArn != "" { 80 | if loadBalancerArn := elbv2.GetTargetGroupLoadBalancerArn(service.TargetGroupArn); loadBalancerArn != "" { 81 | loadBalancer := elbv2.DescribeLoadBalancerByARN(loadBalancerArn) 82 | listeners := elbv2.GetListeners(loadBalancerArn) 83 | 84 | console.KeyValue("Load Balancer", "\n") 85 | console.KeyValue(" Name", "%s\n", loadBalancer.Name) 86 | console.KeyValue(" DNS Name", "%s\n", loadBalancer.DNSName) 87 | 88 | if len(listeners) > 0 { 89 | console.KeyValue(" Ports", "\n") 90 | } 91 | 92 | for _, listener := range listeners { 93 | var ruleOutput []string 94 | 95 | rules := elbv2.DescribeRules(listener.ARN) 96 | 97 | sort.Slice(rules, func(i, j int) bool { return rules[i].Priority > rules[j].Priority }) 98 | 99 | for _, rule := range rules { 100 | if rule.TargetGroupARN == service.TargetGroupArn { 101 | ruleOutput = append(ruleOutput, rule.String()) 102 | } 103 | } 104 | 105 | console.KeyValue(" "+listener.String(), "\n") 106 | console.KeyValue(" Rules", "%s\n", strings.Join(ruleOutput, ", ")) 107 | 108 | if len(listener.CertificateARNs) > 0 { 109 | certificateDomains := acm.ListCertificateDomainNames(listener.CertificateARNs) 110 | console.KeyValue(" Certificates", "%s\n", strings.Join(certificateDomains, ", ")) 111 | } 112 | } 113 | } 114 | 115 | if len(service.EnvVars) > 0 { 116 | console.KeyValue("Environment Variables", "\n") 117 | 118 | for _, envVar := range service.EnvVars { 119 | fmt.Printf(" %s=%s\n", envVar.Key, envVar.Value) 120 | } 121 | } 122 | } 123 | 124 | if len(tasks) > 0 { 125 | console.Header("Tasks") 126 | 127 | for _, task := range tasks { 128 | if task.EniId != "" { 129 | eniIds = append(eniIds, task.EniId) 130 | } 131 | } 132 | 133 | enis := ec2.DescribeNetworkInterfaces(eniIds) 134 | w := new(tabwriter.Writer) 135 | 136 | w.Init(os.Stdout, 0, 8, 1, '\t', 0) 137 | fmt.Fprintln(w, "ID\tIMAGE\tSTATUS\tRUNNING\tIP\tCPU\tMEMORY\tDEPLOYMENT\t") 138 | 139 | for _, t := range tasks { 140 | fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", 141 | t.TaskId, 142 | t.Image, 143 | Humanize(t.LastStatus), 144 | t.RunningFor(), 145 | enis[t.EniId].PublicIpAddress, 146 | t.Cpu, 147 | t.Memory, 148 | t.DeploymentId, 149 | ) 150 | } 151 | 152 | w.Flush() 153 | } 154 | 155 | if len(service.Deployments) > 0 { 156 | console.Header("Deployments") 157 | 158 | w := new(tabwriter.Writer) 159 | w.Init(os.Stdout, 0, 8, 1, '\t', 0) 160 | fmt.Fprintln(w, "ID\tIMAGE\tSTATUS\tCREATED\tDESIRED\tRUNNING\tPENDING") 161 | 162 | for _, d := range service.Deployments { 163 | fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\t%d\t%d\n", 164 | d.Id, 165 | d.Image, 166 | Humanize(d.Status), 167 | d.CreatedAt, 168 | d.DesiredCount, 169 | d.RunningCount, 170 | d.PendingCount, 171 | ) 172 | } 173 | 174 | w.Flush() 175 | } 176 | 177 | if len(service.Events) > 0 { 178 | console.Header("Events") 179 | 180 | for i, event := range service.Events { 181 | fmt.Printf("[%s] %s\n", event.CreatedAt, event.Message) 182 | 183 | if i == 10 && !verbose { 184 | break 185 | } 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /cmd/service_list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "text/tabwriter" 7 | 8 | "github.com/awslabs/fargatecli/console" 9 | ECS "github.com/awslabs/fargatecli/ecs" 10 | ELBV2 "github.com/awslabs/fargatecli/elbv2" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var serviceListCmd = &cobra.Command{ 15 | Use: "list", 16 | Short: "List services", 17 | Run: func(cmd *cobra.Command, args []string) { 18 | listServices() 19 | }, 20 | } 21 | 22 | func init() { 23 | serviceCmd.AddCommand(serviceListCmd) 24 | } 25 | 26 | func listServices() { 27 | var targetGroupArns []string 28 | var loadBalancerArns []string 29 | 30 | targetGroups := make(map[string]ELBV2.TargetGroup) 31 | loadBalancers := make(map[string]ELBV2.LoadBalancer) 32 | 33 | ecs := ECS.New(sess, clusterName) 34 | elbv2 := ELBV2.New(sess) 35 | services := ecs.ListServices() 36 | 37 | for _, service := range services { 38 | if service.TargetGroupArn != "" { 39 | targetGroupArns = append(targetGroupArns, service.TargetGroupArn) 40 | } 41 | } 42 | 43 | if len(targetGroupArns) > 0 { 44 | for _, targetGroup := range elbv2.DescribeTargetGroups(targetGroupArns) { 45 | targetGroups[targetGroup.Arn] = targetGroup 46 | 47 | if targetGroup.LoadBalancerARN != "" { 48 | loadBalancerArns = append(loadBalancerArns, targetGroup.LoadBalancerARN) 49 | } 50 | } 51 | } 52 | 53 | if len(loadBalancerArns) > 0 { 54 | lbs, _ := elbv2.DescribeLoadBalancersByARN(loadBalancerArns) 55 | for _, loadBalancer := range lbs { 56 | loadBalancers[loadBalancer.ARN] = loadBalancer 57 | } 58 | } 59 | 60 | if len(services) > 0 { 61 | w := new(tabwriter.Writer) 62 | w.Init(os.Stdout, 0, 8, 1, '\t', 0) 63 | fmt.Fprintln(w, "NAME\tIMAGE\tCPU\tMEMORY\tLOAD BALANCER\tDESIRED\tRUNNING\tPENDING\t") 64 | 65 | for _, service := range services { 66 | var loadBalancer string 67 | 68 | if service.TargetGroupArn != "" { 69 | tg := targetGroups[service.TargetGroupArn] 70 | lb := loadBalancers[tg.LoadBalancerARN] 71 | 72 | loadBalancer = lb.Name 73 | } 74 | 75 | fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%d\t%d\t%d\t\n", 76 | service.Name, 77 | service.Image, 78 | service.Cpu, 79 | service.Memory, 80 | loadBalancer, 81 | service.DesiredCount, 82 | service.RunningCount, 83 | service.PendingCount, 84 | ) 85 | } 86 | 87 | w.Flush() 88 | } else { 89 | console.Info("No services found") 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /cmd/service_logs.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var ( 10 | flagServiceLogsFilter string 11 | flagServiceLogsEndTime string 12 | flagServiceLogsStartTime string 13 | flagServiceLogsFollow bool 14 | flagServiceLogsTasks []string 15 | ) 16 | 17 | var serviceLogsCmd = &cobra.Command{ 18 | Use: "logs ", 19 | Short: "Show logs from tasks in a service", 20 | Long: `Show logs from tasks in a service 21 | 22 | Return either a specific segment of service logs or tail logs in real-time 23 | using the --follow option. Logs are prefixed by their log stream name which is 24 | in the format of "fargate/\/\." 25 | 26 | Follow will continue to run and return logs until interrupted by Control-C. If 27 | --follow is passed --end cannot be specified. 28 | 29 | Logs can be returned for specific tasks within a service by passing a task ID 30 | via the --task flag. Pass --task with a task ID multiple times in order to 31 | retrieve logs from multiple specific tasks. 32 | 33 | A specific window of logs can be requested by passing --start and --end options 34 | with a time expression. The time expression can be either a duration or a 35 | timestamp: 36 | 37 | - Duration (e.g. -1h [one hour ago], -1h10m30s [one hour, ten minutes, and 38 | thirty seconds ago], 2h [two hours from now]) 39 | - Timestamp with optional timezone in the format of YYYY-MM-DD HH:MM:SS [TZ]; 40 | timezone will default to UTC if omitted (e.g. 2017-12-22 15:10:03 EST) 41 | 42 | You can filter logs for specific term by passing a filter expression via the 43 | --filter flag. Pass a single term to search for that term, pass multiple terms 44 | to search for log messages that include all terms.`, 45 | Args: cobra.ExactArgs(1), 46 | PreRun: func(cmd *cobra.Command, args []string) { 47 | }, 48 | Run: func(cmd *cobra.Command, args []string) { 49 | operation := &GetLogsOperation{ 50 | LogGroupName: fmt.Sprintf(serviceLogGroupFormat, args[0]), 51 | Filter: flagServiceLogsFilter, 52 | Follow: flagServiceLogsFollow, 53 | Namespace: args[0], 54 | } 55 | 56 | operation.AddTasks(flagServiceLogsTasks) 57 | operation.AddStartTime(flagServiceLogsStartTime) 58 | operation.AddEndTime(flagServiceLogsEndTime) 59 | 60 | GetLogs(operation) 61 | }, 62 | } 63 | 64 | func init() { 65 | serviceCmd.AddCommand(serviceLogsCmd) 66 | 67 | serviceLogsCmd.Flags().BoolVarP(&flagServiceLogsFollow, "follow", "f", false, "Poll logs and continuously print new events") 68 | serviceLogsCmd.Flags().StringVar(&flagServiceLogsFilter, "filter", "", "Filter pattern to apply") 69 | serviceLogsCmd.Flags().StringVar(&flagServiceLogsStartTime, "start", "", "Earliest time to return logs (e.g. -1h, 2018-01-01 09:36:00 EST") 70 | serviceLogsCmd.Flags().StringVar(&flagServiceLogsEndTime, "end", "", "Latest time to return logs (e.g. 3y, 2021-01-20 12:00:00 EST") 71 | serviceLogsCmd.Flags().StringSliceVarP(&flagServiceLogsTasks, "task", "t", []string{}, "Show logs from specific task (can be specified multiple times)") 72 | } 73 | -------------------------------------------------------------------------------- /cmd/service_ps.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "text/tabwriter" 7 | 8 | "github.com/awslabs/fargatecli/console" 9 | EC2 "github.com/awslabs/fargatecli/ec2" 10 | ECS "github.com/awslabs/fargatecli/ecs" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | type ServiceProcessListOperation struct { 15 | ServiceName string 16 | } 17 | 18 | var servicePsCmd = &cobra.Command{ 19 | Use: "ps ", 20 | Short: "List running tasks for a service", 21 | Args: cobra.ExactArgs(1), 22 | Run: func(cmd *cobra.Command, args []string) { 23 | operation := &ServiceProcessListOperation{ 24 | ServiceName: args[0], 25 | } 26 | 27 | getServiceProcessList(operation) 28 | }, 29 | } 30 | 31 | func init() { 32 | serviceCmd.AddCommand(servicePsCmd) 33 | } 34 | 35 | func getServiceProcessList(operation *ServiceProcessListOperation) { 36 | var eniIds []string 37 | 38 | ecs := ECS.New(sess, clusterName) 39 | ec2 := EC2.New(sess) 40 | tasks := ecs.DescribeTasksForService(operation.ServiceName) 41 | 42 | for _, task := range tasks { 43 | if task.EniId != "" { 44 | eniIds = append(eniIds, task.EniId) 45 | } 46 | } 47 | 48 | if len(tasks) > 0 { 49 | enis := ec2.DescribeNetworkInterfaces(eniIds) 50 | 51 | w := new(tabwriter.Writer) 52 | w.Init(os.Stdout, 0, 8, 1, '\t', 0) 53 | fmt.Fprintln(w, "ID\tIMAGE\tSTATUS\tRUNNING\tIP\tCPU\tMEMORY\t") 54 | 55 | for _, t := range tasks { 56 | fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", 57 | t.TaskId, 58 | t.Image, 59 | Humanize(t.LastStatus), 60 | t.RunningFor(), 61 | enis[t.EniId].PublicIpAddress, 62 | t.Cpu, 63 | t.Memory, 64 | ) 65 | } 66 | 67 | w.Flush() 68 | } else { 69 | console.Info("No tasks found") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cmd/service_restart.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/awslabs/fargatecli/console" 5 | ECS "github.com/awslabs/fargatecli/ecs" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | type ServiceRestartOperation struct { 10 | ServiceName string 11 | } 12 | 13 | var serviceRestartCmd = &cobra.Command{ 14 | Use: "restart ", 15 | Short: "Restart service", 16 | Long: `Restart service 17 | 18 | Creates a new set of tasks for the service and stops the previous tasks. This 19 | is useful if your service needs to reload data cached from an external source, 20 | for example.`, 21 | Args: cobra.ExactArgs(1), 22 | Run: func(cmd *cobra.Command, args []string) { 23 | operation := &ServiceRestartOperation{ 24 | ServiceName: args[0], 25 | } 26 | 27 | restartService(operation) 28 | }, 29 | } 30 | 31 | func init() { 32 | serviceCmd.AddCommand(serviceRestartCmd) 33 | } 34 | 35 | func restartService(operation *ServiceRestartOperation) { 36 | ecs := ECS.New(sess, clusterName) 37 | 38 | ecs.RestartService(operation.ServiceName) 39 | console.Info("Restarted %s", operation.ServiceName) 40 | } 41 | -------------------------------------------------------------------------------- /cmd/service_scale.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | 8 | "github.com/awslabs/fargatecli/console" 9 | ECS "github.com/awslabs/fargatecli/ecs" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | const validScalePattern = "[-\\+]?[0-9]+" 14 | 15 | type ScaleServiceOperation struct { 16 | ServiceName string 17 | DesiredCount int64 18 | } 19 | 20 | func (o *ScaleServiceOperation) SetScale(scaleExpression string) { 21 | ecs := ECS.New(sess, clusterName) 22 | validScale := regexp.MustCompile(validScalePattern) 23 | 24 | if !validScale.MatchString(scaleExpression) { 25 | console.ErrorExit(fmt.Errorf("Invalid scale expression %s", scaleExpression), "Invalid command line argument") 26 | } 27 | 28 | if scaleExpression[0] == '+' || scaleExpression[0] == '-' { 29 | if s, err := strconv.ParseInt(scaleExpression[1:len(scaleExpression)], 10, 64); err == nil { 30 | currentDesiredCount := ecs.GetDesiredCount(o.ServiceName) 31 | if scaleExpression[0] == '+' { 32 | o.DesiredCount = currentDesiredCount + s 33 | } else if scaleExpression[0] == '-' { 34 | o.DesiredCount = currentDesiredCount - s 35 | } 36 | } 37 | } else if s, err := strconv.ParseInt(scaleExpression, 10, 64); err == nil { 38 | o.DesiredCount = s 39 | } else { 40 | console.ErrorExit(fmt.Errorf("Invalid scale expression %s", scaleExpression), "Invalid command line argument") 41 | } 42 | 43 | if o.DesiredCount < 0 { 44 | console.ErrorExit(fmt.Errorf("requested scale %d < 0", o.DesiredCount), "Invalid command line argument") 45 | } 46 | } 47 | 48 | var serviceScaleCmd = &cobra.Command{ 49 | Use: "scale ", 50 | Short: "Changes the number of tasks running for the service", 51 | Long: `Scale number of tasks in a service 52 | 53 | Changes the number of desired tasks to be run in a service by the given scale 54 | expression. A scale expression can either be an absolute number or a delta 55 | specified with a sign such as +5 or -2.`, 56 | Args: cobra.ExactArgs(2), 57 | Run: func(cmd *cobra.Command, args []string) { 58 | operation := &ScaleServiceOperation{ 59 | ServiceName: args[0], 60 | } 61 | 62 | operation.SetScale(args[1]) 63 | 64 | scaleService(operation) 65 | }, 66 | } 67 | 68 | func init() { 69 | serviceCmd.AddCommand(serviceScaleCmd) 70 | } 71 | 72 | func scaleService(operation *ScaleServiceOperation) { 73 | ecs := ECS.New(sess, clusterName) 74 | 75 | ecs.SetDesiredCount(operation.ServiceName, operation.DesiredCount) 76 | console.Info("Scaled service %s to %d", operation.ServiceName, operation.DesiredCount) 77 | } 78 | -------------------------------------------------------------------------------- /cmd/service_update.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/awslabs/fargatecli/console" 7 | ECS "github.com/awslabs/fargatecli/ecs" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | type ServiceUpdateOperation struct { 12 | ServiceName string 13 | Cpu string 14 | Memory string 15 | Service ECS.Service 16 | } 17 | 18 | func (o *ServiceUpdateOperation) Validate() { 19 | ecs := ECS.New(sess, clusterName) 20 | 21 | if o.Cpu == "" && o.Memory == "" { 22 | console.ErrorExit(fmt.Errorf("--cpu and/or --memory must be supplied"), "Invalid command line arguments") 23 | } 24 | 25 | o.Service = ecs.DescribeService(o.ServiceName) 26 | cpu, memory := ecs.GetCpuAndMemoryFromTaskDefinition(o.Service.TaskDefinitionArn) 27 | 28 | if o.Cpu == "" { 29 | o.Cpu = cpu 30 | } 31 | 32 | if o.Memory == "" { 33 | o.Memory = memory 34 | } 35 | 36 | err := validateCpuAndMemory(o.Cpu, o.Memory) 37 | 38 | if err != nil { 39 | console.ErrorExit(err, "Invalid settings: %s CPU units / %s MiB", o.Cpu, o.Memory) 40 | } 41 | } 42 | 43 | var ( 44 | flagServiceUpdateCpu string 45 | flagServiceUpdateMemory string 46 | ) 47 | 48 | var serviceUpdateCmd = &cobra.Command{ 49 | Use: "update --cpu | --memory ", 50 | Short: "Update service configuration", 51 | Long: `Update service configuration 52 | 53 | CPU and memory settings are specified as CPU units and mebibytes respectively 54 | using the --cpu and --memory flags. Every 1024 CPU units is equivilent to a 55 | single vCPU. AWS Fargate only supports certain combinations of CPU and memory 56 | configurations: 57 | 58 | | CPU (CPU Units) | Memory (MiB) | 59 | | --------------- | ------------------------------------- | 60 | | 256 | 512, 1024, or 2048 | 61 | | 512 | 1024 through 4096 in 1GiB increments | 62 | | 1024 | 2048 through 8192 in 1GiB increments | 63 | | 2048 | 4096 through 16384 in 1GiB increments | 64 | | 4096 | 8192 through 30720 in 1GiB increments | 65 | 66 | At least one of --cpu or --memory must be specified.`, 67 | Args: cobra.ExactArgs(1), 68 | Run: func(cmd *cobra.Command, args []string) { 69 | operation := &ServiceUpdateOperation{ 70 | ServiceName: args[0], 71 | Cpu: flagServiceUpdateCpu, 72 | Memory: flagServiceUpdateMemory, 73 | } 74 | 75 | operation.Validate() 76 | 77 | updateService(operation) 78 | }, 79 | } 80 | 81 | func init() { 82 | serviceCmd.AddCommand(serviceUpdateCmd) 83 | 84 | serviceUpdateCmd.Flags().StringVarP(&flagServiceUpdateCpu, "cpu", "c", "", "Amount of cpu units to allocate for each task") 85 | serviceUpdateCmd.Flags().StringVarP(&flagServiceUpdateMemory, "memory", "m", "", "Amount of MiB to allocate for each task") 86 | } 87 | 88 | func updateService(operation *ServiceUpdateOperation) { 89 | ecs := ECS.New(sess, clusterName) 90 | 91 | newTaskDefinitionArn := ecs.UpdateTaskDefinitionCpuAndMemory( 92 | operation.Service.TaskDefinitionArn, 93 | operation.Cpu, 94 | operation.Memory, 95 | ) 96 | 97 | ecs.UpdateServiceTaskDefinition(operation.ServiceName, newTaskDefinitionArn) 98 | console.Info("Updated service %s to %s CPU units / %s MiB", operation.ServiceName, operation.Cpu, operation.Memory) 99 | } 100 | -------------------------------------------------------------------------------- /cmd/string_util.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "strings" 4 | 5 | // Humanize takes strings intended for machines and prettifies them for humans. 6 | func Humanize(s string) string { 7 | s = strings.Replace(s, "_", " ", -1) 8 | s = strings.ToLower(s) 9 | 10 | return s 11 | } 12 | 13 | // Titleize humanizes a string and returns it in Title Case. 14 | func Titleize(s string) string { 15 | s = Humanize(s) 16 | s = strings.Title(s) 17 | 18 | return s 19 | } 20 | 21 | // Map applies a func to all members of a slice of strings and returns a new slice of the results. 22 | func Map(vs []string, f func(string) string) []string { 23 | vsm := make([]string, len(vs)) 24 | 25 | for i, v := range vs { 26 | vsm[i] = f(v) 27 | } 28 | 29 | return vsm 30 | } 31 | -------------------------------------------------------------------------------- /cmd/string_util_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "fmt" 4 | 5 | func ExampleHumanize() { 6 | fmt.Println(Humanize("HELLO_COMPUTER")) 7 | // Output: hello computer 8 | } 9 | 10 | func ExampleTitleize() { 11 | fmt.Println(Titleize("HELLO_COMPUTER")) 12 | // Output: Hello Computer 13 | } 14 | 15 | func ExampleMap() { 16 | reverse := func(s string) string { 17 | r := []rune(s) 18 | for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 { 19 | r[i], r[j] = r[j], r[i] 20 | } 21 | return string(r) 22 | } 23 | 24 | fmt.Printf("%v", Map([]string{"Pippin", "Merry"}, reverse)) 25 | // Output: [nippiP yrreM] 26 | } 27 | -------------------------------------------------------------------------------- /cmd/task.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | const taskLogGroupFormat = "/fargate/task/%s" 8 | 9 | var taskCmd = &cobra.Command{ 10 | Use: "task", 11 | Short: "Manage tasks", 12 | Long: `Manage tasks 13 | 14 | Tasks are one-time executions of your container. Instances of your task are run 15 | until you manually stop them either through AWS APIs, the AWS Management 16 | Console, or fargate task stop, or until they are interrupted for any reason.`, 17 | } 18 | 19 | func init() { 20 | rootCmd.AddCommand(taskCmd) 21 | } 22 | -------------------------------------------------------------------------------- /cmd/task_info.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/awslabs/fargatecli/console" 8 | EC2 "github.com/awslabs/fargatecli/ec2" 9 | ECS "github.com/awslabs/fargatecli/ecs" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | type TaskInfoOperation struct { 14 | TaskGroupName string 15 | TaskIds []string 16 | } 17 | 18 | var flagTaskInfoTasks []string 19 | 20 | var taskInfoCmd = &cobra.Command{ 21 | Use: "info ", 22 | Short: "Inspect tasks", 23 | Long: `Inspect tasks 24 | 25 | Shows extended information for each running task within a task group or for 26 | specific tasks specified with the --task flag. Information includes environment 27 | variables which could differ between tasks in a task group. To inspect multiple 28 | specific tasks within a task group specific --task with a task ID multiple 29 | times.`, 30 | Args: cobra.ExactArgs(1), 31 | Run: func(cmd *cobra.Command, args []string) { 32 | operation := &TaskInfoOperation{ 33 | TaskGroupName: args[0], 34 | TaskIds: flagTaskInfoTasks, 35 | } 36 | 37 | getTaskInfo(operation) 38 | }, 39 | } 40 | 41 | func init() { 42 | taskCmd.AddCommand(taskInfoCmd) 43 | 44 | taskInfoCmd.Flags().StringSliceVarP(&flagTaskInfoTasks, "task", "t", []string{}, "Get info for specific task instances (can be specified multiple times)") 45 | } 46 | 47 | func getTaskInfo(operation *TaskInfoOperation) { 48 | var tasks []ECS.Task 49 | var eniIds []string 50 | 51 | ecs := ECS.New(sess, clusterName) 52 | ec2 := EC2.New(sess) 53 | 54 | if len(operation.TaskIds) > 0 { 55 | tasks = ecs.DescribeTasks(operation.TaskIds) 56 | } else { 57 | tasks = ecs.DescribeTasksForTaskGroup(operation.TaskGroupName) 58 | } 59 | 60 | if len(tasks) == 0 { 61 | console.InfoExit("No tasks found") 62 | } 63 | 64 | for _, task := range tasks { 65 | if task.EniId != "" { 66 | eniIds = append(eniIds, task.EniId) 67 | } 68 | } 69 | 70 | enis := ec2.DescribeNetworkInterfaces(eniIds) 71 | 72 | console.KeyValue("Task Group Name", "%s\n", operation.TaskGroupName) 73 | console.KeyValue("Task Instances", "%d\n", len(tasks)) 74 | 75 | for _, task := range tasks { 76 | eni := enis[task.EniId] 77 | 78 | console.KeyValue(" "+task.TaskId, "\n") 79 | console.KeyValue(" Image", "%s\n", task.Image) 80 | console.KeyValue(" Status", "%s\n", Humanize(task.LastStatus)) 81 | console.KeyValue(" Started At", "%s\n", task.CreatedAt) 82 | console.KeyValue(" IP", "%s\n", eni.PublicIpAddress) 83 | console.KeyValue(" CPU", "%s\n", task.Cpu) 84 | console.KeyValue(" Memory", "%s\n", task.Memory) 85 | 86 | if task.TaskRole != "" { 87 | console.KeyValue(" Task Role", "%s\n", task.TaskRole) 88 | } 89 | 90 | console.KeyValue(" Subnet", "%s\n", task.SubnetId) 91 | console.KeyValue(" Security Groups", "%s\n", strings.Join(eni.SecurityGroupIds, ", ")) 92 | 93 | if len(task.EnvVars) > 0 { 94 | console.KeyValue(" Environment Variables", "\n") 95 | 96 | for _, envVar := range task.EnvVars { 97 | fmt.Printf(" %s=%s\n", envVar.Key, envVar.Value) 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /cmd/task_list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "text/tabwriter" 7 | 8 | "github.com/awslabs/fargatecli/console" 9 | ECS "github.com/awslabs/fargatecli/ecs" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var taskListCmd = &cobra.Command{ 14 | Use: "list", 15 | Short: "List running task groups", 16 | Run: func(cmd *cobra.Command, args []string) { 17 | listTaskGroups() 18 | }, 19 | } 20 | 21 | func init() { 22 | taskCmd.AddCommand(taskListCmd) 23 | } 24 | 25 | func listTaskGroups() { 26 | ecs := ECS.New(sess, clusterName) 27 | taskGroups := ecs.ListTaskGroups() 28 | 29 | if len(taskGroups) == 0 { 30 | console.InfoExit("No tasks running") 31 | } 32 | 33 | w := new(tabwriter.Writer) 34 | w.Init(os.Stdout, 0, 8, 1, '\t', 0) 35 | fmt.Fprintln(w, "NAME\tINSTANCES") 36 | 37 | for _, taskGroup := range taskGroups { 38 | fmt.Fprintf(w, "%s\t%d\n", 39 | taskGroup.TaskGroupName, 40 | taskGroup.Instances, 41 | ) 42 | } 43 | 44 | w.Flush() 45 | } 46 | -------------------------------------------------------------------------------- /cmd/task_logs.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var ( 10 | flagTaskLogsFilter string 11 | flagTaskLogsEndTime string 12 | flagTaskLogsStartTime string 13 | flagTaskLogsFollow bool 14 | flagTaskLogsTasks []string 15 | ) 16 | 17 | var taskLogsCmd = &cobra.Command{ 18 | Use: "logs ", 19 | Short: "Show logs from tasks", 20 | Long: `Show logs from tasks 21 | 22 | Return either a specific segment of task logs or tail logs in real-time using 23 | the --follow option. Logs are prefixed by their log stream name which is in the 24 | format of "fargate//." 25 | 26 | Follow will continue to run and return logs until interrupted by Control-C. If 27 | --follow is passed --end cannot be specified. 28 | 29 | Logs can be returned for specific tasks within a task group by passing a task 30 | ID via the --task flag. Pass --task with a task ID multiple times in order to 31 | retrieve logs from multiple specific tasks. 32 | 33 | A specific window of logs can be requested by passing --start and --end options 34 | with a time expression. The time expression can be either a duration or a 35 | timestamp: 36 | 37 | - Duration (e.g. -1h [one hour ago], -1h10m30s [one hour, ten minutes, and 38 | thirty seconds ago], 2h [two hours from now]) 39 | - Timestamp with optional timezone in the format of YYYY-MM-DD HH:MM:SS [TZ]; 40 | timezone will default to UTC if omitted (e.g. 2017-12-22 15:10:03 EST) 41 | 42 | You can filter logs for specific term by passing a filter expression via the 43 | --filter flag. Pass a single term to search for that term, pass multiple terms 44 | to search for log messages that include all terms.`, 45 | Args: cobra.ExactArgs(1), 46 | Run: func(cmd *cobra.Command, args []string) { 47 | operation := &GetLogsOperation{ 48 | LogGroupName: fmt.Sprintf(taskLogGroupFormat, args[0]), 49 | Filter: flagTaskLogsFilter, 50 | Follow: flagTaskLogsFollow, 51 | Namespace: args[0], 52 | } 53 | 54 | operation.AddTasks(flagTaskLogsTasks) 55 | operation.AddStartTime(flagTaskLogsStartTime) 56 | operation.AddEndTime(flagTaskLogsEndTime) 57 | 58 | GetLogs(operation) 59 | }, 60 | } 61 | 62 | func init() { 63 | taskCmd.AddCommand(taskLogsCmd) 64 | 65 | taskLogsCmd.Flags().BoolVarP(&flagTaskLogsFollow, "follow", "f", false, "Poll logs and continuously print new events") 66 | taskLogsCmd.Flags().StringVar(&flagTaskLogsFilter, "filter", "", "Filter pattern to apply") 67 | taskLogsCmd.Flags().StringVar(&flagTaskLogsStartTime, "start", "", "Earliest time to return logs (e.g. -1h, 2018-01-01 09:36:00 EST") 68 | taskLogsCmd.Flags().StringVar(&flagTaskLogsEndTime, "end", "", "Latest time to return logs (e.g. 3y, 2021-01-20 12:00:00 EST") 69 | taskLogsCmd.Flags().StringSliceVarP(&flagTaskLogsTasks, "task", "t", []string{}, "Show logs from specific task (can be specified multiple times)") 70 | } 71 | -------------------------------------------------------------------------------- /cmd/task_ps.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "text/tabwriter" 7 | 8 | "github.com/awslabs/fargatecli/console" 9 | EC2 "github.com/awslabs/fargatecli/ec2" 10 | ECS "github.com/awslabs/fargatecli/ecs" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | type TaskProcessListOperation struct { 15 | TaskName string 16 | } 17 | 18 | var taskPsCmd = &cobra.Command{ 19 | Use: "ps ", 20 | Short: "List running tasks", 21 | Args: cobra.ExactArgs(1), 22 | Run: func(cmd *cobra.Command, args []string) { 23 | operation := &TaskProcessListOperation{ 24 | TaskName: args[0], 25 | } 26 | 27 | getTaskProcessList(operation) 28 | }, 29 | } 30 | 31 | func init() { 32 | taskCmd.AddCommand(taskPsCmd) 33 | } 34 | 35 | func getTaskProcessList(operation *TaskProcessListOperation) { 36 | var eniIds []string 37 | 38 | ecs := ECS.New(sess, clusterName) 39 | ec2 := EC2.New(sess) 40 | tasks := ecs.DescribeTasksForTaskGroup(operation.TaskName) 41 | 42 | for _, task := range tasks { 43 | if task.EniId != "" { 44 | eniIds = append(eniIds, task.EniId) 45 | } 46 | } 47 | 48 | if len(tasks) == 0 { 49 | console.InfoExit("No tasks found") 50 | } 51 | 52 | enis := ec2.DescribeNetworkInterfaces(eniIds) 53 | 54 | w := new(tabwriter.Writer) 55 | w.Init(os.Stdout, 0, 8, 1, '\t', 0) 56 | fmt.Fprintln(w, "ID\tIMAGE\tSTATUS\tRUNNING\tIP\tCPU\tMEMORY\t") 57 | 58 | for _, t := range tasks { 59 | fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", 60 | t.TaskId, 61 | t.Image, 62 | Humanize(t.LastStatus), 63 | t.RunningFor(), 64 | enis[t.EniId].PublicIpAddress, 65 | t.Cpu, 66 | t.Memory, 67 | ) 68 | } 69 | 70 | w.Flush() 71 | } 72 | -------------------------------------------------------------------------------- /cmd/task_stop.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/awslabs/fargatecli/console" 5 | ECS "github.com/awslabs/fargatecli/ecs" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | type TaskStopOperation struct { 10 | TaskGroupName string 11 | TaskIds []string 12 | } 13 | 14 | var ( 15 | flagTaskStopTasks []string 16 | ) 17 | 18 | var taskStopCmd = &cobra.Command{ 19 | Use: "stop ", 20 | Short: "Stop tasks", 21 | Long: `Stop tasks 22 | 23 | Stops all tasks within a task group if run with only a task group name or stops 24 | individual tasks if one or more tasks are passed via the --task flag. Specify 25 | --task with a task ID parameter multiple times to stop multiple specific tasks.`, 26 | Args: cobra.ExactArgs(1), 27 | Run: func(cmd *cobra.Command, args []string) { 28 | operation := &TaskStopOperation{ 29 | TaskGroupName: args[0], 30 | TaskIds: flagTaskStopTasks, 31 | } 32 | 33 | stopTasks(operation) 34 | }, 35 | } 36 | 37 | func init() { 38 | taskCmd.AddCommand(taskStopCmd) 39 | 40 | taskStopCmd.Flags().StringSliceVarP(&flagTaskStopTasks, "task", "t", []string{}, "Stop specific task instances (can be specified multiple times)") 41 | } 42 | 43 | func stopTasks(operation *TaskStopOperation) { 44 | var taskCount int 45 | 46 | ecs := ECS.New(sess, clusterName) 47 | 48 | if len(operation.TaskIds) > 0 { 49 | taskCount = len(operation.TaskIds) 50 | 51 | ecs.StopTasks(operation.TaskIds) 52 | } else { 53 | var taskIds []string 54 | 55 | tasks := ecs.DescribeTasksForTaskGroup(operation.TaskGroupName) 56 | 57 | for _, task := range tasks { 58 | taskIds = append(taskIds, task.TaskId) 59 | } 60 | 61 | taskCount = len(taskIds) 62 | 63 | ecs.StopTasks(taskIds) 64 | } 65 | 66 | if taskCount == 1 { 67 | console.Info("Stopped %d task", taskCount) 68 | } else { 69 | console.Info("Stopped %d tasks", taskCount) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cmd/testdata/certificate.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID+TCCAuGgAwIBAgIJAJ2PPB292VTPMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYD 3 | VQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxETAPBgNVBAcMCE5ldyBZb3JrMSEw 4 | HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxGTAXBgNVBAMMEHRlc3Qu 5 | cGlnbmF0YS5jb20xHzAdBgkqhkiG9w0BCQEWEGpvaG5AcGlnbmF0YS5jb20wHhcN 6 | MTgwMjAzMTM0NzM3WhcNMjgwMjAxMTM0NzM3WjCBkjELMAkGA1UEBhMCVVMxETAP 7 | BgNVBAgMCE5ldyBZb3JrMREwDwYDVQQHDAhOZXcgWW9yazEhMB8GA1UECgwYSW50 8 | ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRkwFwYDVQQDDBB0ZXN0LnBpZ25hdGEuY29t 9 | MR8wHQYJKoZIhvcNAQkBFhBqb2huQHBpZ25hdGEuY29tMIIBIjANBgkqhkiG9w0B 10 | AQEFAAOCAQ8AMIIBCgKCAQEAoNOwzw7dzqat3GLi9Zs+AaWVLl8TBrS5cIwcDKnQ 11 | 6B6gDiWemnVAG5gsXbTSHbCtUvjdsaaheuV0H+yPFRzuSQJ+gOufUTvRJm9n80xQ 12 | mSPp/dECsMpJjndT7FMtt0soP7hbiAGT0huZq8vNnOHFIjWfdjfYnGnV9AUKX4g2 13 | J2P7cJQgj9aa+hYu2t0RwGuMZibpjkLfplsPtt+dNa9e5C1mPR6gMNlIeEiRtsfc 14 | 4DdKgaQb+OeoQYJcc51MQC4ZezNhjHQxPcPp7F1o8dEWGyyqIc+BytGg/w8tj6Xo 15 | M4kyzHsCtwJpRp12zgTacQwxHlChroHZ8h5ueGx9Jp9U2QIDAQABo1AwTjAdBgNV 16 | HQ4EFgQUC1qoXFJ1bL1esoNI/0SrTPLA3okwHwYDVR0jBBgwFoAUC1qoXFJ1bL1e 17 | soNI/0SrTPLA3okwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAblky 18 | sS1m2mAKbR+lPjOXf2Q5GcEXyqlYrzlta1ds6LzoZjpm0ezNdXXD/f1oBA6OFaDf 19 | SpDgtxQaZqVqPoCNAvEUJGGojN6KQkDWKieEv3d+FnHZuSrwmzD+YGO0kWDHDnkg 20 | NDhll3RB6/Fij7aOfXLsuHZrGHkzwktWQBMEvnpLs1fHmLJgKTlLXroR3q7QL+jm 21 | Zzv1znn1DqMkrasp+GDixbnLoYeZFcIexeuxWDsjdbEVQThvdC8A4elrVhKWgggV 22 | 2o3ak6iJr5TOOs/7i5TnzHq5YT1gzDX/pETIZXEl/BKafIjqU0zYUism6gWQJ1lo 23 | AKUr4Eclnh1T9rqS5g== 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /cmd/testdata/chain.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEoDCCA4igAwIBAgIQBpaPlkroI1bHThfCtTZbADANBgkqhkiG9w0BAQsFADBs 3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 4 | d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j 5 | ZSBFViBSb290IENBMB4XDTE3MTEwNjEyMjI1N1oXDTI3MTEwNjEyMjI1N1owXzEL 6 | MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 7 | LmRpZ2ljZXJ0LmNvbTEeMBwGA1UEAxMVVGhhd3RlIEVWIFJTQSBDQSAyMDE4MIIB 8 | IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp0Cu52zmdJFnSezXMKvL0rso 9 | WgA/1X7OxjMQHsAllID1eDG836ptJXSTPg+DoEenHfkKyw++wXobgahr0cU/2v8R 10 | WR3fID53ZDhEGHzS+Ol7V+HRtZG5teMWCY7gldtBQH0r7xUEp/3ISVsZUVBqtUmL 11 | VJlf9nxJD6Cxp4LBlcJJ8+N6kSkV+fA+WdQc0HYhXSg3PxJP7XSU28Wc7gf6y9kZ 12 | zQhK4WrZLRrHHbHC2QXdqQYUxR927QV+UCNXnlbTcZy2QpxWTPLzK+/cKXX4cwP6 13 | MGF7+8RnUgHlij/5V2k/tIF9ep4B72ucqaS/UhEPpIN/T7A3OAw995yrB38glQID 14 | AQABo4IBSTCCAUUwHQYDVR0OBBYEFOcB/AwWGMp9sozshyejb2GBO4Q5MB8GA1Ud 15 | IwQYMBaAFLE+w2kD+L9HAdSYJhoIAu9jZCvDMA4GA1UdDwEB/wQEAwIBhjAdBgNV 16 | HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADA0 17 | BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0 18 | LmNvbTBLBgNVHR8ERDBCMECgPqA8hjpodHRwOi8vY3JsMy5kaWdpY2VydC5jb20v 19 | RGlnaUNlcnRIaWdoQXNzdXJhbmNlRVZSb290Q0EuY3JsMD0GA1UdIAQ2MDQwMgYE 20 | VR0gADAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BT 21 | MA0GCSqGSIb3DQEBCwUAA4IBAQAWGka+5ffLpfFuzT+WlwDRwhyTZSunnvecZWZT 22 | PPKXipynjpXx5dK8YG+2XoH74285GR1UABuvHMFV94XeDET9Pzz5s/NHS1/eAr5e 23 | GdwfBl80XwPkwXaYqzRtw6J4RAxeLqcbibhUQv9Iev9QcP0kNPyJu413Xov76mSu 24 | JlGThKzcurJPive2eLmwmoIgTPH11N/IIO9nHLVe8KTkt+FGgZCOWHA3kbFBZR39 25 | Mn2hFS974rhUkM+VS9KbCiQQ5OwkfbZ/6BINkE1CMtiESZ2WkbxJKPsF3dN7p9DF 26 | YWiQSbYjFP+rCT0/MkaHHYUkEvLNPgyJ6z29eMf0DjLu/SXJ 27 | -----END CERTIFICATE----- 28 | -------------------------------------------------------------------------------- /cmd/testdata/private.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAoNOwzw7dzqat3GLi9Zs+AaWVLl8TBrS5cIwcDKnQ6B6gDiWe 3 | mnVAG5gsXbTSHbCtUvjdsaaheuV0H+yPFRzuSQJ+gOufUTvRJm9n80xQmSPp/dEC 4 | sMpJjndT7FMtt0soP7hbiAGT0huZq8vNnOHFIjWfdjfYnGnV9AUKX4g2J2P7cJQg 5 | j9aa+hYu2t0RwGuMZibpjkLfplsPtt+dNa9e5C1mPR6gMNlIeEiRtsfc4DdKgaQb 6 | +OeoQYJcc51MQC4ZezNhjHQxPcPp7F1o8dEWGyyqIc+BytGg/w8tj6XoM4kyzHsC 7 | twJpRp12zgTacQwxHlChroHZ8h5ueGx9Jp9U2QIDAQABAoIBACR1eQqnmx8C6D0i 8 | 6cK2C8uBxxz5Dq4hgDyEdsNkJ+jHMI+kdZ0cYYkf6Ubg/BUg/Vnm8xMX7FmY9Mdb 9 | 8F/f7CD/AMCnKbnXrqVg8hbUwRzGaIBSxqJfaWdzo8HaZW29CwXO/GkLuoASI7cv 10 | f5BGGlOUBm9dX6ytzYQBNIXNskEhbFjbsiqSx3YlHoTP1qukHMz8rwBXc4wWRj2Q 11 | 4M4dqIGVW8OvsBcf4EzUokz8W+FMm14PBF7LkdxP+pAiQi4qqJeJiAmtnc20z7uD 12 | 6YNDcg6a0HosZyq5/AulAxt62JiiPrpP3njGEaEjsEU23qMbWd220CH9d7YXVCZM 13 | r5Cc4MUCgYEAzS4CX/HXu+Prw2wJbBRAfsXn0TiTzqi7E7uW6ZVWWAqW2ZSkIb0x 14 | /aXv+mLWA+ho3/D6/cysSY+pCGaewyPzsZRkOAhcpTww+1cXZFDpa0CKUTzBZ5tT 15 | XjpLO2VTvFJdAaXuad942qwiKr14w7CGaAhBCYUawpcjUM0hirBk0CsCgYEAyKle 16 | TOgkKJhV3VkKan5EyeOyfab0UcUPEQX3EoRoju+K6Z5FHcoaBpebYD9GAXdx1xdd 17 | jYW2k7diAZlKqAy5G+1SBW5ImFaDCZh3pIdRVtXLY9HCLUrac0NKioNEkyu3ZP0m 18 | dK4sfF6ZQTGPLP9hmdrlOeUJiEtnSZ1JbuGaqQsCgYEAmyaAazbAMHb/741BbrW9 19 | s19JlV7X/fx/QkOmsUjYush/G1aX6l9bbvdMiSS1uszCiRx7XvGnEhUM96pJwTvt 20 | acnRIsHH9LaYP2ay7It2hkCOlzF++i5tEyK3gtlzQUNyyu1DZFG03H4vc+xEZo2U 21 | hRRAwcch3iVVciM7itkp0nMCgYEAoDfbu7x/Yop/xMUbs/wuIKVWF03/NmsJpKYG 22 | qRpgAgDyAacFuMtuDGVxAmCDHRiqJPvbDuz84uVBs9UZ7yR5iPrsyrlL7Zbl+ftr 23 | TEtfft4mEAWj7VYfJnlMQ1ycnIYDYPdghTms+4DtDUjs8RjYbWiCLr/Z5KGQTw7v 24 | W0F3pBECgYB4RIMXZVd97zfULizqRwDXRUiXh+6LGGnyx0DSVnt20dWr75e0uOcj 25 | /5xUWNm5CLQ9IVEmKMjzV4Zgs99T35NFOuA1zcUj0PNjxLehtnIKk2GMaFLVvcWs 26 | ylyxsI/61UUXa0qnzvSRNO2GLXkjvxZ6wsbgLCpQsArUWFgdc7URxA== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /cmd/vpc_operation.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/awslabs/fargatecli/ec2" 5 | ) 6 | 7 | type vpcOperation struct { 8 | ec2 ec2.Client 9 | output Output 10 | securityGroupIDs []string 11 | subnetIDs []string 12 | vpcID string 13 | } 14 | 15 | func (o *vpcOperation) setSubnetIDs(subnetIDs []string) error { 16 | o.output.Debug("Finding VPC ID [API=ec2 Action=DescribeSubnets]") 17 | vpcID, err := o.ec2.GetSubnetVPCID(subnetIDs[0]) 18 | 19 | if err != nil { 20 | return err 21 | } 22 | 23 | o.subnetIDs = subnetIDs 24 | o.vpcID = vpcID 25 | 26 | return nil 27 | } 28 | 29 | func (o *vpcOperation) setSecurityGroupIDs(securityGroupIDs []string) { 30 | o.securityGroupIDs = securityGroupIDs 31 | } 32 | 33 | func (o *vpcOperation) setDefaultSecurityGroupID() error { 34 | o.output.Debug("Finding default security group [API=ec2 Action=DescribeSecurityGroups]") 35 | defaultSecurityGroupID, err := o.ec2.GetDefaultSecurityGroupID() 36 | 37 | if err != nil { 38 | return err 39 | } 40 | 41 | if defaultSecurityGroupID == "" { 42 | o.output.Debug("Creating default security group [API=ec2 Action=CreateSecurityGroup]") 43 | defaultSecurityGroupID, err = o.ec2.CreateDefaultSecurityGroup() 44 | 45 | if err != nil { 46 | return err 47 | } 48 | 49 | o.output.Debug("Created default security group [ID=%s]", defaultSecurityGroupID) 50 | 51 | o.output.Debug("Configuring default security group [API=ec2 Action=AuthorizeSecurityGroupIngress]") 52 | if err := o.ec2.AuthorizeAllSecurityGroupIngress(defaultSecurityGroupID); err != nil { 53 | return err 54 | } 55 | } 56 | 57 | o.securityGroupIDs = []string{defaultSecurityGroupID} 58 | 59 | return nil 60 | } 61 | 62 | func (o *vpcOperation) setDefaultSubnetIDs() error { 63 | o.output.Debug("Finding default subnets [API=ec2 Action=DescribeSubnets]") 64 | subnetIDs, err := o.ec2.GetDefaultSubnetIDs() 65 | 66 | if err != nil { 67 | return err 68 | } 69 | 70 | o.output.Debug("Finding VPC ID [API=ec2 Action=DescribeSubnets]") 71 | vpcID, err := o.ec2.GetSubnetVPCID(subnetIDs[0]) 72 | 73 | if err != nil { 74 | return err 75 | } 76 | 77 | o.subnetIDs = subnetIDs 78 | o.vpcID = vpcID 79 | 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /console/main.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | 8 | "github.com/mgutz/ansi" 9 | ) 10 | 11 | var ( 12 | Verbose = false 13 | Color = true 14 | ) 15 | 16 | var ( 17 | info = "[i] " 18 | debug = "[d] " 19 | issue = "[!] " 20 | shell = "[>] " 21 | 22 | colorInfo = white + "[" + blue + "i" + white + "]" + reset + " " 23 | colorDebug = white + "[" + orange + "d" + white + "]" + reset + " " 24 | colorIssue = white + "[" + red + "!" + white + "]" + reset + " " 25 | colorShell = white + "[" + green + ">" + white + "]" + reset + " " 26 | 27 | blue = ansi.ColorCode("blue+bh") 28 | white = ansi.ColorCode("white+bh") 29 | yellow = ansi.ColorCode("yellow+bh") 30 | green = ansi.ColorCode("green+bh") 31 | red = ansi.ColorCode("red+bh") 32 | reset = ansi.ColorCode("reset") 33 | orange = ansi.ColorCode("214+bh") 34 | ) 35 | 36 | func LogLine(prefix, msg string, color int) { 37 | if Color { 38 | colorCode := strconv.Itoa(color) 39 | fmt.Println(ansi.ColorCode(colorCode) + prefix + reset + " " + msg) 40 | } else { 41 | fmt.Println(prefix + " " + msg) 42 | } 43 | } 44 | 45 | func KeyValue(key, value string, a ...interface{}) { 46 | if Color { 47 | fmt.Fprintf(os.Stdout, white+key+reset+": "+value, a...) 48 | } else { 49 | fmt.Fprintf(os.Stdout, key+": "+value, a...) 50 | } 51 | } 52 | 53 | func Header(s string) { 54 | fmt.Print("\n") 55 | 56 | if Color { 57 | fmt.Print(white + s + reset + "\n") 58 | } else { 59 | fmt.Println(s) 60 | } 61 | } 62 | 63 | func Info(msg string, a ...interface{}) { 64 | if Color { 65 | fmt.Fprintf(os.Stdout, colorInfo+msg+reset+"\n", a...) 66 | } else { 67 | fmt.Fprintf(os.Stdout, info+msg+"\n", a...) 68 | } 69 | } 70 | 71 | func Debug(msg string, a ...interface{}) { 72 | if Verbose { 73 | if Color { 74 | fmt.Fprintf(os.Stdout, colorDebug+msg+reset+"\n", a...) 75 | } else { 76 | fmt.Fprintf(os.Stdout, debug+msg+"\n", a...) 77 | } 78 | } 79 | } 80 | 81 | func Shell(msg string, a ...interface{}) { 82 | if Color { 83 | fmt.Fprintf(os.Stdout, colorShell+green+msg+reset+"\n", a...) 84 | } else { 85 | fmt.Fprintf(os.Stdout, shell+msg+"\n", a...) 86 | } 87 | } 88 | 89 | func Issue(msg string, a ...interface{}) { 90 | if Color { 91 | fmt.Fprintf(os.Stderr, colorIssue+red+msg+reset+"\n", a...) 92 | } else { 93 | fmt.Fprintf(os.Stderr, issue+msg+"\n", a...) 94 | } 95 | } 96 | 97 | func Error(err error, msg string, a ...interface{}) { 98 | Issue(msg, a...) 99 | 100 | if err != nil { 101 | os.Stderr.WriteString(err.Error() + "\n") 102 | } 103 | } 104 | 105 | func InfoExit(msg string, a ...interface{}) { 106 | Info(msg, a...) 107 | os.Exit(0) 108 | } 109 | 110 | func ErrorExit(err error, msg string, a ...interface{}) { 111 | Error(err, msg, a...) 112 | os.Exit(1) 113 | } 114 | 115 | func IssueExit(msg string, a ...interface{}) { 116 | Issue(msg, a...) 117 | os.Exit(1) 118 | } 119 | 120 | func Exit(code int) { 121 | os.Exit(code) 122 | } 123 | 124 | func SetVerbose(verbose bool) { 125 | Verbose = verbose 126 | } 127 | -------------------------------------------------------------------------------- /doc/website/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/fargatecli/aa1c09cd404ecdee91a83b4f59171f790d6b480b/doc/website/apple.png -------------------------------------------------------------------------------- /doc/website/fargate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/fargatecli/aa1c09cd404ecdee91a83b4f59171f790d6b480b/doc/website/fargate.png -------------------------------------------------------------------------------- /doc/website/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/fargatecli/aa1c09cd404ecdee91a83b4f59171f790d6b480b/doc/website/github.png -------------------------------------------------------------------------------- /doc/website/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/fargatecli/aa1c09cd404ecdee91a83b4f59171f790d6b480b/doc/website/linux.png -------------------------------------------------------------------------------- /doc/website/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Lato', sans-serif; 3 | margin-bottom: 700px; 4 | } 5 | 6 | pre { 7 | background-color: #111; 8 | color: #f5f5f5; 9 | padding: 1.2em; 10 | border-radius: 0.5em; 11 | } 12 | 13 | .examples .card:first-child { 14 | margin-top: 0; 15 | } 16 | 17 | .examples .card { 18 | margin-top: 2em; 19 | width: 100%; 20 | } 21 | 22 | .github { 23 | margin-top: 3em; 24 | } 25 | 26 | .github img { 27 | margin-right: 10px; 28 | } 29 | 30 | span.red { 31 | color: red; 32 | } 33 | 34 | span.blue { 35 | color: deepskyblue; 36 | } 37 | 38 | span.green { 39 | color: limegreen; 40 | } 41 | 42 | .btn { 43 | margin-right: 10px; 44 | } 45 | 46 | .btn img { 47 | margin-right: 10px; 48 | } 49 | 50 | .clear { 51 | clear: both; 52 | } 53 | 54 | img.icon { 55 | height: 25px; 56 | width: 25px; 57 | } 58 | 59 | .header { 60 | margin-top: 3em; 61 | margin-bottom: 3em; 62 | } 63 | 64 | @media (min-width: 992px) { 65 | .hed { 66 | float: left; 67 | } 68 | 69 | .hed-image { 70 | float: right; 71 | } 72 | 73 | .intro { 74 | clear: both; 75 | } 76 | } 77 | 78 | @media (max-width: 992px) { 79 | .hed { 80 | text-align: center; 81 | } 82 | 83 | .hed-image { 84 | text-align: center; 85 | } 86 | 87 | .intro { 88 | text-align: center; 89 | } 90 | 91 | .actions { 92 | text-align: center; 93 | } 94 | } 95 | 96 | @media (max-width: 575.99px) { 97 | .card { 98 | margin: 0 20px; 99 | } 100 | 101 | .header { 102 | margin-left: 0px; 103 | } 104 | } 105 | 106 | .video iframe { 107 | width: 100% 108 | margin: 0 auto; 109 | margin-bottom: 30px; 110 | } 111 | -------------------------------------------------------------------------------- /docker/main.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "time" 7 | 8 | "github.com/awslabs/fargatecli/console" 9 | ) 10 | 11 | const timestampFormat = "20060102150405" 12 | 13 | func GenerateTag() string { 14 | return time.Now().UTC().Format(timestampFormat) 15 | } 16 | 17 | type Repository struct { 18 | Uri string 19 | } 20 | 21 | func NewRepository(repositoryUri string) Repository { 22 | return Repository{ 23 | Uri: repositoryUri, 24 | } 25 | } 26 | 27 | func (repository *Repository) Login(username, password string) { 28 | console.Debug("Logging into Docker repository [%s]", repository.Uri) 29 | console.Shell("docker login --username %s --password ******* %s", username, repository.Uri) 30 | 31 | cmd := exec.Command("docker", "login", "--username", username, "--password", password, repository.Uri) 32 | 33 | if console.Verbose { 34 | cmd.Stdout = os.Stdout 35 | cmd.Stderr = os.Stderr 36 | } 37 | 38 | if err := cmd.Start(); err != nil { 39 | console.ErrorExit(err, "Couldn't login to Docker repository [%s]", repository.Uri) 40 | } 41 | 42 | if err := cmd.Wait(); err != nil { 43 | console.IssueExit("Couldn't login to Docker repository [%s]", repository.Uri) 44 | } 45 | } 46 | 47 | func (repository *Repository) Build(tag string) { 48 | console.Debug("Building Docker image [%s]", repository.UriFor(tag)) 49 | console.Shell("docker build --rm=false --tag %s .", repository.UriFor(tag)) 50 | 51 | cmd := exec.Command("docker", "build", "--tag", repository.Uri+":"+tag, ".") 52 | 53 | cmd.Stdout = os.Stdout 54 | cmd.Stderr = os.Stderr 55 | 56 | if err := cmd.Start(); err != nil { 57 | console.ErrorExit(err, "Couldn't build Docker image [%s]", repository.UriFor(tag)) 58 | } 59 | 60 | if err := cmd.Wait(); err != nil { 61 | console.IssueExit("Couldn't build Docker image [%s]", repository.Uri) 62 | } 63 | } 64 | 65 | func (repository *Repository) Push(tag string) { 66 | console.Debug("Pushing Docker image [%s]", repository.UriFor(tag)) 67 | console.Shell("docker push %s .", repository.UriFor(tag)) 68 | 69 | cmd := exec.Command("docker", "push", repository.UriFor(tag)) 70 | 71 | cmd.Stdout = os.Stdout 72 | cmd.Stderr = os.Stderr 73 | 74 | if err := cmd.Start(); err != nil { 75 | console.ErrorExit(err, "Couldn't push Docker image [%s]", repository.UriFor(tag)) 76 | } 77 | 78 | if err := cmd.Wait(); err != nil { 79 | console.IssueExit("Couldn't push Docker image [%s]", repository.UriFor(tag)) 80 | } 81 | } 82 | 83 | func (repository *Repository) UriFor(tag string) string { 84 | return repository.Uri + ":" + tag 85 | } 86 | -------------------------------------------------------------------------------- /ec2/eni.go: -------------------------------------------------------------------------------- 1 | package ec2 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | awsec2 "github.com/aws/aws-sdk-go/service/ec2" 6 | "github.com/awslabs/fargatecli/console" 7 | ) 8 | 9 | type Eni struct { 10 | PublicIpAddress string 11 | EniId string 12 | SecurityGroupIds []string 13 | } 14 | 15 | func (ec2 SDKClient) DescribeNetworkInterfaces(eniIds []string) map[string]Eni { 16 | enis := make(map[string]Eni) 17 | 18 | resp, err := ec2.client.DescribeNetworkInterfaces( 19 | &awsec2.DescribeNetworkInterfacesInput{ 20 | NetworkInterfaceIds: aws.StringSlice(eniIds), 21 | }, 22 | ) 23 | 24 | if err != nil { 25 | console.ErrorExit(err, "Could not describe network interfaces") 26 | } 27 | 28 | for _, e := range resp.NetworkInterfaces { 29 | var securityGroupIds []*string 30 | 31 | for _, group := range e.Groups { 32 | securityGroupIds = append(securityGroupIds, group.GroupId) 33 | } 34 | 35 | if e.Association != nil { 36 | eni := Eni{ 37 | EniId: aws.StringValue(e.NetworkInterfaceId), 38 | PublicIpAddress: aws.StringValue(e.Association.PublicIp), 39 | SecurityGroupIds: aws.StringValueSlice(securityGroupIds), 40 | } 41 | 42 | enis[eni.EniId] = eni 43 | } 44 | } 45 | 46 | return enis 47 | } 48 | -------------------------------------------------------------------------------- /ec2/main.go: -------------------------------------------------------------------------------- 1 | package ec2 2 | 3 | //go:generate mockgen -package client -destination=mock/client/client.go github.com/awslabs/fargatecli/ec2 Client 4 | //go:generate mockgen -package sdk -source ../vendor/github.com/aws/aws-sdk-go/service/ec2/ec2iface/interface.go -destination=mock/sdk/ec2iface.go github.com/aws/aws-sdk-go/service/ec2/ec2iface EC2API 5 | 6 | import ( 7 | "github.com/aws/aws-sdk-go/aws/session" 8 | "github.com/aws/aws-sdk-go/service/ec2" 9 | "github.com/aws/aws-sdk-go/service/ec2/ec2iface" 10 | ) 11 | 12 | // Client represents a method for accessing EC2. 13 | type Client interface { 14 | AuthorizeAllSecurityGroupIngress(string) error 15 | CreateDefaultSecurityGroup() (string, error) 16 | GetDefaultSecurityGroupID() (string, error) 17 | GetDefaultSubnetIDs() ([]string, error) 18 | GetSubnetVPCID(string) (string, error) 19 | } 20 | 21 | // SDKClient implements access to EC2 via the AWS SDK. 22 | type SDKClient struct { 23 | client ec2iface.EC2API 24 | } 25 | 26 | // New returns an SDKClient configured with the given session. 27 | func New(sess *session.Session) SDKClient { 28 | return SDKClient{ 29 | client: ec2.New(sess), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ec2/mock/client/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/awslabs/fargatecli/ec2 (interfaces: Client) 3 | 4 | // Package client is a generated GoMock package. 5 | package client 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | reflect "reflect" 10 | ) 11 | 12 | // MockClient is a mock of Client interface 13 | type MockClient struct { 14 | ctrl *gomock.Controller 15 | recorder *MockClientMockRecorder 16 | } 17 | 18 | // MockClientMockRecorder is the mock recorder for MockClient 19 | type MockClientMockRecorder struct { 20 | mock *MockClient 21 | } 22 | 23 | // NewMockClient creates a new mock instance 24 | func NewMockClient(ctrl *gomock.Controller) *MockClient { 25 | mock := &MockClient{ctrl: ctrl} 26 | mock.recorder = &MockClientMockRecorder{mock} 27 | return mock 28 | } 29 | 30 | // EXPECT returns an object that allows the caller to indicate expected use 31 | func (m *MockClient) EXPECT() *MockClientMockRecorder { 32 | return m.recorder 33 | } 34 | 35 | // AuthorizeAllSecurityGroupIngress mocks base method 36 | func (m *MockClient) AuthorizeAllSecurityGroupIngress(arg0 string) error { 37 | m.ctrl.T.Helper() 38 | ret := m.ctrl.Call(m, "AuthorizeAllSecurityGroupIngress", arg0) 39 | ret0, _ := ret[0].(error) 40 | return ret0 41 | } 42 | 43 | // AuthorizeAllSecurityGroupIngress indicates an expected call of AuthorizeAllSecurityGroupIngress 44 | func (mr *MockClientMockRecorder) AuthorizeAllSecurityGroupIngress(arg0 interface{}) *gomock.Call { 45 | mr.mock.ctrl.T.Helper() 46 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthorizeAllSecurityGroupIngress", reflect.TypeOf((*MockClient)(nil).AuthorizeAllSecurityGroupIngress), arg0) 47 | } 48 | 49 | // CreateDefaultSecurityGroup mocks base method 50 | func (m *MockClient) CreateDefaultSecurityGroup() (string, error) { 51 | m.ctrl.T.Helper() 52 | ret := m.ctrl.Call(m, "CreateDefaultSecurityGroup") 53 | ret0, _ := ret[0].(string) 54 | ret1, _ := ret[1].(error) 55 | return ret0, ret1 56 | } 57 | 58 | // CreateDefaultSecurityGroup indicates an expected call of CreateDefaultSecurityGroup 59 | func (mr *MockClientMockRecorder) CreateDefaultSecurityGroup() *gomock.Call { 60 | mr.mock.ctrl.T.Helper() 61 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDefaultSecurityGroup", reflect.TypeOf((*MockClient)(nil).CreateDefaultSecurityGroup)) 62 | } 63 | 64 | // GetDefaultSecurityGroupID mocks base method 65 | func (m *MockClient) GetDefaultSecurityGroupID() (string, error) { 66 | m.ctrl.T.Helper() 67 | ret := m.ctrl.Call(m, "GetDefaultSecurityGroupID") 68 | ret0, _ := ret[0].(string) 69 | ret1, _ := ret[1].(error) 70 | return ret0, ret1 71 | } 72 | 73 | // GetDefaultSecurityGroupID indicates an expected call of GetDefaultSecurityGroupID 74 | func (mr *MockClientMockRecorder) GetDefaultSecurityGroupID() *gomock.Call { 75 | mr.mock.ctrl.T.Helper() 76 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultSecurityGroupID", reflect.TypeOf((*MockClient)(nil).GetDefaultSecurityGroupID)) 77 | } 78 | 79 | // GetDefaultSubnetIDs mocks base method 80 | func (m *MockClient) GetDefaultSubnetIDs() ([]string, error) { 81 | m.ctrl.T.Helper() 82 | ret := m.ctrl.Call(m, "GetDefaultSubnetIDs") 83 | ret0, _ := ret[0].([]string) 84 | ret1, _ := ret[1].(error) 85 | return ret0, ret1 86 | } 87 | 88 | // GetDefaultSubnetIDs indicates an expected call of GetDefaultSubnetIDs 89 | func (mr *MockClientMockRecorder) GetDefaultSubnetIDs() *gomock.Call { 90 | mr.mock.ctrl.T.Helper() 91 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultSubnetIDs", reflect.TypeOf((*MockClient)(nil).GetDefaultSubnetIDs)) 92 | } 93 | 94 | // GetSubnetVPCID mocks base method 95 | func (m *MockClient) GetSubnetVPCID(arg0 string) (string, error) { 96 | m.ctrl.T.Helper() 97 | ret := m.ctrl.Call(m, "GetSubnetVPCID", arg0) 98 | ret0, _ := ret[0].(string) 99 | ret1, _ := ret[1].(error) 100 | return ret0, ret1 101 | } 102 | 103 | // GetSubnetVPCID indicates an expected call of GetSubnetVPCID 104 | func (mr *MockClientMockRecorder) GetSubnetVPCID(arg0 interface{}) *gomock.Call { 105 | mr.mock.ctrl.T.Helper() 106 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubnetVPCID", reflect.TypeOf((*MockClient)(nil).GetSubnetVPCID), arg0) 107 | } 108 | -------------------------------------------------------------------------------- /ec2/vpc.go: -------------------------------------------------------------------------------- 1 | package ec2 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/aws/awserr" 8 | awsec2 "github.com/aws/aws-sdk-go/service/ec2" 9 | ) 10 | 11 | const ( 12 | defaultSecurityGroupName = "fargate-default" 13 | defaultSecurityGroupDescription = "Default Fargate CLI SG" 14 | defaultSecurityGroupIngressCIDR = "0.0.0.0/0" 15 | defaultSecurityGroupIngressProtocol = "-1" 16 | ) 17 | 18 | // GetDefaultSubnetIDs finds and returns the subnet IDs marked as default. 19 | func (ec2 SDKClient) GetDefaultSubnetIDs() ([]string, error) { 20 | var subnetIDs []string 21 | 22 | defaultFilter := &awsec2.Filter{ 23 | Name: aws.String("default-for-az"), 24 | Values: aws.StringSlice([]string{"true"}), 25 | } 26 | 27 | resp, err := ec2.client.DescribeSubnets( 28 | &awsec2.DescribeSubnetsInput{ 29 | Filters: []*awsec2.Filter{defaultFilter}, 30 | }, 31 | ) 32 | 33 | if err != nil { 34 | return subnetIDs, fmt.Errorf("could not retrieve default subnet IDs: %v", err) 35 | } 36 | 37 | for _, subnet := range resp.Subnets { 38 | subnetIDs = append(subnetIDs, aws.StringValue(subnet.SubnetId)) 39 | } 40 | 41 | return subnetIDs, nil 42 | } 43 | 44 | // GetDefaultSecurityGroupID returns the ID of the permissive security group created by default. 45 | func (ec2 SDKClient) GetDefaultSecurityGroupID() (string, error) { 46 | resp, err := ec2.client.DescribeSecurityGroups( 47 | &awsec2.DescribeSecurityGroupsInput{ 48 | GroupNames: aws.StringSlice([]string{defaultSecurityGroupName}), 49 | }, 50 | ) 51 | 52 | if err != nil { 53 | if aerr, ok := err.(awserr.Error); ok { 54 | if aerr.Code() == "InvalidGroup.NotFound" { 55 | return "", nil 56 | } 57 | } 58 | 59 | return "", fmt.Errorf("could not retrieve default security group ID (%s): %v", defaultSecurityGroupName, err) 60 | } 61 | 62 | return aws.StringValue(resp.SecurityGroups[0].GroupId), nil 63 | } 64 | 65 | // GetSubnetVPCID returns the VPC ID for a given subnet ID. 66 | func (ec2 SDKClient) GetSubnetVPCID(subnetID string) (string, error) { 67 | resp, err := ec2.client.DescribeSubnets( 68 | &awsec2.DescribeSubnetsInput{ 69 | SubnetIds: aws.StringSlice([]string{subnetID}), 70 | }, 71 | ) 72 | 73 | switch { 74 | case err != nil: 75 | return "", fmt.Errorf("could not find VPC ID for subnet ID %s: %v", subnetID, err) 76 | case len(resp.Subnets) == 0: 77 | return "", fmt.Errorf("could not find VPC ID: subnet ID %s not found", subnetID) 78 | default: 79 | return aws.StringValue(resp.Subnets[0].VpcId), nil 80 | } 81 | } 82 | 83 | // CreateDefaultSecurityGroup creates a new security group for use as the default. 84 | func (ec2 SDKClient) CreateDefaultSecurityGroup() (string, error) { 85 | resp, err := ec2.client.CreateSecurityGroup( 86 | &awsec2.CreateSecurityGroupInput{ 87 | GroupName: aws.String(defaultSecurityGroupName), 88 | Description: aws.String(defaultSecurityGroupDescription), 89 | }, 90 | ) 91 | 92 | if err != nil { 93 | return "", fmt.Errorf("could not create default security group (%s): %v", defaultSecurityGroupName, err) 94 | } 95 | 96 | return aws.StringValue(resp.GroupId), nil 97 | } 98 | 99 | // AuthorizeAllSecurityGroupIngress configures a security group to allow all ingress traffic. 100 | func (ec2 SDKClient) AuthorizeAllSecurityGroupIngress(groupID string) error { 101 | _, err := ec2.client.AuthorizeSecurityGroupIngress( 102 | &awsec2.AuthorizeSecurityGroupIngressInput{ 103 | CidrIp: aws.String(defaultSecurityGroupIngressCIDR), 104 | GroupId: aws.String(groupID), 105 | IpProtocol: aws.String(defaultSecurityGroupIngressProtocol), 106 | }, 107 | ) 108 | 109 | return err 110 | } 111 | -------------------------------------------------------------------------------- /ecr/main.go: -------------------------------------------------------------------------------- 1 | package ecr 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/session" 5 | "github.com/aws/aws-sdk-go/service/ecr" 6 | ) 7 | 8 | type ECR struct { 9 | svc *ecr.ECR 10 | } 11 | 12 | func New(sess *session.Session) ECR { 13 | return ECR{ 14 | svc: ecr.New(sess), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ecr/repository.go: -------------------------------------------------------------------------------- 1 | package ecr 2 | 3 | import ( 4 | "encoding/base64" 5 | "strings" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/aws/awserr" 9 | awsecr "github.com/aws/aws-sdk-go/service/ecr" 10 | "github.com/awslabs/fargatecli/console" 11 | ) 12 | 13 | func (ecr *ECR) CreateRepository(repositoryName string) string { 14 | console.Debug("Creating Amazon ECR repository") 15 | 16 | resp, err := ecr.svc.CreateRepository( 17 | &awsecr.CreateRepositoryInput{ 18 | RepositoryName: aws.String(repositoryName), 19 | }, 20 | ) 21 | 22 | if err != nil { 23 | console.ErrorExit(err, "Couldn't create Amazon ECR repository") 24 | } 25 | 26 | console.Debug("Created Amazon ECR repository [%s]", *resp.Repository.RepositoryName) 27 | return aws.StringValue(resp.Repository.RepositoryUri) 28 | } 29 | 30 | func (ecr *ECR) IsRepositoryCreated(repositoryName string) bool { 31 | resp, err := ecr.svc.DescribeRepositories( 32 | &awsecr.DescribeRepositoriesInput{ 33 | RepositoryNames: aws.StringSlice([]string{repositoryName}), 34 | }, 35 | ) 36 | 37 | if err != nil { 38 | if awsErr, ok := err.(awserr.Error); ok { 39 | switch awsErr.Code() { 40 | case awsecr.ErrCodeRepositoryNotFoundException: 41 | return false 42 | default: 43 | console.ErrorExit(awsErr, "Could not create Cloudwatch Logs log group") 44 | } 45 | } 46 | 47 | console.ErrorExit(err, "Couldn't describe Amazon ECR repositories") 48 | } 49 | 50 | return len(resp.Repositories) == 1 51 | } 52 | 53 | func (ecr *ECR) GetRepositoryUri(repositoryName string) string { 54 | resp, err := ecr.svc.DescribeRepositories( 55 | &awsecr.DescribeRepositoriesInput{ 56 | RepositoryNames: aws.StringSlice([]string{repositoryName}), 57 | }, 58 | ) 59 | 60 | if err != nil { 61 | console.ErrorExit(err, "Couldn't describe Amazon ECR repositories") 62 | } 63 | 64 | if len(resp.Repositories) != 1 { 65 | console.ErrorExit(err, "Couldn't find Amazon ECR repository: %s", repositoryName) 66 | } 67 | 68 | return aws.StringValue(resp.Repositories[0].RepositoryUri) 69 | } 70 | 71 | func (ecr *ECR) GetUsernameAndPassword() (username, password string) { 72 | resp, err := ecr.svc.GetAuthorizationToken( 73 | &awsecr.GetAuthorizationTokenInput{}, 74 | ) 75 | 76 | if err != nil { 77 | console.ErrorExit(err, "Couldn't get Amazon ECR authorization token") 78 | } 79 | 80 | token, _ := base64.StdEncoding.DecodeString(*resp.AuthorizationData[0].AuthorizationToken) 81 | s := strings.Split(string(token), ":") 82 | username = s[0] 83 | password = s[1] 84 | 85 | return 86 | } 87 | -------------------------------------------------------------------------------- /ecs/cluster.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | awsecs "github.com/aws/aws-sdk-go/service/ecs" 6 | ) 7 | 8 | func (ecs *ECS) CreateCluster() (string, error) { 9 | input := &awsecs.CreateClusterInput{ 10 | ClusterName: aws.String(ecs.ClusterName), 11 | } 12 | 13 | resp, err := ecs.svc.CreateCluster(input) 14 | if err != nil { 15 | return "", err 16 | } 17 | 18 | return aws.StringValue(resp.Cluster.ClusterArn), nil 19 | } 20 | -------------------------------------------------------------------------------- /ecs/main.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/session" 5 | "github.com/aws/aws-sdk-go/service/ecs" 6 | ) 7 | 8 | type ECS struct { 9 | svc *ecs.ECS 10 | ClusterName string 11 | } 12 | 13 | func New(sess *session.Session, clusterName string) ECS { 14 | return ECS{ 15 | ClusterName: clusterName, 16 | svc: ecs.New(sess), 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /elbv2/listener_test.go: -------------------------------------------------------------------------------- 1 | package elbv2 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | awselbv2 "github.com/aws/aws-sdk-go/service/elbv2" 9 | "github.com/golang/mock/gomock" 10 | "github.com/awslabs/fargatecli/elbv2/mock/sdk" 11 | ) 12 | 13 | func TestListenerString(t *testing.T) { 14 | listener := Listener{ 15 | Port: 80, 16 | Protocol: "HTTP", 17 | } 18 | 19 | if expected, got := "HTTP:80", listener.String(); got != expected { 20 | t.Errorf("expected %s, got %s", expected, got) 21 | } 22 | } 23 | 24 | func TestListenersString(t *testing.T) { 25 | listeners := Listeners{ 26 | Listener{ 27 | Port: 80, 28 | Protocol: "HTTP", 29 | }, 30 | Listener{ 31 | Port: 443, 32 | Protocol: "HTTPS", 33 | }, 34 | } 35 | 36 | if expected, got := "HTTP:80, HTTPS:443", listeners.String(); got != expected { 37 | t.Errorf("expected %s, got %s", expected, got) 38 | } 39 | } 40 | 41 | func TestCreateListenerParametersSetCertificateARNs(t *testing.T) { 42 | certificateARNs := []string{"arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012"} 43 | params := CreateListenerParameters{} 44 | 45 | params.SetCertificateARNs(certificateARNs) 46 | 47 | if !reflect.DeepEqual(params.CertificateARNs, certificateARNs) { 48 | t.Errorf("expected %v, got %v", certificateARNs, params.CertificateARNs) 49 | } 50 | } 51 | 52 | func TestDescribeListeners(t *testing.T) { 53 | listenerARN := "arn:aws:elasticloadbalancing:us-west-2:123456789012:listener/app/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2" 54 | certificateARN := "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012" 55 | 56 | resp := &awselbv2.DescribeListenersOutput{ 57 | Listeners: []*awselbv2.Listener{ 58 | &awselbv2.Listener{ 59 | ListenerArn: aws.String(listenerARN), 60 | Port: aws.Int64(80), 61 | Protocol: aws.String("HTTP"), 62 | Certificates: []*awselbv2.Certificate{ 63 | &awselbv2.Certificate{ 64 | CertificateArn: aws.String(certificateARN), 65 | }, 66 | }, 67 | }, 68 | }, 69 | } 70 | 71 | mockClient := sdk.MockDescribeListenersClient{Resp: resp} 72 | elbv2 := SDKClient{client: mockClient} 73 | listeners, err := elbv2.DescribeListeners("lbARN") 74 | 75 | if err != nil { 76 | t.Errorf("expected no error, got %v", err) 77 | } 78 | 79 | if len(listeners) != 1 { 80 | t.Errorf("expected 1 listener, got %d", len(listeners)) 81 | } 82 | 83 | if listeners[0].ARN != listenerARN { 84 | t.Errorf("expected ARN %s, got %s", listenerARN, listeners[0].ARN) 85 | } 86 | 87 | if expected := int64(80); expected != listeners[0].Port { 88 | t.Errorf("expected Port %d, got %d", expected, listeners[0].Port) 89 | } 90 | 91 | if expected := "HTTP"; expected != listeners[0].Protocol { 92 | t.Errorf("expected Port %s, got %s", expected, listeners[0].Protocol) 93 | } 94 | 95 | if listeners[0].CertificateARNs[0] != certificateARN { 96 | t.Errorf("expected certificate ARN %s, got %s", certificateARN, listeners[0].CertificateARNs[0]) 97 | } 98 | } 99 | 100 | func TestCreateListeners(t *testing.T) { 101 | lbARN := "arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/app/my-load-balancer/50dc6c495c0c9188" 102 | port := int64(443) 103 | protocol := "HTTPS" 104 | defaultTargetGroupARN := "arn:aws:elasticloadbalancing:us-west-2:123456789012:targetgroup/my-targets/73e2d6bc24d8a067" 105 | listenerARN := "arn:aws:elasticloadbalancing:us-west-2:123456789012:listener/app/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2" 106 | certificateARNs := []string{"arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012"} 107 | 108 | mockCtrl := gomock.NewController(t) 109 | defer mockCtrl.Finish() 110 | 111 | mockELBV2API := sdk.NewMockELBV2API(mockCtrl) 112 | elbv2 := SDKClient{client: mockELBV2API} 113 | i := &awselbv2.CreateListenerInput{ 114 | Port: aws.Int64(port), 115 | Protocol: aws.String(protocol), 116 | LoadBalancerArn: aws.String(lbARN), 117 | Certificates: []*awselbv2.Certificate{ 118 | &awselbv2.Certificate{ 119 | CertificateArn: aws.String(certificateARNs[0]), 120 | }, 121 | }, 122 | DefaultActions: []*awselbv2.Action{ 123 | &awselbv2.Action{ 124 | TargetGroupArn: aws.String(defaultTargetGroupARN), 125 | Type: aws.String("forward"), 126 | }, 127 | }, 128 | } 129 | o := &awselbv2.CreateListenerOutput{ 130 | Listeners: []*awselbv2.Listener{ 131 | &awselbv2.Listener{ 132 | ListenerArn: aws.String(listenerARN), 133 | }, 134 | }, 135 | } 136 | params := CreateListenerParameters{ 137 | CertificateARNs: certificateARNs, 138 | Port: port, 139 | Protocol: protocol, 140 | LoadBalancerARN: lbARN, 141 | DefaultTargetGroupARN: defaultTargetGroupARN, 142 | } 143 | 144 | mockELBV2API.EXPECT().CreateListener(i).Return(o, nil) 145 | 146 | arn, err := elbv2.CreateListener(params) 147 | 148 | if err != nil { 149 | t.Fatalf("expected no error, got %v", err) 150 | } 151 | 152 | if arn != listenerARN { 153 | t.Errorf("expected ARN %s, got %s", lbARN, arn) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /elbv2/load_balancer.go: -------------------------------------------------------------------------------- 1 | package elbv2 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | awselbv2 "github.com/aws/aws-sdk-go/service/elbv2" 8 | "github.com/awslabs/fargatecli/console" 9 | ) 10 | 11 | // LoadBalancer represents an Elastic Load Balancing (v2) load balancer. 12 | type LoadBalancer struct { 13 | ARN string 14 | DNSName string 15 | HostedZoneID string 16 | Listeners Listeners 17 | Name string 18 | SecurityGroupIDs []string 19 | Status string 20 | SubnetIDs []string 21 | Type string 22 | VPCID string 23 | } 24 | 25 | // LoadBalancers is a collection of Elastic Load Balancing (v2) load balancers. 26 | type LoadBalancers []LoadBalancer 27 | 28 | // CreateLoadBalancerParameters are the parameters required to create a new load balancer. 29 | type CreateLoadBalancerParameters struct { 30 | Name string 31 | SecurityGroupIDs []string 32 | SubnetIDs []string 33 | Type string 34 | Scheme string 35 | } 36 | 37 | // CreateLoadBalancer creates a new load balancer. It returns the ARN of the load balancer if it is successfully 38 | // created. 39 | func (elbv2 SDKClient) CreateLoadBalancer(p CreateLoadBalancerParameters) (string, error) { 40 | sdki := &awselbv2.CreateLoadBalancerInput{ 41 | Name: aws.String(p.Name), 42 | Subnets: aws.StringSlice(p.SubnetIDs), 43 | Type: aws.String(p.Type), 44 | Scheme: aws.String(p.Scheme), 45 | } 46 | 47 | if p.Type == awselbv2.LoadBalancerTypeEnumApplication { 48 | sdki.SetSecurityGroups(aws.StringSlice(p.SecurityGroupIDs)) 49 | } 50 | 51 | resp, err := elbv2.client.CreateLoadBalancer(sdki) 52 | 53 | if err != nil { 54 | return "", err 55 | } 56 | 57 | return aws.StringValue(resp.LoadBalancers[0].LoadBalancerArn), nil 58 | } 59 | 60 | // DescribeLoadBalancers returns all load balancers. 61 | func (elbv2 SDKClient) DescribeLoadBalancers() (LoadBalancers, error) { 62 | return elbv2.describeLoadBalancers(&awselbv2.DescribeLoadBalancersInput{}) 63 | } 64 | 65 | // DescribeLoadBalancersByName returns load balancers that match the given load balancer names. 66 | func (elbv2 SDKClient) DescribeLoadBalancersByName(lbNames []string) (LoadBalancers, error) { 67 | return elbv2.describeLoadBalancers( 68 | &awselbv2.DescribeLoadBalancersInput{Names: aws.StringSlice(lbNames)}, 69 | ) 70 | } 71 | 72 | // DescribeLoadBalancersByARN returns load balancers that match the given load balancer ARNs. 73 | func (elbv2 SDKClient) DescribeLoadBalancersByARN(lbARNs []string) (LoadBalancers, error) { 74 | return elbv2.describeLoadBalancers( 75 | &awselbv2.DescribeLoadBalancersInput{LoadBalancerArns: aws.StringSlice(lbARNs)}, 76 | ) 77 | } 78 | 79 | func (elbv2 SDKClient) DescribeLoadBalancer(lbName string) LoadBalancer { 80 | loadBalancers, _ := elbv2.DescribeLoadBalancersByName([]string{lbName}) 81 | 82 | if len(loadBalancers) == 0 { 83 | console.ErrorExit(fmt.Errorf("%s not found", lbName), "Could not find ELB load balancer") 84 | } 85 | 86 | return loadBalancers[0] 87 | } 88 | 89 | func (elbv2 SDKClient) DescribeLoadBalancerByARN(lbARN string) LoadBalancer { 90 | loadBalancers, _ := elbv2.DescribeLoadBalancersByARN([]string{lbARN}) 91 | 92 | if len(loadBalancers) == 0 { 93 | console.ErrorExit(fmt.Errorf("%s not found", lbARN), "Could not find ELB load balancer") 94 | } 95 | 96 | return loadBalancers[0] 97 | } 98 | 99 | func (elbv2 SDKClient) DeleteLoadBalancer(lbName string) { 100 | loadBalancer := elbv2.DescribeLoadBalancer(lbName) 101 | _, err := elbv2.client.DeleteLoadBalancer( 102 | &awselbv2.DeleteLoadBalancerInput{ 103 | LoadBalancerArn: aws.String(loadBalancer.ARN), 104 | }, 105 | ) 106 | 107 | if err != nil { 108 | console.ErrorExit(err, "Could not destroy ELB load balancer") 109 | } 110 | } 111 | 112 | func (elbv2 SDKClient) describeLoadBalancers(i *awselbv2.DescribeLoadBalancersInput) (LoadBalancers, error) { 113 | var loadBalancers []LoadBalancer 114 | 115 | handler := func(resp *awselbv2.DescribeLoadBalancersOutput, lastPage bool) bool { 116 | for _, loadBalancer := range resp.LoadBalancers { 117 | var subnetIDs []string 118 | 119 | for _, availabilityZone := range loadBalancer.AvailabilityZones { 120 | subnetIDs = append(subnetIDs, aws.StringValue(availabilityZone.SubnetId)) 121 | } 122 | 123 | loadBalancers = append(loadBalancers, 124 | LoadBalancer{ 125 | ARN: aws.StringValue(loadBalancer.LoadBalancerArn), 126 | DNSName: aws.StringValue(loadBalancer.DNSName), 127 | HostedZoneID: aws.StringValue(loadBalancer.CanonicalHostedZoneId), 128 | VPCID: aws.StringValue(loadBalancer.VpcId), 129 | Name: aws.StringValue(loadBalancer.LoadBalancerName), 130 | SecurityGroupIDs: aws.StringValueSlice(loadBalancer.SecurityGroups), 131 | Status: aws.StringValue(loadBalancer.State.Code), 132 | SubnetIDs: subnetIDs, 133 | Type: aws.StringValue(loadBalancer.Type), 134 | }, 135 | ) 136 | } 137 | 138 | return true 139 | } 140 | 141 | err := elbv2.client.DescribeLoadBalancersPages(i, handler) 142 | 143 | return loadBalancers, err 144 | } 145 | -------------------------------------------------------------------------------- /elbv2/load_balancer_test.go: -------------------------------------------------------------------------------- 1 | package elbv2 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | awselbv2 "github.com/aws/aws-sdk-go/service/elbv2" 9 | "github.com/golang/mock/gomock" 10 | "github.com/awslabs/fargatecli/elbv2/mock/sdk" 11 | ) 12 | 13 | var ( 14 | lbARN = "arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/app/my-load-balancer/50dc6c495c0c9188" 15 | dnsName = "my-load-balancer-424835706.us-west-2.elb.amazonaws.com" 16 | hostedZoneID = "Z2P70J7EXAMPLE" 17 | vpcID = "vpc-3ac0fb5f" 18 | lbName = "web" 19 | subnet = "subnet-8360a9e7" 20 | lbType = "application" 21 | status = "active" 22 | 23 | resp = &awselbv2.DescribeLoadBalancersOutput{ 24 | LoadBalancers: []*awselbv2.LoadBalancer{ 25 | &awselbv2.LoadBalancer{ 26 | LoadBalancerArn: aws.String(lbARN), 27 | DNSName: aws.String(dnsName), 28 | CanonicalHostedZoneId: aws.String(hostedZoneID), 29 | VpcId: aws.String(vpcID), 30 | LoadBalancerName: aws.String(lbName), 31 | SecurityGroups: []*string{aws.String("sg-5943793c")}, 32 | AvailabilityZones: []*awselbv2.AvailabilityZone{ 33 | &awselbv2.AvailabilityZone{ 34 | SubnetId: aws.String(subnet), 35 | }, 36 | }, 37 | Type: aws.String(lbType), 38 | State: &awselbv2.LoadBalancerState{ 39 | Code: aws.String(status), 40 | }, 41 | }, 42 | }, 43 | } 44 | ) 45 | 46 | func TestDescribeLoadBalancers(t *testing.T) { 47 | mockClient := sdk.MockDescribeLoadBalancersClient{Resp: resp} 48 | elbv2 := SDKClient{client: mockClient} 49 | loadBalancers, err := elbv2.DescribeLoadBalancers() 50 | 51 | if err != nil { 52 | t.Errorf("expected no error, got %+v", err) 53 | } 54 | 55 | if len(loadBalancers) != 1 { 56 | t.Errorf("expected 1 load balancer, got %d", len(loadBalancers)) 57 | } 58 | 59 | if loadBalancers[0].ARN != lbARN { 60 | t.Errorf("expected ARN %s, got %s", lbARN, loadBalancers[0].ARN) 61 | } 62 | 63 | if loadBalancers[0].DNSName != dnsName { 64 | t.Errorf("expected DNSName %s, got %s", dnsName, loadBalancers[0].DNSName) 65 | } 66 | 67 | if loadBalancers[0].HostedZoneID != hostedZoneID { 68 | t.Errorf("expected HostedZoneID %s, got %s", hostedZoneID, loadBalancers[0].HostedZoneID) 69 | } 70 | 71 | if loadBalancers[0].VPCID != vpcID { 72 | t.Errorf("expected VPCID %s, got %s", vpcID, loadBalancers[0].VPCID) 73 | } 74 | 75 | if loadBalancers[0].Name != lbName { 76 | t.Errorf("expected Name %s, got %s", lbName, loadBalancers[0].Name) 77 | } 78 | 79 | if loadBalancers[0].SubnetIDs[0] != subnet { 80 | t.Errorf("expected subnet %s, got %s", subnet, loadBalancers[0].SubnetIDs[0]) 81 | } 82 | 83 | if loadBalancers[0].Type != lbType { 84 | t.Errorf("expected type %s, got %s", lbType, loadBalancers[0].Type) 85 | } 86 | 87 | if loadBalancers[0].Status != status { 88 | t.Errorf("expected status %s, got %s", status, loadBalancers[0].Status) 89 | } 90 | } 91 | 92 | func TestDescribeLoadBalancersByName(t *testing.T) { 93 | mockClient := sdk.MockDescribeLoadBalancersClient{Resp: resp} 94 | elbv2 := SDKClient{client: mockClient} 95 | loadBalancers, err := elbv2.DescribeLoadBalancersByName([]string{"web"}) 96 | 97 | if err != nil { 98 | t.Errorf("Expected no error, got %+v", err) 99 | } 100 | 101 | if len(loadBalancers) != 1 { 102 | t.Errorf("Expected 1 load balancer, got %d", len(loadBalancers)) 103 | } 104 | } 105 | 106 | func TestDescribeLoadBalancersByARN(t *testing.T) { 107 | mockClient := sdk.MockDescribeLoadBalancersClient{Resp: resp} 108 | elbv2 := SDKClient{client: mockClient} 109 | loadBalancers, err := elbv2.DescribeLoadBalancersByARN([]string{lbARN}) 110 | 111 | if err != nil { 112 | t.Errorf("Expected no error, got %+v", err) 113 | } 114 | 115 | if len(loadBalancers) != 1 { 116 | t.Errorf("Expected 1 load balancer, got %d", len(loadBalancers)) 117 | } 118 | } 119 | 120 | func TestDescribeLoadBalancersByNameError(t *testing.T) { 121 | mockClient := sdk.MockDescribeLoadBalancersClient{Error: errors.New("boom")} 122 | elbv2 := SDKClient{client: mockClient} 123 | _, err := elbv2.DescribeLoadBalancersByName([]string{"web"}) 124 | 125 | if err == nil { 126 | t.Error("Expected error, got none") 127 | } 128 | } 129 | 130 | func TestCreateLoadBalancer(t *testing.T) { 131 | lbARN := "arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/app/my-load-balancer/50dc6c495c0c9188" 132 | name := "cool-load-balancer" 133 | subnetIDs := []string{"subnet-1234567"} 134 | securityGroupIDs := []string{"sg-1234567"} 135 | lbType := "application" 136 | lbScheme := "internet-facing" 137 | 138 | mockCtrl := gomock.NewController(t) 139 | defer mockCtrl.Finish() 140 | 141 | mockELBV2API := sdk.NewMockELBV2API(mockCtrl) 142 | elbv2 := SDKClient{client: mockELBV2API} 143 | i := &awselbv2.CreateLoadBalancerInput{ 144 | Name: aws.String(name), 145 | Subnets: aws.StringSlice(subnetIDs), 146 | SecurityGroups: aws.StringSlice(securityGroupIDs), 147 | Type: aws.String(lbType), 148 | Scheme: aws.String(lbScheme), 149 | } 150 | o := &awselbv2.CreateLoadBalancerOutput{ 151 | LoadBalancers: []*awselbv2.LoadBalancer{ 152 | &awselbv2.LoadBalancer{ 153 | LoadBalancerArn: aws.String(lbARN), 154 | }, 155 | }, 156 | } 157 | params := CreateLoadBalancerParameters{ 158 | Name: name, 159 | SubnetIDs: subnetIDs, 160 | Type: lbType, 161 | Scheme: lbScheme, 162 | SecurityGroupIDs: securityGroupIDs, 163 | } 164 | 165 | mockELBV2API.EXPECT().CreateLoadBalancer(i).Return(o, nil) 166 | 167 | arn, err := elbv2.CreateLoadBalancer(params) 168 | 169 | if err != nil { 170 | t.Fatalf("expected no error, got %v", err) 171 | } 172 | 173 | if arn != lbARN { 174 | t.Errorf("expected ARN %s, got %s", lbARN, arn) 175 | } 176 | } 177 | 178 | func TestCreateLoadBalancerWithError(t *testing.T) { 179 | mockCtrl := gomock.NewController(t) 180 | defer mockCtrl.Finish() 181 | 182 | mockELBV2API := sdk.NewMockELBV2API(mockCtrl) 183 | elbv2 := SDKClient{client: mockELBV2API} 184 | 185 | mockELBV2API.EXPECT().CreateLoadBalancer(gomock.Any()).Return(nil, errors.New("boom")) 186 | 187 | _, err := elbv2.CreateLoadBalancer(CreateLoadBalancerParameters{}) 188 | 189 | if err == nil { 190 | t.Fatalf("expected error, got none") 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /elbv2/main.go: -------------------------------------------------------------------------------- 1 | // Package elbv2 is a client for Elastic Load Balancing (v2). 2 | package elbv2 3 | 4 | //go:generate mockgen -package client -destination=mock/client/client.go github.com/awslabs/fargatecli/elbv2 Client 5 | //go:generate mockgen -package sdk -source ../vendor/github.com/aws/aws-sdk-go/service/elbv2/elbv2iface/interface.go -destination=mock/sdk/elbv2iface.go github.com/aws/aws-sdk-go/service/elbv2/elbv2iface ELBV2API 6 | 7 | import ( 8 | "github.com/aws/aws-sdk-go/aws/session" 9 | "github.com/aws/aws-sdk-go/service/elbv2" 10 | "github.com/aws/aws-sdk-go/service/elbv2/elbv2iface" 11 | ) 12 | 13 | // Client represents a method for accessing Elastic Load Balancing (v2). 14 | type Client interface { 15 | CreateListener(CreateListenerParameters) (string, error) 16 | DescribeListeners(string) (Listeners, error) 17 | 18 | DescribeLoadBalancers() (LoadBalancers, error) 19 | DescribeLoadBalancersByName([]string) (LoadBalancers, error) 20 | CreateLoadBalancer(CreateLoadBalancerParameters) (string, error) 21 | 22 | CreateTargetGroup(CreateTargetGroupParameters) (string, error) 23 | } 24 | 25 | // SDKClient implements access to Elastic Load Balancing (v2) via the AWS SDK. 26 | type SDKClient struct { 27 | client elbv2iface.ELBV2API 28 | } 29 | 30 | // New returns an SDKClient configured with the given session. 31 | func New(sess *session.Session) SDKClient { 32 | return SDKClient{ 33 | client: elbv2.New(sess), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /elbv2/mock/client/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/awslabs/fargatecli/elbv2 (interfaces: Client) 3 | 4 | // Package client is a generated GoMock package. 5 | package client 6 | 7 | import ( 8 | elbv2 "github.com/awslabs/fargatecli/elbv2" 9 | gomock "github.com/golang/mock/gomock" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockClient is a mock of Client interface 14 | type MockClient struct { 15 | ctrl *gomock.Controller 16 | recorder *MockClientMockRecorder 17 | } 18 | 19 | // MockClientMockRecorder is the mock recorder for MockClient 20 | type MockClientMockRecorder struct { 21 | mock *MockClient 22 | } 23 | 24 | // NewMockClient creates a new mock instance 25 | func NewMockClient(ctrl *gomock.Controller) *MockClient { 26 | mock := &MockClient{ctrl: ctrl} 27 | mock.recorder = &MockClientMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockClient) EXPECT() *MockClientMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // CreateListener mocks base method 37 | func (m *MockClient) CreateListener(arg0 elbv2.CreateListenerParameters) (string, error) { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "CreateListener", arg0) 40 | ret0, _ := ret[0].(string) 41 | ret1, _ := ret[1].(error) 42 | return ret0, ret1 43 | } 44 | 45 | // CreateListener indicates an expected call of CreateListener 46 | func (mr *MockClientMockRecorder) CreateListener(arg0 interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateListener", reflect.TypeOf((*MockClient)(nil).CreateListener), arg0) 49 | } 50 | 51 | // CreateLoadBalancer mocks base method 52 | func (m *MockClient) CreateLoadBalancer(arg0 elbv2.CreateLoadBalancerParameters) (string, error) { 53 | m.ctrl.T.Helper() 54 | ret := m.ctrl.Call(m, "CreateLoadBalancer", arg0) 55 | ret0, _ := ret[0].(string) 56 | ret1, _ := ret[1].(error) 57 | return ret0, ret1 58 | } 59 | 60 | // CreateLoadBalancer indicates an expected call of CreateLoadBalancer 61 | func (mr *MockClientMockRecorder) CreateLoadBalancer(arg0 interface{}) *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLoadBalancer", reflect.TypeOf((*MockClient)(nil).CreateLoadBalancer), arg0) 64 | } 65 | 66 | // CreateTargetGroup mocks base method 67 | func (m *MockClient) CreateTargetGroup(arg0 elbv2.CreateTargetGroupParameters) (string, error) { 68 | m.ctrl.T.Helper() 69 | ret := m.ctrl.Call(m, "CreateTargetGroup", arg0) 70 | ret0, _ := ret[0].(string) 71 | ret1, _ := ret[1].(error) 72 | return ret0, ret1 73 | } 74 | 75 | // CreateTargetGroup indicates an expected call of CreateTargetGroup 76 | func (mr *MockClientMockRecorder) CreateTargetGroup(arg0 interface{}) *gomock.Call { 77 | mr.mock.ctrl.T.Helper() 78 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTargetGroup", reflect.TypeOf((*MockClient)(nil).CreateTargetGroup), arg0) 79 | } 80 | 81 | // DescribeListeners mocks base method 82 | func (m *MockClient) DescribeListeners(arg0 string) (elbv2.Listeners, error) { 83 | m.ctrl.T.Helper() 84 | ret := m.ctrl.Call(m, "DescribeListeners", arg0) 85 | ret0, _ := ret[0].(elbv2.Listeners) 86 | ret1, _ := ret[1].(error) 87 | return ret0, ret1 88 | } 89 | 90 | // DescribeListeners indicates an expected call of DescribeListeners 91 | func (mr *MockClientMockRecorder) DescribeListeners(arg0 interface{}) *gomock.Call { 92 | mr.mock.ctrl.T.Helper() 93 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeListeners", reflect.TypeOf((*MockClient)(nil).DescribeListeners), arg0) 94 | } 95 | 96 | // DescribeLoadBalancers mocks base method 97 | func (m *MockClient) DescribeLoadBalancers() (elbv2.LoadBalancers, error) { 98 | m.ctrl.T.Helper() 99 | ret := m.ctrl.Call(m, "DescribeLoadBalancers") 100 | ret0, _ := ret[0].(elbv2.LoadBalancers) 101 | ret1, _ := ret[1].(error) 102 | return ret0, ret1 103 | } 104 | 105 | // DescribeLoadBalancers indicates an expected call of DescribeLoadBalancers 106 | func (mr *MockClientMockRecorder) DescribeLoadBalancers() *gomock.Call { 107 | mr.mock.ctrl.T.Helper() 108 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeLoadBalancers", reflect.TypeOf((*MockClient)(nil).DescribeLoadBalancers)) 109 | } 110 | 111 | // DescribeLoadBalancersByName mocks base method 112 | func (m *MockClient) DescribeLoadBalancersByName(arg0 []string) (elbv2.LoadBalancers, error) { 113 | m.ctrl.T.Helper() 114 | ret := m.ctrl.Call(m, "DescribeLoadBalancersByName", arg0) 115 | ret0, _ := ret[0].(elbv2.LoadBalancers) 116 | ret1, _ := ret[1].(error) 117 | return ret0, ret1 118 | } 119 | 120 | // DescribeLoadBalancersByName indicates an expected call of DescribeLoadBalancersByName 121 | func (mr *MockClientMockRecorder) DescribeLoadBalancersByName(arg0 interface{}) *gomock.Call { 122 | mr.mock.ctrl.T.Helper() 123 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeLoadBalancersByName", reflect.TypeOf((*MockClient)(nil).DescribeLoadBalancersByName), arg0) 124 | } 125 | -------------------------------------------------------------------------------- /elbv2/mock/sdk/paginators.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/service/elbv2" 5 | "github.com/aws/aws-sdk-go/service/elbv2/elbv2iface" 6 | ) 7 | 8 | type MockDescribeLoadBalancersClient struct { 9 | elbv2iface.ELBV2API 10 | Resp *elbv2.DescribeLoadBalancersOutput 11 | Error error 12 | } 13 | 14 | type MockDescribeListenersClient struct { 15 | elbv2iface.ELBV2API 16 | Resp *elbv2.DescribeListenersOutput 17 | Error error 18 | } 19 | 20 | func (m MockDescribeLoadBalancersClient) DescribeLoadBalancersPages(in *elbv2.DescribeLoadBalancersInput, fn func(*elbv2.DescribeLoadBalancersOutput, bool) bool) error { 21 | if m.Error != nil { 22 | return m.Error 23 | } 24 | 25 | fn(m.Resp, true) 26 | 27 | return nil 28 | } 29 | 30 | func (m MockDescribeListenersClient) DescribeListenersPages(in *elbv2.DescribeListenersInput, fn func(*elbv2.DescribeListenersOutput, bool) bool) error { 31 | if m.Error != nil { 32 | return m.Error 33 | } 34 | 35 | fn(m.Resp, true) 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /elbv2/target_group.go: -------------------------------------------------------------------------------- 1 | package elbv2 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | awselbv2 "github.com/aws/aws-sdk-go/service/elbv2" 6 | "github.com/awslabs/fargatecli/console" 7 | ) 8 | 9 | type TargetGroup struct { 10 | Name string 11 | Arn string 12 | LoadBalancerARN string 13 | } 14 | 15 | type CreateTargetGroupParameters struct { 16 | Name string 17 | Port int64 18 | Protocol string 19 | VPCID string 20 | } 21 | 22 | func (elbv2 SDKClient) CreateTargetGroup(i CreateTargetGroupParameters) (string, error) { 23 | resp, err := elbv2.client.CreateTargetGroup( 24 | &awselbv2.CreateTargetGroupInput{ 25 | Name: aws.String(i.Name), 26 | Port: aws.Int64(i.Port), 27 | Protocol: aws.String(i.Protocol), 28 | TargetType: aws.String(awselbv2.TargetTypeEnumIp), 29 | VpcId: aws.String(i.VPCID), 30 | }, 31 | ) 32 | 33 | if err != nil { 34 | return "", err 35 | } 36 | 37 | return aws.StringValue(resp.TargetGroups[0].TargetGroupArn), nil 38 | } 39 | 40 | func (elbv2 SDKClient) DeleteTargetGroup(targetGroupName string) { 41 | console.Debug("Deleting ELB target group") 42 | 43 | targetGroup := elbv2.describeTargetGroupByName(targetGroupName) 44 | 45 | elbv2.client.DeleteTargetGroup( 46 | &awselbv2.DeleteTargetGroupInput{ 47 | TargetGroupArn: targetGroup.TargetGroupArn, 48 | }, 49 | ) 50 | } 51 | 52 | func (elbv2 SDKClient) DeleteTargetGroupByArn(targetGroupARN string) { 53 | _, err := elbv2.client.DeleteTargetGroup( 54 | &awselbv2.DeleteTargetGroupInput{ 55 | TargetGroupArn: aws.String(targetGroupARN), 56 | }, 57 | ) 58 | 59 | if err != nil { 60 | console.ErrorExit(err, "Could not delete ELB target group") 61 | } 62 | } 63 | 64 | func (elbv2 SDKClient) GetTargetGroupArn(targetGroupName string) string { 65 | resp, _ := elbv2.client.DescribeTargetGroups( 66 | &awselbv2.DescribeTargetGroupsInput{ 67 | Names: aws.StringSlice([]string{targetGroupName}), 68 | }, 69 | ) 70 | 71 | if len(resp.TargetGroups) == 1 { 72 | return aws.StringValue(resp.TargetGroups[0].TargetGroupArn) 73 | } 74 | 75 | return "" 76 | } 77 | 78 | func (elbv2 SDKClient) GetTargetGroupLoadBalancerArn(targetGroupARN string) string { 79 | targetGroup := elbv2.describeTargetGroupByArn(targetGroupARN) 80 | 81 | if len(targetGroup.LoadBalancerArns) > 0 { 82 | return aws.StringValue(targetGroup.LoadBalancerArns[0]) 83 | } else { 84 | return "" 85 | } 86 | } 87 | 88 | func (elbv2 SDKClient) DescribeTargetGroups(targetGroupARNs []string) []TargetGroup { 89 | var targetGroups []TargetGroup 90 | 91 | resp, err := elbv2.client.DescribeTargetGroups( 92 | &awselbv2.DescribeTargetGroupsInput{ 93 | TargetGroupArns: aws.StringSlice(targetGroupARNs), 94 | }, 95 | ) 96 | 97 | if err != nil { 98 | console.ErrorExit(err, "Could not describe ELB target groups") 99 | } 100 | 101 | for _, targetGroup := range resp.TargetGroups { 102 | tg := TargetGroup{ 103 | Name: aws.StringValue(targetGroup.TargetGroupName), 104 | Arn: aws.StringValue(targetGroup.TargetGroupArn), 105 | } 106 | 107 | if len(targetGroup.LoadBalancerArns) > 0 { 108 | tg.LoadBalancerARN = aws.StringValue(targetGroup.LoadBalancerArns[0]) 109 | } 110 | 111 | targetGroups = append(targetGroups, tg) 112 | } 113 | 114 | return targetGroups 115 | } 116 | 117 | func (elbv2 SDKClient) describeTargetGroupByName(targetGroupName string) *awselbv2.TargetGroup { 118 | resp, err := elbv2.client.DescribeTargetGroups( 119 | &awselbv2.DescribeTargetGroupsInput{ 120 | Names: aws.StringSlice([]string{targetGroupName}), 121 | }, 122 | ) 123 | 124 | if err != nil { 125 | console.ErrorExit(err, "Could not describe ELB target groups") 126 | } 127 | 128 | if len(resp.TargetGroups) != 1 { 129 | console.IssueExit("Could not describe ELB target groups") 130 | } 131 | 132 | return resp.TargetGroups[0] 133 | } 134 | 135 | func (elbv2 SDKClient) describeTargetGroupByArn(targetGroupARN string) *awselbv2.TargetGroup { 136 | resp, err := elbv2.client.DescribeTargetGroups( 137 | &awselbv2.DescribeTargetGroupsInput{ 138 | TargetGroupArns: aws.StringSlice([]string{targetGroupARN}), 139 | }, 140 | ) 141 | 142 | if err != nil { 143 | console.ErrorExit(err, "Could not describe ELB target groups") 144 | } 145 | 146 | if len(resp.TargetGroups) != 1 { 147 | console.IssueExit("Could not describe ELB target groups") 148 | } 149 | 150 | return resp.TargetGroups[0] 151 | } 152 | -------------------------------------------------------------------------------- /elbv2/target_group_test.go: -------------------------------------------------------------------------------- 1 | package elbv2 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | awselbv2 "github.com/aws/aws-sdk-go/service/elbv2" 9 | "github.com/golang/mock/gomock" 10 | "github.com/awslabs/fargatecli/elbv2/mock/sdk" 11 | ) 12 | 13 | func TestCreateTargetGroup(t *testing.T) { 14 | name := "default" 15 | port := int64(80) 16 | protocol := "HTTP" 17 | vpcID := "vpc-1234567" 18 | targetGroupARN := "arn:aws:elasticloadbalancing:us-west-2:123456789012:targetgroup/my-targets/73e2d6bc24d8a067" 19 | 20 | mockCtrl := gomock.NewController(t) 21 | defer mockCtrl.Finish() 22 | 23 | mockELBV2API := sdk.NewMockELBV2API(mockCtrl) 24 | elbv2 := SDKClient{client: mockELBV2API} 25 | 26 | i := &awselbv2.CreateTargetGroupInput{ 27 | Name: aws.String(name), 28 | Port: aws.Int64(port), 29 | Protocol: aws.String(protocol), 30 | TargetType: aws.String("ip"), 31 | VpcId: aws.String(vpcID), 32 | } 33 | o := &awselbv2.CreateTargetGroupOutput{ 34 | TargetGroups: []*awselbv2.TargetGroup{ 35 | &awselbv2.TargetGroup{ 36 | TargetGroupArn: aws.String(targetGroupARN), 37 | }, 38 | }, 39 | } 40 | 41 | mockELBV2API.EXPECT().CreateTargetGroup(i).Return(o, nil) 42 | 43 | arn, err := elbv2.CreateTargetGroup( 44 | CreateTargetGroupParameters{ 45 | Name: name, 46 | Port: port, 47 | Protocol: protocol, 48 | VPCID: vpcID, 49 | }, 50 | ) 51 | 52 | if err != nil { 53 | t.Fatalf("expected no error, got %v", err) 54 | } 55 | 56 | if arn == "" { 57 | t.Errorf("expected ARN %s, got %s", targetGroupARN, arn) 58 | } 59 | } 60 | 61 | func TestCreateTargetGroupError(t *testing.T) { 62 | mockCtrl := gomock.NewController(t) 63 | defer mockCtrl.Finish() 64 | 65 | mockELBV2API := sdk.NewMockELBV2API(mockCtrl) 66 | elbv2 := SDKClient{client: mockELBV2API} 67 | 68 | mockELBV2API.EXPECT().CreateTargetGroup(gomock.Any()).Return(&awselbv2.CreateTargetGroupOutput{}, errors.New("boom")) 69 | 70 | arn, err := elbv2.CreateTargetGroup( 71 | CreateTargetGroupParameters{ 72 | Name: "default", 73 | Port: int64(80), 74 | Protocol: "HTTP", 75 | VPCID: "vpc-1234567", 76 | }, 77 | ) 78 | 79 | if err == nil { 80 | t.Fatalf("expected error, got none") 81 | } 82 | 83 | if arn != "" { 84 | t.Errorf("expected empty ARN, got %s", arn) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /git/main.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "strings" 7 | 8 | "github.com/awslabs/fargatecli/console" 9 | ) 10 | 11 | func GetShortSha() string { 12 | var sha string 13 | 14 | cmd := exec.Command("git", "rev-parse", "--short", "HEAD") 15 | 16 | if console.Verbose { 17 | cmd.Stderr = os.Stderr 18 | } 19 | 20 | if out, err := cmd.Output(); err == nil { 21 | sha = strings.TrimSpace(string(out)) 22 | } else { 23 | console.ErrorExit(err, "Could not find git HEAD short SHA") 24 | } 25 | 26 | return sha 27 | } 28 | 29 | func IsCwdGitRepo() bool { 30 | cmd := exec.Command("git", "rev-parse", "--is-inside-work-tree") 31 | err := cmd.Run() 32 | 33 | return err == nil 34 | } 35 | -------------------------------------------------------------------------------- /git/main_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestGetShortSha(t *testing.T) { 12 | cwd, err := os.Getwd() 13 | 14 | if err != nil { 15 | t.Error("Could not read current working directory", err) 16 | return 17 | } 18 | 19 | dir, err := ioutil.TempDir("", "fargate-tests") 20 | 21 | if err != nil { 22 | t.Error("Could not create temporary directory", err) 23 | return 24 | } 25 | defer os.RemoveAll(dir) 26 | 27 | os.Chdir(dir) 28 | defer os.Chdir(cwd) 29 | 30 | exec.Command("git", "init").Run() 31 | 32 | gitCommit := exec.Command("git", "commit", "--allow-empty", "--message", "dummy commit") 33 | commitOutput, err := gitCommit.CombinedOutput() 34 | 35 | if err != nil { 36 | t.Errorf("Could not create dummy git commit: %v", err) 37 | t.Errorf("Output: %s", commitOutput) 38 | t.FailNow() 39 | } 40 | 41 | if shortSha := GetShortSha(); !strings.Contains(string(commitOutput), GetShortSha()) { 42 | t.Errorf("expected %s to contain %s", commitOutput, shortSha) 43 | } 44 | } 45 | 46 | func TestIsCwdGitRepoAgainstADir(t *testing.T) { 47 | cwd, err := os.Getwd() 48 | 49 | if err != nil { 50 | t.Error("Could not read current working directory", err) 51 | return 52 | } 53 | 54 | dir, err := ioutil.TempDir("", "fargate-tests") 55 | 56 | if err != nil { 57 | t.Error("Could not create temporary directory", err) 58 | return 59 | } 60 | defer os.RemoveAll(dir) 61 | 62 | os.Chdir(dir) 63 | defer os.Chdir(cwd) 64 | 65 | if isCwdGitRepo := IsCwdGitRepo(); isCwdGitRepo { 66 | t.Errorf("wanted false, got %+v", isCwdGitRepo) 67 | } 68 | } 69 | 70 | func TestIsCwdGitRepoAgainstARepo(t *testing.T) { 71 | cwd, err := os.Getwd() 72 | 73 | if err != nil { 74 | t.Error("Could not read current working directory", err) 75 | return 76 | } 77 | 78 | dir, err := ioutil.TempDir("", "fargate-tests") 79 | 80 | if err != nil { 81 | t.Error("Could not create temporary directory", err) 82 | return 83 | } 84 | defer os.RemoveAll(dir) 85 | 86 | os.Chdir(dir) 87 | defer os.Chdir(cwd) 88 | 89 | cmd := exec.Command("git", "init") 90 | cmd.Run() 91 | 92 | if isCwdGitRepo := IsCwdGitRepo(); !isCwdGitRepo { 93 | t.Errorf("wanted true, got %+v", isCwdGitRepo) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/awslabs/fargatecli 2 | 3 | require ( 4 | github.com/aws/aws-sdk-go v1.30.7 5 | github.com/go-ini/ini v1.32.0 // indirect 6 | github.com/golang/mock v1.4.3 7 | github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47 8 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 9 | github.com/kyokomi/emoji v0.0.0-20161123144355-7e06b236c489 10 | github.com/mattn/go-colorable v0.0.9 // indirect 11 | github.com/mattn/go-isatty v0.0.3 // indirect 12 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b 13 | github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff // indirect 14 | github.com/spf13/cobra v0.0.3 15 | github.com/spf13/pflag v1.0.2 // indirect 16 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 17 | golang.org/x/time v0.0.0-20170927054726-6dc17368e09b 18 | gopkg.in/ini.v1 v1.42.0 // indirect 19 | ) 20 | 21 | go 1.13 22 | -------------------------------------------------------------------------------- /iam/main.go: -------------------------------------------------------------------------------- 1 | package iam 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/session" 5 | "github.com/aws/aws-sdk-go/service/iam" 6 | ) 7 | 8 | type IAM struct { 9 | svc *iam.IAM 10 | } 11 | 12 | func New(sess *session.Session) IAM { 13 | return IAM{ 14 | svc: iam.New(sess), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /iam/role.go: -------------------------------------------------------------------------------- 1 | package iam 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | awsiam "github.com/aws/aws-sdk-go/service/iam" 9 | ) 10 | 11 | const ecsTaskExecutionRoleName = "ecsTaskExecutionRole" 12 | const ecsTaskExecutionPolicyArn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" 13 | const ecsTaskExecutionRoleAssumeRolePolicyDocument = `{ 14 | "Version": "2012-10-17", 15 | "Statement": [ 16 | { 17 | "Sid": "", 18 | "Effect": "Allow", 19 | "Principal": { 20 | "Service": "ecs-tasks.amazonaws.com" 21 | }, 22 | "Action": "sts:AssumeRole" 23 | } 24 | ] 25 | }` 26 | 27 | func (iam *IAM) CreateEcsTaskExecutionRole() string { 28 | getRoleResp, err := iam.svc.GetRole( 29 | &awsiam.GetRoleInput{ 30 | RoleName: aws.String(ecsTaskExecutionRoleName), 31 | }, 32 | ) 33 | 34 | if err == nil { 35 | return *getRoleResp.Role.Arn 36 | } 37 | 38 | createRoleResp, err := iam.svc.CreateRole( 39 | &awsiam.CreateRoleInput{ 40 | AssumeRolePolicyDocument: aws.String(ecsTaskExecutionRoleAssumeRolePolicyDocument), 41 | RoleName: aws.String(ecsTaskExecutionRoleName), 42 | }, 43 | ) 44 | 45 | if err != nil { 46 | fmt.Println(err) 47 | os.Exit(1) 48 | } 49 | 50 | ecsTaskExecutionRoleArn := *createRoleResp.Role.Arn 51 | 52 | _, err = iam.svc.AttachRolePolicy( 53 | &awsiam.AttachRolePolicyInput{ 54 | RoleName: aws.String(ecsTaskExecutionRoleName), 55 | PolicyArn: aws.String(ecsTaskExecutionPolicyArn), 56 | }, 57 | ) 58 | 59 | if err != nil { 60 | fmt.Println(err) 61 | os.Exit(1) 62 | } 63 | 64 | return ecsTaskExecutionRoleArn 65 | } 66 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/awslabs/fargatecli/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /route53/hosted_zone.go: -------------------------------------------------------------------------------- 1 | package route53 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | awsroute53 "github.com/aws/aws-sdk-go/service/route53" 9 | ) 10 | 11 | const defaultTTL = 86400 12 | 13 | // HostedZone is a zone hosted in Amazon Route 53. 14 | type HostedZone struct { 15 | Name string 16 | ID string 17 | } 18 | 19 | func (h HostedZone) isSuperDomainOf(fqdn string) bool { 20 | if !strings.HasSuffix(fqdn, ".") { 21 | fqdn = fqdn + "." 22 | } 23 | 24 | return strings.HasSuffix(fqdn, h.Name) 25 | } 26 | 27 | // HostedZones is a collection of HostedZones. 28 | type HostedZones []HostedZone 29 | 30 | // FindSuperDomainOf searches a HostedZones collection for the zone that is the superdomain of the 31 | // given fully qualified domain name. Returns a HostedZone and a boolean indicating whether a 32 | // match was found. 33 | func (h HostedZones) FindSuperDomainOf(fqdn string) (HostedZone, bool) { 34 | sort.Slice(h, func(i, j int) bool { 35 | return len(h[i].Name) > len(h[j].Name) 36 | }) 37 | 38 | for _, zone := range h { 39 | if zone.isSuperDomainOf(fqdn) { 40 | return zone, true 41 | } 42 | } 43 | 44 | return HostedZone{}, false 45 | } 46 | 47 | // CreateAliasInput holds configuration parameters for CreateAlias. 48 | type CreateAliasInput struct { 49 | HostedZoneID, Name, RecordType, Target, TargetHostedZoneID string 50 | } 51 | 52 | // CreateResourceRecordInput holds configuration parameters for CreateResourceRecord. 53 | type CreateResourceRecordInput struct { 54 | HostedZoneID, RecordType, Name, Value string 55 | } 56 | 57 | // CreateResourceRecord creates a DNS record in an Amazon Route 53 hosted zone. 58 | func (route53 SDKClient) CreateResourceRecord(i CreateResourceRecordInput) (string, error) { 59 | change := &awsroute53.Change{ 60 | Action: aws.String(awsroute53.ChangeActionUpsert), 61 | ResourceRecordSet: &awsroute53.ResourceRecordSet{ 62 | Name: aws.String(i.Name), 63 | Type: aws.String(i.RecordType), 64 | TTL: aws.Int64(defaultTTL), 65 | ResourceRecords: []*awsroute53.ResourceRecord{ 66 | &awsroute53.ResourceRecord{ 67 | Value: aws.String(i.Value), 68 | }, 69 | }, 70 | }, 71 | } 72 | 73 | resp, err := route53.client.ChangeResourceRecordSets( 74 | &awsroute53.ChangeResourceRecordSetsInput{ 75 | HostedZoneId: aws.String(i.HostedZoneID), 76 | ChangeBatch: &awsroute53.ChangeBatch{ 77 | Changes: []*awsroute53.Change{change}, 78 | }, 79 | }, 80 | ) 81 | 82 | if err != nil { 83 | return "", err 84 | } 85 | 86 | return aws.StringValue(resp.ChangeInfo.Id), nil 87 | } 88 | 89 | // CreateAlias creates an alias record in an Amazon Route 53 hosted zone. 90 | func (route53 SDKClient) CreateAlias(i CreateAliasInput) (string, error) { 91 | change := &awsroute53.Change{ 92 | Action: aws.String(awsroute53.ChangeActionUpsert), 93 | ResourceRecordSet: &awsroute53.ResourceRecordSet{ 94 | Name: aws.String(i.Name), 95 | Type: aws.String(i.RecordType), 96 | AliasTarget: &awsroute53.AliasTarget{ 97 | DNSName: aws.String(i.Target), 98 | EvaluateTargetHealth: aws.Bool(false), 99 | HostedZoneId: aws.String(i.TargetHostedZoneID), 100 | }, 101 | }, 102 | } 103 | 104 | resp, err := route53.client.ChangeResourceRecordSets( 105 | &awsroute53.ChangeResourceRecordSetsInput{ 106 | HostedZoneId: aws.String(i.HostedZoneID), 107 | ChangeBatch: &awsroute53.ChangeBatch{ 108 | Changes: []*awsroute53.Change{change}, 109 | }, 110 | }, 111 | ) 112 | 113 | if err != nil { 114 | return "", err 115 | } 116 | 117 | return aws.StringValue(resp.ChangeInfo.Id), nil 118 | } 119 | 120 | // ListHostedZones returns all Amazon Route 53 zones in the caller's account. 121 | func (route53 SDKClient) ListHostedZones() (HostedZones, error) { 122 | var hostedZones HostedZones 123 | 124 | input := &awsroute53.ListHostedZonesInput{} 125 | handler := func(resp *awsroute53.ListHostedZonesOutput, lastPage bool) bool { 126 | for _, hostedZone := range resp.HostedZones { 127 | hostedZones = append( 128 | hostedZones, 129 | HostedZone{ 130 | Name: aws.StringValue(hostedZone.Name), 131 | ID: aws.StringValue(hostedZone.Id), 132 | }, 133 | ) 134 | } 135 | 136 | return true 137 | } 138 | 139 | err := route53.client.ListHostedZonesPages(input, handler) 140 | 141 | return hostedZones, err 142 | } 143 | -------------------------------------------------------------------------------- /route53/hosted_zone_test.go: -------------------------------------------------------------------------------- 1 | package route53 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | awsroute53 "github.com/aws/aws-sdk-go/service/route53" 9 | "github.com/golang/mock/gomock" 10 | "github.com/awslabs/fargatecli/route53/mock/sdk" 11 | ) 12 | 13 | func TestHostedZonesFindSuperDomainOf(t *testing.T) { 14 | examplecom := HostedZone{Name: "example.com."} 15 | amazoncom := HostedZone{Name: "amazon.com."} 16 | intexamplecom := HostedZone{Name: "int.example.com."} 17 | 18 | var tests = []struct { 19 | fqdn string 20 | zones HostedZones 21 | zone HostedZone 22 | }{ 23 | {"staging.example.com", HostedZones{examplecom, amazoncom}, examplecom}, 24 | {"mail.int.example.com", HostedZones{examplecom, intexamplecom, amazoncom}, intexamplecom}, 25 | {"www.amazon.com", HostedZones{examplecom, intexamplecom, amazoncom}, amazoncom}, 26 | } 27 | 28 | for _, test := range tests { 29 | zone, ok := test.zones.FindSuperDomainOf(test.fqdn) 30 | 31 | if !ok { 32 | t.Errorf("No match found for %s, expected %s", test.fqdn, test.zone.Name) 33 | } else if zone != test.zone { 34 | t.Errorf("Expected %s to be superdomain of %s, got: %s", test.zone.Name, test.fqdn, zone.Name) 35 | } 36 | } 37 | } 38 | 39 | func TestHostedZonesFindSuperDomainOfNotFound(t *testing.T) { 40 | zones := HostedZones{ 41 | HostedZone{Name: "zombo.com."}, 42 | } 43 | 44 | zone, ok := zones.FindSuperDomainOf("www.example.com") 45 | 46 | if ok { 47 | t.Errorf("%s matched, expected none", zone.Name) 48 | } 49 | } 50 | 51 | func TestListHostedZones(t *testing.T) { 52 | resp := &awsroute53.ListHostedZonesOutput{ 53 | HostedZones: []*awsroute53.HostedZone{ 54 | &awsroute53.HostedZone{Id: aws.String("1"), Name: aws.String("example.com.")}, 55 | &awsroute53.HostedZone{Id: aws.String("2"), Name: aws.String("amazon.com.")}, 56 | }, 57 | } 58 | 59 | mockClient := sdk.MockListHostedZonesPagesClient{Resp: resp} 60 | route53 := SDKClient{client: mockClient} 61 | hostedZones, err := route53.ListHostedZones() 62 | 63 | if err != nil { 64 | t.Errorf("Expected no error, got %+v", err) 65 | } 66 | 67 | if len(hostedZones) != 2 { 68 | t.Errorf("Expected 2 hosted zones, got %d", len(hostedZones)) 69 | } 70 | 71 | if hostedZones[0].Name != "example.com." { 72 | t.Errorf("Expected hosted zone name to be example.com., got %s", hostedZones[0].Name) 73 | } 74 | } 75 | 76 | func TestListHostedZonesError(t *testing.T) { 77 | mockClient := sdk.MockListHostedZonesPagesClient{ 78 | Error: errors.New("boom"), 79 | Resp: &awsroute53.ListHostedZonesOutput{}, 80 | } 81 | route53 := SDKClient{client: mockClient} 82 | hostedZones, err := route53.ListHostedZones() 83 | 84 | if err == nil { 85 | t.Error("Expected error, got none") 86 | } 87 | 88 | if len(hostedZones) > 0 { 89 | t.Errorf("Expected no hosted zones, got %d", len(hostedZones)) 90 | } 91 | } 92 | 93 | func TestCreateResourceRecord(t *testing.T) { 94 | hostedZoneID := "zone1" 95 | hostedZone := HostedZone{Name: "example.com", ID: hostedZoneID} 96 | recordType := "CNAME" 97 | name := "www.example.com" 98 | value := "example.hosted-websites.com" 99 | 100 | mockCtrl := gomock.NewController(t) 101 | defer mockCtrl.Finish() 102 | 103 | mockRoute53API := sdk.NewMockRoute53API(mockCtrl) 104 | route53 := SDKClient{client: mockRoute53API} 105 | 106 | i := &awsroute53.ChangeResourceRecordSetsInput{ 107 | HostedZoneId: aws.String(hostedZoneID), 108 | ChangeBatch: &awsroute53.ChangeBatch{ 109 | Changes: []*awsroute53.Change{ 110 | &awsroute53.Change{ 111 | Action: aws.String(awsroute53.ChangeActionUpsert), 112 | ResourceRecordSet: &awsroute53.ResourceRecordSet{ 113 | Name: aws.String(name), 114 | Type: aws.String(recordType), 115 | TTL: aws.Int64(defaultTTL), 116 | ResourceRecords: []*awsroute53.ResourceRecord{ 117 | &awsroute53.ResourceRecord{ 118 | Value: aws.String(value), 119 | }, 120 | }, 121 | }, 122 | }, 123 | }, 124 | }, 125 | } 126 | o := &awsroute53.ChangeResourceRecordSetsOutput{ 127 | ChangeInfo: &awsroute53.ChangeInfo{ 128 | Id: aws.String("1"), 129 | }, 130 | } 131 | 132 | mockRoute53API.EXPECT().ChangeResourceRecordSets(i).Return(o, nil) 133 | 134 | id, err := route53.CreateResourceRecord( 135 | CreateResourceRecordInput{ 136 | HostedZoneID: hostedZone.ID, 137 | RecordType: recordType, 138 | Name: name, 139 | Value: value, 140 | }, 141 | ) 142 | 143 | if id != "1" { 144 | t.Errorf("Expected id == 1, got %s", id) 145 | } 146 | 147 | if err != nil { 148 | t.Errorf("Expected no error, got %v", err) 149 | } 150 | } 151 | 152 | func TestCreateAliasRecord(t *testing.T) { 153 | hostedZoneID := "zone1" 154 | targetHostedZoneID := "zone2" 155 | hostedZone := HostedZone{Name: "example.com", ID: hostedZoneID} 156 | recordType := "A" 157 | name := "www.example.com" 158 | target := "example.load-balancers.com" 159 | 160 | mockCtrl := gomock.NewController(t) 161 | defer mockCtrl.Finish() 162 | 163 | mockRoute53API := sdk.NewMockRoute53API(mockCtrl) 164 | route53 := SDKClient{client: mockRoute53API} 165 | 166 | i := &awsroute53.ChangeResourceRecordSetsInput{ 167 | HostedZoneId: aws.String(hostedZoneID), 168 | ChangeBatch: &awsroute53.ChangeBatch{ 169 | Changes: []*awsroute53.Change{ 170 | &awsroute53.Change{ 171 | Action: aws.String(awsroute53.ChangeActionUpsert), 172 | ResourceRecordSet: &awsroute53.ResourceRecordSet{ 173 | Name: aws.String(name), 174 | Type: aws.String(recordType), 175 | AliasTarget: &awsroute53.AliasTarget{ 176 | DNSName: aws.String(target), 177 | EvaluateTargetHealth: aws.Bool(false), 178 | HostedZoneId: aws.String(targetHostedZoneID), 179 | }, 180 | }, 181 | }, 182 | }, 183 | }, 184 | } 185 | o := &awsroute53.ChangeResourceRecordSetsOutput{ 186 | ChangeInfo: &awsroute53.ChangeInfo{ 187 | Id: aws.String("2"), 188 | }, 189 | } 190 | 191 | mockRoute53API.EXPECT().ChangeResourceRecordSets(i).Return(o, nil) 192 | 193 | id, err := route53.CreateAlias( 194 | CreateAliasInput{ 195 | HostedZoneID: hostedZone.ID, 196 | RecordType: recordType, 197 | Name: name, 198 | Target: target, 199 | TargetHostedZoneID: targetHostedZoneID, 200 | }, 201 | ) 202 | 203 | if id != "2" { 204 | t.Errorf("Expected id == 2, got %s", id) 205 | } 206 | 207 | if err != nil { 208 | t.Errorf("Expected no error, got %v", err) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /route53/main.go: -------------------------------------------------------------------------------- 1 | package route53 2 | 3 | //go:generate mockgen -package client -destination=mock/client/client.go github.com/awslabs/fargatecli/route53 Client 4 | //go:generate mockgen -package sdk -source ../vendor/github.com/aws/aws-sdk-go/service/route53/route53iface/interface.go -destination=mock/sdk/route53iface.go github.com/aws/aws-sdk-go/service/route53/route53iface Route53API 5 | 6 | import ( 7 | "github.com/aws/aws-sdk-go/aws/session" 8 | "github.com/aws/aws-sdk-go/service/route53" 9 | "github.com/aws/aws-sdk-go/service/route53/route53iface" 10 | ) 11 | 12 | // Client represents a method for accessing Amazon Route 53. 13 | type Client interface { 14 | CreateAlias(CreateAliasInput) (string, error) 15 | CreateResourceRecord(CreateResourceRecordInput) (string, error) 16 | ListHostedZones() (HostedZones, error) 17 | } 18 | 19 | // SDKClient implements access to Amazon Route 53 via the AWS SDK. 20 | type SDKClient struct { 21 | client route53iface.Route53API 22 | } 23 | 24 | // New returns an SDKClient configured with the given session. 25 | func New(sess *session.Session) SDKClient { 26 | return SDKClient{ 27 | client: route53.New(sess), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /route53/mock/client/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/awslabs/fargatecli/route53 (interfaces: Client) 3 | 4 | // Package client is a generated GoMock package. 5 | package client 6 | 7 | import ( 8 | route53 "github.com/awslabs/fargatecli/route53" 9 | gomock "github.com/golang/mock/gomock" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockClient is a mock of Client interface 14 | type MockClient struct { 15 | ctrl *gomock.Controller 16 | recorder *MockClientMockRecorder 17 | } 18 | 19 | // MockClientMockRecorder is the mock recorder for MockClient 20 | type MockClientMockRecorder struct { 21 | mock *MockClient 22 | } 23 | 24 | // NewMockClient creates a new mock instance 25 | func NewMockClient(ctrl *gomock.Controller) *MockClient { 26 | mock := &MockClient{ctrl: ctrl} 27 | mock.recorder = &MockClientMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockClient) EXPECT() *MockClientMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // CreateAlias mocks base method 37 | func (m *MockClient) CreateAlias(arg0 route53.CreateAliasInput) (string, error) { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "CreateAlias", arg0) 40 | ret0, _ := ret[0].(string) 41 | ret1, _ := ret[1].(error) 42 | return ret0, ret1 43 | } 44 | 45 | // CreateAlias indicates an expected call of CreateAlias 46 | func (mr *MockClientMockRecorder) CreateAlias(arg0 interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAlias", reflect.TypeOf((*MockClient)(nil).CreateAlias), arg0) 49 | } 50 | 51 | // CreateResourceRecord mocks base method 52 | func (m *MockClient) CreateResourceRecord(arg0 route53.CreateResourceRecordInput) (string, error) { 53 | m.ctrl.T.Helper() 54 | ret := m.ctrl.Call(m, "CreateResourceRecord", arg0) 55 | ret0, _ := ret[0].(string) 56 | ret1, _ := ret[1].(error) 57 | return ret0, ret1 58 | } 59 | 60 | // CreateResourceRecord indicates an expected call of CreateResourceRecord 61 | func (mr *MockClientMockRecorder) CreateResourceRecord(arg0 interface{}) *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateResourceRecord", reflect.TypeOf((*MockClient)(nil).CreateResourceRecord), arg0) 64 | } 65 | 66 | // ListHostedZones mocks base method 67 | func (m *MockClient) ListHostedZones() (route53.HostedZones, error) { 68 | m.ctrl.T.Helper() 69 | ret := m.ctrl.Call(m, "ListHostedZones") 70 | ret0, _ := ret[0].(route53.HostedZones) 71 | ret1, _ := ret[1].(error) 72 | return ret0, ret1 73 | } 74 | 75 | // ListHostedZones indicates an expected call of ListHostedZones 76 | func (mr *MockClientMockRecorder) ListHostedZones() *gomock.Call { 77 | mr.mock.ctrl.T.Helper() 78 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListHostedZones", reflect.TypeOf((*MockClient)(nil).ListHostedZones)) 79 | } 80 | -------------------------------------------------------------------------------- /route53/mock/sdk/paginators.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/service/route53" 5 | "github.com/aws/aws-sdk-go/service/route53/route53iface" 6 | ) 7 | 8 | type MockListHostedZonesPagesClient struct { 9 | route53iface.Route53API 10 | Resp *route53.ListHostedZonesOutput 11 | Error error 12 | } 13 | 14 | func (m MockListHostedZonesPagesClient) ListHostedZonesPages(in *route53.ListHostedZonesInput, fn func(*route53.ListHostedZonesOutput, bool) bool) error { 15 | if m.Error != nil { 16 | return m.Error 17 | } 18 | 19 | fn(m.Resp, true) 20 | 21 | return nil 22 | } 23 | --------------------------------------------------------------------------------