Merge pull request #35 from ehazlett/driver-aws

amazon ec2 driver
This commit is contained in:
Ben Firshman 2014-12-13 01:15:26 +00:00
commit 477fffb1ba
30 changed files with 3098 additions and 0 deletions

4
Godeps/Godeps.json generated
View File

@ -110,6 +110,10 @@
"ImportPath": "github.com/google/go-querystring/query",
"Rev": "30f7a39f4a218feb5325f3aebc60c32a572a8274"
},
{
"ImportPath": "github.com/smartystreets/go-aws-auth",
"Rev": "1f0db8c0ee6362470abe06a94e3385927ed72a4b"
},
{
"ImportPath": "github.com/tent/http-link-go",
"Rev": "ac974c61c2f990f4115b119354b5e0b47550e888"

View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 SmartyStreets
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,86 @@
go-aws-auth
===========
[![GoDoc](https://godoc.org/github.com/smartystreets/go-aws-auth?status.svg)](http://godoc.org/github.com/smartystreets/go-aws-auth)
Go-AWS-Auth is a comprehensive, lightweight library for signing requests to Amazon Web Services.
It's easy to use: simply build your HTTP request and call `awsauth.Sign(req)` before sending your request over the wire.
### Supported signing mechanisms
- [Signed Signature Version 2](http://docs.aws.amazon.com/general/latest/gr/signature-version-2.html)
- [Signed Signature Version 3](http://docs.aws.amazon.com/general/latest/gr/signing_aws_api_requests.html)
- [Signed Signature Version 4](http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html)
- [Custom S3 Authentication Scheme](http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html)
- [Security Token Service](http://docs.aws.amazon.com/STS/latest/APIReference/Welcome.html)
- [S3 Query String Authentication](http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth)
- [IAM Role](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials)
For more info about AWS authentication, see the [comprehensive docs](http://docs.aws.amazon.com/general/latest/gr/signing_aws_api_requests.html) at AWS.
### Install
Go get it:
$ go get github.com/smartystreets/go-aws-auth
Then import it:
import "github.com/smartystreets/go-aws-auth"
### Using your AWS Credentials
The library looks for credentials in this order:
1. **Hard-code:** You can manually pass in an instance of `awsauth.Credentials` to any call to a signing function as a second argument:
```go
awsauth.Sign(req, awsauth.Credentials{
AccessKeyID: "Access Key ID",
SecretAccessKey: "Secret Access Key",
SecurityToken: "Security Token", // STS (optional)
})
```
2. **Environment variables:** Set the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables with your credentials. The library will automatically detect and use them. Optionally, you may also set the `AWS_SECURITY_TOKEN` environment variable if you are using temporary credentials from [STS](http://docs.aws.amazon.com/STS/latest/APIReference/Welcome.html).
3. **IAM Role:** If running on EC2 and the credentials are neither hard-coded nor in the environment, go-aws-auth will detect the first IAM role assigned to the current EC2 instance and use those credentials.
(Be especially careful hard-coding credentials into your application if the code is committed to source control.)
### Signing requests
Just make the request, have it signed, and perform the request as you normally would.
```go
url := "https://iam.amazonaws.com/?Action=ListRoles&Version=2010-05-08"
client := new(http.Client)
req, err := http.NewRequest("GET", url, nil)
awsauth.Sign(req) // Automatically chooses the best signing mechanism for the service
resp, err := client.Do(req)
```
You can use `Sign` to have the library choose the best signing algorithm depending on the service, or you can specify it manually if you know what you need:
- `Sign2`
- `Sign3`
- `Sign4`
- `SignS3` (deprecated for Sign4)
- `SignS3Url` (for pre-signed S3 URLs; GETs only)
### Contributing
Please feel free to contribute! Bug fixes are more than welcome any time, as long as tests assert correct behavior. If you'd like to change an existing implementation or see a new feature, open an issue first so we can discuss it. Thanks to all contributors!

View File

@ -0,0 +1,231 @@
// Package awsauth implements AWS request signing using Signed Signature Version 2,
// Signed Signature Version 3, and Signed Signature Version 4. Supports S3 and STS.
package awsauth
import (
"net/http"
"net/url"
"sync"
"time"
)
// Credentials stores the information necessary to authorize with AWS and it
// is from this information that requests are signed.
type Credentials struct {
AccessKeyID string
SecretAccessKey string
SecurityToken string `json:"Token"`
Expiration time.Time
}
// Sign signs a request bound for AWS. It automatically chooses the best
// authentication scheme based on the service the request is going to.
func Sign(req *http.Request, cred ...Credentials) *http.Request {
service, _ := serviceAndRegion(req.URL.Host)
sigVersion := awsSignVersion[service]
switch sigVersion {
case 2:
return Sign2(req, cred...)
case 3:
return Sign3(req, cred...)
case 4:
return Sign4(req, cred...)
case -1:
return SignS3(req, cred...)
}
return nil
}
// Sign4 signs a request with Signed Signature Version 4.
func Sign4(req *http.Request, cred ...Credentials) *http.Request {
signMutex.Lock()
defer signMutex.Unlock()
keys := chooseKeys(cred)
// Add the X-Amz-Security-Token header when using STS
if keys.SecurityToken != "" {
req.Header.Set("X-Amz-Security-Token", keys.SecurityToken)
}
prepareRequestV4(req)
meta := new(metadata)
// Task 1
hashedCanonReq := hashedCanonicalRequestV4(req, meta)
// Task 2
stringToSign := stringToSignV4(req, hashedCanonReq, meta)
// Task 3
signingKey := signingKeyV4(keys.SecretAccessKey, meta.date, meta.region, meta.service)
signature := signatureV4(signingKey, stringToSign)
req.Header.Set("Authorization", buildAuthHeaderV4(signature, meta, keys))
return req
}
// Sign3 signs a request with Signed Signature Version 3.
// If the service you're accessing supports Version 4, use that instead.
func Sign3(req *http.Request, cred ...Credentials) *http.Request {
signMutex.Lock()
defer signMutex.Unlock()
keys := chooseKeys(cred)
// Add the X-Amz-Security-Token header when using STS
if keys.SecurityToken != "" {
req.Header.Set("X-Amz-Security-Token", keys.SecurityToken)
}
prepareRequestV3(req)
// Task 1
stringToSign := stringToSignV3(req)
// Task 2
signature := signatureV3(stringToSign, keys)
// Task 3
req.Header.Set("X-Amzn-Authorization", buildAuthHeaderV3(signature, keys))
return req
}
// Sign2 signs a request with Signed Signature Version 2.
// If the service you're accessing supports Version 4, use that instead.
func Sign2(req *http.Request, cred ...Credentials) *http.Request {
signMutex.Lock()
defer signMutex.Unlock()
keys := chooseKeys(cred)
// Add the SecurityToken parameter when using STS
// This must be added before the signature is calculated
if keys.SecurityToken != "" {
v := url.Values{}
v.Set("SecurityToken", keys.SecurityToken)
augmentRequestQuery(req, v)
}
prepareRequestV2(req, keys)
stringToSign := stringToSignV2(req)
signature := signatureV2(stringToSign, keys)
values := url.Values{}
values.Set("Signature", signature)
augmentRequestQuery(req, values)
return req
}
// SignS3 signs a request bound for Amazon S3 using their custom
// HTTP authentication scheme.
func SignS3(req *http.Request, cred ...Credentials) *http.Request {
signMutex.Lock()
defer signMutex.Unlock()
keys := chooseKeys(cred)
// Add the X-Amz-Security-Token header when using STS
if keys.SecurityToken != "" {
req.Header.Set("X-Amz-Security-Token", keys.SecurityToken)
}
prepareRequestS3(req)
stringToSign := stringToSignS3(req)
signature := signatureS3(stringToSign, keys)
authHeader := "AWS " + keys.AccessKeyID + ":" + signature
req.Header.Set("Authorization", authHeader)
return req
}
// SignS3Url signs a GET request for a resource on Amazon S3 by appending
// query string parameters containing credentials and signature. You must
// specify an expiration date for these signed requests. After that date,
// a request signed with this method will be rejected by S3.
func SignS3Url(req *http.Request, expire time.Time, cred ...Credentials) *http.Request {
signMutex.Lock()
defer signMutex.Unlock()
keys := chooseKeys(cred)
stringToSign := stringToSignS3Url("GET", expire, req.URL.Path)
signature := signatureS3(stringToSign, keys)
qs := req.URL.Query()
qs.Set("AWSAccessKeyId", keys.AccessKeyID)
qs.Set("Signature", signature)
qs.Set("Expires", timeToUnixEpochString(expire))
req.URL.RawQuery = qs.Encode()
return req
}
// expired checks to see if the temporary credentials from an IAM role are
// within 4 minutes of expiration (The IAM documentation says that new keys
// will be provisioned 5 minutes before the old keys expire). Credentials
// that do not have an Expiration cannot expire.
func (k *Credentials) expired() bool {
if k.Expiration.IsZero() {
// Credentials with no expiration can't expire
return false
}
expireTime := k.Expiration.Add(-4 * time.Minute)
// if t - 4 mins is before now, true
if expireTime.Before(time.Now()) {
return true
} else {
return false
}
}
type metadata struct {
algorithm string
credentialScope string
signedHeaders string
date string
region string
service string
}
const (
envAccessKeyID = "AWS_ACCESS_KEY_ID"
envSecretAccessKey = "AWS_SECRET_ACCESS_KEY"
envSecurityToken = "AWS_SECURITY_TOKEN"
)
var (
awsSignVersion = map[string]int{
"autoscaling": 4,
"cloudfront": 4,
"cloudformation": 4,
"cloudsearch": 4,
"monitoring": 4,
"dynamodb": 4,
"ec2": 2,
"elasticmapreduce": 4,
"elastictranscoder": 4,
"elasticache": 2,
"glacier": 4,
"kinesis": 4,
"redshift": 4,
"rds": 4,
"sdb": 2,
"sns": 4,
"sqs": 4,
"s3": 4,
"elasticbeanstalk": 4,
"importexport": 2,
"iam": 4,
"route53": 3,
"elasticloadbalancing": 4,
"email": 3,
}
signMutex sync.Mutex
)

View File

@ -0,0 +1,284 @@
package awsauth
import (
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
)
func TestIntegration(t *testing.T) {
Convey("Given real credentials from environment variables", t, func() {
Convey("A request (with out-of-order query string) with to IAM should succeed (assuming Administrator Access policy)", func() {
req := newRequest("GET", "https://iam.amazonaws.com/?Version=2010-05-08&Action=ListRoles", nil)
if !credentialsSet() {
SkipSo(http.StatusOK, ShouldEqual, http.StatusOK)
} else {
resp := sign4AndDo(req)
if resp.StatusCode != http.StatusOK {
msg, _ := ioutil.ReadAll(resp.Body)
t.Error(string(msg))
}
So(resp.StatusCode, ShouldEqual, http.StatusOK)
}
})
Convey("A request to S3 should succeed", func() {
req, _ := http.NewRequest("GET", "https://s3.amazonaws.com", nil)
if !credentialsSet() {
SkipSo(http.StatusOK, ShouldEqual, http.StatusOK)
} else {
resp := sign4AndDo(req)
if resp.StatusCode != http.StatusOK {
msg, _ := ioutil.ReadAll(resp.Body)
t.Error(string(msg))
}
So(resp.StatusCode, ShouldEqual, http.StatusOK)
}
})
Convey("A request to EC2 should succeed", func() {
req := newRequest("GET", "https://ec2.amazonaws.com/?Version=2013-10-15&Action=DescribeInstances", nil)
if !credentialsSet() {
SkipSo(http.StatusOK, ShouldEqual, http.StatusOK)
} else {
resp := sign2AndDo(req)
if resp.StatusCode != http.StatusOK {
msg, _ := ioutil.ReadAll(resp.Body)
t.Error(string(msg))
}
So(resp.StatusCode, ShouldEqual, http.StatusOK)
}
})
Convey("A request to SQS should succeed", func() {
req := newRequest("POST", "https://sqs.us-west-2.amazonaws.com", url.Values{
"Action": []string{"ListQueues"},
})
if !credentialsSet() {
SkipSo(http.StatusOK, ShouldEqual, http.StatusOK)
} else {
resp := sign4AndDo(req)
if resp.StatusCode != http.StatusOK {
msg, _ := ioutil.ReadAll(resp.Body)
t.Error(string(msg))
}
So(resp.StatusCode, ShouldEqual, http.StatusOK)
}
})
Convey("A request to SES should succeed", func() {
req := newRequest("GET", "https://email.us-east-1.amazonaws.com/?Action=GetSendStatistics", nil)
if !credentialsSet() {
SkipSo(http.StatusOK, ShouldEqual, http.StatusOK)
} else {
resp := sign3AndDo(req)
if resp.StatusCode != http.StatusOK {
msg, _ := ioutil.ReadAll(resp.Body)
t.Error(string(msg))
}
So(resp.StatusCode, ShouldEqual, http.StatusOK)
}
})
Convey("A request to Route 53 should succeed", func() {
req := newRequest("GET", "https://route53.amazonaws.com/2013-04-01/hostedzone?maxitems=1", nil)
if !credentialsSet() {
SkipSo(http.StatusOK, ShouldEqual, http.StatusOK)
} else {
resp := sign3AndDo(req)
if resp.StatusCode != http.StatusOK {
msg, _ := ioutil.ReadAll(resp.Body)
t.Error(string(msg))
}
So(resp.StatusCode, ShouldEqual, http.StatusOK)
}
})
Convey("A request to SimpleDB should succeed", func() {
req := newRequest("GET", "https://sdb.amazonaws.com/?Action=ListDomains&Version=2009-04-15", nil)
if !credentialsSet() {
SkipSo(http.StatusOK, ShouldEqual, http.StatusOK)
} else {
resp := sign2AndDo(req)
if resp.StatusCode != http.StatusOK {
msg, _ := ioutil.ReadAll(resp.Body)
t.Error(string(msg))
}
So(resp.StatusCode, ShouldEqual, http.StatusOK)
}
})
Convey("If S3Resource env variable is set", func() {
s3res := os.Getenv("S3Resource")
Convey("A URL-signed request to that S3 resource should succeed", func() {
req, _ := http.NewRequest("GET", s3res, nil)
if !credentialsSet() || s3res == "" {
SkipSo(http.StatusOK, ShouldEqual, http.StatusOK)
} else {
resp := signS3UrlAndDo(req)
if resp.StatusCode != http.StatusOK {
msg, _ := ioutil.ReadAll(resp.Body)
t.Error(string(msg))
}
So(resp.StatusCode, ShouldEqual, http.StatusOK)
}
})
})
})
}
func TestSign(t *testing.T) {
Convey("Requests to services using Version 2 should be signed accordingly", t, func() {
reqs := []*http.Request{
newRequest("GET", "https://ec2.amazonaws.com", url.Values{}),
newRequest("GET", "https://elasticache.amazonaws.com/", url.Values{}),
}
for _, req := range reqs {
signedReq := Sign(req)
So(signedReq.URL.Query().Get("SignatureVersion"), ShouldEqual, "2")
}
})
Convey("Requests to services using Version 3 should be signed accordingly", t, func() {
reqs := []*http.Request{
newRequest("GET", "https://route53.amazonaws.com", url.Values{}),
newRequest("GET", "https://email.us-east-1.amazonaws.com/", url.Values{}),
}
for _, req := range reqs {
signedReq := Sign(req)
So(signedReq.Header.Get("X-Amzn-Authorization"), ShouldNotBeBlank)
}
})
Convey("Requests to services using Version 4 should be signed accordingly", t, func() {
reqs := []*http.Request{
newRequest("POST", "https://sqs.amazonaws.com/", url.Values{}),
newRequest("GET", "https://iam.amazonaws.com", url.Values{}),
newRequest("GET", "https://s3.amazonaws.com", url.Values{}),
}
for _, req := range reqs {
signedReq := Sign(req)
So(signedReq.Header.Get("Authorization"), ShouldContainSubstring, ", Signature=")
}
})
var keys Credentials
keys = newKeys()
Convey("Requests to services using existing credentials Version 2 should be signed accordingly", t, func() {
reqs := []*http.Request{
newRequest("GET", "https://ec2.amazonaws.com", url.Values{}),
newRequest("GET", "https://elasticache.amazonaws.com/", url.Values{}),
}
for _, req := range reqs {
signedReq := Sign(req, keys)
So(signedReq.URL.Query().Get("SignatureVersion"), ShouldEqual, "2")
}
})
Convey("Requests to services using existing credentials Version 3 should be signed accordingly", t, func() {
reqs := []*http.Request{
newRequest("GET", "https://route53.amazonaws.com", url.Values{}),
newRequest("GET", "https://email.us-east-1.amazonaws.com/", url.Values{}),
}
for _, req := range reqs {
signedReq := Sign(req, keys)
So(signedReq.Header.Get("X-Amzn-Authorization"), ShouldNotBeBlank)
}
})
Convey("Requests to services using existing credentials Version 4 should be signed accordingly", t, func() {
reqs := []*http.Request{
newRequest("POST", "https://sqs.amazonaws.com/", url.Values{}),
newRequest("GET", "https://iam.amazonaws.com", url.Values{}),
newRequest("GET", "https://s3.amazonaws.com", url.Values{}),
}
for _, req := range reqs {
signedReq := Sign(req, keys)
So(signedReq.Header.Get("Authorization"), ShouldContainSubstring, ", Signature=")
}
})
}
func TestExpiration(t *testing.T) {
var c = &Credentials{}
Convey("Credentials without an expiration can't expire", t, func() {
So(c.expired(), ShouldBeFalse)
})
Convey("Credentials that expire in 5 minutes aren't expired", t, func() {
c.Expiration = time.Now().Add(5 * time.Minute)
So(c.expired(), ShouldBeFalse)
})
Convey("Credentials that expire in 1 minute are expired", t, func() {
c.Expiration = time.Now().Add(1 * time.Minute)
So(c.expired(), ShouldBeTrue)
})
Convey("Credentials that expired 2 hours ago are expired", t, func() {
c.Expiration = time.Now().Add(-2 * time.Hour)
So(c.expired(), ShouldBeTrue)
})
}
func credentialsSet() bool {
var keys Credentials
keys = newKeys()
if keys.AccessKeyID == "" {
return false
} else {
return true
}
}
func newRequest(method string, url string, v url.Values) *http.Request {
req, _ := http.NewRequest(method, url, strings.NewReader(v.Encode()))
return req
}
func sign2AndDo(req *http.Request) *http.Response {
Sign2(req)
resp, _ := client.Do(req)
return resp
}
func sign3AndDo(req *http.Request) *http.Response {
Sign3(req)
resp, _ := client.Do(req)
return resp
}
func sign4AndDo(req *http.Request) *http.Response {
Sign4(req)
resp, _ := client.Do(req)
return resp
}
func signS3AndDo(req *http.Request) *http.Response {
SignS3(req)
resp, _ := client.Do(req)
return resp
}
func signS3UrlAndDo(req *http.Request) *http.Response {
SignS3Url(req, time.Now().AddDate(0, 0, 1))
resp, _ := client.Do(req)
return resp
}
var client = &http.Client{}

View File

@ -0,0 +1,300 @@
package awsauth
import (
"bufio"
"bytes"
"crypto/hmac"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"strings"
"time"
)
type location struct {
ec2 bool
checked bool
}
var loc *location
// serviceAndRegion parsers a hostname to find out which ones it is.
// http://docs.aws.amazon.com/general/latest/gr/rande.html
func serviceAndRegion(host string) (service string, region string) {
// These are the defaults if the hostname doesn't suggest something else
region = "us-east-1"
service = "s3"
parts := strings.Split(host, ".")
if len(parts) == 4 {
// Either service.region.amazonaws.com or virtual-host.region.amazonaws.com
if parts[1] == "s3" {
service = "s3"
} else if strings.HasPrefix(parts[1], "s3-") {
region = parts[1][3:]
service = "s3"
} else {
service = parts[0]
region = parts[1]
}
} else {
// Either service.amazonaws.com or s3-region.amazonaws.com
if strings.HasPrefix(parts[0], "s3-") {
region = parts[0][3:]
} else {
service = parts[0]
}
}
if region == "external-1" {
region = "us-east-1"
}
return
}
// newKeys produces a set of credentials based on the environment
func newKeys() (newCredentials Credentials) {
// First use credentials from environment variables
newCredentials.AccessKeyID = os.Getenv(envAccessKeyID)
newCredentials.SecretAccessKey = os.Getenv(envSecretAccessKey)
newCredentials.SecurityToken = os.Getenv(envSecurityToken)
// If there is no Access Key and you are on EC2, get the key from the role
if newCredentials.AccessKeyID == "" && onEC2() {
newCredentials = *getIAMRoleCredentials()
}
// If the key is expiring, get a new key
if newCredentials.expired() && onEC2() {
newCredentials = *getIAMRoleCredentials()
}
return newCredentials
}
// checkKeys gets credentials depending on if any were passed in as an argument
// or it makes new ones based on the environment.
func chooseKeys(cred []Credentials) Credentials {
if len(cred) == 0 {
return newKeys()
} else {
return cred[0]
}
}
// onEC2 checks to see if the program is running on an EC2 instance.
// It does this by looking for the EC2 metadata service.
// This caches that information in a struct so that it doesn't waste time.
func onEC2() bool {
if loc == nil {
loc = &location{}
}
if !(loc.checked) {
c, err := net.DialTimeout("tcp", "169.254.169.254:80", time.Second)
if err != nil {
loc.ec2 = false
} else {
c.Close()
loc.ec2 = true
}
loc.checked = true
}
return loc.ec2
}
// getIAMRoleList gets a list of the roles that are available to this instance
func getIAMRoleList() []string {
var roles []string
url := "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return roles
}
resp, err := client.Do(req)
if err != nil {
return roles
}
defer resp.Body.Close()
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
roles = append(roles, scanner.Text())
}
return roles
}
func getIAMRoleCredentials() *Credentials {
roles := getIAMRoleList()
if len(roles) < 1 {
return &Credentials{}
}
// Use the first role in the list
role := roles[0]
url := "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
// Create the full URL of the role
var buffer bytes.Buffer
buffer.WriteString(url)
buffer.WriteString(role)
roleurl := buffer.String()
// Get the role
rolereq, err := http.NewRequest("GET", roleurl, nil)
if err != nil {
return &Credentials{}
}
client := &http.Client{}
roleresp, err := client.Do(rolereq)
if err != nil {
return &Credentials{}
}
defer roleresp.Body.Close()
rolebuf := new(bytes.Buffer)
rolebuf.ReadFrom(roleresp.Body)
creds := Credentials{}
err = json.Unmarshal(rolebuf.Bytes(), &creds)
if err != nil {
return &Credentials{}
}
return &creds
}
func augmentRequestQuery(req *http.Request, values url.Values) *http.Request {
for key, arr := range req.URL.Query() {
for _, val := range arr {
values.Set(key, val)
}
}
req.URL.RawQuery = values.Encode()
return req
}
func hmacSHA256(key []byte, content string) []byte {
mac := hmac.New(sha256.New, key)
mac.Write([]byte(content))
return mac.Sum(nil)
}
func hmacSHA1(key []byte, content string) []byte {
mac := hmac.New(sha1.New, key)
mac.Write([]byte(content))
return mac.Sum(nil)
}
func hashSHA256(content []byte) string {
h := sha256.New()
h.Write(content)
return fmt.Sprintf("%x", h.Sum(nil))
}
func hashMD5(content []byte) string {
h := md5.New()
h.Write(content)
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
func readAndReplaceBody(req *http.Request) []byte {
if req.Body == nil {
return []byte{}
}
payload, _ := ioutil.ReadAll(req.Body)
req.Body = ioutil.NopCloser(bytes.NewReader(payload))
return payload
}
func concat(delim string, str ...string) string {
return strings.Join(str, delim)
}
var now = func() time.Time {
return time.Now().UTC()
}
func normuri(uri string) string {
parts := strings.Split(uri, "/")
for i := range parts {
parts[i] = encodePathFrag(parts[i])
}
return strings.Join(parts, "/")
}
func encodePathFrag(s string) string {
hexCount := 0
for i := 0; i < len(s); i++ {
c := s[i]
if shouldEscape(c) {
hexCount++
}
}
t := make([]byte, len(s)+2*hexCount)
j := 0
for i := 0; i < len(s); i++ {
c := s[i]
if shouldEscape(c) {
t[j] = '%'
t[j+1] = "0123456789ABCDEF"[c>>4]
t[j+2] = "0123456789ABCDEF"[c&15]
j += 3
} else {
t[j] = c
j++
}
}
return string(t)
}
func shouldEscape(c byte) bool {
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' {
return false
}
if '0' <= c && c <= '9' {
return false
}
if c == '-' || c == '_' || c == '.' || c == '~' {
return false
}
return true
}
func normquery(v url.Values) string {
qs := v.Encode()
// Go encodes a space as '+' but Amazon require '%20'. Luckily any '+' in the
// original query string has been percent escaped so all '+' chars that are left
// were originally spaces.
return strings.Replace(qs, "+", "%20", -1)
}

View File

@ -0,0 +1,89 @@
package awsauth
import (
"testing"
"net/url"
. "github.com/smartystreets/goconvey/convey"
)
func TestCommonFunctions(t *testing.T) {
Convey("Service and region should be properly extracted from host strings", t, func() {
service, region := serviceAndRegion("sqs.us-west-2.amazonaws.com")
So(service, ShouldEqual, "sqs")
So(region, ShouldEqual, "us-west-2")
service, region = serviceAndRegion("iam.amazonaws.com")
So(service, ShouldEqual, "iam")
So(region, ShouldEqual, "us-east-1")
service, region = serviceAndRegion("sns.us-west-2.amazonaws.com")
So(service, ShouldEqual, "sns")
So(region, ShouldEqual, "us-west-2")
service, region = serviceAndRegion("bucketname.s3.amazonaws.com")
So(service, ShouldEqual, "s3")
So(region, ShouldEqual, "us-east-1")
service, region = serviceAndRegion("s3.amazonaws.com")
So(service, ShouldEqual, "s3")
So(region, ShouldEqual, "us-east-1")
service, region = serviceAndRegion("s3-us-west-1.amazonaws.com")
So(service, ShouldEqual, "s3")
So(region, ShouldEqual, "us-west-1")
service, region = serviceAndRegion("s3-external-1.amazonaws.com")
So(service, ShouldEqual, "s3")
So(region, ShouldEqual, "us-east-1")
})
Convey("MD5 hashes should be properly computed and base-64 encoded", t, func() {
input := []byte("Pretend this is a REALLY long byte array...")
actual := hashMD5(input)
So(actual, ShouldEqual, "KbVTY8Vl6VccnzQf1AGOFw==")
})
Convey("SHA-256 hashes should be properly hex-encoded (base 16)", t, func() {
input := []byte("This is... Sparta!!")
actual := hashSHA256(input)
So(actual, ShouldEqual, "5c81a4ef1172e89b1a9d575f4cd82f4ed20ea9137e61aa7f1ab936291d24e79a")
})
Convey("Given a key and contents", t, func() {
key := []byte("asdf1234")
contents := "SmartyStreets was here"
Convey("HMAC-SHA256 should be properly computed", func() {
expected := []byte{65, 46, 186, 78, 2, 155, 71, 104, 49, 37, 5, 66, 195, 129, 159, 227, 239, 53, 240, 107, 83, 21, 235, 198, 238, 216, 108, 149, 143, 222, 144, 94}
actual := hmacSHA256(key, contents)
So(actual, ShouldResemble, expected)
})
Convey("HMAC-SHA1 should be properly computed", func() {
expected := []byte{164, 77, 252, 0, 87, 109, 207, 110, 163, 75, 228, 122, 83, 255, 233, 237, 125, 206, 85, 70}
actual := hmacSHA1(key, contents)
So(actual, ShouldResemble, expected)
})
})
Convey("Strings should be properly concatenated with a delimiter", t, func() {
So(concat("\n", "Test1", "Test2"), ShouldEqual, "Test1\nTest2")
So(concat(".", "Test1"), ShouldEqual, "Test1")
So(concat("\t", "1", "2", "3", "4"), ShouldEqual, "1\t2\t3\t4")
})
Convey("URI components should be properly encoded", t, func() {
So(normuri("/-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"), ShouldEqual, "/-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
So(normuri("/ /foo"), ShouldEqual, "/%20/foo")
So(normuri("/(foo)"), ShouldEqual, "/%28foo%29")
})
Convey("URI query strings should be properly encoded", t, func() {
So(normquery(url.Values{"p": []string{" +&;-=._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"}}), ShouldEqual, "p=%20%2B%26%3B-%3D._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
})
}

View File

@ -0,0 +1,121 @@
package awsauth
import (
"encoding/base64"
"net/http"
"sort"
"strconv"
"strings"
"time"
)
func signatureS3(stringToSign string, keys Credentials) string {
hashed := hmacSHA1([]byte(keys.SecretAccessKey), stringToSign)
return base64.StdEncoding.EncodeToString(hashed)
}
func stringToSignS3(req *http.Request) string {
str := req.Method + "\n"
if req.Header.Get("Content-Md5") != "" {
str += req.Header.Get("Content-Md5")
} else {
body := readAndReplaceBody(req)
if len(body) > 0 {
str += hashMD5(body)
}
}
str += "\n"
str += req.Header.Get("Content-Type") + "\n"
if req.Header.Get("Date") != "" {
str += req.Header.Get("Date")
} else {
str += timestampS3()
}
str += "\n"
canonicalHeaders := canonicalAmzHeadersS3(req)
if canonicalHeaders != "" {
str += canonicalHeaders
}
str += canonicalResourceS3(req)
return str
}
func stringToSignS3Url(method string, expire time.Time, path string) string {
return method + "\n\n\n" + timeToUnixEpochString(expire) + "\n" + path
}
func timeToUnixEpochString(t time.Time) string {
return strconv.FormatInt(t.Unix(), 10)
}
func canonicalAmzHeadersS3(req *http.Request) string {
var headers []string
for header := range req.Header {
standardized := strings.ToLower(strings.TrimSpace(header))
if strings.HasPrefix(standardized, "x-amz") {
headers = append(headers, standardized)
}
}
sort.Strings(headers)
for i, header := range headers {
headers[i] = header + ":" + strings.Replace(req.Header.Get(header), "\n", " ", -1)
}
if len(headers) > 0 {
return strings.Join(headers, "\n") + "\n"
} else {
return ""
}
}
func canonicalResourceS3(req *http.Request) string {
res := ""
if isS3VirtualHostedStyle(req) {
bucketname := strings.Split(req.Host, ".")[0]
res += "/" + bucketname
}
res += req.URL.Path
for _, subres := range strings.Split(subresourcesS3, ",") {
if strings.HasPrefix(req.URL.RawQuery, subres) {
res += "?" + subres
}
}
return res
}
func prepareRequestS3(req *http.Request) *http.Request {
req.Header.Set("Date", timestampS3())
if req.URL.Path == "" {
req.URL.Path += "/"
}
return req
}
// Info: http://docs.aws.amazon.com/AmazonS3/latest/dev/VirtualHosting.html
func isS3VirtualHostedStyle(req *http.Request) bool {
service, _ := serviceAndRegion(req.Host)
return service == "s3" && strings.Count(req.Host, ".") == 3
}
func timestampS3() string {
return now().Format(timeFormatS3)
}
const (
timeFormatS3 = time.RFC1123Z
subresourcesS3 = "acl,lifecycle,location,logging,notification,partNumber,policy,requestPayment,torrent,uploadId,uploads,versionId,versioning,versions,website"
)

View File

@ -0,0 +1,152 @@
package awsauth
import (
"fmt"
"net/http"
"net/url"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
)
func TestSignatureS3(t *testing.T) {
// http://docs.aws.amazon.com/AmazonS3/2006-03-01/dev/RESTAuthentication.html
// Note: S3 now supports signed signature version 4
// (but signed URL requests still utilize a lot of the same functionality)
Convey("Given a GET request to Amazon S3", t, func() {
keys := *testCredS3
req := test_plainRequestS3()
// Mock time
now = func() time.Time {
parsed, _ := time.Parse(timeFormatS3, exampleReqTsS3)
return parsed
}
Convey("The request should be prepared with a Date header", func() {
prepareRequestS3(req)
So(req.Header.Get("Date"), ShouldEqual, exampleReqTsS3)
})
Convey("The CanonicalizedAmzHeaders should be built properly", func() {
req2 := test_headerRequestS3()
actual := canonicalAmzHeadersS3(req2)
So(actual, ShouldEqual, expectedCanonAmzHeadersS3)
})
Convey("The CanonicalizedResource should be built properly", func() {
actual := canonicalResourceS3(req)
So(actual, ShouldEqual, expectedCanonResourceS3)
})
Convey("The string to sign should be correct", func() {
actual := stringToSignS3(req)
So(actual, ShouldEqual, expectedStringToSignS3)
})
Convey("The final signature string should be exactly correct", func() {
actual := signatureS3(stringToSignS3(req), keys)
So(actual, ShouldEqual, "bWq2s1WEIj+Ydj0vQ697zp+IXMU=")
})
})
Convey("Given a GET request for a resource on S3 for query string authentication", t, func() {
keys := *testCredS3
req, _ := http.NewRequest("GET", "https://johnsmith.s3.amazonaws.com/johnsmith/photos/puppy.jpg", nil)
now = func() time.Time {
parsed, _ := time.Parse(timeFormatS3, exampleReqTsS3)
return parsed
}
Convey("The string to sign should be correct", func() {
actual := stringToSignS3Url("GET", now(), req.URL.Path)
So(actual, ShouldEqual, expectedStringToSignS3Url)
})
Convey("The signature of string to sign should be correct", func() {
actual := signatureS3(expectedStringToSignS3Url, keys)
So(actual, ShouldEqual, "R2K/+9bbnBIbVDCs7dqlz3XFtBQ=")
})
Convey("The finished signed URL should be correct", func() {
expiry := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
So(SignS3Url(req, expiry, keys).URL.String(), ShouldEqual, expectedSignedS3Url)
})
})
}
func TestS3STSRequestPreparer(t *testing.T) {
Convey("Given a plain request with no custom headers", t, func() {
req := test_plainRequestS3()
Convey("And a set of credentials with an STS token", func() {
keys := *testCredS3WithSTS
Convey("It should include an X-Amz-Security-Token when the request is signed", func() {
actualSigned := SignS3(req, keys)
actual := actualSigned.Header.Get("X-Amz-Security-Token")
So(actual, ShouldNotBeBlank)
So(actual, ShouldEqual, testCredS3WithSTS.SecurityToken)
})
})
})
}
func test_plainRequestS3() *http.Request {
req, _ := http.NewRequest("GET", "https://johnsmith.s3.amazonaws.com/photos/puppy.jpg", nil)
return req
}
func test_headerRequestS3() *http.Request {
req := test_plainRequestS3()
req.Header.Set("X-Amz-Meta-Something", "more foobar")
req.Header.Set("X-Amz-Date", "foobar")
req.Header.Set("X-Foobar", "nanoo-nanoo")
return req
}
func TestCanonical(t *testing.T) {
expectedCanonicalString := "PUT\nc8fdb181845a4ca6b8fec737b3581d76\ntext/html\nThu, 17 Nov 2005 18:49:58 GMT\nx-amz-magic:abracadabra\nx-amz-meta-author:foo@bar.com\n/quotes/nelson"
origUrl := "https://s3.amazonaws.com/"
resource := "/quotes/nelson"
u, _ := url.ParseRequestURI(origUrl)
u.Path = resource
urlStr := fmt.Sprintf("%v", u)
req, _ := http.NewRequest("PUT", urlStr, nil)
req.Header.Add("Content-Md5", "c8fdb181845a4ca6b8fec737b3581d76")
req.Header.Add("Content-Type", "text/html")
req.Header.Add("Date", "Thu, 17 Nov 2005 18:49:58 GMT")
req.Header.Add("X-Amz-Meta-Author", "foo@bar.com")
req.Header.Add("X-Amz-Magic", "abracadabra")
if stringToSignS3(req) != expectedCanonicalString {
t.Errorf("----Got\n***%s***\n----Expected\n***%s***", stringToSignS3(req), expectedCanonicalString)
}
}
var (
testCredS3 = &Credentials{
AccessKeyID: "AKIAIOSFODNN7EXAMPLE",
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
}
testCredS3WithSTS = &Credentials{
AccessKeyID: "AKIDEXAMPLE",
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
SecurityToken: "AQoDYXdzEHcaoAJ1Aqwx1Sum0iW2NQjXJcWlKR7vuB6lnAeGBaQnjDRZPVyniwc48ml5hx+0qiXenVJdfusMMl9XLhSncfhx9Rb1UF8IAOaQ+CkpWXvoH67YYN+93dgckSVgVEBRByTl/BvLOZhe0ii/pOWkuQtBm5T7lBHRe4Dfmxy9X6hd8L3FrWxgnGV3fWZ3j0gASdYXaa+VBJlU0E2/GmCzn3T+t2mjYaeoInAnYVKVpmVMOrh6lNAeETTOHElLopblSa7TAmROq5xHIyu4a9i2qwjERTwa3Yk4Jk6q7JYVA5Cu7kS8wKVml8LdzzCTsy+elJgvH+Jf6ivpaHt/En0AJ5PZUJDev2+Y5+9j4AYfrmXfm4L73DC1ZJFJrv+Yh+EXAMPLE=",
}
expectedCanonAmzHeadersS3 = "x-amz-date:foobar\nx-amz-meta-something:more foobar\n"
expectedCanonResourceS3 = "/johnsmith/photos/puppy.jpg"
expectedStringToSignS3 = "GET\n\n\nTue, 27 Mar 2007 19:36:42 +0000\n/johnsmith/photos/puppy.jpg"
expectedStringToSignS3Url = "GET\n\n\n1175024202\n/johnsmith/photos/puppy.jpg"
expectedSignedS3Url = "https://johnsmith.s3.amazonaws.com/johnsmith/photos/puppy.jpg?AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&Expires=1257894000&Signature=X%2FarTLAJP08uP1Bsap52rwmsVok%3D"
exampleReqTsS3 = "Tue, 27 Mar 2007 19:36:42 +0000"
)

View File

@ -0,0 +1,50 @@
package awsauth
import (
"encoding/base64"
"net/http"
"net/url"
"strings"
)
func prepareRequestV2(req *http.Request, keys Credentials) *http.Request {
keyID := keys.AccessKeyID
values := url.Values{}
values.Set("AWSAccessKeyId", keyID)
values.Set("SignatureVersion", "2")
values.Set("SignatureMethod", "HmacSHA256")
values.Set("Timestamp", timestampV2())
augmentRequestQuery(req, values)
if req.URL.Path == "" {
req.URL.Path += "/"
}
return req
}
func stringToSignV2(req *http.Request) string {
str := req.Method + "\n"
str += strings.ToLower(req.URL.Host) + "\n"
str += req.URL.Path + "\n"
str += canonicalQueryStringV2(req)
return str
}
func signatureV2(strToSign string, keys Credentials) string {
hashed := hmacSHA256([]byte(keys.SecretAccessKey), strToSign)
return base64.StdEncoding.EncodeToString(hashed)
}
func canonicalQueryStringV2(req *http.Request) string {
return req.URL.RawQuery
}
func timestampV2() string {
return now().Format(timeFormatV2)
}
const timeFormatV2 = "2006-01-02T15:04:05"

View File

@ -0,0 +1,125 @@
package awsauth
import (
"net/http"
"net/url"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
)
func TestSignature2(t *testing.T) {
// http://docs.aws.amazon.com/general/latest/gr/signature-version-2.html
Convey("Given bogus credentials", t, func() {
keys := *testCredV2
// Mock time
now = func() time.Time {
parsed, _ := time.Parse(timeFormatV2, exampleReqTsV2)
return parsed
}
Convey("Given a plain request that is unprepared", func() {
req := test_plainRequestV2()
Convey("The request should be prepared to be signed", func() {
expectedUnsigned := test_unsignedRequestV2()
prepareRequestV2(req, keys)
So(req, ShouldResemble, expectedUnsigned)
})
})
Convey("Given a prepared, but unsigned, request", func() {
req := test_unsignedRequestV2()
Convey("The canonical query string should be correct", func() {
actual := canonicalQueryStringV2(req)
expected := canonicalQsV2
So(actual, ShouldEqual, expected)
})
Convey("The absolute path should be extracted correctly", func() {
So(req.URL.Path, ShouldEqual, "/")
})
Convey("The string to sign should be well-formed", func() {
actual := stringToSignV2(req)
So(actual, ShouldEqual, expectedStringToSignV2)
})
Convey("The resulting signature should be correct", func() {
actual := signatureV2(stringToSignV2(req), keys)
So(actual, ShouldEqual, "i91nKc4PWAt0JJIdXwz9HxZCJDdiy6cf/Mj6vPxyYIs=")
})
Convey("The final signed request should be correctly formed", func() {
Sign2(req, keys)
actual := req.URL.String()
So(actual, ShouldResemble, expectedFinalUrlV2)
})
})
})
}
func TestVersion2STSRequestPreparer(t *testing.T) {
Convey("Given a plain request ", t, func() {
req := test_plainRequestV2()
Convey("And a set of credentials with an STS token", func() {
var keys Credentials
keys = *testCredV2WithSTS
Convey("It should include the SecurityToken parameter when the request is signed", func() {
actualSigned := Sign2(req, keys)
actual := actualSigned.URL.Query()["SecurityToken"][0]
So(actual, ShouldNotBeBlank)
So(actual, ShouldEqual, testCredV2WithSTS.SecurityToken)
})
})
})
}
func test_plainRequestV2() *http.Request {
values := url.Values{}
values.Set("Action", "DescribeJobFlows")
values.Set("Version", "2009-03-31")
url := baseUrlV2 + "?" + values.Encode()
req, err := http.NewRequest("GET", url, nil)
if err != nil {
panic(err)
}
return req
}
func test_unsignedRequestV2() *http.Request {
req := test_plainRequestV2()
newUrl, _ := url.Parse(baseUrlV2 + "/?" + canonicalQsV2)
req.URL = newUrl
return req
}
var (
testCredV2 = &Credentials{
AccessKeyID: "AKIAIOSFODNN7EXAMPLE",
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
}
testCredV2WithSTS = &Credentials{
AccessKeyID: "AKIDEXAMPLE",
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
SecurityToken: "AQoDYXdzEHcaoAJ1Aqwx1Sum0iW2NQjXJcWlKR7vuB6lnAeGBaQnjDRZPVyniwc48ml5hx+0qiXenVJdfusMMl9XLhSncfhx9Rb1UF8IAOaQ+CkpWXvoH67YYN+93dgckSVgVEBRByTl/BvLOZhe0ii/pOWkuQtBm5T7lBHRe4Dfmxy9X6hd8L3FrWxgnGV3fWZ3j0gASdYXaa+VBJlU0E2/GmCzn3T+t2mjYaeoInAnYVKVpmVMOrh6lNAeETTOHElLopblSa7TAmROq5xHIyu4a9i2qwjERTwa3Yk4Jk6q7JYVA5Cu7kS8wKVml8LdzzCTsy+elJgvH+Jf6ivpaHt/En0AJ5PZUJDev2+Y5+9j4AYfrmXfm4L73DC1ZJFJrv+Yh+EXAMPLE=",
}
exampleReqTsV2 = "2011-10-03T15:19:30"
baseUrlV2 = "https://elasticmapreduce.amazonaws.com"
canonicalQsV2 = "AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&Action=DescribeJobFlows&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2011-10-03T15%3A19%3A30&Version=2009-03-31"
expectedStringToSignV2 = "GET\nelasticmapreduce.amazonaws.com\n/\n" + canonicalQsV2
expectedFinalUrlV2 = baseUrlV2 + "/?AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&Action=DescribeJobFlows&Signature=i91nKc4PWAt0JJIdXwz9HxZCJDdiy6cf%2FMj6vPxyYIs%3D&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2011-10-03T15%3A19%3A30&Version=2009-03-31"
)

View File

@ -0,0 +1,58 @@
// Thanks to Michael Vierling for contributing sign3.go
package awsauth
import (
"encoding/base64"
"net/http"
"time"
)
func stringToSignV3(req *http.Request) string {
// TASK 1. http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/RESTAuthentication.html#StringToSign
return req.Header.Get("Date") + req.Header.Get("x-amz-nonce")
}
func signatureV3(stringToSign string, keys Credentials) string {
// TASK 2. http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/RESTAuthentication.html#Signature
hash := hmacSHA256([]byte(keys.SecretAccessKey), stringToSign)
return base64.StdEncoding.EncodeToString(hash)
}
func buildAuthHeaderV3(signature string, keys Credentials) string {
// TASK 3. http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/RESTAuthentication.html#AuthorizationHeader
return "AWS3-HTTPS AWSAccessKeyId=" + keys.AccessKeyID +
", Algorithm=HmacSHA256" +
", Signature=" + signature
}
func prepareRequestV3(req *http.Request) *http.Request {
ts := timestampV3()
necessaryDefaults := map[string]string{
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"x-amz-date": ts,
"Date": ts,
"x-amz-nonce": "",
}
for header, value := range necessaryDefaults {
if req.Header.Get(header) == "" {
req.Header.Set(header, value)
}
}
if req.URL.Path == "" {
req.URL.Path += "/"
}
return req
}
func timestampV3() string {
return now().Format(timeFormatV3)
}
const timeFormatV3 = time.RFC1123

View File

@ -0,0 +1,121 @@
package awsauth
import (
"net/http"
"net/url"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
)
func TestSignature3(t *testing.T) {
// http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/RESTAuthentication.html
// http://docs.aws.amazon.com/ses/latest/DeveloperGuide/query-interface-authentication.html
Convey("Given bogus credentials", t, func() {
keys := *testCredV3
// Mock time
now = func() time.Time {
parsed, _ := time.Parse(timeFormatV3, exampleReqTsV3)
return parsed
}
Convey("Given a plain request that is unprepared", func() {
req := test_plainRequestV3()
Convey("The request should be prepared to be signed", func() {
expectedUnsigned := test_unsignedRequestV3()
prepareRequestV3(req)
So(req, ShouldResemble, expectedUnsigned)
})
})
Convey("Given a prepared, but unsigned, request", func() {
req := test_unsignedRequestV3()
Convey("The absolute path should be extracted correctly", func() {
So(req.URL.Path, ShouldEqual, "/")
})
Convey("The string to sign should be well-formed", func() {
actual := stringToSignV3(req)
So(actual, ShouldEqual, expectedStringToSignV3)
})
Convey("The resulting signature should be correct", func() {
actual := signatureV3(stringToSignV3(req), keys)
So(actual, ShouldEqual, "PjAJ6buiV6l4WyzmmuwtKE59NJXVg5Dr3Sn4PCMZ0Yk=")
})
Convey("The final signed request should be correctly formed", func() {
Sign3(req, keys)
actual := req.Header.Get("X-Amzn-Authorization")
So(actual, ShouldResemble, expectedAuthHeaderV3)
})
})
})
}
func test_plainRequestV3() *http.Request {
values := url.Values{}
values.Set("Action", "GetSendStatistics")
values.Set("Version", "2010-12-01")
url := baseUrlV3 + "/?" + values.Encode()
req, err := http.NewRequest("GET", url, nil)
if err != nil {
panic(err)
}
return req
}
func test_unsignedRequestV3() *http.Request {
req := test_plainRequestV3()
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
req.Header.Set("x-amz-date", exampleReqTsV3)
req.Header.Set("Date", exampleReqTsV3)
req.Header.Set("x-amz-nonce", "")
return req
}
func TestVersion3STSRequestPreparer(t *testing.T) {
Convey("Given a plain request with no custom headers", t, func() {
req := test_plainRequestV3()
Convey("And a set of credentials with an STS token", func() {
var keys Credentials
keys = *testCredV3WithSTS
Convey("It should include an X-Amz-Security-Token when the request is signed", func() {
actualSigned := Sign3(req, keys)
actual := actualSigned.Header.Get("X-Amz-Security-Token")
So(actual, ShouldNotBeBlank)
So(actual, ShouldEqual, testCredV4WithSTS.SecurityToken)
})
})
})
}
var (
testCredV3 = &Credentials{
AccessKeyID: "AKIAIOSFODNN7EXAMPLE",
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
}
testCredV3WithSTS = &Credentials{
AccessKeyID: "AKIDEXAMPLE",
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
SecurityToken: "AQoDYXdzEHcaoAJ1Aqwx1Sum0iW2NQjXJcWlKR7vuB6lnAeGBaQnjDRZPVyniwc48ml5hx+0qiXenVJdfusMMl9XLhSncfhx9Rb1UF8IAOaQ+CkpWXvoH67YYN+93dgckSVgVEBRByTl/BvLOZhe0ii/pOWkuQtBm5T7lBHRe4Dfmxy9X6hd8L3FrWxgnGV3fWZ3j0gASdYXaa+VBJlU0E2/GmCzn3T+t2mjYaeoInAnYVKVpmVMOrh6lNAeETTOHElLopblSa7TAmROq5xHIyu4a9i2qwjERTwa3Yk4Jk6q7JYVA5Cu7kS8wKVml8LdzzCTsy+elJgvH+Jf6ivpaHt/En0AJ5PZUJDev2+Y5+9j4AYfrmXfm4L73DC1ZJFJrv+Yh+EXAMPLE=",
}
exampleReqTsV3 = "Thu, 14 Aug 2008 17:08:48 GMT"
baseUrlV3 = "https://email.us-east-1.amazonaws.com"
expectedStringToSignV3 = exampleReqTsV3
expectedAuthHeaderV3 = "AWS3-HTTPS AWSAccessKeyId=" + testCredV3.AccessKeyID + ", Algorithm=HmacSHA256, Signature=PjAJ6buiV6l4WyzmmuwtKE59NJXVg5Dr3Sn4PCMZ0Yk="
)

View File

@ -0,0 +1,107 @@
package awsauth
import (
"encoding/hex"
"net/http"
"sort"
"strings"
)
func hashedCanonicalRequestV4(req *http.Request, meta *metadata) string {
// TASK 1. http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
payload := readAndReplaceBody(req)
payloadHash := hashSHA256(payload)
req.Header.Set("X-Amz-Content-Sha256", payloadHash)
// Set this in header values to make it appear in the range of headers to sign
req.Header.Set("Host", req.Host)
var sortedHeaderKeys []string
for key, _ := range req.Header {
switch key {
case "Content-Type", "Content-Md5", "Host":
default:
if !strings.HasPrefix(key, "X-Amz-") {
continue
}
}
sortedHeaderKeys = append(sortedHeaderKeys, strings.ToLower(key))
}
sort.Strings(sortedHeaderKeys)
var headersToSign string
for _, key := range sortedHeaderKeys {
value := strings.TrimSpace(req.Header.Get(key))
headersToSign += key + ":" + value + "\n"
}
meta.signedHeaders = concat(";", sortedHeaderKeys...)
canonicalRequest := concat("\n", req.Method, normuri(req.URL.Path), normquery(req.URL.Query()), headersToSign, meta.signedHeaders, payloadHash)
return hashSHA256([]byte(canonicalRequest))
}
func stringToSignV4(req *http.Request, hashedCanonReq string, meta *metadata) string {
// TASK 2. http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
requestTs := req.Header.Get("X-Amz-Date")
meta.algorithm = "AWS4-HMAC-SHA256"
meta.service, meta.region = serviceAndRegion(req.Host)
meta.date = tsDateV4(requestTs)
meta.credentialScope = concat("/", meta.date, meta.region, meta.service, "aws4_request")
return concat("\n", meta.algorithm, requestTs, meta.credentialScope, hashedCanonReq)
}
func signatureV4(signingKey []byte, stringToSign string) string {
// TASK 3. http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
return hex.EncodeToString(hmacSHA256(signingKey, stringToSign))
}
func prepareRequestV4(req *http.Request) *http.Request {
necessaryDefaults := map[string]string{
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"X-Amz-Date": timestampV4(),
}
for header, value := range necessaryDefaults {
if req.Header.Get(header) == "" {
req.Header.Set(header, value)
}
}
if req.URL.Path == "" {
req.URL.Path += "/"
}
return req
}
func signingKeyV4(secretKey, date, region, service string) []byte {
kDate := hmacSHA256([]byte("AWS4"+secretKey), date)
kRegion := hmacSHA256(kDate, region)
kService := hmacSHA256(kRegion, service)
kSigning := hmacSHA256(kService, "aws4_request")
return kSigning
}
func buildAuthHeaderV4(signature string, meta *metadata, keys Credentials) string {
credential := keys.AccessKeyID + "/" + meta.credentialScope
return meta.algorithm +
" Credential=" + credential +
", SignedHeaders=" + meta.signedHeaders +
", Signature=" + signature
}
func timestampV4() string {
return now().Format(timeFormatV4)
}
func tsDateV4(timestamp string) string {
return timestamp[:8]
}
const timeFormatV4 = "20060102T150405Z"

View File

@ -0,0 +1,216 @@
package awsauth
import (
"net/http"
"net/url"
"strings"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestVersion4RequestPreparer(t *testing.T) {
Convey("Given a plain request with no custom headers", t, func() {
req := test_plainRequestV4(false)
expectedUnsigned := test_unsignedRequestV4(true, false)
expectedUnsigned.Header.Set("X-Amz-Date", timestampV4())
Convey("The necessary, default headers should be appended", func() {
prepareRequestV4(req)
So(req, ShouldResemble, expectedUnsigned)
})
Convey("Forward-slash should be appended to URI if not present", func() {
prepareRequestV4(req)
So(req.URL.Path, ShouldEqual, "/")
})
Convey("And a set of credentials", func() {
var keys Credentials
keys = *testCredV4
Convey("It should be signed with an Authorization header", func() {
actualSigned := Sign4(req, keys)
actual := actualSigned.Header.Get("Authorization")
So(actual, ShouldNotBeBlank)
So(actual, ShouldContainSubstring, "Credential="+testCredV4.AccessKeyID)
So(actual, ShouldContainSubstring, "SignedHeaders=")
So(actual, ShouldContainSubstring, "Signature=")
So(actual, ShouldContainSubstring, "AWS4")
})
})
})
Convey("Given a request with custom, necessary headers", t, func() {
Convey("The custom, necessary headers must not be changed", func() {
req := test_unsignedRequestV4(true, false)
prepareRequestV4(req)
So(req, ShouldResemble, test_unsignedRequestV4(true, false))
})
})
}
func TestVersion4STSRequestPreparer(t *testing.T) {
Convey("Given a plain request with no custom headers", t, func() {
req := test_plainRequestV4(false)
Convey("And a set of credentials with an STS token", func() {
var keys Credentials
keys = *testCredV4WithSTS
Convey("It should include an X-Amz-Security-Token when the request is signed", func() {
actualSigned := Sign4(req, keys)
actual := actualSigned.Header.Get("X-Amz-Security-Token")
So(actual, ShouldNotBeBlank)
So(actual, ShouldEqual, testCredV4WithSTS.SecurityToken)
})
})
})
}
func TestVersion4SigningTasks(t *testing.T) {
// http://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
Convey("Given a bogus request and credentials from AWS documentation with an additional meta tag", t, func() {
req := test_unsignedRequestV4(true, true)
meta := new(metadata)
Convey("(Task 1) The canonical request should be built correctly", func() {
hashedCanonReq := hashedCanonicalRequestV4(req, meta)
So(hashedCanonReq, ShouldEqual, expectingV4["CanonicalHash"])
})
Convey("(Task 2) The string to sign should be built correctly", func() {
hashedCanonReq := hashedCanonicalRequestV4(req, meta)
stringToSign := stringToSignV4(req, hashedCanonReq, meta)
So(stringToSign, ShouldEqual, expectingV4["StringToSign"])
})
Convey("(Task 3) The version 4 signed signature should be correct", func() {
hashedCanonReq := hashedCanonicalRequestV4(req, meta)
stringToSign := stringToSignV4(req, hashedCanonReq, meta)
signature := signatureV4(test_signingKeyV4(), stringToSign)
So(signature, ShouldEqual, expectingV4["SignatureV4"])
})
})
}
func TestSignature4Helpers(t *testing.T) {
keys := *testCredV4
Convey("The signing key should be properly generated", t, func() {
expected := []byte{152, 241, 216, 137, 254, 196, 244, 66, 26, 220, 82, 43, 171, 12, 225, 248, 46, 105, 41, 194, 98, 237, 21, 229, 169, 76, 144, 239, 209, 227, 176, 231}
actual := test_signingKeyV4()
So(actual, ShouldResemble, expected)
})
Convey("Authorization headers should be built properly", t, func() {
meta := &metadata{
algorithm: "AWS4-HMAC-SHA256",
credentialScope: "20110909/us-east-1/iam/aws4_request",
signedHeaders: "content-type;host;x-amz-date",
}
expected := expectingV4["AuthHeader"] + expectingV4["SignatureV4"]
actual := buildAuthHeaderV4(expectingV4["SignatureV4"], meta, keys)
So(actual, ShouldEqual, expected)
})
Convey("Timestamps should be in the correct format, in UTC time", t, func() {
actual := timestampV4()
So(len(actual), ShouldEqual, 16)
So(actual, ShouldNotContainSubstring, ":")
So(actual, ShouldNotContainSubstring, "-")
So(actual, ShouldNotContainSubstring, " ")
So(actual, ShouldEndWith, "Z")
So(actual, ShouldContainSubstring, "T")
})
Convey("Given an Version 4 AWS-formatted timestamp", t, func() {
ts := "20110909T233600Z"
Convey("The date string should be extracted properly", func() {
So(tsDateV4(ts), ShouldEqual, "20110909")
})
})
Convey("Given any request with a body", t, func() {
req := test_plainRequestV4(false)
Convey("Its body should be read and replaced without differences", func() {
expected := []byte(requestValuesV4.Encode())
actual1 := readAndReplaceBody(req)
So(actual1, ShouldResemble, expected)
actual2 := readAndReplaceBody(req)
So(actual2, ShouldResemble, expected)
})
})
}
func test_plainRequestV4(trailingSlash bool) *http.Request {
url := "http://iam.amazonaws.com"
body := strings.NewReader(requestValuesV4.Encode())
if trailingSlash {
url += "/"
}
req, err := http.NewRequest("POST", url, body)
if err != nil {
panic(err)
}
return req
}
func test_unsignedRequestV4(trailingSlash, tag bool) *http.Request {
req := test_plainRequestV4(trailingSlash)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
req.Header.Set("X-Amz-Date", "20110909T233600Z")
if tag {
req.Header.Set("X-Amz-Meta-Foo", "Bar!")
}
return req
}
func test_signingKeyV4() []byte {
return signingKeyV4(testCredV4.SecretAccessKey, "20110909", "us-east-1", "iam")
}
var (
testCredV4 = &Credentials{
AccessKeyID: "AKIDEXAMPLE",
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
}
testCredV4WithSTS = &Credentials{
AccessKeyID: "AKIDEXAMPLE",
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
SecurityToken: "AQoDYXdzEHcaoAJ1Aqwx1Sum0iW2NQjXJcWlKR7vuB6lnAeGBaQnjDRZPVyniwc48ml5hx+0qiXenVJdfusMMl9XLhSncfhx9Rb1UF8IAOaQ+CkpWXvoH67YYN+93dgckSVgVEBRByTl/BvLOZhe0ii/pOWkuQtBm5T7lBHRe4Dfmxy9X6hd8L3FrWxgnGV3fWZ3j0gASdYXaa+VBJlU0E2/GmCzn3T+t2mjYaeoInAnYVKVpmVMOrh6lNAeETTOHElLopblSa7TAmROq5xHIyu4a9i2qwjERTwa3Yk4Jk6q7JYVA5Cu7kS8wKVml8LdzzCTsy+elJgvH+Jf6ivpaHt/En0AJ5PZUJDev2+Y5+9j4AYfrmXfm4L73DC1ZJFJrv+Yh+EXAMPLE=",
}
expectingV4 = map[string]string{
"CanonicalHash": "41c56ed0df12052f7c10407a809e64cd61a4b0471956cdea28d6d1bb904f5d92",
"StringToSign": "AWS4-HMAC-SHA256\n20110909T233600Z\n20110909/us-east-1/iam/aws4_request\n41c56ed0df12052f7c10407a809e64cd61a4b0471956cdea28d6d1bb904f5d92",
"SignatureV4": "08292a4b86aae1a6f80f1988182a33cbf73ccc70c5da505303e355a67cc64cb4",
"AuthHeader": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=",
}
requestValuesV4 = &url.Values{
"Action": []string{"ListUsers"},
"Version": []string{"2010-05-08"},
}
)

View File

@ -93,6 +93,20 @@ Options:
- `--azure-subscription-id`: Your Azure subscription ID.
- `--azure-subscription-cert`: Your Azure subscription cert.
### Amazon EC2
Create machines on [Amazon Web Services](http://aws.amazon.com). You will need an Access Key ID, Secret Access Key and Subnet ID. To find the Subnet ID, login to the AWS console and go to Services -> VPC -> Subnets. Select the one where you would like to launch the instance.
Options:
- `--amazonec2-access-key`: Your access key id for the Amazon Web Services API.
- `--amazonec2-ami`: The AMI ID of the instance to use Default: `ami-a00461c8`
- `--amazonec2-instance-type`: The instance type to run. Default: `t2.micro`
- `--amazonec2-region`: The region to use when launching the instance. Default: `us-east-1`
- `--amazonec2-root-size`: The root disk size of the instance (in GB). Default: `16`
- `--amazonec2-secret-key`: Your secret access key for the Amazon Web Services API.
- `--amazonec2-subnet-id`: Your VPC subnet ID to launch the instance in.
## Contributing
[![GoDoc](https://godoc.org/github.com/docker/machine?status.png)](https://godoc.org/github.com/docker/machine)

View File

@ -12,6 +12,7 @@ import (
"github.com/codegangsta/cli"
"github.com/docker/machine/drivers"
_ "github.com/docker/machine/drivers/amazonec2"
_ "github.com/docker/machine/drivers/azure"
_ "github.com/docker/machine/drivers/digitalocean"
_ "github.com/docker/machine/drivers/none"

View File

@ -0,0 +1,531 @@
package amazonec2
import (
"crypto/md5"
"crypto/rand"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"time"
log "github.com/Sirupsen/logrus"
"github.com/codegangsta/cli"
"github.com/docker/machine/drivers"
"github.com/docker/machine/drivers/amazonec2/amz"
"github.com/docker/machine/ssh"
"github.com/docker/machine/state"
)
const (
driverName = "amazonec2"
defaultRegion = "us-east-1"
defaultAMI = "ami-a00461c8"
defaultInstanceType = "t2.micro"
defaultRootSize = 16
ipRange = "0.0.0.0/0"
)
type Driver struct {
Id string
AccessKey string
SecretKey string
Region string
AMI string
SSHKeyID int
KeyName string
InstanceId string
InstanceType string
IPAddress string
SubnetId string
SecurityGroupId string
ReservationId string
RootSize int64
VpcId string
storePath string
keyPath string
}
type CreateFlags struct {
AccessKey *string
SecretKey *string
Region *string
AMI *string
InstanceType *string
SubnetId *string
RootSize *int64
}
func init() {
drivers.Register(driverName, &drivers.RegisteredDriver{
New: NewDriver,
GetCreateFlags: GetCreateFlags,
})
}
func GetCreateFlags() []cli.Flag {
return []cli.Flag{
cli.StringFlag{
Name: "amazonec2-access-key",
Usage: "AWS Access Key",
Value: "",
},
cli.StringFlag{
Name: "amazonec2-secret-key",
Usage: "AWS Secret Key",
Value: "",
},
cli.StringFlag{
Name: "amazonec2-ami",
Usage: "AWS machine image",
Value: defaultAMI,
},
cli.StringFlag{
Name: "amazonec2-region",
Usage: "AWS region",
Value: defaultRegion,
},
cli.StringFlag{
Name: "amazonec2-subnet-id",
Usage: "AWS VPC subnet id",
Value: "",
},
cli.StringFlag{
Name: "amazonec2-instance-type",
Usage: "AWS instance type",
Value: defaultInstanceType,
},
cli.IntFlag{
Name: "amazonec2-root-size",
Usage: "AWS root disk size (in GB)",
Value: defaultRootSize,
},
}
}
func NewDriver(storePath string) (drivers.Driver, error) {
id := generateId()
return &Driver{Id: id, storePath: storePath}, nil
}
func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error {
d.AccessKey = flags.String("amazonec2-access-key")
d.SecretKey = flags.String("amazonec2-secret-key")
d.AMI = flags.String("amazonec2-ami")
d.Region = flags.String("amazonec2-region")
d.InstanceType = flags.String("amazonec2-instance-type")
d.SubnetId = flags.String("amazonec2-subnet-id")
d.RootSize = int64(flags.Int("amazonec2-root-size"))
if d.AccessKey == "" {
return fmt.Errorf("amazonec2 driver requires the --amazonec2-access-key option")
}
if d.SecretKey == "" {
return fmt.Errorf("amazonec2 driver requires the --amazonec2-secret-key option")
}
if d.SubnetId == "" {
return fmt.Errorf("amazonec2 driver requires the --amazonec2-subnet-id option")
}
return nil
}
func (d *Driver) DriverName() string {
return driverName
}
func (d *Driver) Create() error {
log.Infof("Launching instance...")
if err := d.createKeyPair(); err != nil {
fmt.Errorf("unable to create key pair: %s", err)
}
group, err := d.createSecurityGroup()
if err != nil {
return err
}
bdm := &amz.BlockDeviceMapping{
DeviceName: "/dev/sda1",
VolumeSize: d.RootSize,
DeleteOnTermination: true,
VolumeType: "gp2",
}
log.Debugf("launching instance")
instance, err := d.getClient().RunInstance(d.AMI, d.InstanceType, "a", 1, 1, group.GroupId, d.KeyName, d.SubnetId, bdm)
if err != nil {
return fmt.Errorf("Error launching instance: %s", err)
}
d.InstanceId = instance.InstanceId
d.waitForInstance()
log.Debugf("created instance ID %s, IP address %s",
d.InstanceId,
d.IPAddress)
log.Infof("Waiting for SSH...")
if err := ssh.WaitForTCP(fmt.Sprintf("%s:%d", d.IPAddress, 22)); err != nil {
return err
}
log.Debugf("Installing Docker")
cmd, err := d.GetSSHCommand("if [ ! -e /usr/bin/docker ]; then curl get.docker.io | sudo sh -; fi")
if err != nil {
return err
}
if err := cmd.Run(); err != nil {
return err
}
cmd, err = d.GetSSHCommand("sudo stop docker")
if err != nil {
return err
}
if err := cmd.Run(); err != nil {
return err
}
log.Debugf("HACK: Downloading version of Docker with identity auth...")
cmd, err = d.GetSSHCommand("sudo curl -sS -o /usr/bin/docker https://bfirsh.s3.amazonaws.com/docker/docker-1.3.1-dev-identity-auth")
if err != nil {
return err
}
if err := cmd.Run(); err != nil {
return err
}
log.Debugf("Updating /etc/default/docker to use identity auth...")
cmd, err = d.GetSSHCommand("echo 'export DOCKER_OPTS=\"--auth=identity --host=tcp://0.0.0.0:2376 --auth-authorized-dir=/root/.docker/authorized-keys.d\"' | sudo tee -a /etc/default/docker")
if err != nil {
return err
}
if err := cmd.Run(); err != nil {
return err
}
// HACK: create dir for ubuntu user to access
log.Debugf("Adding key to authorized-keys.d...")
cmd, err = d.GetSSHCommand("sudo mkdir -p /root/.docker && sudo chown -R ubuntu /root/.docker")
if err != nil {
return err
}
if err := cmd.Run(); err != nil {
return err
}
f, err := os.Open(filepath.Join(os.Getenv("HOME"), ".docker/public-key.json"))
if err != nil {
return err
}
defer f.Close()
cmdString := fmt.Sprintf("sudo mkdir -p %q && sudo tee -a %q", "/root/.docker/authorized-keys.d", "/root/.docker/authorized-keys.d/docker-host.json")
cmd, err = d.GetSSHCommand(cmdString)
if err != nil {
return err
}
cmd.Stdin = f
if err := cmd.Run(); err != nil {
return err
}
cmd, err = d.GetSSHCommand("sudo start docker")
if err != nil {
return err
}
if err := cmd.Run(); err != nil {
return err
}
return nil
}
func (d *Driver) GetURL() (string, error) {
ip, err := d.GetIP()
if err != nil {
return "", err
}
if ip == "" {
return "", nil
}
return fmt.Sprintf("tcp://%s:2376", ip), nil
}
func (d *Driver) GetIP() (string, error) {
inst, err := d.getInstance()
if err != nil {
return "", err
}
d.IPAddress = inst.IpAddress
return d.IPAddress, nil
}
func (d *Driver) GetState() (state.State, error) {
inst, err := d.getInstance()
if err != nil {
return state.Error, err
}
switch inst.InstanceState.Name {
case "pending":
return state.Starting, nil
case "running":
return state.Running, nil
case "stopping":
return state.Stopping, nil
case "shutting-down":
return state.Stopping, nil
case "stopped":
return state.Stopped, nil
}
return state.None, nil
}
func (d *Driver) Start() error {
if err := d.getClient().StartInstance(d.InstanceId); err != nil {
return err
}
if err := d.waitForInstance(); err != nil {
return err
}
if err := d.updateDriver(); err != nil {
return err
}
return nil
}
func (d *Driver) Stop() error {
if err := d.getClient().StopInstance(d.InstanceId, false); err != nil {
return err
}
return nil
}
func (d *Driver) Remove() error {
if err := d.terminate(); err != nil {
return fmt.Errorf("unabme to terminate instance: %s", err)
}
// wait until terminated so we can remove security group
for {
st, err := d.GetState()
if err != nil {
break
}
if st == state.None {
break
}
time.Sleep(1 * time.Second)
}
if err := d.deleteSecurityGroup(); err != nil {
return fmt.Errorf("unable to remove security group: %s", err)
}
// remove keypair
if err := d.deleteKeyPair(); err != nil {
return fmt.Errorf("unable to remove key pair: %s", err)
}
return nil
}
func (d *Driver) Restart() error {
if err := d.getClient().RestartInstance(d.InstanceId); err != nil {
return fmt.Errorf("unable to restart instance: %s", err)
}
return nil
}
func (d *Driver) Kill() error {
if err := d.getClient().StopInstance(d.InstanceId, true); err != nil {
return err
}
return nil
}
func (d *Driver) Upgrade() error {
return fmt.Errorf("unable to upgrade as we are using the custom docker binary with identity auth")
}
func (d *Driver) GetSSHCommand(args ...string) (*exec.Cmd, error) {
return ssh.GetSSHCommand(d.IPAddress, 22, "ubuntu", d.sshKeyPath(), args...), nil
}
func (d *Driver) getClient() *amz.EC2 {
auth := amz.GetAuth(d.AccessKey, d.SecretKey)
return amz.NewEC2(auth, d.Region)
}
func (d *Driver) sshKeyPath() string {
return path.Join(d.storePath, "id_rsa")
}
func (d *Driver) updateDriver() error {
inst, err := d.getInstance()
if err != nil {
return err
}
d.InstanceId = inst.InstanceId
d.IPAddress = inst.IpAddress
return nil
}
func (d *Driver) publicSSHKeyPath() string {
return d.sshKeyPath() + ".pub"
}
func (d *Driver) getInstance() (*amz.EC2Instance, error) {
instance, err := d.getClient().GetInstance(d.InstanceId)
if err != nil {
return nil, err
}
return &instance, nil
}
func (d *Driver) waitForInstance() error {
for {
st, err := d.GetState()
if err != nil {
return err
}
if st == state.Running {
break
}
time.Sleep(1 * time.Second)
}
if err := d.updateDriver(); err != nil {
return err
}
return nil
}
func (d *Driver) createKeyPair() error {
if err := ssh.GenerateSSHKey(d.sshKeyPath()); err != nil {
return err
}
publicKey, err := ioutil.ReadFile(d.publicSSHKeyPath())
if err != nil {
return err
}
keyName := fmt.Sprintf("docker-machine-%s", d.Id)
log.Debugf("creating key pair: %s", keyName)
if err := d.getClient().ImportKeyPair(keyName, string(publicKey)); err != nil {
return err
}
d.KeyName = keyName
return nil
}
func (d *Driver) terminate() error {
if d.InstanceId == "" {
return fmt.Errorf("unknown instance")
}
log.Debugf("terminating instance: %s", d.InstanceId)
if err := d.getClient().TerminateInstance(d.InstanceId); err != nil {
return fmt.Errorf("unable to terminate instance: %s", err)
}
return nil
}
func (d *Driver) createSecurityGroup() (*amz.SecurityGroup, error) {
subnets, err := d.getClient().GetSubnets()
if err != nil {
return nil, err
}
vpcId := subnets[0].VpcId
d.VpcId = vpcId
log.Debugf("creating security group in %s", d.VpcId)
grpName := fmt.Sprintf("docker-machine-%s", d.Id)
group, err := d.getClient().CreateSecurityGroup(grpName, "Docker Machine", d.VpcId)
if err != nil {
return nil, err
}
d.SecurityGroupId = group.GroupId
perms := []amz.IpPermission{
{
Protocol: "tcp",
FromPort: 22,
ToPort: 22,
IpRange: ipRange,
},
{
Protocol: "tcp",
FromPort: 2376,
ToPort: 2376,
IpRange: ipRange,
},
}
log.Debugf("authorizing %s", ipRange)
if err := d.getClient().AuthorizeSecurityGroup(d.SecurityGroupId, perms); err != nil {
return nil, err
}
return group, nil
}
func (d *Driver) deleteSecurityGroup() error {
log.Debugf("deleting security group %s", d.SecurityGroupId)
if err := d.getClient().DeleteSecurityGroup(d.SecurityGroupId); err != nil {
return err
}
return nil
}
func (d *Driver) deleteKeyPair() error {
log.Debugf("deleting key pair: %s", d.KeyName)
if err := d.getClient().DeleteKeyPair(d.KeyName); err != nil {
return err
}
return nil
}
func generateId() string {
rb := make([]byte, 10)
_, err := rand.Read(rb)
if err != nil {
log.Fatalf("unable to generate id: %s", err)
}
h := md5.New()
io.WriteString(h, string(rb))
return fmt.Sprintf("%x", h.Sum(nil))
}

View File

@ -0,0 +1,9 @@
package amz
type Auth struct {
AccessKey, SecretKey string
}
func GetAuth(accessKey, secretKey string) Auth {
return Auth{accessKey, secretKey}
}

View File

@ -0,0 +1,9 @@
package amz
type BlockDeviceMapping struct {
DeviceName string
VirtualName string
VolumeSize int64
DeleteOnTermination bool
VolumeType string
}

View File

@ -0,0 +1,8 @@
package amz
type DescribeInstancesResponse struct {
RequestId string `xml:"requestId"`
ReservationSet []struct {
InstancesSet []EC2Instance `xml:"instancesSet>item"`
} `xml:"reservationSet>item"`
}

View File

@ -0,0 +1,7 @@
package amz
type DescribeSecurityGroupsResponse struct {
RequestId string `xml"requestId"`
SecurityGroupInfo []struct {
} `xml:"securityGroupInfo>item"`
}

View File

@ -0,0 +1,14 @@
package amz
type DescribeSubnetsResponse struct {
RequestId string `xml:"requestId"`
SubnetSet []Subnet `xml:"subnetSet>item"`
}
type Subnet struct {
SubnetId string `xml:"subnetId"`
State string `xml:"state"`
VpcId string `xml:"vpcId"`
CidrBlock string `xml:"cidrBlock"`
AvailabilityZone string `xml:"availabilityZone"`
}

View File

@ -0,0 +1,487 @@
package amz
import (
"encoding/base64"
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/docker/machine/state"
awsauth "github.com/smartystreets/go-aws-auth"
)
type (
EC2 struct {
Endpoint string
Auth Auth
Region string
}
Instance struct {
info EC2Instance
}
EC2Instance struct {
InstanceId string `xml:"instanceId"`
ImageId string `xml:"imageId"`
InstanceState struct {
Code int `xml:"code"`
Name string `xml:"name"`
} `xml:"instanceState"`
PrivateDnsName string `xml:"privateDnsName"`
DnsName string `xml:"dnsName"`
Reason string `xml:"reason"`
AmiLaunchIndex string `xml:"amiLaunchIndex"`
ProductCodes string `xml:"productCodes"`
InstanceType string `xml:"instanceType"`
LaunchTime string `xml:"launchTime"`
Placement struct {
AvailabilityZone string `xml:"availabilityZone"`
GroupName string `xml:"groupName"`
Tenancy string `xml:"tenancy"`
} `xml:"placement"`
KernelId string `xml:"kernelId"`
Monitoring struct {
State string `xml:"state"`
} `xml:"monitoring"`
SubnetId string `xml:"subnetId"`
VpcId string `xml:"vpcId"`
IpAddress string `xml:"ipAddress"`
PrivateIpAddress string `xml:"privateIpAddress"`
SourceDestCheck bool `xml:"sourceDestCheck"`
GroupSet []struct {
GroupId string `xml:"groupId"`
GroupName string `xml:"groupName"`
} `xml:"groupSet"`
StateReason struct {
Code string `xml:"code"`
Message string `xml:"message"`
} `xml:"stateReason"`
Architecture string `xml:"architecture"`
RootDeviceType string `xml:"rootDeviceType"`
RootDeviceName string `xml:"rootDeviceName"`
BlockDeviceMapping string `xml:"blockDeviceMapping"`
VirtualizationType string `xml:"virtualizationType"`
ClientToken string `xml:"clientToken"`
Hypervisor string `xml:"hypervisor"`
NetworkInterfaceSet []struct {
NetworkInterfaceId string `xml:"networkInterfaceId"`
SubnetId string `xml:"subnetId"`
VpcId string `xml:"vpcId"`
Description string `xml:"description"`
OwnerId string `xml:"ownerId"`
Status string `xml:"status"`
MacAddress string `xml:"macAddress"`
PrivateIpAddress string `xml:"privateIpAddress"`
PrivateDnsName string `xml:"privateDnsName"`
SourceDestCheck string `xml:"sourceDestCheck"`
GroupSet []struct {
GroupId string `xml:"groupId"`
GroupName string `xml:"groupName"`
} `xml:"groupSet>item"`
Attachment struct {
AttachmentId string `xml:"attachmentId"`
DeviceIndex string `xml:"deviceIndex"`
Status string `xml:"status"`
AttachTime string `xml:"attachTime"`
DeleteOnTermination bool `xml:"deleteOnTermination"`
} `xml:"attachment"`
PrivateIpAddressesSet []struct {
PrivateIpAddress string `xml:"privateIpAddress"`
PrivateDnsName string `xml:"privateDnsName"`
Primary bool `xml:"primary"`
} `xml:"privateIpAddressesSet>item"`
} `xml:"networkInterfaceSet>item"`
EbsOptimized bool `xml:"ebsOptimized"`
}
RunInstancesResponse struct {
RequestId string `xml:"requestId"`
ReservationId string `xml:"reservationId"`
OwnerId string `xml:"ownerId"`
Instances []EC2Instance `xml:"instancesSet>item"`
}
)
func newAwsApiResponseError(r http.Response) error {
var errorResponse ErrorResponse
if err := getDecodedResponse(r, &errorResponse); err != nil {
return fmt.Errorf("Error decoding error response: %s", err)
}
msg := ""
for _, e := range errorResponse.Errors {
msg += fmt.Sprintf("%s\n", e.Message)
}
return fmt.Errorf("Non-200 API response: code=%d message=%s", r.StatusCode, msg)
}
func newAwsApiCallError(err error) error {
return fmt.Errorf("Problem with AWS API call: %s", err)
}
func getDecodedResponse(r http.Response, into interface{}) error {
defer r.Body.Close()
if err := xml.NewDecoder(r.Body).Decode(into); err != nil {
return fmt.Errorf("Error decoding error response: %s", err)
}
return nil
}
func NewEC2(auth Auth, region string) *EC2 {
endpoint := fmt.Sprintf("https://ec2.%s.amazonaws.com", region)
return &EC2{
Endpoint: endpoint,
Auth: auth,
Region: region,
}
}
func (e *EC2) awsApiCall(v url.Values) (http.Response, error) {
v.Set("Version", "2014-06-15")
client := &http.Client{}
finalEndpoint := fmt.Sprintf("%s?%s", e.Endpoint, v.Encode())
req, err := http.NewRequest("GET", finalEndpoint, nil)
if err != nil {
return http.Response{}, fmt.Errorf("error creating request from client")
}
req.Header.Add("Content-type", "application/json")
awsauth.Sign(req, awsauth.Credentials{
AccessKeyID: e.Auth.AccessKey,
SecretAccessKey: e.Auth.SecretKey,
})
resp, err := client.Do(req)
if err != nil {
return *resp, fmt.Errorf("client encountered error while doing the request: %s", err)
}
if resp.StatusCode != http.StatusOK {
return *resp, newAwsApiResponseError(*resp)
}
return *resp, nil
}
func (e *EC2) RunInstance(amiId string, instanceType string, zone string, minCount int, maxCount int, securityGroup string, keyName string, subnetId string, bdm *BlockDeviceMapping) (EC2Instance, error) {
instance := Instance{}
v := url.Values{}
v.Set("Action", "RunInstances")
v.Set("ImageId", amiId)
v.Set("Placement.AvailabilityZone", e.Region+zone)
v.Set("MinCount", strconv.Itoa(minCount))
v.Set("MaxCount", strconv.Itoa(maxCount))
v.Set("KeyName", keyName)
v.Set("InstanceType", instanceType)
v.Set("NetworkInterface.0.DeviceIndex", "0")
v.Set("NetworkInterface.0.SecurityGroupId.0", securityGroup)
v.Set("NetworkInterface.0.SubnetId", subnetId)
v.Set("NetworkInterface.0.AssociatePublicIpAddress", "1")
if bdm != nil {
v.Set("BlockDeviceMapping.0.DeviceName", bdm.DeviceName)
v.Set("BlockDeviceMapping.0.VirtualName", bdm.VirtualName)
v.Set("BlockDeviceMapping.0.Ebs.VolumeSize", strconv.FormatInt(bdm.VolumeSize, 10))
v.Set("BlockDeviceMapping.0.Ebs.VolumeType", bdm.VolumeType)
deleteOnTerm := 0
if bdm.DeleteOnTermination {
deleteOnTerm = 1
}
v.Set("BlockDeviceMapping.0.Ebs.DeleteOnTermination", strconv.Itoa(deleteOnTerm))
}
resp, err := e.awsApiCall(v)
if err != nil {
return instance.info, newAwsApiCallError(err)
}
defer resp.Body.Close()
contents, err := ioutil.ReadAll(resp.Body)
if err != nil {
return instance.info, fmt.Errorf("Error reading AWS response body")
}
unmarshalledResponse := RunInstancesResponse{}
err = xml.Unmarshal(contents, &unmarshalledResponse)
if err != nil {
return instance.info, fmt.Errorf("Error unmarshalling AWS response XML: %s", err)
}
instance.info = unmarshalledResponse.Instances[0]
return instance.info, nil
}
func (e *EC2) DeleteKeyPair(name string) error {
v := url.Values{}
v.Set("Action", "DeleteKeyPair")
v.Set("KeyName", name)
_, err := e.awsApiCall(v)
if err != nil {
return fmt.Errorf("Error making API call to delete keypair :%s", err)
}
return nil
}
func (e *EC2) CreateKeyPair(name string) ([]byte, error) {
v := url.Values{}
v.Set("Action", "CreateKeyPair")
v.Set("KeyName", name)
resp, err := e.awsApiCall(v)
if err != nil {
return nil, fmt.Errorf("Error trying API call to create keypair: %s", err)
}
defer resp.Body.Close()
contents, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("Error reading AWS response body")
}
unmarshalledResponse := CreateKeyPairResponse{}
if xml.Unmarshal(contents, &unmarshalledResponse); err != nil {
return nil, fmt.Errorf("Error unmarshalling AWS response XML: %s", err)
}
key := unmarshalledResponse.KeyMaterial
return key, nil
}
func (e *EC2) ImportKeyPair(name, publicKey string) error {
keyMaterial := base64.StdEncoding.EncodeToString([]byte(publicKey))
v := url.Values{}
v.Set("Action", "ImportKeyPair")
v.Set("KeyName", name)
v.Set("PublicKeyMaterial", keyMaterial)
resp, err := e.awsApiCall(v)
if err != nil {
return fmt.Errorf("Error trying API call to create keypair: %s", err)
}
defer resp.Body.Close()
contents, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("Error reading AWS response body")
}
unmarshalledResponse := ImportKeyPairResponse{}
if xml.Unmarshal(contents, &unmarshalledResponse); err != nil {
return fmt.Errorf("Error unmarshalling AWS response XML: %s", err)
}
return nil
}
func (e *EC2) CreateSecurityGroup(name string, description string, vpcId string) (*SecurityGroup, error) {
v := url.Values{}
v.Set("Action", "CreateSecurityGroup")
v.Set("GroupName", name)
v.Set("GroupDescription", url.QueryEscape(description))
v.Set("VpcId", vpcId)
resp, err := e.awsApiCall(v)
defer resp.Body.Close()
if err != nil {
// ugly hack since API has no way to check if SG already exists
if resp.StatusCode == http.StatusBadRequest {
var errorResponse ErrorResponse
if err := getDecodedResponse(resp, &errorResponse); err != nil {
return nil, fmt.Errorf("Error decoding error response: %s", err)
}
if errorResponse.Errors[0].Code == ErrorDuplicateGroup {
return nil, nil
}
}
return nil, fmt.Errorf("Error making API call to create security group: %s", err)
}
createSecurityGroupResponse := CreateSecurityGroupResponse{}
if err := getDecodedResponse(resp, &createSecurityGroupResponse); err != nil {
return nil, fmt.Errorf("Error decoding create security groups response: %s", err)
}
group := &SecurityGroup{
GroupId: createSecurityGroupResponse.GroupId,
VpcId: vpcId,
}
return group, nil
}
func (e *EC2) AuthorizeSecurityGroup(groupId string, permissions []IpPermission) error {
v := url.Values{}
v.Set("Action", "AuthorizeSecurityGroupIngress")
v.Set("GroupId", groupId)
for index, perm := range permissions {
n := index + 1 // amazon starts counting from 1 not 0
v.Set(fmt.Sprintf("IpPermissions.%d.IpProtocol", n), perm.Protocol)
v.Set(fmt.Sprintf("IpPermissions.%d.FromPort", n), strconv.Itoa(perm.FromPort))
v.Set(fmt.Sprintf("IpPermissions.%d.ToPort", n), strconv.Itoa(perm.ToPort))
v.Set(fmt.Sprintf("IpPermissions.%d.IpRanges.1.CidrIp", n), perm.IpRange)
}
resp, err := e.awsApiCall(v)
defer resp.Body.Close()
if err != nil {
return fmt.Errorf("Error making API call to authorize security group ingress: %s", err)
}
return nil
}
func (e *EC2) DeleteSecurityGroup(groupId string) error {
v := url.Values{}
v.Set("Action", "DeleteSecurityGroup")
v.Set("GroupId", groupId)
resp, err := e.awsApiCall(v)
defer resp.Body.Close()
if err != nil {
return fmt.Errorf("Error making API call to delete security group: %s", err)
}
deleteSecurityGroupResponse := DeleteSecurityGroupResponse{}
if err := getDecodedResponse(resp, &deleteSecurityGroupResponse); err != nil {
return fmt.Errorf("Error decoding delete security groups response: %s", err)
}
return nil
}
func (e *EC2) GetSubnets() ([]Subnet, error) {
subnets := []Subnet{}
resp, err := e.performStandardAction("DescribeSubnets")
if err != nil {
return subnets, err
}
defer resp.Body.Close()
contents, err := ioutil.ReadAll(resp.Body)
if err != nil {
return subnets, fmt.Errorf("Error reading AWS response body: %s", err)
}
unmarshalledResponse := DescribeSubnetsResponse{}
if err = xml.Unmarshal(contents, &unmarshalledResponse); err != nil {
return subnets, fmt.Errorf("Error unmarshalling AWS response XML: %s", err)
}
subnets = unmarshalledResponse.SubnetSet
return subnets, nil
}
func (e *EC2) GetInstanceState(instanceId string) (state.State, error) {
resp, err := e.performInstanceAction(instanceId, "DescribeInstances", nil)
if err != nil {
return state.Error, err
}
defer resp.Body.Close()
contents, err := ioutil.ReadAll(resp.Body)
if err != nil {
return state.Error, fmt.Errorf("Error reading AWS response body: %s", err)
}
unmarshalledResponse := DescribeInstancesResponse{}
if err = xml.Unmarshal(contents, &unmarshalledResponse); err != nil {
return state.Error, fmt.Errorf("Error unmarshalling AWS response XML: %s", err)
}
reservationSet := unmarshalledResponse.ReservationSet[0]
instanceState := reservationSet.InstancesSet[0].InstanceState
shortState := strings.TrimSpace(instanceState.Name)
switch shortState {
case "pending":
return state.Starting, nil
case "running":
return state.Running, nil
case "stopped":
return state.Stopped, nil
case "stopping":
return state.Stopped, nil
}
return state.Error, nil
}
func (e *EC2) GetInstance(instanceId string) (EC2Instance, error) {
ec2Instance := EC2Instance{}
resp, err := e.performInstanceAction(instanceId, "DescribeInstances", nil)
if err != nil {
return ec2Instance, err
}
defer resp.Body.Close()
contents, err := ioutil.ReadAll(resp.Body)
if err != nil {
return ec2Instance, fmt.Errorf("Error reading AWS response body: %s", err)
}
unmarshalledResponse := DescribeInstancesResponse{}
if err = xml.Unmarshal(contents, &unmarshalledResponse); err != nil {
return ec2Instance, fmt.Errorf("Error unmarshalling AWS response XML: %s", err)
}
reservationSet := unmarshalledResponse.ReservationSet[0]
instance := reservationSet.InstancesSet[0]
return instance, nil
}
func (e *EC2) StartInstance(instanceId string) error {
if _, err := e.performInstanceAction(instanceId, "StartInstances", nil); err != nil {
return err
}
return nil
}
func (e *EC2) RestartInstance(instanceId string) error {
if _, err := e.performInstanceAction(instanceId, "RebootInstances", nil); err != nil {
return err
}
return nil
}
func (e *EC2) StopInstance(instanceId string, force bool) error {
vars := make(map[string]string)
if force {
vars["Force"] = "1"
}
if _, err := e.performInstanceAction(instanceId, "StopInstances", &vars); err != nil {
return err
}
return nil
}
func (e *EC2) TerminateInstance(instanceId string) error {
if _, err := e.performInstanceAction(instanceId, "TerminateInstances", nil); err != nil {
return err
}
return nil
}
func (e *EC2) performStandardAction(action string) (http.Response, error) {
v := url.Values{}
v.Set("Action", action)
resp, err := e.awsApiCall(v)
if err != nil {
return resp, newAwsApiCallError(err)
}
return resp, nil
}
func (e *EC2) performInstanceAction(instanceId, action string, extraVars *map[string]string) (http.Response, error) {
v := url.Values{}
v.Set("Action", action)
v.Set("InstanceId.1", instanceId)
if extraVars != nil {
for k, val := range *extraVars {
v.Set(k, val)
}
}
resp, err := e.awsApiCall(v)
if err != nil {
return resp, newAwsApiCallError(err)
}
return resp, nil
}

View File

@ -0,0 +1,9 @@
package amz
type ErrorResponse struct {
Errors []struct {
Code string
Message string
} `xml:"Errors>Error"`
RequestID string
}

View File

@ -0,0 +1,5 @@
package amz
const (
ErrorDuplicateGroup = "InvalidGroup.Duplicate"
)

View File

@ -0,0 +1,8 @@
package amz
type IpPermission struct {
Protocol string
FromPort int
ToPort int
IpRange string
}

View File

@ -0,0 +1,13 @@
package amz
type CreateKeyPairResponse struct {
KeyName string `xml:"keyName"`
KeyFingerprint string `xml:"keyFingerprint"`
KeyMaterial []byte `xml:"keyMaterial"`
}
type ImportKeyPairResponse struct {
KeyName string `xml:"keyName"`
KeyFingerprint string `xml:"keyFingerprint"`
KeyMaterial []byte `xml:"keyMaterial"`
}

View File

@ -0,0 +1,17 @@
package amz
type CreateSecurityGroupResponse struct {
RequestId string `xml:"requestId"`
Return bool `xml:"return"`
GroupId string `xml:"groupId"`
}
type DeleteSecurityGroupResponse struct {
RequestId string `xml:"requestId"`
Return bool `xml:"return"`
}
type SecurityGroup struct {
GroupId string
VpcId string
}

View File

@ -9,6 +9,7 @@ const (
Paused
Saved
Stopped
Stopping
Starting
Error
)
@ -19,6 +20,7 @@ var states = []string{
"Paused",
"Saved",
"Stopped",
"Stopping",
"Starting",
"Error",
}