├── .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 |
--------------------------------------------------------------------------------