diff --git a/vendor/src/launchpad.net/goamz/.lbox b/vendor/src/launchpad.net/goamz/.lbox
new file mode 100644
index 0000000000..75e124512f
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/.lbox
@@ -0,0 +1 @@
+propose -for=lp:goamz -cr
diff --git a/vendor/src/launchpad.net/goamz/LICENSE b/vendor/src/launchpad.net/goamz/LICENSE
new file mode 100644
index 0000000000..53320c352b
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/LICENSE
@@ -0,0 +1,185 @@
+This software is licensed under the LGPLv3, included below.
+
+As a special exception to the GNU Lesser General Public License version 3
+("LGPL3"), the copyright holders of this Library give you permission to
+convey to a third party a Combined Work that links statically or dynamically
+to this Library without providing any Minimal Corresponding Source or
+Minimal Application Code as set out in 4d or providing the installation
+information set out in section 4e, provided that you comply with the other
+provisions of LGPL3 and provided that you meet, for the Application the
+terms and conditions of the license(s) which apply to the Application.
+
+Except as stated in this special exception, the provisions of LGPL3 will
+continue to comply in full to this Library. If you modify this Library, you
+may apply this exception to your version of this Library, but you are not
+obliged to do so. If you do not wish to do so, delete this exception
+statement from your version. This exception does not (and cannot) modify any
+license terms which apply to the Application, with which you must still
+comply.
+
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/vendor/src/launchpad.net/goamz/aws/attempt.go b/vendor/src/launchpad.net/goamz/aws/attempt.go
new file mode 100644
index 0000000000..c0654f5d85
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/aws/attempt.go
@@ -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
+}
diff --git a/vendor/src/launchpad.net/goamz/aws/attempt_test.go b/vendor/src/launchpad.net/goamz/aws/attempt_test.go
new file mode 100644
index 0000000000..c8352e02a5
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/aws/attempt_test.go
@@ -0,0 +1,57 @@
+package aws_test
+
+import (
+ "launchpad.net/goamz/aws"
+ . "launchpad.net/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)
+}
diff --git a/vendor/src/launchpad.net/goamz/aws/aws.go b/vendor/src/launchpad.net/goamz/aws/aws.go
new file mode 100644
index 0000000000..366460095b
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/aws/aws.go
@@ -0,0 +1,212 @@
+//
+// goamz - Go packages to interact with the Amazon Web Services.
+//
+// https://wiki.ubuntu.com/goamz
+//
+// Copyright (c) 2011 Canonical Ltd.
+//
+
+package aws
+
+import (
+ "errors"
+ "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
+}
+
+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",
+}
+
+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",
+}
+
+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",
+}
+
+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",
+}
+
+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",
+}
+
+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",
+}
+
+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",
+}
+
+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",
+}
+
+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,
+}
+
+type Auth struct {
+ AccessKey, SecretKey string
+}
+
+var unreserved = make([]bool, 128)
+var hex = "0123456789ABCDEF"
+
+func init() {
+ // RFC3986
+ u := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890-_.~"
+ for _, c := range u {
+ unreserved[c] = true
+ }
+}
+
+// EnvAuth creates an Auth based on environment information.
+// The AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment
+// variables are used as the first preference, but EC2_ACCESS_KEY
+// and EC2_SECRET_KEY environment variables are also supported.
+func EnvAuth() (auth Auth, err error) {
+ auth.AccessKey = os.Getenv("AWS_ACCESS_KEY_ID")
+ auth.SecretKey = os.Getenv("AWS_SECRET_ACCESS_KEY")
+ // We fallback to EC2_ env variables if the AWS_ variants are not used.
+ if auth.AccessKey == "" && auth.SecretKey == "" {
+ auth.AccessKey = os.Getenv("EC2_ACCESS_KEY")
+ auth.SecretKey = os.Getenv("EC2_SECRET_KEY")
+ }
+ if auth.AccessKey == "" {
+ err = errors.New("AWS_ACCESS_KEY_ID not found in environment")
+ }
+ if auth.SecretKey == "" {
+ err = errors.New("AWS_SECRET_ACCESS_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])
+}
diff --git a/vendor/src/launchpad.net/goamz/aws/aws_test.go b/vendor/src/launchpad.net/goamz/aws/aws_test.go
new file mode 100644
index 0000000000..dc14e92987
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/aws/aws_test.go
@@ -0,0 +1,73 @@
+package aws_test
+
+import (
+ "launchpad.net/goamz/aws"
+ . "launchpad.net/gocheck"
+ "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) TestEnvAuthNoSecret(c *C) {
+ os.Clearenv()
+ _, err := aws.EnvAuth()
+ c.Assert(err, ErrorMatches, "AWS_SECRET_ACCESS_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 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) TestEnvAuthLegacy(c *C) {
+ os.Clearenv()
+ os.Setenv("EC2_SECRET_KEY", "secret")
+ os.Setenv("EC2_ACCESS_KEY", "access")
+ auth, err := aws.EnvAuth()
+ 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)
+ }
+}
diff --git a/vendor/src/launchpad.net/goamz/ec2/ec2.go b/vendor/src/launchpad.net/goamz/ec2/ec2.go
new file mode 100644
index 0000000000..54896dfdbf
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/ec2/ec2.go
@@ -0,0 +1,1087 @@
+//
+// goamz - Go packages to interact with the Amazon Web Services.
+//
+// https://wiki.ubuntu.com/goamz
+//
+// Copyright (c) 2011 Canonical Ltd.
+//
+
+package ec2
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+ "encoding/xml"
+ "fmt"
+ "log"
+ "net/http"
+ "net/http/httputil"
+ "net/url"
+ "sort"
+ "strconv"
+ "time"
+
+ "launchpad.net/goamz/aws"
+)
+
+const (
+ debug = false
+
+ // legacyAPIVersion is the AWS API version used for all but
+ // VPC-related requests.
+ legacyAPIVersion = "2011-12-15"
+
+ // AWS API version used for VPC-related calls.
+ vpcAPIVersion = "2013-10-15"
+)
+
+// The EC2 type encapsulates operations with a specific EC2 region.
+type EC2 struct {
+ aws.Auth
+ aws.Region
+ private byte // Reserve the right of using private data.
+}
+
+// New creates a new EC2.
+func New(auth aws.Auth, region aws.Region) *EC2 {
+ return &EC2{auth, region, 0}
+}
+
+// ----------------------------------------------------------------------------
+// Filtering helper.
+
+// Filter builds filtering parameters to be used in an EC2 query which supports
+// filtering. For example:
+//
+// filter := NewFilter()
+// filter.Add("architecture", "i386")
+// filter.Add("launch-index", "0")
+// resp, err := ec2.Instances(nil, filter)
+//
+type Filter struct {
+ m map[string][]string
+}
+
+// NewFilter creates a new Filter.
+func NewFilter() *Filter {
+ return &Filter{make(map[string][]string)}
+}
+
+// Add appends a filtering parameter with the given name and value(s).
+func (f *Filter) Add(name string, value ...string) {
+ f.m[name] = append(f.m[name], value...)
+}
+
+func (f *Filter) addParams(params map[string]string) {
+ if f != nil {
+ a := make([]string, len(f.m))
+ i := 0
+ for k := range f.m {
+ a[i] = k
+ i++
+ }
+ sort.StringSlice(a).Sort()
+ for i, k := range a {
+ prefix := "Filter." + strconv.Itoa(i+1)
+ params[prefix+".Name"] = k
+ for j, v := range f.m[k] {
+ params[prefix+".Value."+strconv.Itoa(j+1)] = v
+ }
+ }
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Request dispatching logic.
+
+// Error encapsulates an error returned by EC2.
+//
+// See http://goo.gl/VZGuC for more details.
+type Error struct {
+ // HTTP status code (200, 403, ...)
+ StatusCode int
+ // EC2 error code ("UnsupportedOperation", ...)
+ Code string
+ // The human-oriented error message
+ Message string
+ RequestId string `xml:"RequestID"`
+}
+
+func (err *Error) Error() string {
+ if err.Code == "" {
+ return err.Message
+ }
+
+ return fmt.Sprintf("%s (%s)", err.Message, err.Code)
+}
+
+// For now a single error inst is being exposed. In the future it may be useful
+// to provide access to all of them, but rather than doing it as an array/slice,
+// use a *next pointer, so that it's backward compatible and it continues to be
+// easy to handle the first error, which is what most people will want.
+type xmlErrors struct {
+ RequestId string `xml:"RequestID"`
+ Errors []Error `xml:"Errors>Error"`
+}
+
+var timeNow = time.Now
+
+func (ec2 *EC2) query(params map[string]string, resp interface{}) error {
+ params["Timestamp"] = timeNow().In(time.UTC).Format(time.RFC3339)
+ endpoint, err := url.Parse(ec2.Region.EC2Endpoint)
+ if err != nil {
+ return err
+ }
+ if endpoint.Path == "" {
+ endpoint.Path = "/"
+ }
+ sign(ec2.Auth, "GET", endpoint.Path, params, endpoint.Host)
+ endpoint.RawQuery = multimap(params).Encode()
+ if debug {
+ log.Printf("get { %v } -> {\n", endpoint.String())
+ }
+ r, err := http.Get(endpoint.String())
+ if err != nil {
+ return err
+ }
+ defer r.Body.Close()
+
+ if debug {
+ dump, _ := httputil.DumpResponse(r, true)
+ log.Printf("response:\n")
+ log.Printf("%v\n}\n", string(dump))
+ }
+ if r.StatusCode != 200 {
+ return buildError(r)
+ }
+ err = xml.NewDecoder(r.Body).Decode(resp)
+ return err
+}
+
+func multimap(p map[string]string) url.Values {
+ q := make(url.Values, len(p))
+ for k, v := range p {
+ q[k] = []string{v}
+ }
+ return q
+}
+
+func buildError(r *http.Response) error {
+ errors := xmlErrors{}
+ xml.NewDecoder(r.Body).Decode(&errors)
+ var err Error
+ if len(errors.Errors) > 0 {
+ err = errors.Errors[0]
+ }
+ err.RequestId = errors.RequestId
+ err.StatusCode = r.StatusCode
+ if err.Message == "" {
+ err.Message = r.Status
+ }
+ return &err
+}
+
+func makeParams(action string) map[string]string {
+ return makeParamsWithVersion(action, legacyAPIVersion)
+}
+
+func makeParamsVPC(action string) map[string]string {
+ return makeParamsWithVersion(action, vpcAPIVersion)
+}
+
+func makeParamsWithVersion(action, version string) map[string]string {
+ params := make(map[string]string)
+ params["Action"] = action
+ params["Version"] = version
+ return params
+}
+
+func addParamsList(params map[string]string, label string, ids []string) {
+ for i, id := range ids {
+ params[label+"."+strconv.Itoa(i+1)] = id
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Instance management functions and types.
+
+// RunNetworkInterface encapsulates options for a single network
+// interface, specified when calling RunInstances.
+//
+// If Id is set, it must match an existing VPC network interface, and
+// in this case only a single instance can be launched. If Id is not
+// set, a new network interface will be created for each instance.
+//
+// The following fields are required when creating a new network
+// interface (i.e. Id is empty): DeviceIndex, SubnetId, Description
+// (only used if set), SecurityGroupIds.
+//
+// PrivateIPs can be used to add one or more private IP addresses to a
+// network interface. Only one of the IP addresses can be set as
+// primary. If none are given, EC2 selects a primary IP for each
+// created interface from the subnet pool.
+//
+// When SecondaryPrivateIPCount is non-zero, EC2 allocates that number
+// of IP addresses from within the subnet range and sets them as
+// secondary IPs. The number of IP addresses that can be assigned to a
+// network interface varies by instance type.
+type RunNetworkInterface struct {
+ Id string
+ DeviceIndex int
+ SubnetId string
+ Description string
+ PrivateIPs []PrivateIP
+ SecurityGroupIds []string
+ DeleteOnTermination bool
+ SecondaryPrivateIPCount int
+}
+
+// The RunInstances type encapsulates options for the respective request in EC2.
+//
+// See http://goo.gl/Mcm3b for more details.
+type RunInstances struct {
+ ImageId string
+ MinCount int
+ MaxCount int
+ KeyName string
+ InstanceType string
+ SecurityGroups []SecurityGroup
+ KernelId string
+ RamdiskId string
+ UserData []byte
+ AvailZone string
+ PlacementGroupName string
+ Monitoring bool
+ SubnetId string
+ DisableAPITermination bool
+ ShutdownBehavior string
+ PrivateIPAddress string
+ BlockDeviceMappings []BlockDeviceMapping
+ NetworkInterfaces []RunNetworkInterface
+}
+
+// Response to a RunInstances request.
+//
+// See http://goo.gl/Mcm3b for more details.
+type RunInstancesResp struct {
+ RequestId string `xml:"requestId"`
+ ReservationId string `xml:"reservationId"`
+ OwnerId string `xml:"ownerId"`
+ SecurityGroups []SecurityGroup `xml:"groupSet>item"`
+ Instances []Instance `xml:"instancesSet>item"`
+}
+
+// Instance encapsulates a running instance in EC2.
+//
+// See http://goo.gl/OCH8a for more details.
+type Instance struct {
+ InstanceId string `xml:"instanceId"`
+ InstanceType string `xml:"instanceType"`
+ ImageId string `xml:"imageId"`
+ PrivateDNSName string `xml:"privateDnsName"`
+ DNSName string `xml:"dnsName"`
+ IPAddress string `xml:"ipAddress"`
+ PrivateIPAddress string `xml:"privateIpAddress"`
+ SubnetId string `xml:"subnetId"`
+ VPCId string `xml:"vpcId"`
+ SourceDestCheck bool `xml:"sourceDestCheck"`
+ KeyName string `xml:"keyName"`
+ AMILaunchIndex int `xml:"amiLaunchIndex"`
+ Hypervisor string `xml:"hypervisor"`
+ VirtType string `xml:"virtualizationType"`
+ Monitoring string `xml:"monitoring>state"`
+ AvailZone string `xml:"placement>availabilityZone"`
+ PlacementGroupName string `xml:"placement>groupName"`
+ State InstanceState `xml:"instanceState"`
+ Tags []Tag `xml:"tagSet>item"`
+ SecurityGroups []SecurityGroup `xml:"groupSet>item"`
+ NetworkInterfaces []NetworkInterface `xml:"networkInterfaceSet>item"`
+}
+
+// RunInstances starts new instances in EC2.
+// If options.MinCount and options.MaxCount are both zero, a single instance
+// will be started; otherwise if options.MaxCount is zero, options.MinCount
+// will be used instead.
+//
+// See http://goo.gl/Mcm3b for more details.
+func (ec2 *EC2) RunInstances(options *RunInstances) (resp *RunInstancesResp, err error) {
+ params := prepareRunParams(*options)
+ params["ImageId"] = options.ImageId
+ params["InstanceType"] = options.InstanceType
+ var min, max int
+ if options.MinCount == 0 && options.MaxCount == 0 {
+ min = 1
+ max = 1
+ } else if options.MaxCount == 0 {
+ min = options.MinCount
+ max = min
+ } else {
+ min = options.MinCount
+ max = options.MaxCount
+ }
+ params["MinCount"] = strconv.Itoa(min)
+ params["MaxCount"] = strconv.Itoa(max)
+ i, j := 1, 1
+ for _, g := range options.SecurityGroups {
+ if g.Id != "" {
+ params["SecurityGroupId."+strconv.Itoa(i)] = g.Id
+ i++
+ } else {
+ params["SecurityGroup."+strconv.Itoa(j)] = g.Name
+ j++
+ }
+ }
+ prepareBlockDevices(params, options.BlockDeviceMappings)
+ prepareNetworkInterfaces(params, options.NetworkInterfaces)
+ token, err := clientToken()
+ if err != nil {
+ return nil, err
+ }
+ params["ClientToken"] = token
+
+ if options.KeyName != "" {
+ params["KeyName"] = options.KeyName
+ }
+ if options.KernelId != "" {
+ params["KernelId"] = options.KernelId
+ }
+ if options.RamdiskId != "" {
+ params["RamdiskId"] = options.RamdiskId
+ }
+ if options.UserData != nil {
+ userData := make([]byte, b64.EncodedLen(len(options.UserData)))
+ b64.Encode(userData, options.UserData)
+ params["UserData"] = string(userData)
+ }
+ if options.AvailZone != "" {
+ params["Placement.AvailabilityZone"] = options.AvailZone
+ }
+ if options.PlacementGroupName != "" {
+ params["Placement.GroupName"] = options.PlacementGroupName
+ }
+ if options.Monitoring {
+ params["Monitoring.Enabled"] = "true"
+ }
+ if options.SubnetId != "" {
+ params["SubnetId"] = options.SubnetId
+ }
+ if options.DisableAPITermination {
+ params["DisableApiTermination"] = "true"
+ }
+ if options.ShutdownBehavior != "" {
+ params["InstanceInitiatedShutdownBehavior"] = options.ShutdownBehavior
+ }
+ if options.PrivateIPAddress != "" {
+ params["PrivateIpAddress"] = options.PrivateIPAddress
+ }
+
+ resp = &RunInstancesResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return
+}
+
+func prepareRunParams(options RunInstances) map[string]string {
+ if options.SubnetId != "" || len(options.NetworkInterfaces) > 0 {
+ // When either SubnetId or NetworkInterfaces are specified, we
+ // need to use the API version with complete VPC support.
+ return makeParamsVPC("RunInstances")
+ } else {
+ return makeParams("RunInstances")
+ }
+}
+
+func prepareBlockDevices(params map[string]string, blockDevs []BlockDeviceMapping) {
+ for i, b := range blockDevs {
+ n := strconv.Itoa(i + 1)
+ prefix := "BlockDeviceMapping." + n
+ if b.DeviceName != "" {
+ params[prefix+".DeviceName"] = b.DeviceName
+ }
+ if b.VirtualName != "" {
+ params[prefix+".VirtualName"] = b.VirtualName
+ }
+ if b.SnapshotId != "" {
+ params[prefix+".Ebs.SnapshotId"] = b.SnapshotId
+ }
+ if b.VolumeType != "" {
+ params[prefix+".Ebs.VolumeType"] = b.VolumeType
+ }
+ if b.VolumeSize > 0 {
+ params[prefix+".Ebs.VolumeSize"] = strconv.FormatInt(b.VolumeSize, 10)
+ }
+ if b.IOPS > 0 {
+ params[prefix+".Ebs.Iops"] = strconv.FormatInt(b.IOPS, 10)
+ }
+ if b.DeleteOnTermination {
+ params[prefix+".Ebs.DeleteOnTermination"] = "true"
+ }
+ }
+}
+
+func prepareNetworkInterfaces(params map[string]string, nics []RunNetworkInterface) {
+ for i, ni := range nics {
+ // Unlike other lists, NetworkInterface and PrivateIpAddresses
+ // should start from 0, not 1, according to the examples
+ // requests in the API documentation here http://goo.gl/Mcm3b.
+ n := strconv.Itoa(i)
+ prefix := "NetworkInterface." + n
+ if ni.Id != "" {
+ params[prefix+".NetworkInterfaceId"] = ni.Id
+ }
+ params[prefix+".DeviceIndex"] = strconv.Itoa(ni.DeviceIndex)
+ if ni.SubnetId != "" {
+ params[prefix+".SubnetId"] = ni.SubnetId
+ }
+ if ni.Description != "" {
+ params[prefix+".Description"] = ni.Description
+ }
+ for j, gid := range ni.SecurityGroupIds {
+ k := strconv.Itoa(j + 1)
+ params[prefix+".SecurityGroupId."+k] = gid
+ }
+ if ni.DeleteOnTermination {
+ params[prefix+".DeleteOnTermination"] = "true"
+ }
+ if ni.SecondaryPrivateIPCount > 0 {
+ val := strconv.Itoa(ni.SecondaryPrivateIPCount)
+ params[prefix+".SecondaryPrivateIpAddressCount"] = val
+ }
+ for j, ip := range ni.PrivateIPs {
+ k := strconv.Itoa(j)
+ subprefix := prefix + ".PrivateIpAddresses." + k
+ params[subprefix+".PrivateIpAddress"] = ip.Address
+ params[subprefix+".Primary"] = strconv.FormatBool(ip.IsPrimary)
+ }
+ }
+}
+
+func clientToken() (string, error) {
+ // Maximum EC2 client token size is 64 bytes.
+ // Each byte expands to two when hex encoded.
+ buf := make([]byte, 32)
+ _, err := rand.Read(buf)
+ if err != nil {
+ return "", err
+ }
+ return hex.EncodeToString(buf), nil
+}
+
+// Response to a TerminateInstances request.
+//
+// See http://goo.gl/3BKHj for more details.
+type TerminateInstancesResp struct {
+ RequestId string `xml:"requestId"`
+ StateChanges []InstanceStateChange `xml:"instancesSet>item"`
+}
+
+// InstanceState encapsulates the state of an instance in EC2.
+//
+// See http://goo.gl/y3ZBq for more details.
+type InstanceState struct {
+ Code int `xml:"code"` // Watch out, bits 15-8 have unpublished meaning.
+ Name string `xml:"name"`
+}
+
+// InstanceStateChange informs of the previous and current states
+// for an instance when a state change is requested.
+type InstanceStateChange struct {
+ InstanceId string `xml:"instanceId"`
+ CurrentState InstanceState `xml:"currentState"`
+ PreviousState InstanceState `xml:"previousState"`
+}
+
+// TerminateInstances requests the termination of instances when the given ids.
+//
+// See http://goo.gl/3BKHj for more details.
+func (ec2 *EC2) TerminateInstances(instIds []string) (resp *TerminateInstancesResp, err error) {
+ params := makeParams("TerminateInstances")
+ addParamsList(params, "InstanceId", instIds)
+ resp = &TerminateInstancesResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return
+}
+
+// Response to a DescribeInstances request.
+//
+// See http://goo.gl/mLbmw for more details.
+type InstancesResp struct {
+ RequestId string `xml:"requestId"`
+ Reservations []Reservation `xml:"reservationSet>item"`
+}
+
+// Reservation represents details about a reservation in EC2.
+//
+// See http://goo.gl/0ItPT for more details.
+type Reservation struct {
+ ReservationId string `xml:"reservationId"`
+ OwnerId string `xml:"ownerId"`
+ RequesterId string `xml:"requesterId"`
+ SecurityGroups []SecurityGroup `xml:"groupSet>item"`
+ Instances []Instance `xml:"instancesSet>item"`
+}
+
+// Instances returns details about instances in EC2. Both parameters
+// are optional, and if provided will limit the instances returned to those
+// matching the given instance ids or filtering rules.
+//
+// See http://goo.gl/4No7c for more details.
+func (ec2 *EC2) Instances(instIds []string, filter *Filter) (resp *InstancesResp, err error) {
+ params := makeParams("DescribeInstances")
+ addParamsList(params, "InstanceId", instIds)
+ filter.addParams(params)
+ resp = &InstancesResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return
+}
+
+// ----------------------------------------------------------------------------
+// Image and snapshot management functions and types.
+
+// Response to a DescribeImages request.
+//
+// See http://goo.gl/hLnyg for more details.
+type ImagesResp struct {
+ RequestId string `xml:"requestId"`
+ Images []Image `xml:"imagesSet>item"`
+}
+
+// BlockDeviceMapping represents the association of a block device with an image.
+//
+// See http://goo.gl/wnDBf for more details.
+type BlockDeviceMapping struct {
+ DeviceName string `xml:"deviceName"`
+ VirtualName string `xml:"virtualName"`
+ SnapshotId string `xml:"ebs>snapshotId"`
+ VolumeType string `xml:"ebs>volumeType"`
+ VolumeSize int64 `xml:"ebs>volumeSize"` // Size is given in GB
+ DeleteOnTermination bool `xml:"ebs>deleteOnTermination"`
+
+ // The number of I/O operations per second (IOPS) that the volume supports.
+ IOPS int64 `xml:"ebs>iops"`
+}
+
+// Image represents details about an image.
+//
+// See http://goo.gl/iSqJG for more details.
+type Image struct {
+ Id string `xml:"imageId"`
+ Name string `xml:"name"`
+ Description string `xml:"description"`
+ Type string `xml:"imageType"`
+ State string `xml:"imageState"`
+ Location string `xml:"imageLocation"`
+ Public bool `xml:"isPublic"`
+ Architecture string `xml:"architecture"`
+ Platform string `xml:"platform"`
+ ProductCodes []string `xml:"productCode>item>productCode"`
+ KernelId string `xml:"kernelId"`
+ RamdiskId string `xml:"ramdiskId"`
+ StateReason string `xml:"stateReason"`
+ OwnerId string `xml:"imageOwnerId"`
+ OwnerAlias string `xml:"imageOwnerAlias"`
+ RootDeviceType string `xml:"rootDeviceType"`
+ RootDeviceName string `xml:"rootDeviceName"`
+ VirtualizationType string `xml:"virtualizationType"`
+ Hypervisor string `xml:"hypervisor"`
+ BlockDevices []BlockDeviceMapping `xml:"blockDeviceMapping>item"`
+}
+
+// Images returns details about available images.
+// The ids and filter parameters, if provided, will limit the images returned.
+// For example, to get all the private images associated with this account set
+// the boolean filter "is-private" to true.
+//
+// Note: calling this function with nil ids and filter parameters will result in
+// a very large number of images being returned.
+//
+// See http://goo.gl/SRBhW for more details.
+func (ec2 *EC2) Images(ids []string, filter *Filter) (resp *ImagesResp, err error) {
+ params := makeParams("DescribeImages")
+ for i, id := range ids {
+ params["ImageId."+strconv.Itoa(i+1)] = id
+ }
+ filter.addParams(params)
+
+ resp = &ImagesResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return
+}
+
+// Response to a CreateSnapshot request.
+//
+// See http://goo.gl/ttcda for more details.
+type CreateSnapshotResp struct {
+ RequestId string `xml:"requestId"`
+ Snapshot
+}
+
+// CreateSnapshot creates a volume snapshot and stores it in S3.
+//
+// See http://goo.gl/ttcda for more details.
+func (ec2 *EC2) CreateSnapshot(volumeId, description string) (resp *CreateSnapshotResp, err error) {
+ params := makeParams("CreateSnapshot")
+ params["VolumeId"] = volumeId
+ params["Description"] = description
+
+ resp = &CreateSnapshotResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return
+}
+
+// DeleteSnapshots deletes the volume snapshots with the given ids.
+//
+// Note: If you make periodic snapshots of a volume, the snapshots are
+// incremental so that only the blocks on the device that have changed
+// since your last snapshot are incrementally saved in the new snapshot.
+// Even though snapshots are saved incrementally, the snapshot deletion
+// process is designed so that you need to retain only the most recent
+// snapshot in order to restore the volume.
+//
+// See http://goo.gl/vwU1y for more details.
+func (ec2 *EC2) DeleteSnapshots(ids []string) (resp *SimpleResp, err error) {
+ params := makeParams("DeleteSnapshot")
+ for i, id := range ids {
+ params["SnapshotId."+strconv.Itoa(i+1)] = id
+ }
+
+ resp = &SimpleResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return
+}
+
+// Response to a DescribeSnapshots request.
+//
+// See http://goo.gl/nClDT for more details.
+type SnapshotsResp struct {
+ RequestId string `xml:"requestId"`
+ Snapshots []Snapshot `xml:"snapshotSet>item"`
+}
+
+// Snapshot represents details about a volume snapshot.
+//
+// See http://goo.gl/nkovs for more details.
+type Snapshot struct {
+ Id string `xml:"snapshotId"`
+ VolumeId string `xml:"volumeId"`
+ VolumeSize string `xml:"volumeSize"`
+ Status string `xml:"status"`
+ StartTime string `xml:"startTime"`
+ Description string `xml:"description"`
+ Progress string `xml:"progress"`
+ OwnerId string `xml:"ownerId"`
+ OwnerAlias string `xml:"ownerAlias"`
+ Tags []Tag `xml:"tagSet>item"`
+}
+
+// Snapshots returns details about volume snapshots available to the user.
+// The ids and filter parameters, if provided, limit the snapshots returned.
+//
+// See http://goo.gl/ogJL4 for more details.
+func (ec2 *EC2) Snapshots(ids []string, filter *Filter) (resp *SnapshotsResp, err error) {
+ params := makeParams("DescribeSnapshots")
+ for i, id := range ids {
+ params["SnapshotId."+strconv.Itoa(i+1)] = id
+ }
+ filter.addParams(params)
+
+ resp = &SnapshotsResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return
+}
+
+// ----------------------------------------------------------------------------
+// Security group management functions and types.
+
+// SimpleResp represents a response to an EC2 request which on success will
+// return no other information besides a request id.
+type SimpleResp struct {
+ XMLName xml.Name
+ RequestId string `xml:"requestId"`
+}
+
+// CreateSecurityGroupResp represents a response to a CreateSecurityGroup request.
+type CreateSecurityGroupResp struct {
+ SecurityGroup
+ RequestId string `xml:"requestId"`
+}
+
+// CreateSecurityGroup creates a security group with the provided name
+// and description.
+//
+// See http://goo.gl/Eo7Yl for more details.
+func (ec2 *EC2) CreateSecurityGroup(name, description string) (resp *CreateSecurityGroupResp, err error) {
+ return ec2.CreateSecurityGroupVPC("", name, description)
+}
+
+// CreateSecurityGroupVPC creates a security group in EC2, associated
+// with the given VPC ID. If vpcId is empty, this call is equivalent
+// to CreateSecurityGroup.
+//
+// See http://goo.gl/Eo7Yl for more details.
+func (ec2 *EC2) CreateSecurityGroupVPC(vpcId, name, description string) (resp *CreateSecurityGroupResp, err error) {
+ params := makeParamsVPC("CreateSecurityGroup")
+ params["GroupName"] = name
+ params["GroupDescription"] = description
+ if vpcId != "" {
+ params["VpcId"] = vpcId
+ }
+
+ resp = &CreateSecurityGroupResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ resp.Name = name
+ return resp, nil
+}
+
+// SecurityGroupsResp represents a response to a DescribeSecurityGroups
+// request in EC2.
+//
+// See http://goo.gl/k12Uy for more details.
+type SecurityGroupsResp struct {
+ RequestId string `xml:"requestId"`
+ Groups []SecurityGroupInfo `xml:"securityGroupInfo>item"`
+}
+
+// SecurityGroup encapsulates details for a security group in EC2.
+//
+// See http://goo.gl/CIdyP for more details.
+type SecurityGroupInfo struct {
+ SecurityGroup
+ VPCId string `xml:"vpcId"`
+ OwnerId string `xml:"ownerId"`
+ Description string `xml:"groupDescription"`
+ IPPerms []IPPerm `xml:"ipPermissions>item"`
+}
+
+// IPPerm represents an allowance within an EC2 security group.
+//
+// See http://goo.gl/4oTxv for more details.
+type IPPerm struct {
+ Protocol string `xml:"ipProtocol"`
+ FromPort int `xml:"fromPort"`
+ ToPort int `xml:"toPort"`
+ SourceIPs []string `xml:"ipRanges>item>cidrIp"`
+ SourceGroups []UserSecurityGroup `xml:"groups>item"`
+}
+
+// UserSecurityGroup holds a security group and the owner
+// of that group.
+type UserSecurityGroup struct {
+ Id string `xml:"groupId"`
+ Name string `xml:"groupName"`
+ OwnerId string `xml:"userId"`
+}
+
+// SecurityGroup represents an EC2 security group.
+// If SecurityGroup is used as a parameter, then one of Id or Name
+// may be empty. If both are set, then Id is used.
+type SecurityGroup struct {
+ Id string `xml:"groupId"`
+ Name string `xml:"groupName"`
+}
+
+// SecurityGroupNames is a convenience function that
+// returns a slice of security groups with the given names.
+func SecurityGroupNames(names ...string) []SecurityGroup {
+ g := make([]SecurityGroup, len(names))
+ for i, name := range names {
+ g[i] = SecurityGroup{Name: name}
+ }
+ return g
+}
+
+// SecurityGroupNames is a convenience function that
+// returns a slice of security groups with the given ids.
+func SecurityGroupIds(ids ...string) []SecurityGroup {
+ g := make([]SecurityGroup, len(ids))
+ for i, id := range ids {
+ g[i] = SecurityGroup{Id: id}
+ }
+ return g
+}
+
+// SecurityGroups returns details about security groups in EC2. Both parameters
+// are optional, and if provided will limit the security groups returned to those
+// matching the given groups or filtering rules.
+//
+// See http://goo.gl/k12Uy for more details.
+func (ec2 *EC2) SecurityGroups(groups []SecurityGroup, filter *Filter) (resp *SecurityGroupsResp, err error) {
+ params := makeParams("DescribeSecurityGroups")
+ i, j := 1, 1
+ for _, g := range groups {
+ if g.Id != "" {
+ params["GroupId."+strconv.Itoa(i)] = g.Id
+ i++
+ } else {
+ params["GroupName."+strconv.Itoa(j)] = g.Name
+ j++
+ }
+ }
+ filter.addParams(params)
+
+ resp = &SecurityGroupsResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// DeleteSecurityGroup removes the given security group in EC2.
+//
+// See http://goo.gl/QJJDO for more details.
+func (ec2 *EC2) DeleteSecurityGroup(group SecurityGroup) (resp *SimpleResp, err error) {
+ params := makeParams("DeleteSecurityGroup")
+ if group.Id != "" {
+ params["GroupId"] = group.Id
+ } else {
+ params["GroupName"] = group.Name
+ }
+
+ resp = &SimpleResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// AuthorizeSecurityGroup creates an allowance for clients matching the provided
+// rules to access instances within the given security group.
+//
+// See http://goo.gl/u2sDJ for more details.
+func (ec2 *EC2) AuthorizeSecurityGroup(group SecurityGroup, perms []IPPerm) (resp *SimpleResp, err error) {
+ return ec2.authOrRevoke("AuthorizeSecurityGroupIngress", group, perms)
+}
+
+// RevokeSecurityGroup revokes permissions from a group.
+//
+// See http://goo.gl/ZgdxA for more details.
+func (ec2 *EC2) RevokeSecurityGroup(group SecurityGroup, perms []IPPerm) (resp *SimpleResp, err error) {
+ return ec2.authOrRevoke("RevokeSecurityGroupIngress", group, perms)
+}
+
+func (ec2 *EC2) authOrRevoke(op string, group SecurityGroup, perms []IPPerm) (resp *SimpleResp, err error) {
+ params := makeParams(op)
+ if group.Id != "" {
+ params["GroupId"] = group.Id
+ } else {
+ params["GroupName"] = group.Name
+ }
+
+ for i, perm := range perms {
+ prefix := "IpPermissions." + strconv.Itoa(i+1)
+ params[prefix+".IpProtocol"] = perm.Protocol
+ params[prefix+".FromPort"] = strconv.Itoa(perm.FromPort)
+ params[prefix+".ToPort"] = strconv.Itoa(perm.ToPort)
+ for j, ip := range perm.SourceIPs {
+ params[prefix+".IpRanges."+strconv.Itoa(j+1)+".CidrIp"] = ip
+ }
+ for j, g := range perm.SourceGroups {
+ subprefix := prefix + ".Groups." + strconv.Itoa(j+1)
+ if g.OwnerId != "" {
+ params[subprefix+".UserId"] = g.OwnerId
+ }
+ if g.Id != "" {
+ params[subprefix+".GroupId"] = g.Id
+ } else {
+ params[subprefix+".GroupName"] = g.Name
+ }
+ }
+ }
+
+ resp = &SimpleResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// Tag represents key-value metadata used to classify and organize
+// EC2 instances.
+//
+// See http://goo.gl/bncl3 for more details
+type Tag struct {
+ Key string `xml:"key"`
+ Value string `xml:"value"`
+}
+
+// CreateTags adds or overwrites one or more tags for the specified instance ids.
+//
+// See http://goo.gl/Vmkqc for more details
+func (ec2 *EC2) CreateTags(instIds []string, tags []Tag) (resp *SimpleResp, err error) {
+ params := makeParams("CreateTags")
+ addParamsList(params, "ResourceId", instIds)
+
+ for j, tag := range tags {
+ params["Tag."+strconv.Itoa(j+1)+".Key"] = tag.Key
+ params["Tag."+strconv.Itoa(j+1)+".Value"] = tag.Value
+ }
+
+ resp = &SimpleResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// Response to a StartInstances request.
+//
+// See http://goo.gl/awKeF for more details.
+type StartInstanceResp struct {
+ RequestId string `xml:"requestId"`
+ StateChanges []InstanceStateChange `xml:"instancesSet>item"`
+}
+
+// Response to a StopInstances request.
+//
+// See http://goo.gl/436dJ for more details.
+type StopInstanceResp struct {
+ RequestId string `xml:"requestId"`
+ StateChanges []InstanceStateChange `xml:"instancesSet>item"`
+}
+
+// StartInstances starts an Amazon EBS-backed AMI that you've previously stopped.
+//
+// See http://goo.gl/awKeF for more details.
+func (ec2 *EC2) StartInstances(ids ...string) (resp *StartInstanceResp, err error) {
+ params := makeParams("StartInstances")
+ addParamsList(params, "InstanceId", ids)
+ resp = &StartInstanceResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// StopInstances requests stopping one or more Amazon EBS-backed instances.
+//
+// See http://goo.gl/436dJ for more details.
+func (ec2 *EC2) StopInstances(ids ...string) (resp *StopInstanceResp, err error) {
+ params := makeParams("StopInstances")
+ addParamsList(params, "InstanceId", ids)
+ resp = &StopInstanceResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// RebootInstance requests a reboot of one or more instances. This operation is asynchronous;
+// it only queues a request to reboot the specified instance(s). The operation will succeed
+// if the instances are valid and belong to you.
+//
+// Requests to reboot terminated instances are ignored.
+//
+// See http://goo.gl/baoUf for more details.
+func (ec2 *EC2) RebootInstances(ids ...string) (resp *SimpleResp, err error) {
+ params := makeParams("RebootInstances")
+ addParamsList(params, "InstanceId", ids)
+ resp = &SimpleResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// ----------------------------------------------------------------------------
+// Availability zone management functions and types.
+// See http://goo.gl/ylxT4R for more details.
+
+// AvailabilityZonesResp represents a response to a DescribeAvailabilityZones
+// request in EC2.
+type AvailabilityZonesResp struct {
+ RequestId string `xml:"requestId"`
+ Zones []AvailabilityZoneInfo `xml:"availabilityZoneInfo>item"`
+}
+
+// AvailabilityZoneInfo encapsulates details for an availability zone in EC2.
+type AvailabilityZoneInfo struct {
+ AvailabilityZone
+ State string `xml:"zoneState"`
+ MessageSet []string `xml:"messageSet>item"`
+}
+
+// AvailabilityZone represents an EC2 availability zone.
+type AvailabilityZone struct {
+ Name string `xml:"zoneName"`
+ Region string `xml:"regionName"`
+}
+
+// AvailabilityZones returns details about availability zones in EC2.
+// The filter parameter is optional, and if provided will limit the
+// availability zones returned to those matching the given filtering
+// rules.
+//
+// See http://goo.gl/ylxT4R for more details.
+func (ec2 *EC2) AvailabilityZones(filter *Filter) (resp *AvailabilityZonesResp, err error) {
+ params := makeParams("DescribeAvailabilityZones")
+ filter.addParams(params)
+ resp = &AvailabilityZonesResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return
+}
+
+// AccountAttribute holds information about an account attribute.
+//
+// See http://goo.gl/hBc28j for more details.
+type AccountAttribute struct {
+ Name string `xml:"attributeName"`
+ Values []string `xml:"attributeValueSet>item>attributeValue"`
+}
+
+// AccountAttributesResp is the response to an AccountAttributes request.
+//
+// See http://goo.gl/hBc28j for more details.
+type AccountAttributesResp struct {
+ RequestId string `xml:"requestId"`
+ Attributes []AccountAttribute `xml:"accountAttributeSet>item"`
+}
+
+// AccountAttributes returns the values of one or more account
+// attributes.
+//
+// See http://goo.gl/hBc28j for more details.
+func (ec2 *EC2) AccountAttributes(attrNames ...string) (*AccountAttributesResp, error) {
+ params := makeParamsVPC("DescribeAccountAttributes")
+ for i, attrName := range attrNames {
+ params["AttributeName."+strconv.Itoa(i+1)] = attrName
+ }
+
+ resp := &AccountAttributesResp{}
+ if err := ec2.query(params, resp); err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
diff --git a/vendor/src/launchpad.net/goamz/ec2/ec2_test.go b/vendor/src/launchpad.net/goamz/ec2/ec2_test.go
new file mode 100644
index 0000000000..8ce5078625
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/ec2/ec2_test.go
@@ -0,0 +1,900 @@
+package ec2_test
+
+import (
+ "testing"
+
+ "launchpad.net/goamz/aws"
+ "launchpad.net/goamz/ec2"
+ "launchpad.net/goamz/testutil"
+ . "launchpad.net/gocheck"
+)
+
+func Test(t *testing.T) {
+ TestingT(t)
+}
+
+var _ = Suite(&S{})
+
+type S struct {
+ ec2 *ec2.EC2
+}
+
+var testServer = testutil.NewHTTPServer()
+
+func (s *S) SetUpSuite(c *C) {
+ testServer.Start()
+ auth := aws.Auth{"abc", "123"}
+ s.ec2 = ec2.New(auth, aws.Region{EC2Endpoint: testServer.URL})
+}
+
+func (s *S) TearDownSuite(c *C) {
+ testServer.Stop()
+}
+
+func (s *S) TearDownTest(c *C) {
+ testServer.Flush()
+}
+
+func (s *S) TestRunInstancesErrorDump(c *C) {
+ testServer.Response(400, nil, ErrorDump)
+
+ options := ec2.RunInstances{
+ ImageId: "ami-a6f504cf", // Ubuntu Maverick, i386, instance store
+ InstanceType: "t1.micro", // Doesn't work with micro, results in 400.
+ }
+
+ msg := `AMIs with an instance-store root device are not supported for the instance type 't1\.micro'\.`
+
+ resp, err := s.ec2.RunInstances(&options)
+
+ testServer.WaitRequest()
+
+ c.Assert(resp, IsNil)
+ c.Assert(err, ErrorMatches, msg+` \(UnsupportedOperation\)`)
+
+ 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, msg)
+ c.Assert(ec2err.RequestId, Equals, "0503f4e9-bbd6-483c-b54f-c4ae9f3b30f4")
+}
+
+func (s *S) TestRunInstancesErrorWithoutXML(c *C) {
+ testServer.Response(500, nil, "")
+ options := ec2.RunInstances{ImageId: "image-id"}
+
+ resp, err := s.ec2.RunInstances(&options)
+
+ testServer.WaitRequest()
+
+ c.Assert(resp, IsNil)
+ c.Assert(err, ErrorMatches, "500 Internal Server Error")
+
+ ec2err, ok := err.(*ec2.Error)
+ c.Assert(ok, Equals, true)
+ c.Assert(ec2err.StatusCode, Equals, 500)
+ c.Assert(ec2err.Code, Equals, "")
+ c.Assert(ec2err.Message, Equals, "500 Internal Server Error")
+ c.Assert(ec2err.RequestId, Equals, "")
+}
+
+func (s *S) TestRunInstancesExample(c *C) {
+ testServer.Response(200, nil, RunInstancesExample)
+
+ options := ec2.RunInstances{
+ KeyName: "my-keys",
+ ImageId: "image-id",
+ InstanceType: "inst-type",
+ SecurityGroups: []ec2.SecurityGroup{{Name: "g1"}, {Id: "g2"}, {Name: "g3"}, {Id: "g4"}},
+ UserData: []byte("1234"),
+ KernelId: "kernel-id",
+ RamdiskId: "ramdisk-id",
+ AvailZone: "zone",
+ PlacementGroupName: "group",
+ Monitoring: true,
+ SubnetId: "subnet-id",
+ DisableAPITermination: true,
+ ShutdownBehavior: "terminate",
+ PrivateIPAddress: "10.0.0.25",
+ BlockDeviceMappings: []ec2.BlockDeviceMapping{{
+ DeviceName: "device-name",
+ VirtualName: "virtual-name",
+ SnapshotId: "snapshot-id",
+ VolumeType: "volume-type",
+ VolumeSize: 10,
+ DeleteOnTermination: true,
+ IOPS: 1000,
+ }},
+ NetworkInterfaces: []ec2.RunNetworkInterface{{
+ DeviceIndex: 0,
+ SubnetId: "subnet-id",
+ Description: "eth0",
+ PrivateIPs: []ec2.PrivateIP{
+ {Address: "10.0.0.25", IsPrimary: true},
+ },
+ DeleteOnTermination: true,
+ SecurityGroupIds: []string{"sg-1", "sg-2"},
+ SecondaryPrivateIPCount: 2,
+ }, {
+ Id: "eni-id",
+ DeviceIndex: 1,
+ PrivateIPs: []ec2.PrivateIP{{
+ Address: "10.0.1.10",
+ IsPrimary: true,
+ }, {
+ Address: "10.0.1.20",
+ IsPrimary: false,
+ }},
+ }},
+ }
+ params := ec2.PrepareRunParams(options)
+ c.Assert(params, DeepEquals, map[string]string{
+ "Version": "2013-10-15",
+ "Action": "RunInstances",
+ })
+ resp, err := s.ec2.RunInstances(&options)
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Form["Action"], DeepEquals, []string{"RunInstances"})
+ c.Assert(req.Form["ImageId"], DeepEquals, []string{"image-id"})
+ c.Assert(req.Form["MinCount"], DeepEquals, []string{"1"})
+ c.Assert(req.Form["MaxCount"], DeepEquals, []string{"1"})
+ c.Assert(req.Form["KeyName"], DeepEquals, []string{"my-keys"})
+ c.Assert(req.Form["InstanceType"], DeepEquals, []string{"inst-type"})
+ c.Assert(req.Form["SecurityGroup.1"], DeepEquals, []string{"g1"})
+ c.Assert(req.Form["SecurityGroup.2"], DeepEquals, []string{"g3"})
+ c.Assert(req.Form["SecurityGroupId.1"], DeepEquals, []string{"g2"})
+ c.Assert(req.Form["SecurityGroupId.2"], DeepEquals, []string{"g4"})
+ c.Assert(req.Form["UserData"], DeepEquals, []string{"MTIzNA=="})
+ c.Assert(req.Form["KernelId"], DeepEquals, []string{"kernel-id"})
+ c.Assert(req.Form["RamdiskId"], DeepEquals, []string{"ramdisk-id"})
+ c.Assert(req.Form["Placement.AvailabilityZone"], DeepEquals, []string{"zone"})
+ c.Assert(req.Form["Placement.GroupName"], DeepEquals, []string{"group"})
+ c.Assert(req.Form["Monitoring.Enabled"], DeepEquals, []string{"true"})
+ c.Assert(req.Form["SubnetId"], DeepEquals, []string{"subnet-id"})
+ c.Assert(req.Form["DisableApiTermination"], DeepEquals, []string{"true"})
+ c.Assert(req.Form["InstanceInitiatedShutdownBehavior"], DeepEquals, []string{"terminate"})
+ c.Assert(req.Form["PrivateIpAddress"], DeepEquals, []string{"10.0.0.25"})
+ c.Assert(req.Form["BlockDeviceMapping.1.DeviceName"], DeepEquals, []string{"device-name"})
+ c.Assert(req.Form["BlockDeviceMapping.1.VirtualName"], DeepEquals, []string{"virtual-name"})
+ c.Assert(req.Form["BlockDeviceMapping.1.Ebs.SnapshotId"], DeepEquals, []string{"snapshot-id"})
+ c.Assert(req.Form["BlockDeviceMapping.1.Ebs.VolumeType"], DeepEquals, []string{"volume-type"})
+ c.Assert(req.Form["BlockDeviceMapping.1.Ebs.VolumeSize"], DeepEquals, []string{"10"})
+ c.Assert(req.Form["BlockDeviceMapping.1.Ebs.Iops"], DeepEquals, []string{"1000"})
+ c.Assert(req.Form["BlockDeviceMapping.1.Ebs.DeleteOnTermination"], DeepEquals, []string{"true"})
+ c.Assert(req.Form["NetworkInterface.0.DeviceIndex"], DeepEquals, []string{"0"})
+ c.Assert(req.Form["NetworkInterface.0.SubnetId"], DeepEquals, []string{"subnet-id"})
+ c.Assert(req.Form["NetworkInterface.0.Description"], DeepEquals, []string{"eth0"})
+ c.Assert(req.Form["NetworkInterface.0.SecurityGroupId.1"], DeepEquals, []string{"sg-1"})
+ c.Assert(req.Form["NetworkInterface.0.SecurityGroupId.2"], DeepEquals, []string{"sg-2"})
+ c.Assert(req.Form["NetworkInterface.0.DeleteOnTermination"], DeepEquals, []string{"true"})
+ c.Assert(req.Form["NetworkInterface.0.SecondaryPrivateIpAddressCount"], DeepEquals, []string{"2"})
+ c.Assert(req.Form["NetworkInterface.1.NetworkInterfaceId"], DeepEquals, []string{"eni-id"})
+ c.Assert(req.Form["NetworkInterface.1.DeviceIndex"], DeepEquals, []string{"1"})
+ c.Assert(req.Form["NetworkInterface.1.PrivateIpAddresses.0.PrivateIpAddress"], DeepEquals, []string{"10.0.1.10"})
+ c.Assert(req.Form["NetworkInterface.1.PrivateIpAddresses.0.Primary"], DeepEquals, []string{"true"})
+ c.Assert(req.Form["NetworkInterface.1.PrivateIpAddresses.1.PrivateIpAddress"], DeepEquals, []string{"10.0.1.20"})
+ c.Assert(req.Form["NetworkInterface.1.PrivateIpAddresses.1.Primary"], DeepEquals, []string{"false"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE")
+ c.Assert(resp.ReservationId, Equals, "r-47a5402e")
+ c.Assert(resp.OwnerId, Equals, "999988887777")
+ c.Assert(resp.SecurityGroups, DeepEquals, []ec2.SecurityGroup{{Name: "default", Id: "sg-67ad940e"}})
+ c.Assert(resp.Instances, HasLen, 3)
+
+ i0 := resp.Instances[0]
+ c.Assert(i0.InstanceId, Equals, "i-2ba64342")
+ c.Assert(i0.InstanceType, Equals, "m1.small")
+ c.Assert(i0.ImageId, Equals, "ami-60a54009")
+ c.Assert(i0.Monitoring, Equals, "enabled")
+ c.Assert(i0.KeyName, Equals, "example-key-name")
+ c.Assert(i0.AMILaunchIndex, Equals, 0)
+ c.Assert(i0.VirtType, Equals, "paravirtual")
+ c.Assert(i0.Hypervisor, Equals, "xen")
+ c.Assert(i0.SubnetId, Equals, "subnet-id")
+ c.Assert(i0.VPCId, Equals, "vpc-id")
+ c.Assert(i0.NetworkInterfaces, HasLen, 2)
+ c.Assert(i0.NetworkInterfaces, DeepEquals, []ec2.NetworkInterface{{
+ Id: "eni-c6bb50ae",
+ SubnetId: "subnet-id",
+ VPCId: "vpc-id",
+ Description: "eth0",
+ SourceDestCheck: true,
+ OwnerId: "111122223333",
+ Status: "in-use",
+ Groups: []ec2.SecurityGroup{
+ {Name: "vpc sg-1", Id: "sg-1"},
+ {Name: "vpc sg-2", Id: "sg-2"},
+ },
+ MACAddress: "11:22:33:44:55:66",
+ PrivateIPAddress: "10.0.0.25",
+ PrivateIPs: []ec2.PrivateIP{{Address: "10.0.0.25", IsPrimary: true}},
+ Attachment: ec2.NetworkInterfaceAttachment{
+ Id: "eni-attach-0326646a",
+ DeviceIndex: 0,
+ Status: "attaching",
+ AttachTime: "2011-12-20T08:29:31.000Z",
+ DeleteOnTermination: true,
+ },
+ }, {
+ Id: "eni-id",
+ SubnetId: "subnet-id",
+ VPCId: "vpc-id",
+ SourceDestCheck: true,
+ OwnerId: "111122223333",
+ Status: "in-use",
+ Groups: []ec2.SecurityGroup{{Name: "vpc default", Id: "sg-id"}},
+ MACAddress: "11:22:33:44:55:66",
+ PrivateIPAddress: "10.0.1.10",
+ PrivateIPs: []ec2.PrivateIP{
+ {Address: "10.0.1.10", IsPrimary: true},
+ {Address: "10.0.1.20", IsPrimary: false},
+ },
+ Attachment: ec2.NetworkInterfaceAttachment{
+ Id: "eni-attach-id",
+ DeviceIndex: 1,
+ Status: "attaching",
+ AttachTime: "2011-12-20T08:29:31.000Z",
+ DeleteOnTermination: false,
+ },
+ }})
+
+ i1 := resp.Instances[1]
+ c.Assert(i1.InstanceId, Equals, "i-2bc64242")
+ c.Assert(i1.InstanceType, Equals, "m1.small")
+ c.Assert(i1.ImageId, Equals, "ami-60a54009")
+ c.Assert(i1.Monitoring, Equals, "enabled")
+ c.Assert(i1.KeyName, Equals, "example-key-name")
+ c.Assert(i1.AMILaunchIndex, Equals, 1)
+ c.Assert(i1.VirtType, Equals, "paravirtual")
+ c.Assert(i1.Hypervisor, Equals, "xen")
+
+ i2 := resp.Instances[2]
+ c.Assert(i2.InstanceId, Equals, "i-2be64332")
+ c.Assert(i2.InstanceType, Equals, "m1.small")
+ c.Assert(i2.ImageId, Equals, "ami-60a54009")
+ c.Assert(i2.Monitoring, Equals, "enabled")
+ c.Assert(i2.KeyName, Equals, "example-key-name")
+ c.Assert(i2.AMILaunchIndex, Equals, 2)
+ c.Assert(i2.VirtType, Equals, "paravirtual")
+ c.Assert(i2.Hypervisor, Equals, "xen")
+}
+
+func (s *S) TestTerminateInstancesExample(c *C) {
+ testServer.Response(200, nil, TerminateInstancesExample)
+
+ resp, err := s.ec2.TerminateInstances([]string{"i-1", "i-2"})
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Form["Action"], DeepEquals, []string{"TerminateInstances"})
+ c.Assert(req.Form["InstanceId.1"], DeepEquals, []string{"i-1"})
+ c.Assert(req.Form["InstanceId.2"], DeepEquals, []string{"i-2"})
+ c.Assert(req.Form["UserData"], IsNil)
+ c.Assert(req.Form["KernelId"], IsNil)
+ c.Assert(req.Form["RamdiskId"], IsNil)
+ c.Assert(req.Form["Placement.AvailabilityZone"], IsNil)
+ c.Assert(req.Form["Placement.GroupName"], IsNil)
+ c.Assert(req.Form["Monitoring.Enabled"], IsNil)
+ c.Assert(req.Form["SubnetId"], IsNil)
+ c.Assert(req.Form["DisableApiTermination"], IsNil)
+ c.Assert(req.Form["InstanceInitiatedShutdownBehavior"], IsNil)
+ c.Assert(req.Form["PrivateIpAddress"], IsNil)
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE")
+ c.Assert(resp.StateChanges, HasLen, 1)
+ c.Assert(resp.StateChanges[0].InstanceId, Equals, "i-3ea74257")
+ c.Assert(resp.StateChanges[0].CurrentState.Code, Equals, 32)
+ c.Assert(resp.StateChanges[0].CurrentState.Name, Equals, "shutting-down")
+ c.Assert(resp.StateChanges[0].PreviousState.Code, Equals, 16)
+ c.Assert(resp.StateChanges[0].PreviousState.Name, Equals, "running")
+}
+
+func (s *S) TestDescribeInstancesExample1(c *C) {
+ testServer.Response(200, nil, DescribeInstancesExample1)
+
+ filter := ec2.NewFilter()
+ filter.Add("key1", "value1")
+ filter.Add("key2", "value2", "value3")
+
+ resp, err := s.ec2.Instances([]string{"i-1", "i-2"}, nil)
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeInstances"})
+ c.Assert(req.Form["InstanceId.1"], DeepEquals, []string{"i-1"})
+ c.Assert(req.Form["InstanceId.2"], DeepEquals, []string{"i-2"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "98e3c9a4-848c-4d6d-8e8a-b1bdEXAMPLE")
+ c.Assert(resp.Reservations, HasLen, 2)
+
+ expectedGroups := []ec2.SecurityGroup{{Name: "default", Id: "sg-67ad940e"}}
+ r0 := resp.Reservations[0]
+ c.Assert(r0.ReservationId, Equals, "r-b27e30d9")
+ c.Assert(r0.OwnerId, Equals, "999988887777")
+ c.Assert(r0.RequesterId, Equals, "854251627541")
+ c.Assert(r0.SecurityGroups, DeepEquals, expectedGroups)
+ c.Assert(r0.Instances, HasLen, 1)
+
+ r0i := r0.Instances[0]
+ c.Assert(r0i.InstanceId, Equals, "i-c5cd56af")
+ c.Assert(r0i.PrivateDNSName, Equals, "domU-12-31-39-10-56-34.compute-1.internal")
+ c.Assert(r0i.DNSName, Equals, "ec2-174-129-165-232.compute-1.amazonaws.com")
+ c.Assert(r0i.PrivateIPAddress, Equals, "10.198.85.190")
+ c.Assert(r0i.IPAddress, Equals, "174.129.165.232")
+ c.Assert(r0i.AvailZone, Equals, "us-east-1b")
+ c.Assert(r0i.SecurityGroups, DeepEquals, expectedGroups)
+}
+
+func (s *S) TestDescribeInstancesExample2(c *C) {
+ testServer.Response(200, nil, DescribeInstancesExample2)
+
+ filter := ec2.NewFilter()
+ filter.Add("key1", "value1")
+ filter.Add("key2", "value2", "value3")
+
+ resp, err := s.ec2.Instances([]string{"i-1", "i-2"}, filter)
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeInstances"})
+ c.Assert(req.Form["InstanceId.1"], DeepEquals, []string{"i-1"})
+ c.Assert(req.Form["InstanceId.2"], DeepEquals, []string{"i-2"})
+ c.Assert(req.Form["Filter.1.Name"], DeepEquals, []string{"key1"})
+ c.Assert(req.Form["Filter.1.Value.1"], DeepEquals, []string{"value1"})
+ c.Assert(req.Form["Filter.1.Value.2"], IsNil)
+ c.Assert(req.Form["Filter.2.Name"], DeepEquals, []string{"key2"})
+ c.Assert(req.Form["Filter.2.Value.1"], DeepEquals, []string{"value2"})
+ c.Assert(req.Form["Filter.2.Value.2"], DeepEquals, []string{"value3"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE")
+ c.Assert(resp.Reservations, HasLen, 1)
+
+ r0 := resp.Reservations[0]
+ r0i := r0.Instances[0]
+ c.Assert(r0i.State.Code, Equals, 16)
+ c.Assert(r0i.State.Name, Equals, "running")
+
+ r0t0 := r0i.Tags[0]
+ r0t1 := r0i.Tags[1]
+ c.Assert(r0t0.Key, Equals, "webserver")
+ c.Assert(r0t0.Value, Equals, "")
+ c.Assert(r0t1.Key, Equals, "stack")
+ c.Assert(r0t1.Value, Equals, "Production")
+}
+
+func (s *S) TestDescribeImagesExample(c *C) {
+ testServer.Response(200, nil, DescribeImagesExample)
+
+ filter := ec2.NewFilter()
+ filter.Add("key1", "value1")
+ filter.Add("key2", "value2", "value3")
+
+ resp, err := s.ec2.Images([]string{"ami-1", "ami-2"}, filter)
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeImages"})
+ c.Assert(req.Form["ImageId.1"], DeepEquals, []string{"ami-1"})
+ c.Assert(req.Form["ImageId.2"], DeepEquals, []string{"ami-2"})
+ c.Assert(req.Form["Filter.1.Name"], DeepEquals, []string{"key1"})
+ c.Assert(req.Form["Filter.1.Value.1"], DeepEquals, []string{"value1"})
+ c.Assert(req.Form["Filter.1.Value.2"], IsNil)
+ c.Assert(req.Form["Filter.2.Name"], DeepEquals, []string{"key2"})
+ c.Assert(req.Form["Filter.2.Value.1"], DeepEquals, []string{"value2"})
+ c.Assert(req.Form["Filter.2.Value.2"], DeepEquals, []string{"value3"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "4a4a27a2-2e7c-475d-b35b-ca822EXAMPLE")
+ c.Assert(resp.Images, HasLen, 1)
+
+ i0 := resp.Images[0]
+ c.Assert(i0.Id, Equals, "ami-a2469acf")
+ c.Assert(i0.Type, Equals, "machine")
+ c.Assert(i0.Name, Equals, "example-marketplace-amzn-ami.1")
+ c.Assert(i0.Description, Equals, "Amazon Linux AMI i386 EBS")
+ c.Assert(i0.Location, Equals, "aws-marketplace/example-marketplace-amzn-ami.1")
+ c.Assert(i0.State, Equals, "available")
+ c.Assert(i0.Public, Equals, true)
+ c.Assert(i0.OwnerId, Equals, "123456789999")
+ c.Assert(i0.OwnerAlias, Equals, "aws-marketplace")
+ c.Assert(i0.Architecture, Equals, "i386")
+ c.Assert(i0.KernelId, Equals, "aki-805ea7e9")
+ c.Assert(i0.RootDeviceType, Equals, "ebs")
+ c.Assert(i0.RootDeviceName, Equals, "/dev/sda1")
+ c.Assert(i0.VirtualizationType, Equals, "paravirtual")
+ c.Assert(i0.Hypervisor, Equals, "xen")
+
+ c.Assert(i0.BlockDevices, HasLen, 1)
+ c.Assert(i0.BlockDevices[0].DeviceName, Equals, "/dev/sda1")
+ c.Assert(i0.BlockDevices[0].SnapshotId, Equals, "snap-787e9403")
+ c.Assert(i0.BlockDevices[0].VolumeSize, Equals, int64(8))
+ c.Assert(i0.BlockDevices[0].DeleteOnTermination, Equals, true)
+}
+
+func (s *S) TestCreateSnapshotExample(c *C) {
+ testServer.Response(200, nil, CreateSnapshotExample)
+
+ resp, err := s.ec2.CreateSnapshot("vol-4d826724", "Daily Backup")
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Form["Action"], DeepEquals, []string{"CreateSnapshot"})
+ c.Assert(req.Form["VolumeId"], DeepEquals, []string{"vol-4d826724"})
+ c.Assert(req.Form["Description"], DeepEquals, []string{"Daily Backup"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE")
+ c.Assert(resp.Snapshot.Id, Equals, "snap-78a54011")
+ c.Assert(resp.Snapshot.VolumeId, Equals, "vol-4d826724")
+ c.Assert(resp.Snapshot.Status, Equals, "pending")
+ c.Assert(resp.Snapshot.StartTime, Equals, "2008-05-07T12:51:50.000Z")
+ c.Assert(resp.Snapshot.Progress, Equals, "60%")
+ c.Assert(resp.Snapshot.OwnerId, Equals, "111122223333")
+ c.Assert(resp.Snapshot.VolumeSize, Equals, "10")
+ c.Assert(resp.Snapshot.Description, Equals, "Daily Backup")
+}
+
+func (s *S) TestDeleteSnapshotsExample(c *C) {
+ testServer.Response(200, nil, DeleteSnapshotExample)
+
+ resp, err := s.ec2.DeleteSnapshots([]string{"snap-78a54011"})
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Form["Action"], DeepEquals, []string{"DeleteSnapshot"})
+ c.Assert(req.Form["SnapshotId.1"], DeepEquals, []string{"snap-78a54011"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE")
+}
+
+func (s *S) TestDescribeSnapshotsExample(c *C) {
+ testServer.Response(200, nil, DescribeSnapshotsExample)
+
+ filter := ec2.NewFilter()
+ filter.Add("key1", "value1")
+ filter.Add("key2", "value2", "value3")
+
+ resp, err := s.ec2.Snapshots([]string{"snap-1", "snap-2"}, filter)
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeSnapshots"})
+ c.Assert(req.Form["SnapshotId.1"], DeepEquals, []string{"snap-1"})
+ c.Assert(req.Form["SnapshotId.2"], DeepEquals, []string{"snap-2"})
+ c.Assert(req.Form["Filter.1.Name"], DeepEquals, []string{"key1"})
+ c.Assert(req.Form["Filter.1.Value.1"], DeepEquals, []string{"value1"})
+ c.Assert(req.Form["Filter.1.Value.2"], IsNil)
+ c.Assert(req.Form["Filter.2.Name"], DeepEquals, []string{"key2"})
+ c.Assert(req.Form["Filter.2.Value.1"], DeepEquals, []string{"value2"})
+ c.Assert(req.Form["Filter.2.Value.2"], DeepEquals, []string{"value3"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE")
+ c.Assert(resp.Snapshots, HasLen, 1)
+
+ s0 := resp.Snapshots[0]
+ c.Assert(s0.Id, Equals, "snap-1a2b3c4d")
+ c.Assert(s0.VolumeId, Equals, "vol-8875daef")
+ c.Assert(s0.VolumeSize, Equals, "15")
+ c.Assert(s0.Status, Equals, "pending")
+ c.Assert(s0.StartTime, Equals, "2010-07-29T04:12:01.000Z")
+ c.Assert(s0.Progress, Equals, "30%")
+ c.Assert(s0.OwnerId, Equals, "111122223333")
+ c.Assert(s0.Description, Equals, "Daily Backup")
+
+ c.Assert(s0.Tags, HasLen, 1)
+ c.Assert(s0.Tags[0].Key, Equals, "Purpose")
+ c.Assert(s0.Tags[0].Value, Equals, "demo_db_14_backup")
+}
+
+func (s *S) checkCreateSGResponse(c *C, resp *ec2.CreateSecurityGroupResp, id, name, description, vpcId string) {
+ req := testServer.WaitRequest()
+ c.Assert(req.Form["Action"], DeepEquals, []string{"CreateSecurityGroup"})
+ c.Assert(req.Form["GroupName"], DeepEquals, []string{name})
+ c.Assert(req.Form["GroupDescription"], DeepEquals, []string{description})
+ if vpcId != "" {
+ c.Assert(req.Form["VpcId"], DeepEquals, []string{vpcId})
+ }
+
+ c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE")
+ c.Assert(resp.Name, Equals, name)
+ c.Assert(resp.Id, Equals, id)
+}
+
+func (s *S) TestCreateSecurityGroupExample(c *C) {
+ testServer.Response(200, nil, CreateSecurityGroupExample)
+ resp, err := s.ec2.CreateSecurityGroup("websrv", "Web Servers")
+ c.Assert(err, IsNil)
+ s.checkCreateSGResponse(c, resp, "sg-67ad940e", "websrv", "Web Servers", "")
+
+ testServer.Response(200, nil, CreateSecurityGroupExample)
+ resp, err = s.ec2.CreateSecurityGroupVPC("vpc-id", "websrv", "Web Servers")
+ c.Assert(err, IsNil)
+ s.checkCreateSGResponse(c, resp, "sg-67ad940e", "websrv", "Web Servers", "vpc-id")
+}
+
+func (s *S) TestDescribeSecurityGroupsExample(c *C) {
+ testServer.Response(200, nil, DescribeSecurityGroupsExample)
+
+ resp, err := s.ec2.SecurityGroups([]ec2.SecurityGroup{{Name: "WebServers"}, {Name: "RangedPortsBySource"}}, nil)
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeSecurityGroups"})
+ c.Assert(req.Form["GroupName.1"], DeepEquals, []string{"WebServers"})
+ c.Assert(req.Form["GroupName.2"], DeepEquals, []string{"RangedPortsBySource"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE")
+ c.Assert(resp.Groups, HasLen, 2)
+
+ g0 := resp.Groups[0]
+ c.Assert(g0.OwnerId, Equals, "999988887777")
+ c.Assert(g0.Name, Equals, "WebServers")
+ c.Assert(g0.Id, Equals, "sg-67ad940e")
+ c.Assert(g0.Description, Equals, "Web Servers")
+ c.Assert(g0.IPPerms, HasLen, 1)
+
+ g0ipp := g0.IPPerms[0]
+ c.Assert(g0ipp.Protocol, Equals, "tcp")
+ c.Assert(g0ipp.FromPort, Equals, 80)
+ c.Assert(g0ipp.ToPort, Equals, 80)
+ c.Assert(g0ipp.SourceIPs, DeepEquals, []string{"0.0.0.0/0"})
+
+ g1 := resp.Groups[1]
+ c.Assert(g1.OwnerId, Equals, "999988887777")
+ c.Assert(g1.Name, Equals, "RangedPortsBySource")
+ c.Assert(g1.Id, Equals, "sg-76abc467")
+ c.Assert(g1.Description, Equals, "Group A")
+ c.Assert(g1.IPPerms, HasLen, 1)
+
+ g1ipp := g1.IPPerms[0]
+ c.Assert(g1ipp.Protocol, Equals, "tcp")
+ c.Assert(g1ipp.FromPort, Equals, 6000)
+ c.Assert(g1ipp.ToPort, Equals, 7000)
+ c.Assert(g1ipp.SourceIPs, IsNil)
+}
+
+func (s *S) TestDescribeSecurityGroupsExampleWithFilter(c *C) {
+ testServer.Response(200, nil, DescribeSecurityGroupsExample)
+
+ filter := ec2.NewFilter()
+ filter.Add("ip-permission.protocol", "tcp")
+ filter.Add("ip-permission.from-port", "22")
+ filter.Add("ip-permission.to-port", "22")
+ filter.Add("ip-permission.group-name", "app_server_group", "database_group")
+
+ _, err := s.ec2.SecurityGroups(nil, filter)
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeSecurityGroups"})
+ c.Assert(req.Form["Filter.1.Name"], DeepEquals, []string{"ip-permission.from-port"})
+ c.Assert(req.Form["Filter.1.Value.1"], DeepEquals, []string{"22"})
+ c.Assert(req.Form["Filter.2.Name"], DeepEquals, []string{"ip-permission.group-name"})
+ c.Assert(req.Form["Filter.2.Value.1"], DeepEquals, []string{"app_server_group"})
+ c.Assert(req.Form["Filter.2.Value.2"], DeepEquals, []string{"database_group"})
+ c.Assert(req.Form["Filter.3.Name"], DeepEquals, []string{"ip-permission.protocol"})
+ c.Assert(req.Form["Filter.3.Value.1"], DeepEquals, []string{"tcp"})
+ c.Assert(req.Form["Filter.4.Name"], DeepEquals, []string{"ip-permission.to-port"})
+ c.Assert(req.Form["Filter.4.Value.1"], DeepEquals, []string{"22"})
+
+ c.Assert(err, IsNil)
+}
+
+func (s *S) TestDescribeSecurityGroupsDumpWithGroup(c *C) {
+ testServer.Response(200, nil, DescribeSecurityGroupsDump)
+
+ resp, err := s.ec2.SecurityGroups(nil, nil)
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeSecurityGroups"})
+ c.Assert(err, IsNil)
+ c.Check(resp.Groups, HasLen, 1)
+ c.Check(resp.Groups[0].IPPerms, HasLen, 2)
+
+ ipp0 := resp.Groups[0].IPPerms[0]
+ c.Assert(ipp0.SourceIPs, IsNil)
+ c.Check(ipp0.Protocol, Equals, "icmp")
+ c.Assert(ipp0.SourceGroups, HasLen, 1)
+ c.Check(ipp0.SourceGroups[0].OwnerId, Equals, "12345")
+ c.Check(ipp0.SourceGroups[0].Name, Equals, "default")
+ c.Check(ipp0.SourceGroups[0].Id, Equals, "sg-67ad940e")
+
+ ipp1 := resp.Groups[0].IPPerms[1]
+ c.Check(ipp1.Protocol, Equals, "tcp")
+ c.Assert(ipp0.SourceIPs, IsNil)
+ c.Assert(ipp0.SourceGroups, HasLen, 1)
+ c.Check(ipp1.SourceGroups[0].Id, Equals, "sg-76abc467")
+ c.Check(ipp1.SourceGroups[0].OwnerId, Equals, "12345")
+ c.Check(ipp1.SourceGroups[0].Name, Equals, "other")
+}
+
+func (s *S) TestDeleteSecurityGroupExample(c *C) {
+ testServer.Response(200, nil, DeleteSecurityGroupExample)
+
+ resp, err := s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Name: "websrv"})
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Form["Action"], DeepEquals, []string{"DeleteSecurityGroup"})
+ c.Assert(req.Form["GroupName"], DeepEquals, []string{"websrv"})
+ c.Assert(req.Form["GroupId"], IsNil)
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE")
+}
+
+func (s *S) TestDeleteSecurityGroupExampleWithId(c *C) {
+ testServer.Response(200, nil, DeleteSecurityGroupExample)
+
+ // ignore return and error - we're only want to check the parameter handling.
+ s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Id: "sg-67ad940e", Name: "ignored"})
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Form["GroupName"], IsNil)
+ c.Assert(req.Form["GroupId"], DeepEquals, []string{"sg-67ad940e"})
+}
+
+func (s *S) TestAuthorizeSecurityGroupExample1(c *C) {
+ testServer.Response(200, nil, AuthorizeSecurityGroupIngressExample)
+
+ perms := []ec2.IPPerm{{
+ Protocol: "tcp",
+ FromPort: 80,
+ ToPort: 80,
+ SourceIPs: []string{"205.192.0.0/16", "205.159.0.0/16"},
+ }}
+ resp, err := s.ec2.AuthorizeSecurityGroup(ec2.SecurityGroup{Name: "websrv"}, perms)
+
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Form["Action"], DeepEquals, []string{"AuthorizeSecurityGroupIngress"})
+ c.Assert(req.Form["GroupName"], DeepEquals, []string{"websrv"})
+ c.Assert(req.Form["IpPermissions.1.IpProtocol"], DeepEquals, []string{"tcp"})
+ c.Assert(req.Form["IpPermissions.1.FromPort"], DeepEquals, []string{"80"})
+ c.Assert(req.Form["IpPermissions.1.ToPort"], DeepEquals, []string{"80"})
+ c.Assert(req.Form["IpPermissions.1.IpRanges.1.CidrIp"], DeepEquals, []string{"205.192.0.0/16"})
+ c.Assert(req.Form["IpPermissions.1.IpRanges.2.CidrIp"], DeepEquals, []string{"205.159.0.0/16"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE")
+}
+
+func (s *S) TestAuthorizeSecurityGroupExample1WithId(c *C) {
+ testServer.Response(200, nil, AuthorizeSecurityGroupIngressExample)
+
+ perms := []ec2.IPPerm{{
+ Protocol: "tcp",
+ FromPort: 80,
+ ToPort: 80,
+ SourceIPs: []string{"205.192.0.0/16", "205.159.0.0/16"},
+ }}
+ // ignore return and error - we're only want to check the parameter handling.
+ s.ec2.AuthorizeSecurityGroup(ec2.SecurityGroup{Id: "sg-67ad940e", Name: "ignored"}, perms)
+
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Form["GroupName"], IsNil)
+ c.Assert(req.Form["GroupId"], DeepEquals, []string{"sg-67ad940e"})
+}
+
+func (s *S) TestAuthorizeSecurityGroupExample2(c *C) {
+ testServer.Response(200, nil, AuthorizeSecurityGroupIngressExample)
+
+ perms := []ec2.IPPerm{{
+ Protocol: "tcp",
+ FromPort: 80,
+ ToPort: 81,
+ SourceGroups: []ec2.UserSecurityGroup{
+ {OwnerId: "999988887777", Name: "OtherAccountGroup"},
+ {Id: "sg-67ad940e"},
+ },
+ }}
+ resp, err := s.ec2.AuthorizeSecurityGroup(ec2.SecurityGroup{Name: "websrv"}, perms)
+
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Form["Action"], DeepEquals, []string{"AuthorizeSecurityGroupIngress"})
+ c.Assert(req.Form["GroupName"], DeepEquals, []string{"websrv"})
+ c.Assert(req.Form["IpPermissions.1.IpProtocol"], DeepEquals, []string{"tcp"})
+ c.Assert(req.Form["IpPermissions.1.FromPort"], DeepEquals, []string{"80"})
+ c.Assert(req.Form["IpPermissions.1.ToPort"], DeepEquals, []string{"81"})
+ c.Assert(req.Form["IpPermissions.1.Groups.1.UserId"], DeepEquals, []string{"999988887777"})
+ c.Assert(req.Form["IpPermissions.1.Groups.1.GroupName"], DeepEquals, []string{"OtherAccountGroup"})
+ c.Assert(req.Form["IpPermissions.1.Groups.2.UserId"], IsNil)
+ c.Assert(req.Form["IpPermissions.1.Groups.2.GroupName"], IsNil)
+ c.Assert(req.Form["IpPermissions.1.Groups.2.GroupId"], DeepEquals, []string{"sg-67ad940e"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE")
+}
+
+func (s *S) TestRevokeSecurityGroupExample(c *C) {
+ // RevokeSecurityGroup is implemented by the same code as AuthorizeSecurityGroup
+ // so there's no need to duplicate all the tests.
+ testServer.Response(200, nil, RevokeSecurityGroupIngressExample)
+
+ resp, err := s.ec2.RevokeSecurityGroup(ec2.SecurityGroup{Name: "websrv"}, nil)
+
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Form["Action"], DeepEquals, []string{"RevokeSecurityGroupIngress"})
+ c.Assert(req.Form["GroupName"], DeepEquals, []string{"websrv"})
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE")
+}
+
+func (s *S) TestCreateTags(c *C) {
+ testServer.Response(200, nil, CreateTagsExample)
+
+ resp, err := s.ec2.CreateTags([]string{"ami-1a2b3c4d", "i-7f4d3a2b"}, []ec2.Tag{{"webserver", ""}, {"stack", "Production"}})
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Form["ResourceId.1"], DeepEquals, []string{"ami-1a2b3c4d"})
+ c.Assert(req.Form["ResourceId.2"], DeepEquals, []string{"i-7f4d3a2b"})
+ c.Assert(req.Form["Tag.1.Key"], DeepEquals, []string{"webserver"})
+ c.Assert(req.Form["Tag.1.Value"], DeepEquals, []string{""})
+ c.Assert(req.Form["Tag.2.Key"], DeepEquals, []string{"stack"})
+ c.Assert(req.Form["Tag.2.Value"], DeepEquals, []string{"Production"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE")
+}
+
+func (s *S) TestStartInstances(c *C) {
+ testServer.Response(200, nil, StartInstancesExample)
+
+ resp, err := s.ec2.StartInstances("i-10a64379")
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Form["Action"], DeepEquals, []string{"StartInstances"})
+ c.Assert(req.Form["InstanceId.1"], DeepEquals, []string{"i-10a64379"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE")
+
+ s0 := resp.StateChanges[0]
+ c.Assert(s0.InstanceId, Equals, "i-10a64379")
+ c.Assert(s0.CurrentState.Code, Equals, 0)
+ c.Assert(s0.CurrentState.Name, Equals, "pending")
+ c.Assert(s0.PreviousState.Code, Equals, 80)
+ c.Assert(s0.PreviousState.Name, Equals, "stopped")
+}
+
+func (s *S) TestStopInstances(c *C) {
+ testServer.Response(200, nil, StopInstancesExample)
+
+ resp, err := s.ec2.StopInstances("i-10a64379")
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Form["Action"], DeepEquals, []string{"StopInstances"})
+ c.Assert(req.Form["InstanceId.1"], DeepEquals, []string{"i-10a64379"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE")
+
+ s0 := resp.StateChanges[0]
+ c.Assert(s0.InstanceId, Equals, "i-10a64379")
+ c.Assert(s0.CurrentState.Code, Equals, 64)
+ c.Assert(s0.CurrentState.Name, Equals, "stopping")
+ c.Assert(s0.PreviousState.Code, Equals, 16)
+ c.Assert(s0.PreviousState.Name, Equals, "running")
+}
+
+func (s *S) TestRebootInstances(c *C) {
+ testServer.Response(200, nil, RebootInstancesExample)
+
+ resp, err := s.ec2.RebootInstances("i-10a64379")
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Form["Action"], DeepEquals, []string{"RebootInstances"})
+ c.Assert(req.Form["InstanceId.1"], DeepEquals, []string{"i-10a64379"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE")
+}
+
+func (s *S) TestSignatureWithEndpointPath(c *C) {
+ ec2.FakeTime(true)
+ defer ec2.FakeTime(false)
+
+ testServer.Response(200, nil, RebootInstancesExample)
+
+ // https://bugs.launchpad.net/goamz/+bug/1022749
+ ec2 := ec2.New(s.ec2.Auth, aws.Region{EC2Endpoint: testServer.URL + "/services/Cloud"})
+
+ _, err := ec2.RebootInstances("i-10a64379")
+ c.Assert(err, IsNil)
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Form["Signature"], DeepEquals, []string{"gdG/vEm+c6ehhhfkrJy3+wuVzw/rzKR42TYelMwti7M="})
+}
+
+func (s *S) TestAvailabilityZonesExample1(c *C) {
+ testServer.Response(200, nil, DescribeAvailabilityZonesExample1)
+
+ resp, err := s.ec2.AvailabilityZones(nil)
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeAvailabilityZones"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE")
+ c.Assert(resp.Zones, HasLen, 4)
+
+ z0 := resp.Zones[0]
+ c.Assert(z0.Name, Equals, "us-east-1a")
+ c.Assert(z0.Region, Equals, "us-east-1")
+ c.Assert(z0.State, Equals, "available")
+ c.Assert(z0.MessageSet, HasLen, 0)
+
+ z1 := resp.Zones[1]
+ c.Assert(z1.Name, Equals, "us-east-1b")
+ c.Assert(z1.Region, Equals, "us-east-1")
+ c.Assert(z1.State, Equals, "available")
+ c.Assert(z1.MessageSet, HasLen, 0)
+
+ z2 := resp.Zones[2]
+ c.Assert(z2.Name, Equals, "us-east-1c")
+ c.Assert(z2.Region, Equals, "us-east-1")
+ c.Assert(z2.State, Equals, "available")
+ c.Assert(z2.MessageSet, HasLen, 0)
+
+ z3 := resp.Zones[3]
+ c.Assert(z3.Name, Equals, "us-east-1d")
+ c.Assert(z3.Region, Equals, "us-east-1")
+ c.Assert(z3.State, Equals, "available")
+ c.Assert(z3.MessageSet, HasLen, 0)
+}
+
+func (s *S) TestAvailabilityZonesExample2(c *C) {
+ testServer.Response(200, nil, DescribeAvailabilityZonesExample2)
+
+ resp, err := s.ec2.AvailabilityZones(nil)
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeAvailabilityZones"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE")
+ c.Assert(resp.Zones, HasLen, 2)
+
+ z0 := resp.Zones[0]
+ c.Assert(z0.Name, Equals, "us-east-1a")
+ c.Assert(z0.Region, Equals, "us-east-1")
+ c.Assert(z0.State, Equals, "impaired")
+ c.Assert(z0.MessageSet, HasLen, 0)
+
+ z1 := resp.Zones[1]
+ c.Assert(z1.Name, Equals, "us-east-1b")
+ c.Assert(z1.Region, Equals, "us-east-1")
+ c.Assert(z1.State, Equals, "unavailable")
+ c.Assert(z1.MessageSet, DeepEquals, []string{"us-east-1b is currently down for maintenance."})
+}
+
+func (s *S) TestDescribeAccountAttributesExamples(c *C) {
+ testServer.Response(200, nil, DescribeAccountAttributesExample)
+
+ resp, err := s.ec2.AccountAttributes("supported-platforms")
+ req := testServer.WaitRequest()
+
+ assertAttribute := func(name, value string) {
+ c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeAccountAttributes"})
+ c.Assert(req.Form["AttributeName.1"], DeepEquals, []string{name})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE")
+ c.Assert(resp.Attributes, HasLen, 1)
+ attr := resp.Attributes[0]
+ c.Check(attr.Name, Equals, name)
+ c.Check(attr.Values, DeepEquals, []string{value})
+ }
+ assertAttribute("supported-platforms", "VPC")
+
+ testServer.Response(200, nil, DescribeAccountAttributesExample2)
+ resp, err = s.ec2.AccountAttributes("default-vpc")
+ req = testServer.WaitRequest()
+ assertAttribute("default-vpc", "vpc-xxxxxxxx")
+
+ testServer.Response(200, nil, DescribeAccountAttributesExample3)
+ resp, err = s.ec2.AccountAttributes("default-vpc")
+ req = testServer.WaitRequest()
+ assertAttribute("default-vpc", "none")
+}
diff --git a/vendor/src/launchpad.net/goamz/ec2/ec2i_test.go b/vendor/src/launchpad.net/goamz/ec2/ec2i_test.go
new file mode 100644
index 0000000000..9028a41473
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/ec2/ec2i_test.go
@@ -0,0 +1,204 @@
+package ec2_test
+
+import (
+ "crypto/rand"
+ "fmt"
+
+ "launchpad.net/goamz/aws"
+ "launchpad.net/goamz/ec2"
+ "launchpad.net/goamz/testutil"
+ . "launchpad.net/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.New(s.srv.auth, aws.USEast)
+}
+
+// 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(name, 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(name, 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.New(s.ec2.Auth, r)
+ _, 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")
+ }
+ }
+}
diff --git a/vendor/src/launchpad.net/goamz/ec2/ec2t_test.go b/vendor/src/launchpad.net/goamz/ec2/ec2t_test.go
new file mode 100644
index 0000000000..717d254957
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/ec2/ec2t_test.go
@@ -0,0 +1,1072 @@
+package ec2_test
+
+import (
+ "fmt"
+ "net"
+ "regexp"
+ "sort"
+ "time"
+
+ "launchpad.net/goamz/aws"
+ "launchpad.net/goamz/ec2"
+ "launchpad.net/goamz/ec2/ec2test"
+ "launchpad.net/goamz/testutil"
+ . "launchpad.net/gocheck"
+)
+
+// 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)
+
+ // Add default attributes.
+ srv.SetInitialAttributes(map[string][]string{
+ "supported-platforms": []string{"VPC", "EC2"},
+ "default-vpc": []string{"vpc-xxxxxxx"},
+ })
+
+ 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.New(s.srv.auth, s.srv.region)
+ s.clientTests.ec2 = ec2.New(s.srv.auth, s.srv.region)
+}
+
+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)
+
+ 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)
+}
+
+func (s *LocalServerSuite) TestInstanceInfo(c *C) {
+ list, err := s.ec2.RunInstances(&ec2.RunInstances{
+ ImageId: imageId,
+ InstanceType: "t1.micro",
+ })
+ c.Assert(err, IsNil)
+
+ inst := list.Instances[0]
+ c.Assert(inst, NotNil)
+
+ id := inst.InstanceId
+ defer s.ec2.TerminateInstances([]string{id})
+
+ masked := func(addr string) string {
+ return net.ParseIP(addr).Mask(net.CIDRMask(24, 32)).String()
+ }
+ c.Check(masked(inst.IPAddress), Equals, "8.0.0.0")
+ c.Check(masked(inst.PrivateIPAddress), Equals, "127.0.0.0")
+ // DNSName is empty initially, to check it we need to refresh.
+ c.Check(inst.DNSName, Equals, "")
+ c.Check(inst.PrivateDNSName, Equals, id+".internal.invalid")
+
+ // Get the instance again to verify DNSName.
+ resp, err := s.ec2.Instances([]string{id}, nil)
+ c.Assert(err, IsNil)
+ c.Assert(resp.Reservations, HasLen, 1)
+ c.Assert(resp.Reservations[0].Instances, HasLen, 1)
+ c.Check(resp.Reservations[0].Instances[0].DNSName, Equals, id+".testing.invalid")
+}
+
+// getDefaultVPCIdAndSubnets returns the default VPC id and a list of
+// its default subnets. If fails if there is no default VPC or at
+// least one subnet in it.
+func (s *ServerTests) getDefaultVPCIdAndSubnets(c *C) (string, []ec2.Subnet) {
+ resp1, err := s.ec2.AccountAttributes("default-vpc")
+ c.Assert(err, IsNil)
+ c.Assert(resp1.Attributes, HasLen, 1)
+ c.Assert(resp1.Attributes[0].Name, Equals, "default-vpc")
+ c.Assert(resp1.Attributes[0].Values, HasLen, 1)
+ c.Assert(resp1.Attributes[0].Values[0], Not(Equals), "")
+ defaultVPCId := resp1.Attributes[0].Values[0]
+ if defaultVPCId == "none" {
+ c.Fatalf("no default VPC for region %q", s.ec2.Region.Name)
+ }
+ filter := ec2.NewFilter()
+ filter.Add("defaultForAz", "true")
+ filter.Add("vpc-id", defaultVPCId)
+ resp2, err := s.ec2.Subnets(nil, filter)
+ c.Assert(err, IsNil)
+ defaultSubnets := resp2.Subnets
+ if len(defaultSubnets) < 1 {
+ c.Fatalf("no default subnets for VPC %q", defaultVPCId)
+ }
+
+ return defaultVPCId, defaultSubnets
+}
+
+func validIPForSubnet(c *C, subnet ec2.Subnet, startFrom byte) string {
+ ip, ipnet, err := net.ParseCIDR(subnet.CIDRBlock)
+ c.Assert(err, IsNil)
+ ip[len(ip)-1] = startFrom
+ c.Assert(ipnet.Contains(ip), Equals, true)
+ return ip.String()
+}
+
+func (s *LocalServerSuite) TestAvailabilityZones(c *C) {
+ s.srv.srv.SetAvailabilityZones([]ec2.AvailabilityZoneInfo{{
+ AvailabilityZone: ec2.AvailabilityZone{
+ Name: "us-east-1a",
+ Region: "us-east-1",
+ },
+ State: "available",
+ }, {
+ AvailabilityZone: ec2.AvailabilityZone{
+ Name: "us-east-1b",
+ Region: "us-east-1",
+ },
+ State: "impaired",
+ }, {
+ AvailabilityZone: ec2.AvailabilityZone{
+ Name: "us-west-1a",
+ Region: "us-west-1",
+ },
+ State: "available",
+ }, {
+ AvailabilityZone: ec2.AvailabilityZone{
+ Name: "us-west-1b",
+ Region: "us-west-1",
+ },
+ State: "unavailable",
+ MessageSet: []string{"down for maintenance"},
+ }})
+
+ resp, err := s.ec2.AvailabilityZones(nil)
+ c.Assert(err, IsNil)
+ c.Assert(resp.Zones, HasLen, 4)
+ c.Assert(resp.Zones[0].Name, Equals, "us-east-1a")
+ c.Assert(resp.Zones[1].Name, Equals, "us-east-1b")
+ c.Assert(resp.Zones[2].Name, Equals, "us-west-1a")
+ c.Assert(resp.Zones[3].Name, Equals, "us-west-1b")
+
+ filter := ec2.NewFilter()
+ filter.Add("region-name", "us-east-1")
+ resp, err = s.ec2.AvailabilityZones(filter)
+ c.Assert(err, IsNil)
+ c.Assert(resp.Zones, HasLen, 2)
+ c.Assert(resp.Zones[0].Name, Equals, "us-east-1a")
+ c.Assert(resp.Zones[1].Name, Equals, "us-east-1b")
+}
+
+// AmazonServerSuite runs the ec2test server tests against a live EC2 server.
+// It will only be activated if the -amazon 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.New(s.srv.auth, aws.USEast)
+}
+
+// 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 (s *ServerTests) TestDescribeAccountAttributes(c *C) {
+ resp, err := s.ec2.AccountAttributes("supported-platforms", "default-vpc")
+ c.Assert(err, IsNil)
+ c.Assert(resp.Attributes, HasLen, 2)
+ for _, attr := range resp.Attributes {
+ switch attr.Name {
+ case "supported-platforms":
+ sort.Strings(attr.Values)
+ c.Assert(attr.Values, DeepEquals, []string{"EC2", "VPC"})
+ case "default-vpc":
+ c.Assert(attr.Values, HasLen, 1)
+ c.Assert(attr.Values[0], Not(Equals), "")
+ default:
+ c.Fatalf("unexpected account attribute %q: %v", attr.Name, attr)
+ }
+ }
+}
+
+func (s *ServerTests) TestRunInstancesVPCCreatesNICsWhenSpecified(c *C) {
+ defaultVPCId, defaultSubnets := s.getDefaultVPCIdAndSubnets(c)
+ subnet := defaultSubnets[0]
+
+ g0 := s.makeTestGroupVPC(c, defaultVPCId, "goamz-test0", "ec2test group 0")
+ g1 := s.makeTestGroupVPC(c, defaultVPCId, "goamz-test1", "ec2test group 1")
+ defer s.deleteGroups(c, []ec2.SecurityGroup{g0, g1})
+
+ ip1 := validIPForSubnet(c, subnet, 5)
+ ip2 := validIPForSubnet(c, subnet, 6)
+ list, err := s.ec2.RunInstances(&ec2.RunInstances{
+ ImageId: imageId,
+ InstanceType: "t1.micro",
+ NetworkInterfaces: []ec2.RunNetworkInterface{{
+ DeviceIndex: 0,
+ SubnetId: subnet.Id,
+ Description: "first nic",
+ SecurityGroupIds: []string{g0.Id, g1.Id},
+ DeleteOnTermination: true,
+ PrivateIPs: []ec2.PrivateIP{
+ {Address: ip1, IsPrimary: true},
+ {Address: ip2, IsPrimary: false},
+ }}}})
+ c.Assert(err, IsNil)
+
+ inst := list.Instances[0]
+ c.Assert(inst, NotNil)
+ c.Assert(inst.NetworkInterfaces, HasLen, 1)
+
+ id := inst.InstanceId
+ defer s.ec2.TerminateInstances([]string{id})
+
+ c.Check(inst.VPCId, Equals, subnet.VPCId)
+ c.Check(inst.SubnetId, Equals, subnet.Id)
+ iface := inst.NetworkInterfaces[0]
+ c.Check(iface.Id, Matches, "eni-.+")
+ c.Check(iface.Attachment.DeviceIndex, Equals, 0)
+ c.Check(iface.Attachment.Id, Matches, "eni-attach-.+")
+ c.Check(iface.SubnetId, Equals, subnet.Id)
+ c.Check(iface.PrivateIPAddress, Equals, ip1)
+ c.Check(iface.Description, Equals, "first nic")
+ c.Check(iface.Groups, HasLen, 2)
+ for _, group := range iface.Groups {
+ if group.Id == g0.Id {
+ c.Check(group.Name, Equals, g0.Name)
+ } else if group.Id == g1.Id {
+ c.Check(group.Name, Equals, g1.Name)
+ }
+ }
+ for _, ip := range iface.PrivateIPs {
+ if ip.IsPrimary {
+ c.Check(ip.Address, Equals, ip1)
+ } else {
+ c.Check(ip.Address, Equals, ip2)
+ }
+ }
+}
+
+func (s *ServerTests) TestRunInstancesVPCReturnsErrorWithBothInstanceAndNICSubnetIds(c *C) {
+ _, defaultSubnets := s.getDefaultVPCIdAndSubnets(c)
+ subnet := defaultSubnets[0]
+
+ _, err := s.ec2.RunInstances(&ec2.RunInstances{
+ ImageId: imageId,
+ InstanceType: "t1.micro",
+ SubnetId: subnet.Id,
+ NetworkInterfaces: []ec2.RunNetworkInterface{{
+ DeviceIndex: 0,
+ SubnetId: subnet.Id,
+ Description: "first nic",
+ DeleteOnTermination: true,
+ }},
+ })
+ c.Assert(err, NotNil)
+ c.Check(errorCode(err), Equals, "InvalidParameterCombination")
+}
+
+func (s *ServerTests) testCreateDefaultNIC(c *C, subnet *ec2.Subnet) {
+ defaultVPCId, defaultSubnets := s.getDefaultVPCIdAndSubnets(c)
+
+ params := &ec2.RunInstances{
+ ImageId: imageId,
+ InstanceType: "t1.micro",
+ }
+ if subnet != nil {
+ params.SubnetId = subnet.Id
+ } else {
+ subnet = &defaultSubnets[0]
+ }
+ list, err := s.ec2.RunInstances(params)
+ c.Assert(err, IsNil)
+
+ inst := list.Instances[0]
+ c.Assert(inst, NotNil)
+ c.Assert(inst.NetworkInterfaces, HasLen, 1)
+
+ id := inst.InstanceId
+ defer s.ec2.TerminateInstances([]string{id})
+
+ c.Check(inst.VPCId, Equals, defaultVPCId)
+ if inst.SubnetId != subnet.Id {
+ // Since we don't specify which AZ to use,
+ // the instance might launch in any one.
+ for _, sub := range defaultSubnets {
+ if inst.SubnetId == sub.Id {
+ subnet = &sub
+ }
+ }
+ }
+ _, ipnet, err := net.ParseCIDR(subnet.CIDRBlock)
+ c.Assert(err, IsNil)
+
+ c.Check(inst.SubnetId, Equals, subnet.Id)
+ iface := inst.NetworkInterfaces[0]
+ c.Assert(iface.Id, Matches, "eni-.+")
+ c.Assert(iface.SubnetId, Equals, subnet.Id)
+ c.Assert(iface.VPCId, Equals, subnet.VPCId)
+ if len(iface.PrivateIPs) > 0 {
+ // AWS doesn't always fill in the PrivateIPs slice.
+ expectIP := ec2.PrivateIP{
+ Address: iface.PrivateIPAddress,
+ DNSName: iface.PrivateDNSName,
+ IsPrimary: true,
+ }
+ c.Assert(iface.PrivateIPs, DeepEquals, []ec2.PrivateIP{expectIP})
+ }
+ c.Assert(ipnet.Contains(net.ParseIP(iface.PrivateIPAddress)), Equals, true)
+ c.Assert(iface.PrivateDNSName, Not(Equals), "")
+ c.Assert(iface.Attachment.Id, Matches, "eni-attach-.+")
+}
+
+func (s *ServerTests) TestRunInstancesVPCCreatesDefaultNICWithoutSubnetIdOrNICs(c *C) {
+ s.testCreateDefaultNIC(c, nil)
+}
+
+func (s *ServerTests) TestRunInstancesVPCCreatesDefaultNICWithSubnetIdNoNICs(c *C) {
+ _, defaultSubnets := s.getDefaultVPCIdAndSubnets(c)
+ s.testCreateDefaultNIC(c, &defaultSubnets[0])
+}
+
+func terminateInstances(c *C, e *ec2.EC2, ids []string) {
+ _, err := e.TerminateInstances(ids)
+ c.Assert(err, IsNil, Commentf("%v INSTANCES LEFT RUNNING!!!", ids))
+ // We need to wait until the instances are really off, because
+ // entities that depend on them won't be deleted (i.e. groups,
+ // NICs, subnets, etc.)
+ testAttempt := aws.AttemptStrategy{
+ Total: 10 * time.Minute,
+ Delay: 5 * time.Second,
+ }
+ f := ec2.NewFilter()
+ f.Add("instance-state-name", "terminated")
+ idsLeft := make(map[string]bool)
+ for _, id := range ids {
+ idsLeft[id] = true
+ }
+ for a := testAttempt.Start(); a.Next(); {
+ c.Logf("waiting for %v to get terminated", ids)
+ resp, err := e.Instances(ids, f)
+ if err != nil {
+ c.Fatalf("not waiting for %v to terminate: %v", ids, err)
+ }
+ for _, r := range resp.Reservations {
+ for _, inst := range r.Instances {
+ delete(idsLeft, inst.InstanceId)
+ }
+ }
+ ids = []string{}
+ for id, _ := range idsLeft {
+ ids = append(ids, id)
+ }
+ if len(ids) == 0 {
+ c.Logf("all instances terminated.")
+ return
+ }
+ }
+ c.Fatalf("%v INSTANCES LEFT RUNNING!!!", ids)
+}
+
+func (s *ServerTests) makeTestGroup(c *C, name, descr string) ec2.SecurityGroup {
+ return s.makeTestGroupVPC(c, "", name, descr)
+}
+
+func (s *ServerTests) makeTestGroupVPC(c *C, vpcId, 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 && errorCode(err) != "InvalidGroup.NotFound" {
+ c.Fatalf("delete security group: %v", err)
+ }
+
+ resp, err := s.ec2.CreateSecurityGroupVPC(vpcId, name, 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")
+ g1 := s.makeTestGroup(c, "goamz-test1", "ec2test group 1")
+ defer s.deleteGroups(c, []ec2.SecurityGroup{g0, 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(errorCode(err), 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(errorCode(err), 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(name, 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(errorCode(err), Equals, "InvalidPermission.Duplicate")
+}
+
+type filterSpec struct {
+ name string
+ values []string
+}
+
+func (s *ServerTests) TestInstanceFiltering(c *C) {
+ vpcResp, err := s.ec2.CreateVPC("10.4.0.0/16", "")
+ c.Assert(err, IsNil)
+ vpcId := vpcResp.VPC.Id
+ defer s.deleteVPCs(c, []string{vpcId})
+
+ subResp := s.createSubnet(c, vpcId, "10.4.1.0/24", "")
+ subId := subResp.Subnet.Id
+ defer s.deleteSubnets(c, []string{subId})
+
+ groupResp, err := s.ec2.CreateSecurityGroup(
+ sessionName("testgroup1"),
+ "testgroup one description",
+ )
+ c.Assert(err, IsNil)
+ group1 := groupResp.SecurityGroup
+
+ groupResp, err = s.ec2.CreateSecurityGroupVPC(
+ vpcId,
+ sessionName("testgroup2"),
+ "testgroup two description vpc",
+ )
+ c.Assert(err, IsNil)
+ group2 := groupResp.SecurityGroup
+
+ defer s.deleteGroups(c, []ec2.SecurityGroup{group1, 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]
+
+ imageId2 := "ami-e358958a" // Natty server, i386, EBS store
+ inst, err = s.ec2.RunInstances(&ec2.RunInstances{
+ ImageId: imageId2,
+ InstanceType: "t1.micro",
+ SubnetId: subId,
+ 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
+ }
+
+ defer terminateInstances(c, s.ec2, ids(0, 1, 2))
+
+ 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 id with instance prefix works",
+ filters: []filterSpec{
+ {"instance.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 group name with instance prefix works",
+ filters: []filterSpec{
+ {"instance.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}},
+ },
+ }, {
+ about: "VPC filters in combination",
+ filters: []filterSpec{
+ {"vpc-id", []string{vpcId}},
+ {"subnet-id", []string{subId}},
+ },
+ resultIds: ids(2),
+ },
+ }
+ 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 (s *AmazonServerSuite) TestRunInstancesVPC(c *C) {
+ vpcResp, err := s.ec2.CreateVPC("10.6.0.0/16", "")
+ c.Assert(err, IsNil)
+ vpcId := vpcResp.VPC.Id
+ defer s.deleteVPCs(c, []string{vpcId})
+
+ subResp := s.createSubnet(c, vpcId, "10.6.1.0/24", "")
+ subId := subResp.Subnet.Id
+ defer s.deleteSubnets(c, []string{subId})
+
+ groupResp, err := s.ec2.CreateSecurityGroupVPC(
+ vpcId,
+ sessionName("testgroup1 vpc"),
+ "testgroup description vpc",
+ )
+ c.Assert(err, IsNil)
+ group := groupResp.SecurityGroup
+
+ defer s.deleteGroups(c, []ec2.SecurityGroup{group})
+
+ // Run a single instance with a new network interface.
+ ips := []ec2.PrivateIP{
+ {Address: "10.6.1.10", IsPrimary: true},
+ {Address: "10.6.1.20", IsPrimary: false},
+ }
+ instResp, err := s.ec2.RunInstances(&ec2.RunInstances{
+ MinCount: 1,
+ ImageId: imageId,
+ InstanceType: "t1.micro",
+ NetworkInterfaces: []ec2.RunNetworkInterface{{
+ DeviceIndex: 0,
+ SubnetId: subId,
+ PrivateIPs: ips,
+ SecurityGroupIds: []string{group.Id},
+ DeleteOnTermination: true,
+ }},
+ })
+ c.Assert(err, IsNil)
+ inst := &instResp.Instances[0]
+
+ defer terminateInstances(c, s.ec2, []string{inst.InstanceId})
+
+ // Now list the network interfaces and find ours.
+ testAttempt := aws.AttemptStrategy{
+ Total: 5 * time.Minute,
+ Delay: 5 * time.Second,
+ }
+ f := ec2.NewFilter()
+ f.Add("subnet-id", subId)
+ var newNIC *ec2.NetworkInterface
+ for a := testAttempt.Start(); a.Next(); {
+ c.Logf("waiting for NIC to become available")
+ listNICs, err := s.ec2.NetworkInterfaces(nil, f)
+ if err != nil {
+ c.Logf("retrying; NetworkInterfaces returned: %v", err)
+ continue
+ }
+ for _, iface := range listNICs.Interfaces {
+ c.Logf("found NIC %v", iface)
+ if iface.Attachment.InstanceId == inst.InstanceId {
+ c.Logf("instance %v new NIC appeared", inst.InstanceId)
+ newNIC = &iface
+ break
+ }
+ }
+ if newNIC != nil {
+ break
+ }
+ }
+ if newNIC == nil {
+ c.Fatalf("timeout while waiting for NIC to appear.")
+ }
+ c.Check(newNIC.Id, Matches, `^eni-[0-9a-f]+$`)
+ c.Check(newNIC.SubnetId, Equals, subId)
+ c.Check(newNIC.VPCId, Equals, vpcId)
+ c.Check(newNIC.Status, Matches, `^(attaching|in-use)$`)
+ c.Check(newNIC.PrivateIPAddress, Equals, ips[0].Address)
+ c.Check(newNIC.PrivateIPs, DeepEquals, ips)
+ c.Check(newNIC.Groups, HasLen, 1)
+ c.Check(newNIC.Groups[0].Id, Equals, group.Id)
+ c.Check(newNIC.Attachment.Status, Matches, `^(attaching|attached)$`)
+ c.Check(newNIC.Attachment.DeviceIndex, Equals, 0)
+ c.Check(newNIC.Attachment.DeleteOnTermination, Equals, true)
+}
+
+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) {
+ vpcResp, err := s.ec2.CreateVPC("10.5.0.0/16", "")
+ c.Assert(err, IsNil)
+ vpcId := vpcResp.VPC.Id
+ defer s.deleteVPCs(c, []string{vpcId})
+
+ subResp := s.createSubnet(c, vpcId, "10.5.1.0/24", "")
+ subId := subResp.Subnet.Id
+ defer s.deleteSubnets(c, []string{subId})
+
+ g := make([]ec2.SecurityGroup, 5)
+ for i := range g {
+ var resp *ec2.CreateSecurityGroupResp
+ gid := sessionName(fmt.Sprintf("testgroup%d", i))
+ desc := fmt.Sprintf("testdescription%d", i)
+ if i == 0 {
+ // Create the first one as a VPC group.
+ gid += " vpc"
+ desc += " vpc"
+ resp, err = s.ec2.CreateSecurityGroupVPC(vpcId, gid, desc)
+ } else {
+ resp, err = s.ec2.CreateSecurityGroup(gid, desc)
+ }
+ c.Assert(err, IsNil)
+ g[i] = resp.SecurityGroup
+ c.Logf("group %d: %v", i, g[i])
+ }
+ // Reorder the groups below, so that g[3] is first (some of the
+ // reset depend on it, so they can't be deleted before g[3]). A
+ // slight optimization for local live tests, so that we don't need
+ // to wait 5s each time deleteGroups runs.
+ defer s.deleteGroups(c, []ec2.SecurityGroup{g[3], g[0], g[1], g[2], g[4]})
+
+ 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[2].Id}},
+ }},
+ {{
+ Protocol: "udp",
+ FromPort: 200,
+ ToPort: 400,
+ SourceGroups: []ec2.UserSecurityGroup{{Id: g[2].Id}},
+ }},
+ }
+ for i, ps := range perms {
+ _, err := s.ec2.AuthorizeSecurityGroup(g[i+1], 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, 4),
+ 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(1, 2)),
+ results: groups(1, 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(1)),
+ filterCheck("ip-permission.group-name", g[2].Name, groups(2, 3)),
+ filterCheck("ip-permission.protocol", "udp", groups(3)),
+ filterCheck("ip-permission.from-port", "200", groups(2, 3)),
+ filterCheck("ip-permission.to-port", "200", groups(1)),
+ filterCheck("vpc-id", vpcId, 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))
+ }
+ }
+}
+
+// deleteGroups ensures the given groups are deleted, by retrying
+// until a timeout or all groups cannot be found anymore.
+// This should be used to make sure tests leave no groups around.
+func (s *ServerTests) deleteGroups(c *C, groups []ec2.SecurityGroup) {
+ testAttempt := aws.AttemptStrategy{
+ Total: 2 * time.Minute,
+ Delay: 5 * time.Second,
+ }
+ for a := testAttempt.Start(); a.Next(); {
+ deleted := 0
+ c.Logf("deleting groups %v", groups)
+ for _, group := range groups {
+ _, err := s.ec2.DeleteSecurityGroup(group)
+ if err == nil || errorCode(err) == "InvalidGroup.NotFound" {
+ c.Logf("group %v deleted", group)
+ deleted++
+ continue
+ }
+ if err != nil {
+ c.Logf("retrying; DeleteSecurityGroup returned: %v", err)
+ }
+ }
+ if deleted == len(groups) {
+ c.Logf("all groups deleted")
+ return
+ }
+ }
+ c.Fatalf("timeout while waiting %v groups to get deleted!", groups)
+}
+
+// errorCode returns the code of the given error, assuming it's not
+// nil and it's an instance of *ec2.Error. It returns an empty string
+// otherwise.
+func errorCode(err error) string {
+ if err, _ := err.(*ec2.Error); err != nil {
+ return err.Code
+ }
+ return ""
+}
diff --git a/vendor/src/launchpad.net/goamz/ec2/ec2test/filter.go b/vendor/src/launchpad.net/goamz/ec2/ec2test/filter.go
new file mode 100644
index 0000000000..1a0c046193
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/ec2/ec2test/filter.go
@@ -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
+}
diff --git a/vendor/src/launchpad.net/goamz/ec2/ec2test/server.go b/vendor/src/launchpad.net/goamz/ec2/ec2test/server.go
new file mode 100644
index 0000000000..f47886b67f
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/ec2/ec2test/server.go
@@ -0,0 +1,2032 @@
+// 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"
+ "io"
+ "net"
+ "net/http"
+ "net/url"
+ "regexp"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "launchpad.net/goamz/ec2"
+)
+
+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
+
+ attributes map[string][]string // attr name -> values
+ instances map[string]*Instance // id -> instance
+ reservations map[string]*reservation // id -> reservation
+ groups map[string]*securityGroup // id -> group
+ zones []availabilityZone
+ vpcs map[string]*vpc // id -> vpc
+ subnets map[string]*subnet // id -> subnet
+ ifaces map[string]*iface // id -> iface
+ attachments map[string]*attachment // id -> attachment
+ maxId counter
+ reqId counter
+ reservationId counter
+ groupId counter
+ vpcId counter
+ dhcpOptsId counter
+ subnetId counter
+ ifaceId counter
+ attachId 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 {
+ seq int
+ dnsNameSet bool
+ // UserData holds the data that was passed to the RunInstances request
+ // when the instance was started.
+ UserData []byte
+ imageId string
+ reservation *reservation
+ instType string
+ availZone string
+ state ec2.InstanceState
+ subnetId string
+ vpcId string
+ ifaces []ec2.NetworkInterface
+}
+
+// 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
+ vpcId 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
+ case "vpc-id":
+ return g.vpcId == value, 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
+}
+
+type vpc struct {
+ ec2.VPC
+}
+
+func (v *vpc) matchAttr(attr, value string) (ok bool, err error) {
+ switch attr {
+ case "cidr":
+ return v.CIDRBlock == value, nil
+ case "state":
+ return v.State == value, nil
+ case "vpc-id":
+ return v.Id == value, nil
+ case "tag", "tag-key", "tag-value", "dhcp-options-id", "isDefault":
+ return false, fmt.Errorf("%q filter is not implemented", attr)
+ }
+ return false, fmt.Errorf("unknown attribute %q", attr)
+}
+
+type subnet struct {
+ ec2.Subnet
+}
+
+func (s *subnet) matchAttr(attr, value string) (ok bool, err error) {
+ switch attr {
+ case "cidr":
+ return s.CIDRBlock == value, nil
+ case "availability-zone":
+ return s.AvailZone == value, nil
+ case "state":
+ return s.State == value, nil
+ case "subnet-id":
+ return s.Id == value, nil
+ case "vpc-id":
+ return s.VPCId == value, nil
+ case "defaultForAz", "default-for-az":
+ val, err := strconv.ParseBool(value)
+ if err != nil {
+ return false, fmt.Errorf("bad flag %q: %s", attr, value)
+ }
+ return s.DefaultForAZ == val, nil
+ case "tag", "tag-key", "tag-value", "available-ip-address-count":
+ return false, fmt.Errorf("%q filter not implemented", attr)
+ }
+ return false, fmt.Errorf("unknown attribute %q", attr)
+}
+
+type iface struct {
+ ec2.NetworkInterface
+}
+
+func (i *iface) matchAttr(attr, value string) (ok bool, err error) {
+ notImplemented := []string{
+ "addresses.", "association.", "tag", "requester-",
+ "attachment.", "source-dest-check", "mac-address",
+ "group-", "description", "private-", "owner-id",
+ }
+ switch attr {
+ case "availability-zone":
+ return i.AvailZone == value, nil
+ case "network-interface-id":
+ return i.Id == value, nil
+ case "status":
+ return i.Status == value, nil
+ case "subnet-id":
+ return i.SubnetId == value, nil
+ case "vpc-id":
+ return i.VPCId == value, nil
+ default:
+ for _, item := range notImplemented {
+ if strings.HasPrefix(attr, item) {
+ return false, fmt.Errorf("%q filter not implemented", attr)
+ }
+ }
+ }
+ return false, fmt.Errorf("unknown attribute %q", attr)
+}
+
+func (srv *Server) addPrivateIPs(nic *iface, numToAdd int, addrs []string) error {
+ for _, addr := range addrs {
+ newIP := ec2.PrivateIP{Address: addr, IsPrimary: false}
+ nic.PrivateIPs = append(nic.PrivateIPs, newIP)
+ }
+ if numToAdd == 0 {
+ // Nothing more to do.
+ return nil
+ }
+ firstIP := ""
+ if len(nic.PrivateIPs) > 0 {
+ firstIP = nic.PrivateIPs[len(nic.PrivateIPs)-1].Address
+ } else {
+ // Find the primary IP, if available, otherwise use
+ // the subnet CIDR to generate a valid IP to use.
+ firstIP = nic.PrivateIPAddress
+ if firstIP == "" {
+ sub := srv.subnets[nic.SubnetId]
+ if sub == nil {
+ return fmt.Errorf("NIC %q uses invalid subnet id: %v", nic.Id, nic.SubnetId)
+ }
+ netIP, _, err := net.ParseCIDR(sub.CIDRBlock)
+ if err != nil {
+ return fmt.Errorf("subnet %q has bad CIDR: %v", sub.Id, err)
+ }
+ firstIP = netIP.String()
+ }
+ }
+ ip := net.ParseIP(firstIP)
+ for i := 0; i < numToAdd; i++ {
+ ip[len(ip)-1] += 1
+ newIP := ec2.PrivateIP{Address: ip.String(), IsPrimary: false}
+ nic.PrivateIPs = append(nic.PrivateIPs, newIP)
+ }
+ return nil
+}
+
+func (srv *Server) removePrivateIP(nic *iface, addr string) error {
+ for i, privateIP := range nic.PrivateIPs {
+ if privateIP.Address == addr {
+ // Remove it, preserving order.
+ nic.PrivateIPs = append(nic.PrivateIPs[:i], nic.PrivateIPs[i+1:]...)
+ return nil
+ }
+ }
+ return fmt.Errorf("NIC %q does not have IP %q to remove", nic.Id, addr)
+}
+
+type attachment struct {
+ ec2.NetworkInterfaceAttachment
+}
+
+var actions = map[string]func(*Server, http.ResponseWriter, *http.Request, string) interface{}{
+ "RunInstances": (*Server).runInstances,
+ "TerminateInstances": (*Server).terminateInstances,
+ "DescribeInstances": (*Server).describeInstances,
+ "CreateSecurityGroup": (*Server).createSecurityGroup,
+ "DescribeAvailabilityZones": (*Server).describeAvailabilityZones,
+ "DescribeSecurityGroups": (*Server).describeSecurityGroups,
+ "DeleteSecurityGroup": (*Server).deleteSecurityGroup,
+ "AuthorizeSecurityGroupIngress": (*Server).authorizeSecurityGroupIngress,
+ "RevokeSecurityGroupIngress": (*Server).revokeSecurityGroupIngress,
+ "CreateVpc": (*Server).createVpc,
+ "DeleteVpc": (*Server).deleteVpc,
+ "DescribeVpcs": (*Server).describeVpcs,
+ "CreateSubnet": (*Server).createSubnet,
+ "DeleteSubnet": (*Server).deleteSubnet,
+ "DescribeSubnets": (*Server).describeSubnets,
+ "CreateNetworkInterface": (*Server).createIFace,
+ "DeleteNetworkInterface": (*Server).deleteIFace,
+ "DescribeNetworkInterfaces": (*Server).describeIFaces,
+ "AttachNetworkInterface": (*Server).attachIFace,
+ "DetachNetworkInterface": (*Server).detachIFace,
+ "DescribeAccountAttributes": (*Server).accountAttributes,
+ "AssignPrivateIpAddresses": (*Server).assignPrivateIP,
+ "UnassignPrivateIpAddresses": (*Server).unassignPrivateIP,
+}
+
+const (
+ ownerId = "9876"
+ defaultAvailZone = "us-east-1a"
+)
+
+// 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{
+ attributes: make(map[string][]string),
+ instances: make(map[string]*Instance),
+ groups: make(map[string]*securityGroup),
+ vpcs: make(map[string]*vpc),
+ subnets: make(map[string]*subnet),
+ ifaces: make(map[string]*iface),
+ attachments: make(map[string]*attachment),
+ 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
+
+ // Add a default availability zone.
+ var z availabilityZone
+ z.Name = defaultAvailZone
+ z.Region = "us-east-1"
+ z.State = "available"
+ srv.zones = []availabilityZone{z}
+
+ 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()
+}
+
+func (srv *Server) SetAvailabilityZones(zones []ec2.AvailabilityZoneInfo) {
+ srv.mu.Lock()
+ srv.zones = make([]availabilityZone, len(zones))
+ for i, z := range zones {
+ srv.zones[i] = availabilityZone{z}
+ }
+ srv.mu.Unlock()
+}
+
+// SetInitialAttributes sets the given account attributes on the server.
+func (srv *Server) SetInitialAttributes(attrs map[string][]string) {
+ for attrName, values := range attrs {
+ srv.attributes[attrName] = values
+ if attrName == "default-vpc" {
+ // The default-vpc attribute was provided, so create the
+ // respective VPCs and their subnets.
+ for _, vpcId := range values {
+ srv.vpcs[vpcId] = &vpc{ec2.VPC{
+ Id: vpcId,
+ State: "available",
+ CIDRBlock: "10.0.0.0/16",
+ DHCPOptionsId: fmt.Sprintf("dopt-%d", srv.dhcpOptsId.next()),
+ InstanceTenancy: "default",
+ IsDefault: true,
+ }}
+ subnetId := fmt.Sprintf("subnet-%d", srv.subnetId.next())
+ cidrBlock := "10.10.0.0/20"
+ availIPs, _ := srv.calcSubnetAvailIPs(cidrBlock)
+ srv.subnets[subnetId] = &subnet{ec2.Subnet{
+ Id: subnetId,
+ VPCId: vpcId,
+ State: "available",
+ CIDRBlock: cidrBlock,
+ AvailZone: "us-east-1b",
+ AvailableIPCount: availIPs,
+ DefaultForAZ: true,
+ }}
+ }
+ }
+ }
+}
+
+// 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
+}
+
+// parseRunNetworkInterfaces parses any RunNetworkInterface parameters
+// passed to RunInstances, and returns them, along with a
+// limitToOneInstance flag. The flag is set only when an existing
+// network interface id is specified, which according to the API
+// limits the number of instances to 1.
+func (srv *Server) parseRunNetworkInterfaces(req *http.Request) ([]ec2.RunNetworkInterface, bool) {
+ ifaces := []ec2.RunNetworkInterface{}
+ limitToOneInstance := false
+ for attr, vals := range req.Form {
+ if !strings.HasPrefix(attr, "NetworkInterface.") {
+ // Only process network interface params.
+ continue
+ }
+ fields := strings.Split(attr, ".")
+ if len(fields) < 3 || len(vals) != 1 {
+ fatalf(400, "InvalidParameterValue", "bad param %q: %v", attr, vals)
+ }
+ index := atoi(fields[1])
+ // Field name format: NetworkInterface......
+ for len(ifaces)-1 < index {
+ ifaces = append(ifaces, ec2.RunNetworkInterface{})
+ }
+ iface := ifaces[index]
+ fieldName := fields[2]
+ switch fieldName {
+ case "NetworkInterfaceId":
+ // We're using an existing NIC, hence only a single
+ // instance can be launched.
+ id := vals[0]
+ if _, ok := srv.ifaces[id]; !ok {
+ fatalf(400, "InvalidNetworkInterfaceID.NotFound", "no such nic id %q", id)
+ }
+ iface.Id = id
+ limitToOneInstance = true
+ case "DeviceIndex":
+ // This applies both when creating a new NIC and when using an existing one.
+ iface.DeviceIndex = atoi(vals[0])
+ case "SubnetId":
+ // We're creating a new NIC (from here on).
+ id := vals[0]
+ if _, ok := srv.subnets[id]; !ok {
+ fatalf(400, "InvalidSubnetID.NotFound", "no such subnet id %q", id)
+ }
+ iface.SubnetId = id
+ case "Description":
+ iface.Description = vals[0]
+ case "DeleteOnTermination":
+ val, err := strconv.ParseBool(vals[0])
+ if err != nil {
+ fatalf(400, "InvalidParameterValue", "bad flag %s: %s", fieldName, vals[0])
+ }
+ iface.DeleteOnTermination = val
+ case "PrivateIpAddress":
+ privateIP := ec2.PrivateIP{Address: vals[0], IsPrimary: true}
+ iface.PrivateIPs = append(iface.PrivateIPs, privateIP)
+ // When a single private IP address is explicitly specified,
+ // only one instance can be launched according to the API docs.
+ limitToOneInstance = true
+ case "SecondaryPrivateIpAddressCount":
+ iface.SecondaryPrivateIPCount = atoi(vals[0])
+ case "PrivateIpAddresses":
+ // ...PrivateIpAddress..: vals[0]
+ if len(fields) < 4 {
+ fatalf(400, "InvalidParameterValue", "bad param %q: %v", attr, vals)
+ }
+ ipIndex := atoi(fields[3])
+ for len(iface.PrivateIPs)-1 < ipIndex {
+ iface.PrivateIPs = append(iface.PrivateIPs, ec2.PrivateIP{})
+ }
+ privateIP := iface.PrivateIPs[ipIndex]
+ subFieldName := fields[4]
+ switch subFieldName {
+ case "PrivateIpAddress":
+ privateIP.Address = vals[0]
+ case "Primary":
+ val, err := strconv.ParseBool(vals[0])
+ if err != nil {
+ fatalf(400, "InvalidParameterValue", "bad flag %s: %s", subFieldName, vals[0])
+ }
+ privateIP.IsPrimary = val
+ default:
+ fatalf(400, "InvalidParameterValue", "unknown field %s, subfield %s: %s", fieldName, subFieldName, vals[0])
+ }
+ iface.PrivateIPs[ipIndex] = privateIP
+ case "SecurityGroupId":
+ // ...SecurityGroupId.<#>:
+ for _, sgId := range vals {
+ if _, ok := srv.groups[sgId]; !ok {
+ fatalf(400, "InvalidParameterValue", "no such security group id %q", sgId)
+ }
+ iface.SecurityGroupIds = append(iface.SecurityGroupIds, sgId)
+ }
+ default:
+ fatalf(400, "InvalidParameterValue", "unknown field %s: %s", fieldName, vals[0])
+ }
+ ifaces[index] = iface
+ }
+ return ifaces, limitToOneInstance
+}
+
+// getDefaultSubnet returns the first default subnet for the AZ in the
+// default VPC (if available).
+func (srv *Server) getDefaultSubnet() *subnet {
+ // We need to get the default VPC id and one of its subnets to use.
+ defaultVPCId := ""
+ for _, vpc := range srv.vpcs {
+ if vpc.IsDefault {
+ defaultVPCId = vpc.Id
+ break
+ }
+ }
+ if defaultVPCId == "" {
+ // No default VPC, so nothing to do.
+ return nil
+ }
+ for _, subnet := range srv.subnets {
+ if subnet.VPCId == defaultVPCId && subnet.DefaultForAZ {
+ return subnet
+ }
+ }
+ return nil
+}
+
+// addDefaultNIC requests the creation of a default network interface
+// for each instance to launch in RunInstances, using the given
+// instance subnet, and it's called when no explicit NICs are given.
+// It returns the populated RunNetworkInterface slice to add to
+// RunInstances params.
+func (srv *Server) addDefaultNIC(instSubnet *subnet) []ec2.RunNetworkInterface {
+ if instSubnet == nil {
+ // No subnet specified, nothing to do.
+ return nil
+ }
+
+ ip, ipnet, err := net.ParseCIDR(instSubnet.CIDRBlock)
+ if err != nil {
+ panic(fmt.Sprintf("subnet %q has invalid CIDR: %v", instSubnet.Id, err.Error()))
+ }
+ // Just pick a valid subnet IP, it doesn't have to be unique
+ // across instances, as this is a testing server.
+ ip[len(ip)-1] = 5
+ if !ipnet.Contains(ip) {
+ panic(fmt.Sprintf("%q does not contain IP %q", instSubnet.Id, ip))
+ }
+ return []ec2.RunNetworkInterface{{
+ Id: fmt.Sprintf("eni-%d", srv.ifaceId.next()),
+ DeviceIndex: 0,
+ Description: "created by ec2test server",
+ DeleteOnTermination: true,
+ PrivateIPs: []ec2.PrivateIP{
+ {Address: ip.String(), IsPrimary: true},
+ },
+ }}
+}
+
+// createNICsOnRun creates and returns any network interfaces
+// specified in ifacesToCreate in the server for the given instance id
+// and subnet.
+func (srv *Server) createNICsOnRun(instId string, instSubnet *subnet, ifacesToCreate []ec2.RunNetworkInterface) []ec2.NetworkInterface {
+ if instSubnet == nil {
+ // No subnet specified, nothing to do.
+ return nil
+ }
+
+ var createdNICs []ec2.NetworkInterface
+ for _, ifaceToCreate := range ifacesToCreate {
+ nicId := ifaceToCreate.Id
+ macAddress := fmt.Sprintf("20:%02x:60:cb:27:37", srv.ifaceId)
+ if nicId == "" {
+ // Simulate a NIC got created.
+ nicId = fmt.Sprintf("eni-%d", srv.ifaceId.next())
+ macAddress = fmt.Sprintf("20:%02x:60:cb:27:37", srv.ifaceId)
+ }
+ groups := make([]ec2.SecurityGroup, len(ifaceToCreate.SecurityGroupIds))
+ for i, sgId := range ifaceToCreate.SecurityGroupIds {
+ groups[i] = srv.group(ec2.SecurityGroup{Id: sgId}).ec2SecurityGroup()
+ }
+ // Find the primary private IP address for the NIC
+ // inside the PrivateIPs slice.
+ primaryIP := ""
+ privateDNSName := fmt.Sprintf("%s.internal.invalid", instId)
+ for i, ip := range ifaceToCreate.PrivateIPs {
+ if ip.IsPrimary {
+ primaryIP = ip.Address
+ ifaceToCreate.PrivateIPs[i].DNSName = privateDNSName
+ break
+ }
+ }
+ attach := ec2.NetworkInterfaceAttachment{
+ Id: fmt.Sprintf("eni-attach-%d", srv.attachId.next()),
+ InstanceId: instId,
+ InstanceOwnerId: ownerId,
+ DeviceIndex: ifaceToCreate.DeviceIndex,
+ Status: "in-use",
+ AttachTime: time.Now().Format(time.RFC3339),
+ DeleteOnTermination: true,
+ }
+ srv.attachments[attach.Id] = &attachment{attach}
+ nic := ec2.NetworkInterface{
+ Id: nicId,
+ SubnetId: instSubnet.Id,
+ VPCId: instSubnet.VPCId,
+ AvailZone: instSubnet.AvailZone,
+ Description: ifaceToCreate.Description,
+ OwnerId: ownerId,
+ Status: "in-use",
+ MACAddress: macAddress,
+ PrivateIPAddress: primaryIP,
+ PrivateDNSName: privateDNSName,
+ SourceDestCheck: true,
+ Groups: groups,
+ PrivateIPs: ifaceToCreate.PrivateIPs,
+ Attachment: attach,
+ }
+ srv.ifaces[nicId] = &iface{nic}
+ createdNICs = append(createdNICs, nic)
+ }
+ return createdNICs
+}
+
+// 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?
+ // 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")
+ availZone := req.Form.Get("Placement.AvailabilityZone")
+
+ r := srv.newReservation(srv.formToGroups(req.Form))
+
+ // If the user specifies an explicit subnet id, use it.
+ // Otherwise, get a subnet from the default VPC.
+ userSubnetId := req.Form.Get("SubnetId")
+ instSubnet := srv.subnets[userSubnetId]
+ if instSubnet == nil && userSubnetId != "" {
+ fatalf(400, "InvalidSubnetID.NotFound", "subnet %s not found", userSubnetId)
+ }
+ if userSubnetId == "" {
+ instSubnet = srv.getDefaultSubnet()
+ }
+
+ // Handle network interfaces parsing.
+ ifacesToCreate, limitToOneInstance := srv.parseRunNetworkInterfaces(req)
+ if len(ifacesToCreate) > 0 && userSubnetId != "" {
+ // Since we have an instance-level subnet id
+ // specified, we cannot add network interfaces
+ // in the same request. See http://goo.gl/9aqbT9.
+ fatalf(400, "InvalidParameterCombination", "Network interfaces and an instance-level subnet ID may not be specified on the same request")
+ }
+ if limitToOneInstance {
+ max = 1
+ }
+ if len(ifacesToCreate) == 0 {
+ // No NICs specified, so create a default one to simulate what
+ // EC2 does.
+ ifacesToCreate = srv.addDefaultNIC(instSubnet)
+ }
+
+ 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, availZone, srv.initialInstanceState)
+ // Create any NICs on the instance subnet (if any), and then
+ // save the VPC and subnet ids on the instance, as EC2 does.
+ inst.ifaces = srv.createNICsOnRun(inst.id(), instSubnet, ifacesToCreate)
+ if instSubnet != nil {
+ inst.subnetId = instSubnet.Id
+ inst.vpcId = instSubnet.VPCId
+ }
+ 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
+}
+
+// NewInstancesVPC creates n new VPC instances in srv with the given
+// instance type, image ID, initial state, and security groups,
+// belonging to the given vpcId and subnetId. If any group does not
+// already exist, it will be created. NewInstancesVPC returns the ids
+// of the new instances.
+//
+// If vpcId and subnetId are both empty, this call is equivalent to
+// calling NewInstances.
+func (srv *Server) NewInstancesVPC(vpcId, subnetId string, 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, defaultAvailZone, state)
+ inst.vpcId = vpcId
+ inst.subnetId = subnetId
+ ids[i] = inst.id()
+ }
+ return ids
+}
+
+// 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 {
+ return srv.NewInstancesVPC("", "", n, instType, imageId, state, groups)
+}
+
+func (srv *Server) newInstance(r *reservation, instType string, imageId string, availZone string, state ec2.InstanceState) *Instance {
+ inst := &Instance{
+ seq: srv.maxId.next(),
+ instType: instType,
+ imageId: imageId,
+ availZone: availZone,
+ state: state,
+ reservation: r,
+ }
+ id := inst.id()
+ srv.instances[id] = inst
+ r.instances[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) id() string {
+ return fmt.Sprintf("i-%d", inst.seq)
+}
+
+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 {
+ id := inst.id()
+ // The first time the instance is returned, its DNSName
+ // will be empty. The client should then refresh the instance.
+ var dnsName string
+ if inst.dnsNameSet {
+ dnsName = fmt.Sprintf("%s.testing.invalid", id)
+ } else {
+ inst.dnsNameSet = true
+ }
+ return ec2.Instance{
+ InstanceId: id,
+ InstanceType: inst.instType,
+ ImageId: inst.imageId,
+ DNSName: dnsName,
+ PrivateDNSName: fmt.Sprintf("%s.internal.invalid", id),
+ IPAddress: fmt.Sprintf("8.0.0.%d", inst.seq%256),
+ PrivateIPAddress: fmt.Sprintf("127.0.0.%d", inst.seq%256),
+ State: inst.state,
+ AvailZone: inst.availZone,
+ VPCId: inst.vpcId,
+ SubnetId: inst.subnetId,
+ NetworkInterfaces: inst.ifaces,
+ // TODO the rest
+ }
+}
+
+func (inst *Instance) matchAttr(attr, value string) (ok bool, err error) {
+ switch attr {
+ case "architecture":
+ return value == "i386", nil
+ case "availability-zone":
+ return value == inst.availZone, nil
+ case "instance-id":
+ return inst.id() == value, nil
+ case "subnet-id":
+ return inst.subnetId == value, nil
+ case "vpc-id":
+ return inst.vpcId == value, nil
+ case "instance.group-id", "group-id":
+ for _, g := range inst.reservation.groups {
+ if g.id == value {
+ return true, nil
+ }
+ }
+ return false, nil
+ case "instance.group-name", "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),
+ }
+ vpcId := req.Form.Get("VpcId")
+ if vpcId != "" {
+ g.vpcId = vpcId
+ }
+ 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
+ var groups []ec2.SecurityGroup
+ for _, g := range r.groups {
+ groups = append(groups, g.ec2SecurityGroup())
+ }
+ for _, inst := range r.instances {
+ if len(insts) > 0 && !insts[inst] {
+ continue
+ }
+ // make instances in state "shutting-down" to transition
+ // to "terminated" first, so we can simulate: shutdown,
+ // subsequent refresh of the state with Instances(),
+ // terminated.
+ if inst.state == ShuttingDown {
+ inst.state = Terminated
+ }
+
+ ok, err := f.ok(inst)
+ if ok {
+ instance := inst.ec2instance()
+ instance.SecurityGroups = groups
+ instances = append(instances, instance)
+ } else if err != nil {
+ fatalf(400, "InvalidParameterValue", "describe instances: %v", err)
+ }
+ }
+ if len(instances) > 0 {
+ 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]+$`)
+ cidrIpPat = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/([0-9]+)$`)
+ 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 !cidrIpPat.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,
+ }
+}
+
+type availabilityZone struct {
+ ec2.AvailabilityZoneInfo
+}
+
+func (z *availabilityZone) matchAttr(attr, value string) (ok bool, err error) {
+ switch attr {
+ case "message":
+ for _, m := range z.MessageSet {
+ if m == value {
+ return true, nil
+ }
+ }
+ return false, nil
+ case "region-name":
+ return z.Region == value, nil
+ case "state":
+ switch value {
+ case "available", "impaired", "unavailable":
+ return z.State == value, nil
+ }
+ return false, fmt.Errorf("invalid state %q", value)
+ case "zone-name":
+ return z.Name == value, nil
+ }
+ return false, fmt.Errorf("unknown attribute %q", attr)
+}
+
+func (srv *Server) describeAvailabilityZones(w http.ResponseWriter, req *http.Request, reqId string) interface{} {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ f := newFilter(req.Form)
+ var resp ec2.AvailabilityZonesResp
+ resp.RequestId = reqId
+ for _, zone := range srv.zones {
+ ok, err := f.ok(&zone)
+ if ok {
+ resp.Zones = append(resp.Zones, zone.AvailabilityZoneInfo)
+ } else if err != nil {
+ fatalf(400, "InvalidParameterValue", "describe availability zones: %v", err)
+ }
+ }
+ return &resp
+}
+
+func (srv *Server) createVpc(w http.ResponseWriter, req *http.Request, reqId string) interface{} {
+ cidrBlock := parseCidr(req.Form.Get("CidrBlock"))
+ tenancy := req.Form.Get("InstanceTenancy")
+ if tenancy == "" {
+ tenancy = "default"
+ }
+
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+ v := &vpc{ec2.VPC{
+ Id: fmt.Sprintf("vpc-%d", srv.vpcId.next()),
+ State: "available",
+ CIDRBlock: cidrBlock,
+ DHCPOptionsId: fmt.Sprintf("dopt-%d", srv.dhcpOptsId.next()),
+ InstanceTenancy: tenancy,
+ }}
+ srv.vpcs[v.Id] = v
+ r := &ec2.CreateVPCResp{
+ RequestId: reqId,
+ VPC: v.VPC,
+ }
+ return r
+}
+
+func (srv *Server) deleteVpc(w http.ResponseWriter, req *http.Request, reqId string) interface{} {
+ v := srv.vpc(req.Form.Get("VpcId"))
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ delete(srv.vpcs, v.Id)
+ return &ec2.SimpleResp{
+ XMLName: xml.Name{"", "DeleteVpcResponse"},
+ RequestId: reqId,
+ }
+}
+
+func (srv *Server) describeVpcs(w http.ResponseWriter, req *http.Request, reqId string) interface{} {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ idMap := parseIDs(req.Form, "VpcId.")
+ f := newFilter(req.Form)
+ var resp ec2.VPCsResp
+ resp.RequestId = reqId
+ for _, v := range srv.vpcs {
+ ok, err := f.ok(v)
+ _, known := idMap[v.Id]
+ if ok && (len(idMap) == 0 || known) {
+ resp.VPCs = append(resp.VPCs, v.VPC)
+ } else if err != nil {
+ fatalf(400, "InvalidParameterValue", "describe VPCs: %v", err)
+ }
+ }
+ return &resp
+}
+
+func (srv *Server) calcSubnetAvailIPs(cidrBlock string) (int, error) {
+ _, ipnet, err := net.ParseCIDR(cidrBlock)
+ if err != nil {
+ return 0, err
+ }
+ // calculate the available IP addresses, removing the first 4 and
+ // the last, which are reserved by AWS.
+ maskOnes, maskBits := ipnet.Mask.Size()
+ return 1<item"`
+ Attachment NetworkInterfaceAttachment `xml:"attachment"`
+ Tags []Tag `xml:"tagSet>item"`
+ PrivateIPs []PrivateIP `xml:"privateIpAddressesSet>item"`
+}
+
+// CreateNetworkInterface encapsulates options for the
+// CreateNetworkInterface call.
+//
+// SubnetId is the only required field.
+//
+// One or more private IP addresses can be specified by using the
+// PrivateIPs slice. Only one provided PrivateIP may be set as
+// primary.
+//
+// If PrivateIPs is empty, EC2 selects a primary private IP from the
+// subnet range.
+//
+// When SecondaryPrivateIPCount is non-zero, EC2 allocates that
+// number of IP addresses from within the subnet range. The number of
+// IP addresses you can assign to a network interface varies by
+// instance type.
+type CreateNetworkInterface struct {
+ SubnetId string
+ PrivateIPs []PrivateIP
+ SecondaryPrivateIPCount int
+ Description string
+ SecurityGroupIds []string
+}
+
+// CreateNetworkInterfaceResp is the response to a
+// CreateNetworkInterface request.
+//
+// See http://goo.gl/ze3VhA for more details.
+type CreateNetworkInterfaceResp struct {
+ RequestId string `xml:"requestId"`
+ NetworkInterface NetworkInterface `xml:"networkInterface"`
+}
+
+// CreateNetworkInterface creates a network interface in the specified
+// subnet.
+//
+// See http://goo.gl/ze3VhA for more details.
+func (ec2 *EC2) CreateNetworkInterface(opts CreateNetworkInterface) (resp *CreateNetworkInterfaceResp, err error) {
+ params := makeParamsVPC("CreateNetworkInterface")
+ params["SubnetId"] = opts.SubnetId
+ for i, ip := range opts.PrivateIPs {
+ prefix := fmt.Sprintf("PrivateIpAddresses.%d.", i+1)
+ params[prefix+"PrivateIpAddress"] = ip.Address
+ params[prefix+"Primary"] = strconv.FormatBool(ip.IsPrimary)
+ }
+ if opts.Description != "" {
+ params["Description"] = opts.Description
+ }
+ if opts.SecondaryPrivateIPCount > 0 {
+ count := strconv.Itoa(opts.SecondaryPrivateIPCount)
+ params["SecondaryPrivateIpAddressCount"] = count
+ }
+ for i, groupId := range opts.SecurityGroupIds {
+ params["SecurityGroupId."+strconv.Itoa(i+1)] = groupId
+ }
+ resp = &CreateNetworkInterfaceResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// DeleteNetworkInterface deletes the specified network interface,
+// which must have been previously detached.
+//
+// See http://goo.gl/MC1yOj for more details.
+func (ec2 *EC2) DeleteNetworkInterface(id string) (resp *SimpleResp, err error) {
+ params := makeParamsVPC("DeleteNetworkInterface")
+ params["NetworkInterfaceId"] = id
+ resp = &SimpleResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// NetworkInterfacesResp is the response to a NetworkInterfaces
+// request.
+//
+// See http://goo.gl/2LcXtM for more details.
+type NetworkInterfacesResp struct {
+ RequestId string `xml:"requestId"`
+ Interfaces []NetworkInterface `xml:"networkInterfaceSet>item"`
+}
+
+// NetworkInterfaces returns a list of network interfaces.
+//
+// If the ids or filter parameters are provided, only matching
+// interfaces are returned.
+//
+// See http://goo.gl/2LcXtM for more details.
+func (ec2 *EC2) NetworkInterfaces(ids []string, filter *Filter) (resp *NetworkInterfacesResp, err error) {
+ params := makeParamsVPC("DescribeNetworkInterfaces")
+ for i, id := range ids {
+ params["NetworkInterfaceId."+strconv.Itoa(i+1)] = id
+ }
+ filter.addParams(params)
+
+ resp = &NetworkInterfacesResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// AttachNetworkInterfaceResp is the response to an
+// AttachNetworkInterface request.
+//
+// See http://goo.gl/rEbSii for more details.
+type AttachNetworkInterfaceResp struct {
+ RequestId string `xml:"requestId"`
+ AttachmentId string `xml:"attachmentId"`
+}
+
+// AttachNetworkInterface attaches a network interface to an instance.
+//
+// See http://goo.gl/rEbSii for more details.
+func (ec2 *EC2) AttachNetworkInterface(interfaceId, instanceId string, deviceIndex int) (resp *AttachNetworkInterfaceResp, err error) {
+ params := makeParamsVPC("AttachNetworkInterface")
+ params["NetworkInterfaceId"] = interfaceId
+ params["InstanceId"] = instanceId
+ params["DeviceIndex"] = strconv.Itoa(deviceIndex)
+ resp = &AttachNetworkInterfaceResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// DetachNetworkInterface detaches a network interface from an
+// instance.
+//
+// See http://goo.gl/0Xc1px for more details.
+func (ec2 *EC2) DetachNetworkInterface(attachmentId string, force bool) (resp *SimpleResp, err error) {
+ params := makeParamsVPC("DetachNetworkInterface")
+ params["AttachmentId"] = attachmentId
+ if force {
+ // Force is optional.
+ params["Force"] = "true"
+ }
+ resp = &SimpleResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
diff --git a/vendor/src/launchpad.net/goamz/ec2/networkinterfaces_test.go b/vendor/src/launchpad.net/goamz/ec2/networkinterfaces_test.go
new file mode 100644
index 0000000000..9af1c521a4
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/ec2/networkinterfaces_test.go
@@ -0,0 +1,360 @@
+//
+// goamz - Go packages to interact with the Amazon Web Services.
+//
+// https://wiki.ubuntu.com/goamz
+//
+// Copyright (c) 2014 Canonical Ltd.
+//
+
+package ec2_test
+
+import (
+ "launchpad.net/goamz/aws"
+ "launchpad.net/goamz/ec2"
+ . "launchpad.net/gocheck"
+ "time"
+)
+
+// Network interface tests with example responses
+
+func (s *S) TestCreateNetworkInterfaceExample(c *C) {
+ testServer.Response(200, nil, CreateNetworkInterfaceExample)
+
+ resp, err := s.ec2.CreateNetworkInterface(ec2.CreateNetworkInterface{
+ SubnetId: "subnet-b2a249da",
+ PrivateIPs: []ec2.PrivateIP{
+ {Address: "10.0.2.157", IsPrimary: true},
+ },
+ SecurityGroupIds: []string{"sg-1a2b3c4d"},
+ })
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Form["Action"], DeepEquals, []string{"CreateNetworkInterface"})
+ c.Assert(req.Form["SubnetId"], DeepEquals, []string{"subnet-b2a249da"})
+ c.Assert(req.Form["PrivateIpAddress"], HasLen, 0)
+ c.Assert(
+ req.Form["PrivateIpAddresses.1.PrivateIpAddress"],
+ DeepEquals,
+ []string{"10.0.2.157"},
+ )
+ c.Assert(
+ req.Form["PrivateIpAddresses.1.Primary"],
+ DeepEquals,
+ []string{"true"},
+ )
+ c.Assert(req.Form["Description"], HasLen, 0)
+ c.Assert(req.Form["SecurityGroupId.1"], DeepEquals, []string{"sg-1a2b3c4d"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "8dbe591e-5a22-48cb-b948-dd0aadd55adf")
+ iface := resp.NetworkInterface
+ c.Check(iface.Id, Equals, "eni-cfca76a6")
+ c.Check(iface.SubnetId, Equals, "subnet-b2a249da")
+ c.Check(iface.VPCId, Equals, "vpc-c31dafaa")
+ c.Check(iface.AvailZone, Equals, "ap-southeast-1b")
+ c.Check(iface.Description, Equals, "")
+ c.Check(iface.OwnerId, Equals, "251839141158")
+ c.Check(iface.RequesterManaged, Equals, false)
+ c.Check(iface.Status, Equals, "available")
+ c.Check(iface.MACAddress, Equals, "02:74:b0:72:79:61")
+ c.Check(iface.PrivateIPAddress, Equals, "10.0.2.157")
+ c.Check(iface.SourceDestCheck, Equals, true)
+ c.Check(iface.Groups, DeepEquals, []ec2.SecurityGroup{
+ {Id: "sg-1a2b3c4d", Name: "default"},
+ })
+ c.Check(iface.Tags, HasLen, 0)
+ c.Check(iface.PrivateIPs, DeepEquals, []ec2.PrivateIP{
+ {Address: "10.0.2.157", IsPrimary: true},
+ })
+}
+
+func (s *S) TestDeleteNetworkInterfaceExample(c *C) {
+ testServer.Response(200, nil, DeleteNetworkInterfaceExample)
+
+ resp, err := s.ec2.DeleteNetworkInterface("eni-id")
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Form["Action"], DeepEquals, []string{"DeleteNetworkInterface"})
+ c.Assert(req.Form["NetworkInterfaceId"], DeepEquals, []string{"eni-id"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "e1c6d73b-edaa-4e62-9909-6611404e1739")
+}
+
+func (s *S) TestNetworkInterfacesExample(c *C) {
+ testServer.Response(200, nil, DescribeNetworkInterfacesExample)
+
+ ids := []string{"eni-0f62d866", "eni-a66ed5cf"}
+ resp, err := s.ec2.NetworkInterfaces(ids, nil)
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeNetworkInterfaces"})
+ c.Assert(req.Form["NetworkInterfaceId.1"], DeepEquals, []string{ids[0]})
+ c.Assert(req.Form["NetworkInterfaceId.2"], DeepEquals, []string{ids[1]})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "fc45294c-006b-457b-bab9-012f5b3b0e40")
+ c.Check(resp.Interfaces, HasLen, 2)
+ iface := resp.Interfaces[0]
+ c.Check(iface.Id, Equals, ids[0])
+ c.Check(iface.SubnetId, Equals, "subnet-c53c87ac")
+ c.Check(iface.VPCId, Equals, "vpc-cc3c87a5")
+ c.Check(iface.AvailZone, Equals, "ap-southeast-1b")
+ c.Check(iface.Description, Equals, "")
+ c.Check(iface.OwnerId, Equals, "053230519467")
+ c.Check(iface.RequesterManaged, Equals, false)
+ c.Check(iface.Status, Equals, "in-use")
+ c.Check(iface.MACAddress, Equals, "02:81:60:cb:27:37")
+ c.Check(iface.PrivateIPAddress, Equals, "10.0.0.146")
+ c.Check(iface.SourceDestCheck, Equals, true)
+ c.Check(iface.Groups, DeepEquals, []ec2.SecurityGroup{
+ {Id: "sg-3f4b5653", Name: "default"},
+ })
+ c.Check(iface.Attachment, DeepEquals, ec2.NetworkInterfaceAttachment{
+ Id: "eni-attach-6537fc0c",
+ InstanceId: "i-22197876",
+ InstanceOwnerId: "053230519467",
+ DeviceIndex: 0,
+ Status: "attached",
+ AttachTime: "2012-07-01T21:45:27.000Z",
+ DeleteOnTermination: true,
+ })
+ c.Check(iface.PrivateIPs, DeepEquals, []ec2.PrivateIP{
+ {Address: "10.0.0.146", IsPrimary: true},
+ {Address: "10.0.0.148", IsPrimary: false},
+ {Address: "10.0.0.150", IsPrimary: false},
+ })
+ c.Check(iface.Tags, HasLen, 0)
+
+ iface = resp.Interfaces[1]
+ c.Check(iface.Id, Equals, ids[1])
+ c.Check(iface.SubnetId, Equals, "subnet-cd8a35a4")
+ c.Check(iface.VPCId, Equals, "vpc-f28a359b")
+ c.Check(iface.AvailZone, Equals, "ap-southeast-1b")
+ c.Check(iface.Description, Equals, "Primary network interface")
+ c.Check(iface.OwnerId, Equals, "053230519467")
+ c.Check(iface.RequesterManaged, Equals, false)
+ c.Check(iface.Status, Equals, "in-use")
+ c.Check(iface.MACAddress, Equals, "02:78:d7:00:8a:1e")
+ c.Check(iface.PrivateIPAddress, Equals, "10.0.1.233")
+ c.Check(iface.SourceDestCheck, Equals, true)
+ c.Check(iface.Groups, DeepEquals, []ec2.SecurityGroup{
+ {Id: "sg-a2a0b2ce", Name: "quick-start-1"},
+ })
+ c.Check(iface.Attachment, DeepEquals, ec2.NetworkInterfaceAttachment{
+ Id: "eni-attach-a99c57c0",
+ InstanceId: "i-886401dc",
+ InstanceOwnerId: "053230519467",
+ DeviceIndex: 0,
+ Status: "attached",
+ AttachTime: "2012-06-27T20:08:44.000Z",
+ DeleteOnTermination: true,
+ })
+ c.Check(iface.PrivateIPs, DeepEquals, []ec2.PrivateIP{
+ {Address: "10.0.1.233", IsPrimary: true},
+ {Address: "10.0.1.20", IsPrimary: false},
+ })
+ c.Check(iface.Tags, HasLen, 0)
+}
+
+func (s *S) TestAttachNetworkInterfaceExample(c *C) {
+ testServer.Response(200, nil, AttachNetworkInterfaceExample)
+
+ resp, err := s.ec2.AttachNetworkInterface("eni-id", "i-id", 0)
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Form["Action"], DeepEquals, []string{"AttachNetworkInterface"})
+ c.Assert(req.Form["NetworkInterfaceId"], DeepEquals, []string{"eni-id"})
+ c.Assert(req.Form["InstanceId"], DeepEquals, []string{"i-id"})
+ c.Assert(req.Form["DeviceIndex"], DeepEquals, []string{"0"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "ace8cd1e-e685-4e44-90fb-92014d907212")
+ c.Assert(resp.AttachmentId, Equals, "eni-attach-d94b09b0")
+}
+
+func (s *S) TestDetachNetworkInterfaceExample(c *C) {
+ testServer.Response(200, nil, DetachNetworkInterfaceExample)
+
+ resp, err := s.ec2.DetachNetworkInterface("eni-attach-id", true)
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Form["Action"], DeepEquals, []string{"DetachNetworkInterface"})
+ c.Assert(req.Form["AttachmentId"], DeepEquals, []string{"eni-attach-id"})
+ c.Assert(req.Form["Force"], DeepEquals, []string{"true"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "ce540707-0635-46bc-97da-33a8a362a0e8")
+}
+
+// Network interface tests run against either a local test server or
+// live on EC2.
+
+func (s *ServerTests) TestNetworkInterfaces(c *C) {
+ vpcResp, err := s.ec2.CreateVPC("10.3.0.0/16", "")
+ c.Assert(err, IsNil)
+ vpcId := vpcResp.VPC.Id
+ defer s.deleteVPCs(c, []string{vpcId})
+
+ subResp := s.createSubnet(c, vpcId, "10.3.1.0/24", "")
+ subId := subResp.Subnet.Id
+ defer s.deleteSubnets(c, []string{subId})
+
+ sg := s.makeTestGroupVPC(c, vpcId, "vpc-sg-1", "vpc test group1")
+ defer s.deleteGroups(c, []ec2.SecurityGroup{sg})
+
+ instList, err := s.ec2.RunInstances(&ec2.RunInstances{
+ ImageId: imageId,
+ InstanceType: "t1.micro",
+ SubnetId: subId,
+ })
+ c.Assert(err, IsNil)
+ inst := instList.Instances[0]
+ c.Assert(inst, NotNil)
+ instId := inst.InstanceId
+ defer terminateInstances(c, s.ec2, []string{instId})
+
+ ips1 := []ec2.PrivateIP{{Address: "10.3.1.10", IsPrimary: true}}
+ resp1, err := s.ec2.CreateNetworkInterface(ec2.CreateNetworkInterface{
+ SubnetId: subId,
+ PrivateIPs: ips1,
+ Description: "My first iface",
+ })
+ c.Assert(err, IsNil)
+ assertNetworkInterface(c, resp1.NetworkInterface, "", subId, ips1)
+ c.Check(resp1.NetworkInterface.Description, Equals, "My first iface")
+ id1 := resp1.NetworkInterface.Id
+
+ ips2 := []ec2.PrivateIP{
+ {Address: "10.3.1.20", IsPrimary: true},
+ {Address: "10.3.1.22", IsPrimary: false},
+ }
+ resp2, err := s.ec2.CreateNetworkInterface(ec2.CreateNetworkInterface{
+ SubnetId: subId,
+ PrivateIPs: ips2,
+ SecurityGroupIds: []string{sg.Id},
+ })
+ c.Assert(err, IsNil)
+ assertNetworkInterface(c, resp2.NetworkInterface, "", subId, ips2)
+ c.Assert(resp2.NetworkInterface.Groups, DeepEquals, []ec2.SecurityGroup{sg})
+ id2 := resp2.NetworkInterface.Id
+
+ // We only check for the network interfaces we just created,
+ // because the user might have others in his account (when testing
+ // against the EC2 servers). In some cases it takes a short while
+ // until both interfaces are created, so we need to retry a few
+ // times to make sure.
+ testAttempt := aws.AttemptStrategy{
+ Total: 5 * time.Minute,
+ Delay: 5 * time.Second,
+ }
+ var list *ec2.NetworkInterfacesResp
+ done := false
+ for a := testAttempt.Start(); a.Next(); {
+ c.Logf("waiting for %v to be created", []string{id1, id2})
+ list, err = s.ec2.NetworkInterfaces(nil, nil)
+ if err != nil {
+ c.Logf("retrying; NetworkInterfaces returned: %v", err)
+ continue
+ }
+ found := 0
+ for _, iface := range list.Interfaces {
+ c.Logf("found NIC %v", iface)
+ switch iface.Id {
+ case id1:
+ assertNetworkInterface(c, iface, id1, subId, ips1)
+ found++
+ case id2:
+ assertNetworkInterface(c, iface, id2, subId, ips2)
+ found++
+ }
+ if found == 2 {
+ done = true
+ break
+ }
+ }
+ if done {
+ c.Logf("all NICs were created")
+ break
+ }
+ }
+ if !done {
+ c.Fatalf("timeout while waiting for NICs %v", []string{id1, id2})
+ }
+
+ list, err = s.ec2.NetworkInterfaces([]string{id1}, nil)
+ c.Assert(err, IsNil)
+ c.Assert(list.Interfaces, HasLen, 1)
+ assertNetworkInterface(c, list.Interfaces[0], id1, subId, ips1)
+
+ f := ec2.NewFilter()
+ f.Add("network-interface-id", id2)
+ list, err = s.ec2.NetworkInterfaces(nil, f)
+ c.Assert(err, IsNil)
+ c.Assert(list.Interfaces, HasLen, 1)
+ assertNetworkInterface(c, list.Interfaces[0], id2, subId, ips2)
+
+ // Attachment might fail if the instance is not running yet,
+ // so we retry for a while until it succeeds.
+ var attResp *ec2.AttachNetworkInterfaceResp
+ for a := testAttempt.Start(); a.Next(); {
+ attResp, err = s.ec2.AttachNetworkInterface(id2, instId, 1)
+ if err != nil {
+ c.Logf("AttachNetworkInterface returned: %v; retrying...", err)
+ attResp = nil
+ continue
+ }
+ c.Logf("AttachNetworkInterface succeeded")
+ c.Check(attResp.AttachmentId, Not(Equals), "")
+ break
+ }
+ if attResp == nil {
+ c.Fatalf("timeout while waiting for AttachNetworkInterface to succeed")
+ }
+
+ list, err = s.ec2.NetworkInterfaces([]string{id2}, nil)
+ c.Assert(err, IsNil)
+ att := list.Interfaces[0].Attachment
+ c.Check(att.Id, Equals, attResp.AttachmentId)
+ c.Check(att.InstanceId, Equals, instId)
+ c.Check(att.DeviceIndex, Equals, 1)
+ c.Check(att.Status, Matches, "(attaching|in-use)")
+
+ _, err = s.ec2.DetachNetworkInterface(att.Id, true)
+ c.Check(err, IsNil)
+
+ _, err = s.ec2.DeleteNetworkInterface(id1)
+ c.Assert(err, IsNil)
+
+ // We might not be able to delete the interface until the
+ // detachment is completed, so we need to retry here as well.
+ for a := testAttempt.Start(); a.Next(); {
+ _, err = s.ec2.DeleteNetworkInterface(id2)
+ if err != nil {
+ c.Logf("DeleteNetworkInterface returned: %v; retrying...", err)
+ continue
+ }
+ c.Logf("DeleteNetworkInterface succeeded")
+ return
+ }
+ c.Fatalf("timeout while waiting for DeleteNetworkInterface to succeed")
+}
+
+func assertNetworkInterface(c *C, obtained ec2.NetworkInterface, expectId, expectSubId string, expectIPs []ec2.PrivateIP) {
+ if expectId != "" {
+ c.Check(obtained.Id, Equals, expectId)
+ } else {
+ c.Check(obtained.Id, Matches, `^eni-[0-9a-f]+$`)
+ }
+ c.Check(obtained.Status, Matches, "(available|pending|in-use)")
+ if expectSubId != "" {
+ c.Check(obtained.SubnetId, Equals, expectSubId)
+ } else {
+ c.Check(obtained.SubnetId, Matches, `^subnet-[0-9a-f]+$`)
+ }
+ c.Check(obtained.Attachment, DeepEquals, ec2.NetworkInterfaceAttachment{})
+ if len(expectIPs) > 0 {
+ c.Check(obtained.PrivateIPs, DeepEquals, expectIPs)
+ c.Check(obtained.PrivateIPAddress, DeepEquals, expectIPs[0].Address)
+ }
+}
diff --git a/vendor/src/launchpad.net/goamz/ec2/privateips.go b/vendor/src/launchpad.net/goamz/ec2/privateips.go
new file mode 100644
index 0000000000..a4cd622b4f
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/ec2/privateips.go
@@ -0,0 +1,67 @@
+//
+// goamz - Go packages to interact with the Amazon Web Services.
+//
+// https://wiki.ubuntu.com/goamz
+//
+// Copyright (c) 2014 Canonical Ltd.
+//
+
+package ec2
+
+import (
+ "strconv"
+)
+
+// AssignPrivateIPAddresses assigns secondary IP addresses to the
+// network interface interfaceId.
+//
+// If secondaryIPCount is non-zero, ipAddresses must be nil, and the
+// specified number of secondary IP addresses will be allocated within
+// the subnet's CIDR block range.
+//
+// allowReassignment specifies whether to allow reassignment of
+// addresses currently assigned to a different network interface.
+//
+// See http://goo.gl/MoeH0L more details.
+func (ec2 *EC2) AssignPrivateIPAddresses(interfaceId string, ipAddresses []string, secondaryIPCount int, allowReassignment bool) (resp *SimpleResp, err error) {
+ params := makeParamsVPC("AssignPrivateIpAddresses")
+ params["NetworkInterfaceId"] = interfaceId
+ if secondaryIPCount > 0 {
+ params["SecondaryPrivateIpAddressCount"] = strconv.Itoa(secondaryIPCount)
+ } else {
+ for i, ip := range ipAddresses {
+ // PrivateIpAddress is zero indexed.
+ n := strconv.Itoa(i)
+ params["PrivateIpAddress."+n] = ip
+ }
+ }
+ if allowReassignment {
+ params["AllowReassignment"] = "true"
+ }
+ resp = &SimpleResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// UnassignPrivateIPAddresses unassigns one or more secondary private
+// IP addresses from a network interface.
+//
+// See http://goo.gl/RjGZdB for more details.
+func (ec2 *EC2) UnassignPrivateIPAddresses(interfaceId string, ipAddresses []string) (resp *SimpleResp, err error) {
+ params := makeParamsVPC("UnassignPrivateIpAddresses")
+ params["NetworkInterfaceId"] = interfaceId
+ for i, ip := range ipAddresses {
+ // PrivateIpAddress is zero indexed.
+ n := strconv.Itoa(i)
+ params["PrivateIpAddress."+n] = ip
+ }
+ resp = &SimpleResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
diff --git a/vendor/src/launchpad.net/goamz/ec2/privateips_test.go b/vendor/src/launchpad.net/goamz/ec2/privateips_test.go
new file mode 100644
index 0000000000..9e49d8c0ab
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/ec2/privateips_test.go
@@ -0,0 +1,177 @@
+//
+// goamz - Go packages to interact with the Amazon Web Services.
+//
+// https://wiki.ubuntu.com/goamz
+//
+// Copyright (c) 2014 Canonical Ltd.
+//
+
+package ec2_test
+
+import (
+ "strings"
+ "time"
+
+ "launchpad.net/goamz/aws"
+ "launchpad.net/goamz/ec2"
+ . "launchpad.net/gocheck"
+)
+
+// Private IP tests with example responses
+
+func (s *S) TestAssignPrivateIPAddressesExample(c *C) {
+ testServer.Response(200, nil, AssignPrivateIpAddressesExample)
+
+ resp, err := s.ec2.AssignPrivateIPAddresses("eni-id", []string{"1.2.3.4", "4.3.2.1"}, 0, true)
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Form["Action"], DeepEquals, []string{"AssignPrivateIpAddresses"})
+ c.Assert(req.Form["NetworkInterfaceId"], DeepEquals, []string{"eni-id"})
+ c.Assert(req.Form["PrivateIpAddress.0"], DeepEquals, []string{"1.2.3.4"})
+ c.Assert(req.Form["PrivateIpAddress.1"], DeepEquals, []string{"4.3.2.1"})
+ c.Assert(req.Form["SecondaryPrivateIpAddressCount"], HasLen, 0)
+ c.Assert(req.Form["AllowReassignment"], DeepEquals, []string{"true"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE")
+}
+
+func (s *S) TestUnassignPrivateIPAddressesExample(c *C) {
+ testServer.Response(200, nil, UnassignPrivateIpAddressesExample)
+
+ resp, err := s.ec2.UnassignPrivateIPAddresses("eni-id", []string{"1.2.3.4", "4.3.2.1"})
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Form["Action"], DeepEquals, []string{"UnassignPrivateIpAddresses"})
+ c.Assert(req.Form["NetworkInterfaceId"], DeepEquals, []string{"eni-id"})
+ c.Assert(req.Form["PrivateIpAddress.0"], DeepEquals, []string{"1.2.3.4"})
+ c.Assert(req.Form["PrivateIpAddress.1"], DeepEquals, []string{"4.3.2.1"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE")
+}
+
+// Private IP tests run live against EC2.
+
+func (s *ServerTests) TestAssignUnassignPrivateIPs(c *C) {
+ vpcResp, err := s.ec2.CreateVPC("10.7.0.0/16", "")
+ c.Assert(err, IsNil)
+ vpcId := vpcResp.VPC.Id
+ defer s.deleteVPCs(c, []string{vpcId})
+
+ subResp := s.createSubnet(c, vpcId, "10.7.1.0/24", "")
+ subId := subResp.Subnet.Id
+ defer s.deleteSubnets(c, []string{subId})
+
+ // Launch a m1.medium instance, so we can later assign up to 6
+ // private IPs per NIC.
+ instList, err := s.ec2.RunInstances(&ec2.RunInstances{
+ ImageId: imageId,
+ InstanceType: "m1.medium",
+ SubnetId: subId,
+ })
+ c.Assert(err, IsNil)
+ inst := instList.Instances[0]
+ c.Assert(inst, NotNil)
+ instId := inst.InstanceId
+ defer terminateInstances(c, s.ec2, []string{instId})
+
+ // We need to wait for the instance to change state to 'running',
+ // so its automatically created network interface on the created
+ // subnet will appear.
+ testAttempt := aws.AttemptStrategy{
+ Total: 5 * time.Minute,
+ Delay: 5 * time.Second,
+ }
+ var newNIC *ec2.NetworkInterface
+ f := ec2.NewFilter()
+ f.Add("subnet-id", subId)
+ for a := testAttempt.Start(); a.Next(); {
+ resp, err := s.ec2.NetworkInterfaces(nil, f)
+ if err != nil {
+ c.Logf("NetworkInterfaces returned: %v; retrying...", err)
+ continue
+ }
+ for _, iface := range resp.Interfaces {
+ c.Logf("found NIC %v", iface)
+ if iface.Attachment.InstanceId == instId {
+ c.Logf("found instance %v NIC", instId)
+ newNIC = &iface
+ break
+ }
+ }
+ if newNIC != nil {
+ break
+ }
+ }
+ if newNIC == nil {
+ c.Fatalf("timeout while waiting for the NIC to appear")
+ }
+
+ c.Check(newNIC.PrivateIPAddress, Matches, `^10\.7\.1\.\d+$`)
+ c.Check(newNIC.PrivateIPs, HasLen, 1)
+
+ // Now let's try assigning some more private IPs.
+ ips := []string{"10.7.1.25", "10.7.1.30"}
+ _, err = s.ec2.AssignPrivateIPAddresses(newNIC.Id, ips, 0, false)
+ c.Assert(err, IsNil)
+
+ expectIPs := append([]string{newNIC.PrivateIPAddress}, ips...)
+ s.waitForAddresses(c, newNIC.Id, expectIPs, false)
+
+ // Try using SecondaryPrivateIPCount.
+ _, err = s.ec2.AssignPrivateIPAddresses(newNIC.Id, nil, 2, false)
+ c.Assert(err, IsNil)
+
+ expectIPs = append(expectIPs, []string{"10.7.1.*", "10.7.1.*"}...)
+ ips = s.waitForAddresses(c, newNIC.Id, expectIPs, true)
+
+ // And finally, unassign them all, except the primary.
+ _, err = s.ec2.UnassignPrivateIPAddresses(newNIC.Id, ips)
+ c.Assert(err, IsNil)
+
+ expectIPs = []string{newNIC.PrivateIPAddress}
+ s.waitForAddresses(c, newNIC.Id, expectIPs, false)
+}
+
+func (s *ServerTests) waitForAddresses(c *C, nicId string, ips []string, skipPrimary bool) []string {
+ // Wait for the given IPs to appear on the NIC, retrying if needed.
+ testAttempt := aws.AttemptStrategy{
+ Total: 5 * time.Minute,
+ Delay: 5 * time.Second,
+ }
+ for a := testAttempt.Start(); a.Next(); {
+ c.Logf("waiting for %v IPs on NIC %v", ips, nicId)
+ resp, err := s.ec2.NetworkInterfaces([]string{nicId}, nil)
+ if err != nil {
+ c.Logf("NetworkInterfaces returned: %v; retrying...", err)
+ continue
+ }
+ if len(resp.Interfaces) != 1 {
+ c.Logf("found %d NICs; retrying", len(resp.Interfaces))
+ continue
+ }
+ iface := resp.Interfaces[0]
+ if len(iface.PrivateIPs) != len(ips) {
+ c.Logf("addresses in %v: %v; still waiting", iface.Id, iface.PrivateIPs)
+ continue
+ }
+
+ var foundIPs []string
+ for i, ip := range iface.PrivateIPs {
+ if strings.HasSuffix(ips[i], ".*") {
+ c.Check(ip.Address, Matches, ips[i])
+ } else {
+ c.Check(ip.Address, Equals, ips[i])
+ }
+ if skipPrimary && ip.IsPrimary {
+ continue
+ }
+ foundIPs = append(foundIPs, ip.Address)
+ }
+ c.Logf("all addresses updated")
+ return foundIPs
+ }
+ c.Fatalf("timeout while waiting for the IPs to get updated")
+ return nil
+}
diff --git a/vendor/src/launchpad.net/goamz/ec2/responses_test.go b/vendor/src/launchpad.net/goamz/ec2/responses_test.go
new file mode 100644
index 0000000000..7b999c6a5f
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/ec2/responses_test.go
@@ -0,0 +1,1030 @@
+package ec2_test
+
+var ErrorDump = `
+
+UnsupportedOperation
+AMIs with an instance-store root device are not supported for the instance type 't1.micro'.
+0503f4e9-bbd6-483c-b54f-c4ae9f3b30f4
+`
+
+// http://goo.gl/Mcm3b
+var RunInstancesExample = `
+
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+ r-47a5402e
+ 999988887777
+
+ -
+ sg-67ad940e
+ default
+
+
+
+ -
+ i-2ba64342
+ ami-60a54009
+
+
0
+ pending
+
+
+
+ example-key-name
+ 0
+ m1.small
+ 2007-08-07T11:51:50.000Z
+
+ us-east-1b
+
+
+ enabled
+
+ paravirtual
+ subnet-id
+ vpc-id
+ true
+
+
+ xen
+
+ -
+ eni-c6bb50ae
+ subnet-id
+ vpc-id
+ eth0
+ 111122223333
+ in-use
+ 10.0.0.25
+ 11:22:33:44:55:66
+ true
+
+
-
+ sg-1
+ vpc sg-1
+
+ -
+ sg-2
+ vpc sg-2
+
+
+
+ eni-attach-0326646a
+ 0
+ attaching
+ 2011-12-20T08:29:31.000Z
+ true
+
+
+ -
+ 10.0.0.25
+ true
+
+
+
+ -
+ eni-id
+ subnet-id
+ vpc-id
+
+ 111122223333
+ in-use
+ 10.0.1.10
+ 11:22:33:44:55:66
+ true
+
+
-
+ sg-id
+ vpc default
+
+
+
+ eni-attach-id
+ 1
+ attaching
+ 2011-12-20T08:29:31.000Z
+ false
+
+
+ -
+ 10.0.1.10
+ true
+
+ -
+ 10.0.1.20
+ false
+
+
+
+
+
+ -
+ i-2bc64242
+ ami-60a54009
+
+
0
+ pending
+
+
+
+ example-key-name
+ 1
+ m1.small
+ 2007-08-07T11:51:50.000Z
+
+ us-east-1b
+
+
+ enabled
+
+ paravirtual
+
+
+ xen
+
+ -
+ i-2be64332
+ ami-60a54009
+
+
0
+ pending
+
+
+
+ example-key-name
+ 2
+ m1.small
+ 2007-08-07T11:51:50.000Z
+
+ us-east-1b
+
+
+ enabled
+
+ paravirtual
+
+
+ xen
+
+
+
+`
+
+// http://goo.gl/3BKHj
+var TerminateInstancesExample = `
+
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+
+ -
+ i-3ea74257
+
+
32
+ shutting-down
+
+
+ 16
+ running
+
+
+
+
+`
+
+// http://goo.gl/mLbmw
+var DescribeInstancesExample1 = `
+
+ 98e3c9a4-848c-4d6d-8e8a-b1bdEXAMPLE
+
+ -
+ r-b27e30d9
+ 999988887777
+
+
-
+ sg-67ad940e
+ default
+
+
+
+ -
+ i-c5cd56af
+ ami-1a2b3c4d
+
+
16
+ running
+
+ domU-12-31-39-10-56-34.compute-1.internal
+ ec2-174-129-165-232.compute-1.amazonaws.com
+
+ GSG_Keypair
+ 0
+
+ m1.small
+ 2010-08-17T01:15:18.000Z
+
+ us-east-1b
+
+
+ aki-94c527fd
+ ari-96c527ff
+
+ disabled
+
+ 10.198.85.190
+ 174.129.165.232
+ i386
+ ebs
+ /dev/sda1
+
+ -
+ /dev/sda1
+
+ vol-a082c1c9
+ attached
+ 2010-08-17T01:15:21.000Z
+ false
+
+
+
+ spot
+ sir-7a688402
+ paravirtual
+
+
+ xen
+
+ -
+ sg-67ad940e
+ default
+
+
+
+
+ 854251627541
+
+ -
+ r-b67e30dd
+ 999988887777
+
+
-
+ sg-67ad940e
+ default
+
+
+
+ -
+ i-d9cd56b3
+ ami-1a2b3c4d
+
+
16
+ running
+
+ domU-12-31-39-10-54-E5.compute-1.internal
+ ec2-184-73-58-78.compute-1.amazonaws.com
+
+ GSG_Keypair
+ 0
+
+ m1.large
+ 2010-08-17T01:15:19.000Z
+
+ us-east-1b
+
+
+ aki-94c527fd
+ ari-96c527ff
+
+ disabled
+
+ 10.198.87.19
+ 184.73.58.78
+ i386
+ ebs
+ /dev/sda1
+
+ -
+ /dev/sda1
+
+ vol-a282c1cb
+ attached
+ 2010-08-17T01:15:23.000Z
+ false
+
+
+
+ spot
+ sir-55a3aa02
+ paravirtual
+
+
+ xen
+
+
+ 854251627541
+
+
+
+`
+
+// http://goo.gl/mLbmw
+var DescribeInstancesExample2 = `
+
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+
+ -
+ r-bc7e30d7
+ 999988887777
+
+
-
+ sg-67ad940e
+ default
+
+
+
+ -
+ i-c7cd56ad
+ ami-b232d0db
+
+
16
+ running
+
+ domU-12-31-39-01-76-06.compute-1.internal
+ ec2-72-44-52-124.compute-1.amazonaws.com
+ GSG_Keypair
+ 0
+
+ m1.small
+ 2010-08-17T01:15:16.000Z
+
+ us-east-1b
+
+ aki-94c527fd
+ ari-96c527ff
+
+ disabled
+
+ 10.255.121.240
+ 72.44.52.124
+ i386
+ ebs
+ /dev/sda1
+
+ -
+ /dev/sda1
+
+ vol-a482c1cd
+ attached
+ 2010-08-17T01:15:26.000Z
+ true
+
+
+
+ paravirtual
+
+
+ -
+ webserver
+
+
+ -
+ stack
+ Production
+
+
+ xen
+
+
+
+
+
+`
+
+// http://goo.gl/V0U25
+var DescribeImagesExample = `
+
+ 4a4a27a2-2e7c-475d-b35b-ca822EXAMPLE
+
+ -
+ ami-a2469acf
+ aws-marketplace/example-marketplace-amzn-ami.1
+ available
+ 123456789999
+ true
+
+
-
+ a1b2c3d4e5f6g7h8i9j10k11
+ marketplace
+
+
+ i386
+ machine
+ aki-805ea7e9
+ aws-marketplace
+ example-marketplace-amzn-ami.1
+ Amazon Linux AMI i386 EBS
+ ebs
+ /dev/sda1
+
+ -
+ /dev/sda1
+
+ snap-787e9403
+ 8
+ true
+
+
+
+ paravirtual
+ xen
+
+
+
+`
+
+// http://goo.gl/ttcda
+var CreateSnapshotExample = `
+
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+ snap-78a54011
+ vol-4d826724
+ pending
+ 2008-05-07T12:51:50.000Z
+
+ 111122223333
+ 10
+ Daily Backup
+
+`
+
+// http://goo.gl/vwU1y
+var DeleteSnapshotExample = `
+
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+ true
+
+`
+
+// http://goo.gl/nkovs
+var DescribeSnapshotsExample = `
+
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+
+ -
+ snap-1a2b3c4d
+ vol-8875daef
+ pending
+ 2010-07-29T04:12:01.000Z
+
+ 111122223333
+ 15
+ Daily Backup
+
+
-
+ Purpose
+ demo_db_14_backup
+
+
+
+
+
+`
+
+// http://goo.gl/Eo7Yl
+var CreateSecurityGroupExample = `
+
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+ true
+ sg-67ad940e
+
+`
+
+// http://goo.gl/k12Uy
+var DescribeSecurityGroupsExample = `
+
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+
+ -
+ 999988887777
+ WebServers
+ sg-67ad940e
+ Web Servers
+
+
-
+ tcp
+ 80
+ 80
+
+
+
-
+ 0.0.0.0/0
+
+
+
+
+
+ -
+ 999988887777
+ RangedPortsBySource
+ sg-76abc467
+ Group A
+
+
-
+ tcp
+ 6000
+ 7000
+
+
+
+
+
+
+
+`
+
+// A dump which includes groups within ip permissions.
+var DescribeSecurityGroupsDump = `
+
+
+ 87b92b57-cc6e-48b2-943f-f6f0e5c9f46c
+
+ -
+ 12345
+ default
+ default group
+
+
-
+ icmp
+ -1
+ -1
+
+
-
+ 12345
+ default
+ sg-67ad940e
+
+
+
+
+ -
+ tcp
+ 0
+ 65535
+
+
-
+ 12345
+ other
+ sg-76abc467
+
+
+
+
+
+
+
+
+`
+
+// http://goo.gl/QJJDO
+var DeleteSecurityGroupExample = `
+
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+ true
+
+`
+
+// http://goo.gl/u2sDJ
+var AuthorizeSecurityGroupIngressExample = `
+
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+ true
+
+`
+
+// http://goo.gl/Mz7xr
+var RevokeSecurityGroupIngressExample = `
+
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+ true
+
+`
+
+// http://goo.gl/Vmkqc
+var CreateTagsExample = `
+
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+ true
+
+`
+
+// http://goo.gl/awKeF
+var StartInstancesExample = `
+
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+
+ -
+ i-10a64379
+
+
0
+ pending
+
+
+ 80
+ stopped
+
+
+
+
+`
+
+// http://goo.gl/436dJ
+var StopInstancesExample = `
+
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+
+ -
+ i-10a64379
+
+
64
+ stopping
+
+
+ 16
+ running
+
+
+
+
+`
+
+// http://goo.gl/baoUf
+var RebootInstancesExample = `
+
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+ true
+
+`
+
+// http://goo.gl/ylxT4R
+var DescribeAvailabilityZonesExample1 = `
+
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+
+ -
+ us-east-1a
+ available
+ us-east-1
+
+
+ -
+ us-east-1b
+ available
+ us-east-1
+
+
+ -
+ us-east-1c
+ available
+ us-east-1
+
+
+ -
+ us-east-1d
+ available
+ us-east-1
+
+
+
+
+`
+
+// http://goo.gl/ylxT4R
+var DescribeAvailabilityZonesExample2 = `
+
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+
+ -
+ us-east-1a
+ impaired
+ us-east-1
+
+
+ -
+ us-east-1b
+ unavailable
+ us-east-1
+
+
- us-east-1b is currently down for maintenance.
+
+
+
+
+`
+
+// http://goo.gl/nkwjv
+var CreateVpcExample = `
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+ vpc-1a2b3c4d
+ pending
+ 10.0.0.0/16
+ dopt-1a2b3c4d2
+ default
+
+
+
+`
+
+// http://goo.gl/bcxtbf
+var DeleteVpcExample = `
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+ true
+
+`
+
+// http://goo.gl/Y5kHqG
+var DescribeVpcsExample = `
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+ -
+ vpc-1a2b3c4d
+ available
+ 10.0.0.0/23
+ dopt-7a8b9c2d
+ default
+ false
+
+
+
+
+`
+
+// http://goo.gl/wLPhf
+var CreateSubnetExample = `
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+ subnet-9d4a7b6c
+ pending
+ vpc-1a2b3c4d
+ 10.0.1.0/24
+ 251
+ us-east-1a
+
+
+
+`
+
+// http://goo.gl/KmhcBM
+var DeleteSubnetExample = `
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+ true
+
+`
+
+// http://goo.gl/NTKQVI
+var DescribeSubnetsExample = `
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+ -
+ subnet-9d4a7b6c
+ available
+ vpc-1a2b3c4d
+ 10.0.1.0/24
+ 251
+ us-east-1a
+ false
+ false
+
+
+ -
+ subnet-6e7f829e
+ available
+ vpc-1a2b3c4d
+ 10.0.0.0/24
+ 251
+ us-east-1a
+ false
+ false
+
+
+
+
+`
+
+// http://goo.gl/ze3VhA
+var CreateNetworkInterfaceExample = `
+
+ 8dbe591e-5a22-48cb-b948-dd0aadd55adf
+
+ eni-cfca76a6
+ subnet-b2a249da
+ vpc-c31dafaa
+ ap-southeast-1b
+
+ 251839141158
+ false
+ available
+ 02:74:b0:72:79:61
+ 10.0.2.157
+ true
+
+ -
+ sg-1a2b3c4d
+ default
+
+
+
+
+ -
+ 10.0.2.157
+ true
+
+
+
+
+`
+
+// http://goo.gl/MC1yOj
+var DeleteNetworkInterfaceExample = `
+
+ e1c6d73b-edaa-4e62-9909-6611404e1739
+ true
+
+`
+
+// http://goo.gl/2LcXtM
+var DescribeNetworkInterfacesExample = `
+
+ fc45294c-006b-457b-bab9-012f5b3b0e40
+
+ -
+ eni-0f62d866
+ subnet-c53c87ac
+ vpc-cc3c87a5
+ ap-southeast-1b
+
+ 053230519467
+ false
+ in-use
+ 02:81:60:cb:27:37
+ 10.0.0.146
+ true
+
+
-
+ sg-3f4b5653
+ default
+
+
+
+ eni-attach-6537fc0c
+ i-22197876
+ 053230519467
+ 0
+ attached
+ 2012-07-01T21:45:27.000Z
+ true
+
+
+
+ -
+ 10.0.0.146
+ true
+
+ -
+ 10.0.0.148
+ false
+
+ -
+ 10.0.0.150
+ false
+
+
+
+ -
+ eni-a66ed5cf
+ subnet-cd8a35a4
+ vpc-f28a359b
+ ap-southeast-1b
+ Primary network interface
+ 053230519467
+ false
+ in-use
+ 02:78:d7:00:8a:1e
+ 10.0.1.233
+ true
+
+
-
+ sg-a2a0b2ce
+ quick-start-1
+
+
+
+ eni-attach-a99c57c0
+ i-886401dc
+ 053230519467
+ 0
+ attached
+ 2012-06-27T20:08:44.000Z
+ true
+
+
+
+ -
+ 10.0.1.233
+ true
+
+ -
+ 10.0.1.20
+ false
+
+
+
+
+
+`
+
+// http://goo.gl/rEbSii
+var AttachNetworkInterfaceExample = `
+
+ ace8cd1e-e685-4e44-90fb-92014d907212
+ eni-attach-d94b09b0
+
+`
+
+// http://goo.gl/0Xc1px
+var DetachNetworkInterfaceExample = `
+
+ ce540707-0635-46bc-97da-33a8a362a0e8
+ true
+
+`
+
+// http://goo.gl/MoeH0L
+var AssignPrivateIpAddressesExample = `
+
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+ true
+
+`
+
+// http://goo.gl/RjGZdB
+var UnassignPrivateIpAddressesExample = `
+
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+ true
+
+`
+
+// http://goo.gl/hBc28j
+var DescribeAccountAttributesExample = `
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+ -
+ supported-platforms
+
+
-
+ VPC
+
+
+
+
+
+`
+
+// http://goo.gl/hBc28j
+var DescribeAccountAttributesExample2 = `
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+ -
+ default-vpc
+
+
-
+ vpc-xxxxxxxx
+
+
+
+
+
+`
+
+// http://goo.gl/hBc28j
+var DescribeAccountAttributesExample3 = `
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+ -
+ default-vpc
+
+
-
+ none
+
+
+
+
+
+`
diff --git a/vendor/src/launchpad.net/goamz/ec2/sign.go b/vendor/src/launchpad.net/goamz/ec2/sign.go
new file mode 100644
index 0000000000..3c0292feda
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/ec2/sign.go
@@ -0,0 +1,42 @@
+package ec2
+
+import (
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/base64"
+ "launchpad.net/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"
+
+ // 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)
+}
diff --git a/vendor/src/launchpad.net/goamz/ec2/sign_test.go b/vendor/src/launchpad.net/goamz/ec2/sign_test.go
new file mode 100644
index 0000000000..36f16ea41f
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/ec2/sign_test.go
@@ -0,0 +1,68 @@
+package ec2_test
+
+import (
+ "launchpad.net/goamz/aws"
+ "launchpad.net/goamz/ec2"
+ . "launchpad.net/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)
+}
diff --git a/vendor/src/launchpad.net/goamz/ec2/subnets.go b/vendor/src/launchpad.net/goamz/ec2/subnets.go
new file mode 100644
index 0000000000..4014b41816
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/ec2/subnets.go
@@ -0,0 +1,109 @@
+//
+// goamz - Go packages to interact with the Amazon Web Services.
+//
+// https://wiki.ubuntu.com/goamz
+//
+// Copyright (c) 2014 Canonical Ltd.
+//
+
+package ec2
+
+import (
+ "strconv"
+)
+
+// Subnet describes an Amazon VPC subnet.
+//
+// See http://goo.gl/CdkvO2 for more details.
+type Subnet struct {
+ Id string `xml:"subnetId"`
+ State string `xml:"state"`
+ VPCId string `xml:"vpcId"`
+ CIDRBlock string `xml:"cidrBlock"`
+ AvailableIPCount int `xml:"availableIpAddressCount"`
+ AvailZone string `xml:"availabilityZone"`
+ DefaultForAZ bool `xml:"defaultForAz"`
+ MapPublicIPOnLaunch bool `xml:"mapPublicIpOnLaunch"`
+ Tags []Tag `xml:"tagSet>item"`
+}
+
+// CreateSubnetResp is the response to a CreateSubnet request.
+//
+// See http://goo.gl/wLPhfI for more details.
+type CreateSubnetResp struct {
+ RequestId string `xml:"requestId"`
+ Subnet Subnet `xml:"subnet"`
+}
+
+// CreateSubnet creates a subnet in an existing VPC.
+//
+// The vpcId and cidrBlock parameters specify the VPC id and CIDR
+// block respectively - these cannot be changed after creation. The
+// subnet's CIDR block can be the same as the VPC's CIDR block
+// (assuming a single subnet is wanted), or a subset of the VPC's CIDR
+// block. If more than one subnet is created in a VPC, their CIDR
+// blocks must not overlap. The smallest subnet (and VPC) that can be
+// created uses a /28 netmask (16 IP addresses), and the largest uses
+// a /16 netmask (65,536 IP addresses).
+//
+// availZone may be empty, an availability zone is automatically
+// selected.
+//
+// See http://goo.gl/wLPhfI for more details.
+func (ec2 *EC2) CreateSubnet(vpcId, cidrBlock, availZone string) (resp *CreateSubnetResp, err error) {
+ params := makeParamsVPC("CreateSubnet")
+ params["VpcId"] = vpcId
+ params["CidrBlock"] = cidrBlock
+ if availZone != "" {
+ params["AvailabilityZone"] = availZone
+ }
+ resp = &CreateSubnetResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// DeleteSubnet deletes the specified subnet. All running instances in
+// the subnet must have been terminated.
+//
+// See http://goo.gl/KmhcBM for more details.
+func (ec2 *EC2) DeleteSubnet(id string) (resp *SimpleResp, err error) {
+ params := makeParamsVPC("DeleteSubnet")
+ params["SubnetId"] = id
+ resp = &SimpleResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// SubnetsResp is the response to a Subnets request.
+//
+// See http://goo.gl/NTKQVI for more details.
+type SubnetsResp struct {
+ RequestId string `xml:"requestId"`
+ Subnets []Subnet `xml:"subnetSet>item"`
+}
+
+// Subnets returns one or more subnets. Both parameters are optional,
+// and if specified will limit the returned subnets to the matching
+// ids or filtering rules.
+//
+// See http://goo.gl/NTKQVI for more details.
+func (ec2 *EC2) Subnets(ids []string, filter *Filter) (resp *SubnetsResp, err error) {
+ params := makeParamsVPC("DescribeSubnets")
+ for i, id := range ids {
+ params["SubnetId."+strconv.Itoa(i+1)] = id
+ }
+ filter.addParams(params)
+
+ resp = &SubnetsResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
diff --git a/vendor/src/launchpad.net/goamz/ec2/subnets_test.go b/vendor/src/launchpad.net/goamz/ec2/subnets_test.go
new file mode 100644
index 0000000000..a897e1b507
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/ec2/subnets_test.go
@@ -0,0 +1,247 @@
+//
+// goamz - Go packages to interact with the Amazon Web Services.
+//
+// https://wiki.ubuntu.com/goamz
+//
+// Copyright (c) 2014 Canonical Ltd.
+//
+
+package ec2_test
+
+import (
+ "launchpad.net/goamz/aws"
+ "launchpad.net/goamz/ec2"
+ . "launchpad.net/gocheck"
+ "time"
+)
+
+// Subnet tests with example responses
+
+func (s *S) TestCreateSubnetExample(c *C) {
+ testServer.Response(200, nil, CreateSubnetExample)
+
+ resp, err := s.ec2.CreateSubnet("vpc-1a2b3c4d", "10.0.1.0/24", "us-east-1a")
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Form["Action"], DeepEquals, []string{"CreateSubnet"})
+ c.Assert(req.Form["VpcId"], DeepEquals, []string{"vpc-1a2b3c4d"})
+ c.Assert(req.Form["CidrBlock"], DeepEquals, []string{"10.0.1.0/24"})
+ c.Assert(req.Form["AvailabilityZone"], DeepEquals, []string{"us-east-1a"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE")
+ subnet := resp.Subnet
+ c.Check(subnet.Id, Equals, "subnet-9d4a7b6c")
+ c.Check(subnet.State, Equals, "pending")
+ c.Check(subnet.VPCId, Equals, "vpc-1a2b3c4d")
+ c.Check(subnet.CIDRBlock, Equals, "10.0.1.0/24")
+ c.Check(subnet.AvailableIPCount, Equals, 251)
+ c.Check(subnet.AvailZone, Equals, "us-east-1a")
+ c.Check(subnet.Tags, HasLen, 0)
+}
+
+func (s *S) TestDeleteSubnetExample(c *C) {
+ testServer.Response(200, nil, DeleteSubnetExample)
+
+ resp, err := s.ec2.DeleteSubnet("subnet-id")
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Form["Action"], DeepEquals, []string{"DeleteSubnet"})
+ c.Assert(req.Form["SubnetId"], DeepEquals, []string{"subnet-id"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE")
+}
+
+func (s *S) TestSubnetsExample(c *C) {
+ testServer.Response(200, nil, DescribeSubnetsExample)
+
+ ids := []string{"subnet-9d4a7b6c", "subnet-6e7f829e"}
+ resp, err := s.ec2.Subnets(ids, nil)
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeSubnets"})
+ c.Assert(req.Form["SubnetId.1"], DeepEquals, []string{ids[0]})
+ c.Assert(req.Form["SubnetId.2"], DeepEquals, []string{ids[1]})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE")
+ c.Check(resp.Subnets, HasLen, 2)
+ subnet := resp.Subnets[0]
+ c.Check(subnet.Id, Equals, "subnet-9d4a7b6c")
+ c.Check(subnet.State, Equals, "available")
+ c.Check(subnet.VPCId, Equals, "vpc-1a2b3c4d")
+ c.Check(subnet.CIDRBlock, Equals, "10.0.1.0/24")
+ c.Check(subnet.AvailableIPCount, Equals, 251)
+ c.Check(subnet.AvailZone, Equals, "us-east-1a")
+ c.Check(subnet.DefaultForAZ, Equals, false)
+ c.Check(subnet.MapPublicIPOnLaunch, Equals, false)
+ c.Check(subnet.Tags, HasLen, 0)
+ subnet = resp.Subnets[1]
+ c.Check(subnet.Id, Equals, "subnet-6e7f829e")
+ c.Check(subnet.State, Equals, "available")
+ c.Check(subnet.VPCId, Equals, "vpc-1a2b3c4d")
+ c.Check(subnet.CIDRBlock, Equals, "10.0.0.0/24")
+ c.Check(subnet.AvailableIPCount, Equals, 251)
+ c.Check(subnet.AvailZone, Equals, "us-east-1a")
+ c.Check(subnet.DefaultForAZ, Equals, false)
+ c.Check(subnet.MapPublicIPOnLaunch, Equals, false)
+ c.Check(subnet.Tags, HasLen, 0)
+}
+
+// Subnet tests run against either a local test server or live on EC2.
+
+func (s *ServerTests) TestSubnets(c *C) {
+ resp, err := s.ec2.CreateVPC("10.2.0.0/16", "")
+ c.Assert(err, IsNil)
+ vpcId := resp.VPC.Id
+ defer s.deleteVPCs(c, []string{vpcId})
+
+ resp1 := s.createSubnet(c, vpcId, "10.2.1.0/24", "")
+ assertSubnet(c, resp1.Subnet, "", vpcId, "10.2.1.0/24")
+ id1 := resp1.Subnet.Id
+
+ resp2, err := s.ec2.CreateSubnet(vpcId, "10.2.2.0/24", "")
+ c.Assert(err, IsNil)
+ assertSubnet(c, resp2.Subnet, "", vpcId, "10.2.2.0/24")
+ id2 := resp2.Subnet.Id
+
+ // We only check for the subnets we just created, because the user
+ // might have others in his account (when testing against the EC2
+ // servers). In some cases it takes a short while until both
+ // subnets are created, so we need to retry a few times to make
+ // sure.
+ testAttempt := aws.AttemptStrategy{
+ Total: 2 * time.Minute,
+ Delay: 5 * time.Second,
+ }
+ var list *ec2.SubnetsResp
+ done := false
+ for a := testAttempt.Start(); a.Next(); {
+ c.Logf("waiting for %v to be created", []string{id1, id2})
+ list, err = s.ec2.Subnets(nil, nil)
+ if err != nil {
+ c.Logf("retrying; Subnets returned: %v", err)
+ continue
+ }
+ found := 0
+ for _, subnet := range list.Subnets {
+ c.Logf("found subnet %v", subnet)
+ switch subnet.Id {
+ case id1:
+ assertSubnet(c, subnet, id1, vpcId, resp1.Subnet.CIDRBlock)
+ found++
+ case id2:
+ assertSubnet(c, subnet, id2, vpcId, resp2.Subnet.CIDRBlock)
+ found++
+ }
+ if found == 2 {
+ done = true
+ break
+ }
+ }
+ if done {
+ c.Logf("all subnets were created")
+ break
+ }
+ }
+ if !done {
+ c.Fatalf("timeout while waiting for subnets %v", []string{id1, id2})
+ }
+
+ list, err = s.ec2.Subnets([]string{id1}, nil)
+ c.Assert(err, IsNil)
+ c.Assert(list.Subnets, HasLen, 1)
+ assertSubnet(c, list.Subnets[0], id1, vpcId, resp1.Subnet.CIDRBlock)
+
+ f := ec2.NewFilter()
+ f.Add("cidr", resp2.Subnet.CIDRBlock)
+ list, err = s.ec2.Subnets(nil, f)
+ c.Assert(err, IsNil)
+ c.Assert(list.Subnets, HasLen, 1)
+ assertSubnet(c, list.Subnets[0], id2, vpcId, resp2.Subnet.CIDRBlock)
+
+ _, err = s.ec2.DeleteSubnet(id1)
+ c.Assert(err, IsNil)
+ _, err = s.ec2.DeleteSubnet(id2)
+ c.Assert(err, IsNil)
+}
+
+// createSubnet ensures a subnet with the given vpcId and cidrBlock
+// gets created, retrying a few times with a timeout. This needs to be
+// done when testing against EC2 servers, because if the VPC was just
+// created it might take some time for it to show up, so the subnet
+// can be created.
+func (s *ServerTests) createSubnet(c *C, vpcId, cidrBlock, availZone string) *ec2.CreateSubnetResp {
+ testAttempt := aws.AttemptStrategy{
+ Total: 2 * time.Minute,
+ Delay: 5 * time.Second,
+ }
+ for a := testAttempt.Start(); a.Next(); {
+ resp, err := s.ec2.CreateSubnet(vpcId, cidrBlock, availZone)
+ if errorCode(err) == "InvalidVpcID.NotFound" {
+ c.Logf("VPC %v not created yet; retrying", vpcId)
+ continue
+ }
+ if err != nil {
+ c.Logf("retrying; CreateSubnet returned: %v", err)
+ continue
+ }
+ return resp
+ }
+ c.Fatalf("timeout while waiting for VPC and subnet")
+ return nil
+}
+
+// deleteSubnets ensures the given subnets are deleted, by retrying
+// until a timeout or all subnets cannot be found anymore. This
+// should be used to make sure tests leave no subnets around.
+func (s *ServerTests) deleteSubnets(c *C, ids []string) {
+ testAttempt := aws.AttemptStrategy{
+ Total: 2 * time.Minute,
+ Delay: 5 * time.Second,
+ }
+ for a := testAttempt.Start(); a.Next(); {
+ deleted := 0
+ c.Logf("deleting subnets %v", ids)
+ for _, id := range ids {
+ _, err := s.ec2.DeleteSubnet(id)
+ if err == nil || errorCode(err) == "InvalidSubnetID.NotFound" {
+ c.Logf("subnet %s deleted", id)
+ deleted++
+ continue
+ }
+ if err != nil {
+ c.Logf("retrying; DeleteSubnet returned: %v", err)
+ }
+ }
+ if deleted == len(ids) {
+ c.Logf("all subnets deleted")
+ return
+ }
+ }
+ c.Fatalf("timeout while waiting %v subnets to get deleted!", ids)
+}
+
+func assertSubnet(c *C, obtained ec2.Subnet, expectId, expectVpcId, expectCidr string) {
+ if expectId != "" {
+ c.Check(obtained.Id, Equals, expectId)
+ } else {
+ c.Check(obtained.Id, Matches, `^subnet-[0-9a-f]+$`)
+ }
+ c.Check(obtained.State, Matches, "(available|pending)")
+ if expectVpcId != "" {
+ c.Check(obtained.VPCId, Equals, expectVpcId)
+ } else {
+ c.Check(obtained.VPCId, Matches, `^vpc-[0-9a-f]+$`)
+ }
+ if expectCidr != "" {
+ c.Check(obtained.CIDRBlock, Equals, expectCidr)
+ } else {
+ c.Check(obtained.CIDRBlock, Matches, `^\d+\.\d+\.\d+\.\d+/\d+$`)
+ }
+ c.Check(obtained.AvailZone, Not(Equals), "")
+ c.Check(obtained.AvailableIPCount, Not(Equals), 0)
+ c.Check(obtained.DefaultForAZ, Equals, false)
+ c.Check(obtained.MapPublicIPOnLaunch, Equals, false)
+}
diff --git a/vendor/src/launchpad.net/goamz/ec2/vpc.go b/vendor/src/launchpad.net/goamz/ec2/vpc.go
new file mode 100644
index 0000000000..68ab434fb2
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/ec2/vpc.go
@@ -0,0 +1,105 @@
+//
+// goamz - Go packages to interact with the Amazon Web Services.
+//
+// https://wiki.ubuntu.com/goamz
+//
+// Copyright (c) 2014 Canonical Ltd.
+//
+
+package ec2
+
+import (
+ "strconv"
+)
+
+// VPC describes a Virtual Private Cloud (VPC).
+//
+// See http://goo.gl/Uy6ZLL for more details.
+type VPC struct {
+ Id string `xml:"vpcId"`
+ State string `xml:"state"`
+ CIDRBlock string `xml:"cidrBlock"`
+ DHCPOptionsId string `xml:"dhcpOptionsId"`
+ Tags []Tag `xml:"tagSet>item"`
+ InstanceTenancy string `xml:"instanceTenancy"`
+ IsDefault bool `xml:"isDefault"`
+}
+
+// CreateVPCResp is the response to a CreateVPC request.
+//
+// See http://goo.gl/nkwjvN for more details.
+type CreateVPCResp struct {
+ RequestId string `xml:"requestId"`
+ VPC VPC `xml:"vpc"`
+}
+
+// CreateVPC creates a VPC with the specified CIDR block.
+//
+// The smallest VPC that can be created uses a /28 netmask (16 IP
+// addresses), and the largest uses a /16 netmask (65,536 IP
+// addresses).
+//
+// The instanceTenancy value holds the tenancy options for instances
+// launched into the VPC - either "default" or "dedicated".
+//
+// See http://goo.gl/nkwjvN for more details.
+func (ec2 *EC2) CreateVPC(CIDRBlock, instanceTenancy string) (resp *CreateVPCResp, err error) {
+ params := makeParamsVPC("CreateVpc")
+ params["CidrBlock"] = CIDRBlock
+ if instanceTenancy == "" {
+ instanceTenancy = "default"
+ }
+ params["InstanceTenancy"] = instanceTenancy
+ resp = &CreateVPCResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// DeleteVPC deletes the VPC with the specified id. All gateways and
+// resources that are associated with the VPC must have been
+// previously deleted, including instances running in the VPC, and
+// non-default security groups and route tables associated with the
+// VPC.
+//
+// See http://goo.gl/bcxtbf for more details.
+func (ec2 *EC2) DeleteVPC(id string) (resp *SimpleResp, err error) {
+ params := makeParamsVPC("DeleteVpc")
+ params["VpcId"] = id
+ resp = &SimpleResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// VPCsResp is the response to a VPCs request.
+//
+// See http://goo.gl/Y5kHqG for more details.
+type VPCsResp struct {
+ RequestId string `xml:"requestId"`
+ VPCs []VPC `xml:"vpcSet>item"`
+}
+
+// VPCs describes one or more VPCs. Both parameters are optional, and
+// if specified will limit the returned VPCs to the matching ids or
+// filtering rules.
+//
+// See http://goo.gl/Y5kHqG for more details.
+func (ec2 *EC2) VPCs(ids []string, filter *Filter) (resp *VPCsResp, err error) {
+ params := makeParamsVPC("DescribeVpcs")
+ for i, id := range ids {
+ params["VpcId."+strconv.Itoa(i+1)] = id
+ }
+ filter.addParams(params)
+
+ resp = &VPCsResp{}
+ err = ec2.query(params, resp)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
diff --git a/vendor/src/launchpad.net/goamz/ec2/vpc_test.go b/vendor/src/launchpad.net/goamz/ec2/vpc_test.go
new file mode 100644
index 0000000000..6160e9a87a
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/ec2/vpc_test.go
@@ -0,0 +1,196 @@
+//
+// goamz - Go packages to interact with the Amazon Web Services.
+//
+// https://wiki.ubuntu.com/goamz
+//
+// Copyright (c) 2014 Canonical Ltd.
+//
+
+package ec2_test
+
+import (
+ "launchpad.net/goamz/aws"
+ "launchpad.net/goamz/ec2"
+ . "launchpad.net/gocheck"
+ "time"
+)
+
+// VPC tests with example responses
+
+func (s *S) TestCreateVPCExample(c *C) {
+ testServer.Response(200, nil, CreateVpcExample)
+
+ resp, err := s.ec2.CreateVPC("10.0.0.0/16", "default")
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Form["Action"], DeepEquals, []string{"CreateVpc"})
+ c.Assert(req.Form["CidrBlock"], DeepEquals, []string{"10.0.0.0/16"})
+ c.Assert(req.Form["InstanceTenancy"], DeepEquals, []string{"default"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE")
+ vpc := resp.VPC
+ c.Check(vpc.Id, Equals, "vpc-1a2b3c4d")
+ c.Check(vpc.State, Equals, "pending")
+ c.Check(vpc.CIDRBlock, Equals, "10.0.0.0/16")
+ c.Check(vpc.DHCPOptionsId, Equals, "dopt-1a2b3c4d2")
+ c.Check(vpc.Tags, HasLen, 0)
+ c.Check(vpc.IsDefault, Equals, false)
+ c.Check(vpc.InstanceTenancy, Equals, "default")
+}
+
+func (s *S) TestDeleteVPCExample(c *C) {
+ testServer.Response(200, nil, DeleteVpcExample)
+
+ resp, err := s.ec2.DeleteVPC("vpc-id")
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Form["Action"], DeepEquals, []string{"DeleteVpc"})
+ c.Assert(req.Form["VpcId"], DeepEquals, []string{"vpc-id"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE")
+}
+
+func (s *S) TestVPCsExample(c *C) {
+ testServer.Response(200, nil, DescribeVpcsExample)
+
+ resp, err := s.ec2.VPCs([]string{"vpc-1a2b3c4d"}, nil)
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeVpcs"})
+ c.Assert(req.Form["VpcId.1"], DeepEquals, []string{"vpc-1a2b3c4d"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE")
+ c.Assert(resp.VPCs, HasLen, 1)
+ vpc := resp.VPCs[0]
+ c.Check(vpc.Id, Equals, "vpc-1a2b3c4d")
+ c.Check(vpc.State, Equals, "available")
+ c.Check(vpc.CIDRBlock, Equals, "10.0.0.0/23")
+ c.Check(vpc.DHCPOptionsId, Equals, "dopt-7a8b9c2d")
+ c.Check(vpc.Tags, HasLen, 0)
+ c.Check(vpc.IsDefault, Equals, false)
+ c.Check(vpc.InstanceTenancy, Equals, "default")
+}
+
+// VPC tests to run against either a local test server or live on EC2.
+
+func (s *ServerTests) TestVPCs(c *C) {
+ resp1, err := s.ec2.CreateVPC("10.0.0.0/16", "")
+ c.Assert(err, IsNil)
+ assertVPC(c, resp1.VPC, "", "10.0.0.0/16")
+ id1 := resp1.VPC.Id
+
+ resp2, err := s.ec2.CreateVPC("10.1.0.0/16", "default")
+ c.Assert(err, IsNil)
+ assertVPC(c, resp2.VPC, "", "10.1.0.0/16")
+ id2 := resp2.VPC.Id
+
+ // We only check for the VPCs we just created, because the user
+ // might have others in his account (when testing against the EC2
+ // servers). In some cases it takes a short while until both VPCs
+ // are created, so we need to retry a few times to make sure.
+ var list *ec2.VPCsResp
+ done := false
+ testAttempt := aws.AttemptStrategy{
+ Total: 2 * time.Minute,
+ Delay: 5 * time.Second,
+ }
+ for a := testAttempt.Start(); a.Next(); {
+ c.Logf("waiting for %v to be created", []string{id1, id2})
+ list, err = s.ec2.VPCs(nil, nil)
+ if err != nil {
+ c.Logf("retrying; VPCs returned: %v", err)
+ continue
+ }
+ found := 0
+ for _, vpc := range list.VPCs {
+ c.Logf("found VPC %v", vpc)
+ switch vpc.Id {
+ case id1:
+ assertVPC(c, vpc, id1, resp1.VPC.CIDRBlock)
+ found++
+ case id2:
+ assertVPC(c, vpc, id2, resp2.VPC.CIDRBlock)
+ found++
+ }
+ if found == 2 {
+ done = true
+ break
+ }
+ }
+ if done {
+ c.Logf("all VPCs were created")
+ break
+ }
+ }
+ if !done {
+ c.Fatalf("timeout while waiting for VPCs %v", []string{id1, id2})
+ }
+
+ list, err = s.ec2.VPCs([]string{id1}, nil)
+ c.Assert(err, IsNil)
+ c.Assert(list.VPCs, HasLen, 1)
+ assertVPC(c, list.VPCs[0], id1, resp1.VPC.CIDRBlock)
+
+ f := ec2.NewFilter()
+ f.Add("cidr", resp2.VPC.CIDRBlock)
+ list, err = s.ec2.VPCs(nil, f)
+ c.Assert(err, IsNil)
+ c.Assert(list.VPCs, HasLen, 1)
+ assertVPC(c, list.VPCs[0], id2, resp2.VPC.CIDRBlock)
+
+ _, err = s.ec2.DeleteVPC(id1)
+ c.Assert(err, IsNil)
+ _, err = s.ec2.DeleteVPC(id2)
+ c.Assert(err, IsNil)
+}
+
+// deleteVPCs ensures the given VPCs are deleted, by retrying until a
+// timeout or all VPC cannot be found anymore. This should be used to
+// make sure tests leave no VPCs around.
+func (s *ServerTests) deleteVPCs(c *C, ids []string) {
+ testAttempt := aws.AttemptStrategy{
+ Total: 2 * time.Minute,
+ Delay: 5 * time.Second,
+ }
+ for a := testAttempt.Start(); a.Next(); {
+ deleted := 0
+ c.Logf("deleting VPCs %v", ids)
+ for _, id := range ids {
+ _, err := s.ec2.DeleteVPC(id)
+ if err == nil || errorCode(err) == "InvalidVpcID.NotFound" {
+ c.Logf("VPC %s deleted", id)
+ deleted++
+ continue
+ }
+ if err != nil {
+ c.Logf("retrying; DeleteVPC returned: %v", err)
+ }
+ }
+ if deleted == len(ids) {
+ c.Logf("all VPCs deleted")
+ return
+ }
+ }
+ c.Fatalf("timeout while waiting %v VPCs to get deleted!", ids)
+}
+
+func assertVPC(c *C, obtained ec2.VPC, expectId, expectCidr string) {
+ if expectId != "" {
+ c.Check(obtained.Id, Equals, expectId)
+ } else {
+ c.Check(obtained.Id, Matches, `^vpc-[0-9a-f]+$`)
+ }
+ c.Check(obtained.State, Matches, "(available|pending)")
+ if expectCidr != "" {
+ c.Check(obtained.CIDRBlock, Equals, expectCidr)
+ } else {
+ c.Check(obtained.CIDRBlock, Matches, `^\d+\.\d+\.\d+\.\d+/\d+$`)
+ }
+ c.Check(obtained.DHCPOptionsId, Matches, `^dopt-[0-9a-f]+$`)
+ c.Check(obtained.IsDefault, Equals, false)
+ c.Check(obtained.Tags, HasLen, 0)
+ c.Check(obtained.InstanceTenancy, Matches, "(default|dedicated)")
+}
diff --git a/vendor/src/launchpad.net/goamz/exp/mturk/export_test.go b/vendor/src/launchpad.net/goamz/exp/mturk/export_test.go
new file mode 100644
index 0000000000..aca7771a19
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/exp/mturk/export_test.go
@@ -0,0 +1,9 @@
+package mturk
+
+import (
+ "launchpad.net/goamz/aws"
+)
+
+func Sign(auth aws.Auth, service, method, timestamp string, params map[string]string) {
+ sign(auth, service, method, timestamp, params)
+}
diff --git a/vendor/src/launchpad.net/goamz/exp/mturk/mturk.go b/vendor/src/launchpad.net/goamz/exp/mturk/mturk.go
new file mode 100644
index 0000000000..fd8d8b1a41
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/exp/mturk/mturk.go
@@ -0,0 +1,281 @@
+//
+// goamz - Go packages to interact with the Amazon Web Services.
+//
+// https://wiki.ubuntu.com/goamz
+//
+// Copyright (c) 2011 Canonical Ltd.
+//
+// Written by Graham Miller
+
+// This package is in an experimental state, and does not currently
+// follow conventions and style of the rest of goamz or common
+// Go conventions. It must be polished before it's considered a
+// first-class package in goamz.
+package mturk
+
+import (
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "launchpad.net/goamz/aws"
+ "net/http"
+ //"net/http/httputil"
+ "net/url"
+ "strconv"
+ "time"
+)
+
+type MTurk struct {
+ aws.Auth
+ URL *url.URL
+}
+
+func New(auth aws.Auth) *MTurk {
+ mt := &MTurk{Auth: auth}
+ var err error
+ mt.URL, err = url.Parse("http://mechanicalturk.amazonaws.com/")
+ if err != nil {
+ panic(err.Error())
+ }
+ return mt
+}
+
+// ----------------------------------------------------------------------------
+// Request dispatching logic.
+
+// Error encapsulates an error returned by MTurk.
+type Error struct {
+ StatusCode int // HTTP status code (200, 403, ...)
+ Code string // EC2 error code ("UnsupportedOperation", ...)
+ Message string // The human-oriented error message
+ RequestId string
+}
+
+func (err *Error) Error() string {
+ return err.Message
+}
+
+// The request stanza included in several response types, for example
+// in a "CreateHITResponse". http://goo.gl/qGeKf
+type xmlRequest struct {
+ RequestId string
+ IsValid string
+ Errors []Error `xml:"Errors>Error"`
+}
+
+// Common price structure used in requests and responses
+// http://goo.gl/tE4AV
+type Price struct {
+ Amount string
+ CurrencyCode string
+ FormattedPrice string
+}
+
+// Really just a country string
+// http://goo.gl/mU4uG
+type Locale string
+
+// Data structure used to specify requirements for the worker
+// used in CreateHIT, for example
+// http://goo.gl/LvRo9
+type QualificationRequirement struct {
+ QualificationTypeId string
+ Comparator string
+ IntegerValue int
+ LocaleValue Locale
+ RequiredToPreview string
+}
+
+// Data structure holding the contents of an "external"
+// question. http://goo.gl/NP8Aa
+type ExternalQuestion struct {
+ XMLName xml.Name `xml:"http://mechanicalturk.amazonaws.com/AWSMechanicalTurkDataSchemas/2006-07-14/ExternalQuestion.xsd ExternalQuestion"`
+ ExternalURL string
+ FrameHeight int
+}
+
+// The data structure representing a "human interface task" (HIT)
+// Currently only supports "external" questions, because Go
+// structs don't support union types. http://goo.gl/NP8Aa
+// This type is returned, for example, from SearchHITs
+// http://goo.gl/PskcX
+type HIT struct {
+ Request xmlRequest
+
+ HITId string
+ HITTypeId string
+ CreationTime string
+ Title string
+ Description string
+ Keywords string
+ HITStatus string
+ Reward Price
+ LifetimeInSeconds uint
+ AssignmentDurationInSeconds uint
+ MaxAssignments uint
+ AutoApprovalDelayInSeconds uint
+ QualificationRequirement QualificationRequirement
+ Question ExternalQuestion
+ RequesterAnnotation string
+ NumberofSimilarHITs uint
+ HITReviewStatus string
+ NumberOfAssignmentsPending uint
+ NumberOfAssignmentsAvailable uint
+ NumberOfAssignmentsCompleted uint
+}
+
+// The main data structure returned by SearchHITs
+// http://goo.gl/PskcX
+type SearchHITsResult struct {
+ NumResults uint
+ PageNumber uint
+ TotalNumResults uint
+ HITs []HIT `xml:"HIT"`
+}
+
+// The wrapper data structure returned by SearchHITs
+// http://goo.gl/PskcX
+type SearchHITsResponse struct {
+ RequestId string `xml:"OperationRequest>RequestId"`
+ SearchHITsResult SearchHITsResult
+}
+
+// The wrapper data structure returned by CreateHIT
+// http://goo.gl/PskcX
+type CreateHITResponse struct {
+ RequestId string `xml:"OperationRequest>RequestId"`
+ HIT HIT
+}
+
+// Corresponds to the "CreateHIT" operation of the Mechanical Turk
+// API. http://goo.gl/cDBRc Currently only supports "external"
+// questions (see "HIT" struct above). If "keywords", "maxAssignments",
+// "qualificationRequirement" or "requesterAnnotation" are the zero
+// value for their types, they will not be included in the request.
+func (mt *MTurk) CreateHIT(title, description string, question ExternalQuestion, reward Price, assignmentDurationInSeconds, lifetimeInSeconds uint, keywords string, maxAssignments uint, qualificationRequirement *QualificationRequirement, requesterAnnotation string) (h *HIT, err error) {
+ params := make(map[string]string)
+ params["Title"] = title
+ params["Description"] = description
+ params["Question"], err = xmlEncode(&question)
+ if err != nil {
+ return
+ }
+ params["Reward.1.Amount"] = reward.Amount
+ params["Reward.1.CurrencyCode"] = reward.CurrencyCode
+ params["AssignmentDurationInSeconds"] = strconv.FormatUint(uint64(assignmentDurationInSeconds), 10)
+
+ params["LifetimeInSeconds"] = strconv.FormatUint(uint64(lifetimeInSeconds), 10)
+ if keywords != "" {
+ params["Keywords"] = keywords
+ }
+ if maxAssignments != 0 {
+ params["MaxAssignments"] = strconv.FormatUint(uint64(maxAssignments), 10)
+ }
+ if qualificationRequirement != nil {
+ params["QualificationRequirement"], err = xmlEncode(qualificationRequirement)
+ if err != nil {
+ return
+ }
+ }
+ if requesterAnnotation != "" {
+ params["RequesterAnnotation"] = requesterAnnotation
+ }
+
+ var response CreateHITResponse
+ err = mt.query(params, "CreateHIT", &response)
+ if err == nil {
+ h = &response.HIT
+ }
+ return
+}
+
+// Corresponds to the "CreateHIT" operation of the Mechanical Turk
+// API, using an existing "hit type". http://goo.gl/cDBRc Currently only
+// supports "external" questions (see "HIT" struct above). If
+// "maxAssignments" or "requesterAnnotation" are the zero value for
+// their types, they will not be included in the request.
+func (mt *MTurk) CreateHITOfType(hitTypeId string, q ExternalQuestion, lifetimeInSeconds uint, maxAssignments uint, requesterAnnotation string) (h *HIT, err error) {
+ params := make(map[string]string)
+ params["HITTypeId"] = hitTypeId
+ params["Question"], err = xmlEncode(&q)
+ if err != nil {
+ return
+ }
+ params["LifetimeInSeconds"] = strconv.FormatUint(uint64(lifetimeInSeconds), 10)
+ if maxAssignments != 0 {
+ params["MaxAssignments"] = strconv.FormatUint(uint64(maxAssignments), 10)
+ }
+ if requesterAnnotation != "" {
+ params["RequesterAnnotation"] = requesterAnnotation
+ }
+
+ var response CreateHITResponse
+ err = mt.query(params, "CreateHIT", &response)
+ if err == nil {
+ h = &response.HIT
+ }
+ return
+}
+
+// Corresponds to "SearchHITs" operation of Mechanical Turk. http://goo.gl/PskcX
+// Currenlty supports none of the optional parameters.
+func (mt *MTurk) SearchHITs() (s *SearchHITsResult, err error) {
+ params := make(map[string]string)
+ var response SearchHITsResponse
+ err = mt.query(params, "SearchHITs", &response)
+ if err == nil {
+ s = &response.SearchHITsResult
+ }
+ return
+}
+
+// Adds common parameters to the "params" map, signs the request,
+// adds the signature to the "params" map and sends the request
+// to the server. It then unmarshals the response in to the "resp"
+// parameter using xml.Unmarshal()
+func (mt *MTurk) query(params map[string]string, operation string, resp interface{}) error {
+ service := "AWSMechanicalTurkRequester"
+ timestamp := time.Now().UTC().Format("2006-01-02T15:04:05Z")
+
+ params["AWSAccessKeyId"] = mt.Auth.AccessKey
+ params["Service"] = service
+ params["Timestamp"] = timestamp
+ params["Operation"] = operation
+
+ // make a copy
+ url := *mt.URL
+
+ sign(mt.Auth, service, operation, timestamp, params)
+ url.RawQuery = multimap(params).Encode()
+ r, err := http.Get(url.String())
+ if err != nil {
+ return err
+ }
+ //dump, _ := httputil.DumpResponse(r, true)
+ //println("DUMP:\n", string(dump))
+ if r.StatusCode != 200 {
+ return errors.New(fmt.Sprintf("%d: unexpected status code", r.StatusCode))
+ }
+ dec := xml.NewDecoder(r.Body)
+ err = dec.Decode(resp)
+ r.Body.Close()
+ return err
+}
+
+func multimap(p map[string]string) url.Values {
+ q := make(url.Values, len(p))
+ for k, v := range p {
+ q[k] = []string{v}
+ }
+ return q
+}
+
+func xmlEncode(i interface{}) (s string, err error) {
+ var buf []byte
+ buf, err = xml.Marshal(i)
+ if err != nil {
+ return
+ }
+ s = string(buf)
+ return
+}
diff --git a/vendor/src/launchpad.net/goamz/exp/mturk/mturk_test.go b/vendor/src/launchpad.net/goamz/exp/mturk/mturk_test.go
new file mode 100644
index 0000000000..30351fe233
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/exp/mturk/mturk_test.go
@@ -0,0 +1,95 @@
+package mturk_test
+
+import (
+ "launchpad.net/goamz/aws"
+ "launchpad.net/goamz/exp/mturk"
+ "launchpad.net/goamz/testutil"
+ . "launchpad.net/gocheck"
+ "net/url"
+ "testing"
+)
+
+func Test(t *testing.T) {
+ TestingT(t)
+}
+
+var _ = Suite(&S{})
+
+type S struct {
+ mturk *mturk.MTurk
+}
+
+var testServer = testutil.NewHTTPServer()
+
+func (s *S) SetUpSuite(c *C) {
+ testServer.Start()
+ auth := aws.Auth{"abc", "123"}
+ u, err := url.Parse(testServer.URL)
+ if err != nil {
+ panic(err.Error())
+ }
+
+ s.mturk = &mturk.MTurk{
+ Auth: auth,
+ URL: u,
+ }
+}
+
+func (s *S) TearDownSuite(c *C) {
+ testServer.Stop()
+}
+
+func (s *S) TearDownTest(c *C) {
+ testServer.Flush()
+}
+
+func (s *S) TestCreateHIT(c *C) {
+ testServer.Response(200, nil, BasicHitResponse)
+
+ question := mturk.ExternalQuestion{
+ ExternalURL: "http://www.amazon.com",
+ FrameHeight: 200,
+ }
+ reward := mturk.Price{
+ Amount: "0.01",
+ CurrencyCode: "USD",
+ }
+ hit, err := s.mturk.CreateHIT("title", "description", question, reward, 1, 2, "key1,key2", 3, nil, "annotation")
+
+ testServer.WaitRequest()
+
+ c.Assert(err, IsNil)
+ c.Assert(hit, NotNil)
+
+ c.Assert(hit.HITId, Equals, "28J4IXKO2L927XKJTHO34OCDNASCDW")
+ c.Assert(hit.HITTypeId, Equals, "2XZ7D1X3V0FKQVW7LU51S7PKKGFKDF")
+}
+
+func (s *S) TestSearchHITs(c *C) {
+ testServer.Response(200, nil, SearchHITResponse)
+
+ hitResult, err := s.mturk.SearchHITs()
+
+ c.Assert(err, IsNil)
+ c.Assert(hitResult, NotNil)
+
+ c.Assert(hitResult.NumResults, Equals, uint(1))
+ c.Assert(hitResult.PageNumber, Equals, uint(1))
+ c.Assert(hitResult.TotalNumResults, Equals, uint(1))
+
+ c.Assert(len(hitResult.HITs), Equals, 1)
+ c.Assert(hitResult.HITs[0].HITId, Equals, "2BU26DG67D1XTE823B3OQ2JF2XWF83")
+ c.Assert(hitResult.HITs[0].HITTypeId, Equals, "22OWJ5OPB0YV6IGL5727KP9U38P5XR")
+ c.Assert(hitResult.HITs[0].CreationTime, Equals, "2011-12-28T19:56:20Z")
+ c.Assert(hitResult.HITs[0].Title, Equals, "test hit")
+ c.Assert(hitResult.HITs[0].Description, Equals, "please disregard, testing only")
+ c.Assert(hitResult.HITs[0].HITStatus, Equals, "Reviewable")
+ c.Assert(hitResult.HITs[0].MaxAssignments, Equals, uint(1))
+ c.Assert(hitResult.HITs[0].Reward.Amount, Equals, "0.01")
+ c.Assert(hitResult.HITs[0].Reward.CurrencyCode, Equals, "USD")
+ c.Assert(hitResult.HITs[0].AutoApprovalDelayInSeconds, Equals, uint(2592000))
+ c.Assert(hitResult.HITs[0].AssignmentDurationInSeconds, Equals, uint(30))
+ c.Assert(hitResult.HITs[0].NumberOfAssignmentsPending, Equals, uint(0))
+ c.Assert(hitResult.HITs[0].NumberOfAssignmentsAvailable, Equals, uint(1))
+ c.Assert(hitResult.HITs[0].NumberOfAssignmentsCompleted, Equals, uint(0))
+}
diff --git a/vendor/src/launchpad.net/goamz/exp/mturk/responses_test.go b/vendor/src/launchpad.net/goamz/exp/mturk/responses_test.go
new file mode 100644
index 0000000000..4467b96713
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/exp/mturk/responses_test.go
@@ -0,0 +1,9 @@
+package mturk_test
+
+var BasicHitResponse = `
+643b794b-66b6-4427-bb8a-4d3df5c9a20eTrue28J4IXKO2L927XKJTHO34OCDNASCDW2XZ7D1X3V0FKQVW7LU51S7PKKGFKDF
+`
+
+var SearchHITResponse = `
+38862d9c-f015-4177-a2d3-924110a9d6f2True1112BU26DG67D1XTE823B3OQ2JF2XWF8322OWJ5OPB0YV6IGL5727KP9U38P5XR2011-12-28T19:56:20Ztest hitplease disregard, testing onlyReviewable10.01USD$0.0125920002011-12-28T19:56:50Z30010
+`
diff --git a/vendor/src/launchpad.net/goamz/exp/mturk/sign.go b/vendor/src/launchpad.net/goamz/exp/mturk/sign.go
new file mode 100644
index 0000000000..556384543e
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/exp/mturk/sign.go
@@ -0,0 +1,22 @@
+package mturk
+
+import (
+ "crypto/hmac"
+ "crypto/sha1"
+ "encoding/base64"
+ "launchpad.net/goamz/aws"
+)
+
+var b64 = base64.StdEncoding
+
+// ----------------------------------------------------------------------------
+// Mechanical Turk signing (http://goo.gl/wrzfn)
+func sign(auth aws.Auth, service, method, timestamp string, params map[string]string) {
+ payload := service + method + timestamp
+ hash := hmac.New(sha1.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)
+}
diff --git a/vendor/src/launchpad.net/goamz/exp/mturk/sign_test.go b/vendor/src/launchpad.net/goamz/exp/mturk/sign_test.go
new file mode 100644
index 0000000000..99865d86e7
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/exp/mturk/sign_test.go
@@ -0,0 +1,19 @@
+package mturk_test
+
+import (
+ "launchpad.net/goamz/aws"
+ "launchpad.net/goamz/exp/mturk"
+ . "launchpad.net/gocheck"
+)
+
+// Mechanical Turk REST authentication docs: http://goo.gl/wrzfn
+
+var testAuth = aws.Auth{"user", "secret"}
+
+// == fIJy9wCApBNL2R4J2WjJGtIBFX4=
+func (s *S) TestBasicSignature(c *C) {
+ params := map[string]string{}
+ mturk.Sign(testAuth, "AWSMechanicalTurkRequester", "CreateHIT", "2012-02-16T20:30:47Z", params)
+ expected := "b/TnvzrdeD/L/EyzdFrznPXhido="
+ c.Assert(params["Signature"], Equals, expected)
+}
diff --git a/vendor/src/launchpad.net/goamz/exp/sdb/export_test.go b/vendor/src/launchpad.net/goamz/exp/sdb/export_test.go
new file mode 100644
index 0000000000..9c50703453
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/exp/sdb/export_test.go
@@ -0,0 +1,9 @@
+package sdb
+
+import (
+ "launchpad.net/goamz/aws"
+)
+
+func Sign(auth aws.Auth, method, path string, params map[string][]string, headers map[string][]string) {
+ sign(auth, method, path, params, headers)
+}
diff --git a/vendor/src/launchpad.net/goamz/exp/sdb/responses_test.go b/vendor/src/launchpad.net/goamz/exp/sdb/responses_test.go
new file mode 100644
index 0000000000..034c2b31ce
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/exp/sdb/responses_test.go
@@ -0,0 +1,120 @@
+package sdb_test
+
+var TestCreateDomainXmlOK = `
+
+
+
+ 63264005-7a5f-e01a-a224-395c63b89f6d
+ 0.0055590279
+
+
+`
+
+var TestListDomainsXmlOK = `
+
+
+
+ Account
+ Domain
+ Record
+
+
+ 15fcaf55-9914-63c2-21f3-951e31193790
+ 0.0000071759
+
+
+`
+
+var TestListDomainsWithNextTokenXmlOK = `
+
+
+
+ Domain1-200706011651
+ Domain2-200706011652
+ TWV0ZXJpbmdUZXN0RG9tYWluMS0yMDA3MDYwMTE2NTY=
+
+
+ eb13162f-1b95-4511-8b12-489b86acfd28
+ 0.0000219907
+
+
+`
+
+var TestDeleteDomainXmlOK = `
+
+
+
+ 039e1e25-9a64-2a74-93da-2fda36122a97
+ 0.0055590278
+
+
+`
+
+var TestDomainMetadataXmlNoSuchDomain = `
+
+
+
+
+ NoSuchDomain
+ The specified domain does not exist.
+ 0.0000071759
+
+
+ e050cea2-a772-f90e-2cb0-98ebd42c2898
+
+`
+
+var TestPutAttrsXmlOK = `
+
+
+
+ 490206ce-8292-456c-a00f-61b335eb202b
+ 0.0000219907
+
+
+`
+
+var TestAttrsXmlOK = `
+
+
+
+ ColorBlue
+ SizeMed
+
+
+ b1e8f1f7-42e9-494c-ad09-2674e557526d
+ 0.0000219942
+
+
+`
+
+var TestSelectXmlOK = `
+
+
+
+ -
+ Item_03
+ CategoryClothes
+ SubcategoryPants
+ NameSweatpants
+ ColorBlue
+ ColorYellow
+ ColorPink
+ SizeLarge
+
+ -
+ Item_06
+ CategoryMotorcycle Parts
+ SubcategoryBodywork
+ NameFender Eliminator
+ ColorBlue
+ MakeYamaha
+ ModelR1
+
+
+
+ b1e8f1f7-42e9-494c-ad09-2674e557526d
+ 0.0000219907
+
+
+`
diff --git a/vendor/src/launchpad.net/goamz/exp/sdb/sdb.go b/vendor/src/launchpad.net/goamz/exp/sdb/sdb.go
new file mode 100644
index 0000000000..805cb7d63e
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/exp/sdb/sdb.go
@@ -0,0 +1,413 @@
+//
+// goamz - Go packages to interact with the Amazon Web Services.
+//
+// https://wiki.ubuntu.com/goamz
+//
+// Copyright (c) 2011 AppsAttic Ltd.
+//
+// sdb package written by:
+//
+// Andrew Chilton
+// Brad Rydzewski
+
+// This package is in an experimental state, and does not currently
+// follow conventions and style of the rest of goamz or common
+// Go conventions. It must be polished before it's considered a
+// first-class package in goamz.
+package sdb
+
+// BUG: SelectResp isn't properly organized. It must change.
+
+//
+
+import (
+ "encoding/xml"
+ "launchpad.net/goamz/aws"
+ "log"
+ "net/http"
+ "net/http/httputil"
+ "net/url"
+ "strconv"
+ "time"
+)
+
+const debug = false
+
+// The SDB type encapsulates operations with a specific SimpleDB region.
+type SDB struct {
+ aws.Auth
+ aws.Region
+ private byte // Reserve the right of using private data.
+}
+
+// New creates a new SDB.
+func New(auth aws.Auth, region aws.Region) *SDB {
+ return &SDB{auth, region, 0}
+}
+
+// The Domain type represents a collection of items that are described
+// by name-value attributes.
+type Domain struct {
+ *SDB
+ Name string
+}
+
+// Domain returns a Domain with the given name.
+func (sdb *SDB) Domain(name string) *Domain {
+ return &Domain{sdb, name}
+}
+
+// The Item type represent individual objects that contain one or more
+// name-value attributes stored within a SDB Domain as rows.
+type Item struct {
+ *SDB
+ *Domain
+ Name string
+}
+
+// Item returns an Item with the given name.
+func (domain *Domain) Item(name string) *Item {
+ return &Item{domain.SDB, domain, name}
+}
+
+// The Attr type represent categories of data that can be assigned to items.
+type Attr struct {
+ Name string
+ Value string
+}
+
+// ----------------------------------------------------------------------------
+// Service-level operations.
+
+// --- ListDomains
+
+// Response to a ListDomains request.
+//
+// See http://goo.gl/3u0Cf for more details.
+type ListDomainsResp struct {
+ Domains []string `xml:"ListDomainsResult>DomainName"`
+ NextToken string `xml:"ListDomainsResult>NextToken"`
+ ResponseMetadata ResponseMetadata
+}
+
+// ListDomains lists all domains in sdb.
+//
+// See http://goo.gl/Dsw15 for more details.
+func (sdb *SDB) ListDomains() (resp *ListDomainsResp, err error) {
+ return sdb.ListDomainsN(0, "")
+}
+
+// ListDomainsN lists domains in sdb up to maxDomains.
+// If nextToken is not empty, domains listed will start at the given token.
+//
+// See http://goo.gl/Dsw15 for more details.
+func (sdb *SDB) ListDomainsN(maxDomains int, nextToken string) (resp *ListDomainsResp, err error) {
+ params := makeParams("ListDomains")
+ if maxDomains != 0 {
+ params["MaxNumberOfDomains"] = []string{strconv.Itoa(maxDomains)}
+ }
+ if nextToken != "" {
+ params["NextToken"] = []string{nextToken}
+ }
+ resp = &ListDomainsResp{}
+ err = sdb.query(nil, nil, params, nil, resp)
+ return
+}
+
+// --- SelectExpression
+
+// Response to a Select request.
+//
+// See http://goo.gl/GTsSZ for more details.
+type SelectResp struct {
+ Items []struct {
+ Name string
+ Attrs []Attr `xml:"Attribute"`
+ } `xml:"SelectResult>Item"`
+ ResponseMetadata ResponseMetadata
+}
+
+// Select returns a set of items and attributes that match expr.
+// Select is similar to the standard SQL SELECT statement.
+//
+// See http://goo.gl/GTsSZ for more details.
+func (sdb *SDB) Select(expr string, consistent bool) (resp *SelectResp, err error) {
+ resp = &SelectResp{}
+ params := makeParams("Select")
+ params["SelectExpression"] = []string{expr}
+ if consistent {
+ params["ConsistentRead"] = []string{"true"}
+ }
+ err = sdb.query(nil, nil, params, nil, resp)
+ return
+}
+
+// ----------------------------------------------------------------------------
+// Domain-level operations.
+
+// --- CreateDomain
+
+// CreateDomain creates a new domain.
+//
+// See http://goo.gl/jDjGH for more details.
+func (domain *Domain) CreateDomain() (resp *SimpleResp, err error) {
+ params := makeParams("CreateDomain")
+ resp = &SimpleResp{}
+ err = domain.SDB.query(domain, nil, params, nil, resp)
+ return
+}
+
+// DeleteDomain deletes an existing domain.
+//
+// See http://goo.gl/S0dCL for more details.
+func (domain *Domain) DeleteDomain() (resp *SimpleResp, err error) {
+ params := makeParams("DeleteDomain")
+ resp = &SimpleResp{}
+ err = domain.SDB.query(domain, nil, params, nil, resp)
+ return
+}
+
+// ----------------------------------------------------------------------------
+// Item-level operations.
+
+type PutAttrs struct {
+ attrs []Attr
+ expected []Attr
+ replace map[string]bool
+ missing map[string]bool
+}
+
+func (pa *PutAttrs) Add(name, value string) {
+ pa.attrs = append(pa.attrs, Attr{name, value})
+}
+
+func (pa *PutAttrs) Replace(name, value string) {
+ pa.Add(name, value)
+ if pa.replace == nil {
+ pa.replace = make(map[string]bool)
+ }
+ pa.replace[name] = true
+}
+
+// The PutAttrs request will only succeed if the existing
+// item in SimpleDB contains a matching name / value pair.
+func (pa *PutAttrs) IfValue(name, value string) {
+ pa.expected = append(pa.expected, Attr{name, value})
+}
+
+// Flag to test the existence of an attribute while performing
+// conditional updates. X can be any positive integer or 0.
+//
+// This should set Expected.N.Name=name and Expected.N.Exists=false
+func (pa *PutAttrs) IfMissing(name string) {
+ if pa.missing == nil {
+ pa.missing = make(map[string]bool)
+ }
+ pa.missing[name] = true
+}
+
+// PutAttrs adds attrs to item.
+//
+// See http://goo.gl/yTAV4 for more details.
+func (item *Item) PutAttrs(attrs *PutAttrs) (resp *SimpleResp, err error) {
+ params := makeParams("PutAttributes")
+ resp = &SimpleResp{}
+
+ // copy these attrs over to the parameters
+ itemNum := 1
+ for _, attr := range attrs.attrs {
+ itemNumStr := strconv.Itoa(itemNum)
+
+ // do the name, value and replace
+ params["Attribute."+itemNumStr+".Name"] = []string{attr.Name}
+ params["Attribute."+itemNumStr+".Value"] = []string{attr.Value}
+
+ if _, ok := attrs.replace[attr.Name]; ok {
+ params["Attribute."+itemNumStr+".Replace"] = []string{"true"}
+ }
+
+ itemNum++
+ }
+
+ //append expected values to params
+ expectedNum := 1
+ for _, attr := range attrs.expected {
+ expectedNumStr := strconv.Itoa(expectedNum)
+ params["Expected."+expectedNumStr+".Name"] = []string{attr.Name}
+ params["Expected."+expectedNumStr+".Value"] = []string{attr.Value}
+
+ if attrs.missing[attr.Name] {
+ params["Expected."+expectedNumStr+".Exists"] = []string{"false"}
+ }
+ expectedNum++
+ }
+
+ err = item.query(params, nil, resp)
+ if err != nil {
+ return nil, err
+ }
+ return
+}
+
+// Response to an Attrs request.
+//
+// See http://goo.gl/45X1M for more details.
+type AttrsResp struct {
+ Attrs []Attr `xml:"GetAttributesResult>Attribute"`
+ ResponseMetadata ResponseMetadata
+}
+
+// Attrs returns one or more of the named attributes, or
+// all of item's attributes if names is nil.
+// If consistent is true, previous writes will necessarily
+// be observed.
+//
+// See http://goo.gl/45X1M for more details.
+func (item *Item) Attrs(names []string, consistent bool) (resp *AttrsResp, err error) {
+ params := makeParams("GetAttributes")
+ params["ItemName"] = []string{item.Name}
+ if consistent {
+ params["ConsistentRead"] = []string{"true"}
+ }
+
+ // Copy these attributes over to the parameters
+ for i, name := range names {
+ params["AttributeName."+strconv.Itoa(i+1)] = []string{name}
+ }
+
+ resp = &AttrsResp{}
+ err = item.query(params, nil, resp)
+ if err != nil {
+ return nil, err
+ }
+ return
+}
+
+// ----------------------------------------------------------------------------
+// Generic data structures for all requests/responses.
+
+// Error encapsulates an error returned by SDB.
+type Error struct {
+ StatusCode int // HTTP status code (200, 403, ...)
+ StatusMsg string // HTTP status message ("Service Unavailable", "Bad Request", ...)
+ Code string // SimpleDB error code ("InvalidParameterValue", ...)
+ Message string // The human-oriented error message
+ RequestId string // A unique ID for this request
+ BoxUsage float64 // The measure of machine utilization for this request.
+}
+
+func (err *Error) Error() string {
+ return err.Message
+}
+
+// SimpleResp represents a response to an SDB request which on success
+// will return no other information besides ResponseMetadata.
+type SimpleResp struct {
+ ResponseMetadata ResponseMetadata
+}
+
+// ResponseMetadata
+type ResponseMetadata struct {
+ RequestId string // A unique ID for tracking the request
+ BoxUsage float64 // The measure of machine utilization for this request.
+}
+
+func buildError(r *http.Response) error {
+ err := Error{}
+ err.StatusCode = r.StatusCode
+ err.StatusMsg = r.Status
+ xml.NewDecoder(r.Body).Decode(&err)
+ return &err
+}
+
+// ----------------------------------------------------------------------------
+// Request dispatching logic.
+
+func (item *Item) query(params url.Values, headers http.Header, resp interface{}) error {
+ return item.Domain.SDB.query(item.Domain, item, params, headers, resp)
+}
+
+func (domain *Domain) query(item *Item, params url.Values, headers http.Header, resp interface{}) error {
+ return domain.SDB.query(domain, item, params, headers, resp)
+}
+
+func (sdb *SDB) query(domain *Domain, item *Item, params url.Values, headers http.Header, resp interface{}) error {
+ // all SimpleDB operations have path="/"
+ method := "GET"
+ path := "/"
+
+ // if we have been given no headers or params, create them
+ if headers == nil {
+ headers = map[string][]string{}
+ }
+ if params == nil {
+ params = map[string][]string{}
+ }
+
+ // setup some default parameters
+ params["Version"] = []string{"2009-04-15"}
+ params["Timestamp"] = []string{time.Now().UTC().Format(time.RFC3339)}
+
+ // set the DomainName param (every request must have one)
+ if domain != nil {
+ params["DomainName"] = []string{domain.Name}
+ }
+
+ // set the ItemName if we have one
+ if item != nil {
+ params["ItemName"] = []string{item.Name}
+ }
+
+ // check the endpoint URL
+ u, err := url.Parse(sdb.Region.SDBEndpoint)
+ if err != nil {
+ return err
+ }
+ headers["Host"] = []string{u.Host}
+ sign(sdb.Auth, method, path, params, headers)
+
+ u.Path = path
+ if len(params) > 0 {
+ u.RawQuery = params.Encode()
+ }
+ req := http.Request{
+ URL: u,
+ Method: method,
+ ProtoMajor: 1,
+ ProtoMinor: 1,
+ Close: true,
+ Header: headers,
+ }
+
+ if v, ok := headers["Content-Length"]; ok {
+ req.ContentLength, _ = strconv.ParseInt(v[0], 10, 64)
+ delete(headers, "Content-Length")
+ }
+
+ r, err := http.DefaultClient.Do(&req)
+ if err != nil {
+ return err
+ }
+ defer r.Body.Close()
+
+ if debug {
+ dump, _ := httputil.DumpResponse(r, true)
+ log.Printf("response:\n")
+ log.Printf("%v\n}\n", string(dump))
+ }
+
+ // status code is always 200 when successful (since we're always doing a GET)
+ if r.StatusCode != 200 {
+ return buildError(r)
+ }
+
+ // everything was fine, so unmarshal the XML and return what it's err is (if any)
+ err = xml.NewDecoder(r.Body).Decode(resp)
+ return err
+}
+
+func makeParams(action string) map[string][]string {
+ params := make(map[string][]string)
+ params["Action"] = []string{action}
+ return params
+}
diff --git a/vendor/src/launchpad.net/goamz/exp/sdb/sdb_test.go b/vendor/src/launchpad.net/goamz/exp/sdb/sdb_test.go
new file mode 100644
index 0000000000..9f47472fd6
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/exp/sdb/sdb_test.go
@@ -0,0 +1,222 @@
+package sdb_test
+
+import (
+ "launchpad.net/goamz/aws"
+ "launchpad.net/goamz/exp/sdb"
+ "launchpad.net/goamz/testutil"
+ . "launchpad.net/gocheck"
+ "testing"
+)
+
+func Test(t *testing.T) {
+ TestingT(t)
+}
+
+var _ = Suite(&S{})
+
+type S struct {
+ sdb *sdb.SDB
+}
+
+var testServer = testutil.NewHTTPServer()
+
+func (s *S) SetUpSuite(c *C) {
+ testServer.Start()
+ auth := aws.Auth{"abc", "123"}
+ s.sdb = sdb.New(auth, aws.Region{SDBEndpoint: testServer.URL})
+}
+
+func (s *S) TearDownSuite(c *C) {
+ testServer.Stop()
+}
+
+func (s *S) TearDownTest(c *C) {
+ testServer.Flush()
+}
+
+func (s *S) TestCreateDomainOK(c *C) {
+ testServer.Response(200, nil, TestCreateDomainXmlOK)
+
+ domain := s.sdb.Domain("domain")
+ resp, err := domain.CreateDomain()
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+
+ c.Assert(resp.ResponseMetadata.RequestId, Equals, "63264005-7a5f-e01a-a224-395c63b89f6d")
+ c.Assert(resp.ResponseMetadata.BoxUsage, Equals, 0.0055590279)
+
+ c.Assert(err, IsNil)
+}
+
+func (s *S) TestListDomainsOK(c *C) {
+ testServer.Response(200, nil, TestListDomainsXmlOK)
+
+ resp, err := s.sdb.ListDomains()
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+
+ c.Assert(resp.ResponseMetadata.RequestId, Equals, "15fcaf55-9914-63c2-21f3-951e31193790")
+ c.Assert(resp.ResponseMetadata.BoxUsage, Equals, 0.0000071759)
+ c.Assert(resp.Domains, DeepEquals, []string{"Account", "Domain", "Record"})
+
+ c.Assert(err, IsNil)
+}
+
+func (s *S) TestListDomainsWithNextTokenXmlOK(c *C) {
+ testServer.Response(200, nil, TestListDomainsWithNextTokenXmlOK)
+
+ resp, err := s.sdb.ListDomains()
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+
+ c.Assert(resp.ResponseMetadata.RequestId, Equals, "eb13162f-1b95-4511-8b12-489b86acfd28")
+ c.Assert(resp.ResponseMetadata.BoxUsage, Equals, 0.0000219907)
+ c.Assert(resp.Domains, DeepEquals, []string{"Domain1-200706011651", "Domain2-200706011652"})
+ c.Assert(resp.NextToken, Equals, "TWV0ZXJpbmdUZXN0RG9tYWluMS0yMDA3MDYwMTE2NTY=")
+
+ c.Assert(err, IsNil)
+}
+
+func (s *S) TestDeleteDomainOK(c *C) {
+ testServer.Response(200, nil, TestDeleteDomainXmlOK)
+
+ domain := s.sdb.Domain("domain")
+ resp, err := domain.DeleteDomain()
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+
+ c.Assert(resp.ResponseMetadata.RequestId, Equals, "039e1e25-9a64-2a74-93da-2fda36122a97")
+ c.Assert(resp.ResponseMetadata.BoxUsage, Equals, 0.0055590278)
+
+ c.Assert(err, IsNil)
+}
+
+func (s *S) TestPutAttrsOK(c *C) {
+ testServer.Response(200, nil, TestPutAttrsXmlOK)
+
+ domain := s.sdb.Domain("MyDomain")
+ item := domain.Item("Item123")
+
+ putAttrs := new(sdb.PutAttrs)
+ putAttrs.Add("FirstName", "john")
+ putAttrs.Add("LastName", "smith")
+ putAttrs.Replace("MiddleName", "jacob")
+
+ putAttrs.IfValue("FirstName", "john")
+ putAttrs.IfMissing("FirstName")
+
+ resp, err := item.PutAttrs(putAttrs)
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/")
+ c.Assert(req.Form["Action"], DeepEquals, []string{"PutAttributes"})
+ c.Assert(req.Form["ItemName"], DeepEquals, []string{"Item123"})
+ c.Assert(req.Form["DomainName"], DeepEquals, []string{"MyDomain"})
+ c.Assert(req.Form["Attribute.1.Name"], DeepEquals, []string{"FirstName"})
+ c.Assert(req.Form["Attribute.1.Value"], DeepEquals, []string{"john"})
+ c.Assert(req.Form["Attribute.2.Name"], DeepEquals, []string{"LastName"})
+ c.Assert(req.Form["Attribute.2.Value"], DeepEquals, []string{"smith"})
+ c.Assert(req.Form["Attribute.3.Name"], DeepEquals, []string{"MiddleName"})
+ c.Assert(req.Form["Attribute.3.Value"], DeepEquals, []string{"jacob"})
+ c.Assert(req.Form["Attribute.3.Replace"], DeepEquals, []string{"true"})
+
+ c.Assert(req.Form["Expected.1.Name"], DeepEquals, []string{"FirstName"})
+ c.Assert(req.Form["Expected.1.Value"], DeepEquals, []string{"john"})
+ c.Assert(req.Form["Expected.1.Exists"], DeepEquals, []string{"false"})
+
+ c.Assert(err, IsNil)
+ c.Assert(resp.ResponseMetadata.RequestId, Equals, "490206ce-8292-456c-a00f-61b335eb202b")
+ c.Assert(resp.ResponseMetadata.BoxUsage, Equals, 0.0000219907)
+
+}
+
+func (s *S) TestAttrsOK(c *C) {
+ testServer.Response(200, nil, TestAttrsXmlOK)
+
+ domain := s.sdb.Domain("MyDomain")
+ item := domain.Item("Item123")
+
+ resp, err := item.Attrs(nil, true)
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+ c.Assert(req.Form["Action"], DeepEquals, []string{"GetAttributes"})
+ c.Assert(req.Form["ItemName"], DeepEquals, []string{"Item123"})
+ c.Assert(req.Form["DomainName"], DeepEquals, []string{"MyDomain"})
+ c.Assert(req.Form["ConsistentRead"], DeepEquals, []string{"true"})
+
+ c.Assert(resp.Attrs[0].Name, Equals, "Color")
+ c.Assert(resp.Attrs[0].Value, Equals, "Blue")
+ c.Assert(resp.Attrs[1].Name, Equals, "Size")
+ c.Assert(resp.Attrs[1].Value, Equals, "Med")
+ c.Assert(resp.ResponseMetadata.RequestId, Equals, "b1e8f1f7-42e9-494c-ad09-2674e557526d")
+ c.Assert(resp.ResponseMetadata.BoxUsage, Equals, 0.0000219942)
+
+ c.Assert(err, IsNil)
+}
+
+func (s *S) TestAttrsSelectOK(c *C) {
+ testServer.Response(200, nil, TestAttrsXmlOK)
+
+ domain := s.sdb.Domain("MyDomain")
+ item := domain.Item("Item123")
+
+ resp, err := item.Attrs([]string{"Color", "Size"}, true)
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+ c.Assert(req.Form["Action"], DeepEquals, []string{"GetAttributes"})
+ c.Assert(req.Form["ItemName"], DeepEquals, []string{"Item123"})
+ c.Assert(req.Form["DomainName"], DeepEquals, []string{"MyDomain"})
+ c.Assert(req.Form["ConsistentRead"], DeepEquals, []string{"true"})
+ c.Assert(req.Form["AttributeName.1"], DeepEquals, []string{"Color"})
+ c.Assert(req.Form["AttributeName.2"], DeepEquals, []string{"Size"})
+
+ c.Assert(resp.Attrs[0].Name, Equals, "Color")
+ c.Assert(resp.Attrs[0].Value, Equals, "Blue")
+ c.Assert(resp.Attrs[1].Name, Equals, "Size")
+ c.Assert(resp.Attrs[1].Value, Equals, "Med")
+ c.Assert(resp.ResponseMetadata.RequestId, Equals, "b1e8f1f7-42e9-494c-ad09-2674e557526d")
+ c.Assert(resp.ResponseMetadata.BoxUsage, Equals, 0.0000219942)
+
+ c.Assert(err, IsNil)
+}
+
+func (s *S) TestSelectOK(c *C) {
+ testServer.Response(200, nil, TestSelectXmlOK)
+
+ resp, err := s.sdb.Select("select Color from MyDomain where Color like 'Blue%'", true)
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+ c.Assert(req.Form["Action"], DeepEquals, []string{"Select"})
+ c.Assert(req.Form["ConsistentRead"], DeepEquals, []string{"true"})
+
+ c.Assert(resp.ResponseMetadata.RequestId, Equals, "b1e8f1f7-42e9-494c-ad09-2674e557526d")
+ c.Assert(resp.ResponseMetadata.BoxUsage, Equals, 0.0000219907)
+ c.Assert(len(resp.Items), Equals, 2)
+ c.Assert(resp.Items[0].Name, Equals, "Item_03")
+ c.Assert(resp.Items[1].Name, Equals, "Item_06")
+ c.Assert(resp.Items[0].Attrs[0].Name, Equals, "Category")
+ c.Assert(resp.Items[0].Attrs[0].Value, Equals, "Clothes")
+
+ c.Assert(err, IsNil)
+}
diff --git a/vendor/src/launchpad.net/goamz/exp/sdb/sign.go b/vendor/src/launchpad.net/goamz/exp/sdb/sign.go
new file mode 100644
index 0000000000..7ac601cc0d
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/exp/sdb/sign.go
@@ -0,0 +1,51 @@
+package sdb
+
+import (
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/base64"
+ "launchpad.net/goamz/aws"
+ "net/http"
+ "net/url"
+ "sort"
+ "strings"
+)
+
+var b64 = base64.StdEncoding
+
+// ----------------------------------------------------------------------------
+// SimpleDB signing (http://goo.gl/CaY81)
+
+func sign(auth aws.Auth, method, path string, params url.Values, headers http.Header) {
+ var host string
+ for k, v := range headers {
+ k = strings.ToLower(k)
+ switch k {
+ case "host":
+ host = v[0]
+ }
+ }
+
+ // set up some defaults used for signing the request
+ params["AWSAccessKeyId"] = []string{auth.AccessKey}
+ params["SignatureVersion"] = []string{"2"}
+ params["SignatureMethod"] = []string{"HmacSHA256"}
+
+ // join up all the incoming params
+ var sarray []string
+ for k, v := range params {
+ sarray = append(sarray, aws.Encode(k)+"="+aws.Encode(v[0]))
+ }
+ sort.StringSlice(sarray).Sort()
+ joined := strings.Join(sarray, "&")
+
+ // create the payload, sign it and create the signature
+ payload := strings.Join([]string{method, host, "/", joined}, "\n")
+ 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))
+
+ // add the signature to the outgoing params
+ params["Signature"] = []string{string(signature)}
+}
diff --git a/vendor/src/launchpad.net/goamz/exp/sdb/sign_test.go b/vendor/src/launchpad.net/goamz/exp/sdb/sign_test.go
new file mode 100644
index 0000000000..234687c465
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/exp/sdb/sign_test.go
@@ -0,0 +1,29 @@
+package sdb_test
+
+import (
+ "launchpad.net/goamz/aws"
+ "launchpad.net/goamz/exp/sdb"
+ . "launchpad.net/gocheck"
+)
+
+// SimpleDB ReST authentication docs: http://goo.gl/CaY81
+
+var testAuth = aws.Auth{"access-key-id-s8eBOWuU", "secret-access-key-UkQjTLd9"}
+
+func (s *S) TestSignExampleDomainCreate(c *C) {
+ method := "GET"
+ params := map[string][]string{
+ "Action": {"CreateDomain"},
+ "DomainName": {"MyDomain"},
+ "Timestamp": {"2011-08-20T07:23:57+12:00"},
+ "Version": {"2009-04-15"},
+ }
+ headers := map[string][]string{
+ "Host": {"sdb.amazonaws.com"},
+ }
+ sdb.Sign(testAuth, method, "", params, headers)
+ expected := "ot2JaeeqMRJqgAqW67hkzUlffgxdOz4RykbrECB+tDU="
+ c.Assert(params["Signature"], DeepEquals, []string{expected})
+}
+
+// Do a few test methods which takes combinations of params
diff --git a/vendor/src/launchpad.net/goamz/exp/sns/Makefile b/vendor/src/launchpad.net/goamz/exp/sns/Makefile
new file mode 100644
index 0000000000..1e5b9da3b0
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/exp/sns/Makefile
@@ -0,0 +1,21 @@
+include $(GOROOT)/src/Make.inc
+
+TARG=launchpad.net/goamz/sns
+
+GOFILES=\
+ sns.go\
+ sign.go\
+
+include $(GOROOT)/src/Make.pkg
+
+GOFMT=gofmt
+BADFMT=$(shell $(GOFMT) -l $(GOFILES) 2> /dev/null)
+
+gofmt: $(BADFMT)
+ @for F in $(BADFMT); do $(GOFMT) -w $$F && echo $$F; done
+
+ifneq ($(BADFMT),)
+ifneq ($(MAKECMDGOALS), gofmt)
+#$(warning WARNING: make gofmt: $(BADFMT))
+endif
+endif
diff --git a/vendor/src/launchpad.net/goamz/exp/sns/README b/vendor/src/launchpad.net/goamz/exp/sns/README
new file mode 100644
index 0000000000..87770adb50
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/exp/sns/README
@@ -0,0 +1 @@
+Amazon Simple Notification Service API for Golang.
diff --git a/vendor/src/launchpad.net/goamz/exp/sns/responses_test.go b/vendor/src/launchpad.net/goamz/exp/sns/responses_test.go
new file mode 100644
index 0000000000..5f8ab6541c
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/exp/sns/responses_test.go
@@ -0,0 +1,164 @@
+package sns_test
+
+var TestListTopicsXmlOK = `
+
+
+
+
+
+ arn:aws:sns:us-west-1:331995417492:Transcoding
+
+
+
+
+ bd10b26c-e30e-11e0-ba29-93c3aca2f103
+
+
+`
+
+var TestCreateTopicXmlOK = `
+
+
+
+ arn:aws:sns:us-east-1:123456789012:My-Topic
+
+
+ a8dec8b3-33a4-11df-8963-01868b7c937a
+
+
+`
+
+var TestDeleteTopicXmlOK = `
+
+
+ f3aa9ac9-3c3d-11df-8235-9dab105e9c32
+
+
+`
+
+var TestListSubscriptionsXmlOK = `
+
+
+
+
+ arn:aws:sns:us-east-1:698519295917:My-Topic
+ email
+ arn:aws:sns:us-east-1:123456789012:My-Topic:80289ba6-0fd4-4079-afb4-ce8c8260f0ca
+ 123456789012
+ example@amazon.com
+
+
+
+
+ 384ac68d-3775-11df-8963-01868b7c937a
+
+
+`
+
+var TestGetTopicAttributesXmlOK = `
+
+
+
+
+ Owner
+ 123456789012
+
+
+ Policy
+ {"Version":"2008-10-17","Id":"us-east-1/698519295917/test__default_policy_ID","Statement" : [{"Effect":"Allow","Sid":"us-east-1/698519295917/test__default_statement_ID","Principal" : {"AWS": "*"},"Action":["SNS:GetTopicAttributes","SNS:SetTopicAttributes","SNS:AddPermission","SNS:RemovePermission","SNS:DeleteTopic","SNS:Subscribe","SNS:ListSubscriptionsByTopic","SNS:Publish","SNS:Receive"],"Resource":"arn:aws:sns:us-east-1:698519295917:test","Condition" : {"StringLike" : {"AWS:SourceArn": "arn:aws:*:*:698519295917:*"}}}]}
+
+
+ TopicArn
+ arn:aws:sns:us-east-1:123456789012:My-Topic
+
+
+
+
+ 057f074c-33a7-11df-9540-99d0768312d3
+
+
+`
+
+var TestPublishXmlOK = `
+
+
+ 94f20ce6-13c5-43a0-9a9e-ca52d816e90b
+
+
+ f187a3c1-376f-11df-8963-01868b7c937a
+
+
+`
+
+var TestSetTopicAttributesXmlOK = `
+
+
+ a8763b99-33a7-11df-a9b7-05d48da6f042
+
+
+`
+
+var TestSubscribeXmlOK = `
+
+
+ pending confirmation
+
+
+ a169c740-3766-11df-8963-01868b7c937a
+
+
+`
+
+var TestUnsubscribeXmlOK = `
+
+
+ 18e0ac39-3776-11df-84c0-b93cc1666b84
+
+
+`
+
+var TestConfirmSubscriptionXmlOK = `
+
+
+ arn:aws:sns:us-east-1:123456789012:My-Topic:80289ba6-0fd4-4079-afb4-ce8c8260f0ca
+
+
+ 7a50221f-3774-11df-a9b7-05d48da6f042
+
+
+`
+
+var TestAddPermissionXmlOK = `
+
+
+ 6a213e4e-33a8-11df-9540-99d0768312d3
+
+
+`
+
+var TestRemovePermissionXmlOK = `
+
+
+ d170b150-33a8-11df-995a-2d6fbe836cc1
+
+
+`
+
+var TestListSubscriptionsByTopicXmlOK = `
+
+
+
+
+ arn:aws:sns:us-east-1:123456789012:My-Topic
+ email
+ arn:aws:sns:us-east-1:123456789012:My-Topic:80289ba6-0fd4-4079-afb4-ce8c8260f0ca
+ 123456789012
+ example@amazon.com
+
+
+
+
+ b9275252-3774-11df-9540-99d0768312d3
+
+
+`
diff --git a/vendor/src/launchpad.net/goamz/exp/sns/sign.go b/vendor/src/launchpad.net/goamz/exp/sns/sign.go
new file mode 100644
index 0000000000..e5cfa1fff6
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/exp/sns/sign.go
@@ -0,0 +1,66 @@
+package sns
+
+import (
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/base64"
+ "launchpad.net/goamz/aws"
+ "sort"
+ "strings"
+)
+
+var b64 = base64.StdEncoding
+
+/*
+func sign(auth aws.Auth, method, path string, params url.Values, headers http.Header) {
+ var host string
+ for k, v := range headers {
+ k = strings.ToLower(k)
+ switch k {
+ case "host":
+ host = v[0]
+ }
+ }
+
+ params["AWSAccessKeyId"] = []string{auth.AccessKey}
+ params["SignatureVersion"] = []string{"2"}
+ params["SignatureMethod"] = []string{"HmacSHA256"}
+
+ var sarry []string
+ for k, v := range params {
+ sarry = append(sarry, aws.Encode(k) + "=" + aws.Encode(v[0]))
+ }
+
+ sort.StringSlice(sarry).Sort()
+ joined := strings.Join(sarry, "&")
+
+ payload := strings.Join([]string{method, host, "/", joined}, "\n")
+ hash := hmac.NewSHA256([]byte(auth.SecretKey))
+ hash.Write([]byte(payload))
+ signature := make([]byte, b64.EncodedLen(hash.Size()))
+ b64.Encode(signature, hash.Sum())
+
+ params["Signature"] = []string{"AWS " + string(signature)}
+ println("Payload:", payload)
+ println("Signature:", strings.Join(params["Signature"], "|"))
+}*/
+
+func sign(auth aws.Auth, method, path string, params map[string]string, host string) {
+ params["AWSAccessKeyId"] = auth.AccessKey
+ params["SignatureVersion"] = "2"
+ params["SignatureMethod"] = "HmacSHA256"
+
+ var sarray []string
+ for k, v := range params {
+ sarray = append(sarray, aws.Encode(k)+"="+aws.Encode(v))
+ }
+ sort.StringSlice(sarray).Sort()
+ 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)
+}
diff --git a/vendor/src/launchpad.net/goamz/exp/sns/sns.go b/vendor/src/launchpad.net/goamz/exp/sns/sns.go
new file mode 100644
index 0000000000..84c434a543
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/exp/sns/sns.go
@@ -0,0 +1,444 @@
+//
+// goamz - Go packages to interact with the Amazon Web Services.
+//
+// https://wiki.ubuntu.com/goamz
+//
+// Copyright (c) 2011 Memeo Inc.
+//
+// Written by Prudhvi Krishna Surapaneni
+
+// This package is in an experimental state, and does not currently
+// follow conventions and style of the rest of goamz or common
+// Go conventions. It must be polished before it's considered a
+// first-class package in goamz.
+package sns
+
+// BUG(niemeyer): Package needs significant clean up.
+
+// BUG(niemeyer): Topic values in responses are not being initialized
+// properly, since they're supposed to reference *SNS.
+
+// BUG(niemeyer): Package needs documentation.
+
+// BUG(niemeyer): Message.Message should be "Payload []byte"
+
+// BUG(niemeyer): Message.SNS must be dropped.
+
+import (
+ "encoding/xml"
+ "errors"
+ "launchpad.net/goamz/aws"
+ "net/http"
+ "net/url"
+ "strconv"
+ "time"
+)
+
+// The SNS type encapsulates operation with an SNS region.
+type SNS struct {
+ aws.Auth
+ aws.Region
+ private byte // Reserve the right of using private data.
+}
+
+type Topic struct {
+ SNS *SNS
+ TopicArn string
+}
+
+func New(auth aws.Auth, region aws.Region) *SNS {
+ return &SNS{auth, region, 0}
+}
+
+type Message struct {
+ SNS *SNS
+ Topic *Topic
+ Message [8192]byte
+ Subject string
+}
+
+type Subscription struct {
+ Endpoint string
+ Owner string
+ Protocol string
+ SubscriptionArn string
+ TopicArn string
+}
+
+func (topic *Topic) Message(message [8192]byte, subject string) *Message {
+ return &Message{topic.SNS, topic, message, subject}
+}
+
+type ResponseMetadata struct {
+ RequestId string `xml:"ResponseMetadata>RequestId"`
+ BoxUsage float64 `xml:"ResponseMetadata>BoxUsage"`
+}
+
+type ListTopicsResp struct {
+ Topics []Topic `xml:"ListTopicsResult>Topics>member"`
+ NextToken string
+ ResponseMetadata
+}
+
+type CreateTopicResp struct {
+ Topic Topic `xml:"CreateTopicResult"`
+ ResponseMetadata
+}
+
+type DeleteTopicResp struct {
+ ResponseMetadata
+}
+
+type ListSubscriptionsResp struct {
+ Subscriptions []Subscription `xml:"ListSubscriptionsResult>Subscriptions>member"`
+ NextToken string
+ ResponseMetadata
+}
+
+type AttributeEntry struct {
+ Key string `xml:"key"`
+ Value string `xml:"value"`
+}
+
+type GetTopicAttributesResp struct {
+ Attributes []AttributeEntry `xml:"GetTopicAttributesResult>Attributes>entry"`
+ ResponseMetadata
+}
+
+func makeParams(action string) map[string]string {
+ params := make(map[string]string)
+ params["Action"] = action
+ return params
+}
+
+// ListTopics
+//
+// See http://goo.gl/lfrMK for more details.
+func (sns *SNS) ListTopics(NextToken *string) (resp *ListTopicsResp, err error) {
+ resp = &ListTopicsResp{}
+ params := makeParams("ListTopics")
+ if NextToken != nil {
+ params["NextToken"] = *NextToken
+ }
+ err = sns.query(nil, nil, params, resp)
+ return
+}
+
+// CreateTopic
+//
+// See http://goo.gl/m9aAt for more details.
+func (sns *SNS) CreateTopic(Name string) (resp *CreateTopicResp, err error) {
+ resp = &CreateTopicResp{}
+ params := makeParams("CreateTopic")
+ params["Name"] = Name
+ err = sns.query(nil, nil, params, resp)
+ return
+}
+
+// DeleteTopic
+//
+// See http://goo.gl/OXNcY for more details.
+func (sns *SNS) DeleteTopic(topic Topic) (resp *DeleteTopicResp, err error) {
+ resp = &DeleteTopicResp{}
+ params := makeParams("DeleteTopic")
+ params["TopicArn"] = topic.TopicArn
+ err = sns.query(nil, nil, params, resp)
+ return
+}
+
+// Delete
+//
+// Helper function for deleting a topic
+func (topic *Topic) Delete() (resp *DeleteTopicResp, err error) {
+ return topic.SNS.DeleteTopic(*topic)
+}
+
+// ListSubscriptions
+//
+// See http://goo.gl/k3aGn for more details.
+func (sns *SNS) ListSubscriptions(NextToken *string) (resp *ListSubscriptionsResp, err error) {
+ resp = &ListSubscriptionsResp{}
+ params := makeParams("ListSubscriptions")
+ if NextToken != nil {
+ params["NextToken"] = *NextToken
+ }
+ err = sns.query(nil, nil, params, resp)
+ return
+}
+
+// GetTopicAttributes
+//
+// See http://goo.gl/WXRoX for more details.
+func (sns *SNS) GetTopicAttributes(TopicArn string) (resp *GetTopicAttributesResp, err error) {
+ resp = &GetTopicAttributesResp{}
+ params := makeParams("GetTopicAttributes")
+ params["TopicArn"] = TopicArn
+ err = sns.query(nil, nil, params, resp)
+ return
+}
+
+type PublishOpt struct {
+ Message string
+ MessageStructure string
+ Subject string
+ TopicArn string
+}
+
+type PublishResp struct {
+ MessageId string `xml:"PublishResult>MessageId"`
+ ResponseMetadata
+}
+
+// Publish
+//
+// See http://goo.gl/AY2D8 for more details.
+func (sns *SNS) Publish(options *PublishOpt) (resp *PublishResp, err error) {
+ resp = &PublishResp{}
+ params := makeParams("Publish")
+
+ if options.Subject != "" {
+ params["Subject"] = options.Subject
+ }
+
+ if options.MessageStructure != "" {
+ params["MessageStructure"] = options.MessageStructure
+ }
+
+ if options.Message != "" {
+ params["Message"] = options.Message
+ }
+
+ if options.TopicArn != "" {
+ params["TopicArn"] = options.TopicArn
+ }
+
+ err = sns.query(nil, nil, params, resp)
+ return
+}
+
+type SetTopicAttributesResponse struct {
+ ResponseMetadata
+}
+
+// SetTopicAttributes
+//
+// See http://goo.gl/oVYW7 for more details.
+func (sns *SNS) SetTopicAttributes(AttributeName, AttributeValue, TopicArn string) (resp *SetTopicAttributesResponse, err error) {
+ resp = &SetTopicAttributesResponse{}
+ params := makeParams("SetTopicAttributes")
+
+ if AttributeName == "" || TopicArn == "" {
+ return nil, errors.New("Invalid Attribute Name or TopicArn")
+ }
+
+ params["AttributeName"] = AttributeName
+ params["AttributeValue"] = AttributeValue
+ params["TopicArn"] = TopicArn
+
+ err = sns.query(nil, nil, params, resp)
+ return
+}
+
+type SubscribeResponse struct {
+ SubscriptionArn string `xml:"SubscribeResult>SubscriptionArn"`
+ ResponseMetadata
+}
+
+// Subscribe
+//
+// See http://goo.gl/c3iGS for more details.
+func (sns *SNS) Subscribe(Endpoint, Protocol, TopicArn string) (resp *SubscribeResponse, err error) {
+ resp = &SubscribeResponse{}
+ params := makeParams("Subscribe")
+
+ params["Endpoint"] = Endpoint
+ params["Protocol"] = Protocol
+ params["TopicArn"] = TopicArn
+
+ err = sns.query(nil, nil, params, resp)
+ return
+}
+
+type UnsubscribeResponse struct {
+ ResponseMetadata
+}
+
+// Unsubscribe
+//
+// See http://goo.gl/4l5Ge for more details.
+func (sns *SNS) Unsubscribe(SubscriptionArn string) (resp *UnsubscribeResponse, err error) {
+ resp = &UnsubscribeResponse{}
+ params := makeParams("Unsubscribe")
+
+ params["SubscriptionArn"] = SubscriptionArn
+
+ err = sns.query(nil, nil, params, resp)
+ return
+}
+
+type ConfirmSubscriptionResponse struct {
+ SubscriptionArn string `xml:"ConfirmSubscriptionResult>SubscriptionArn"`
+ ResponseMetadata
+}
+
+type ConfirmSubscriptionOpt struct {
+ AuthenticateOnUnsubscribe string
+ Token string
+ TopicArn string
+}
+
+// ConfirmSubscription
+//
+// See http://goo.gl/3hXzH for more details.
+func (sns *SNS) ConfirmSubscription(options *ConfirmSubscriptionOpt) (resp *ConfirmSubscriptionResponse, err error) {
+ resp = &ConfirmSubscriptionResponse{}
+ params := makeParams("ConfirmSubscription")
+
+ if options.AuthenticateOnUnsubscribe != "" {
+ params["AuthenticateOnUnsubscribe"] = options.AuthenticateOnUnsubscribe
+ }
+
+ params["Token"] = options.Token
+ params["TopicArn"] = options.TopicArn
+
+ err = sns.query(nil, nil, params, resp)
+ return
+}
+
+type Permission struct {
+ ActionName string
+ AccountId string
+}
+
+type AddPermissionResponse struct {
+ ResponseMetadata
+}
+
+// AddPermission
+//
+// See http://goo.gl/mbY4a for more details.
+func (sns *SNS) AddPermission(permissions []Permission, Label, TopicArn string) (resp *AddPermissionResponse, err error) {
+ resp = &AddPermissionResponse{}
+ params := makeParams("AddPermission")
+
+ for i, p := range permissions {
+ params["AWSAccountId.member."+strconv.Itoa(i+1)] = p.AccountId
+ params["ActionName.member."+strconv.Itoa(i+1)] = p.ActionName
+ }
+
+ params["Label"] = Label
+ params["TopicArn"] = TopicArn
+
+ err = sns.query(nil, nil, params, resp)
+ return
+}
+
+type RemovePermissionResponse struct {
+ ResponseMetadata
+}
+
+// RemovePermission
+//
+// See http://goo.gl/wGl5j for more details.
+func (sns *SNS) RemovePermission(Label, TopicArn string) (resp *RemovePermissionResponse, err error) {
+ resp = &RemovePermissionResponse{}
+ params := makeParams("RemovePermission")
+
+ params["Label"] = Label
+ params["TopicArn"] = TopicArn
+
+ err = sns.query(nil, nil, params, resp)
+ return
+}
+
+type ListSubscriptionByTopicResponse struct {
+ Subscriptions []Subscription `xml:"ListSubscriptionsByTopicResult>Subscriptions>member"`
+ ResponseMetadata
+}
+
+type ListSubscriptionByTopicOpt struct {
+ NextToken string
+ TopicArn string
+}
+
+// ListSubscriptionByTopic
+//
+// See http://goo.gl/LaVcC for more details.
+func (sns *SNS) ListSubscriptionByTopic(options *ListSubscriptionByTopicOpt) (resp *ListSubscriptionByTopicResponse, err error) {
+ resp = &ListSubscriptionByTopicResponse{}
+ params := makeParams("ListSbubscriptionByTopic")
+
+ if options.NextToken != "" {
+ params["NextToken"] = options.NextToken
+ }
+
+ params["TopicArn"] = options.TopicArn
+
+ err = sns.query(nil, nil, params, resp)
+ return
+}
+
+type Error struct {
+ StatusCode int
+ Code string
+ Message string
+ RequestId string
+}
+
+func (err *Error) Error() string {
+ return err.Message
+}
+
+type xmlErrors struct {
+ RequestId string
+ Errors []Error `xml:"Errors>Error"`
+}
+
+func (sns *SNS) query(topic *Topic, message *Message, params map[string]string, resp interface{}) error {
+ params["Timestamp"] = time.Now().UTC().Format(time.RFC3339)
+ u, err := url.Parse(sns.Region.SNSEndpoint)
+ if err != nil {
+ return err
+ }
+
+ sign(sns.Auth, "GET", "/", params, u.Host)
+ u.RawQuery = multimap(params).Encode()
+ r, err := http.Get(u.String())
+ if err != nil {
+ return err
+ }
+ defer r.Body.Close()
+
+ //dump, _ := http.DumpResponse(r, true)
+ //println("DUMP:\n", string(dump))
+ //return nil
+
+ if r.StatusCode != 200 {
+ return buildError(r)
+ }
+ err = xml.NewDecoder(r.Body).Decode(resp)
+ return err
+}
+
+func buildError(r *http.Response) error {
+ errors := xmlErrors{}
+ xml.NewDecoder(r.Body).Decode(&errors)
+ var err Error
+ if len(errors.Errors) > 0 {
+ err = errors.Errors[0]
+ }
+ err.RequestId = errors.RequestId
+ err.StatusCode = r.StatusCode
+ if err.Message == "" {
+ err.Message = r.Status
+ }
+ return &err
+}
+
+func multimap(p map[string]string) url.Values {
+ q := make(url.Values, len(p))
+ for k, v := range p {
+ q[k] = []string{v}
+ }
+ return q
+}
diff --git a/vendor/src/launchpad.net/goamz/exp/sns/sns_test.go b/vendor/src/launchpad.net/goamz/exp/sns/sns_test.go
new file mode 100644
index 0000000000..6ecaf3be4d
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/exp/sns/sns_test.go
@@ -0,0 +1,245 @@
+package sns_test
+
+import (
+ "launchpad.net/goamz/aws"
+ "launchpad.net/goamz/exp/sns"
+ "launchpad.net/goamz/testutil"
+ . "launchpad.net/gocheck"
+ "testing"
+)
+
+func Test(t *testing.T) {
+ TestingT(t)
+}
+
+var _ = Suite(&S{})
+
+type S struct {
+ sns *sns.SNS
+}
+
+var testServer = testutil.NewHTTPServer()
+
+func (s *S) SetUpSuite(c *C) {
+ testServer.Start()
+ auth := aws.Auth{"abc", "123"}
+ s.sns = sns.New(auth, aws.Region{SNSEndpoint: testServer.URL})
+}
+
+func (s *S) TearDownSuite(c *C) {
+ testServer.Stop()
+}
+
+func (s *S) TearDownTest(c *C) {
+ testServer.Flush()
+}
+
+func (s *S) TestListTopicsOK(c *C) {
+ testServer.Response(200, nil, TestListTopicsXmlOK)
+
+ resp, err := s.sns.ListTopics(nil)
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+
+ c.Assert(resp.ResponseMetadata.RequestId, Equals, "bd10b26c-e30e-11e0-ba29-93c3aca2f103")
+ c.Assert(err, IsNil)
+}
+
+func (s *S) TestCreateTopic(c *C) {
+ testServer.Response(200, nil, TestCreateTopicXmlOK)
+
+ resp, err := s.sns.CreateTopic("My-Topic")
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+
+ c.Assert(resp.Topic.TopicArn, Equals, "arn:aws:sns:us-east-1:123456789012:My-Topic")
+ c.Assert(resp.ResponseMetadata.RequestId, Equals, "a8dec8b3-33a4-11df-8963-01868b7c937a")
+ c.Assert(err, IsNil)
+}
+
+func (s *S) TestDeleteTopic(c *C) {
+ testServer.Response(200, nil, TestDeleteTopicXmlOK)
+
+ t := sns.Topic{nil, "arn:aws:sns:us-east-1:123456789012:My-Topic"}
+ resp, err := s.sns.DeleteTopic(t)
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+
+ c.Assert(resp.ResponseMetadata.RequestId, Equals, "f3aa9ac9-3c3d-11df-8235-9dab105e9c32")
+ c.Assert(err, IsNil)
+}
+
+func (s *S) TestListSubscriptions(c *C) {
+ testServer.Response(200, nil, TestListSubscriptionsXmlOK)
+
+ resp, err := s.sns.ListSubscriptions(nil)
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+
+ c.Assert(len(resp.Subscriptions), Not(Equals), 0)
+ c.Assert(resp.Subscriptions[0].Protocol, Equals, "email")
+ c.Assert(resp.Subscriptions[0].Endpoint, Equals, "example@amazon.com")
+ c.Assert(resp.Subscriptions[0].SubscriptionArn, Equals, "arn:aws:sns:us-east-1:123456789012:My-Topic:80289ba6-0fd4-4079-afb4-ce8c8260f0ca")
+ c.Assert(resp.Subscriptions[0].TopicArn, Equals, "arn:aws:sns:us-east-1:698519295917:My-Topic")
+ c.Assert(resp.Subscriptions[0].Owner, Equals, "123456789012")
+ c.Assert(err, IsNil)
+}
+
+func (s *S) TestGetTopicAttributes(c *C) {
+ testServer.Response(200, nil, TestGetTopicAttributesXmlOK)
+
+ resp, err := s.sns.GetTopicAttributes("arn:aws:sns:us-east-1:123456789012:My-Topic")
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+
+ c.Assert(len(resp.Attributes), Not(Equals), 0)
+ c.Assert(resp.Attributes[0].Key, Equals, "Owner")
+ c.Assert(resp.Attributes[0].Value, Equals, "123456789012")
+ c.Assert(resp.Attributes[1].Key, Equals, "Policy")
+ c.Assert(resp.Attributes[1].Value, Equals, `{"Version":"2008-10-17","Id":"us-east-1/698519295917/test__default_policy_ID","Statement" : [{"Effect":"Allow","Sid":"us-east-1/698519295917/test__default_statement_ID","Principal" : {"AWS": "*"},"Action":["SNS:GetTopicAttributes","SNS:SetTopicAttributes","SNS:AddPermission","SNS:RemovePermission","SNS:DeleteTopic","SNS:Subscribe","SNS:ListSubscriptionsByTopic","SNS:Publish","SNS:Receive"],"Resource":"arn:aws:sns:us-east-1:698519295917:test","Condition" : {"StringLike" : {"AWS:SourceArn": "arn:aws:*:*:698519295917:*"}}}]}`)
+ c.Assert(resp.ResponseMetadata.RequestId, Equals, "057f074c-33a7-11df-9540-99d0768312d3")
+ c.Assert(err, IsNil)
+}
+
+func (s *S) TestPublish(c *C) {
+ testServer.Response(200, nil, TestPublishXmlOK)
+
+ pubOpt := &sns.PublishOpt{"foobar", "", "subject", "arn:aws:sns:us-east-1:123456789012:My-Topic"}
+ resp, err := s.sns.Publish(pubOpt)
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+
+ c.Assert(resp.MessageId, Equals, "94f20ce6-13c5-43a0-9a9e-ca52d816e90b")
+ c.Assert(resp.ResponseMetadata.RequestId, Equals, "f187a3c1-376f-11df-8963-01868b7c937a")
+ c.Assert(err, IsNil)
+}
+
+func (s *S) TestSetTopicAttributes(c *C) {
+ testServer.Response(200, nil, TestSetTopicAttributesXmlOK)
+
+ resp, err := s.sns.SetTopicAttributes("DisplayName", "MyTopicName", "arn:aws:sns:us-east-1:123456789012:My-Topic")
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+
+ c.Assert(resp.ResponseMetadata.RequestId, Equals, "a8763b99-33a7-11df-a9b7-05d48da6f042")
+ c.Assert(err, IsNil)
+}
+
+func (s *S) TestSubscribe(c *C) {
+ testServer.Response(200, nil, TestSubscribeXmlOK)
+
+ resp, err := s.sns.Subscribe("example@amazon.com", "email", "arn:aws:sns:us-east-1:123456789012:My-Topic")
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+
+ c.Assert(resp.SubscriptionArn, Equals, "pending confirmation")
+ c.Assert(resp.ResponseMetadata.RequestId, Equals, "a169c740-3766-11df-8963-01868b7c937a")
+ c.Assert(err, IsNil)
+}
+
+func (s *S) TestUnsubscribe(c *C) {
+ testServer.Response(200, nil, TestUnsubscribeXmlOK)
+
+ resp, err := s.sns.Unsubscribe("arn:aws:sns:us-east-1:123456789012:My-Topic:a169c740-3766-11df-8963-01868b7c937a")
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+
+ c.Assert(resp.ResponseMetadata.RequestId, Equals, "18e0ac39-3776-11df-84c0-b93cc1666b84")
+ c.Assert(err, IsNil)
+}
+
+func (s *S) TestConfirmSubscription(c *C) {
+ testServer.Response(200, nil, TestConfirmSubscriptionXmlOK)
+
+ opt := &sns.ConfirmSubscriptionOpt{"", "51b2ff3edb475b7d91550e0ab6edf0c1de2a34e6ebaf6", "arn:aws:sns:us-east-1:123456789012:My-Topic"}
+ resp, err := s.sns.ConfirmSubscription(opt)
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+
+ c.Assert(resp.SubscriptionArn, Equals, "arn:aws:sns:us-east-1:123456789012:My-Topic:80289ba6-0fd4-4079-afb4-ce8c8260f0ca")
+ c.Assert(resp.ResponseMetadata.RequestId, Equals, "7a50221f-3774-11df-a9b7-05d48da6f042")
+ c.Assert(err, IsNil)
+}
+
+func (s *S) TestAddPermission(c *C) {
+ testServer.Response(200, nil, TestAddPermissionXmlOK)
+ perm := make([]sns.Permission, 2)
+ perm[0].ActionName = "Publish"
+ perm[1].ActionName = "GetTopicAttributes"
+ perm[0].AccountId = "987654321000"
+ perm[1].AccountId = "876543210000"
+
+ resp, err := s.sns.AddPermission(perm, "NewPermission", "arn:aws:sns:us-east-1:123456789012:My-Topic")
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+
+ c.Assert(resp.RequestId, Equals, "6a213e4e-33a8-11df-9540-99d0768312d3")
+ c.Assert(err, IsNil)
+}
+
+func (s *S) TestRemovePermission(c *C) {
+ testServer.Response(200, nil, TestRemovePermissionXmlOK)
+
+ resp, err := s.sns.RemovePermission("NewPermission", "arn:aws:sns:us-east-1:123456789012:My-Topic")
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+
+ c.Assert(resp.RequestId, Equals, "d170b150-33a8-11df-995a-2d6fbe836cc1")
+ c.Assert(err, IsNil)
+}
+
+func (s *S) TestListSubscriptionByTopic(c *C) {
+ testServer.Response(200, nil, TestListSubscriptionsByTopicXmlOK)
+
+ opt := &sns.ListSubscriptionByTopicOpt{"", "arn:aws:sns:us-east-1:123456789012:My-Topic"}
+ resp, err := s.sns.ListSubscriptionByTopic(opt)
+ req := testServer.WaitRequest()
+
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+
+ c.Assert(len(resp.Subscriptions), Not(Equals), 0)
+ c.Assert(resp.Subscriptions[0].TopicArn, Equals, "arn:aws:sns:us-east-1:123456789012:My-Topic")
+ c.Assert(resp.Subscriptions[0].SubscriptionArn, Equals, "arn:aws:sns:us-east-1:123456789012:My-Topic:80289ba6-0fd4-4079-afb4-ce8c8260f0ca")
+ c.Assert(resp.Subscriptions[0].Owner, Equals, "123456789012")
+ c.Assert(resp.Subscriptions[0].Endpoint, Equals, "example@amazon.com")
+ c.Assert(resp.Subscriptions[0].Protocol, Equals, "email")
+ c.Assert(err, IsNil)
+}
diff --git a/vendor/src/launchpad.net/goamz/iam/iam.go b/vendor/src/launchpad.net/goamz/iam/iam.go
new file mode 100644
index 0000000000..348cfea177
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/iam/iam.go
@@ -0,0 +1,432 @@
+// The iam package provides types and functions for interaction with the AWS
+// Identity and Access Management (IAM) service.
+package iam
+
+import (
+ "encoding/xml"
+ "launchpad.net/goamz/aws"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// The IAM type encapsulates operations operations with the IAM endpoint.
+type IAM struct {
+ aws.Auth
+ aws.Region
+}
+
+// New creates a new IAM instance.
+func New(auth aws.Auth, region aws.Region) *IAM {
+ return &IAM{auth, region}
+}
+
+func (iam *IAM) query(params map[string]string, resp interface{}) error {
+ params["Version"] = "2010-05-08"
+ params["Timestamp"] = time.Now().In(time.UTC).Format(time.RFC3339)
+ endpoint, err := url.Parse(iam.IAMEndpoint)
+ if err != nil {
+ return err
+ }
+ sign(iam.Auth, "GET", "/", params, endpoint.Host)
+ endpoint.RawQuery = multimap(params).Encode()
+ r, err := http.Get(endpoint.String())
+ if err != nil {
+ return err
+ }
+ defer r.Body.Close()
+ if r.StatusCode > 200 {
+ return buildError(r)
+ }
+ return xml.NewDecoder(r.Body).Decode(resp)
+}
+
+func (iam *IAM) postQuery(params map[string]string, resp interface{}) error {
+ endpoint, err := url.Parse(iam.IAMEndpoint)
+ if err != nil {
+ return err
+ }
+ params["Version"] = "2010-05-08"
+ params["Timestamp"] = time.Now().In(time.UTC).Format(time.RFC3339)
+ sign(iam.Auth, "POST", "/", params, endpoint.Host)
+ encoded := multimap(params).Encode()
+ body := strings.NewReader(encoded)
+ req, err := http.NewRequest("POST", endpoint.String(), body)
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Host", endpoint.Host)
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Set("Content-Length", strconv.Itoa(len(encoded)))
+ r, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return err
+ }
+ defer r.Body.Close()
+ if r.StatusCode > 200 {
+ return buildError(r)
+ }
+ return xml.NewDecoder(r.Body).Decode(resp)
+}
+
+func buildError(r *http.Response) error {
+ var (
+ err Error
+ errors xmlErrors
+ )
+ xml.NewDecoder(r.Body).Decode(&errors)
+ if len(errors.Errors) > 0 {
+ err = errors.Errors[0]
+ }
+ err.StatusCode = r.StatusCode
+ if err.Message == "" {
+ err.Message = r.Status
+ }
+ return &err
+}
+
+func multimap(p map[string]string) url.Values {
+ q := make(url.Values, len(p))
+ for k, v := range p {
+ q[k] = []string{v}
+ }
+ return q
+}
+
+// Response to a CreateUser request.
+//
+// See http://goo.gl/JS9Gz for more details.
+type CreateUserResp struct {
+ RequestId string `xml:"ResponseMetadata>RequestId"`
+ User User `xml:"CreateUserResult>User"`
+}
+
+// User encapsulates a user managed by IAM.
+//
+// See http://goo.gl/BwIQ3 for more details.
+type User struct {
+ Arn string
+ Path string
+ Id string `xml:"UserId"`
+ Name string `xml:"UserName"`
+}
+
+// CreateUser creates a new user in IAM.
+//
+// See http://goo.gl/JS9Gz for more details.
+func (iam *IAM) CreateUser(name, path string) (*CreateUserResp, error) {
+ params := map[string]string{
+ "Action": "CreateUser",
+ "Path": path,
+ "UserName": name,
+ }
+ resp := new(CreateUserResp)
+ if err := iam.query(params, resp); err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// Response for GetUser requests.
+//
+// See http://goo.gl/ZnzRN for more details.
+type GetUserResp struct {
+ RequestId string `xml:"ResponseMetadata>RequestId"`
+ User User `xml:"GetUserResult>User"`
+}
+
+// GetUser gets a user from IAM.
+//
+// See http://goo.gl/ZnzRN for more details.
+func (iam *IAM) GetUser(name string) (*GetUserResp, error) {
+ params := map[string]string{
+ "Action": "GetUser",
+ "UserName": name,
+ }
+ resp := new(GetUserResp)
+ if err := iam.query(params, resp); err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// DeleteUser deletes a user from IAM.
+//
+// See http://goo.gl/jBuCG for more details.
+func (iam *IAM) DeleteUser(name string) (*SimpleResp, error) {
+ params := map[string]string{
+ "Action": "DeleteUser",
+ "UserName": name,
+ }
+ resp := new(SimpleResp)
+ if err := iam.query(params, resp); err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// Response to a CreateGroup request.
+//
+// See http://goo.gl/n7NNQ for more details.
+type CreateGroupResp struct {
+ Group Group `xml:"CreateGroupResult>Group"`
+ RequestId string `xml:"ResponseMetadata>RequestId"`
+}
+
+// Group encapsulates a group managed by IAM.
+//
+// See http://goo.gl/ae7Vs for more details.
+type Group struct {
+ Arn string
+ Id string `xml:"GroupId"`
+ Name string `xml:"GroupName"`
+ Path string
+}
+
+// CreateGroup creates a new group in IAM.
+//
+// The path parameter can be used to identify which division or part of the
+// organization the user belongs to.
+//
+// If path is unset ("") it defaults to "/".
+//
+// See http://goo.gl/n7NNQ for more details.
+func (iam *IAM) CreateGroup(name string, path string) (*CreateGroupResp, error) {
+ params := map[string]string{
+ "Action": "CreateGroup",
+ "GroupName": name,
+ }
+ if path != "" {
+ params["Path"] = path
+ }
+ resp := new(CreateGroupResp)
+ if err := iam.query(params, resp); err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// Response to a ListGroups request.
+//
+// See http://goo.gl/W2TRj for more details.
+type GroupsResp struct {
+ Groups []Group `xml:"ListGroupsResult>Groups>member"`
+ RequestId string `xml:"ResponseMetadata>RequestId"`
+}
+
+// Groups list the groups that have the specified path prefix.
+//
+// The parameter pathPrefix is optional. If pathPrefix is "", all groups are
+// returned.
+//
+// See http://goo.gl/W2TRj for more details.
+func (iam *IAM) Groups(pathPrefix string) (*GroupsResp, error) {
+ params := map[string]string{
+ "Action": "ListGroups",
+ }
+ if pathPrefix != "" {
+ params["PathPrefix"] = pathPrefix
+ }
+ resp := new(GroupsResp)
+ if err := iam.query(params, resp); err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// DeleteGroup deletes a group from IAM.
+//
+// See http://goo.gl/d5i2i for more details.
+func (iam *IAM) DeleteGroup(name string) (*SimpleResp, error) {
+ params := map[string]string{
+ "Action": "DeleteGroup",
+ "GroupName": name,
+ }
+ resp := new(SimpleResp)
+ if err := iam.query(params, resp); err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// Response to a CreateAccessKey request.
+//
+// See http://goo.gl/L46Py for more details.
+type CreateAccessKeyResp struct {
+ RequestId string `xml:"ResponseMetadata>RequestId"`
+ AccessKey AccessKey `xml:"CreateAccessKeyResult>AccessKey"`
+}
+
+// AccessKey encapsulates an access key generated for a user.
+//
+// See http://goo.gl/LHgZR for more details.
+type AccessKey struct {
+ UserName string
+ Id string `xml:"AccessKeyId"`
+ Secret string `xml:"SecretAccessKey,omitempty"`
+ Status string
+}
+
+// CreateAccessKey creates a new access key in IAM.
+//
+// See http://goo.gl/L46Py for more details.
+func (iam *IAM) CreateAccessKey(userName string) (*CreateAccessKeyResp, error) {
+ params := map[string]string{
+ "Action": "CreateAccessKey",
+ "UserName": userName,
+ }
+ resp := new(CreateAccessKeyResp)
+ if err := iam.query(params, resp); err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// Response to AccessKeys request.
+//
+// See http://goo.gl/Vjozx for more details.
+type AccessKeysResp struct {
+ RequestId string `xml:"ResponseMetadata>RequestId"`
+ AccessKeys []AccessKey `xml:"ListAccessKeysResult>AccessKeyMetadata>member"`
+}
+
+// AccessKeys lists all acccess keys associated with a user.
+//
+// The userName parameter is optional. If set to "", the userName is determined
+// implicitly based on the AWS Access Key ID used to sign the request.
+//
+// See http://goo.gl/Vjozx for more details.
+func (iam *IAM) AccessKeys(userName string) (*AccessKeysResp, error) {
+ params := map[string]string{
+ "Action": "ListAccessKeys",
+ }
+ if userName != "" {
+ params["UserName"] = userName
+ }
+ resp := new(AccessKeysResp)
+ if err := iam.query(params, resp); err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// DeleteAccessKey deletes an access key from IAM.
+//
+// The userName parameter is optional. If set to "", the userName is determined
+// implicitly based on the AWS Access Key ID used to sign the request.
+//
+// See http://goo.gl/hPGhw for more details.
+func (iam *IAM) DeleteAccessKey(id, userName string) (*SimpleResp, error) {
+ params := map[string]string{
+ "Action": "DeleteAccessKey",
+ "AccessKeyId": id,
+ }
+ if userName != "" {
+ params["UserName"] = userName
+ }
+ resp := new(SimpleResp)
+ if err := iam.query(params, resp); err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// Response to a GetUserPolicy request.
+//
+// See http://goo.gl/BH04O for more details.
+type GetUserPolicyResp struct {
+ Policy UserPolicy `xml:"GetUserPolicyResult"`
+ RequestId string `xml:"ResponseMetadata>RequestId"`
+}
+
+// UserPolicy encapsulates an IAM group policy.
+//
+// See http://goo.gl/C7hgS for more details.
+type UserPolicy struct {
+ Name string `xml:"PolicyName"`
+ UserName string `xml:"UserName"`
+ Document string `xml:"PolicyDocument"`
+}
+
+// GetUserPolicy gets a user policy in IAM.
+//
+// See http://goo.gl/BH04O for more details.
+func (iam *IAM) GetUserPolicy(userName, policyName string) (*GetUserPolicyResp, error) {
+ params := map[string]string{
+ "Action": "GetUserPolicy",
+ "UserName": userName,
+ "PolicyName": policyName,
+ }
+ resp := new(GetUserPolicyResp)
+ if err := iam.query(params, resp); err != nil {
+ return nil, err
+ }
+ return resp, nil
+ return nil, nil
+}
+
+// PutUserPolicy creates a user policy in IAM.
+//
+// See http://goo.gl/ldCO8 for more details.
+func (iam *IAM) PutUserPolicy(userName, policyName, policyDocument string) (*SimpleResp, error) {
+ params := map[string]string{
+ "Action": "PutUserPolicy",
+ "UserName": userName,
+ "PolicyName": policyName,
+ "PolicyDocument": policyDocument,
+ }
+ resp := new(SimpleResp)
+ if err := iam.postQuery(params, resp); err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// DeleteUserPolicy deletes a user policy from IAM.
+//
+// See http://goo.gl/7Jncn for more details.
+func (iam *IAM) DeleteUserPolicy(userName, policyName string) (*SimpleResp, error) {
+ params := map[string]string{
+ "Action": "DeleteUserPolicy",
+ "PolicyName": policyName,
+ "UserName": userName,
+ }
+ resp := new(SimpleResp)
+ if err := iam.query(params, resp); err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+type SimpleResp struct {
+ RequestId string `xml:"ResponseMetadata>RequestId"`
+}
+
+type xmlErrors struct {
+ Errors []Error `xml:"Error"`
+}
+
+// Error encapsulates an IAM error.
+type Error struct {
+ // HTTP status code of the error.
+ StatusCode int
+
+ // AWS code of the error.
+ Code string
+
+ // Message explaining the error.
+ Message string
+}
+
+func (e *Error) Error() string {
+ var prefix string
+ if e.Code != "" {
+ prefix = e.Code + ": "
+ }
+ if prefix == "" && e.StatusCode > 0 {
+ prefix = strconv.Itoa(e.StatusCode) + ": "
+ }
+ return prefix + e.Message
+}
diff --git a/vendor/src/launchpad.net/goamz/iam/iam_test.go b/vendor/src/launchpad.net/goamz/iam/iam_test.go
new file mode 100644
index 0000000000..fa2631d04c
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/iam/iam_test.go
@@ -0,0 +1,282 @@
+package iam_test
+
+import (
+ "launchpad.net/goamz/aws"
+ "launchpad.net/goamz/iam"
+ "launchpad.net/goamz/testutil"
+ . "launchpad.net/gocheck"
+ "strings"
+ "testing"
+)
+
+func Test(t *testing.T) {
+ TestingT(t)
+}
+
+type S struct {
+ iam *iam.IAM
+}
+
+var _ = Suite(&S{})
+
+var testServer = testutil.NewHTTPServer()
+
+func (s *S) SetUpSuite(c *C) {
+ testServer.Start()
+ auth := aws.Auth{"abc", "123"}
+ s.iam = iam.New(auth, aws.Region{IAMEndpoint: testServer.URL})
+}
+
+func (s *S) TearDownSuite(c *C) {
+ testServer.Stop()
+}
+
+func (s *S) TearDownTest(c *C) {
+ testServer.Flush()
+}
+
+func (s *S) TestCreateUser(c *C) {
+ testServer.Response(200, nil, CreateUserExample)
+ resp, err := s.iam.CreateUser("Bob", "/division_abc/subdivision_xyz/")
+ values := testServer.WaitRequest().URL.Query()
+ c.Assert(values.Get("Action"), Equals, "CreateUser")
+ c.Assert(values.Get("UserName"), Equals, "Bob")
+ c.Assert(values.Get("Path"), Equals, "/division_abc/subdivision_xyz/")
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE")
+ expected := iam.User{
+ Path: "/division_abc/subdivision_xyz/",
+ Name: "Bob",
+ Id: "AIDACKCEVSQ6C2EXAMPLE",
+ Arn: "arn:aws:iam::123456789012:user/division_abc/subdivision_xyz/Bob",
+ }
+ c.Assert(resp.User, DeepEquals, expected)
+}
+
+func (s *S) TestCreateUserConflict(c *C) {
+ testServer.Response(409, nil, DuplicateUserExample)
+ resp, err := s.iam.CreateUser("Bob", "/division_abc/subdivision_xyz/")
+ testServer.WaitRequest()
+ c.Assert(resp, IsNil)
+ c.Assert(err, NotNil)
+ e, ok := err.(*iam.Error)
+ c.Assert(ok, Equals, true)
+ c.Assert(e.Message, Equals, "User with name Bob already exists.")
+ c.Assert(e.Code, Equals, "EntityAlreadyExists")
+}
+
+func (s *S) TestGetUser(c *C) {
+ testServer.Response(200, nil, GetUserExample)
+ resp, err := s.iam.GetUser("Bob")
+ values := testServer.WaitRequest().URL.Query()
+ c.Assert(values.Get("Action"), Equals, "GetUser")
+ c.Assert(values.Get("UserName"), Equals, "Bob")
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE")
+ expected := iam.User{
+ Path: "/division_abc/subdivision_xyz/",
+ Name: "Bob",
+ Id: "AIDACKCEVSQ6C2EXAMPLE",
+ Arn: "arn:aws:iam::123456789012:user/division_abc/subdivision_xyz/Bob",
+ }
+ c.Assert(resp.User, DeepEquals, expected)
+}
+
+func (s *S) TestDeleteUser(c *C) {
+ testServer.Response(200, nil, RequestIdExample)
+ resp, err := s.iam.DeleteUser("Bob")
+ values := testServer.WaitRequest().URL.Query()
+ c.Assert(values.Get("Action"), Equals, "DeleteUser")
+ c.Assert(values.Get("UserName"), Equals, "Bob")
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE")
+}
+
+func (s *S) TestCreateGroup(c *C) {
+ testServer.Response(200, nil, CreateGroupExample)
+ resp, err := s.iam.CreateGroup("Admins", "/admins/")
+ values := testServer.WaitRequest().URL.Query()
+ c.Assert(values.Get("Action"), Equals, "CreateGroup")
+ c.Assert(values.Get("GroupName"), Equals, "Admins")
+ c.Assert(values.Get("Path"), Equals, "/admins/")
+ c.Assert(err, IsNil)
+ c.Assert(resp.Group.Path, Equals, "/admins/")
+ c.Assert(resp.Group.Name, Equals, "Admins")
+ c.Assert(resp.Group.Id, Equals, "AGPACKCEVSQ6C2EXAMPLE")
+ c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE")
+}
+
+func (s *S) TestCreateGroupWithoutPath(c *C) {
+ testServer.Response(200, nil, CreateGroupExample)
+ _, err := s.iam.CreateGroup("Managers", "")
+ values := testServer.WaitRequest().URL.Query()
+ c.Assert(values.Get("Action"), Equals, "CreateGroup")
+ c.Assert(err, IsNil)
+ _, ok := map[string][]string(values)["Path"]
+ c.Assert(ok, Equals, false)
+}
+
+func (s *S) TestDeleteGroup(c *C) {
+ testServer.Response(200, nil, RequestIdExample)
+ resp, err := s.iam.DeleteGroup("Admins")
+ values := testServer.WaitRequest().URL.Query()
+ c.Assert(values.Get("Action"), Equals, "DeleteGroup")
+ c.Assert(values.Get("GroupName"), Equals, "Admins")
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE")
+}
+
+func (s *S) TestListGroups(c *C) {
+ testServer.Response(200, nil, ListGroupsExample)
+ resp, err := s.iam.Groups("/division_abc/")
+ values := testServer.WaitRequest().URL.Query()
+ c.Assert(values.Get("Action"), Equals, "ListGroups")
+ c.Assert(values.Get("PathPrefix"), Equals, "/division_abc/")
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE")
+ expected := []iam.Group{
+ {
+ Path: "/division_abc/subdivision_xyz/",
+ Name: "Admins",
+ Id: "AGPACKCEVSQ6C2EXAMPLE",
+ Arn: "arn:aws:iam::123456789012:group/Admins",
+ },
+ {
+ Path: "/division_abc/subdivision_xyz/product_1234/engineering/",
+ Name: "Test",
+ Id: "AGP2MAB8DPLSRHEXAMPLE",
+ Arn: "arn:aws:iam::123456789012:group/division_abc/subdivision_xyz/product_1234/engineering/Test",
+ },
+ {
+ Path: "/division_abc/subdivision_xyz/product_1234/",
+ Name: "Managers",
+ Id: "AGPIODR4TAW7CSEXAMPLE",
+ Arn: "arn:aws:iam::123456789012:group/division_abc/subdivision_xyz/product_1234/Managers",
+ },
+ }
+ c.Assert(resp.Groups, DeepEquals, expected)
+}
+
+func (s *S) TestListGroupsWithoutPathPrefix(c *C) {
+ testServer.Response(200, nil, ListGroupsExample)
+ _, err := s.iam.Groups("")
+ values := testServer.WaitRequest().URL.Query()
+ c.Assert(values.Get("Action"), Equals, "ListGroups")
+ c.Assert(err, IsNil)
+ _, ok := map[string][]string(values)["PathPrefix"]
+ c.Assert(ok, Equals, false)
+}
+
+func (s *S) TestCreateAccessKey(c *C) {
+ testServer.Response(200, nil, CreateAccessKeyExample)
+ resp, err := s.iam.CreateAccessKey("Bob")
+ values := testServer.WaitRequest().URL.Query()
+ c.Assert(values.Get("Action"), Equals, "CreateAccessKey")
+ c.Assert(values.Get("UserName"), Equals, "Bob")
+ c.Assert(err, IsNil)
+ c.Assert(resp.AccessKey.UserName, Equals, "Bob")
+ c.Assert(resp.AccessKey.Id, Equals, "AKIAIOSFODNN7EXAMPLE")
+ c.Assert(resp.AccessKey.Secret, Equals, "wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY")
+ c.Assert(resp.AccessKey.Status, Equals, "Active")
+}
+
+func (s *S) TestDeleteAccessKey(c *C) {
+ testServer.Response(200, nil, RequestIdExample)
+ resp, err := s.iam.DeleteAccessKey("ysa8hasdhasdsi", "Bob")
+ values := testServer.WaitRequest().URL.Query()
+ c.Assert(values.Get("Action"), Equals, "DeleteAccessKey")
+ c.Assert(values.Get("AccessKeyId"), Equals, "ysa8hasdhasdsi")
+ c.Assert(values.Get("UserName"), Equals, "Bob")
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE")
+}
+
+func (s *S) TestDeleteAccessKeyBlankUserName(c *C) {
+ testServer.Response(200, nil, RequestIdExample)
+ _, err := s.iam.DeleteAccessKey("ysa8hasdhasdsi", "")
+ c.Assert(err, IsNil)
+ values := testServer.WaitRequest().URL.Query()
+ c.Assert(values.Get("Action"), Equals, "DeleteAccessKey")
+ c.Assert(values.Get("AccessKeyId"), Equals, "ysa8hasdhasdsi")
+ _, ok := map[string][]string(values)["UserName"]
+ c.Assert(ok, Equals, false)
+}
+
+func (s *S) TestAccessKeys(c *C) {
+ testServer.Response(200, nil, ListAccessKeyExample)
+ resp, err := s.iam.AccessKeys("Bob")
+ values := testServer.WaitRequest().URL.Query()
+ c.Assert(values.Get("Action"), Equals, "ListAccessKeys")
+ c.Assert(values.Get("UserName"), Equals, "Bob")
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE")
+ c.Assert(resp.AccessKeys, HasLen, 2)
+ c.Assert(resp.AccessKeys[0].Id, Equals, "AKIAIOSFODNN7EXAMPLE")
+ c.Assert(resp.AccessKeys[0].UserName, Equals, "Bob")
+ c.Assert(resp.AccessKeys[0].Status, Equals, "Active")
+ c.Assert(resp.AccessKeys[1].Id, Equals, "AKIAI44QH8DHBEXAMPLE")
+ c.Assert(resp.AccessKeys[1].UserName, Equals, "Bob")
+ c.Assert(resp.AccessKeys[1].Status, Equals, "Inactive")
+}
+
+func (s *S) TestAccessKeysBlankUserName(c *C) {
+ testServer.Response(200, nil, ListAccessKeyExample)
+ _, err := s.iam.AccessKeys("")
+ c.Assert(err, IsNil)
+ values := testServer.WaitRequest().URL.Query()
+ c.Assert(values.Get("Action"), Equals, "ListAccessKeys")
+ _, ok := map[string][]string(values)["UserName"]
+ c.Assert(ok, Equals, false)
+}
+
+func (s *S) TestGetUserPolicy(c *C) {
+ testServer.Response(200, nil, GetUserPolicyExample)
+ resp, err := s.iam.GetUserPolicy("Bob", "AllAccessPolicy")
+ values := testServer.WaitRequest().URL.Query()
+ c.Assert(values.Get("Action"), Equals, "GetUserPolicy")
+ c.Assert(values.Get("UserName"), Equals, "Bob")
+ c.Assert(values.Get("PolicyName"), Equals, "AllAccessPolicy")
+ c.Assert(err, IsNil)
+ c.Assert(resp.Policy.UserName, Equals, "Bob")
+ c.Assert(resp.Policy.Name, Equals, "AllAccessPolicy")
+ c.Assert(strings.TrimSpace(resp.Policy.Document), Equals, `{"Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}`)
+ c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE")
+}
+
+func (s *S) TestPutUserPolicy(c *C) {
+ document := `{
+ "Statement": [
+ {
+ "Action": [
+ "s3:*"
+ ],
+ "Effect": "Allow",
+ "Resource": [
+ "arn:aws:s3:::8shsns19s90ajahadsj/*",
+ "arn:aws:s3:::8shsns19s90ajahadsj"
+ ]
+ }]
+ }`
+ testServer.Response(200, nil, RequestIdExample)
+ resp, err := s.iam.PutUserPolicy("Bob", "AllAccessPolicy", document)
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "POST")
+ c.Assert(req.FormValue("Action"), Equals, "PutUserPolicy")
+ c.Assert(req.FormValue("PolicyName"), Equals, "AllAccessPolicy")
+ c.Assert(req.FormValue("UserName"), Equals, "Bob")
+ c.Assert(req.FormValue("PolicyDocument"), Equals, document)
+ c.Assert(req.FormValue("Version"), Equals, "2010-05-08")
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE")
+}
+
+func (s *S) TestDeleteUserPolicy(c *C) {
+ testServer.Response(200, nil, RequestIdExample)
+ resp, err := s.iam.DeleteUserPolicy("Bob", "AllAccessPolicy")
+ values := testServer.WaitRequest().URL.Query()
+ c.Assert(values.Get("Action"), Equals, "DeleteUserPolicy")
+ c.Assert(values.Get("PolicyName"), Equals, "AllAccessPolicy")
+ c.Assert(values.Get("UserName"), Equals, "Bob")
+ c.Assert(err, IsNil)
+ c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE")
+}
diff --git a/vendor/src/launchpad.net/goamz/iam/iami_test.go b/vendor/src/launchpad.net/goamz/iam/iami_test.go
new file mode 100644
index 0000000000..37d07c8343
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/iam/iami_test.go
@@ -0,0 +1,208 @@
+package iam_test
+
+import (
+ "launchpad.net/goamz/aws"
+ "launchpad.net/goamz/iam"
+ "launchpad.net/goamz/testutil"
+ . "launchpad.net/gocheck"
+ "net/url"
+)
+
+// AmazonServer represents an Amazon AWS server.
+type AmazonServer struct {
+ auth aws.Auth
+}
+
+func (s *AmazonServer) SetUp(c *C) {
+ auth, err := aws.EnvAuth()
+ if err != nil {
+ c.Fatal(err)
+ }
+ s.auth = auth
+}
+
+var _ = Suite(&AmazonClientSuite{})
+
+// AmazonClientSuite tests the client against a live AWS 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.iam = iam.New(s.srv.auth, aws.USEast)
+}
+
+// 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 {
+ iam *iam.IAM
+}
+
+func (s *ClientTests) TestCreateAndDeleteUser(c *C) {
+ createResp, err := s.iam.CreateUser("gopher", "/gopher/")
+ c.Assert(err, IsNil)
+ getResp, err := s.iam.GetUser("gopher")
+ c.Assert(err, IsNil)
+ c.Assert(createResp.User, DeepEquals, getResp.User)
+ _, err = s.iam.DeleteUser("gopher")
+ c.Assert(err, IsNil)
+}
+
+func (s *ClientTests) TestCreateUserError(c *C) {
+ _, err := s.iam.CreateUser("gopher", "/gopher/")
+ c.Assert(err, IsNil)
+ defer s.iam.DeleteUser("gopher")
+ _, err = s.iam.CreateUser("gopher", "/")
+ iamErr, ok := err.(*iam.Error)
+ c.Assert(ok, Equals, true)
+ c.Assert(iamErr.StatusCode, Equals, 409)
+ c.Assert(iamErr.Code, Equals, "EntityAlreadyExists")
+ c.Assert(iamErr.Message, Equals, "User with name gopher already exists.")
+}
+
+func (s *ClientTests) TestDeleteUserError(c *C) {
+ _, err := s.iam.DeleteUser("gopher")
+ iamErr, ok := err.(*iam.Error)
+ c.Assert(ok, Equals, true)
+ c.Assert(iamErr.StatusCode, Equals, 404)
+ c.Assert(iamErr.Code, Equals, "NoSuchEntity")
+ c.Assert(iamErr.Message, Equals, "The user with name gopher cannot be found.")
+}
+
+func (s *ClientTests) TestGetUserError(c *C) {
+ _, err := s.iam.GetUser("gopher")
+ iamErr, ok := err.(*iam.Error)
+ c.Assert(ok, Equals, true)
+ c.Assert(iamErr.StatusCode, Equals, 404)
+ c.Assert(iamErr.Code, Equals, "NoSuchEntity")
+ c.Assert(iamErr.Message, Equals, "The user with name gopher cannot be found.")
+}
+
+func (s *ClientTests) TestCreateListAndDeleteAccessKey(c *C) {
+ createUserResp, err := s.iam.CreateUser("gopher", "/gopher/")
+ c.Assert(err, IsNil)
+ defer s.iam.DeleteUser(createUserResp.User.Name)
+ createKeyResp, err := s.iam.CreateAccessKey(createUserResp.User.Name)
+ c.Assert(err, IsNil)
+ listKeyResp, err := s.iam.AccessKeys(createUserResp.User.Name)
+ c.Assert(err, IsNil)
+ c.Assert(listKeyResp.AccessKeys, HasLen, 1)
+ createKeyResp.AccessKey.Secret = ""
+ c.Assert(listKeyResp.AccessKeys[0], DeepEquals, createKeyResp.AccessKey)
+ _, err = s.iam.DeleteAccessKey(createKeyResp.AccessKey.Id, createUserResp.User.Name)
+ c.Assert(err, IsNil)
+}
+
+func (s *ClientTests) TestCreateAccessKeyError(c *C) {
+ _, err := s.iam.CreateAccessKey("unknowngopher")
+ c.Assert(err, NotNil)
+ iamErr, ok := err.(*iam.Error)
+ c.Assert(ok, Equals, true)
+ c.Assert(iamErr.StatusCode, Equals, 404)
+ c.Assert(iamErr.Code, Equals, "NoSuchEntity")
+ c.Assert(iamErr.Message, Equals, "The user with name unknowngopher cannot be found.")
+}
+
+func (s *ClientTests) TestListAccessKeysUserNotFound(c *C) {
+ _, err := s.iam.AccessKeys("unknowngopher")
+ c.Assert(err, NotNil)
+ iamErr, ok := err.(*iam.Error)
+ c.Assert(ok, Equals, true)
+ c.Assert(iamErr.StatusCode, Equals, 404)
+ c.Assert(iamErr.Code, Equals, "NoSuchEntity")
+ c.Assert(iamErr.Message, Equals, "The user with name unknowngopher cannot be found.")
+}
+
+func (s *ClientTests) TestListAccessKeysUserWithoutKeys(c *C) {
+ createUserResp, err := s.iam.CreateUser("gopher", "/")
+ c.Assert(err, IsNil)
+ defer s.iam.DeleteUser(createUserResp.User.Name)
+ resp, err := s.iam.AccessKeys(createUserResp.User.Name)
+ c.Assert(err, IsNil)
+ c.Assert(resp.AccessKeys, HasLen, 0)
+}
+
+func (s *ClientTests) TestCreateListAndDeleteGroup(c *C) {
+ cResp1, err := s.iam.CreateGroup("Finances", "/finances/")
+ c.Assert(err, IsNil)
+ cResp2, err := s.iam.CreateGroup("DevelopmentManagers", "/development/managers/")
+ c.Assert(err, IsNil)
+ lResp, err := s.iam.Groups("/development/")
+ c.Assert(err, IsNil)
+ c.Assert(lResp.Groups, HasLen, 1)
+ c.Assert(cResp2.Group, DeepEquals, lResp.Groups[0])
+ lResp, err = s.iam.Groups("")
+ c.Assert(err, IsNil)
+ c.Assert(lResp.Groups, HasLen, 2)
+ if lResp.Groups[0].Name == cResp1.Group.Name {
+ c.Assert([]iam.Group{cResp1.Group, cResp2.Group}, DeepEquals, lResp.Groups)
+ } else {
+ c.Assert([]iam.Group{cResp2.Group, cResp1.Group}, DeepEquals, lResp.Groups)
+ }
+ _, err = s.iam.DeleteGroup("DevelopmentManagers")
+ c.Assert(err, IsNil)
+ lResp, err = s.iam.Groups("/development/")
+ c.Assert(err, IsNil)
+ c.Assert(lResp.Groups, HasLen, 0)
+ _, err = s.iam.DeleteGroup("Finances")
+ c.Assert(err, IsNil)
+}
+
+func (s *ClientTests) TestCreateGroupError(c *C) {
+ _, err := s.iam.CreateGroup("Finances", "/finances/")
+ c.Assert(err, IsNil)
+ defer s.iam.DeleteGroup("Finances")
+ _, err = s.iam.CreateGroup("Finances", "/something-else/")
+ iamErr, ok := err.(*iam.Error)
+ c.Assert(ok, Equals, true)
+ c.Assert(iamErr.StatusCode, Equals, 409)
+ c.Assert(iamErr.Code, Equals, "EntityAlreadyExists")
+ c.Assert(iamErr.Message, Equals, "Group with name Finances already exists.")
+}
+
+func (s *ClientTests) TestDeleteGroupError(c *C) {
+ _, err := s.iam.DeleteGroup("Finances")
+ iamErr, ok := err.(*iam.Error)
+ c.Assert(ok, Equals, true)
+ c.Assert(iamErr.StatusCode, Equals, 404)
+ c.Assert(iamErr.Code, Equals, "NoSuchEntity")
+ c.Assert(iamErr.Message, Equals, "The group with name Finances cannot be found.")
+}
+
+func (s *ClientTests) TestPutGetAndDeleteUserPolicy(c *C) {
+ userResp, err := s.iam.CreateUser("gopher", "/gopher/")
+ c.Assert(err, IsNil)
+ defer s.iam.DeleteUser(userResp.User.Name)
+ document := `{
+ "Statement": [
+ {
+ "Action": [
+ "s3:*"
+ ],
+ "Effect": "Allow",
+ "Resource": [
+ "arn:aws:s3:::8shsns19s90ajahadsj/*",
+ "arn:aws:s3:::8shsns19s90ajahadsj"
+ ]
+ }]
+ }`
+ _, err = s.iam.PutUserPolicy(userResp.User.Name, "EverythingS3", document)
+ c.Assert(err, IsNil)
+ resp, err := s.iam.GetUserPolicy(userResp.User.Name, "EverythingS3")
+ c.Assert(err, IsNil)
+ c.Assert(resp.Policy.Name, Equals, "EverythingS3")
+ c.Assert(resp.Policy.UserName, Equals, userResp.User.Name)
+ gotDocument, err := url.QueryUnescape(resp.Policy.Document)
+ c.Assert(err, IsNil)
+ c.Assert(gotDocument, Equals, document)
+ _, err = s.iam.DeleteUserPolicy(userResp.User.Name, "EverythingS3")
+ c.Assert(err, IsNil)
+ _, err = s.iam.GetUserPolicy(userResp.User.Name, "EverythingS3")
+ c.Assert(err, NotNil)
+}
diff --git a/vendor/src/launchpad.net/goamz/iam/iamt_test.go b/vendor/src/launchpad.net/goamz/iam/iamt_test.go
new file mode 100644
index 0000000000..21fe984350
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/iam/iamt_test.go
@@ -0,0 +1,39 @@
+package iam_test
+
+import (
+ "launchpad.net/goamz/aws"
+ "launchpad.net/goamz/iam"
+ "launchpad.net/goamz/iam/iamtest"
+ . "launchpad.net/gocheck"
+)
+
+// LocalServer represents a local ec2test fake server.
+type LocalServer struct {
+ auth aws.Auth
+ region aws.Region
+ srv *iamtest.Server
+}
+
+func (s *LocalServer) SetUp(c *C) {
+ srv, err := iamtest.NewServer()
+ c.Assert(err, IsNil)
+ c.Assert(srv, NotNil)
+
+ s.srv = srv
+ s.region = aws.Region{IAMEndpoint: srv.URL()}
+}
+
+// LocalServerSuite defines tests that will run
+// against the local iamtest server. It includes
+// tests from ClientTests.
+type LocalServerSuite struct {
+ srv LocalServer
+ ClientTests
+}
+
+var _ = Suite(&LocalServerSuite{})
+
+func (s *LocalServerSuite) SetUpSuite(c *C) {
+ s.srv.SetUp(c)
+ s.ClientTests.iam = iam.New(s.srv.auth, s.srv.region)
+}
diff --git a/vendor/src/launchpad.net/goamz/iam/iamtest/server.go b/vendor/src/launchpad.net/goamz/iam/iamtest/server.go
new file mode 100644
index 0000000000..d79ddef0a9
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/iam/iamtest/server.go
@@ -0,0 +1,432 @@
+// Package iamtest implements a fake IAM provider with the capability of
+// inducing errors on any given operation, and retrospectively determining what
+// operations have been carried out.
+package iamtest
+
+import (
+ "encoding/json"
+ "encoding/xml"
+ "fmt"
+ "launchpad.net/goamz/iam"
+ "net"
+ "net/http"
+ "strings"
+ "sync"
+)
+
+type action struct {
+ srv *Server
+ w http.ResponseWriter
+ req *http.Request
+ reqId string
+}
+
+// Server implements an IAM simulator for use in tests.
+type Server struct {
+ reqId int
+ url string
+ listener net.Listener
+ users []iam.User
+ groups []iam.Group
+ accessKeys []iam.AccessKey
+ userPolicies []iam.UserPolicy
+ mutex sync.Mutex
+}
+
+func NewServer() (*Server, error) {
+ l, err := net.Listen("tcp", "localhost:0")
+ if err != nil {
+ return nil, fmt.Errorf("cannot listen on localhost: %v", err)
+ }
+ srv := &Server{
+ listener: l,
+ url: "http://" + l.Addr().String(),
+ }
+ 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() error {
+ return srv.listener.Close()
+}
+
+// URL returns a URL for the server.
+func (srv *Server) URL() string {
+ return srv.url
+}
+
+type xmlErrors struct {
+ XMLName string `xml:"ErrorResponse"`
+ Error iam.Error
+}
+
+func (srv *Server) error(w http.ResponseWriter, err *iam.Error) {
+ w.WriteHeader(err.StatusCode)
+ xmlErr := xmlErrors{Error: *err}
+ if e := xml.NewEncoder(w).Encode(xmlErr); e != nil {
+ panic(e)
+ }
+}
+
+func (srv *Server) serveHTTP(w http.ResponseWriter, req *http.Request) {
+ req.ParseForm()
+ srv.mutex.Lock()
+ defer srv.mutex.Unlock()
+ action := req.FormValue("Action")
+ if action == "" {
+ srv.error(w, &iam.Error{
+ StatusCode: 400,
+ Code: "MissingAction",
+ Message: "Missing action",
+ })
+ }
+ if a, ok := actions[action]; ok {
+ reqId := fmt.Sprintf("req%0X", srv.reqId)
+ srv.reqId++
+ if resp, err := a(srv, w, req, reqId); err == nil {
+ if err := xml.NewEncoder(w).Encode(resp); err != nil {
+ panic(err)
+ }
+ } else {
+ switch err.(type) {
+ case *iam.Error:
+ srv.error(w, err.(*iam.Error))
+ default:
+ panic(err)
+ }
+ }
+ } else {
+ srv.error(w, &iam.Error{
+ StatusCode: 400,
+ Code: "InvalidAction",
+ Message: "Invalid action: " + action,
+ })
+ }
+}
+
+func (srv *Server) createUser(w http.ResponseWriter, req *http.Request, reqId string) (interface{}, error) {
+ if err := srv.validate(req, []string{"UserName"}); err != nil {
+ return nil, err
+ }
+ path := req.FormValue("Path")
+ if path == "" {
+ path = "/"
+ }
+ name := req.FormValue("UserName")
+ for _, user := range srv.users {
+ if user.Name == name {
+ return nil, &iam.Error{
+ StatusCode: 409,
+ Code: "EntityAlreadyExists",
+ Message: fmt.Sprintf("User with name %s already exists.", name),
+ }
+ }
+ }
+ user := iam.User{
+ Id: "USER" + reqId + "EXAMPLE",
+ Arn: fmt.Sprintf("arn:aws:iam:::123456789012:user%s%s", path, name),
+ Name: name,
+ Path: path,
+ }
+ srv.users = append(srv.users, user)
+ return iam.CreateUserResp{
+ RequestId: reqId,
+ User: user,
+ }, nil
+}
+
+func (srv *Server) getUser(w http.ResponseWriter, req *http.Request, reqId string) (interface{}, error) {
+ if err := srv.validate(req, []string{"UserName"}); err != nil {
+ return nil, err
+ }
+ name := req.FormValue("UserName")
+ index, err := srv.findUser(name)
+ if err != nil {
+ return nil, err
+ }
+ return iam.GetUserResp{RequestId: reqId, User: srv.users[index]}, nil
+}
+
+func (srv *Server) deleteUser(w http.ResponseWriter, req *http.Request, reqId string) (interface{}, error) {
+ if err := srv.validate(req, []string{"UserName"}); err != nil {
+ return nil, err
+ }
+ name := req.FormValue("UserName")
+ index, err := srv.findUser(name)
+ if err != nil {
+ return nil, err
+ }
+ copy(srv.users[index:], srv.users[index+1:])
+ srv.users = srv.users[:len(srv.users)-1]
+ return iam.SimpleResp{RequestId: reqId}, nil
+}
+
+func (srv *Server) createAccessKey(w http.ResponseWriter, req *http.Request, reqId string) (interface{}, error) {
+ if err := srv.validate(req, []string{"UserName"}); err != nil {
+ return nil, err
+ }
+ userName := req.FormValue("UserName")
+ if _, err := srv.findUser(userName); err != nil {
+ return nil, err
+ }
+ key := iam.AccessKey{
+ Id: fmt.Sprintf("%s%d", userName, len(srv.accessKeys)),
+ Secret: "",
+ UserName: userName,
+ Status: "Active",
+ }
+ srv.accessKeys = append(srv.accessKeys, key)
+ return iam.CreateAccessKeyResp{RequestId: reqId, AccessKey: key}, nil
+}
+
+func (srv *Server) deleteAccessKey(w http.ResponseWriter, req *http.Request, reqId string) (interface{}, error) {
+ if err := srv.validate(req, []string{"AccessKeyId", "UserName"}); err != nil {
+ return nil, err
+ }
+ key := req.FormValue("AccessKeyId")
+ index := -1
+ for i, ak := range srv.accessKeys {
+ if ak.Id == key {
+ index = i
+ break
+ }
+ }
+ if index < 0 {
+ return nil, &iam.Error{
+ StatusCode: 404,
+ Code: "NoSuchEntity",
+ Message: "No such key.",
+ }
+ }
+ copy(srv.accessKeys[index:], srv.accessKeys[index+1:])
+ srv.accessKeys = srv.accessKeys[:len(srv.accessKeys)-1]
+ return iam.SimpleResp{RequestId: reqId}, nil
+}
+
+func (srv *Server) listAccessKeys(w http.ResponseWriter, req *http.Request, reqId string) (interface{}, error) {
+ if err := srv.validate(req, []string{"UserName"}); err != nil {
+ return nil, err
+ }
+ userName := req.FormValue("UserName")
+ if _, err := srv.findUser(userName); err != nil {
+ return nil, err
+ }
+ var keys []iam.AccessKey
+ for _, k := range srv.accessKeys {
+ if k.UserName == userName {
+ keys = append(keys, k)
+ }
+ }
+ return iam.AccessKeysResp{
+ RequestId: reqId,
+ AccessKeys: keys,
+ }, nil
+}
+
+func (srv *Server) createGroup(w http.ResponseWriter, req *http.Request, reqId string) (interface{}, error) {
+ if err := srv.validate(req, []string{"GroupName"}); err != nil {
+ return nil, err
+ }
+ name := req.FormValue("GroupName")
+ path := req.FormValue("Path")
+ for _, group := range srv.groups {
+ if group.Name == name {
+ return nil, &iam.Error{
+ StatusCode: 409,
+ Code: "EntityAlreadyExists",
+ Message: fmt.Sprintf("Group with name %s already exists.", name),
+ }
+ }
+ }
+ group := iam.Group{
+ Id: "GROUP " + reqId + "EXAMPLE",
+ Arn: fmt.Sprintf("arn:aws:iam:::123456789012:group%s%s", path, name),
+ Name: name,
+ Path: path,
+ }
+ srv.groups = append(srv.groups, group)
+ return iam.CreateGroupResp{
+ RequestId: reqId,
+ Group: group,
+ }, nil
+}
+
+func (srv *Server) listGroups(w http.ResponseWriter, req *http.Request, reqId string) (interface{}, error) {
+ pathPrefix := req.FormValue("PathPrefix")
+ if pathPrefix == "" {
+ return iam.GroupsResp{
+ RequestId: reqId,
+ Groups: srv.groups,
+ }, nil
+ }
+ var groups []iam.Group
+ for _, group := range srv.groups {
+ if strings.HasPrefix(group.Path, pathPrefix) {
+ groups = append(groups, group)
+ }
+ }
+ return iam.GroupsResp{
+ RequestId: reqId,
+ Groups: groups,
+ }, nil
+}
+
+func (srv *Server) deleteGroup(w http.ResponseWriter, req *http.Request, reqId string) (interface{}, error) {
+ if err := srv.validate(req, []string{"GroupName"}); err != nil {
+ return nil, err
+ }
+ name := req.FormValue("GroupName")
+ index := -1
+ for i, group := range srv.groups {
+ if group.Name == name {
+ index = i
+ break
+ }
+ }
+ if index == -1 {
+ return nil, &iam.Error{
+ StatusCode: 404,
+ Code: "NoSuchEntity",
+ Message: fmt.Sprintf("The group with name %s cannot be found.", name),
+ }
+ }
+ copy(srv.groups[index:], srv.groups[index+1:])
+ srv.groups = srv.groups[:len(srv.groups)-1]
+ return iam.SimpleResp{RequestId: reqId}, nil
+}
+
+func (srv *Server) putUserPolicy(w http.ResponseWriter, req *http.Request, reqId string) (interface{}, error) {
+ if err := srv.validate(req, []string{"UserName", "PolicyDocument", "PolicyName"}); err != nil {
+ return nil, err
+ }
+ var exists bool
+ policyName := req.FormValue("PolicyName")
+ userName := req.FormValue("UserName")
+ for _, policy := range srv.userPolicies {
+ if policyName == policy.Name && userName == policy.UserName {
+ exists = true
+ break
+ }
+ }
+ if !exists {
+ policy := iam.UserPolicy{
+ Name: policyName,
+ UserName: userName,
+ Document: req.FormValue("PolicyDocument"),
+ }
+ var dumb interface{}
+ if err := json.Unmarshal([]byte(policy.Document), &dumb); err != nil {
+ return nil, &iam.Error{
+ StatusCode: 400,
+ Code: "MalformedPolicyDocument",
+ Message: "Malformed policy document",
+ }
+ }
+ srv.userPolicies = append(srv.userPolicies, policy)
+ }
+ return iam.SimpleResp{RequestId: reqId}, nil
+}
+
+func (srv *Server) deleteUserPolicy(w http.ResponseWriter, req *http.Request, reqId string) (interface{}, error) {
+ if err := srv.validate(req, []string{"UserName", "PolicyName"}); err != nil {
+ return nil, err
+ }
+ policyName := req.FormValue("PolicyName")
+ userName := req.FormValue("UserName")
+ index := -1
+ for i, policy := range srv.userPolicies {
+ if policyName == policy.Name && userName == policy.UserName {
+ index = i
+ break
+ }
+ }
+ if index < 0 {
+ return nil, &iam.Error{
+ StatusCode: 404,
+ Code: "NoSuchEntity",
+ Message: "No such user policy",
+ }
+ }
+ copy(srv.userPolicies[index:], srv.userPolicies[index+1:])
+ srv.userPolicies = srv.userPolicies[:len(srv.userPolicies)-1]
+ return iam.SimpleResp{RequestId: reqId}, nil
+}
+
+func (srv *Server) getUserPolicy(w http.ResponseWriter, req *http.Request, reqId string) (interface{}, error) {
+ if err := srv.validate(req, []string{"UserName", "PolicyName"}); err != nil {
+ return nil, err
+ }
+ policyName := req.FormValue("PolicyName")
+ userName := req.FormValue("UserName")
+ index := -1
+ for i, policy := range srv.userPolicies {
+ if policyName == policy.Name && userName == policy.UserName {
+ index = i
+ break
+ }
+ }
+ if index < 0 {
+ return nil, &iam.Error{
+ StatusCode: 404,
+ Code: "NoSuchEntity",
+ Message: "No such user policy",
+ }
+ }
+ return iam.GetUserPolicyResp{
+ Policy: srv.userPolicies[index],
+ RequestId: reqId,
+ }, nil
+}
+
+func (srv *Server) findUser(userName string) (int, error) {
+ var (
+ err error
+ index = -1
+ )
+ for i, user := range srv.users {
+ if user.Name == userName {
+ index = i
+ break
+ }
+ }
+ if index < 0 {
+ err = &iam.Error{
+ StatusCode: 404,
+ Code: "NoSuchEntity",
+ Message: fmt.Sprintf("The user with name %s cannot be found.", userName),
+ }
+ }
+ return index, err
+}
+
+// Validates the presence of required request parameters.
+func (srv *Server) validate(req *http.Request, required []string) error {
+ for _, r := range required {
+ if req.FormValue(r) == "" {
+ return &iam.Error{
+ StatusCode: 400,
+ Code: "InvalidParameterCombination",
+ Message: fmt.Sprintf("%s is required.", r),
+ }
+ }
+ }
+ return nil
+}
+
+var actions = map[string]func(*Server, http.ResponseWriter, *http.Request, string) (interface{}, error){
+ "CreateUser": (*Server).createUser,
+ "DeleteUser": (*Server).deleteUser,
+ "GetUser": (*Server).getUser,
+ "CreateAccessKey": (*Server).createAccessKey,
+ "DeleteAccessKey": (*Server).deleteAccessKey,
+ "ListAccessKeys": (*Server).listAccessKeys,
+ "PutUserPolicy": (*Server).putUserPolicy,
+ "DeleteUserPolicy": (*Server).deleteUserPolicy,
+ "GetUserPolicy": (*Server).getUserPolicy,
+ "CreateGroup": (*Server).createGroup,
+ "DeleteGroup": (*Server).deleteGroup,
+ "ListGroups": (*Server).listGroups,
+}
diff --git a/vendor/src/launchpad.net/goamz/iam/responses_test.go b/vendor/src/launchpad.net/goamz/iam/responses_test.go
new file mode 100644
index 0000000000..cd617d8fb0
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/iam/responses_test.go
@@ -0,0 +1,155 @@
+package iam_test
+
+// http://goo.gl/EUIvl
+var CreateUserExample = `
+
+
+
+ /division_abc/subdivision_xyz/
+ Bob
+ AIDACKCEVSQ6C2EXAMPLE
+ arn:aws:iam::123456789012:user/division_abc/subdivision_xyz/Bob
+
+
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+
+`
+
+var DuplicateUserExample = `
+
+
+ Sender
+ EntityAlreadyExists
+ User with name Bob already exists.
+
+ 1d5f5000-1316-11e2-a60f-91a8e6fb6d21
+
+`
+
+var GetUserExample = `
+
+
+
+ /division_abc/subdivision_xyz/
+ Bob
+ AIDACKCEVSQ6C2EXAMPLE
+ arn:aws:iam::123456789012:user/division_abc/subdivision_xyz/Bob
+
+
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+
+`
+
+var CreateGroupExample = `
+
+
+
+ /admins/
+ Admins
+ AGPACKCEVSQ6C2EXAMPLE
+ arn:aws:iam::123456789012:group/Admins
+
+
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+
+`
+
+var ListGroupsExample = `
+
+
+
+
+ /division_abc/subdivision_xyz/
+ Admins
+ AGPACKCEVSQ6C2EXAMPLE
+ arn:aws:iam::123456789012:group/Admins
+
+
+ /division_abc/subdivision_xyz/product_1234/engineering/
+ Test
+ AGP2MAB8DPLSRHEXAMPLE
+ arn:aws:iam::123456789012:group/division_abc/subdivision_xyz/product_1234/engineering/Test
+
+
+ /division_abc/subdivision_xyz/product_1234/
+ Managers
+ AGPIODR4TAW7CSEXAMPLE
+ arn:aws:iam::123456789012:group/division_abc/subdivision_xyz/product_1234/Managers
+
+
+ false
+
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+
+`
+
+var RequestIdExample = `
+
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+
+`
+
+var CreateAccessKeyExample = `
+
+
+
+ Bob
+ AKIAIOSFODNN7EXAMPLE
+ Active
+ wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY
+
+
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+
+`
+
+var ListAccessKeyExample = `
+
+
+ Bob
+
+
+ Bob
+ AKIAIOSFODNN7EXAMPLE
+ Active
+
+
+ Bob
+ AKIAI44QH8DHBEXAMPLE
+ Inactive
+
+
+ false
+
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+
+`
+
+var GetUserPolicyExample = `
+
+
+ Bob
+ AllAccessPolicy
+
+ {"Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}
+
+
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+
+`
diff --git a/vendor/src/launchpad.net/goamz/iam/sign.go b/vendor/src/launchpad.net/goamz/iam/sign.go
new file mode 100644
index 0000000000..51e9efc973
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/iam/sign.go
@@ -0,0 +1,35 @@
+package iam
+
+import (
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/base64"
+ "launchpad.net/goamz/aws"
+ "sort"
+ "strings"
+)
+
+// ----------------------------------------------------------------------------
+// Version 2 signing (http://goo.gl/RSRp5)
+
+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"
+
+ var sarray []string
+ for k, v := range params {
+ sarray = append(sarray, aws.Encode(k)+"="+aws.Encode(v))
+ }
+ sort.StringSlice(sarray).Sort()
+ 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)
+}
diff --git a/vendor/src/launchpad.net/goamz/s3/export_test.go b/vendor/src/launchpad.net/goamz/s3/export_test.go
new file mode 100644
index 0000000000..8707cdac2c
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/s3/export_test.go
@@ -0,0 +1,31 @@
+package s3
+
+import (
+ "launchpad.net/goamz/aws"
+)
+
+var originalStrategy = attempts
+
+func SetAttemptStrategy(s *aws.AttemptStrategy) {
+ if s == nil {
+ attempts = originalStrategy
+ } else {
+ attempts = *s
+ }
+}
+
+func AttemptStrategy() aws.AttemptStrategy {
+ return attempts
+}
+
+func Sign(auth aws.Auth, method, path string, params, headers map[string][]string) {
+ sign(auth, method, path, params, headers)
+}
+
+func SetListPartsMax(n int) {
+ listPartsMax = n
+}
+
+func SetListMultiMax(n int) {
+ listMultiMax = n
+}
diff --git a/vendor/src/launchpad.net/goamz/s3/multi.go b/vendor/src/launchpad.net/goamz/s3/multi.go
new file mode 100644
index 0000000000..ba20047409
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/s3/multi.go
@@ -0,0 +1,410 @@
+package s3
+
+import (
+ "bytes"
+ "crypto/md5"
+ "encoding/base64"
+ "encoding/hex"
+ "encoding/xml"
+ "errors"
+ "io"
+ "sort"
+ "strconv"
+)
+
+// Multi represents an unfinished multipart upload.
+//
+// Multipart uploads allow sending big objects in smaller chunks.
+// After all parts have been sent, the upload must be explicitly
+// completed by calling Complete with the list of parts.
+//
+// See http://goo.gl/vJfTG for an overview of multipart uploads.
+type Multi struct {
+ Bucket *Bucket
+ Key string
+ UploadId string
+}
+
+// That's the default. Here just for testing.
+var listMultiMax = 1000
+
+type listMultiResp struct {
+ NextKeyMarker string
+ NextUploadIdMarker string
+ IsTruncated bool
+ Upload []Multi
+ CommonPrefixes []string `xml:"CommonPrefixes>Prefix"`
+}
+
+// ListMulti returns the list of unfinished multipart uploads in b.
+//
+// The prefix parameter limits the response to keys that begin with the
+// specified prefix. You can use prefixes to separate a bucket into different
+// groupings of keys (to get the feeling of folders, for example).
+//
+// The delim parameter causes the response to group all of the keys that
+// share a common prefix up to the next delimiter in a single entry within
+// the CommonPrefixes field. You can use delimiters to separate a bucket
+// into different groupings of keys, similar to how folders would work.
+//
+// See http://goo.gl/ePioY for details.
+func (b *Bucket) ListMulti(prefix, delim string) (multis []*Multi, prefixes []string, err error) {
+ params := map[string][]string{
+ "uploads": {""},
+ "max-uploads": {strconv.FormatInt(int64(listMultiMax), 10)},
+ "prefix": {prefix},
+ "delimiter": {delim},
+ }
+ for attempt := attempts.Start(); attempt.Next(); {
+ req := &request{
+ method: "GET",
+ bucket: b.Name,
+ params: params,
+ }
+ var resp listMultiResp
+ err := b.S3.query(req, &resp)
+ if shouldRetry(err) && attempt.HasNext() {
+ continue
+ }
+ if err != nil {
+ return nil, nil, err
+ }
+ for i := range resp.Upload {
+ multi := &resp.Upload[i]
+ multi.Bucket = b
+ multis = append(multis, multi)
+ }
+ prefixes = append(prefixes, resp.CommonPrefixes...)
+ if !resp.IsTruncated {
+ return multis, prefixes, nil
+ }
+ params["key-marker"] = []string{resp.NextKeyMarker}
+ params["upload-id-marker"] = []string{resp.NextUploadIdMarker}
+ attempt = attempts.Start() // Last request worked.
+ }
+ panic("unreachable")
+}
+
+// Multi returns a multipart upload handler for the provided key
+// inside b. If a multipart upload exists for key, it is returned,
+// otherwise a new multipart upload is initiated with contType and perm.
+func (b *Bucket) Multi(key, contType string, perm ACL) (*Multi, error) {
+ multis, _, err := b.ListMulti(key, "")
+ if err != nil && !hasCode(err, "NoSuchUpload") {
+ return nil, err
+ }
+ for _, m := range multis {
+ if m.Key == key {
+ return m, nil
+ }
+ }
+ return b.InitMulti(key, contType, perm)
+}
+
+// InitMulti initializes a new multipart upload at the provided
+// key inside b and returns a value for manipulating it.
+//
+// See http://goo.gl/XP8kL for details.
+func (b *Bucket) InitMulti(key string, contType string, perm ACL) (*Multi, error) {
+ headers := map[string][]string{
+ "Content-Type": {contType},
+ "Content-Length": {"0"},
+ "x-amz-acl": {string(perm)},
+ }
+ params := map[string][]string{
+ "uploads": {""},
+ }
+ req := &request{
+ method: "POST",
+ bucket: b.Name,
+ path: key,
+ headers: headers,
+ params: params,
+ }
+ var err error
+ var resp struct {
+ UploadId string `xml:"UploadId"`
+ }
+ for attempt := attempts.Start(); attempt.Next(); {
+ err = b.S3.query(req, &resp)
+ if !shouldRetry(err) {
+ break
+ }
+ }
+ if err != nil {
+ return nil, err
+ }
+ return &Multi{Bucket: b, Key: key, UploadId: resp.UploadId}, nil
+}
+
+// PutPart sends part n of the multipart upload, reading all the content from r.
+// Each part, except for the last one, must be at least 5MB in size.
+//
+// See http://goo.gl/pqZer for details.
+func (m *Multi) PutPart(n int, r io.ReadSeeker) (Part, error) {
+ partSize, _, md5b64, err := seekerInfo(r)
+ if err != nil {
+ return Part{}, err
+ }
+ return m.putPart(n, r, partSize, md5b64)
+}
+
+func (m *Multi) putPart(n int, r io.ReadSeeker, partSize int64, md5b64 string) (Part, error) {
+ headers := map[string][]string{
+ "Content-Length": {strconv.FormatInt(partSize, 10)},
+ "Content-MD5": {md5b64},
+ }
+ params := map[string][]string{
+ "uploadId": {m.UploadId},
+ "partNumber": {strconv.FormatInt(int64(n), 10)},
+ }
+ for attempt := attempts.Start(); attempt.Next(); {
+ _, err := r.Seek(0, 0)
+ if err != nil {
+ return Part{}, err
+ }
+ req := &request{
+ method: "PUT",
+ bucket: m.Bucket.Name,
+ path: m.Key,
+ headers: headers,
+ params: params,
+ payload: r,
+ }
+ err = m.Bucket.S3.prepare(req)
+ if err != nil {
+ return Part{}, err
+ }
+ hresp, err := m.Bucket.S3.run(req)
+ if shouldRetry(err) && attempt.HasNext() {
+ continue
+ }
+ if err != nil {
+ return Part{}, err
+ }
+ hresp.Body.Close()
+ etag := hresp.Header.Get("ETag")
+ if etag == "" {
+ return Part{}, errors.New("part upload succeeded with no ETag")
+ }
+ return Part{n, etag, partSize}, nil
+ }
+ panic("unreachable")
+}
+
+func seekerInfo(r io.ReadSeeker) (size int64, md5hex string, md5b64 string, err error) {
+ _, err = r.Seek(0, 0)
+ if err != nil {
+ return 0, "", "", err
+ }
+ digest := md5.New()
+ size, err = io.Copy(digest, r)
+ if err != nil {
+ return 0, "", "", err
+ }
+ sum := digest.Sum(nil)
+ md5hex = hex.EncodeToString(sum)
+ md5b64 = base64.StdEncoding.EncodeToString(sum)
+ return size, md5hex, md5b64, nil
+}
+
+type Part struct {
+ N int `xml:"PartNumber"`
+ ETag string
+ Size int64
+}
+
+type partSlice []Part
+
+func (s partSlice) Len() int { return len(s) }
+func (s partSlice) Less(i, j int) bool { return s[i].N < s[j].N }
+func (s partSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+
+type listPartsResp struct {
+ NextPartNumberMarker string
+ IsTruncated bool
+ Part []Part
+}
+
+// That's the default. Here just for testing.
+var listPartsMax = 1000
+
+// ListParts returns the list of previously uploaded parts in m,
+// ordered by part number.
+//
+// See http://goo.gl/ePioY for details.
+func (m *Multi) ListParts() ([]Part, error) {
+ params := map[string][]string{
+ "uploadId": {m.UploadId},
+ "max-parts": {strconv.FormatInt(int64(listPartsMax), 10)},
+ }
+ var parts partSlice
+ for attempt := attempts.Start(); attempt.Next(); {
+ req := &request{
+ method: "GET",
+ bucket: m.Bucket.Name,
+ path: m.Key,
+ params: params,
+ }
+ var resp listPartsResp
+ err := m.Bucket.S3.query(req, &resp)
+ if shouldRetry(err) && attempt.HasNext() {
+ continue
+ }
+ if err != nil {
+ return nil, err
+ }
+ parts = append(parts, resp.Part...)
+ if !resp.IsTruncated {
+ sort.Sort(parts)
+ return parts, nil
+ }
+ params["part-number-marker"] = []string{resp.NextPartNumberMarker}
+ attempt = attempts.Start() // Last request worked.
+ }
+ panic("unreachable")
+}
+
+type ReaderAtSeeker interface {
+ io.ReaderAt
+ io.ReadSeeker
+}
+
+// PutAll sends all of r via a multipart upload with parts no larger
+// than partSize bytes, which must be set to at least 5MB.
+// Parts previously uploaded are either reused if their checksum
+// and size match the new part, or otherwise overwritten with the
+// new content.
+// PutAll returns all the parts of m (reused or not).
+func (m *Multi) PutAll(r ReaderAtSeeker, partSize int64) ([]Part, error) {
+ old, err := m.ListParts()
+ if err != nil && !hasCode(err, "NoSuchUpload") {
+ return nil, err
+ }
+ reuse := 0 // Index of next old part to consider reusing.
+ current := 1 // Part number of latest good part handled.
+ totalSize, err := r.Seek(0, 2)
+ if err != nil {
+ return nil, err
+ }
+ first := true // Must send at least one empty part if the file is empty.
+ var result []Part
+NextSection:
+ for offset := int64(0); offset < totalSize || first; offset += partSize {
+ first = false
+ if offset+partSize > totalSize {
+ partSize = totalSize - offset
+ }
+ section := io.NewSectionReader(r, offset, partSize)
+ _, md5hex, md5b64, err := seekerInfo(section)
+ if err != nil {
+ return nil, err
+ }
+ for reuse < len(old) && old[reuse].N <= current {
+ // Looks like this part was already sent.
+ part := &old[reuse]
+ etag := `"` + md5hex + `"`
+ if part.N == current && part.Size == partSize && part.ETag == etag {
+ // Checksum matches. Reuse the old part.
+ result = append(result, *part)
+ current++
+ continue NextSection
+ }
+ reuse++
+ }
+
+ // Part wasn't found or doesn't match. Send it.
+ part, err := m.putPart(current, section, partSize, md5b64)
+ if err != nil {
+ return nil, err
+ }
+ result = append(result, part)
+ current++
+ }
+ return result, nil
+}
+
+type completeUpload struct {
+ XMLName xml.Name `xml:"CompleteMultipartUpload"`
+ Parts completeParts `xml:"Part"`
+}
+
+type completePart struct {
+ PartNumber int
+ ETag string
+}
+
+type completeParts []completePart
+
+func (p completeParts) Len() int { return len(p) }
+func (p completeParts) Less(i, j int) bool { return p[i].PartNumber < p[j].PartNumber }
+func (p completeParts) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
+
+// Complete assembles the given previously uploaded parts into the
+// final object. This operation may take several minutes.
+//
+// See http://goo.gl/2Z7Tw for details.
+func (m *Multi) Complete(parts []Part) error {
+ params := map[string][]string{
+ "uploadId": {m.UploadId},
+ }
+ c := completeUpload{}
+ for _, p := range parts {
+ c.Parts = append(c.Parts, completePart{p.N, p.ETag})
+ }
+ sort.Sort(c.Parts)
+ data, err := xml.Marshal(&c)
+ if err != nil {
+ return err
+ }
+ for attempt := attempts.Start(); attempt.Next(); {
+ req := &request{
+ method: "POST",
+ bucket: m.Bucket.Name,
+ path: m.Key,
+ params: params,
+ payload: bytes.NewReader(data),
+ }
+ err := m.Bucket.S3.query(req, nil)
+ if shouldRetry(err) && attempt.HasNext() {
+ continue
+ }
+ return err
+ }
+ panic("unreachable")
+}
+
+// Abort deletes an unifinished multipart upload and any previously
+// uploaded parts for it.
+//
+// After a multipart upload is aborted, no additional parts can be
+// uploaded using it. However, if any part uploads are currently in
+// progress, those part uploads might or might not succeed. As a result,
+// it might be necessary to abort a given multipart upload multiple
+// times in order to completely free all storage consumed by all parts.
+//
+// NOTE: If the described scenario happens to you, please report back to
+// the goamz authors with details. In the future such retrying should be
+// handled internally, but it's not clear what happens precisely (Is an
+// error returned? Is the issue completely undetectable?).
+//
+// See http://goo.gl/dnyJw for details.
+func (m *Multi) Abort() error {
+ params := map[string][]string{
+ "uploadId": {m.UploadId},
+ }
+ for attempt := attempts.Start(); attempt.Next(); {
+ req := &request{
+ method: "DELETE",
+ bucket: m.Bucket.Name,
+ path: m.Key,
+ params: params,
+ }
+ err := m.Bucket.S3.query(req, nil)
+ if shouldRetry(err) && attempt.HasNext() {
+ continue
+ }
+ return err
+ }
+ panic("unreachable")
+}
diff --git a/vendor/src/launchpad.net/goamz/s3/multi_test.go b/vendor/src/launchpad.net/goamz/s3/multi_test.go
new file mode 100644
index 0000000000..6699e3d77b
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/s3/multi_test.go
@@ -0,0 +1,370 @@
+package s3_test
+
+import (
+ "encoding/xml"
+ "io"
+ "io/ioutil"
+ "launchpad.net/goamz/s3"
+ . "launchpad.net/gocheck"
+ "strings"
+)
+
+func (s *S) TestInitMulti(c *C) {
+ testServer.Response(200, nil, InitMultiResultDump)
+
+ b := s.s3.Bucket("sample")
+
+ multi, err := b.InitMulti("multi", "text/plain", s3.Private)
+ c.Assert(err, IsNil)
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "POST")
+ c.Assert(req.URL.Path, Equals, "/sample/multi")
+ c.Assert(req.Header["Content-Type"], DeepEquals, []string{"text/plain"})
+ c.Assert(req.Header["X-Amz-Acl"], DeepEquals, []string{"private"})
+ c.Assert(req.Form["uploads"], DeepEquals, []string{""})
+
+ c.Assert(multi.UploadId, Matches, "JNbR_[A-Za-z0-9.]+QQ--")
+}
+
+func (s *S) TestMultiNoPreviousUpload(c *C) {
+ // Don't retry the NoSuchUpload error.
+ s3.RetryAttempts(false)
+
+ testServer.Response(404, nil, NoSuchUploadErrorDump)
+ testServer.Response(200, nil, InitMultiResultDump)
+
+ b := s.s3.Bucket("sample")
+
+ multi, err := b.Multi("multi", "text/plain", s3.Private)
+ c.Assert(err, IsNil)
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/sample/")
+ c.Assert(req.Form["uploads"], DeepEquals, []string{""})
+ c.Assert(req.Form["prefix"], DeepEquals, []string{"multi"})
+
+ req = testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "POST")
+ c.Assert(req.URL.Path, Equals, "/sample/multi")
+ c.Assert(req.Form["uploads"], DeepEquals, []string{""})
+
+ c.Assert(multi.UploadId, Matches, "JNbR_[A-Za-z0-9.]+QQ--")
+}
+
+func (s *S) TestMultiReturnOld(c *C) {
+ testServer.Response(200, nil, ListMultiResultDump)
+
+ b := s.s3.Bucket("sample")
+
+ multi, err := b.Multi("multi1", "text/plain", s3.Private)
+ c.Assert(err, IsNil)
+ c.Assert(multi.Key, Equals, "multi1")
+ c.Assert(multi.UploadId, Equals, "iUVug89pPvSswrikD")
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/sample/")
+ c.Assert(req.Form["uploads"], DeepEquals, []string{""})
+ c.Assert(req.Form["prefix"], DeepEquals, []string{"multi1"})
+}
+
+func (s *S) TestListParts(c *C) {
+ testServer.Response(200, nil, InitMultiResultDump)
+ testServer.Response(200, nil, ListPartsResultDump1)
+ testServer.Response(404, nil, NoSuchUploadErrorDump) // :-(
+ testServer.Response(200, nil, ListPartsResultDump2)
+
+ b := s.s3.Bucket("sample")
+
+ multi, err := b.InitMulti("multi", "text/plain", s3.Private)
+ c.Assert(err, IsNil)
+
+ parts, err := multi.ListParts()
+ c.Assert(err, IsNil)
+ c.Assert(parts, HasLen, 3)
+ c.Assert(parts[0].N, Equals, 1)
+ c.Assert(parts[0].Size, Equals, int64(5))
+ c.Assert(parts[0].ETag, Equals, `"ffc88b4ca90a355f8ddba6b2c3b2af5c"`)
+ c.Assert(parts[1].N, Equals, 2)
+ c.Assert(parts[1].Size, Equals, int64(5))
+ c.Assert(parts[1].ETag, Equals, `"d067a0fa9dc61a6e7195ca99696b5a89"`)
+ c.Assert(parts[2].N, Equals, 3)
+ c.Assert(parts[2].Size, Equals, int64(5))
+ c.Assert(parts[2].ETag, Equals, `"49dcd91231f801159e893fb5c6674985"`)
+ testServer.WaitRequest()
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/sample/multi")
+ c.Assert(req.Form.Get("uploadId"), Matches, "JNbR_[A-Za-z0-9.]+QQ--")
+ c.Assert(req.Form["max-parts"], DeepEquals, []string{"1000"})
+
+ testServer.WaitRequest() // The internal error.
+ req = testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/sample/multi")
+ c.Assert(req.Form.Get("uploadId"), Matches, "JNbR_[A-Za-z0-9.]+QQ--")
+ c.Assert(req.Form["max-parts"], DeepEquals, []string{"1000"})
+ c.Assert(req.Form["part-number-marker"], DeepEquals, []string{"2"})
+}
+
+func (s *S) TestPutPart(c *C) {
+ headers := map[string]string{
+ "ETag": `"26f90efd10d614f100252ff56d88dad8"`,
+ }
+ testServer.Response(200, nil, InitMultiResultDump)
+ testServer.Response(200, headers, "")
+
+ b := s.s3.Bucket("sample")
+
+ multi, err := b.InitMulti("multi", "text/plain", s3.Private)
+ c.Assert(err, IsNil)
+
+ part, err := multi.PutPart(1, strings.NewReader(""))
+ c.Assert(err, IsNil)
+ c.Assert(part.N, Equals, 1)
+ c.Assert(part.Size, Equals, int64(8))
+ c.Assert(part.ETag, Equals, headers["ETag"])
+
+ testServer.WaitRequest()
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "PUT")
+ c.Assert(req.URL.Path, Equals, "/sample/multi")
+ c.Assert(req.Form.Get("uploadId"), Matches, "JNbR_[A-Za-z0-9.]+QQ--")
+ c.Assert(req.Form["partNumber"], DeepEquals, []string{"1"})
+ c.Assert(req.Header["Content-Length"], DeepEquals, []string{"8"})
+ c.Assert(req.Header["Content-Md5"], DeepEquals, []string{"JvkO/RDWFPEAJS/1bYja2A=="})
+}
+
+func readAll(r io.Reader) string {
+ data, err := ioutil.ReadAll(r)
+ if err != nil {
+ panic(err)
+ }
+ return string(data)
+}
+
+func (s *S) TestPutAllNoPreviousUpload(c *C) {
+ // Don't retry the NoSuchUpload error.
+ s3.RetryAttempts(false)
+
+ etag1 := map[string]string{"ETag": `"etag1"`}
+ etag2 := map[string]string{"ETag": `"etag2"`}
+ etag3 := map[string]string{"ETag": `"etag3"`}
+ testServer.Response(200, nil, InitMultiResultDump)
+ testServer.Response(404, nil, NoSuchUploadErrorDump)
+ testServer.Response(200, etag1, "")
+ testServer.Response(200, etag2, "")
+ testServer.Response(200, etag3, "")
+
+ b := s.s3.Bucket("sample")
+
+ multi, err := b.InitMulti("multi", "text/plain", s3.Private)
+ c.Assert(err, IsNil)
+
+ parts, err := multi.PutAll(strings.NewReader("part1part2last"), 5)
+ c.Assert(parts, HasLen, 3)
+ c.Assert(parts[0].ETag, Equals, `"etag1"`)
+ c.Assert(parts[1].ETag, Equals, `"etag2"`)
+ c.Assert(parts[2].ETag, Equals, `"etag3"`)
+ c.Assert(err, IsNil)
+
+ // Init
+ testServer.WaitRequest()
+
+ // List old parts. Won't find anything.
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/sample/multi")
+
+ // Send part 1.
+ req = testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "PUT")
+ c.Assert(req.URL.Path, Equals, "/sample/multi")
+ c.Assert(req.Form["partNumber"], DeepEquals, []string{"1"})
+ c.Assert(req.Header["Content-Length"], DeepEquals, []string{"5"})
+ c.Assert(readAll(req.Body), Equals, "part1")
+
+ // Send part 2.
+ req = testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "PUT")
+ c.Assert(req.URL.Path, Equals, "/sample/multi")
+ c.Assert(req.Form["partNumber"], DeepEquals, []string{"2"})
+ c.Assert(req.Header["Content-Length"], DeepEquals, []string{"5"})
+ c.Assert(readAll(req.Body), Equals, "part2")
+
+ // Send part 3 with shorter body.
+ req = testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "PUT")
+ c.Assert(req.URL.Path, Equals, "/sample/multi")
+ c.Assert(req.Form["partNumber"], DeepEquals, []string{"3"})
+ c.Assert(req.Header["Content-Length"], DeepEquals, []string{"4"})
+ c.Assert(readAll(req.Body), Equals, "last")
+}
+
+func (s *S) TestPutAllZeroSizeFile(c *C) {
+ // Don't retry the NoSuchUpload error.
+ s3.RetryAttempts(false)
+
+ etag1 := map[string]string{"ETag": `"etag1"`}
+ testServer.Response(200, nil, InitMultiResultDump)
+ testServer.Response(404, nil, NoSuchUploadErrorDump)
+ testServer.Response(200, etag1, "")
+
+ b := s.s3.Bucket("sample")
+
+ multi, err := b.InitMulti("multi", "text/plain", s3.Private)
+ c.Assert(err, IsNil)
+
+ // Must send at least one part, so that completing it will work.
+ parts, err := multi.PutAll(strings.NewReader(""), 5)
+ c.Assert(parts, HasLen, 1)
+ c.Assert(parts[0].ETag, Equals, `"etag1"`)
+ c.Assert(err, IsNil)
+
+ // Init
+ testServer.WaitRequest()
+
+ // List old parts. Won't find anything.
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/sample/multi")
+
+ // Send empty part.
+ req = testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "PUT")
+ c.Assert(req.URL.Path, Equals, "/sample/multi")
+ c.Assert(req.Form["partNumber"], DeepEquals, []string{"1"})
+ c.Assert(req.Header["Content-Length"], DeepEquals, []string{"0"})
+ c.Assert(readAll(req.Body), Equals, "")
+}
+
+func (s *S) TestPutAllResume(c *C) {
+ etag2 := map[string]string{"ETag": `"etag2"`}
+ testServer.Response(200, nil, InitMultiResultDump)
+ testServer.Response(200, nil, ListPartsResultDump1)
+ testServer.Response(200, nil, ListPartsResultDump2)
+ testServer.Response(200, etag2, "")
+
+ b := s.s3.Bucket("sample")
+
+ multi, err := b.InitMulti("multi", "text/plain", s3.Private)
+ c.Assert(err, IsNil)
+
+ // "part1" and "part3" match the checksums in ResultDump1.
+ // The middle one is a mismatch (it refers to "part2").
+ parts, err := multi.PutAll(strings.NewReader("part1partXpart3"), 5)
+ c.Assert(parts, HasLen, 3)
+ c.Assert(parts[0].N, Equals, 1)
+ c.Assert(parts[0].Size, Equals, int64(5))
+ c.Assert(parts[0].ETag, Equals, `"ffc88b4ca90a355f8ddba6b2c3b2af5c"`)
+ c.Assert(parts[1].N, Equals, 2)
+ c.Assert(parts[1].Size, Equals, int64(5))
+ c.Assert(parts[1].ETag, Equals, `"etag2"`)
+ c.Assert(parts[2].N, Equals, 3)
+ c.Assert(parts[2].Size, Equals, int64(5))
+ c.Assert(parts[2].ETag, Equals, `"49dcd91231f801159e893fb5c6674985"`)
+ c.Assert(err, IsNil)
+
+ // Init
+ testServer.WaitRequest()
+
+ // List old parts, broken in two requests.
+ for i := 0; i < 2; i++ {
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/sample/multi")
+ }
+
+ // Send part 2, as it didn't match the checksum.
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "PUT")
+ c.Assert(req.URL.Path, Equals, "/sample/multi")
+ c.Assert(req.Form["partNumber"], DeepEquals, []string{"2"})
+ c.Assert(req.Header["Content-Length"], DeepEquals, []string{"5"})
+ c.Assert(readAll(req.Body), Equals, "partX")
+}
+
+func (s *S) TestMultiComplete(c *C) {
+ testServer.Response(200, nil, InitMultiResultDump)
+ // Note the 200 response. Completing will hold the connection on some
+ // kind of long poll, and may return a late error even after a 200.
+ testServer.Response(200, nil, InternalErrorDump)
+ testServer.Response(200, nil, "")
+
+ b := s.s3.Bucket("sample")
+
+ multi, err := b.InitMulti("multi", "text/plain", s3.Private)
+ c.Assert(err, IsNil)
+
+ err = multi.Complete([]s3.Part{{2, `"ETag2"`, 32}, {1, `"ETag1"`, 64}})
+ c.Assert(err, IsNil)
+
+ testServer.WaitRequest()
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "POST")
+ c.Assert(req.URL.Path, Equals, "/sample/multi")
+ c.Assert(req.Form.Get("uploadId"), Matches, "JNbR_[A-Za-z0-9.]+QQ--")
+
+ var payload struct {
+ XMLName xml.Name
+ Part []struct {
+ PartNumber int
+ ETag string
+ }
+ }
+
+ dec := xml.NewDecoder(req.Body)
+ err = dec.Decode(&payload)
+ c.Assert(err, IsNil)
+
+ c.Assert(payload.XMLName.Local, Equals, "CompleteMultipartUpload")
+ c.Assert(len(payload.Part), Equals, 2)
+ c.Assert(payload.Part[0].PartNumber, Equals, 1)
+ c.Assert(payload.Part[0].ETag, Equals, `"ETag1"`)
+ c.Assert(payload.Part[1].PartNumber, Equals, 2)
+ c.Assert(payload.Part[1].ETag, Equals, `"ETag2"`)
+}
+
+func (s *S) TestMultiAbort(c *C) {
+ testServer.Response(200, nil, InitMultiResultDump)
+ testServer.Response(200, nil, "")
+
+ b := s.s3.Bucket("sample")
+
+ multi, err := b.InitMulti("multi", "text/plain", s3.Private)
+ c.Assert(err, IsNil)
+
+ err = multi.Abort()
+ c.Assert(err, IsNil)
+
+ testServer.WaitRequest()
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "DELETE")
+ c.Assert(req.URL.Path, Equals, "/sample/multi")
+ c.Assert(req.Form.Get("uploadId"), Matches, "JNbR_[A-Za-z0-9.]+QQ--")
+}
+
+func (s *S) TestListMulti(c *C) {
+ testServer.Response(200, nil, ListMultiResultDump)
+
+ b := s.s3.Bucket("sample")
+
+ multis, prefixes, err := b.ListMulti("", "/")
+ c.Assert(err, IsNil)
+ c.Assert(prefixes, DeepEquals, []string{"a/", "b/"})
+ c.Assert(multis, HasLen, 2)
+ c.Assert(multis[0].Key, Equals, "multi1")
+ c.Assert(multis[0].UploadId, Equals, "iUVug89pPvSswrikD")
+ c.Assert(multis[1].Key, Equals, "multi2")
+ c.Assert(multis[1].UploadId, Equals, "DkirwsSvPp98guVUi")
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/sample/")
+ c.Assert(req.Form["uploads"], DeepEquals, []string{""})
+ c.Assert(req.Form["prefix"], DeepEquals, []string{""})
+ c.Assert(req.Form["delimiter"], DeepEquals, []string{"/"})
+ c.Assert(req.Form["max-uploads"], DeepEquals, []string{"1000"})
+}
diff --git a/vendor/src/launchpad.net/goamz/s3/responses_test.go b/vendor/src/launchpad.net/goamz/s3/responses_test.go
new file mode 100644
index 0000000000..7ceaba81b8
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/s3/responses_test.go
@@ -0,0 +1,198 @@
+package s3_test
+
+var GetObjectErrorDump = `
+
+NoSuchBucketThe specified bucket does not exist
+non-existent-bucket3F1B667FAD71C3D8
+L4ee/zrm1irFXY5F45fKXIRdOf9ktsKY/8TDVawuMK2jWRb1RF84i1uBzkdNqS5D
+`
+
+var GetListResultDump1 = `
+
+
+ quotes
+ N
+ false
+
+ Nelson
+ 2006-01-01T12:00:00.000Z
+ "828ef3fdfa96f00ad9f27c383fc9ac7f"
+ 5
+ STANDARD
+
+ bcaf161ca5fb16fd081034f
+ webfile
+
+
+
+ Neo
+ 2006-01-01T12:00:00.000Z
+ "828ef3fdfa96f00ad9f27c383fc9ac7f"
+ 4
+ STANDARD
+
+ bcaf1ffd86a5fb16fd081034f
+ webfile
+
+
+
+`
+
+var GetListResultDump2 = `
+
+ example-bucket
+ photos/2006/
+ some-marker
+ 1000
+ /
+ false
+
+
+ photos/2006/feb/
+
+
+ photos/2006/jan/
+
+
+`
+
+var InitMultiResultDump = `
+
+
+ sample
+ multi
+ JNbR_cMdwnGiD12jKAd6WK2PUkfj2VxA7i4nCwjE6t71nI9Tl3eVDPFlU0nOixhftH7I17ZPGkV3QA.l7ZD.QQ--
+
+`
+
+var ListPartsResultDump1 = `
+
+
+ sample
+ multi
+ JNbR_cMdwnGiD12jKAd6WK2PUkfj2VxA7i4nCwjE6t71nI9Tl3eVDPFlU0nOixhftH7I17ZPGkV3QA.l7ZD.QQ--
+
+ bb5c0f63b0b25f2d099c
+ joe
+
+
+ bb5c0f63b0b25f2d099c
+ joe
+
+ STANDARD
+ 0
+ 2
+ 2
+ true
+
+ 1
+ 2013-01-30T13:45:51.000Z
+ "ffc88b4ca90a355f8ddba6b2c3b2af5c"
+ 5
+
+
+ 2
+ 2013-01-30T13:45:52.000Z
+ "d067a0fa9dc61a6e7195ca99696b5a89"
+ 5
+
+
+`
+
+var ListPartsResultDump2 = `
+
+
+ sample
+ multi
+ JNbR_cMdwnGiD12jKAd6WK2PUkfj2VxA7i4nCwjE6t71nI9Tl3eVDPFlU0nOixhftH7I17ZPGkV3QA.l7ZD.QQ--
+
+ bb5c0f63b0b25f2d099c
+ joe
+
+
+ bb5c0f63b0b25f2d099c
+ joe
+
+ STANDARD
+ 2
+ 3
+ 2
+ false
+
+ 3
+ 2013-01-30T13:46:50.000Z
+ "49dcd91231f801159e893fb5c6674985"
+ 5
+
+
+`
+
+var ListMultiResultDump = `
+
+
+ goamz-test-bucket-us-east-1-akiajk3wyewhctyqbf7a
+
+
+ multi1
+ iUVug89pPvSswrikD72p8uO62EzhNtpDxRmwC5WSiWDdK9SfzmDqe3xpP1kMWimyimSnz4uzFc3waVM5ufrKYQ--
+ /
+ 1000
+ false
+
+ multi1
+ iUVug89pPvSswrikD
+
+ bb5c0f63b0b25f2d0
+ gustavoniemeyer
+
+
+ bb5c0f63b0b25f2d0
+ gustavoniemeyer
+
+ STANDARD
+ 2013-01-30T18:15:47.000Z
+
+
+ multi2
+ DkirwsSvPp98guVUi
+
+ bb5c0f63b0b25f2d0
+ joe
+
+
+ bb5c0f63b0b25f2d0
+ joe
+
+ STANDARD
+ 2013-01-30T18:15:47.000Z
+
+
+ a/
+
+
+ b/
+
+
+`
+
+var NoSuchUploadErrorDump = `
+
+
+ NoSuchUpload
+ Not relevant
+ sample
+ 3F1B667FAD71C3D8
+ kjhwqk
+
+`
+
+var InternalErrorDump = `
+
+
+ InternalError
+ Not relevant
+ sample
+ 3F1B667FAD71C3D8
+ kjhwqk
+
+`
diff --git a/vendor/src/launchpad.net/goamz/s3/s3.go b/vendor/src/launchpad.net/goamz/s3/s3.go
new file mode 100644
index 0000000000..de03b3b56f
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/s3/s3.go
@@ -0,0 +1,572 @@
+//
+// goamz - Go packages to interact with the Amazon Web Services.
+//
+// https://wiki.ubuntu.com/goamz
+//
+// Copyright (c) 2011 Canonical Ltd.
+//
+
+package s3
+
+import (
+ "bytes"
+ "encoding/xml"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "launchpad.net/goamz/aws"
+ "log"
+ "net"
+ "net/http"
+ "net/http/httputil"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+)
+
+const debug = false
+
+// The S3 type encapsulates operations with an S3 region.
+type S3 struct {
+ aws.Auth
+ aws.Region
+ private byte // Reserve the right of using private data.
+}
+
+// The Bucket type encapsulates operations with an S3 bucket.
+type Bucket struct {
+ *S3
+ Name string
+}
+
+// The Owner type represents the owner of the object in an S3 bucket.
+type Owner struct {
+ ID string
+ DisplayName string
+}
+
+var (
+ attempts = defaultAttempts
+ defaultAttempts = aws.AttemptStrategy{
+ Min: 5,
+ Total: 5 * time.Second,
+ Delay: 200 * time.Millisecond,
+ }
+)
+
+// RetryAttempts sets whether failing S3 requests may be retried to cope
+// with eventual consistency or temporary failures. It should not be
+// called while operations are in progress.
+func RetryAttempts(retry bool) {
+ if retry {
+ attempts = defaultAttempts
+ } else {
+ attempts = aws.AttemptStrategy{}
+ }
+}
+
+// New creates a new S3.
+func New(auth aws.Auth, region aws.Region) *S3 {
+ return &S3{auth, region, 0}
+}
+
+// Bucket returns a Bucket with the given name.
+func (s3 *S3) Bucket(name string) *Bucket {
+ if s3.Region.S3BucketEndpoint != "" || s3.Region.S3LowercaseBucket {
+ name = strings.ToLower(name)
+ }
+ return &Bucket{s3, name}
+}
+
+var createBucketConfiguration = `
+ %s
+`
+
+// locationConstraint returns an io.Reader specifying a LocationConstraint if
+// required for the region.
+//
+// See http://goo.gl/bh9Kq for details.
+func (s3 *S3) locationConstraint() io.Reader {
+ constraint := ""
+ if s3.Region.S3LocationConstraint {
+ constraint = fmt.Sprintf(createBucketConfiguration, s3.Region.Name)
+ }
+ return strings.NewReader(constraint)
+}
+
+type ACL string
+
+const (
+ Private = ACL("private")
+ PublicRead = ACL("public-read")
+ PublicReadWrite = ACL("public-read-write")
+ AuthenticatedRead = ACL("authenticated-read")
+ BucketOwnerRead = ACL("bucket-owner-read")
+ BucketOwnerFull = ACL("bucket-owner-full-control")
+)
+
+// PutBucket creates a new bucket.
+//
+// See http://goo.gl/ndjnR for details.
+func (b *Bucket) PutBucket(perm ACL) error {
+ headers := map[string][]string{
+ "x-amz-acl": {string(perm)},
+ }
+ req := &request{
+ method: "PUT",
+ bucket: b.Name,
+ path: "/",
+ headers: headers,
+ payload: b.locationConstraint(),
+ }
+ return b.S3.query(req, nil)
+}
+
+// DelBucket removes an existing S3 bucket. All objects in the bucket must
+// be removed before the bucket itself can be removed.
+//
+// See http://goo.gl/GoBrY for details.
+func (b *Bucket) DelBucket() (err error) {
+ req := &request{
+ method: "DELETE",
+ bucket: b.Name,
+ path: "/",
+ }
+ for attempt := attempts.Start(); attempt.Next(); {
+ err = b.S3.query(req, nil)
+ if !shouldRetry(err) {
+ break
+ }
+ }
+ return err
+}
+
+// Get retrieves an object from an S3 bucket.
+//
+// See http://goo.gl/isCO7 for details.
+func (b *Bucket) Get(path string) (data []byte, err error) {
+ body, err := b.GetReader(path)
+ if err != nil {
+ return nil, err
+ }
+ data, err = ioutil.ReadAll(body)
+ body.Close()
+ return data, err
+}
+
+// GetReader retrieves an object from an S3 bucket.
+// It is the caller's responsibility to call Close on rc when
+// finished reading.
+func (b *Bucket) GetReader(path string) (rc io.ReadCloser, err error) {
+ req := &request{
+ bucket: b.Name,
+ path: path,
+ }
+ err = b.S3.prepare(req)
+ if err != nil {
+ return nil, err
+ }
+ for attempt := attempts.Start(); attempt.Next(); {
+ hresp, err := b.S3.run(req)
+ if shouldRetry(err) && attempt.HasNext() {
+ continue
+ }
+ if err != nil {
+ return nil, err
+ }
+ return hresp.Body, nil
+ }
+ panic("unreachable")
+}
+
+// Put inserts an object into the S3 bucket.
+//
+// See http://goo.gl/FEBPD for details.
+func (b *Bucket) Put(path string, data []byte, contType string, perm ACL) error {
+ body := bytes.NewBuffer(data)
+ return b.PutReader(path, body, int64(len(data)), contType, perm)
+}
+
+// PutReader inserts an object into the S3 bucket by consuming data
+// from r until EOF.
+func (b *Bucket) PutReader(path string, r io.Reader, length int64, contType string, perm ACL) error {
+ headers := map[string][]string{
+ "Content-Length": {strconv.FormatInt(length, 10)},
+ "Content-Type": {contType},
+ "x-amz-acl": {string(perm)},
+ }
+ req := &request{
+ method: "PUT",
+ bucket: b.Name,
+ path: path,
+ headers: headers,
+ payload: r,
+ }
+ return b.S3.query(req, nil)
+}
+
+// Del removes an object from the S3 bucket.
+//
+// See http://goo.gl/APeTt for details.
+func (b *Bucket) Del(path string) error {
+ req := &request{
+ method: "DELETE",
+ bucket: b.Name,
+ path: path,
+ }
+ return b.S3.query(req, nil)
+}
+
+// The ListResp type holds the results of a List bucket operation.
+type ListResp struct {
+ Name string
+ Prefix string
+ Delimiter string
+ Marker string
+ MaxKeys int
+ // IsTruncated is true if the results have been truncated because
+ // there are more keys and prefixes than can fit in MaxKeys.
+ // N.B. this is the opposite sense to that documented (incorrectly) in
+ // http://goo.gl/YjQTc
+ IsTruncated bool
+ Contents []Key
+ CommonPrefixes []string `xml:">Prefix"`
+}
+
+// The Key type represents an item stored in an S3 bucket.
+type Key struct {
+ Key string
+ LastModified string
+ Size int64
+ // ETag gives the hex-encoded MD5 sum of the contents,
+ // surrounded with double-quotes.
+ ETag string
+ StorageClass string
+ Owner Owner
+}
+
+// List returns information about objects in an S3 bucket.
+//
+// The prefix parameter limits the response to keys that begin with the
+// specified prefix.
+//
+// The delim parameter causes the response to group all of the keys that
+// share a common prefix up to the next delimiter in a single entry within
+// the CommonPrefixes field. You can use delimiters to separate a bucket
+// into different groupings of keys, similar to how folders would work.
+//
+// The marker parameter specifies the key to start with when listing objects
+// in a bucket. Amazon S3 lists objects in alphabetical order and
+// will return keys alphabetically greater than the marker.
+//
+// The max parameter specifies how many keys + common prefixes to return in
+// the response. The default is 1000.
+//
+// For example, given these keys in a bucket:
+//
+// index.html
+// index2.html
+// photos/2006/January/sample.jpg
+// photos/2006/February/sample2.jpg
+// photos/2006/February/sample3.jpg
+// photos/2006/February/sample4.jpg
+//
+// Listing this bucket with delimiter set to "/" would yield the
+// following result:
+//
+// &ListResp{
+// Name: "sample-bucket",
+// MaxKeys: 1000,
+// Delimiter: "/",
+// Contents: []Key{
+// {Key: "index.html", "index2.html"},
+// },
+// CommonPrefixes: []string{
+// "photos/",
+// },
+// }
+//
+// Listing the same bucket with delimiter set to "/" and prefix set to
+// "photos/2006/" would yield the following result:
+//
+// &ListResp{
+// Name: "sample-bucket",
+// MaxKeys: 1000,
+// Delimiter: "/",
+// Prefix: "photos/2006/",
+// CommonPrefixes: []string{
+// "photos/2006/February/",
+// "photos/2006/January/",
+// },
+// }
+//
+// See http://goo.gl/YjQTc for details.
+func (b *Bucket) List(prefix, delim, marker string, max int) (result *ListResp, err error) {
+ params := map[string][]string{
+ "prefix": {prefix},
+ "delimiter": {delim},
+ "marker": {marker},
+ }
+ if max != 0 {
+ params["max-keys"] = []string{strconv.FormatInt(int64(max), 10)}
+ }
+ req := &request{
+ bucket: b.Name,
+ params: params,
+ }
+ result = &ListResp{}
+ for attempt := attempts.Start(); attempt.Next(); {
+ err = b.S3.query(req, result)
+ if !shouldRetry(err) {
+ break
+ }
+ }
+ if err != nil {
+ return nil, err
+ }
+ return result, nil
+}
+
+// URL returns a non-signed URL that allows retriving the
+// object at path. It only works if the object is publicly
+// readable (see SignedURL).
+func (b *Bucket) URL(path string) string {
+ req := &request{
+ bucket: b.Name,
+ path: path,
+ }
+ err := b.S3.prepare(req)
+ if err != nil {
+ panic(err)
+ }
+ u, err := req.url()
+ if err != nil {
+ panic(err)
+ }
+ u.RawQuery = ""
+ return u.String()
+}
+
+// SignedURL returns a signed URL that allows anyone holding the URL
+// to retrieve the object at path. The signature is valid until expires.
+func (b *Bucket) SignedURL(path string, expires time.Time) string {
+ req := &request{
+ bucket: b.Name,
+ path: path,
+ params: url.Values{"Expires": {strconv.FormatInt(expires.Unix(), 10)}},
+ }
+ err := b.S3.prepare(req)
+ if err != nil {
+ panic(err)
+ }
+ u, err := req.url()
+ if err != nil {
+ panic(err)
+ }
+ return u.String()
+}
+
+type request struct {
+ method string
+ bucket string
+ path string
+ signpath string
+ params url.Values
+ headers http.Header
+ baseurl string
+ payload io.Reader
+ prepared bool
+}
+
+func (req *request) url() (*url.URL, error) {
+ u, err := url.Parse(req.baseurl)
+ if err != nil {
+ return nil, fmt.Errorf("bad S3 endpoint URL %q: %v", req.baseurl, err)
+ }
+ u.RawQuery = req.params.Encode()
+ u.Path = req.path
+ return u, nil
+}
+
+// query prepares and runs the req request.
+// If resp is not nil, the XML data contained in the response
+// body will be unmarshalled on it.
+func (s3 *S3) query(req *request, resp interface{}) error {
+ err := s3.prepare(req)
+ if err != nil {
+ return err
+ }
+ hresp, err := s3.run(req)
+ if err != nil {
+ return err
+ }
+ if resp != nil {
+ err = xml.NewDecoder(hresp.Body).Decode(resp)
+ }
+ hresp.Body.Close()
+ return nil
+}
+
+// prepare sets up req to be delivered to S3.
+func (s3 *S3) prepare(req *request) error {
+ if !req.prepared {
+ req.prepared = true
+ if req.method == "" {
+ req.method = "GET"
+ }
+ // Copy so they can be mutated without affecting on retries.
+ params := make(url.Values)
+ headers := make(http.Header)
+ for k, v := range req.params {
+ params[k] = v
+ }
+ for k, v := range req.headers {
+ headers[k] = v
+ }
+ req.params = params
+ req.headers = headers
+ if !strings.HasPrefix(req.path, "/") {
+ req.path = "/" + req.path
+ }
+ req.signpath = req.path
+ if req.bucket != "" {
+ req.baseurl = s3.Region.S3BucketEndpoint
+ if req.baseurl == "" {
+ // Use the path method to address the bucket.
+ req.baseurl = s3.Region.S3Endpoint
+ req.path = "/" + req.bucket + req.path
+ } else {
+ // Just in case, prevent injection.
+ if strings.IndexAny(req.bucket, "/:@") >= 0 {
+ return fmt.Errorf("bad S3 bucket: %q", req.bucket)
+ }
+ req.baseurl = strings.Replace(req.baseurl, "${bucket}", req.bucket, -1)
+ }
+ req.signpath = "/" + req.bucket + req.signpath
+ }
+ }
+
+ // Always sign again as it's not clear how far the
+ // server has handled a previous attempt.
+ u, err := url.Parse(req.baseurl)
+ if err != nil {
+ return fmt.Errorf("bad S3 endpoint URL %q: %v", req.baseurl, err)
+ }
+ req.headers["Host"] = []string{u.Host}
+ req.headers["Date"] = []string{time.Now().In(time.UTC).Format(time.RFC1123)}
+ sign(s3.Auth, req.method, req.signpath, req.params, req.headers)
+ return nil
+}
+
+// run sends req and returns the http response from the server.
+func (s3 *S3) run(req *request) (*http.Response, error) {
+ if debug {
+ log.Printf("Running S3 request: %#v", req)
+ }
+
+ u, err := req.url()
+ if err != nil {
+ return nil, err
+ }
+
+ hreq := http.Request{
+ URL: u,
+ Method: req.method,
+ ProtoMajor: 1,
+ ProtoMinor: 1,
+ Close: true,
+ Header: req.headers,
+ }
+
+ if v, ok := req.headers["Content-Length"]; ok {
+ hreq.ContentLength, _ = strconv.ParseInt(v[0], 10, 64)
+ delete(req.headers, "Content-Length")
+ }
+ if req.payload != nil {
+ hreq.Body = ioutil.NopCloser(req.payload)
+ }
+
+ hresp, err := http.DefaultClient.Do(&hreq)
+ if err != nil {
+ return nil, err
+ }
+ if debug {
+ dump, _ := httputil.DumpResponse(hresp, true)
+ log.Printf("} -> %s\n", dump)
+ }
+ if hresp.StatusCode != 200 && hresp.StatusCode != 204 {
+ return nil, buildError(hresp)
+ }
+ return hresp, err
+}
+
+// Error represents an error in an operation with S3.
+type Error struct {
+ StatusCode int // HTTP status code (200, 403, ...)
+ Code string // EC2 error code ("UnsupportedOperation", ...)
+ Message string // The human-oriented error message
+ BucketName string
+ RequestId string
+ HostId string
+}
+
+func (e *Error) Error() string {
+ return e.Message
+}
+
+func buildError(r *http.Response) error {
+ if debug {
+ log.Printf("got error (status code %v)", r.StatusCode)
+ data, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ log.Printf("\tread error: %v", err)
+ } else {
+ log.Printf("\tdata:\n%s\n\n", data)
+ }
+ r.Body = ioutil.NopCloser(bytes.NewBuffer(data))
+ }
+
+ err := Error{}
+ // TODO return error if Unmarshal fails?
+ xml.NewDecoder(r.Body).Decode(&err)
+ r.Body.Close()
+ err.StatusCode = r.StatusCode
+ if err.Message == "" {
+ err.Message = r.Status
+ }
+ if debug {
+ log.Printf("err: %#v\n", err)
+ }
+ return &err
+}
+
+func shouldRetry(err error) bool {
+ if err == nil {
+ return false
+ }
+ switch err {
+ case io.ErrUnexpectedEOF, io.EOF:
+ return true
+ }
+ switch e := err.(type) {
+ case *net.DNSError:
+ return true
+ case *net.OpError:
+ switch e.Op {
+ case "read", "write":
+ return true
+ }
+ case *Error:
+ switch e.Code {
+ case "InternalError", "NoSuchUpload", "NoSuchBucket":
+ return true
+ }
+ }
+ return false
+}
+
+func hasCode(err error, code string) bool {
+ s3err, ok := err.(*Error)
+ return ok && s3err.Code == code
+}
diff --git a/vendor/src/launchpad.net/goamz/s3/s3_test.go b/vendor/src/launchpad.net/goamz/s3/s3_test.go
new file mode 100644
index 0000000000..4a37af5da0
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/s3/s3_test.go
@@ -0,0 +1,283 @@
+package s3_test
+
+import (
+ "bytes"
+ "io/ioutil"
+ "net/http"
+ "testing"
+
+ "launchpad.net/goamz/aws"
+ "launchpad.net/goamz/s3"
+ "launchpad.net/goamz/testutil"
+ . "launchpad.net/gocheck"
+ "time"
+)
+
+func Test(t *testing.T) {
+ TestingT(t)
+}
+
+type S struct {
+ s3 *s3.S3
+}
+
+var _ = Suite(&S{})
+
+var testServer = testutil.NewHTTPServer()
+
+func (s *S) SetUpSuite(c *C) {
+ testServer.Start()
+ auth := aws.Auth{"abc", "123"}
+ s.s3 = s3.New(auth, aws.Region{Name: "faux-region-1", S3Endpoint: testServer.URL})
+}
+
+func (s *S) TearDownSuite(c *C) {
+ s3.SetAttemptStrategy(nil)
+ testServer.Stop()
+}
+
+func (s *S) SetUpTest(c *C) {
+ attempts := aws.AttemptStrategy{
+ Total: 300 * time.Millisecond,
+ Delay: 100 * time.Millisecond,
+ }
+ s3.SetAttemptStrategy(&attempts)
+}
+
+func (s *S) TearDownTest(c *C) {
+ testServer.Flush()
+}
+
+// PutBucket docs: http://goo.gl/kBTCu
+
+func (s *S) TestPutBucket(c *C) {
+ testServer.Response(200, nil, "")
+
+ b := s.s3.Bucket("bucket")
+ err := b.PutBucket(s3.Private)
+ c.Assert(err, IsNil)
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "PUT")
+ c.Assert(req.URL.Path, Equals, "/bucket/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+}
+
+// DeleteBucket docs: http://goo.gl/GoBrY
+
+func (s *S) TestDelBucket(c *C) {
+ testServer.Response(204, nil, "")
+
+ b := s.s3.Bucket("bucket")
+ err := b.DelBucket()
+ c.Assert(err, IsNil)
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "DELETE")
+ c.Assert(req.URL.Path, Equals, "/bucket/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+}
+
+// GetObject docs: http://goo.gl/isCO7
+
+func (s *S) TestGet(c *C) {
+ testServer.Response(200, nil, "content")
+
+ b := s.s3.Bucket("bucket")
+ data, err := b.Get("name")
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/bucket/name")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+
+ c.Assert(err, IsNil)
+ c.Assert(string(data), Equals, "content")
+}
+
+func (s *S) TestURL(c *C) {
+ testServer.Response(200, nil, "content")
+
+ b := s.s3.Bucket("bucket")
+ url := b.URL("name")
+ r, err := http.Get(url)
+ c.Assert(err, IsNil)
+ data, err := ioutil.ReadAll(r.Body)
+ r.Body.Close()
+ c.Assert(err, IsNil)
+ c.Assert(string(data), Equals, "content")
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/bucket/name")
+}
+
+func (s *S) TestGetReader(c *C) {
+ testServer.Response(200, nil, "content")
+
+ b := s.s3.Bucket("bucket")
+ rc, err := b.GetReader("name")
+ c.Assert(err, IsNil)
+ data, err := ioutil.ReadAll(rc)
+ rc.Close()
+ c.Assert(err, IsNil)
+ c.Assert(string(data), Equals, "content")
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/bucket/name")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+}
+
+func (s *S) TestGetNotFound(c *C) {
+ for i := 0; i < 10; i++ {
+ testServer.Response(404, nil, GetObjectErrorDump)
+ }
+
+ b := s.s3.Bucket("non-existent-bucket")
+ data, err := b.Get("non-existent")
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/non-existent-bucket/non-existent")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+
+ s3err, _ := err.(*s3.Error)
+ c.Assert(s3err, NotNil)
+ c.Assert(s3err.StatusCode, Equals, 404)
+ c.Assert(s3err.BucketName, Equals, "non-existent-bucket")
+ c.Assert(s3err.RequestId, Equals, "3F1B667FAD71C3D8")
+ c.Assert(s3err.HostId, Equals, "L4ee/zrm1irFXY5F45fKXIRdOf9ktsKY/8TDVawuMK2jWRb1RF84i1uBzkdNqS5D")
+ c.Assert(s3err.Code, Equals, "NoSuchBucket")
+ c.Assert(s3err.Message, Equals, "The specified bucket does not exist")
+ c.Assert(s3err.Error(), Equals, "The specified bucket does not exist")
+ c.Assert(data, IsNil)
+}
+
+// PutObject docs: http://goo.gl/FEBPD
+
+func (s *S) TestPutObject(c *C) {
+ testServer.Response(200, nil, "")
+
+ b := s.s3.Bucket("bucket")
+ err := b.Put("name", []byte("content"), "content-type", s3.Private)
+ c.Assert(err, IsNil)
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "PUT")
+ c.Assert(req.URL.Path, Equals, "/bucket/name")
+ c.Assert(req.Header["Date"], Not(DeepEquals), []string{""})
+ c.Assert(req.Header["Content-Type"], DeepEquals, []string{"content-type"})
+ c.Assert(req.Header["Content-Length"], DeepEquals, []string{"7"})
+ //c.Assert(req.Header["Content-MD5"], DeepEquals, "...")
+ c.Assert(req.Header["X-Amz-Acl"], DeepEquals, []string{"private"})
+}
+
+func (s *S) TestPutReader(c *C) {
+ testServer.Response(200, nil, "")
+
+ b := s.s3.Bucket("bucket")
+ buf := bytes.NewBufferString("content")
+ err := b.PutReader("name", buf, int64(buf.Len()), "content-type", s3.Private)
+ c.Assert(err, IsNil)
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "PUT")
+ c.Assert(req.URL.Path, Equals, "/bucket/name")
+ c.Assert(req.Header["Date"], Not(DeepEquals), []string{""})
+ c.Assert(req.Header["Content-Type"], DeepEquals, []string{"content-type"})
+ c.Assert(req.Header["Content-Length"], DeepEquals, []string{"7"})
+ //c.Assert(req.Header["Content-MD5"], Equals, "...")
+ c.Assert(req.Header["X-Amz-Acl"], DeepEquals, []string{"private"})
+}
+
+// DelObject docs: http://goo.gl/APeTt
+
+func (s *S) TestDelObject(c *C) {
+ testServer.Response(200, nil, "")
+
+ b := s.s3.Bucket("bucket")
+ err := b.Del("name")
+ c.Assert(err, IsNil)
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "DELETE")
+ c.Assert(req.URL.Path, Equals, "/bucket/name")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+}
+
+// Bucket List Objects docs: http://goo.gl/YjQTc
+
+func (s *S) TestList(c *C) {
+ testServer.Response(200, nil, GetListResultDump1)
+
+ b := s.s3.Bucket("quotes")
+
+ data, err := b.List("N", "", "", 0)
+ c.Assert(err, IsNil)
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/quotes/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+ c.Assert(req.Form["prefix"], DeepEquals, []string{"N"})
+ c.Assert(req.Form["delimiter"], DeepEquals, []string{""})
+ c.Assert(req.Form["marker"], DeepEquals, []string{""})
+ c.Assert(req.Form["max-keys"], DeepEquals, []string(nil))
+
+ c.Assert(data.Name, Equals, "quotes")
+ c.Assert(data.Prefix, Equals, "N")
+ c.Assert(data.IsTruncated, Equals, false)
+ c.Assert(len(data.Contents), Equals, 2)
+
+ c.Assert(data.Contents[0].Key, Equals, "Nelson")
+ c.Assert(data.Contents[0].LastModified, Equals, "2006-01-01T12:00:00.000Z")
+ c.Assert(data.Contents[0].ETag, Equals, `"828ef3fdfa96f00ad9f27c383fc9ac7f"`)
+ c.Assert(data.Contents[0].Size, Equals, int64(5))
+ c.Assert(data.Contents[0].StorageClass, Equals, "STANDARD")
+ c.Assert(data.Contents[0].Owner.ID, Equals, "bcaf161ca5fb16fd081034f")
+ c.Assert(data.Contents[0].Owner.DisplayName, Equals, "webfile")
+
+ c.Assert(data.Contents[1].Key, Equals, "Neo")
+ c.Assert(data.Contents[1].LastModified, Equals, "2006-01-01T12:00:00.000Z")
+ c.Assert(data.Contents[1].ETag, Equals, `"828ef3fdfa96f00ad9f27c383fc9ac7f"`)
+ c.Assert(data.Contents[1].Size, Equals, int64(4))
+ c.Assert(data.Contents[1].StorageClass, Equals, "STANDARD")
+ c.Assert(data.Contents[1].Owner.ID, Equals, "bcaf1ffd86a5fb16fd081034f")
+ c.Assert(data.Contents[1].Owner.DisplayName, Equals, "webfile")
+}
+
+func (s *S) TestListWithDelimiter(c *C) {
+ testServer.Response(200, nil, GetListResultDump2)
+
+ b := s.s3.Bucket("quotes")
+
+ data, err := b.List("photos/2006/", "/", "some-marker", 1000)
+ c.Assert(err, IsNil)
+
+ req := testServer.WaitRequest()
+ c.Assert(req.Method, Equals, "GET")
+ c.Assert(req.URL.Path, Equals, "/quotes/")
+ c.Assert(req.Header["Date"], Not(Equals), "")
+ c.Assert(req.Form["prefix"], DeepEquals, []string{"photos/2006/"})
+ c.Assert(req.Form["delimiter"], DeepEquals, []string{"/"})
+ c.Assert(req.Form["marker"], DeepEquals, []string{"some-marker"})
+ c.Assert(req.Form["max-keys"], DeepEquals, []string{"1000"})
+
+ c.Assert(data.Name, Equals, "example-bucket")
+ c.Assert(data.Prefix, Equals, "photos/2006/")
+ c.Assert(data.Delimiter, Equals, "/")
+ c.Assert(data.Marker, Equals, "some-marker")
+ c.Assert(data.IsTruncated, Equals, false)
+ c.Assert(len(data.Contents), Equals, 0)
+ c.Assert(data.CommonPrefixes, DeepEquals, []string{"photos/2006/feb/", "photos/2006/jan/"})
+}
+
+func (s *S) TestRetryAttempts(c *C) {
+ s3.SetAttemptStrategy(nil)
+ orig := s3.AttemptStrategy()
+ s3.RetryAttempts(false)
+ c.Assert(s3.AttemptStrategy(), Equals, aws.AttemptStrategy{})
+ s3.RetryAttempts(true)
+ c.Assert(s3.AttemptStrategy(), Equals, orig)
+}
diff --git a/vendor/src/launchpad.net/goamz/s3/s3i_test.go b/vendor/src/launchpad.net/goamz/s3/s3i_test.go
new file mode 100644
index 0000000000..a33aaf4d6b
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/s3/s3i_test.go
@@ -0,0 +1,590 @@
+package s3_test
+
+import (
+ "bytes"
+ "crypto/md5"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "strings"
+
+ "launchpad.net/goamz/aws"
+ "launchpad.net/goamz/s3"
+ "launchpad.net/goamz/testutil"
+ . "launchpad.net/gocheck"
+ "net"
+ "sort"
+ "time"
+)
+
+// AmazonServer represents an Amazon S3 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
+}
+
+var _ = Suite(&AmazonClientSuite{Region: aws.USEast})
+var _ = Suite(&AmazonClientSuite{Region: aws.EUWest})
+var _ = Suite(&AmazonDomainClientSuite{Region: aws.USEast})
+
+// AmazonClientSuite tests the client against a live S3 server.
+type AmazonClientSuite struct {
+ aws.Region
+ srv AmazonServer
+ ClientTests
+}
+
+func (s *AmazonClientSuite) SetUpSuite(c *C) {
+ if !testutil.Amazon {
+ c.Skip("live tests against AWS disabled (no -amazon)")
+ }
+ s.srv.SetUp(c)
+ s.s3 = s3.New(s.srv.auth, s.Region)
+ // In case tests were interrupted in the middle before.
+ s.ClientTests.Cleanup()
+}
+
+func (s *AmazonClientSuite) TearDownTest(c *C) {
+ s.ClientTests.Cleanup()
+}
+
+// AmazonDomainClientSuite tests the client against a live S3
+// server using bucket names in the endpoint domain name rather
+// than the request path.
+type AmazonDomainClientSuite struct {
+ aws.Region
+ srv AmazonServer
+ ClientTests
+}
+
+func (s *AmazonDomainClientSuite) SetUpSuite(c *C) {
+ if !testutil.Amazon {
+ c.Skip("live tests against AWS disabled (no -amazon)")
+ }
+ s.srv.SetUp(c)
+ region := s.Region
+ region.S3BucketEndpoint = "https://${bucket}.s3.amazonaws.com"
+ s.s3 = s3.New(s.srv.auth, region)
+ s.ClientTests.Cleanup()
+}
+
+func (s *AmazonDomainClientSuite) TearDownTest(c *C) {
+ s.ClientTests.Cleanup()
+}
+
+// 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 {
+ s3 *s3.S3
+ authIsBroken bool
+}
+
+func (s *ClientTests) Cleanup() {
+ killBucket(testBucket(s.s3))
+}
+
+func testBucket(s *s3.S3) *s3.Bucket {
+ // Watch out! If this function is corrupted and made to match with something
+ // people own, killBucket will happily remove *everything* inside the bucket.
+ key := s.Auth.AccessKey
+ if len(key) >= 8 {
+ key = s.Auth.AccessKey[:8]
+ }
+ return s.Bucket(fmt.Sprintf("goamz-%s-%s", s.Region.Name, key))
+}
+
+var attempts = aws.AttemptStrategy{
+ Min: 5,
+ Total: 20 * time.Second,
+ Delay: 100 * time.Millisecond,
+}
+
+func killBucket(b *s3.Bucket) {
+ var err error
+ for attempt := attempts.Start(); attempt.Next(); {
+ err = b.DelBucket()
+ if err == nil {
+ return
+ }
+ if _, ok := err.(*net.DNSError); ok {
+ return
+ }
+ e, ok := err.(*s3.Error)
+ if ok && e.Code == "NoSuchBucket" {
+ return
+ }
+ if ok && e.Code == "BucketNotEmpty" {
+ // Errors are ignored here. Just retry.
+ resp, err := b.List("", "", "", 1000)
+ if err == nil {
+ for _, key := range resp.Contents {
+ _ = b.Del(key.Key)
+ }
+ }
+ multis, _, _ := b.ListMulti("", "")
+ for _, m := range multis {
+ _ = m.Abort()
+ }
+ }
+ }
+ message := "cannot delete test bucket"
+ if err != nil {
+ message += ": " + err.Error()
+ }
+ panic(message)
+}
+
+func get(url string) ([]byte, error) {
+ for attempt := attempts.Start(); attempt.Next(); {
+ resp, err := http.Get(url)
+ if err != nil {
+ if attempt.HasNext() {
+ continue
+ }
+ return nil, err
+ }
+ data, err := ioutil.ReadAll(resp.Body)
+ resp.Body.Close()
+ if err != nil {
+ if attempt.HasNext() {
+ continue
+ }
+ return nil, err
+ }
+ return data, err
+ }
+ panic("unreachable")
+}
+
+func (s *ClientTests) TestBasicFunctionality(c *C) {
+ b := testBucket(s.s3)
+ err := b.PutBucket(s3.PublicRead)
+ c.Assert(err, IsNil)
+
+ err = b.Put("name", []byte("yo!"), "text/plain", s3.PublicRead)
+ c.Assert(err, IsNil)
+ defer b.Del("name")
+
+ data, err := b.Get("name")
+ c.Assert(err, IsNil)
+ c.Assert(string(data), Equals, "yo!")
+
+ data, err = get(b.URL("name"))
+ c.Assert(err, IsNil)
+ c.Assert(string(data), Equals, "yo!")
+
+ buf := bytes.NewBufferString("hey!")
+ err = b.PutReader("name2", buf, int64(buf.Len()), "text/plain", s3.Private)
+ c.Assert(err, IsNil)
+ defer b.Del("name2")
+
+ rc, err := b.GetReader("name2")
+ c.Assert(err, IsNil)
+ data, err = ioutil.ReadAll(rc)
+ c.Check(err, IsNil)
+ c.Check(string(data), Equals, "hey!")
+ rc.Close()
+
+ data, err = get(b.SignedURL("name2", time.Now().Add(time.Hour)))
+ c.Assert(err, IsNil)
+ c.Assert(string(data), Equals, "hey!")
+
+ if !s.authIsBroken {
+ data, err = get(b.SignedURL("name2", time.Now().Add(-time.Hour)))
+ c.Assert(err, IsNil)
+ c.Assert(string(data), Matches, "(?s).*AccessDenied.*")
+ }
+
+ err = b.DelBucket()
+ c.Assert(err, NotNil)
+
+ s3err, ok := err.(*s3.Error)
+ c.Assert(ok, Equals, true)
+ c.Assert(s3err.Code, Equals, "BucketNotEmpty")
+ c.Assert(s3err.BucketName, Equals, b.Name)
+ c.Assert(s3err.Message, Equals, "The bucket you tried to delete is not empty")
+
+ err = b.Del("name")
+ c.Assert(err, IsNil)
+ err = b.Del("name2")
+ c.Assert(err, IsNil)
+
+ err = b.DelBucket()
+ c.Assert(err, IsNil)
+}
+
+func (s *ClientTests) TestGetNotFound(c *C) {
+ b := s.s3.Bucket("goamz-" + s.s3.Auth.AccessKey)
+ data, err := b.Get("non-existent")
+
+ s3err, _ := err.(*s3.Error)
+ c.Assert(s3err, NotNil)
+ c.Assert(s3err.StatusCode, Equals, 404)
+ c.Assert(s3err.Code, Equals, "NoSuchBucket")
+ c.Assert(s3err.Message, Equals, "The specified bucket does not exist")
+ c.Assert(data, IsNil)
+}
+
+// Communicate with all endpoints to see if they are alive.
+func (s *ClientTests) TestRegions(c *C) {
+ errs := make(chan error, len(aws.Regions))
+ for _, region := range aws.Regions {
+ go func(r aws.Region) {
+ s := s3.New(s.s3.Auth, r)
+ b := s.Bucket("goamz-" + s.Auth.AccessKey)
+ _, err := b.Get("non-existent")
+ errs <- err
+ }(region)
+ }
+ for _ = range aws.Regions {
+ err := <-errs
+ if err != nil {
+ s3_err, ok := err.(*s3.Error)
+ if ok {
+ c.Check(s3_err.Code, Matches, "NoSuchBucket")
+ } else if _, ok = err.(*net.DNSError); ok {
+ // Okay as well.
+ } else {
+ c.Errorf("Non-S3 error: %s", err)
+ }
+ } else {
+ c.Errorf("Test should have errored but it seems to have succeeded")
+ }
+ }
+}
+
+var objectNames = []string{
+ "index.html",
+ "index2.html",
+ "photos/2006/February/sample2.jpg",
+ "photos/2006/February/sample3.jpg",
+ "photos/2006/February/sample4.jpg",
+ "photos/2006/January/sample.jpg",
+ "test/bar",
+ "test/foo",
+}
+
+func keys(names ...string) []s3.Key {
+ ks := make([]s3.Key, len(names))
+ for i, name := range names {
+ ks[i].Key = name
+ }
+ return ks
+}
+
+// As the ListResp specifies all the parameters to the
+// request too, we use it to specify request parameters
+// and expected results. The Contents field is
+// used only for the key names inside it.
+var listTests = []s3.ListResp{
+ // normal list.
+ {
+ Contents: keys(objectNames...),
+ }, {
+ Marker: objectNames[0],
+ Contents: keys(objectNames[1:]...),
+ }, {
+ Marker: objectNames[0] + "a",
+ Contents: keys(objectNames[1:]...),
+ }, {
+ Marker: "z",
+ },
+
+ // limited results.
+ {
+ MaxKeys: 2,
+ Contents: keys(objectNames[0:2]...),
+ IsTruncated: true,
+ }, {
+ MaxKeys: 2,
+ Marker: objectNames[0],
+ Contents: keys(objectNames[1:3]...),
+ IsTruncated: true,
+ }, {
+ MaxKeys: 2,
+ Marker: objectNames[len(objectNames)-2],
+ Contents: keys(objectNames[len(objectNames)-1:]...),
+ },
+
+ // with delimiter
+ {
+ Delimiter: "/",
+ CommonPrefixes: []string{"photos/", "test/"},
+ Contents: keys("index.html", "index2.html"),
+ }, {
+ Delimiter: "/",
+ Prefix: "photos/2006/",
+ CommonPrefixes: []string{"photos/2006/February/", "photos/2006/January/"},
+ }, {
+ Delimiter: "/",
+ Prefix: "t",
+ CommonPrefixes: []string{"test/"},
+ }, {
+ Delimiter: "/",
+ MaxKeys: 1,
+ Contents: keys("index.html"),
+ IsTruncated: true,
+ }, {
+ Delimiter: "/",
+ MaxKeys: 1,
+ Marker: "index2.html",
+ CommonPrefixes: []string{"photos/"},
+ IsTruncated: true,
+ }, {
+ Delimiter: "/",
+ MaxKeys: 1,
+ Marker: "photos/",
+ CommonPrefixes: []string{"test/"},
+ IsTruncated: false,
+ }, {
+ Delimiter: "Feb",
+ CommonPrefixes: []string{"photos/2006/Feb"},
+ Contents: keys("index.html", "index2.html", "photos/2006/January/sample.jpg", "test/bar", "test/foo"),
+ },
+}
+
+func (s *ClientTests) TestDoublePutBucket(c *C) {
+ b := testBucket(s.s3)
+ err := b.PutBucket(s3.PublicRead)
+ c.Assert(err, IsNil)
+
+ err = b.PutBucket(s3.PublicRead)
+ if err != nil {
+ c.Assert(err, FitsTypeOf, new(s3.Error))
+ c.Assert(err.(*s3.Error).Code, Equals, "BucketAlreadyOwnedByYou")
+ }
+}
+
+func (s *ClientTests) TestBucketList(c *C) {
+ b := testBucket(s.s3)
+ err := b.PutBucket(s3.Private)
+ c.Assert(err, IsNil)
+
+ objData := make(map[string][]byte)
+ for i, path := range objectNames {
+ data := []byte(strings.Repeat("a", i))
+ err := b.Put(path, data, "text/plain", s3.Private)
+ c.Assert(err, IsNil)
+ defer b.Del(path)
+ objData[path] = data
+ }
+
+ for i, t := range listTests {
+ c.Logf("test %d", i)
+ resp, err := b.List(t.Prefix, t.Delimiter, t.Marker, t.MaxKeys)
+ c.Assert(err, IsNil)
+ c.Check(resp.Name, Equals, b.Name)
+ c.Check(resp.Delimiter, Equals, t.Delimiter)
+ c.Check(resp.IsTruncated, Equals, t.IsTruncated)
+ c.Check(resp.CommonPrefixes, DeepEquals, t.CommonPrefixes)
+ checkContents(c, resp.Contents, objData, t.Contents)
+ }
+}
+
+func etag(data []byte) string {
+ sum := md5.New()
+ sum.Write(data)
+ return fmt.Sprintf(`"%x"`, sum.Sum(nil))
+}
+
+func checkContents(c *C, contents []s3.Key, data map[string][]byte, expected []s3.Key) {
+ c.Assert(contents, HasLen, len(expected))
+ for i, k := range contents {
+ c.Check(k.Key, Equals, expected[i].Key)
+ // TODO mtime
+ c.Check(k.Size, Equals, int64(len(data[k.Key])))
+ c.Check(k.ETag, Equals, etag(data[k.Key]))
+ }
+}
+
+func (s *ClientTests) TestMultiInitPutList(c *C) {
+ b := testBucket(s.s3)
+ err := b.PutBucket(s3.Private)
+ c.Assert(err, IsNil)
+
+ multi, err := b.InitMulti("multi", "text/plain", s3.Private)
+ c.Assert(err, IsNil)
+ c.Assert(multi.UploadId, Matches, ".+")
+ defer multi.Abort()
+
+ var sent []s3.Part
+
+ for i := 0; i < 5; i++ {
+ p, err := multi.PutPart(i+1, strings.NewReader(fmt.Sprintf("", i+1)))
+ c.Assert(err, IsNil)
+ c.Assert(p.N, Equals, i+1)
+ c.Assert(p.Size, Equals, int64(8))
+ c.Assert(p.ETag, Matches, ".+")
+ sent = append(sent, p)
+ }
+
+ s3.SetListPartsMax(2)
+
+ parts, err := multi.ListParts()
+ c.Assert(err, IsNil)
+ c.Assert(parts, HasLen, len(sent))
+ for i := range parts {
+ c.Assert(parts[i].N, Equals, sent[i].N)
+ c.Assert(parts[i].Size, Equals, sent[i].Size)
+ c.Assert(parts[i].ETag, Equals, sent[i].ETag)
+ }
+
+ err = multi.Complete(parts)
+ s3err, failed := err.(*s3.Error)
+ c.Assert(failed, Equals, true)
+ c.Assert(s3err.Code, Equals, "EntityTooSmall")
+
+ err = multi.Abort()
+ c.Assert(err, IsNil)
+ _, err = multi.ListParts()
+ s3err, ok := err.(*s3.Error)
+ c.Assert(ok, Equals, true)
+ c.Assert(s3err.Code, Equals, "NoSuchUpload")
+}
+
+// This may take a minute or more due to the minimum size accepted S3
+// on multipart upload parts.
+func (s *ClientTests) TestMultiComplete(c *C) {
+ b := testBucket(s.s3)
+ err := b.PutBucket(s3.Private)
+ c.Assert(err, IsNil)
+
+ multi, err := b.InitMulti("multi", "text/plain", s3.Private)
+ c.Assert(err, IsNil)
+ c.Assert(multi.UploadId, Matches, ".+")
+ defer multi.Abort()
+
+ // Minimum size S3 accepts for all but the last part is 5MB.
+ data1 := make([]byte, 5*1024*1024)
+ data2 := []byte("")
+
+ part1, err := multi.PutPart(1, bytes.NewReader(data1))
+ c.Assert(err, IsNil)
+ part2, err := multi.PutPart(2, bytes.NewReader(data2))
+ c.Assert(err, IsNil)
+
+ // Purposefully reversed. The order requirement must be handled.
+ err = multi.Complete([]s3.Part{part2, part1})
+ c.Assert(err, IsNil)
+
+ data, err := b.Get("multi")
+ c.Assert(err, IsNil)
+
+ c.Assert(len(data), Equals, len(data1)+len(data2))
+ for i := range data1 {
+ if data[i] != data1[i] {
+ c.Fatalf("uploaded object at byte %d: want %d, got %d", data1[i], data[i])
+ }
+ }
+ c.Assert(string(data[len(data1):]), Equals, string(data2))
+}
+
+type multiList []*s3.Multi
+
+func (l multiList) Len() int { return len(l) }
+func (l multiList) Less(i, j int) bool { return l[i].Key < l[j].Key }
+func (l multiList) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
+
+func (s *ClientTests) TestListMulti(c *C) {
+ b := testBucket(s.s3)
+ err := b.PutBucket(s3.Private)
+ c.Assert(err, IsNil)
+
+ // Ensure an empty state before testing its behavior.
+ multis, _, err := b.ListMulti("", "")
+ for _, m := range multis {
+ err := m.Abort()
+ c.Assert(err, IsNil)
+ }
+
+ keys := []string{
+ "a/multi2",
+ "a/multi3",
+ "b/multi4",
+ "multi1",
+ }
+ for _, key := range keys {
+ m, err := b.InitMulti(key, "", s3.Private)
+ c.Assert(err, IsNil)
+ defer m.Abort()
+ }
+
+ // Amazon's implementation of the multiple-request listing for
+ // multipart uploads in progress seems broken in multiple ways.
+ // (next tokens are not provided, etc).
+ //s3.SetListMultiMax(2)
+
+ multis, prefixes, err := b.ListMulti("", "")
+ c.Assert(err, IsNil)
+ for attempt := attempts.Start(); attempt.Next() && len(multis) < len(keys); {
+ multis, prefixes, err = b.ListMulti("", "")
+ c.Assert(err, IsNil)
+ }
+ sort.Sort(multiList(multis))
+ c.Assert(prefixes, IsNil)
+ var gotKeys []string
+ for _, m := range multis {
+ gotKeys = append(gotKeys, m.Key)
+ }
+ c.Assert(gotKeys, DeepEquals, keys)
+ for _, m := range multis {
+ c.Assert(m.Bucket, Equals, b)
+ c.Assert(m.UploadId, Matches, ".+")
+ }
+
+ multis, prefixes, err = b.ListMulti("", "/")
+ for attempt := attempts.Start(); attempt.Next() && len(prefixes) < 2; {
+ multis, prefixes, err = b.ListMulti("", "")
+ c.Assert(err, IsNil)
+ }
+ c.Assert(err, IsNil)
+ c.Assert(prefixes, DeepEquals, []string{"a/", "b/"})
+ c.Assert(multis, HasLen, 1)
+ c.Assert(multis[0].Bucket, Equals, b)
+ c.Assert(multis[0].Key, Equals, "multi1")
+ c.Assert(multis[0].UploadId, Matches, ".+")
+
+ for attempt := attempts.Start(); attempt.Next() && len(multis) < 2; {
+ multis, prefixes, err = b.ListMulti("", "")
+ c.Assert(err, IsNil)
+ }
+ multis, prefixes, err = b.ListMulti("a/", "/")
+ c.Assert(err, IsNil)
+ c.Assert(prefixes, IsNil)
+ c.Assert(multis, HasLen, 2)
+ c.Assert(multis[0].Bucket, Equals, b)
+ c.Assert(multis[0].Key, Equals, "a/multi2")
+ c.Assert(multis[0].UploadId, Matches, ".+")
+ c.Assert(multis[1].Bucket, Equals, b)
+ c.Assert(multis[1].Key, Equals, "a/multi3")
+ c.Assert(multis[1].UploadId, Matches, ".+")
+}
+
+func (s *ClientTests) TestMultiPutAllZeroLength(c *C) {
+ b := testBucket(s.s3)
+ err := b.PutBucket(s3.Private)
+ c.Assert(err, IsNil)
+
+ multi, err := b.InitMulti("multi", "text/plain", s3.Private)
+ c.Assert(err, IsNil)
+ defer multi.Abort()
+
+ // This tests an edge case. Amazon requires at least one
+ // part for multiprat uploads to work, even the part is empty.
+ parts, err := multi.PutAll(strings.NewReader(""), 5*1024*1024)
+ c.Assert(err, IsNil)
+ c.Assert(parts, HasLen, 1)
+ c.Assert(parts[0].Size, Equals, int64(0))
+ c.Assert(parts[0].ETag, Equals, `"d41d8cd98f00b204e9800998ecf8427e"`)
+
+ err = multi.Complete(parts)
+ c.Assert(err, IsNil)
+}
diff --git a/vendor/src/launchpad.net/goamz/s3/s3t_test.go b/vendor/src/launchpad.net/goamz/s3/s3t_test.go
new file mode 100644
index 0000000000..552107b0b7
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/s3/s3t_test.go
@@ -0,0 +1,79 @@
+package s3_test
+
+import (
+ "launchpad.net/goamz/aws"
+ "launchpad.net/goamz/s3"
+ "launchpad.net/goamz/s3/s3test"
+ . "launchpad.net/gocheck"
+)
+
+type LocalServer struct {
+ auth aws.Auth
+ region aws.Region
+ srv *s3test.Server
+ config *s3test.Config
+}
+
+func (s *LocalServer) SetUp(c *C) {
+ srv, err := s3test.NewServer(s.config)
+ c.Assert(err, IsNil)
+ c.Assert(srv, NotNil)
+
+ s.srv = srv
+ s.region = aws.Region{
+ Name: "faux-region-1",
+ S3Endpoint: srv.URL(),
+ S3LocationConstraint: true, // s3test server requires a LocationConstraint
+ }
+}
+
+// LocalServerSuite defines tests that will run
+// against the local s3test server. It includes
+// selected tests from ClientTests;
+// when the s3test functionality is sufficient, it should
+// include all of them, and ClientTests can be simply embedded.
+type LocalServerSuite struct {
+ srv LocalServer
+ clientTests ClientTests
+}
+
+var (
+ // run tests twice, once in us-east-1 mode, once not.
+ _ = Suite(&LocalServerSuite{})
+ _ = Suite(&LocalServerSuite{
+ srv: LocalServer{
+ config: &s3test.Config{
+ Send409Conflict: true,
+ },
+ },
+ })
+)
+
+func (s *LocalServerSuite) SetUpSuite(c *C) {
+ s.srv.SetUp(c)
+ s.clientTests.s3 = s3.New(s.srv.auth, s.srv.region)
+
+ // TODO Sadly the fake server ignores auth completely right now. :-(
+ s.clientTests.authIsBroken = true
+ s.clientTests.Cleanup()
+}
+
+func (s *LocalServerSuite) TearDownTest(c *C) {
+ s.clientTests.Cleanup()
+}
+
+func (s *LocalServerSuite) TestBasicFunctionality(c *C) {
+ s.clientTests.TestBasicFunctionality(c)
+}
+
+func (s *LocalServerSuite) TestGetNotFound(c *C) {
+ s.clientTests.TestGetNotFound(c)
+}
+
+func (s *LocalServerSuite) TestBucketList(c *C) {
+ s.clientTests.TestBucketList(c)
+}
+
+func (s *LocalServerSuite) TestDoublePutBucket(c *C) {
+ s.clientTests.TestDoublePutBucket(c)
+}
diff --git a/vendor/src/launchpad.net/goamz/s3/s3test/server.go b/vendor/src/launchpad.net/goamz/s3/s3test/server.go
new file mode 100644
index 0000000000..eb1a2bbc1f
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/s3/s3test/server.go
@@ -0,0 +1,628 @@
+package s3test
+
+import (
+ "bytes"
+ "crypto/md5"
+ "encoding/hex"
+ "encoding/xml"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "launchpad.net/goamz/s3"
+ "log"
+ "net"
+ "net/http"
+ "net/url"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+)
+
+const debug = false
+
+type s3Error struct {
+ statusCode int
+ XMLName struct{} `xml:"Error"`
+ Code string
+ Message string
+ BucketName string
+ RequestId string
+ HostId string
+}
+
+type action struct {
+ srv *Server
+ w http.ResponseWriter
+ req *http.Request
+ reqId string
+}
+
+// Config controls the internal behaviour of the Server. A nil config is the default
+// and behaves as if all configurations assume their default behaviour. Once passed
+// to NewServer, the configuration must not be modified.
+type Config struct {
+ // Send409Conflict controls how the Server will respond to calls to PUT on a
+ // previously existing bucket. The default is false, and corresponds to the
+ // us-east-1 s3 enpoint. Setting this value to true emulates the behaviour of
+ // all other regions.
+ // http://docs.amazonwebservices.com/AmazonS3/latest/API/ErrorResponses.html
+ Send409Conflict bool
+}
+
+func (c *Config) send409Conflict() bool {
+ if c != nil {
+ return c.Send409Conflict
+ }
+ return false
+}
+
+// Server is a fake S3 server for testing purposes.
+// All of the data for the server is kept in memory.
+type Server struct {
+ url string
+ reqId int
+ listener net.Listener
+ mu sync.Mutex
+ buckets map[string]*bucket
+ config *Config
+}
+
+type bucket struct {
+ name string
+ acl s3.ACL
+ ctime time.Time
+ objects map[string]*object
+}
+
+type object struct {
+ name string
+ mtime time.Time
+ meta http.Header // metadata to return with requests.
+ checksum []byte // also held as Content-MD5 in meta.
+ data []byte
+}
+
+// A resource encapsulates the subject of an HTTP request.
+// The resource referred to may or may not exist
+// when the request is made.
+type resource interface {
+ put(a *action) interface{}
+ get(a *action) interface{}
+ post(a *action) interface{}
+ delete(a *action) interface{}
+}
+
+func NewServer(config *Config) (*Server, error) {
+ l, err := net.Listen("tcp", "localhost:0")
+ if err != nil {
+ return nil, fmt.Errorf("cannot listen on localhost: %v", err)
+ }
+ srv := &Server{
+ listener: l,
+ url: "http://" + l.Addr().String(),
+ buckets: make(map[string]*bucket),
+ config: config,
+ }
+ 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()
+}
+
+// URL returns a URL for the server.
+func (srv *Server) URL() string {
+ return srv.url
+}
+
+func fatalf(code int, codeStr string, errf string, a ...interface{}) {
+ panic(&s3Error{
+ statusCode: code,
+ Code: codeStr,
+ Message: fmt.Sprintf(errf, a...),
+ })
+}
+
+// serveHTTP serves the S3 protocol.
+func (srv *Server) serveHTTP(w http.ResponseWriter, req *http.Request) {
+ // ignore error from ParseForm as it's usually spurious.
+ req.ParseForm()
+
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ if debug {
+ log.Printf("s3test %q %q", req.Method, req.URL)
+ }
+ a := &action{
+ srv: srv,
+ w: w,
+ req: req,
+ reqId: fmt.Sprintf("%09X", srv.reqId),
+ }
+ srv.reqId++
+
+ var r resource
+ defer func() {
+ switch err := recover().(type) {
+ case *s3Error:
+ switch r := r.(type) {
+ case objectResource:
+ err.BucketName = r.bucket.name
+ case bucketResource:
+ err.BucketName = r.name
+ }
+ err.RequestId = a.reqId
+ // TODO HostId
+ w.Header().Set("Content-Type", `xml version="1.0" encoding="UTF-8"`)
+ w.WriteHeader(err.statusCode)
+ xmlMarshal(w, err)
+ case nil:
+ default:
+ panic(err)
+ }
+ }()
+
+ r = srv.resourceForURL(req.URL)
+
+ var resp interface{}
+ switch req.Method {
+ case "PUT":
+ resp = r.put(a)
+ case "GET", "HEAD":
+ resp = r.get(a)
+ case "DELETE":
+ resp = r.delete(a)
+ case "POST":
+ resp = r.post(a)
+ default:
+ fatalf(400, "MethodNotAllowed", "unknown http request method %q", req.Method)
+ }
+ if resp != nil && req.Method != "HEAD" {
+ xmlMarshal(w, resp)
+ }
+}
+
+// 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))
+ }
+}
+
+// In a fully implemented test server, each of these would have
+// its own resource type.
+var unimplementedBucketResourceNames = map[string]bool{
+ "acl": true,
+ "lifecycle": true,
+ "policy": true,
+ "location": true,
+ "logging": true,
+ "notification": true,
+ "versions": true,
+ "requestPayment": true,
+ "versioning": true,
+ "website": true,
+ "uploads": true,
+}
+
+var unimplementedObjectResourceNames = map[string]bool{
+ "uploadId": true,
+ "acl": true,
+ "torrent": true,
+ "uploads": true,
+}
+
+var pathRegexp = regexp.MustCompile("/(([^/]+)(/(.*))?)?")
+
+// resourceForURL returns a resource object for the given URL.
+func (srv *Server) resourceForURL(u *url.URL) (r resource) {
+ m := pathRegexp.FindStringSubmatch(u.Path)
+ if m == nil {
+ fatalf(404, "InvalidURI", "Couldn't parse the specified URI")
+ }
+ bucketName := m[2]
+ objectName := m[4]
+ if bucketName == "" {
+ return nullResource{} // root
+ }
+ b := bucketResource{
+ name: bucketName,
+ bucket: srv.buckets[bucketName],
+ }
+ q := u.Query()
+ if objectName == "" {
+ for name := range q {
+ if unimplementedBucketResourceNames[name] {
+ return nullResource{}
+ }
+ }
+ return b
+
+ }
+ if b.bucket == nil {
+ fatalf(404, "NoSuchBucket", "The specified bucket does not exist")
+ }
+ objr := objectResource{
+ name: objectName,
+ version: q.Get("versionId"),
+ bucket: b.bucket,
+ }
+ for name := range q {
+ if unimplementedObjectResourceNames[name] {
+ return nullResource{}
+ }
+ }
+ if obj := objr.bucket.objects[objr.name]; obj != nil {
+ objr.object = obj
+ }
+ return objr
+}
+
+// nullResource has error stubs for all resource methods.
+type nullResource struct{}
+
+func notAllowed() interface{} {
+ fatalf(400, "MethodNotAllowed", "The specified method is not allowed against this resource")
+ return nil
+}
+
+func (nullResource) put(a *action) interface{} { return notAllowed() }
+func (nullResource) get(a *action) interface{} { return notAllowed() }
+func (nullResource) post(a *action) interface{} { return notAllowed() }
+func (nullResource) delete(a *action) interface{} { return notAllowed() }
+
+const timeFormat = "2006-01-02T15:04:05.000Z07:00"
+
+type bucketResource struct {
+ name string
+ bucket *bucket // non-nil if the bucket already exists.
+}
+
+// GET on a bucket lists the objects in the bucket.
+// http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTBucketGET.html
+func (r bucketResource) get(a *action) interface{} {
+ if r.bucket == nil {
+ fatalf(404, "NoSuchBucket", "The specified bucket does not exist")
+ }
+ delimiter := a.req.Form.Get("delimiter")
+ marker := a.req.Form.Get("marker")
+ maxKeys := -1
+ if s := a.req.Form.Get("max-keys"); s != "" {
+ i, err := strconv.Atoi(s)
+ if err != nil || i < 0 {
+ fatalf(400, "invalid value for max-keys: %q", s)
+ }
+ maxKeys = i
+ }
+ prefix := a.req.Form.Get("prefix")
+ a.w.Header().Set("Content-Type", "application/xml")
+
+ if a.req.Method == "HEAD" {
+ return nil
+ }
+
+ var objs orderedObjects
+
+ // first get all matching objects and arrange them in alphabetical order.
+ for name, obj := range r.bucket.objects {
+ if strings.HasPrefix(name, prefix) {
+ objs = append(objs, obj)
+ }
+ }
+ sort.Sort(objs)
+
+ if maxKeys <= 0 {
+ maxKeys = 1000
+ }
+ resp := &s3.ListResp{
+ Name: r.bucket.name,
+ Prefix: prefix,
+ Delimiter: delimiter,
+ Marker: marker,
+ MaxKeys: maxKeys,
+ }
+
+ var prefixes []string
+ for _, obj := range objs {
+ if !strings.HasPrefix(obj.name, prefix) {
+ continue
+ }
+ name := obj.name
+ isPrefix := false
+ if delimiter != "" {
+ if i := strings.Index(obj.name[len(prefix):], delimiter); i >= 0 {
+ name = obj.name[:len(prefix)+i+len(delimiter)]
+ if prefixes != nil && prefixes[len(prefixes)-1] == name {
+ continue
+ }
+ isPrefix = true
+ }
+ }
+ if name <= marker {
+ continue
+ }
+ if len(resp.Contents)+len(prefixes) >= maxKeys {
+ resp.IsTruncated = true
+ break
+ }
+ if isPrefix {
+ prefixes = append(prefixes, name)
+ } else {
+ // Contents contains only keys not found in CommonPrefixes
+ resp.Contents = append(resp.Contents, obj.s3Key())
+ }
+ }
+ resp.CommonPrefixes = prefixes
+ return resp
+}
+
+// orderedObjects holds a slice of objects that can be sorted
+// by name.
+type orderedObjects []*object
+
+func (s orderedObjects) Len() int {
+ return len(s)
+}
+func (s orderedObjects) Swap(i, j int) {
+ s[i], s[j] = s[j], s[i]
+}
+func (s orderedObjects) Less(i, j int) bool {
+ return s[i].name < s[j].name
+}
+
+func (obj *object) s3Key() s3.Key {
+ return s3.Key{
+ Key: obj.name,
+ LastModified: obj.mtime.Format(timeFormat),
+ Size: int64(len(obj.data)),
+ ETag: fmt.Sprintf(`"%x"`, obj.checksum),
+ // TODO StorageClass
+ // TODO Owner
+ }
+}
+
+// DELETE on a bucket deletes the bucket if it's not empty.
+func (r bucketResource) delete(a *action) interface{} {
+ b := r.bucket
+ if b == nil {
+ fatalf(404, "NoSuchBucket", "The specified bucket does not exist")
+ }
+ if len(b.objects) > 0 {
+ fatalf(400, "BucketNotEmpty", "The bucket you tried to delete is not empty")
+ }
+ delete(a.srv.buckets, b.name)
+ return nil
+}
+
+// PUT on a bucket creates the bucket.
+// http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTBucketPUT.html
+func (r bucketResource) put(a *action) interface{} {
+ var created bool
+ if r.bucket == nil {
+ if !validBucketName(r.name) {
+ fatalf(400, "InvalidBucketName", "The specified bucket is not valid")
+ }
+ if loc := locationConstraint(a); loc == "" {
+ fatalf(400, "InvalidRequets", "The unspecified location constraint is incompatible for the region specific endpoint this request was sent to.")
+ }
+ // TODO validate acl
+ r.bucket = &bucket{
+ name: r.name,
+ // TODO default acl
+ objects: make(map[string]*object),
+ }
+ a.srv.buckets[r.name] = r.bucket
+ created = true
+ }
+ if !created && a.srv.config.send409Conflict() {
+ fatalf(409, "BucketAlreadyOwnedByYou", "Your previous request to create the named bucket succeeded and you already own it.")
+ }
+ r.bucket.acl = s3.ACL(a.req.Header.Get("x-amz-acl"))
+ return nil
+}
+
+func (bucketResource) post(a *action) interface{} {
+ fatalf(400, "Method", "bucket POST method not available")
+ return nil
+}
+
+// validBucketName returns whether name is a valid bucket name.
+// Here are the rules, from:
+// http://docs.amazonwebservices.com/AmazonS3/2006-03-01/dev/BucketRestrictions.html
+//
+// Can contain lowercase letters, numbers, periods (.), underscores (_),
+// and dashes (-). You can use uppercase letters for buckets only in the
+// US Standard region.
+//
+// Must start with a number or letter
+//
+// Must be between 3 and 255 characters long
+//
+// There's one extra rule (Must not be formatted as an IP address (e.g., 192.168.5.4)
+// but the real S3 server does not seem to check that rule, so we will not
+// check it either.
+//
+func validBucketName(name string) bool {
+ if len(name) < 3 || len(name) > 255 {
+ return false
+ }
+ r := name[0]
+ if !(r >= '0' && r <= '9' || r >= 'a' && r <= 'z') {
+ return false
+ }
+ for _, r := range name {
+ switch {
+ case r >= '0' && r <= '9':
+ case r >= 'a' && r <= 'z':
+ case r == '_' || r == '-':
+ case r == '.':
+ default:
+ return false
+ }
+ }
+ return true
+}
+
+var responseParams = map[string]bool{
+ "content-type": true,
+ "content-language": true,
+ "expires": true,
+ "cache-control": true,
+ "content-disposition": true,
+ "content-encoding": true,
+}
+
+type objectResource struct {
+ name string
+ version string
+ bucket *bucket // always non-nil.
+ object *object // may be nil.
+}
+
+// GET on an object gets the contents of the object.
+// http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTObjectGET.html
+func (objr objectResource) get(a *action) interface{} {
+ obj := objr.object
+ if obj == nil {
+ fatalf(404, "NoSuchKey", "The specified key does not exist.")
+ }
+ h := a.w.Header()
+ // add metadata
+ for name, d := range obj.meta {
+ h[name] = d
+ }
+ // override header values in response to request parameters.
+ for name, vals := range a.req.Form {
+ if strings.HasPrefix(name, "response-") {
+ name = name[len("response-"):]
+ if !responseParams[name] {
+ continue
+ }
+ h.Set(name, vals[0])
+ }
+ }
+ if r := a.req.Header.Get("Range"); r != "" {
+ fatalf(400, "NotImplemented", "range unimplemented")
+ }
+ // TODO Last-Modified-Since
+ // TODO If-Modified-Since
+ // TODO If-Unmodified-Since
+ // TODO If-Match
+ // TODO If-None-Match
+ // TODO Connection: close ??
+ // TODO x-amz-request-id
+ h.Set("Content-Length", fmt.Sprint(len(obj.data)))
+ h.Set("ETag", hex.EncodeToString(obj.checksum))
+ h.Set("Last-Modified", obj.mtime.Format(time.RFC1123))
+ if a.req.Method == "HEAD" {
+ return nil
+ }
+ // TODO avoid holding the lock when writing data.
+ _, err := a.w.Write(obj.data)
+ if err != nil {
+ // we can't do much except just log the fact.
+ log.Printf("error writing data: %v", err)
+ }
+ return nil
+}
+
+var metaHeaders = map[string]bool{
+ "Content-MD5": true,
+ "x-amz-acl": true,
+ "Content-Type": true,
+ "Content-Encoding": true,
+ "Content-Disposition": true,
+}
+
+// PUT on an object creates the object.
+func (objr objectResource) put(a *action) interface{} {
+ // TODO Cache-Control header
+ // TODO Expires header
+ // TODO x-amz-server-side-encryption
+ // TODO x-amz-storage-class
+
+ // TODO is this correct, or should we erase all previous metadata?
+ obj := objr.object
+ if obj == nil {
+ obj = &object{
+ name: objr.name,
+ meta: make(http.Header),
+ }
+ }
+
+ var expectHash []byte
+ if c := a.req.Header.Get("Content-MD5"); c != "" {
+ var err error
+ expectHash, err = hex.DecodeString(c)
+ if err != nil || len(expectHash) != md5.Size {
+ fatalf(400, "InvalidDigest", "The Content-MD5 you specified was invalid")
+ }
+ }
+ sum := md5.New()
+ // TODO avoid holding lock while reading data.
+ data, err := ioutil.ReadAll(io.TeeReader(a.req.Body, sum))
+ if err != nil {
+ fatalf(400, "TODO", "read error")
+ }
+ gotHash := sum.Sum(nil)
+ if expectHash != nil && bytes.Compare(gotHash, expectHash) != 0 {
+ fatalf(400, "BadDigest", "The Content-MD5 you specified did not match what we received")
+ }
+ if a.req.ContentLength >= 0 && int64(len(data)) != a.req.ContentLength {
+ fatalf(400, "IncompleteBody", "You did not provide the number of bytes specified by the Content-Length HTTP header")
+ }
+
+ // PUT request has been successful - save data and metadata
+ for key, values := range a.req.Header {
+ key = http.CanonicalHeaderKey(key)
+ if metaHeaders[key] || strings.HasPrefix(key, "X-Amz-Meta-") {
+ obj.meta[key] = values
+ }
+ }
+ obj.data = data
+ obj.checksum = gotHash
+ obj.mtime = time.Now()
+ objr.bucket.objects[objr.name] = obj
+ return nil
+}
+
+func (objr objectResource) delete(a *action) interface{} {
+ delete(objr.bucket.objects, objr.name)
+ return nil
+}
+
+func (objr objectResource) post(a *action) interface{} {
+ fatalf(400, "MethodNotAllowed", "The specified method is not allowed against this resource")
+ return nil
+}
+
+type CreateBucketConfiguration struct {
+ LocationConstraint string
+}
+
+// locationConstraint parses the request body (if present).
+// If there is no body, an empty string will be returned.
+func locationConstraint(a *action) string {
+ var body bytes.Buffer
+ if _, err := io.Copy(&body, a.req.Body); err != nil {
+ fatalf(400, "InvalidRequest", err.Error())
+ }
+ if body.Len() == 0 {
+ return ""
+ }
+ var loc CreateBucketConfiguration
+ if err := xml.NewDecoder(&body).Decode(&loc); err != nil {
+ fatalf(400, "InvalidRequest", err.Error())
+ }
+ return loc.LocationConstraint
+}
diff --git a/vendor/src/launchpad.net/goamz/s3/sign.go b/vendor/src/launchpad.net/goamz/s3/sign.go
new file mode 100644
index 0000000000..d6de27abae
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/s3/sign.go
@@ -0,0 +1,112 @@
+package s3
+
+import (
+ "crypto/hmac"
+ "crypto/sha1"
+ "encoding/base64"
+ "launchpad.net/goamz/aws"
+ "log"
+ "sort"
+ "strings"
+)
+
+var b64 = base64.StdEncoding
+
+// ----------------------------------------------------------------------------
+// S3 signing (http://goo.gl/G1LrK)
+
+var s3ParamsToSign = map[string]bool{
+ "acl": true,
+ "location": true,
+ "logging": true,
+ "notification": true,
+ "partNumber": true,
+ "policy": true,
+ "requestPayment": true,
+ "torrent": true,
+ "uploadId": true,
+ "uploads": true,
+ "versionId": true,
+ "versioning": true,
+ "versions": true,
+ "response-content-type": true,
+ "response-content-language": true,
+ "response-expires": true,
+ "response-cache-control": true,
+ "response-content-disposition": true,
+ "response-content-encoding": true,
+}
+
+func sign(auth aws.Auth, method, canonicalPath string, params, headers map[string][]string) {
+ var md5, ctype, date, xamz string
+ var xamzDate bool
+ var sarray []string
+ for k, v := range headers {
+ k = strings.ToLower(k)
+ switch k {
+ case "content-md5":
+ md5 = v[0]
+ case "content-type":
+ ctype = v[0]
+ case "date":
+ if !xamzDate {
+ date = v[0]
+ }
+ default:
+ if strings.HasPrefix(k, "x-amz-") {
+ vall := strings.Join(v, ",")
+ sarray = append(sarray, k+":"+vall)
+ if k == "x-amz-date" {
+ xamzDate = true
+ date = ""
+ }
+ }
+ }
+ }
+ if len(sarray) > 0 {
+ sort.StringSlice(sarray).Sort()
+ xamz = strings.Join(sarray, "\n") + "\n"
+ }
+
+ expires := false
+ if v, ok := params["Expires"]; ok {
+ // Query string request authentication alternative.
+ expires = true
+ date = v[0]
+ params["AWSAccessKeyId"] = []string{auth.AccessKey}
+ }
+
+ sarray = sarray[0:0]
+ for k, v := range params {
+ if s3ParamsToSign[k] {
+ for _, vi := range v {
+ if vi == "" {
+ sarray = append(sarray, k)
+ } else {
+ // "When signing you do not encode these values."
+ sarray = append(sarray, k+"="+vi)
+ }
+ }
+ }
+ }
+ if len(sarray) > 0 {
+ sort.StringSlice(sarray).Sort()
+ canonicalPath = canonicalPath + "?" + strings.Join(sarray, "&")
+ }
+
+ payload := method + "\n" + md5 + "\n" + ctype + "\n" + date + "\n" + xamz + canonicalPath
+ hash := hmac.New(sha1.New, []byte(auth.SecretKey))
+ hash.Write([]byte(payload))
+ signature := make([]byte, b64.EncodedLen(hash.Size()))
+ b64.Encode(signature, hash.Sum(nil))
+
+ if expires {
+ params["Signature"] = []string{string(signature)}
+ } else {
+ headers["Authorization"] = []string{"AWS " + auth.AccessKey + ":" + string(signature)}
+ }
+ if debug {
+ log.Printf("Signature payload: %q", payload)
+ log.Printf("Signature: %q", signature)
+ }
+}
diff --git a/vendor/src/launchpad.net/goamz/s3/sign_test.go b/vendor/src/launchpad.net/goamz/s3/sign_test.go
new file mode 100644
index 0000000000..b856f9b3bf
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/s3/sign_test.go
@@ -0,0 +1,132 @@
+package s3_test
+
+import (
+ "launchpad.net/goamz/aws"
+ "launchpad.net/goamz/s3"
+ . "launchpad.net/gocheck"
+)
+
+// S3 ReST authentication docs: http://goo.gl/G1LrK
+
+var testAuth = aws.Auth{"0PN5J17HBGZHT7JJ3X82", "uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o"}
+
+func (s *S) TestSignExampleObjectGet(c *C) {
+ method := "GET"
+ path := "/johnsmith/photos/puppy.jpg"
+ headers := map[string][]string{
+ "Host": {"johnsmith.s3.amazonaws.com"},
+ "Date": {"Tue, 27 Mar 2007 19:36:42 +0000"},
+ }
+ s3.Sign(testAuth, method, path, nil, headers)
+ expected := "AWS 0PN5J17HBGZHT7JJ3X82:xXjDGYUmKxnwqr5KXNPGldn5LbA="
+ c.Assert(headers["Authorization"], DeepEquals, []string{expected})
+}
+
+func (s *S) TestSignExampleObjectPut(c *C) {
+ method := "PUT"
+ path := "/johnsmith/photos/puppy.jpg"
+ headers := map[string][]string{
+ "Host": {"johnsmith.s3.amazonaws.com"},
+ "Date": {"Tue, 27 Mar 2007 21:15:45 +0000"},
+ "Content-Type": {"image/jpeg"},
+ "Content-Length": {"94328"},
+ }
+ s3.Sign(testAuth, method, path, nil, headers)
+ expected := "AWS 0PN5J17HBGZHT7JJ3X82:hcicpDDvL9SsO6AkvxqmIWkmOuQ="
+ c.Assert(headers["Authorization"], DeepEquals, []string{expected})
+}
+
+func (s *S) TestSignExampleList(c *C) {
+ method := "GET"
+ path := "/johnsmith/"
+ params := map[string][]string{
+ "prefix": {"photos"},
+ "max-keys": {"50"},
+ "marker": {"puppy"},
+ }
+ headers := map[string][]string{
+ "Host": {"johnsmith.s3.amazonaws.com"},
+ "Date": {"Tue, 27 Mar 2007 19:42:41 +0000"},
+ "User-Agent": {"Mozilla/5.0"},
+ }
+ s3.Sign(testAuth, method, path, params, headers)
+ expected := "AWS 0PN5J17HBGZHT7JJ3X82:jsRt/rhG+Vtp88HrYL706QhE4w4="
+ c.Assert(headers["Authorization"], DeepEquals, []string{expected})
+}
+
+func (s *S) TestSignExampleFetch(c *C) {
+ method := "GET"
+ path := "/johnsmith/"
+ params := map[string][]string{
+ "acl": {""},
+ }
+ headers := map[string][]string{
+ "Host": {"johnsmith.s3.amazonaws.com"},
+ "Date": {"Tue, 27 Mar 2007 19:44:46 +0000"},
+ }
+ s3.Sign(testAuth, method, path, params, headers)
+ expected := "AWS 0PN5J17HBGZHT7JJ3X82:thdUi9VAkzhkniLj96JIrOPGi0g="
+ c.Assert(headers["Authorization"], DeepEquals, []string{expected})
+}
+
+func (s *S) TestSignExampleDelete(c *C) {
+ method := "DELETE"
+ path := "/johnsmith/photos/puppy.jpg"
+ params := map[string][]string{}
+ headers := map[string][]string{
+ "Host": {"s3.amazonaws.com"},
+ "Date": {"Tue, 27 Mar 2007 21:20:27 +0000"},
+ "User-Agent": {"dotnet"},
+ "x-amz-date": {"Tue, 27 Mar 2007 21:20:26 +0000"},
+ }
+ s3.Sign(testAuth, method, path, params, headers)
+ expected := "AWS 0PN5J17HBGZHT7JJ3X82:k3nL7gH3+PadhTEVn5Ip83xlYzk="
+ c.Assert(headers["Authorization"], DeepEquals, []string{expected})
+}
+
+func (s *S) TestSignExampleUpload(c *C) {
+ method := "PUT"
+ path := "/static.johnsmith.net/db-backup.dat.gz"
+ params := map[string][]string{}
+ headers := map[string][]string{
+ "Host": {"static.johnsmith.net:8080"},
+ "Date": {"Tue, 27 Mar 2007 21:06:08 +0000"},
+ "User-Agent": {"curl/7.15.5"},
+ "x-amz-acl": {"public-read"},
+ "content-type": {"application/x-download"},
+ "Content-MD5": {"4gJE4saaMU4BqNR0kLY+lw=="},
+ "X-Amz-Meta-ReviewedBy": {"joe@johnsmith.net,jane@johnsmith.net"},
+ "X-Amz-Meta-FileChecksum": {"0x02661779"},
+ "X-Amz-Meta-ChecksumAlgorithm": {"crc32"},
+ "Content-Disposition": {"attachment; filename=database.dat"},
+ "Content-Encoding": {"gzip"},
+ "Content-Length": {"5913339"},
+ }
+ s3.Sign(testAuth, method, path, params, headers)
+ expected := "AWS 0PN5J17HBGZHT7JJ3X82:C0FlOtU8Ylb9KDTpZqYkZPX91iI="
+ c.Assert(headers["Authorization"], DeepEquals, []string{expected})
+}
+
+func (s *S) TestSignExampleListAllMyBuckets(c *C) {
+ method := "GET"
+ path := "/"
+ headers := map[string][]string{
+ "Host": {"s3.amazonaws.com"},
+ "Date": {"Wed, 28 Mar 2007 01:29:59 +0000"},
+ }
+ s3.Sign(testAuth, method, path, nil, headers)
+ expected := "AWS 0PN5J17HBGZHT7JJ3X82:Db+gepJSUbZKwpx1FR0DLtEYoZA="
+ c.Assert(headers["Authorization"], DeepEquals, []string{expected})
+}
+
+func (s *S) TestSignExampleUnicodeKeys(c *C) {
+ method := "GET"
+ path := "/dictionary/fran%C3%A7ais/pr%c3%a9f%c3%a8re"
+ headers := map[string][]string{
+ "Host": {"s3.amazonaws.com"},
+ "Date": {"Wed, 28 Mar 2007 01:49:49 +0000"},
+ }
+ s3.Sign(testAuth, method, path, nil, headers)
+ expected := "AWS 0PN5J17HBGZHT7JJ3X82:dxhSBHoI6eVSPcXJqEghlUzZMnY="
+ c.Assert(headers["Authorization"], DeepEquals, []string{expected})
+}
diff --git a/vendor/src/launchpad.net/goamz/testutil/http.go b/vendor/src/launchpad.net/goamz/testutil/http.go
new file mode 100644
index 0000000000..733edd49d7
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/testutil/http.go
@@ -0,0 +1,186 @@
+package testutil
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "net/http"
+ "net/url"
+ "os"
+ "time"
+)
+
+type HTTPServer struct {
+ URL string
+ Timeout time.Duration
+ started bool
+ listener net.Listener
+ request chan *http.Request
+ response chan ResponseFunc
+}
+
+type Response struct {
+ Status int
+ Headers map[string]string
+ Body string
+}
+
+func NewHTTPServer() *HTTPServer {
+ return &HTTPServer{URL: "http://localhost:4444", Timeout: 5 * time.Second}
+}
+
+type ResponseFunc func(path string) Response
+
+func (s *HTTPServer) Start() {
+ if s.started {
+ return
+ }
+ s.started = true
+ s.request = make(chan *http.Request, 1024)
+ s.response = make(chan ResponseFunc, 1024)
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ panic(err)
+ }
+ s.listener, err = net.Listen("tcp", u.Host)
+ if err != nil {
+ panic(err)
+ }
+ go http.Serve(s.listener, s)
+
+ s.Response(203, nil, "")
+ for {
+ // Wait for it to be up.
+ resp, err := http.Get(s.URL)
+ if err == nil && resp.StatusCode == 203 {
+ break
+ }
+ time.Sleep(1e8)
+ }
+ s.WaitRequest() // Consume dummy request.
+}
+
+func (s *HTTPServer) Stop() {
+ if s.listener == nil {
+ return
+ }
+ err := s.listener.Close()
+ if err != nil {
+ panic(err)
+ }
+ s.listener = nil
+}
+
+// Flush discards all pending requests and responses.
+func (s *HTTPServer) Flush() {
+ for {
+ select {
+ case <-s.request:
+ case <-s.response:
+ default:
+ return
+ }
+ }
+}
+
+func body(req *http.Request) string {
+ data, err := ioutil.ReadAll(req.Body)
+ if err != nil {
+ panic(err)
+ }
+ return string(data)
+}
+
+func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+ req.ParseMultipartForm(1e6)
+ data, err := ioutil.ReadAll(req.Body)
+ if err != nil {
+ panic(err)
+ }
+ req.Body = ioutil.NopCloser(bytes.NewBuffer(data))
+ s.request <- req
+ var resp Response
+ select {
+ case respFunc := <-s.response:
+ resp = respFunc(req.URL.Path)
+ case <-time.After(s.Timeout):
+ const msg = "ERROR: Timeout waiting for test to prepare a response\n"
+ fmt.Fprintf(os.Stderr, msg)
+ resp = Response{500, nil, msg}
+ }
+ if resp.Headers != nil {
+ h := w.Header()
+ for k, v := range resp.Headers {
+ h.Set(k, v)
+ }
+ }
+ if resp.Status != 0 {
+ w.WriteHeader(resp.Status)
+ }
+ w.Write([]byte(resp.Body))
+}
+
+// WaitRequests returns the next n requests made to the http server from
+// the queue. If not enough requests were previously made, it waits until
+// the timeout value for them to be made.
+func (s *HTTPServer) WaitRequests(n int) []*http.Request {
+ reqs := make([]*http.Request, 0, n)
+ for i := 0; i < n; i++ {
+ select {
+ case req := <-s.request:
+ reqs = append(reqs, req)
+ case <-time.After(s.Timeout):
+ panic("Timeout waiting for request")
+ }
+ }
+ return reqs
+}
+
+// WaitRequest returns the next request made to the http server from
+// the queue. If no requests were previously made, it waits until the
+// timeout value for one to be made.
+func (s *HTTPServer) WaitRequest() *http.Request {
+ return s.WaitRequests(1)[0]
+}
+
+// ResponseFunc prepares the test server to respond the following n
+// requests using f to build each response.
+func (s *HTTPServer) ResponseFunc(n int, f ResponseFunc) {
+ for i := 0; i < n; i++ {
+ s.response <- f
+ }
+}
+
+// ResponseMap maps request paths to responses.
+type ResponseMap map[string]Response
+
+// ResponseMap prepares the test server to respond the following n
+// requests using the m to obtain the responses.
+func (s *HTTPServer) ResponseMap(n int, m ResponseMap) {
+ f := func(path string) Response {
+ for rpath, resp := range m {
+ if rpath == path {
+ return resp
+ }
+ }
+ body := "Path not found in response map: " + path
+ return Response{Status: 500, Body: body}
+ }
+ s.ResponseFunc(n, f)
+}
+
+// Responses prepares the test server to respond the following n requests
+// using the provided response parameters.
+func (s *HTTPServer) Responses(n int, status int, headers map[string]string, body string) {
+ f := func(path string) Response {
+ return Response{status, headers, body}
+ }
+ s.ResponseFunc(n, f)
+}
+
+// Response prepares the test server to respond the following request
+// using the provided response parameters.
+func (s *HTTPServer) Response(status int, headers map[string]string, body string) {
+ s.Responses(1, status, headers, body)
+}
diff --git a/vendor/src/launchpad.net/goamz/testutil/suite.go b/vendor/src/launchpad.net/goamz/testutil/suite.go
new file mode 100644
index 0000000000..75b7c83563
--- /dev/null
+++ b/vendor/src/launchpad.net/goamz/testutil/suite.go
@@ -0,0 +1,30 @@
+package testutil
+
+import (
+ "flag"
+ "launchpad.net/goamz/aws"
+ . "launchpad.net/gocheck"
+)
+
+// Amazon must be used by all tested packages to determine whether to
+// run functional tests against the real AWS servers.
+var Amazon bool
+
+func init() {
+ flag.BoolVar(&Amazon, "amazon", false, "Enable tests against amazon server")
+}
+
+type LiveSuite struct {
+ auth aws.Auth
+}
+
+func (s *LiveSuite) SetUpSuite(c *C) {
+ if !Amazon {
+ c.Skip("amazon tests not enabled (-amazon flag)")
+ }
+ auth, err := aws.EnvAuth()
+ if err != nil {
+ c.Fatal(err.Error())
+ }
+ s.auth = auth
+}