├── .gitignore ├── CHANGELOG.md ├── Gopkg.lock ├── Gopkg.toml ├── Makefile ├── README.md ├── aws ├── ec2.go ├── metadata.go ├── route_table.go └── vip.go ├── build └── info.go ├── command ├── list.go ├── list_test.go ├── switch.go └── switch_test.go ├── commands.go ├── main.go └── release.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | .idea 3 | vendor 4 | dist 5 | bin 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 (2016-12-20) 2 | 3 | Initial release 4 | 5 | ### Added 6 | 7 | - Add Fundamental features 8 | 9 | ### Deprecated 10 | 11 | - Nothing 12 | 13 | ### Removed 14 | 15 | - Nothing 16 | 17 | ### Fixed 18 | 19 | - Nothing 20 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/Songmu/prompter" 6 | packages = ["."] 7 | revision = "b5721e8d556682bcef33262731d98a39dee14dc4" 8 | version = "0.1.0" 9 | 10 | [[projects]] 11 | name = "github.com/aws/aws-sdk-go" 12 | packages = [ 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/endpoints", 26 | "aws/request", 27 | "aws/session", 28 | "aws/signer/v4", 29 | "private/protocol", 30 | "private/protocol/ec2query", 31 | "private/protocol/query", 32 | "private/protocol/query/queryutil", 33 | "private/protocol/rest", 34 | "private/protocol/xml/xmlutil", 35 | "private/waiter", 36 | "service/ec2", 37 | "service/ec2/ec2iface", 38 | "service/sts" 39 | ] 40 | revision = "9dcf25708294b7b416ffd4b238b9708417f1d351" 41 | version = "v1.6.7" 42 | 43 | [[projects]] 44 | name = "github.com/go-ini/ini" 45 | packages = ["."] 46 | revision = "6f66b0e091edb3c7b380f7c4f0f884274d550b67" 47 | version = "v1.23.0" 48 | 49 | [[projects]] 50 | name = "github.com/jmespath/go-jmespath" 51 | packages = ["."] 52 | revision = "bd40a432e4c76585ef6b72d3fd96fb9b6dc7b68d" 53 | 54 | [[projects]] 55 | name = "github.com/mattn/go-isatty" 56 | packages = ["."] 57 | revision = "30a891c33c7cde7b02a981314b4228ec99380cca" 58 | 59 | [[projects]] 60 | name = "github.com/urfave/cli" 61 | packages = ["."] 62 | revision = "0bdeddeeb0f650497d603c4ad7b20cfe685682f6" 63 | version = "v1.19.1" 64 | 65 | [[projects]] 66 | name = "golang.org/x/crypto" 67 | packages = ["ssh/terminal"] 68 | revision = "f6b343c37ca80bfa8ea539da67a0b621f84fab1d" 69 | 70 | [[projects]] 71 | name = "golang.org/x/sys" 72 | packages = ["unix"] 73 | revision = "d75a52659825e75fff6158388dddc6a5b04f9ba5" 74 | 75 | [solve-meta] 76 | analyzer-name = "dep" 77 | analyzer-version = 1 78 | inputs-digest = "c9d84fe73ec639bdbf7243ec7dc0587add9bfe09c6f5e6a0232515a513d880e8" 79 | solver-name = "gps-cdcl" 80 | solver-version = 1 81 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | 22 | 23 | [[constraint]] 24 | name = "github.com/Songmu/prompter" 25 | version = "0.1.0" 26 | 27 | [[constraint]] 28 | name = "github.com/aws/aws-sdk-go" 29 | version = "1.6.7" 30 | 31 | [[constraint]] 32 | name = "github.com/urfave/cli" 33 | version = "1.19.1" 34 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME := swiro 2 | VERSION := $(shell git describe --tags --exact-match 2> /dev/null || git rev-parse --short HEAD || echo "unknown") 3 | 4 | SRCS := $(shell find . -type f -name '*.go') 5 | LDFLAGS := -s -w -extldflags "-static" 6 | LDFLAGS += \ 7 | -X "github.com/hatena/swiro/build.version=$(VERSION)" \ 8 | -X "github.com/hatena/swiro/build.name=$(NAME)" 9 | 10 | .DEFAULT_GOAL := bin/$(NAME) 11 | 12 | bin/$(NAME): $(SRCS) 13 | go build -a -tags netgo -installsuffix netgo -ldflags '$(LDFLAGS)' -o bin/$(NAME) 14 | 15 | .PHONY: cross-build 16 | cross-build: deps 17 | GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -a -tags netgo -installsuffix netgo -ldflags '$(LDFLAGS)' -o dist/$(NAME)_darwin_amd64 18 | GOOS=darwin GOARCH=386 CGO_ENABLED=0 go build -a -tags netgo -installsuffix netgo -ldflags '$(LDFLAGS)' -o dist/$(NAME)_darwin_386 19 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -a -tags netgo -installsuffix netgo -ldflags '$(LDFLAGS)' -o dist/$(NAME)_linux_amd64 20 | GOOS=linux GOARCH=386 CGO_ENABLED=0 go build -a -tags netgo -installsuffix netgo -ldflags '$(LDFLAGS)' -o dist/$(NAME)_linux_386 21 | GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -a -tags netgo -installsuffix netgo -ldflags '$(LDFLAGS)' -o dist/$(NAME)_windows_amd64.exe 22 | GOOS=windows GOARCH=386 CGO_ENABLED=0 go build -a -tags netgo -installsuffix netgo -ldflags '$(LDFLAGS)' -o dist/$(NAME)_windows_386.exe 23 | 24 | .PHONY: dep 25 | dep: 26 | ifeq ($(shell command -v dep 2> /dev/null),) 27 | go get github.com/golang/dep/cmd/dep 28 | endif 29 | 30 | .PHONY: deps 31 | deps: dep 32 | dep ensure 33 | 34 | .PHONY: fmt 35 | fmt: 36 | gofmt -s -w $$(find . -type f -name '*.go' | grep -v -e vendor) 37 | 38 | .PHONY: imports 39 | imports: 40 | goimports -w $$(find . -type f -name '*.go' | grep -v -e vendor) 41 | 42 | .PHONY: test 43 | test: 44 | go test -v -race $$(go list ./...) 45 | 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swiro 2 | 3 | swiro is a switching route tool for AWS to realize VIP (Virtual IP) with Routing-Based High Availability pattern. 4 | 5 | This pattern is possible to perform failover (switching of the connection destination) of the EC2 redundant across the subnet (AZ). 6 | 7 | 8 | ## Usage 9 | 10 | * List routes 11 | 12 | ``` 13 | $ swiro list 14 | ``` 15 | 16 | * Switching routes 17 | 18 | ``` 19 | $ swiro switch -r rtb-xxxxxx -v 10.0.0.1 -I i-xxxxxx 20 | ``` 21 | 22 | 23 | ## Example 24 | 25 | ### List routes 26 | 27 | ``` 28 | $ swiro list 29 | ===> Route Table: route_table_1 (rtb-xxxxxx1) 30 | ---> Virtual IP: 10.0.0.1/32 =======> src_instance_1 (i-yyyyyy1) 31 | ---> Virtual IP: 10.0.0.2/32 =======> src_instance_2 (i-yyyyyy2) 32 | ===> Route Table: route_table_2 (rtb-xxxxxx2) 33 | ---> Virtual IP: 10.0.0.3/32 =======> src_instance_3 (i-yyyyyy3) 34 | ``` 35 | 36 | ### Switching routes 37 | 38 | In most cases you can switch the routing with the Route Table ID as follows: 39 | 40 | ``` 41 | $ swiro switch -r rtb-xxxxxx -v 10.0.0.1 -I i-xxxxxx 42 | Switch the route below setting: 43 | ===> Route Table: route_table (rtb-xxxxxx) 44 | ---> Virtual IP: 10.0.0.1 -------- Src: src_instance (i-yyyyyy) 45 | \\ 46 | ======> Dest: i-xxxxxx 47 | Are you sure? (y/n) [y]: y 48 | Success!! 49 | ``` 50 | 51 | You can also switch by specifying Route Table Name instead of Route Table ID. 52 | 53 | ``` 54 | $ swiro switch -r route_table -v 10.0.0.1 -I dest_instance 55 | ``` 56 | 57 | ## Install 58 | 59 | To install, use `go get`: 60 | 61 | ```bash 62 | $ go get -u github.com/taku-k/swiro 63 | ``` 64 | 65 | ## Contribution 66 | 67 | 1. Fork ([https://github.com/taku-k/swiro/fork](https://github.com/taku-k/swiro/fork)) 68 | 1. Create a feature branch 69 | 1. Commit your changes 70 | 1. Rebase your local changes against the master branch 71 | 1. Run test suite with the `go test ./...` command and confirm that it passes 72 | 1. Run `gofmt -s` 73 | 1. Create a new Pull Request 74 | 75 | ## Release 76 | 77 | Prerequisite: 78 | 79 | ```bash 80 | $ go get github.com/tcnksm/ghr 81 | $ go get github.com/Songmu/ghch/cmd/ghch 82 | ``` 83 | 84 | When you'd like to release master branch as v0.2.8: 85 | 86 | ```bash 87 | $ ./release.sh v0.2.8 88 | ``` 89 | 90 | ## Author 91 | 92 | [taku-k](https://github.com/taku-k) 93 | -------------------------------------------------------------------------------- /aws/ec2.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/aws/aws-sdk-go/aws" 13 | "github.com/aws/aws-sdk-go/aws/session" 14 | "github.com/aws/aws-sdk-go/service/ec2" 15 | "github.com/aws/aws-sdk-go/service/ec2/ec2iface" 16 | ) 17 | 18 | type Ec2Client struct { 19 | ec2Svc ec2iface.EC2API 20 | } 21 | 22 | func newEc2Client() *Ec2Client { 23 | sess, err := session.NewSession() 24 | if err != nil { 25 | log.Fatal("Creating session is failed") 26 | } 27 | region := os.Getenv("AWS_REGION") 28 | if region == "" { 29 | region, _ = NewMetaDataClientFromSession(sess).GetRegion() 30 | } 31 | ec2Svc := ec2.New(sess, aws.NewConfig().WithRegion("ap-northeast-1")) 32 | 33 | return &Ec2Client{ec2Svc: ec2Svc} 34 | } 35 | 36 | func (c *Ec2Client) getRouteTables(ctx context.Context, retry int, retryWait int) ([]*ec2.RouteTable, error) { 37 | req, resp := c.ec2Svc.DescribeRouteTablesRequest(nil) 38 | req.HTTPRequest = req.HTTPRequest.WithContext(ctx) 39 | var err error 40 | for i := 0; i <= retry; i++ { 41 | if i > 0 { 42 | fmt.Printf("Retry (%v/%v): describe route tables API\n", i, retry) 43 | } 44 | err = req.Send() 45 | if err == nil && len(resp.RouteTables) > 0 { 46 | break 47 | } 48 | time.Sleep(time.Duration(i*retryWait) * time.Millisecond) 49 | } 50 | if err != nil || len(resp.RouteTables) < 1 { 51 | return nil, err 52 | } 53 | 54 | return resp.RouteTables, nil 55 | } 56 | 57 | func (c *Ec2Client) getRouteTableByKey(ctx context.Context, retry int, retryWait int, key string) (*ec2.RouteTable, error) { 58 | var input *ec2.DescribeRouteTablesInput 59 | if strings.HasPrefix(key, "rtb-") { 60 | input = &ec2.DescribeRouteTablesInput{ 61 | RouteTableIds: []*string{ 62 | aws.String(key), 63 | }, 64 | } 65 | } else { 66 | input = &ec2.DescribeRouteTablesInput{ 67 | Filters: []*ec2.Filter{ 68 | { 69 | Name: aws.String("tag-key"), 70 | Values: []*string{ 71 | aws.String("Name"), 72 | }, 73 | }, 74 | { 75 | Name: aws.String("tag-value"), 76 | Values: []*string{ 77 | aws.String(key), 78 | }, 79 | }, 80 | }, 81 | } 82 | } 83 | 84 | req, resp := c.ec2Svc.DescribeRouteTablesRequest(input) 85 | req.HTTPRequest = req.HTTPRequest.WithContext(ctx) 86 | var err error 87 | for i := 0; i <= retry; i++ { 88 | if i > 0 { 89 | fmt.Printf("Retry (%v/%v): describe route tables API\n", i, retry) 90 | } 91 | err := req.Send() 92 | if err == nil && len(resp.RouteTables) > 0 { 93 | break 94 | } 95 | time.Sleep(time.Duration(i*retryWait) * time.Millisecond) 96 | } 97 | switch { 98 | case err != nil: 99 | return nil, err 100 | case len(resp.RouteTables) == 0: 101 | return nil, errors.New("Route table is not found") 102 | case len(resp.RouteTables) > 1: 103 | return nil, errors.New("Too much tables are found") 104 | } 105 | return resp.RouteTables[0], nil 106 | } 107 | 108 | func (c *Ec2Client) replaceRoute(ctx context.Context, retry int, retryWait int, routeTableId, destinationCidrBlock, instanceId string) error { 109 | req, _ := c.ec2Svc.ReplaceRouteRequest(&ec2.ReplaceRouteInput{ 110 | RouteTableId: aws.String(routeTableId), 111 | InstanceId: aws.String(instanceId), 112 | DestinationCidrBlock: aws.String(destinationCidrBlock), 113 | }) 114 | req.HTTPRequest = req.HTTPRequest.WithContext(ctx) 115 | var err error 116 | for i := 0; i <= retry; i++ { 117 | if i > 0 { 118 | fmt.Printf("Retry (%v/%v): replace route API\n", i, retry) 119 | } 120 | err := req.Send() 121 | if err == nil { 122 | break 123 | } 124 | time.Sleep(time.Duration(i*retryWait) * time.Millisecond) 125 | } 126 | if err != nil { 127 | return err 128 | } 129 | return nil 130 | } 131 | 132 | func (c *Ec2Client) getInstanceIdByDest(ctx context.Context, retry int, retryWait int, routeTableId, dest string) (string, error) { 133 | t, err := c.getRouteTableByKey(ctx, retry, retryWait, routeTableId) 134 | if err != nil { 135 | return "", err 136 | } 137 | for _, r := range t.Routes { 138 | if *r.DestinationCidrBlock == dest { 139 | return *r.InstanceId, nil 140 | } 141 | } 142 | return "", errors.New("Not found") 143 | } 144 | 145 | func (c *Ec2Client) getInstanceByKey(ctx context.Context, retry int, retryWait int, key string) (*ec2.Instance, error) { 146 | var input *ec2.DescribeInstancesInput 147 | if strings.HasPrefix(key, "i-") { 148 | input = &ec2.DescribeInstancesInput{ 149 | InstanceIds: []*string{ 150 | aws.String(key), 151 | }, 152 | } 153 | } else { 154 | input = &ec2.DescribeInstancesInput{ 155 | Filters: []*ec2.Filter{ 156 | { 157 | Name: aws.String("tag-key"), 158 | Values: []*string{ 159 | aws.String("Name"), 160 | }, 161 | }, 162 | { 163 | Name: aws.String("tag-value"), 164 | Values: []*string{ 165 | aws.String(key), 166 | }, 167 | }, 168 | }, 169 | } 170 | } 171 | 172 | var err error 173 | req, resp := c.ec2Svc.DescribeInstancesRequest(input) 174 | req.HTTPRequest = req.HTTPRequest.WithContext(ctx) 175 | for i := 0; i <= retry; i++ { 176 | if i > 0 { 177 | fmt.Printf("Retry (%v/%v): describe instances API\n", i, retry) 178 | } 179 | err := req.Send() 180 | if err == nil && len(resp.Reservations) > 0 { 181 | break 182 | } 183 | time.Sleep(time.Duration(i*retryWait) * time.Millisecond) 184 | } 185 | switch { 186 | case err != nil: 187 | return nil, err 188 | case len(resp.Reservations) == 0: 189 | return nil, errors.New("Given instance is not found") 190 | case len(resp.Reservations[0].Instances) != 1: 191 | return nil, errors.New("Too much instances are fetched") 192 | } 193 | return resp.Reservations[0].Instances[0], nil 194 | } 195 | 196 | func (c *Ec2Client) getInstanceId(ctx context.Context, retry int, retryWait int, key string) (string, error) { 197 | instance, err := c.getInstanceByKey(ctx, retry, retryWait, key) 198 | if err != nil { 199 | return "", err 200 | } 201 | return *instance.InstanceId, nil 202 | } 203 | 204 | func (c *Ec2Client) getInstanceNameById(ctx context.Context, retry int, retryWait int, instanceId string) (string, error) { 205 | instance, err := c.getInstanceByKey(ctx, retry, retryWait, instanceId) 206 | if err != nil { 207 | return "", err 208 | } 209 | for _, tag := range instance.Tags { 210 | if *tag.Key == "Name" { 211 | return *tag.Value, nil 212 | } 213 | } 214 | return "", nil 215 | } 216 | 217 | func (c *Ec2Client) getENINameById(ctx context.Context, retry int, retryWait int, ENIId string) (string, error) { 218 | input := &ec2.DescribeNetworkInterfacesInput{ 219 | Filters: []*ec2.Filter{ 220 | { 221 | Name: aws.String("network-interface-id"), 222 | Values: []*string{ 223 | aws.String(ENIId), 224 | }, 225 | }, 226 | }, 227 | } 228 | 229 | req, resp := c.ec2Svc.DescribeNetworkInterfacesRequest(input) 230 | req.HTTPRequest = req.HTTPRequest.WithContext(ctx) 231 | var err error 232 | for i := 0; i <= retry; i++ { 233 | if i > 0 { 234 | fmt.Printf("Retry (%v/%v): describe network interfaces API\n", i, retry) 235 | } 236 | err := req.Send() 237 | if err == nil && len(resp.NetworkInterfaces) > 0 { 238 | break 239 | } 240 | time.Sleep(time.Duration(i*retryWait) * time.Millisecond) 241 | } 242 | switch { 243 | case err != nil: 244 | return "", err 245 | case len(resp.NetworkInterfaces) == 0: 246 | return "", errors.New("Given interface is not found") 247 | case len(resp.NetworkInterfaces) != 1: 248 | return "", errors.New("Too much instances are fetched") 249 | } 250 | eni := resp.NetworkInterfaces[0] 251 | for _, tag := range eni.TagSet { 252 | if *tag.Key == "Name" { 253 | return *tag.Value, nil 254 | } 255 | } 256 | return "", nil 257 | } 258 | -------------------------------------------------------------------------------- /aws/metadata.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/ec2metadata" 5 | "github.com/aws/aws-sdk-go/aws/session" 6 | ) 7 | 8 | type MetaDataClient struct { 9 | svc *ec2metadata.EC2Metadata 10 | } 11 | 12 | func NewMetaDataClient() *MetaDataClient { 13 | return &MetaDataClient{svc: ec2metadata.New(session.New())} 14 | } 15 | 16 | func NewMetaDataClientFromSession(s *session.Session) *MetaDataClient { 17 | return &MetaDataClient{svc: ec2metadata.New(s)} 18 | } 19 | 20 | func (c *MetaDataClient) GetInstanceID() (string, error) { 21 | return c.svc.GetMetadata("instance-id") 22 | } 23 | 24 | func (c *MetaDataClient) GetRegion() (string, error) { 25 | return c.svc.Region() 26 | } 27 | -------------------------------------------------------------------------------- /aws/route_table.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | "github.com/aws/aws-sdk-go/service/ec2" 11 | ) 12 | 13 | const timeOut = 3 * time.Second 14 | const retry = 3 15 | 16 | // retryWait wait millisecond for retry 17 | const retryWait = 200 18 | 19 | type RouteTable struct { 20 | table *ec2.RouteTable 21 | e *Ec2Client 22 | } 23 | 24 | type Ec2Meta struct { 25 | Name string 26 | Id string 27 | } 28 | 29 | func NewRouteTables() ([]*RouteTable, error) { 30 | ctx, cancel := context.WithTimeout(context.Background(), timeOut) 31 | defer cancel() 32 | 33 | e := newEc2Client() 34 | ec2Tables, err := e.getRouteTables(ctx, retry, retryWait) 35 | if err != nil { 36 | return nil, err 37 | } 38 | tables := make([]*RouteTable, 0, len(ec2Tables)) 39 | for _, t := range ec2Tables { 40 | tables = append(tables, &RouteTable{table: t, e: e}) 41 | } 42 | return tables, nil 43 | } 44 | 45 | func NewRouteTable(routeTableKey string) (*RouteTable, error) { 46 | ctx, cancel := context.WithTimeout(context.Background(), timeOut) 47 | defer cancel() 48 | 49 | e := newEc2Client() 50 | ec2Table, err := e.getRouteTableByKey(ctx, retry, retryWait, routeTableKey) 51 | if err != nil { 52 | return nil, err 53 | } 54 | return &RouteTable{table: ec2Table, e: e}, nil 55 | } 56 | 57 | func (t *RouteTable) ReplaceRoute(vip, instance string) error { 58 | ctx, cancel := context.WithTimeout(context.Background(), timeOut) 59 | defer cancel() 60 | 61 | routeTableId := *t.table.RouteTableId 62 | destinationCidrBlock := fmt.Sprintf("%s/32", vip) 63 | instanceId, err := t.e.getInstanceId(ctx, retry, retryWait, instance) 64 | if err != nil { 65 | return err 66 | } 67 | // Replace Route API may be delayed. So, should not call it many times. 68 | if err = t.e.replaceRoute(ctx, retry, retryWait, routeTableId, destinationCidrBlock, instanceId); err != nil { 69 | return err 70 | } 71 | 72 | var changed string 73 | for i := 0; i <= retry; i++ { 74 | if i > 0 { 75 | fmt.Printf("Retry (%v/%v): confirm route table destination changes\n", i, retry) 76 | } 77 | changed, err = t.e.getInstanceIdByDest(ctx, retry, retryWait, routeTableId, destinationCidrBlock) 78 | if err != nil { 79 | return err 80 | } 81 | if changed == instanceId { 82 | break 83 | } 84 | time.Sleep(time.Duration(i*retryWait) * time.Millisecond) 85 | } 86 | if changed != instanceId { 87 | return errors.New("Route has not been replaced yet") 88 | } 89 | 90 | return nil 91 | } 92 | 93 | func (t *RouteTable) ListPossibleVips() *MaybeVips { 94 | ctx, cancel := context.WithTimeout(context.Background(), timeOut) 95 | defer cancel() 96 | ids := make([]string, 0, len(t.table.Routes)) 97 | vips := make([]string, 0, len(t.table.Routes)) 98 | names := make([]string, 0, len(t.table.Routes)) 99 | for _, r := range t.table.Routes { 100 | // VPC Endpoint route does not have DestinationCidrBlock field and It can not be modified 101 | if r.DestinationCidrBlock == nil { 102 | continue 103 | } 104 | if strings.HasSuffix(*r.DestinationCidrBlock, "/32") { 105 | if r.InstanceId != nil { 106 | ids = append(ids, *r.InstanceId) 107 | vips = append(vips, *r.DestinationCidrBlock) 108 | name, err := t.e.getInstanceNameById(ctx, retry, retryWait, *r.InstanceId) 109 | if err != nil { 110 | name = "unknown" 111 | } 112 | names = append(names, name) 113 | } 114 | } 115 | } 116 | return &MaybeVips{t, ids, vips, names} 117 | } 118 | 119 | func (t *RouteTable) GetSrcByVip(vip string) (*Ec2Meta, error) { 120 | ctx, cancel := context.WithTimeout(context.Background(), timeOut) 121 | defer cancel() 122 | 123 | id, state, err := t.getSrcByVip(vip) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | var name string 129 | switch { 130 | case strings.HasPrefix(id, "i-") && state == ec2.RouteStateActive: 131 | name, err = t.e.getInstanceNameById(ctx, retry, retryWait, id) 132 | if err != nil { 133 | return nil, err 134 | } 135 | case strings.HasPrefix(id, "eni-") && state == ec2.RouteStateActive: 136 | name, err = t.e.getENINameById(ctx, retry, retryWait, id) 137 | if err != nil { 138 | return nil, err 139 | } 140 | case state == "blackhole": 141 | name = ec2.RouteStateBlackhole 142 | default: 143 | return nil, errors.New("Not support to switch from neither instance nor ENI destination") 144 | } 145 | return &Ec2Meta{Name: name, Id: id}, nil 146 | } 147 | 148 | func (t *RouteTable) getSrcByVip(vip string) (string, string, error) { 149 | vipCidrBlock := vip 150 | if !strings.HasSuffix(vipCidrBlock, "/32") { 151 | vipCidrBlock = fmt.Sprintf("%s/32", vipCidrBlock) 152 | } 153 | for _, route := range t.table.Routes { 154 | if *route.DestinationCidrBlock == vipCidrBlock { 155 | switch { 156 | case route.InstanceId != nil && *route.InstanceId != "": 157 | return *route.InstanceId, *route.State, nil 158 | case route.NetworkInterfaceId != nil && *route.NetworkInterfaceId != "": 159 | return *route.NetworkInterfaceId, *route.State, nil 160 | } 161 | } 162 | } 163 | return "", "", errors.New("Given vip is not found") 164 | } 165 | 166 | func (t *RouteTable) GetRouteTableId() string { 167 | return *t.table.RouteTableId 168 | } 169 | 170 | func (t *RouteTable) GetRouteTableName() string { 171 | ret := "-" 172 | if len(t.table.Tags) != 0 { 173 | for _, tag := range t.table.Tags { 174 | if *tag.Key == "Name" { 175 | ret = *tag.Value 176 | } 177 | } 178 | } 179 | return ret 180 | } 181 | 182 | func (t *RouteTable) String() string { 183 | return t.table.String() 184 | } 185 | -------------------------------------------------------------------------------- /aws/vip.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "os" 5 | "text/template" 6 | ) 7 | 8 | type MaybeVips struct { 9 | table *RouteTable 10 | instanceIds []string 11 | vips []string 12 | names []string 13 | } 14 | 15 | func (v *MaybeVips) Output() { 16 | if len(v.instanceIds) == 0 { 17 | return 18 | } 19 | tmpl := `===> Route Table: {{.RouteTableName}} ({{.RouteTableId}}) 20 | {{range $i, $vip := .Vips}}---> Virtual IP: {{$vip}} =======> {{index $.Names $i}} ({{index $.InstanceIds $i}}) 21 | {{end}}` 22 | t := template.New("maybevips") 23 | template.Must(t.Parse(tmpl)) 24 | t.Execute(os.Stdout, map[string]interface{}{ 25 | "RouteTableName": v.table.GetRouteTableName(), 26 | "RouteTableId": v.table.GetRouteTableId(), 27 | "InstanceIds": v.instanceIds, 28 | "Vips": v.vips, 29 | "Names": v.names, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /build/info.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | type Info struct { 4 | Name string 5 | Version string 6 | } 7 | 8 | var ( 9 | version string 10 | name string 11 | ) 12 | 13 | func GetInfo() *Info { 14 | return &Info{ 15 | Name: name, 16 | Version: version, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /command/list.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "github.com/hatena/swiro/aws" 5 | "github.com/urfave/cli" 6 | ) 7 | 8 | func CmdList(c *cli.Context) error { 9 | ts, err := aws.NewRouteTables() 10 | if err != nil { 11 | return err 12 | } 13 | for _, t := range ts { 14 | t.ListPossibleVips().Output() 15 | } 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /command/list_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import "testing" 4 | 5 | func TestCmdList(t *testing.T) { 6 | // Write your code here 7 | } 8 | -------------------------------------------------------------------------------- /command/switch.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/Songmu/prompter" 9 | "github.com/hatena/swiro/aws" 10 | "github.com/urfave/cli" 11 | ) 12 | 13 | func CmdSwitch(c *cli.Context) error { 14 | table := c.StringSlice("route-table") 15 | vip := c.String("vip") 16 | force := c.Bool("force") 17 | 18 | var instanceKey string 19 | if instanceKey = c.String("instance"); instanceKey == "" { 20 | var err error 21 | if instanceKey, err = aws.NewMetaDataClient().GetInstanceID(); err != nil { 22 | return err 23 | } 24 | } 25 | for _, t := range table { 26 | routeTable, err := aws.NewRouteTable(t) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | promptStr := `Switch the route below setting: 32 | ===> Route Table: %s (%s) 33 | ---> Virtual IP: %s -------- Src: %s (%s) 34 | %s \\ 35 | %s ======> Dest: %s 36 | ` 37 | routeTableName := routeTable.GetRouteTableName() 38 | routeTableId := routeTable.GetRouteTableId() 39 | src, err := routeTable.GetSrcByVip(vip) 40 | if err != nil { 41 | return err 42 | } 43 | ws := strings.Repeat(" ", len(vip)) 44 | fmt.Fprintf(os.Stdout, promptStr, routeTableName, routeTableId, vip, src.Name, src.Id, ws, ws, instanceKey) 45 | if !force && !prompter.YN("Are you sure?", false) { 46 | fmt.Fprintln(os.Stderr, "Switching is canceled") 47 | return nil 48 | } 49 | 50 | err = routeTable.ReplaceRoute(vip, instanceKey) 51 | if err != nil { 52 | return err 53 | } 54 | fmt.Fprintln(os.Stdout, "Success!!") 55 | } 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /command/switch_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import "testing" 4 | 5 | func TestCmdGrab(t *testing.T) { 6 | // Write your code here 7 | } 8 | -------------------------------------------------------------------------------- /commands.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/hatena/swiro/command" 8 | "github.com/urfave/cli" 9 | ) 10 | 11 | var GlobalFlags = []cli.Flag{} 12 | 13 | var Commands = []cli.Command{ 14 | { 15 | Name: "switch", 16 | Usage: "Switch the route based on given arguments", 17 | Action: command.CmdSwitch, 18 | Flags: []cli.Flag{ 19 | cli.StringSliceFlag{Name: "r, route-table", Usage: "route table id or name"}, 20 | cli.StringFlag{Name: "v, vip", Usage: "Virtual IP address"}, 21 | cli.StringFlag{Name: "I, instance", Usage: "instance id or name"}, 22 | cli.BoolFlag{Name: "f, force", Usage: "force switching (default: false"}, 23 | }, 24 | }, 25 | { 26 | Name: "list", 27 | Usage: "List all route tables associated with Virtual IP", 28 | Action: command.CmdList, 29 | Flags: []cli.Flag{}, 30 | }, 31 | } 32 | 33 | func CommandNotFound(c *cli.Context, command string) { 34 | fmt.Fprintf(os.Stderr, "%s: '%s' is not a %s command. See '%s --help'.", c.App.Name, command, c.App.Name, c.App.Name) 35 | os.Exit(2) 36 | } 37 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/hatena/swiro/build" 8 | "github.com/urfave/cli" 9 | ) 10 | 11 | func main() { 12 | info := build.GetInfo() 13 | 14 | app := cli.NewApp() 15 | app.Name = info.Name 16 | app.Version = info.Version 17 | app.Author = "taku-k" 18 | app.Email = "taakuu19@gmail.com" 19 | app.Usage = "" 20 | 21 | app.Flags = GlobalFlags 22 | app.Commands = Commands 23 | app.CommandNotFound = CommandNotFound 24 | 25 | err := app.Run(os.Args) 26 | if err != nil { 27 | fmt.Println(err) 28 | os.Exit(1) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | USAGE="Usage: $0 " 5 | 6 | if [ -z "$1" ]; then 7 | echo "$USAGE" 8 | exit 1 9 | fi 10 | 11 | if [[ $(git symbolic-ref --short HEAD) != "master" ]]; then 12 | echo "Your current branch should be master" 13 | exit 1 14 | fi 15 | 16 | RELEASE_VERSION=$(echo $1 | sed 's/^\([0-9]\)/v\1/') 17 | 18 | echo "STEP1: tag the version" 19 | if [[ $(git tag) =~ "${RELEASE_VERSION}" ]]; then 20 | echo "${RELEASE_VERSION} is already existed. You must delete this tag and try again." 21 | exit 1 22 | fi 23 | BODY=$(ghch --format=markdown --next-version=${RELEASE_VERSION}) 24 | git tag ${RELEASE_VERSION} 25 | 26 | echo "STEP2: build" 27 | make cross-build 28 | 29 | echo "STEP3: push binaries" 30 | ghr -u hatena -b "${BODY}" ${RELEASE_VERSION} dist 31 | 32 | echo "Completed" 33 | --------------------------------------------------------------------------------