diff --git a/Gemfile b/Gemfile index e1e66da8..fa8e26db 100644 --- a/Gemfile +++ b/Gemfile @@ -2,6 +2,8 @@ source "https://rubygems.org" gemspec +gem 'aws-sdk', '~> 1.60.2' + group :development do # We depend on Vagrant for development, but we don't add it as a # gem dependency because we expect to be installed within the diff --git a/lib/vagrant-aws/config.rb b/lib/vagrant-aws/config.rb index 2588a104..08dcd454 100644 --- a/lib/vagrant-aws/config.rb +++ b/lib/vagrant-aws/config.rb @@ -1,4 +1,6 @@ require "vagrant" +require "log4r" +require "aws/core" module VagrantPlugins module AWS @@ -75,6 +77,11 @@ class Config < Vagrant.plugin("2", :config) # @return [String] attr_accessor :session_token + # Path to file containing AWS credentials + # + # @return [String] + attr_accessor :credential_file + # The security groups to set on the instance. For VPC this must # be a list of IDs. For EC2, it can be either. # @@ -156,6 +163,8 @@ class Config < Vagrant.plugin("2", :config) attr_accessor :elb def initialize(region_specific=false) + @logger = Log4r::Logger.new("vagrant_aws::config") + @access_key_id = UNSET_VALUE @ami = UNSET_VALUE @availability_zone = UNSET_VALUE @@ -169,6 +178,7 @@ def initialize(region_specific=false) @version = UNSET_VALUE @secret_access_key = UNSET_VALUE @session_token = UNSET_VALUE + @credential_file = UNSET_VALUE @security_groups = UNSET_VALUE @subnet_id = UNSET_VALUE @tags = {} @@ -266,9 +276,43 @@ def merge(other) def finalize! # Try to get access keys from standard AWS environment variables; they # will default to nil if the environment variables are not present. - @access_key_id = ENV['AWS_ACCESS_KEY'] if @access_key_id == UNSET_VALUE - @secret_access_key = ENV['AWS_SECRET_KEY'] if @secret_access_key == UNSET_VALUE - @session_token = ENV['AWS_SESSION_TOKEN'] if @session_token == UNSET_VALUE + + @credential_file = ENV['AWS_CREDENTIAL_FILE'] if @credential_file == UNSET_VALUE + + unless @credential_file.nil? + path = File.expand_path(@credential_file) + + @logger.info("Loading AWS credentials from #{path}") + if !File.exist?(path) + @logger.warn("Provided credential file #{path} doesn't exist") + + @access_key_id = nil + @secret_access_key = nil + @session_token = nil + else + provider = ::AWS::Core::CredentialProviders::SharedCredentialFileProvider.new( + :path => path + ) + credentials = provider.get_credentials() + + @logger.debug("Loaded credentials via AWS SDK: #{credentials}") + + @access_key_id = credentials[:access_key_id] + @secret_access_key = credentials[:secret_access_key] + @session_token = credentials[:session_token] + end + else + @logger.debug("Using AWS credentials provided directly") + + @access_key_id = ENV['AWS_ACCESS_KEY'] if @access_key_id == UNSET_VALUE + @secret_access_key = ENV['AWS_SECRET_KEY'] if @secret_access_key == UNSET_VALUE + @session_token = ENV['AWS_SESSION_TOKEN'] if @session_token == UNSET_VALUE + end + + _ = '' + @logger.debug("Using AWS access key: #{@access_key_id||_}") + @logger.debug("Using AWS secret key: #{@secret_access_key||_}") + @logger.debug("Using AWS session token: #{@session_token||_}") # AMI must be nil, since we can't default that @ami = nil if @ami == UNSET_VALUE @@ -367,6 +411,21 @@ def validate(machine) # that region. config = get_region_config(@region) + # TODO - lifecycle misunderstanding, it's always defined (even when loaded from file as above) + if !config.credential_file.nil? + if !config.access_key_id.nil? + errors << I18n.t("vagrant_aws.config.access_key_id_cred_path_conflict") + end + + if !config.secret_access_key.nil? + errors << I18n.t("vagrant_aws.config.secret_access_key_cred_path_conflict") + end + + if !config.session_token.nil? + errors << I18n.t("vagrant_aws.config.session_token_cred_path_conflict") + end + end + if !config.use_iam_profile errors << I18n.t("vagrant_aws.config.access_key_id_required") if \ config.access_key_id.nil? diff --git a/locales/en.yml b/locales/en.yml index 00513ba6..495ae6dd 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -73,6 +73,12 @@ en: A secret access key is required via "secret_access_key" subnet_id_required_with_public_ip: |- If you assign a public IP address to an instance in a VPC, a subnet must be specifed via "subnet_id" + access_key_id_cred_path_conflict: |- + Either "access_key_id" or "credential_file" must be specifed, not both + secret_access_key_cred_path_conflict: |- + Either "secret_access_key" or "credential_file" must be specifed, not both + session_token_cred_path_conflict: |- + Either "session_token" or "credential_file" must be specifed, not both errors: fog_error: |- diff --git a/spec/vagrant-aws/config_spec.rb b/spec/vagrant-aws/config_spec.rb index ac7aed5f..7e2b98db 100644 --- a/spec/vagrant-aws/config_spec.rb +++ b/spec/vagrant-aws/config_spec.rb @@ -1,3 +1,5 @@ +require 'vagrant-aws' +require 'vagrant-aws/plugin' require "vagrant-aws/config" require 'rspec/its' @@ -27,6 +29,7 @@ its("region") { should == "us-east-1" } its("secret_access_key") { should be_nil } its("session_token") { should be_nil } + its("credential_file") { should be_nil } its("security_groups") { should == [] } its("subnet_id") { should be_nil } its("iam_instance_profile_arn") { should be_nil } @@ -49,11 +52,12 @@ # each of these attributes to "foo" in isolation, and reads the value # and asserts the proper result comes back out. [:access_key_id, :ami, :availability_zone, :instance_ready_timeout, - :instance_package_timeout, :instance_type, :keypair_name, :ssh_host_attribute, - :ebs_optimized, :region, :secret_access_key, :session_token, :monitoring, - :associate_public_ip, :subnet_id, :tags, :elastic_ip, :terminate_on_shutdown, - :iam_instance_profile_arn, :iam_instance_profile_name, - :use_iam_profile, :user_data, :block_device_mapping].each do |attribute| + :instance_package_timeout, :instance_type, :keypair_name, + :ssh_host_attribute, :ebs_optimized, :region, :secret_access_key, + :session_token, :credential_file, :monitoring, :associate_public_ip, + :subnet_id, :tags, :elastic_ip, :terminate_on_shutdown, + :iam_instance_profile_arn, :iam_instance_profile_name, :use_iam_profile, + :user_data, :block_device_mapping].each do |attribute| it "should not default #{attribute} if overridden" do instance.send("#{attribute}=".to_sym, "foo") @@ -76,6 +80,7 @@ end end + its("credential_file") { should be_nil } its("access_key_id") { should be_nil } its("secret_access_key") { should be_nil } its("session_token") { should be_nil } @@ -86,6 +91,7 @@ ENV.stub(:[]).with("AWS_ACCESS_KEY").and_return("access_key") ENV.stub(:[]).with("AWS_SECRET_KEY").and_return("secret_key") ENV.stub(:[]).with("AWS_SESSION_TOKEN").and_return("session_token") + ENV.stub(:[]).with("AWS_CREDENTIAL_FILE").and_return(nil) end subject do @@ -97,6 +103,7 @@ its("access_key_id") { should == "access_key" } its("secret_access_key") { should == "secret_key" } its("session_token") { should == "session_token" } + its("credential_file") { should be_nil } end end @@ -108,6 +115,7 @@ let(:config_region) { "foo" } let(:config_secret_access_key) { "foo" } let(:config_session_token) { "foo" } + let(:config_credential_file) { nil } def set_test_values(instance) instance.access_key_id = config_access_key_id @@ -117,6 +125,7 @@ def set_test_values(instance) instance.region = config_region instance.secret_access_key = config_secret_access_key instance.session_token = config_session_token + instance.credential_file = config_credential_file end it "should raise an exception if not finalized" do @@ -143,6 +152,7 @@ def set_test_values(instance) its("region") { should == config_region } its("secret_access_key") { should == config_secret_access_key } its("session_token") { should == config_session_token } + its("credential_file") { should == config_credential_file } end context "with a specific config set" do @@ -168,6 +178,26 @@ def set_test_values(instance) its("region") { should == region_name } its("secret_access_key") { should == config_secret_access_key } its("session_token") { should == config_session_token } + its("credential_file") { should == config_credential_file } + end + + context "with conflicting config options" do + before(:each) do + VagrantPlugins::AWS::Plugin.setup_i18n + set_test_values(instance) + instance.finalize! + @config = instance.get_region_config("us-east-1") + end + + it "should throw error if keys & credential_file is defined" do + @config.access_key_id = "access_key" + @config.secret_access_key = "secret key" + @config.session_token = "token" + @config.credential_file = "credential file" + + errors = instance.validate(nil) + errors['AWS Provider'].length.should eq(3) + end end describe "inheritance of parent config" do