├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── _sample_apps └── echo │ ├── .gitignore │ ├── Dockerfile │ ├── Makefile │ └── main.go ├── aws ├── autoscaling │ └── client.go ├── client.go ├── consts.go ├── ec2 │ └── client.go ├── ecr │ └── client.go ├── ecs │ ├── client.go │ ├── load_balancer.go │ └── port_mapping.go ├── elb │ ├── client.go │ └── health_check.go ├── iam │ └── client.go ├── logs │ └── client.go ├── sns │ └── client.go └── utils.go ├── commands ├── clustercreate │ ├── aws.go │ ├── command.go │ └── flags.go ├── clusterdelete │ ├── aws.go │ ├── command.go │ └── flags.go ├── clusterscale │ ├── command.go │ └── flags.go ├── clusterstatus │ ├── aws.go │ ├── command.go │ └── flags.go ├── command.go ├── create │ ├── command.go │ └── flags.go ├── delete │ ├── command.go │ └── flags.go ├── deploy │ ├── aws_ecs.go │ ├── aws_elb.go │ ├── command.go │ ├── docker.go │ ├── flags.go │ └── flags_test.go └── status │ ├── command.go │ └── flags.go ├── config ├── config.go ├── config_test.go ├── default_config.go ├── default_config_test.go ├── load.go ├── load_test.go ├── persist.go ├── persist_test.go ├── validate.go └── validate_test.go ├── console ├── ask.go ├── console.go └── output.go ├── core ├── apps.go ├── aws.go ├── clusters.go ├── errors.go └── validation.go ├── docker └── client.go ├── exec └── exec.go ├── flags ├── flags_test.go ├── global_flags.go └── global_flags_test.go ├── glide.lock ├── glide.yaml ├── main.go └── utils ├── conv ├── conv.go └── conv_test.go ├── utils.go └── utils_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | vendor/ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.9 5 | 6 | before_install: 7 | - pip install --user awscli 8 | - export PATH=$PATH:$HOME/.local/bin 9 | - wget "https://github.com/Masterminds/glide/releases/download/v0.12.3/glide-v0.12.3-linux-amd64.tar.gz" 10 | - mkdir -p $HOME/bin 11 | - tar -vxz -C $HOME/bin --strip=1 -f glide-v0.12.3-linux-amd64.tar.gz 12 | - rm glide-v0.12.3-linux-amd64.tar.gz 13 | - export PATH="$HOME/bin:$PATH" 14 | 15 | install: 16 | - make deps 17 | 18 | script: 19 | - make build 20 | 21 | deploy: 22 | provider: s3 23 | access_key_id: $AWS_ACCESS_KEY_ID 24 | secret_access_key: $AWS_SECRET_ACCESS_KEY 25 | bucket: files.coldbrewcloud.com 26 | region: us-west-2 27 | acl: public_read 28 | skip_cleanup: true 29 | local_dir: bin 30 | upload-dir: cli 31 | detect_encoding: true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 COLDBREW CLOUD LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION := $(shell cat VERSION) 2 | OSS := linux darwin windows 3 | 4 | deps: 5 | glide -q install 6 | 7 | test: deps 8 | go test `glide nv` 9 | 10 | build: test 11 | @for OS in $(OSS); do \ 12 | echo "Building $$OS..."; \ 13 | export OUTFILE=coldbrew; if [ ! "$$OS" != "windows" ]; then export OUTFILE=coldbrew.exe; fi; \ 14 | GOOS=$$OS GOARCH=amd64 CGO_ENABLED=0 go build -tags production -ldflags "-X main.appVersion=$(VERSION)" -o bin/$$OS/amd64/v$(VERSION)/$$OUTFILE; \ 15 | (cd bin/$$OS/amd64/v$(VERSION); tar -cvzf coldbrew.tar.gz $$OUTFILE; rm $$OUTFILE); \ 16 | GOOS=$$OS GOARCH=386 CGO_ENABLED=0 go build -tags production -ldflags "-X main.appVersion=$(VERSION)" -o bin/$$OS/386/v$(VERSION)/$$OUTFILE; \ 17 | (cd bin/$$OS/386/v$(VERSION); tar -cvzf coldbrew.tar.gz $$OUTFILE; rm $$OUTFILE); \ 18 | mkdir -p bin/$$OS/amd64/latest/; cp bin/$$OS/amd64/v$(VERSION)/* bin/$$OS/amd64/latest/; \ 19 | mkdir -p bin/$$OS/386/latest/; cp bin/$$OS/386/v$(VERSION)/* bin/$$OS/386/latest/; \ 20 | done 21 | 22 | .PHONY: deps test build -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # coldbrew-cli 2 | 3 | **NOTE: Unfortunately, coldbrew-cli is not actively maintained at the moment. [ecs-cli](https://github.com/aws/amazon-ecs-cli) could be an option instead.** 4 | 5 | ### Objectives 6 | 7 | **coldbrew-cli** can provide 8 | 9 | * faster access to ECS _(jumpstart with little knowledge on AWS specifics)_ 10 | * lower maintenance costs _(most cases you don't even need AWS console or SDK)_ 11 | * lessen mistakes by removing boring repetitions 12 | * easier integration with CI 13 | 14 | ### Features 15 | 16 | - ECS cluster with EC2 Auto Scaling Group configured 17 | - Support ELB Application Load Balance _(multiple app instances on a single EC2 instance)_ 18 | - Logging: most of Docker logging drivers and AWS CloudWatch Logs 19 | 20 | ## Getting Started 21 | 22 | ### Install and Configure CLI 23 | 24 | - [Download](https://github.com/coldbrewcloud/coldbrew-cli/wiki/Downloads) CLI executable (`coldbrew` or `coldbrew.exe`) and put it in your `$PATH`. 25 | - Configure AWS credentials, region, and VPC through [environment variables](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Environment-Variables) or [CLI Flags](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Global-Flags). 26 | - Make sure you have [docker](https://docs.docker.com/engine/installation/) installed in your system. You will also need [Dockerfile](https://docs.docker.com/engine/reference/builder/) for your application if you want to build Docker image using **coldbrew-cli**. 27 | 28 | ### Core Concepts 29 | 30 | **coldbrew-cli** operates on two simple concepts: applications _(apps)_ and clusters. 31 | 32 | - An **app** is the minimum deployment unit. 33 | - One or more apps can run in a **cluster**, and, they share the computing resources. 34 | 35 | 36 | 37 | This is what a typical deployment workflow might look like: 38 | 39 | 1. Create new cluster _(See: [cluster-create](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-cluster-create))_ 40 | 2. Create app configuration _(See: [init](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-init))_ 41 | 3. Development iteration: 42 | - Make code/configuration changes 43 | - Deploy app to cluster _(See [deploy](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-deploy))_ 44 | - Check app/cluster status _(See: [status](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-status) and [cluster-status](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-cluster-status))_ and adjust cluster capacity as needed _(See: [cluster-scale](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-cluster-scale))_ 45 | 4. Delete app and its resources _(See: [delete](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-delete) )_ 46 | 5. Delete cluster and its resources _(See: [cluster-delete](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-cluster-delete))_ 47 | 48 | See [Concepts](https://github.com/coldbrewcloud/coldbrew-cli/wiki/Concepts) for more details. 49 | 50 | ### Tutorials 51 | 52 | Check out tutorials: 53 | - [Running a Node.JS application on AWS](https://github.com/coldbrewcloud/tutorial-nodejs) 54 | - [Running a Slack bot on AWS](https://github.com/coldbrewcloud/tutorial-echo-slack-bot) 55 | - [Running a Meteor application on AWS](https://github.com/coldbrewcloud/tutorial-meteor) 56 | - [Running a Go application on AWS](https://github.com/coldbrewcloud/tutorial-echo) 57 | - [Running a scalable WordPress website on AWS](https://github.com/coldbrewcloud/tutorial-wordpress) 58 | 59 | ## Core Functions 60 | 61 | ### Create Cluster 62 | 63 | To start deploying your applications, you need to have at least one cluster set up. 64 | 65 | ```bash 66 | coldbrew cluster-create {cluster-name} 67 | ``` 68 | 69 | [cluster-create](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-cluster-create) command will look into your current AWS environment, and, will perform all necessary changes to build the cluster. Note that it can take several minutes until all Docker hosts (EC2 instances) become fully available in your cluster. Use [cluster-status](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-cluster-status) command to check the status. You can also adjust the cluster's computing capacity using [cluster-scale](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-cluster-scale) command. 70 | 71 | 72 | 73 | ### Configure App 74 | 75 | The next step is prepare the app [configuration file](https://github.com/coldbrewcloud/coldbrew-cli/wiki/Configuration-File). 76 | 77 | ```bash 78 | coldbrew init --default 79 | ``` 80 | 81 | You can manually create/edit your configuration file, or, you can use [init](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-init) command to generate a proper default configuraiton. 82 | 83 | 84 | 85 | ### Deploy App 86 | 87 | Once the configuration file is ready, now you can deploy your app in the cluster. 88 | 89 | ```bash 90 | coldbrew deploy 91 | ``` 92 | 93 | Basically [deploy](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-deploy) command does: 94 | - build Docker image using your `Dockerfile` _(but this is completely optional if provide your own local Docker image; see [--docker-image](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-deploy#--docker-image) flag)_ 95 | - push Docker image to a remote repository (ECR) 96 | - analyze the current AWS environment and setup, and, perform all necessary changes to initiate ECS deployments 97 | 98 | Then, within a couple minutes _(mostly less than a minute)_, you will see your new application units up and running. 99 | 100 | 101 | 102 | ### Check Status 103 | 104 | You can use [status](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-status) and [cluster-status](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-cluster-status) commands to check the running status of your app and cluster respectively. 105 | 106 | ```bash 107 | coldbrew status 108 | ``` 109 | 110 | 111 | 112 | ```bash 113 | coldbrew cluster-status {cluster-name} 114 | ``` 115 | 116 | 117 | 118 | ### Delete App 119 | 120 | When you no longer need your app, you can remove your app from the cluster using [delete](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-delete) command. 121 | 122 | ```bash 123 | coldbrew delete 124 | ``` 125 | 126 | [delete](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-delete) command gathers a list of AWS resources that need to be deleted, and, if you confirm, it will start cleaning them up. It can take several minutes for the full process. 127 | 128 | 129 | 130 | 131 | ### Delete Cluster 132 | 133 | You can use a cluster for more than one apps, but, when you no longer need the cluster, you use [cluster-delete](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-cluster-delete) command to clean up all the resources. 134 | 135 | ```bash 136 | coldbrew cluster-delete 137 | ``` 138 | 139 | Similar to [delete](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-delete) command, [cluster-delete](https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-cluster-delete) will delete all AWS resources that are no longer needed. It can take several minutes for the full process. 140 | 141 | 142 | 143 | ## Documentations 144 | 145 | - [Documentations Home](https://github.com/coldbrewcloud/coldbrew-cli/wiki) 146 | - [Managed AWS Resources](https://github.com/coldbrewcloud/coldbrew-cli/wiki/Managed-AWS-Resources) 147 | - [FAQ](https://github.com/coldbrewcloud/coldbrew-cli/wiki/FAQ) 148 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.4.3 -------------------------------------------------------------------------------- /_sample_apps/echo/.gitignore: -------------------------------------------------------------------------------- 1 | echo 2 | coldbrew.conf -------------------------------------------------------------------------------- /_sample_apps/echo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.4 2 | 3 | COPY echo /echo 4 | 5 | EXPOSE 8888 6 | 7 | CMD ["/echo"] -------------------------------------------------------------------------------- /_sample_apps/echo/Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o echo 3 | 4 | .PHONY: build -------------------------------------------------------------------------------- /_sample_apps/echo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "net/http" 8 | ) 9 | 10 | func echo(w http.ResponseWriter, r *http.Request) { 11 | data, err := ioutil.ReadAll(r.Body) 12 | if err != nil { 13 | w.WriteHeader(http.StatusInternalServerError) 14 | w.Write([]byte(err.Error())) 15 | return 16 | } 17 | 18 | fmt.Println(string(data)) 19 | io.WriteString(w, string(data)) 20 | } 21 | 22 | func main() { 23 | http.HandleFunc("/", echo) 24 | http.ListenAndServe("0.0.0.0:8888", nil) 25 | } 26 | -------------------------------------------------------------------------------- /aws/autoscaling/client.go: -------------------------------------------------------------------------------- 1 | package autoscaling 2 | 3 | import ( 4 | "strings" 5 | 6 | "fmt" 7 | 8 | _aws "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/aws/session" 10 | _autoscaling "github.com/aws/aws-sdk-go/service/autoscaling" 11 | "github.com/coldbrewcloud/coldbrew-cli/utils" 12 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 13 | ) 14 | 15 | type Client struct { 16 | svc *_autoscaling.AutoScaling 17 | } 18 | 19 | func New(session *session.Session, config *_aws.Config) *Client { 20 | return &Client{ 21 | svc: _autoscaling.New(session, config), 22 | } 23 | } 24 | 25 | func (c *Client) CreateLaunchConfiguration(launchConfigurationName, instanceType, imageID string, securityGroupIDs []string, keyPairName, iamInstanceProfileNameOrARN, userData string) error { 26 | params := &_autoscaling.CreateLaunchConfigurationInput{ 27 | IamInstanceProfile: _aws.String(iamInstanceProfileNameOrARN), 28 | ImageId: _aws.String(imageID), 29 | InstanceType: _aws.String(instanceType), 30 | LaunchConfigurationName: _aws.String(launchConfigurationName), 31 | SecurityGroups: _aws.StringSlice(securityGroupIDs), 32 | UserData: _aws.String(userData), 33 | InstanceMonitoring: &_autoscaling.InstanceMonitoring{Enabled: _aws.Bool(false)}, 34 | } 35 | 36 | if !utils.IsBlank(keyPairName) { 37 | params.KeyName = _aws.String(keyPairName) 38 | } 39 | 40 | _, err := c.svc.CreateLaunchConfiguration(params) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func (c *Client) RetrieveLaunchConfiguration(launchConfigurationName string) (*_autoscaling.LaunchConfiguration, error) { 49 | params := &_autoscaling.DescribeLaunchConfigurationsInput{ 50 | LaunchConfigurationNames: _aws.StringSlice([]string{launchConfigurationName}), 51 | } 52 | 53 | res, err := c.svc.DescribeLaunchConfigurations(params) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | if res != nil && len(res.LaunchConfigurations) > 0 { 59 | return res.LaunchConfigurations[0], nil 60 | } 61 | 62 | return nil, nil 63 | } 64 | 65 | func (c *Client) DeleteLaunchConfiguration(launchConfigurationName string) error { 66 | params := &_autoscaling.DeleteLaunchConfigurationInput{ 67 | LaunchConfigurationName: _aws.String(launchConfigurationName), 68 | } 69 | 70 | _, err := c.svc.DeleteLaunchConfiguration(params) 71 | 72 | return err 73 | } 74 | 75 | func (c *Client) CreateAutoScalingGroup(autoScalingGroupName, launchConfigurationName string, subnetIDs []string, minCapacity, maxCapacity, initialCapacity uint16) error { 76 | params := &_autoscaling.CreateAutoScalingGroupInput{ 77 | AutoScalingGroupName: _aws.String(autoScalingGroupName), 78 | DesiredCapacity: _aws.Int64(int64(initialCapacity)), 79 | LaunchConfigurationName: _aws.String(launchConfigurationName), 80 | MaxSize: _aws.Int64(int64(maxCapacity)), 81 | MinSize: _aws.Int64(int64(minCapacity)), 82 | VPCZoneIdentifier: _aws.String(strings.Join(subnetIDs, ",")), 83 | } 84 | 85 | _, err := c.svc.CreateAutoScalingGroup(params) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | return nil 91 | } 92 | 93 | func (c *Client) RetrieveAutoScalingGroup(autoScalingGroupName string) (*_autoscaling.Group, error) { 94 | params := &_autoscaling.DescribeAutoScalingGroupsInput{ 95 | AutoScalingGroupNames: _aws.StringSlice([]string{autoScalingGroupName}), 96 | } 97 | 98 | res, err := c.svc.DescribeAutoScalingGroups(params) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | if res != nil && len(res.AutoScalingGroups) > 0 { 104 | return res.AutoScalingGroups[0], nil 105 | } 106 | 107 | return nil, nil 108 | } 109 | 110 | func (c *Client) UpdateAutoScalingGroupCapacity(autoScalingGroupName string, minCapacity, maxCapacity, desiredCapacity uint16) error { 111 | params := &_autoscaling.UpdateAutoScalingGroupInput{ 112 | AutoScalingGroupName: _aws.String(autoScalingGroupName), 113 | DesiredCapacity: _aws.Int64(int64(desiredCapacity)), 114 | MaxSize: _aws.Int64(int64(maxCapacity)), 115 | MinSize: _aws.Int64(int64(minCapacity)), 116 | } 117 | 118 | _, err := c.svc.UpdateAutoScalingGroup(params) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | return nil 124 | } 125 | 126 | func (c *Client) SetAutoScalingGroupDesiredCapacity(autoScalingGroupName string, desiredCapacity uint16) error { 127 | params := &_autoscaling.SetDesiredCapacityInput{ 128 | AutoScalingGroupName: _aws.String(autoScalingGroupName), 129 | DesiredCapacity: _aws.Int64(int64(desiredCapacity)), 130 | } 131 | 132 | _, err := c.svc.SetDesiredCapacity(params) 133 | 134 | return err 135 | } 136 | 137 | func (c *Client) DeleteAutoScalingGroup(autoScalingGroupName string, forceDelete bool) error { 138 | params := &_autoscaling.DeleteAutoScalingGroupInput{ 139 | AutoScalingGroupName: _aws.String(autoScalingGroupName), 140 | ForceDelete: _aws.Bool(forceDelete), 141 | } 142 | 143 | _, err := c.svc.DeleteAutoScalingGroup(params) 144 | 145 | return err 146 | } 147 | 148 | func (c *Client) AddTagsToAutoScalingGroup(autoScalingGroupName string, tags map[string]string, tagNewInstances bool) error { 149 | params := &_autoscaling.CreateOrUpdateTagsInput{} 150 | 151 | for tk, tv := range tags { 152 | params.Tags = append(params.Tags, &_autoscaling.Tag{ 153 | ResourceId: _aws.String(autoScalingGroupName), 154 | ResourceType: _aws.String("auto-scaling-group"), 155 | Key: _aws.String(tk), 156 | Value: _aws.String(tv), 157 | PropagateAtLaunch: _aws.Bool(tagNewInstances), 158 | }) 159 | } 160 | 161 | _, err := c.svc.CreateOrUpdateTags(params) 162 | 163 | return err 164 | } 165 | 166 | func (c *Client) RetrieveTagsForAutoScalingGroup(autoScalingGroupName string) (map[string]string, error) { 167 | params := &_autoscaling.DescribeAutoScalingGroupsInput{ 168 | AutoScalingGroupNames: _aws.StringSlice([]string{autoScalingGroupName}), 169 | } 170 | 171 | res, err := c.svc.DescribeAutoScalingGroups(params) 172 | if err != nil { 173 | return nil, err 174 | } 175 | 176 | if len(res.AutoScalingGroups) == 0 { 177 | return nil, fmt.Errorf("EC2 Auto Scaling Group [%s] was not found.", autoScalingGroupName) 178 | } 179 | 180 | tags := map[string]string{} 181 | for _, t := range res.AutoScalingGroups[0].Tags { 182 | tags[conv.S(t.Key)] = conv.S(t.Value) 183 | } 184 | 185 | return tags, nil 186 | } 187 | -------------------------------------------------------------------------------- /aws/client.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | _aws "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/aws/credentials" 6 | "github.com/aws/aws-sdk-go/aws/session" 7 | "github.com/coldbrewcloud/coldbrew-cli/aws/autoscaling" 8 | "github.com/coldbrewcloud/coldbrew-cli/aws/ec2" 9 | "github.com/coldbrewcloud/coldbrew-cli/aws/ecr" 10 | "github.com/coldbrewcloud/coldbrew-cli/aws/ecs" 11 | "github.com/coldbrewcloud/coldbrew-cli/aws/elb" 12 | "github.com/coldbrewcloud/coldbrew-cli/aws/iam" 13 | "github.com/coldbrewcloud/coldbrew-cli/aws/logs" 14 | "github.com/coldbrewcloud/coldbrew-cli/aws/sns" 15 | ) 16 | 17 | type Client struct { 18 | session *session.Session 19 | config *_aws.Config 20 | 21 | autoScalingClient *autoscaling.Client 22 | ec2Client *ec2.Client 23 | ecsClient *ecs.Client 24 | elbClient *elb.Client 25 | ecrClient *ecr.Client 26 | iamClient *iam.Client 27 | snsClient *sns.Client 28 | logsClient *logs.Client 29 | } 30 | 31 | func NewClient(region, accessKey, secretKey string) *Client { 32 | config := _aws.NewConfig().WithRegion(region) 33 | if accessKey != "" { 34 | config = config.WithCredentials(credentials.NewStaticCredentials(accessKey, secretKey, "")) 35 | } 36 | 37 | return &Client{ 38 | session: session.New(), 39 | config: config, 40 | } 41 | } 42 | 43 | func (c *Client) AutoScaling() *autoscaling.Client { 44 | if c.autoScalingClient == nil { 45 | c.autoScalingClient = autoscaling.New(c.session, c.config) 46 | } 47 | return c.autoScalingClient 48 | } 49 | 50 | func (c *Client) EC2() *ec2.Client { 51 | if c.ec2Client == nil { 52 | c.ec2Client = ec2.New(c.session, c.config) 53 | } 54 | return c.ec2Client 55 | } 56 | 57 | func (c *Client) ECS() *ecs.Client { 58 | if c.ecsClient == nil { 59 | c.ecsClient = ecs.New(c.session, c.config) 60 | } 61 | return c.ecsClient 62 | } 63 | 64 | func (c *Client) ELB() *elb.Client { 65 | if c.elbClient == nil { 66 | c.elbClient = elb.New(c.session, c.config) 67 | } 68 | return c.elbClient 69 | } 70 | 71 | func (c *Client) ECR() *ecr.Client { 72 | if c.ecrClient == nil { 73 | c.ecrClient = ecr.New(c.session, c.config) 74 | } 75 | return c.ecrClient 76 | } 77 | 78 | func (c *Client) IAM() *iam.Client { 79 | if c.iamClient == nil { 80 | c.iamClient = iam.New(c.session, c.config) 81 | } 82 | return c.iamClient 83 | } 84 | 85 | func (c *Client) SNS() *sns.Client { 86 | if c.snsClient == nil { 87 | c.snsClient = sns.New(c.session, c.config) 88 | } 89 | return c.snsClient 90 | } 91 | 92 | func (c *Client) CloudWatchLogs() *logs.Client { 93 | if c.logsClient == nil { 94 | c.logsClient = logs.New(c.session, c.config) 95 | } 96 | return c.logsClient 97 | } 98 | -------------------------------------------------------------------------------- /aws/consts.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | const ( 4 | AWSRegionUSEast1 = "us-east-1" 5 | AWSRegionUSEast2 = "us-east-2" 6 | AWSRegionUSWest1 = "us-west-1" 7 | AWSRegionUSWest2 = "us-west-2" 8 | AWSRegionEUWest1 = "eu-west-1" 9 | AWSRegionEUCentral1 = "eu-central-1" 10 | AWSRegionAPNorthEast1 = "ap-northeast-1" 11 | AWSRegionAPSouthEast1 = "ap-southeast-1" 12 | AWSRegionAPSouthEast2 = "ap-southeast-2" 13 | AWSRegionSAEast1 = "sa-east-1" 14 | 15 | ECSTaskDefinitionLogDriverJSONFile = "json-file" 16 | ECSTaskDefinitionLogDriverAWSLogs = "awslogs" 17 | ECSTaskDefinitionLogDriverSyslog = "syslog" 18 | ECSTaskDefinitionLogDriverJournald = "journald" 19 | ECSTaskDefinitionLogDriverGelf = "gelf" 20 | ECSTaskDefinitionLogDriverFluentd = "fluentd" 21 | ECSTaskDefinitionLogDriverSplunk = "splunk" 22 | ) 23 | -------------------------------------------------------------------------------- /aws/ec2/client.go: -------------------------------------------------------------------------------- 1 | package ec2 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | _aws "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/aws/session" 10 | _ec2 "github.com/aws/aws-sdk-go/service/ec2" 11 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 12 | ) 13 | 14 | const ( 15 | SecurityGroupProtocolTCP = "tcp" 16 | SecurityGroupProtocolUDP = "udp" 17 | SecurityGroupProtocolICMP = "icmp" 18 | SecurityGroupProtocolAll = "all" 19 | ) 20 | 21 | var ( 22 | cidrRE = regexp.MustCompile(`^[\d/.]+$`) // loose matcher 23 | ) 24 | 25 | type Client struct { 26 | svc *_ec2.EC2 27 | } 28 | 29 | func New(session *session.Session, config *_aws.Config) *Client { 30 | return &Client{ 31 | svc: _ec2.New(session, config), 32 | } 33 | } 34 | 35 | func (c *Client) CreateSecurityGroup(name, description, vpcID string) (string, error) { 36 | params := &_ec2.CreateSecurityGroupInput{ 37 | GroupName: _aws.String(name), 38 | Description: _aws.String(description), 39 | VpcId: _aws.String(vpcID), 40 | } 41 | 42 | res, err := c.svc.CreateSecurityGroup(params) 43 | if err != nil { 44 | return "", err 45 | } 46 | 47 | return conv.S(res.GroupId), nil 48 | } 49 | 50 | func (c *Client) AddInboundToSecurityGroup(securityGroupID, protocol string, portRangeFrom, portRangeTo uint16, source string) error { 51 | params := &_ec2.AuthorizeSecurityGroupIngressInput{ 52 | GroupId: _aws.String(securityGroupID), 53 | } 54 | 55 | if strings.HasPrefix(source, "sg-") { 56 | // Source: other security group 57 | params.IpPermissions = []*_ec2.IpPermission{ 58 | { 59 | UserIdGroupPairs: []*_ec2.UserIdGroupPair{ 60 | {GroupId: _aws.String(source)}, 61 | }, 62 | IpProtocol: _aws.String(protocol), 63 | FromPort: _aws.Int64(int64(portRangeFrom)), 64 | ToPort: _aws.Int64(int64(portRangeTo)), 65 | }, 66 | } 67 | } else if cidrRE.MatchString(source) { 68 | // Source: IP CIDR 69 | params.CidrIp = _aws.String(source) 70 | params.IpProtocol = _aws.String(protocol) 71 | params.FromPort = _aws.Int64(int64(portRangeFrom)) 72 | params.ToPort = _aws.Int64(int64(portRangeTo)) 73 | } else { 74 | return fmt.Errorf("Invalid source [%s]", source) 75 | } 76 | 77 | _, err := c.svc.AuthorizeSecurityGroupIngress(params) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | return nil 83 | } 84 | 85 | func (c *Client) RemoveInboundToSecurityGroup(securityGroupID, protocol string, portRangeFrom, portRangeTo uint16, source string) error { 86 | params := &_ec2.RevokeSecurityGroupIngressInput{ 87 | GroupId: _aws.String(securityGroupID), 88 | } 89 | 90 | if strings.HasPrefix(source, "sg-") { 91 | // Source: other security group 92 | params.IpPermissions = []*_ec2.IpPermission{ 93 | { 94 | UserIdGroupPairs: []*_ec2.UserIdGroupPair{ 95 | {GroupId: _aws.String(source)}, 96 | }, 97 | IpProtocol: _aws.String(protocol), 98 | FromPort: _aws.Int64(int64(portRangeFrom)), 99 | ToPort: _aws.Int64(int64(portRangeTo)), 100 | }, 101 | } 102 | } else if cidrRE.MatchString(source) { 103 | // Source: IP CIDR 104 | params.CidrIp = _aws.String(source) 105 | params.IpProtocol = _aws.String(protocol) 106 | params.FromPort = _aws.Int64(int64(portRangeFrom)) 107 | params.ToPort = _aws.Int64(int64(portRangeTo)) 108 | } else { 109 | return fmt.Errorf("Invalid source [%s]", source) 110 | } 111 | 112 | _, err := c.svc.RevokeSecurityGroupIngress(params) 113 | if err != nil { 114 | return err 115 | } 116 | 117 | return nil 118 | } 119 | 120 | func (c *Client) RetrieveSecurityGroup(id string) (*_ec2.SecurityGroup, error) { 121 | // NOTE: used Filter instead of GroupIds attribute because GroupIds 122 | // returns error when it cannot find the matching security groups. 123 | params := &_ec2.DescribeSecurityGroupsInput{ 124 | Filters: []*_ec2.Filter{ 125 | { 126 | Name: _aws.String("group-id"), 127 | Values: _aws.StringSlice([]string{id}), 128 | }, 129 | }, 130 | } 131 | 132 | res, err := c.svc.DescribeSecurityGroups(params) 133 | if err != nil { 134 | return nil, err 135 | } 136 | 137 | if len(res.SecurityGroups) > 0 { 138 | return res.SecurityGroups[0], nil 139 | } else { 140 | return nil, nil 141 | } 142 | } 143 | 144 | func (c *Client) RetrieveSecurityGroups(securityGroupIDs []string) ([]*_ec2.SecurityGroup, error) { 145 | // NOTE: used Filter instead of GroupIds attribute because GroupIds 146 | // returns error when it cannot find the matching security groups. 147 | params := &_ec2.DescribeSecurityGroupsInput{ 148 | Filters: []*_ec2.Filter{ 149 | { 150 | Name: _aws.String("group-id"), 151 | Values: _aws.StringSlice(securityGroupIDs), 152 | }, 153 | }, 154 | } 155 | 156 | res, err := c.svc.DescribeSecurityGroups(params) 157 | if err != nil { 158 | return nil, err 159 | } 160 | 161 | return res.SecurityGroups, nil 162 | } 163 | 164 | func (c *Client) RetrieveSecurityGroupByName(name string) (*_ec2.SecurityGroup, error) { 165 | // NOTE: used Filter instead of GroupNames attribute because GroupNames 166 | // returns error when it cannot find the matching security groups. 167 | params := &_ec2.DescribeSecurityGroupsInput{ 168 | Filters: []*_ec2.Filter{ 169 | { 170 | Name: _aws.String("group-name"), 171 | Values: _aws.StringSlice([]string{name}), 172 | }, 173 | }, 174 | } 175 | 176 | res, err := c.svc.DescribeSecurityGroups(params) 177 | if err != nil { 178 | return nil, err 179 | } 180 | 181 | if len(res.SecurityGroups) > 0 { 182 | return res.SecurityGroups[0], nil 183 | } else { 184 | return nil, nil 185 | } 186 | } 187 | 188 | func (c *Client) RetrieveSecurityGroupByNameOrID(nameOrID string) (*_ec2.SecurityGroup, error) { 189 | if strings.HasPrefix(nameOrID, "sg-") { 190 | return c.RetrieveSecurityGroup(nameOrID) 191 | } else { 192 | return c.RetrieveSecurityGroupByName(nameOrID) 193 | } 194 | } 195 | 196 | func (c *Client) DeleteSecurityGroup(securityGroupID string) error { 197 | params := &_ec2.DeleteSecurityGroupInput{ 198 | GroupId: _aws.String(securityGroupID), 199 | } 200 | 201 | _, err := c.svc.DeleteSecurityGroup(params) 202 | 203 | return err 204 | } 205 | 206 | func (c *Client) CreateInstances(instanceType, imageID string, instanceCount uint16, securityGroupIDs []string, keyPairName, subnetID, iamInstanceProfileName, userData string) ([]*_ec2.Instance, error) { 207 | params := &_ec2.RunInstancesInput{ 208 | EbsOptimized: _aws.Bool(false), 209 | IamInstanceProfile: &_ec2.IamInstanceProfileSpecification{Name: _aws.String(iamInstanceProfileName)}, 210 | ImageId: _aws.String(imageID), 211 | InstanceType: _aws.String(instanceType), 212 | KeyName: _aws.String(keyPairName), 213 | MaxCount: _aws.Int64(int64(instanceCount)), 214 | MinCount: _aws.Int64(int64(instanceCount)), 215 | SecurityGroupIds: _aws.StringSlice(securityGroupIDs), 216 | SubnetId: _aws.String(subnetID), 217 | UserData: _aws.String(userData), 218 | } 219 | 220 | res, err := c.svc.RunInstances(params) 221 | if err != nil { 222 | return nil, err 223 | } 224 | 225 | return res.Instances, nil 226 | } 227 | 228 | func (c *Client) RetrieveVPC(vpcID string) (*_ec2.Vpc, error) { 229 | params := &_ec2.DescribeVpcsInput{ 230 | VpcIds: _aws.StringSlice([]string{vpcID}), 231 | } 232 | 233 | res, err := c.svc.DescribeVpcs(params) 234 | if err != nil { 235 | return nil, err 236 | } 237 | 238 | if res.Vpcs != nil && len(res.Vpcs) > 0 { 239 | return res.Vpcs[0], nil 240 | } 241 | 242 | return nil, nil 243 | } 244 | 245 | func (c *Client) RetrieveDefaultVPC() (*_ec2.Vpc, error) { 246 | params := &_ec2.DescribeVpcsInput{ 247 | Filters: []*_ec2.Filter{ 248 | { 249 | Name: _aws.String("isDefault"), 250 | Values: _aws.StringSlice([]string{"true"}), 251 | }, 252 | }, 253 | } 254 | 255 | res, err := c.svc.DescribeVpcs(params) 256 | if err != nil { 257 | return nil, err 258 | } 259 | 260 | if res.Vpcs != nil && len(res.Vpcs) > 0 { 261 | return res.Vpcs[0], nil 262 | } 263 | 264 | return nil, nil 265 | } 266 | 267 | func (c *Client) ListVPCs() ([]string, error) { 268 | params := &_ec2.DescribeVpcsInput{} 269 | 270 | res, err := c.svc.DescribeVpcs(params) 271 | if err != nil { 272 | return nil, err 273 | } 274 | 275 | vpcIDs := []string{} 276 | for _, v := range res.Vpcs { 277 | vpcIDs = append(vpcIDs, conv.S(v.VpcId)) 278 | } 279 | 280 | return vpcIDs, nil 281 | } 282 | 283 | func (c *Client) ListVPCSubnets(vpcID string) ([]string, error) { 284 | params := &_ec2.DescribeSubnetsInput{ 285 | Filters: []*_ec2.Filter{ 286 | { 287 | Name: _aws.String("vpc-id"), 288 | Values: _aws.StringSlice([]string{vpcID}), 289 | }, 290 | }, 291 | } 292 | 293 | res, err := c.svc.DescribeSubnets(params) 294 | if err != nil { 295 | return nil, err 296 | } 297 | 298 | subnetIDs := []string{} 299 | for _, s := range res.Subnets { 300 | subnetIDs = append(subnetIDs, conv.S(s.SubnetId)) 301 | } 302 | 303 | return subnetIDs, nil 304 | } 305 | 306 | func (c *Client) RetrieveKeyPair(keyPairName string) (*_ec2.KeyPairInfo, error) { 307 | params := &_ec2.DescribeKeyPairsInput{ 308 | KeyNames: _aws.StringSlice([]string{keyPairName}), 309 | } 310 | 311 | res, err := c.svc.DescribeKeyPairs(params) 312 | if err != nil { 313 | return nil, err 314 | } 315 | 316 | if res.KeyPairs != nil && len(res.KeyPairs) > 0 { 317 | return res.KeyPairs[0], nil 318 | } 319 | 320 | return nil, nil 321 | } 322 | 323 | func (c *Client) ListKeyPairs() ([]*_ec2.KeyPairInfo, error) { 324 | params := &_ec2.DescribeKeyPairsInput{} 325 | 326 | res, err := c.svc.DescribeKeyPairs(params) 327 | if err != nil { 328 | return nil, err 329 | } 330 | 331 | keyPairs := []*_ec2.KeyPairInfo{} 332 | for _, kp := range res.KeyPairs { 333 | keyPairs = append(keyPairs, kp) 334 | } 335 | 336 | return keyPairs, nil 337 | } 338 | 339 | func (c *Client) CreateTags(resourceID string, tags map[string]string) error { 340 | params := &_ec2.CreateTagsInput{ 341 | Resources: _aws.StringSlice([]string{resourceID}), 342 | Tags: []*_ec2.Tag{}, 343 | } 344 | 345 | for tk, tv := range tags { 346 | params.Tags = append(params.Tags, &_ec2.Tag{ 347 | Key: _aws.String(tk), 348 | Value: _aws.String(tv), 349 | }) 350 | } 351 | 352 | _, err := c.svc.CreateTags(params) 353 | 354 | return err 355 | } 356 | 357 | func (c *Client) RetrieveTags(resourceID string) (map[string]string, error) { 358 | params := &_ec2.DescribeTagsInput{ 359 | Filters: []*_ec2.Filter{ 360 | { 361 | Name: _aws.String("resource-id"), 362 | Values: _aws.StringSlice([]string{resourceID}), 363 | }, 364 | }, 365 | } 366 | 367 | res, err := c.svc.DescribeTags(params) 368 | if err != nil { 369 | return nil, err 370 | } 371 | 372 | tags := map[string]string{} 373 | for _, t := range res.Tags { 374 | tags[conv.S(t.Key)] = conv.S(t.Value) 375 | } 376 | 377 | return tags, nil 378 | } 379 | 380 | func (c *Client) RetrieveInstances(instanceIDs []string) ([]*_ec2.Instance, error) { 381 | if len(instanceIDs) == 0 { 382 | return []*_ec2.Instance{}, nil 383 | } 384 | 385 | var nextToken *string 386 | instances := []*_ec2.Instance{} 387 | 388 | for { 389 | params := &_ec2.DescribeInstancesInput{ 390 | NextToken: nextToken, 391 | } 392 | 393 | res, err := c.svc.DescribeInstances(params) 394 | if err != nil { 395 | return nil, err 396 | } 397 | 398 | for _, r := range res.Reservations { 399 | instances = append(instances, r.Instances...) 400 | } 401 | 402 | if res.NextToken == nil { 403 | break 404 | } else { 405 | nextToken = res.NextToken 406 | } 407 | } 408 | 409 | return instances, nil 410 | } 411 | 412 | func (c *Client) FindImage(ownerID, tagName string) ([]*_ec2.Image, error) { 413 | params := &_ec2.DescribeImagesInput{ 414 | Owners: _aws.StringSlice([]string{ownerID}), 415 | Filters: []*_ec2.Filter{ 416 | { 417 | Name: _aws.String("tag-key"), 418 | Values: _aws.StringSlice([]string{tagName}), 419 | }, 420 | }, 421 | } 422 | 423 | res, err := c.svc.DescribeImages(params) 424 | if err != nil { 425 | return nil, err 426 | } 427 | 428 | return res.Images, nil 429 | } 430 | -------------------------------------------------------------------------------- /aws/ecr/client.go: -------------------------------------------------------------------------------- 1 | package ecr 2 | 3 | import ( 4 | "encoding/base64" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "strings" 9 | 10 | _aws "github.com/aws/aws-sdk-go/aws" 11 | "github.com/aws/aws-sdk-go/aws/awserr" 12 | "github.com/aws/aws-sdk-go/aws/session" 13 | _ecr "github.com/aws/aws-sdk-go/service/ecr" 14 | ) 15 | 16 | type Client struct { 17 | svc *_ecr.ECR 18 | } 19 | 20 | func New(session *session.Session, config *_aws.Config) *Client { 21 | return &Client{ 22 | svc: _ecr.New(session, config), 23 | } 24 | } 25 | 26 | func (c *Client) RetrieveRepository(repoName string) (*_ecr.Repository, error) { 27 | if repoName == "" { 28 | return nil, errors.New("repoName is empty") 29 | } 30 | 31 | params := &_ecr.DescribeRepositoriesInput{ 32 | RepositoryNames: _aws.StringSlice([]string{repoName}), 33 | } 34 | 35 | res, err := c.svc.DescribeRepositories(params) 36 | if err != nil { 37 | if reqFail, ok := err.(awserr.RequestFailure); ok { 38 | if reqFail.StatusCode() == http.StatusBadRequest { 39 | return nil, nil 40 | } 41 | } 42 | return nil, err 43 | } 44 | 45 | if len(res.Repositories) != 1 { 46 | return nil, fmt.Errorf("Invali result: %v", res.Repositories) 47 | } 48 | 49 | return res.Repositories[0], nil 50 | } 51 | 52 | func (c *Client) CreateRepository(repoName string) (*_ecr.Repository, error) { 53 | if repoName == "" { 54 | return nil, errors.New("repoName is empty") 55 | } 56 | 57 | params := &_ecr.CreateRepositoryInput{ 58 | RepositoryName: _aws.String(repoName), 59 | } 60 | 61 | res, err := c.svc.CreateRepository(params) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | return res.Repository, nil 67 | } 68 | 69 | func (c *Client) GetDockerLogin() (string, string, string, error) { 70 | params := &_ecr.GetAuthorizationTokenInput{} 71 | 72 | res, err := c.svc.GetAuthorizationToken(params) 73 | if err != nil { 74 | return "", "", "", err 75 | } 76 | 77 | data, err := base64.StdEncoding.DecodeString(*res.AuthorizationData[0].AuthorizationToken) 78 | if err != nil { 79 | return "", "", "", err 80 | } 81 | 82 | tokens := strings.SplitN(string(data), ":", 2) 83 | 84 | return tokens[0], tokens[1], *res.AuthorizationData[0].ProxyEndpoint, nil 85 | } 86 | 87 | func (c *Client) DeleteRepository(repoName string) error { 88 | params := &_ecr.DeleteRepositoryInput{ 89 | Force: _aws.Bool(true), 90 | RepositoryName: _aws.String(repoName), 91 | } 92 | 93 | _, err := c.svc.DeleteRepository(params) 94 | 95 | return err 96 | } 97 | -------------------------------------------------------------------------------- /aws/ecs/client.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | _aws "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/aws/session" 9 | _ecs "github.com/aws/aws-sdk-go/service/ecs" 10 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 11 | ) 12 | 13 | type Client struct { 14 | svc *_ecs.ECS 15 | awsRegion string 16 | } 17 | 18 | func New(session *session.Session, config *_aws.Config) *Client { 19 | return &Client{ 20 | awsRegion: *config.Region, 21 | svc: _ecs.New(session, config), 22 | } 23 | } 24 | 25 | func (c *Client) RetrieveCluster(clusterName string) (*_ecs.Cluster, error) { 26 | if clusterName == "" { 27 | return nil, errors.New("clusterName is empty") 28 | } 29 | 30 | params := &_ecs.DescribeClustersInput{ 31 | Clusters: _aws.StringSlice([]string{clusterName}), 32 | } 33 | res, err := c.svc.DescribeClusters(params) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | if len(res.Clusters) == 0 { 39 | return nil, nil 40 | } else if len(res.Clusters) == 1 { 41 | return res.Clusters[0], nil 42 | } 43 | 44 | return nil, fmt.Errorf("Invalid result: %v", res.Clusters) 45 | } 46 | 47 | func (c *Client) CreateCluster(clusterName string) (*_ecs.Cluster, error) { 48 | if clusterName == "" { 49 | return nil, errors.New("clusterName is empty") 50 | } 51 | 52 | params := &_ecs.CreateClusterInput{ 53 | ClusterName: _aws.String(clusterName), 54 | } 55 | 56 | res, err := c.svc.CreateCluster(params) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | return res.Cluster, nil 62 | } 63 | 64 | func (c *Client) DeleteCluster(clusterName string) error { 65 | params := &_ecs.DeleteClusterInput{ 66 | Cluster: _aws.String(clusterName), 67 | } 68 | 69 | _, err := c.svc.DeleteCluster(params) 70 | 71 | return err 72 | } 73 | 74 | func (c *Client) UpdateTaskDefinition(taskDefinitionName, image, taskContainerName string, cpu, memory uint64, envs map[string]string, portMappings []PortMapping, logDriver string, logDriverOptions map[string]string) (*_ecs.TaskDefinition, error) { 75 | if taskDefinitionName == "" { 76 | return nil, errors.New("taskDefinitionName is empty") 77 | } 78 | if image == "" { 79 | return nil, errors.New("image is empty") 80 | } 81 | if taskContainerName == "" { 82 | return nil, errors.New("taskContainerName is empty") 83 | } 84 | 85 | params := &_ecs.RegisterTaskDefinitionInput{ 86 | ContainerDefinitions: []*_ecs.ContainerDefinition{ 87 | { 88 | Name: _aws.String(taskContainerName), 89 | Cpu: _aws.Int64(int64(cpu)), 90 | Memory: _aws.Int64(int64(memory)), 91 | Essential: _aws.Bool(true), 92 | Image: _aws.String(image), 93 | LogConfiguration: nil, 94 | }, 95 | }, 96 | Family: _aws.String(taskDefinitionName), 97 | } 98 | 99 | if logDriver != "" { 100 | params.ContainerDefinitions[0].LogConfiguration = &_ecs.LogConfiguration{ 101 | LogDriver: _aws.String(logDriver), 102 | Options: _aws.StringMap(logDriverOptions), 103 | } 104 | } 105 | 106 | for ek, ev := range envs { 107 | params.ContainerDefinitions[0].Environment = append(params.ContainerDefinitions[0].Environment, &_ecs.KeyValuePair{ 108 | Name: _aws.String(ek), 109 | Value: _aws.String(ev), 110 | }) 111 | } 112 | 113 | for _, pm := range portMappings { 114 | params.ContainerDefinitions[0].PortMappings = append(params.ContainerDefinitions[0].PortMappings, &_ecs.PortMapping{ 115 | ContainerPort: _aws.Int64(int64(pm.ContainerPort)), 116 | HostPort: _aws.Int64(0), 117 | Protocol: _aws.String(pm.Protocol), 118 | }) 119 | } 120 | 121 | res, err := c.svc.RegisterTaskDefinition(params) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | return res.TaskDefinition, nil 127 | } 128 | 129 | func (c *Client) RetrieveTaskDefinition(taskDefinitionNameOrARN string) (*_ecs.TaskDefinition, error) { 130 | params := &_ecs.DescribeTaskDefinitionInput{ 131 | TaskDefinition: _aws.String(taskDefinitionNameOrARN), 132 | } 133 | 134 | res, err := c.svc.DescribeTaskDefinition(params) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | return res.TaskDefinition, nil 140 | } 141 | 142 | func (c *Client) RetrieveService(clusterName, serviceName string) (*_ecs.Service, error) { 143 | if clusterName == "" { 144 | return nil, errors.New("clusterName is empty") 145 | } 146 | if serviceName == "" { 147 | return nil, errors.New("serviceName is empty") 148 | } 149 | 150 | params := &_ecs.DescribeServicesInput{ 151 | Cluster: _aws.String(clusterName), 152 | Services: _aws.StringSlice([]string{serviceName}), 153 | } 154 | 155 | res, err := c.svc.DescribeServices(params) 156 | if err != nil { 157 | return nil, err 158 | } 159 | 160 | if len(res.Services) == 0 { 161 | return nil, nil 162 | } else if len(res.Services) == 1 { 163 | return res.Services[0], nil 164 | } 165 | 166 | return nil, fmt.Errorf("Invalid result: %v", res.Services) 167 | } 168 | 169 | func (c *Client) CreateService(clusterName, serviceName, taskDefARN string, desiredCount uint16, loadBalancers []*LoadBalancer, serviceRole string) (*_ecs.Service, error) { 170 | if clusterName == "" { 171 | return nil, errors.New("clusterName is empty") 172 | } 173 | if serviceName == "" { 174 | return nil, errors.New("serviceName is empty") 175 | } 176 | if taskDefARN == "" { 177 | return nil, errors.New("taskDefARN is empty") 178 | } 179 | 180 | params := &_ecs.CreateServiceInput{ 181 | DesiredCount: _aws.Int64(int64(desiredCount)), 182 | ServiceName: _aws.String(serviceName), 183 | TaskDefinition: _aws.String(taskDefARN), 184 | Cluster: _aws.String(clusterName), 185 | DeploymentConfiguration: &_ecs.DeploymentConfiguration{ 186 | MaximumPercent: _aws.Int64(200), 187 | MinimumHealthyPercent: _aws.Int64(50), 188 | }, 189 | } 190 | 191 | if loadBalancers != nil && len(loadBalancers) > 0 { 192 | params.LoadBalancers = []*_ecs.LoadBalancer{} 193 | 194 | for _, lb := range loadBalancers { 195 | 196 | params.LoadBalancers = append(params.LoadBalancers, &_ecs.LoadBalancer{ 197 | ContainerName: _aws.String(lb.TaskContainerName), 198 | ContainerPort: _aws.Int64(int64(lb.TaskContainerPort)), 199 | TargetGroupArn: _aws.String(lb.ELBTargetGroupARN), 200 | }) 201 | } 202 | 203 | params.Role = _aws.String(serviceRole) 204 | } 205 | 206 | res, err := c.svc.CreateService(params) 207 | if err != nil { 208 | return nil, err 209 | } 210 | 211 | return res.Service, nil 212 | } 213 | 214 | func (c *Client) UpdateService(clusterName, serviceName, taskDefARN string, desiredCount uint16) (*_ecs.Service, error) { 215 | if clusterName == "" { 216 | return nil, errors.New("clusterName is empty") 217 | } 218 | if serviceName == "" { 219 | return nil, errors.New("serviceName is empty") 220 | } 221 | if taskDefARN == "" { 222 | return nil, errors.New("taskDefARN is empty") 223 | } 224 | 225 | params := &_ecs.UpdateServiceInput{ 226 | Service: _aws.String(serviceName), 227 | Cluster: _aws.String(clusterName), 228 | DesiredCount: _aws.Int64(int64(desiredCount)), 229 | TaskDefinition: _aws.String(taskDefARN), 230 | DeploymentConfiguration: &_ecs.DeploymentConfiguration{ 231 | MaximumPercent: _aws.Int64(200), 232 | MinimumHealthyPercent: _aws.Int64(50), 233 | }, 234 | } 235 | 236 | res, err := c.svc.UpdateService(params) 237 | if err != nil { 238 | return nil, err 239 | } 240 | 241 | return res.Service, nil 242 | } 243 | 244 | func (c *Client) DeleteService(clusterName, serviceName string) error { 245 | params := &_ecs.DeleteServiceInput{ 246 | Cluster: _aws.String(clusterName), 247 | Service: _aws.String(serviceName), 248 | } 249 | 250 | _, err := c.svc.DeleteService(params) 251 | 252 | return err 253 | } 254 | 255 | func (c *Client) ListServiceTaskARNs(clusterName, serviceName string) ([]string, error) { 256 | var nextToken *string 257 | taskARNs := []string{} 258 | 259 | for { 260 | params := &_ecs.ListTasksInput{ 261 | Cluster: _aws.String(clusterName), 262 | ServiceName: _aws.String(serviceName), 263 | NextToken: nextToken, 264 | } 265 | 266 | res, err := c.svc.ListTasks(params) 267 | if err != nil { 268 | return nil, err 269 | } 270 | 271 | for _, t := range res.TaskArns { 272 | taskARNs = append(taskARNs, conv.S(t)) 273 | } 274 | 275 | if res.NextToken == nil { 276 | break 277 | } else { 278 | nextToken = res.NextToken 279 | } 280 | } 281 | 282 | return taskARNs, nil 283 | } 284 | 285 | func (c *Client) RetrieveTasks(clusterName string, taskARNs []string) ([]*_ecs.Task, error) { 286 | if len(taskARNs) == 0 { 287 | return []*_ecs.Task{}, nil 288 | } 289 | 290 | params := &_ecs.DescribeTasksInput{ 291 | Cluster: _aws.String(clusterName), 292 | Tasks: _aws.StringSlice(taskARNs), 293 | } 294 | 295 | res, err := c.svc.DescribeTasks(params) 296 | if err != nil { 297 | return nil, err 298 | } 299 | 300 | return res.Tasks, nil 301 | } 302 | 303 | func (c *Client) ListContainerInstanceARNs(clusterName string) ([]string, error) { 304 | var nextToken *string 305 | containerInstanceARNs := []string{} 306 | 307 | for { 308 | params := &_ecs.ListContainerInstancesInput{ 309 | Cluster: _aws.String(clusterName), 310 | NextToken: nextToken, 311 | } 312 | 313 | res, err := c.svc.ListContainerInstances(params) 314 | if err != nil { 315 | return nil, err 316 | } 317 | 318 | for _, t := range res.ContainerInstanceArns { 319 | containerInstanceARNs = append(containerInstanceARNs, conv.S(t)) 320 | } 321 | 322 | if res.NextToken == nil { 323 | break 324 | } else { 325 | nextToken = res.NextToken 326 | } 327 | } 328 | 329 | return containerInstanceARNs, nil 330 | } 331 | 332 | func (c *Client) RetrieveContainerInstances(clusterName string, containerInstanceARNs []string) ([]*_ecs.ContainerInstance, error) { 333 | if len(containerInstanceARNs) == 0 { 334 | return []*_ecs.ContainerInstance{}, nil 335 | } 336 | 337 | params := &_ecs.DescribeContainerInstancesInput{ 338 | Cluster: _aws.String(clusterName), 339 | ContainerInstances: _aws.StringSlice(containerInstanceARNs), 340 | } 341 | 342 | res, err := c.svc.DescribeContainerInstances(params) 343 | if err != nil { 344 | return nil, err 345 | } 346 | 347 | return res.ContainerInstances, nil 348 | } 349 | -------------------------------------------------------------------------------- /aws/ecs/load_balancer.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | type LoadBalancer struct { 4 | ELBTargetGroupARN string `json:"elb_target_group_arn"` 5 | TaskContainerName string `json:"task_container_name"` 6 | TaskContainerPort uint16 `json:"task_container_port"` 7 | } 8 | -------------------------------------------------------------------------------- /aws/ecs/port_mapping.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | type PortMapping struct { 4 | ContainerPort uint16 `json:"container_port"` 5 | Protocol string `json:"protocol"` 6 | } 7 | -------------------------------------------------------------------------------- /aws/elb/client.go: -------------------------------------------------------------------------------- 1 | package elb 2 | 3 | import ( 4 | "fmt" 5 | 6 | _aws "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/aws/awserr" 8 | "github.com/aws/aws-sdk-go/aws/session" 9 | _elb "github.com/aws/aws-sdk-go/service/elbv2" 10 | "github.com/coldbrewcloud/coldbrew-cli/utils" 11 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 12 | ) 13 | 14 | type Client struct { 15 | svc *_elb.ELBV2 16 | } 17 | 18 | func New(session *session.Session, config *_aws.Config) *Client { 19 | return &Client{ 20 | svc: _elb.New(session, config), 21 | } 22 | } 23 | 24 | func (c *Client) CreateLoadBalancer(elbName string, internetFacing bool, securityGroupIDs, subnetIDs []string) (*_elb.LoadBalancer, error) { 25 | params := &_elb.CreateLoadBalancerInput{ 26 | Name: _aws.String(elbName), 27 | SecurityGroups: _aws.StringSlice(securityGroupIDs), 28 | Subnets: _aws.StringSlice(subnetIDs), 29 | } 30 | 31 | if internetFacing { 32 | params.Scheme = _aws.String(_elb.LoadBalancerSchemeEnumInternetFacing) 33 | } else { 34 | params.Scheme = _aws.String(_elb.LoadBalancerSchemeEnumInternal) 35 | } 36 | 37 | res, err := c.svc.CreateLoadBalancer(params) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return res.LoadBalancers[0], nil 43 | } 44 | 45 | func (c *Client) RetrieveLoadBalancer(elbARN string) (*_elb.LoadBalancer, error) { 46 | params := &_elb.DescribeLoadBalancersInput{ 47 | LoadBalancerArns: _aws.StringSlice([]string{elbARN}), 48 | } 49 | res, err := c.svc.DescribeLoadBalancers(params) 50 | if err != nil { 51 | if awsErr, ok := err.(awserr.Error); ok { 52 | if awsErr.Code() == "LoadBalancerNotFound" { 53 | return nil, nil 54 | } 55 | } 56 | return nil, err 57 | } 58 | 59 | if len(res.LoadBalancers) == 0 { 60 | return nil, nil 61 | } else if len(res.LoadBalancers) == 1 { 62 | return res.LoadBalancers[0], nil 63 | } 64 | 65 | return nil, fmt.Errorf("Invalid result: %v", res.LoadBalancers) 66 | } 67 | 68 | func (c *Client) RetrieveLoadBalancerByName(elbName string) (*_elb.LoadBalancer, error) { 69 | params := &_elb.DescribeLoadBalancersInput{ 70 | Names: _aws.StringSlice([]string{elbName}), 71 | } 72 | res, err := c.svc.DescribeLoadBalancers(params) 73 | if err != nil { 74 | if awsErr, ok := err.(awserr.Error); ok { 75 | if awsErr.Code() == "LoadBalancerNotFound" { 76 | return nil, nil 77 | } 78 | } 79 | return nil, err 80 | } 81 | 82 | if len(res.LoadBalancers) == 0 { 83 | return nil, nil 84 | } else if len(res.LoadBalancers) == 1 { 85 | return res.LoadBalancers[0], nil 86 | } 87 | 88 | return nil, fmt.Errorf("Invalid result: %v", res.LoadBalancers) 89 | } 90 | 91 | func (c *Client) RetrieveLoadBalancerListeners(loadBalancerARN string) ([]*_elb.Listener, error) { 92 | listeners := []*_elb.Listener{} 93 | var marker *string 94 | 95 | for { 96 | params := &_elb.DescribeListenersInput{ 97 | Marker: marker, 98 | LoadBalancerArn: _aws.String(loadBalancerARN), 99 | } 100 | 101 | res, err := c.svc.DescribeListeners(params) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | for _, p := range res.Listeners { 107 | listeners = append(listeners, p) 108 | } 109 | 110 | if utils.IsBlank(conv.S(res.NextMarker)) { 111 | break 112 | } 113 | 114 | marker = res.NextMarker 115 | } 116 | 117 | return listeners, nil 118 | } 119 | 120 | func (c *Client) DeleteLoadBalancer(loadBalancerARN string) error { 121 | params := &_elb.DeleteLoadBalancerInput{ 122 | LoadBalancerArn: _aws.String(loadBalancerARN), 123 | } 124 | 125 | _, err := c.svc.DeleteLoadBalancer(params) 126 | 127 | return err 128 | } 129 | 130 | func (c *Client) CreateTargetGroup(name string, port uint16, protocol string, vpcID string, healthCheck *HealthCheckParams) (*_elb.TargetGroup, error) { 131 | params := &_elb.CreateTargetGroupInput{ 132 | Name: _aws.String(name), 133 | Port: _aws.Int64(int64(port)), 134 | Protocol: _aws.String(protocol), 135 | VpcId: _aws.String(vpcID), 136 | } 137 | 138 | if healthCheck != nil { 139 | params.HealthCheckIntervalSeconds = _aws.Int64(int64(healthCheck.CheckIntervalSeconds)) 140 | params.HealthCheckPath = _aws.String(healthCheck.CheckPath) 141 | if healthCheck.CheckPort != nil { 142 | params.HealthCheckPort = _aws.String(fmt.Sprintf("%d", *healthCheck.CheckPort)) 143 | } 144 | params.HealthCheckProtocol = _aws.String(healthCheck.Protocol) 145 | params.HealthCheckTimeoutSeconds = _aws.Int64(int64(healthCheck.CheckTimeoutSeconds)) 146 | params.HealthyThresholdCount = _aws.Int64(int64(healthCheck.HealthyThresholdCount)) 147 | params.UnhealthyThresholdCount = _aws.Int64(int64(healthCheck.UnhealthyThresholdCount)) 148 | params.Matcher = &_elb.Matcher{HttpCode: _aws.String(healthCheck.ExpectedHTTPStatusCodes)} 149 | } 150 | 151 | res, err := c.svc.CreateTargetGroup(params) 152 | if err != nil { 153 | return nil, err 154 | } 155 | 156 | return res.TargetGroups[0], nil 157 | } 158 | 159 | func (c *Client) RetrieveTargetGroup(targetGroupARN string) (*_elb.TargetGroup, error) { 160 | params := &_elb.DescribeTargetGroupsInput{ 161 | TargetGroupArns: _aws.StringSlice([]string{targetGroupARN}), 162 | } 163 | res, err := c.svc.DescribeTargetGroups(params) 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | if len(res.TargetGroups) > 0 { 169 | return res.TargetGroups[0], nil 170 | } 171 | 172 | return nil, nil 173 | } 174 | 175 | func (c *Client) UpdateTargetGroupHealthCheck(targetGroupARN string, healthCheck *HealthCheckParams) error { 176 | params := &_elb.ModifyTargetGroupInput{ 177 | TargetGroupArn: _aws.String(targetGroupARN), 178 | HealthCheckIntervalSeconds: _aws.Int64(int64(healthCheck.CheckIntervalSeconds)), 179 | HealthCheckPath: _aws.String(healthCheck.CheckPath), 180 | HealthCheckProtocol: _aws.String(healthCheck.Protocol), 181 | HealthCheckTimeoutSeconds: _aws.Int64(int64(healthCheck.CheckTimeoutSeconds)), 182 | HealthyThresholdCount: _aws.Int64(int64(healthCheck.HealthyThresholdCount)), 183 | UnhealthyThresholdCount: _aws.Int64(int64(healthCheck.UnhealthyThresholdCount)), 184 | Matcher: &_elb.Matcher{HttpCode: _aws.String(healthCheck.ExpectedHTTPStatusCodes)}, 185 | } 186 | 187 | _, err := c.svc.ModifyTargetGroup(params) 188 | 189 | return err 190 | } 191 | 192 | func (c *Client) RetrieveTargetGroupByName(targetGroupName string) (*_elb.TargetGroup, error) { 193 | params := &_elb.DescribeTargetGroupsInput{ 194 | Names: _aws.StringSlice([]string{targetGroupName}), 195 | } 196 | res, err := c.svc.DescribeTargetGroups(params) 197 | if err != nil { 198 | if awsErr, ok := err.(awserr.Error); ok { 199 | if awsErr.Code() == "TargetGroupNotFound" { 200 | return nil, nil 201 | } 202 | } 203 | return nil, err 204 | } 205 | 206 | if len(res.TargetGroups) > 0 { 207 | return res.TargetGroups[0], nil 208 | } 209 | 210 | return nil, nil 211 | } 212 | 213 | func (c *Client) DeleteTargetGroup(targetGroupARN string) error { 214 | params := &_elb.DeleteTargetGroupInput{ 215 | TargetGroupArn: _aws.String(targetGroupARN), 216 | } 217 | 218 | _, err := c.svc.DeleteTargetGroup(params) 219 | 220 | return err 221 | } 222 | 223 | func (c *Client) CreateListener(loadBalancerARN, targetGroupARN string, port uint16, protocol, certificateARN string) error { 224 | params := &_elb.CreateListenerInput{ 225 | DefaultActions: []*_elb.Action{ 226 | { 227 | TargetGroupArn: _aws.String(targetGroupARN), 228 | Type: _aws.String(_elb.ActionTypeEnumForward), 229 | }, 230 | }, 231 | LoadBalancerArn: _aws.String(loadBalancerARN), 232 | Port: _aws.Int64(int64(port)), 233 | Protocol: _aws.String(protocol), 234 | } 235 | if certificateARN != "" { 236 | params.Certificates = []*_elb.Certificate{{CertificateArn: _aws.String(certificateARN)}} 237 | } 238 | 239 | _, err := c.svc.CreateListener(params) 240 | if err != nil { 241 | return err 242 | } 243 | 244 | return nil 245 | } 246 | 247 | func (c *Client) CreateTags(resourceARN string, tags map[string]string) error { 248 | params := &_elb.AddTagsInput{ 249 | ResourceArns: _aws.StringSlice([]string{resourceARN}), 250 | Tags: []*_elb.Tag{}, 251 | } 252 | 253 | for tk, tv := range tags { 254 | params.Tags = append(params.Tags, &_elb.Tag{ 255 | Key: _aws.String(tk), 256 | Value: _aws.String(tv), 257 | }) 258 | } 259 | 260 | _, err := c.svc.AddTags(params) 261 | 262 | return err 263 | } 264 | 265 | func (c *Client) RetrieveTags(resourceARN string) (map[string]string, error) { 266 | params := &_elb.DescribeTagsInput{ 267 | ResourceArns: _aws.StringSlice([]string{resourceARN}), 268 | } 269 | 270 | res, err := c.svc.DescribeTags(params) 271 | if err != nil { 272 | return nil, err 273 | } 274 | 275 | tags := map[string]string{} 276 | if len(res.TagDescriptions) == 0 { 277 | return tags, nil 278 | } 279 | for _, t := range res.TagDescriptions[0].Tags { 280 | tags[conv.S(t.Key)] = conv.S(t.Value) 281 | } 282 | 283 | return tags, nil 284 | } 285 | -------------------------------------------------------------------------------- /aws/elb/health_check.go: -------------------------------------------------------------------------------- 1 | package elb 2 | 3 | type HealthCheckParams struct { 4 | CheckIntervalSeconds uint16 5 | CheckPath string 6 | CheckPort *uint16 7 | Protocol string 8 | ExpectedHTTPStatusCodes string 9 | CheckTimeoutSeconds uint16 10 | HealthyThresholdCount uint16 11 | UnhealthyThresholdCount uint16 12 | } 13 | -------------------------------------------------------------------------------- /aws/iam/client.go: -------------------------------------------------------------------------------- 1 | package iam 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | _aws "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/aws/awserr" 9 | "github.com/aws/aws-sdk-go/aws/session" 10 | _iam "github.com/aws/aws-sdk-go/service/iam" 11 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 12 | ) 13 | 14 | type Client struct { 15 | svc *_iam.IAM 16 | } 17 | 18 | func New(session *session.Session, config *_aws.Config) *Client { 19 | return &Client{ 20 | svc: _iam.New(session, config), 21 | } 22 | } 23 | 24 | func (c *Client) RetrieveRole(roleName string) (*_iam.Role, error) { 25 | if roleName == "" { 26 | return nil, errors.New("roleName is empty") 27 | } 28 | 29 | params := &_iam.GetRoleInput{ 30 | RoleName: _aws.String(roleName), 31 | } 32 | 33 | res, err := c.svc.GetRole(params) 34 | if err != nil { 35 | if reqFail, ok := err.(awserr.RequestFailure); ok { 36 | if reqFail.StatusCode() == http.StatusNotFound { 37 | return nil, nil 38 | } 39 | } 40 | return nil, err 41 | } 42 | 43 | return res.Role, nil 44 | } 45 | 46 | func (c *Client) CreateRole(assumeRolePolicyDocument, roleName string) (*_iam.Role, error) { 47 | if assumeRolePolicyDocument == "" { 48 | return nil, errors.New("assumeRolePolicyDocument is empty") 49 | } 50 | if roleName == "" { 51 | return nil, errors.New("roleName is empty") 52 | } 53 | 54 | params := &_iam.CreateRoleInput{ 55 | AssumeRolePolicyDocument: _aws.String(assumeRolePolicyDocument), 56 | RoleName: _aws.String(roleName), 57 | Path: _aws.String("/"), 58 | } 59 | 60 | res, err := c.svc.CreateRole(params) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | return res.Role, nil 66 | } 67 | 68 | func (c *Client) AttachRolePolicy(policyARN, roleName string) error { 69 | if policyARN == "" { 70 | return errors.New("policyARN is empty") 71 | } 72 | if roleName == "" { 73 | return errors.New("roleName is empty") 74 | } 75 | 76 | params := &_iam.AttachRolePolicyInput{ 77 | PolicyArn: _aws.String(policyARN), 78 | RoleName: _aws.String(roleName), 79 | } 80 | 81 | _, err := c.svc.AttachRolePolicy(params) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | return nil 87 | } 88 | 89 | func (c *Client) ListRolePolicyNames(roleName string) ([]string, error) { 90 | policyNames := []string{} 91 | var marker *string 92 | 93 | for { 94 | params := &_iam.ListRolePoliciesInput{ 95 | Marker: marker, 96 | RoleName: _aws.String(roleName), 97 | } 98 | 99 | res, err := c.svc.ListRolePolicies(params) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | for _, p := range res.PolicyNames { 105 | policyNames = append(policyNames, conv.S(p)) 106 | } 107 | 108 | if !conv.B(res.IsTruncated) { 109 | break 110 | } 111 | 112 | marker = res.Marker 113 | } 114 | 115 | return policyNames, nil 116 | } 117 | 118 | func (c *Client) DetachRolePolicy(policyARN, roleName string) error { 119 | params := &_iam.DetachRolePolicyInput{ 120 | PolicyArn: _aws.String(policyARN), 121 | RoleName: _aws.String(roleName), 122 | } 123 | 124 | _, err := c.svc.DetachRolePolicy(params) 125 | 126 | return err 127 | } 128 | 129 | func (c *Client) DeleteRolePolicy(policyName, roleName string) error { 130 | params := &_iam.DeleteRolePolicyInput{ 131 | PolicyName: _aws.String(policyName), 132 | RoleName: _aws.String(roleName), 133 | } 134 | 135 | _, err := c.svc.DeleteRolePolicy(params) 136 | 137 | return err 138 | } 139 | 140 | func (c *Client) DeleteRole(roleName string) error { 141 | params := &_iam.DeleteRoleInput{ 142 | RoleName: _aws.String(roleName), 143 | } 144 | 145 | _, err := c.svc.DeleteRole(params) 146 | 147 | return err 148 | } 149 | 150 | func (c *Client) CreateInstanceProfile(profileName string) (*_iam.InstanceProfile, error) { 151 | params := &_iam.CreateInstanceProfileInput{ 152 | InstanceProfileName: _aws.String(profileName), 153 | } 154 | 155 | res, err := c.svc.CreateInstanceProfile(params) 156 | if err != nil { 157 | return nil, err 158 | } 159 | 160 | if res != nil { 161 | return res.InstanceProfile, nil 162 | } 163 | 164 | return nil, nil 165 | } 166 | 167 | func (c *Client) AddRoleToInstanceProfile(profileName, roleName string) error { 168 | params := &_iam.AddRoleToInstanceProfileInput{ 169 | InstanceProfileName: _aws.String(profileName), 170 | RoleName: _aws.String(roleName), 171 | } 172 | 173 | _, err := c.svc.AddRoleToInstanceProfile(params) 174 | 175 | return err 176 | } 177 | 178 | func (c *Client) RemoveRoleFromInstanceProfile(profileName, roleName string) error { 179 | params := &_iam.RemoveRoleFromInstanceProfileInput{ 180 | InstanceProfileName: _aws.String(profileName), 181 | RoleName: _aws.String(roleName), 182 | } 183 | 184 | _, err := c.svc.RemoveRoleFromInstanceProfile(params) 185 | 186 | return err 187 | } 188 | 189 | func (c *Client) RetrieveInstanceProfile(profileName string) (*_iam.InstanceProfile, error) { 190 | params := &_iam.GetInstanceProfileInput{ 191 | InstanceProfileName: _aws.String(profileName), 192 | } 193 | 194 | res, err := c.svc.GetInstanceProfile(params) 195 | if err != nil { 196 | if awsErr, ok := err.(awserr.Error); ok { 197 | if awsErr.Code() == "NoSuchEntity" { 198 | return nil, nil // instance profile not found 199 | } 200 | } 201 | return nil, err 202 | } 203 | 204 | return res.InstanceProfile, nil 205 | } 206 | 207 | func (c *Client) DeleteInstanceProfile(profileName string) error { 208 | params := &_iam.DeleteInstanceProfileInput{ 209 | InstanceProfileName: _aws.String(profileName), 210 | } 211 | 212 | _, err := c.svc.DeleteInstanceProfile(params) 213 | 214 | return err 215 | } 216 | -------------------------------------------------------------------------------- /aws/logs/client.go: -------------------------------------------------------------------------------- 1 | package logs 2 | 3 | import ( 4 | _aws "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/aws/session" 6 | _logs "github.com/aws/aws-sdk-go/service/cloudwatchlogs" 7 | ) 8 | 9 | type Client struct { 10 | svc *_logs.CloudWatchLogs 11 | awsRegion string 12 | } 13 | 14 | func New(session *session.Session, config *_aws.Config) *Client { 15 | return &Client{ 16 | awsRegion: *config.Region, 17 | svc: _logs.New(session, config), 18 | } 19 | } 20 | 21 | func (c *Client) CreateGroup(groupName string) error { 22 | params := &_logs.CreateLogGroupInput{ 23 | LogGroupName: _aws.String(groupName), 24 | } 25 | 26 | _, err := c.svc.CreateLogGroup(params) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | return nil 32 | } 33 | 34 | func (c *Client) ListGroups(groupNamePrefix string) ([]*_logs.LogGroup, error) { 35 | var nextToken *string 36 | groups := []*_logs.LogGroup{} 37 | 38 | for { 39 | params := &_logs.DescribeLogGroupsInput{ 40 | LogGroupNamePrefix: _aws.String(groupNamePrefix), 41 | NextToken: nextToken, 42 | } 43 | 44 | res, err := c.svc.DescribeLogGroups(params) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | groups = append(groups, res.LogGroups...) 50 | 51 | if res.NextToken == nil { 52 | break 53 | } else { 54 | nextToken = res.NextToken 55 | } 56 | } 57 | 58 | return groups, nil 59 | } 60 | -------------------------------------------------------------------------------- /aws/sns/client.go: -------------------------------------------------------------------------------- 1 | package sns 2 | 3 | import ( 4 | _aws "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/aws/session" 6 | _sns "github.com/aws/aws-sdk-go/service/sns" 7 | ) 8 | 9 | type Client struct { 10 | svc *_sns.SNS 11 | } 12 | 13 | func New(session *session.Session, config *_aws.Config) *Client { 14 | return &Client{ 15 | svc: _sns.New(session, config), 16 | } 17 | } 18 | 19 | func (c *Client) PublishToTopic(subject, message, topicARN string) error { 20 | params := &_sns.PublishInput{ 21 | Message: _aws.String(message), 22 | Subject: _aws.String(subject), 23 | TopicArn: _aws.String(topicARN), 24 | } 25 | 26 | _, err := c.svc.Publish(params) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /aws/utils.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import "strings" 4 | 5 | func GetIAMInstanceProfileNameFromARN(arn string) string { 6 | // format: "arn:aws:iam::865092420289:instance-profile/coldbrew_cluster1_instance_profile" 7 | tokens := strings.Split(arn, "/") 8 | if len(tokens) == 0 { 9 | return "" 10 | } 11 | return tokens[len(tokens)-1] 12 | } 13 | 14 | func GetECSTaskDefinitionFamilyAndRevisionFromARN(arn string) string { 15 | // format: "arn:aws:ecs:us-west-2:865092420289:task-definition/echo:112" 16 | tokens := strings.Split(arn, "/") 17 | if len(tokens) == 0 { 18 | return "" 19 | } 20 | return tokens[len(tokens)-1] 21 | } 22 | 23 | func GetECSContainerInstanceIDFromARN(arn string) string { 24 | // format: "arn:aws:ecs:us-west-2:865092420289:container-instance/72b93c91-0572-4d9d-b3d6-6e5cc5a0d2be" 25 | tokens := strings.Split(arn, "/") 26 | if len(tokens) == 0 { 27 | return "" 28 | } 29 | return tokens[len(tokens)-1] 30 | } 31 | -------------------------------------------------------------------------------- /commands/clustercreate/aws.go: -------------------------------------------------------------------------------- 1 | package clustercreate 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/aws/aws-sdk-go/service/ec2" 9 | "github.com/coldbrewcloud/coldbrew-cli/aws" 10 | "github.com/coldbrewcloud/coldbrew-cli/console" 11 | "github.com/coldbrewcloud/coldbrew-cli/core" 12 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 13 | ) 14 | 15 | const ( 16 | defaultECSContainerInstanceImageIDBaseURL = "https://s3-us-west-2.amazonaws.com/files.coldbrewcloud.com/coldbrew-cli/ecs-ci-ami/default/" 17 | defaultECSContainerInstanceImageOwnerID = "865092420289" 18 | ) 19 | 20 | var defaultImageID = map[string]string{ 21 | aws.AWSRegionAPNorthEast1: "ami-3217ed54", 22 | aws.AWSRegionAPSouthEast1: "ami-b30b67d0", 23 | aws.AWSRegionAPSouthEast2: "ami-5f38dd3d", 24 | aws.AWSRegionEUCentral1: "ami-3645f059", 25 | aws.AWSRegionEUWest1: "ami-d104c1a8", 26 | aws.AWSRegionUSEast1: "ami-c25a4eb9", 27 | aws.AWSRegionUSEast2: "ami-498dae2c", 28 | aws.AWSRegionUSWest1: "ami-fdcefa9d", 29 | aws.AWSRegionUSWest2: "ami-1d28dd65", 30 | } 31 | 32 | var defaultECSContainerInstanceAmazonImageID = map[string]string{ 33 | aws.AWSRegionUSEast1: "ami-1924770e", 34 | aws.AWSRegionUSEast2: "ami-bd3e64d8", 35 | aws.AWSRegionUSWest1: "ami-7f004b1f", 36 | aws.AWSRegionUSWest2: "ami-56ed4936", 37 | aws.AWSRegionEUWest1: "ami-c8337dbb", 38 | aws.AWSRegionEUCentral1: "ami-dd12ebb2", 39 | aws.AWSRegionAPNorthEast1: "ami-c8b016a9", 40 | aws.AWSRegionAPSouthEast1: "ami-6d22840e", 41 | aws.AWSRegionAPSouthEast2: "ami-73407d10", 42 | } 43 | 44 | func (c *Command) getAWSInfo() (string, string, []string, error) { 45 | regionName, vpcID, err := c.globalFlags.GetAWSRegionAndVPCID() 46 | if err != nil { 47 | return "", "", nil, err 48 | } 49 | 50 | // Subnet IDs 51 | subnetIDs, err := c.awsClient.EC2().ListVPCSubnets(vpcID) 52 | if err != nil { 53 | return "", "", nil, fmt.Errorf("Failed to list subnets of VPC [%s]: %s", vpcID, err.Error()) 54 | } 55 | if len(subnetIDs) == 0 { 56 | return "", "", nil, fmt.Errorf("VPC [%s] does not have any subnets.", vpcID) 57 | } 58 | 59 | return regionName, vpcID, subnetIDs, nil 60 | } 61 | 62 | func (c *Command) retrieveDefaultECSContainerInstancesImageID(region string) string { 63 | /*defaultImages, err := c.awsClient.EC2().FindImage(defaultECSContainerInstanceImageOwnerID, core.AWSTagNameCreatedTimestamp) 64 | if err == nil { 65 | var latestImage *ec2.Image 66 | var latestImageCreationTime string 67 | 68 | for _, image := range defaultImages { 69 | if conv.S(image.OwnerId) == defaultECSContainerInstanceImageOwnerID { 70 | if latestImage == nil { 71 | latestImageCreationTime = getCreationTimeFromTags(image.Tags) 72 | if latestImageCreationTime != "" { 73 | latestImage = image 74 | } 75 | } else { 76 | creationTime := getCreationTimeFromTags(image.Tags) 77 | if creationTime != "" { 78 | if strings.Compare(latestImageCreationTime, creationTime) < 0 { 79 | latestImage = image 80 | latestImageCreationTime = creationTime 81 | } 82 | } 83 | } 84 | } 85 | } 86 | 87 | if latestImage != nil { 88 | return conv.S(latestImage.ImageId) 89 | } 90 | }*/ 91 | if imageID, ok := defaultImageID[region]; ok { 92 | return imageID 93 | } 94 | 95 | // if failed to find coldbrew-cli default image, use Amazon ECS optimized image as fallback 96 | console.Error("Failed to retrieve default image ID for ECS Container Instances. Amazon ECS Optimized AMI will be used instead.") 97 | if imageID, ok := defaultECSContainerInstanceAmazonImageID[region]; ok { 98 | return imageID 99 | } 100 | return "" 101 | } 102 | 103 | func (c *Command) getDefaultInstanceUserData(ecsClusterName string) string { 104 | userData := fmt.Sprintf(`#!/bin/bash 105 | echo ECS_CLUSTER=%s >> /etc/ecs/ecs.config`, ecsClusterName) 106 | return base64.StdEncoding.EncodeToString([]byte(userData)) 107 | } 108 | 109 | func (c *Command) createDefaultInstanceProfile(profileName string) (string, error) { 110 | _, err := c.awsClient.IAM().CreateRole(core.EC2AssumeRolePolicy, profileName) 111 | if err != nil { 112 | return "", fmt.Errorf("Failed to create IAM Role [%s]: %s", profileName, err.Error()) 113 | } 114 | if err := c.awsClient.IAM().AttachRolePolicy(core.AdministratorAccessPolicyARN, profileName); err != nil { 115 | return "", fmt.Errorf("Failed to attach policy to IAM Role [%s]: %s", profileName, err.Error()) 116 | } 117 | 118 | iamInstanceProfile, err := c.awsClient.IAM().CreateInstanceProfile(profileName) 119 | if err != nil { 120 | return "", fmt.Errorf("Failed to create IAM Instance Profile [%s]: %s", profileName, err.Error()) 121 | } 122 | if iamInstanceProfile == nil { 123 | return "", fmt.Errorf("Failed to create IAM Instance Profile [%s]: empty result", profileName) 124 | } 125 | if err := c.awsClient.IAM().AddRoleToInstanceProfile(profileName, profileName); err != nil { 126 | return "", fmt.Errorf("Failed to add IAM Role [%s] to IAM Instance Profile [%s]: %s", profileName, profileName, err.Error()) 127 | } 128 | 129 | return conv.S(iamInstanceProfile.Arn), nil 130 | } 131 | 132 | func (c *Command) createECSServiceRole(roleName string) (string, error) { 133 | iamRole, err := c.awsClient.IAM().CreateRole(core.ECSAssumeRolePolicy, roleName) 134 | if err != nil { 135 | return "", fmt.Errorf("Failed to create IAM Role [%s]: %s", roleName, err.Error()) 136 | } 137 | if err := c.awsClient.IAM().AttachRolePolicy(core.ECSServiceRolePolicyARN, roleName); err != nil { 138 | return "", fmt.Errorf("Failed to attach policy to IAM Role [%s]: %s", roleName, err.Error()) 139 | } 140 | 141 | return conv.S(iamRole.Arn), nil 142 | } 143 | 144 | func (c *Command) waitAutoScalingGroupDeletion(autoScalingGroupName string) error { 145 | maxRetries := 60 146 | for i := 0; i < maxRetries; i++ { 147 | autoScalingGroup, err := c.awsClient.AutoScaling().RetrieveAutoScalingGroup(autoScalingGroupName) 148 | if err != nil { 149 | return fmt.Errorf("Failed to retrieve Auto Scaling Group [%s]: %s", autoScalingGroupName, err.Error()) 150 | } 151 | if autoScalingGroup == nil { 152 | break 153 | } 154 | 155 | time.Sleep(1 * time.Second) 156 | } 157 | return nil 158 | } 159 | 160 | func getCreationTimeFromTags(tags []*ec2.Tag) string { 161 | for _, tag := range tags { 162 | if conv.S(tag.Key) == core.AWSTagNameCreatedTimestamp { 163 | return conv.S(tag.Value) 164 | break 165 | } 166 | } 167 | return "" 168 | } 169 | -------------------------------------------------------------------------------- /commands/clustercreate/flags.go: -------------------------------------------------------------------------------- 1 | package clustercreate 2 | 3 | import ( 4 | "github.com/coldbrewcloud/coldbrew-cli/core" 5 | "gopkg.in/alecthomas/kingpin.v2" 6 | ) 7 | 8 | type Flags struct { 9 | InstanceType *string `json:"instance_type"` 10 | InitialCapacity *uint16 `json:"initial_capacity"` 11 | NoKeyPair *bool `json:"no-keypair"` 12 | KeyPairName *string `json:"keypair_name"` 13 | InstanceProfile *string `json:"instance_profile"` 14 | InstanceImageID *string `json:"instance_image_id"` 15 | InstanceUserDataFile *string `json:"instance_user_data_file"` 16 | ForceCreate *bool `json:"force"` 17 | } 18 | 19 | func NewFlags(kc *kingpin.CmdClause) *Flags { 20 | return &Flags{ 21 | InstanceType: kc.Flag("instance-type", "Container instance type").Default(core.DefaultContainerInstanceType()).String(), 22 | InitialCapacity: kc.Flag("instance-count", "Initial number of container instances").Default("1").Uint16(), 23 | NoKeyPair: kc.Flag("disable-keypair", "Do not assign EC2 keypairs").Bool(), 24 | KeyPairName: kc.Flag("key", "EC2 keypair name").Default("").String(), 25 | InstanceProfile: kc.Flag("instance-profile", "IAM instance profile name for container instances").Default("").String(), 26 | InstanceImageID: kc.Flag("instance-image", "EC2 Image (AMI) ID for ECS Container Instances").Default("").String(), 27 | InstanceUserDataFile: kc.Flag("instance-userdata", "File path that contains userdata for ECS Container Instances").Default("").String(), 28 | ForceCreate: kc.Flag("yes", "Create all resource with no confirmation").Short('y').Default("false").Bool(), 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /commands/clusterdelete/aws.go: -------------------------------------------------------------------------------- 1 | package clusterdelete 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/aws/aws-sdk-go/service/autoscaling" 8 | "github.com/coldbrewcloud/coldbrew-cli/core" 9 | "github.com/coldbrewcloud/coldbrew-cli/utils" 10 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 11 | ) 12 | 13 | func (c *Command) getAWSInfo() (string, string, []string, error) { 14 | regionName, vpcID, err := c.globalFlags.GetAWSRegionAndVPCID() 15 | if err != nil { 16 | return "", "", nil, err 17 | } 18 | 19 | // Subnet IDs 20 | subnetIDs, err := c.awsClient.EC2().ListVPCSubnets(vpcID) 21 | if err != nil { 22 | return "", "", nil, fmt.Errorf("Failed to list subnets of VPC [%s]: %s", vpcID, err.Error()) 23 | } 24 | if len(subnetIDs) == 0 { 25 | return "", "", nil, fmt.Errorf("VPC [%s] does not have any subnets.", vpcID) 26 | } 27 | 28 | return regionName, vpcID, subnetIDs, nil 29 | } 30 | 31 | func (c *Command) scaleDownAutoScalingGroup(autoScalingGroup *autoscaling.Group) error { 32 | if autoScalingGroup == nil { 33 | return nil 34 | } 35 | 36 | asgName := conv.S(autoScalingGroup.AutoScalingGroupName) 37 | if err := c.awsClient.AutoScaling().SetAutoScalingGroupDesiredCapacity(asgName, 0); err != nil { 38 | return fmt.Errorf("Failed to change Auto Scaling Group [%s] desired capacity to 0: %s", asgName, err.Error()) 39 | } 40 | 41 | return utils.Retry(func() (bool, error) { 42 | asg, err := c.awsClient.AutoScaling().RetrieveAutoScalingGroup(asgName) 43 | if err != nil { 44 | return false, fmt.Errorf("Failed to retrieve Auto Scaling Group [%s]: %s", asgName, err.Error()) 45 | } 46 | if asg == nil { 47 | return false, fmt.Errorf("Auto Scaling Group [%s] not found", asgName) 48 | } 49 | if len(asg.Instances) == 0 { 50 | return false, nil 51 | } 52 | return true, nil 53 | }, time.Second, 5*time.Minute) 54 | } 55 | 56 | func (c *Command) deleteECSServiceRole(roleName string) error { 57 | if err := c.awsClient.IAM().DetachRolePolicy(core.ECSServiceRolePolicyARN, roleName); err != nil { 58 | return fmt.Errorf("Failed to detach ECS Service Role Policy from IAM Role [%s]: %s", roleName, err.Error()) 59 | } 60 | 61 | if err := c.awsClient.IAM().DeleteRole(roleName); err != nil { 62 | return fmt.Errorf("Failed to delete IAM Role [%s]: %s", roleName, err.Error()) 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func (c *Command) deleteDefaultInstanceProfile(profileName string) error { 69 | if err := c.awsClient.IAM().RemoveRoleFromInstanceProfile(profileName, profileName); err != nil { 70 | return fmt.Errorf("Failed to remove IAM Role [%s] from Instance Profile [%s]: %s", profileName, profileName, err.Error()) 71 | } 72 | 73 | if err := c.awsClient.IAM().DetachRolePolicy(core.AdministratorAccessPolicyARN, profileName); err != nil { 74 | return fmt.Errorf("Failed to detach Administrator Access Policy from IAM Role [%s]: %s", profileName, err.Error()) 75 | } 76 | 77 | if err := c.awsClient.IAM().DeleteRole(profileName); err != nil { 78 | return fmt.Errorf("Failed to delete IAM Role [%s]: %s", profileName, err.Error()) 79 | } 80 | 81 | if err := c.awsClient.IAM().DeleteInstanceProfile(profileName); err != nil { 82 | return fmt.Errorf("Failed to delete Instance Profile [%s]: %s", profileName, err.Error()) 83 | } 84 | 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /commands/clusterdelete/command.go: -------------------------------------------------------------------------------- 1 | package clusterdelete 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/coldbrewcloud/coldbrew-cli/aws" 9 | "github.com/coldbrewcloud/coldbrew-cli/console" 10 | "github.com/coldbrewcloud/coldbrew-cli/core" 11 | "github.com/coldbrewcloud/coldbrew-cli/flags" 12 | "github.com/coldbrewcloud/coldbrew-cli/utils" 13 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 14 | "gopkg.in/alecthomas/kingpin.v2" 15 | ) 16 | 17 | type Command struct { 18 | globalFlags *flags.GlobalFlags 19 | commandFlags *Flags 20 | awsClient *aws.Client 21 | clusterNameArg *string 22 | } 23 | 24 | func (c *Command) Init(ka *kingpin.Application, globalFlags *flags.GlobalFlags) *kingpin.CmdClause { 25 | c.globalFlags = globalFlags 26 | 27 | cmd := ka.Command("cluster-delete", 28 | "See: "+console.ColorFnHelpLink("https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-cluster-delete")) 29 | c.commandFlags = NewFlags(cmd) 30 | 31 | c.clusterNameArg = cmd.Arg("cluster-name", "Cluster name").Required().String() 32 | 33 | return cmd 34 | } 35 | 36 | func (c *Command) Run() error { 37 | c.awsClient = c.globalFlags.GetAWSClient() 38 | 39 | clusterName := strings.TrimSpace(conv.S(c.clusterNameArg)) 40 | if !core.ClusterNameRE.MatchString(clusterName) { 41 | return console.ExitWithError(core.NewErrorExtraInfo( 42 | fmt.Errorf("Invalid cluster name [%s]", clusterName), "https://github.com/coldbrewcloud/coldbrew-cli/wiki/Configuration-File#cluster")) 43 | } 44 | 45 | console.Info("Determining AWS resources that need to be deleted...") 46 | deleteECSCluster := false 47 | deleteECSServiceRole := false 48 | deleteInstanceProfile := false 49 | deleteInstanceSecurityGroups := false 50 | deleteLaunchConfiguration := false 51 | deleteAutoScalingGroup := false 52 | 53 | // ECS cluster 54 | ecsClusterName := core.DefaultECSClusterName(clusterName) 55 | ecsCluster, err := c.awsClient.ECS().RetrieveCluster(ecsClusterName) 56 | if err != nil { 57 | return console.ExitWithErrorString("Failed to retrieve ECS Cluster [%s]: %s", ecsClusterName, err.Error()) 58 | } 59 | if ecsCluster != nil && conv.S(ecsCluster.Status) != "INACTIVE" { 60 | deleteECSCluster = true 61 | console.DetailWithResource("ECS Cluster", ecsClusterName) 62 | } 63 | 64 | // ECS service role 65 | ecsServiceRoleName := core.DefaultECSServiceRoleName(clusterName) 66 | ecsServiceRole, err := c.awsClient.IAM().RetrieveRole(ecsServiceRoleName) 67 | if err != nil { 68 | return console.ExitWithErrorString("Failed to retrieve IAM Role [%s]: %s", ecsServiceRoleName, err.Error()) 69 | } 70 | if ecsServiceRole != nil { 71 | deleteECSServiceRole = true 72 | console.DetailWithResource("IAM Role for ECS Services", ecsServiceRoleName) 73 | } 74 | 75 | // launch configuration 76 | lcName := core.DefaultLaunchConfigurationName(clusterName) 77 | launchConfiguration, err := c.awsClient.AutoScaling().RetrieveLaunchConfiguration(lcName) 78 | if err != nil { 79 | return console.ExitWithErrorString("Failed to delete Launch Configuration [%s]: %s", lcName, err.Error()) 80 | } 81 | if launchConfiguration != nil { 82 | deleteLaunchConfiguration = true 83 | console.DetailWithResource("EC2 Launch Configuration for ECS Container Instances", lcName) 84 | } 85 | 86 | // auto scaling group 87 | asgName := core.DefaultAutoScalingGroupName(clusterName) 88 | autoScalingGroup, err := c.awsClient.AutoScaling().RetrieveAutoScalingGroup(asgName) 89 | if err != nil { 90 | return console.ExitWithErrorString("Failed to retrieve Auto Scaling Group [%s]: %s", asgName, err.Error()) 91 | } 92 | if autoScalingGroup != nil && utils.IsBlank(conv.S(autoScalingGroup.Status)) { 93 | tags, err := c.awsClient.AutoScaling().RetrieveTagsForAutoScalingGroup(asgName) 94 | if err != nil { 95 | return console.ExitWithErrorString("Failed to retrieve tags for EC2 Auto Scaling Group [%s]: %s", asgName, err.Error()) 96 | } 97 | if _, ok := tags[core.AWSTagNameCreatedTimestamp]; ok { 98 | deleteAutoScalingGroup = true 99 | console.DetailWithResource("EC2 Auto Scaling Group for ECS Container Instances", asgName) 100 | } 101 | } 102 | 103 | // instance profile 104 | instanceProfileName := core.DefaultInstanceProfileName(clusterName) 105 | instanceProfile, err := c.awsClient.IAM().RetrieveInstanceProfile(instanceProfileName) 106 | if err != nil { 107 | return console.ExitWithErrorString("Failed to retrieve Instance Profile [%s]: %s", instanceProfileName, err.Error()) 108 | } 109 | if instanceProfile != nil { 110 | deleteInstanceProfile = true 111 | console.DetailWithResource("IAM Instance Profile for ECS Container Instances", instanceProfileName) 112 | } 113 | 114 | // instance security group 115 | sgName := core.DefaultInstanceSecurityGroupName(clusterName) 116 | securityGroup, err := c.awsClient.EC2().RetrieveSecurityGroupByName(sgName) 117 | if err != nil { 118 | return console.ExitWithErrorString("Failed to retrieve Security Group [%s]: %s", sgName, err.Error()) 119 | } 120 | if securityGroup != nil { 121 | tags, err := c.awsClient.EC2().RetrieveTags(conv.S(securityGroup.GroupId)) 122 | if err != nil { 123 | return console.ExitWithErrorString("Failed to retrieve tags for EC2 Security Group [%s]: %s", sgName, err.Error()) 124 | } 125 | if _, ok := tags[core.AWSTagNameCreatedTimestamp]; ok { 126 | deleteInstanceSecurityGroups = true 127 | console.DetailWithResource("EC2 Security Group for ECS Container Instances", sgName) 128 | } 129 | } 130 | 131 | if !deleteECSServiceRole && !deleteECSCluster && !deleteLaunchConfiguration && !deleteAutoScalingGroup && 132 | !deleteInstanceProfile && !deleteInstanceSecurityGroups { 133 | console.Info("Looks like everything's already cleaned up.") 134 | return nil 135 | } 136 | 137 | console.Blank() 138 | 139 | // confirmation 140 | if !conv.B(c.commandFlags.ForceDelete) && !console.AskConfirm("Do you want to delete these resources?", false) { 141 | return nil 142 | } 143 | 144 | console.Blank() 145 | 146 | // delete auto scaling group 147 | if deleteAutoScalingGroup { 148 | console.UpdatingResource("Terminating instances in EC2 Auto Scaling Group", asgName, true) 149 | 150 | if err := c.scaleDownAutoScalingGroup(autoScalingGroup); err != nil { 151 | if conv.B(c.commandFlags.ContinueOnError) { 152 | console.Error(err.Error()) 153 | } else { 154 | return console.ExitWithError(err) 155 | } 156 | } else { 157 | console.RemovingResource("Deleting EC2 Auto Scaling Group", asgName, true) 158 | if err := c.awsClient.AutoScaling().DeleteAutoScalingGroup(asgName, true); err != nil { 159 | if conv.B(c.commandFlags.ContinueOnError) { 160 | console.Error(err.Error()) 161 | } else { 162 | return console.ExitWithError(err) 163 | } 164 | } 165 | } 166 | } 167 | 168 | // delete launch configuration 169 | if deleteLaunchConfiguration { 170 | console.RemovingResource("Deleting EC2 Launch Configuration", lcName, false) 171 | 172 | if err := c.awsClient.AutoScaling().DeleteLaunchConfiguration(lcName); err != nil { 173 | err = fmt.Errorf("Failed to delete Launch Configuration [%s]: %s", lcName, err.Error()) 174 | if conv.B(c.commandFlags.ContinueOnError) { 175 | console.Error(err.Error()) 176 | } else { 177 | return console.ExitWithError(err) 178 | } 179 | } 180 | } 181 | 182 | // delete instance profile 183 | if deleteInstanceProfile { 184 | console.RemovingResource("Deleting IAM Instance Profile", instanceProfileName, false) 185 | 186 | if err := c.deleteDefaultInstanceProfile(instanceProfileName); err != nil { 187 | if conv.B(c.commandFlags.ContinueOnError) { 188 | console.Error(err.Error()) 189 | } else { 190 | return console.ExitWithError(err) 191 | } 192 | } 193 | } 194 | 195 | // delete instance security groups 196 | if deleteInstanceSecurityGroups { 197 | console.RemovingResource("Deleting EC2 Security Group", sgName, false) 198 | 199 | err = utils.RetryOnAWSErrorCode(func() error { 200 | return c.awsClient.EC2().DeleteSecurityGroup(conv.S(securityGroup.GroupId)) 201 | }, []string{"DependencyViolation", "ResourceInUse"}, time.Second, 1*time.Minute) 202 | if err != nil { 203 | err = fmt.Errorf("Failed to delete Security Group [%s]: %s", sgName, err.Error()) 204 | if conv.B(c.commandFlags.ContinueOnError) { 205 | console.Error(err.Error()) 206 | } else { 207 | return console.ExitWithError(err) 208 | } 209 | } 210 | } 211 | 212 | // delete ECS cluster 213 | if deleteECSCluster { 214 | console.RemovingResource("Deleting ECS Cluster", ecsClusterName, false) 215 | 216 | if err := c.awsClient.ECS().DeleteCluster(ecsClusterName); err != nil { 217 | //if awsErr, ok := err.(awserr.Error); ok { 218 | // if awsErr.Code() == "ClusterContainsContainerInstancesException" { 219 | // } 220 | //} 221 | // 222 | err = fmt.Errorf("Failed to delete ECS Cluster [%s]: %s", ecsServiceRoleName, err.Error()) 223 | if conv.B(c.commandFlags.ContinueOnError) { 224 | console.Error(err.Error()) 225 | } else { 226 | return console.ExitWithError(err) 227 | } 228 | } 229 | } 230 | 231 | // delete ECS service role 232 | if deleteECSServiceRole { 233 | console.RemovingResource("Deleting IAM Role", ecsServiceRoleName, false) 234 | 235 | if err := c.deleteECSServiceRole(ecsServiceRoleName); err != nil { 236 | if conv.B(c.commandFlags.ContinueOnError) { 237 | console.Error(err.Error()) 238 | } else { 239 | return console.ExitWithError(err) 240 | } 241 | } 242 | } 243 | 244 | return nil 245 | } 246 | -------------------------------------------------------------------------------- /commands/clusterdelete/flags.go: -------------------------------------------------------------------------------- 1 | package clusterdelete 2 | 3 | import "gopkg.in/alecthomas/kingpin.v2" 4 | 5 | type Flags struct { 6 | ForceDelete *bool `json:"force"` 7 | ContinueOnError *bool `json:"continue"` 8 | } 9 | 10 | func NewFlags(kc *kingpin.CmdClause) *Flags { 11 | return &Flags{ 12 | ForceDelete: kc.Flag("yes", "Delete all resources with no confirmation").Short('y').Default("false").Bool(), 13 | ContinueOnError: kc.Flag("continue", "Continue deleting resources on error").Default("false").Bool(), 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /commands/clusterscale/command.go: -------------------------------------------------------------------------------- 1 | package clusterscale 2 | 3 | import ( 4 | "fmt" 5 | 6 | "time" 7 | 8 | "strings" 9 | 10 | "github.com/coldbrewcloud/coldbrew-cli/aws" 11 | "github.com/coldbrewcloud/coldbrew-cli/console" 12 | "github.com/coldbrewcloud/coldbrew-cli/core" 13 | "github.com/coldbrewcloud/coldbrew-cli/flags" 14 | "github.com/coldbrewcloud/coldbrew-cli/utils" 15 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 16 | "gopkg.in/alecthomas/kingpin.v2" 17 | ) 18 | 19 | type Command struct { 20 | globalFlags *flags.GlobalFlags 21 | commandFlags *Flags 22 | awsClient *aws.Client 23 | clusterNameArg *string 24 | instanceCount *uint16 25 | } 26 | 27 | func (c *Command) Init(ka *kingpin.Application, globalFlags *flags.GlobalFlags) *kingpin.CmdClause { 28 | c.globalFlags = globalFlags 29 | 30 | cmd := ka.Command("cluster-scale", 31 | "See: "+console.ColorFnHelpLink("https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-cluster-scale")) 32 | c.commandFlags = NewFlags(cmd) 33 | 34 | c.clusterNameArg = cmd.Arg("cluster-name", "Cluster name").Required().String() 35 | 36 | c.instanceCount = cmd.Arg("instance-count", "Number of instances").Uint16() 37 | 38 | return cmd 39 | } 40 | 41 | func (c *Command) Run() error { 42 | c.awsClient = c.globalFlags.GetAWSClient() 43 | 44 | clusterName := strings.TrimSpace(conv.S(c.clusterNameArg)) 45 | if !core.ClusterNameRE.MatchString(clusterName) { 46 | return console.ExitWithError(core.NewErrorExtraInfo( 47 | fmt.Errorf("Invalid cluster name [%s]", clusterName), "https://github.com/coldbrewcloud/coldbrew-cli/wiki/Configuration-File#cluster")) 48 | } 49 | 50 | autoScalingGroupName := core.DefaultAutoScalingGroupName(clusterName) 51 | 52 | console.Info("Auto Scaling Group") 53 | console.DetailWithResource("Name", autoScalingGroupName) 54 | 55 | autoScalingGroup, err := c.awsClient.AutoScaling().RetrieveAutoScalingGroup(autoScalingGroupName) 56 | if err != nil { 57 | return console.ExitWithErrorString("Failed to retrieve EC2 Auto Scaling Group [%s]: %s", autoScalingGroupName, err.Error()) 58 | } 59 | if autoScalingGroup == nil { 60 | return console.ExitWithErrorString("EC2 Auto Scaling Group [%s] was not found.", autoScalingGroupName) 61 | } 62 | if !utils.IsBlank(conv.S(autoScalingGroup.Status)) { 63 | return console.ExitWithErrorString("EC2 Auto Scaling Group [%s] is being deleted: %s", autoScalingGroupName, conv.S(autoScalingGroup.Status)) 64 | } 65 | 66 | currentTarget := uint16(conv.I64(autoScalingGroup.DesiredCapacity)) 67 | newTarget := conv.U16(c.instanceCount) 68 | 69 | console.DetailWithResource("Current Target", fmt.Sprintf("%d", currentTarget)) 70 | console.DetailWithResource("New Target", fmt.Sprintf("%d", newTarget)) 71 | 72 | console.Blank() 73 | 74 | if currentTarget < newTarget { 75 | if err := c.scaleOut(autoScalingGroupName, currentTarget, newTarget); err != nil { 76 | return console.ExitWithError(err) 77 | } 78 | } else if currentTarget > newTarget { 79 | if err := c.scaleIn(autoScalingGroupName, currentTarget, newTarget); err != nil { 80 | return console.ExitWithError(err) 81 | } 82 | } else { 83 | 84 | } 85 | 86 | return nil 87 | } 88 | 89 | func (c *Command) scaleOut(autoScalingGroupName string, currentTarget, newTarget uint16) error { 90 | console.UpdatingResource(fmt.Sprintf("Updating desired capacity to %d", newTarget), autoScalingGroupName, true) 91 | 92 | err := c.awsClient.AutoScaling().UpdateAutoScalingGroupCapacity(autoScalingGroupName, 0, newTarget, newTarget) 93 | if err != nil { 94 | return fmt.Errorf("Failed to update capacity of EC2 Auto Scaling Group [%s]: %s", autoScalingGroupName, err.Error()) 95 | } 96 | 97 | err = utils.Retry(func() (bool, error) { 98 | autoScalingGroup, err := c.awsClient.AutoScaling().RetrieveAutoScalingGroup(autoScalingGroupName) 99 | if err != nil { 100 | return false, fmt.Errorf("Failed to retrieve EC2 Auto Scaling Group [%s]: %s", autoScalingGroupName, err.Error()) 101 | } 102 | if autoScalingGroup == nil { 103 | return false, fmt.Errorf("EC2 Auto Scaling Group [%s] was not found.", autoScalingGroupName) 104 | } 105 | if uint16(len(autoScalingGroup.Instances)) == newTarget { 106 | return false, nil 107 | } 108 | return true, nil 109 | }, time.Second, 5*time.Minute) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | console.Blank() 115 | //console.Info(fmt.Sprintf("EC2 Auto Scaling Group [%s] now has %d instances.", autoScalingGroupName, newTarget)) 116 | 117 | return nil 118 | } 119 | 120 | func (c *Command) scaleIn(autoScalingGroupName string, currentTarget, newTarget uint16) error { 121 | console.UpdatingResource(fmt.Sprintf("Updating desired capacity to %d", newTarget), autoScalingGroupName, true) 122 | 123 | err := c.awsClient.AutoScaling().UpdateAutoScalingGroupCapacity(autoScalingGroupName, 0, newTarget, newTarget) 124 | if err != nil { 125 | return fmt.Errorf("Failed to update capacity of EC2 Auto Scaling Group [%s]: %s", autoScalingGroupName, err.Error()) 126 | } 127 | 128 | err = utils.Retry(func() (bool, error) { 129 | autoScalingGroup, err := c.awsClient.AutoScaling().RetrieveAutoScalingGroup(autoScalingGroupName) 130 | if err != nil { 131 | return false, fmt.Errorf("Failed to retrieve EC2 Auto Scaling Group [%s]: %s", autoScalingGroupName, err.Error()) 132 | } 133 | if autoScalingGroup == nil { 134 | return false, fmt.Errorf("EC2 Auto Scaling Group [%s] was not found.", autoScalingGroupName) 135 | } 136 | if uint16(len(autoScalingGroup.Instances)) == newTarget { 137 | return false, nil 138 | } 139 | return true, nil 140 | }, time.Second, 5*time.Minute) 141 | if err != nil { 142 | return err 143 | } 144 | 145 | console.Blank() 146 | //console.Info(fmt.Sprintf("EC2 Auto Scaling Group [%s] now has %d instances.", autoScalingGroupName, newTarget)) 147 | 148 | return nil 149 | } 150 | -------------------------------------------------------------------------------- /commands/clusterscale/flags.go: -------------------------------------------------------------------------------- 1 | package clusterscale 2 | 3 | import "gopkg.in/alecthomas/kingpin.v2" 4 | 5 | type Flags struct { 6 | } 7 | 8 | func NewFlags(kc *kingpin.CmdClause) *Flags { 9 | return &Flags{} 10 | } 11 | -------------------------------------------------------------------------------- /commands/clusterstatus/aws.go: -------------------------------------------------------------------------------- 1 | package clusterstatus 2 | 3 | import "fmt" 4 | 5 | func (c *Command) getAWSInfo() (string, string, []string, error) { 6 | regionName, vpcID, err := c.globalFlags.GetAWSRegionAndVPCID() 7 | if err != nil { 8 | return "", "", nil, err 9 | } 10 | 11 | // Subnet IDs 12 | subnetIDs, err := c.awsClient.EC2().ListVPCSubnets(vpcID) 13 | if err != nil { 14 | return "", "", nil, fmt.Errorf("Failed to list subnets of VPC [%s]: %s", vpcID, err.Error()) 15 | } 16 | if len(subnetIDs) == 0 { 17 | return "", "", nil, fmt.Errorf("VPC [%s] does not have any subnets.", vpcID) 18 | } 19 | 20 | return regionName, vpcID, subnetIDs, nil 21 | } 22 | -------------------------------------------------------------------------------- /commands/clusterstatus/command.go: -------------------------------------------------------------------------------- 1 | package clusterstatus 2 | 3 | import ( 4 | "strings" 5 | 6 | "fmt" 7 | 8 | "github.com/coldbrewcloud/coldbrew-cli/aws" 9 | "github.com/coldbrewcloud/coldbrew-cli/console" 10 | "github.com/coldbrewcloud/coldbrew-cli/core" 11 | "github.com/coldbrewcloud/coldbrew-cli/flags" 12 | "github.com/coldbrewcloud/coldbrew-cli/utils" 13 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 14 | "gopkg.in/alecthomas/kingpin.v2" 15 | ) 16 | 17 | type Command struct { 18 | globalFlags *flags.GlobalFlags 19 | commandFlags *Flags 20 | awsClient *aws.Client 21 | clusterNameArg *string 22 | } 23 | 24 | func (c *Command) Init(ka *kingpin.Application, globalFlags *flags.GlobalFlags) *kingpin.CmdClause { 25 | c.globalFlags = globalFlags 26 | 27 | cmd := ka.Command("cluster-status", 28 | "See: "+console.ColorFnHelpLink("https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-cluster-status")) 29 | c.commandFlags = NewFlags(cmd) 30 | 31 | c.clusterNameArg = cmd.Arg("cluster-name", "Cluster name").Required().String() 32 | 33 | return cmd 34 | } 35 | 36 | func (c *Command) Run() error { 37 | c.awsClient = c.globalFlags.GetAWSClient() 38 | 39 | // AWS networking 40 | regionName, vpcID, subnetIDs, err := c.getAWSInfo() 41 | if err != nil { 42 | return console.ExitWithError(err) 43 | } 44 | 45 | // cluster name 46 | clusterName := strings.TrimSpace(conv.S(c.clusterNameArg)) 47 | if !core.ClusterNameRE.MatchString(clusterName) { 48 | return console.ExitWithError(core.NewErrorExtraInfo( 49 | fmt.Errorf("Invalid cluster name [%s]", clusterName), "https://github.com/coldbrewcloud/coldbrew-cli/wiki/Configuration-File#cluster")) 50 | } 51 | 52 | console.Info("Cluster") 53 | console.DetailWithResource("Name", clusterName) 54 | 55 | // AWS env 56 | console.Info("AWS") 57 | console.DetailWithResource("Region", regionName) 58 | console.DetailWithResource("VPC", vpcID) 59 | console.DetailWithResource("Subnets", strings.Join(subnetIDs, " ")) 60 | 61 | // ECS 62 | console.Info("ECS") 63 | showECSClusterDetails := false 64 | 65 | // ecs cluster 66 | ecsClusterName := core.DefaultECSClusterName(clusterName) 67 | ecsCluster, err := c.awsClient.ECS().RetrieveCluster(ecsClusterName) 68 | if err != nil { 69 | return console.ExitWithErrorString("Failed to retrieve ECS Cluster [%s]: %s", ecsClusterName, err.Error()) 70 | } 71 | if ecsCluster == nil || conv.S(ecsCluster.Status) == "INACTIVE" { 72 | console.DetailWithResourceNote("ECS Cluster", ecsClusterName, "(not found)", true) 73 | ecsCluster = nil 74 | } else { 75 | console.DetailWithResource("ECS Cluster", ecsClusterName) 76 | showECSClusterDetails = true 77 | } 78 | 79 | // ecs service role 80 | ecsServiceRoleName := core.DefaultECSServiceRoleName(clusterName) 81 | ecsServiceRole, err := c.awsClient.IAM().RetrieveRole(ecsServiceRoleName) 82 | if err != nil { 83 | return console.ExitWithErrorString("Failed to retrieve IAM Role [%s]: %s", ecsServiceRoleName, err.Error()) 84 | } 85 | if ecsServiceRole == nil { 86 | console.DetailWithResourceNote("IAM Role for ECS Services", ecsServiceRoleName, "(not found)", true) 87 | } else { 88 | console.DetailWithResource("IAM Role for ECS Services", ecsServiceRoleName) 89 | } 90 | 91 | // ecs cluster details 92 | if showECSClusterDetails { 93 | console.DetailWithResource("ECS Services", conv.I64S(conv.I64(ecsCluster.ActiveServicesCount))) 94 | console.DetailWithResource("ECS Tasks (running/pending)", 95 | fmt.Sprintf("%d/%d", 96 | conv.I64(ecsCluster.RunningTasksCount), 97 | conv.I64(ecsCluster.PendingTasksCount))) 98 | console.DetailWithResource("ECS Container Instances", conv.I64S(conv.I64(ecsCluster.RegisteredContainerInstancesCount))) 99 | 100 | } 101 | 102 | // launch config and auto scaling group 103 | console.Info("Auto Scaling") 104 | 105 | // launch configuration 106 | launchConfigName := core.DefaultLaunchConfigurationName(clusterName) 107 | launchConfig, err := c.awsClient.AutoScaling().RetrieveLaunchConfiguration(launchConfigName) 108 | if err != nil { 109 | return console.ExitWithErrorString("Failed to retrieve Launch Configuration [%s]: %s", launchConfigName, err.Error()) 110 | } 111 | if launchConfig == nil { 112 | console.DetailWithResourceNote("EC2 Launch Configuration", launchConfigName, "(not found)", true) 113 | } else { 114 | console.DetailWithResource("EC2 Launch Configuration", launchConfigName) 115 | 116 | instanceProfileARN := conv.S(launchConfig.IamInstanceProfile) 117 | console.DetailWithResource(" IAM Instance Profile", aws.GetIAMInstanceProfileNameFromARN(instanceProfileARN)) 118 | 119 | console.DetailWithResource(" Instance Type", conv.S(launchConfig.InstanceType)) 120 | console.DetailWithResource(" Image ID", conv.S(launchConfig.ImageId)) 121 | console.DetailWithResource(" Key Pair", conv.S(launchConfig.KeyName)) 122 | 123 | securityGroupIDs := []string{} 124 | for _, sg := range launchConfig.SecurityGroups { 125 | securityGroupIDs = append(securityGroupIDs, conv.S(sg)) 126 | } 127 | securityGroups, err := c.awsClient.EC2().RetrieveSecurityGroups(securityGroupIDs) 128 | if err != nil { 129 | return console.ExitWithErrorString("Failed to retrieve Security Groups [%s]: %s", strings.Join(securityGroupIDs, ","), err.Error()) 130 | } 131 | securityGroupNames := []string{} 132 | for _, sg := range securityGroups { 133 | securityGroupNames = append(securityGroupNames, conv.S(sg.GroupName)) 134 | } 135 | console.DetailWithResource(" Security Groups", strings.Join(securityGroupNames, " ")) 136 | } 137 | 138 | // auto scaling group 139 | autoScalingGroupName := core.DefaultAutoScalingGroupName(clusterName) 140 | autoScalingGroup, err := c.awsClient.AutoScaling().RetrieveAutoScalingGroup(autoScalingGroupName) 141 | if err != nil { 142 | return console.ExitWithErrorString("Failed to retrieve Auto Scaling Group [%s]: %s", autoScalingGroupName, err.Error()) 143 | } 144 | if autoScalingGroup == nil { 145 | console.DetailWithResourceNote("EC2 Auto Scaling Group", autoScalingGroupName, "(not found)", true) 146 | } else if utils.IsBlank(conv.S(autoScalingGroup.Status)) { 147 | console.DetailWithResource("EC2 Auto Scaling Group", autoScalingGroupName) 148 | console.DetailWithResource(" Instances (current/desired/min/max)", 149 | fmt.Sprintf("%d/%d/%d/%d", 150 | len(autoScalingGroup.Instances), 151 | conv.I64(autoScalingGroup.DesiredCapacity), 152 | conv.I64(autoScalingGroup.MinSize), 153 | conv.I64(autoScalingGroup.MaxSize))) 154 | } else { 155 | console.DetailWithResourceNote("EC2 Auto Scaling Group", autoScalingGroupName, "(deleting)", true) 156 | } 157 | 158 | // ECS Container Instances 159 | if ecsCluster != nil && !conv.B(c.commandFlags.ExcludeContainerInstanceInfos) { 160 | containerInstanceARNs, err := c.awsClient.ECS().ListContainerInstanceARNs(ecsClusterName) 161 | if err != nil { 162 | return console.ExitWithErrorString("Failed to list ECS Container Instances: %s", err.Error()) 163 | } 164 | containerInstances, err := c.awsClient.ECS().RetrieveContainerInstances(ecsClusterName, containerInstanceARNs) 165 | if err != nil { 166 | return console.ExitWithErrorString("Failed to retrieve ECS Container Instances: %s", err.Error()) 167 | } 168 | 169 | // retrieve EC2 Instance info 170 | ec2InstanceIDs := []string{} 171 | for _, ci := range containerInstances { 172 | ec2InstanceIDs = append(ec2InstanceIDs, conv.S(ci.Ec2InstanceId)) 173 | } 174 | ec2Instances, err := c.awsClient.EC2().RetrieveInstances(ec2InstanceIDs) 175 | if err != nil { 176 | return console.ExitWithErrorString("Failed to retrieve EC2 Instances: %s", err.Error()) 177 | } 178 | 179 | for _, ci := range containerInstances { 180 | console.Info("ECS Container Instance") 181 | 182 | console.DetailWithResource("ID", aws.GetECSContainerInstanceIDFromARN(conv.S(ci.ContainerInstanceArn))) 183 | 184 | if conv.B(ci.AgentConnected) { 185 | console.DetailWithResource("Status", conv.S(ci.Status)) 186 | } else { 187 | console.DetailWithResourceNote("Status", conv.S(ci.Status), "(agent not connected)", true) 188 | } 189 | 190 | console.DetailWithResource("Tasks (running/pending)", fmt.Sprintf("%d/%d", 191 | conv.I64(ci.RunningTasksCount), 192 | conv.I64(ci.PendingTasksCount))) 193 | 194 | var registeredCPU, registeredMemory, remainingCPU, remainingMemory int64 195 | for _, rr := range ci.RegisteredResources { 196 | switch strings.ToLower(conv.S(rr.Name)) { 197 | case "cpu": 198 | registeredCPU = conv.I64(rr.IntegerValue) 199 | case "memory": 200 | registeredMemory = conv.I64(rr.IntegerValue) 201 | } 202 | } 203 | for _, rr := range ci.RemainingResources { 204 | switch strings.ToLower(conv.S(rr.Name)) { 205 | case "cpu": 206 | remainingCPU = conv.I64(rr.IntegerValue) 207 | case "memory": 208 | remainingMemory = conv.I64(rr.IntegerValue) 209 | } 210 | } 211 | 212 | console.DetailWithResource("CPU (remaining/registered)", fmt.Sprintf("%.2f/%.2f", 213 | float64(remainingCPU)/1024.0, float64(registeredCPU)/1024.0)) 214 | console.DetailWithResource("Memory (remaining/registered)", fmt.Sprintf("%dM/%dM,", 215 | remainingMemory, registeredMemory)) 216 | 217 | console.DetailWithResource("EC2 Instance ID", conv.S(ci.Ec2InstanceId)) 218 | for _, ei := range ec2Instances { 219 | if conv.S(ei.InstanceId) == conv.S(ci.Ec2InstanceId) { 220 | if !utils.IsBlank(conv.S(ei.PrivateIpAddress)) { 221 | console.DetailWithResource(" Private IP", conv.S(ei.PrivateIpAddress)) 222 | } 223 | if !utils.IsBlank(conv.S(ei.PublicIpAddress)) { 224 | console.DetailWithResource(" Public IP", conv.S(ei.PublicIpAddress)) 225 | } 226 | break 227 | } 228 | } 229 | } 230 | } 231 | 232 | return nil 233 | } 234 | -------------------------------------------------------------------------------- /commands/clusterstatus/flags.go: -------------------------------------------------------------------------------- 1 | package clusterstatus 2 | 3 | import "gopkg.in/alecthomas/kingpin.v2" 4 | 5 | type Flags struct { 6 | ExcludeContainerInstanceInfos *bool 7 | } 8 | 9 | func NewFlags(kc *kingpin.CmdClause) *Flags { 10 | return &Flags{ 11 | ExcludeContainerInstanceInfos: kc.Flag("exclude-container-instances", "Exclude ECS Container Instance infos").Bool(), 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /commands/command.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/coldbrewcloud/coldbrew-cli/flags" 5 | "gopkg.in/alecthomas/kingpin.v2" 6 | ) 7 | 8 | type Command interface { 9 | Init(app *kingpin.Application, appFlags *flags.GlobalFlags) *kingpin.CmdClause 10 | 11 | // Run should return error only for critical issue. All other errors should be handled inside Run() function. 12 | Run() error 13 | } 14 | -------------------------------------------------------------------------------- /commands/create/command.go: -------------------------------------------------------------------------------- 1 | package create 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "path/filepath" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/coldbrewcloud/coldbrew-cli/aws" 11 | "github.com/coldbrewcloud/coldbrew-cli/config" 12 | "github.com/coldbrewcloud/coldbrew-cli/console" 13 | "github.com/coldbrewcloud/coldbrew-cli/core" 14 | "github.com/coldbrewcloud/coldbrew-cli/flags" 15 | "github.com/coldbrewcloud/coldbrew-cli/utils" 16 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 17 | "gopkg.in/alecthomas/kingpin.v2" 18 | ) 19 | 20 | type Command struct { 21 | globalFlags *flags.GlobalFlags 22 | commandFlags *Flags 23 | awsClient *aws.Client 24 | } 25 | 26 | func (c *Command) Init(ka *kingpin.Application, globalFlags *flags.GlobalFlags) *kingpin.CmdClause { 27 | c.globalFlags = globalFlags 28 | 29 | cmd := ka.Command( 30 | "init", 31 | "See: "+console.ColorFnHelpLink("https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-init")) 32 | c.commandFlags = NewFlags(cmd) 33 | 34 | return cmd 35 | } 36 | 37 | func (c *Command) Run() error { 38 | var err error 39 | 40 | appDirectory, err := c.globalFlags.GetApplicationDirectory() 41 | if err != nil { 42 | return err 43 | } 44 | 45 | // AWS client 46 | c.awsClient = c.globalFlags.GetAWSClient() 47 | 48 | if conv.B(c.commandFlags.Default) { 49 | console.Info("Generating default configuration...") 50 | } 51 | 52 | // default config 53 | defConf := config.DefaultConfig(core.DefaultAppName(appDirectory)) 54 | 55 | conf := &config.Config{} 56 | 57 | // app name 58 | conf.Name = conv.SP(c.askQuestion("Name of your application", "App Name", conv.S(defConf.Name))) 59 | 60 | // cluster name 61 | conf.ClusterName = conv.SP(c.askQuestion("Name of the cluster your application will be deployed", "Cluster Name", conv.S(defConf.ClusterName))) 62 | 63 | // app port 64 | input := c.askQuestion("Does your application expose TCP port? (Enter 0 if not)", "Port", fmt.Sprintf("%d", conv.U16(defConf.Port))) 65 | parsed, err := strconv.ParseUint(input, 10, 16) 66 | if err != nil { 67 | return console.ExitWithErrorString("Invalid port number [%s]", input) 68 | } 69 | conf.Port = conv.U16P(uint16(parsed)) 70 | 71 | // cpu 72 | input = c.askQuestion("CPU allocation per unit (1core = 1.0)", "CPU", fmt.Sprintf("%.2f", conv.F64(defConf.CPU))) 73 | parsedF, err := strconv.ParseFloat(input, 64) 74 | if err != nil { 75 | return console.ExitWithErrorString("Invalid CPU [%s]", input) 76 | } 77 | conf.CPU = conv.F64P(parsedF) 78 | 79 | // Memory 80 | conf.Memory = conv.SP(c.askQuestion("Memory allocation per unit", "Memory", conv.S(defConf.Memory))) 81 | 82 | // Units 83 | input = c.askQuestion("Number of application units", "Units", fmt.Sprintf("%d", conv.U16(defConf.Units))) 84 | parsed, err = strconv.ParseUint(input, 10, 16) 85 | if err != nil { 86 | return console.ExitWithErrorString("Invalid units [%s]", input) 87 | } 88 | conf.Units = conv.U16P(uint16(parsed)) 89 | 90 | // load balancer 91 | if conv.B(c.commandFlags.Default) || console.AskConfirm("Does your application need load balancing?", true) { 92 | conf.LoadBalancer.Enabled = conv.BP(true) 93 | 94 | // port 95 | input := c.askQuestion("Load balancer port number (HTTP)", "Load Balancer Port (HTTP)", fmt.Sprintf("%d", conv.U16(defConf.LoadBalancer.Port))) 96 | parsed, err := strconv.ParseUint(input, 10, 16) 97 | if err != nil || parsed == 0 { 98 | return console.ExitWithErrorString("Invalid port number [%s]", input) 99 | } 100 | conf.LoadBalancer.Port = conv.U16P(uint16(parsed)) 101 | 102 | // https 103 | if !conv.B(c.commandFlags.Default) { 104 | // https port 105 | input := c.askQuestion("Enter HTTPS port number if you want to enable HTTPS traffic.", "Load Balancer HTTPS Port", "0") 106 | parsed, err := strconv.ParseUint(input, 10, 16) 107 | if err != nil || parsed == 0 { 108 | return console.ExitWithErrorString("Invalid port number [%s]", input) 109 | } 110 | conf.LoadBalancer.HTTPSPort = conv.U16P(uint16(parsed)) 111 | } 112 | 113 | // health check 114 | conf.LoadBalancer.HealthCheck.Path = conv.SP(c.askQuestion("Health check destination path", "Health Check Path", conv.S(defConf.LoadBalancer.HealthCheck.Path))) 115 | conf.LoadBalancer.HealthCheck.Status = conv.SP(c.askQuestion("HTTP codes to use when checking for a successful response", "Health Check Status", conv.S(defConf.LoadBalancer.HealthCheck.Status))) 116 | conf.LoadBalancer.HealthCheck.Interval = conv.SP(c.askQuestion("Approximate amount of time between health checks of an individual instance", "Health Check Interval", conv.S(defConf.LoadBalancer.HealthCheck.Interval))) 117 | conf.LoadBalancer.HealthCheck.Timeout = conv.SP(c.askQuestion("Amount of time during which no response from an instance means a failed health check", "Health Check Timeout", conv.S(defConf.LoadBalancer.HealthCheck.Timeout))) 118 | 119 | input = c.askQuestion("Number of consecutive health check successes required before considering an unhealthy instance to healthy.", "Healthy Limits", fmt.Sprintf("%d", conv.U16(defConf.LoadBalancer.HealthCheck.HealthyLimit))) 120 | parsed, err = strconv.ParseUint(input, 10, 16) 121 | if err != nil { 122 | return console.ExitWithErrorString("Invalid number [%s]", input) 123 | } 124 | conf.LoadBalancer.HealthCheck.HealthyLimit = conv.U16P(uint16(parsed)) 125 | 126 | input = c.askQuestion("Number of consecutive health check failures required before considering an instance unhealthy.", "Unhealthy Limits", fmt.Sprintf("%d", conv.U16(defConf.LoadBalancer.HealthCheck.UnhealthyLimit))) 127 | parsed, err = strconv.ParseUint(input, 10, 16) 128 | if err != nil { 129 | return console.ExitWithErrorString("Invalid number [%s]", input) 130 | } 131 | conf.LoadBalancer.HealthCheck.UnhealthyLimit = conv.U16P(uint16(parsed)) 132 | } 133 | 134 | // AWS 135 | { 136 | // elb lb name 137 | conf.AWS.ELBLoadBalancerName = conv.SP(c.askQuestion("ELB load balancer name", "ELB Load Balancer Name", 138 | core.DefaultELBLoadBalancerName(conv.S(conf.Name)))) 139 | 140 | // elb target name 141 | conf.AWS.ELBTargetGroupName = conv.SP(c.askQuestion("ELB target name", "ELB Target Group Name", 142 | core.DefaultELBTargetGroupName(conv.S(conf.Name)))) 143 | 144 | // elb security group 145 | conf.AWS.ELBSecurityGroupName = conv.SP(c.askQuestion("Security group ID/name for ELB Load Balancer. Leave it blank to create default one.", "ELB Security Group", 146 | core.DefaultELBLoadBalancerSecurityGroupName(conv.S(conf.Name)))) 147 | 148 | // elb certificate ARN 149 | if conv.U16(conf.LoadBalancer.HTTPSPort) > 0 { 150 | conf.AWS.ELBCertificateARN = conv.SP(c.askQuestion("HTTPS Certificate ARN for ELB Load Balancer", "ELB Certificate ARN", "")) 151 | } 152 | 153 | // ecr repo name 154 | conf.AWS.ECRRepositoryName = conv.SP(c.askQuestion("ECR repository name", "ECR Namespace", 155 | core.DefaultECRRepository(conv.S(conf.Name)))) 156 | } 157 | 158 | // Docker 159 | { 160 | conf.Docker.Bin = conv.SP(c.askQuestion("Docker executable path", "Docker Bin", conv.S(defConf.Docker.Bin))) 161 | } 162 | 163 | console.Blank() 164 | 165 | // validate 166 | console.Info("Validating configuration...") 167 | if err := conf.Validate(); err != nil { 168 | return console.ExitWithError(core.NewErrorExtraInfo(err, "https://github.com/coldbrewcloud/coldbrew-cli/wiki/Configuration-File")) 169 | } 170 | 171 | // config file path and format 172 | configFile, err := c.globalFlags.GetConfigFile() 173 | if err != nil { 174 | return err 175 | } 176 | configFileFormat := strings.ToLower(conv.S(c.globalFlags.ConfigFileFormat)) 177 | if utils.IsBlank(configFileFormat) { 178 | switch strings.ToLower(filepath.Ext(configFile)) { 179 | case ".json": 180 | configFileFormat = flags.GlobalFlagsConfigFileFormatJSON 181 | default: 182 | configFileFormat = flags.GlobalFlagsConfigFileFormatYAML 183 | } 184 | } 185 | 186 | // write to file 187 | var configData []byte 188 | switch configFileFormat { 189 | case flags.GlobalFlagsConfigFileFormatYAML: 190 | configData, err = conf.ToYAML() 191 | if err != nil { 192 | return console.ExitWithErrorString("Failed to format configuration in YAML: %s", err.Error()) 193 | } 194 | case flags.GlobalFlagsConfigFileFormatJSON: 195 | configData, err = conf.ToJSONWithIndent() 196 | if err != nil { 197 | return console.ExitWithErrorString("Failed to format configuration in JSON: %s", err.Error()) 198 | } 199 | default: 200 | return console.ExitWithErrorString("Unsupported configuration file format [%s]", configFileFormat) 201 | } 202 | if err := ioutil.WriteFile(configFile, configData, 0644); err != nil { 203 | return console.ExitWithErrorString("Failed to write configuration file [%s]: %s", configFile, err.Error()) 204 | } 205 | 206 | console.Blank() 207 | console.Info(fmt.Sprintf("Configuration file: %s", configFile)) 208 | 209 | return nil 210 | } 211 | 212 | func (c *Command) askQuestion(description, question, defaultValue string) string { 213 | if conv.B(c.commandFlags.Default) { 214 | console.DetailWithResource(question, defaultValue) 215 | return defaultValue 216 | } 217 | 218 | return console.AskQuestionWithNote(question, defaultValue, description) 219 | } 220 | -------------------------------------------------------------------------------- /commands/create/flags.go: -------------------------------------------------------------------------------- 1 | package create 2 | 3 | import "gopkg.in/alecthomas/kingpin.v2" 4 | 5 | type Flags struct { 6 | Default *bool 7 | } 8 | 9 | func NewFlags(kc *kingpin.CmdClause) *Flags { 10 | return &Flags{ 11 | Default: kc.Flag("default", "Generate default configuration").Bool(), 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /commands/delete/flags.go: -------------------------------------------------------------------------------- 1 | package delete 2 | 3 | import "gopkg.in/alecthomas/kingpin.v2" 4 | 5 | type Flags struct { 6 | AppName *string 7 | ClusterName *string 8 | NoConfirm *bool 9 | ContinueOnError *bool 10 | } 11 | 12 | func NewFlags(kc *kingpin.CmdClause) *Flags { 13 | return &Flags{ 14 | AppName: kc.Flag("app-name", "App name").Default("").String(), 15 | ClusterName: kc.Flag("cluster-name", "App name").Default("").String(), 16 | NoConfirm: kc.Flag("yes", "Delete all resources with no confirmation").Short('y').Default("false").Bool(), 17 | ContinueOnError: kc.Flag("continue", "Continue deleting resources on error").Default("false").Bool(), 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /commands/deploy/aws_ecs.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | 8 | "github.com/coldbrewcloud/coldbrew-cli/aws" 9 | "github.com/coldbrewcloud/coldbrew-cli/aws/ecs" 10 | "github.com/coldbrewcloud/coldbrew-cli/console" 11 | "github.com/coldbrewcloud/coldbrew-cli/core" 12 | "github.com/coldbrewcloud/coldbrew-cli/utils" 13 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 14 | ) 15 | 16 | func (c *Command) updateECSTaskDefinition(dockerImageFullURI string) (string, error) { 17 | // port mappings 18 | var portMappings []ecs.PortMapping 19 | if conv.U16(c.conf.Port) > 0 { 20 | portMappings = []ecs.PortMapping{ 21 | { 22 | ContainerPort: conv.U16(c.conf.Port), 23 | Protocol: "tcp", 24 | }, 25 | } 26 | } 27 | 28 | ecsTaskDefinitionName := core.DefaultECSTaskDefinitionName(conv.S(c.conf.Name)) 29 | ecsTaskContainerName := core.DefaultECSTaskMainContainerName(conv.S(c.conf.Name)) 30 | cpu := uint64(math.Ceil(conv.F64(c.conf.CPU) * 1024.0)) 31 | memory, err := core.ParseSizeExpression(conv.S(c.conf.Memory)) 32 | if err != nil { 33 | return "", err 34 | } 35 | memory /= 1000 * 1000 36 | 37 | // logging 38 | loggingDriver := conv.S(c.conf.Logging.Driver) 39 | if c.conf.Logging.Options == nil { 40 | c.conf.Logging.Options = make(map[string]string) 41 | } 42 | switch loggingDriver { 43 | case aws.ECSTaskDefinitionLogDriverAWSLogs: 44 | // test if group needs to be created 45 | awsLogsGroupName, ok := c.conf.Logging.Options["awslogs-group"] 46 | if !ok || utils.IsBlank(awsLogsGroupName) { 47 | awsLogsGroupName = core.DefaultCloudWatchLogsGroupName(conv.S(c.conf.Name), conv.S(c.conf.ClusterName)) 48 | c.conf.Logging.Options["awslogs-group"] = awsLogsGroupName 49 | } 50 | if err := c.PrepareCloudWatchLogsGroup(awsLogsGroupName); err != nil { 51 | return "", err 52 | } 53 | 54 | // assign region if not provided 55 | awsLogsRegionName, ok := c.conf.Logging.Options["awslogs-region"] 56 | if !ok || utils.IsBlank(awsLogsRegionName) { 57 | c.conf.Logging.Options["awslogs-region"] = conv.S(c.globalFlags.AWSRegion) 58 | } 59 | } 60 | 61 | console.UpdatingResource("Updating ECS Task Definition", ecsTaskDefinitionName, false) 62 | ecsTaskDef, err := c.awsClient.ECS().UpdateTaskDefinition( 63 | ecsTaskDefinitionName, 64 | dockerImageFullURI, 65 | ecsTaskContainerName, 66 | cpu, 67 | memory, 68 | c.conf.Env, 69 | portMappings, 70 | loggingDriver, c.conf.Logging.Options) 71 | if err != nil { 72 | return "", fmt.Errorf("Failed to update ECS Task Definition [%s]: %s", ecsTaskDefinitionName, err.Error()) 73 | } 74 | 75 | return conv.S(ecsTaskDef.TaskDefinitionArn), nil 76 | } 77 | 78 | func (c *Command) createOrUpdateECSService(ecsTaskDefinitionARN string) error { 79 | ecsClusterName := core.DefaultECSClusterName(conv.S(c.conf.ClusterName)) 80 | ecsServiceName := core.DefaultECSServiceName(conv.S(c.conf.Name)) 81 | 82 | ecsService, err := c.awsClient.ECS().RetrieveService(ecsClusterName, ecsServiceName) 83 | if err != nil { 84 | return fmt.Errorf("Failed to retrieve ECS Service [%s/%s]: %s", ecsClusterName, ecsServiceName, err.Error()) 85 | } 86 | 87 | if ecsService != nil && conv.S(ecsService.Status) == "ACTIVE" { 88 | elbLoadBalancerName := "" 89 | elbTargetGroupARN := "" 90 | if ecsService.LoadBalancers != nil && len(ecsService.LoadBalancers) > 0 { 91 | elbLoadBalancerName = conv.S(ecsService.LoadBalancers[0].LoadBalancerName) 92 | elbTargetGroupARN = conv.S(ecsService.LoadBalancers[0].TargetGroupArn) 93 | 94 | // check if task container port has changed or not 95 | if conv.I64(ecsService.LoadBalancers[0].ContainerPort) != int64(conv.U16(c.conf.Port)) { 96 | return core.NewErrorExtraInfo( 97 | errors.New("App port cannot be changed."), 98 | "https://github.com/coldbrewcloud/coldbrew-cli/wiki/Configuration-Changes-and-Their-Effects#app-level-changes") 99 | } 100 | } 101 | 102 | if err := c.updateECSService(ecsClusterName, ecsServiceName, ecsTaskDefinitionARN, elbLoadBalancerName, elbTargetGroupARN); err != nil { 103 | return err 104 | } 105 | } else { 106 | if err := c.createECSService(ecsClusterName, ecsServiceName, ecsTaskDefinitionARN); err != nil { 107 | return err 108 | } 109 | } 110 | 111 | return nil 112 | } 113 | 114 | func (c *Command) createECSService(ecsClusterName, ecsServiceName, ecsTaskDefinitionARN string) error { 115 | ecsServiceRoleName := core.DefaultECSServiceRoleName(conv.S(c.conf.ClusterName)) 116 | ecsTaskContainerName := conv.S(c.conf.Name) 117 | ecsTaskContainerPort := conv.U16(c.conf.Port) 118 | 119 | var loadBalancers []*ecs.LoadBalancer 120 | if conv.B(c.conf.LoadBalancer.Enabled) { 121 | if conv.U16(c.conf.Port) == 0 { 122 | return errors.New("App port must be specified to enable load balancer.") 123 | } 124 | 125 | loadBalancer, err := c.prepareELBLoadBalancer( 126 | ecsServiceRoleName, 127 | ecsTaskContainerName, 128 | ecsTaskContainerPort) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | loadBalancers = []*ecs.LoadBalancer{loadBalancer} 134 | } 135 | 136 | console.AddingResource("Creating ECS Service", ecsServiceName, false) 137 | _, err := c.awsClient.ECS().CreateService( 138 | ecsClusterName, ecsServiceName, ecsTaskDefinitionARN, conv.U16(c.conf.Units), 139 | loadBalancers, ecsServiceRoleName) 140 | if err != nil { 141 | return fmt.Errorf("Failed to create ECS Service [%s]: %s", ecsServiceName, err.Error()) 142 | } 143 | 144 | return nil 145 | } 146 | 147 | func (c *Command) updateECSService(ecsClusterName, ecsServiceName, ecsTaskDefinitionARN, elbLoadBalancerName, elbTargetGroupARN string) error { 148 | // check if ELB Target Group health check needs to be updated 149 | if elbTargetGroupARN != "" { 150 | if err := c.checkLoadBalancerHealthCheckChanges(elbTargetGroupARN); err != nil { 151 | return err 152 | } 153 | } 154 | 155 | // update ECS service 156 | console.UpdatingResource("Updating ECS Service", ecsServiceName, false) 157 | _, err := c.awsClient.ECS().UpdateService(ecsClusterName, ecsServiceName, ecsTaskDefinitionARN, conv.U16(c.conf.Units)) 158 | if err != nil { 159 | return fmt.Errorf("Failed to update ECS Service [%s]: %s", ecsServiceName, err.Error()) 160 | } 161 | 162 | return nil 163 | } 164 | 165 | func (c *Command) PrepareCloudWatchLogsGroup(groupName string) error { 166 | groups, err := c.awsClient.CloudWatchLogs().ListGroups(groupName) 167 | if err != nil { 168 | return fmt.Errorf("Failed to list CloudWatch Logs Group [%s]: %s", groupName, err.Error()) 169 | } 170 | 171 | for _, group := range groups { 172 | if conv.S(group.LogGroupName) == groupName { 173 | // log group exists; return with no error 174 | return nil 175 | } 176 | } 177 | 178 | // log group does not exist; create a new group 179 | console.AddingResource("Creating CloudWatch Logs Group", groupName, false) 180 | if err := c.awsClient.CloudWatchLogs().CreateGroup(groupName); err != nil { 181 | return fmt.Errorf("Failed to create CloudWatch Logs Group [%s]: %s", groupName, err.Error()) 182 | } 183 | 184 | return nil 185 | } 186 | -------------------------------------------------------------------------------- /commands/deploy/command.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | 7 | "github.com/coldbrewcloud/coldbrew-cli/aws" 8 | "github.com/coldbrewcloud/coldbrew-cli/config" 9 | "github.com/coldbrewcloud/coldbrew-cli/console" 10 | "github.com/coldbrewcloud/coldbrew-cli/core" 11 | "github.com/coldbrewcloud/coldbrew-cli/docker" 12 | "github.com/coldbrewcloud/coldbrew-cli/flags" 13 | "github.com/coldbrewcloud/coldbrew-cli/utils" 14 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 15 | "gopkg.in/alecthomas/kingpin.v2" 16 | ) 17 | 18 | type Command struct { 19 | kingpinApp *kingpin.Application 20 | globalFlags *flags.GlobalFlags 21 | _commandFlags *Flags // NOTE: this name intentionally starts with underscore because main configuration (conf) should be used throughout Run() after merging them 22 | awsClient *aws.Client 23 | dockerClient *docker.Client 24 | conf *config.Config 25 | } 26 | 27 | func (c *Command) Init(ka *kingpin.Application, globalFlags *flags.GlobalFlags) *kingpin.CmdClause { 28 | c.kingpinApp = ka 29 | c.globalFlags = globalFlags 30 | 31 | cmd := ka.Command("deploy", 32 | "See: "+console.ColorFnHelpLink("https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-deploy")) 33 | c._commandFlags = NewFlags(cmd) 34 | 35 | return cmd 36 | } 37 | 38 | func (c *Command) Run() error { 39 | var err error 40 | 41 | // app configuration 42 | configFilePath, err := c.globalFlags.GetConfigFile() 43 | if err != nil { 44 | return console.ExitWithError(err) 45 | } 46 | configData, err := ioutil.ReadFile(configFilePath) 47 | if err != nil { 48 | return console.ExitWithErrorString("Failed to read configuration file [%s]: %s", configFilePath, err.Error()) 49 | } 50 | c.conf, err = config.Load(configData, conv.S(c.globalFlags.ConfigFileFormat), core.DefaultAppName(configFilePath)) 51 | if err != nil { 52 | return console.ExitWithError(err) 53 | } 54 | 55 | // CLI flags validation 56 | if err := c.validateFlags(c._commandFlags); err != nil { 57 | return console.ExitWithError(core.NewErrorExtraInfo(err, "https://github.com/coldbrewcloud/coldbrew-cli/wiki/Command:-deploy")) 58 | } 59 | 60 | // merge flags into main configuration 61 | c.conf = c.mergeFlagsIntoConfiguration(c.conf, c._commandFlags) 62 | 63 | // AWS client 64 | c.awsClient = c.globalFlags.GetAWSClient() 65 | 66 | // test if target cluster is available to use 67 | console.ProcessingOnResource("Checking cluster availability", conv.S(c.conf.ClusterName), false) 68 | if err := c.isClusterAvailable(conv.S(c.conf.ClusterName)); err != nil { 69 | return console.ExitWithError(core.NewErrorExtraInfo(err, "https://github.com/coldbrewcloud/coldbrew-cli/wiki/Error:-Cluster-not-found")) 70 | } 71 | 72 | // docker client 73 | c.dockerClient = docker.NewClient(conv.S(c.conf.Docker.Bin)) 74 | if !c.dockerClient.DockerBinAvailable() { 75 | return console.ExitWithError(core.NewErrorExtraInfo( 76 | fmt.Errorf("Failed to find Docker binary [%s].", c.conf.Docker.Bin), 77 | "https://github.com/coldbrewcloud/coldbrew-cli/wiki/Error:-Docker-binary-not-found")) 78 | } 79 | 80 | // prepare ECR repo (create one if needed) 81 | ecrRepoURI, err := c.prepareECRRepo(conv.S(c.conf.AWS.ECRRepositoryName)) 82 | if err != nil { 83 | return console.ExitWithError(err) 84 | } 85 | 86 | // prepare docker image (build one if needed) 87 | dockerImage := conv.S(c._commandFlags.DockerImage) 88 | if utils.IsBlank(dockerImage) { // build local docker image 89 | dockerImage = fmt.Sprintf("%s:latest", ecrRepoURI) 90 | console.ProcessingOnResource("Building Docker image", dockerImage, true) 91 | if err := c.buildDockerImage(dockerImage); err != nil { 92 | return console.ExitWithError(err) 93 | } 94 | } else { // use local docker image 95 | // if needed, re-tag local image so it can be pushed to target ECR repo 96 | m := core.DockerImageURIRE.FindAllStringSubmatch(dockerImage, -1) 97 | if len(m) != 1 { 98 | return console.ExitWithErrorString("Invalid Docker image [%s]", dockerImage) 99 | } 100 | if m[0][1] != ecrRepoURI { 101 | tag := m[0][2] 102 | if tag == "" { 103 | tag = "latest" 104 | } 105 | newImage := fmt.Sprintf("%s:%s", ecrRepoURI, tag) 106 | 107 | console.AddingResource("Tagging Docker image", fmt.Sprintf("%s -> %s", dockerImage, newImage), false) 108 | if err := c.dockerClient.TagImage(dockerImage, newImage); err != nil { 109 | return console.ExitWithError(err) 110 | } 111 | 112 | dockerImage = newImage 113 | } 114 | } 115 | 116 | // push docker image to ECR 117 | if err := c.pushDockerImage(dockerImage); err != nil { 118 | return console.ExitWithError(err) 119 | } 120 | 121 | // create/update ECS task definition 122 | ecsTaskDefinitionARN, err := c.updateECSTaskDefinition(dockerImage) 123 | if err != nil { 124 | return console.ExitWithError(err) 125 | } 126 | 127 | // create/update ECS service 128 | if err := c.createOrUpdateECSService(ecsTaskDefinitionARN); err != nil { 129 | return console.ExitWithError(err) 130 | } 131 | 132 | console.Blank() 133 | console.Info("Application deployment completed.") 134 | 135 | return nil 136 | } 137 | 138 | func (c *Command) isClusterAvailable(clusterName string) error { 139 | // check ECS cluster 140 | ecsClusterName := core.DefaultECSClusterName(clusterName) 141 | ecsCluster, err := c.awsClient.ECS().RetrieveCluster(ecsClusterName) 142 | if err != nil { 143 | return fmt.Errorf("Failed to retrieve ECS Cluster [%s]: %s", ecsClusterName, err.Error()) 144 | } 145 | if ecsCluster == nil || conv.S(ecsCluster.Status) == "INACTIVE" { 146 | return fmt.Errorf("ECS Cluster [%s] not found", ecsClusterName) 147 | } 148 | 149 | // check ECS service role 150 | ecsServiceRoleName := core.DefaultECSServiceRoleName(clusterName) 151 | ecsServiceRole, err := c.awsClient.IAM().RetrieveRole(ecsServiceRoleName) 152 | if err != nil { 153 | return fmt.Errorf("Failed to retrieve IAM Role [%s]: %s", ecsServiceRoleName, err.Error()) 154 | } 155 | if ecsServiceRole == nil { 156 | return fmt.Errorf("IAM Role [%s] not found", ecsServiceRoleName) 157 | } 158 | 159 | return nil 160 | } 161 | 162 | func (c *Command) prepareECRRepo(repoName string) (string, error) { 163 | ecrRepo, err := c.awsClient.ECR().RetrieveRepository(repoName) 164 | if err != nil { 165 | return "", fmt.Errorf("Failed to retrieve ECR repository [%s]: %s", repoName, err.Error()) 166 | } 167 | 168 | if ecrRepo == nil { 169 | console.AddingResource("Creating ECR Repository", repoName, false) 170 | ecrRepo, err = c.awsClient.ECR().CreateRepository(repoName) 171 | if err != nil { 172 | return "", fmt.Errorf("Failed to create ECR repository [%s]: %s", repoName, err.Error()) 173 | } 174 | } 175 | 176 | return *ecrRepo.RepositoryUri, nil 177 | } 178 | -------------------------------------------------------------------------------- /commands/deploy/docker.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | "github.com/coldbrewcloud/coldbrew-cli/console" 8 | "github.com/coldbrewcloud/coldbrew-cli/utils" 9 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 10 | ) 11 | 12 | func (c *Command) buildDockerImage(image string) error { 13 | buildPath, err := c.globalFlags.GetApplicationDirectory() 14 | if err != nil { 15 | return err 16 | } 17 | 18 | dockerfilePath := conv.S(c._commandFlags.DockerfilePath) 19 | if utils.IsBlank(dockerfilePath) { 20 | dockerfilePath = "Dockerfile" 21 | } 22 | 23 | if !filepath.IsAbs(dockerfilePath) { 24 | var err error 25 | dockerfilePath, err = filepath.Abs(dockerfilePath) 26 | if err != nil { 27 | return fmt.Errorf("Error retrieving absolute path [%s]: %s", dockerfilePath, err.Error()) 28 | } 29 | } 30 | 31 | // docker build 32 | if err = c.dockerClient.BuildImage(buildPath, dockerfilePath, image); err != nil { 33 | return err 34 | } 35 | 36 | return nil 37 | } 38 | 39 | func (c *Command) pushDockerImage(image string) error { 40 | console.Info("Authenticating to push to ECR Repository...") 41 | 42 | // docker login 43 | userName, password, proxyURL, err := c.awsClient.ECR().GetDockerLogin() 44 | if err != nil { 45 | return fmt.Errorf("Failed to retrieve docker login info: %s", err.Error()) 46 | } 47 | if err := c.dockerClient.Login(userName, password, proxyURL); err != nil { 48 | return fmt.Errorf("Docker login [%s] failed: %s", userName, err.Error()) 49 | } 50 | 51 | // docker push 52 | console.ProcessingOnResource("Pushing Docker image", image, true) 53 | if err = c.dockerClient.PushImage(image); err != nil { 54 | return fmt.Errorf("Failed to push Docker image [%s]: %s", image, err.Error()) 55 | } 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /commands/deploy/flags.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/coldbrewcloud/coldbrew-cli/config" 10 | "github.com/coldbrewcloud/coldbrew-cli/core" 11 | "github.com/coldbrewcloud/coldbrew-cli/utils" 12 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 13 | "gopkg.in/alecthomas/kingpin.v2" 14 | ) 15 | 16 | type Flags struct { 17 | DockerImage *string `json:"docker-image,omitempty"` 18 | DockerfilePath *string `json:"dockerfile,omitempty"` 19 | Units *int64 `json:"units,omitempty"` 20 | CPU *float64 `json:"cpu,omitempty"` 21 | Memory *string `json:"memory,omitempty"` 22 | Envs *map[string]string `json:"env,omitempty"` 23 | } 24 | 25 | func NewFlags(kc *kingpin.CmdClause) *Flags { 26 | return &Flags{ 27 | DockerfilePath: kc.Flag("dockerfile", "Dockerfile path").Default("").String(), 28 | DockerImage: kc.Flag("docker-image", "Docker image (should include image tag)").String(), 29 | Units: kc.Flag("units", "Desired count").Default("-1").Int64(), 30 | CPU: kc.Flag("cpu", "Docker CPU resource (1 unit: 1024)").Default("-1").Float64(), 31 | Memory: kc.Flag("memory", "Docker memory resource").Default("").String(), 32 | Envs: kc.Flag("env", "Environment variable (\"key=value\")").Short('E').StringMap(), 33 | } 34 | } 35 | 36 | func (c *Command) mergeFlagsIntoConfiguration(conf *config.Config, flags *Flags) *config.Config { 37 | if conv.I64(flags.Units) >= 0 { 38 | conf.Units = conv.U16P(uint16(conv.I64(flags.Units))) 39 | } 40 | 41 | if conv.F64(flags.CPU) >= 0 { 42 | conf.CPU = conv.F64P(conv.F64(flags.CPU)) 43 | } 44 | 45 | if !utils.IsBlank(conv.S(flags.Memory)) { 46 | conf.Memory = conv.SP(conv.S(flags.Memory)) 47 | } 48 | 49 | // envs 50 | for ek, ev := range *flags.Envs { 51 | conf.Env[ek] = ev 52 | } 53 | 54 | return conf 55 | } 56 | 57 | func (c *Command) validateFlags(flags *Flags) error { 58 | if !utils.IsBlank(conv.S(flags.DockerImage)) && !core.DockerImageURIRE.MatchString(conv.S(flags.DockerImage)) { 59 | return fmt.Errorf("Invalid Docker image [%s]", conv.S(flags.DockerImage)) 60 | } 61 | 62 | if !utils.IsBlank(conv.S(flags.DockerfilePath)) { 63 | if err := c.validatePath(conv.S(flags.DockerfilePath)); err != nil { 64 | return fmt.Errorf("Invalid Dockerfile path [%s]", conv.S(flags.DockerfilePath)) 65 | } 66 | } 67 | 68 | if conv.I64(flags.Units) >= 0 && uint16(conv.I64(flags.Units)) > core.MaxAppUnits { 69 | return fmt.Errorf("Units [%d] cannot exceed %d", conv.I64(flags.Units), core.MaxAppUnits) 70 | } 71 | 72 | if conv.F64(flags.CPU) > core.MaxAppCPU { 73 | return fmt.Errorf("CPU [%.2f] cannot exceed %d", conv.F64(flags.CPU), core.MaxAppCPU) 74 | } 75 | 76 | if !utils.IsBlank(conv.S(flags.Memory)) && !core.SizeExpressionRE.MatchString(conv.S(flags.Memory)) { 77 | return fmt.Errorf("Invalid app memory [%s]", conv.S(flags.Memory)) 78 | } 79 | 80 | return nil 81 | } 82 | 83 | func (c *Command) validatePath(path string) error { 84 | if utils.IsBlank(path) { 85 | return fmt.Errorf("Path [%s] is blank", path) 86 | } 87 | 88 | absPath, err := filepath.Abs(path) 89 | if err != nil { 90 | return fmt.Errorf("Failed to determine absolute path [%s]", path) 91 | } 92 | 93 | if _, err := os.Stat(absPath); os.IsNotExist(err) { 94 | return errors.New("Path [%s] does not exist") 95 | } 96 | 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /commands/deploy/flags_test.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | /* 4 | func TestNewDeployFlags(t *testing.T) { 5 | app := kingpin.New("app", "") 6 | app.Writer(&nullWriter{}) 7 | app.Terminate(nil) 8 | deployFlags := NewDeployFlags(app.Command("deploy", "")) 9 | 10 | // command 11 | cmd, err := app.Parse([]string{}) // no command 12 | assert.NotNil(t, err) 13 | assert.Empty(t, cmd) 14 | cmd, err = app.Parse([]string{"deploy"}) 15 | assert.Nil(t, err) 16 | assert.Equal(t, "deploy", cmd) 17 | 18 | testStringFlag(t, app, &deployFlags.AppName, testSptr("deploy"), "app-name", nil, testSptr("app1"), nil) 19 | testStringFlag(t, app, &deployFlags.AppVersion, testSptr("deploy"), "app-version", nil, testSptr("1.0.0"), nil) 20 | testStringFlag(t, app, &deployFlags.AppPath, testSptr("deploy"), "app-path", nil, testSptr("."), nil) 21 | testUint16Flag(t, app, &deployFlags.ContainerPort, testSptr("deploy"), "container-port", nil, testU16ptr(0), nil) 22 | testStringFlag(t, app, &deployFlags.LoadBalancerName, testSptr("deploy"), "load-balancer", nil, nil, nil) 23 | testStringFlag(t, app, &deployFlags.LoadBalancerTargetGroupName, testSptr("deploy"), "load-balancer-target-group", nil, nil, nil) 24 | testStringFlag(t, app, &deployFlags.DockerBinPath, testSptr("deploy"), "docker-bin", nil, testSptr("docker"), nil) 25 | testStringFlag(t, app, &deployFlags.DockerfilePath, testSptr("deploy"), "dockerfile", nil, testSptr("./Dockerfile"), nil) 26 | testStringFlag(t, app, &deployFlags.DockerImage, testSptr("deploy"), "docker-image", nil, nil, nil) 27 | testUint16Flag(t, app, &deployFlags.Units, testSptr("deploy"), "units", nil, testU16ptr(1), nil) 28 | testUint64Flag(t, app, &deployFlags.CPU, testSptr("deploy"), "cpu", nil, testU64ptr(128), nil) 29 | testUint64Flag(t, app, &deployFlags.Memory, testSptr("deploy"), "memory", nil, testU64ptr(128), nil) 30 | testStringFlag(t, app, &deployFlags.EnvsFile, testSptr("deploy"), "env-file", nil, nil, nil) 31 | testStringFlag(t, app, &deployFlags.ECSClusterName, testSptr("deploy"), "cluster-name", nil, testSptr("coldbrew"), nil) 32 | testStringFlag(t, app, &deployFlags.ECSServiceRoleName, testSptr("deploy"), "service-role-name", nil, testSptr("ecsServiceRole"), nil) 33 | testStringFlag(t, app, &deployFlags.ECRNamespace, testSptr("deploy"), "ecr-namespace", nil, testSptr("coldbrew"), nil) 34 | testStringFlag(t, app, &deployFlags.VPCID, testSptr("deploy"), "vpc", nil, nil, nil) 35 | testBoolFlag(t, app, &deployFlags.CloudWatchLogs, testSptr("deploy"), "cloud-watch-logs", nil, nil) 36 | 37 | // envs 38 | _, err = app.Parse([]string{"deploy"}) // default 39 | assert.Nil(t, err) 40 | assert.NotNil(t, deployFlags.Envs) 41 | assert.Empty(t, *deployFlags.Envs) 42 | *deployFlags.Envs = make(map[string]string) 43 | _, err = app.Parse([]string{"deploy"}) // default 44 | assert.Nil(t, err) 45 | assert.NotNil(t, deployFlags.Envs) 46 | assert.Empty(t, *deployFlags.Envs) 47 | *deployFlags.Envs = make(map[string]string) 48 | _, err = app.Parse([]string{"deploy", "--env", "key1=value1"}) // 1 pair 49 | assert.Nil(t, err) 50 | assert.NotNil(t, deployFlags.Envs) 51 | assert.Len(t, *deployFlags.Envs, 1) 52 | assert.Equal(t, "value1", (*deployFlags.Envs)["key1"]) 53 | *deployFlags.Envs = make(map[string]string) 54 | _, err = app.Parse([]string{"deploy", "--env", "key1=value1", "--env", "key2=value2"}) // 2 pairs 55 | assert.Nil(t, err) 56 | assert.NotNil(t, deployFlags.Envs) 57 | assert.Len(t, *deployFlags.Envs, 2) 58 | assert.Equal(t, "value1", (*deployFlags.Envs)["key1"]) 59 | assert.Equal(t, "value2", (*deployFlags.Envs)["key2"]) 60 | *deployFlags.Envs = make(map[string]string) 61 | _, err = app.Parse([]string{"deploy", "-E", "key1=value1"}) // 1 pair (short) 62 | assert.Nil(t, err) 63 | assert.NotNil(t, deployFlags.Envs) 64 | assert.Len(t, *deployFlags.Envs, 1) 65 | assert.Equal(t, "value1", (*deployFlags.Envs)["key1"]) 66 | *deployFlags.Envs = make(map[string]string) 67 | _, err = app.Parse([]string{"deploy", "-E", "key1=value1", "-E", "key2=value2"}) // 2 pairs (short) 68 | assert.Nil(t, err) 69 | assert.NotNil(t, deployFlags.Envs) 70 | assert.Len(t, *deployFlags.Envs, 2) 71 | assert.Equal(t, "value1", (*deployFlags.Envs)["key1"]) 72 | assert.Equal(t, "value2", (*deployFlags.Envs)["key2"]) 73 | *deployFlags.Envs = make(map[string]string) 74 | _, err = app.Parse([]string{"deploy", "-E", "key1=value1", "-E", "key2=value2", "--env", "key3=value3"}) // mixed 75 | assert.Nil(t, err) 76 | assert.NotNil(t, deployFlags.Envs) 77 | assert.Len(t, *deployFlags.Envs, 3) 78 | assert.Equal(t, "value1", (*deployFlags.Envs)["key1"]) 79 | assert.Equal(t, "value2", (*deployFlags.Envs)["key2"]) 80 | assert.Equal(t, "value3", (*deployFlags.Envs)["key3"]) 81 | } 82 | */ 83 | -------------------------------------------------------------------------------- /commands/status/command.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "strings" 7 | 8 | "github.com/coldbrewcloud/coldbrew-cli/aws" 9 | "github.com/coldbrewcloud/coldbrew-cli/config" 10 | "github.com/coldbrewcloud/coldbrew-cli/console" 11 | "github.com/coldbrewcloud/coldbrew-cli/core" 12 | "github.com/coldbrewcloud/coldbrew-cli/flags" 13 | "github.com/coldbrewcloud/coldbrew-cli/utils" 14 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 15 | "gopkg.in/alecthomas/kingpin.v2" 16 | ) 17 | 18 | type Command struct { 19 | globalFlags *flags.GlobalFlags 20 | commandFlags *Flags 21 | awsClient *aws.Client 22 | } 23 | 24 | func (c *Command) Init(ka *kingpin.Application, globalFlags *flags.GlobalFlags) *kingpin.CmdClause { 25 | c.globalFlags = globalFlags 26 | 27 | cmd := ka.Command("status", 28 | "See: "+console.ColorFnHelpLink("https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Command:-status")) 29 | c.commandFlags = NewFlags(cmd) 30 | 31 | return cmd 32 | } 33 | 34 | func (c *Command) Run() error { 35 | c.awsClient = c.globalFlags.GetAWSClient() 36 | 37 | appName := "" 38 | clusterName := "" 39 | 40 | // app configuration 41 | configFilePath, err := c.globalFlags.GetConfigFile() 42 | if err != nil { 43 | return console.ExitWithError(err) 44 | } 45 | if utils.FileExists(configFilePath) { 46 | configData, err := ioutil.ReadFile(configFilePath) 47 | if err != nil { 48 | return console.ExitWithErrorString("Failed to read configuration file [%s]: %s", configFilePath, err.Error()) 49 | } 50 | conf, err := config.Load(configData, conv.S(c.globalFlags.ConfigFileFormat), core.DefaultAppName(configFilePath)) 51 | if err != nil { 52 | return console.ExitWithError(err) 53 | } 54 | 55 | appName = conv.S(conf.Name) 56 | clusterName = conv.S(conf.ClusterName) 57 | } 58 | 59 | // app/cluster name from CLI will override configuration file 60 | if !utils.IsBlank(conv.S(c.commandFlags.AppName)) { 61 | appName = conv.S(c.commandFlags.AppName) 62 | } 63 | if !utils.IsBlank(conv.S(c.commandFlags.ClusterName)) { 64 | clusterName = conv.S(c.commandFlags.ClusterName) 65 | } 66 | 67 | if utils.IsBlank(appName) { 68 | return console.ExitWithErrorString("App name is required.") 69 | } 70 | if utils.IsBlank(clusterName) { 71 | return console.ExitWithErrorString("Cluster name is required.") 72 | } 73 | 74 | console.Info("Application") 75 | console.DetailWithResource("Name", appName) 76 | console.DetailWithResource("Cluster", clusterName) 77 | 78 | // AWS networking 79 | regionName, vpcID, err := c.globalFlags.GetAWSRegionAndVPCID() 80 | if err != nil { 81 | return console.ExitWithError(err) 82 | } 83 | subnetIDs, err := c.awsClient.EC2().ListVPCSubnets(vpcID) 84 | if err != nil { 85 | return console.ExitWithErrorString("Failed to list subnets for VPC [%s]: %s", vpcID, err.Error()) 86 | } 87 | 88 | // AWS env 89 | console.Info("AWS") 90 | console.DetailWithResource("Region", regionName) 91 | console.DetailWithResource("VPC", vpcID) 92 | console.DetailWithResource("Subnets", strings.Join(subnetIDs, " ")) 93 | 94 | // ECS 95 | console.Info("ECS") 96 | 97 | // ECS cluster 98 | ecsClusterName := core.DefaultECSClusterName(clusterName) 99 | ecsCluster, err := c.awsClient.ECS().RetrieveCluster(ecsClusterName) 100 | if err != nil { 101 | return console.ExitWithErrorString("Failed to retrieve ECS Cluster [%s]: %s", ecsClusterName, err.Error()) 102 | } 103 | if ecsCluster == nil || conv.S(ecsCluster.Status) != "ACTIVE" { 104 | console.DetailWithResourceNote("ECS Cluster", ecsClusterName, "(not found)", true) 105 | return nil // stop here 106 | } else { 107 | console.DetailWithResource("ECS Cluster", ecsClusterName) 108 | } 109 | 110 | // ECS Service 111 | ecsServiceName := core.DefaultECSServiceName(appName) 112 | ecsService, err := c.awsClient.ECS().RetrieveService(ecsClusterName, ecsServiceName) 113 | if err != nil { 114 | return console.ExitWithErrorString("Failed to retrieve ECS Service [%s]: %s", ecsServiceName, err.Error()) 115 | } 116 | if ecsService == nil { 117 | console.DetailWithResourceNote("ECS Service", ecsServiceName, "(not found)", true) 118 | return nil // stop here 119 | } else if conv.S(ecsService.Status) == "ACTIVE" { 120 | console.DetailWithResource("ECS Service", ecsServiceName) 121 | } else { 122 | console.DetailWithResourceNote("ECS Service", ecsServiceName, fmt.Sprintf("(%s)", conv.S(ecsService.Status)), true) 123 | return nil // stop here 124 | } 125 | 126 | // ECS Task Definition 127 | ecsTaskDefinitionName := conv.S(ecsService.TaskDefinition) 128 | ecsTaskDefinition, err := c.awsClient.ECS().RetrieveTaskDefinition(ecsTaskDefinitionName) 129 | if err != nil { 130 | return console.ExitWithErrorString("Failed to retrieve ECS Task Definition [%s]: %s", ecsTaskDefinitionName, err.Error()) 131 | } 132 | if ecsTaskDefinition == nil { 133 | console.DetailWithResourceNote("ECS Task Definition", ecsTaskDefinitionName, "(not found)", true) 134 | return nil // stop here 135 | } else { 136 | console.DetailWithResource("ECS Task Definition", 137 | fmt.Sprintf("%s:%d", conv.S(ecsTaskDefinition.Family), conv.I64(ecsTaskDefinition.Revision))) 138 | } 139 | 140 | // Tasks count / status 141 | isDeploying := false 142 | if ecsService.Deployments != nil { 143 | for _, d := range ecsService.Deployments { 144 | switch conv.S(d.Status) { 145 | case "ACTIVE": 146 | isDeploying = true 147 | case "PRIMARY": 148 | } 149 | } 150 | } 151 | if isDeploying { 152 | console.DetailWithResourceNote("Tasks (current/desired/pending)", fmt.Sprintf("%d/%d/%d", 153 | conv.I64(ecsService.RunningCount), 154 | conv.I64(ecsService.DesiredCount), 155 | conv.I64(ecsService.PendingCount)), 156 | "(deploying)", true) 157 | } else { 158 | console.DetailWithResource("Tasks (current/desired/pending)", fmt.Sprintf("%d/%d/%d", 159 | conv.I64(ecsService.RunningCount), 160 | conv.I64(ecsService.DesiredCount), 161 | conv.I64(ecsService.PendingCount))) 162 | } 163 | 164 | // Container Definition 165 | for _, containerDefinition := range ecsTaskDefinition.ContainerDefinitions { 166 | console.Info("Container Definition") 167 | 168 | console.DetailWithResource("Name", conv.S(containerDefinition.Name)) 169 | console.DetailWithResource("Image", conv.S(containerDefinition.Image)) 170 | 171 | cpu := float64(conv.I64(containerDefinition.Cpu)) / 1024.0 172 | console.DetailWithResource("CPU", fmt.Sprintf("%.2f", cpu)) 173 | 174 | memory := conv.I64(containerDefinition.Memory) 175 | console.DetailWithResource("Memory", fmt.Sprintf("%dm", memory)) 176 | 177 | for _, pm := range containerDefinition.PortMappings { 178 | console.DetailWithResource("Port Mapping (protocol:container:host)", fmt.Sprintf("%s:%d:%d", 179 | conv.S(pm.Protocol), conv.I64(pm.ContainerPort), conv.I64(pm.HostPort))) 180 | } 181 | 182 | for _, ev := range containerDefinition.Environment { 183 | console.DetailWithResource("Env", fmt.Sprintf("%s=%s", 184 | conv.S(ev.Name), conv.S(ev.Value))) 185 | } 186 | } 187 | 188 | // Tasks 189 | taskARNs, err := c.awsClient.ECS().ListServiceTaskARNs(ecsClusterName, ecsServiceName) 190 | if err != nil { 191 | return console.ExitWithErrorString("Failed to list ECS Tasks for ECS Service [%s]: %s", ecsServiceName, err.Error()) 192 | } 193 | tasks, err := c.awsClient.ECS().RetrieveTasks(ecsClusterName, taskARNs) 194 | if err != nil { 195 | return console.ExitWithErrorString("Failed to retrieve ECS Tasks for ECS Service [%s]: %s", ecsServiceName, err.Error()) 196 | } 197 | 198 | // retrieve container instance info 199 | containerInstanceARNs := []string{} 200 | for _, task := range tasks { 201 | containerInstanceARNs = append(containerInstanceARNs, conv.S(task.ContainerInstanceArn)) 202 | } 203 | containerInstances, err := c.awsClient.ECS().RetrieveContainerInstances(ecsClusterName, containerInstanceARNs) 204 | if err != nil { 205 | return console.ExitWithErrorString("Failed to retrieve ECS Container Instances: %s", err.Error()) 206 | } 207 | 208 | // retrieve EC2 Instance info 209 | ec2InstanceIDs := []string{} 210 | for _, ci := range containerInstances { 211 | ec2InstanceIDs = append(ec2InstanceIDs, conv.S(ci.Ec2InstanceId)) 212 | } 213 | ec2Instances, err := c.awsClient.EC2().RetrieveInstances(ec2InstanceIDs) 214 | if err != nil { 215 | return console.ExitWithErrorString("Failed to retrieve EC2 Instances: %s", err.Error()) 216 | } 217 | 218 | for _, task := range tasks { 219 | console.Info("ECS Task") 220 | 221 | taskDefinition := aws.GetECSTaskDefinitionFamilyAndRevisionFromARN(conv.S(task.TaskDefinitionArn)) 222 | console.DetailWithResource("Task Definition", taskDefinition) 223 | 224 | console.DetailWithResource("Status (current/desired)", fmt.Sprintf("%s/%s", 225 | conv.S(task.LastStatus), conv.S(task.DesiredStatus))) 226 | 227 | for _, ci := range containerInstances { 228 | if conv.S(task.ContainerInstanceArn) == conv.S(ci.ContainerInstanceArn) { 229 | console.DetailWithResource("EC2 Instance ID", conv.S(ci.Ec2InstanceId)) 230 | 231 | for _, ec2Instance := range ec2Instances { 232 | if conv.S(ci.Ec2InstanceId) == conv.S(ec2Instance.InstanceId) { 233 | if !utils.IsBlank(conv.S(ec2Instance.PrivateIpAddress)) { 234 | console.DetailWithResource(" Private IP", conv.S(ec2Instance.PrivateIpAddress)) 235 | } 236 | if !utils.IsBlank(conv.S(ec2Instance.PublicIpAddress)) { 237 | console.DetailWithResource(" Public IP", conv.S(ec2Instance.PublicIpAddress)) 238 | } 239 | break 240 | } 241 | } 242 | break 243 | } 244 | } 245 | } 246 | 247 | // Load Balancer 248 | if ecsService.LoadBalancers != nil && len(ecsService.LoadBalancers) > 0 { 249 | for _, lb := range ecsService.LoadBalancers { 250 | console.Info("Load Balancer") 251 | 252 | elbTargetGroup, err := c.awsClient.ELB().RetrieveTargetGroup(conv.S(lb.TargetGroupArn)) 253 | if err != nil { 254 | return console.ExitWithErrorString("Failed to retrieve ELB Target Group [%s]: %s", conv.S(lb.TargetGroupArn), err.Error()) 255 | } 256 | 257 | console.DetailWithResource("Container Port", fmt.Sprintf("%d", conv.I64(lb.ContainerPort))) 258 | console.DetailWithResource("ELB Target Group", conv.S(elbTargetGroup.TargetGroupName)) 259 | 260 | if elbTargetGroup.LoadBalancerArns != nil { 261 | for _, elbARN := range elbTargetGroup.LoadBalancerArns { 262 | elbLoadBalancer, err := c.awsClient.ELB().RetrieveLoadBalancer(conv.S(elbARN)) 263 | if err != nil { 264 | return console.ExitWithErrorString("Failed to retrieve ELB Load Balancer [%s]: %s", elbARN, err.Error()) 265 | } 266 | 267 | console.DetailWithResource("ELB Load Balancer", conv.S(elbLoadBalancer.LoadBalancerName)) 268 | console.DetailWithResource(" Scheme", conv.S(elbLoadBalancer.Scheme)) 269 | //console.DetailWithResource(" DNS", conv.S(elbLoadBalancer.DNSName)) 270 | if elbLoadBalancer.State != nil { 271 | console.DetailWithResource(" State", fmt.Sprintf("%s %s", 272 | conv.S(elbLoadBalancer.State.Code), 273 | conv.S(elbLoadBalancer.State.Reason))) 274 | } 275 | 276 | // listeners 277 | listeners, err := c.awsClient.ELB().RetrieveLoadBalancerListeners(conv.S(elbARN)) 278 | if err != nil { 279 | return console.ExitWithErrorString("Failed to retrieve Listeners for ELB Load Balancer [%s]: %s", elbARN, err.Error()) 280 | } 281 | for _, listener := range listeners { 282 | if listener.DefaultActions != nil && 283 | len(listener.DefaultActions) > 0 && 284 | conv.S(listener.DefaultActions[0].TargetGroupArn) == conv.S(elbTargetGroup.TargetGroupArn) { 285 | console.DetailWithResource(" Endpoint", fmt.Sprintf("%s://%s:%d", 286 | strings.ToLower(conv.S(listener.Protocol)), 287 | conv.S(elbLoadBalancer.DNSName), 288 | conv.I64(listener.Port))) 289 | } 290 | } 291 | } 292 | } 293 | } 294 | } 295 | 296 | return nil 297 | } 298 | -------------------------------------------------------------------------------- /commands/status/flags.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import "gopkg.in/alecthomas/kingpin.v2" 4 | 5 | type Flags struct { 6 | AppName *string 7 | ClusterName *string 8 | } 9 | 10 | func NewFlags(kc *kingpin.CmdClause) *Flags { 11 | return &Flags{ 12 | AppName: kc.Flag("app-name", "App name").Default("").String(), 13 | ClusterName: kc.Flag("cluster-name", "App name").Default("").String(), 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Config struct { 4 | Name *string `json:"name,omitempty" yaml:"name,omitempty"` 5 | ClusterName *string `json:"cluster,omitempty" yaml:"cluster,omitempty"` 6 | Port *uint16 `json:"port,omitempty" yaml:"port,omitempty"` 7 | CPU *float64 `json:"cpu,omitempty" yaml:"cpu,omitempty"` 8 | Memory *string `json:"memory,omitempty" yaml:"memory,omitempty"` 9 | Units *uint16 `json:"units,omitempty" yaml:"units,omitempty"` 10 | Env map[string]string `json:"env,omitempty" yaml:"env,omitempty"` 11 | LoadBalancer ConfigLoadBalancer `json:"load_balancer" yaml:"load_balancer"` 12 | Logging ConfigLogging `json:"logging" yaml:"logging"` 13 | AWS ConfigAWS `json:"aws" yaml:"aws"` 14 | Docker ConfigDocker `json:"docker" yaml:"docker"` 15 | } 16 | 17 | type ConfigLoadBalancer struct { 18 | Enabled *bool `json:"enabled" yaml:"enabled"` 19 | Port *uint16 `json:"port,omitempty" yaml:"port,omitempty"` 20 | HTTPSPort *uint16 `json:"https_port,omitempty" yaml:"https_port,omitempty"` 21 | HealthCheck ConfigLoadBalancerHealthCheck `json:"health_check,omitempty" yaml:"health_check,omitempty"` 22 | } 23 | 24 | type ConfigLoadBalancerHealthCheck struct { 25 | Interval *string `json:"interval,omitempty" yaml:"interval,omitempty"` 26 | Path *string `json:"path,omitempty" yaml:"path,omitempty"` 27 | Status *string `json:"status,omitempty" yaml:"status,omitempty"` 28 | Timeout *string `json:"timeout,omitempty" yaml:"timeout,omitempty"` 29 | HealthyLimit *uint16 `json:"healthy_limit,omitempty" yaml:"healthy_limit,omitempty"` 30 | UnhealthyLimit *uint16 `json:"unhealthy_limit,omitempty" yaml:"unhealthy_limit,omitempty"` 31 | } 32 | 33 | type ConfigLogging struct { 34 | Driver *string `json:"driver,omitempty" yaml:"driver,omitempty"` 35 | Options map[string]string `json:"options" yaml:"options"` 36 | } 37 | 38 | type ConfigAWS struct { 39 | ELBLoadBalancerName *string `json:"elb_name,omitempty" yaml:"elb_name,omitempty"` 40 | ELBTargetGroupName *string `json:"elb_target_group_name,omitempty" yaml:"elb_target_group_name,omitempty"` 41 | ELBSecurityGroupName *string `json:"elb_security_group_name,omitempty" yaml:"elb_security_group_name,omitempty"` 42 | ELBCertificateARN *string `json:"elb_certificate_arn,omitempty" yaml:"elb_certificate_arn,omitempty"` 43 | ECRRepositoryName *string `json:"ecr_repo_name,omitempty" yaml:"ecr_repo_name,omitempty"` 44 | } 45 | 46 | type ConfigDocker struct { 47 | Bin *string `json:"bin,omitempty" yaml:"bin,omitempty"` 48 | } 49 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 4 | 5 | const refConfigYAML = ` 6 | name: echo 7 | cluster: cluster1 8 | port: 8080 9 | cpu: 1.0 10 | memory: 200m 11 | units: 4 12 | 13 | env: 14 | key1: value1 15 | key2: value2 16 | 17 | load_balancer: 18 | enabled: true 19 | port: 80 20 | https_port: 443 21 | 22 | health_check: 23 | interval: 30s 24 | path: "/ping" 25 | status: "200-299" 26 | timeout: 5s 27 | healthy_limit: 5 28 | unhealthy_limit: 2 29 | 30 | logging: 31 | driver: json-file 32 | options: 33 | logopt1: value1 34 | logopt2: value2 35 | 36 | aws: 37 | elb_name: echo-lb 38 | elb_target_group_name: echo-target 39 | elb_security_group_name: echo-lb-sg 40 | elb_certificate_arn: arn:aws:acm:us-west-2:aws-account-id:certificate/certificiate-identifier 41 | ecr_repo_name: echo-repo 42 | 43 | docker: 44 | bin: "/usr/local/bin/docker" 45 | ` 46 | 47 | const refConfigJSON = ` 48 | { 49 | "name": "echo", 50 | "cluster": "cluster1", 51 | "port": 8080, 52 | "cpu": 1.0, 53 | "memory": "200m", 54 | "units": 4, 55 | "env": { 56 | "key1": "value1", 57 | "key2": "value2" 58 | }, 59 | "load_balancer": { 60 | "enabled": true, 61 | "port": 80, 62 | "https_port": 443, 63 | "health_check": { 64 | "interval": "30s", 65 | "path": "/ping", 66 | "status": "200-299", 67 | "timeout": "5s", 68 | "healthy_limit": 5, 69 | "unhealthy_limit": 2 70 | } 71 | }, 72 | "logging": { 73 | "driver": "json-file", 74 | "options": { 75 | "logopt1": "value1", 76 | "logopt2": "value2" 77 | } 78 | }, 79 | "aws": { 80 | "elb_name": "echo-lb", 81 | "elb_target_group_name": "echo-target", 82 | "elb_security_group_name": "echo-lb-sg", 83 | "elb_certificate_arn": "arn:aws:acm:us-west-2:aws-account-id:certificate/certificiate-identifier", 84 | "ecr_repo_name": "echo-repo" 85 | }, 86 | "docker": { 87 | "bin": "/usr/local/bin/docker" 88 | } 89 | }` 90 | 91 | var refConfig = &Config{ 92 | Name: conv.SP("echo"), 93 | ClusterName: conv.SP("cluster1"), 94 | Port: conv.U16P(8080), 95 | CPU: conv.F64P(1.0), 96 | Memory: conv.SP("200m"), 97 | Units: conv.U16P(4), 98 | Env: map[string]string{ 99 | "key1": "value1", 100 | "key2": "value2", 101 | }, 102 | LoadBalancer: ConfigLoadBalancer{ 103 | Enabled: conv.BP(true), 104 | Port: conv.U16P(80), 105 | HTTPSPort: conv.U16P(443), 106 | HealthCheck: ConfigLoadBalancerHealthCheck{ 107 | Interval: conv.SP("30s"), 108 | Path: conv.SP("/ping"), 109 | Status: conv.SP("200-299"), 110 | Timeout: conv.SP("5s"), 111 | HealthyLimit: conv.U16P(5), 112 | UnhealthyLimit: conv.U16P(2), 113 | }, 114 | }, 115 | Logging: ConfigLogging{ 116 | Driver: conv.SP("json-file"), 117 | Options: map[string]string{ 118 | "logopt1": "value1", 119 | "logopt2": "value2", 120 | }, 121 | }, 122 | AWS: ConfigAWS{ 123 | ELBLoadBalancerName: conv.SP("echo-lb"), 124 | ELBTargetGroupName: conv.SP("echo-target"), 125 | ELBSecurityGroupName: conv.SP("echo-lb-sg"), 126 | ELBCertificateARN: conv.SP("arn:aws:acm:us-west-2:aws-account-id:certificate/certificiate-identifier"), 127 | ECRRepositoryName: conv.SP("echo-repo"), 128 | }, 129 | Docker: ConfigDocker{ 130 | Bin: conv.SP("/usr/local/bin/docker"), 131 | }, 132 | } 133 | 134 | var partialConfigYAML = ` 135 | name: hello 136 | port: 0 137 | cpu: 1.0 138 | memory: 512m 139 | 140 | load_balancer: 141 | enabled: false 142 | ` 143 | 144 | var partialConfig = &Config{ 145 | Name: conv.SP("hello"), 146 | Port: conv.U16P(0), 147 | CPU: conv.F64P(1.0), 148 | Memory: conv.SP("512m"), 149 | LoadBalancer: ConfigLoadBalancer{ 150 | Enabled: conv.BP(false), 151 | }, 152 | } 153 | -------------------------------------------------------------------------------- /config/default_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/coldbrewcloud/coldbrew-cli/core" 5 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 6 | ) 7 | 8 | func DefaultConfig(appName string) *Config { 9 | conf := new(Config) 10 | 11 | conf.Name = conv.SP(appName) 12 | conf.ClusterName = conv.SP("cluster1") 13 | conf.Port = conv.U16P(80) 14 | conf.CPU = conv.F64P(0.5) 15 | conf.Memory = conv.SP("500m") 16 | conf.Units = conv.U16P(1) 17 | 18 | // Environment variables 19 | conf.Env = make(map[string]string) 20 | 21 | // load balancer 22 | { 23 | conf.LoadBalancer.Enabled = conv.BP(false) 24 | conf.LoadBalancer.Port = conv.U16P(80) 25 | 26 | // health check 27 | conf.LoadBalancer.HealthCheck.Path = conv.SP("/") 28 | conf.LoadBalancer.HealthCheck.Status = conv.SP("200-299") 29 | conf.LoadBalancer.HealthCheck.Interval = conv.SP("15s") 30 | conf.LoadBalancer.HealthCheck.Timeout = conv.SP("10s") 31 | conf.LoadBalancer.HealthCheck.HealthyLimit = conv.U16P(3) 32 | conf.LoadBalancer.HealthCheck.UnhealthyLimit = conv.U16P(3) 33 | } 34 | 35 | // logging 36 | { 37 | conf.Logging.Driver = nil 38 | conf.Logging.Options = make(map[string]string) 39 | } 40 | 41 | // AWS 42 | { 43 | // ELB name: cannot exceed 32 chars 44 | elbLoadBalancerName := "" 45 | if len(appName) > 28 { 46 | elbLoadBalancerName = core.DefaultELBLoadBalancerName(appName[:28]) 47 | } else { 48 | elbLoadBalancerName = core.DefaultELBLoadBalancerName(appName) 49 | } 50 | conf.AWS.ELBLoadBalancerName = conv.SP(elbLoadBalancerName) 51 | 52 | // ELB target group name: cannot exceed 32 chars 53 | elbLoadBalancerTargetGroupName := "" 54 | if len(appName) > 25 { 55 | elbLoadBalancerTargetGroupName = core.DefaultELBTargetGroupName(appName[:25]) 56 | } else { 57 | elbLoadBalancerTargetGroupName = core.DefaultELBTargetGroupName(appName) 58 | } 59 | conf.AWS.ELBTargetGroupName = conv.SP(elbLoadBalancerTargetGroupName) 60 | 61 | // ELB security group 62 | elbSecurityGroupName := "" 63 | if len(appName) > 25 { 64 | elbSecurityGroupName = core.DefaultELBLoadBalancerSecurityGroupName(appName[:25]) 65 | } else { 66 | elbSecurityGroupName = core.DefaultELBLoadBalancerSecurityGroupName(appName) 67 | } 68 | conf.AWS.ELBSecurityGroupName = conv.SP(elbSecurityGroupName) 69 | 70 | // ECR Repository name 71 | conf.AWS.ECRRepositoryName = conv.SP(core.DefaultECRRepository(appName)) 72 | } 73 | 74 | // Docker 75 | conf.Docker.Bin = conv.SP("docker") 76 | 77 | return conf 78 | } 79 | -------------------------------------------------------------------------------- /config/default_config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDefaultConfig(t *testing.T) { 11 | defConf := DefaultConfig("app1") 12 | assert.Equal(t, "app1", conv.S(defConf.Name)) 13 | err := defConf.Validate() 14 | assert.Nil(t, err) 15 | 16 | // max app name: 32 chars 17 | defConf = DefaultConfig("12345678901234567890123456789012") 18 | assert.Equal(t, "12345678901234567890123456789012", conv.S(defConf.Name)) 19 | err = defConf.Validate() 20 | assert.Nil(t, err) 21 | assert.Len(t, conv.S(defConf.AWS.ELBLoadBalancerName), 32) 22 | assert.Len(t, conv.S(defConf.AWS.ELBTargetGroupName), 32) 23 | 24 | // app name's too long 25 | defConf = DefaultConfig("123456789012345678901234567890123") 26 | err = defConf.Validate() 27 | assert.NotNil(t, err) 28 | } 29 | -------------------------------------------------------------------------------- /config/load.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/coldbrewcloud/coldbrew-cli/core" 8 | "github.com/coldbrewcloud/coldbrew-cli/flags" 9 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 10 | ) 11 | 12 | func Load(data []byte, configFormat string, defaultAppName string) (*Config, error) { 13 | conf := &Config{} 14 | configFormat = strings.ToLower(configFormat) 15 | switch configFormat { 16 | case flags.GlobalFlagsConfigFileFormatYAML: 17 | if err := conf.FromYAML(data); err != nil { 18 | return nil, fmt.Errorf("Failed to read configuration in YAML: %s\n", err.Error()) 19 | } 20 | case flags.GlobalFlagsConfigFileFormatJSON: 21 | if err := conf.FromJSON(data); err != nil { 22 | return nil, fmt.Errorf("Failed to read configuration in JSON: %s\n", err.Error()) 23 | } 24 | default: 25 | return nil, fmt.Errorf("Unsupported configuration format [%s]", configFormat) 26 | } 27 | 28 | // env 29 | if conf.Env == nil { 30 | conf.Env = make(map[string]string) 31 | } 32 | 33 | // merge with defaults: defaultAppName is used only if loaded configuration does not have app name 34 | appName := conv.S(conf.Name) 35 | if appName == "" { 36 | appName = defaultAppName 37 | } 38 | conf.Defaults(DefaultConfig(appName)) 39 | 40 | // validation 41 | if err := conf.Validate(); err != nil { 42 | return nil, core.NewErrorExtraInfo(err, "https://github.com/coldbrewcloud/coldbrew-cli/wiki/Configuration-File") 43 | } 44 | 45 | return conf, nil 46 | } 47 | 48 | func (c *Config) Defaults(source *Config) { 49 | if source == nil { 50 | return 51 | } 52 | 53 | defS(&c.Name, source.Name) 54 | defS(&c.ClusterName, source.ClusterName) 55 | defU16(&c.Port, source.Port) 56 | defF64(&c.CPU, source.CPU) 57 | defS(&c.Memory, source.Memory) 58 | defU16(&c.Units, source.Units) 59 | 60 | // envs 61 | if c.Env == nil { 62 | c.Env = make(map[string]string) 63 | } 64 | for ek, ev := range source.Env { 65 | c.Env[ek] = ev 66 | } 67 | 68 | // load balancer 69 | defB(&c.LoadBalancer.Enabled, source.LoadBalancer.Enabled) 70 | defU16(&c.LoadBalancer.Port, source.LoadBalancer.Port) 71 | defU16(&c.LoadBalancer.HTTPSPort, source.LoadBalancer.HTTPSPort) 72 | defS(&c.LoadBalancer.HealthCheck.Interval, source.LoadBalancer.HealthCheck.Interval) 73 | defS(&c.LoadBalancer.HealthCheck.Path, source.LoadBalancer.HealthCheck.Path) 74 | defS(&c.LoadBalancer.HealthCheck.Status, source.LoadBalancer.HealthCheck.Status) 75 | defS(&c.LoadBalancer.HealthCheck.Timeout, source.LoadBalancer.HealthCheck.Timeout) 76 | defU16(&c.LoadBalancer.HealthCheck.HealthyLimit, source.LoadBalancer.HealthCheck.HealthyLimit) 77 | defU16(&c.LoadBalancer.HealthCheck.UnhealthyLimit, source.LoadBalancer.HealthCheck.UnhealthyLimit) 78 | 79 | // logging 80 | if conv.S(c.Logging.Driver) == "" { 81 | // logging option is copied only when logging driver was copied 82 | defS(&c.Logging.Driver, source.Logging.Driver) 83 | if source.Logging.Options != nil { 84 | c.Logging.Options = make(map[string]string) 85 | for k, v := range source.Logging.Options { 86 | c.Logging.Options[k] = v 87 | } 88 | } 89 | } 90 | 91 | // AWS 92 | defS(&c.AWS.ELBLoadBalancerName, source.AWS.ELBLoadBalancerName) 93 | defS(&c.AWS.ELBTargetGroupName, source.AWS.ELBTargetGroupName) 94 | defS(&c.AWS.ELBSecurityGroupName, source.AWS.ELBSecurityGroupName) 95 | defS(&c.AWS.ELBCertificateARN, source.AWS.ELBCertificateARN) 96 | defS(&c.AWS.ECRRepositoryName, source.AWS.ECRRepositoryName) 97 | 98 | // docker 99 | defS(&c.Docker.Bin, source.Docker.Bin) 100 | } 101 | 102 | func defS(src **string, dest *string) { 103 | if *src == nil && dest != nil { 104 | *src = conv.SP(conv.S(dest)) 105 | } 106 | } 107 | 108 | func defU16(src **uint16, dest *uint16) { 109 | if *src == nil && dest != nil { 110 | *src = conv.U16P(conv.U16(dest)) 111 | } 112 | } 113 | 114 | func defB(src **bool, dest *bool) { 115 | if *src == nil && dest != nil { 116 | *src = conv.BP(conv.B(dest)) 117 | } 118 | } 119 | 120 | func defF64(src **float64, dest *float64) { 121 | if *src == nil && dest != nil { 122 | *src = conv.F64P(conv.F64(dest)) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /config/load_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/coldbrewcloud/coldbrew-cli/flags" 7 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type testObject struct { 12 | String *string 13 | Bool *bool 14 | Uint16 *uint16 15 | Float64 *float64 16 | } 17 | 18 | func TestLoad(t *testing.T) { 19 | // loading empty data 20 | conf, err := Load([]byte(""), flags.GlobalFlagsConfigFileFormatYAML, "app1") 21 | assert.Nil(t, err) 22 | assert.NotNil(t, conf) 23 | assert.Equal(t, "app1", conv.S(conf.Name)) 24 | conf, err = Load([]byte("{}"), flags.GlobalFlagsConfigFileFormatJSON, "app1") 25 | assert.Nil(t, err) 26 | assert.NotNil(t, conf) 27 | assert.Equal(t, "app1", conv.S(conf.Name)) 28 | 29 | // empty data and empty app name 30 | conf, err = Load([]byte(""), flags.GlobalFlagsConfigFileFormatYAML, "") 31 | assert.NotNil(t, err) 32 | conf, err = Load([]byte(""), flags.GlobalFlagsConfigFileFormatJSON, "") 33 | assert.NotNil(t, err) 34 | 35 | // loading "name" only data 36 | conf, err = Load([]byte("name: app2"), flags.GlobalFlagsConfigFileFormatYAML, "app3") 37 | assert.Nil(t, err) 38 | assert.NotNil(t, conf) 39 | assert.Equal(t, "app2", conv.S(conf.Name)) 40 | conf, err = Load([]byte("{\"name\":\"app2\"}"), flags.GlobalFlagsConfigFileFormatJSON, "app3") 41 | assert.Nil(t, err) 42 | assert.NotNil(t, conf) 43 | assert.Equal(t, "app2", conv.S(conf.Name)) 44 | 45 | // reference config data (YAML) 46 | conf, err = Load([]byte(refConfigYAML), flags.GlobalFlagsConfigFileFormatYAML, "app4") 47 | assert.Nil(t, err) 48 | assert.NotNil(t, conf) 49 | assert.Equal(t, conv.S(refConfig.Name), conv.S(conf.Name)) 50 | assert.Equal(t, refConfig, conf) 51 | 52 | // reference config data (JSON) 53 | conf, err = Load([]byte(refConfigJSON), flags.GlobalFlagsConfigFileFormatJSON, "app5") 54 | assert.Nil(t, err) 55 | assert.NotNil(t, conf) 56 | assert.Equal(t, conv.S(refConfig.Name), conv.S(conf.Name)) 57 | assert.Equal(t, refConfig, conf) 58 | 59 | // partial config data (YAML) 60 | conf, err = Load([]byte(partialConfigYAML), flags.GlobalFlagsConfigFileFormatYAML, "app6") 61 | assert.Nil(t, err) 62 | assert.NotNil(t, conf) 63 | assert.Equal(t, partialConfig.Name, conf.Name) 64 | assert.Equal(t, partialConfig.Port, conf.Port) 65 | assert.Equal(t, partialConfig.CPU, conf.CPU) 66 | assert.Equal(t, partialConfig.Memory, conf.Memory) 67 | assert.Equal(t, partialConfig.LoadBalancer.Enabled, conf.LoadBalancer.Enabled) 68 | defConf := DefaultConfig(conv.S(conf.Name)) 69 | assert.Equal(t, defConf.ClusterName, conf.ClusterName) 70 | assert.Equal(t, defConf.Units, conf.Units) 71 | assert.Equal(t, defConf.AWS, conf.AWS) 72 | assert.Equal(t, defConf.Docker, conf.Docker) 73 | } 74 | 75 | func TestConfig_Defaults(t *testing.T) { 76 | // defaulting with nil: should not change anything 77 | conf := testClone(refConfig) 78 | conf.Defaults(nil) 79 | assert.Equal(t, refConfig, conf) 80 | 81 | // test envs (other attributes test covered by def* tests) 82 | conf1 := &Config{} 83 | conf2 := &Config{Env: map[string]string{ 84 | "key1": "value1", 85 | "key2": "value2", 86 | }} 87 | conf3 := &Config{Env: map[string]string{ 88 | "key2": "value2-2", 89 | "key3": "value3", 90 | }} 91 | conf1.Defaults(conf2) 92 | assert.Len(t, conf1.Env, 2) 93 | assert.Equal(t, "value1", conf1.Env["key1"]) 94 | assert.Equal(t, "value2", conf1.Env["key2"]) 95 | conf2.Defaults(conf3) 96 | assert.Len(t, conf2.Env, 3) 97 | assert.Equal(t, "value1", conf2.Env["key1"]) 98 | assert.Equal(t, "value2-2", conf2.Env["key2"]) 99 | assert.Equal(t, "value3", conf2.Env["key3"]) 100 | 101 | // test defS() 102 | obj1 := &testObject{} 103 | obj2 := &testObject{String: conv.SP("foo")} 104 | obj3 := &testObject{String: conv.SP("bar")} 105 | defS(&obj1.String, obj2.String) 106 | assert.Equal(t, "foo", conv.S(obj1.String)) 107 | assert.Equal(t, "foo", conv.S(obj2.String)) 108 | defS(&obj1.String, obj2.String) 109 | assert.Equal(t, "foo", conv.S(obj2.String)) 110 | assert.Equal(t, "bar", conv.S(obj3.String)) 111 | obj1 = &testObject{} 112 | obj2 = &testObject{} 113 | defS(&obj1.String, obj2.String) 114 | assert.Nil(t, obj1.String) 115 | assert.Nil(t, obj2.String) 116 | obj1 = &testObject{String: conv.SP("foo")} 117 | obj2 = &testObject{} 118 | defS(&obj1.String, obj2.String) 119 | assert.Equal(t, "foo", conv.S(obj1.String)) 120 | assert.Nil(t, obj2.String) 121 | 122 | // test defB() 123 | obj1 = &testObject{} 124 | obj2 = &testObject{Bool: conv.BP(true)} 125 | obj3 = &testObject{Bool: conv.BP(false)} 126 | defB(&obj1.Bool, obj2.Bool) 127 | assert.Equal(t, true, conv.B(obj1.Bool)) 128 | assert.Equal(t, true, conv.B(obj2.Bool)) 129 | defB(&obj1.Bool, obj2.Bool) 130 | assert.Equal(t, true, conv.B(obj2.Bool)) 131 | assert.Equal(t, false, conv.B(obj3.Bool)) 132 | obj1 = &testObject{} 133 | obj2 = &testObject{} 134 | defB(&obj1.Bool, obj2.Bool) 135 | assert.Nil(t, obj1.Bool) 136 | assert.Nil(t, obj2.Bool) 137 | obj1 = &testObject{Bool: conv.BP(true)} 138 | obj2 = &testObject{} 139 | defB(&obj1.Bool, obj2.Bool) 140 | assert.Equal(t, true, conv.B(obj1.Bool)) 141 | assert.Nil(t, obj2.Bool) 142 | 143 | // test defU16() 144 | obj1 = &testObject{} 145 | obj2 = &testObject{Uint16: conv.U16P(39)} 146 | obj3 = &testObject{Uint16: conv.U16P(42)} 147 | defU16(&obj1.Uint16, obj2.Uint16) 148 | assert.Equal(t, uint16(39), conv.U16(obj1.Uint16)) 149 | assert.Equal(t, uint16(39), conv.U16(obj2.Uint16)) 150 | defU16(&obj1.Uint16, obj2.Uint16) 151 | assert.Equal(t, uint16(39), conv.U16(obj2.Uint16)) 152 | assert.Equal(t, uint16(42), conv.U16(obj3.Uint16)) 153 | obj1 = &testObject{} 154 | obj2 = &testObject{} 155 | defU16(&obj1.Uint16, obj2.Uint16) 156 | assert.Nil(t, obj1.Uint16) 157 | assert.Nil(t, obj2.Uint16) 158 | obj1 = &testObject{Uint16: conv.U16P(39)} 159 | obj2 = &testObject{} 160 | defU16(&obj1.Uint16, obj2.Uint16) 161 | assert.Equal(t, uint16(39), conv.U16(obj1.Uint16)) 162 | assert.Nil(t, obj2.Uint16) 163 | 164 | // test defF64() 165 | obj1 = &testObject{} 166 | obj2 = &testObject{Float64: conv.F64P(52.64)} 167 | obj3 = &testObject{Float64: conv.F64P(-20.22)} 168 | defF64(&obj1.Float64, obj2.Float64) 169 | assert.Equal(t, 52.64, conv.F64(obj1.Float64)) 170 | assert.Equal(t, 52.64, conv.F64(obj2.Float64)) 171 | defF64(&obj1.Float64, obj2.Float64) 172 | assert.Equal(t, 52.64, conv.F64(obj2.Float64)) 173 | assert.Equal(t, -20.22, conv.F64(obj3.Float64)) 174 | obj1 = &testObject{} 175 | obj2 = &testObject{} 176 | defF64(&obj1.Float64, obj2.Float64) 177 | assert.Nil(t, obj1.Float64) 178 | assert.Nil(t, obj2.Float64) 179 | obj1 = &testObject{Float64: conv.F64P(52.64)} 180 | obj2 = &testObject{} 181 | defF64(&obj1.Float64, obj2.Float64) 182 | assert.Equal(t, 52.64, conv.F64(obj1.Float64)) 183 | assert.Nil(t, obj2.Float64) 184 | } 185 | 186 | func testClone(src *Config) *Config { 187 | yaml, err := src.ToYAML() 188 | if err != nil { 189 | panic(err) 190 | } 191 | dest := &Config{} 192 | if err := dest.FromYAML(yaml); err != nil { 193 | panic(err) 194 | } 195 | return dest 196 | } 197 | -------------------------------------------------------------------------------- /config/persist.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "gopkg.in/yaml.v2" 8 | ) 9 | 10 | func (c *Config) FromJSON(data []byte) error { 11 | if err := json.Unmarshal(data, c); err != nil { 12 | return fmt.Errorf("Failed to parse JSON: %s", err.Error()) 13 | } 14 | 15 | return nil 16 | } 17 | 18 | func (c *Config) FromYAML(data []byte) error { 19 | if err := yaml.Unmarshal(data, c); err != nil { 20 | return fmt.Errorf("Failed to parse YAML: %s", err.Error()) 21 | } 22 | 23 | return nil 24 | } 25 | 26 | func (c *Config) ToJSON() ([]byte, error) { 27 | data, err := json.Marshal(c) 28 | if err != nil { 29 | return nil, fmt.Errorf("Failed to convert to JSON: %s", err.Error()) 30 | } 31 | 32 | return data, nil 33 | } 34 | 35 | func (c *Config) ToJSONWithIndent() ([]byte, error) { 36 | data, err := json.MarshalIndent(c, "", " ") 37 | if err != nil { 38 | return nil, fmt.Errorf("Failed to convert to JSON: %s", err.Error()) 39 | } 40 | 41 | return data, nil 42 | } 43 | 44 | func (c *Config) ToYAML() ([]byte, error) { 45 | data, err := yaml.Marshal(c) 46 | if err != nil { 47 | return nil, fmt.Errorf("Failed to convert to YAML: %s", err.Error()) 48 | } 49 | 50 | return data, nil 51 | } 52 | -------------------------------------------------------------------------------- /config/persist_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestConfig_FromYAML(t *testing.T) { 10 | testConfig := &Config{} 11 | err := testConfig.FromYAML([]byte(refConfigYAML)) 12 | assert.Nil(t, err) 13 | assert.Equal(t, refConfig, testConfig) 14 | } 15 | 16 | func TestConfig_FromJSON(t *testing.T) { 17 | testConfig := &Config{} 18 | err := testConfig.FromJSON([]byte(refConfigJSON)) 19 | assert.Nil(t, err) 20 | assert.Equal(t, refConfig, testConfig) 21 | } 22 | 23 | func TestConfig_ToYAML(t *testing.T) { 24 | data, err := refConfig.ToYAML() 25 | assert.Nil(t, err) 26 | assert.NotNil(t, data) 27 | 28 | testConfig := &Config{} 29 | err = testConfig.FromYAML(data) 30 | assert.Nil(t, err) 31 | assert.Equal(t, refConfig, testConfig) 32 | } 33 | 34 | func TestConfig_ToJSON(t *testing.T) { 35 | data, err := refConfig.ToJSON() 36 | assert.Nil(t, err) 37 | assert.NotNil(t, data) 38 | 39 | testConfig := &Config{} 40 | err = testConfig.FromJSON(data) 41 | assert.Nil(t, err) 42 | assert.Equal(t, refConfig, testConfig) 43 | } 44 | 45 | func TestConfig_ToJSONWithIndent(t *testing.T) { 46 | data, err := refConfig.ToJSONWithIndent() 47 | assert.Nil(t, err) 48 | assert.NotNil(t, data) 49 | 50 | testConfig := &Config{} 51 | err = testConfig.FromJSON(data) 52 | assert.Nil(t, err) 53 | assert.Equal(t, refConfig, testConfig) 54 | } 55 | 56 | func TestConfig_YAMLJSON(t *testing.T) { 57 | jsonConfig := &Config{} 58 | err := jsonConfig.FromJSON([]byte(refConfigJSON)) 59 | assert.Nil(t, err) 60 | assert.Equal(t, refConfig, jsonConfig) 61 | 62 | yamlData, err := jsonConfig.ToYAML() 63 | assert.Nil(t, err) 64 | assert.NotNil(t, yamlData) 65 | 66 | yamlConfig := &Config{} 67 | err = yamlConfig.FromYAML(yamlData) 68 | assert.Nil(t, err) 69 | assert.Equal(t, jsonConfig, yamlConfig) 70 | 71 | jsonData, err := yamlConfig.ToJSON() 72 | assert.Nil(t, err) 73 | assert.NotNil(t, jsonData) 74 | 75 | jsonConfig2 := &Config{} 76 | err = jsonConfig2.FromJSON(jsonData) 77 | assert.Nil(t, err) 78 | assert.Equal(t, jsonConfig, jsonConfig2) 79 | } 80 | -------------------------------------------------------------------------------- /config/validate.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/coldbrewcloud/coldbrew-cli/aws" 8 | "github.com/coldbrewcloud/coldbrew-cli/core" 9 | "github.com/coldbrewcloud/coldbrew-cli/utils" 10 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 11 | ) 12 | 13 | func (c *Config) Validate() error { 14 | if !core.AppNameRE.MatchString(conv.S(c.Name)) { 15 | return fmt.Errorf("Invalid app name [%s]", conv.S(c.Name)) 16 | } 17 | 18 | if !core.ClusterNameRE.MatchString(conv.S(c.ClusterName)) { 19 | return fmt.Errorf("Invalid cluster name [%s]", conv.S(c.ClusterName)) 20 | } 21 | 22 | if conv.U16(c.Units) > core.MaxAppUnits { 23 | return fmt.Errorf("Units cannot exceed %d", core.MaxAppUnits) 24 | } 25 | 26 | if conv.F64(c.CPU) == 0 { 27 | return errors.New("CPU cannot be 0") 28 | } 29 | if conv.F64(c.CPU) > core.MaxAppCPU { 30 | return fmt.Errorf("CPU cannot exceed %d", core.MaxAppCPU) 31 | } 32 | 33 | if !core.SizeExpressionRE.MatchString(conv.S(c.Memory)) { 34 | return fmt.Errorf("Invalid app memory [%s] (1)", conv.S(c.Memory)) 35 | } else { 36 | sizeInBytes, err := core.ParseSizeExpression((conv.S(c.Memory))) 37 | if err != nil { 38 | return fmt.Errorf("Invalid app memory: %s", err.Error()) 39 | } 40 | if sizeInBytes > core.MaxAppMemoryInMB*1000*1000 { 41 | return fmt.Errorf("App memory cannot exceed %dM", core.MaxAppMemoryInMB) 42 | } 43 | } 44 | 45 | if conv.U16(c.LoadBalancer.HTTPSPort) == 0 && 46 | conv.U16(c.LoadBalancer.Port) == 0 { 47 | return errors.New("Load balancer ort number is required.") 48 | } 49 | 50 | if !core.TimeExpressionRE.MatchString(conv.S(c.LoadBalancer.HealthCheck.Interval)) { 51 | return fmt.Errorf("Invalid health check interval [%s]", conv.S(c.LoadBalancer.HealthCheck.Interval)) 52 | } 53 | 54 | if !core.HealthCheckPathRE.MatchString(conv.S(c.LoadBalancer.HealthCheck.Path)) { 55 | return fmt.Errorf("Invalid health check path [%s]", conv.S(c.LoadBalancer.HealthCheck.Path)) 56 | } 57 | 58 | if !core.HealthCheckStatusRE.MatchString(conv.S(c.LoadBalancer.HealthCheck.Status)) { 59 | return fmt.Errorf("Invalid health check status [%s]", conv.S(c.LoadBalancer.HealthCheck.Status)) 60 | } 61 | 62 | if !core.TimeExpressionRE.MatchString(conv.S(c.LoadBalancer.HealthCheck.Timeout)) { 63 | return fmt.Errorf("Invalid health check timeout [%s]", conv.S(c.LoadBalancer.HealthCheck.Timeout)) 64 | } 65 | 66 | if conv.U16(c.LoadBalancer.HealthCheck.HealthyLimit) == 0 { 67 | return errors.New("Health check healthy limit cannot be 0.") 68 | } 69 | 70 | if conv.U16(c.LoadBalancer.HealthCheck.UnhealthyLimit) == 0 { 71 | return errors.New("Health check unhealthy limit cannot be 0.") 72 | } 73 | 74 | if !core.ECRRepoNameRE.MatchString(conv.S(c.AWS.ECRRepositoryName)) { 75 | return fmt.Errorf("Invalid ECR Resitory name [%s]", conv.S(c.AWS.ECRRepositoryName)) 76 | } 77 | 78 | if !core.ELBNameRE.MatchString(conv.S(c.AWS.ELBLoadBalancerName)) { 79 | return fmt.Errorf("Invalid ELB Load Balancer name [%s]", conv.S(c.AWS.ELBLoadBalancerName)) 80 | } 81 | 82 | if !core.ELBTargetGroupNameRE.MatchString(conv.S(c.AWS.ELBTargetGroupName)) { 83 | return fmt.Errorf("Invalid ELB Target Group name [%s]", conv.S(c.AWS.ELBTargetGroupName)) 84 | } 85 | 86 | if conv.U16(c.LoadBalancer.HTTPSPort) > 0 && utils.IsBlank(conv.S(c.AWS.ELBCertificateARN)) { 87 | return errors.New("Certificate ARN required to enable HTTPS.") 88 | } 89 | 90 | if !core.ELBSecurityGroupNameRE.MatchString(conv.S(c.AWS.ELBSecurityGroupName)) { 91 | return fmt.Errorf("Invalid ELB Security Group name [%s]", conv.S(c.AWS.ELBSecurityGroupName)) 92 | } 93 | 94 | switch conv.S(c.Logging.Driver) { 95 | case "", 96 | aws.ECSTaskDefinitionLogDriverAWSLogs, 97 | aws.ECSTaskDefinitionLogDriverJSONFile, 98 | aws.ECSTaskDefinitionLogDriverSyslog, 99 | aws.ECSTaskDefinitionLogDriverFluentd, 100 | aws.ECSTaskDefinitionLogDriverGelf, 101 | aws.ECSTaskDefinitionLogDriverJournald, 102 | aws.ECSTaskDefinitionLogDriverSplunk: 103 | // need more validation for other driver types 104 | default: 105 | return fmt.Errorf("Log driver [%s] not supported.", conv.S(c.Logging.Driver)) 106 | } 107 | 108 | if utils.IsBlank(conv.S(c.Docker.Bin)) { 109 | return fmt.Errorf("Invalid docker executable path [%s]", conv.S(c.Docker.Bin)) 110 | } 111 | 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /console/ask.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | func AskConfirm(message string, defaultYes bool) bool { 10 | return AskConfirmWithNote(message, defaultYes, "") 11 | } 12 | 13 | func AskConfirmWithNote(message string, defaultYes bool, note string) bool { 14 | reader := bufio.NewReader(os.Stdin) 15 | 16 | if note != "" { 17 | stdout("%s\n", ColorFnAskConfirmNote(note)) 18 | } 19 | 20 | for { 21 | if defaultYes { 22 | stdout("%s %s [%s/%s]: ", 23 | ColorFnMarkQuestion(MarkQuestion), 24 | ColorFnAskConfirmMain(message), 25 | ColorFnAskConfirmDefaultAnswer("Y"), 26 | ColorFnAskConfirmAnswer("n")) 27 | } else { 28 | stdout("%s %s [%s/%s]: ", 29 | ColorFnMarkQuestion(MarkQuestion), 30 | ColorFnAskConfirmMain(message), 31 | ColorFnAskConfirmAnswer("y"), 32 | ColorFnAskConfirmDefaultAnswer("N")) 33 | } 34 | 35 | response, err := reader.ReadString('\n') 36 | if err != nil { 37 | stderr("Error: %s\n", err.Error()) 38 | return false 39 | } 40 | 41 | switch strings.ToLower(strings.TrimSpace(response)) { 42 | case "y", "yes": 43 | return true 44 | case "n", "no": 45 | return false 46 | case "": 47 | return defaultYes 48 | } 49 | } 50 | } 51 | 52 | func AskQuestion(message, defaultValue string) string { 53 | return AskQuestionWithNote(message, defaultValue, "") 54 | } 55 | 56 | func AskQuestionWithNote(message, defaultValue, note string) string { 57 | reader := bufio.NewReader(os.Stdin) 58 | 59 | if note != "" { 60 | stdout("%s\n", ColorFnAskQuestionNote(note)) 61 | } 62 | 63 | stdout("%s %s [%s]: ", 64 | ColorFnMarkQuestion(MarkQuestion), 65 | ColorFnAskQuestionMain(message), 66 | ColorFnAskQuestionDefaultValue(defaultValue)) 67 | 68 | response, err := reader.ReadString('\n') 69 | if err != nil { 70 | stderr("Error: %s\n", err.Error()) 71 | return "" 72 | } 73 | 74 | response = strings.TrimSpace(response) 75 | if response == "" { 76 | return defaultValue 77 | } else { 78 | return response 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /console/console.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/coldbrewcloud/coldbrew-cli/core" 9 | ) 10 | 11 | func stdout(format string, a ...interface{}) (int, error) { 12 | return fmt.Fprintf(os.Stdout, format, a...) 13 | } 14 | 15 | func stderr(format string, a ...interface{}) (int, error) { 16 | return fmt.Fprintf(os.Stderr, format, a...) 17 | } 18 | 19 | func noop(string, ...interface{}) (int, error) { 20 | return 0, nil 21 | } 22 | 23 | var ( 24 | debugfFn = noop 25 | debugLogPrefix = "" 26 | 27 | printfFn = stdout 28 | errorfFn = stderr 29 | ) 30 | 31 | func EnablePrintf(enable bool) { 32 | if enable { 33 | printfFn = stdout 34 | } else { 35 | printfFn = noop 36 | } 37 | } 38 | 39 | func EnableErrorf(enable bool) { 40 | if enable { 41 | errorfFn = stderr 42 | } else { 43 | errorfFn = noop 44 | } 45 | } 46 | 47 | func EnableDebugf(enable bool, prefix string) { 48 | if enable { 49 | debugfFn = stdout 50 | debugLogPrefix = prefix 51 | } else { 52 | debugfFn = noop 53 | debugLogPrefix = "" 54 | } 55 | } 56 | 57 | func Debug(tokens ...string) (int, error) { 58 | return debugfFn(debugLogPrefix + strings.Join(tokens, " ")) 59 | } 60 | 61 | func Debugln(tokens ...string) (int, error) { 62 | return debugfFn(debugLogPrefix + strings.Join(tokens, " ") + "\n") 63 | } 64 | 65 | func Debugf(format string, a ...interface{}) (int, error) { 66 | if debugLogPrefix != "" { 67 | return debugfFn(debugLogPrefix+format, a...) 68 | } else { 69 | return debugfFn(format, a...) 70 | } 71 | 72 | } 73 | 74 | func ExitWithErrorString(format string, a ...interface{}) error { 75 | return ExitWithError(fmt.Errorf(format, a...)) 76 | } 77 | 78 | func ExitWithError(err error) error { 79 | errorfFn("\n") 80 | if ei, ok := err.(*core.Error); ok { 81 | errorfFn("%s %s\n %s\n", 82 | ColorFnErrorHeader("Error:"), 83 | ColorFnErrorMessage(ei.Error()), 84 | ColorFnSideNote("(See: "+ei.ExtraInfo()+")")) 85 | } else { 86 | errorfFn("%s %s\n", 87 | ColorFnErrorHeader("Error:"), 88 | ColorFnErrorMessage(err.Error())) 89 | } 90 | errorfFn("\n") 91 | 92 | os.Exit(100) 93 | return nil 94 | } 95 | 96 | func Error(message string) { 97 | errorfFn("%s %s\n", 98 | ColorFnErrorHeader("Error:"), 99 | ColorFnErrorMessage(message)) 100 | } 101 | -------------------------------------------------------------------------------- /console/output.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/d5/cc" 7 | ) 8 | 9 | type colorFn func(s string, a ...interface{}) string 10 | 11 | func regularFn(s string, a ...interface{}) string { 12 | return fmt.Sprintf(s, a...) 13 | } 14 | 15 | func concat(fns ...colorFn) colorFn { 16 | return func(s string, a ...interface{}) string { 17 | out := fmt.Sprintf(s, a...) 18 | for _, fn := range fns { 19 | out = fn(out) 20 | } 21 | return out 22 | } 23 | } 24 | 25 | var ( 26 | ColorFnHelpLink = cc.Yellow 27 | 28 | ColorFnAskQuestionNote = cc.BlackH 29 | ColorFnAskQuestionMain = regularFn 30 | ColorFnAskQuestionDefaultValue = cc.YellowH 31 | 32 | ColorFnAskConfirmNote = cc.BlackH 33 | ColorFnAskConfirmMain = regularFn 34 | ColorFnAskConfirmDefaultAnswer = regularFn 35 | ColorFnAskConfirmAnswer = regularFn 36 | 37 | ColorFnInfoMessage = regularFn 38 | ColorFnDetailMessage = cc.BlackH 39 | ColorFnSideNote = cc.BlackH 40 | ColorFnSideNoteNegative = cc.Red 41 | 42 | ColorFnResource = cc.Green 43 | ColorFnResourceNegative = cc.Red 44 | 45 | ColorFnErrorHeader = cc.Red 46 | ColorFnErrorMessage = regularFn 47 | 48 | //ColorFnShellCommand = concat(cc.Bold, cc.YellowH) 49 | ColorFnShellCommand = cc.Cyan 50 | ColorFnShellOutput = cc.BlackH 51 | ColorFnShellError = cc.Red 52 | 53 | ColorFnMarkAdd = cc.Green 54 | ColorFnMarkRemove = cc.Red 55 | ColorFnMarkUpdate = cc.BlueH 56 | ColorFnMarkProcessing = cc.BlueH 57 | ColorFnMarkQuestion = cc.BlackH 58 | ColorFnMarkShell = regularFn 59 | ) 60 | 61 | var ( 62 | MarkAdd = "[+]" 63 | MarkRemove = "[-]" 64 | MarkUpdate = "[*]" 65 | MarkProcessing = "[*]" 66 | MarkQuestion = ">" 67 | MarkShell = ">" 68 | ) 69 | 70 | func Blank() { 71 | printfFn("\n") 72 | } 73 | 74 | func Info(message string) { 75 | printfFn("%s\n", ColorFnInfoMessage(message)) 76 | } 77 | 78 | func DetailWithResource(message, resourceName string) { 79 | //Println(" " + 80 | // ColorFnDetailMessage(message+" [") + 81 | // ColorFnResource(resourceName) + 82 | // ColorFnDetailMessage("]")) 83 | printfFn(" %s %s\n", ColorFnDetailMessage(message+":"), ColorFnResource(resourceName)) 84 | } 85 | 86 | func DetailWithResourceNote(message, resourceName, note string, negative bool) { 87 | sideNote := "" 88 | if note != "" { 89 | if negative { 90 | sideNote = ColorFnSideNoteNegative(note) 91 | } else { 92 | sideNote = ColorFnSideNote(note) 93 | } 94 | } 95 | 96 | //Printf(" %s%s%s %s\n", 97 | // ColorFnDetailMessage(message+" ["), 98 | // ColorFnResource(resourceName), 99 | // ColorFnDetailMessage("]"), 100 | // sideNote) 101 | printfFn(" %s %s %s\n", 102 | ColorFnDetailMessage(message+":"), 103 | ColorFnResource(resourceName), 104 | sideNote) 105 | } 106 | 107 | func AddingResource(message, resourceName string, mayTakeLong bool) { 108 | sideNote := "" 109 | if mayTakeLong { 110 | sideNote = ColorFnSideNote("(this may take long)") 111 | } 112 | 113 | printfFn("%s %s%s%s... %s\n", 114 | ColorFnMarkAdd(MarkAdd), 115 | ColorFnInfoMessage(message+" ["), 116 | ColorFnResource(resourceName), 117 | ColorFnInfoMessage("]"), 118 | sideNote) 119 | 120 | } 121 | 122 | func RemovingResource(message, resourceName string, mayTakeLong bool) { 123 | sideNote := "" 124 | if mayTakeLong { 125 | sideNote = ColorFnSideNote("(this may take long)") 126 | } 127 | 128 | printfFn("%s %s%s%s... %s\n", 129 | ColorFnMarkRemove(MarkRemove), 130 | ColorFnInfoMessage(message+" ["), 131 | ColorFnResourceNegative(resourceName), 132 | ColorFnInfoMessage("]"), 133 | sideNote) 134 | } 135 | 136 | func UpdatingResource(message, resourceName string, mayTakeLong bool) { 137 | sideNote := "" 138 | if mayTakeLong { 139 | sideNote = ColorFnSideNote("(this may take long)") 140 | } 141 | 142 | printfFn("%s %s%s%s... %s\n", 143 | ColorFnMarkUpdate(MarkUpdate), 144 | ColorFnInfoMessage(message+" ["), 145 | ColorFnResource(resourceName), 146 | ColorFnInfoMessage("]"), 147 | sideNote) 148 | } 149 | 150 | func ProcessingOnResource(message, resourceName string, mayTakeLong bool) { 151 | sideNote := "" 152 | if mayTakeLong { 153 | sideNote = ColorFnSideNote("(this may take long)") 154 | } 155 | 156 | printfFn("%s %s%s%s... %s\n", 157 | ColorFnMarkProcessing(MarkProcessing), 158 | ColorFnInfoMessage(message+" ["), 159 | ColorFnResource(resourceName), 160 | ColorFnInfoMessage("]"), 161 | sideNote) 162 | } 163 | 164 | func ShellCommand(message string) { 165 | printfFn("%s %s\n", 166 | ColorFnMarkShell(MarkShell), 167 | ColorFnShellCommand(message)) 168 | } 169 | 170 | func ShellOutput(message string) { 171 | printfFn("%s\n", ColorFnShellOutput(message)) 172 | } 173 | 174 | func ShellError(message string) { 175 | printfFn("%s\n", ColorFnShellError(message)) 176 | } 177 | -------------------------------------------------------------------------------- /core/apps.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "fmt" 7 | 8 | "github.com/coldbrewcloud/coldbrew-cli/utils" 9 | ) 10 | 11 | func DefaultECSTaskDefinitionName(appName string) string { 12 | return appName 13 | } 14 | 15 | func DefaultECSServiceName(appName string) string { 16 | return appName 17 | } 18 | 19 | func DefaultECSTaskMainContainerName(appName string) string { 20 | return appName 21 | } 22 | 23 | func DefaultAppName(appDirectoryOrConfigFile string) string { 24 | isDir, err := utils.IsDirectory(appDirectoryOrConfigFile) 25 | if err != nil { 26 | return "app1" 27 | } 28 | if !isDir { 29 | appDirectoryOrConfigFile = filepath.Dir(appDirectoryOrConfigFile) 30 | } 31 | 32 | base := filepath.Base(appDirectoryOrConfigFile) 33 | if base == "/" { 34 | return "app1" 35 | } 36 | 37 | // validation check 38 | if !AppNameRE.MatchString(base) { 39 | // TODO: probably better to strip unacceptable characters instead of "app1" 40 | return "app1" 41 | } 42 | 43 | return base 44 | } 45 | 46 | func DefaultELBLoadBalancerName(appName string) string { 47 | return fmt.Sprintf("%s-elb", appName) 48 | } 49 | 50 | func DefaultELBTargetGroupName(appName string) string { 51 | return fmt.Sprintf("%s-elb-tg", appName) 52 | } 53 | 54 | func DefaultELBLoadBalancerSecurityGroupName(appName string) string { 55 | return fmt.Sprintf("%s-elb-sg", appName) 56 | } 57 | 58 | func DefaultECRRepository(appName string) string { 59 | return fmt.Sprintf("coldbrew/%s", appName) 60 | } 61 | 62 | func DefaultCloudWatchLogsGroupName(appName, clusterName string) string { 63 | return fmt.Sprintf("coldbrew-%s-%s", clusterName, appName) 64 | } 65 | -------------------------------------------------------------------------------- /core/aws.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | const ( 9 | AWSTagNameResourceName = "Name" 10 | AWSTagNameCreatedTimestamp = "coldbrew_cli_created" 11 | ) 12 | 13 | func DefaultTagsForAWSResources(resourceName string) map[string]string { 14 | return map[string]string{ 15 | AWSTagNameResourceName: resourceName, 16 | AWSTagNameCreatedTimestamp: fmt.Sprintf("%d", time.Now().Unix()), 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /core/clusters.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "fmt" 4 | 5 | const defaultPrefix = "coldbrew-" 6 | 7 | const ( 8 | EC2AssumeRolePolicy = `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action": "sts:AssumeRole"}]}` 9 | ECSAssumeRolePolicy = `{"Version":"2008-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ecs.amazonaws.com"},"Action": "sts:AssumeRole"}]}` 10 | 11 | AdministratorAccessPolicyARN = "arn:aws:iam::aws:policy/AdministratorAccess" 12 | ECSServiceRolePolicyARN = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceRole" 13 | ) 14 | 15 | func DefaultECSClusterName(clusterName string) string { 16 | return fmt.Sprintf("%s%s", defaultPrefix, clusterName) 17 | } 18 | 19 | func DefaultLaunchConfigurationName(clusterName string) string { 20 | return fmt.Sprintf("%s%s-lc", defaultPrefix, clusterName) 21 | } 22 | 23 | func DefaultAutoScalingGroupName(clusterName string) string { 24 | return fmt.Sprintf("%s%s-asg", defaultPrefix, clusterName) 25 | } 26 | 27 | func DefaultInstanceProfileName(clusterName string) string { 28 | return fmt.Sprintf("%s%s-instance-profile", defaultPrefix, clusterName) 29 | } 30 | 31 | func DefaultInstanceSecurityGroupName(clusterName string) string { 32 | return fmt.Sprintf("%s%s-instance-sg", defaultPrefix, clusterName) 33 | } 34 | 35 | func DefaultECSServiceRoleName(clusterName string) string { 36 | return fmt.Sprintf("%s%s-ecs-service-role", defaultPrefix, clusterName) 37 | } 38 | 39 | func DefaultContainerInstanceType() string { 40 | return "t2.micro" 41 | } 42 | -------------------------------------------------------------------------------- /core/errors.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "fmt" 4 | 5 | type Error struct { 6 | originalError error 7 | extraInfo string 8 | } 9 | 10 | func NewError(format string, a ...interface{}) *Error { 11 | return &Error{ 12 | originalError: fmt.Errorf(format, a...), 13 | extraInfo: "", 14 | } 15 | } 16 | 17 | func NewErrorExtraInfo(originalError error, extraInfo string) *Error { 18 | return &Error{ 19 | originalError: originalError, 20 | extraInfo: extraInfo, 21 | } 22 | } 23 | 24 | func (e *Error) Error() string { 25 | return e.originalError.Error() 26 | } 27 | 28 | func (e *Error) ExtraInfo() string { 29 | return e.extraInfo 30 | } 31 | 32 | func (e *Error) OriginalError() error { 33 | return e.originalError 34 | } 35 | -------------------------------------------------------------------------------- /core/validation.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | MaxAppUnits = uint16(1000) 12 | MaxAppCPU = float64(1024 * 16) 13 | MaxAppMemoryInMB = uint64(1024 * 16) 14 | ) 15 | 16 | var ( 17 | AppNameRE = regexp.MustCompile(`^[\w\-]{1,32}$`) 18 | ClusterNameRE = regexp.MustCompile(`^[\w\-]{1,32}$`) 19 | ELBNameRE = regexp.MustCompile(`^(?:[a-zA-Z0-9][a-zA-Z0-9\-]{0,30})?[a-zA-Z0-9]$`) 20 | ELBTargetGroupNameRE = regexp.MustCompile(`^(?:[a-zA-Z0-9][a-zA-Z0-9\-]{0,30})?[a-zA-Z0-9]$`) 21 | ELBSecurityGroupNameRE = regexp.MustCompile(`^(?:[a-zA-Z0-9][a-zA-Z0-9\-]{0,30})?[a-zA-Z0-9]$`) 22 | ECRRepoNameRE = regexp.MustCompile(`^.{1,256}$`) // TODO: need better matcher 23 | HealthCheckPathRE = regexp.MustCompile(`^.+$`) // TODO: need better matcher 24 | HealthCheckStatusRE = regexp.MustCompile(`^\d{3}-\d{3}$|^\d{3}(?:,\d{3})*$`) // "200", "200-299", "200,204,201" 25 | DockerImageURIRE = regexp.MustCompile(`^([^:]+)(?::([^:]+))?$`) 26 | 27 | SizeExpressionRE = regexp.MustCompile(`^(\d+)(?:([kmgtKMGT])([bB])?)?$`) 28 | TimeExpressionRE = regexp.MustCompile(`^(\d+)([smhSMH])?$`) 29 | ) 30 | 31 | func ParseSizeExpression(expression string) (uint64, error) { 32 | m := SizeExpressionRE.FindAllStringSubmatch(expression, -1) 33 | if len(m) != 1 || len(m[0]) < 2 { 34 | return 0, fmt.Errorf("Invalid size expression [%s]", expression) 35 | } 36 | 37 | multiplier := uint64(1) 38 | switch strings.ToLower(m[0][2]) { 39 | case "k": 40 | multiplier = uint64(1000) 41 | case "m": 42 | multiplier = uint64(1000 * 1000) 43 | case "g": 44 | multiplier = uint64(1000 * 1000 * 1000) 45 | case "t": 46 | multiplier = uint64(1000 * 1000 * 1000 * 1000) 47 | } 48 | 49 | parsed, err := strconv.ParseUint(m[0][1], 10, 64) 50 | if err != nil { 51 | return 0, fmt.Errorf("Invalid size expression [%s]: %s", expression, err.Error()) 52 | } 53 | 54 | return parsed * multiplier, nil 55 | } 56 | 57 | func ParseTimeExpression(expression string) (uint64, error) { 58 | m := TimeExpressionRE.FindAllStringSubmatch(expression, -1) 59 | if len(m) != 1 || len(m[0]) < 1 { 60 | return 0, fmt.Errorf("Invalid time expression [%s]", expression) 61 | } 62 | 63 | multiplier := uint64(1) 64 | switch strings.ToLower(m[0][2]) { 65 | case "m": 66 | multiplier = uint64(60) 67 | case "h": 68 | multiplier = uint64(60 * 60) 69 | } 70 | 71 | parsed, err := strconv.ParseUint(m[0][1], 10, 64) 72 | if err != nil { 73 | return 0, fmt.Errorf("Invalid time expression [%s]: %s", expression, err.Error()) 74 | } 75 | 76 | return parsed * multiplier, nil 77 | } 78 | -------------------------------------------------------------------------------- /docker/client.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/coldbrewcloud/coldbrew-cli/console" 7 | "github.com/coldbrewcloud/coldbrew-cli/exec" 8 | ) 9 | 10 | type Client struct { 11 | dockerBin string 12 | outputIndent string 13 | } 14 | 15 | func NewClient(dockerBin string) *Client { 16 | return &Client{ 17 | dockerBin: dockerBin, 18 | outputIndent: " ", 19 | } 20 | } 21 | 22 | func (c *Client) DockerBinAvailable() bool { 23 | _, _, _, err := exec.Exec(c.dockerBin, "version") 24 | return err == nil 25 | } 26 | 27 | func (c *Client) PrintVersion() error { 28 | return c.exec(c.dockerBin, "--version") 29 | } 30 | 31 | func (c *Client) BuildImage(buildPath, dockerfilePath, image string) error { 32 | return c.exec(c.dockerBin, "build", "-t", image, "-f", dockerfilePath, buildPath) 33 | } 34 | 35 | func (c *Client) Login(userName, password, proxyURL string) error { 36 | // NOTE: use slightly different implementation to hide password in output 37 | //return c.exec(c.dockerBin, "login", "-u", userName, "-p", password, proxyURL) 38 | 39 | console.Blank() 40 | console.ShellCommand(c.dockerBin + " login -u " + userName + " -p ****** " + proxyURL) 41 | 42 | stdout, stderr, exit, err := exec.Exec(c.dockerBin, "login", "-u", userName, "-p", password, proxyURL) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | for { 48 | select { 49 | case line := <-stdout: 50 | console.ShellOutput(line) 51 | case line := <-stderr: 52 | console.ShellError(line) 53 | case exitErr := <-exit: 54 | console.Blank() 55 | return exitErr 56 | } 57 | } 58 | 59 | return nil 60 | } 61 | 62 | func (c *Client) PushImage(image string) error { 63 | return c.exec(c.dockerBin, "push", image) 64 | } 65 | 66 | func (c *Client) TagImage(src, dest string) error { 67 | return c.exec(c.dockerBin, "tag", src, dest) 68 | } 69 | 70 | func (c *Client) exec(name string, args ...string) error { 71 | console.Blank() 72 | console.ShellCommand(name + " " + strings.Join(args, " ")) 73 | 74 | stdout, stderr, exit, err := exec.Exec(name, args...) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | for { 80 | select { 81 | case line := <-stdout: 82 | console.ShellOutput(line) 83 | case line := <-stderr: 84 | console.ShellError(line) 85 | case exitErr := <-exit: 86 | console.Blank() 87 | return exitErr 88 | } 89 | } 90 | 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /exec/exec.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "os/exec" 7 | ) 8 | 9 | type ExecCallback func(stdout, stderr *string, exitError *exec.ExitError, err error) 10 | 11 | func Exec(name string, args ...string) (stdout chan string, stderr chan string, exit chan error, err error) { 12 | if name == "" { 13 | return nil, nil, nil, errors.New("name is empty") 14 | } 15 | 16 | stdout = make(chan string) 17 | stderr = make(chan string) 18 | exit = make(chan error) 19 | 20 | cmd := exec.Command(name, args...) 21 | 22 | // redirect std out 23 | stdoutPipe, err := cmd.StdoutPipe() 24 | if err != nil { 25 | return nil, nil, nil, err 26 | } 27 | go func() { 28 | scanner := bufio.NewScanner(stdoutPipe) 29 | for scanner.Scan() { 30 | line := scanner.Text() 31 | stdout <- line 32 | } 33 | if err := scanner.Err(); err != nil { 34 | // ignored 35 | } 36 | }() 37 | 38 | // redirect std err 39 | stderrPipe, err := cmd.StderrPipe() 40 | if err != nil { 41 | return nil, nil, nil, err 42 | } 43 | go func() { 44 | scanner := bufio.NewScanner(stderrPipe) 45 | for scanner.Scan() { 46 | line := scanner.Text() 47 | stderr <- line 48 | } 49 | if err := scanner.Err(); err != nil { 50 | // ignored 51 | } 52 | }() 53 | 54 | // start command 55 | if err := cmd.Start(); err != nil { 56 | return nil, nil, nil, err 57 | } 58 | 59 | // wait until it exits 60 | go func() { 61 | exit <- cmd.Wait() 62 | }() 63 | 64 | return 65 | } 66 | -------------------------------------------------------------------------------- /flags/flags_test.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "gopkg.in/alecthomas/kingpin.v2" 10 | ) 11 | 12 | type nullWriter struct{} 13 | 14 | func (w *nullWriter) Write(p []byte) (int, error) { 15 | return 0, nil 16 | } 17 | 18 | func testSptr(s string) *string { 19 | p := s 20 | return &p 21 | } 22 | 23 | func testU16ptr(u uint16) *uint16 { 24 | p := u 25 | return &p 26 | } 27 | 28 | func testU64ptr(u uint64) *uint64 { 29 | p := u 30 | return &p 31 | } 32 | 33 | func testBptr(b bool) *bool { 34 | p := b 35 | return &p 36 | } 37 | 38 | func testBytePtr(b byte) *byte { 39 | p := b 40 | return &p 41 | } 42 | 43 | func testArgs(command *string, args ...string) []string { 44 | newArgs := []string{} 45 | if command != nil { 46 | newArgs = append(newArgs, *command) 47 | } 48 | return append(newArgs, args...) 49 | } 50 | 51 | func testUint16Flag(t *testing.T, app *kingpin.Application, target **uint16, command *string, flag string, short *byte, defaultValue *uint16, envVar *string) { 52 | var err error 53 | 54 | if envVar != nil { 55 | os.Setenv(*envVar, "") 56 | } 57 | 58 | if defaultValue != nil { 59 | _, err = app.Parse(testArgs(command)) // default 60 | assert.Nil(t, err) 61 | assert.NotNil(t, *target) 62 | assert.Equal(t, *defaultValue, **target) 63 | } 64 | 65 | _, err = app.Parse(testArgs(command, "--"+flag+"=10")) // set by param (long) 66 | assert.Nil(t, err) 67 | assert.NotNil(t, *target) 68 | assert.Equal(t, uint16(10), **target) 69 | 70 | _, err = app.Parse(testArgs(command, "--"+flag, "20")) // set by param (long) 71 | assert.Nil(t, err) 72 | assert.NotNil(t, *target) 73 | assert.Equal(t, uint16(20), **target) 74 | 75 | if short != nil { 76 | _, err = app.Parse(testArgs(command, fmt.Sprintf("-%c=30", *short))) // set by param (short) 77 | assert.Nil(t, err) 78 | assert.NotNil(t, *target) 79 | assert.Equal(t, uint16(30), **target) 80 | 81 | _, err = app.Parse(testArgs(command, fmt.Sprintf("-%c", *short), "40")) // set by param (short) 82 | assert.Nil(t, err) 83 | assert.NotNil(t, *target) 84 | assert.Equal(t, uint16(40), **target) 85 | } 86 | 87 | if envVar != nil { 88 | os.Setenv(*envVar, "50") 89 | _, err = app.Parse(testArgs(command)) // set by env var 90 | assert.Nil(t, err) 91 | assert.NotNil(t, *target) 92 | assert.Equal(t, uint16(50), **target) 93 | 94 | os.Setenv(*envVar, "60") 95 | _, err = app.Parse(testArgs(command, "--"+flag+"=70")) // param overrides env var 96 | assert.Nil(t, err) 97 | assert.NotNil(t, *target) 98 | assert.Equal(t, uint16(70), **target) 99 | } 100 | 101 | _, err = app.Parse(testArgs(command, "--"+flag+"=")) // invalid 102 | assert.NotNil(t, err) 103 | _, err = app.Parse(testArgs(command, "--"+flag)) // invalid 104 | assert.NotNil(t, err) 105 | _, err = app.Parse(testArgs(command, "--"+flag+" 80")) // invalid 106 | assert.NotNil(t, err) 107 | } 108 | 109 | func testUint64Flag(t *testing.T, app *kingpin.Application, target **uint64, command *string, flag string, short *byte, defaultValue *uint64, envVar *string) { 110 | var err error 111 | 112 | if envVar != nil { 113 | os.Setenv(*envVar, "") 114 | } 115 | 116 | if defaultValue != nil { 117 | _, err = app.Parse(testArgs(command)) // default 118 | assert.Nil(t, err) 119 | assert.NotNil(t, *target) 120 | assert.Equal(t, *defaultValue, **target) 121 | } 122 | 123 | _, err = app.Parse(testArgs(command, "--"+flag+"=10")) // set by param (long) 124 | assert.Nil(t, err) 125 | assert.NotNil(t, *target) 126 | assert.Equal(t, uint64(10), **target) 127 | 128 | _, err = app.Parse(testArgs(command, "--"+flag, "20")) // set by param (long) 129 | assert.Nil(t, err) 130 | assert.NotNil(t, *target) 131 | assert.Equal(t, uint64(20), **target) 132 | 133 | if short != nil { 134 | _, err = app.Parse(testArgs(command, fmt.Sprintf("-%c=30", *short))) // set by param (short) 135 | assert.Nil(t, err) 136 | assert.NotNil(t, *target) 137 | assert.Equal(t, uint64(30), **target) 138 | 139 | _, err = app.Parse(testArgs(command, fmt.Sprintf("-%c", *short), "40")) // set by param (short) 140 | assert.Nil(t, err) 141 | assert.NotNil(t, *target) 142 | assert.Equal(t, uint64(40), **target) 143 | } 144 | 145 | if envVar != nil { 146 | os.Setenv(*envVar, "50") 147 | _, err = app.Parse(testArgs(command)) // set by env var 148 | assert.Nil(t, err) 149 | assert.NotNil(t, *target) 150 | assert.Equal(t, uint64(50), **target) 151 | 152 | os.Setenv(*envVar, "60") 153 | _, err = app.Parse(testArgs(command, "--"+flag+"=70")) // param overrides env var 154 | assert.Nil(t, err) 155 | assert.NotNil(t, *target) 156 | assert.Equal(t, uint64(70), **target) 157 | } 158 | 159 | _, err = app.Parse(testArgs(command, "--"+flag+"=")) // invalid 160 | assert.NotNil(t, err) 161 | _, err = app.Parse(testArgs(command, "--"+flag)) // invalid 162 | assert.NotNil(t, err) 163 | _, err = app.Parse(testArgs(command, "--"+flag+" 80")) // invalid 164 | assert.NotNil(t, err) 165 | } 166 | 167 | func testStringFlag(t *testing.T, app *kingpin.Application, target **string, command *string, flag string, short *byte, defaultValue *string, envVar *string) { 168 | var err error 169 | 170 | if envVar != nil { 171 | os.Setenv(*envVar, "") 172 | } 173 | 174 | if defaultValue != nil { 175 | _, err = app.Parse(testArgs(command)) // default 176 | assert.Nil(t, err) 177 | assert.NotNil(t, *target) 178 | assert.Equal(t, *defaultValue, **target) 179 | } 180 | 181 | _, err = app.Parse(testArgs(command, "--"+flag+"=value1")) // set by param (long) 182 | assert.Nil(t, err) 183 | assert.NotNil(t, *target) 184 | assert.Equal(t, "value1", **target) 185 | 186 | _, err = app.Parse(testArgs(command, "--"+flag, "value2")) // set by param (long) 187 | assert.Nil(t, err) 188 | assert.NotNil(t, *target) 189 | assert.Equal(t, "value2", **target) 190 | 191 | if short != nil { 192 | _, err = app.Parse(testArgs(command, fmt.Sprintf("-%c", *short), "value4")) // set by param (short) 193 | assert.Nil(t, err) 194 | assert.NotNil(t, *target) 195 | assert.Equal(t, "value4", **target) 196 | } 197 | 198 | if envVar != nil { 199 | os.Setenv(*envVar, "value5") 200 | _, err = app.Parse(testArgs(command)) // set by env var 201 | assert.Nil(t, err) 202 | assert.NotNil(t, *target) 203 | assert.Equal(t, "value5", **target) 204 | 205 | os.Setenv(*envVar, "value6") 206 | _, err = app.Parse(testArgs(command, "--"+flag+"=value7")) // param overrides env var 207 | assert.Nil(t, err) 208 | assert.NotNil(t, *target) 209 | assert.Equal(t, "value7", **target) 210 | } 211 | 212 | _, err = app.Parse(testArgs(command, "--"+flag+"=")) // empty (valid but should be handled by app) 213 | assert.Nil(t, err) 214 | assert.NotNil(t, *target) 215 | assert.Equal(t, "", **target) 216 | 217 | _, err = app.Parse(testArgs(command, "--"+flag)) // invalid 218 | assert.NotNil(t, err) 219 | _, err = app.Parse(testArgs(command, "--"+flag+" ver3")) // invalid 220 | assert.NotNil(t, err) 221 | } 222 | 223 | func testBoolFlag(t *testing.T, app *kingpin.Application, target **bool, command *string, flag string, short *byte, defaultValue *bool) { 224 | var err error 225 | 226 | if defaultValue != nil { 227 | _, err = app.Parse(testArgs(command)) // default 228 | assert.Nil(t, err) 229 | assert.NotNil(t, *target) 230 | assert.Equal(t, *defaultValue, **target) 231 | } 232 | 233 | _, err = app.Parse(testArgs(command, "--"+flag)) // set by param (long) 234 | assert.Nil(t, err) 235 | assert.NotNil(t, *target) 236 | assert.Equal(t, true, **target) 237 | 238 | if short != nil { 239 | _, err = app.Parse(testArgs(command, fmt.Sprintf("-%c", *short))) // set by param (short) 240 | assert.Nil(t, err) 241 | assert.NotNil(t, *target) 242 | assert.Equal(t, true, **target) 243 | } 244 | 245 | // disable 246 | _, err = app.Parse(testArgs(command, "--no-"+flag)) // set by param (long) 247 | assert.Nil(t, err) 248 | assert.NotNil(t, *target) 249 | assert.Equal(t, false, **target) 250 | 251 | _, err = app.Parse(testArgs(command, "--"+flag+"=")) // invalid 252 | assert.NotNil(t, err) 253 | _, err = app.Parse(testArgs(command, "--"+flag+" true")) // invalid 254 | assert.NotNil(t, err) 255 | } 256 | -------------------------------------------------------------------------------- /flags/global_flags.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "path/filepath" 7 | 8 | "github.com/coldbrewcloud/coldbrew-cli/aws" 9 | "github.com/coldbrewcloud/coldbrew-cli/utils" 10 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 11 | "gopkg.in/alecthomas/kingpin.v2" 12 | ) 13 | 14 | const ( 15 | GlobalFlagsConfigFileFormatJSON = "json" 16 | GlobalFlagsConfigFileFormatYAML = "yaml" 17 | ) 18 | 19 | type GlobalFlags struct { 20 | AppDirectory *string `json:"app-dir,omitempty"` 21 | ConfigFile *string `json:"config,omitempty"` 22 | ConfigFileFormat *string `json:"config-format,omitempty"` 23 | DisableColoring *bool `json:"disable-color,omitempty"` 24 | Verbose *bool `json:"verbose,omitempty"` 25 | AWSAccessKey *string `json:"aws-access-key,omitempty"` 26 | AWSSecretKey *string `json:"aws-secret-key,omitempty"` 27 | AWSRegion *string `json:"aws-region,omitempty"` 28 | AWSVPC *string `json:"aws-vpc,omitempty"` 29 | } 30 | 31 | func NewGlobalFlags(ka *kingpin.Application) *GlobalFlags { 32 | return &GlobalFlags{ 33 | AppDirectory: ka.Flag("app-dir", "Application directory").Short('D').Default(".").String(), 34 | ConfigFile: ka.Flag("config", "Configuration file path").Short('C').Default("").String(), 35 | ConfigFileFormat: ka.Flag("config-format", "Configuraiton file format (JSON/YAML)").Default(GlobalFlagsConfigFileFormatYAML).String(), 36 | DisableColoring: ka.Flag("disable-color", "Disable colored outputs").Bool(), 37 | Verbose: ka.Flag("verbose", "Enable verbose logging").Short('V').Default("false").Bool(), 38 | AWSAccessKey: ka.Flag("aws-access-key", "AWS Access Key ID ($AWS_ACCESS_KEY_ID)").Envar("AWS_ACCESS_KEY_ID").Default("").String(), 39 | AWSSecretKey: ka.Flag("aws-secret-key", "AWS Secret Access Key ($AWS_SECRET_ACCESS_KEY)").Envar("AWS_SECRET_ACCESS_KEY").Default("").String(), 40 | AWSRegion: ka.Flag("aws-region", "AWS region name ($AWS_REGION)").Envar("AWS_REGION").Default("us-west-2").String(), 41 | AWSVPC: ka.Flag("aws-vpc", "AWS VPC ID ($AWS_VPC)").Envar("AWS_VPC").Default("").String(), 42 | } 43 | } 44 | 45 | // GetApplicationDirectory returns an absolute path of the application directory. 46 | func (gf *GlobalFlags) GetApplicationDirectory() (string, error) { 47 | appDir := conv.S(gf.AppDirectory) 48 | if utils.IsBlank(appDir) { 49 | appDir = "." // default: current working directory 50 | } 51 | 52 | // resolve to absolute path 53 | absPath, err := filepath.Abs(appDir) 54 | if err != nil { 55 | return "", fmt.Errorf("Error retrieving absolute path [%s]: %s", appDir, err.Error()) 56 | } 57 | 58 | return absPath, nil 59 | } 60 | 61 | // GetConfigFile returns an absolute path of the configuration file. 62 | func (gf *GlobalFlags) GetConfigFile() (string, error) { 63 | configFile := conv.S(gf.ConfigFile) 64 | 65 | // if specified config file is absolute path, just use it 66 | if !utils.IsBlank(configFile) && filepath.IsAbs(configFile) { 67 | return configFile, nil 68 | } 69 | 70 | if utils.IsBlank(configFile) { 71 | configFile = "./coldbrew.conf" // default: coldbrew.conf 72 | } 73 | 74 | // join with application directory 75 | appDir, err := gf.GetApplicationDirectory() 76 | if err != nil { 77 | return "", err 78 | } 79 | 80 | return filepath.Join(appDir, configFile), nil 81 | } 82 | 83 | func (gf *GlobalFlags) GetAWSClient() *aws.Client { 84 | return aws.NewClient(conv.S(gf.AWSRegion), conv.S(gf.AWSAccessKey), conv.S(gf.AWSSecretKey)) 85 | } 86 | 87 | func (gf *GlobalFlags) GetAWSRegionAndVPCID() (string, string, error) { 88 | if utils.IsBlank(conv.S(gf.AWSRegion)) { 89 | return "", "", errors.New("AWS region cannot be blank.") 90 | } 91 | 92 | awsClient := gf.GetAWSClient() 93 | 94 | // VPC ID explicitly specified: make sure it's really there 95 | if !utils.IsBlank(conv.S(gf.AWSVPC)) { 96 | vpc, err := awsClient.EC2().RetrieveVPC(conv.S(gf.AWSVPC)) 97 | if err != nil { 98 | return "", "", fmt.Errorf("Failed to retrieve VPC [%s]: %s", conv.S(gf.AWSVPC), err.Error()) 99 | } 100 | if vpc == nil { 101 | return "", "", fmt.Errorf("VPC [%s] was not found.", conv.S(gf.AWSVPC)) 102 | } 103 | return conv.S(gf.AWSRegion), conv.S(gf.AWSVPC), nil 104 | } 105 | 106 | // if VPC is not specified, try to find account default VPC 107 | defaultVPC, err := awsClient.EC2().RetrieveDefaultVPC() 108 | if err != nil { 109 | return "", "", fmt.Errorf("Failed to retrieve default VPC: %s", err.Error()) 110 | } 111 | if defaultVPC == nil { 112 | return "", "", errors.New("Your AWS account does not have default VPC. You must explicitly specify VPC ID using --aws-vpc flag.") 113 | } 114 | return conv.S(gf.AWSRegion), conv.S(defaultVPC.VpcId), nil 115 | } 116 | -------------------------------------------------------------------------------- /flags/global_flags_test.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "testing" 5 | 6 | "gopkg.in/alecthomas/kingpin.v2" 7 | ) 8 | 9 | func TestNewGlobalFlags(t *testing.T) { 10 | app := kingpin.New("app", "") 11 | app.Writer(&nullWriter{}) 12 | gf := NewGlobalFlags(app) 13 | 14 | testStringFlag(t, app, &gf.ConfigFile, nil, "config", testBytePtr('C'), nil, nil) 15 | testStringFlag(t, app, &gf.ConfigFileFormat, nil, "config-format", nil, testSptr(GlobalFlagsConfigFileFormatYAML), nil) 16 | testStringFlag(t, app, &gf.AppDirectory, nil, "app-dir", testBytePtr('D'), testSptr("."), nil) 17 | testBoolFlag(t, app, &gf.Verbose, nil, "verbose", testBytePtr('V'), testBptr(false)) 18 | testStringFlag(t, app, &gf.AWSAccessKey, nil, "aws-access-key", nil, nil, testSptr("AWS_ACCESS_KEY_ID")) 19 | testStringFlag(t, app, &gf.AWSSecretKey, nil, "aws-secret-key", nil, nil, testSptr("AWS_SECRET_ACCESS_KEY")) 20 | testStringFlag(t, app, &gf.AWSRegion, nil, "aws-region", nil, testSptr("us-west-2"), testSptr("AWS_REGION")) 21 | testStringFlag(t, app, &gf.AWSVPC, nil, "aws-vpc", nil, nil, testSptr("AWS_VPC")) 22 | } 23 | 24 | func TestGlobalFlags_ResolveAppDirectory(t *testing.T) { 25 | // TODO: implement 26 | } 27 | 28 | func TestGlobalFlags_ResolveConfigFile(t *testing.T) { 29 | // TODO: implement 30 | } 31 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: 002c97ae413cac5ad4320b20f166b0e5084e26e07ade3d516d4ea5be576b0cc3 2 | updated: 2018-03-18T16:08:47.351643-07:00 3 | imports: 4 | - name: github.com/alecthomas/template 5 | version: a0175ee3bccc567396460bf5acd36800cb10c49c 6 | subpackages: 7 | - parse 8 | - name: github.com/alecthomas/units 9 | version: 2efee857e7cfd4f3d0138cc3cbb1b4966962b93a 10 | - name: github.com/aws/aws-sdk-go 11 | version: 1b2abe886743dc2bcc78472bfd30a15dc0a61fb8 12 | subpackages: 13 | - aws 14 | - aws/awserr 15 | - aws/awsutil 16 | - aws/client 17 | - aws/client/metadata 18 | - aws/corehandlers 19 | - aws/credentials 20 | - aws/credentials/ec2rolecreds 21 | - aws/credentials/endpointcreds 22 | - aws/credentials/stscreds 23 | - aws/defaults 24 | - aws/ec2metadata 25 | - aws/request 26 | - aws/session 27 | - aws/signer/v4 28 | - private/endpoints 29 | - private/protocol 30 | - private/protocol/ec2query 31 | - private/protocol/json/jsonutil 32 | - private/protocol/jsonrpc 33 | - private/protocol/query 34 | - private/protocol/query/queryutil 35 | - private/protocol/rest 36 | - private/protocol/xml/xmlutil 37 | - private/waiter 38 | - service/autoscaling 39 | - service/cloudwatchlogs 40 | - service/ec2 41 | - service/ecr 42 | - service/ecs 43 | - service/elbv2 44 | - service/iam 45 | - service/sns 46 | - service/sts 47 | - name: github.com/d5/cc 48 | version: 61e59598c69a49fd4d901b6d5cf946e67d649349 49 | - name: github.com/go-ini/ini 50 | version: 6ecc596bd756a16c6e93e4ef9225ecdb9f54ed2c 51 | - name: github.com/jmespath/go-jmespath 52 | version: c2b33e8439af944379acbdd9c3a5fe0bc44bd8a5 53 | - name: github.com/mattn/go-colorable 54 | version: efa589957cd060542a26d2dd7832fd6a6c6c3ade 55 | - name: github.com/mattn/go-isatty 56 | version: 6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c 57 | - name: github.com/stretchr/testify 58 | version: d77da356e56a7428ad25149ca77381849a6a5232 59 | subpackages: 60 | - assert 61 | - name: golang.org/x/sys 62 | version: 01acb38716e021ed1fc03a602bdb5838e1358c5e 63 | subpackages: 64 | - unix 65 | - name: gopkg.in/alecthomas/kingpin.v2 66 | version: 8cccfa8eb2e3183254457fb1749b2667fbc364c7 67 | - name: gopkg.in/yaml.v2 68 | version: a5b47d31c556af34a302ce5d659e6fea44d90de0 69 | testImports: 70 | - name: github.com/davecgh/go-spew 71 | version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d 72 | subpackages: 73 | - spew 74 | - name: github.com/pmezard/go-difflib 75 | version: d8ed2627bdf02c080bf22230dbb337003b7aba2d 76 | subpackages: 77 | - difflib 78 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/coldbrewcloud/coldbrew-cli 2 | import: 3 | - package: github.com/stretchr/testify 4 | version: d77da356e56a7428ad25149ca77381849a6a5232 5 | - package: gopkg.in/alecthomas/kingpin.v2 6 | version: 8cccfa8eb2e3183254457fb1749b2667fbc364c7 7 | - package: github.com/d5/cc 8 | version: 61e59598c69a49fd4d901b6d5cf946e67d649349 9 | - package: gopkg.in/yaml.v2 10 | version: a5b47d31c556af34a302ce5d659e6fea44d90de0 11 | - package: github.com/aws/aws-sdk-go 12 | version: 1b2abe886743dc2bcc78472bfd30a15dc0a61fb8 13 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/coldbrewcloud/coldbrew-cli/commands" 8 | "github.com/coldbrewcloud/coldbrew-cli/commands/clustercreate" 9 | "github.com/coldbrewcloud/coldbrew-cli/commands/clusterdelete" 10 | "github.com/coldbrewcloud/coldbrew-cli/commands/clusterscale" 11 | "github.com/coldbrewcloud/coldbrew-cli/commands/clusterstatus" 12 | "github.com/coldbrewcloud/coldbrew-cli/commands/create" 13 | "github.com/coldbrewcloud/coldbrew-cli/commands/delete" 14 | "github.com/coldbrewcloud/coldbrew-cli/commands/deploy" 15 | "github.com/coldbrewcloud/coldbrew-cli/commands/status" 16 | "github.com/coldbrewcloud/coldbrew-cli/console" 17 | "github.com/coldbrewcloud/coldbrew-cli/flags" 18 | "github.com/coldbrewcloud/coldbrew-cli/utils/conv" 19 | "github.com/d5/cc" 20 | "gopkg.in/alecthomas/kingpin.v2" 21 | ) 22 | 23 | var ( 24 | appName = "coldbrew" 25 | appHelp = "See: " + console.ColorFnHelpLink("https://github.com/coldbrewcloud/coldbrew-cli/wiki/CLI-Global-Flags") 26 | appVersion = "" 27 | ) 28 | 29 | type CLIApp struct { 30 | kingpinApp *kingpin.Application 31 | globalFlags *flags.GlobalFlags 32 | commands map[string]commands.Command 33 | } 34 | 35 | func main() { 36 | kingpinApp := kingpin.New(appName, appHelp) 37 | kingpinApp.Version(appVersion) 38 | globalFlags := flags.NewGlobalFlags(kingpinApp) 39 | 40 | // register commands 41 | registeredCommands := registerCommands(kingpinApp, globalFlags) 42 | 43 | // parse CLI inputs 44 | command, err := kingpinApp.Parse(os.Args[1:]) 45 | if err != nil { 46 | console.Error(err.Error()) 47 | os.Exit(5) 48 | } 49 | 50 | // setup logging 51 | console.EnableDebugf(*globalFlags.Verbose, "") 52 | if conv.B(globalFlags.DisableColoring) { 53 | cc.Disable() 54 | } 55 | 56 | // execute command 57 | if c := registeredCommands[command]; c != nil { 58 | if err := c.Run(); err != nil { 59 | console.Error(err.Error()) 60 | os.Exit(40) 61 | } 62 | os.Exit(0) 63 | } else { 64 | panic(fmt.Errorf("Unknown command: %s", command)) 65 | } 66 | } 67 | 68 | func registerCommands(ka *kingpin.Application, globalFlags *flags.GlobalFlags) map[string]commands.Command { 69 | registeredCommands := make(map[string]commands.Command) 70 | 71 | cmds := []commands.Command{ 72 | &create.Command{}, 73 | &deploy.Command{}, 74 | &status.Command{}, 75 | &delete.Command{}, 76 | &clustercreate.Command{}, 77 | &clusterstatus.Command{}, 78 | &clusterscale.Command{}, 79 | &clusterdelete.Command{}, 80 | } 81 | for _, c := range cmds { 82 | kpc := c.Init(ka, globalFlags) 83 | registeredCommands[kpc.FullCommand()] = c 84 | } 85 | 86 | return registeredCommands 87 | } 88 | -------------------------------------------------------------------------------- /utils/conv/conv.go: -------------------------------------------------------------------------------- 1 | package conv 2 | 3 | import "strconv" 4 | 5 | func SP(v string) *string { 6 | p := v 7 | return &p 8 | } 9 | 10 | func S(p *string) string { 11 | if p == nil { 12 | return "" 13 | } 14 | return *p 15 | } 16 | 17 | func U16P(v uint16) *uint16 { 18 | p := v 19 | return &p 20 | } 21 | 22 | func U16(p *uint16) uint16 { 23 | if p == nil { 24 | return 0 25 | } 26 | return *p 27 | } 28 | 29 | func U64P(v uint64) *uint64 { 30 | p := v 31 | return &p 32 | } 33 | 34 | func U64(p *uint64) uint64 { 35 | if p == nil { 36 | return 0 37 | } 38 | return *p 39 | } 40 | 41 | func F64(p *float64) float64 { 42 | if p == nil { 43 | return 0 44 | } 45 | return *p 46 | } 47 | 48 | func F64P(v float64) *float64 { 49 | p := v 50 | return &p 51 | } 52 | 53 | func B(p *bool) bool { 54 | if p == nil { 55 | return false 56 | } 57 | return *p 58 | } 59 | 60 | func BP(v bool) *bool { 61 | p := v 62 | return &p 63 | } 64 | 65 | func I64(p *int64) int64 { 66 | if p == nil { 67 | return 0 68 | } 69 | return *p 70 | } 71 | 72 | func I64S(v int64) string { 73 | return strconv.FormatInt(v, 10) 74 | } 75 | -------------------------------------------------------------------------------- /utils/conv/conv_test.go: -------------------------------------------------------------------------------- 1 | package conv 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSP(t *testing.T) { 10 | assert.Equal(t, "", *SP("")) 11 | assert.Equal(t, " ", *SP(" ")) 12 | assert.Equal(t, "a", *SP("a")) 13 | assert.Equal(t, "foo", *SP("foo")) 14 | assert.Equal(t, "foo bar foo bar", *SP("foo bar foo bar")) 15 | } 16 | 17 | func TestS(t *testing.T) { 18 | assert.Equal(t, "", S(nil)) 19 | assert.Equal(t, "", S(SP(""))) 20 | assert.Equal(t, " ", S(SP(" "))) 21 | assert.Equal(t, " ", S(SP(" "))) 22 | assert.Equal(t, "a", S(SP("a"))) 23 | assert.Equal(t, "foo", S(SP("foo"))) 24 | assert.Equal(t, "foo bar", S(SP("foo bar"))) 25 | } 26 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "regexp" 7 | "time" 8 | 9 | "github.com/aws/aws-sdk-go/aws/awserr" 10 | ) 11 | 12 | var ( 13 | blankRE = regexp.MustCompile(`^\s*$`) 14 | ) 15 | 16 | func AsMap(v interface{}) (map[string]interface{}, error) { 17 | data, err := json.Marshal(v) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | asMap := make(map[string]interface{}) 23 | if err := json.Unmarshal(data, &asMap); err != nil { 24 | return nil, err 25 | } 26 | 27 | return asMap, nil 28 | } 29 | 30 | func ToJSON(v interface{}) string { 31 | data, err := json.MarshalIndent(v, "", " ") 32 | if err != nil { 33 | return "(error) " + err.Error() 34 | } 35 | return string(data) 36 | } 37 | 38 | func IsBlank(s string) bool { 39 | return blankRE.MatchString(s) 40 | } 41 | 42 | func FileExists(path string) bool { 43 | _, err := os.Stat(path) 44 | return err == nil 45 | } 46 | 47 | func IsDirectory(path string) (bool, error) { 48 | stat, err := os.Stat(path) 49 | if err != nil { 50 | return false, err 51 | } 52 | return stat.IsDir(), nil 53 | } 54 | 55 | func RetryOnAWSErrorCode(fn func() error, retryErrorCodes []string, interval, timeout time.Duration) error { 56 | return Retry(func() (bool, error) { 57 | err := fn() 58 | if err != nil { 59 | if awsErr, ok := err.(awserr.Error); ok { 60 | for _, rec := range retryErrorCodes { 61 | if awsErr.Code() == rec { 62 | return true, err 63 | } 64 | } 65 | } 66 | } 67 | return false, err 68 | }, interval, timeout) 69 | } 70 | 71 | func Retry(fn func() (bool, error), interval, timeout time.Duration) error { 72 | startTime := time.Now() 73 | endTime := startTime.Add(timeout) 74 | 75 | var cont bool 76 | var lastErr error 77 | 78 | for time.Now().Before(endTime) { 79 | cont, lastErr = fn() 80 | if !cont { 81 | break 82 | } 83 | 84 | time.Sleep(interval) 85 | } 86 | 87 | return lastErr 88 | } 89 | -------------------------------------------------------------------------------- /utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestIsBlank(t *testing.T) { 10 | assert.True(t, IsBlank("")) 11 | assert.True(t, IsBlank(" ")) 12 | assert.True(t, IsBlank(" ")) 13 | assert.True(t, IsBlank("\n")) 14 | assert.True(t, IsBlank("\t")) 15 | assert.True(t, IsBlank("\t ")) 16 | assert.True(t, IsBlank("\n ")) 17 | assert.True(t, IsBlank("\n \t")) 18 | 19 | assert.False(t, IsBlank("a")) 20 | assert.False(t, IsBlank("a ")) 21 | assert.False(t, IsBlank(" a")) 22 | assert.False(t, IsBlank(" a")) 23 | assert.False(t, IsBlank("\ta")) 24 | assert.False(t, IsBlank("\na")) 25 | } 26 | --------------------------------------------------------------------------------