diff --git a/docs/drivers/aws.md b/docs/drivers/aws.md index 308093d59a..aae8303dfd 100644 --- a/docs/drivers/aws.md +++ b/docs/drivers/aws.md @@ -10,21 +10,44 @@ parent="smn_machine_drivers" # Amazon Web Services -Create machines on [Amazon Web Services](http://aws.amazon.com). To create machines on [Amazon Web Services](http://aws.amazon.com), you must supply two required parameters: +Create machines on [Amazon Web Services](http://aws.amazon.com). -- Access Key ID -- Secret Access Key +To create machines on [Amazon Web Services](http://aws.amazon.com), you must supply two parameters: the AWS Access Key ID and the AWS Secret Access Key. -Obtain your IDs and Keys from AWS. -To create the machine instance, specify `--driver amazonec2` and the three required parameters. +## Configuring credentials + +Before using the amazonec2 driver, ensure that you've configured credentials. + +### AWS credential file +One way to configure credentials is to use the standard credential file for Amazon AWS `~/.aws/credentials` file, which might look like: + + [default] + aws_access_key_id = AKID1234567890 + aws_secret_access_key = MY-SECRET-KEY + +You can learn more about the credentials file from this [blog post](http://blogs.aws.amazon.com/security/post/Tx3D6U6WSFGOK2H/A-New-and-Standardized-Way-to-Manage-Credentials-in-the-AWS-SDKs). + +This is the simplest case, you can then create a new machine with: + + $ docker-machine create --driver amazonec2 aws01 + +### Command line flags +Alternatively, you can use the flags `--amazonec2-access-key` and `--amazonec2-secret-key` on the command line: $ docker-machine create --driver amazonec2 --amazonec2-access-key AKI******* --amazonec2-secret-key 8T93C******* aws01 +### Environment variables +You can use environment variables: + + $ export AWS_ACCESS_KEY_ID=AKID1234567890 + $ export AWS_SECRET_ACCESS_KEY=MY-SECRET-KEY + $ docker-machine create --driver amazonec2 aws01 + ## Options -- `--amazonec2-access-key`: **required** Your access key id for the Amazon Web Services API. -- `--amazonec2-secret-key`: **required** Your secret access key for the Amazon Web Services API. +- `--amazonec2-access-key`: Your access key id for the Amazon Web Services API. +- `--amazonec2-secret-key`: Your secret access key for the Amazon Web Services API. - `--amazonec2-session-token`: Your session token for the Amazon Web Services API. - `--amazonec2-ami`: The AMI ID of the instance to use. - `--amazonec2-region`: The region to use when launching the instance. @@ -66,8 +89,8 @@ Environment variables and default values: | CLI option | Environment variable | Default | | ---------------------------------------- | ----------------------- | ---------------- | -| **`--amazonec2-access-key`** | `AWS_ACCESS_KEY_ID` | - | -| **`--amazonec2-secret-key`** | `AWS_SECRET_ACCESS_KEY` | - | +| `--amazonec2-access-key` | `AWS_ACCESS_KEY_ID` | - | +| `--amazonec2-secret-key` | `AWS_SECRET_ACCESS_KEY` | - | | `--amazonec2-session-token` | `AWS_SESSION_TOKEN` | - | | `--amazonec2-ami` | `AWS_AMI` | `ami-5f709f34` | | `--amazonec2-region` | `AWS_DEFAULT_REGION` | `us-east-1` | @@ -90,8 +113,7 @@ Environment variables and default values: | `--amazonec2-use-ebs-optimized-instance` | - | `false` | ## Security Group - -Note that a security group will be created and associated to the host. This security group will have the following ports opened inbound : +Note that a security group will be created and associated to the host. This security group will have the following ports opened inbound: - ssh (22/tcp) - docker (2376/tcp) diff --git a/drivers/amazonec2/amazonec2.go b/drivers/amazonec2/amazonec2.go index 66d3267242..8f2dbc5e01 100644 --- a/drivers/amazonec2/amazonec2.go +++ b/drivers/amazonec2/amazonec2.go @@ -15,7 +15,6 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/ec2" "github.com/docker/machine/libmachine/drivers" @@ -49,14 +48,15 @@ const ( var ( dockerPort = 2376 swarmPort = 3376 - errorMissingAccessKeyOption = errors.New("amazonec2 driver requires the --amazonec2-access-key option") - errorMissingSecretKeyOption = errors.New("amazonec2 driver requires the --amazonec2-secret-key option") + errorMissingAccessKeyOption = errors.New("amazonec2 driver requires the --amazonec2-access-key option or proper credentials in ~/.aws/credentials") + errorMissingSecretKeyOption = errors.New("amazonec2 driver requires the --amazonec2-secret-key option or proper credentials in ~/.aws/credentials") errorNoVPCIdFound = errors.New("amazonec2 driver requires either the --amazonec2-subnet-id or --amazonec2-vpc-id option or an AWS Account with a default vpc-id") ) type Driver struct { *drivers.BaseDriver clientFactory func() Ec2Client + awsCredentials awsCredentials Id string AccessKey string SecretKey string @@ -226,6 +226,7 @@ func NewDriver(hostName, storePath string) *Driver { MachineName: hostName, StorePath: storePath, }, + awsCredentials: &defaultAWSCredentials{}, } driver.clientFactory = driver.buildClient @@ -237,7 +238,7 @@ func (d *Driver) buildClient() Ec2Client { config := aws.NewConfig() alogger := AwsLogger() config = config.WithRegion(d.Region) - config = config.WithCredentials(credentials.NewStaticCredentials(d.AccessKey, d.SecretKey, d.SessionToken)) + config = config.WithCredentials(d.awsCredentials.NewStaticCredentials(d.AccessKey, d.SecretKey, d.SessionToken)) config = config.WithLogger(alogger) config = config.WithLogLevel(aws.LogDebugWithHTTPBody) return ec2.New(session.New(config)) @@ -284,6 +285,18 @@ func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error { d.UseEbsOptimizedInstance = flags.Bool("amazonec2-use-ebs-optimized-instance") d.SetSwarmConfigFromFlags(flags) + if d.AccessKey == "" && d.SecretKey == "" { + credentials, err := d.awsCredentials.NewSharedCredentials("", "").Get() + if err != nil { + log.Debug("Could not load credentials from ~/.aws/credentials") + } else { + log.Debug("Successfully loaded credentials from ~/.aws/credentials") + d.AccessKey = credentials.AccessKeyID + d.SecretKey = credentials.SecretAccessKey + d.SessionToken = credentials.SessionToken + } + } + if d.AccessKey == "" { return errorMissingAccessKeyOption } diff --git a/drivers/amazonec2/amazonec2_test.go b/drivers/amazonec2/amazonec2_test.go index 6e9c0ee850..9c933e7afe 100644 --- a/drivers/amazonec2/amazonec2_test.go +++ b/drivers/amazonec2/amazonec2_test.go @@ -240,34 +240,10 @@ func TestSetConfigFromFlags(t *testing.T) { assert.Empty(t, checkFlags.InvalidFlags) } -type fakeEC2WithDescribe struct { - *ec2.EC2 - output *ec2.DescribeAccountAttributesOutput - err error -} - -func (f *fakeEC2WithDescribe) DescribeAccountAttributes(input *ec2.DescribeAccountAttributesInput) (*ec2.DescribeAccountAttributesOutput, error) { - return f.output, f.err -} - func TestFindDefaultVPC(t *testing.T) { - defaultVpc := "default-vpc" - vpcName := "vpc-9999" - driver := NewDriver("machineFoo", "path") driver.clientFactory = func() Ec2Client { - return &fakeEC2WithDescribe{ - output: &ec2.DescribeAccountAttributesOutput{ - AccountAttributes: []*ec2.AccountAttribute{ - { - AttributeName: &defaultVpc, - AttributeValues: []*ec2.AccountAttributeValue{ - {AttributeValue: &vpcName}, - }, - }, - }, - }, - } + return &fakeEC2WithLogin{} } vpc, err := driver.getDefaultVPCId() @@ -305,3 +281,97 @@ func TestDescribeAccountAttributeFails(t *testing.T) { assert.EqualError(t, err, "Not Found") assert.Empty(t, vpc) } + +func TestAccessKeyIsMandatory(t *testing.T) { + driver := NewDriver("machineFoo", "path") + driver.clientFactory = func() Ec2Client { return &fakeEC2{} } + driver.awsCredentials = &cliCredentials{} + options := &commandstest.FakeFlagger{ + Data: map[string]interface{}{ + "name": "test", + "amazonec2-region": "us-east-1", + "amazonec2-zone": "e", + }, + } + + err := driver.SetConfigFromFlags(options) + + assert.Equal(t, err, errorMissingAccessKeyOption) +} + +func TestAccessKeyIsMandatoryEvenIfSecretKeyIsPassed(t *testing.T) { + driver := NewDriver("machineFoo", "path") + driver.clientFactory = func() Ec2Client { return &fakeEC2{} } + driver.awsCredentials = &cliCredentials{} + options := &commandstest.FakeFlagger{ + Data: map[string]interface{}{ + "name": "test", + "amazonec2-secret-key": "123", + "amazonec2-region": "us-east-1", + "amazonec2-zone": "e", + }, + } + + err := driver.SetConfigFromFlags(options) + + assert.Equal(t, err, errorMissingAccessKeyOption) +} + +func TestSecretKeyIsMandatory(t *testing.T) { + driver := NewDriver("machineFoo", "path") + driver.clientFactory = func() Ec2Client { return &fakeEC2{} } + driver.awsCredentials = &cliCredentials{} + options := &commandstest.FakeFlagger{ + Data: map[string]interface{}{ + "name": "test", + "amazonec2-access-key": "foobar", + "amazonec2-region": "us-east-1", + "amazonec2-zone": "e", + }, + } + + err := driver.SetConfigFromFlags(options) + + assert.Equal(t, err, errorMissingSecretKeyOption) +} + +func TestLoadingFromCredentialsWorked(t *testing.T) { + driver := NewDriver("machineFoo", "path") + driver.clientFactory = func() Ec2Client { return &fakeEC2WithLogin{} } + driver.awsCredentials = &fileCredentials{} + options := &commandstest.FakeFlagger{ + Data: map[string]interface{}{ + "name": "test", + "amazonec2-region": "us-east-1", + "amazonec2-zone": "e", + }, + } + + err := driver.SetConfigFromFlags(options) + + assert.NoError(t, err) + assert.Equal(t, "access", driver.AccessKey) + assert.Equal(t, "secret", driver.SecretKey) + assert.Equal(t, "token", driver.SessionToken) +} + +func TestPassingBothCLIArgWorked(t *testing.T) { + driver := NewDriver("machineFoo", "path") + driver.clientFactory = func() Ec2Client { return &fakeEC2WithLogin{} } + driver.awsCredentials = &cliCredentials{} + options := &commandstest.FakeFlagger{ + Data: map[string]interface{}{ + "name": "test", + "amazonec2-access-key": "foobar", + "amazonec2-secret-key": "123", + "amazonec2-region": "us-east-1", + "amazonec2-zone": "e", + }, + } + + err := driver.SetConfigFromFlags(options) + + assert.NoError(t, err) + assert.Equal(t, "foobar", driver.AccessKey) + assert.Equal(t, "123", driver.SecretKey) +} diff --git a/drivers/amazonec2/awscredentials.go b/drivers/amazonec2/awscredentials.go new file mode 100644 index 0000000000..15e3a326d3 --- /dev/null +++ b/drivers/amazonec2/awscredentials.go @@ -0,0 +1,19 @@ +package amazonec2 + +import "github.com/aws/aws-sdk-go/aws/credentials" + +type awsCredentials interface { + NewStaticCredentials(id, secret, token string) *credentials.Credentials + + NewSharedCredentials(filename, profile string) *credentials.Credentials +} + +type defaultAWSCredentials struct{} + +func (c *defaultAWSCredentials) NewStaticCredentials(id, secret, token string) *credentials.Credentials { + return credentials.NewStaticCredentials(id, secret, token) +} + +func (c *defaultAWSCredentials) NewSharedCredentials(filename, profile string) *credentials.Credentials { + return credentials.NewSharedCredentials(filename, profile) +} diff --git a/drivers/amazonec2/stub_test.go b/drivers/amazonec2/stub_test.go new file mode 100644 index 0000000000..680da8bd4c --- /dev/null +++ b/drivers/amazonec2/stub_test.go @@ -0,0 +1,90 @@ +package amazonec2 + +import ( + "errors" + + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/service/ec2" +) + +type fakeEC2 struct { + *ec2.EC2 +} + +type errorProvider struct{} + +func (p *errorProvider) Retrieve() (credentials.Value, error) { + return credentials.Value{}, errors.New("bad credentials") +} + +func (p *errorProvider) IsExpired() bool { + return true +} + +type okProvider struct { + accessKeyID string + secretAccessKey string + sessionToken string +} + +func (p *okProvider) Retrieve() (credentials.Value, error) { + return credentials.Value{ + AccessKeyID: p.accessKeyID, + SecretAccessKey: p.secretAccessKey, + SessionToken: p.sessionToken, + }, nil +} + +func (p *okProvider) IsExpired() bool { + return true +} + +type cliCredentials struct{} + +func (c *cliCredentials) NewStaticCredentials(id, secret, token string) *credentials.Credentials { + return credentials.NewCredentials(&okProvider{id, secret, token}) +} + +func (c *cliCredentials) NewSharedCredentials(filename, profile string) *credentials.Credentials { + return credentials.NewCredentials(&errorProvider{}) +} + +type fileCredentials struct{} + +func (c *fileCredentials) NewStaticCredentials(id, secret, token string) *credentials.Credentials { + return nil +} + +func (c *fileCredentials) NewSharedCredentials(filename, profile string) *credentials.Credentials { + return credentials.NewCredentials(&okProvider{"access", "secret", "token"}) +} + +type fakeEC2WithDescribe struct { + *fakeEC2 + output *ec2.DescribeAccountAttributesOutput + err error +} + +func (f *fakeEC2WithDescribe) DescribeAccountAttributes(input *ec2.DescribeAccountAttributesInput) (*ec2.DescribeAccountAttributesOutput, error) { + return f.output, f.err +} + +type fakeEC2WithLogin struct { + *fakeEC2 +} + +func (f *fakeEC2WithLogin) DescribeAccountAttributes(input *ec2.DescribeAccountAttributesInput) (*ec2.DescribeAccountAttributesOutput, error) { + defaultVpc := "default-vpc" + vpcName := "vpc-9999" + + return &ec2.DescribeAccountAttributesOutput{ + AccountAttributes: []*ec2.AccountAttribute{ + { + AttributeName: &defaultVpc, + AttributeValues: []*ec2.AccountAttributeValue{ + {AttributeValue: &vpcName}, + }, + }, + }, + }, nil +} diff --git a/test/integration/amazonec2/amazon.bats b/test/integration/amazonec2/amazon.bats new file mode 100644 index 0000000000..2801943993 --- /dev/null +++ b/test/integration/amazonec2/amazon.bats @@ -0,0 +1,16 @@ +#!/usr/bin/env bats + +load ${BASE_TEST_DIR}/helpers.bash + +only_if_env DRIVER amazonec2 + +use_disposable_machine + +require_env AWS_ACCESS_KEY_ID +require_env AWS_SECRET_ACCESS_KEY + +@test "$DRIVER: Should Create a default host" { + run machine create -d amazonec2 $NAME + echo ${output} + [ "$status" -eq 0 ] +}