1
0
mirror of https://github.com/docker/docs.git synced 2026-11-05 00:14:17 +03:00

Vendor goamz to avoid bzr build dependency

Signed-off-by: Solomon Hykes <solomon@docker.com>
This commit is contained in:
Solomon Hykes
2014-07-01 01:10:25 -07:00
parent 0d768de24b
commit d26dea3b76
62 changed files with 16042 additions and 0 deletions

1
vendor/src/launchpad.net/goamz/.lbox vendored Normal file
View File

@@ -0,0 +1 @@
propose -for=lp:goamz -cr

185
vendor/src/launchpad.net/goamz/LICENSE vendored Normal file
View File

@@ -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. <http://fsf.org/>
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.

View File

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

View File

@@ -0,0 +1,57 @@
package aws_test
import (
"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)
}

View File

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

View File

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

1087
vendor/src/launchpad.net/goamz/ec2/ec2.go vendored Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,212 @@
//
// goamz - Go packages to interact with the Amazon Web Services.
//
// https://wiki.ubuntu.com/goamz
//
// Copyright (c) 2014 Canonical Ltd.
//
package ec2
import (
"fmt"
"strconv"
)
// NetworkInterfaceAttachment describes a network interface
// attachment.
//
// See http://goo.gl/KtiKuV for more details.
type NetworkInterfaceAttachment struct {
Id string `xml:"attachmentId"`
InstanceId string `xml:"instanceId"`
InstanceOwnerId string `xml:"instanceOwnerId"`
DeviceIndex int `xml:"deviceIndex"`
Status string `xml:"status"`
AttachTime string `xml:"attachTime"`
DeleteOnTermination bool `xml:"deleteOnTermination"`
}
// PrivateIP describes a private IP address of a network interface.
//
// See http://goo.gl/jtuQEJ for more details.
type PrivateIP struct {
Address string `xml:"privateIpAddress"`
DNSName string `xml:"privateDnsName"`
IsPrimary bool `xml:"primary"`
}
// NetworkInterface describes a network interface for VPC.
//
// See http://goo.gl/G63OQL for more details.
type NetworkInterface struct {
Id string `xml:"networkInterfaceId"`
SubnetId string `xml:"subnetId"`
VPCId string `xml:"vpcId"`
AvailZone string `xml:"availabilityZone"`
Description string `xml:"description"`
OwnerId string `xml:"ownerId"`
RequesterId string `xml:"requesterId"`
RequesterManaged bool `xml:"requesterManaged"`
Status string `xml:"status"`
MACAddress string `xml:"macAddress"`
PrivateIPAddress string `xml:"privateIpAddress"`
PrivateDNSName string `xml:"privateDnsName"`
SourceDestCheck bool `xml:"sourceDestCheck"`
Groups []SecurityGroup `xml:"groupSet>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
}

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 <graham.miller@gmail.com>
// 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
}

View File

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

View File

@@ -0,0 +1,9 @@
package mturk_test
var BasicHitResponse = `<?xml version="1.0"?>
<CreateHITResponse><OperationRequest><RequestId>643b794b-66b6-4427-bb8a-4d3df5c9a20e</RequestId></OperationRequest><HIT><Request><IsValid>True</IsValid></Request><HITId>28J4IXKO2L927XKJTHO34OCDNASCDW</HITId><HITTypeId>2XZ7D1X3V0FKQVW7LU51S7PKKGFKDF</HITTypeId></HIT></CreateHITResponse>
`
var SearchHITResponse = `<?xml version="1.0"?>
<SearchHITsResponse><OperationRequest><RequestId>38862d9c-f015-4177-a2d3-924110a9d6f2</RequestId></OperationRequest><SearchHITsResult><Request><IsValid>True</IsValid></Request><NumResults>1</NumResults><TotalNumResults>1</TotalNumResults><PageNumber>1</PageNumber><HIT><HITId>2BU26DG67D1XTE823B3OQ2JF2XWF83</HITId><HITTypeId>22OWJ5OPB0YV6IGL5727KP9U38P5XR</HITTypeId><CreationTime>2011-12-28T19:56:20Z</CreationTime><Title>test hit</Title><Description>please disregard, testing only</Description><HITStatus>Reviewable</HITStatus><MaxAssignments>1</MaxAssignments><Reward><Amount>0.01</Amount><CurrencyCode>USD</CurrencyCode><FormattedPrice>$0.01</FormattedPrice></Reward><AutoApprovalDelayInSeconds>2592000</AutoApprovalDelayInSeconds><Expiration>2011-12-28T19:56:50Z</Expiration><AssignmentDurationInSeconds>30</AssignmentDurationInSeconds><NumberOfAssignmentsPending>0</NumberOfAssignmentsPending><NumberOfAssignmentsAvailable>1</NumberOfAssignmentsAvailable><NumberOfAssignmentsCompleted>0</NumberOfAssignmentsCompleted></HIT></SearchHITsResult></SearchHITsResponse>
`

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,120 @@
package sdb_test
var TestCreateDomainXmlOK = `
<?xml version="1.0"?>
<CreateDomainResponse xmlns="http://sdb.amazonaws.com/doc/2009-04-15/">
<ResponseMetadata>
<RequestId>63264005-7a5f-e01a-a224-395c63b89f6d</RequestId>
<BoxUsage>0.0055590279</BoxUsage>
</ResponseMetadata>
</CreateDomainResponse>
`
var TestListDomainsXmlOK = `
<?xml version="1.0"?>
<ListDomainsResponse xmlns="http://sdb.amazonaws.com/doc/2009-04-15/">
<ListDomainsResult>
<DomainName>Account</DomainName>
<DomainName>Domain</DomainName>
<DomainName>Record</DomainName>
</ListDomainsResult>
<ResponseMetadata>
<RequestId>15fcaf55-9914-63c2-21f3-951e31193790</RequestId>
<BoxUsage>0.0000071759</BoxUsage>
</ResponseMetadata>
</ListDomainsResponse>
`
var TestListDomainsWithNextTokenXmlOK = `
<?xml version="1.0"?>
<ListDomainsResponse xmlns="http://sdb.amazonaws.com/doc/2009-04-15/">
<ListDomainsResult>
<DomainName>Domain1-200706011651</DomainName>
<DomainName>Domain2-200706011652</DomainName>
<NextToken>TWV0ZXJpbmdUZXN0RG9tYWluMS0yMDA3MDYwMTE2NTY=</NextToken>
</ListDomainsResult>
<ResponseMetadata>
<RequestId>eb13162f-1b95-4511-8b12-489b86acfd28</RequestId>
<BoxUsage>0.0000219907</BoxUsage>
</ResponseMetadata>
</ListDomainsResponse>
`
var TestDeleteDomainXmlOK = `
<?xml version="1.0"?>
<DeleteDomainResponse xmlns="http://sdb.amazonaws.com/doc/2009-04-15/">
<ResponseMetadata>
<RequestId>039e1e25-9a64-2a74-93da-2fda36122a97</RequestId>
<BoxUsage>0.0055590278</BoxUsage>
</ResponseMetadata>
</DeleteDomainResponse>
`
var TestDomainMetadataXmlNoSuchDomain = `
<?xml version="1.0"?>
<Response xmlns="http://sdb.amazonaws.com/doc/2009-04-15/">
<Errors>
<Error>
<Code>NoSuchDomain</Code>
<Message>The specified domain does not exist.</Message>
<BoxUsage>0.0000071759</BoxUsage>
</Error>
</Errors>
<RequestID>e050cea2-a772-f90e-2cb0-98ebd42c2898</RequestID>
</Response>
`
var TestPutAttrsXmlOK = `
<?xml version="1.0"?>
<PutAttributesResponse xmlns="http://sdb.amazonaws.com/doc/2009-04-15/">
<ResponseMetadata>
<RequestId>490206ce-8292-456c-a00f-61b335eb202b</RequestId>
<BoxUsage>0.0000219907</BoxUsage>
</ResponseMetadata>
</PutAttributesResponse>
`
var TestAttrsXmlOK = `
<?xml version="1.0"?>
<GetAttributesResponse xmlns="http://sdb.amazonaws.com/doc/2009-04-15/">
<GetAttributesResult>
<Attribute><Name>Color</Name><Value>Blue</Value></Attribute>
<Attribute><Name>Size</Name><Value>Med</Value></Attribute>
</GetAttributesResult>
<ResponseMetadata>
<RequestId>b1e8f1f7-42e9-494c-ad09-2674e557526d</RequestId>
<BoxUsage>0.0000219942</BoxUsage>
</ResponseMetadata>
</GetAttributesResponse>
`
var TestSelectXmlOK = `
<?xml version="1.0"?>
<SelectResponse xmlns="http://sdb.amazonaws.com/doc/2009-04-15/">
<SelectResult>
<Item>
<Name>Item_03</Name>
<Attribute><Name>Category</Name><Value>Clothes</Value></Attribute>
<Attribute><Name>Subcategory</Name><Value>Pants</Value></Attribute>
<Attribute><Name>Name</Name><Value>Sweatpants</Value></Attribute>
<Attribute><Name>Color</Name><Value>Blue</Value></Attribute>
<Attribute><Name>Color</Name><Value>Yellow</Value></Attribute>
<Attribute><Name>Color</Name><Value>Pink</Value></Attribute>
<Attribute><Name>Size</Name><Value>Large</Value></Attribute>
</Item>
<Item>
<Name>Item_06</Name>
<Attribute><Name>Category</Name><Value>Motorcycle Parts</Value></Attribute>
<Attribute><Name>Subcategory</Name><Value>Bodywork</Value></Attribute>
<Attribute><Name>Name</Name><Value>Fender Eliminator</Value></Attribute>
<Attribute><Name>Color</Name><Value>Blue</Value></Attribute>
<Attribute><Name>Make</Name><Value>Yamaha</Value></Attribute>
<Attribute><Name>Model</Name><Value>R1</Value></Attribute>
</Item>
</SelectResult>
<ResponseMetadata>
<RequestId>b1e8f1f7-42e9-494c-ad09-2674e557526d</RequestId>
<BoxUsage>0.0000219907</BoxUsage>
</ResponseMetadata>
</SelectResponse>
`

View File

@@ -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 <chilts@appsattic.com>
// Brad Rydzewski <brad.rydzewski@gmail.com>
// 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
}

View File

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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1 @@
Amazon Simple Notification Service API for Golang.

View File

@@ -0,0 +1,164 @@
package sns_test
var TestListTopicsXmlOK = `
<?xml version="1.0"?>
<ListTopicsResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<ListTopicsResult>
<Topics>
<member>
<TopicArn>arn:aws:sns:us-west-1:331995417492:Transcoding</TopicArn>
</member>
</Topics>
</ListTopicsResult>
<ResponseMetadata>
<RequestId>bd10b26c-e30e-11e0-ba29-93c3aca2f103</RequestId>
</ResponseMetadata>
</ListTopicsResponse>
`
var TestCreateTopicXmlOK = `
<?xml version="1.0"?>
<CreateTopicResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<CreateTopicResult>
<TopicArn>arn:aws:sns:us-east-1:123456789012:My-Topic</TopicArn>
</CreateTopicResult>
<ResponseMetadata>
<RequestId>a8dec8b3-33a4-11df-8963-01868b7c937a</RequestId>
</ResponseMetadata>
</CreateTopicResponse>
`
var TestDeleteTopicXmlOK = `
<DeleteTopicResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<ResponseMetadata>
<RequestId>f3aa9ac9-3c3d-11df-8235-9dab105e9c32</RequestId>
</ResponseMetadata>
</DeleteTopicResponse>
`
var TestListSubscriptionsXmlOK = `
<ListSubscriptionsResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<ListSubscriptionsResult>
<Subscriptions>
<member>
<TopicArn>arn:aws:sns:us-east-1:698519295917:My-Topic</TopicArn>
<Protocol>email</Protocol>
<SubscriptionArn>arn:aws:sns:us-east-1:123456789012:My-Topic:80289ba6-0fd4-4079-afb4-ce8c8260f0ca</SubscriptionArn>
<Owner>123456789012</Owner>
<Endpoint>example@amazon.com</Endpoint>
</member>
</Subscriptions>
</ListSubscriptionsResult>
<ResponseMetadata>
<RequestId>384ac68d-3775-11df-8963-01868b7c937a</RequestId>
</ResponseMetadata>
</ListSubscriptionsResponse>
`
var TestGetTopicAttributesXmlOK = `
<GetTopicAttributesResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<GetTopicAttributesResult>
<Attributes>
<entry>
<key>Owner</key>
<value>123456789012</value>
</entry>
<entry>
<key>Policy</key>
<value>{"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:*"}}}]}</value>
</entry>
<entry>
<key>TopicArn</key>
<value>arn:aws:sns:us-east-1:123456789012:My-Topic</value>
</entry>
</Attributes>
</GetTopicAttributesResult>
<ResponseMetadata>
<RequestId>057f074c-33a7-11df-9540-99d0768312d3</RequestId>
</ResponseMetadata>
</GetTopicAttributesResponse>
`
var TestPublishXmlOK = `
<PublishResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<PublishResult>
<MessageId>94f20ce6-13c5-43a0-9a9e-ca52d816e90b</MessageId>
</PublishResult>
<ResponseMetadata>
<RequestId>f187a3c1-376f-11df-8963-01868b7c937a</RequestId>
</ResponseMetadata>
</PublishResponse>
`
var TestSetTopicAttributesXmlOK = `
<SetTopicAttributesResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<ResponseMetadata>
<RequestId>a8763b99-33a7-11df-a9b7-05d48da6f042</RequestId>
</ResponseMetadata>
</SetTopicAttributesResponse>
`
var TestSubscribeXmlOK = `
<SubscribeResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<SubscribeResult>
<SubscriptionArn>pending confirmation</SubscriptionArn>
</SubscribeResult>
<ResponseMetadata>
<RequestId>a169c740-3766-11df-8963-01868b7c937a</RequestId>
</ResponseMetadata>
</SubscribeResponse>
`
var TestUnsubscribeXmlOK = `
<UnsubscribeResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<ResponseMetadata>
<RequestId>18e0ac39-3776-11df-84c0-b93cc1666b84</RequestId>
</ResponseMetadata>
</UnsubscribeResponse>
`
var TestConfirmSubscriptionXmlOK = `
<ConfirmSubscriptionResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<ConfirmSubscriptionResult>
<SubscriptionArn>arn:aws:sns:us-east-1:123456789012:My-Topic:80289ba6-0fd4-4079-afb4-ce8c8260f0ca</SubscriptionArn>
</ConfirmSubscriptionResult>
<ResponseMetadata>
<RequestId>7a50221f-3774-11df-a9b7-05d48da6f042</RequestId>
</ResponseMetadata>
</ConfirmSubscriptionResponse>
`
var TestAddPermissionXmlOK = `
<AddPermissionResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<ResponseMetadata>
<RequestId>6a213e4e-33a8-11df-9540-99d0768312d3</RequestId>
</ResponseMetadata>
</AddPermissionResponse>
`
var TestRemovePermissionXmlOK = `
<RemovePermissionResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<ResponseMetadata>
<RequestId>d170b150-33a8-11df-995a-2d6fbe836cc1</RequestId>
</ResponseMetadata>
</RemovePermissionResponse>
`
var TestListSubscriptionsByTopicXmlOK = `
<ListSubscriptionsByTopicResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<ListSubscriptionsByTopicResult>
<Subscriptions>
<member>
<TopicArn>arn:aws:sns:us-east-1:123456789012:My-Topic</TopicArn>
<Protocol>email</Protocol>
<SubscriptionArn>arn:aws:sns:us-east-1:123456789012:My-Topic:80289ba6-0fd4-4079-afb4-ce8c8260f0ca</SubscriptionArn>
<Owner>123456789012</Owner>
<Endpoint>example@amazon.com</Endpoint>
</member>
</Subscriptions>
</ListSubscriptionsByTopicResult>
<ResponseMetadata>
<RequestId>b9275252-3774-11df-9540-99d0768312d3</RequestId>
</ResponseMetadata>
</ListSubscriptionsByTopicResponse>
`

View File

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

View File

@@ -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 <me@prudhvi.net>
// 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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
}

View File

@@ -0,0 +1,155 @@
package iam_test
// http://goo.gl/EUIvl
var CreateUserExample = `
<CreateUserResponse>
<CreateUserResult>
<User>
<Path>/division_abc/subdivision_xyz/</Path>
<UserName>Bob</UserName>
<UserId>AIDACKCEVSQ6C2EXAMPLE</UserId>
<Arn>arn:aws:iam::123456789012:user/division_abc/subdivision_xyz/Bob</Arn>
</User>
</CreateUserResult>
<ResponseMetadata>
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
</ResponseMetadata>
</CreateUserResponse>
`
var DuplicateUserExample = `
<ErrorResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<Error>
<Type>Sender</Type>
<Code>EntityAlreadyExists</Code>
<Message>User with name Bob already exists.</Message>
</Error>
<RequestId>1d5f5000-1316-11e2-a60f-91a8e6fb6d21</RequestId>
</ErrorResponse>
`
var GetUserExample = `
<GetUserResponse>
<GetUserResult>
<User>
<Path>/division_abc/subdivision_xyz/</Path>
<UserName>Bob</UserName>
<UserId>AIDACKCEVSQ6C2EXAMPLE</UserId>
<Arn>arn:aws:iam::123456789012:user/division_abc/subdivision_xyz/Bob</Arn>
</User>
</GetUserResult>
<ResponseMetadata>
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
</ResponseMetadata>
</GetUserResponse>
`
var CreateGroupExample = `
<CreateGroupResponse>
<CreateGroupResult>
<Group>
<Path>/admins/</Path>
<GroupName>Admins</GroupName>
<GroupId>AGPACKCEVSQ6C2EXAMPLE</GroupId>
<Arn>arn:aws:iam::123456789012:group/Admins</Arn>
</Group>
</CreateGroupResult>
<ResponseMetadata>
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
</ResponseMetadata>
</CreateGroupResponse>
`
var ListGroupsExample = `
<ListGroupsResponse>
<ListGroupsResult>
<Groups>
<member>
<Path>/division_abc/subdivision_xyz/</Path>
<GroupName>Admins</GroupName>
<GroupId>AGPACKCEVSQ6C2EXAMPLE</GroupId>
<Arn>arn:aws:iam::123456789012:group/Admins</Arn>
</member>
<member>
<Path>/division_abc/subdivision_xyz/product_1234/engineering/</Path>
<GroupName>Test</GroupName>
<GroupId>AGP2MAB8DPLSRHEXAMPLE</GroupId>
<Arn>arn:aws:iam::123456789012:group/division_abc/subdivision_xyz/product_1234/engineering/Test</Arn>
</member>
<member>
<Path>/division_abc/subdivision_xyz/product_1234/</Path>
<GroupName>Managers</GroupName>
<GroupId>AGPIODR4TAW7CSEXAMPLE</GroupId>
<Arn>arn:aws:iam::123456789012:group/division_abc/subdivision_xyz/product_1234/Managers</Arn>
</member>
</Groups>
<IsTruncated>false</IsTruncated>
</ListGroupsResult>
<ResponseMetadata>
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
</ResponseMetadata>
</ListGroupsResponse>
`
var RequestIdExample = `
<AddUserToGroupResponse>
<ResponseMetadata>
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
</ResponseMetadata>
</AddUserToGroupResponse>
`
var CreateAccessKeyExample = `
<CreateAccessKeyResponse>
<CreateAccessKeyResult>
<AccessKey>
<UserName>Bob</UserName>
<AccessKeyId>AKIAIOSFODNN7EXAMPLE</AccessKeyId>
<Status>Active</Status>
<SecretAccessKey>wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY</SecretAccessKey>
</AccessKey>
</CreateAccessKeyResult>
<ResponseMetadata>
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
</ResponseMetadata>
</CreateAccessKeyResponse>
`
var ListAccessKeyExample = `
<ListAccessKeysResponse>
<ListAccessKeysResult>
<UserName>Bob</UserName>
<AccessKeyMetadata>
<member>
<UserName>Bob</UserName>
<AccessKeyId>AKIAIOSFODNN7EXAMPLE</AccessKeyId>
<Status>Active</Status>
</member>
<member>
<UserName>Bob</UserName>
<AccessKeyId>AKIAI44QH8DHBEXAMPLE</AccessKeyId>
<Status>Inactive</Status>
</member>
</AccessKeyMetadata>
<IsTruncated>false</IsTruncated>
</ListAccessKeysResult>
<ResponseMetadata>
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
</ResponseMetadata>
</ListAccessKeysResponse>
`
var GetUserPolicyExample = `
<GetUserPolicyResponse>
<GetUserPolicyResult>
<UserName>Bob</UserName>
<PolicyName>AllAccessPolicy</PolicyName>
<PolicyDocument>
{"Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}
</PolicyDocument>
</GetUserPolicyResult>
<ResponseMetadata>
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
</ResponseMetadata>
</GetUserPolicyResponse>
`

View File

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

View File

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

View File

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

View File

@@ -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("<part 1>"))
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"})
}

View File

@@ -0,0 +1,198 @@
package s3_test
var GetObjectErrorDump = `
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>NoSuchBucket</Code><Message>The specified bucket does not exist</Message>
<BucketName>non-existent-bucket</BucketName><RequestId>3F1B667FAD71C3D8</RequestId>
<HostId>L4ee/zrm1irFXY5F45fKXIRdOf9ktsKY/8TDVawuMK2jWRb1RF84i1uBzkdNqS5D</HostId></Error>
`
var GetListResultDump1 = `
<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01">
<Name>quotes</Name>
<Prefix>N</Prefix>
<IsTruncated>false</IsTruncated>
<Contents>
<Key>Nelson</Key>
<LastModified>2006-01-01T12:00:00.000Z</LastModified>
<ETag>&quot;828ef3fdfa96f00ad9f27c383fc9ac7f&quot;</ETag>
<Size>5</Size>
<StorageClass>STANDARD</StorageClass>
<Owner>
<ID>bcaf161ca5fb16fd081034f</ID>
<DisplayName>webfile</DisplayName>
</Owner>
</Contents>
<Contents>
<Key>Neo</Key>
<LastModified>2006-01-01T12:00:00.000Z</LastModified>
<ETag>&quot;828ef3fdfa96f00ad9f27c383fc9ac7f&quot;</ETag>
<Size>4</Size>
<StorageClass>STANDARD</StorageClass>
<Owner>
<ID>bcaf1ffd86a5fb16fd081034f</ID>
<DisplayName>webfile</DisplayName>
</Owner>
</Contents>
</ListBucketResult>
`
var GetListResultDump2 = `
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Name>example-bucket</Name>
<Prefix>photos/2006/</Prefix>
<Marker>some-marker</Marker>
<MaxKeys>1000</MaxKeys>
<Delimiter>/</Delimiter>
<IsTruncated>false</IsTruncated>
<CommonPrefixes>
<Prefix>photos/2006/feb/</Prefix>
</CommonPrefixes>
<CommonPrefixes>
<Prefix>photos/2006/jan/</Prefix>
</CommonPrefixes>
</ListBucketResult>
`
var InitMultiResultDump = `
<?xml version="1.0" encoding="UTF-8"?>
<InitiateMultipartUploadResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Bucket>sample</Bucket>
<Key>multi</Key>
<UploadId>JNbR_cMdwnGiD12jKAd6WK2PUkfj2VxA7i4nCwjE6t71nI9Tl3eVDPFlU0nOixhftH7I17ZPGkV3QA.l7ZD.QQ--</UploadId>
</InitiateMultipartUploadResult>
`
var ListPartsResultDump1 = `
<?xml version="1.0" encoding="UTF-8"?>
<ListPartsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Bucket>sample</Bucket>
<Key>multi</Key>
<UploadId>JNbR_cMdwnGiD12jKAd6WK2PUkfj2VxA7i4nCwjE6t71nI9Tl3eVDPFlU0nOixhftH7I17ZPGkV3QA.l7ZD.QQ--</UploadId>
<Initiator>
<ID>bb5c0f63b0b25f2d099c</ID>
<DisplayName>joe</DisplayName>
</Initiator>
<Owner>
<ID>bb5c0f63b0b25f2d099c</ID>
<DisplayName>joe</DisplayName>
</Owner>
<StorageClass>STANDARD</StorageClass>
<PartNumberMarker>0</PartNumberMarker>
<NextPartNumberMarker>2</NextPartNumberMarker>
<MaxParts>2</MaxParts>
<IsTruncated>true</IsTruncated>
<Part>
<PartNumber>1</PartNumber>
<LastModified>2013-01-30T13:45:51.000Z</LastModified>
<ETag>&quot;ffc88b4ca90a355f8ddba6b2c3b2af5c&quot;</ETag>
<Size>5</Size>
</Part>
<Part>
<PartNumber>2</PartNumber>
<LastModified>2013-01-30T13:45:52.000Z</LastModified>
<ETag>&quot;d067a0fa9dc61a6e7195ca99696b5a89&quot;</ETag>
<Size>5</Size>
</Part>
</ListPartsResult>
`
var ListPartsResultDump2 = `
<?xml version="1.0" encoding="UTF-8"?>
<ListPartsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Bucket>sample</Bucket>
<Key>multi</Key>
<UploadId>JNbR_cMdwnGiD12jKAd6WK2PUkfj2VxA7i4nCwjE6t71nI9Tl3eVDPFlU0nOixhftH7I17ZPGkV3QA.l7ZD.QQ--</UploadId>
<Initiator>
<ID>bb5c0f63b0b25f2d099c</ID>
<DisplayName>joe</DisplayName>
</Initiator>
<Owner>
<ID>bb5c0f63b0b25f2d099c</ID>
<DisplayName>joe</DisplayName>
</Owner>
<StorageClass>STANDARD</StorageClass>
<PartNumberMarker>2</PartNumberMarker>
<NextPartNumberMarker>3</NextPartNumberMarker>
<MaxParts>2</MaxParts>
<IsTruncated>false</IsTruncated>
<Part>
<PartNumber>3</PartNumber>
<LastModified>2013-01-30T13:46:50.000Z</LastModified>
<ETag>&quot;49dcd91231f801159e893fb5c6674985&quot;</ETag>
<Size>5</Size>
</Part>
</ListPartsResult>
`
var ListMultiResultDump = `
<?xml version="1.0"?>
<ListMultipartUploadsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Bucket>goamz-test-bucket-us-east-1-akiajk3wyewhctyqbf7a</Bucket>
<KeyMarker/>
<UploadIdMarker/>
<NextKeyMarker>multi1</NextKeyMarker>
<NextUploadIdMarker>iUVug89pPvSswrikD72p8uO62EzhNtpDxRmwC5WSiWDdK9SfzmDqe3xpP1kMWimyimSnz4uzFc3waVM5ufrKYQ--</NextUploadIdMarker>
<Delimiter>/</Delimiter>
<MaxUploads>1000</MaxUploads>
<IsTruncated>false</IsTruncated>
<Upload>
<Key>multi1</Key>
<UploadId>iUVug89pPvSswrikD</UploadId>
<Initiator>
<ID>bb5c0f63b0b25f2d0</ID>
<DisplayName>gustavoniemeyer</DisplayName>
</Initiator>
<Owner>
<ID>bb5c0f63b0b25f2d0</ID>
<DisplayName>gustavoniemeyer</DisplayName>
</Owner>
<StorageClass>STANDARD</StorageClass>
<Initiated>2013-01-30T18:15:47.000Z</Initiated>
</Upload>
<Upload>
<Key>multi2</Key>
<UploadId>DkirwsSvPp98guVUi</UploadId>
<Initiator>
<ID>bb5c0f63b0b25f2d0</ID>
<DisplayName>joe</DisplayName>
</Initiator>
<Owner>
<ID>bb5c0f63b0b25f2d0</ID>
<DisplayName>joe</DisplayName>
</Owner>
<StorageClass>STANDARD</StorageClass>
<Initiated>2013-01-30T18:15:47.000Z</Initiated>
</Upload>
<CommonPrefixes>
<Prefix>a/</Prefix>
</CommonPrefixes>
<CommonPrefixes>
<Prefix>b/</Prefix>
</CommonPrefixes>
</ListMultipartUploadsResult>
`
var NoSuchUploadErrorDump = `
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>NoSuchUpload</Code>
<Message>Not relevant</Message>
<BucketName>sample</BucketName>
<RequestId>3F1B667FAD71C3D8</RequestId>
<HostId>kjhwqk</HostId>
</Error>
`
var InternalErrorDump = `
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>InternalError</Code>
<Message>Not relevant</Message>
<BucketName>sample</BucketName>
<RequestId>3F1B667FAD71C3D8</RequestId>
<HostId>kjhwqk</HostId>
</Error>
`

572
vendor/src/launchpad.net/goamz/s3/s3.go vendored Normal file
View File

@@ -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 = `<CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<LocationConstraint>%s</LocationConstraint>
</CreateBucketConfiguration>`
// 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
}

View File

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

View File

@@ -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("<part %d>", 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("<part 2>")
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)
}

View File

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

View File

@@ -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 <CreateBucketConfiguration /> 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
}

View File

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

View File

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

View File

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

View File

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