Merge pull request #1229 from ragnard/aws-provider

Initial impl. of cloud provider for AWS
This commit is contained in:
Daniel Smith
2014-09-12 15:16:47 -07:00
26 changed files with 8390 additions and 0 deletions

16
Godeps/Godeps.json generated
View File

@@ -5,6 +5,10 @@
"./..."
],
"Deps": [
{
"ImportPath": "code.google.com/p/gcfg",
"Rev": "c2d3050044d05357eaf6c3547249ba57c5e235cb"
},
{
"ImportPath": "code.google.com/p/go-uuid/uuid",
"Comment": "null-12",
@@ -68,6 +72,14 @@
"ImportPath": "github.com/google/gofuzz",
"Rev": "aef70dacbc78771e35beb261bb3a72986adf7906"
},
{
"ImportPath": "github.com/mitchellh/goamz/aws",
"Rev": "9cad7da945e699385c1a3e115aa255211921c9bb"
},
{
"ImportPath": "github.com/mitchellh/goamz/ec2",
"Rev": "9cad7da945e699385c1a3e115aa255211921c9bb"
},
{
"ImportPath": "github.com/stretchr/objx",
"Rev": "d40df0cc104c06eae2dfe03d7dddb83802d52f9a"
@@ -80,6 +92,10 @@
"ImportPath": "github.com/stretchr/testify/mock",
"Rev": "37614ac27794505bf7867ca93aac883cadb6a5f7"
},
{
"ImportPath": "github.com/vaughan0/go-ini",
"Rev": "a98ad7ee00ec53921f08832bc06ecf7fd600e6a1"
},
{
"ImportPath": "gopkg.in/v1/yaml",
"Rev": "1b9791953ba4027efaeb728c7355e542a203be5e"

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)
}

View File

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

View File

@@ -0,0 +1,70 @@
go-ini
======
INI parsing library for Go (golang).
View the API documentation [here](http://godoc.org/github.com/vaughan0/go-ini).
Usage
-----
Parse an INI file:
```go
import "github.com/vaughan0/go-ini"
file, err := ini.LoadFile("myfile.ini")
```
Get data from the parsed file:
```go
name, ok := file.Get("person", "name")
if !ok {
panic("'name' variable missing from 'person' section")
}
```
Iterate through values in a section:
```go
for key, value := range file["mysection"] {
fmt.Printf("%s => %s\n", key, value)
}
```
Iterate through sections in a file:
```go
for name, section := range file {
fmt.Printf("Section name: %s\n", name)
}
```
File Format
-----------
INI files are parsed by go-ini line-by-line. Each line may be one of the following:
* A section definition: [section-name]
* A property: key = value
* A comment: #blahblah _or_ ;blahblah
* Blank. The line will be ignored.
Properties defined before any section headers are placed in the default section, which has
the empty string as it's key.
Example:
```ini
# I am a comment
; So am I!
[apples]
colour = red or green
shape = applish
[oranges]
shape = square
colour = blue
```

123
Godeps/_workspace/src/github.com/vaughan0/go-ini/ini.go generated vendored Normal file
View File

@@ -0,0 +1,123 @@
// Package ini provides functions for parsing INI configuration files.
package ini
import (
"bufio"
"fmt"
"io"
"os"
"regexp"
"strings"
)
var (
sectionRegex = regexp.MustCompile(`^\[(.*)\]$`)
assignRegex = regexp.MustCompile(`^([^=]+)=(.*)$`)
)
// ErrSyntax is returned when there is a syntax error in an INI file.
type ErrSyntax struct {
Line int
Source string // The contents of the erroneous line, without leading or trailing whitespace
}
func (e ErrSyntax) Error() string {
return fmt.Sprintf("invalid INI syntax on line %d: %s", e.Line, e.Source)
}
// A File represents a parsed INI file.
type File map[string]Section
// A Section represents a single section of an INI file.
type Section map[string]string
// Returns a named Section. A Section will be created if one does not already exist for the given name.
func (f File) Section(name string) Section {
section := f[name]
if section == nil {
section = make(Section)
f[name] = section
}
return section
}
// Looks up a value for a key in a section and returns that value, along with a boolean result similar to a map lookup.
func (f File) Get(section, key string) (value string, ok bool) {
if s := f[section]; s != nil {
value, ok = s[key]
}
return
}
// Loads INI data from a reader and stores the data in the File.
func (f File) Load(in io.Reader) (err error) {
bufin, ok := in.(*bufio.Reader)
if !ok {
bufin = bufio.NewReader(in)
}
return parseFile(bufin, f)
}
// Loads INI data from a named file and stores the data in the File.
func (f File) LoadFile(file string) (err error) {
in, err := os.Open(file)
if err != nil {
return
}
defer in.Close()
return f.Load(in)
}
func parseFile(in *bufio.Reader, file File) (err error) {
section := ""
lineNum := 0
for done := false; !done; {
var line string
if line, err = in.ReadString('\n'); err != nil {
if err == io.EOF {
done = true
} else {
return
}
}
lineNum++
line = strings.TrimSpace(line)
if len(line) == 0 {
// Skip blank lines
continue
}
if line[0] == ';' || line[0] == '#' {
// Skip comments
continue
}
if groups := assignRegex.FindStringSubmatch(line); groups != nil {
key, val := groups[1], groups[2]
key, val = strings.TrimSpace(key), strings.TrimSpace(val)
file.Section(section)[key] = val
} else if groups := sectionRegex.FindStringSubmatch(line); groups != nil {
name := strings.TrimSpace(groups[1])
section = name
// Create the section if it does not exist
file.Section(section)
} else {
return ErrSyntax{lineNum, line}
}
}
return nil
}
// Loads and returns a File from a reader.
func Load(in io.Reader) (File, error) {
file := make(File)
err := file.Load(in)
return file, err
}
// Loads and returns an INI File from a file on disk.
func LoadFile(filename string) (File, error) {
file := make(File)
err := file.LoadFile(filename)
return file, err
}

View File

@@ -0,0 +1,43 @@
package ini
import (
"reflect"
"syscall"
"testing"
)
func TestLoadFile(t *testing.T) {
originalOpenFiles := numFilesOpen(t)
file, err := LoadFile("test.ini")
if err != nil {
t.Fatal(err)
}
if originalOpenFiles != numFilesOpen(t) {
t.Error("test.ini not closed")
}
if !reflect.DeepEqual(file, File{"default": {"stuff": "things"}}) {
t.Error("file not read correctly")
}
}
func numFilesOpen(t *testing.T) (num uint64) {
var rlimit syscall.Rlimit
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlimit)
if err != nil {
t.Fatal(err)
}
maxFds := int(rlimit.Cur)
var stat syscall.Stat_t
for i := 0; i < maxFds; i++ {
if syscall.Fstat(i, &stat) == nil {
num++
} else {
return
}
}
return
}

View File

@@ -0,0 +1,89 @@
package ini
import (
"reflect"
"strings"
"testing"
)
func TestLoad(t *testing.T) {
src := `
# Comments are ignored
herp = derp
[foo]
hello=world
whitespace should = not matter
; sneaky semicolon-style comment
multiple = equals = signs
[bar]
this = that`
file, err := Load(strings.NewReader(src))
if err != nil {
t.Fatal(err)
}
check := func(section, key, expect string) {
if value, _ := file.Get(section, key); value != expect {
t.Errorf("Get(%q, %q): expected %q, got %q", section, key, expect, value)
}
}
check("", "herp", "derp")
check("foo", "hello", "world")
check("foo", "whitespace should", "not matter")
check("foo", "multiple", "equals = signs")
check("bar", "this", "that")
}
func TestSyntaxError(t *testing.T) {
src := `
# Line 2
[foo]
bar = baz
# Here's an error on line 6:
wut?
herp = derp`
_, err := Load(strings.NewReader(src))
t.Logf("%T: %v", err, err)
if err == nil {
t.Fatal("expected an error, got nil")
}
syntaxErr, ok := err.(ErrSyntax)
if !ok {
t.Fatal("expected an error of type ErrSyntax")
}
if syntaxErr.Line != 6 {
t.Fatal("incorrect line number")
}
if syntaxErr.Source != "wut?" {
t.Fatal("incorrect source")
}
}
func TestDefinedSectionBehaviour(t *testing.T) {
check := func(src string, expect File) {
file, err := Load(strings.NewReader(src))
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(file, expect) {
t.Errorf("expected %v, got %v", expect, file)
}
}
// No sections for an empty file
check("", File{})
// Default section only if there are actually values for it
check("foo=bar", File{"": {"foo": "bar"}})
// User-defined sections should always be present, even if empty
check("[a]\n[b]\nfoo=bar", File{
"a": {},
"b": {"foo": "bar"},
})
check("foo=bar\n[a]\nthis=that", File{
"": {"foo": "bar"},
"a": {"this": "that"},
})
}

View File

@@ -0,0 +1,2 @@
[default]
stuff = things

View File

@@ -20,6 +20,7 @@ package main
// This should probably be part of some configuration fed into the build for a
// given binary target.
import (
_ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/aws"
_ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/gce"
_ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/vagrant"
_ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/ovirt"

View File

@@ -0,0 +1,181 @@
/*
Copyright 2014 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package aws_cloud
import (
"fmt"
"io"
"net"
"regexp"
"code.google.com/p/gcfg"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/goamz/ec2"
"github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider"
)
type EC2 interface {
Instances(instIds []string, filter *ec2.Filter) (resp *ec2.InstancesResp, err error)
}
// AWSCloud is an implementation of Interface, TCPLoadBalancer and Instances for Amazon Web Services.
type AWSCloud struct {
ec2 EC2
cfg *AWSCloudConfig
}
type AWSCloudConfig struct {
Global struct {
Region string
}
}
type AuthFunc func() (auth aws.Auth, err error)
func init() {
cloudprovider.RegisterCloudProvider("aws", func(config io.Reader) (cloudprovider.Interface, error) {
return newAWSCloud(config, getAuth)
})
}
func getAuth() (auth aws.Auth, err error) {
return aws.GetAuth("", "")
}
// readAWSCloudConfig reads an instance of AWSCloudConfig from config reader.
func readAWSCloudConfig(config io.Reader) (*AWSCloudConfig, error) {
if config == nil {
return nil, fmt.Errorf("No AWS cloud provider config file given")
}
var cfg AWSCloudConfig
err := gcfg.ReadInto(&cfg, config)
if err != nil {
return nil, err
}
if cfg.Global.Region == "" {
return nil, fmt.Errorf("No region specified in configuration file")
}
return &cfg, nil
}
// newAWSCloud creates a new instance of AWSCloud.
func newAWSCloud(config io.Reader, authFunc AuthFunc) (*AWSCloud, error) {
cfg, err := readAWSCloudConfig(config)
if err != nil {
return nil, fmt.Errorf("Unable to read AWS cloud provider config file: %s", err)
}
auth, err := authFunc()
if err != nil {
return nil, err
}
region, ok := aws.Regions[cfg.Global.Region]
if !ok {
return nil, fmt.Errorf("Not a valid AWS region: %s", cfg.Global.Region)
}
ec2 := ec2.New(auth, region)
return &AWSCloud{
ec2: ec2,
cfg: cfg,
}, nil
}
// TCPLoadBalancer returns an implementation of TCPLoadBalancer for Amazon Web Services.
func (aws *AWSCloud) TCPLoadBalancer() (cloudprovider.TCPLoadBalancer, bool) {
return nil, false
}
// Instances returns an implementation of Instances for Amazon Web Services.
func (aws *AWSCloud) Instances() (cloudprovider.Instances, bool) {
return aws, true
}
// Zones returns an implementation of Zones for Amazon Web Services.
func (aws *AWSCloud) Zones() (cloudprovider.Zones, bool) {
return nil, false
}
// IPAddress is an implementation of Instances.IPAddress.
func (aws *AWSCloud) IPAddress(name string) (net.IP, error) {
f := ec2.NewFilter()
f.Add("private-dns-name", name)
resp, err := aws.ec2.Instances(nil, f)
if err != nil {
return nil, err
}
if len(resp.Reservations) == 0 {
return nil, fmt.Errorf("No reservations found for host: %s", name)
}
if len(resp.Reservations) > 1 {
return nil, fmt.Errorf("Multiple reservations found for host: %s", name)
}
if len(resp.Reservations[0].Instances) == 0 {
return nil, fmt.Errorf("No instances found for host: %s", name)
}
if len(resp.Reservations[0].Instances) > 1 {
return nil, fmt.Errorf("Multiple instances found for host: %s", name)
}
ipAddress := resp.Reservations[0].Instances[0].PrivateIpAddress
ip := net.ParseIP(ipAddress)
if ip == nil {
return nil, fmt.Errorf("Invalid network IP: %s", ipAddress)
}
return ip, nil
}
// Return a list of instances matching regex string.
func (aws *AWSCloud) getInstancesByRegex(regex string) ([]string, error) {
resp, err := aws.ec2.Instances(nil, nil)
if err != nil {
return []string{}, err
}
if resp == nil {
return []string{}, fmt.Errorf("No InstanceResp returned")
}
re, err := regexp.Compile(regex)
if err != nil {
return []string{}, err
}
instances := []string{}
for _, reservation := range resp.Reservations {
for _, instance := range reservation.Instances {
for _, tag := range instance.Tags {
if tag.Key == "Name" && re.MatchString(tag.Value) {
instances = append(instances, instance.PrivateDNSName)
break
}
}
}
}
return instances, nil
}
// List is an implementation of Instances.List.
func (aws *AWSCloud) List(filter string) ([]string, error) {
// TODO: Should really use tag query. No need to go regexp.
return aws.getInstancesByRegex(filter)
}

View File

@@ -0,0 +1,157 @@
/*
Copyright 2014 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package aws_cloud
import (
"reflect"
"strings"
"testing"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/goamz/ec2"
)
func TestReadAWSCloudConfig(t *testing.T) {
_, err1 := readAWSCloudConfig(nil)
if err1 == nil {
t.Errorf("Should error when no config reader is given")
}
_, err2 := readAWSCloudConfig(strings.NewReader(""))
if err2 == nil {
t.Errorf("Should error when config is empty")
}
_, err3 := readAWSCloudConfig(strings.NewReader("[global]\n"))
if err3 == nil {
t.Errorf("Should error when no region is specified")
}
cfg, err4 := readAWSCloudConfig(strings.NewReader("[global]\nregion = eu-west-1"))
if err4 != nil {
t.Errorf("Should succeed when a region is specified: %s", err4)
}
if cfg.Global.Region != "eu-west-1" {
t.Errorf("Should read region from config")
}
}
func TestNewAWSCloud(t *testing.T) {
fakeAuthFunc := func() (auth aws.Auth, err error) {
return aws.Auth{"", "", ""}, nil
}
_, err1 := newAWSCloud(nil, fakeAuthFunc)
if err1 == nil {
t.Errorf("Should error when no config reader is given")
}
_, err2 := newAWSCloud(strings.NewReader(
"[global]\nregion = blahonga"),
fakeAuthFunc)
if err2 == nil {
t.Errorf("Should error when config specifies invalid region")
}
_, err3 := newAWSCloud(
strings.NewReader("[global]\nregion = eu-west-1"),
fakeAuthFunc)
if err3 != nil {
t.Errorf("Should succeed when a valid region is specified: %s", err3)
}
}
type FakeEC2 struct {
instances func(instanceIds []string, filter *ec2.Filter) (resp *ec2.InstancesResp, err error)
}
func (ec2 *FakeEC2) Instances(instanceIds []string, filter *ec2.Filter) (resp *ec2.InstancesResp, err error) {
return ec2.instances(instanceIds, filter)
}
func mockInstancesResp(instances []ec2.Instance) (aws *AWSCloud) {
return &AWSCloud{
&FakeEC2{
func(instanceIds []string, filter *ec2.Filter) (resp *ec2.InstancesResp, err error) {
return &ec2.InstancesResp{"",
[]ec2.Reservation{
ec2.Reservation{"", "", "", nil, instances}}}, nil
}},
nil}
}
func TestList(t *testing.T) {
instances := make([]ec2.Instance, 4)
instances[0].Tags = []ec2.Tag{ec2.Tag{"Name", "foo"}}
instances[0].PrivateDNSName = "instance1"
instances[1].Tags = []ec2.Tag{ec2.Tag{"Name", "bar"}}
instances[1].PrivateDNSName = "instance2"
instances[2].Tags = []ec2.Tag{ec2.Tag{"Name", "baz"}}
instances[2].PrivateDNSName = "instance3"
instances[3].Tags = []ec2.Tag{ec2.Tag{"Name", "quux"}}
instances[3].PrivateDNSName = "instance4"
aws := mockInstancesResp(instances)
table := []struct {
input string
expect []string
}{
{"blahonga", []string{}},
{"quux", []string{"instance4"}},
{"a", []string{"instance2", "instance3"}},
}
for _, item := range table {
result, err := aws.List(item.input)
if err != nil {
t.Errorf("Expected call with %v to succeed, failed with %s", item.input, err)
}
if e, a := item.expect, result; !reflect.DeepEqual(e, a) {
t.Errorf("Expected %v, got %v", e, a)
}
}
}
func TestIPAddress(t *testing.T) {
instances := make([]ec2.Instance, 2)
instances[0].PrivateDNSName = "instance1"
instances[0].PrivateIpAddress = "192.168.0.1"
instances[1].PrivateDNSName = "instance2"
instances[1].PrivateIpAddress = "192.168.0.2"
aws1 := mockInstancesResp([]ec2.Instance{})
_, err1 := aws1.IPAddress("instance")
if err1 == nil {
t.Errorf("Should error when no instance found")
}
aws2 := mockInstancesResp(instances)
_, err2 := aws2.IPAddress("instance1")
if err2 == nil {
t.Errorf("Should error when multiple instances found")
}
aws3 := mockInstancesResp(instances[0:1])
ip3, err3 := aws3.IPAddress("instance1")
if err3 != nil {
t.Errorf("Should not error when instance found")
}
if e, a := instances[0].PrivateIpAddress, ip3.String(); e != a {
t.Errorf("Expected %v, got %v", e, a)
}
}