Add new dependencies for AWS support

- goamz: for interacting with AWS API
- gcfg: for cloud provider config file (as suggested in issue)
This commit is contained in:
Ragnar Dahlén
2014-09-09 22:13:02 +01:00
parent 57b721dd05
commit f085f4d5d4
23 changed files with 8051 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
package aws
import (
"time"
)
// AttemptStrategy represents a strategy for waiting for an action
// to complete successfully. This is an internal type used by the
// implementation of other goamz packages.
type AttemptStrategy struct {
Total time.Duration // total duration of attempt.
Delay time.Duration // interval between each try in the burst.
Min int // minimum number of retries; overrides Total
}
type Attempt struct {
strategy AttemptStrategy
last time.Time
end time.Time
force bool
count int
}
// Start begins a new sequence of attempts for the given strategy.
func (s AttemptStrategy) Start() *Attempt {
now := time.Now()
return &Attempt{
strategy: s,
last: now,
end: now.Add(s.Total),
force: true,
}
}
// Next waits until it is time to perform the next attempt or returns
// false if it is time to stop trying.
func (a *Attempt) Next() bool {
now := time.Now()
sleep := a.nextSleep(now)
if !a.force && !now.Add(sleep).Before(a.end) && a.strategy.Min <= a.count {
return false
}
a.force = false
if sleep > 0 && a.count > 0 {
time.Sleep(sleep)
now = time.Now()
}
a.count++
a.last = now
return true
}
func (a *Attempt) nextSleep(now time.Time) time.Duration {
sleep := a.strategy.Delay - now.Sub(a.last)
if sleep < 0 {
return 0
}
return sleep
}
// HasNext returns whether another attempt will be made if the current
// one fails. If it returns true, the following call to Next is
// guaranteed to return true.
func (a *Attempt) HasNext() bool {
if a.force || a.strategy.Min > a.count {
return true
}
now := time.Now()
if now.Add(a.nextSleep(now)).Before(a.end) {
a.force = true
return true
}
return false
}

View File

@@ -0,0 +1,57 @@
package aws_test
import (
"github.com/mitchellh/goamz/aws"
. "github.com/motain/gocheck"
"time"
)
func (S) TestAttemptTiming(c *C) {
testAttempt := aws.AttemptStrategy{
Total: 0.25e9,
Delay: 0.1e9,
}
want := []time.Duration{0, 0.1e9, 0.2e9, 0.2e9}
got := make([]time.Duration, 0, len(want)) // avoid allocation when testing timing
t0 := time.Now()
for a := testAttempt.Start(); a.Next(); {
got = append(got, time.Now().Sub(t0))
}
got = append(got, time.Now().Sub(t0))
c.Assert(got, HasLen, len(want))
const margin = 0.01e9
for i, got := range want {
lo := want[i] - margin
hi := want[i] + margin
if got < lo || got > hi {
c.Errorf("attempt %d want %g got %g", i, want[i].Seconds(), got.Seconds())
}
}
}
func (S) TestAttemptNextHasNext(c *C) {
a := aws.AttemptStrategy{}.Start()
c.Assert(a.Next(), Equals, true)
c.Assert(a.Next(), Equals, false)
a = aws.AttemptStrategy{}.Start()
c.Assert(a.Next(), Equals, true)
c.Assert(a.HasNext(), Equals, false)
c.Assert(a.Next(), Equals, false)
a = aws.AttemptStrategy{Total: 2e8}.Start()
c.Assert(a.Next(), Equals, true)
c.Assert(a.HasNext(), Equals, true)
time.Sleep(2e8)
c.Assert(a.HasNext(), Equals, true)
c.Assert(a.Next(), Equals, true)
c.Assert(a.Next(), Equals, false)
a = aws.AttemptStrategy{Total: 1e8, Min: 2}.Start()
time.Sleep(1e8)
c.Assert(a.Next(), Equals, true)
c.Assert(a.HasNext(), Equals, true)
c.Assert(a.Next(), Equals, true)
c.Assert(a.HasNext(), Equals, false)
c.Assert(a.Next(), Equals, false)
}

View File

@@ -0,0 +1,423 @@
//
// goamz - Go packages to interact with the Amazon Web Services.
//
// https://wiki.ubuntu.com/goamz
//
// Copyright (c) 2011 Canonical Ltd.
//
// Written by Gustavo Niemeyer <gustavo.niemeyer@canonical.com>
//
package aws
import (
"encoding/json"
"errors"
"fmt"
"github.com/vaughan0/go-ini"
"io/ioutil"
"os"
)
// Region defines the URLs where AWS services may be accessed.
//
// See http://goo.gl/d8BP1 for more details.
type Region struct {
Name string // the canonical name of this region.
EC2Endpoint string
S3Endpoint string
S3BucketEndpoint string // Not needed by AWS S3. Use ${bucket} for bucket name.
S3LocationConstraint bool // true if this region requires a LocationConstraint declaration.
S3LowercaseBucket bool // true if the region requires bucket names to be lower case.
SDBEndpoint string
SNSEndpoint string
SQSEndpoint string
IAMEndpoint string
ELBEndpoint string
AutoScalingEndpoint string
RdsEndpoint string
Route53Endpoint string
}
var USGovWest = Region{
"us-gov-west-1",
"https://ec2.us-gov-west-1.amazonaws.com",
"https://s3-fips-us-gov-west-1.amazonaws.com",
"",
true,
true,
"",
"https://sns.us-gov-west-1.amazonaws.com",
"https://sqs.us-gov-west-1.amazonaws.com",
"https://iam.us-gov.amazonaws.com",
"https://elasticloadbalancing.us-gov-west-1.amazonaws.com",
"https://autoscaling.us-gov-west-1.amazonaws.com",
"https://rds.us-gov-west-1.amazonaws.com",
"https://route53.amazonaws.com",
}
var USEast = Region{
"us-east-1",
"https://ec2.us-east-1.amazonaws.com",
"https://s3.amazonaws.com",
"",
false,
false,
"https://sdb.amazonaws.com",
"https://sns.us-east-1.amazonaws.com",
"https://sqs.us-east-1.amazonaws.com",
"https://iam.amazonaws.com",
"https://elasticloadbalancing.us-east-1.amazonaws.com",
"https://autoscaling.us-east-1.amazonaws.com",
"https://rds.us-east-1.amazonaws.com",
"https://route53.amazonaws.com",
}
var USWest = Region{
"us-west-1",
"https://ec2.us-west-1.amazonaws.com",
"https://s3-us-west-1.amazonaws.com",
"",
true,
true,
"https://sdb.us-west-1.amazonaws.com",
"https://sns.us-west-1.amazonaws.com",
"https://sqs.us-west-1.amazonaws.com",
"https://iam.amazonaws.com",
"https://elasticloadbalancing.us-west-1.amazonaws.com",
"https://autoscaling.us-west-1.amazonaws.com",
"https://rds.us-west-1.amazonaws.com",
"https://route53.amazonaws.com",
}
var USWest2 = Region{
"us-west-2",
"https://ec2.us-west-2.amazonaws.com",
"https://s3-us-west-2.amazonaws.com",
"",
true,
true,
"https://sdb.us-west-2.amazonaws.com",
"https://sns.us-west-2.amazonaws.com",
"https://sqs.us-west-2.amazonaws.com",
"https://iam.amazonaws.com",
"https://elasticloadbalancing.us-west-2.amazonaws.com",
"https://autoscaling.us-west-2.amazonaws.com",
"https://rds.us-west-2.amazonaws.com",
"https://route53.amazonaws.com",
}
var EUWest = Region{
"eu-west-1",
"https://ec2.eu-west-1.amazonaws.com",
"https://s3-eu-west-1.amazonaws.com",
"",
true,
true,
"https://sdb.eu-west-1.amazonaws.com",
"https://sns.eu-west-1.amazonaws.com",
"https://sqs.eu-west-1.amazonaws.com",
"https://iam.amazonaws.com",
"https://elasticloadbalancing.eu-west-1.amazonaws.com",
"https://autoscaling.eu-west-1.amazonaws.com",
"https://rds.eu-west-1.amazonaws.com",
"https://route53.amazonaws.com",
}
var APSoutheast = Region{
"ap-southeast-1",
"https://ec2.ap-southeast-1.amazonaws.com",
"https://s3-ap-southeast-1.amazonaws.com",
"",
true,
true,
"https://sdb.ap-southeast-1.amazonaws.com",
"https://sns.ap-southeast-1.amazonaws.com",
"https://sqs.ap-southeast-1.amazonaws.com",
"https://iam.amazonaws.com",
"https://elasticloadbalancing.ap-southeast-1.amazonaws.com",
"https://autoscaling.ap-southeast-1.amazonaws.com",
"https://rds.ap-southeast-1.amazonaws.com",
"https://route53.amazonaws.com",
}
var APSoutheast2 = Region{
"ap-southeast-2",
"https://ec2.ap-southeast-2.amazonaws.com",
"https://s3-ap-southeast-2.amazonaws.com",
"",
true,
true,
"https://sdb.ap-southeast-2.amazonaws.com",
"https://sns.ap-southeast-2.amazonaws.com",
"https://sqs.ap-southeast-2.amazonaws.com",
"https://iam.amazonaws.com",
"https://elasticloadbalancing.ap-southeast-2.amazonaws.com",
"https://autoscaling.ap-southeast-2.amazonaws.com",
"https://rds.ap-southeast-2.amazonaws.com",
"https://route53.amazonaws.com",
}
var APNortheast = Region{
"ap-northeast-1",
"https://ec2.ap-northeast-1.amazonaws.com",
"https://s3-ap-northeast-1.amazonaws.com",
"",
true,
true,
"https://sdb.ap-northeast-1.amazonaws.com",
"https://sns.ap-northeast-1.amazonaws.com",
"https://sqs.ap-northeast-1.amazonaws.com",
"https://iam.amazonaws.com",
"https://elasticloadbalancing.ap-northeast-1.amazonaws.com",
"https://autoscaling.ap-northeast-1.amazonaws.com",
"https://rds.ap-northeast-1.amazonaws.com",
"https://route53.amazonaws.com",
}
var SAEast = Region{
"sa-east-1",
"https://ec2.sa-east-1.amazonaws.com",
"https://s3-sa-east-1.amazonaws.com",
"",
true,
true,
"https://sdb.sa-east-1.amazonaws.com",
"https://sns.sa-east-1.amazonaws.com",
"https://sqs.sa-east-1.amazonaws.com",
"https://iam.amazonaws.com",
"https://elasticloadbalancing.sa-east-1.amazonaws.com",
"https://autoscaling.sa-east-1.amazonaws.com",
"https://rds.sa-east-1.amazonaws.com",
"https://route53.amazonaws.com",
}
var CNNorth = Region{
"cn-north-1",
"https://ec2.cn-north-1.amazonaws.com.cn",
"https://s3.cn-north-1.amazonaws.com.cn",
"",
true,
true,
"",
"https://sns.cn-north-1.amazonaws.com.cn",
"https://sqs.cn-north-1.amazonaws.com.cn",
"https://iam.cn-north-1.amazonaws.com.cn",
"https://elasticloadbalancing.cn-north-1.amazonaws.com.cn",
"https://autoscaling.cn-north-1.amazonaws.com.cn",
"https://rds.cn-north-1.amazonaws.com.cn",
"https://route53.amazonaws.com",
}
var Regions = map[string]Region{
APNortheast.Name: APNortheast,
APSoutheast.Name: APSoutheast,
APSoutheast2.Name: APSoutheast2,
EUWest.Name: EUWest,
USEast.Name: USEast,
USWest.Name: USWest,
USWest2.Name: USWest2,
SAEast.Name: SAEast,
USGovWest.Name: USGovWest,
CNNorth.Name: CNNorth,
}
type Auth struct {
AccessKey, SecretKey, Token string
}
var unreserved = make([]bool, 128)
var hex = "0123456789ABCDEF"
func init() {
// RFC3986
u := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890-_.~"
for _, c := range u {
unreserved[c] = true
}
}
type credentials struct {
Code string
LastUpdated string
Type string
AccessKeyId string
SecretAccessKey string
Token string
Expiration string
}
// GetMetaData retrieves instance metadata about the current machine.
//
// See http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AESDG-chapter-instancedata.html for more details.
func GetMetaData(path string) (contents []byte, err error) {
url := "http://169.254.169.254/latest/meta-data/" + path
resp, err := RetryingClient.Get(url)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
err = fmt.Errorf("Code %d returned for url %s", resp.StatusCode, url)
return
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
return []byte(body), err
}
func getInstanceCredentials() (cred credentials, err error) {
credentialPath := "iam/security-credentials/"
// Get the instance role
role, err := GetMetaData(credentialPath)
if err != nil {
return
}
// Get the instance role credentials
credentialJSON, err := GetMetaData(credentialPath + string(role))
if err != nil {
return
}
err = json.Unmarshal([]byte(credentialJSON), &cred)
return
}
// GetAuth creates an Auth based on either passed in credentials,
// environment information or instance based role credentials.
func GetAuth(accessKey string, secretKey string) (auth Auth, err error) {
// First try passed in credentials
if accessKey != "" && secretKey != "" {
return Auth{accessKey, secretKey, ""}, nil
}
// Next try to get auth from the environment
auth, err = SharedAuth()
if err == nil {
// Found auth, return
return
}
// Next try to get auth from the environment
auth, err = EnvAuth()
if err == nil {
// Found auth, return
return
}
// Next try getting auth from the instance role
cred, err := getInstanceCredentials()
if err == nil {
// Found auth, return
auth.AccessKey = cred.AccessKeyId
auth.SecretKey = cred.SecretAccessKey
auth.Token = cred.Token
return
}
err = errors.New("No valid AWS authentication found")
return
}
// SharedAuth creates an Auth based on shared credentials stored in
// $HOME/.aws/credentials. The AWS_PROFILE environment variables is used to
// select the profile.
func SharedAuth() (auth Auth, err error) {
var profileName = os.Getenv("AWS_PROFILE")
if profileName == "" {
profileName = "default"
}
var homeDir = os.Getenv("HOME")
if homeDir == "" {
err = errors.New("Could not get HOME")
return
}
var credentialsFile = homeDir + "/.aws/credentials"
file, err := ini.LoadFile(credentialsFile)
if err != nil {
err = errors.New("Couldn't parse AWS credentials file")
return
}
var profile = file[profileName]
if profile == nil {
err = errors.New("Couldn't find profile in AWS credentials file")
return
}
auth.AccessKey = profile["aws_access_key_id"]
auth.SecretKey = profile["aws_secret_access_key"]
if auth.AccessKey == "" {
err = errors.New("AWS_ACCESS_KEY_ID not found in environment in credentials file")
}
if auth.SecretKey == "" {
err = errors.New("AWS_SECRET_ACCESS_KEY not found in credentials file")
}
return
}
// EnvAuth creates an Auth based on environment information.
// The AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment
// For accounts that require a security token, it is read from AWS_SECURITY_TOKEN
// variables are used.
func EnvAuth() (auth Auth, err error) {
auth.AccessKey = os.Getenv("AWS_ACCESS_KEY_ID")
if auth.AccessKey == "" {
auth.AccessKey = os.Getenv("AWS_ACCESS_KEY")
}
auth.SecretKey = os.Getenv("AWS_SECRET_ACCESS_KEY")
if auth.SecretKey == "" {
auth.SecretKey = os.Getenv("AWS_SECRET_KEY")
}
auth.Token = os.Getenv("AWS_SECURITY_TOKEN")
if auth.AccessKey == "" {
err = errors.New("AWS_ACCESS_KEY_ID or AWS_ACCESS_KEY not found in environment")
}
if auth.SecretKey == "" {
err = errors.New("AWS_SECRET_ACCESS_KEY or AWS_SECRET_KEY not found in environment")
}
return
}
// Encode takes a string and URI-encodes it in a way suitable
// to be used in AWS signatures.
func Encode(s string) string {
encode := false
for i := 0; i != len(s); i++ {
c := s[i]
if c > 127 || !unreserved[c] {
encode = true
break
}
}
if !encode {
return s
}
e := make([]byte, len(s)*3)
ei := 0
for i := 0; i != len(s); i++ {
c := s[i]
if c > 127 || !unreserved[c] {
e[ei] = '%'
e[ei+1] = hex[c>>4]
e[ei+2] = hex[c&0xF]
ei += 3
} else {
e[ei] = c
ei += 1
}
}
return string(e[:ei])
}

View File

@@ -0,0 +1,203 @@
package aws_test
import (
"github.com/mitchellh/goamz/aws"
. "github.com/motain/gocheck"
"io/ioutil"
"os"
"strings"
"testing"
)
func Test(t *testing.T) {
TestingT(t)
}
var _ = Suite(&S{})
type S struct {
environ []string
}
func (s *S) SetUpSuite(c *C) {
s.environ = os.Environ()
}
func (s *S) TearDownTest(c *C) {
os.Clearenv()
for _, kv := range s.environ {
l := strings.SplitN(kv, "=", 2)
os.Setenv(l[0], l[1])
}
}
func (s *S) TestSharedAuthNoHome(c *C) {
os.Clearenv()
os.Setenv("AWS_PROFILE", "foo")
_, err := aws.SharedAuth()
c.Assert(err, ErrorMatches, "Could not get HOME")
}
func (s *S) TestSharedAuthNoCredentialsFile(c *C) {
os.Clearenv()
os.Setenv("AWS_PROFILE", "foo")
os.Setenv("HOME", "/tmp")
_, err := aws.SharedAuth()
c.Assert(err, ErrorMatches, "Couldn't parse AWS credentials file")
}
func (s *S) TestSharedAuthNoProfileInFile(c *C) {
os.Clearenv()
os.Setenv("AWS_PROFILE", "foo")
d, err := ioutil.TempDir("", "")
if err != nil {
panic(err)
}
defer os.RemoveAll(d)
err = os.Mkdir(d+"/.aws", 0755)
if err != nil {
panic(err)
}
ioutil.WriteFile(d+"/.aws/credentials", []byte("[bar]\n"), 0644)
os.Setenv("HOME", d)
_, err = aws.SharedAuth()
c.Assert(err, ErrorMatches, "Couldn't find profile in AWS credentials file")
}
func (s *S) TestSharedAuthNoKeysInProfile(c *C) {
os.Clearenv()
os.Setenv("AWS_PROFILE", "bar")
d, err := ioutil.TempDir("", "")
if err != nil {
panic(err)
}
defer os.RemoveAll(d)
err = os.Mkdir(d+"/.aws", 0755)
if err != nil {
panic(err)
}
ioutil.WriteFile(d+"/.aws/credentials", []byte("[bar]\nawsaccesskeyid = AK.."), 0644)
os.Setenv("HOME", d)
_, err = aws.SharedAuth()
c.Assert(err, ErrorMatches, "AWS_SECRET_ACCESS_KEY not found in credentials file")
}
func (s *S) TestSharedAuthDefaultCredentials(c *C) {
os.Clearenv()
d, err := ioutil.TempDir("", "")
if err != nil {
panic(err)
}
defer os.RemoveAll(d)
err = os.Mkdir(d+"/.aws", 0755)
if err != nil {
panic(err)
}
ioutil.WriteFile(d+"/.aws/credentials", []byte("[default]\naws_access_key_id = access\naws_secret_access_key = secret\n"), 0644)
os.Setenv("HOME", d)
auth, err := aws.SharedAuth()
c.Assert(err, IsNil)
c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"})
}
func (s *S) TestSharedAuth(c *C) {
os.Clearenv()
os.Setenv("AWS_PROFILE", "bar")
d, err := ioutil.TempDir("", "")
if err != nil {
panic(err)
}
defer os.RemoveAll(d)
err = os.Mkdir(d+"/.aws", 0755)
if err != nil {
panic(err)
}
ioutil.WriteFile(d+"/.aws/credentials", []byte("[bar]\naws_access_key_id = access\naws_secret_access_key = secret\n"), 0644)
os.Setenv("HOME", d)
auth, err := aws.SharedAuth()
c.Assert(err, IsNil)
c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"})
}
func (s *S) TestEnvAuthNoSecret(c *C) {
os.Clearenv()
_, err := aws.EnvAuth()
c.Assert(err, ErrorMatches, "AWS_SECRET_ACCESS_KEY or AWS_SECRET_KEY not found in environment")
}
func (s *S) TestEnvAuthNoAccess(c *C) {
os.Clearenv()
os.Setenv("AWS_SECRET_ACCESS_KEY", "foo")
_, err := aws.EnvAuth()
c.Assert(err, ErrorMatches, "AWS_ACCESS_KEY_ID or AWS_ACCESS_KEY not found in environment")
}
func (s *S) TestEnvAuth(c *C) {
os.Clearenv()
os.Setenv("AWS_SECRET_ACCESS_KEY", "secret")
os.Setenv("AWS_ACCESS_KEY_ID", "access")
auth, err := aws.EnvAuth()
c.Assert(err, IsNil)
c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"})
}
func (s *S) TestEnvAuthWithToken(c *C) {
os.Clearenv()
os.Setenv("AWS_SECRET_ACCESS_KEY", "secret")
os.Setenv("AWS_ACCESS_KEY_ID", "access")
os.Setenv("AWS_SECURITY_TOKEN", "token")
auth, err := aws.EnvAuth()
c.Assert(err, IsNil)
c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access", Token: "token"})
}
func (s *S) TestEnvAuthAlt(c *C) {
os.Clearenv()
os.Setenv("AWS_SECRET_KEY", "secret")
os.Setenv("AWS_ACCESS_KEY", "access")
auth, err := aws.EnvAuth()
c.Assert(err, IsNil)
c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"})
}
func (s *S) TestGetAuthStatic(c *C) {
auth, err := aws.GetAuth("access", "secret")
c.Assert(err, IsNil)
c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"})
}
func (s *S) TestGetAuthEnv(c *C) {
os.Clearenv()
os.Setenv("AWS_SECRET_ACCESS_KEY", "secret")
os.Setenv("AWS_ACCESS_KEY_ID", "access")
auth, err := aws.GetAuth("", "")
c.Assert(err, IsNil)
c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"})
}
func (s *S) TestEncode(c *C) {
c.Assert(aws.Encode("foo"), Equals, "foo")
c.Assert(aws.Encode("/"), Equals, "%2F")
}
func (s *S) TestRegionsAreNamed(c *C) {
for n, r := range aws.Regions {
c.Assert(n, Equals, r.Name)
}
}

View File

@@ -0,0 +1,125 @@
package aws
import (
"math"
"net"
"net/http"
"time"
)
type RetryableFunc func(*http.Request, *http.Response, error) bool
type WaitFunc func(try int)
type DeadlineFunc func() time.Time
type ResilientTransport struct {
// Timeout is the maximum amount of time a dial will wait for
// a connect to complete.
//
// The default is no timeout.
//
// With or without a timeout, the operating system may impose
// its own earlier timeout. For instance, TCP timeouts are
// often around 3 minutes.
DialTimeout time.Duration
// MaxTries, if non-zero, specifies the number of times we will retry on
// failure. Retries are only attempted for temporary network errors or known
// safe failures.
MaxTries int
Deadline DeadlineFunc
ShouldRetry RetryableFunc
Wait WaitFunc
transport *http.Transport
}
// Convenience method for creating an http client
func NewClient(rt *ResilientTransport) *http.Client {
rt.transport = &http.Transport{
Dial: func(netw, addr string) (net.Conn, error) {
c, err := net.DialTimeout(netw, addr, rt.DialTimeout)
if err != nil {
return nil, err
}
c.SetDeadline(rt.Deadline())
return c, nil
},
DisableKeepAlives: true,
Proxy: http.ProxyFromEnvironment,
}
// TODO: Would be nice is ResilientTransport allowed clients to initialize
// with http.Transport attributes.
return &http.Client{
Transport: rt,
}
}
var retryingTransport = &ResilientTransport{
Deadline: func() time.Time {
return time.Now().Add(5 * time.Second)
},
DialTimeout: 10 * time.Second,
MaxTries: 3,
ShouldRetry: awsRetry,
Wait: ExpBackoff,
}
// Exported default client
var RetryingClient = NewClient(retryingTransport)
func (t *ResilientTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return t.tries(req)
}
// Retry a request a maximum of t.MaxTries times.
// We'll only retry if the proper criteria are met.
// If a wait function is specified, wait that amount of time
// In between requests.
func (t *ResilientTransport) tries(req *http.Request) (res *http.Response, err error) {
for try := 0; try < t.MaxTries; try += 1 {
res, err = t.transport.RoundTrip(req)
if !t.ShouldRetry(req, res, err) {
break
}
if res != nil {
res.Body.Close()
}
if t.Wait != nil {
t.Wait(try)
}
}
return
}
func ExpBackoff(try int) {
time.Sleep(100 * time.Millisecond *
time.Duration(math.Exp2(float64(try))))
}
func LinearBackoff(try int) {
time.Sleep(time.Duration(try*100) * time.Millisecond)
}
// Decide if we should retry a request.
// In general, the criteria for retrying a request is described here
// http://docs.aws.amazon.com/general/latest/gr/api-retries.html
func awsRetry(req *http.Request, res *http.Response, err error) bool {
retry := false
// Retry if there's a temporary network error.
if neterr, ok := err.(net.Error); ok {
if neterr.Temporary() {
retry = true
}
}
// Retry if we get a 5xx series error.
if res != nil {
if res.StatusCode >= 500 && res.StatusCode < 600 {
retry = true
}
}
return retry
}

View File

@@ -0,0 +1,121 @@
package aws_test
import (
"fmt"
"github.com/mitchellh/goamz/aws"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
// Retrieve the response from handler using aws.RetryingClient
func serveAndGet(handler http.HandlerFunc) (body string, err error) {
ts := httptest.NewServer(handler)
defer ts.Close()
resp, err := aws.RetryingClient.Get(ts.URL)
if err != nil {
return
}
if resp.StatusCode != 200 {
return "", fmt.Errorf("Bad status code: %d", resp.StatusCode)
}
greeting, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return
}
return strings.TrimSpace(string(greeting)), nil
}
func TestClient_expected(t *testing.T) {
body := "foo bar"
resp, err := serveAndGet(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, body)
})
if err != nil {
t.Fatal(err)
}
if resp != body {
t.Fatal("Body not as expected.")
}
}
func TestClient_delay(t *testing.T) {
body := "baz"
wait := 4
resp, err := serveAndGet(func(w http.ResponseWriter, r *http.Request) {
if wait < 0 {
// If we dipped to zero delay and still failed.
t.Fatal("Never succeeded.")
}
wait -= 1
time.Sleep(time.Second * time.Duration(wait))
fmt.Fprintln(w, body)
})
if err != nil {
t.Fatal(err)
}
if resp != body {
t.Fatal("Body not as expected.", resp)
}
}
func TestClient_no4xxRetry(t *testing.T) {
tries := 0
// Fail once before succeeding.
_, err := serveAndGet(func(w http.ResponseWriter, r *http.Request) {
tries += 1
http.Error(w, "error", 404)
})
if err == nil {
t.Fatal("should have error")
}
if tries != 1 {
t.Fatalf("should only try once: %d", tries)
}
}
func TestClient_retries(t *testing.T) {
body := "biz"
failed := false
// Fail once before succeeding.
resp, err := serveAndGet(func(w http.ResponseWriter, r *http.Request) {
if !failed {
http.Error(w, "error", 500)
failed = true
} else {
fmt.Fprintln(w, body)
}
})
if failed != true {
t.Error("We didn't retry!")
}
if err != nil {
t.Fatal(err)
}
if resp != body {
t.Fatal("Body not as expected.")
}
}
func TestClient_fails(t *testing.T) {
tries := 0
// Fail 3 times and return the last error.
_, err := serveAndGet(func(w http.ResponseWriter, r *http.Request) {
tries += 1
http.Error(w, "error", 500)
})
if err == nil {
t.Fatal(err)
}
if tries != 3 {
t.Fatal("Didn't retry enough")
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,203 @@
package ec2_test
import (
"crypto/rand"
"fmt"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/goamz/testutil"
. "github.com/motain/gocheck"
)
// AmazonServer represents an Amazon EC2 server.
type AmazonServer struct {
auth aws.Auth
}
func (s *AmazonServer) SetUp(c *C) {
auth, err := aws.EnvAuth()
if err != nil {
c.Fatal(err.Error())
}
s.auth = auth
}
// Suite cost per run: 0.02 USD
var _ = Suite(&AmazonClientSuite{})
// AmazonClientSuite tests the client against a live EC2 server.
type AmazonClientSuite struct {
srv AmazonServer
ClientTests
}
func (s *AmazonClientSuite) SetUpSuite(c *C) {
if !testutil.Amazon {
c.Skip("AmazonClientSuite tests not enabled")
}
s.srv.SetUp(c)
s.ec2 = ec2.NewWithClient(s.srv.auth, aws.USEast, testutil.DefaultClient)
}
// ClientTests defines integration tests designed to test the client.
// It is not used as a test suite in itself, but embedded within
// another type.
type ClientTests struct {
ec2 *ec2.EC2
}
var imageId = "ami-ccf405a5" // Ubuntu Maverick, i386, EBS store
// Cost: 0.00 USD
func (s *ClientTests) TestRunInstancesError(c *C) {
options := ec2.RunInstances{
ImageId: "ami-a6f504cf", // Ubuntu Maverick, i386, instance store
InstanceType: "t1.micro", // Doesn't work with micro, results in 400.
}
resp, err := s.ec2.RunInstances(&options)
c.Assert(resp, IsNil)
c.Assert(err, ErrorMatches, "AMI.*root device.*not supported.*")
ec2err, ok := err.(*ec2.Error)
c.Assert(ok, Equals, true)
c.Assert(ec2err.StatusCode, Equals, 400)
c.Assert(ec2err.Code, Equals, "UnsupportedOperation")
c.Assert(ec2err.Message, Matches, "AMI.*root device.*not supported.*")
c.Assert(ec2err.RequestId, Matches, ".+")
}
// Cost: 0.02 USD
func (s *ClientTests) TestRunAndTerminate(c *C) {
options := ec2.RunInstances{
ImageId: imageId,
InstanceType: "t1.micro",
}
resp1, err := s.ec2.RunInstances(&options)
c.Assert(err, IsNil)
c.Check(resp1.ReservationId, Matches, "r-[0-9a-f]*")
c.Check(resp1.OwnerId, Matches, "[0-9]+")
c.Check(resp1.Instances, HasLen, 1)
c.Check(resp1.Instances[0].InstanceType, Equals, "t1.micro")
instId := resp1.Instances[0].InstanceId
resp2, err := s.ec2.Instances([]string{instId}, nil)
c.Assert(err, IsNil)
if c.Check(resp2.Reservations, HasLen, 1) && c.Check(len(resp2.Reservations[0].Instances), Equals, 1) {
inst := resp2.Reservations[0].Instances[0]
c.Check(inst.InstanceId, Equals, instId)
}
resp3, err := s.ec2.TerminateInstances([]string{instId})
c.Assert(err, IsNil)
c.Check(resp3.StateChanges, HasLen, 1)
c.Check(resp3.StateChanges[0].InstanceId, Equals, instId)
c.Check(resp3.StateChanges[0].CurrentState.Name, Equals, "shutting-down")
c.Check(resp3.StateChanges[0].CurrentState.Code, Equals, 32)
}
// Cost: 0.00 USD
func (s *ClientTests) TestSecurityGroups(c *C) {
name := "goamz-test"
descr := "goamz security group for tests"
// Clean it up, if a previous test left it around and avoid leaving it around.
s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Name: name})
defer s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Name: name})
resp1, err := s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: name, Description: descr})
c.Assert(err, IsNil)
c.Assert(resp1.RequestId, Matches, ".+")
c.Assert(resp1.Name, Equals, name)
c.Assert(resp1.Id, Matches, ".+")
resp1, err = s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: name, Description: descr})
ec2err, _ := err.(*ec2.Error)
c.Assert(resp1, IsNil)
c.Assert(ec2err, NotNil)
c.Assert(ec2err.Code, Equals, "InvalidGroup.Duplicate")
perms := []ec2.IPPerm{{
Protocol: "tcp",
FromPort: 0,
ToPort: 1024,
SourceIPs: []string{"127.0.0.1/24"},
}}
resp2, err := s.ec2.AuthorizeSecurityGroup(ec2.SecurityGroup{Name: name}, perms)
c.Assert(err, IsNil)
c.Assert(resp2.RequestId, Matches, ".+")
resp3, err := s.ec2.SecurityGroups(ec2.SecurityGroupNames(name), nil)
c.Assert(err, IsNil)
c.Assert(resp3.RequestId, Matches, ".+")
c.Assert(resp3.Groups, HasLen, 1)
g0 := resp3.Groups[0]
c.Assert(g0.Name, Equals, name)
c.Assert(g0.Description, Equals, descr)
c.Assert(g0.IPPerms, HasLen, 1)
c.Assert(g0.IPPerms[0].Protocol, Equals, "tcp")
c.Assert(g0.IPPerms[0].FromPort, Equals, 0)
c.Assert(g0.IPPerms[0].ToPort, Equals, 1024)
c.Assert(g0.IPPerms[0].SourceIPs, DeepEquals, []string{"127.0.0.1/24"})
resp2, err = s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Name: name})
c.Assert(err, IsNil)
c.Assert(resp2.RequestId, Matches, ".+")
}
var sessionId = func() string {
buf := make([]byte, 8)
// if we have no randomness, we'll just make do, so ignore the error.
rand.Read(buf)
return fmt.Sprintf("%x", buf)
}()
// sessionName reutrns a name that is probably
// unique to this test session.
func sessionName(prefix string) string {
return prefix + "-" + sessionId
}
var allRegions = []aws.Region{
aws.USEast,
aws.USWest,
aws.EUWest,
aws.APSoutheast,
aws.APNortheast,
}
// Communicate with all EC2 endpoints to see if they are alive.
func (s *ClientTests) TestRegions(c *C) {
name := sessionName("goamz-region-test")
perms := []ec2.IPPerm{{
Protocol: "tcp",
FromPort: 80,
ToPort: 80,
SourceIPs: []string{"127.0.0.1/32"},
}}
errs := make(chan error, len(allRegions))
for _, region := range allRegions {
go func(r aws.Region) {
e := ec2.NewWithClient(s.ec2.Auth, r, testutil.DefaultClient)
_, err := e.AuthorizeSecurityGroup(ec2.SecurityGroup{Name: name}, perms)
errs <- err
}(region)
}
for _ = range allRegions {
err := <-errs
if err != nil {
ec2_err, ok := err.(*ec2.Error)
if ok {
c.Check(ec2_err.Code, Matches, "InvalidGroup.NotFound")
} else {
c.Errorf("Non-EC2 error: %s", err)
}
} else {
c.Errorf("Test should have errored but it seems to have succeeded")
}
}
}

View File

@@ -0,0 +1,580 @@
package ec2_test
import (
"fmt"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/goamz/ec2/ec2test"
"github.com/mitchellh/goamz/testutil"
. "github.com/motain/gocheck"
"regexp"
"sort"
)
// LocalServer represents a local ec2test fake server.
type LocalServer struct {
auth aws.Auth
region aws.Region
srv *ec2test.Server
}
func (s *LocalServer) SetUp(c *C) {
srv, err := ec2test.NewServer()
c.Assert(err, IsNil)
c.Assert(srv, NotNil)
s.srv = srv
s.region = aws.Region{EC2Endpoint: srv.URL()}
}
// LocalServerSuite defines tests that will run
// against the local ec2test server. It includes
// selected tests from ClientTests;
// when the ec2test functionality is sufficient, it should
// include all of them, and ClientTests can be simply embedded.
type LocalServerSuite struct {
srv LocalServer
ServerTests
clientTests ClientTests
}
var _ = Suite(&LocalServerSuite{})
func (s *LocalServerSuite) SetUpSuite(c *C) {
s.srv.SetUp(c)
s.ServerTests.ec2 = ec2.NewWithClient(s.srv.auth, s.srv.region, testutil.DefaultClient)
s.clientTests.ec2 = ec2.NewWithClient(s.srv.auth, s.srv.region, testutil.DefaultClient)
}
func (s *LocalServerSuite) TestRunAndTerminate(c *C) {
s.clientTests.TestRunAndTerminate(c)
}
func (s *LocalServerSuite) TestSecurityGroups(c *C) {
s.clientTests.TestSecurityGroups(c)
}
// TestUserData is not defined on ServerTests because it
// requires the ec2test server to function.
func (s *LocalServerSuite) TestUserData(c *C) {
data := make([]byte, 256)
for i := range data {
data[i] = byte(i)
}
inst, err := s.ec2.RunInstances(&ec2.RunInstances{
ImageId: imageId,
InstanceType: "t1.micro",
UserData: data,
})
c.Assert(err, IsNil)
c.Assert(inst, NotNil)
c.Assert(inst.Instances[0].DNSName, Equals, inst.Instances[0].InstanceId+".example.com")
id := inst.Instances[0].InstanceId
defer s.ec2.TerminateInstances([]string{id})
tinst := s.srv.srv.Instance(id)
c.Assert(tinst, NotNil)
c.Assert(tinst.UserData, DeepEquals, data)
}
// AmazonServerSuite runs the ec2test server tests against a live EC2 server.
// It will only be activated if the -all flag is specified.
type AmazonServerSuite struct {
srv AmazonServer
ServerTests
}
var _ = Suite(&AmazonServerSuite{})
func (s *AmazonServerSuite) SetUpSuite(c *C) {
if !testutil.Amazon {
c.Skip("AmazonServerSuite tests not enabled")
}
s.srv.SetUp(c)
s.ServerTests.ec2 = ec2.NewWithClient(s.srv.auth, aws.USEast, testutil.DefaultClient)
}
// ServerTests defines a set of tests designed to test
// the ec2test local fake ec2 server.
// It is not used as a test suite in itself, but embedded within
// another type.
type ServerTests struct {
ec2 *ec2.EC2
}
func terminateInstances(c *C, e *ec2.EC2, insts []*ec2.Instance) {
var ids []string
for _, inst := range insts {
if inst != nil {
ids = append(ids, inst.InstanceId)
}
}
_, err := e.TerminateInstances(ids)
c.Check(err, IsNil, Commentf("%d INSTANCES LEFT RUNNING!!!", len(ids)))
}
func (s *ServerTests) makeTestGroup(c *C, name, descr string) ec2.SecurityGroup {
// Clean it up if a previous test left it around.
_, err := s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Name: name})
if err != nil && err.(*ec2.Error).Code != "InvalidGroup.NotFound" {
c.Fatalf("delete security group: %v", err)
}
resp, err := s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: name, Description: descr})
c.Assert(err, IsNil)
c.Assert(resp.Name, Equals, name)
return resp.SecurityGroup
}
func (s *ServerTests) TestIPPerms(c *C) {
g0 := s.makeTestGroup(c, "goamz-test0", "ec2test group 0")
defer s.ec2.DeleteSecurityGroup(g0)
g1 := s.makeTestGroup(c, "goamz-test1", "ec2test group 1")
defer s.ec2.DeleteSecurityGroup(g1)
resp, err := s.ec2.SecurityGroups([]ec2.SecurityGroup{g0, g1}, nil)
c.Assert(err, IsNil)
c.Assert(resp.Groups, HasLen, 2)
c.Assert(resp.Groups[0].IPPerms, HasLen, 0)
c.Assert(resp.Groups[1].IPPerms, HasLen, 0)
ownerId := resp.Groups[0].OwnerId
// test some invalid parameters
// TODO more
_, err = s.ec2.AuthorizeSecurityGroup(g0, []ec2.IPPerm{{
Protocol: "tcp",
FromPort: 0,
ToPort: 1024,
SourceIPs: []string{"z127.0.0.1/24"},
}})
c.Assert(err, NotNil)
c.Check(err.(*ec2.Error).Code, Equals, "InvalidPermission.Malformed")
// Check that AuthorizeSecurityGroup adds the correct authorizations.
_, err = s.ec2.AuthorizeSecurityGroup(g0, []ec2.IPPerm{{
Protocol: "tcp",
FromPort: 2000,
ToPort: 2001,
SourceIPs: []string{"127.0.0.0/24"},
SourceGroups: []ec2.UserSecurityGroup{{
Name: g1.Name,
}, {
Id: g0.Id,
}},
}, {
Protocol: "tcp",
FromPort: 2000,
ToPort: 2001,
SourceIPs: []string{"200.1.1.34/32"},
}})
c.Assert(err, IsNil)
resp, err = s.ec2.SecurityGroups([]ec2.SecurityGroup{g0}, nil)
c.Assert(err, IsNil)
c.Assert(resp.Groups, HasLen, 1)
c.Assert(resp.Groups[0].IPPerms, HasLen, 1)
perm := resp.Groups[0].IPPerms[0]
srcg := perm.SourceGroups
c.Assert(srcg, HasLen, 2)
// Normalize so we don't care about returned order.
if srcg[0].Name == g1.Name {
srcg[0], srcg[1] = srcg[1], srcg[0]
}
c.Check(srcg[0].Name, Equals, g0.Name)
c.Check(srcg[0].Id, Equals, g0.Id)
c.Check(srcg[0].OwnerId, Equals, ownerId)
c.Check(srcg[1].Name, Equals, g1.Name)
c.Check(srcg[1].Id, Equals, g1.Id)
c.Check(srcg[1].OwnerId, Equals, ownerId)
sort.Strings(perm.SourceIPs)
c.Check(perm.SourceIPs, DeepEquals, []string{"127.0.0.0/24", "200.1.1.34/32"})
// Check that we can't delete g1 (because g0 is using it)
_, err = s.ec2.DeleteSecurityGroup(g1)
c.Assert(err, NotNil)
c.Check(err.(*ec2.Error).Code, Equals, "InvalidGroup.InUse")
_, err = s.ec2.RevokeSecurityGroup(g0, []ec2.IPPerm{{
Protocol: "tcp",
FromPort: 2000,
ToPort: 2001,
SourceGroups: []ec2.UserSecurityGroup{{Id: g1.Id}},
}, {
Protocol: "tcp",
FromPort: 2000,
ToPort: 2001,
SourceIPs: []string{"200.1.1.34/32"},
}})
c.Assert(err, IsNil)
resp, err = s.ec2.SecurityGroups([]ec2.SecurityGroup{g0}, nil)
c.Assert(err, IsNil)
c.Assert(resp.Groups, HasLen, 1)
c.Assert(resp.Groups[0].IPPerms, HasLen, 1)
perm = resp.Groups[0].IPPerms[0]
srcg = perm.SourceGroups
c.Assert(srcg, HasLen, 1)
c.Check(srcg[0].Name, Equals, g0.Name)
c.Check(srcg[0].Id, Equals, g0.Id)
c.Check(srcg[0].OwnerId, Equals, ownerId)
c.Check(perm.SourceIPs, DeepEquals, []string{"127.0.0.0/24"})
// We should be able to delete g1 now because we've removed its only use.
_, err = s.ec2.DeleteSecurityGroup(g1)
c.Assert(err, IsNil)
_, err = s.ec2.DeleteSecurityGroup(g0)
c.Assert(err, IsNil)
f := ec2.NewFilter()
f.Add("group-id", g0.Id, g1.Id)
resp, err = s.ec2.SecurityGroups(nil, f)
c.Assert(err, IsNil)
c.Assert(resp.Groups, HasLen, 0)
}
func (s *ServerTests) TestDuplicateIPPerm(c *C) {
name := "goamz-test"
descr := "goamz security group for tests"
// Clean it up, if a previous test left it around and avoid leaving it around.
s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Name: name})
defer s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Name: name})
resp1, err := s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: name, Description: descr})
c.Assert(err, IsNil)
c.Assert(resp1.Name, Equals, name)
perms := []ec2.IPPerm{{
Protocol: "tcp",
FromPort: 200,
ToPort: 1024,
SourceIPs: []string{"127.0.0.1/24"},
}, {
Protocol: "tcp",
FromPort: 0,
ToPort: 100,
SourceIPs: []string{"127.0.0.1/24"},
}}
_, err = s.ec2.AuthorizeSecurityGroup(ec2.SecurityGroup{Name: name}, perms[0:1])
c.Assert(err, IsNil)
_, err = s.ec2.AuthorizeSecurityGroup(ec2.SecurityGroup{Name: name}, perms[0:2])
c.Assert(err, ErrorMatches, `.*\(InvalidPermission.Duplicate\)`)
}
type filterSpec struct {
name string
values []string
}
func (s *ServerTests) TestInstanceFiltering(c *C) {
groupResp, err := s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: sessionName("testgroup1"), Description: "testgroup one description"})
c.Assert(err, IsNil)
group1 := groupResp.SecurityGroup
defer s.ec2.DeleteSecurityGroup(group1)
groupResp, err = s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: sessionName("testgroup2"), Description: "testgroup two description"})
c.Assert(err, IsNil)
group2 := groupResp.SecurityGroup
defer s.ec2.DeleteSecurityGroup(group2)
insts := make([]*ec2.Instance, 3)
inst, err := s.ec2.RunInstances(&ec2.RunInstances{
MinCount: 2,
ImageId: imageId,
InstanceType: "t1.micro",
SecurityGroups: []ec2.SecurityGroup{group1},
})
c.Assert(err, IsNil)
insts[0] = &inst.Instances[0]
insts[1] = &inst.Instances[1]
defer terminateInstances(c, s.ec2, insts)
imageId2 := "ami-e358958a" // Natty server, i386, EBS store
inst, err = s.ec2.RunInstances(&ec2.RunInstances{
ImageId: imageId2,
InstanceType: "t1.micro",
SecurityGroups: []ec2.SecurityGroup{group2},
})
c.Assert(err, IsNil)
insts[2] = &inst.Instances[0]
ids := func(indices ...int) (instIds []string) {
for _, index := range indices {
instIds = append(instIds, insts[index].InstanceId)
}
return
}
tests := []struct {
about string
instanceIds []string // instanceIds argument to Instances method.
filters []filterSpec // filters argument to Instances method.
resultIds []string // set of instance ids of expected results.
allowExtra bool // resultIds may be incomplete.
err string // expected error.
}{
{
about: "check that Instances returns all instances",
resultIds: ids(0, 1, 2),
allowExtra: true,
}, {
about: "check that specifying two instance ids returns them",
instanceIds: ids(0, 2),
resultIds: ids(0, 2),
}, {
about: "check that specifying a non-existent instance id gives an error",
instanceIds: append(ids(0), "i-deadbeef"),
err: `.*\(InvalidInstanceID\.NotFound\)`,
}, {
about: "check that a filter allowed both instances returns both of them",
filters: []filterSpec{
{"instance-id", ids(0, 2)},
},
resultIds: ids(0, 2),
}, {
about: "check that a filter allowing only one instance returns it",
filters: []filterSpec{
{"instance-id", ids(1)},
},
resultIds: ids(1),
}, {
about: "check that a filter allowing no instances returns none",
filters: []filterSpec{
{"instance-id", []string{"i-deadbeef12345"}},
},
}, {
about: "check that filtering on group id works",
filters: []filterSpec{
{"group-id", []string{group1.Id}},
},
resultIds: ids(0, 1),
}, {
about: "check that filtering on group name works",
filters: []filterSpec{
{"group-name", []string{group1.Name}},
},
resultIds: ids(0, 1),
}, {
about: "check that filtering on image id works",
filters: []filterSpec{
{"image-id", []string{imageId}},
},
resultIds: ids(0, 1),
allowExtra: true,
}, {
about: "combination filters 1",
filters: []filterSpec{
{"image-id", []string{imageId, imageId2}},
{"group-name", []string{group1.Name}},
},
resultIds: ids(0, 1),
}, {
about: "combination filters 2",
filters: []filterSpec{
{"image-id", []string{imageId2}},
{"group-name", []string{group1.Name}},
},
},
}
for i, t := range tests {
c.Logf("%d. %s", i, t.about)
var f *ec2.Filter
if t.filters != nil {
f = ec2.NewFilter()
for _, spec := range t.filters {
f.Add(spec.name, spec.values...)
}
}
resp, err := s.ec2.Instances(t.instanceIds, f)
if t.err != "" {
c.Check(err, ErrorMatches, t.err)
continue
}
c.Assert(err, IsNil)
insts := make(map[string]*ec2.Instance)
for _, r := range resp.Reservations {
for j := range r.Instances {
inst := &r.Instances[j]
c.Check(insts[inst.InstanceId], IsNil, Commentf("duplicate instance id: %q", inst.InstanceId))
insts[inst.InstanceId] = inst
}
}
if !t.allowExtra {
c.Check(insts, HasLen, len(t.resultIds), Commentf("expected %d instances got %#v", len(t.resultIds), insts))
}
for j, id := range t.resultIds {
c.Check(insts[id], NotNil, Commentf("instance id %d (%q) not found; got %#v", j, id, insts))
}
}
}
func idsOnly(gs []ec2.SecurityGroup) []ec2.SecurityGroup {
for i := range gs {
gs[i].Name = ""
}
return gs
}
func namesOnly(gs []ec2.SecurityGroup) []ec2.SecurityGroup {
for i := range gs {
gs[i].Id = ""
}
return gs
}
func (s *ServerTests) TestGroupFiltering(c *C) {
g := make([]ec2.SecurityGroup, 4)
for i := range g {
resp, err := s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: sessionName(fmt.Sprintf("testgroup%d", i)), Description: fmt.Sprintf("testdescription%d", i)})
c.Assert(err, IsNil)
g[i] = resp.SecurityGroup
c.Logf("group %d: %v", i, g[i])
defer s.ec2.DeleteSecurityGroup(g[i])
}
perms := [][]ec2.IPPerm{
{{
Protocol: "tcp",
FromPort: 100,
ToPort: 200,
SourceIPs: []string{"1.2.3.4/32"},
}},
{{
Protocol: "tcp",
FromPort: 200,
ToPort: 300,
SourceGroups: []ec2.UserSecurityGroup{{Id: g[1].Id}},
}},
{{
Protocol: "udp",
FromPort: 200,
ToPort: 400,
SourceGroups: []ec2.UserSecurityGroup{{Id: g[1].Id}},
}},
}
for i, ps := range perms {
_, err := s.ec2.AuthorizeSecurityGroup(g[i], ps)
c.Assert(err, IsNil)
}
groups := func(indices ...int) (gs []ec2.SecurityGroup) {
for _, index := range indices {
gs = append(gs, g[index])
}
return
}
type groupTest struct {
about string
groups []ec2.SecurityGroup // groupIds argument to SecurityGroups method.
filters []filterSpec // filters argument to SecurityGroups method.
results []ec2.SecurityGroup // set of expected result groups.
allowExtra bool // specified results may be incomplete.
err string // expected error.
}
filterCheck := func(name, val string, gs []ec2.SecurityGroup) groupTest {
return groupTest{
about: "filter check " + name,
filters: []filterSpec{{name, []string{val}}},
results: gs,
allowExtra: true,
}
}
tests := []groupTest{
{
about: "check that SecurityGroups returns all groups",
results: groups(0, 1, 2, 3),
allowExtra: true,
}, {
about: "check that specifying two group ids returns them",
groups: idsOnly(groups(0, 2)),
results: groups(0, 2),
}, {
about: "check that specifying names only works",
groups: namesOnly(groups(0, 2)),
results: groups(0, 2),
}, {
about: "check that specifying a non-existent group id gives an error",
groups: append(groups(0), ec2.SecurityGroup{Id: "sg-eeeeeeeee"}),
err: `.*\(InvalidGroup\.NotFound\)`,
}, {
about: "check that a filter allowed two groups returns both of them",
filters: []filterSpec{
{"group-id", []string{g[0].Id, g[2].Id}},
},
results: groups(0, 2),
},
{
about: "check that the previous filter works when specifying a list of ids",
groups: groups(1, 2),
filters: []filterSpec{
{"group-id", []string{g[0].Id, g[2].Id}},
},
results: groups(2),
}, {
about: "check that a filter allowing no groups returns none",
filters: []filterSpec{
{"group-id", []string{"sg-eeeeeeeee"}},
},
},
filterCheck("description", "testdescription1", groups(1)),
filterCheck("group-name", g[2].Name, groups(2)),
filterCheck("ip-permission.cidr", "1.2.3.4/32", groups(0)),
filterCheck("ip-permission.group-name", g[1].Name, groups(1, 2)),
filterCheck("ip-permission.protocol", "udp", groups(2)),
filterCheck("ip-permission.from-port", "200", groups(1, 2)),
filterCheck("ip-permission.to-port", "200", groups(0)),
// TODO owner-id
}
for i, t := range tests {
c.Logf("%d. %s", i, t.about)
var f *ec2.Filter
if t.filters != nil {
f = ec2.NewFilter()
for _, spec := range t.filters {
f.Add(spec.name, spec.values...)
}
}
resp, err := s.ec2.SecurityGroups(t.groups, f)
if t.err != "" {
c.Check(err, ErrorMatches, t.err)
continue
}
c.Assert(err, IsNil)
groups := make(map[string]*ec2.SecurityGroup)
for j := range resp.Groups {
group := &resp.Groups[j].SecurityGroup
c.Check(groups[group.Id], IsNil, Commentf("duplicate group id: %q", group.Id))
groups[group.Id] = group
}
// If extra groups may be returned, eliminate all groups that
// we did not create in this session apart from the default group.
if t.allowExtra {
namePat := regexp.MustCompile(sessionName("testgroup[0-9]"))
for id, g := range groups {
if !namePat.MatchString(g.Name) {
delete(groups, id)
}
}
}
c.Check(groups, HasLen, len(t.results))
for j, g := range t.results {
rg := groups[g.Id]
c.Assert(rg, NotNil, Commentf("group %d (%v) not found; got %#v", j, g, groups))
c.Check(rg.Name, Equals, g.Name, Commentf("group %d (%v)", j, g))
}
}
}

View File

@@ -0,0 +1,84 @@
package ec2test
import (
"fmt"
"net/url"
"strings"
)
// filter holds an ec2 filter. A filter maps an attribute to a set of
// possible values for that attribute. For an item to pass through the
// filter, every attribute of the item mentioned in the filter must match
// at least one of its given values.
type filter map[string][]string
// newFilter creates a new filter from the Filter fields in the url form.
//
// The filtering is specified through a map of name=>values, where the
// name is a well-defined key identifying the data to be matched,
// and the list of values holds the possible values the filtered
// item can take for the key to be included in the
// result set. For example:
//
// Filter.1.Name=instance-type
// Filter.1.Value.1=m1.small
// Filter.1.Value.2=m1.large
//
func newFilter(form url.Values) filter {
// TODO return an error if the fields are not well formed?
names := make(map[int]string)
values := make(map[int][]string)
maxId := 0
for name, fvalues := range form {
var rest string
var id int
if x, _ := fmt.Sscanf(name, "Filter.%d.%s", &id, &rest); x != 2 {
continue
}
if id > maxId {
maxId = id
}
if rest == "Name" {
names[id] = fvalues[0]
continue
}
if !strings.HasPrefix(rest, "Value.") {
continue
}
values[id] = append(values[id], fvalues[0])
}
f := make(filter)
for id, name := range names {
f[name] = values[id]
}
return f
}
func notDigit(r rune) bool {
return r < '0' || r > '9'
}
// filterable represents an object that can be passed through a filter.
type filterable interface {
// matchAttr returns true if given attribute of the
// object matches value. It returns an error if the
// attribute is not recognised or the value is malformed.
matchAttr(attr, value string) (bool, error)
}
// ok returns true if x passes through the filter.
func (f filter) ok(x filterable) (bool, error) {
next:
for a, vs := range f {
for _, v := range vs {
if ok, err := x.matchAttr(a, v); ok {
continue next
} else if err != nil {
return false, fmt.Errorf("bad attribute or value %q=%q for type %T: %v", a, v, x, err)
}
}
return false, nil
}
return true, nil
}

View File

@@ -0,0 +1,993 @@
// The ec2test package implements a fake EC2 provider with
// the capability of inducing errors on any given operation,
// and retrospectively determining what operations have been
// carried out.
package ec2test
import (
"encoding/base64"
"encoding/xml"
"fmt"
"github.com/mitchellh/goamz/ec2"
"io"
"net"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"sync"
)
var b64 = base64.StdEncoding
// Action represents a request that changes the ec2 state.
type Action struct {
RequestId string
// Request holds the requested action as a url.Values instance
Request url.Values
// If the action succeeded, Response holds the value that
// was marshalled to build the XML response for the request.
Response interface{}
// If the action failed, Err holds an error giving details of the failure.
Err *ec2.Error
}
// TODO possible other things:
// - some virtual time stamp interface, so a client
// can ask for all actions after a certain virtual time.
// Server implements an EC2 simulator for use in testing.
type Server struct {
url string
listener net.Listener
mu sync.Mutex
reqs []*Action
instances map[string]*Instance // id -> instance
reservations map[string]*reservation // id -> reservation
groups map[string]*securityGroup // id -> group
maxId counter
reqId counter
reservationId counter
groupId counter
initialInstanceState ec2.InstanceState
}
// reservation holds a simulated ec2 reservation.
type reservation struct {
id string
instances map[string]*Instance
groups []*securityGroup
}
// instance holds a simulated ec2 instance
type Instance struct {
// UserData holds the data that was passed to the RunInstances request
// when the instance was started.
UserData []byte
id string
imageId string
reservation *reservation
instType string
state ec2.InstanceState
}
// permKey represents permission for a given security
// group or IP address (but not both) to access a given range of
// ports. Equality of permKeys is used in the implementation of
// permission sets, relying on the uniqueness of securityGroup
// instances.
type permKey struct {
protocol string
fromPort int
toPort int
group *securityGroup
ipAddr string
}
// securityGroup holds a simulated ec2 security group.
// Instances of securityGroup should only be created through
// Server.createSecurityGroup to ensure that groups can be
// compared by pointer value.
type securityGroup struct {
id string
name string
description string
perms map[permKey]bool
}
func (g *securityGroup) ec2SecurityGroup() ec2.SecurityGroup {
return ec2.SecurityGroup{
Name: g.name,
Id: g.id,
}
}
func (g *securityGroup) matchAttr(attr, value string) (ok bool, err error) {
switch attr {
case "description":
return g.description == value, nil
case "group-id":
return g.id == value, nil
case "group-name":
return g.name == value, nil
case "ip-permission.cidr":
return g.hasPerm(func(k permKey) bool { return k.ipAddr == value }), nil
case "ip-permission.group-name":
return g.hasPerm(func(k permKey) bool {
return k.group != nil && k.group.name == value
}), nil
case "ip-permission.from-port":
port, err := strconv.Atoi(value)
if err != nil {
return false, err
}
return g.hasPerm(func(k permKey) bool { return k.fromPort == port }), nil
case "ip-permission.to-port":
port, err := strconv.Atoi(value)
if err != nil {
return false, err
}
return g.hasPerm(func(k permKey) bool { return k.toPort == port }), nil
case "ip-permission.protocol":
return g.hasPerm(func(k permKey) bool { return k.protocol == value }), nil
case "owner-id":
return value == ownerId, nil
}
return false, fmt.Errorf("unknown attribute %q", attr)
}
func (g *securityGroup) hasPerm(test func(k permKey) bool) bool {
for k := range g.perms {
if test(k) {
return true
}
}
return false
}
// ec2Perms returns the list of EC2 permissions granted
// to g. It groups permissions by port range and protocol.
func (g *securityGroup) ec2Perms() (perms []ec2.IPPerm) {
// The grouping is held in result. We use permKey for convenience,
// (ensuring that the group and ipAddr of each key is zero). For
// each protocol/port range combination, we build up the permission
// set in the associated value.
result := make(map[permKey]*ec2.IPPerm)
for k := range g.perms {
groupKey := k
groupKey.group = nil
groupKey.ipAddr = ""
ec2p := result[groupKey]
if ec2p == nil {
ec2p = &ec2.IPPerm{
Protocol: k.protocol,
FromPort: k.fromPort,
ToPort: k.toPort,
}
result[groupKey] = ec2p
}
if k.group != nil {
ec2p.SourceGroups = append(ec2p.SourceGroups,
ec2.UserSecurityGroup{
Id: k.group.id,
Name: k.group.name,
OwnerId: ownerId,
})
} else {
ec2p.SourceIPs = append(ec2p.SourceIPs, k.ipAddr)
}
}
for _, ec2p := range result {
perms = append(perms, *ec2p)
}
return
}
var actions = map[string]func(*Server, http.ResponseWriter, *http.Request, string) interface{}{
"RunInstances": (*Server).runInstances,
"TerminateInstances": (*Server).terminateInstances,
"DescribeInstances": (*Server).describeInstances,
"CreateSecurityGroup": (*Server).createSecurityGroup,
"DescribeSecurityGroups": (*Server).describeSecurityGroups,
"DeleteSecurityGroup": (*Server).deleteSecurityGroup,
"AuthorizeSecurityGroupIngress": (*Server).authorizeSecurityGroupIngress,
"RevokeSecurityGroupIngress": (*Server).revokeSecurityGroupIngress,
}
const ownerId = "9876"
// newAction allocates a new action and adds it to the
// recorded list of server actions.
func (srv *Server) newAction() *Action {
srv.mu.Lock()
defer srv.mu.Unlock()
a := new(Action)
srv.reqs = append(srv.reqs, a)
return a
}
// NewServer returns a new server.
func NewServer() (*Server, error) {
srv := &Server{
instances: make(map[string]*Instance),
groups: make(map[string]*securityGroup),
reservations: make(map[string]*reservation),
initialInstanceState: Pending,
}
// Add default security group.
g := &securityGroup{
name: "default",
description: "default group",
id: fmt.Sprintf("sg-%d", srv.groupId.next()),
}
g.perms = map[permKey]bool{
permKey{
protocol: "icmp",
fromPort: -1,
toPort: -1,
group: g,
}: true,
permKey{
protocol: "tcp",
fromPort: 0,
toPort: 65535,
group: g,
}: true,
permKey{
protocol: "udp",
fromPort: 0,
toPort: 65535,
group: g,
}: true,
}
srv.groups[g.id] = g
l, err := net.Listen("tcp", "localhost:0")
if err != nil {
return nil, fmt.Errorf("cannot listen on localhost: %v", err)
}
srv.listener = l
srv.url = "http://" + l.Addr().String()
// we use HandlerFunc rather than *Server directly so that we
// can avoid exporting HandlerFunc from *Server.
go http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
srv.serveHTTP(w, req)
}))
return srv, nil
}
// Quit closes down the server.
func (srv *Server) Quit() {
srv.listener.Close()
}
// SetInitialInstanceState sets the state that any new instances will be started in.
func (srv *Server) SetInitialInstanceState(state ec2.InstanceState) {
srv.mu.Lock()
srv.initialInstanceState = state
srv.mu.Unlock()
}
// URL returns the URL of the server.
func (srv *Server) URL() string {
return srv.url
}
// serveHTTP serves the EC2 protocol.
func (srv *Server) serveHTTP(w http.ResponseWriter, req *http.Request) {
req.ParseForm()
a := srv.newAction()
a.RequestId = fmt.Sprintf("req%d", srv.reqId.next())
a.Request = req.Form
// Methods on Server that deal with parsing user data
// may fail. To save on error handling code, we allow these
// methods to call fatalf, which will panic with an *ec2.Error
// which will be caught here and returned
// to the client as a properly formed EC2 error.
defer func() {
switch err := recover().(type) {
case *ec2.Error:
a.Err = err
err.RequestId = a.RequestId
writeError(w, err)
case nil:
default:
panic(err)
}
}()
f := actions[req.Form.Get("Action")]
if f == nil {
fatalf(400, "InvalidParameterValue", "Unrecognized Action")
}
response := f(srv, w, req, a.RequestId)
a.Response = response
w.Header().Set("Content-Type", `xml version="1.0" encoding="UTF-8"`)
xmlMarshal(w, response)
}
// Instance returns the instance for the given instance id.
// It returns nil if there is no such instance.
func (srv *Server) Instance(id string) *Instance {
srv.mu.Lock()
defer srv.mu.Unlock()
return srv.instances[id]
}
// writeError writes an appropriate error response.
// TODO how should we deal with errors when the
// error itself is potentially generated by backend-agnostic
// code?
func writeError(w http.ResponseWriter, err *ec2.Error) {
// Error encapsulates an error returned by EC2.
// TODO merge with ec2.Error when xml supports ignoring a field.
type ec2error struct {
Code string // EC2 error code ("UnsupportedOperation", ...)
Message string // The human-oriented error message
RequestId string
}
type Response struct {
RequestId string
Errors []ec2error `xml:"Errors>Error"`
}
w.Header().Set("Content-Type", `xml version="1.0" encoding="UTF-8"`)
w.WriteHeader(err.StatusCode)
xmlMarshal(w, Response{
RequestId: err.RequestId,
Errors: []ec2error{{
Code: err.Code,
Message: err.Message,
}},
})
}
// xmlMarshal is the same as xml.Marshal except that
// it panics on error. The marshalling should not fail,
// but we want to know if it does.
func xmlMarshal(w io.Writer, x interface{}) {
if err := xml.NewEncoder(w).Encode(x); err != nil {
panic(fmt.Errorf("error marshalling %#v: %v", x, err))
}
}
// formToGroups parses a set of SecurityGroup form values
// as found in a RunInstances request, and returns the resulting
// slice of security groups.
// It calls fatalf if a group is not found.
func (srv *Server) formToGroups(form url.Values) []*securityGroup {
var groups []*securityGroup
for name, values := range form {
switch {
case strings.HasPrefix(name, "SecurityGroupId."):
if g := srv.groups[values[0]]; g != nil {
groups = append(groups, g)
} else {
fatalf(400, "InvalidGroup.NotFound", "unknown group id %q", values[0])
}
case strings.HasPrefix(name, "SecurityGroup."):
var found *securityGroup
for _, g := range srv.groups {
if g.name == values[0] {
found = g
}
}
if found == nil {
fatalf(400, "InvalidGroup.NotFound", "unknown group name %q", values[0])
}
groups = append(groups, found)
}
}
return groups
}
// runInstances implements the EC2 RunInstances entry point.
func (srv *Server) runInstances(w http.ResponseWriter, req *http.Request, reqId string) interface{} {
min := atoi(req.Form.Get("MinCount"))
max := atoi(req.Form.Get("MaxCount"))
if min < 0 || max < 1 {
fatalf(400, "InvalidParameterValue", "bad values for MinCount or MaxCount")
}
if min > max {
fatalf(400, "InvalidParameterCombination", "MinCount is greater than MaxCount")
}
var userData []byte
if data := req.Form.Get("UserData"); data != "" {
var err error
userData, err = b64.DecodeString(data)
if err != nil {
fatalf(400, "InvalidParameterValue", "bad UserData value: %v", err)
}
}
// TODO attributes still to consider:
// ImageId: accept anything, we can verify later
// KeyName ?
// InstanceType ?
// KernelId ?
// RamdiskId ?
// AvailZone ?
// GroupName tag
// Monitoring ignore?
// SubnetId ?
// DisableAPITermination bool
// ShutdownBehavior string
// PrivateIPAddress string
srv.mu.Lock()
defer srv.mu.Unlock()
// make sure that form fields are correct before creating the reservation.
instType := req.Form.Get("InstanceType")
imageId := req.Form.Get("ImageId")
r := srv.newReservation(srv.formToGroups(req.Form))
var resp ec2.RunInstancesResp
resp.RequestId = reqId
resp.ReservationId = r.id
resp.OwnerId = ownerId
for i := 0; i < max; i++ {
inst := srv.newInstance(r, instType, imageId, srv.initialInstanceState)
inst.UserData = userData
resp.Instances = append(resp.Instances, inst.ec2instance())
}
return &resp
}
func (srv *Server) group(group ec2.SecurityGroup) *securityGroup {
if group.Id != "" {
return srv.groups[group.Id]
}
for _, g := range srv.groups {
if g.name == group.Name {
return g
}
}
return nil
}
// NewInstances creates n new instances in srv with the given instance type,
// image ID, initial state and security groups. If any group does not already
// exist, it will be created. NewInstances returns the ids of the new instances.
func (srv *Server) NewInstances(n int, instType string, imageId string, state ec2.InstanceState, groups []ec2.SecurityGroup) []string {
srv.mu.Lock()
defer srv.mu.Unlock()
rgroups := make([]*securityGroup, len(groups))
for i, group := range groups {
g := srv.group(group)
if g == nil {
fatalf(400, "InvalidGroup.NotFound", "no such group %v", g)
}
rgroups[i] = g
}
r := srv.newReservation(rgroups)
ids := make([]string, n)
for i := 0; i < n; i++ {
inst := srv.newInstance(r, instType, imageId, state)
ids[i] = inst.id
}
return ids
}
func (srv *Server) newInstance(r *reservation, instType string, imageId string, state ec2.InstanceState) *Instance {
inst := &Instance{
id: fmt.Sprintf("i-%d", srv.maxId.next()),
instType: instType,
imageId: imageId,
state: state,
reservation: r,
}
srv.instances[inst.id] = inst
r.instances[inst.id] = inst
return inst
}
func (srv *Server) newReservation(groups []*securityGroup) *reservation {
r := &reservation{
id: fmt.Sprintf("r-%d", srv.reservationId.next()),
instances: make(map[string]*Instance),
groups: groups,
}
srv.reservations[r.id] = r
return r
}
func (srv *Server) terminateInstances(w http.ResponseWriter, req *http.Request, reqId string) interface{} {
srv.mu.Lock()
defer srv.mu.Unlock()
var resp ec2.TerminateInstancesResp
resp.RequestId = reqId
var insts []*Instance
for attr, vals := range req.Form {
if strings.HasPrefix(attr, "InstanceId.") {
id := vals[0]
inst := srv.instances[id]
if inst == nil {
fatalf(400, "InvalidInstanceID.NotFound", "no such instance id %q", id)
}
insts = append(insts, inst)
}
}
for _, inst := range insts {
resp.StateChanges = append(resp.StateChanges, inst.terminate())
}
return &resp
}
func (inst *Instance) terminate() (d ec2.InstanceStateChange) {
d.PreviousState = inst.state
inst.state = ShuttingDown
d.CurrentState = inst.state
d.InstanceId = inst.id
return d
}
func (inst *Instance) ec2instance() ec2.Instance {
return ec2.Instance{
InstanceId: inst.id,
InstanceType: inst.instType,
ImageId: inst.imageId,
DNSName: fmt.Sprintf("%s.example.com", inst.id),
// TODO the rest
}
}
func (inst *Instance) matchAttr(attr, value string) (ok bool, err error) {
switch attr {
case "architecture":
return value == "i386", nil
case "instance-id":
return inst.id == value, nil
case "group-id":
for _, g := range inst.reservation.groups {
if g.id == value {
return true, nil
}
}
return false, nil
case "group-name":
for _, g := range inst.reservation.groups {
if g.name == value {
return true, nil
}
}
return false, nil
case "image-id":
return value == inst.imageId, nil
case "instance-state-code":
code, err := strconv.Atoi(value)
if err != nil {
return false, err
}
return code&0xff == inst.state.Code, nil
case "instance-state-name":
return value == inst.state.Name, nil
}
return false, fmt.Errorf("unknown attribute %q", attr)
}
var (
Pending = ec2.InstanceState{0, "pending"}
Running = ec2.InstanceState{16, "running"}
ShuttingDown = ec2.InstanceState{32, "shutting-down"}
Terminated = ec2.InstanceState{16, "terminated"}
Stopped = ec2.InstanceState{16, "stopped"}
)
func (srv *Server) createSecurityGroup(w http.ResponseWriter, req *http.Request, reqId string) interface{} {
name := req.Form.Get("GroupName")
if name == "" {
fatalf(400, "InvalidParameterValue", "empty security group name")
}
srv.mu.Lock()
defer srv.mu.Unlock()
if srv.group(ec2.SecurityGroup{Name: name}) != nil {
fatalf(400, "InvalidGroup.Duplicate", "group %q already exists", name)
}
g := &securityGroup{
name: name,
description: req.Form.Get("GroupDescription"),
id: fmt.Sprintf("sg-%d", srv.groupId.next()),
perms: make(map[permKey]bool),
}
srv.groups[g.id] = g
// we define a local type for this because ec2.CreateSecurityGroupResp
// contains SecurityGroup, but the response to this request
// should not contain the security group name.
type CreateSecurityGroupResponse struct {
RequestId string `xml:"requestId"`
Return bool `xml:"return"`
GroupId string `xml:"groupId"`
}
r := &CreateSecurityGroupResponse{
RequestId: reqId,
Return: true,
GroupId: g.id,
}
return r
}
func (srv *Server) notImplemented(w http.ResponseWriter, req *http.Request, reqId string) interface{} {
fatalf(500, "InternalError", "not implemented")
panic("not reached")
}
func (srv *Server) describeInstances(w http.ResponseWriter, req *http.Request, reqId string) interface{} {
srv.mu.Lock()
defer srv.mu.Unlock()
insts := make(map[*Instance]bool)
for name, vals := range req.Form {
if !strings.HasPrefix(name, "InstanceId.") {
continue
}
inst := srv.instances[vals[0]]
if inst == nil {
fatalf(400, "InvalidInstanceID.NotFound", "instance %q not found", vals[0])
}
insts[inst] = true
}
f := newFilter(req.Form)
var resp ec2.InstancesResp
resp.RequestId = reqId
for _, r := range srv.reservations {
var instances []ec2.Instance
for _, inst := range r.instances {
if len(insts) > 0 && !insts[inst] {
continue
}
ok, err := f.ok(inst)
if ok {
instances = append(instances, inst.ec2instance())
} else if err != nil {
fatalf(400, "InvalidParameterValue", "describe instances: %v", err)
}
}
if len(instances) > 0 {
var groups []ec2.SecurityGroup
for _, g := range r.groups {
groups = append(groups, g.ec2SecurityGroup())
}
resp.Reservations = append(resp.Reservations, ec2.Reservation{
ReservationId: r.id,
OwnerId: ownerId,
Instances: instances,
SecurityGroups: groups,
})
}
}
return &resp
}
func (srv *Server) describeSecurityGroups(w http.ResponseWriter, req *http.Request, reqId string) interface{} {
// BUG similar bug to describeInstances, but for GroupName and GroupId
srv.mu.Lock()
defer srv.mu.Unlock()
var groups []*securityGroup
for name, vals := range req.Form {
var g ec2.SecurityGroup
switch {
case strings.HasPrefix(name, "GroupName."):
g.Name = vals[0]
case strings.HasPrefix(name, "GroupId."):
g.Id = vals[0]
default:
continue
}
sg := srv.group(g)
if sg == nil {
fatalf(400, "InvalidGroup.NotFound", "no such group %v", g)
}
groups = append(groups, sg)
}
if len(groups) == 0 {
for _, g := range srv.groups {
groups = append(groups, g)
}
}
f := newFilter(req.Form)
var resp ec2.SecurityGroupsResp
resp.RequestId = reqId
for _, group := range groups {
ok, err := f.ok(group)
if ok {
resp.Groups = append(resp.Groups, ec2.SecurityGroupInfo{
OwnerId: ownerId,
SecurityGroup: group.ec2SecurityGroup(),
Description: group.description,
IPPerms: group.ec2Perms(),
})
} else if err != nil {
fatalf(400, "InvalidParameterValue", "describe security groups: %v", err)
}
}
return &resp
}
func (srv *Server) authorizeSecurityGroupIngress(w http.ResponseWriter, req *http.Request, reqId string) interface{} {
srv.mu.Lock()
defer srv.mu.Unlock()
g := srv.group(ec2.SecurityGroup{
Name: req.Form.Get("GroupName"),
Id: req.Form.Get("GroupId"),
})
if g == nil {
fatalf(400, "InvalidGroup.NotFound", "group not found")
}
perms := srv.parsePerms(req)
for _, p := range perms {
if g.perms[p] {
fatalf(400, "InvalidPermission.Duplicate", "Permission has already been authorized on the specified group")
}
}
for _, p := range perms {
g.perms[p] = true
}
return &ec2.SimpleResp{
XMLName: xml.Name{"", "AuthorizeSecurityGroupIngressResponse"},
RequestId: reqId,
}
}
func (srv *Server) revokeSecurityGroupIngress(w http.ResponseWriter, req *http.Request, reqId string) interface{} {
srv.mu.Lock()
defer srv.mu.Unlock()
g := srv.group(ec2.SecurityGroup{
Name: req.Form.Get("GroupName"),
Id: req.Form.Get("GroupId"),
})
if g == nil {
fatalf(400, "InvalidGroup.NotFound", "group not found")
}
perms := srv.parsePerms(req)
// Note EC2 does not give an error if asked to revoke an authorization
// that does not exist.
for _, p := range perms {
delete(g.perms, p)
}
return &ec2.SimpleResp{
XMLName: xml.Name{"", "RevokeSecurityGroupIngressResponse"},
RequestId: reqId,
}
}
var secGroupPat = regexp.MustCompile(`^sg-[a-z0-9]+$`)
var ipPat = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$`)
var ownerIdPat = regexp.MustCompile(`^[0-9]+$`)
// parsePerms returns a slice of permKey values extracted
// from the permission fields in req.
func (srv *Server) parsePerms(req *http.Request) []permKey {
// perms maps an index found in the form to its associated
// IPPerm. For instance, the form value with key
// "IpPermissions.3.FromPort" will be stored in perms[3].FromPort
perms := make(map[int]ec2.IPPerm)
type subgroupKey struct {
id1, id2 int
}
// Each IPPerm can have many source security groups. The form key
// for a source security group contains two indices: the index
// of the IPPerm and the sub-index of the security group. The
// sourceGroups map maps from a subgroupKey containing these
// two indices to the associated security group. For instance,
// the form value with key "IPPermissions.3.Groups.2.GroupName"
// will be stored in sourceGroups[subgroupKey{3, 2}].Name.
sourceGroups := make(map[subgroupKey]ec2.UserSecurityGroup)
// For each value in the form we store its associated information in the
// above maps. The maps are necessary because the form keys may
// arrive in any order, and the indices are not
// necessarily sequential or even small.
for name, vals := range req.Form {
val := vals[0]
var id1 int
var rest string
if x, _ := fmt.Sscanf(name, "IpPermissions.%d.%s", &id1, &rest); x != 2 {
continue
}
ec2p := perms[id1]
switch {
case rest == "FromPort":
ec2p.FromPort = atoi(val)
case rest == "ToPort":
ec2p.ToPort = atoi(val)
case rest == "IpProtocol":
switch val {
case "tcp", "udp", "icmp":
ec2p.Protocol = val
default:
// check it's a well formed number
atoi(val)
ec2p.Protocol = val
}
case strings.HasPrefix(rest, "Groups."):
k := subgroupKey{id1: id1}
if x, _ := fmt.Sscanf(rest[len("Groups."):], "%d.%s", &k.id2, &rest); x != 2 {
continue
}
g := sourceGroups[k]
switch rest {
case "UserId":
// BUG if the user id is blank, this does not conform to the
// way that EC2 handles it - a specified but blank owner id
// can cause RevokeSecurityGroupIngress to fail with
// "group not found" even if the security group id has been
// correctly specified.
// By failing here, we ensure that we fail early in this case.
if !ownerIdPat.MatchString(val) {
fatalf(400, "InvalidUserID.Malformed", "Invalid user ID: %q", val)
}
g.OwnerId = val
case "GroupName":
g.Name = val
case "GroupId":
if !secGroupPat.MatchString(val) {
fatalf(400, "InvalidGroupId.Malformed", "Invalid group ID: %q", val)
}
g.Id = val
default:
fatalf(400, "UnknownParameter", "unknown parameter %q", name)
}
sourceGroups[k] = g
case strings.HasPrefix(rest, "IpRanges."):
var id2 int
if x, _ := fmt.Sscanf(rest[len("IpRanges."):], "%d.%s", &id2, &rest); x != 2 {
continue
}
switch rest {
case "CidrIp":
if !ipPat.MatchString(val) {
fatalf(400, "InvalidPermission.Malformed", "Invalid IP range: %q", val)
}
ec2p.SourceIPs = append(ec2p.SourceIPs, val)
default:
fatalf(400, "UnknownParameter", "unknown parameter %q", name)
}
default:
fatalf(400, "UnknownParameter", "unknown parameter %q", name)
}
perms[id1] = ec2p
}
// Associate each set of source groups with its IPPerm.
for k, g := range sourceGroups {
p := perms[k.id1]
p.SourceGroups = append(p.SourceGroups, g)
perms[k.id1] = p
}
// Now that we have built up the IPPerms we need, we check for
// parameter errors and build up a permKey for each permission,
// looking up security groups from srv as we do so.
var result []permKey
for _, p := range perms {
if p.FromPort > p.ToPort {
fatalf(400, "InvalidParameterValue", "invalid port range")
}
k := permKey{
protocol: p.Protocol,
fromPort: p.FromPort,
toPort: p.ToPort,
}
for _, g := range p.SourceGroups {
if g.OwnerId != "" && g.OwnerId != ownerId {
fatalf(400, "InvalidGroup.NotFound", "group %q not found", g.Name)
}
var ec2g ec2.SecurityGroup
switch {
case g.Id != "":
ec2g.Id = g.Id
case g.Name != "":
ec2g.Name = g.Name
}
k.group = srv.group(ec2g)
if k.group == nil {
fatalf(400, "InvalidGroup.NotFound", "group %v not found", g)
}
result = append(result, k)
}
k.group = nil
for _, ip := range p.SourceIPs {
k.ipAddr = ip
result = append(result, k)
}
}
return result
}
func (srv *Server) deleteSecurityGroup(w http.ResponseWriter, req *http.Request, reqId string) interface{} {
srv.mu.Lock()
defer srv.mu.Unlock()
g := srv.group(ec2.SecurityGroup{
Name: req.Form.Get("GroupName"),
Id: req.Form.Get("GroupId"),
})
if g == nil {
fatalf(400, "InvalidGroup.NotFound", "group not found")
}
for _, r := range srv.reservations {
for _, h := range r.groups {
if h == g && r.hasRunningMachine() {
fatalf(500, "InvalidGroup.InUse", "group is currently in use by a running instance")
}
}
}
for _, sg := range srv.groups {
// If a group refers to itself, it's ok to delete it.
if sg == g {
continue
}
for k := range sg.perms {
if k.group == g {
fatalf(500, "InvalidGroup.InUse", "group is currently in use by group %q", sg.id)
}
}
}
delete(srv.groups, g.id)
return &ec2.SimpleResp{
XMLName: xml.Name{"", "DeleteSecurityGroupResponse"},
RequestId: reqId,
}
}
func (r *reservation) hasRunningMachine() bool {
for _, inst := range r.instances {
if inst.state.Code != ShuttingDown.Code && inst.state.Code != Terminated.Code {
return true
}
}
return false
}
type counter int
func (c *counter) next() (i int) {
i = int(*c)
(*c)++
return
}
// atoi is like strconv.Atoi but is fatal if the
// string is not well formed.
func atoi(s string) int {
i, err := strconv.Atoi(s)
if err != nil {
fatalf(400, "InvalidParameterValue", "bad number: %v", err)
}
return i
}
func fatalf(statusCode int, code string, f string, a ...interface{}) {
panic(&ec2.Error{
StatusCode: statusCode,
Code: code,
Message: fmt.Sprintf(f, a...),
})
}

View File

@@ -0,0 +1,22 @@
package ec2
import (
"github.com/mitchellh/goamz/aws"
"time"
)
func Sign(auth aws.Auth, method, path string, params map[string]string, host string) {
sign(auth, method, path, params, host)
}
func fixedTime() time.Time {
return time.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC)
}
func FakeTime(fakeIt bool) {
if fakeIt {
timeNow = fixedTime
} else {
timeNow = time.Now
}
}

View File

@@ -0,0 +1,854 @@
package ec2_test
var ErrorDump = `
<?xml version="1.0" encoding="UTF-8"?>
<Response><Errors><Error><Code>UnsupportedOperation</Code>
<Message>AMIs with an instance-store root device are not supported for the instance type 't1.micro'.</Message>
</Error></Errors><RequestID>0503f4e9-bbd6-483c-b54f-c4ae9f3b30f4</RequestID></Response>
`
// http://goo.gl/Mcm3b
var RunInstancesExample = `
<RunInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2011-12-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<reservationId>r-47a5402e</reservationId>
<ownerId>999988887777</ownerId>
<groupSet>
<item>
<groupId>sg-67ad940e</groupId>
<groupName>default</groupName>
</item>
</groupSet>
<instancesSet>
<item>
<instanceId>i-2ba64342</instanceId>
<imageId>ami-60a54009</imageId>
<instanceState>
<code>0</code>
<name>pending</name>
</instanceState>
<privateDnsName></privateDnsName>
<dnsName></dnsName>
<keyName>example-key-name</keyName>
<amiLaunchIndex>0</amiLaunchIndex>
<instanceType>m1.small</instanceType>
<launchTime>2007-08-07T11:51:50.000Z</launchTime>
<placement>
<availabilityZone>us-east-1b</availabilityZone>
</placement>
<monitoring>
<state>enabled</state>
</monitoring>
<virtualizationType>paravirtual</virtualizationType>
<clientToken/>
<tagSet/>
<hypervisor>xen</hypervisor>
</item>
<item>
<instanceId>i-2bc64242</instanceId>
<imageId>ami-60a54009</imageId>
<instanceState>
<code>0</code>
<name>pending</name>
</instanceState>
<privateDnsName></privateDnsName>
<dnsName></dnsName>
<keyName>example-key-name</keyName>
<amiLaunchIndex>1</amiLaunchIndex>
<instanceType>m1.small</instanceType>
<launchTime>2007-08-07T11:51:50.000Z</launchTime>
<placement>
<availabilityZone>us-east-1b</availabilityZone>
</placement>
<monitoring>
<state>enabled</state>
</monitoring>
<virtualizationType>paravirtual</virtualizationType>
<clientToken/>
<tagSet/>
<hypervisor>xen</hypervisor>
</item>
<item>
<instanceId>i-2be64332</instanceId>
<imageId>ami-60a54009</imageId>
<instanceState>
<code>0</code>
<name>pending</name>
</instanceState>
<privateDnsName></privateDnsName>
<dnsName></dnsName>
<keyName>example-key-name</keyName>
<amiLaunchIndex>2</amiLaunchIndex>
<instanceType>m1.small</instanceType>
<launchTime>2007-08-07T11:51:50.000Z</launchTime>
<placement>
<availabilityZone>us-east-1b</availabilityZone>
</placement>
<monitoring>
<state>enabled</state>
</monitoring>
<virtualizationType>paravirtual</virtualizationType>
<clientToken/>
<tagSet/>
<hypervisor>xen</hypervisor>
</item>
</instancesSet>
</RunInstancesResponse>
`
// http://goo.gl/GRZgCD
var RequestSpotInstancesExample = `
<RequestSpotInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2014-02-01/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<spotInstanceRequestSet>
<item>
<spotInstanceRequestId>sir-1a2b3c4d</spotInstanceRequestId>
<spotPrice>0.5</spotPrice>
<type>one-time</type>
<state>open</state>
<status>
<code>pending-evaluation</code>
<updateTime>2008-05-07T12:51:50.000Z</updateTime>
<message>Your Spot request has been submitted for review, and is pending evaluation.</message>
</status>
<availabilityZoneGroup>MyAzGroup</availabilityZoneGroup>
<launchSpecification>
<imageId>ami-1a2b3c4d</imageId>
<keyName>gsg-keypair</keyName>
<groupSet>
<item>
<groupId>sg-1a2b3c4d</groupId>
<groupName>websrv</groupName>
</item>
</groupSet>
<instanceType>m1.small</instanceType>
<blockDeviceMapping/>
<monitoring>
<enabled>false</enabled>
</monitoring>
<ebsOptimized>false</ebsOptimized>
</launchSpecification>
<createTime>YYYY-MM-DDTHH:MM:SS.000Z</createTime>
<productDescription>Linux/UNIX</productDescription>
</item>
</spotInstanceRequestSet>
</RequestSpotInstancesResponse>
`
// http://goo.gl/KsKJJk
var DescribeSpotRequestsExample = `
<DescribeSpotInstanceRequestsResponse xmlns="http://ec2.amazonaws.com/doc/2014-02-01/">
<requestId>b1719f2a-5334-4479-b2f1-26926EXAMPLE</requestId>
<spotInstanceRequestSet>
<item>
<spotInstanceRequestId>sir-1a2b3c4d</spotInstanceRequestId>
<spotPrice>0.5</spotPrice>
<type>one-time</type>
<state>active</state>
<status>
<code>fulfilled</code>
<updateTime>2008-05-07T12:51:50.000Z</updateTime>
<message>Your Spot request is fulfilled.</message>
</status>
<launchSpecification>
<imageId>ami-1a2b3c4d</imageId>
<keyName>gsg-keypair</keyName>
<groupSet>
<item>
<groupId>sg-1a2b3c4d</groupId>
<groupName>websrv</groupName>
</item>
</groupSet>
<instanceType>m1.small</instanceType>
<monitoring>
<enabled>false</enabled>
</monitoring>
<ebsOptimized>false</ebsOptimized>
</launchSpecification>
<instanceId>i-1a2b3c4d</instanceId>
<createTime>YYYY-MM-DDTHH:MM:SS.000Z</createTime>
<productDescription>Linux/UNIX</productDescription>
<launchedAvailabilityZone>us-east-1a</launchedAvailabilityZone>
</item>
</spotInstanceRequestSet>
</DescribeSpotInstanceRequestsResponse>
`
// http://goo.gl/DcfFgJ
var CancelSpotRequestsExample = `
<CancelSpotInstanceRequestsResponse xmlns="http://ec2.amazonaws.com/doc/2014-02-01/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<spotInstanceRequestSet>
<item>
<spotInstanceRequestId>sir-1a2b3c4d</spotInstanceRequestId>
<state>cancelled</state>
</item>
</spotInstanceRequestSet>
</CancelSpotInstanceRequestsResponse>
`
// http://goo.gl/3BKHj
var TerminateInstancesExample = `
<TerminateInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2011-12-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<instancesSet>
<item>
<instanceId>i-3ea74257</instanceId>
<currentState>
<code>32</code>
<name>shutting-down</name>
</currentState>
<previousState>
<code>16</code>
<name>running</name>
</previousState>
</item>
</instancesSet>
</TerminateInstancesResponse>
`
// http://goo.gl/mLbmw
var DescribeInstancesExample1 = `
<DescribeInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2011-12-15/">
<requestId>98e3c9a4-848c-4d6d-8e8a-b1bdEXAMPLE</requestId>
<reservationSet>
<item>
<reservationId>r-b27e30d9</reservationId>
<ownerId>999988887777</ownerId>
<groupSet>
<item>
<groupId>sg-67ad940e</groupId>
<groupName>default</groupName>
</item>
</groupSet>
<instancesSet>
<item>
<instanceId>i-c5cd56af</instanceId>
<imageId>ami-1a2b3c4d</imageId>
<instanceState>
<code>16</code>
<name>running</name>
</instanceState>
<privateDnsName>domU-12-31-39-10-56-34.compute-1.internal</privateDnsName>
<dnsName>ec2-174-129-165-232.compute-1.amazonaws.com</dnsName>
<reason/>
<keyName>GSG_Keypair</keyName>
<amiLaunchIndex>0</amiLaunchIndex>
<productCodes/>
<instanceType>m1.small</instanceType>
<launchTime>2010-08-17T01:15:18.000Z</launchTime>
<placement>
<availabilityZone>us-east-1b</availabilityZone>
<groupName/>
</placement>
<kernelId>aki-94c527fd</kernelId>
<ramdiskId>ari-96c527ff</ramdiskId>
<monitoring>
<state>disabled</state>
</monitoring>
<privateIpAddress>10.198.85.190</privateIpAddress>
<ipAddress>174.129.165.232</ipAddress>
<architecture>i386</architecture>
<rootDeviceType>ebs</rootDeviceType>
<rootDeviceName>/dev/sda1</rootDeviceName>
<blockDeviceMapping>
<item>
<deviceName>/dev/sda1</deviceName>
<ebs>
<volumeId>vol-a082c1c9</volumeId>
<status>attached</status>
<attachTime>2010-08-17T01:15:21.000Z</attachTime>
<deleteOnTermination>false</deleteOnTermination>
</ebs>
</item>
</blockDeviceMapping>
<instanceLifecycle>spot</instanceLifecycle>
<spotInstanceRequestId>sir-7a688402</spotInstanceRequestId>
<virtualizationType>paravirtual</virtualizationType>
<clientToken/>
<tagSet/>
<hypervisor>xen</hypervisor>
</item>
</instancesSet>
<requesterId>854251627541</requesterId>
</item>
<item>
<reservationId>r-b67e30dd</reservationId>
<ownerId>999988887777</ownerId>
<groupSet>
<item>
<groupId>sg-67ad940e</groupId>
<groupName>default</groupName>
</item>
</groupSet>
<instancesSet>
<item>
<instanceId>i-d9cd56b3</instanceId>
<imageId>ami-1a2b3c4d</imageId>
<instanceState>
<code>16</code>
<name>running</name>
</instanceState>
<privateDnsName>domU-12-31-39-10-54-E5.compute-1.internal</privateDnsName>
<dnsName>ec2-184-73-58-78.compute-1.amazonaws.com</dnsName>
<reason/>
<keyName>GSG_Keypair</keyName>
<amiLaunchIndex>0</amiLaunchIndex>
<productCodes/>
<instanceType>m1.large</instanceType>
<launchTime>2010-08-17T01:15:19.000Z</launchTime>
<placement>
<availabilityZone>us-east-1b</availabilityZone>
<groupName/>
</placement>
<kernelId>aki-94c527fd</kernelId>
<ramdiskId>ari-96c527ff</ramdiskId>
<monitoring>
<state>disabled</state>
</monitoring>
<privateIpAddress>10.198.87.19</privateIpAddress>
<ipAddress>184.73.58.78</ipAddress>
<architecture>i386</architecture>
<rootDeviceType>ebs</rootDeviceType>
<rootDeviceName>/dev/sda1</rootDeviceName>
<blockDeviceMapping>
<item>
<deviceName>/dev/sda1</deviceName>
<ebs>
<volumeId>vol-a282c1cb</volumeId>
<status>attached</status>
<attachTime>2010-08-17T01:15:23.000Z</attachTime>
<deleteOnTermination>false</deleteOnTermination>
</ebs>
</item>
</blockDeviceMapping>
<instanceLifecycle>spot</instanceLifecycle>
<spotInstanceRequestId>sir-55a3aa02</spotInstanceRequestId>
<virtualizationType>paravirtual</virtualizationType>
<clientToken/>
<tagSet/>
<hypervisor>xen</hypervisor>
</item>
</instancesSet>
<requesterId>854251627541</requesterId>
</item>
</reservationSet>
</DescribeInstancesResponse>
`
// http://goo.gl/mLbmw
var DescribeInstancesExample2 = `
<DescribeInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2011-12-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<reservationSet>
<item>
<reservationId>r-bc7e30d7</reservationId>
<ownerId>999988887777</ownerId>
<groupSet>
<item>
<groupId>sg-67ad940e</groupId>
<groupName>default</groupName>
</item>
</groupSet>
<instancesSet>
<item>
<instanceId>i-c7cd56ad</instanceId>
<imageId>ami-b232d0db</imageId>
<instanceState>
<code>16</code>
<name>running</name>
</instanceState>
<privateDnsName>domU-12-31-39-01-76-06.compute-1.internal</privateDnsName>
<dnsName>ec2-72-44-52-124.compute-1.amazonaws.com</dnsName>
<keyName>GSG_Keypair</keyName>
<amiLaunchIndex>0</amiLaunchIndex>
<productCodes/>
<instanceType>m1.small</instanceType>
<launchTime>2010-08-17T01:15:16.000Z</launchTime>
<placement>
<availabilityZone>us-east-1b</availabilityZone>
</placement>
<kernelId>aki-94c527fd</kernelId>
<ramdiskId>ari-96c527ff</ramdiskId>
<monitoring>
<state>disabled</state>
</monitoring>
<privateIpAddress>10.255.121.240</privateIpAddress>
<ipAddress>72.44.52.124</ipAddress>
<architecture>i386</architecture>
<rootDeviceType>ebs</rootDeviceType>
<rootDeviceName>/dev/sda1</rootDeviceName>
<blockDeviceMapping>
<item>
<deviceName>/dev/sda1</deviceName>
<ebs>
<volumeId>vol-a482c1cd</volumeId>
<status>attached</status>
<attachTime>2010-08-17T01:15:26.000Z</attachTime>
<deleteOnTermination>true</deleteOnTermination>
</ebs>
</item>
</blockDeviceMapping>
<virtualizationType>paravirtual</virtualizationType>
<clientToken/>
<tagSet>
<item>
<key>webserver</key>
<value></value>
</item>
<item>
<key>stack</key>
<value>Production</value>
</item>
</tagSet>
<hypervisor>xen</hypervisor>
</item>
</instancesSet>
</item>
</reservationSet>
</DescribeInstancesResponse>
`
// http://goo.gl/cxU41
var CreateImageExample = `
<CreateImageResponse xmlns="http://ec2.amazonaws.com/doc/2013-02-01/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<imageId>ami-4fa54026</imageId>
</CreateImageResponse>
`
// http://goo.gl/V0U25
var DescribeImagesExample = `
<DescribeImagesResponse xmlns="http://ec2.amazonaws.com/doc/2012-08-15/">
<requestId>4a4a27a2-2e7c-475d-b35b-ca822EXAMPLE</requestId>
<imagesSet>
<item>
<imageId>ami-a2469acf</imageId>
<imageLocation>aws-marketplace/example-marketplace-amzn-ami.1</imageLocation>
<imageState>available</imageState>
<imageOwnerId>123456789999</imageOwnerId>
<isPublic>true</isPublic>
<productCodes>
<item>
<productCode>a1b2c3d4e5f6g7h8i9j10k11</productCode>
<type>marketplace</type>
</item>
</productCodes>
<architecture>i386</architecture>
<imageType>machine</imageType>
<kernelId>aki-805ea7e9</kernelId>
<imageOwnerAlias>aws-marketplace</imageOwnerAlias>
<name>example-marketplace-amzn-ami.1</name>
<description>Amazon Linux AMI i386 EBS</description>
<rootDeviceType>ebs</rootDeviceType>
<rootDeviceName>/dev/sda1</rootDeviceName>
<blockDeviceMapping>
<item>
<deviceName>/dev/sda1</deviceName>
<ebs>
<snapshotId>snap-787e9403</snapshotId>
<volumeSize>8</volumeSize>
<deleteOnTermination>true</deleteOnTermination>
</ebs>
</item>
</blockDeviceMapping>
<virtualizationType>paravirtual</virtualizationType>
<hypervisor>xen</hypervisor>
</item>
</imagesSet>
</DescribeImagesResponse>
`
// http://goo.gl/bHO3z
var ImageAttributeExample = `
<DescribeImageAttributeResponse xmlns="http://ec2.amazonaws.com/doc/2013-07-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<imageId>ami-61a54008</imageId>
<launchPermission>
<item>
<group>all</group>
</item>
<item>
<userId>495219933132</userId>
</item>
</launchPermission>
</DescribeImageAttributeResponse>
`
// http://goo.gl/ttcda
var CreateSnapshotExample = `
<CreateSnapshotResponse xmlns="http://ec2.amazonaws.com/doc/2012-10-01/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<snapshotId>snap-78a54011</snapshotId>
<volumeId>vol-4d826724</volumeId>
<status>pending</status>
<startTime>2008-05-07T12:51:50.000Z</startTime>
<progress>60%</progress>
<ownerId>111122223333</ownerId>
<volumeSize>10</volumeSize>
<description>Daily Backup</description>
</CreateSnapshotResponse>
`
// http://goo.gl/vwU1y
var DeleteSnapshotExample = `
<DeleteSnapshotResponse xmlns="http://ec2.amazonaws.com/doc/2012-10-01/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<return>true</return>
</DeleteSnapshotResponse>
`
// http://goo.gl/nkovs
var DescribeSnapshotsExample = `
<DescribeSnapshotsResponse xmlns="http://ec2.amazonaws.com/doc/2012-10-01/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<snapshotSet>
<item>
<snapshotId>snap-1a2b3c4d</snapshotId>
<volumeId>vol-8875daef</volumeId>
<status>pending</status>
<startTime>2010-07-29T04:12:01.000Z</startTime>
<progress>30%</progress>
<ownerId>111122223333</ownerId>
<volumeSize>15</volumeSize>
<description>Daily Backup</description>
<tagSet>
<item>
<key>Purpose</key>
<value>demo_db_14_backup</value>
</item>
</tagSet>
</item>
</snapshotSet>
</DescribeSnapshotsResponse>
`
// http://goo.gl/YUjO4G
var ModifyImageAttributeExample = `
<ModifyImageAttributeResponse xmlns="http://ec2.amazonaws.com/doc/2013-06-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<return>true</return>
</ModifyImageAttributeResponse>
`
// http://goo.gl/hQwPCK
var CopyImageExample = `
<CopyImageResponse xmlns="http://ec2.amazonaws.com/doc/2013-06-15/">
<requestId>60bc441d-fa2c-494d-b155-5d6a3EXAMPLE</requestId>
<imageId>ami-4d3c2b1a</imageId>
</CopyImageResponse>
`
var CreateKeyPairExample = `
<CreateKeyPairResponse xmlns="http://ec2.amazonaws.com/doc/2013-02-01/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<keyName>foo</keyName>
<keyFingerprint>
00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
</keyFingerprint>
<keyMaterial>---- BEGIN RSA PRIVATE KEY ----
MIICiTCCAfICCQD6m7oRw0uXOjANBgkqhkiG9w0BAQUFADCBiDELMAkGA1UEBhMC
VVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZBbWF6
b24xFDASBgNVBAsTC0lBTSBDb25zb2xlMRIwEAYDVQQDEwlUZXN0Q2lsYWMxHzAd
BgkqhkiG9w0BCQEWEG5vb25lQGFtYXpvbi5jb20wHhcNMTEwNDI1MjA0NTIxWhcN
MTIwNDI0MjA0NTIxWjCBiDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYD
VQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZBbWF6b24xFDASBgNVBAsTC0lBTSBDb25z
b2xlMRIwEAYDVQQDEwlUZXN0Q2lsYWMxHzAdBgkqhkiG9w0BCQEWEG5vb25lQGFt
YXpvbi5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMaK0dn+a4GmWIWJ
21uUSfwfEvySWtC2XADZ4nB+BLYgVIk60CpiwsZ3G93vUEIO3IyNoH/f0wYK8m9T
rDHudUZg3qX4waLG5M43q7Wgc/MbQITxOUSQv7c7ugFFDzQGBzZswY6786m86gpE
Ibb3OhjZnzcvQAaRHhdlQWIMm2nrAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAtCu4
nUhVVxYUntneD9+h8Mg9q6q+auNKyExzyLwaxlAoo7TJHidbtS4J5iNmZgXL0Fkb
FFBjvSfpJIlJ00zbhNYS5f6GuoEDmFJl0ZxBHjJnyp378OD8uTs7fLvjx79LjSTb
NYiytVbZPQUQ5Yaxu2jXnimvw3rrszlaEXAMPLE=
-----END RSA PRIVATE KEY-----
</keyMaterial>
</CreateKeyPairResponse>
`
var DeleteKeyPairExample = `
<DeleteKeyPairResponse xmlns="http://ec2.amazonaws.com/doc/2013-02-01/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<return>true</return>
</DeleteKeyPairResponse>
`
// http://goo.gl/Eo7Yl
var CreateSecurityGroupExample = `
<CreateSecurityGroupResponse xmlns="http://ec2.amazonaws.com/doc/2011-12-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<return>true</return>
<groupId>sg-67ad940e</groupId>
</CreateSecurityGroupResponse>
`
// http://goo.gl/k12Uy
var DescribeSecurityGroupsExample = `
<DescribeSecurityGroupsResponse xmlns="http://ec2.amazonaws.com/doc/2011-12-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<securityGroupInfo>
<item>
<ownerId>999988887777</ownerId>
<groupName>WebServers</groupName>
<groupId>sg-67ad940e</groupId>
<groupDescription>Web Servers</groupDescription>
<ipPermissions>
<item>
<ipProtocol>tcp</ipProtocol>
<fromPort>80</fromPort>
<toPort>80</toPort>
<groups/>
<ipRanges>
<item>
<cidrIp>0.0.0.0/0</cidrIp>
</item>
</ipRanges>
</item>
</ipPermissions>
</item>
<item>
<ownerId>999988887777</ownerId>
<groupName>RangedPortsBySource</groupName>
<groupId>sg-76abc467</groupId>
<groupDescription>Group A</groupDescription>
<ipPermissions>
<item>
<ipProtocol>tcp</ipProtocol>
<fromPort>6000</fromPort>
<toPort>7000</toPort>
<groups/>
<ipRanges/>
</item>
</ipPermissions>
</item>
</securityGroupInfo>
</DescribeSecurityGroupsResponse>
`
// A dump which includes groups within ip permissions.
var DescribeSecurityGroupsDump = `
<?xml version="1.0" encoding="UTF-8"?>
<DescribeSecurityGroupsResponse xmlns="http://ec2.amazonaws.com/doc/2011-12-15/">
<requestId>87b92b57-cc6e-48b2-943f-f6f0e5c9f46c</requestId>
<securityGroupInfo>
<item>
<ownerId>12345</ownerId>
<groupName>default</groupName>
<groupDescription>default group</groupDescription>
<ipPermissions>
<item>
<ipProtocol>icmp</ipProtocol>
<fromPort>-1</fromPort>
<toPort>-1</toPort>
<groups>
<item>
<userId>12345</userId>
<groupName>default</groupName>
<groupId>sg-67ad940e</groupId>
</item>
</groups>
<ipRanges/>
</item>
<item>
<ipProtocol>tcp</ipProtocol>
<fromPort>0</fromPort>
<toPort>65535</toPort>
<groups>
<item>
<userId>12345</userId>
<groupName>other</groupName>
<groupId>sg-76abc467</groupId>
</item>
</groups>
<ipRanges/>
</item>
</ipPermissions>
</item>
</securityGroupInfo>
</DescribeSecurityGroupsResponse>
`
// http://goo.gl/QJJDO
var DeleteSecurityGroupExample = `
<DeleteSecurityGroupResponse xmlns="http://ec2.amazonaws.com/doc/2011-12-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<return>true</return>
</DeleteSecurityGroupResponse>
`
// http://goo.gl/u2sDJ
var AuthorizeSecurityGroupIngressExample = `
<AuthorizeSecurityGroupIngressResponse xmlns="http://ec2.amazonaws.com/doc/2011-12-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<return>true</return>
</AuthorizeSecurityGroupIngressResponse>
`
// http://goo.gl/u2sDJ
var AuthorizeSecurityGroupEgressExample = `
<AuthorizeSecurityGroupEgressResponse xmlns="http://ec2.amazonaws.com/doc/2014-06-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<return>true</return>
</AuthorizeSecurityGroupEgressResponse>
`
// http://goo.gl/Mz7xr
var RevokeSecurityGroupIngressExample = `
<RevokeSecurityGroupIngressResponse xmlns="http://ec2.amazonaws.com/doc/2011-12-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<return>true</return>
</RevokeSecurityGroupIngressResponse>
`
// http://goo.gl/Vmkqc
var CreateTagsExample = `
<CreateTagsResponse xmlns="http://ec2.amazonaws.com/doc/2011-12-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<return>true</return>
</CreateTagsResponse>
`
// http://goo.gl/awKeF
var StartInstancesExample = `
<StartInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2011-12-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<instancesSet>
<item>
<instanceId>i-10a64379</instanceId>
<currentState>
<code>0</code>
<name>pending</name>
</currentState>
<previousState>
<code>80</code>
<name>stopped</name>
</previousState>
</item>
</instancesSet>
</StartInstancesResponse>
`
// http://goo.gl/436dJ
var StopInstancesExample = `
<StopInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2011-12-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<instancesSet>
<item>
<instanceId>i-10a64379</instanceId>
<currentState>
<code>64</code>
<name>stopping</name>
</currentState>
<previousState>
<code>16</code>
<name>running</name>
</previousState>
</item>
</instancesSet>
</StopInstancesResponse>
`
// http://goo.gl/baoUf
var RebootInstancesExample = `
<RebootInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2011-12-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<return>true</return>
</RebootInstancesResponse>
`
// http://goo.gl/9rprDN
var AllocateAddressExample = `
<AllocateAddressResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<publicIp>198.51.100.1</publicIp>
<domain>vpc</domain>
<allocationId>eipalloc-5723d13e</allocationId>
</AllocateAddressResponse>
`
// http://goo.gl/3Q0oCc
var ReleaseAddressExample = `
<ReleaseAddressResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<return>true</return>
</ReleaseAddressResponse>
`
// http://goo.gl/uOSQE
var AssociateAddressExample = `
<AssociateAddressResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<return>true</return>
<associationId>eipassoc-fc5ca095</associationId>
</AssociateAddressResponse>
`
// http://goo.gl/LrOa0
var DisassociateAddressExample = `
<DisassociateAddressResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<return>true</return>
</DisassociateAddressResponse>
`
// http://goo.gl/icuXh5
var ModifyInstanceExample = `
<ModifyImageAttributeResponse xmlns="http://ec2.amazonaws.com/doc/2013-06-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<return>true</return>
</ModifyImageAttributeResponse>
`
var CreateVpcExample = `
<CreateVpcResponse xmlns="http://ec2.amazonaws.com/doc/2014-06-15/">
<requestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</requestId>
<vpc>
<vpcId>vpc-1a2b3c4d</vpcId>
<state>pending</state>
<cidrBlock>10.0.0.0/16</cidrBlock>
<dhcpOptionsId>dopt-1a2b3c4d2</dhcpOptionsId>
<instanceTenancy>default</instanceTenancy>
<tagSet/>
</vpc>
</CreateVpcResponse>
`
var DescribeVpcsExample = `
<DescribeVpcsResponse xmlns="http://ec2.amazonaws.com/doc/2014-06-15/">
<requestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</requestId>
<vpcSet>
<item>
<vpcId>vpc-1a2b3c4d</vpcId>
<state>available</state>
<cidrBlock>10.0.0.0/23</cidrBlock>
<dhcpOptionsId>dopt-7a8b9c2d</dhcpOptionsId>
<instanceTenancy>default</instanceTenancy>
<isDefault>false</isDefault>
<tagSet/>
</item>
</vpcSet>
</DescribeVpcsResponse>
`
var CreateSubnetExample = `
<CreateSubnetResponse xmlns="http://ec2.amazonaws.com/doc/2014-06-15/">
<requestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</requestId>
<subnet>
<subnetId>subnet-9d4a7b6c</subnetId>
<state>pending</state>
<vpcId>vpc-1a2b3c4d</vpcId>
<cidrBlock>10.0.1.0/24</cidrBlock>
<availableIpAddressCount>251</availableIpAddressCount>
<availabilityZone>us-east-1a</availabilityZone>
<tagSet/>
</subnet>
</CreateSubnetResponse>
`
// http://goo.gl/r6ZCPm
var ResetImageAttributeExample = `
<ResetImageAttributeResponse xmlns="http://ec2.amazonaws.com/doc/2014-06-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<return>true</return>
</ResetImageAttributeResponse>
`

View File

@@ -0,0 +1,45 @@
package ec2
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"github.com/mitchellh/goamz/aws"
"sort"
"strings"
)
// ----------------------------------------------------------------------------
// EC2 signing (http://goo.gl/fQmAN)
var b64 = base64.StdEncoding
func sign(auth aws.Auth, method, path string, params map[string]string, host string) {
params["AWSAccessKeyId"] = auth.AccessKey
params["SignatureVersion"] = "2"
params["SignatureMethod"] = "HmacSHA256"
if auth.Token != "" {
params["SecurityToken"] = auth.Token
}
// AWS specifies that the parameters in a signed request must
// be provided in the natural order of the keys. This is distinct
// from the natural order of the encoded value of key=value.
// Percent and equals affect the sorting order.
var keys, sarray []string
for k, _ := range params {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
sarray = append(sarray, aws.Encode(k)+"="+aws.Encode(params[k]))
}
joined := strings.Join(sarray, "&")
payload := method + "\n" + host + "\n" + path + "\n" + joined
hash := hmac.New(sha256.New, []byte(auth.SecretKey))
hash.Write([]byte(payload))
signature := make([]byte, b64.EncodedLen(hash.Size()))
b64.Encode(signature, hash.Sum(nil))
params["Signature"] = string(signature)
}

View File

@@ -0,0 +1,68 @@
package ec2_test
import (
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/goamz/ec2"
. "github.com/motain/gocheck"
)
// EC2 ReST authentication docs: http://goo.gl/fQmAN
var testAuth = aws.Auth{"user", "secret", ""}
func (s *S) TestBasicSignature(c *C) {
params := map[string]string{}
ec2.Sign(testAuth, "GET", "/path", params, "localhost")
c.Assert(params["SignatureVersion"], Equals, "2")
c.Assert(params["SignatureMethod"], Equals, "HmacSHA256")
expected := "6lSe5QyXum0jMVc7cOUz32/52ZnL7N5RyKRk/09yiK4="
c.Assert(params["Signature"], Equals, expected)
}
func (s *S) TestParamSignature(c *C) {
params := map[string]string{
"param1": "value1",
"param2": "value2",
"param3": "value3",
}
ec2.Sign(testAuth, "GET", "/path", params, "localhost")
expected := "XWOR4+0lmK8bD8CGDGZ4kfuSPbb2JibLJiCl/OPu1oU="
c.Assert(params["Signature"], Equals, expected)
}
func (s *S) TestManyParams(c *C) {
params := map[string]string{
"param1": "value10",
"param2": "value2",
"param3": "value3",
"param4": "value4",
"param5": "value5",
"param6": "value6",
"param7": "value7",
"param8": "value8",
"param9": "value9",
"param10": "value1",
}
ec2.Sign(testAuth, "GET", "/path", params, "localhost")
expected := "di0sjxIvezUgQ1SIL6i+C/H8lL+U0CQ9frLIak8jkVg="
c.Assert(params["Signature"], Equals, expected)
}
func (s *S) TestEscaping(c *C) {
params := map[string]string{"Nonce": "+ +"}
ec2.Sign(testAuth, "GET", "/path", params, "localhost")
c.Assert(params["Nonce"], Equals, "+ +")
expected := "bqffDELReIqwjg/W0DnsnVUmfLK4wXVLO4/LuG+1VFA="
c.Assert(params["Signature"], Equals, expected)
}
func (s *S) TestSignatureExample1(c *C) {
params := map[string]string{
"Timestamp": "2009-02-01T12:53:20+00:00",
"Version": "2007-11-07",
"Action": "ListDomains",
}
ec2.Sign(aws.Auth{"access", "secret", ""}, "GET", "/", params, "sdb.amazonaws.com")
expected := "okj96/5ucWBSc1uR2zXVfm6mDHtgfNv657rRtt/aunQ="
c.Assert(params["Signature"], Equals, expected)
}