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 + 60% + 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 + 30% + 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 +}